From dec2ad8bead671b4790f2998884816867ba8a8db Mon Sep 17 00:00:00 2001 From: Unrud Date: Thu, 6 Sep 2018 09:12:53 +0200 Subject: [PATCH] Add support for IPv6 hostnames --- radicale/server.py | 38 ++++++++++++++++++++++++++++++----- radicale/tests/test_server.py | 25 ++++++++++++++++++++++- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/radicale/server.py b/radicale/server.py index d310462..0aa5609 100644 --- a/radicale/server.py +++ b/radicale/server.py @@ -47,6 +47,30 @@ if os.name == "posix": else: ParallelizationMixIn = socketserver.ThreadingMixIn +HAS_IPV6 = socket.has_ipv6 +if hasattr(socket, "EAI_NONAME"): + EAI_NONAME = socket.EAI_NONAME +else: + HAS_IPV6 = False +if hasattr(socket, "EAI_ADDRFAMILY"): + EAI_ADDRFAMILY = socket.EAI_ADDRFAMILY +elif os.name == "nt": + EAI_ADDRFAMILY = None +else: + HAS_IPV6 = False +if hasattr(socket, "IPPROTO_IPV6"): + IPPROTO_IPV6 = socket.IPPROTO_IPV6 +elif os.name == "nt": + IPPROTO_IPV6 = 41 +else: + HAS_IPV6 = False +if hasattr(socket, "IPV6_V6ONLY"): + IPV6_V6ONLY = socket.IPV6_V6ONLY +elif os.name == "nt": + IPV6_V6ONLY = 27 +else: + HAS_IPV6 = False + class ParallelHTTPServer(ParallelizationMixIn, wsgiref.simple_server.WSGIServer): @@ -74,14 +98,18 @@ class ParallelHTTPServer(ParallelizationMixIn, self.server_port = port self.setup_environ() return - ipv6 = ":" in self.server_address[0] - if ipv6 and self.address_family == socket.AF_INET: + try: + super().server_bind() + except socket.gaierror as e: + if (not HAS_IPV6 or self.address_family != socket.AF_INET or + e.errno not in (EAI_NONAME, EAI_ADDRFAMILY)): + raise + # Try again with IPv6 self.address_family = socket.AF_INET6 self.socket = socket.socket(self.address_family, self.socket_type) - if ipv6: # Only allow IPv6 connections to the IPv6 socket - self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) - super().server_bind() + self.socket.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1) + super().server_bind() def get_request(self): # Set timeout for client diff --git a/radicale/tests/test_server.py b/radicale/tests/test_server.py index 9b413ca..d72958e 100644 --- a/radicale/tests/test_server.py +++ b/radicale/tests/test_server.py @@ -19,12 +19,14 @@ Test the internal server. """ +import os import shutil import socket import ssl import tempfile import threading import time +import warnings from urllib import request from urllib.error import HTTPError, URLError @@ -32,6 +34,8 @@ from radicale import config, server from .helpers import get_file_path +import pytest # isort:skip + class DisabledRedirectHandler(request.HTTPRedirectHandler): def http_error_302(self, req, fp, code, msg, headers): @@ -66,7 +70,10 @@ class TestBaseServerRequests: def teardown(self): self.shutdown_socket.sendall(b" ") - self.thread.join() + try: + self.thread.join() + except RuntimeError: # Thread never started + pass shutil.rmtree(self.colpath) def request(self, method, path, data=None, **headers): @@ -100,3 +107,19 @@ class TestBaseServerRequests: self.thread.start() status, _, _ = self.request("GET", "/") assert status == 302 + + def test_ipv6(self): + if not server.HAS_IPV6: + pytest.skip("IPv6 not support") + if os.name == "nt" and os.environ.get("WINE_PYTHON"): + warnings.warn("WORKAROUND: incomplete errno conversion in WINE") + server.EAI_ADDRFAMILY = -9 + with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock: + sock.setsockopt(server.IPPROTO_IPV6, server.IPV6_V6ONLY, 1) + # Find available port + sock.bind(("localhost", 0)) + self.sockname = sock.getsockname()[:2] + self.configuration["server"]["hosts"] = "[%s]:%d" % self.sockname + self.thread.start() + status, _, _ = self.request("GET", "/") + assert status == 302