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.
.. 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)
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:
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
-------
__version__ = "0.6"
__all__ = [
- "HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler",
- "SimpleHTTPRequestHandler", "CGIHTTPRequestHandler",
+ "HTTPServer", "ThreadingHTTPServer",
+ "HTTPSServer", "ThreadingHTTPSServer",
+ "BaseHTTPRequestHandler", "SimpleHTTPRequestHandler",
+ "CGIHTTPRequestHandler",
]
import copy
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.
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).
"""
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()
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:
port=args.port,
bind=args.bind,
protocol=args.protocol,
+ tls_cert=args.tls_cert,
+ tls_key=args.tls_key,
+ tls_password=tls_key_password,
)
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
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:
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
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()
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'
--- /dev/null
+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.