urls.go 6.5 KB

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