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 := &registry.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  }