github.com/mika/distribution@v2.2.2-0.20160108133430-a75790e3d8e0+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/digest" 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 string) (string, error) { 117 route := ub.cloneRoute(RouteNameTags) 118 119 tagsURL, err := route.URL("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(name, reference string) (string, error) { 130 route := ub.cloneRoute(RouteNameManifest) 131 132 manifestURL, err := route.URL("name", name, "reference", reference) 133 if err != nil { 134 return "", err 135 } 136 137 return manifestURL.String(), nil 138 } 139 140 // BuildBlobURL constructs the url for the blob identified by name and dgst. 141 func (ub *URLBuilder) BuildBlobURL(name string, dgst digest.Digest) (string, error) { 142 route := ub.cloneRoute(RouteNameBlob) 143 144 layerURL, err := route.URL("name", name, "digest", dgst.String()) 145 if err != nil { 146 return "", err 147 } 148 149 return layerURL.String(), nil 150 } 151 152 // BuildBlobUploadURL constructs a url to begin a blob upload in the 153 // repository identified by name. 154 func (ub *URLBuilder) BuildBlobUploadURL(name string, values ...url.Values) (string, error) { 155 route := ub.cloneRoute(RouteNameBlobUpload) 156 157 uploadURL, err := route.URL("name", name) 158 if err != nil { 159 return "", err 160 } 161 162 return appendValuesURL(uploadURL, values...).String(), nil 163 } 164 165 // BuildBlobUploadChunkURL constructs a url for the upload identified by uuid, 166 // including any url values. This should generally not be used by clients, as 167 // this url is provided by server implementations during the blob upload 168 // process. 169 func (ub *URLBuilder) BuildBlobUploadChunkURL(name, uuid string, values ...url.Values) (string, error) { 170 route := ub.cloneRoute(RouteNameBlobUploadChunk) 171 172 uploadURL, err := route.URL("name", name, "uuid", uuid) 173 if err != nil { 174 return "", err 175 } 176 177 return appendValuesURL(uploadURL, values...).String(), nil 178 } 179 180 // clondedRoute returns a clone of the named route from the router. Routes 181 // must be cloned to avoid modifying them during url generation. 182 func (ub *URLBuilder) cloneRoute(name string) clonedRoute { 183 route := new(mux.Route) 184 root := new(url.URL) 185 186 *route = *ub.router.GetRoute(name) // clone the route 187 *root = *ub.root 188 189 return clonedRoute{Route: route, root: root} 190 } 191 192 type clonedRoute struct { 193 *mux.Route 194 root *url.URL 195 } 196 197 func (cr clonedRoute) URL(pairs ...string) (*url.URL, error) { 198 routeURL, err := cr.Route.URL(pairs...) 199 if err != nil { 200 return nil, err 201 } 202 203 if routeURL.Scheme == "" && routeURL.User == nil && routeURL.Host == "" { 204 routeURL.Path = routeURL.Path[1:] 205 } 206 207 url := cr.root.ResolveReference(routeURL) 208 url.Scheme = cr.root.Scheme 209 return url, nil 210 } 211 212 // appendValuesURL appends the parameters to the url. 213 func appendValuesURL(u *url.URL, values ...url.Values) *url.URL { 214 merged := u.Query() 215 216 for _, v := range values { 217 for k, vv := range v { 218 merged[k] = append(merged[k], vv...) 219 } 220 } 221 222 u.RawQuery = merged.Encode() 223 return u 224 } 225 226 // appendValues appends the parameters to the url. Panics if the string is not 227 // a url. 228 func appendValues(u string, values ...url.Values) string { 229 up, err := url.Parse(u) 230 231 if err != nil { 232 panic(err) // should never happen 233 } 234 235 return appendValuesURL(up, values...).String() 236 }