diff options
Diffstat (limited to 'dummyserver')
-rwxr-xr-x | dummyserver/server.py | 159 | ||||
-rw-r--r-- | dummyserver/testcase.py | 63 |
2 files changed, 139 insertions, 83 deletions
diff --git a/dummyserver/server.py b/dummyserver/server.py index f4f98a4..22de456 100755 --- a/dummyserver/server.py +++ b/dummyserver/server.py @@ -5,21 +5,21 @@ Dummy server used for unit testing. """ from __future__ import print_function +import errno import logging import os +import random +import string import sys import threading import socket -from tornado import netutil +from tornado.platform.auto import set_close_exec import tornado.wsgi import tornado.httpserver import tornado.ioloop import tornado.web -from dummyserver.handlers import TestingApp -from dummyserver.proxy import ProxyHandler - log = logging.getLogger(__name__) @@ -51,7 +51,7 @@ class SocketServerThread(threading.Thread): self.ready_event = ready_event def _start_server(self): - sock = socket.socket() + sock = socket.socket(socket.AF_INET6) if sys.platform != 'win32': sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((self.host, 0)) @@ -70,59 +70,112 @@ class SocketServerThread(threading.Thread): self.server = self._start_server() -class TornadoServerThread(threading.Thread): - app = tornado.wsgi.WSGIContainer(TestingApp()) +# FIXME: there is a pull request patching bind_sockets in Tornado directly. +# If it gets merged and released we can drop this and use +# `tornado.netutil.bind_sockets` again. +# https://github.com/facebook/tornado/pull/977 - def __init__(self, host='localhost', scheme='http', certs=None, - ready_event=None): - threading.Thread.__init__(self) +def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, + flags=None): + """Creates listening sockets bound to the given port and address. - self.host = host - self.scheme = scheme - self.certs = certs - self.ready_event = ready_event + Returns a list of socket objects (multiple sockets are returned if + the given address maps to multiple IP addresses, which is most common + for mixed IPv4 and IPv6 use). - def _start_server(self): - if self.scheme == 'https': - http_server = tornado.httpserver.HTTPServer(self.app, - ssl_options=self.certs) - else: - http_server = tornado.httpserver.HTTPServer(self.app) + Address may be either an IP address or hostname. If it's a hostname, + the server will listen on all IP addresses associated with the + name. Address may be an empty string or None to listen on all + available interfaces. Family may be set to either `socket.AF_INET` + or `socket.AF_INET6` to restrict to IPv4 or IPv6 addresses, otherwise + both will be used if available. - family = socket.AF_INET6 if ':' in self.host else socket.AF_INET - sock, = netutil.bind_sockets(None, address=self.host, family=family) - self.port = sock.getsockname()[1] - http_server.add_sockets([sock]) - return http_server + The ``backlog`` argument has the same meaning as for + `socket.listen() <socket.socket.listen>`. - def run(self): - self.ioloop = tornado.ioloop.IOLoop.instance() - self.server = self._start_server() - if self.ready_event: - self.ready_event.set() - self.ioloop.start() - - def stop(self): - self.ioloop.add_callback(self.server.stop) - self.ioloop.add_callback(self.ioloop.stop) - - -class ProxyServerThread(TornadoServerThread): - app = tornado.web.Application([(r'.*', ProxyHandler)]) - - -if __name__ == '__main__': - log.setLevel(logging.DEBUG) - log.addHandler(logging.StreamHandler(sys.stderr)) - - from urllib3 import get_host + ``flags`` is a bitmask of AI_* flags to `~socket.getaddrinfo`, like + ``socket.AI_PASSIVE | socket.AI_NUMERICHOST``. + """ + sockets = [] + if address == "": + address = None + if not socket.has_ipv6 and family == socket.AF_UNSPEC: + # Python can be compiled with --disable-ipv6, which causes + # operations on AF_INET6 sockets to fail, but does not + # automatically exclude those results from getaddrinfo + # results. + # http://bugs.python.org/issue16208 + family = socket.AF_INET + if flags is None: + flags = socket.AI_PASSIVE + binded_port = None + for res in set(socket.getaddrinfo(address, port, family, + socket.SOCK_STREAM, 0, flags)): + af, socktype, proto, canonname, sockaddr = res + try: + sock = socket.socket(af, socktype, proto) + except socket.error as e: + if e.args[0] == errno.EAFNOSUPPORT: + continue + raise + set_close_exec(sock.fileno()) + if os.name != 'nt': + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if af == socket.AF_INET6: + # On linux, ipv6 sockets accept ipv4 too by default, + # but this makes it impossible to bind to both + # 0.0.0.0 in ipv4 and :: in ipv6. On other systems, + # separate sockets *must* be used to listen for both ipv4 + # and ipv6. For consistency, always disable ipv4 on our + # ipv6 sockets and use a separate ipv4 socket when needed. + # + # Python 2.x on windows doesn't have IPPROTO_IPV6. + if hasattr(socket, "IPPROTO_IPV6"): + sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) + + # automatic port allocation with port=None + # should bind on the same port on IPv4 and IPv6 + host, requested_port = sockaddr[:2] + if requested_port == 0 and binded_port is not None: + sockaddr = tuple([host, binded_port] + list(sockaddr[2:])) + + sock.setblocking(0) + sock.bind(sockaddr) + binded_port = sock.getsockname()[1] + sock.listen(backlog) + sockets.append(sock) + return sockets + + +def run_tornado_app(app, io_loop, certs, scheme, host): + if scheme == 'https': + http_server = tornado.httpserver.HTTPServer(app, ssl_options=certs, + io_loop=io_loop) + else: + http_server = tornado.httpserver.HTTPServer(app, io_loop=io_loop) + + sockets = bind_sockets(None, address=host) + port = sockets[0].getsockname()[1] + http_server.add_sockets(sockets) + return http_server, port + + +def run_loop_in_thread(io_loop): + t = threading.Thread(target=io_loop.start) + t.start() + return t - url = "http://localhost:8081" - if len(sys.argv) > 1: - url = sys.argv[1] - print("Starting WSGI server at: %s" % url) +def get_unreachable_address(): + while True: + host = ''.join(random.choice(string.ascii_lowercase) + for _ in range(60)) + sockaddr = (host, 54321) - scheme, host, port = get_host(url) - t = TornadoServerThread(scheme=scheme, host=host, port=port) - t.start() + # check if we are really "lucky" and hit an actual server + try: + s = socket.create_connection(sockaddr) + except socket.error: + return sockaddr + else: + s.close() diff --git a/dummyserver/testcase.py b/dummyserver/testcase.py index a2a1da1..35769ef 100644 --- a/dummyserver/testcase.py +++ b/dummyserver/testcase.py @@ -2,14 +2,17 @@ import unittest import socket import threading from nose.plugins.skip import SkipTest +from tornado import ioloop, web, wsgi from dummyserver.server import ( - TornadoServerThread, SocketServerThread, + SocketServerThread, + run_tornado_app, + run_loop_in_thread, DEFAULT_CERTS, - ProxyServerThread, ) +from dummyserver.handlers import TestingApp +from dummyserver.proxy import ProxyHandler -has_ipv6 = hasattr(socket, 'has_ipv6') class SocketDummyServerTestCase(unittest.TestCase): @@ -33,7 +36,7 @@ class SocketDummyServerTestCase(unittest.TestCase): @classmethod def tearDownClass(cls): if hasattr(cls, 'server_thread'): - cls.server_thread.join() + cls.server_thread.join(0.1) class HTTPDummyServerTestCase(unittest.TestCase): @@ -44,18 +47,16 @@ class HTTPDummyServerTestCase(unittest.TestCase): @classmethod def _start_server(cls): - ready_event = threading.Event() - cls.server_thread = TornadoServerThread(host=cls.host, - scheme=cls.scheme, - certs=cls.certs, - ready_event=ready_event) - cls.server_thread.start() - ready_event.wait() - cls.port = cls.server_thread.port + cls.io_loop = ioloop.IOLoop() + app = wsgi.WSGIContainer(TestingApp()) + cls.server, cls.port = run_tornado_app(app, cls.io_loop, cls.certs, + cls.scheme, cls.host) + cls.server_thread = run_loop_in_thread(cls.io_loop) @classmethod def _stop_server(cls): - cls.server_thread.stop() + cls.io_loop.add_callback(cls.server.stop) + cls.io_loop.add_callback(cls.io_loop.stop) cls.server_thread.join() @classmethod @@ -87,27 +88,29 @@ class HTTPDummyProxyTestCase(unittest.TestCase): @classmethod def setUpClass(cls): - cls.http_thread = TornadoServerThread(host=cls.http_host, - scheme='http') - cls.http_thread._start_server() - cls.http_port = cls.http_thread.port + cls.io_loop = ioloop.IOLoop() - cls.https_thread = TornadoServerThread( - host=cls.https_host, scheme='https', certs=cls.https_certs) - cls.https_thread._start_server() - cls.https_port = cls.https_thread.port + app = wsgi.WSGIContainer(TestingApp()) + cls.http_server, cls.http_port = run_tornado_app( + app, cls.io_loop, None, 'http', cls.http_host) - ready_event = threading.Event() - cls.proxy_thread = ProxyServerThread( - host=cls.proxy_host, ready_event=ready_event) - cls.proxy_thread.start() - ready_event.wait() - cls.proxy_port = cls.proxy_thread.port + app = wsgi.WSGIContainer(TestingApp()) + cls.https_server, cls.https_port = run_tornado_app( + app, cls.io_loop, cls.https_certs, 'https', cls.http_host) + + app = web.Application([(r'.*', ProxyHandler)]) + cls.proxy_server, cls.proxy_port = run_tornado_app( + app, cls.io_loop, None, 'http', cls.proxy_host) + + cls.server_thread = run_loop_in_thread(cls.io_loop) @classmethod def tearDownClass(cls): - cls.proxy_thread.stop() - cls.proxy_thread.join() + cls.io_loop.add_callback(cls.http_server.stop) + cls.io_loop.add_callback(cls.https_server.stop) + cls.io_loop.add_callback(cls.proxy_server.stop) + cls.io_loop.add_callback(cls.io_loop.stop) + cls.server_thread.join() class IPv6HTTPDummyServerTestCase(HTTPDummyServerTestCase): @@ -115,7 +118,7 @@ class IPv6HTTPDummyServerTestCase(HTTPDummyServerTestCase): @classmethod def setUpClass(cls): - if not has_ipv6: + if not socket.has_ipv6: raise SkipTest('IPv6 not available') else: super(IPv6HTTPDummyServerTestCase, cls).setUpClass() |