Skip to content

bpo-40990: Add HTTPS support to http.server.HTTPServer #20923

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions Doc/library/http.server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +35 to +36
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a separate class, not an argument.


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spelling: purposes

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
Expand Down
41 changes: 36 additions & 5 deletions Lib/http/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
import socket # For gethostbyaddr()
import socketserver
import sys
import ssl
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ssl support is optional, you have to wrap the import in a try/except block.

import time
import urllib.parse
import contextlib
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a cert file, it's a cert chain file.

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use ssl.create_default_context.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default SSL context requires a TLS 1.3 certificate so it doesn't work with ssl_cert.pem:

ssl.SSLError: [SSL: TLSV13_ALERT_CERTIFICATE_REQUIRED] tlsv13 alert certificate required

Is this something that we want to force for a development server?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no such thing as a TLS 1.3 certificate. This is a TLS alert with message certificate required. Looks like the certificate does not have correct extensions to handle TLS 1.3.

You cannot use any file from test directory any way. The directory is often not installed or shipped to reduce disk space usage in containers or on small systems.

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."""
Expand Down Expand Up @@ -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).
Expand All @@ -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()
Expand All @@ -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')
Comment on lines +1296 to +1297
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cert chain

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)
Comment on lines +1304 to +1307
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's common to have the private key and the cert chain in the same file. The key argument should be optional.


if args.cgi:
handler_class = CGIHTTPRequestHandler
else:
Expand All @@ -1296,4 +1326,5 @@ def server_bind(self):
ServerClass=DualStackServer,
port=args.port,
bind=args.bind,
tls=tls
)
39 changes: 36 additions & 3 deletions Lib/test/test_httpservers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import html
import http.client
import urllib.parse
import ssl
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try/except ImportError

import tempfile
import time
import datetime
Expand All @@ -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
Expand All @@ -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()

Expand Down Expand Up @@ -291,6 +299,31 @@ def test_head_via_send_error(self):
self.assertEqual(b'', data)


class BaseHTTPSServerTestCase(BaseTestCase):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skip the test if ssl module is not supported

# 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')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.path.join(os.path.dirname(__file__), 'ssl_cert.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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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.