Skip to content

Commit 37bc386

Browse files
authored
gh-85162: Add HTTPSServer to http.server to serve files over HTTPS (#129607)
The `http.server` module now supports serving over HTTPS using the `http.server.HTTPSServer` class. This functionality is also exposed by the command-line interface (`python -m http.server`) through the `--tls-cert`, `--tls-key` and `--tls-password-file` options.
1 parent 99e9798 commit 37bc386

File tree

5 files changed

+287
-14
lines changed

5 files changed

+287
-14
lines changed

Doc/library/http.server.rst

+72-3
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,49 @@ handler. Code to create and run the server looks like this::
5151
.. versionadded:: 3.7
5252

5353

54-
The :class:`HTTPServer` and :class:`ThreadingHTTPServer` must be given
55-
a *RequestHandlerClass* on instantiation, of which this module
56-
provides three different variants:
54+
.. class:: HTTPSServer(server_address, RequestHandlerClass,\
55+
bind_and_activate=True, *, certfile, keyfile=None,\
56+
password=None, alpn_protocols=None)
57+
58+
Subclass of :class:`HTTPServer` with a wrapped socket using the :mod:`ssl` module.
59+
If the :mod:`ssl` module is not available, instantiating a :class:`!HTTPSServer`
60+
object fails with a :exc:`RuntimeError`.
61+
62+
The *certfile* argument is the path to the SSL certificate chain file,
63+
and the *keyfile* is the path to file containing the private key.
64+
65+
A *password* can be specified for files protected and wrapped with PKCS#8,
66+
but beware that this could possibly expose hardcoded passwords in clear.
67+
68+
.. seealso::
69+
70+
See :meth:`ssl.SSLContext.load_cert_chain` for additional
71+
information on the accepted values for *certfile*, *keyfile*
72+
and *password*.
73+
74+
When specified, the *alpn_protocols* argument must be a sequence of strings
75+
specifying the "Application-Layer Protocol Negotiation" (ALPN) protocols
76+
supported by the server. ALPN allows the server and the client to negotiate
77+
the application protocol during the TLS handshake.
78+
79+
By default, it is set to ``["http/1.1"]``, meaning the server supports HTTP/1.1.
80+
81+
.. versionadded:: next
82+
83+
.. class:: ThreadingHTTPSServer(server_address, RequestHandlerClass,\
84+
bind_and_activate=True, *, certfile, keyfile=None,\
85+
password=None, alpn_protocols=None)
86+
87+
This class is identical to :class:`HTTPSServer` but uses threads to handle
88+
requests by inheriting from :class:`~socketserver.ThreadingMixIn`. This is
89+
analogous to :class:`ThreadingHTTPServer` only using :class:`HTTPSServer`.
90+
91+
.. versionadded:: next
92+
93+
94+
The :class:`HTTPServer`, :class:`ThreadingHTTPServer`, :class:`HTTPSServer` and
95+
:class:`ThreadingHTTPSServer` must be given a *RequestHandlerClass* on
96+
instantiation, of which this module provides three different variants:
5797

5898
.. class:: BaseHTTPRequestHandler(request, client_address, server)
5999

@@ -542,6 +582,35 @@ The following options are accepted:
542582
are not intended for use by untrusted clients and may be vulnerable
543583
to exploitation. Always use within a secure environment.
544584

585+
.. option:: --tls-cert
586+
587+
Specifies a TLS certificate chain for HTTPS connections::
588+
589+
python -m http.server --tls-cert fullchain.pem
590+
591+
.. versionadded:: next
592+
593+
.. option:: --tls-key
594+
595+
Specifies a private key file for HTTPS connections.
596+
597+
This option requires ``--tls-cert`` to be specified.
598+
599+
.. versionadded:: next
600+
601+
.. option:: --tls-password-file
602+
603+
Specifies the password file for password-protected private keys::
604+
605+
python -m http.server \
606+
--tls-cert cert.pem \
607+
--tls-key key.pem \
608+
--tls-password-file password.txt
609+
610+
This option requires `--tls-cert`` to be specified.
611+
612+
.. versionadded:: next
613+
545614

546615
.. _http.server-security:
547616

Doc/whatsnew/3.14.rst

+11
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,17 @@ http
728728
module allow the browser to apply its default dark mode.
729729
(Contributed by Yorik Hansen in :gh:`123430`.)
730730

731+
* The :mod:`http.server` module now supports serving over HTTPS using the
732+
:class:`http.server.HTTPSServer` class. This functionality is exposed by
733+
the command-line interface (``python -m http.server``) through the following
734+
options:
735+
736+
* ``--tls-cert <path>``: Path to the TLS certificate file.
737+
* ``--tls-key <path>``: Optional path to the private key file.
738+
* ``--tls-password-file <path>``: Optional path to the password file for the private key.
739+
740+
(Contributed by Semyon Moroz in :gh:`85162`.)
741+
731742

732743
imaplib
733744
-------

Lib/http/server.py

+82-6
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,10 @@
8383
__version__ = "0.6"
8484

8585
__all__ = [
86-
"HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler",
87-
"SimpleHTTPRequestHandler", "CGIHTTPRequestHandler",
86+
"HTTPServer", "ThreadingHTTPServer",
87+
"HTTPSServer", "ThreadingHTTPSServer",
88+
"BaseHTTPRequestHandler", "SimpleHTTPRequestHandler",
89+
"CGIHTTPRequestHandler",
8890
]
8991

9092
import copy
@@ -149,6 +151,47 @@ class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
149151
daemon_threads = True
150152

151153

154+
class HTTPSServer(HTTPServer):
155+
def __init__(self, server_address, RequestHandlerClass,
156+
bind_and_activate=True, *, certfile, keyfile=None,
157+
password=None, alpn_protocols=None):
158+
try:
159+
import ssl
160+
except ImportError:
161+
raise RuntimeError("SSL module is missing; "
162+
"HTTPS support is unavailable")
163+
164+
self.ssl = ssl
165+
self.certfile = certfile
166+
self.keyfile = keyfile
167+
self.password = password
168+
# Support by default HTTP/1.1
169+
self.alpn_protocols = (
170+
["http/1.1"] if alpn_protocols is None else alpn_protocols
171+
)
172+
173+
super().__init__(server_address,
174+
RequestHandlerClass,
175+
bind_and_activate)
176+
177+
def server_activate(self):
178+
"""Wrap the socket in SSLSocket."""
179+
super().server_activate()
180+
context = self._create_context()
181+
self.socket = context.wrap_socket(self.socket, server_side=True)
182+
183+
def _create_context(self):
184+
"""Create a secure SSL context."""
185+
context = self.ssl.create_default_context(self.ssl.Purpose.CLIENT_AUTH)
186+
context.load_cert_chain(self.certfile, self.keyfile, self.password)
187+
context.set_alpn_protocols(self.alpn_protocols)
188+
return context
189+
190+
191+
class ThreadingHTTPSServer(socketserver.ThreadingMixIn, HTTPSServer):
192+
daemon_threads = True
193+
194+
152195
class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
153196

154197
"""HTTP request handler base class.
@@ -1263,20 +1306,29 @@ def _get_best_family(*address):
12631306

12641307
def test(HandlerClass=BaseHTTPRequestHandler,
12651308
ServerClass=ThreadingHTTPServer,
1266-
protocol="HTTP/1.0", port=8000, bind=None):
1309+
protocol="HTTP/1.0", port=8000, bind=None,
1310+
tls_cert=None, tls_key=None, tls_password=None):
12671311
"""Test the HTTP request handler class.
12681312
12691313
This runs an HTTP server on port 8000 (or the port argument).
12701314
12711315
"""
12721316
ServerClass.address_family, addr = _get_best_family(bind, port)
12731317
HandlerClass.protocol_version = protocol
1274-
with ServerClass(addr, HandlerClass) as httpd:
1318+
1319+
if tls_cert:
1320+
server = ThreadingHTTPSServer(addr, HandlerClass, certfile=tls_cert,
1321+
keyfile=tls_key, password=tls_password)
1322+
else:
1323+
server = ServerClass(addr, HandlerClass)
1324+
1325+
with server as httpd:
12751326
host, port = httpd.socket.getsockname()[:2]
12761327
url_host = f'[{host}]' if ':' in host else host
1328+
protocol = 'HTTPS' if tls_cert else 'HTTP'
12771329
print(
1278-
f"Serving HTTP on {host} port {port} "
1279-
f"(http://{url_host}:{port}/) ..."
1330+
f"Serving {protocol} on {host} port {port} "
1331+
f"({protocol.lower()}://{url_host}:{port}/) ..."
12801332
)
12811333
try:
12821334
httpd.serve_forever()
@@ -1301,10 +1353,31 @@ def test(HandlerClass=BaseHTTPRequestHandler,
13011353
default='HTTP/1.0',
13021354
help='conform to this HTTP version '
13031355
'(default: %(default)s)')
1356+
parser.add_argument('--tls-cert', metavar='PATH',
1357+
help='path to the TLS certificate chain file')
1358+
parser.add_argument('--tls-key', metavar='PATH',
1359+
help='path to the TLS key file')
1360+
parser.add_argument('--tls-password-file', metavar='PATH',
1361+
help='path to the password file for the TLS key')
13041362
parser.add_argument('port', default=8000, type=int, nargs='?',
13051363
help='bind to this port '
13061364
'(default: %(default)s)')
13071365
args = parser.parse_args()
1366+
1367+
if not args.tls_cert and args.tls_key:
1368+
parser.error("--tls-key requires --tls-cert to be set")
1369+
1370+
tls_key_password = None
1371+
if args.tls_password_file:
1372+
if not args.tls_cert:
1373+
parser.error("--tls-password-file requires --tls-cert to be set")
1374+
1375+
try:
1376+
with open(args.tls_password_file, "r", encoding="utf-8") as f:
1377+
tls_key_password = f.read().strip()
1378+
except OSError as e:
1379+
parser.error(f"Failed to read TLS password file: {e}")
1380+
13081381
if args.cgi:
13091382
handler_class = CGIHTTPRequestHandler
13101383
else:
@@ -1330,4 +1403,7 @@ def finish_request(self, request, client_address):
13301403
port=args.port,
13311404
bind=args.bind,
13321405
protocol=args.protocol,
1406+
tls_cert=args.tls_cert,
1407+
tls_key=args.tls_key,
1408+
tls_password=tls_key_password,
13331409
)

0 commit comments

Comments
 (0)