1 package v2
2
3 import (
4 "fmt"
5 "net/http"
6 "net/url"
7 "reflect"
8 "testing"
9
10 "github.com/distribution/reference"
11 )
12
13 type urlBuilderTestCase struct {
14 description string
15 expectedPath string
16 expectedErr error
17 build func() (string, error)
18 }
19
20 func makeURLBuilderTestCases(urlBuilder *URLBuilder) []urlBuilderTestCase {
21 fooBarRef, _ := reference.WithName("foo/bar")
22 return []urlBuilderTestCase{
23 {
24 description: "test base url",
25 expectedPath: "/v2/",
26 expectedErr: nil,
27 build: urlBuilder.BuildBaseURL,
28 },
29 {
30 description: "test tags url",
31 expectedPath: "/v2/foo/bar/tags/list",
32 expectedErr: nil,
33 build: func() (string, error) {
34 return urlBuilder.BuildTagsURL(fooBarRef)
35 },
36 },
37 {
38 description: "test manifest url tagged ref",
39 expectedPath: "/v2/foo/bar/manifests/tag",
40 expectedErr: nil,
41 build: func() (string, error) {
42 ref, _ := reference.WithTag(fooBarRef, "tag")
43 return urlBuilder.BuildManifestURL(ref)
44 },
45 },
46 {
47 description: "test manifest url bare ref",
48 expectedPath: "",
49 expectedErr: fmt.Errorf("reference must have a tag or digest"),
50 build: func() (string, error) {
51 return urlBuilder.BuildManifestURL(fooBarRef)
52 },
53 },
54 {
55 description: "build blob url",
56 expectedPath: "/v2/foo/bar/blobs/sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5",
57 expectedErr: nil,
58 build: func() (string, error) {
59 ref, _ := reference.WithDigest(fooBarRef, "sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5")
60 return urlBuilder.BuildBlobURL(ref)
61 },
62 },
63 {
64 description: "build blob upload url",
65 expectedPath: "/v2/foo/bar/blobs/uploads/",
66 expectedErr: nil,
67 build: func() (string, error) {
68 return urlBuilder.BuildBlobUploadURL(fooBarRef)
69 },
70 },
71 {
72 description: "build blob upload url with digest and size",
73 expectedPath: "/v2/foo/bar/blobs/uploads/?digest=sha256%3A3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5&size=10000",
74 expectedErr: nil,
75 build: func() (string, error) {
76 return urlBuilder.BuildBlobUploadURL(fooBarRef, url.Values{
77 "size": []string{"10000"},
78 "digest": []string{"sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5"},
79 })
80 },
81 },
82 {
83 description: "build blob upload chunk url",
84 expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part",
85 expectedErr: nil,
86 build: func() (string, error) {
87 return urlBuilder.BuildBlobUploadChunkURL(fooBarRef, "uuid-part")
88 },
89 },
90 {
91 description: "build blob upload chunk url with digest and size",
92 expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part?digest=sha256%3A3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5&size=10000",
93 expectedErr: nil,
94 build: func() (string, error) {
95 return urlBuilder.BuildBlobUploadChunkURL(fooBarRef, "uuid-part", url.Values{
96 "size": []string{"10000"},
97 "digest": []string{"sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5"},
98 })
99 },
100 },
101 }
102 }
103
104
105
106 func TestURLBuilder(t *testing.T) {
107 roots := []string{
108 "http://example.com",
109 "https://example.com",
110 "http://localhost:5000",
111 "https://localhost:5443",
112 }
113
114 doTest := func(relative bool) {
115 for _, root := range roots {
116 urlBuilder, err := NewURLBuilderFromString(root, relative)
117 if err != nil {
118 t.Fatalf("unexpected error creating urlbuilder: %v", err)
119 }
120
121 for _, testCase := range makeURLBuilderTestCases(urlBuilder) {
122 url, err := testCase.build()
123 expectedErr := testCase.expectedErr
124 if !reflect.DeepEqual(expectedErr, err) {
125 t.Fatalf("%s: Expecting %v but got error %v", testCase.description, expectedErr, err)
126 }
127 if expectedErr != nil {
128 continue
129 }
130
131 expectedURL := testCase.expectedPath
132 if !relative {
133 expectedURL = root + expectedURL
134 }
135
136 if url != expectedURL {
137 t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
138 }
139 }
140 }
141 }
142 doTest(true)
143 doTest(false)
144 }
145
146 func TestURLBuilderWithPrefix(t *testing.T) {
147 roots := []string{
148 "http://example.com/prefix/",
149 "https://example.com/prefix/",
150 "http://localhost:5000/prefix/",
151 "https://localhost:5443/prefix/",
152 }
153
154 doTest := func(relative bool) {
155 for _, root := range roots {
156 urlBuilder, err := NewURLBuilderFromString(root, relative)
157 if err != nil {
158 t.Fatalf("unexpected error creating urlbuilder: %v", err)
159 }
160
161 for _, testCase := range makeURLBuilderTestCases(urlBuilder) {
162 url, err := testCase.build()
163 expectedErr := testCase.expectedErr
164 if !reflect.DeepEqual(expectedErr, err) {
165 t.Fatalf("%s: Expecting %v but got error %v", testCase.description, expectedErr, err)
166 }
167 if expectedErr != nil {
168 continue
169 }
170
171 expectedURL := testCase.expectedPath
172 if !relative {
173 expectedURL = root[0:len(root)-1] + expectedURL
174 }
175 if url != expectedURL {
176 t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
177 }
178 }
179 }
180 }
181 doTest(true)
182 doTest(false)
183 }
184
185 func TestBuilderFromRequest(t *testing.T) {
186 u, err := url.Parse("http://example.com")
187 if err != nil {
188 t.Fatal(err)
189 }
190
191 testRequests := []struct {
192 name string
193 request *http.Request
194 base string
195 configHost url.URL
196 }{
197 {
198 name: "no forwarded header",
199 request: &http.Request{URL: u, Host: u.Host},
200 base: "http://example.com",
201 },
202 {
203 name: "https protocol forwarded with a non-standard header",
204 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
205 "X-Custom-Forwarded-Proto": []string{"https"},
206 }},
207 base: "http://example.com",
208 },
209 {
210 name: "forwarded protocol is the same",
211 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
212 "X-Forwarded-Proto": []string{"https"},
213 }},
214 base: "https://example.com",
215 },
216 {
217 name: "forwarded host with a non-standard header",
218 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
219 "X-Forwarded-Host": []string{"first.example.com"},
220 }},
221 base: "http://first.example.com",
222 },
223 {
224 name: "forwarded multiple hosts a with non-standard header",
225 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
226 "X-Forwarded-Host": []string{"first.example.com, proxy1.example.com"},
227 }},
228 base: "http://first.example.com",
229 },
230 {
231 name: "host configured in config file takes priority",
232 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
233 "X-Forwarded-Host": []string{"first.example.com, proxy1.example.com"},
234 }},
235 base: "https://third.example.com:5000",
236 configHost: url.URL{
237 Scheme: "https",
238 Host: "third.example.com:5000",
239 },
240 },
241 {
242 name: "forwarded host and port with just one non-standard header",
243 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
244 "X-Forwarded-Host": []string{"first.example.com:443"},
245 }},
246 base: "http://first.example.com:443",
247 },
248 {
249 name: "forwarded port with a non-standard header",
250 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
251 "X-Forwarded-Host": []string{"example.com:5000"},
252 "X-Forwarded-Port": []string{"5000"},
253 }},
254 base: "http://example.com:5000",
255 },
256 {
257 name: "forwarded multiple ports with a non-standard header",
258 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
259 "X-Forwarded-Port": []string{"443 , 5001"},
260 }},
261 base: "http://example.com",
262 },
263 {
264 name: "forwarded standard port with non-standard headers",
265 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
266 "X-Forwarded-Proto": []string{"https"},
267 "X-Forwarded-Host": []string{"example.com"},
268 "X-Forwarded-Port": []string{"443"},
269 }},
270 base: "https://example.com",
271 },
272 {
273 name: "forwarded standard port with non-standard headers and explicit port",
274 request: &http.Request{URL: u, Host: u.Host + ":443", Header: http.Header{
275 "X-Forwarded-Proto": []string{"https"},
276 "X-Forwarded-Host": []string{u.Host + ":443"},
277 "X-Forwarded-Port": []string{"443"},
278 }},
279 base: "https://example.com:443",
280 },
281 {
282 name: "several non-standard headers",
283 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
284 "X-Forwarded-Proto": []string{"https"},
285 "X-Forwarded-Host": []string{" first.example.com:12345 "},
286 }},
287 base: "https://first.example.com:12345",
288 },
289 {
290 name: "forwarded host with port supplied takes priority",
291 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
292 "X-Forwarded-Host": []string{"first.example.com:5000"},
293 "X-Forwarded-Port": []string{"80"},
294 }},
295 base: "http://first.example.com:5000",
296 },
297 {
298 name: "malformed forwarded port",
299 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
300 "X-Forwarded-Host": []string{"first.example.com"},
301 "X-Forwarded-Port": []string{"abcd"},
302 }},
303 base: "http://first.example.com",
304 },
305 {
306 name: "forwarded protocol and addr using standard header",
307 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
308 "Forwarded": []string{`proto=https;host="192.168.22.30:80"`},
309 }},
310 base: "https://192.168.22.30:80",
311 },
312 {
313 name: "forwarded host takes priority over for",
314 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
315 "Forwarded": []string{`host="reg.example.com:5000";for="192.168.22.30"`},
316 }},
317 base: "http://reg.example.com:5000",
318 },
319 {
320 name: "forwarded host and protocol using standard header",
321 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
322 "Forwarded": []string{`host=reg.example.com;proto=https`},
323 }},
324 base: "https://reg.example.com",
325 },
326 {
327 name: "process just the first standard forwarded header",
328 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
329 "Forwarded": []string{`host="reg.example.com:88";proto=http`, `host=reg.example.com;proto=https`},
330 }},
331 base: "http://reg.example.com:88",
332 },
333 {
334 name: "process just the first list element of standard header",
335 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
336 "Forwarded": []string{`host="reg.example.com:443";proto=https, host="reg.example.com:80";proto=http`},
337 }},
338 base: "https://reg.example.com:443",
339 },
340 {
341 name: "IPv6 address use host",
342 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
343 "Forwarded": []string{`for="2607:f0d0:1002:51::4";host="[2607:f0d0:1002:51::4]:5001"`},
344 "X-Forwarded-Port": []string{"5002"},
345 }},
346 base: "http://[2607:f0d0:1002:51::4]:5001",
347 },
348 {
349 name: "IPv6 address with port",
350 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
351 "Forwarded": []string{`host="[2607:f0d0:1002:51::4]:4000"`},
352 "X-Forwarded-Port": []string{"5001"},
353 }},
354 base: "http://[2607:f0d0:1002:51::4]:4000",
355 },
356 {
357 name: "non-standard and standard forward headers",
358 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
359 "X-Forwarded-Proto": []string{`https`},
360 "X-Forwarded-Host": []string{`first.example.com`},
361 "X-Forwarded-Port": []string{``},
362 "Forwarded": []string{`host=first.example.com; proto=https`},
363 }},
364 base: "https://first.example.com",
365 },
366 {
367 name: "standard header takes precedence over non-standard headers",
368 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
369 "X-Forwarded-Proto": []string{`http`},
370 "Forwarded": []string{`host=second.example.com; proto=https`},
371 "X-Forwarded-Host": []string{`first.example.com`},
372 "X-Forwarded-Port": []string{`4000`},
373 }},
374 base: "https://second.example.com",
375 },
376 {
377 name: "incomplete standard header uses default",
378 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
379 "X-Forwarded-Proto": []string{`https`},
380 "Forwarded": []string{`for=127.0.0.1`},
381 "X-Forwarded-Host": []string{`first.example.com`},
382 "X-Forwarded-Port": []string{`4000`},
383 }},
384 base: "http://" + u.Host,
385 },
386 {
387 name: "standard with just proto",
388 request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
389 "X-Forwarded-Proto": []string{`https`},
390 "Forwarded": []string{`proto=https`},
391 "X-Forwarded-Host": []string{`first.example.com`},
392 "X-Forwarded-Port": []string{`4000`},
393 }},
394 base: "https://" + u.Host,
395 },
396 }
397
398 doTest := func(relative bool) {
399 for _, tr := range testRequests {
400 var builder *URLBuilder
401 if tr.configHost.Scheme != "" && tr.configHost.Host != "" {
402 builder = NewURLBuilder(&tr.configHost, relative)
403 } else {
404 builder = NewURLBuilderFromRequest(tr.request, relative)
405 }
406
407 for _, testCase := range makeURLBuilderTestCases(builder) {
408 buildURL, err := testCase.build()
409 expectedErr := testCase.expectedErr
410 if !reflect.DeepEqual(expectedErr, err) {
411 t.Fatalf("%s: Expecting %v but got error %v", testCase.description, expectedErr, err)
412 }
413 if expectedErr != nil {
414 continue
415 }
416
417 expectedURL := testCase.expectedPath
418 if !relative {
419 expectedURL = tr.base + expectedURL
420 }
421
422 if buildURL != expectedURL {
423 t.Errorf("[relative=%t, request=%q, case=%q]: %q != %q", relative, tr.name, testCase.description, buildURL, expectedURL)
424 }
425 }
426 }
427 }
428
429 doTest(true)
430 doTest(false)
431 }
432
433 func TestBuilderFromRequestWithPrefix(t *testing.T) {
434 u, err := url.Parse("http://example.com/prefix/v2/")
435 if err != nil {
436 t.Fatal(err)
437 }
438
439 forwardedProtoHeader := make(http.Header, 1)
440 forwardedProtoHeader.Set("X-Forwarded-Proto", "https")
441
442 testRequests := []struct {
443 request *http.Request
444 base string
445 configHost url.URL
446 }{
447 {
448 request: &http.Request{URL: u, Host: u.Host},
449 base: "http://example.com/prefix/",
450 },
451
452 {
453 request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
454 base: "http://example.com/prefix/",
455 },
456 {
457 request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
458 base: "https://example.com/prefix/",
459 },
460 {
461 request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
462 base: "https://subdomain.example.com/prefix/",
463 configHost: url.URL{
464 Scheme: "https",
465 Host: "subdomain.example.com",
466 Path: "/prefix/",
467 },
468 },
469 }
470
471 var relative bool
472 for _, tr := range testRequests {
473 var builder *URLBuilder
474 if tr.configHost.Scheme != "" && tr.configHost.Host != "" {
475 builder = NewURLBuilder(&tr.configHost, false)
476 } else {
477 builder = NewURLBuilderFromRequest(tr.request, false)
478 }
479
480 for _, testCase := range makeURLBuilderTestCases(builder) {
481 buildURL, err := testCase.build()
482 expectedErr := testCase.expectedErr
483 if !reflect.DeepEqual(expectedErr, err) {
484 t.Fatalf("%s: Expecting %v but got error %v", testCase.description, expectedErr, err)
485 }
486 if expectedErr != nil {
487 continue
488 }
489
490 var expectedURL string
491 proto, ok := tr.request.Header["X-Forwarded-Proto"]
492 if !ok {
493 expectedURL = testCase.expectedPath
494 if !relative {
495 expectedURL = tr.base[0:len(tr.base)-1] + expectedURL
496 }
497 } else {
498 urlBase, err := url.Parse(tr.base)
499 if err != nil {
500 t.Fatal(err)
501 }
502 urlBase.Scheme = proto[0]
503 expectedURL = testCase.expectedPath
504 if !relative {
505 expectedURL = urlBase.String()[0:len(urlBase.String())-1] + expectedURL
506 }
507
508 }
509
510 if buildURL != expectedURL {
511 t.Fatalf("%s: %q != %q", testCase.description, buildURL, expectedURL)
512 }
513 }
514 }
515 }
516
View as plain text