Do not hiccup on ipv6 addresses in the HTTP Host header

Fixed port extraction. Now supports hostname, hostname + port,
ipv4, ipv4 + port, ipv6, ipv6 + port

Fixes #1571
This commit is contained in:
Gina Häußge 2016-11-07 16:17:16 +01:00
parent 7efdeced62
commit 2952b2622e
2 changed files with 138 additions and 4 deletions

View file

@ -232,6 +232,15 @@ class ReverseProxiedEnvironment(object):
to_wsgi_format = lambda header: "HTTP_" + header.upper().replace("-", "_")
return map(to_wsgi_format, values)
@staticmethod
def valid_ip(address):
import netaddr
try:
netaddr.IPAddress(address)
return True
except:
return False
def __init__(self,
header_prefix=None,
header_scheme=None,
@ -286,11 +295,43 @@ class ReverseProxiedEnvironment(object):
if host is None:
return None, None
default_port = "443" if scheme == "https" else "80"
host = host.strip()
if ":" in host:
server, port = host.split(":", 1)
# we might have an ipv6 address here, or a port, or both
if host[0] == "[":
# that looks like an ipv6 address with port, e.g. [fec1::1]:80
address_end = host.find("]")
if address_end == -1:
# no ], that looks like a seriously broken address
return None, None
# extract server ip, skip enclosing [ and ]
server = host[1:address_end]
tail = host[address_end + 1:]
# now check if there's also a port
if len(tail) and tail[0] == ":":
# port included as well
port = tail[1:]
else:
# no port, use default one
port = default_port
elif self.__class__.valid_ip(host):
# ipv6 address without port
server = host
port = default_port
else:
# ipv4 address with port
server, port = host.rsplit(":", 1)
else:
server = host
port = "443" if scheme == "https" else "80"
port = default_port
return server, port
@ -348,7 +389,12 @@ class ReverseProxiedEnvironment(object):
# default port for scheme, can be skipped
environ["HTTP_HOST"] = environ["SERVER_NAME"]
else:
environ["HTTP_HOST"] = environ["SERVER_NAME"] + ":" + environ["SERVER_PORT"]
server_name_component = environ["SERVER_NAME"]
if ":" in server_name_component and self.__class__.valid_ip(server_name_component):
# this is an ipv6 address, we need to wrap that in [ and ] before appending the port
server_name_component = "[" + server_name_component + "]"
environ["HTTP_HOST"] = server_name_component + ":" + environ["SERVER_PORT"]
# call wrapped app with rewritten environment
return environ

View file

@ -143,7 +143,95 @@ class ReverseProxiedEnvironmentTest(unittest.TestCase):
"HTTP_HOST": "example.com",
"SERVER_NAME": "example.com",
"SERVER_PORT": "80"
})
}),
# host = none, default port -> server & port used for reconstruction (ipv4)
({
"HTTP_HOST": None,
"SERVER_NAME": "127.0.0.1",
"SERVER_PORT": "80"
}, {
"HTTP_HOST": "127.0.0.1",
"SERVER_NAME": "127.0.0.1",
"SERVER_PORT": "80"
}),
# host = none, non standard port -> server & port used for reconstruction (ipv4)
({
"HTTP_HOST": None,
"SERVER_NAME": "127.0.0.1",
"SERVER_PORT": "444"
}, {
"HTTP_HOST": "127.0.0.1:444",
"SERVER_NAME": "127.0.0.1",
"SERVER_PORT": "444"
}),
# host = none, default port -> server & port used for reconstruction (ipv6)
({
"HTTP_HOST": None,
"SERVER_NAME": "fec1::1",
"SERVER_PORT": "80"
}, {
"HTTP_HOST": "fec1::1",
"SERVER_NAME": "fec1::1",
"SERVER_PORT": "80"
}),
# host = none, non standard port -> server & port used for reconstruction (ipv6)
({
"HTTP_HOST": None,
"SERVER_NAME": "fec1::1",
"SERVER_PORT": "444"
}, {
"HTTP_HOST": "[fec1::1]:444",
"SERVER_NAME": "fec1::1",
"SERVER_PORT": "444"
}),
# host set, server and port not, default port -> server & port derived from host (ipv4)
({
"HTTP_HOST": "127.0.0.1",
"SERVER_NAME": None,
"SERVER_PORT": None
}, {
"HTTP_HOST": "127.0.0.1",
"SERVER_NAME": "127.0.0.1",
"SERVER_PORT": "80"
}),
# host set, server and port not, non standard port -> server & port derived from host (ipv4)
({
"HTTP_HOST": "127.0.0.1:444",
"SERVER_NAME": None,
"SERVER_PORT": None
}, {
"HTTP_HOST": "127.0.0.1:444",
"SERVER_NAME": "127.0.0.1",
"SERVER_PORT": "444"
}),
# host set, server and port not, default port -> server & port derived from host (ipv6)
({
"HTTP_HOST": "fec1::1",
"SERVER_NAME": None,
"SERVER_PORT": None
}, {
"HTTP_HOST": "fec1::1",
"SERVER_NAME": "fec1::1",
"SERVER_PORT": "80"
}),
# host set, server and port not, non standard port -> server & port derived from host (ipv6)
({
"HTTP_HOST": "[fec1::1]:444",
"SERVER_NAME": None,
"SERVER_PORT": None
}, {
"HTTP_HOST": "[fec1::1]:444",
"SERVER_NAME": "fec1::1",
"SERVER_PORT": "444"
}),
)
@unpack
def test_stock(self, environ, expected):