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