github.com/docker/engine@v22.0.0-20211208180946-d456264580cf+incompatible/registry/session.go (about)

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