mirror of
https://github.com/python/cpython.git
synced 2025-09-25 09:50:37 +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
|
.. versionadded:: 3.7
|
||||||
|
|
||||||
|
|
||||||
The :class:`HTTPServer` and :class:`ThreadingHTTPServer` must be given
|
.. class:: HTTPSServer(server_address, RequestHandlerClass,\
|
||||||
a *RequestHandlerClass* on instantiation, of which this module
|
bind_and_activate=True, *, certfile, keyfile=None,\
|
||||||
provides three different variants:
|
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)
|
.. 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
|
are not intended for use by untrusted clients and may be vulnerable
|
||||||
to exploitation. Always use within a secure environment.
|
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:
|
.. _http.server-security:
|
||||||
|
|
||||||
|
|
|
@ -728,6 +728,17 @@ http
|
||||||
module allow the browser to apply its default dark mode.
|
module allow the browser to apply its default dark mode.
|
||||||
(Contributed by Yorik Hansen in :gh:`123430`.)
|
(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
|
imaplib
|
||||||
-------
|
-------
|
||||||
|
|
|
@ -83,8 +83,10 @@ XXX To do:
|
||||||
__version__ = "0.6"
|
__version__ = "0.6"
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler",
|
"HTTPServer", "ThreadingHTTPServer",
|
||||||
"SimpleHTTPRequestHandler", "CGIHTTPRequestHandler",
|
"HTTPSServer", "ThreadingHTTPSServer",
|
||||||
|
"BaseHTTPRequestHandler", "SimpleHTTPRequestHandler",
|
||||||
|
"CGIHTTPRequestHandler",
|
||||||
]
|
]
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
@ -149,6 +151,47 @@ class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
|
||||||
daemon_threads = True
|
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):
|
class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
|
||||||
|
|
||||||
"""HTTP request handler base class.
|
"""HTTP request handler base class.
|
||||||
|
@ -1263,7 +1306,8 @@ def _get_best_family(*address):
|
||||||
|
|
||||||
def test(HandlerClass=BaseHTTPRequestHandler,
|
def test(HandlerClass=BaseHTTPRequestHandler,
|
||||||
ServerClass=ThreadingHTTPServer,
|
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.
|
"""Test the HTTP request handler class.
|
||||||
|
|
||||||
This runs an HTTP server on port 8000 (or the port argument).
|
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)
|
ServerClass.address_family, addr = _get_best_family(bind, port)
|
||||||
HandlerClass.protocol_version = protocol
|
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]
|
host, port = httpd.socket.getsockname()[:2]
|
||||||
url_host = f'[{host}]' if ':' in host else host
|
url_host = f'[{host}]' if ':' in host else host
|
||||||
|
protocol = 'HTTPS' if tls_cert else 'HTTP'
|
||||||
print(
|
print(
|
||||||
f"Serving HTTP on {host} port {port} "
|
f"Serving {protocol} on {host} port {port} "
|
||||||
f"(http://{url_host}:{port}/) ..."
|
f"({protocol.lower()}://{url_host}:{port}/) ..."
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
|
@ -1301,10 +1353,31 @@ if __name__ == '__main__':
|
||||||
default='HTTP/1.0',
|
default='HTTP/1.0',
|
||||||
help='conform to this HTTP version '
|
help='conform to this HTTP version '
|
||||||
'(default: %(default)s)')
|
'(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='?',
|
parser.add_argument('port', default=8000, type=int, nargs='?',
|
||||||
help='bind to this port '
|
help='bind to this port '
|
||||||
'(default: %(default)s)')
|
'(default: %(default)s)')
|
||||||
args = parser.parse_args()
|
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:
|
if args.cgi:
|
||||||
handler_class = CGIHTTPRequestHandler
|
handler_class = CGIHTTPRequestHandler
|
||||||
else:
|
else:
|
||||||
|
@ -1330,4 +1403,7 @@ if __name__ == '__main__':
|
||||||
port=args.port,
|
port=args.port,
|
||||||
bind=args.bind,
|
bind=args.bind,
|
||||||
protocol=args.protocol,
|
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.
|
Josip Dzolonga, and Michael Otteneder for the 2007/08 GHOP contest.
|
||||||
"""
|
"""
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer, \
|
from http.server import BaseHTTPRequestHandler, HTTPServer, HTTPSServer, \
|
||||||
SimpleHTTPRequestHandler, CGIHTTPRequestHandler
|
SimpleHTTPRequestHandler, CGIHTTPRequestHandler
|
||||||
from http import server, HTTPStatus
|
from http import server, HTTPStatus
|
||||||
|
|
||||||
|
@ -31,9 +31,14 @@ from io import BytesIO, StringIO
|
||||||
import unittest
|
import unittest
|
||||||
from test import support
|
from test import support
|
||||||
from test.support import (
|
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)
|
support.requires_working_socket(module=True)
|
||||||
|
|
||||||
class NoLogRequestHandler:
|
class NoLogRequestHandler:
|
||||||
|
@ -45,13 +50,48 @@ class NoLogRequestHandler:
|
||||||
return ''
|
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):
|
class TestServerThread(threading.Thread):
|
||||||
def __init__(self, test_object, request_handler):
|
def __init__(self, test_object, request_handler, tls=None):
|
||||||
threading.Thread.__init__(self)
|
threading.Thread.__init__(self)
|
||||||
self.request_handler = request_handler
|
self.request_handler = request_handler
|
||||||
self.test_object = test_object
|
self.test_object = test_object
|
||||||
|
self.tls = tls
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
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.server = HTTPServer(('localhost', 0), self.request_handler)
|
||||||
self.test_object.HOST, self.test_object.PORT = self.server.socket.getsockname()
|
self.test_object.HOST, self.test_object.PORT = self.server.socket.getsockname()
|
||||||
self.test_object.server_started.set()
|
self.test_object.server_started.set()
|
||||||
|
@ -67,11 +107,15 @@ class TestServerThread(threading.Thread):
|
||||||
|
|
||||||
|
|
||||||
class BaseTestCase(unittest.TestCase):
|
class BaseTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
# Optional tuple (certfile, keyfile, password) to use for HTTPS servers.
|
||||||
|
tls = None
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self._threads = threading_helper.threading_setup()
|
self._threads = threading_helper.threading_setup()
|
||||||
os.environ = os_helper.EnvironmentVarGuard()
|
os.environ = os_helper.EnvironmentVarGuard()
|
||||||
self.server_started = threading.Event()
|
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.thread.start()
|
||||||
self.server_started.wait()
|
self.server_started.wait()
|
||||||
|
|
||||||
|
@ -315,6 +359,74 @@ class BaseHTTPServerTestCase(BaseTestCase):
|
||||||
self.assertEqual(b'', data)
|
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 RequestHandlerLoggingTestCase(BaseTestCase):
|
||||||
class request_handler(BaseHTTPRequestHandler):
|
class request_handler(BaseHTTPRequestHandler):
|
||||||
protocol_version = 'HTTP/1.1'
|
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