session.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. package registry // import "github.com/docker/docker/registry"
  2. import (
  3. // this is required for some certificates
  4. "context"
  5. _ "crypto/sha512"
  6. "encoding/json"
  7. "fmt"
  8. "net/http"
  9. "net/http/cookiejar"
  10. "net/url"
  11. "strconv"
  12. "strings"
  13. "sync"
  14. "github.com/containerd/containerd/log"
  15. "github.com/docker/docker/api/types/registry"
  16. "github.com/docker/docker/errdefs"
  17. "github.com/docker/docker/pkg/ioutils"
  18. "github.com/docker/docker/pkg/jsonmessage"
  19. "github.com/pkg/errors"
  20. )
  21. // A session is used to communicate with a V1 registry
  22. type session struct {
  23. indexEndpoint *v1Endpoint
  24. client *http.Client
  25. }
  26. type authTransport struct {
  27. http.RoundTripper
  28. *registry.AuthConfig
  29. alwaysSetBasicAuth bool
  30. token []string
  31. mu sync.Mutex // guards modReq
  32. modReq map[*http.Request]*http.Request // original -> modified
  33. }
  34. // newAuthTransport handles the auth layer when communicating with a v1 registry (private or official)
  35. //
  36. // For private v1 registries, set alwaysSetBasicAuth to true.
  37. //
  38. // For the official v1 registry, if there isn't already an Authorization header in the request,
  39. // but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header.
  40. // After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing
  41. // a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent
  42. // requests.
  43. //
  44. // If the server sends a token without the client having requested it, it is ignored.
  45. //
  46. // This RoundTripper also has a CancelRequest method important for correct timeout handling.
  47. func newAuthTransport(base http.RoundTripper, authConfig *registry.AuthConfig, alwaysSetBasicAuth bool) *authTransport {
  48. if base == nil {
  49. base = http.DefaultTransport
  50. }
  51. return &authTransport{
  52. RoundTripper: base,
  53. AuthConfig: authConfig,
  54. alwaysSetBasicAuth: alwaysSetBasicAuth,
  55. modReq: make(map[*http.Request]*http.Request),
  56. }
  57. }
  58. // cloneRequest returns a clone of the provided *http.Request.
  59. // The clone is a shallow copy of the struct and its Header map.
  60. func cloneRequest(r *http.Request) *http.Request {
  61. // shallow copy of the struct
  62. r2 := new(http.Request)
  63. *r2 = *r
  64. // deep copy of the Header
  65. r2.Header = make(http.Header, len(r.Header))
  66. for k, s := range r.Header {
  67. r2.Header[k] = append([]string(nil), s...)
  68. }
  69. return r2
  70. }
  71. // RoundTrip changes an HTTP request's headers to add the necessary
  72. // authentication-related headers
  73. func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
  74. // Authorization should not be set on 302 redirect for untrusted locations.
  75. // This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests.
  76. // As the authorization logic is currently implemented in RoundTrip,
  77. // a 302 redirect is detected by looking at the Referrer header as go http package adds said header.
  78. // This is safe as Docker doesn't set Referrer in other scenarios.
  79. if orig.Header.Get("Referer") != "" && !trustedLocation(orig) {
  80. return tr.RoundTripper.RoundTrip(orig)
  81. }
  82. req := cloneRequest(orig)
  83. tr.mu.Lock()
  84. tr.modReq[orig] = req
  85. tr.mu.Unlock()
  86. if tr.alwaysSetBasicAuth {
  87. if tr.AuthConfig == nil {
  88. return nil, errors.New("unexpected error: empty auth config")
  89. }
  90. req.SetBasicAuth(tr.Username, tr.Password)
  91. return tr.RoundTripper.RoundTrip(req)
  92. }
  93. // Don't override
  94. if req.Header.Get("Authorization") == "" {
  95. if req.Header.Get("X-Docker-Token") == "true" && tr.AuthConfig != nil && len(tr.Username) > 0 {
  96. req.SetBasicAuth(tr.Username, tr.Password)
  97. } else if len(tr.token) > 0 {
  98. req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ","))
  99. }
  100. }
  101. resp, err := tr.RoundTripper.RoundTrip(req)
  102. if err != nil {
  103. tr.mu.Lock()
  104. delete(tr.modReq, orig)
  105. tr.mu.Unlock()
  106. return nil, err
  107. }
  108. if len(resp.Header["X-Docker-Token"]) > 0 {
  109. tr.token = resp.Header["X-Docker-Token"]
  110. }
  111. resp.Body = &ioutils.OnEOFReader{
  112. Rc: resp.Body,
  113. Fn: func() {
  114. tr.mu.Lock()
  115. delete(tr.modReq, orig)
  116. tr.mu.Unlock()
  117. },
  118. }
  119. return resp, nil
  120. }
  121. // CancelRequest cancels an in-flight request by closing its connection.
  122. func (tr *authTransport) CancelRequest(req *http.Request) {
  123. type canceler interface {
  124. CancelRequest(*http.Request)
  125. }
  126. if cr, ok := tr.RoundTripper.(canceler); ok {
  127. tr.mu.Lock()
  128. modReq := tr.modReq[req]
  129. delete(tr.modReq, req)
  130. tr.mu.Unlock()
  131. cr.CancelRequest(modReq)
  132. }
  133. }
  134. func authorizeClient(client *http.Client, authConfig *registry.AuthConfig, endpoint *v1Endpoint) error {
  135. var alwaysSetBasicAuth bool
  136. // If we're working with a standalone private registry over HTTPS, send Basic Auth headers
  137. // alongside all our requests.
  138. if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" {
  139. info, err := endpoint.ping()
  140. if err != nil {
  141. return err
  142. }
  143. if info.Standalone && authConfig != nil {
  144. log.G(context.TODO()).Debugf("Endpoint %s is eligible for private registry. Enabling decorator.", endpoint.String())
  145. alwaysSetBasicAuth = true
  146. }
  147. }
  148. // Annotate the transport unconditionally so that v2 can
  149. // properly fallback on v1 when an image is not found.
  150. client.Transport = newAuthTransport(client.Transport, authConfig, alwaysSetBasicAuth)
  151. jar, err := cookiejar.New(nil)
  152. if err != nil {
  153. return errdefs.System(errors.New("cookiejar.New is not supposed to return an error"))
  154. }
  155. client.Jar = jar
  156. return nil
  157. }
  158. func newSession(client *http.Client, endpoint *v1Endpoint) *session {
  159. return &session{
  160. client: client,
  161. indexEndpoint: endpoint,
  162. }
  163. }
  164. // defaultSearchLimit is the default value for maximum number of returned search results.
  165. const defaultSearchLimit = 25
  166. // searchRepositories performs a search against the remote repository
  167. func (r *session) searchRepositories(term string, limit int) (*registry.SearchResults, error) {
  168. if limit == 0 {
  169. limit = defaultSearchLimit
  170. }
  171. if limit < 1 || limit > 100 {
  172. return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit)
  173. }
  174. u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(fmt.Sprintf("%d", limit))
  175. log.G(context.TODO()).WithField("url", u).Debug("searchRepositories")
  176. req, err := http.NewRequest(http.MethodGet, u, nil)
  177. if err != nil {
  178. return nil, invalidParamWrapf(err, "error building request")
  179. }
  180. // Have the AuthTransport send authentication, when logged in.
  181. req.Header.Set("X-Docker-Token", "true")
  182. res, err := r.client.Do(req)
  183. if err != nil {
  184. return nil, errdefs.System(err)
  185. }
  186. defer res.Body.Close()
  187. if res.StatusCode != http.StatusOK {
  188. return nil, errdefs.Unknown(&jsonmessage.JSONError{
  189. Message: "Unexpected status code " + strconv.Itoa(res.StatusCode),
  190. Code: res.StatusCode,
  191. })
  192. }
  193. result := &registry.SearchResults{}
  194. err = json.NewDecoder(res.Body).Decode(result)
  195. if err != nil {
  196. return nil, errdefs.System(errors.Wrap(err, "error decoding registry search results"))
  197. }
  198. return result, nil
  199. }