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