endpoint_v1.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. package registry // import "github.com/docker/docker/registry"
  2. import (
  3. "context"
  4. "crypto/tls"
  5. "encoding/json"
  6. "io"
  7. "net/http"
  8. "net/url"
  9. "strings"
  10. "github.com/containerd/containerd/log"
  11. "github.com/docker/distribution/registry/client/transport"
  12. "github.com/docker/docker/api/types/registry"
  13. )
  14. // v1PingResult contains the information returned when pinging a registry. It
  15. // indicates the registry's version and whether the registry claims to be a
  16. // standalone registry.
  17. type v1PingResult struct {
  18. // Version is the registry version supplied by the registry in an HTTP
  19. // header
  20. Version string `json:"version"`
  21. // Standalone is set to true if the registry indicates it is a
  22. // standalone registry in the X-Docker-Registry-Standalone
  23. // header
  24. Standalone bool `json:"standalone"`
  25. }
  26. // v1Endpoint stores basic information about a V1 registry endpoint.
  27. type v1Endpoint struct {
  28. client *http.Client
  29. URL *url.URL
  30. IsSecure bool
  31. }
  32. // newV1Endpoint parses the given address to return a registry endpoint.
  33. // TODO: remove. This is only used by search.
  34. func newV1Endpoint(index *registry.IndexInfo, headers http.Header) (*v1Endpoint, error) {
  35. tlsConfig, err := newTLSConfig(index.Name, index.Secure)
  36. if err != nil {
  37. return nil, err
  38. }
  39. endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, headers)
  40. if err != nil {
  41. return nil, err
  42. }
  43. err = validateEndpoint(endpoint)
  44. if err != nil {
  45. return nil, err
  46. }
  47. return endpoint, nil
  48. }
  49. func validateEndpoint(endpoint *v1Endpoint) error {
  50. log.G(context.TODO()).Debugf("pinging registry endpoint %s", endpoint)
  51. // Try HTTPS ping to registry
  52. endpoint.URL.Scheme = "https"
  53. if _, err := endpoint.ping(); err != nil {
  54. if endpoint.IsSecure {
  55. // If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
  56. // in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP.
  57. return invalidParamf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host)
  58. }
  59. // If registry is insecure and HTTPS failed, fallback to HTTP.
  60. log.G(context.TODO()).WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint)
  61. endpoint.URL.Scheme = "http"
  62. var err2 error
  63. if _, err2 = endpoint.ping(); err2 == nil {
  64. return nil
  65. }
  66. return invalidParamf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
  67. }
  68. return nil
  69. }
  70. // trimV1Address trims the version off the address and returns the
  71. // trimmed address or an error if there is a non-V1 version.
  72. func trimV1Address(address string) (string, error) {
  73. address = strings.TrimSuffix(address, "/")
  74. chunks := strings.Split(address, "/")
  75. apiVersionStr := chunks[len(chunks)-1]
  76. if apiVersionStr == "v1" {
  77. return strings.Join(chunks[:len(chunks)-1], "/"), nil
  78. }
  79. for k, v := range apiVersions {
  80. if k != APIVersion1 && apiVersionStr == v {
  81. return "", invalidParamf("unsupported V1 version path %s", apiVersionStr)
  82. }
  83. }
  84. return address, nil
  85. }
  86. func newV1EndpointFromStr(address string, tlsConfig *tls.Config, headers http.Header) (*v1Endpoint, error) {
  87. if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
  88. address = "https://" + address
  89. }
  90. address, err := trimV1Address(address)
  91. if err != nil {
  92. return nil, err
  93. }
  94. uri, err := url.Parse(address)
  95. if err != nil {
  96. return nil, invalidParam(err)
  97. }
  98. // TODO(tiborvass): make sure a ConnectTimeout transport is used
  99. tr := newTransport(tlsConfig)
  100. return &v1Endpoint{
  101. IsSecure: tlsConfig == nil || !tlsConfig.InsecureSkipVerify,
  102. URL: uri,
  103. client: httpClient(transport.NewTransport(tr, Headers("", headers)...)),
  104. }, nil
  105. }
  106. // Get the formatted URL for the root of this registry Endpoint
  107. func (e *v1Endpoint) String() string {
  108. return e.URL.String() + "/v1/"
  109. }
  110. // ping returns a v1PingResult which indicates whether the registry is standalone or not.
  111. func (e *v1Endpoint) ping() (v1PingResult, error) {
  112. if e.String() == IndexServer {
  113. // Skip the check, we know this one is valid
  114. // (and we never want to fallback to http in case of error)
  115. return v1PingResult{}, nil
  116. }
  117. log.G(context.TODO()).Debugf("attempting v1 ping for registry endpoint %s", e)
  118. pingURL := e.String() + "_ping"
  119. req, err := http.NewRequest(http.MethodGet, pingURL, nil)
  120. if err != nil {
  121. return v1PingResult{}, invalidParam(err)
  122. }
  123. resp, err := e.client.Do(req)
  124. if err != nil {
  125. return v1PingResult{}, invalidParam(err)
  126. }
  127. defer resp.Body.Close()
  128. jsonString, err := io.ReadAll(resp.Body)
  129. if err != nil {
  130. return v1PingResult{}, invalidParamWrapf(err, "error while reading response from %s", pingURL)
  131. }
  132. // If the header is absent, we assume true for compatibility with earlier
  133. // versions of the registry. default to true
  134. info := v1PingResult{
  135. Standalone: true,
  136. }
  137. if err := json.Unmarshal(jsonString, &info); err != nil {
  138. log.G(context.TODO()).WithError(err).Debug("error unmarshaling _ping response")
  139. // don't stop here. Just assume sane defaults
  140. }
  141. if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" {
  142. info.Version = hdr
  143. }
  144. log.G(context.TODO()).Debugf("v1PingResult.Version: %q", info.Version)
  145. standalone := resp.Header.Get("X-Docker-Registry-Standalone")
  146. // Accepted values are "true" (case-insensitive) and "1".
  147. if strings.EqualFold(standalone, "true") || standalone == "1" {
  148. info.Standalone = true
  149. } else if len(standalone) > 0 {
  150. // there is a header set, and it is not "true" or "1", so assume fails
  151. info.Standalone = false
  152. }
  153. log.G(context.TODO()).Debugf("v1PingResult.Standalone: %t", info.Standalone)
  154. return info, nil
  155. }