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

Lines changed: 72 additions & 3 deletions
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

Lines changed: 11 additions & 0 deletions
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

Lines changed: 82 additions & 6 deletions
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)
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