* refactor(registries): move to portainer * feat(registries): show browse link * feat(registry): move registry extension code * fix(registry): revert files * refactor(registry): use component * refactor(registry): replace $scope with this * refactor(registry): use async await * refactor(registry): rename and extract * refactor(registry): rename progression-modal files * refactor(registry): replace view with component * refactor(registry): replace with component * style(regirstries): sort handler keys * feat(registry): force the recreation of a proxy client * fix(registry): ignore 404 tags
134 lines
3.6 KiB
Go
134 lines
3.6 KiB
Go
package registryproxy
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"time"
|
|
|
|
portainer "github.com/portainer/portainer/api"
|
|
)
|
|
|
|
type (
|
|
tokenSecuredTransport struct {
|
|
config *portainer.RegistryManagementConfiguration
|
|
client *http.Client
|
|
}
|
|
|
|
genericAuthenticationResponse struct {
|
|
AccessToken string `json:"token"`
|
|
}
|
|
|
|
azureAuthenticationResponse struct {
|
|
AccessToken string `json:"access_token"`
|
|
}
|
|
)
|
|
|
|
func newTokenSecuredRegistryProxy(uri string, config *portainer.RegistryManagementConfiguration) (http.Handler, error) {
|
|
url, err := url.Parse("https://" + uri)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
proxy := newSingleHostReverseProxyWithHostHeader(url)
|
|
proxy.Transport = &tokenSecuredTransport{
|
|
config: config,
|
|
client: &http.Client{
|
|
Timeout: time.Second * 10,
|
|
},
|
|
}
|
|
|
|
return proxy, nil
|
|
}
|
|
|
|
// RoundTrip will first send a lightweight copy of the original request (same URL and method) and
|
|
// will then inspect the response code of the response.
|
|
// If the response code is 401 (Unauthorized), it will send an authentication request
|
|
// based on the information retrieved in the Www-Authenticate response header
|
|
// (https://docs.docker.com/registry/spec/auth/scope/#resource-provider-use) and
|
|
// retrieve an authentication token. It will then retry the original request
|
|
// decorated with a new Authorization header containing the authentication token.
|
|
func (transport *tokenSecuredTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
requestCopy, err := http.NewRequest(request.Method, request.URL.String(), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response, err := http.DefaultTransport.RoundTrip(requestCopy)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
if response.StatusCode == http.StatusUnauthorized {
|
|
wwwAuthenticateHeader := response.Header.Get("Www-Authenticate")
|
|
authenticationDetails := extractWWWAuthenticateValues(wwwAuthenticateHeader)
|
|
|
|
authRequest, err := http.NewRequest(http.MethodGet, authenticationDetails["realm"], nil)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
q := authRequest.URL.Query()
|
|
if authenticationDetails["service"] != "" {
|
|
q.Add("service", authenticationDetails["service"])
|
|
}
|
|
if authenticationDetails["scope"] != "" {
|
|
q.Add("scope", authenticationDetails["scope"])
|
|
}
|
|
authRequest.URL.RawQuery = q.Encode()
|
|
authRequest.SetBasicAuth(transport.config.Username, transport.config.Password)
|
|
|
|
authResponse, err := transport.client.Do(authRequest)
|
|
if err != nil {
|
|
return authResponse, err
|
|
}
|
|
defer authResponse.Body.Close()
|
|
|
|
token, err := retrieveToken(authResponse, transport.config.Type)
|
|
if err != nil {
|
|
return authResponse, err
|
|
}
|
|
|
|
request.Header.Set("Authorization", "Bearer "+token)
|
|
return http.DefaultTransport.RoundTrip(request)
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func retrieveToken(response *http.Response, registryType portainer.RegistryType) (string, error) {
|
|
token := ""
|
|
if registryType == portainer.AzureRegistry {
|
|
var responseData azureAuthenticationResponse
|
|
err := json.NewDecoder(response.Body).Decode(&responseData)
|
|
if err != nil {
|
|
return token, err
|
|
}
|
|
token = responseData.AccessToken
|
|
} else {
|
|
var responseData genericAuthenticationResponse
|
|
err := json.NewDecoder(response.Body).Decode(&responseData)
|
|
if err != nil {
|
|
return token, err
|
|
}
|
|
token = responseData.AccessToken
|
|
}
|
|
return token, nil
|
|
}
|
|
|
|
var wwwAuthenticateHeaderRegexp = regexp.MustCompile(`(realm|service|scope)="(.*?)"`)
|
|
|
|
func extractWWWAuthenticateValues(s string) map[string]string {
|
|
data := wwwAuthenticateHeaderRegexp.FindAllStringSubmatch(s, -1)
|
|
|
|
result := make(map[string]string)
|
|
for _, kv := range data {
|
|
k := kv[1]
|
|
v := kv[2]
|
|
result[k] = v
|
|
}
|
|
|
|
return result
|
|
}
|