package registry // import "github.com/docker/docker/registry" import ( "context" "net/http" "net/url" "strings" "time" "github.com/containerd/log" "github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/auth/challenge" "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/api/types/registry" "github.com/pkg/errors" ) // AuthClientID is used the ClientID used for the token server const AuthClientID = "docker" type loginCredentialStore struct { authConfig *registry.AuthConfig } func (lcs loginCredentialStore) Basic(*url.URL) (string, string) { return lcs.authConfig.Username, lcs.authConfig.Password } func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string { return lcs.authConfig.IdentityToken } func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) { lcs.authConfig.IdentityToken = token } type staticCredentialStore struct { auth *registry.AuthConfig } // NewStaticCredentialStore returns a credential store // which always returns the same credential values. func NewStaticCredentialStore(auth *registry.AuthConfig) auth.CredentialStore { return staticCredentialStore{ auth: auth, } } func (scs staticCredentialStore) Basic(*url.URL) (string, string) { if scs.auth == nil { return "", "" } return scs.auth.Username, scs.auth.Password } func (scs staticCredentialStore) RefreshToken(*url.URL, string) string { if scs.auth == nil { return "" } return scs.auth.IdentityToken } func (scs staticCredentialStore) SetRefreshToken(*url.URL, string, string) { } // loginV2 tries to login to the v2 registry server. The given registry // endpoint will be pinged to get authorization challenges. These challenges // will be used to authenticate against the registry to validate credentials. func loginV2(authConfig *registry.AuthConfig, endpoint APIEndpoint, userAgent string) (string, string, error) { var ( endpointStr = strings.TrimRight(endpoint.URL.String(), "/") + "/v2/" modifiers = Headers(userAgent, nil) authTransport = transport.NewTransport(newTransport(endpoint.TLSConfig), modifiers...) credentialAuthConfig = *authConfig creds = loginCredentialStore{authConfig: &credentialAuthConfig} ) log.G(context.TODO()).Debugf("attempting v2 login to registry endpoint %s", endpointStr) loginClient, err := v2AuthHTTPClient(endpoint.URL, authTransport, modifiers, creds, nil) if err != nil { return "", "", err } req, err := http.NewRequest(http.MethodGet, endpointStr, nil) if err != nil { return "", "", err } resp, err := loginClient.Do(req) if err != nil { err = translateV2AuthError(err) return "", "", err } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { return "Login Succeeded", credentialAuthConfig.IdentityToken, nil } // TODO(dmcgowan): Attempt to further interpret result, status code and error code string return "", "", errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode)) } func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, error) { challengeManager, err := PingV2Registry(endpoint, authTransport) if err != nil { return nil, err } tokenHandlerOptions := auth.TokenHandlerOptions{ Transport: authTransport, Credentials: creds, OfflineAccess: true, ClientID: AuthClientID, Scopes: scopes, } tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions) basicHandler := auth.NewBasicHandler(creds) modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) return &http.Client{ Transport: transport.NewTransport(authTransport, modifiers...), Timeout: 15 * time.Second, }, nil } // ConvertToHostname normalizes a registry URL which has http|https prepended // to just its hostname. It is used to match credentials, which may be either // stored as hostname or as hostname including scheme (in legacy configuration // files). func ConvertToHostname(url string) string { stripped := url if strings.HasPrefix(url, "http://") { stripped = strings.TrimPrefix(url, "http://") } else if strings.HasPrefix(url, "https://") { stripped = strings.TrimPrefix(url, "https://") } return strings.SplitN(stripped, "/", 2)[0] } // ResolveAuthConfig matches an auth configuration to a server address or a URL func ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, index *registry.IndexInfo) registry.AuthConfig { configKey := GetAuthConfigKey(index) // First try the happy case if c, found := authConfigs[configKey]; found || index.Official { return c } // Maybe they have a legacy config file, we will iterate the keys converting // them to the new format and testing for registryURL, ac := range authConfigs { if configKey == ConvertToHostname(registryURL) { return ac } } // When all else fails, return an empty auth config return registry.AuthConfig{} } // PingResponseError is used when the response from a ping // was received but invalid. type PingResponseError struct { Err error } func (err PingResponseError) Error() string { return err.Err.Error() } // PingV2Registry attempts to ping a v2 registry and on success return a // challenge manager for the supported authentication types. // If a response is received but cannot be interpreted, a PingResponseError will be returned. func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (challenge.Manager, error) { pingClient := &http.Client{ Transport: transport, Timeout: 15 * time.Second, } endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/" req, err := http.NewRequest(http.MethodGet, endpointStr, nil) if err != nil { return nil, err } resp, err := pingClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() challengeManager := challenge.NewSimpleManager() if err := challengeManager.AddResponse(resp); err != nil { return nil, PingResponseError{ Err: err, } } return challengeManager, nil }