distribution/registry/api/v2/urls_test.go
Sebastiaan van Stijn 152af63ec5
deprecate reference package, migrate to github.com/distribution/reference
This integrates the new module, which was extracted from this repository
at commit b9b19409cf458dcb9e1253ff44ba75bd0620faa6;

    # install filter-repo (https://github.com/newren/git-filter-repo/blob/main/INSTALL.md)
    brew install git-filter-repo

    # create a temporary clone of docker
    cd ~/Projects
    git clone https://github.com/distribution/distribution.git reference
    cd reference

    # commit taken from
    git rev-parse --verify HEAD
    b9b19409cf

    # remove all code, except for general files, 'reference/', and rename to /
    git filter-repo \
      --path .github/workflows/codeql-analysis.yml \
      --path .github/workflows/fossa.yml \
      --path .golangci.yml \
      --path distribution-logo.svg \
      --path CODE-OF-CONDUCT.md \
      --path CONTRIBUTING.md \
      --path GOVERNANCE.md \
      --path README.md \
      --path LICENSE \
      --path MAINTAINERS \
      --path-glob 'reference/*.*' \
      --path-rename reference/:

    # initialize go.mod
    go mod init github.com/distribution/reference
    go mod tidy -go=1.20

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-08-31 15:47:06 +02:00

535 lines
16 KiB
Go

package v2
import (
"fmt"
"net/http"
"net/url"
"reflect"
"testing"
"github.com/distribution/reference"
)
type urlBuilderTestCase struct {
description string
expectedPath string
expectedErr error
build func() (string, error)
}
func makeURLBuilderTestCases(urlBuilder *URLBuilder) []urlBuilderTestCase {
fooBarRef, _ := reference.WithName("foo/bar")
return []urlBuilderTestCase{
{
description: "test base url",
expectedPath: "/v2/",
expectedErr: nil,
build: urlBuilder.BuildBaseURL,
},
{
description: "test tags url",
expectedPath: "/v2/foo/bar/tags/list",
expectedErr: nil,
build: func() (string, error) {
return urlBuilder.BuildTagsURL(fooBarRef)
},
},
{
description: "test tags url with n query parameter",
expectedPath: "/v2/foo/bar/tags/list?n=10",
expectedErr: nil,
build: func() (string, error) {
return urlBuilder.BuildTagsURL(fooBarRef, url.Values{
"n": []string{"10"},
})
},
},
{
description: "test tags url with last query parameter",
expectedPath: "/v2/foo/bar/tags/list?last=abc-def",
expectedErr: nil,
build: func() (string, error) {
return urlBuilder.BuildTagsURL(fooBarRef, url.Values{
"last": []string{"abc-def"},
})
},
},
{
description: "test manifest url tagged ref",
expectedPath: "/v2/foo/bar/manifests/tag",
expectedErr: nil,
build: func() (string, error) {
ref, _ := reference.WithTag(fooBarRef, "tag")
return urlBuilder.BuildManifestURL(ref)
},
},
{
description: "test manifest url bare ref",
expectedPath: "",
expectedErr: fmt.Errorf("reference must have a tag or digest"),
build: func() (string, error) {
return urlBuilder.BuildManifestURL(fooBarRef)
},
},
{
description: "build blob url",
expectedPath: "/v2/foo/bar/blobs/sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5",
expectedErr: nil,
build: func() (string, error) {
ref, _ := reference.WithDigest(fooBarRef, "sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5")
return urlBuilder.BuildBlobURL(ref)
},
},
{
description: "build blob upload url",
expectedPath: "/v2/foo/bar/blobs/uploads/",
expectedErr: nil,
build: func() (string, error) {
return urlBuilder.BuildBlobUploadURL(fooBarRef)
},
},
{
description: "build blob upload url with digest and size",
expectedPath: "/v2/foo/bar/blobs/uploads/?digest=sha256%3A3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5&size=10000",
expectedErr: nil,
build: func() (string, error) {
return urlBuilder.BuildBlobUploadURL(fooBarRef, url.Values{
"size": []string{"10000"},
"digest": []string{"sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5"},
})
},
},
{
description: "build blob upload chunk url",
expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part",
expectedErr: nil,
build: func() (string, error) {
return urlBuilder.BuildBlobUploadChunkURL(fooBarRef, "uuid-part")
},
},
{
description: "build blob upload chunk url with digest and size",
expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part?digest=sha256%3A3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5&size=10000",
expectedErr: nil,
build: func() (string, error) {
return urlBuilder.BuildBlobUploadChunkURL(fooBarRef, "uuid-part", url.Values{
"size": []string{"10000"},
"digest": []string{"sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5"},
})
},
},
}
}
// TestURLBuilder tests the various url building functions, ensuring they are
// returning the expected values.
func TestURLBuilder(t *testing.T) {
roots := []string{
"http://example.com",
"https://example.com",
"http://localhost:5000",
"https://localhost:5443",
}
doTest := func(relative bool) {
for _, root := range roots {
urlBuilder, err := NewURLBuilderFromString(root, relative)
if err != nil {
t.Fatalf("unexpected error creating urlbuilder: %v", err)
}
for _, tc := range makeURLBuilderTestCases(urlBuilder) {
buildURL, err := tc.build()
expectedErr := tc.expectedErr
if !reflect.DeepEqual(expectedErr, err) {
t.Fatalf("%s: Expecting %v but got error %v", tc.description, expectedErr, err)
}
if expectedErr != nil {
continue
}
expectedURL := tc.expectedPath
if !relative {
expectedURL = root + expectedURL
}
if buildURL != expectedURL {
t.Fatalf("%s: %q != %q", tc.description, buildURL, expectedURL)
}
}
}
}
doTest(true)
doTest(false)
}
func TestURLBuilderWithPrefix(t *testing.T) {
roots := []string{
"http://example.com/prefix/",
"https://example.com/prefix/",
"http://localhost:5000/prefix/",
"https://localhost:5443/prefix/",
}
doTest := func(relative bool) {
for _, root := range roots {
urlBuilder, err := NewURLBuilderFromString(root, relative)
if err != nil {
t.Fatalf("unexpected error creating urlbuilder: %v", err)
}
for _, tc := range makeURLBuilderTestCases(urlBuilder) {
buildURL, err := tc.build()
expectedErr := tc.expectedErr
if !reflect.DeepEqual(expectedErr, err) {
t.Fatalf("%s: Expecting %v but got error %v", tc.description, expectedErr, err)
}
if expectedErr != nil {
continue
}
expectedURL := tc.expectedPath
if !relative {
expectedURL = root[0:len(root)-1] + expectedURL
}
if buildURL != expectedURL {
t.Fatalf("%s: %q != %q", tc.description, buildURL, expectedURL)
}
}
}
}
doTest(true)
doTest(false)
}
func TestBuilderFromRequest(t *testing.T) {
u, err := url.Parse("http://example.com")
if err != nil {
t.Fatal(err)
}
testRequests := []struct {
name string
request *http.Request
base string
configHost url.URL
}{
{
name: "no forwarded header",
request: &http.Request{URL: u, Host: u.Host},
base: "http://example.com",
},
{
name: "https protocol forwarded with a non-standard header",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Custom-Forwarded-Proto": []string{"https"},
}},
base: "http://example.com",
},
{
name: "forwarded protocol is the same",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Proto": []string{"https"},
}},
base: "https://example.com",
},
{
name: "forwarded host with a non-standard header",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Host": []string{"first.example.com"},
}},
base: "http://first.example.com",
},
{
name: "forwarded multiple hosts a with non-standard header",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Host": []string{"first.example.com, proxy1.example.com"},
}},
base: "http://first.example.com",
},
{
name: "host configured in config file takes priority",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Host": []string{"first.example.com, proxy1.example.com"},
}},
base: "https://third.example.com:5000",
configHost: url.URL{
Scheme: "https",
Host: "third.example.com:5000",
},
},
{
name: "forwarded host and port with just one non-standard header",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Host": []string{"first.example.com:443"},
}},
base: "http://first.example.com:443",
},
{
name: "forwarded port with a non-standard header",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Host": []string{"example.com:5000"},
"X-Forwarded-Port": []string{"5000"},
}},
base: "http://example.com:5000",
},
{
name: "forwarded multiple ports with a non-standard header",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Port": []string{"443 , 5001"},
}},
base: "http://example.com",
},
{
name: "forwarded standard port with non-standard headers",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Proto": []string{"https"},
"X-Forwarded-Host": []string{"example.com"},
"X-Forwarded-Port": []string{"443"},
}},
base: "https://example.com",
},
{
name: "forwarded standard port with non-standard headers and explicit port",
request: &http.Request{URL: u, Host: u.Host + ":443", Header: http.Header{
"X-Forwarded-Proto": []string{"https"},
"X-Forwarded-Host": []string{u.Host + ":443"},
"X-Forwarded-Port": []string{"443"},
}},
base: "https://example.com:443",
},
{
name: "several non-standard headers",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Proto": []string{"https"},
"X-Forwarded-Host": []string{" first.example.com:12345 "},
}},
base: "https://first.example.com:12345",
},
{
name: "forwarded host with port supplied takes priority",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Host": []string{"first.example.com:5000"},
"X-Forwarded-Port": []string{"80"},
}},
base: "http://first.example.com:5000",
},
{
name: "malformed forwarded port",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Host": []string{"first.example.com"},
"X-Forwarded-Port": []string{"abcd"},
}},
base: "http://first.example.com",
},
{
name: "forwarded protocol and addr using standard header",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"Forwarded": []string{`proto=https;host="192.168.22.30:80"`},
}},
base: "https://192.168.22.30:80",
},
{
name: "forwarded host takes priority over for",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"Forwarded": []string{`host="reg.example.com:5000";for="192.168.22.30"`},
}},
base: "http://reg.example.com:5000",
},
{
name: "forwarded host and protocol using standard header",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"Forwarded": []string{`host=reg.example.com;proto=https`},
}},
base: "https://reg.example.com",
},
{
name: "process just the first standard forwarded header",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"Forwarded": []string{`host="reg.example.com:88";proto=http`, `host=reg.example.com;proto=https`},
}},
base: "http://reg.example.com:88",
},
{
name: "process just the first list element of standard header",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"Forwarded": []string{`host="reg.example.com:443";proto=https, host="reg.example.com:80";proto=http`},
}},
base: "https://reg.example.com:443",
},
{
name: "IPv6 address use host",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"Forwarded": []string{`for="2607:f0d0:1002:51::4";host="[2607:f0d0:1002:51::4]:5001"`},
"X-Forwarded-Port": []string{"5002"},
}},
base: "http://[2607:f0d0:1002:51::4]:5001",
},
{
name: "IPv6 address with port",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"Forwarded": []string{`host="[2607:f0d0:1002:51::4]:4000"`},
"X-Forwarded-Port": []string{"5001"},
}},
base: "http://[2607:f0d0:1002:51::4]:4000",
},
{
name: "non-standard and standard forward headers",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Proto": []string{`https`},
"X-Forwarded-Host": []string{`first.example.com`},
"X-Forwarded-Port": []string{``},
"Forwarded": []string{`host=first.example.com; proto=https`},
}},
base: "https://first.example.com",
},
{
name: "standard header takes precedence over non-standard headers",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Proto": []string{`http`},
"Forwarded": []string{`host=second.example.com; proto=https`},
"X-Forwarded-Host": []string{`first.example.com`},
"X-Forwarded-Port": []string{`4000`},
}},
base: "https://second.example.com",
},
{
name: "incomplete standard header uses default",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Proto": []string{`https`},
"Forwarded": []string{`for=127.0.0.1`},
"X-Forwarded-Host": []string{`first.example.com`},
"X-Forwarded-Port": []string{`4000`},
}},
base: "http://" + u.Host,
},
{
name: "standard with just proto",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Proto": []string{`https`},
"Forwarded": []string{`proto=https`},
"X-Forwarded-Host": []string{`first.example.com`},
"X-Forwarded-Port": []string{`4000`},
}},
base: "https://" + u.Host,
},
}
doTest := func(relative bool) {
for _, tr := range testRequests {
var builder *URLBuilder
if tr.configHost.Scheme != "" && tr.configHost.Host != "" {
builder = NewURLBuilder(&tr.configHost, relative)
} else {
builder = NewURLBuilderFromRequest(tr.request, relative)
}
for _, tc := range makeURLBuilderTestCases(builder) {
buildURL, err := tc.build()
expectedErr := tc.expectedErr
if !reflect.DeepEqual(expectedErr, err) {
t.Fatalf("%s: Expecting %v but got error %v", tc.description, expectedErr, err)
}
if expectedErr != nil {
continue
}
expectedURL := tc.expectedPath
if !relative {
expectedURL = tr.base + expectedURL
}
if buildURL != expectedURL {
t.Errorf("[relative=%t, request=%q, case=%q]: %q != %q", relative, tr.name, tc.description, buildURL, expectedURL)
}
}
}
}
doTest(true)
doTest(false)
}
func TestBuilderFromRequestWithPrefix(t *testing.T) {
u, err := url.Parse("http://example.com/prefix/v2/")
if err != nil {
t.Fatal(err)
}
forwardedProtoHeader := make(http.Header, 1)
forwardedProtoHeader.Set("X-Forwarded-Proto", "https")
testRequests := []struct {
request *http.Request
base string
configHost url.URL
}{
{
request: &http.Request{URL: u, Host: u.Host},
base: "http://example.com/prefix/",
},
{
request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
base: "http://example.com/prefix/",
},
{
request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
base: "https://example.com/prefix/",
},
{
request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
base: "https://subdomain.example.com/prefix/",
configHost: url.URL{
Scheme: "https",
Host: "subdomain.example.com",
Path: "/prefix/",
},
},
}
var relative bool
for _, tr := range testRequests {
var builder *URLBuilder
if tr.configHost.Scheme != "" && tr.configHost.Host != "" {
builder = NewURLBuilder(&tr.configHost, false)
} else {
builder = NewURLBuilderFromRequest(tr.request, false)
}
for _, tc := range makeURLBuilderTestCases(builder) {
buildURL, err := tc.build()
expectedErr := tc.expectedErr
if !reflect.DeepEqual(expectedErr, err) {
t.Fatalf("%s: Expecting %v but got error %v", tc.description, expectedErr, err)
}
if expectedErr != nil {
continue
}
var expectedURL string
proto, ok := tr.request.Header["X-Forwarded-Proto"]
if !ok {
expectedURL = tc.expectedPath
if !relative {
expectedURL = tr.base[0:len(tr.base)-1] + expectedURL
}
} else {
urlBase, err := url.Parse(tr.base)
if err != nil {
t.Fatal(err)
}
urlBase.Scheme = proto[0]
expectedURL = tc.expectedPath
if !relative {
expectedURL = urlBase.String()[0:len(urlBase.String())-1] + expectedURL
}
}
if buildURL != expectedURL {
t.Fatalf("%s: %q != %q", tc.description, buildURL, expectedURL)
}
}
}
}