Merge master into breakout_swarm

Signed-off-by: Alex Ellis <alexellis2@gmail.com>
This commit is contained in:
Alex Ellis
2018-02-01 09:25:39 +00:00
parent afeb7bbce4
commit f954bf0733
1953 changed files with 614131 additions and 175582 deletions

View File

@ -4,9 +4,9 @@ import (
"net/http"
"regexp"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/errcode"
"github.com/opencontainers/go-digest"
)
var (

View File

@ -0,0 +1,161 @@
package v2
import (
"testing"
)
func TestParseForwardedHeader(t *testing.T) {
for _, tc := range []struct {
name string
raw string
expected map[string]string
expectedRest string
expectedError bool
}{
{
name: "empty",
raw: "",
},
{
name: "one pair",
raw: " key = value ",
expected: map[string]string{"key": "value"},
},
{
name: "two pairs",
raw: " key1 = value1; key2=value2",
expected: map[string]string{"key1": "value1", "key2": "value2"},
},
{
name: "uppercase parameter",
raw: "KeY=VaL",
expected: map[string]string{"key": "VaL"},
},
{
name: "missing key=value pair - be tolerant",
raw: "key=val;",
expected: map[string]string{"key": "val"},
},
{
name: "quoted values",
raw: `key="val";param = "[[ $((1 + 1)) == 3 ]] && echo panic!;" ; p=" abcd "`,
expected: map[string]string{"key": "val", "param": "[[ $((1 + 1)) == 3 ]] && echo panic!;", "p": " abcd "},
},
{
name: "empty quoted value",
raw: `key=""`,
expected: map[string]string{"key": ""},
},
{
name: "quoted double quotes",
raw: `key="\"value\""`,
expected: map[string]string{"key": `"value"`},
},
{
name: "quoted backslash",
raw: `key="\"\\\""`,
expected: map[string]string{"key": `"\"`},
},
{
name: "ignore subsequent elements",
raw: "key=a, param= b",
expected: map[string]string{"key": "a"},
expectedRest: " param= b",
},
{
name: "empty element - be tolerant",
raw: " , key=val",
expectedRest: " key=val",
},
{
name: "obscure key",
raw: `ob₷C&r€ = value`,
expected: map[string]string{`ob₷c&r€`: "value"},
},
{
name: "duplicate parameter",
raw: "key=a; p=b; key=c",
expectedError: true,
},
{
name: "empty parameter",
raw: "=value",
expectedError: true,
},
{
name: "empty value",
raw: "key= ",
expectedError: true,
},
{
name: "empty value before a new element ",
raw: "key=,",
expectedError: true,
},
{
name: "empty value before a new pair",
raw: "key=;",
expectedError: true,
},
{
name: "just parameter",
raw: "key",
expectedError: true,
},
{
name: "missing key-value",
raw: "a=b;;",
expectedError: true,
},
{
name: "unclosed quoted value",
raw: `key="value`,
expectedError: true,
},
{
name: "escaped terminating dquote",
raw: `key="value\"`,
expectedError: true,
},
{
name: "just a quoted value",
raw: `"key=val"`,
expectedError: true,
},
{
name: "quoted key",
raw: `"key"=val`,
expectedError: true,
},
} {
parsed, rest, err := parseForwardedHeader(tc.raw)
if err != nil && !tc.expectedError {
t.Errorf("[%s] got unexpected error: %v", tc.name, err)
}
if err == nil && tc.expectedError {
t.Errorf("[%s] got unexpected non-error", tc.name)
}
if err != nil || tc.expectedError {
continue
}
for key, value := range tc.expected {
v, exists := parsed[key]
if !exists {
t.Errorf("[%s] missing expected parameter %q", tc.name, key)
continue
}
if v != value {
t.Errorf("[%s] got unexpected value for parameter %q: %q != %q", tc.name, key, v, value)
}
}
for key, value := range parsed {
if _, exists := tc.expected[key]; !exists {
t.Errorf("[%s] got unexpected key/value pair: %q=%q", tc.name, key, value)
}
}
if rest != tc.expectedRest {
t.Errorf("[%s] got unexpected unparsed string: %q != %q", tc.name, rest, tc.expectedRest)
}
}
}

View File

@ -0,0 +1,355 @@
package v2
import (
"encoding/json"
"fmt"
"math/rand"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"time"
"github.com/gorilla/mux"
)
type routeTestCase struct {
RequestURI string
ExpectedURI string
Vars map[string]string
RouteName string
StatusCode int
}
// TestRouter registers a test handler with all the routes and ensures that
// each route returns the expected path variables. Not method verification is
// present. This not meant to be exhaustive but as check to ensure that the
// expected variables are extracted.
//
// This may go away as the application structure comes together.
func TestRouter(t *testing.T) {
testCases := []routeTestCase{
{
RouteName: RouteNameBase,
RequestURI: "/v2/",
Vars: map[string]string{},
},
{
RouteName: RouteNameManifest,
RequestURI: "/v2/foo/manifests/bar",
Vars: map[string]string{
"name": "foo",
"reference": "bar",
},
},
{
RouteName: RouteNameManifest,
RequestURI: "/v2/foo/bar/manifests/tag",
Vars: map[string]string{
"name": "foo/bar",
"reference": "tag",
},
},
{
RouteName: RouteNameManifest,
RequestURI: "/v2/foo/bar/manifests/sha256:abcdef01234567890",
Vars: map[string]string{
"name": "foo/bar",
"reference": "sha256:abcdef01234567890",
},
},
{
RouteName: RouteNameTags,
RequestURI: "/v2/foo/bar/tags/list",
Vars: map[string]string{
"name": "foo/bar",
},
},
{
RouteName: RouteNameTags,
RequestURI: "/v2/docker.com/foo/tags/list",
Vars: map[string]string{
"name": "docker.com/foo",
},
},
{
RouteName: RouteNameTags,
RequestURI: "/v2/docker.com/foo/bar/tags/list",
Vars: map[string]string{
"name": "docker.com/foo/bar",
},
},
{
RouteName: RouteNameTags,
RequestURI: "/v2/docker.com/foo/bar/baz/tags/list",
Vars: map[string]string{
"name": "docker.com/foo/bar/baz",
},
},
{
RouteName: RouteNameBlob,
RequestURI: "/v2/foo/bar/blobs/sha256:abcdef0919234",
Vars: map[string]string{
"name": "foo/bar",
"digest": "sha256:abcdef0919234",
},
},
{
RouteName: RouteNameBlobUpload,
RequestURI: "/v2/foo/bar/blobs/uploads/",
Vars: map[string]string{
"name": "foo/bar",
},
},
{
RouteName: RouteNameBlobUploadChunk,
RequestURI: "/v2/foo/bar/blobs/uploads/uuid",
Vars: map[string]string{
"name": "foo/bar",
"uuid": "uuid",
},
},
{
// support uuid proper
RouteName: RouteNameBlobUploadChunk,
RequestURI: "/v2/foo/bar/blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
Vars: map[string]string{
"name": "foo/bar",
"uuid": "D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
},
},
{
RouteName: RouteNameBlobUploadChunk,
RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==",
Vars: map[string]string{
"name": "foo/bar",
"uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==",
},
},
{
// supports urlsafe base64
RouteName: RouteNameBlobUploadChunk,
RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA_-==",
Vars: map[string]string{
"name": "foo/bar",
"uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA_-==",
},
},
{
// does not match
RouteName: RouteNameBlobUploadChunk,
RequestURI: "/v2/foo/bar/blobs/uploads/totalandcompletejunk++$$-==",
StatusCode: http.StatusNotFound,
},
{
// Check ambiguity: ensure we can distinguish between tags for
// "foo/bar/image/image" and image for "foo/bar/image" with tag
// "tags"
RouteName: RouteNameManifest,
RequestURI: "/v2/foo/bar/manifests/manifests/tags",
Vars: map[string]string{
"name": "foo/bar/manifests",
"reference": "tags",
},
},
{
// This case presents an ambiguity between foo/bar with tag="tags"
// and list tags for "foo/bar/manifest"
RouteName: RouteNameTags,
RequestURI: "/v2/foo/bar/manifests/tags/list",
Vars: map[string]string{
"name": "foo/bar/manifests",
},
},
{
RouteName: RouteNameManifest,
RequestURI: "/v2/locahost:8080/foo/bar/baz/manifests/tag",
Vars: map[string]string{
"name": "locahost:8080/foo/bar/baz",
"reference": "tag",
},
},
}
checkTestRouter(t, testCases, "", true)
checkTestRouter(t, testCases, "/prefix/", true)
}
func TestRouterWithPathTraversals(t *testing.T) {
testCases := []routeTestCase{
{
RouteName: RouteNameBlobUploadChunk,
RequestURI: "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
ExpectedURI: "/blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
StatusCode: http.StatusNotFound,
},
{
// Testing for path traversal attack handling
RouteName: RouteNameTags,
RequestURI: "/v2/foo/../bar/baz/tags/list",
ExpectedURI: "/v2/bar/baz/tags/list",
Vars: map[string]string{
"name": "bar/baz",
},
},
}
checkTestRouter(t, testCases, "", false)
}
func TestRouterWithBadCharacters(t *testing.T) {
if testing.Short() {
testCases := []routeTestCase{
{
RouteName: RouteNameBlobUploadChunk,
RequestURI: "/v2/foo/blob/uploads/不95306FA-FAD3-4E36-8D41-CF1C93EF8286",
StatusCode: http.StatusNotFound,
},
{
// Testing for path traversal attack handling
RouteName: RouteNameTags,
RequestURI: "/v2/foo/不bar/tags/list",
StatusCode: http.StatusNotFound,
},
}
checkTestRouter(t, testCases, "", true)
} else {
// in the long version we're going to fuzz the router
// with random UTF8 characters not in the 128 bit ASCII range.
// These are not valid characters for the router and we expect
// 404s on every test.
rand.Seed(time.Now().UTC().UnixNano())
testCases := make([]routeTestCase, 1000)
for idx := range testCases {
testCases[idx] = routeTestCase{
RouteName: RouteNameTags,
RequestURI: fmt.Sprintf("/v2/%v/%v/tags/list", randomString(10), randomString(10)),
StatusCode: http.StatusNotFound,
}
}
checkTestRouter(t, testCases, "", true)
}
}
func checkTestRouter(t *testing.T, testCases []routeTestCase, prefix string, deeplyEqual bool) {
router := RouterWithPrefix(prefix)
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
testCase := routeTestCase{
RequestURI: r.RequestURI,
Vars: mux.Vars(r),
RouteName: mux.CurrentRoute(r).GetName(),
}
enc := json.NewEncoder(w)
if err := enc.Encode(testCase); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
// Startup test server
server := httptest.NewServer(router)
for _, testcase := range testCases {
testcase.RequestURI = strings.TrimSuffix(prefix, "/") + testcase.RequestURI
// Register the endpoint
route := router.GetRoute(testcase.RouteName)
if route == nil {
t.Fatalf("route for name %q not found", testcase.RouteName)
}
route.Handler(testHandler)
u := server.URL + testcase.RequestURI
resp, err := http.Get(u)
if err != nil {
t.Fatalf("error issuing get request: %v", err)
}
if testcase.StatusCode == 0 {
// Override default, zero-value
testcase.StatusCode = http.StatusOK
}
if testcase.ExpectedURI == "" {
// Override default, zero-value
testcase.ExpectedURI = testcase.RequestURI
}
if resp.StatusCode != testcase.StatusCode {
t.Fatalf("unexpected status for %s: %v %v", u, resp.Status, resp.StatusCode)
}
if testcase.StatusCode != http.StatusOK {
resp.Body.Close()
// We don't care about json response.
continue
}
dec := json.NewDecoder(resp.Body)
var actualRouteInfo routeTestCase
if err := dec.Decode(&actualRouteInfo); err != nil {
t.Fatalf("error reading json response: %v", err)
}
// Needs to be set out of band
actualRouteInfo.StatusCode = resp.StatusCode
if actualRouteInfo.RequestURI != testcase.ExpectedURI {
t.Fatalf("URI %v incorrectly parsed, expected %v", actualRouteInfo.RequestURI, testcase.ExpectedURI)
}
if actualRouteInfo.RouteName != testcase.RouteName {
t.Fatalf("incorrect route %q matched, expected %q", actualRouteInfo.RouteName, testcase.RouteName)
}
// when testing deep equality, the actualRouteInfo has an empty ExpectedURI, we don't want
// that to make the comparison fail. We're otherwise done with the testcase so empty the
// testcase.ExpectedURI
testcase.ExpectedURI = ""
if deeplyEqual && !reflect.DeepEqual(actualRouteInfo, testcase) {
t.Fatalf("actual does not equal expected: %#v != %#v", actualRouteInfo, testcase)
}
resp.Body.Close()
}
}
// -------------- START LICENSED CODE --------------
// The following code is derivative of https://github.com/google/gofuzz
// gofuzz is licensed under the Apache License, Version 2.0, January 2004,
// a copy of which can be found in the LICENSE file at the root of this
// repository.
// These functions allow us to generate strings containing only multibyte
// characters that are invalid in our URLs. They are used above for fuzzing
// to ensure we always get 404s on these invalid strings
type charRange struct {
first, last rune
}
// choose returns a random unicode character from the given range, using the
// given randomness source.
func (r *charRange) choose() rune {
count := int64(r.last - r.first)
return r.first + rune(rand.Int63n(count))
}
var unicodeRanges = []charRange{
{'\u00a0', '\u02af'}, // Multi-byte encoded characters
{'\u4e00', '\u9fff'}, // Common CJK (even longer encodings)
}
func randomString(length int) string {
runes := make([]rune, length)
for i := range runes {
runes[i] = unicodeRanges[rand.Intn(len(unicodeRanges))].choose()
}
return string(runes)
}
// -------------- END LICENSED CODE --------------

View File

@ -1,7 +1,6 @@
package v2
import (
"fmt"
"net/http"
"net/url"
"strings"
@ -150,8 +149,6 @@ func (ub *URLBuilder) BuildManifestURL(ref reference.Named) (string, error) {
tagOrDigest = v.Tag()
case reference.Digested:
tagOrDigest = v.Digest().String()
default:
return "", fmt.Errorf("reference must have a tag or digest")
}
manifestURL, err := route.URL("name", ref.Name(), "reference", tagOrDigest)

View File

@ -0,0 +1,484 @@
package v2
import (
"net/http"
"net/url"
"testing"
"github.com/docker/distribution/reference"
)
type urlBuilderTestCase struct {
description string
expectedPath string
build func() (string, error)
}
func makeURLBuilderTestCases(urlBuilder *URLBuilder) []urlBuilderTestCase {
fooBarRef, _ := reference.ParseNamed("foo/bar")
return []urlBuilderTestCase{
{
description: "test base url",
expectedPath: "/v2/",
build: urlBuilder.BuildBaseURL,
},
{
description: "test tags url",
expectedPath: "/v2/foo/bar/tags/list",
build: func() (string, error) {
return urlBuilder.BuildTagsURL(fooBarRef)
},
},
{
description: "test manifest url",
expectedPath: "/v2/foo/bar/manifests/tag",
build: func() (string, error) {
ref, _ := reference.WithTag(fooBarRef, "tag")
return urlBuilder.BuildManifestURL(ref)
},
},
{
description: "build blob url",
expectedPath: "/v2/foo/bar/blobs/sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5",
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/",
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",
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",
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",
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 _, testCase := range makeURLBuilderTestCases(urlBuilder) {
url, err := testCase.build()
if err != nil {
t.Fatalf("%s: error building url: %v", testCase.description, err)
}
expectedURL := testCase.expectedPath
if !relative {
expectedURL = root + expectedURL
}
if url != expectedURL {
t.Fatalf("%s: %q != %q", testCase.description, url, 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 _, testCase := range makeURLBuilderTestCases(urlBuilder) {
url, err := testCase.build()
if err != nil {
t.Fatalf("%s: error building url: %v", testCase.description, err)
}
expectedURL := testCase.expectedPath
if !relative {
expectedURL = root[0:len(root)-1] + expectedURL
}
if url != expectedURL {
t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
}
}
}
}
doTest(true)
doTest(false)
}
type builderFromRequestTestCase struct {
request *http.Request
base string
}
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 _, testCase := range makeURLBuilderTestCases(builder) {
buildURL, err := testCase.build()
if err != nil {
t.Fatalf("[relative=%t, request=%q, case=%q]: error building url: %v", relative, tr.name, testCase.description, err)
}
expectedURL := testCase.expectedPath
if !relative {
expectedURL = tr.base + expectedURL
}
if buildURL != expectedURL {
t.Errorf("[relative=%t, request=%q, case=%q]: %q != %q", relative, tr.name, testCase.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 _, testCase := range makeURLBuilderTestCases(builder) {
buildURL, err := testCase.build()
if err != nil {
t.Fatalf("%s: error building url: %v", testCase.description, err)
}
var expectedURL string
proto, ok := tr.request.Header["X-Forwarded-Proto"]
if !ok {
expectedURL = testCase.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 = testCase.expectedPath
if !relative {
expectedURL = urlBase.String()[0:len(urlBase.String())-1] + expectedURL
}
}
if buildURL != expectedURL {
t.Fatalf("%s: %q != %q", testCase.description, buildURL, expectedURL)
}
}
}
}