...

Source file src/github.com/docker/distribution/registry/api/v2/urls_test.go

Documentation: github.com/docker/distribution/registry/api/v2

     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  // TestURLBuilder tests the various url building functions, ensuring they are
   105  // returning the expected values.
   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