package distribution import ( "fmt" "net" "net/http" "net/url" "strings" "syscall" "time" "github.com/docker/distribution" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/client" "github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/distribution/xfer" "github.com/docker/docker/dockerversion" "github.com/docker/docker/registry" "github.com/docker/engine-api/types" "golang.org/x/net/context" ) // fallbackError wraps an error that can possibly allow fallback to a different // endpoint. type fallbackError struct { // err is the error being wrapped. err error // confirmedV2 is set to true if it was confirmed that the registry // supports the v2 protocol. This is used to limit fallbacks to the v1 // protocol. confirmedV2 bool } // Error renders the FallbackError as a string. func (f fallbackError) Error() string { return f.err.Error() } type dumbCredentialStore struct { auth *types.AuthConfig } func (dcs dumbCredentialStore) Basic(*url.URL) (string, string) { return dcs.auth.Username, dcs.auth.Password } // conn wraps a net.Conn, and sets a deadline for every read // and write operation. type conn struct { net.Conn readTimeout time.Duration writeTimeout time.Duration } func (c *conn) Read(b []byte) (int, error) { err := c.Conn.SetReadDeadline(time.Now().Add(c.readTimeout)) if err != nil { return 0, err } return c.Conn.Read(b) } func (c *conn) Write(b []byte) (int, error) { err := c.Conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)) if err != nil { return 0, err } return c.Conn.Write(b) } // NewV2Repository returns a repository (v2 only). It creates a HTTP transport // providing timeout settings and authentication support, and also verifies the // remote API version. func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, endpoint registry.APIEndpoint, metaHeaders http.Header, authConfig *types.AuthConfig, actions ...string) (repo distribution.Repository, foundVersion bool, err error) { repoName := repoInfo.FullName() // If endpoint does not support CanonicalName, use the RemoteName instead if endpoint.TrimHostname { repoName = repoInfo.RemoteName() } // TODO(dmcgowan): Call close idle connections when complete, use keep alive base := &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: func(network, address string) (net.Conn, error) { dialer := &net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, } netConn, err := dialer.Dial(network, address) if err != nil { return netConn, err } return &conn{ Conn: netConn, readTimeout: time.Minute, writeTimeout: time.Minute, }, nil }, TLSHandshakeTimeout: 10 * time.Second, TLSClientConfig: endpoint.TLSConfig, // TODO(dmcgowan): Call close idle connections when complete and use keep alive DisableKeepAlives: true, } modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(), metaHeaders) authTransport := transport.NewTransport(base, modifiers...) pingClient := &http.Client{ Transport: authTransport, Timeout: 15 * time.Second, } endpointStr := strings.TrimRight(endpoint.URL, "/") + "/v2/" req, err := http.NewRequest("GET", endpointStr, nil) if err != nil { return nil, false, err } resp, err := pingClient.Do(req) if err != nil { return nil, false, err } defer resp.Body.Close() v2Version := auth.APIVersion{ Type: "registry", Version: "2.0", } versions := auth.APIVersions(resp, registry.DefaultRegistryVersionHeader) for _, pingVersion := range versions { if pingVersion == v2Version { // The version header indicates we're definitely // talking to a v2 registry. So don't allow future // fallbacks to the v1 protocol. foundVersion = true break } } challengeManager := auth.NewSimpleChallengeManager() if err := challengeManager.AddResponse(resp); err != nil { return nil, foundVersion, err } if authConfig.RegistryToken != "" { passThruTokenHandler := &existingTokenHandler{token: authConfig.RegistryToken} modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler)) } else { creds := dumbCredentialStore{auth: authConfig} tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...) basicHandler := auth.NewBasicHandler(creds) modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) } tr := transport.NewTransport(base, modifiers...) repo, err = client.NewRepository(ctx, repoName, endpoint.URL, tr) return repo, foundVersion, err } type existingTokenHandler struct { token string } func (th *existingTokenHandler) Scheme() string { return "bearer" } func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.token)) return nil } // retryOnError wraps the error in xfer.DoNotRetry if we should not retry the // operation after this error. func retryOnError(err error) error { switch v := err.(type) { case errcode.Errors: return retryOnError(v[0]) case errcode.Error: switch v.Code { case errcode.ErrorCodeUnauthorized, errcode.ErrorCodeUnsupported, errcode.ErrorCodeDenied: return xfer.DoNotRetry{Err: err} } case *url.Error: return retryOnError(v.Err) case *client.UnexpectedHTTPResponseError: return xfer.DoNotRetry{Err: err} case error: if strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error())) { return xfer.DoNotRetry{Err: err} } } // let's be nice and fallback if the error is a completely // unexpected one. // If new errors have to be handled in some way, please // add them to the switch above. return err }