| package registry // import "github.com/docker/docker/registry" |
| |
| import ( |
| // this is required for some certificates |
| _ "crypto/sha512" |
| "encoding/json" |
| "fmt" |
| "net/http" |
| "net/http/cookiejar" |
| "net/url" |
| "strings" |
| "sync" |
| |
| "github.com/docker/docker/api/types" |
| registrytypes "github.com/docker/docker/api/types/registry" |
| "github.com/docker/docker/errdefs" |
| "github.com/docker/docker/pkg/ioutils" |
| "github.com/docker/docker/pkg/jsonmessage" |
| "github.com/docker/docker/pkg/stringid" |
| "github.com/pkg/errors" |
| "github.com/sirupsen/logrus" |
| ) |
| |
| // A Session is used to communicate with a V1 registry |
| type Session struct { |
| indexEndpoint *V1Endpoint |
| client *http.Client |
| // TODO(tiborvass): remove authConfig |
| authConfig *types.AuthConfig |
| id string |
| } |
| |
| type authTransport struct { |
| http.RoundTripper |
| *types.AuthConfig |
| |
| alwaysSetBasicAuth bool |
| token []string |
| |
| mu sync.Mutex // guards modReq |
| modReq map[*http.Request]*http.Request // original -> modified |
| } |
| |
| // AuthTransport handles the auth layer when communicating with a v1 registry (private or official) |
| // |
| // For private v1 registries, set alwaysSetBasicAuth to true. |
| // |
| // For the official v1 registry, if there isn't already an Authorization header in the request, |
| // but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header. |
| // After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing |
| // a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent |
| // requests. |
| // |
| // If the server sends a token without the client having requested it, it is ignored. |
| // |
| // This RoundTripper also has a CancelRequest method important for correct timeout handling. |
| func AuthTransport(base http.RoundTripper, authConfig *types.AuthConfig, alwaysSetBasicAuth bool) http.RoundTripper { |
| if base == nil { |
| base = http.DefaultTransport |
| } |
| return &authTransport{ |
| RoundTripper: base, |
| AuthConfig: authConfig, |
| alwaysSetBasicAuth: alwaysSetBasicAuth, |
| modReq: make(map[*http.Request]*http.Request), |
| } |
| } |
| |
| // cloneRequest returns a clone of the provided *http.Request. |
| // The clone is a shallow copy of the struct and its Header map. |
| func cloneRequest(r *http.Request) *http.Request { |
| // shallow copy of the struct |
| r2 := new(http.Request) |
| *r2 = *r |
| // deep copy of the Header |
| r2.Header = make(http.Header, len(r.Header)) |
| for k, s := range r.Header { |
| r2.Header[k] = append([]string(nil), s...) |
| } |
| |
| return r2 |
| } |
| |
| // RoundTrip changes an HTTP request's headers to add the necessary |
| // authentication-related headers |
| func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) { |
| // Authorization should not be set on 302 redirect for untrusted locations. |
| // This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests. |
| // As the authorization logic is currently implemented in RoundTrip, |
| // a 302 redirect is detected by looking at the Referrer header as go http package adds said header. |
| // This is safe as Docker doesn't set Referrer in other scenarios. |
| if orig.Header.Get("Referer") != "" && !trustedLocation(orig) { |
| return tr.RoundTripper.RoundTrip(orig) |
| } |
| |
| req := cloneRequest(orig) |
| tr.mu.Lock() |
| tr.modReq[orig] = req |
| tr.mu.Unlock() |
| |
| if tr.alwaysSetBasicAuth { |
| if tr.AuthConfig == nil { |
| return nil, errors.New("unexpected error: empty auth config") |
| } |
| req.SetBasicAuth(tr.Username, tr.Password) |
| return tr.RoundTripper.RoundTrip(req) |
| } |
| |
| // Don't override |
| if req.Header.Get("Authorization") == "" { |
| if req.Header.Get("X-Docker-Token") == "true" && tr.AuthConfig != nil && len(tr.Username) > 0 { |
| req.SetBasicAuth(tr.Username, tr.Password) |
| } else if len(tr.token) > 0 { |
| req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ",")) |
| } |
| } |
| resp, err := tr.RoundTripper.RoundTrip(req) |
| if err != nil { |
| tr.mu.Lock() |
| delete(tr.modReq, orig) |
| tr.mu.Unlock() |
| return nil, err |
| } |
| if len(resp.Header["X-Docker-Token"]) > 0 { |
| tr.token = resp.Header["X-Docker-Token"] |
| } |
| resp.Body = &ioutils.OnEOFReader{ |
| Rc: resp.Body, |
| Fn: func() { |
| tr.mu.Lock() |
| delete(tr.modReq, orig) |
| tr.mu.Unlock() |
| }, |
| } |
| return resp, nil |
| } |
| |
| // CancelRequest cancels an in-flight request by closing its connection. |
| func (tr *authTransport) CancelRequest(req *http.Request) { |
| type canceler interface { |
| CancelRequest(*http.Request) |
| } |
| if cr, ok := tr.RoundTripper.(canceler); ok { |
| tr.mu.Lock() |
| modReq := tr.modReq[req] |
| delete(tr.modReq, req) |
| tr.mu.Unlock() |
| cr.CancelRequest(modReq) |
| } |
| } |
| |
| func authorizeClient(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) error { |
| var alwaysSetBasicAuth bool |
| |
| // If we're working with a standalone private registry over HTTPS, send Basic Auth headers |
| // alongside all our requests. |
| if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" { |
| info, err := endpoint.Ping() |
| if err != nil { |
| return err |
| } |
| if info.Standalone && authConfig != nil { |
| logrus.Debugf("Endpoint %s is eligible for private registry. Enabling decorator.", endpoint.String()) |
| alwaysSetBasicAuth = true |
| } |
| } |
| |
| // Annotate the transport unconditionally so that v2 can |
| // properly fallback on v1 when an image is not found. |
| client.Transport = AuthTransport(client.Transport, authConfig, alwaysSetBasicAuth) |
| |
| jar, err := cookiejar.New(nil) |
| if err != nil { |
| return errors.New("cookiejar.New is not supposed to return an error") |
| } |
| client.Jar = jar |
| |
| return nil |
| } |
| |
| func newSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) *Session { |
| return &Session{ |
| authConfig: authConfig, |
| client: client, |
| indexEndpoint: endpoint, |
| id: stringid.GenerateRandomID(), |
| } |
| } |
| |
| // NewSession creates a new session |
| // TODO(tiborvass): remove authConfig param once registry client v2 is vendored |
| func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) (*Session, error) { |
| if err := authorizeClient(client, authConfig, endpoint); err != nil { |
| return nil, err |
| } |
| |
| return newSession(client, authConfig, endpoint), nil |
| } |
| |
| // SearchRepositories performs a search against the remote repository |
| func (r *Session) SearchRepositories(term string, limit int) (*registrytypes.SearchResults, error) { |
| if limit < 1 || limit > 100 { |
| return nil, errdefs.InvalidParameter(errors.Errorf("Limit %d is outside the range of [1, 100]", limit)) |
| } |
| logrus.Debugf("Index server: %s", r.indexEndpoint) |
| u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(fmt.Sprintf("%d", limit)) |
| |
| req, err := http.NewRequest(http.MethodGet, u, nil) |
| if err != nil { |
| return nil, errors.Wrap(errdefs.InvalidParameter(err), "Error building request") |
| } |
| // Have the AuthTransport send authentication, when logged in. |
| req.Header.Set("X-Docker-Token", "true") |
| res, err := r.client.Do(req) |
| if err != nil { |
| return nil, errdefs.System(err) |
| } |
| defer res.Body.Close() |
| if res.StatusCode != http.StatusOK { |
| return nil, &jsonmessage.JSONError{ |
| Message: fmt.Sprintf("Unexpected status code %d", res.StatusCode), |
| Code: res.StatusCode, |
| } |
| } |
| result := new(registrytypes.SearchResults) |
| return result, errors.Wrap(json.NewDecoder(res.Body).Decode(result), "error decoding registry search results") |
| } |