urls.go 8.5 KB

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