mirror of
https://github.com/python/cpython.git
synced 2025-08-04 08:59:19 +00:00
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.
This commit is contained in:
parent
99e9798d61
commit
37bc3865c8
5 changed files with 287 additions and 14 deletions
|
@ -51,9 +51,49 @@ handler. Code to create and run the server looks like this::
|
|||
.. versionadded:: 3.7
|
||||
|
||||
|
||||
The :class:`HTTPServer` and :class:`ThreadingHTTPServer` must be given
|
||||
a *RequestHandlerClass* on instantiation, of which this module
|
||||
provides three different variants:
|
||||
.. class:: HTTPSServer(server_address, RequestHandlerClass,\
|
||||
bind_and_activate=True, *, certfile, keyfile=None,\
|
||||
password=None, alpn_protocols=None)
|
||||
|
||||
Subclass of :class:`HTTPServer` with a wrapped socket using the :mod:`ssl` module.
|
||||
If the :mod:`ssl` module is not available, instantiating a :class:`!HTTPSServer`
|
||||
object fails with a :exc:`RuntimeError`.
|
||||
|
||||
The *certfile* argument is the path to the SSL certificate chain file,
|
||||
and the *keyfile* is the path to file containing the private key.
|
||||
|
||||
A *password* can be specified for files protected and wrapped with PKCS#8,
|
||||
but beware that this could possibly expose hardcoded passwords in clear.
|
||||
|
||||
.. seealso::
|
||||
|
||||
See :meth:`ssl.SSLContext.load_cert_chain` for additional
|
||||
information on the accepted values for *certfile*, *keyfile*
|
||||
and *password*.
|
||||
|
||||
When specified, the *alpn_protocols* argument must be a sequence of strings
|
||||
specifying the "Application-Layer Protocol Negotiation" (ALPN) protocols
|
||||
supported by the server. ALPN allows the server and the client to negotiate
|
||||
the application protocol during the TLS handshake.
|
||||
|
||||
By default, it is set to ``["http/1.1"]``, meaning the server supports HTTP/1.1.
|
||||
|
||||
.. versionadded:: next
|
||||
|
||||
.. class:: ThreadingHTTPSServer(server_address, RequestHandlerClass,\
|
||||
bind_and_activate=True, *, certfile, keyfile=None,\
|
||||
password=None, alpn_protocols=None)
|
||||
|
||||
This class is identical to :class:`HTTPSServer` but uses threads to handle
|
||||
requests by inheriting from :class:`~socketserver.ThreadingMixIn`. This is
|
||||
analogous to :class:`ThreadingHTTPServer` only using :class:`HTTPSServer`.
|
||||
|
||||
.. versionadded:: next
|
||||
|
||||
|
||||
The :class:`HTTPServer`, :class:`ThreadingHTTPServer`, :class:`HTTPSServer` and
|
||||
:class:`ThreadingHTTPSServer` must be given a *RequestHandlerClass* on
|
||||
instantiation, of which this module provides three different variants:
|
||||
|
||||
.. class:: BaseHTTPRequestHandler(request, client_address, server)
|
||||
|
||||
|
@ -542,6 +582,35 @@ The following options are accepted:
|
|||
are not intended for use by untrusted clients and may be vulnerable
|
||||
to exploitation. Always use within a secure environment.
|
||||
|
||||
.. option:: --tls-cert
|
||||
|
||||
Specifies a TLS certificate chain for HTTPS connections::
|
||||
|
||||
python -m http.server --tls-cert fullchain.pem
|
||||
|
||||
.. versionadded:: next
|
||||
|
||||
.. option:: --tls-key
|
||||
|
||||
Specifies a private key file for HTTPS connections.
|
||||
|
||||
This option requires ``--tls-cert`` to be specified.
|
||||
|
||||
.. versionadded:: next
|
||||
|
||||
.. option:: --tls-password-file
|
||||
|
||||
Specifies the password file for password-protected private keys::
|
||||
|
||||
python -m http.server \
|
||||
--tls-cert cert.pem \
|
||||
--tls-key key.pem \
|
||||
--tls-password-file password.txt
|
||||
|
||||
This option requires `--tls-cert`` to be specified.
|
||||
|
||||
.. versionadded:: next
|
||||
|
||||
|
||||
.. _http.server-security:
|
||||
|
||||
|
|
|
@ -728,6 +728,17 @@ http
|
|||
module allow the browser to apply its default dark mode.
|
||||
(Contributed by Yorik Hansen in :gh:`123430`.)
|
||||
|
||||
* The :mod:`http.server` module now supports serving over HTTPS using the
|
||||
:class:`http.server.HTTPSServer` class. This functionality is exposed by
|
||||
the command-line interface (``python -m http.server``) through the following
|
||||
options:
|
||||
|
||||
* ``--tls-cert <path>``: Path to the TLS certificate file.
|
||||
* ``--tls-key <path>``: Optional path to the private key file.
|
||||
* ``--tls-password-file <path>``: Optional path to the password file for the private key.
|
||||
|
||||
(Contributed by Semyon Moroz in :gh:`85162`.)
|
||||
|
||||
|
||||
imaplib
|
||||
-------
|
||||
|
|
|
@ -83,8 +83,10 @@ XXX To do:
|
|||
__version__ = "0.6"
|
||||
|
||||
__all__ = [
|
||||
"HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler",
|
||||
"SimpleHTTPRequestHandler", "CGIHTTPRequestHandler",
|
||||
"HTTPServer", "ThreadingHTTPServer",
|
||||
"HTTPSServer", "ThreadingHTTPSServer",
|
||||
"BaseHTTPRequestHandler", "SimpleHTTPRequestHandler",
|
||||
"CGIHTTPRequestHandler",
|
||||
]
|
||||
|
||||
import copy
|
||||
|
@ -149,6 +151,47 @@ class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
|
|||
daemon_threads = True
|
||||
|
||||
|
||||
class HTTPSServer(HTTPServer):
|
||||
def __init__(self, server_address, RequestHandlerClass,
|
||||
bind_and_activate=True, *, certfile, keyfile=None,
|
||||
password=None, alpn_protocols=None):
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
raise RuntimeError("SSL module is missing; "
|
||||
"HTTPS support is unavailable")
|
||||
|
||||
self.ssl = ssl
|
||||
self.certfile = certfile
|
||||
self.keyfile = keyfile
|
||||
self.password = password
|
||||
# Support by default HTTP/1.1
|
||||
self.alpn_protocols = (
|
||||
["http/1.1"] if alpn_protocols is None else alpn_protocols
|
||||
)
|
||||
|
||||
super().__init__(server_address,
|
||||
RequestHandlerClass,
|
||||
bind_and_activate)
|
||||
|
||||
def server_activate(self):
|
||||
"""Wrap the socket in SSLSocket."""
|
||||
super().server_activate()
|
||||
context = self._create_context()
|
||||
self.socket = context.wrap_socket(self.socket, server_side=True)
|
||||
|
||||
def _create_context(self):
|
||||
"""Create a secure SSL context."""
|
||||
context = self.ssl.create_default_context(self.ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain(self.certfile, self.keyfile, self.password)
|
||||
context.set_alpn_protocols(self.alpn_protocols)
|
||||
return context
|
||||
|
||||
|
||||
class ThreadingHTTPSServer(socketserver.ThreadingMixIn, HTTPSServer):
|
||||
daemon_threads = True
|
||||
|
||||
|
||||
class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
|
||||
|
||||
"""HTTP request handler base class.
|
||||
|
@ -1263,7 +1306,8 @@ 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_cert=None, tls_key=None, tls_password=None):
|
||||
"""Test the HTTP request handler class.
|
||||
|
||||
This runs an HTTP server on port 8000 (or the port argument).
|
||||
|
@ -1271,12 +1315,20 @@ def test(HandlerClass=BaseHTTPRequestHandler,
|
|||
"""
|
||||
ServerClass.address_family, addr = _get_best_family(bind, port)
|
||||
HandlerClass.protocol_version = protocol
|
||||
with ServerClass(addr, HandlerClass) as httpd:
|
||||
|
||||
if tls_cert:
|
||||
server = ThreadingHTTPSServer(addr, HandlerClass, certfile=tls_cert,
|
||||
keyfile=tls_key, password=tls_password)
|
||||
else:
|
||||
server = ServerClass(addr, HandlerClass)
|
||||
|
||||
with server as httpd:
|
||||
host, port = httpd.socket.getsockname()[:2]
|
||||
url_host = f'[{host}]' if ':' in host else host
|
||||
protocol = 'HTTPS' if tls_cert 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()
|
||||
|
@ -1301,10 +1353,31 @@ if __name__ == '__main__':
|
|||
default='HTTP/1.0',
|
||||
help='conform to this HTTP version '
|
||||
'(default: %(default)s)')
|
||||
parser.add_argument('--tls-cert', metavar='PATH',
|
||||
help='path to the TLS certificate chain file')
|
||||
parser.add_argument('--tls-key', metavar='PATH',
|
||||
help='path to the TLS key file')
|
||||
parser.add_argument('--tls-password-file', metavar='PATH',
|
||||
help='path to the password file for the TLS key')
|
||||
parser.add_argument('port', default=8000, type=int, nargs='?',
|
||||
help='bind to this port '
|
||||
'(default: %(default)s)')
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.tls_cert and args.tls_key:
|
||||
parser.error("--tls-key requires --tls-cert to be set")
|
||||
|
||||
tls_key_password = None
|
||||
if args.tls_password_file:
|
||||
if not args.tls_cert:
|
||||
parser.error("--tls-password-file requires --tls-cert to be set")
|
||||
|
||||
try:
|
||||
with open(args.tls_password_file, "r", encoding="utf-8") as f:
|
||||
tls_key_password = f.read().strip()
|
||||
except OSError as e:
|
||||
parser.error(f"Failed to read TLS password file: {e}")
|
||||
|
||||
if args.cgi:
|
||||
handler_class = CGIHTTPRequestHandler
|
||||
else:
|
||||
|
@ -1330,4 +1403,7 @@ if __name__ == '__main__':
|
|||
port=args.port,
|
||||
bind=args.bind,
|
||||
protocol=args.protocol,
|
||||
tls_cert=args.tls_cert,
|
||||
tls_key=args.tls_key,
|
||||
tls_password=tls_key_password,
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@ Written by Cody A.W. Somerville <cody-somerville@ubuntu.com>,
|
|||
Josip Dzolonga, and Michael Otteneder for the 2007/08 GHOP contest.
|
||||
"""
|
||||
from collections import OrderedDict
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer, \
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer, HTTPSServer, \
|
||||
SimpleHTTPRequestHandler, CGIHTTPRequestHandler
|
||||
from http import server, HTTPStatus
|
||||
|
||||
|
@ -31,9 +31,14 @@ from io import BytesIO, StringIO
|
|||
import unittest
|
||||
from test import support
|
||||
from test.support import (
|
||||
is_apple, os_helper, requires_subprocess, threading_helper
|
||||
is_apple, import_helper, os_helper, requires_subprocess, threading_helper
|
||||
)
|
||||
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
ssl = None
|
||||
|
||||
support.requires_working_socket(module=True)
|
||||
|
||||
class NoLogRequestHandler:
|
||||
|
@ -45,14 +50,49 @@ class NoLogRequestHandler:
|
|||
return ''
|
||||
|
||||
|
||||
class DummyRequestHandler(NoLogRequestHandler, SimpleHTTPRequestHandler):
|
||||
pass
|
||||
|
||||
|
||||
def create_https_server(
|
||||
certfile,
|
||||
keyfile=None,
|
||||
password=None,
|
||||
*,
|
||||
address=('localhost', 0),
|
||||
request_handler=DummyRequestHandler,
|
||||
):
|
||||
return HTTPSServer(
|
||||
address, request_handler,
|
||||
certfile=certfile, keyfile=keyfile, password=password
|
||||
)
|
||||
|
||||
|
||||
class TestSSLDisabled(unittest.TestCase):
|
||||
def test_https_server_raises_runtime_error(self):
|
||||
with import_helper.isolated_modules():
|
||||
sys.modules['ssl'] = None
|
||||
certfile = certdata_file("keycert.pem")
|
||||
with self.assertRaises(RuntimeError):
|
||||
create_https_server(certfile)
|
||||
|
||||
|
||||
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)
|
||||
if self.tls:
|
||||
certfile, keyfile, password = self.tls
|
||||
self.server = create_https_server(
|
||||
certfile, keyfile, password,
|
||||
request_handler=self.request_handler,
|
||||
)
|
||||
else:
|
||||
self.server = HTTPServer(('localhost', 0), self.request_handler)
|
||||
self.test_object.HOST, self.test_object.PORT = self.server.socket.getsockname()
|
||||
self.test_object.server_started.set()
|
||||
self.test_object = None
|
||||
|
@ -67,11 +107,15 @@ class TestServerThread(threading.Thread):
|
|||
|
||||
|
||||
class BaseTestCase(unittest.TestCase):
|
||||
|
||||
# Optional tuple (certfile, keyfile, password) to use for HTTPS servers.
|
||||
tls = None
|
||||
|
||||
def setUp(self):
|
||||
self._threads = threading_helper.threading_setup()
|
||||
os.environ = os_helper.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()
|
||||
|
||||
|
@ -315,6 +359,74 @@ class BaseHTTPServerTestCase(BaseTestCase):
|
|||
self.assertEqual(b'', data)
|
||||
|
||||
|
||||
def certdata_file(*path):
|
||||
return os.path.join(os.path.dirname(__file__), "certdata", *path)
|
||||
|
||||
|
||||
@unittest.skipIf(ssl is None, "requires ssl")
|
||||
class BaseHTTPSServerTestCase(BaseTestCase):
|
||||
CERTFILE = certdata_file("keycert.pem")
|
||||
ONLYCERT = certdata_file("ssl_cert.pem")
|
||||
ONLYKEY = certdata_file("ssl_key.pem")
|
||||
CERTFILE_PROTECTED = certdata_file("keycert.passwd.pem")
|
||||
ONLYKEY_PROTECTED = certdata_file("ssl_key.passwd.pem")
|
||||
EMPTYCERT = certdata_file("nullcert.pem")
|
||||
BADCERT = certdata_file("badcert.pem")
|
||||
KEY_PASSWORD = "somepass"
|
||||
BADPASSWORD = "badpass"
|
||||
|
||||
tls = (ONLYCERT, ONLYKEY, None) # values by default
|
||||
|
||||
request_handler = DummyRequestHandler
|
||||
|
||||
def test_get(self):
|
||||
response = self.request('/')
|
||||
self.assertEqual(response.status, HTTPStatus.OK)
|
||||
|
||||
def request(self, uri, method='GET', body=None, headers={}):
|
||||
context = ssl._create_unverified_context()
|
||||
self.connection = http.client.HTTPSConnection(
|
||||
self.HOST, self.PORT, context=context
|
||||
)
|
||||
self.connection.request(method, uri, body, headers)
|
||||
return self.connection.getresponse()
|
||||
|
||||
def test_valid_certdata(self):
|
||||
valid_certdata= [
|
||||
(self.CERTFILE, None, None),
|
||||
(self.CERTFILE, self.CERTFILE, None),
|
||||
(self.CERTFILE_PROTECTED, None, self.KEY_PASSWORD),
|
||||
(self.ONLYCERT, self.ONLYKEY_PROTECTED, self.KEY_PASSWORD),
|
||||
]
|
||||
for certfile, keyfile, password in valid_certdata:
|
||||
with self.subTest(
|
||||
certfile=certfile, keyfile=keyfile, password=password
|
||||
):
|
||||
server = create_https_server(certfile, keyfile, password)
|
||||
self.assertIsInstance(server, HTTPSServer)
|
||||
server.server_close()
|
||||
|
||||
def test_invalid_certdata(self):
|
||||
invalid_certdata = [
|
||||
(self.BADCERT, None, None),
|
||||
(self.EMPTYCERT, None, None),
|
||||
(self.ONLYCERT, None, None),
|
||||
(self.ONLYKEY, None, None),
|
||||
(self.ONLYKEY, self.ONLYCERT, None),
|
||||
(self.CERTFILE_PROTECTED, None, self.BADPASSWORD),
|
||||
# TODO: test the next case and add same case to test_ssl (We
|
||||
# specify a cert and a password-protected file, but no password):
|
||||
# (self.CERTFILE_PROTECTED, None, None),
|
||||
# see issue #132102
|
||||
]
|
||||
for certfile, keyfile, password in invalid_certdata:
|
||||
with self.subTest(
|
||||
certfile=certfile, keyfile=keyfile, password=password
|
||||
):
|
||||
with self.assertRaises(ssl.SSLError):
|
||||
create_https_server(certfile, keyfile, password)
|
||||
|
||||
|
||||
class RequestHandlerLoggingTestCase(BaseTestCase):
|
||||
class request_handler(BaseHTTPRequestHandler):
|
||||
protocol_version = 'HTTP/1.1'
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
The :mod:`http.server` module now includes built-in support for HTTPS
|
||||
servers exposed by :class:`http.server.HTTPSServer`. This functionality
|
||||
is exposed by the command-line interface (``python -m http.server``) through
|
||||
the ``--tls-cert``, ``--tls-key`` and ``--tls-password-file`` options.
|
||||
Patch by Semyon Moroz.
|
Loading…
Add table
Add a link
Reference in a new issue