Use python -m http.server in SSL

4 min read Original article ↗

python -m http.server is a very convenient command line tool to start an ad-hoc static web server. This tool is very useful when you want to develop small web applications or serve static files.

But it does not support SSL. A lot of modern web applications as well as browser features require a secured connection. So I want to wrap http.server in SSL and try making it simple to use.

Use http.server with SSL

I modified several functions from the module http.server and write a script ssl_server.py. I'll show you how to use the script and mkcert to serve a static sites with a self-signed SSL certificate.

The first step is to copy the ssl_server.py script to ~/.local/bin and make it executable.

1cp ssl_server.py ~/.local/bin
2chmod +x ~/.local/bin/ssl_server.py

Then install the mkcert tool. You can find the installation instructions on the mkcert GitHub page.

Generate a self-signed certificate and a private key using mkcert(for a domain like example.local).

The command will generate two files example.local.pem and example.local-key.pem. You can use these files to start the SSL server.

1~/.local/bin/ssl_server.py --cert example.local.pem --key example.local-key.pem --port 8443

Now that you have a static web server running on https://example.local:8443.

To visit the sites in your browser, you need to do 2 more things:

  1. add the domain example.local to your /etc/hosts file.
1echo "127.0.0.1 example.local" | sudo tee -a /etc/hosts
  1. Trust the certificate in your browser.
1# Install using mkcert
2mkcert -install
3
4# Or install manually from mkcert root CA directory
5mkcert -CAROOT
6# copy the root CA file 

The ssl_server.py script

The script is as follows:

 1#! /usr/bin/env python
 2'''
 3A wrapper around the standard library's http.server module that adds SSL support.
 4'''
 5import sys
 6import os
 7import socket
 8import ssl
 9from http.server import (
10    SimpleHTTPRequestHandler,
11    CGIHTTPRequestHandler,
12    ThreadingHTTPServer,
13    BaseHTTPRequestHandler,
14    _get_best_family
15)
16
17
18def test(HandlerClass=BaseHTTPRequestHandler,
19         ServerClass=ThreadingHTTPServer,
20         protocol="HTTP/1.0", port=8000, bind=None,
21         cert=None, key=None
22         ):
23    """Test the HTTP request handler class.
24
25    This runs an HTTP server on port 8000 (or the port argument).
26
27    """
28    ServerClass.address_family, addr = _get_best_family(bind, port)
29    HandlerClass.protocol_version = protocol
30    with ServerClass(addr, HandlerClass) as httpd:
31        if cert and key:
32            ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
33            ssl_context.load_cert_chain(cert, key)
34            httpd.socket = ssl_context.wrap_socket(httpd.socket, server_side=True)
35        host, port = httpd.socket.getsockname()[:2]
36        url_host = f'[{host}]' if ':' in host else host
37        print(
38            f"Serving HTTP on {host} port {port} "
39            f"(http://{url_host}:{port}/) ..."
40        )
41        try:
42            httpd.serve_forever()
43        except KeyboardInterrupt:
44            print("\nKeyboard interrupt received, exiting.")
45            sys.exit(0)
46
47if __name__ == '__main__':
48    import argparse
49    import contextlib
50
51    parser = argparse.ArgumentParser()
52    parser.add_argument('--cgi', action='store_true',
53                        help='run as CGI server')
54    parser.add_argument('--bind', '-b', metavar='ADDRESS',
55                        help='specify alternate bind address '
56                             '(default: all interfaces)')
57    parser.add_argument('--directory', '-d', default=os.getcwd(),
58                        help='specify alternate directory '
59                             '(default: current directory)')
60    parser.add_argument('--port', action='store', default=8000, type=int,
61                        nargs='?',
62                        help='specify alternate port (default: 8000)')
63    parser.add_argument('--cert', help='specify a certificate file')
64    parser.add_argument('--key', help='specify a private key file')
65    args = parser.parse_args()
66    if args.cgi:
67        handler_class = CGIHTTPRequestHandler
68    else:
69        handler_class = SimpleHTTPRequestHandler
70
71    # ensure dual-stack is not disabled; ref #38907
72    class DualStackServer(ThreadingHTTPServer):
73
74        def server_bind(self):
75            # suppress exception when protocol is IPv4
76            with contextlib.suppress(Exception):
77                self.socket.setsockopt(
78                    socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
79            return super().server_bind()
80
81        def finish_request(self, request, client_address):
82            self.RequestHandlerClass(request, client_address, self,
83                                     directory=args.directory)
84
85    test(
86        HandlerClass=handler_class,
87        ServerClass=DualStackServer,
88        port=args.port,
89        bind=args.bind,
90        cert=args.cert,
91        key=args.key
92    )

The script wraps the httpd.socket with an SSL socket if the cert and key options are provided.

The script keeps the same interface as http.server, but it adds two more options --cert and --key which are used to specify the certificate and the private key files.

Generally, you can use the script like this:

1python ssl_server.py --cert cert.pem --key key.pem

For convenience, you may put cert and key files in a directory and use a bash script to start the server.

1#!/bin/bash
2cert_dir=~/certs  # change to your cert directory
3cert=$cert_dir/cert.pem
4key=$cert_dir/key.pem
5python ssl_server.py --cert $cert --key $key $@