Remove support for X-Forwarded-Port

Partially reverts change adding support for X-Forwarded-Port.
Changes the logic to prefer the standard Forwarded header over
X-Forwarded headers. Prefer forwarded "host" over "for" since
"for" represents the client and not the client's request.

Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
This commit is contained in:
Derek McGowan 2017-03-20 16:13:33 -07:00
parent d4c3e88426
commit 81a47d9766
2 changed files with 72 additions and 278 deletions

View file

@ -2,10 +2,8 @@ package v2
import ( import (
"fmt" "fmt"
"net"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"strings" "strings"
"github.com/docker/distribution/reference" "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 // NewURLBuilderFromRequest uses information from an *http.Request to
// construct the root url. // construct the root url.
func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder { func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder {
var scheme string var (
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:
scheme = "http" 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 // Handle fowarded headers
// Prefer "Forwarded" header as defined by rfc7239 if given
if forwardedHost := r.Header.Get("X-Forwarded-Host"); len(forwardedHost) > 0 { // see https://tools.ietf.org/html/rfc7239
// According to the Apache mod_proxy docs, X-Forwarded-Host can be a if forwarded := r.Header.Get("Forwarded"); len(forwarded) > 0 {
// comma-separated list of hosts, to which each proxy appends the forwardedHeader, _, err := parseForwardedHeader(forwarded)
// requested host. We want to grab the first from this comma-separated if err == nil {
// list. if fproto := forwardedHeader["proto"]; len(fproto) > 0 {
hosts := strings.SplitN(forwardedHost, ",", 2) scheme = fproto
host = strings.TrimSpace(hosts[0]) }
} else if addr, exists := forwardedHeader["for"]; exists { if fhost := forwardedHeader["host"]; len(fhost) > 0 {
host = addr host = fhost
} 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
} }
} } else {
if forwardedProto := r.Header.Get("X-Forwarded-Proto"); len(forwardedProto) > 0 {
if len(portLessHost) > 0 { scheme = forwardedProto
host = portLessHost }
} if forwardedHost := r.Header.Get("X-Forwarded-Host"); len(forwardedHost) > 0 {
if len(port) > 0 { // According to the Apache mod_proxy docs, X-Forwarded-Host can be a
// remove enclosing brackets of ipv6 address otherwise they will be duplicated // comma-separated list of hosts, to which each proxy appends the
if len(host) > 1 && host[0] == '[' && host[len(host)-1] == ']' { // requested host. We want to grab the first from this comma-separated
host = host[1 : len(host)-1] // 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 basePath := routeDescriptorsMap[RouteNameBase].Path
@ -290,28 +264,3 @@ func appendValues(u string, values ...url.Values) string {
return appendValuesURL(up, 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] == ':'
}

View file

@ -207,7 +207,7 @@ func TestBuilderFromRequest(t *testing.T) {
{ {
name: "https protocol forwarded with a non-standard header", name: "https protocol forwarded with a non-standard header",
request: &http.Request{URL: u, Host: u.Host, Header: http.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", base: "http://example.com",
}, },
@ -253,6 +253,7 @@ func TestBuilderFromRequest(t *testing.T) {
{ {
name: "forwarded port with a non-standard header", name: "forwarded port with a non-standard header",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Host": []string{"example.com:5000"},
"X-Forwarded-Port": []string{"5000"}, "X-Forwarded-Port": []string{"5000"},
}}, }},
base: "http://example.com:5000", base: "http://example.com:5000",
@ -262,12 +263,13 @@ func TestBuilderFromRequest(t *testing.T) {
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Port": []string{"443 , 5001"}, "X-Forwarded-Port": []string{"443 , 5001"},
}}, }},
base: "http://example.com:443", base: "http://example.com",
}, },
{ {
name: "forwarded standard port with non-standard headers", name: "forwarded standard port with non-standard headers",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Proto": []string{"https"},
"X-Forwarded-Host": []string{"example.com"},
"X-Forwarded-Port": []string{"443"}, "X-Forwarded-Port": []string{"443"},
}}, }},
base: "https://example.com", base: "https://example.com",
@ -276,6 +278,7 @@ func TestBuilderFromRequest(t *testing.T) {
name: "forwarded standard port with non-standard headers and explicit port", name: "forwarded standard port with non-standard headers and explicit port",
request: &http.Request{URL: u, Host: u.Host + ":443", Header: http.Header{ request: &http.Request{URL: u, Host: u.Host + ":443", Header: http.Header{
"X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Proto": []string{"https"},
"X-Forwarded-Host": []string{u.Host + ":443"},
"X-Forwarded-Port": []string{"443"}, "X-Forwarded-Port": []string{"443"},
}}, }},
base: "https://example.com:443", base: "https://example.com:443",
@ -284,10 +287,9 @@ func TestBuilderFromRequest(t *testing.T) {
name: "several non-standard headers", name: "several non-standard headers",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Proto": []string{"https"},
"X-Forwarded-Host": []string{" first.example.com "}, "X-Forwarded-Host": []string{" first.example.com:12345 "},
"X-Forwarded-Port": []string{" 12345 \t"},
}}, }},
base: "http://first.example.com:12345", base: "https://first.example.com:12345",
}, },
{ {
name: "forwarded host with port supplied takes priority", name: "forwarded host with port supplied takes priority",
@ -308,16 +310,16 @@ func TestBuilderFromRequest(t *testing.T) {
{ {
name: "forwarded protocol and addr using standard header", name: "forwarded protocol and addr using standard header",
request: &http.Request{URL: u, Host: u.Host, Header: http.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", 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{ 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", name: "forwarded host and protocol using standard header",
@ -336,73 +338,26 @@ func TestBuilderFromRequest(t *testing.T) {
{ {
name: "process just the first list element of standard header", name: "process just the first list element of standard header",
request: &http.Request{URL: u, Host: u.Host, Header: http.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", 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{ request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"Forwarded": []string{`for="2607:f0d0:1002:51::4"`}, "Forwarded": []string{`for="2607:f0d0:1002:51::4";host="[2607:f0d0:1002:51::4]:5001"`},
"X-Forwarded-Port": []string{"5001"}, "X-Forwarded-Port": []string{"5002"},
}}, }},
base: "http://[2607:f0d0:1002:51::4]:5001", base: "http://[2607:f0d0:1002:51::4]:5001",
}, },
{ {
name: "IPv6 address with port", name: "IPv6 address with port",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ 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"}, "X-Forwarded-Port": []string{"5001"},
}}, }},
base: "http://[2607:f0d0:1002:51::4]:4000", 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", name: "non-standard and standard forward headers",
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
@ -414,14 +369,34 @@ func TestBuilderFromRequest(t *testing.T) {
base: "https://first.example.com", 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{ request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
"X-Forwarded-Proto": []string{`http`}, "X-Forwarded-Proto": []string{`http`},
"Forwarded": []string{`host=second.example.com; proto=https`}, "Forwarded": []string{`host=second.example.com; proto=https`},
"X-Forwarded-Host": []string{`first.example.com`}, "X-Forwarded-Host": []string{`first.example.com`},
"X-Forwarded-Port": []string{`4000`}, "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,
}, },
} }
@ -444,23 +419,9 @@ func TestBuilderFromRequest(t *testing.T) {
continue continue
} }
var expectedURL string expectedURL := testCase.expectedPath
proto, ok := tr.request.Header["X-Forwarded-Proto"] if !relative {
if !ok { expectedURL = tr.base + expectedURL
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
}
} }
if buildURL != expectedURL { if buildURL != expectedURL {
@ -557,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)
}
}
}