Add HTTPS support to http.server.HTTPServer
This commit is contained in:
parent
c4862e333a
commit
821b738c54
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue