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