Starting Python server with HTTPS

2023-12-28 | Tutorials

In this tutorial, I will teach you how to easily setup a webserver in python, that will serve the contents over HTTPS, rather than over HTTP. Its purpose is to only serve for quick testing and demonstration, and should not be taken as an advice on how to deal with the problem for production-intended systems. I also provide a ready made script to simplify your testing.

Disclaimer: This tutorial should only be used for quick PoCs or testing purposes. It should not be understood as a guide on how to work with or manage certificates or keys - the aim is swiftness of going from "I need HTTPS for this test/demo" to "I have it". From security perspective, this should never make it to any system that is even remotely exposed to the real world or real users.

One of the common commands I tend to use when working on projects or making quick PoCs is starting up an HTTP server with python - it's a simple one-liner in cmd:

python -m http.server 8000

And voila, there's a webserver up and running on my machine on port 8000.

But a lot of things these days require HTTPS to be present - whether it is a program, that simply doesn't allow you to provide callback URL starting with http://, or a Progressive Web Application that refuses to offer itself for installation, if not served over HTTPS.

In this post I am covering how to quickly spin up a python server that will serve its contents over https.

If you are only interested in quick, ready-made script to use and move on, feel free to leverage the linked gist I prepared. README is included.

Design / Principle

This time, unfortunately, it is no longer possible to do this out of the box with a simple one-liner. We will have to take two major steps:

  1. Generate the certificate file with openssl
  2. Create a webserver with corresponding python module that consumes this certificate.

Implementation

Luckily, the certificate file creation is pretty straight forward and IS a one-liner:

openssl req -newkey rsa:2048 -x509 -days 365 -nodes -out cert.pem -keyout cert.pem

A bit of an explanation, not going too much into the details: 1. We create a new private key and a certificate file. 2. The private key uses 2048 bit key. 3. The certificate is valid for 1 year (365 days) 4. The private key is not encrypted (aka - protected by the pass phrase), because we provided the -nodes (no DES, (encryption)) parameter. 5. Both certificate and private key are written into the same cert.pem file.

In the same directory as cert.pem file, create a server.py file. Assuming we want to start a local server on https://localhost:4443, this is what contents of the server.py will look like:

import ssl
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler

httpd = ThreadingHTTPServer(("localhost", 4443), SimpleHTTPRequestHandler)
context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(certfile="cert.pem")
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
print(f"Starting server @ https://localhost:4443")
httpd.serve_forever()

If we now run the file python server.py, we will see the message about server starting and a prompt "hanging", until you make a very first request towards the address.

When we attempt to access the website, browser will tell us that the connection is not secure and we will have to Accept the risk and Continue. For our purposes this is however completely fine and the content is still served over HTTPS.

This behavior happens because the certificate we provide is signed by private key we just generated (known as self-signed certificate). Browsers have lists of entities they trust, when they verify signatures, and we are not one the list (these entities are called Certificate Authorities, or CA for short).

For convenience, I wrapped this (and the certificate generating functionality) into a single serve.py script, which you can find in the Simple HTTPS Server in Python for quick testing Gist.

FAQ & Troubleshooting

Server is running, but I am getting connection reset/refused hanging

One of the possibilities is that you are forgetting to specify the port on which you are supposed to connect (in the example implementation this is 4443).

Other possibility is that you forget to use the https:// infront of your local address, and instead attempt to access the website over http://, which is not possible. I wish I could tell you this didn't cost me about 2 hours of debugging while I was working this out for the first time.

Why is ThreadingHTTPServer used instead of more standard HTTPServer

Certain users reported issue when they would run the server and try to access it via Google Chrome (as opposed to them requesting the server via curl or Invoke-WebRequest), and the server would hang and the connection wouldn't be established.

This is reportedly because the browser keeps the connection "alive" (continuously going), and since our python code uses only one thread, it hangs in the thread, waiting for browser to finish the connection, which never happens.

Solution to this is using the ThreadingHTTPServer, that offloads request processing to a separate thread, and in turn avoids hanging. More details of this behavior are summarized in this answer on Stack Overflow: Http request from Chrome hangs python webserver.