From 821b738c54da93c5df903092de477040ff6379c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Lapeyre?= Date: Wed, 17 Jun 2020 00:44:12 +0200 Subject: [PATCH] Add HTTPS support to http.server.HTTPServer --- Doc/library/http.server.rst | 16 +++++++- Lib/http/server.py | 41 ++++++++++++++++--- Lib/test/test_httpservers.py | 39 ++++++++++++++++-- .../2020-06-17-00-43-12.bpo-40990.dRTBRp.rst | 4 ++ 4 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-06-17-00-43-12.bpo-40990.dRTBRp.rst diff --git a/Doc/library/http.server.rst b/Doc/library/http.server.rst index 478a5b31475..c3fcf809e27 100644 --- a/Doc/library/http.server.rst +++ b/Doc/library/http.server.rst @@ -32,14 +32,26 @@ handler. Code to create and run the server looks like this:: httpd.serve_forever() -.. class:: HTTPServer(server_address, RequestHandlerClass) +.. class:: HTTPServer(server_address, RequestHandlerClass, \ + bind_and_activate=True, *, tls=None) This class builds on the :class:`~socketserver.TCPServer` class by storing the server address as instance variables named :attr:`server_name` and :attr:`server_port`. The server is accessible by the handler, typically through the handler's :attr:`server` instance variable. -.. class:: ThreadingHTTPServer(server_address, RequestHandlerClass) + HTTPS support can be enabled using the *tls* argument. In this case, it must + be a tuple of two strings, the first being the path to an SSL certificate and + the second the path to its private key. + + .. warning:: + + The HTTPS support is for development and test puposes and must not be used + in production. + + +.. class:: ThreadingHTTPServer(server_address, RequestHandlerClass, \ + bind_and_activate=True, *, tls=None) This class is identical to HTTPServer but uses threads to handle requests by using the :class:`~socketserver.ThreadingMixIn`. This diff --git a/Lib/http/server.py b/Lib/http/server.py index fa204fbc15e..10cb123603c 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -101,6 +101,7 @@ import shutil import socket # For gethostbyaddr() import socketserver import sys +import ssl import time import urllib.parse import contextlib @@ -131,7 +132,23 @@ DEFAULT_ERROR_CONTENT_TYPE = "text/html;charset=utf-8" class HTTPServer(socketserver.TCPServer): - allow_reuse_address = 1 # Seems to make sense in testing environment + allow_reuse_address = True # Seems to make sense in testing environment + + def __init__(self, server_address, RequestHandlerClass, + bind_and_activate=True, *, tls=None): + if tls is None: + self.tls_cert = self.tls_key = None + else: + self.tls_cert, self.tls_key = tls + super().__init__(server_address, RequestHandlerClass, bind_and_activate) + + def server_activate(self): + """Wrap the socket in SSLSocket if TLS is enabled""" + super().server_activate() + if self.tls_cert and self.tls_key: + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_cert_chain(self.tls_cert, self.tls_key) + self.socket = context.wrap_socket(self.socket, server_side=True) def server_bind(self): """Override server_bind to store the server name.""" @@ -1237,7 +1254,7 @@ def _get_best_family(*address): def test(HandlerClass=BaseHTTPRequestHandler, ServerClass=ThreadingHTTPServer, - protocol="HTTP/1.0", port=8000, bind=None): + protocol="HTTP/1.0", port=8000, bind=None, tls=None): """Test the HTTP request handler class. This runs an HTTP server on port 8000 (or the port argument). @@ -1246,12 +1263,13 @@ def test(HandlerClass=BaseHTTPRequestHandler, ServerClass.address_family, addr = _get_best_family(bind, port) HandlerClass.protocol_version = protocol - with ServerClass(addr, HandlerClass) as httpd: + with ServerClass(addr, HandlerClass, tls=tls) as httpd: host, port = httpd.socket.getsockname()[:2] url_host = f'[{host}]' if ':' in host else host + protocol = 'HTTPS' if tls else 'HTTP' print( - f"Serving HTTP on {host} port {port} " - f"(http://{url_host}:{port}/) ..." + f"Serving {protocol} on {host} port {port} " + f"({protocol.lower()}://{url_host}:{port}/) ..." ) try: httpd.serve_forever() @@ -1275,7 +1293,19 @@ if __name__ == '__main__': default=8000, type=int, nargs='?', help='Specify alternate port [default: 8000]') + parser.add_argument('--tls-cert', + help='Specify the path to a TLS certificate') + parser.add_argument('--tls-key', + help='Specify the path to a TLS key') args = parser.parse_args() + + if args.tls_cert is None and args.tls_key is None: + tls = None + elif not args.tls_cert or not args.tls_key: + parser.error('Both --tls-cert and --tls-key must be provided to enable TLS') + else: + tls = (args.tls_cert, args.tls_key) + if args.cgi: handler_class = CGIHTTPRequestHandler else: @@ -1296,4 +1326,5 @@ if __name__ == '__main__': ServerClass=DualStackServer, port=args.port, bind=args.bind, + tls=tls ) diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index 71a0511e53a..c4b49692507 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -21,6 +21,7 @@ import email.utils import html import http.client import urllib.parse +import ssl import tempfile import time import datetime @@ -43,13 +44,18 @@ class NoLogRequestHandler: class TestServerThread(threading.Thread): - def __init__(self, test_object, request_handler): + def __init__(self, test_object, request_handler, tls=None): threading.Thread.__init__(self) self.request_handler = request_handler self.test_object = test_object + self.tls = tls def run(self): - self.server = HTTPServer(('localhost', 0), self.request_handler) + self.server = HTTPServer( + ('localhost', 0), + self.request_handler, + tls=self.tls + ) self.test_object.HOST, self.test_object.PORT = self.server.socket.getsockname() self.test_object.server_started.set() self.test_object = None @@ -64,11 +70,13 @@ class TestServerThread(threading.Thread): class BaseTestCase(unittest.TestCase): + tls = None + def setUp(self): self._threads = threading_helper.threading_setup() os.environ = support.EnvironmentVarGuard() self.server_started = threading.Event() - self.thread = TestServerThread(self, self.request_handler) + self.thread = TestServerThread(self, self.request_handler, self.tls) self.thread.start() self.server_started.wait() @@ -291,6 +299,31 @@ class BaseHTTPServerTestCase(BaseTestCase): self.assertEqual(b'', data) +class BaseHTTPSServerTestCase(BaseTestCase): + # This is a simple test for the HTTPS support. If the GET works we don't + # need to test everything else as it will have been covered by + # BaseHTTPServerTestCase. + + # We have to use the correct path from the folder created by regtest + tls = ('../../Lib/test/ssl_cert.pem', '../../Lib/test/ssl_key.pem') + + class request_handler(NoLogRequestHandler, SimpleHTTPRequestHandler): + pass + + def test_get(self): + response = self.request('/') + self.assertEqual(response.status, HTTPStatus.OK) + + def request(self, uri, method='GET', body=None, headers={}): + self.connection = http.client.HTTPSConnection( + self.HOST, + self.PORT, + context=ssl._create_unverified_context() + ) + self.connection.request(method, uri, body, headers) + return self.connection.getresponse() + + class RequestHandlerLoggingTestCase(BaseTestCase): class request_handler(BaseHTTPRequestHandler): protocol_version = 'HTTP/1.1' diff --git a/Misc/NEWS.d/next/Library/2020-06-17-00-43-12.bpo-40990.dRTBRp.rst b/Misc/NEWS.d/next/Library/2020-06-17-00-43-12.bpo-40990.dRTBRp.rst new file mode 100644 index 00000000000..7971f0203ad --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-06-17-00-43-12.bpo-40990.dRTBRp.rst @@ -0,0 +1,4 @@ +A ``tls`` argument has been added to :class:`http.server.HTTPServer` and +:class:`ThreadingHTTPServer` to enable HTTPS. The ``--tls-cert`` and +``--tls-key`` argument have been added to ``python -m http.server``. Patch +contributed by RĂ©mi Lapeyre.