server.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. #!/usr/bin/env python
  2. """
  3. Dummy server used for unit testing.
  4. """
  5. from __future__ import print_function
  6. import errno
  7. import logging
  8. import os
  9. import random
  10. import string
  11. import sys
  12. import threading
  13. import socket
  14. import warnings
  15. from datetime import datetime
  16. from urllib3.exceptions import HTTPWarning
  17. from tornado.platform.auto import set_close_exec
  18. import tornado.httpserver
  19. import tornado.ioloop
  20. import tornado.web
  21. log = logging.getLogger(__name__)
  22. CERTS_PATH = os.path.join(os.path.dirname(__file__), 'certs')
  23. DEFAULT_CERTS = {
  24. 'certfile': os.path.join(CERTS_PATH, 'server.crt'),
  25. 'keyfile': os.path.join(CERTS_PATH, 'server.key'),
  26. }
  27. NO_SAN_CERTS = {
  28. 'certfile': os.path.join(CERTS_PATH, 'server.no_san.crt'),
  29. 'keyfile': DEFAULT_CERTS['keyfile']
  30. }
  31. IP_SAN_CERTS = {
  32. 'certfile': os.path.join(CERTS_PATH, 'server.ip_san.crt'),
  33. 'keyfile': DEFAULT_CERTS['keyfile']
  34. }
  35. IPV6_ADDR_CERTS = {
  36. 'certfile': os.path.join(CERTS_PATH, 'server.ipv6addr.crt'),
  37. 'keyfile': os.path.join(CERTS_PATH, 'server.ipv6addr.key'),
  38. }
  39. DEFAULT_CA = os.path.join(CERTS_PATH, 'cacert.pem')
  40. DEFAULT_CA_BAD = os.path.join(CERTS_PATH, 'client_bad.pem')
  41. NO_SAN_CA = os.path.join(CERTS_PATH, 'cacert.no_san.pem')
  42. DEFAULT_CA_DIR = os.path.join(CERTS_PATH, 'ca_path_test')
  43. IPV6_ADDR_CA = os.path.join(CERTS_PATH, 'server.ipv6addr.crt')
  44. COMBINED_CERT_AND_KEY = os.path.join(CERTS_PATH, 'server.combined.pem')
  45. def _has_ipv6(host):
  46. """ Returns True if the system can bind an IPv6 address. """
  47. sock = None
  48. has_ipv6 = False
  49. if socket.has_ipv6:
  50. # has_ipv6 returns true if cPython was compiled with IPv6 support.
  51. # It does not tell us if the system has IPv6 support enabled. To
  52. # determine that we must bind to an IPv6 address.
  53. # https://github.com/shazow/urllib3/pull/611
  54. # https://bugs.python.org/issue658327
  55. try:
  56. sock = socket.socket(socket.AF_INET6)
  57. sock.bind((host, 0))
  58. has_ipv6 = True
  59. except:
  60. pass
  61. if sock:
  62. sock.close()
  63. return has_ipv6
  64. # Some systems may have IPv6 support but DNS may not be configured
  65. # properly. We can not count that localhost will resolve to ::1 on all
  66. # systems. See https://github.com/shazow/urllib3/pull/611 and
  67. # https://bugs.python.org/issue18792
  68. HAS_IPV6_AND_DNS = _has_ipv6('localhost')
  69. HAS_IPV6 = _has_ipv6('::1')
  70. # Different types of servers we have:
  71. class NoIPv6Warning(HTTPWarning):
  72. "IPv6 is not available"
  73. pass
  74. class SocketServerThread(threading.Thread):
  75. """
  76. :param socket_handler: Callable which receives a socket argument for one
  77. request.
  78. :param ready_event: Event which gets set when the socket handler is
  79. ready to receive requests.
  80. """
  81. USE_IPV6 = HAS_IPV6_AND_DNS
  82. def __init__(self, socket_handler, host='localhost', port=8081,
  83. ready_event=None):
  84. threading.Thread.__init__(self)
  85. self.daemon = True
  86. self.socket_handler = socket_handler
  87. self.host = host
  88. self.ready_event = ready_event
  89. def _start_server(self):
  90. if self.USE_IPV6:
  91. sock = socket.socket(socket.AF_INET6)
  92. else:
  93. warnings.warn("No IPv6 support. Falling back to IPv4.",
  94. NoIPv6Warning)
  95. sock = socket.socket(socket.AF_INET)
  96. if sys.platform != 'win32':
  97. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  98. sock.bind((self.host, 0))
  99. self.port = sock.getsockname()[1]
  100. # Once listen() returns, the server socket is ready
  101. sock.listen(1)
  102. if self.ready_event:
  103. self.ready_event.set()
  104. self.socket_handler(sock)
  105. sock.close()
  106. def run(self):
  107. self.server = self._start_server()
  108. # FIXME: there is a pull request patching bind_sockets in Tornado directly.
  109. # If it gets merged and released we can drop this and use
  110. # `tornado.netutil.bind_sockets` again.
  111. # https://github.com/facebook/tornado/pull/977
  112. def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128,
  113. flags=None):
  114. """Creates listening sockets bound to the given port and address.
  115. Returns a list of socket objects (multiple sockets are returned if
  116. the given address maps to multiple IP addresses, which is most common
  117. for mixed IPv4 and IPv6 use).
  118. Address may be either an IP address or hostname. If it's a hostname,
  119. the server will listen on all IP addresses associated with the
  120. name. Address may be an empty string or None to listen on all
  121. available interfaces. Family may be set to either `socket.AF_INET`
  122. or `socket.AF_INET6` to restrict to IPv4 or IPv6 addresses, otherwise
  123. both will be used if available.
  124. The ``backlog`` argument has the same meaning as for
  125. `socket.listen() <socket.socket.listen>`.
  126. ``flags`` is a bitmask of AI_* flags to `~socket.getaddrinfo`, like
  127. ``socket.AI_PASSIVE | socket.AI_NUMERICHOST``.
  128. """
  129. sockets = []
  130. if address == "":
  131. address = None
  132. if not HAS_IPV6 and family == socket.AF_UNSPEC:
  133. # Python can be compiled with --disable-ipv6, which causes
  134. # operations on AF_INET6 sockets to fail, but does not
  135. # automatically exclude those results from getaddrinfo
  136. # results.
  137. # http://bugs.python.org/issue16208
  138. family = socket.AF_INET
  139. if flags is None:
  140. flags = socket.AI_PASSIVE
  141. binded_port = None
  142. for res in set(socket.getaddrinfo(address, port, family,
  143. socket.SOCK_STREAM, 0, flags)):
  144. af, socktype, proto, canonname, sockaddr = res
  145. try:
  146. sock = socket.socket(af, socktype, proto)
  147. except socket.error as e:
  148. if e.args[0] == errno.EAFNOSUPPORT:
  149. continue
  150. raise
  151. set_close_exec(sock.fileno())
  152. if os.name != 'nt':
  153. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  154. if af == socket.AF_INET6:
  155. # On linux, ipv6 sockets accept ipv4 too by default,
  156. # but this makes it impossible to bind to both
  157. # 0.0.0.0 in ipv4 and :: in ipv6. On other systems,
  158. # separate sockets *must* be used to listen for both ipv4
  159. # and ipv6. For consistency, always disable ipv4 on our
  160. # ipv6 sockets and use a separate ipv4 socket when needed.
  161. #
  162. # Python 2.x on windows doesn't have IPPROTO_IPV6.
  163. if hasattr(socket, "IPPROTO_IPV6"):
  164. sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
  165. # automatic port allocation with port=None
  166. # should bind on the same port on IPv4 and IPv6
  167. host, requested_port = sockaddr[:2]
  168. if requested_port == 0 and binded_port is not None:
  169. sockaddr = tuple([host, binded_port] + list(sockaddr[2:]))
  170. sock.setblocking(0)
  171. sock.bind(sockaddr)
  172. binded_port = sock.getsockname()[1]
  173. sock.listen(backlog)
  174. sockets.append(sock)
  175. return sockets
  176. def run_tornado_app(app, io_loop, certs, scheme, host):
  177. # We can't use fromtimestamp(0) because of CPython issue 29097, so we'll
  178. # just construct the datetime object directly.
  179. app.last_req = datetime(1970, 1, 1)
  180. if scheme == 'https':
  181. http_server = tornado.httpserver.HTTPServer(app, ssl_options=certs,
  182. io_loop=io_loop)
  183. else:
  184. http_server = tornado.httpserver.HTTPServer(app, io_loop=io_loop)
  185. sockets = bind_sockets(None, address=host)
  186. port = sockets[0].getsockname()[1]
  187. http_server.add_sockets(sockets)
  188. return http_server, port
  189. def run_loop_in_thread(io_loop):
  190. t = threading.Thread(target=io_loop.start)
  191. t.start()
  192. return t
  193. def get_unreachable_address():
  194. while True:
  195. host = ''.join(random.choice(string.ascii_lowercase)
  196. for _ in range(60))
  197. sockaddr = (host, 54321)
  198. # check if we are really "lucky" and hit an actual server
  199. try:
  200. s = socket.create_connection(sockaddr)
  201. except socket.error:
  202. return sockaddr
  203. else:
  204. s.close()
  205. if __name__ == '__main__':
  206. # For debugging dummyserver itself - python -m dummyserver.server
  207. from .testcase import TestingApp
  208. host = '127.0.0.1'
  209. io_loop = tornado.ioloop.IOLoop()
  210. app = tornado.web.Application([(r".*", TestingApp)])
  211. server, port = run_tornado_app(app, io_loop, None,
  212. 'http', host)
  213. server_thread = run_loop_in_thread(io_loop)
  214. print("Listening on http://{host}:{port}".format(host=host, port=port))