Repository: aio-libs/aiosmtpd
Pull request #43 (2017-03-07 16:16:04)

Crossplatform  Windows compatibility
Removed socket opening code to rely on asyncio more complicated solution.
+ setuid test skip if it is unavailable (on Windows)
+ Windows compatible asyncio-way socket pair creation
After this tests are passed on Windows with python 3.4/3.5.


Patch:
diff --git a/aiosmtpd/controller.py b/aiosmtpd/controller.py
index da1c061..cbcb6c1 100644
--- a/aiosmtpd/controller.py
+++ b/aiosmtpd/controller.py
@@ -6,13 +6,18 @@ import threading
 from aiosmtpd.smtp import SMTP
 from public import public
 
+try:
+    from socket import socketpair
+except ImportError:                                          # pragma: nocover
+    from asyncio.windows_utils import socketpair
+
 
 @public
 class Controller:
-    def __init__(self, handler, loop=None, hostname='::0', port=8025,
+    def __init__(self, handler, loop=None, hostname=None, port=8025,
                  ready_timeout=1.0):
         self.handler = handler
-        self.hostname = '::0' if hostname is None else hostname
+        self.hostname = '::1' if hostname is None else hostname
         self.port = port
         self.loop = asyncio.new_event_loop() if loop is None else loop
         self.server = None
@@ -21,7 +26,7 @@ class Controller:
         self.ready_timeout = os.getenv(
             'AIOSMTPD_CONTROLLER_TIMEOUT', ready_timeout)
         # For exiting the loop.
-        self._rsock, self._wsock = socket.socketpair()
+        self._rsock, self._wsock = socketpair()
         self.loop.add_reader(self._rsock, self._reader)
 
     def _reader(self):
@@ -36,23 +41,15 @@ class Controller:
         """Allow subclasses to customize the handler/server creation."""
         return SMTP(self.handler)
 
-    def make_socket(self):
-        """Allow subclasses to customize socket creation."""
-        sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
-        sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False)
-        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
-        return sock
-
     def _run(self, ready_event):
+        asyncio.set_event_loop(self.loop)
         try:
-            sock = self.make_socket()
-            sock.bind((self.hostname, self.port))
+            self.server = self.loop.run_until_complete(
+                self.loop.create_server(
+                    self.factory, host=self.hostname, port=self.port))
         except socket.error as error:
             self.thread_exception = error
             return
-        asyncio.set_event_loop(self.loop)
-        self.server = self.loop.run_until_complete(
-            self.loop.create_server(self.factory, sock=sock))
         self.loop.call_soon(ready_event.set)
         self.loop.run_forever()
         self.server.close()
diff --git a/aiosmtpd/main.py b/aiosmtpd/main.py
index e623bb2..ceb95e5 100644
--- a/aiosmtpd/main.py
+++ b/aiosmtpd/main.py
@@ -1,7 +1,6 @@
 import os
 import sys
 import signal
-import socket
 import asyncio
 import logging
 
@@ -126,10 +125,10 @@ def main(args=None):
     if args.debug > 2:
         loop.set_debug(enabled=True)
 
-    sock = setup_sock(args.host, args.port)
     log.info('Server listening on %s:%s', args.host, args.port)
 
-    server = loop.run_until_complete(loop.create_server(factory, sock=sock))
+    server = loop.run_until_complete(
+        loop.create_server(factory, host=args.host, port=args.port))
     loop.add_signal_handler(signal.SIGINT, loop.stop)
 
     log.info('Starting asyncio loop')
@@ -143,46 +142,5 @@ def main(args=None):
     loop.close()
 
 
-def setup_sock(host, port):
-    try:
-        # First try to determine the socket type.
-        info = socket.getaddrinfo(
-            host, port,
-            socket.AF_UNSPEC,
-            socket.SOCK_STREAM,
-            0,
-            socket.AI_PASSIVE,
-            )
-    except socket.gaierror:
-        # Infer the type from the host.
-        addr = host, port
-        if ':' in host:
-            addr += 0, 0
-            type_ = socket.AF_INET6
-        else:
-            type_ = socket.AF_INET
-        info_0 = type_, socket.SOCK_STREAM, 0, '', addr
-        info = info_0,
-
-    family, type, proto, canonname, addr = next(iter(info))
-    sock = bind(family, type, proto)
-    log = logging.getLogger('mail.log')
-    log.info('Binding to %s', addr)
-    sock.bind(addr)
-    return sock
-
-
-def bind(family, type, proto):
-    """Create (or recreate) the actual socket object."""
-    sock = socket.socket(family, type, proto)
-    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
-
-    # If listening on IPv6, activate dual-stack.
-    if family == socket.AF_INET6:
-        sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False)
-
-    return sock
-
-
 if __name__ == '__main__':                          # pragma: nocover
     main()


Test patch:
diff --git a/aiosmtpd/tests/test_main.py b/aiosmtpd/tests/test_main.py
index fb4fcda..0eb7357 100644
--- a/aiosmtpd/tests/test_main.py
+++ b/aiosmtpd/tests/test_main.py
@@ -1,23 +1,23 @@
+import os
 import signal
-import socket
 import asyncio
 import logging
 import unittest
 
 from aiosmtpd.handlers import Debugging
-from aiosmtpd.main import main, parseargs, setup_sock
+from aiosmtpd.main import main, parseargs
 from aiosmtpd.smtp import SMTP
 from contextlib import ExitStack
 from functools import partial
 from io import StringIO
-from unittest.mock import call, patch
+from unittest.mock import patch
 
 try:
     import pwd
 except ImportError:
     pwd = None
 
-
+has_setuid = hasattr(os, 'setuid')
 log = logging.getLogger('mail.log')
 
 
@@ -83,6 +83,7 @@ class TestMain(unittest.TestCase):
                 stderr.getvalue(),
                 'Cannot import module "pwd"; try running with -n option.\n')
 
+    @unittest.skipUnless(has_setuid, 'setuid is unvailable')
     def test_n(self):
         with ExitStack() as resources:
             resources.enter_context(patch('aiosmtpd.main.pwd', None))
@@ -95,6 +96,7 @@ class TestMain(unittest.TestCase):
             # triggered in the setuid section.
             self.assertRaises(RuntimeError, main, ('-n',))
 
+    @unittest.skipUnless(has_setuid, 'setuid is unvailable')
     def test_nosetuid(self):
         with ExitStack() as resources:
             resources.enter_context(patch('aiosmtpd.main.pwd', None))
@@ -181,7 +183,7 @@ class TestLoop(unittest.TestCase):
         self.assertEqual(positional[0].keywords, dict(
             data_size_limit=None,
             enable_SMTPUTF8=False))
-        self.assertEqual(list(keywords), ['sock'])
+        self.assertEqual(sorted(keywords), ['host', 'port'])
         # run_until_complete() was called once.  The argument isn't important.
         self.assertTrue(self.run_until_complete.called)
         # add_signal_handler() is called with two arguments.
@@ -307,40 +309,3 @@ class TestParseArgs(unittest.TestCase):
             self.assertEqual(cm.exception.code, 2)
         usage_lines = stderr.getvalue().splitlines()
         self.assertEqual(usage_lines[-1][-24:], 'Invalid port number: foo')
-
-
-class TestSocket(unittest.TestCase):
-    # Usually the socket will be set up from socket.getaddrinfo() but if that
-    # raises socket.gaierror, then it tries to infer the IPv4/IPv6 type from
-    # the host name.
-    def setUp(self):
-        self._resources = ExitStack()
-        self.addCleanup(self._resources.close)
-        self._resources.enter_context(patch('aiosmtpd.main.socket.getaddrinfo',
-                                            side_effect=socket.gaierror))
-
-    def test_ipv4(self):
-        bind = self._resources.enter_context(patch('aiosmtpd.main.bind'))
-        mock_sock = setup_sock('host.example.com', 8025)
-        bind.assert_called_once_with(socket.AF_INET, socket.SOCK_STREAM, 0)
-        mock_sock.bind.assert_called_once_with(('host.example.com', 8025))
-
-    def test_ipv6(self):
-        bind = self._resources.enter_context(patch('aiosmtpd.main.bind'))
-        mock_sock = setup_sock('::1', 8025)
-        bind.assert_called_once_with(socket.AF_INET6, socket.SOCK_STREAM, 0)
-        mock_sock.bind.assert_called_once_with(('::1', 8025, 0, 0))
-
-    def test_bind_ipv4(self):
-        self._resources.enter_context(patch('aiosmtpd.main.socket.socket'))
-        mock_sock = setup_sock('host.example.com', 8025)
-        mock_sock.setsockopt.assert_called_once_with(
-            socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
-
-    def test_bind_ipv6(self):
-        self._resources.enter_context(patch('aiosmtpd.main.socket.socket'))
-        mock_sock = setup_sock('::1', 8025)
-        self.assertEqual(mock_sock.setsockopt.call_args_list, [
-            call(socket.SOL_SOCKET, socket.SO_REUSEADDR, True),
-            call(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False),
-            ])
diff --git a/aiosmtpd/tests/test_smtp.py b/aiosmtpd/tests/test_smtp.py
index c73dc9a..55bc335 100644
--- a/aiosmtpd/tests/test_smtp.py
+++ b/aiosmtpd/tests/test_smtp.py
@@ -37,9 +37,9 @@ class ReceivingHandler:
 
 
 class SizedController(Controller):
-    def __init__(self, handler, size, loop=None, hostname='::0', port=8025):
+    def __init__(self, handler, size):
         self.size = size
-        super().__init__(handler, loop, hostname, port)
+        super().__init__(handler)
 
     def factory(self):
         return Server(self.handler, data_size_limit=self.size)