github.com/npaton/distribution@v2.3.1-rc.0+incompatible/registry/api/v2/urls.go (about)

     1  package v2
     2  
     3  import (
     4  	"net/http"
     5  	"net/url"
     6  	"strings"
     7  
     8  	"github.com/docker/distribution/reference"
     9  	"github.com/gorilla/mux"
    10  )
    11  
    12  // URLBuilder creates registry API urls from a single base endpoint. It can be
    13  // used to create urls for use in a registry client or server.
    14  //
    15  // All urls will be created from the given base, including the api version.
    16  // For example, if a root of "/foo/" is provided, urls generated will be fall
    17  // under "/foo/v2/...". Most application will only provide a schema, host and
    18  // port, such as "https://localhost:5000/".
    19  type URLBuilder struct {
    20  	root   *url.URL // url root (ie http://localhost/)
    21  	router *mux.Router
    22  }
    23  
    24  // NewURLBuilder creates a URLBuilder with provided root url object.
    25  func NewURLBuilder(root *url.URL) *URLBuilder {
    26  	return &URLBuilder{
    27  		root:   root,
    28  		router: Router(),
    29  	}
    30  }
    31  
    32  // NewURLBuilderFromString workes identically to NewURLBuilder except it takes
    33  // a string argument for the root, returning an error if it is not a valid
    34  // url.
    35  func NewURLBuilderFromString(root string) (*URLBuilder, error) {
    36  	u, err := url.Parse(root)
    37  	if err != nil {
    38  		return nil, err
    39  	}
    40  
    41  	return NewURLBuilder(u), nil
    42  }
    43  
    44  // NewURLBuilderFromRequest uses information from an *http.Request to
    45  // construct the root url.
    46  func NewURLBuilderFromRequest(r *http.Request) *URLBuilder {
    47  	var scheme string
    48  
    49  	forwardedProto := r.Header.Get("X-Forwarded-Proto")
    50  
    51  	switch {
    52  	case len(forwardedProto) > 0:
    53  		scheme = forwardedProto
    54  	case r.TLS != nil:
    55  		scheme = "https"
    56  	case len(r.URL.Scheme) > 0:
    57  		scheme = r.URL.Scheme
    58  	default:
    59  		scheme = "http"
    60  	}
    61  
    62  	host := r.Host
    63  	forwardedHost := r.Header.Get("X-Forwarded-Host")
    64  	if len(forwardedHost) > 0 {
    65  		// According to the Apache mod_proxy docs, X-Forwarded-Host can be a
    66  		// comma-separated list of hosts, to which each proxy appends the
    67  		// requested host. We want to grab the first from this comma-separated
    68  		// list.
    69  		hosts := strings.SplitN(forwardedHost, ",", 2)
    70  		host = strings.TrimSpace(hosts[0])
    71  	}
    72  
    73  	basePath := routeDescriptorsMap[RouteNameBase].Path
    74  
    75  	requestPath := r.URL.Path
    76  	index := strings.Index(requestPath, basePath)
    77  
    78  	u := &url.URL{
    79  		Scheme: scheme,
    80  		Host:   host,
    81  	}
    82  
    83  	if index > 0 {
    84  		// N.B. index+1 is important because we want to include the trailing /
    85  		u.Path = requestPath[0 : index+1]
    86  	}
    87  
    88  	return NewURLBuilder(u)
    89  }
    90  
    91  // BuildBaseURL constructs a base url for the API, typically just "/v2/".
    92  func (ub *URLBuilder) BuildBaseURL() (string, error) {
    93  	route := ub.cloneRoute(RouteNameBase)
    94  
    95  	baseURL, err := route.URL()
    96  	if err != nil {
    97  		return "", err
    98  	}
    99  
   100  	return baseURL.String(), nil
   101  }
   102  
   103  // BuildCatalogURL constructs a url get a catalog of repositories
   104  func (ub *URLBuilder) BuildCatalogURL(values ...url.Values) (string, error) {
   105  	route := ub.cloneRoute(RouteNameCatalog)
   106  
   107  	catalogURL, err := route.URL()
   108  	if err != nil {
   109  		return "", err
   110  	}
   111  
   112  	return appendValuesURL(catalogURL, values...).String(), nil
   113  }
   114  
   115  // BuildTagsURL constructs a url to list the tags in the named repository.
   116  func (ub *URLBuilder) BuildTagsURL(name reference.Named) (string, error) {
   117  	route := ub.cloneRoute(RouteNameTags)
   118  
   119  	tagsURL, err := route.URL("name", name.Name())
   120  	if err != nil {
   121  		return "", err
   122  	}
   123  
   124  	return tagsURL.String(), nil
   125  }
   126  
   127  // BuildManifestURL constructs a url for the manifest identified by name and
   128  // reference. The argument reference may be either a tag or digest.
   129  func (ub *URLBuilder) BuildManifestURL(ref reference.Named) (string, error) {
   130  	route := ub.cloneRoute(RouteNameManifest)
   131  
   132  	tagOrDigest := ""
   133  	switch v := ref.(type) {
   134  	case reference.Tagged:
   135  		tagOrDigest = v.Tag()
   136  	case reference.Digested:
   137  		tagOrDigest = v.Digest().String()
   138  	}
   139  
   140  	manifestURL, err := route.URL("name", ref.Name(), "reference", tagOrDigest)
   141  	if err != nil {
   142  		return "", err
   143  	}
   144  
   145  	return manifestURL.String(), nil
   146  }
   147  
   148  // BuildBlobURL constructs the url for the blob identified by name and dgst.
   149  func (ub *URLBuilder) BuildBlobURL(ref reference.Canonical) (string, error) {
   150  	route := ub.cloneRoute(RouteNameBlob)
   151  
   152  	layerURL, err := route.URL("name", ref.Name(), "digest", ref.Digest().String())
   153  	if err != nil {
   154  		return "", err
   155  	}
   156  
   157  	return layerURL.String(), nil
   158  }
   159  
   160  // BuildBlobUploadURL constructs a url to begin a blob upload in the
   161  // repository identified by name.
   162  func (ub *URLBuilder) BuildBlobUploadURL(name reference.Named, values ...url.Values) (string, error) {
   163  	route := ub.cloneRoute(RouteNameBlobUpload)
   164  
   165  	uploadURL, err := route.URL("name", name.Name())
   166  	if err != nil {
   167  		return "", err
   168  	}
   169  
   170  	return appendValuesURL(uploadURL, values...).String(), nil
   171  }
   172  
   173  // BuildBlobUploadChunkURL constructs a url for the upload identified by uuid,
   174  // including any url values. This should generally not be used by clients, as
   175  // this url is provided by server implementations during the blob upload
   176  // process.
   177  func (ub *URLBuilder) BuildBlobUploadChunkURL(name reference.Named, uuid string, values ...url.Values) (string, error) {
   178  	route := ub.cloneRoute(RouteNameBlobUploadChunk)
   179  
   180  	uploadURL, err := route.URL("name", name.Name(), "uuid", uuid)
   181  	if err != nil {
   182  		return "", err
   183  	}
   184  
   185  	return appendValuesURL(uploadURL, values...).String(), nil
   186  }
   187  
   188  // clondedRoute returns a clone of the named route from the router. Routes
   189  // must be cloned to avoid modifying them during url generation.
   190  func (ub *URLBuilder) cloneRoute(name string) clonedRoute {
   191  	route := new(mux.Route)
   192  	root := new(url.URL)
   193  
   194  	*route = *ub.router.GetRoute(name) // clone the route
   195  	*root = *ub.root
   196  
   197  	return clonedRoute{Route: route, root: root}
   198  }
   199  
   200  type clonedRoute struct {
   201  	*mux.Route
   202  	root *url.URL
   203  }
   204  
   205  func (cr clonedRoute) URL(pairs ...string) (*url.URL, error) {
   206  	routeURL, err := cr.Route.URL(pairs...)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  
   211  	if routeURL.Scheme == "" && routeURL.User == nil && routeURL.Host == "" {
   212  		routeURL.Path = routeURL.Path[1:]
   213  	}
   214  
   215  	url := cr.root.ResolveReference(routeURL)
   216  	url.Scheme = cr.root.Scheme
   217  	return url, nil
   218  }
   219  
   220  // appendValuesURL appends the parameters to the url.
   221  func appendValuesURL(u *url.URL, values ...url.Values) *url.URL {
   222  	merged := u.Query()
   223  
   224  	for _, v := range values {
   225  		for k, vv := range v {
   226  			merged[k] = append(merged[k], vv...)
   227  		}
   228  	}
   229  
   230  	u.RawQuery = merged.Encode()
   231  	return u
   232  }
   233  
   234  // appendValues appends the parameters to the url. Panics if the string is not
   235  // a url.
   236  func appendValues(u string, values ...url.Values) string {
   237  	up, err := url.Parse(u)
   238  
   239  	if err != nil {
   240  		panic(err) // should never happen
   241  	}
   242  
   243  	return appendValuesURL(up, values...).String()
   244  }