Skip to content

Commit 7504fca

Browse files
authored
Fix/port normalization overrides host port (#793)
1 parent fcb544e commit 7504fca

File tree

3 files changed

+111
-57
lines changed

3 files changed

+111
-57
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414

1515
## [Unreleased]
1616

17+
- Fixed port normalization to allow host port and bind port to be different.
18+
1719
## [6.1.3] - 2025-07-11
1820

1921
- Updated Starkiller to v3.0.1

empire/server/core/listener_service.py

100644100755
Lines changed: 31 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import copy
22
import hashlib
33
import logging
4+
import re
45
import typing
56
from typing import Any
67

@@ -236,58 +237,43 @@ def _validate_listener_options(
236237
return template_instance, None
237238

238239
@staticmethod
239-
def _normalize_listener_options(instance) -> None: # noqa: PLR0912 PLR0915
240+
def _normalize_listener_options(instance) -> None: # noqa: PLR0912
240241
"""
241242
This is adapted from the old set_listener_option which does some coercions on the http fields.
242243
"""
243244
for option_name, option_meta in instance.options.items():
244245
value = option_meta["Value"]
245246
# parse and auto-set some host parameters
246247
if option_name == "Host":
247-
if not value.startswith("http"):
248-
parts = value.split(":")
249-
# if there's a current ssl cert path set, assume this is https
250-
if ("CertPath" in instance.options) and (
251-
instance.options["CertPath"]["Value"] != ""
248+
host_rexp = r"^(https?)?:?/?/?([^:]+):?(\d+)?$"
249+
matches = re.match(host_rexp, value)
250+
try:
251+
protocol, host, port = matches.groups()
252+
except AttributeError:
253+
log.error(f"Unable to parse Host value: {value}")
254+
protocol = None
255+
host = value
256+
port = None
257+
if not protocol:
258+
if (
259+
"CertPath" in instance.options
260+
and instance.options["CertPath"]["Value"] != ""
252261
):
253262
protocol = "https"
254-
default_port = 443
255263
else:
256264
protocol = "http"
257-
default_port = 80
258-
259-
elif value.startswith("https"):
260-
value = value.split("//")[1]
261-
parts = value.split(":")
262-
protocol = "https"
263-
default_port = 443
264-
265-
elif value.startswith("http"):
266-
value = value.split("//")[1]
267-
parts = value.split(":")
268-
protocol = "http"
269-
default_port = 80
270-
271-
##################################################################################################################################
272-
# Added functionality to Port
273-
# Unsure if this section is needed
274-
if len(parts) != 1 and parts[-1].isdigit():
275-
# if a port is specified with http://host:port
276-
instance.options["Host"]["Value"] = f"{protocol}://{value}"
277-
if instance.options["Port"]["Value"] == "":
278-
instance.options["Port"]["Value"] = parts[-1]
279-
elif instance.options["Port"]["Value"] != "":
280-
# otherwise, check if the port value was manually set
281-
instance.options["Host"]["Value"] = "{}://{}:{}".format(
282-
protocol,
283-
value,
284-
instance.options["Port"]["Value"],
285-
)
286-
else:
287-
# otherwise use default port
288-
instance.options["Host"]["Value"] = f"{protocol}://{value}"
289-
if instance.options["Port"]["Value"] == "":
265+
default_port = 443 if protocol == "https" else 80
266+
try:
267+
int(instance.options["Port"]["Value"])
268+
except ValueError:
269+
if port:
270+
instance.options["Port"]["Value"] = port
271+
else:
290272
instance.options["Port"]["Value"] = default_port
273+
if port:
274+
instance.options["Host"]["Value"] = f"{protocol}://{host}:{port}"
275+
else:
276+
instance.options["Host"]["Value"] = f"{protocol}://{host}"
291277

292278
elif option_name == "CertPath" and value != "":
293279
instance.options[option_name]["Value"] = value
@@ -301,23 +287,11 @@ def _normalize_listener_options(instance) -> None: # noqa: PLR0912 PLR0915
301287
elif option_name == "Port":
302288
instance.options[option_name]["Value"] = value
303289
# Check if Port is set and add it to host
304-
parts = instance.options["Host"]["Value"]
305-
if parts.startswith("https"):
306-
address = parts[8:]
307-
address = "".join(address.split(":")[0])
308-
protocol = "https"
309-
instance.options["Host"]["Value"] = "{}://{}:{}".format(
310-
protocol,
311-
address,
312-
instance.options["Port"]["Value"],
313-
)
314-
elif parts.startswith("http"):
315-
address = parts[7:]
316-
address = "".join(address.split(":")[0])
317-
protocol = "http"
318-
instance.options["Host"]["Value"] = "{}://{}:{}".format(
319-
protocol,
320-
address,
290+
try:
291+
protocol, host, port = instance.options["Host"]["Value"].split(":")
292+
except ValueError:
293+
instance.options["Host"]["Value"] = "{}:{}".format(
294+
instance.options["Host"]["Value"],
321295
instance.options["Port"]["Value"],
322296
)
323297

empire/test/test_listener_api.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,84 @@ def test_create_listener_template_not_found(client, admin_auth_header):
122122
assert response.json()["detail"] == "Listener Template qwerty not found"
123123

124124

125+
def test_create_listener_normalization_adds_protocol_and_default_port(
126+
client, admin_auth_header
127+
):
128+
base_listener = get_base_listener()
129+
base_listener["name"] = "temp123"
130+
base_listener["options"]["Host"] = "localhost"
131+
base_listener["options"]["Port"] = None
132+
133+
response = client.post(
134+
"/api/v2/listeners/", headers=admin_auth_header, json=base_listener
135+
)
136+
assert response.status_code == status.HTTP_201_CREATED
137+
assert response.json()["options"]["Host"] == "http://localhost:80"
138+
assert response.json()["options"]["Port"] == "80"
139+
140+
client.delete(
141+
f"/api/v2/listeners/{response.json()['id']}", headers=admin_auth_header
142+
)
143+
144+
145+
def test_create_listener_normalization_adds_port_to_host(client, admin_auth_header):
146+
base_listener = get_base_listener()
147+
base_listener["name"] = "temp123"
148+
base_listener["options"]["Host"] = "http://localhost"
149+
base_listener["options"]["Port"] = "1234"
150+
151+
response = client.post(
152+
"/api/v2/listeners/", headers=admin_auth_header, json=base_listener
153+
)
154+
assert response.status_code == status.HTTP_201_CREATED
155+
assert response.json()["options"]["Host"] == "http://localhost:1234"
156+
assert response.json()["options"]["Port"] == "1234"
157+
158+
client.delete(
159+
f"/api/v2/listeners/{response.json()['id']}", headers=admin_auth_header
160+
)
161+
162+
163+
def test_create_listener_normalization_preserves_user_defined_ports(
164+
client, admin_auth_header
165+
):
166+
base_listener = get_base_listener()
167+
base_listener["name"] = "temp123"
168+
base_listener["options"]["Host"] = "http://localhost:443"
169+
base_listener["options"]["Port"] = "1234"
170+
171+
response = client.post(
172+
"/api/v2/listeners/", headers=admin_auth_header, json=base_listener
173+
)
174+
assert response.status_code == status.HTTP_201_CREATED
175+
assert response.json()["options"]["Host"] == "http://localhost:443"
176+
assert response.json()["options"]["Port"] == "1234"
177+
178+
client.delete(
179+
f"/api/v2/listeners/{response.json()['id']}", headers=admin_auth_header
180+
)
181+
182+
183+
def test_create_listener_normalization_sets_host_port_as_bind_port(
184+
client, admin_auth_header
185+
):
186+
base_listener = get_base_listener()
187+
base_listener["name"] = "temp123"
188+
base_listener["options"]["Host"] = "http://localhost:443"
189+
base_listener["options"]["Port"] = None
190+
191+
response = client.post(
192+
"/api/v2/listeners/", headers=admin_auth_header, json=base_listener
193+
)
194+
assert response.status_code == status.HTTP_201_CREATED
195+
assert response.json()["options"]["Host"] == "http://localhost:443"
196+
assert response.json()["options"]["Port"] == "443"
197+
198+
client.delete(
199+
f"/api/v2/listeners/{response.json()['id']}", headers=admin_auth_header
200+
)
201+
202+
125203
def test_create_listener(client, admin_auth_header):
126204
base_listener = get_base_listener()
127205
base_listener["name"] = "temp123"

0 commit comments

Comments
 (0)