registry/session.go
4f0d95fa
 package registry // import "github.com/docker/docker/registry"
752dd707
 
 import (
ae3b59c1
 	// this is required for some certificates
752dd707
 	_ "crypto/sha512"
 	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/http/cookiejar"
 	"net/url"
 	"strings"
f23c00d8
 	"sync"
752dd707
 
91e197d6
 	"github.com/docker/docker/api/types"
 	registrytypes "github.com/docker/docker/api/types/registry"
d453fe35
 	"github.com/docker/docker/errdefs"
276c640b
 	"github.com/docker/docker/pkg/ioutils"
a6ac5495
 	"github.com/docker/docker/pkg/jsonmessage"
1b67c38f
 	"github.com/docker/docker/pkg/stringid"
ebcb7d6b
 	"github.com/pkg/errors"
1009e6a4
 	"github.com/sirupsen/logrus"
752dd707
 )
 
4fcb9ac4
 // A Session is used to communicate with a V1 registry
752dd707
 type Session struct {
f2d481a2
 	indexEndpoint *V1Endpoint
a01cc3ca
 	client        *http.Client
 	// TODO(tiborvass): remove authConfig
5b321e32
 	authConfig *types.AuthConfig
1b67c38f
 	id         string
752dd707
 }
 
73823e5e
 type authTransport struct {
 	http.RoundTripper
5b321e32
 	*types.AuthConfig
73823e5e
 
 	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)
a01cc3ca
 //
 // 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.
5b321e32
 func AuthTransport(base http.RoundTripper, authConfig *types.AuthConfig, alwaysSetBasicAuth bool) http.RoundTripper {
73823e5e
 	if base == nil {
 		base = http.DefaultTransport
 	}
 	return &authTransport{
 		RoundTripper:       base,
 		AuthConfig:         authConfig,
 		alwaysSetBasicAuth: alwaysSetBasicAuth,
 		modReq:             make(map[*http.Request]*http.Request),
 	}
a01cc3ca
 }
752dd707
 
276c640b
 // 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
 }
 
c1be45fa
 // RoundTrip changes an HTTP request's headers to add the necessary
4fcb9ac4
 // authentication-related headers
73823e5e
 func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
123a0582
 	// Authorization should not be set on 302 redirect for untrusted locations.
4fcb9ac4
 	// This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests.
123a0582
 	// As the authorization logic is currently implemented in RoundTrip,
927b334e
 	// 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.
123a0582
 	if orig.Header.Get("Referer") != "" && !trustedLocation(orig) {
 		return tr.RoundTripper.RoundTrip(orig)
 	}
 
19515a7a
 	req := cloneRequest(orig)
73823e5e
 	tr.mu.Lock()
 	tr.modReq[orig] = req
 	tr.mu.Unlock()
a01cc3ca
 
 	if tr.alwaysSetBasicAuth {
b32c4cb4
 		if tr.AuthConfig == nil {
 			return nil, errors.New("unexpected error: empty auth config")
 		}
a01cc3ca
 		req.SetBasicAuth(tr.Username, tr.Password)
 		return tr.RoundTripper.RoundTrip(req)
752dd707
 	}
 
a01cc3ca
 	// Don't override
 	if req.Header.Get("Authorization") == "" {
b32c4cb4
 		if req.Header.Get("X-Docker-Token") == "true" && tr.AuthConfig != nil && len(tr.Username) > 0 {
a01cc3ca
 			req.SetBasicAuth(tr.Username, tr.Password)
123a0582
 		} else if len(tr.token) > 0 {
a01cc3ca
 			req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ","))
 		}
 	}
 	resp, err := tr.RoundTripper.RoundTrip(req)
752dd707
 	if err != nil {
49fbb9c9
 		tr.mu.Lock()
73823e5e
 		delete(tr.modReq, orig)
49fbb9c9
 		tr.mu.Unlock()
752dd707
 		return nil, err
 	}
fc29f7f7
 	if len(resp.Header["X-Docker-Token"]) > 0 {
a01cc3ca
 		tr.token = resp.Header["X-Docker-Token"]
 	}
276c640b
 	resp.Body = &ioutils.OnEOFReader{
73823e5e
 		Rc: resp.Body,
9d98c288
 		Fn: func() {
 			tr.mu.Lock()
 			delete(tr.modReq, orig)
 			tr.mu.Unlock()
 		},
73823e5e
 	}
a01cc3ca
 	return resp, nil
 }
 
73823e5e
 // 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)
 	}
 }
 
19d48f0b
 func authorizeClient(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) error {
a01cc3ca
 	var alwaysSetBasicAuth bool
752dd707
 
 	// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
a01cc3ca
 	// alongside all our requests.
f2d481a2
 	if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" {
a01cc3ca
 		info, err := endpoint.Ping()
752dd707
 		if err != nil {
19d48f0b
 			return err
752dd707
 		}
a01cc3ca
 		if info.Standalone && authConfig != nil {
 			logrus.Debugf("Endpoint %s is eligible for private registry. Enabling decorator.", endpoint.String())
 			alwaysSetBasicAuth = true
752dd707
 		}
 	}
 
c2315102
 	// 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)
752dd707
 
a01cc3ca
 	jar, err := cookiejar.New(nil)
 	if err != nil {
19d48f0b
 		return errors.New("cookiejar.New is not supposed to return an error")
a01cc3ca
 	}
 	client.Jar = jar
 
19d48f0b
 	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
752dd707
 }
 
4fcb9ac4
 // SearchRepositories performs a search against the remote repository
92f10fe2
 func (r *Session) SearchRepositories(term string, limit int) (*registrytypes.SearchResults, error) {
 	if limit < 1 || limit > 100 {
87a12421
 		return nil, errdefs.InvalidParameter(errors.Errorf("Limit %d is outside the range of [1, 100]", limit))
92f10fe2
 	}
6f4d8470
 	logrus.Debugf("Index server: %s", r.indexEndpoint)
92f10fe2
 	u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(fmt.Sprintf("%d", limit))
5a170484
 
441b031b
 	req, err := http.NewRequest(http.MethodGet, u, nil)
5a170484
 	if err != nil {
87a12421
 		return nil, errors.Wrap(errdefs.InvalidParameter(err), "Error building request")
5a170484
 	}
 	// Have the AuthTransport send authentication, when logged in.
 	req.Header.Set("X-Docker-Token", "true")
 	res, err := r.client.Do(req)
752dd707
 	if err != nil {
87a12421
 		return nil, errdefs.System(err)
752dd707
 	}
 	defer res.Body.Close()
63e62d13
 	if res.StatusCode != http.StatusOK {
3f7c62f6
 		return nil, &jsonmessage.JSONError{
 			Message: fmt.Sprintf("Unexpected status code %d", res.StatusCode),
 			Code:    res.StatusCode,
 		}
752dd707
 	}
c4472b38
 	result := new(registrytypes.SearchResults)
ebcb7d6b
 	return result, errors.Wrap(json.NewDecoder(res.Body).Decode(result), "error decoding registry search results")
752dd707
 }