Merge pull request #2219 from dmcgowan/fix-forwarded-logic
Fix forwarded logic
This commit is contained in:
commit
0700fa570d
2 changed files with 88 additions and 278 deletions
|
@ -2,10 +2,8 @@ package v2
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
|
@ -49,66 +47,42 @@ func NewURLBuilderFromString(root string, relative bool) (*URLBuilder, error) {
|
|||
// NewURLBuilderFromRequest uses information from an *http.Request to
|
||||
// construct the root url.
|
||||
func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder {
|
||||
var scheme string
|
||||
|
||||
forwardedProto := r.Header.Get("X-Forwarded-Proto")
|
||||
// TODO: log the error
|
||||
forwardedHeader, _, _ := parseForwardedHeader(r.Header.Get("Forwarded"))
|
||||
|
||||
switch {
|
||||
case len(forwardedProto) > 0:
|
||||
scheme = forwardedProto
|
||||
case len(forwardedHeader["proto"]) > 0:
|
||||
scheme = forwardedHeader["proto"]
|
||||
case r.TLS != nil:
|
||||
scheme = "https"
|
||||
case len(r.URL.Scheme) > 0:
|
||||
scheme = r.URL.Scheme
|
||||
default:
|
||||
var (
|
||||
scheme = "http"
|
||||
host = r.Host
|
||||
)
|
||||
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
} else if len(r.URL.Scheme) > 0 {
|
||||
scheme = r.URL.Scheme
|
||||
}
|
||||
|
||||
host := r.Host
|
||||
|
||||
if forwardedHost := r.Header.Get("X-Forwarded-Host"); len(forwardedHost) > 0 {
|
||||
// According to the Apache mod_proxy docs, X-Forwarded-Host can be a
|
||||
// comma-separated list of hosts, to which each proxy appends the
|
||||
// requested host. We want to grab the first from this comma-separated
|
||||
// list.
|
||||
hosts := strings.SplitN(forwardedHost, ",", 2)
|
||||
host = strings.TrimSpace(hosts[0])
|
||||
} else if addr, exists := forwardedHeader["for"]; exists {
|
||||
host = addr
|
||||
} else if h, exists := forwardedHeader["host"]; exists {
|
||||
host = h
|
||||
}
|
||||
|
||||
portLessHost, port := host, ""
|
||||
if !isIPv6Address(portLessHost) {
|
||||
// with go 1.6, this would treat the last part of IPv6 address as a port
|
||||
portLessHost, port, _ = net.SplitHostPort(host)
|
||||
}
|
||||
if forwardedPort := r.Header.Get("X-Forwarded-Port"); len(port) == 0 && len(forwardedPort) > 0 {
|
||||
ports := strings.SplitN(forwardedPort, ",", 2)
|
||||
forwardedPort = strings.TrimSpace(ports[0])
|
||||
if _, err := strconv.ParseInt(forwardedPort, 10, 32); err == nil {
|
||||
port = forwardedPort
|
||||
// Handle fowarded headers
|
||||
// Prefer "Forwarded" header as defined by rfc7239 if given
|
||||
// see https://tools.ietf.org/html/rfc7239
|
||||
if forwarded := r.Header.Get("Forwarded"); len(forwarded) > 0 {
|
||||
forwardedHeader, _, err := parseForwardedHeader(forwarded)
|
||||
if err == nil {
|
||||
if fproto := forwardedHeader["proto"]; len(fproto) > 0 {
|
||||
scheme = fproto
|
||||
}
|
||||
if fhost := forwardedHeader["host"]; len(fhost) > 0 {
|
||||
host = fhost
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(portLessHost) > 0 {
|
||||
host = portLessHost
|
||||
}
|
||||
if len(port) > 0 {
|
||||
// remove enclosing brackets of ipv6 address otherwise they will be duplicated
|
||||
if len(host) > 1 && host[0] == '[' && host[len(host)-1] == ']' {
|
||||
host = host[1 : len(host)-1]
|
||||
} else {
|
||||
if forwardedProto := r.Header.Get("X-Forwarded-Proto"); len(forwardedProto) > 0 {
|
||||
scheme = forwardedProto
|
||||
}
|
||||
if forwardedHost := r.Header.Get("X-Forwarded-Host"); len(forwardedHost) > 0 {
|
||||
// According to the Apache mod_proxy docs, X-Forwarded-Host can be a
|
||||
// comma-separated list of hosts, to which each proxy appends the
|
||||
// requested host. We want to grab the first from this comma-separated
|
||||
// list.
|
||||
hosts := strings.SplitN(forwardedHost, ",", 2)
|
||||
host = strings.TrimSpace(hosts[0])
|
||||
}
|
||||
// JoinHostPort properly encloses ipv6 addresses in square brackets
|
||||
host = net.JoinHostPort(host, port)
|
||||
} else if isIPv6Address(host) && host[0] != '[' {
|
||||
// ipv6 needs to be enclosed in square brackets in urls
|
||||
host = "[" + host + "]"
|
||||
}
|
||||
|
||||
basePath := routeDescriptorsMap[RouteNameBase].Path
|
||||
|
@ -290,28 +264,3 @@ func appendValues(u string, values ...url.Values) string {
|
|||
|
||||
return appendValuesURL(up, values...).String()
|
||||
}
|
||||
|
||||
// isIPv6Address returns true if given string is a valid IPv6 address. No port is allowed. The address may be
|
||||
// enclosed in square brackets.
|
||||
func isIPv6Address(host string) bool {
|
||||
if len(host) > 1 && host[0] == '[' && host[len(host)-1] == ']' {
|
||||
host = host[1 : len(host)-1]
|
||||
}
|
||||
// The IPv6 scoped addressing zone identifier starts after the last percent sign.
|
||||
if i := strings.LastIndexByte(host, '%'); i > 0 {
|
||||
host = host[:i]
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
if ip.To16() == nil {
|
||||
return false
|
||||
}
|
||||
if ip.To4() == nil {
|
||||
return true
|
||||
}
|
||||
// dot can be present in ipv4-mapped address, it needs to come after a colon though
|
||||
i := strings.IndexAny(host, ":.")
|
||||
return i >= 0 && host[i] == ':'
|
||||
}
|
||||
|
|
|
@ -207,7 +207,7 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
{
|
||||
name: "https protocol forwarded with a non-standard header",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"X-Forwarded-Proto": []string{"https"},
|
||||
"X-Custom-Forwarded-Proto": []string{"https"},
|
||||
}},
|
||||
base: "http://example.com",
|
||||
},
|
||||
|
@ -253,6 +253,7 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
{
|
||||
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",
|
||||
|
@ -262,16 +263,33 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"X-Forwarded-Port": []string{"443 , 5001"},
|
||||
}},
|
||||
base: "http://example.com:443",
|
||||
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 "},
|
||||
"X-Forwarded-Port": []string{" 12345 \t"},
|
||||
"X-Forwarded-Host": []string{" first.example.com:12345 "},
|
||||
}},
|
||||
base: "http://first.example.com:12345",
|
||||
base: "https://first.example.com:12345",
|
||||
},
|
||||
{
|
||||
name: "forwarded host with port supplied takes priority",
|
||||
|
@ -292,16 +310,16 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
{
|
||||
name: "forwarded protocol and addr using standard header",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`proto=https;for="192.168.22.30:80"`},
|
||||
"Forwarded": []string{`proto=https;host="192.168.22.30:80"`},
|
||||
}},
|
||||
base: "https://192.168.22.30:80",
|
||||
},
|
||||
{
|
||||
name: "forwarded addr takes priority over host",
|
||||
name: "forwarded host takes priority over for",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`host=reg.example.com;for="192.168.22.30:5000"`},
|
||||
"Forwarded": []string{`host="reg.example.com:5000";for="192.168.22.30"`},
|
||||
}},
|
||||
base: "http://192.168.22.30:5000",
|
||||
base: "http://reg.example.com:5000",
|
||||
},
|
||||
{
|
||||
name: "forwarded host and protocol using standard header",
|
||||
|
@ -320,73 +338,26 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
{
|
||||
name: "process just the first list element of standard header",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="reg.example.com:443";proto=https, for="reg.example.com:80";proto=http`},
|
||||
"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 override port",
|
||||
name: "IPv6 address use host",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="2607:f0d0:1002:51::4"`},
|
||||
"X-Forwarded-Port": []string{"5001"},
|
||||
"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{`for="[2607:f0d0:1002:51::4]:4000"`},
|
||||
"Forwarded": []string{`host="[2607:f0d0:1002:51::4]:4000"`},
|
||||
"X-Forwarded-Port": []string{"5001"},
|
||||
}},
|
||||
base: "http://[2607:f0d0:1002:51::4]:4000",
|
||||
},
|
||||
{
|
||||
name: "IPv6 long address override port",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="2607:f0d0:1002:0051:0000:0000:0000:0004"`},
|
||||
"X-Forwarded-Port": []string{"5001"},
|
||||
}},
|
||||
base: "http://[2607:f0d0:1002:0051:0000:0000:0000:0004]:5001",
|
||||
},
|
||||
{
|
||||
name: "IPv6 long address enclosed in brackets - be benevolent",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="[2607:f0d0:1002:0051:0000:0000:0000:0004]"`},
|
||||
"X-Forwarded-Port": []string{"5001"},
|
||||
}},
|
||||
base: "http://[2607:f0d0:1002:0051:0000:0000:0000:0004]:5001",
|
||||
},
|
||||
{
|
||||
name: "IPv6 long address with port",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="[2607:f0d0:1002:0051:0000:0000:0000:0004]:4321"`},
|
||||
"X-Forwarded-Port": []string{"5001"},
|
||||
}},
|
||||
base: "http://[2607:f0d0:1002:0051:0000:0000:0000:0004]:4321",
|
||||
},
|
||||
{
|
||||
name: "IPv6 address with zone ID",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="fe80::bd0f:a8bc:6480:238b%11"`},
|
||||
"X-Forwarded-Port": []string{"5001"},
|
||||
}},
|
||||
base: "http://[fe80::bd0f:a8bc:6480:238b%2511]:5001",
|
||||
},
|
||||
{
|
||||
name: "IPv6 address with zone ID and port",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="[fe80::bd0f:a8bc:6480:238b%eth0]:12345"`},
|
||||
"X-Forwarded-Port": []string{"5001"},
|
||||
}},
|
||||
base: "http://[fe80::bd0f:a8bc:6480:238b%25eth0]:12345",
|
||||
},
|
||||
{
|
||||
name: "IPv6 address without port",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="::FFFF:129.144.52.38"`},
|
||||
}},
|
||||
base: "http://[::FFFF:129.144.52.38]",
|
||||
},
|
||||
{
|
||||
name: "non-standard and standard forward headers",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
|
@ -398,14 +369,34 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
base: "https://first.example.com",
|
||||
},
|
||||
{
|
||||
name: "non-standard headers take precedence over standard one",
|
||||
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: "http://first.example.com: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,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -428,23 +419,9 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
continue
|
||||
}
|
||||
|
||||
var expectedURL string
|
||||
proto, ok := tr.request.Header["X-Forwarded-Proto"]
|
||||
if !ok {
|
||||
expectedURL = testCase.expectedPath
|
||||
if !relative {
|
||||
expectedURL = tr.base + 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() + expectedURL
|
||||
}
|
||||
expectedURL := testCase.expectedPath
|
||||
if !relative {
|
||||
expectedURL = tr.base + expectedURL
|
||||
}
|
||||
|
||||
if buildURL != expectedURL {
|
||||
|
@ -541,119 +518,3 @@ func TestBuilderFromRequestWithPrefix(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsIPv6Address(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
address string
|
||||
isIPv6 bool
|
||||
}{
|
||||
{
|
||||
name: "IPv6 short address",
|
||||
address: `2607:f0d0:1002:51::4`,
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 short address enclosed in brackets",
|
||||
address: "[2607:f0d0:1002:51::4]",
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 address",
|
||||
address: `2607:f0d0:1002:0051:0000:0000:0000:0004`,
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 address with numeric zone ID",
|
||||
address: `fe80::bd0f:a8bc:6480:238b%11`,
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 address with device name as zone ID",
|
||||
address: `fe80::bd0f:a8bc:6480:238b%eth0`,
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 address with device name as zone ID enclosed in brackets",
|
||||
address: `[fe80::bd0f:a8bc:6480:238b%eth0]`,
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "IPv4-mapped address",
|
||||
address: "::FFFF:129.144.52.38",
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "localhost",
|
||||
address: "::1",
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "localhost",
|
||||
address: "::1",
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "long localhost address",
|
||||
address: "0:0:0:0:0:0:0:1",
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 long address with port",
|
||||
address: "[2607:f0d0:1002:0051:0000:0000:0000:0004]:4321",
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "too many groups",
|
||||
address: "2607:f0d0:1002:0051:0000:0000:0000:0004:4321",
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "square brackets don't make an IPv6 address",
|
||||
address: "[2607:f0d0]",
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "require two consecutive colons in localhost",
|
||||
address: ":1",
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "more then 4 hexadecimal digits",
|
||||
address: "2607:f0d0b:1002:0051:0000:0000:0000:0004",
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "too short address",
|
||||
address: `2607:f0d0:1002:0000:0000:0000:0004`,
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "IPv4 address",
|
||||
address: `192.168.100.1`,
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "unclosed bracket",
|
||||
address: `[2607:f0d0:1002:0051:0000:0000:0000:0004`,
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "trailing bracket",
|
||||
address: `2607:f0d0:1002:0051:0000:0000:0000:0004]`,
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "domain name",
|
||||
address: `localhost`,
|
||||
isIPv6: false,
|
||||
},
|
||||
} {
|
||||
isIPv6 := isIPv6Address(tc.address)
|
||||
if isIPv6 && !tc.isIPv6 {
|
||||
t.Errorf("[%s] address %q falsely detected as IPv6 address", tc.name, tc.address)
|
||||
} else if !isIPv6 && tc.isIPv6 {
|
||||
t.Errorf("[%s] address %q not recognized as IPv6", tc.name, tc.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue