Skip to content

Commit 7b52794

Browse files
committed
Fix swarm tests
1 parent 95f94a4 commit 7b52794

6 files changed

Lines changed: 130 additions & 6 deletions

File tree

nginx/Url.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,20 @@ def parse(entry_string: str, default_scheme=None, default_port=None, default_loc
3636
return Url(scheme, host if host else None, port, location)
3737

3838
@staticmethod
39-
def is_valid_hostname(hostname: str) -> bool:
39+
def is_valid_hostname(hostname: str, allow_wildcard: bool = False, max_length: int = 253) -> bool:
4040
"""
4141
https://stackoverflow.com/a/33214423/2804342
4242
:return: True if for valid hostname False otherwise
4343
"""
44+
if not hostname:
45+
return False
4446
if hostname[-1] == ".":
4547
# strip exactly one dot from the right, if present
4648
hostname = hostname[:-1]
47-
if len(hostname) > 253:
49+
if len(hostname) > max_length:
4850
return False
51+
if allow_wildcard and hostname.startswith("*."):
52+
hostname = hostname[2:]
4953

5054
labels = hostname.split(".")
5155

nginx_proxy/BackendTarget.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,10 @@ def __init__(self, network_names: List[str], backend_type="container"):
133133
class NoHostConfiguration(UnconfiguredBackend):
134134
def __init__(self, backend_type="container"):
135135
super().__init__(backend_type)
136+
137+
138+
class InvalidHostConfiguration(UnconfiguredBackend):
139+
def __init__(self, hostname: str, reason: str, backend_type="container"):
140+
super().__init__(backend_type)
141+
self.hostname = hostname
142+
self.reason = reason

nginx_proxy/pre_processors/redirect_processor.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
from nginx_proxy.Host import Host
99

1010

11+
def _is_certificate_redirect_target(target: Url):
12+
return "https" in target.scheme or "wss" in target.scheme or int(target.port or 80) == 443
13+
14+
1115
def process_redirection(backend: BackendTarget, environments: map, vhost_map: Dict[str, Dict[int, Host]]):
1216
redirect_env = [e[1] for e in environments.items() if e[0].startswith("PROXY_FULL_REDIRECT")]
1317
hosts = []
@@ -39,8 +43,19 @@ def process_redirection(backend: BackendTarget, environments: map, vhost_map: Di
3943
target.port = 443 if "https" in target.scheme or "wss" in target.scheme else 80
4044
if not target.scheme:
4145
target.scheme = {"https"} if target.port == 443 else {"http"}
46+
if not Url.is_valid_hostname(target.hostname, allow_wildcard=True):
47+
print("Invalid PROXY_FULL_REDIRECT target hostname: " + target.hostname)
48+
continue
49+
if _is_certificate_redirect_target(target) and not Url.is_valid_hostname(
50+
target.hostname, allow_wildcard=True, max_length=64
51+
):
52+
print("Invalid PROXY_FULL_REDIRECT target certificate hostname: " + target.hostname)
53+
continue
4254
for source in sources:
4355
if source.hostname is not None:
56+
if not Url.is_valid_hostname(source.hostname, allow_wildcard=True):
57+
print("Invalid PROXY_FULL_REDIRECT source hostname: " + source.hostname)
58+
continue
4459
port = 80 if source.port is None else int(source.port)
4560
if source.hostname not in vhost_map:
4661
host = Host(source.hostname, port)

nginx_proxy/pre_processors/virtual_host_processor.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import re
22

3+
from nginx import Url
34
from nginx_proxy import Host, ProxyConfigData
4-
from nginx_proxy.BackendTarget import BackendTarget, NoHostConfiguration, UnreachableNetwork
5+
from nginx_proxy.BackendTarget import BackendTarget, InvalidHostConfiguration, NoHostConfiguration, UnreachableNetwork
56
from nginx_proxy.utils import split_url
67

78

@@ -20,6 +21,17 @@ def _default_external_port(schemes):
2021
return 443 if has_secure_scheme and not has_insecure_scheme else 80
2122

2223

24+
def _requires_certificate(host: Host) -> bool:
25+
return "https" in host.scheme or "wss" in host.scheme or int(host.port or 80) == 443
26+
27+
28+
def _validate_external_host(host: Host):
29+
if not Url.is_valid_hostname(host.hostname, allow_wildcard=True):
30+
raise InvalidHostConfiguration(host.hostname, "invalid hostname")
31+
if _requires_certificate(host) and not Url.is_valid_hostname(host.hostname, allow_wildcard=True, max_length=64):
32+
raise InvalidHostConfiguration(host.hostname, "certificate hostnames must be 64 characters or fewer")
33+
34+
2335
def _parse_extra_directive(raw_directive: str):
2436
directive = raw_directive.strip()
2537
if not directive:
@@ -96,6 +108,14 @@ def process_virtual_hosts(backend: BackendTarget, known_networks: set) -> ProxyC
96108
"networks: " + ", ".join(list(e.network_names)),
97109
sep="\t",
98110
)
111+
except InvalidHostConfiguration as e:
112+
print(
113+
"Invalid VIRTUAL_HOST ",
114+
f"{backend.type:>9}".title() + " Id: " + backend.id[:12],
115+
backend.name,
116+
f"{e.hostname}: {e.reason}",
117+
sep="\t",
118+
)
99119
return hosts
100120

101121

@@ -176,6 +196,7 @@ def host_generator(backend: BackendTarget, known_networks: set = {}):
176196

177197
for host_config in static_hosts:
178198
host, location, container_data, extras = _parse_host_entry(host_config)
199+
_validate_external_host(host)
179200

180201
if container_data.address is None:
181202
print(
@@ -208,6 +229,7 @@ def host_generator(backend: BackendTarget, known_networks: set = {}):
208229

209230
for host_config in virtual_hosts:
210231
host, location, container_data, extras = _parse_host_entry(host_config)
232+
_validate_external_host(host)
211233
# Protect double / in urls.
212234
if location and not location.endswith("/") and container_data.path and container_data.path.endswith("/"):
213235
location = location + "/"

tests/integration/test_nginx_proxy.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ def get_request_url(virtual_host, request_path, scheme="http"):
3838
return f"{scheme}://{hostname}{request_path}"
3939

4040

41+
def _hostname_mode_token(swarm_mode):
42+
return {"prefer-local": "pl"}.get(swarm_mode, swarm_mode)
43+
44+
4145
def _has_proxy_server(config_str, server_name):
4246
config = HttpBlock.parse(config_str)
4347
for server in config.servers:
@@ -230,9 +234,9 @@ def test_proxy_full_redirect_to_https_target_response(
230234
if not is_reachable(swarm_mode, backend_type):
231235
pytest.skip("Backend discovery not available for this swarm mode/backend type combination.")
232236

233-
suffix = f"{backend_type}.{swarm_mode}.{datetime.now(timezone.utc).strftime('%H%M%S%f')}"
234-
target_host = f"{suffix}.full-redirect-target.example.com"
235-
source_host = f"{suffix}.full-redirect-source.example.com"
237+
suffix = f"{backend_type[:3]}.{_hostname_mode_token(swarm_mode)}.{datetime.now(timezone.utc).strftime('%H%M%S%f')}"
238+
target_host = f"{suffix}.frt.example.com"
239+
source_host = f"{suffix}.frs.example.com"
236240
env = {
237241
"VIRTUAL_HOST": f"https://{target_host} -> :8080",
238242
"VIRTUAL_PORT": "8080",

tests/unit/test_backend_target.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,78 @@ def test_process_virtual_hosts_uses_next_network_when_first_ip_blank(self):
228228
backend = location.backends[0]
229229
assert backend.address == "10.0.0.12"
230230

231+
def test_https_virtual_host_rejects_certificate_hostname_longer_than_64_chars(self):
232+
long_hostname = f"{'a' * 55}.example.com"
233+
assert len(long_hostname) > 64
234+
bt = BackendTarget(
235+
id="long-https-id",
236+
name="long-https-test",
237+
env={"VIRTUAL_HOST": f"https://{long_hostname}"},
238+
network_settings={"my-net": {"NetworkID": "my-net-id", "IPAddress": "10.0.0.12"}},
239+
)
240+
241+
config_data = process_virtual_hosts(bt, {"my-net-id"})
242+
243+
assert len(list(config_data.host_list())) == 0
244+
245+
def test_http_virtual_host_allows_dns_valid_hostname_longer_than_64_chars(self):
246+
long_hostname = f"{'a' * 55}.example.com"
247+
assert len(long_hostname) > 64
248+
bt = BackendTarget(
249+
id="long-http-id",
250+
name="long-http-test",
251+
env={"VIRTUAL_HOST": long_hostname},
252+
network_settings={"my-net": {"NetworkID": "my-net-id", "IPAddress": "10.0.0.12"}},
253+
)
254+
255+
config_data = process_virtual_hosts(bt, {"my-net-id"})
256+
257+
hosts = list(config_data.host_list())
258+
assert len(hosts) == 1
259+
assert hosts[0].hostname == long_hostname
260+
261+
def test_https_virtual_host_allows_certificate_hostname_at_64_chars(self):
262+
hostname = f"{'a' * 52}.example.com"
263+
assert len(hostname) == 64
264+
bt = BackendTarget(
265+
id="max-https-id",
266+
name="max-https-test",
267+
env={"VIRTUAL_HOST": f"https://{hostname}"},
268+
network_settings={"my-net": {"NetworkID": "my-net-id", "IPAddress": "10.0.0.12"}},
269+
)
270+
271+
config_data = process_virtual_hosts(bt, {"my-net-id"})
272+
273+
hosts = list(config_data.host_list())
274+
assert len(hosts) == 1
275+
assert hosts[0].hostname == hostname
276+
277+
def test_virtual_host_rejects_invalid_hostname(self):
278+
bt = BackendTarget(
279+
id="invalid-host-id",
280+
name="invalid-host-test",
281+
env={"VIRTUAL_HOST": "bad_host.example.com"},
282+
network_settings={"my-net": {"NetworkID": "my-net-id", "IPAddress": "10.0.0.12"}},
283+
)
284+
285+
config_data = process_virtual_hosts(bt, {"my-net-id"})
286+
287+
assert len(list(config_data.host_list())) == 0
288+
289+
def test_virtual_host_allows_wildcard_hostname(self):
290+
bt = BackendTarget(
291+
id="wildcard-host-id",
292+
name="wildcard-host-test",
293+
env={"VIRTUAL_HOST": "https://*.example.com"},
294+
network_settings={"my-net": {"NetworkID": "my-net-id", "IPAddress": "10.0.0.12"}},
295+
)
296+
297+
config_data = process_virtual_hosts(bt, {"my-net-id"})
298+
299+
hosts = list(config_data.host_list())
300+
assert len(hosts) == 1
301+
assert hosts[0].hostname == "*.example.com"
302+
231303
def test_parse_host_entry_simple(self):
232304
h, loc, c, extras = _parse_host_entry("example.com")
233305
assert h.hostname == "example.com"

0 commit comments

Comments
 (0)