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 }