From 2fc9012416d3ac21905e6f91e526ce909a34255c Mon Sep 17 00:00:00 2001 From: Jiri Kozel Date: Thu, 21 Sep 2023 19:50:18 +0200 Subject: [PATCH] Read also X-Forwarded-Proto and -Host --- src/layman/error_list.py | 1 + src/layman/util.py | 53 +++++++++++++++++++++++++++++++++++++--- src/layman/util_test.py | 29 ++++++++++++++++++++-- 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/layman/error_list.py b/src/layman/error_list.py index b0eccf9bb..dbfaa69c6 100644 --- a/src/layman/error_list.py +++ b/src/layman/error_list.py @@ -55,4 +55,5 @@ 52: (400, 'GeoServer HTTP or connection error'), 53: (500, 'Error when publishing on GeoServer. It happens for example for raster files with wrong explicit CRS.'), 54: (400, 'Wrong header value'), + 55: (400, 'Wrong combination of headers'), } diff --git a/src/layman/util.py b/src/layman/util.py index 54a12c2b4..917325c84 100644 --- a/src/layman/util.py +++ b/src/layman/util.py @@ -59,9 +59,24 @@ def get(self): @dataclass(frozen=True) class XForwardedClass: - def __init__(self, *, prefix=None): + def __init__(self, *, proto=None, host=None, prefix=None): + assert proto is None and host is None and prefix is None \ + or proto is None and host is None and prefix is not None \ + or proto is not None and host is not None and prefix is not None + object.__setattr__(self, '_proto', proto) + object.__setattr__(self, '_host', host) object.__setattr__(self, '_prefix', prefix) + @property + def proto(self): + # pylint: disable=no-member + return self._proto + + @property + def host(self): + # pylint: disable=no-member + return self._host + @property def prefix(self): # pylint: disable=no-member @@ -70,6 +85,10 @@ def prefix(self): @property def headers(self): result = {} + if self.proto is not None: + result['x-Forwarded-Proto'] = self.proto + if self.host is not None: + result['x-Forwarded-Host'] = self.host if self.prefix is not None: result['x-Forwarded-Prefix'] = self.prefix return result @@ -78,10 +97,17 @@ def __bool__(self): return bool(self.headers) def __eq__(self, other): - return isinstance(other, XForwardedClass) and self.prefix == other.prefix + return isinstance(other, XForwardedClass) \ + and self.proto == other.proto \ + and self.host == other.host \ + and self.prefix == other.prefix def __repr__(self): parts = [] + if self.proto is not None: + parts.append(('proto', json.dumps(self.proto))) + if self.host is not None: + parts.append(('host', json.dumps(self.host))) if self.prefix is not None: parts.append(('prefix', json.dumps(self.prefix))) parts_str = ', '.join(f"{key}={value}" for key, value in parts) @@ -585,8 +611,24 @@ def ensure_home_dir(): def get_x_forwarded_items(request_headers): - prefix = None + proto_key = 'X-Forwarded-Proto' + host_key = 'X-Forwarded-Host' prefix_key = 'X-Forwarded-Prefix' + exp_combinations = [ + set(), + {prefix_key}, + {proto_key, host_key, prefix_key}, + ] + found_combination = {k for k in [proto_key, host_key, prefix_key] if k in request_headers} + if found_combination not in exp_combinations: + raise LaymanError(55, { + 'expected': f'Either no X-Forwarded-* header, or only {prefix_key} header, or three headers ({proto_key}, {host_key}, {prefix_key}) to be set.', + 'found': sorted(list(found_combination)), + }) + + host = None + proto = None + prefix = None if prefix_key in request_headers: prefix = request_headers[prefix_key] if not re.match(CLIENT_PROXY_PATTERN, prefix): @@ -597,4 +639,7 @@ def get_x_forwarded_items(request_headers): 'found': prefix, } ) - return XForwardedClass(prefix=prefix) + if proto_key in request_headers: + proto = request_headers[proto_key] + host = request_headers[host_key] + return XForwardedClass(proto=proto, host=host, prefix=prefix) diff --git a/src/layman/util_test.py b/src/layman/util_test.py index 4eef42188..956becf5a 100644 --- a/src/layman/util_test.py +++ b/src/layman/util_test.py @@ -139,7 +139,12 @@ def test__url_for(endpoint, internal, params, expected_url): @pytest.mark.parametrize('headers, exp_result', [ pytest.param({'X-Forwarded-Prefix': '/layman-proxy'}, util.XForwardedClass(prefix='/layman-proxy'), - id='simple_header'), + id='prefix_header'), + pytest.param({ + 'X-Forwarded-Proto': 'https', + 'X-Forwarded-Host': 'example.com', + 'X-Forwarded-Prefix': '/another-layman-proxy', + }, util.XForwardedClass(proto='https', host='example.com', prefix='/another-layman-proxy'), id='three_headers'), pytest.param({}, util.XForwardedClass(), id='without_header'), ]) def test_get_x_forwarded_prefix(headers, exp_result): @@ -159,7 +164,27 @@ def test_get_x_forwarded_prefix(headers, exp_result): 'expected': 'Expected header matching regular expression ^(?:/[a-z0-9_-]+)*$', 'found': 'layman-proxy', }, - }, id='without_slash'), + }, id='prefix_without_slash'), + pytest.param( + {'X-Forwarded-Proto': 'http'}, + { + 'http_code': 400, + 'code': 55, + 'data': { + 'expected': 'Either no X-Forwarded-* header, or only X-Forwarded-Prefix header, or three headers (X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Prefix) to be set.', + 'found': ['X-Forwarded-Proto'], + }, + }, id='only_proto_header'), + pytest.param( + {'X-Forwarded-Host': 'example.com', 'X-Forwarded-Prefix': 'layman-proxy'}, + { + 'http_code': 400, + 'code': 55, + 'data': { + 'expected': 'Either no X-Forwarded-* header, or only X-Forwarded-Prefix header, or three headers (X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Prefix) to be set.', + 'found': ['X-Forwarded-Host', 'X-Forwarded-Prefix'], + }, + }, id='host_and_prefix_header'), ]) def test_get_x_forwarded_prefix_raises(headers, exp_error): with pytest.raises(LaymanError) as exc_info: