Add HTTPS support to http.server.HTTPServer

This commit is contained in:
Rémi Lapeyre 2020-06-17 00:44:12 +02:00
parent c4862e333a
commit 821b738c54
4 changed files with 90 additions and 10 deletions

View File

@ -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

View File

@ -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
)

View File

@ -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'

View File

@ -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.