From 2952b2622ea9fc825b259df0bd34aa6501a36065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 7 Nov 2016 16:17:16 +0100 Subject: [PATCH] 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 --- src/octoprint/server/util/flask.py | 52 ++++++++++++++++- tests/server/util/flask.py | 90 +++++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 4 deletions(-) diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index fdc8efd8..35305408 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -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 diff --git a/tests/server/util/flask.py b/tests/server/util/flask.py index b732416c..cfd6e19d 100644 --- a/tests/server/util/flask.py +++ b/tests/server/util/flask.py @@ -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):