urls.go 6.8 KB

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