Set up a simple HTTPS server in Python 3

Overview

When trying out JavaScript code in a browser, Python's HTTP server comes in handy as an easy way. The Progressive Web App (PWA), which has been the focus of attention in the JavaScript community since 2016, requires HTTPS, but since there was only an article about changing the ssl module in Python's HTTPS server and Python 3 series. , I decided to summarize what I investigated.

Premise

It assumes Python 3.6 and OpenSSL 1.0.2 and later versions. Various methods and constants have been added to the Python 3 series ssl module, and the sample code may not work on Python 3.6 and earlier versions.

You can find out the version of OpenSSL from the ssl constants.

>>> import ssl
>>> ssl.OPENSSL_VERSION

HTTP server review

If you want to start the server that provides the HTML files from the command line, run the following command:

python3 -m http.server 8000

If you want to use CGI, specify the --cgi option.

python3 -m http.server --cgi 8000

The script you want to run must be in / cgi-bin or / htbin.

#!/usr/bin/env python3

print("Content-Type: text/plain; charset=utf-8;\r\n")
print("hello world")

Self-signed certificate and private key generation

You can generate a self-signed certificate and private key with the following one-liner.

# https://stackoverflow.com/a/41366949/531320
openssl req -x509 -newkey rsa:4096 -sha256 \
-nodes -keyout server.key -out server.crt \
-subj "/CN=example.com" -days 3650

Set up an HTTPS server

In addition to the http.server module, you need to load the ssl module. Create a SSLContext object, specify various settings, and then use SSLContext.wrap_socket to create a SSLSocket object.

server.py


from http.server import HTTPServer, SimpleHTTPRequestHandler
import ssl

def run(host, port, ctx, handler):
    server = HTTPServer((host, port), handler)
    server.socket = ctx.wrap_socket(server.socket)
    print('Server Starts - %s:%s' % (host, port))

    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    server.server_close()
    print('Server Stops - %s:%s' % (host, port))

if __name__ == '__main__':
    host = 'localhost'
    port = 8000

    ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    ctx.load_cert_chain('server.crt', keyfile='server.key')
    ctx.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
    handler = SimpleHTTPRequestHandler

    run(host, port, ctx, handler)

ssl.OP_NO_TLSv1 and ssl.OP_NO_TLSv1_1 mean to disable TLS 1.0 and TLS 1.1, respectively.

Supports CGI

For CGI, use CGIHTTPRequestHandler instead of SimpleHTTPRequestHandler.

server.py


from http.server import HTTPServer, CGIHTTPRequestHandler
import ssl

def run(host, port, ctx, handler):
    server = HTTPServer((host, port), handler)
    server.socket = ctx.wrap_socket(server.socket)
    print('Server Starts - %s:%s' % (host, port))

    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    server.server_close()
    print('Server Stops - %s:%s' % (host, port))

if __name__ == '__main__':
    host = 'localhost'
    port = 8000

    ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    ctx.load_cert_chain('server.crt', keyfile='server.key')
    ctx.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1

    handler = CGIHTTPRequestHandler
    handler.cgi_directories = ['/cgi-bin', '/htbin']
    # https://stackoverflow.com/a/27303995/531320
    handler.have_fork=False

    run(host, port, ctx, handler)

For macOS and Unix, the server will not work without adding handler.have_fork = False.

The advantage of CGI is that it can run programs in languages other than Python. The Node.js code looks like this:

node.cgi


#!/usr/bin/env node

console.log("Content-type: text/plain; charset=utf-8\n");
console.log("Hello World");

Supports WSGI

You can use the wsgiref module to set up a WSGI-enabled server.

server.py


from wsgiref.simple_server import make_server
from pathlib import Path
import ssl

def simple_app(env, start_response):
    info = env['PATH_INFO'][1:]

    if info == '':
        info = 'index.html'

    root = Path.cwd()
    path = root.joinpath(info).resolve()

    if root in path.parents and path.is_file():
        status = '200 OK'
        body = path.read_bytes()
        suffix = path.suffix
        content_type = {
          '.html': 'text/html',
          '.txt': 'text/plain',
          '.json': 'application/json'
        }.get(suffix, 'text/plain')
    else :
        body = b'404 Not Found'
        status = '404 Not Found'
        content_type = 'text/plain'

    headers = [
       ('Content-Type', content_type),
       ('Content-Length', str(len(body)))
    ]

    start_response(status, headers)
    return [body]

def run(host, port, ctx, app):
    server = make_server(host, port, app)
    server.socket = ctx.wrap_socket(server.socket)
    print('Server Starts - %s:%s' % (host, port))

    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    server.server_close()
    print('Server Stops - %s:%s' % (host, port))

if __name__ == '__main__':
    host = 'localhost'
    port = 8000
    ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    ctx.load_cert_chain('server.crt', keyfile='server.key')
    ctx.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1

    run(host, port, ctx, simple_app)

If you want to know all the defined environment variables (ʻenv), go to [PEP-333](https://www.python.org/dev/peps/pep-0333/#environ-variables) Please refer. You can also check from the page displayed when wsgiref.simple_server.demo_app` is made an execution application.

SSLContext

Generate

ssl.create_default_context is recommended for general usage.

ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)

The constants you can specify are Purpose.SERVER_AUTH (used to create a client-side socket) and Purpose.CLIENT_AUTH (used to create a server-side socket).

You can also use the SSLContext constructor directly.

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)

Possible constants are PROTOCOL_TLS (default), PROTOCOL_TLS_CLIENT, and PROTOCOL_TLS_SERVER. Various constants that were available in Python 3.6 and earlier have been deprecated.

option

You can check or add settings through options. You can see the default options as follows:

>>> import ssl
>>> ssl.create_default_context().options
<Options.OP_ALL|OP_NO_SSLv3|OP_NO_SSLv2|
OP_CIPHER_SERVER_PREFERENCE|
OP_SINGLE_DH_USE|OP_SINGLE_ECDH_USE|
OP_NO_COMPRESSION: 2203714559>

The following code gives the same result.

>>> ssl.SSLContext().options

The default options for Python 3.6 are:

You can access or change context options after wrapping a socket object with wrap_socket.

server = HTTPServer((host, port), handler)
server.socket = ctx.wrap_socket(server.socket)
print(server.socket.context.options)

Difference between two wrap_socket

The arguments that can be specified for ssl.wrap_socket and SSLContext.wrap_socket are different. The arguments in Python 3.6 are:

ssl.wrap_socket(
  sock,
  keyfile=None,
  certfile=None,
  server_side=False,
  cert_reqs=CERT_NONE,
  ssl_version={see docs},
  ca_certs=None,
  do_handshake_on_connect=True,
  suppress_ragged_eofs=True,
  ciphers=None
)

SSLContext.wrap_socket(
  sock,
  server_side=False,
  do_handshake_on_connect=True,
  suppress_ragged_eofs=True,
  server_hostname=None,
  session=None
)

The difference between the two is that SSLContext.wrap_socket has fewer options to specify. The session added to SSLContext.wrap_socket in Python 3.6 cannot be specified in ssl.wrap_socket.

The criteria for using ssl.wrap_socket and SSLContext.wrap_socket is whether there are many or few settings.

If you use ssl.wrap.socket, the settings will be changed through server.socket.context, so a large number of settings will make the code harder to read.

Another advantage of creating a SSLContext object directly is that it can be used for a third-party HTTP library.

Cipher suite

Cipher suites can be found with get_ciphers. Use set_ciphers to change the cipher suite.

>>> import ssl
>>> ctx = ssl.create_default_context().get_ciphers()
>>> ctx.set_ciphers('ECDHE+AESGCM:!ECDSA')
>>> ctx.get_ciphers()

A recommended cipher suite is available on Mozilla's Site (https://wiki.mozilla.org/Security/Server_Side_TLS). Cloudflare has published an nginx config file (https://github.com/cloudflare/sslconfig) for TLS 1.3. OpenSSL 1.1.0 is part of TLS 1.3 and will be fully supported by OpenSSL 1.1.1.

Other

Minimum version of TLS

PCI DSS v3.2, which is used for credit card payment services, requires SSL and TSL 1.0 to be disabled until the end of June 2018. Fastly, the CDN service used by PyPI, also disables TLS 1.1.

You can find out which version of TLS supported by Python 3 installed on your system with the following one-liner.

# https://news.ycombinator.com/item?id=13539034
python3 -c "import json, urllib.request; print(json.loads(urllib.request.urlopen('https://www.howsmyssl.com/a/check').read().decode('UTF-8'))['tls_version'])"

Proposal of new API for TLS

PEP 543 proposes the introduction of a new API for TLS. Development motivations include the fact that the ssl module relies on OpenSSL, which means that Python must be recompiled to introduce the new OpenSSL and that it is not possible to switch to different TLS libraries than OpenSSL. I will.

The main SSL / TLS libraries are:

Recommended Posts

Set up a simple HTTPS server in Python 3
Set up a simple SMTP server in Python
Set up a simple HTTPS server with asyncio
Set up a test SMTP server in Python.
[Vagrant] Set up a simple API server with python
Set up a UDP server in C language
How to set up a simple SMTP server that can be tested locally in Python
Set up a local web server in 30 seconds using python 3's http.server
Set up a simple local server on your Mac
Set up a free server on AWS in 30 minutes
Implementing a simple algorithm in Python 2
Run a simple algorithm in Python
A simple HTTP client implemented in Python
Set up a Samba server with Docker
Try drawing a simple animation in Python
Create a simple GUI app in Python
Set up a mail server using Twisted
Write a simple greedy algorithm in Python
Write a simple Vim Plugin in Python 3
Set up Ubuntu as a Linux cheat sheet and https server
Simple gRPC in Python
Set up a local server with Go-File upload-
Send mail with mailx to a dummy SMTP server set up with python.
Start a simple Python web server with Docker
A simple Pub / Sub program note in Python
Set up a local server with Go-File download-
Create a simple momentum investment model in Python
How to set up a local development server
Set up a Python development environment on Marvericks
DNS server in Python ....
Set up a dummy SMTP server in Python and check the operation of sending from Action Mailer
Introduction and usage of Python bottle ・ Try to set up a simple web server with login function
Put Docker in Windows Home and run a simple web server with Python
Write a super simple molecular dynamics program in python
How to set up a Python environment using pyenv
Set up a Minecraft resource (Spigot) server via docker (2)
Set up a file server on Ubuntu 20.04 using Samba
A simple Python HTTP server that supports Range Requests
Make a simple Slackbot with interactive button in python
[Part 1] Let's set up a Minecraft server on Linux
Set up a Minecraft resource (Spigot) server via docker
Create a fake Minecraft server in Python with Quarry
Setting up Jupyter Lab in a Python 3.9 venv environment
Set up a Python development environment with Sublime Text 2
Set a proxy for Python pip (described in pip.ini)
Set up Python 3.4 on Ubuntu
Take a screenshot in Python
Create a function in Python
Create a dictionary in Python
Create a (simple) REST server
Set Up for Mac (Python)
Make a bookmarklet in Python
Simple regression analysis in Python
Simple HTTP Server for python
Create a simple textlint server
Draw a heart in Python
Simple IRC client in python
Set up Nunjucks in Node.js
Set python test in jenkins
I want to set up a mock server for python-flask in seconds using swagger-codegen.
Hello World is a simple web server that follows WSGI (Web Server Gateway Interface) in Python.