diff --git a/Doc/library/http.server.rst b/Doc/library/http.server.rst index 478a5b31475cfd..c3fcf809e273bf 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 fa204fbc15e3d7..10cb123603c8ba 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -101,6 +101,7 @@ import socket # For gethostbyaddr() import socketserver import sys +import ssl import time import urllib.parse import contextlib @@ -131,7 +132,23 @@ 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 @@ def test(HandlerClass=BaseHTTPRequestHandler, 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 @@ def server_bind(self): 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 71a0511e53a72f..c4b49692507424 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -21,6 +21,7 @@ import html import http.client import urllib.parse +import ssl import tempfile import time import datetime @@ -43,13 +44,18 @@ def read(self, n=None): 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 @@ def stop(self): 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 @@ def test_head_via_send_error(self): 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 00000000000000..7971f0203adccc --- /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. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy