Skip to content

Commit 4181f15

Browse files
committed
Fix bind unspecified address controller will raise TimeoutError
The controller throws a TimeoutError when binding a unspecified address (e.g. 0.0.0.0)
1 parent 215b854 commit 4181f15

File tree

3 files changed

+57
-4
lines changed

3 files changed

+57
-4
lines changed

aiosmtpd/controller.py

+28-3
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,24 @@ def get_localhost() -> Literal["::1", "127.0.0.1"]:
7979
raise
8080

8181

82+
@public
83+
def is_unspecified_address(address: str) -> bool:
84+
unspecified_address_list = ["", "0.0.0.0", "::"]
85+
return address in unspecified_address_list
86+
87+
88+
@public
89+
def convert_unspecified_address_to_localhost(
90+
address: str
91+
) -> Literal["::1", "127.0.0.1"]:
92+
address_dict = {
93+
"": get_localhost(),
94+
"0.0.0.0": "127.0.0.1",
95+
"::": "::1",
96+
}
97+
return address_dict.get(address, get_localhost())
98+
99+
82100
class _FakeServer(asyncio.StreamReaderProtocol):
83101
"""
84102
Returned by _factory_invoker() in lieu of an SMTP instance in case
@@ -396,8 +414,15 @@ def __init__(
396414
loop,
397415
**kwargs,
398416
)
399-
self._localhost = get_localhost()
400-
self.hostname = self._localhost if hostname is None else hostname
417+
if is_unspecified_address(hostname) is True:
418+
self._localhost = convert_unspecified_address_to_localhost(hostname)
419+
self.hostname = hostname
420+
elif hostname is None:
421+
self._localhost = get_localhost()
422+
self.hostname = self._localhost
423+
else:
424+
self._localhost = get_localhost()
425+
self.hostname = hostname
401426
self.port = port
402427

403428
def _create_server(self) -> Coroutine:
@@ -421,7 +446,7 @@ def _trigger_server(self):
421446
"""
422447
# At this point, if self.hostname is Falsy, it most likely is "" (bind to all
423448
# addresses). In such case, it should be safe to connect to localhost)
424-
hostname = self.hostname or self._localhost
449+
hostname = self._localhost if is_unspecified_address(self.hostname) is True else self.hostname
425450
with ExitStack() as stk:
426451
s = stk.enter_context(create_connection((hostname, self.port), 1.0))
427452
if self.ssl_context:

aiosmtpd/docs/NEWS.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Fixed/Improved
1414
--------------
1515
* All Controllers now have more rationale design, as they are now composited from a Base + a Mixin
1616
* A whole bunch of annotations
17-
17+
* Fix bind unspecified address controller will raise TimeoutError
1818

1919
1.4.2 (2021-03-08)
2020
=====================

aiosmtpd/tests/test_server.py

+28
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
UnixSocketUnthreadedController,
2929
_FakeServer,
3030
get_localhost,
31+
is_unspecified_address,
32+
convert_unspecified_address_to_localhost,
3133
)
3234
from aiosmtpd.handlers import Sink
3335
from aiosmtpd.smtp import SMTP as Server
@@ -293,6 +295,20 @@ def test_hostname_none(self):
293295
finally:
294296
cont.stop()
295297

298+
def test_hostname_unspecified_ipv4(self):
299+
cont = Controller(Sink(), hostname="0.0.0.0")
300+
try:
301+
cont.start()
302+
finally:
303+
cont.stop()
304+
305+
def test_hostname_unspecified_ipv6(self):
306+
cont = Controller(Sink(), hostname="::")
307+
try:
308+
cont.start()
309+
finally:
310+
cont.stop()
311+
296312
def test_testconn_raises(self, mocker: MockFixture):
297313
mocker.patch("socket.socket.recv", side_effect=RuntimeError("MockError"))
298314
cont = Controller(Sink(), hostname="")
@@ -347,6 +363,18 @@ def test_getlocalhost_error(self, mocker):
347363
assert exc.value.errno == errno.EFAULT
348364
mock_makesock.assert_called_with(socket.AF_INET6, socket.SOCK_STREAM)
349365

366+
def test_is_unspecified_address(self):
367+
assert is_unspecified_address("127.0.0.1") is False
368+
assert is_unspecified_address("0.0.0.0") is True
369+
assert is_unspecified_address("::") is True
370+
assert is_unspecified_address("") is True
371+
372+
def test_convert_unspecified_address_to_localhost(self):
373+
assert convert_unspecified_address_to_localhost("") == get_localhost()
374+
assert convert_unspecified_address_to_localhost("0.0.0.0") == "127.0.0.1"
375+
assert convert_unspecified_address_to_localhost("::") == "::1"
376+
assert convert_unspecified_address_to_localhost("0.0.0.1") == get_localhost()
377+
350378
def test_stop_default(self):
351379
controller = Controller(Sink())
352380
with pytest.raises(AssertionError, match="SMTP daemon not running"):

0 commit comments

Comments
 (0)