Python allow you to start a simple HTTP server with a pretty handy command:

python3 -m http.server
python -m SimpleHTTPServer # Using python 2

But what about a one-liner for starting a HTTP server with self-signed certificates? A simple command that handles:

  • Generating a self-signed certificate for localhost
  • Starting an HTTPS TLS server based on it

I hadn't found a one-liner to do that, so I made my own.

HTTPS server (basic): one-liner

Execute the following to start a simple HTTPS server with the same functionality of the SimpleHTTPServer above:

i="0.0.0.0";p="8443"&&t=$(mktemp -d)&&openssl req -x509 -newkey rsa:3072 -nodes -keyout $t/k -out $t/c -sha256 -days 5 -subj "/CN=localhost"&&python3 -c "from ssl import wrap_socket as w,PROTOCOL_TLSv1_2 as p;from http.server import HTTPServer as t,SimpleHTTPRequestHandler as h;s=t(('$i',$p),h);s.socket=w(s.socket,server_side=True,certfile='$t/c',keyfile='$t/k',ssl_version=p);print('Serving HTTPS on $i port $p ...');s.serve_forever()";shred $t/k||true;rm -r $t;

HTTPS server with extended security: one-liner

Execute the following to start a HTTPS server with the same functionality of the SimpleHTTPServer above, but with extended security features:

  • Basic authentication with username and random password
  • Provides a configured curl command line with server key pinning and authentication included for quick usage
  • Actually creates two separate certificates based on RSA(3072 bit) and ECC(secp256r1) to allow for more ciphers and choice
  • Selects only safe TLSv1.2 ciphers (based on the BSI TR 02102 cryptographical recommendations)
  • The server shuts down after 15 minutes of inactivity
i="0.0.0.0";p="8443";w="15";u="$USER";S="$(head -c18 /dev/urandom|base64)";l=openssl;o="-nodes -batch -sha256 -days 5 -subj /CN=localhost";y='sha256//';g(){ $l x509 -in $t/$1 -pubkey -noout|$l pkey -pubin -outform der|$l dgst -sha256 -binary|$l enc -base64; };n() { $l req -x509 $1 -out $t/$2 $3 $t/$4 $o; };t=$(mktemp -d)&&$l ecparam -name prime256v1 -genkey -out $t/j&&n -new b -key j&&O=$(g b)&&n '-newkey rsa:3072' c -keyout k&&P=$(g c)&&echo "$S"|python3 -c "from ssl import SSLContext as w,PROTOCOL_TLSv1_2 as p;from http.server import HTTPServer as t,SimpleHTTPRequestHandler as h;from base64 import b64encode as b,b64decode as d;from zlib import decompress as m;from sys import exit as E;e='utf-8';k='$u:%s'%input();W=$w*60 if $w>0 else None;z='Authorization';B='Basic ';K=B+b(bytes(k,e)).decode(e);exec(\"class H(h):\n def r(self,c,d):\n  [self.send_response(c)]+[self.send_header(a,b) for a,b in [('Content-type','text/html')]+d]+[self.end_headers()]\n def do_HEAD(self):\n  self.r(200,[])\n def do_GET(self):\n  self.r(401,[('WWW-Authenticate','%srealm=\\\"https.server\\\"'%B)]) if z not in self.headers or self.headers[z]!=K else h.do_GET(self)\");s=t(('$i',$p),H);s.timeout=W;s.handle_timeout=lambda:[print('Shutdown: no requests since $w minutes.'),E(0)];R=w(p);R.load_cert_chain(certfile='$t/b',keyfile='$t/j');R.load_cert_chain(certfile='$t/c',keyfile='$t/k');R.set_ciphers(m(d(b'eJx9kTsOwCAMQ0/E0K+qbgiidmFp7n+XFgkkkjhdGGx4MTGlfIeHY4jE87YHvuNyrCcp+UoFWdN8VPm7YeT2olv0QyOF83TDgnO+E35HOSaDcFMqWjYhhOPHqG7HueF+s7kZ3AimUysOM2CfbpuqS6wKunDqKnC1frHK6YjMDFIMqkrRHDVyUOUGEd6lQzhmvxQv2gE=')).decode(e));s.socket=R.wrap_socket(sock=s.socket,server_side=True);print('Serving HTTPS on $i port $p, timeout $w minutes. Login with:\n\n\t%s\n\nor use:\n\n\tcurl -k --pinnedpubkey \"$y$P;$y$O\" -H \"%s: %s\" https://localhost:$p/\n'%(k,z,K));exec(\"while True:\n s.handle_request()\")";shred $t/k||true;rm -r $t;

The first variables assignments allow for customization.

HTTPS server with extended security: readable sources

I felt that the code blob above was already stretching the bounds for a one-liner, so here's the much more readable source for it.

Copy the following code in a file called https.sh and make it executable:

#!/bin/bash
set -e; set -u

echo -en "Starting $(basename "$0") [ip] [port] [timeout] [user] [password]\n-----\n"

ip="${1:-0.0.0.0}"; port="${2:-8443}"; timeout="${3:-15}"
user="${4:-$USER}"; password="${5:-$(head -c18 /dev/urandom | base64)}"

tmp="$(mktemp -d)"
openssl_params="-nodes -batch -sha256 -days 5 -subj /CN=localhost"
target_key1="$tmp/key1.pem"; target_cert1="$tmp/cert1.pem"
target_key2="$tmp/key2.pem"; target_cert2="$tmp/cert2.pem"

hash() { openssl x509 -in "$1" -pubkey -noout | openssl pkey -pubin -outform der | \
         openssl dgst -sha256 -binary | openssl enc -base64; }

openssl ecparam -name prime256v1 -genkey -out "$target_key1"
openssl req -new -x509 -key "$target_key1" -out "$target_cert1" $openssl_params
hash1="$(hash "$target_cert1")";

openssl req -x509 -newkey rsa:3072 -keyout "$target_key2" -out "$target_cert2" $openssl_params
hash2="$(hash "$target_cert2")";

echo "$password" | python3 "$(cd "$(dirname "$0")" && pwd)/https.py" "$ip" "$port" "$user" \
    "$timeout" "$target_key1" "$target_cert1" "$hash1" "$target_key2" "$target_cert2" "$hash2";

shred "$target_key" || true
rm -r "$tmp";

Then save the following as https.py in the same folder:

#!/usr/bin/python3

from ssl import SSLContext, PROTOCOL_TLSv1_2
from http.server import HTTPServer, SimpleHTTPRequestHandler
from base64 import b64encode, b64decode
from zlib import decompress
import sys

try:
    LISTEN_IP = sys.argv[1]
    LISTEN_PORT = int(sys.argv[2])
    USER = sys.argv[3]
    TIMEOUT = int(sys.argv[4])
    KEY1 = sys.argv[5]
    CERT1 = sys.argv[6]
    HASH1 = sys.argv[7]
    KEY2 = sys.argv[8]
    CERT2 = sys.argv[9]
    HASH2 = sys.argv[10]
    PASSWORD = input()
except ValueError | IndexError | EOFError:
    print('Error: please start with https.sh')
    sys.exit(1)

ENCODING = 'utf-8'
PASSWORD = '%s:%s'%(USER, PASSWORD)
HASH_ALG = "sha256//"
AUTH_HEADER = 'Authorization'
AUTH_TYPE = 'Basic '
AUTH_VALUE = AUTH_TYPE + b64encode(bytes(PASSWORD, ENCODING)).decode(ENCODING)

TIMEOUT_SECONDS = TIMEOUT * 60 if TIMEOUT > 0 else None

class BasicAuthHandler(SimpleHTTPRequestHandler):
    def exit_with_headers(self, code, headers):
        headers = [('Content-type', 'text/html')] + headers

        self.send_response(code)

        for key, value in headers:
            self.send_header(key, value)

        self.end_headers()

    def do_HEAD(self):
        self.exit_with_headers(200, [])

    def do_GET(self):
        if AUTH_HEADER not in self.headers or self.headers[AUTH_HEADER] != '%s' % AUTH_VALUE:
            self.exit_with_headers(401, [('WWW-Authenticate',
                                          '%srealm=\"https.server\"' % AUTH_TYPE)])
        else:
            SimpleHTTPRequestHandler.do_GET(self)

def handle_timeout():
    print('Shutdown: no requests since %s minutes.' % TIMEOUT)
    sys.exit(0)

SERVER = HTTPServer((LISTEN_IP, LISTEN_PORT), BasicAuthHandler)
SERVER.timeout = TIMEOUT_SECONDS
SERVER.handle_timeout = handle_timeout

SSL = SSLContext(PROTOCOL_TLSv1_2)
SSL.load_cert_chain(certfile=CERT1, keyfile=KEY1)
SSL.load_cert_chain(certfile=CERT2, keyfile=KEY2)
SSL.set_ciphers(decompress(b64decode(
    b'eJx9kTsOwCAMQ0/E0K+qbgiidmFp7n+XFgkkkjhdGGx4MTGlfIeHY4jE87YHvuNyrCcp+UoFWdN8VPm7YeT2olv0' + \
    b'QyOF83TDgnO+E35HOSaDcFMqWjYhhOPHqG7HueF+s7kZ3AimUysOM2CfbpuqS6wKunDqKnC1frHK6YjMDFIMqkrR' + \
    b'HDVyUOUGEd6lQzhmvxQv2gE=')).decode(ENCODING))

SERVER.socket = SSL.wrap_socket(sock=SERVER.socket, server_side=True)

print('Serving HTTPS on %s port %s, TIMEOUT %s minutes.' % (LISTEN_IP, LISTEN_PORT, TIMEOUT) + \
      ' Login with:\n\n\t%s\n\nor use:\n' % PASSWORD + \
      '\n\tcurl -k --pinnedpubkey \"%s%s;%s%s\"' % (HASH_ALG, HASH1, HASH_ALG, HASH2) + \
      ' -H \"%s: %s\" https://localhost:%s/\n' % (AUTH_HEADER, AUTH_VALUE, LISTEN_PORT))

while True:
    SERVER.handle_request()

Usage

To start the server, run https.sh from the folder you want to share. The following positional parameter are optional:

  • [IP]: defaults to 0.0.0.0, it's the IP onto which the server should listen
  • [port]: defaults to 8443, the server listens on this port
  • [timeout]: defaults to 15, an it's the amount of minutes of inactivity (no requests) after which the server shuts down
  • [user]: defaults to $USER (the current Linux user), used as the username for the basic authentication
  • [password]: overrides the automatically generated password. Discouraged, since passing it as a parameter will make it easily visible in the program list

Selected TLS ciphers

As mentioned I selected the TLS ciphers from the BSI TR 02102 cryptographical recommendations to only allow approved algorithms.

This reflects the cipher suites for TLSv1.2 selected in the BSI-TR-02102-2 including (EC)DHE cipher suites with Perfect Forward Secrecy as well as (EC)DH cipher suites without PFS, and includes the following openssl suites (alphabetical order):

  • DH-DSS-AES128-GCM-SHA256
  • DH-DSS-AES128-SHA256
  • DH-DSS-AES256-GCM-SHA384
  • DH-DSS-AES256-SHA256
  • DHE-DSS-AES128-GCM-SHA256
  • DHE-DSS-AES128-SHA256
  • DHE-DSS-AES256-GCM-SHA384
  • DHE-DSS-AES256-SHA256
  • DHE-RSA-AES128-CCM
  • DHE-RSA-AES128-GCM-SHA256
  • DHE-RSA-AES128-SHA256
  • DHE-RSA-AES256-CCM
  • DHE-RSA-AES256-GCM-SHA384
  • DHE-RSA-AES256-SHA256
  • DH-RSA-AES128-GCM-SHA256
  • DH-RSA-AES128-SHA256
  • DH-RSA-AES256-GCM-SHA384
  • DH-RSA-AES256-SHA256
  • ECDH-ECDSA-AES128-GCM-SHA256
  • ECDH-ECDSA-AES128-SHA256
  • ECDH-ECDSA-AES256-GCM-SHA384
  • ECDH-ECDSA-AES256-SHA384
  • ECDHE-ECDSA-AES128-CCM
  • ECDHE-ECDSA-AES128-GCM-SHA256
  • ECDHE-ECDSA-AES128-SHA256
  • ECDHE-ECDSA-AES256-CCM
  • ECDHE-ECDSA-AES256-GCM-SHA384
  • ECDHE-ECDSA-AES256-SHA384
  • ECDHE-RSA-AES128-GCM-SHA256
  • ECDHE-RSA-AES128-SHA256
  • ECDHE-RSA-AES128-SHA384
  • ECDHE-RSA-AES256-GCM-SHA384
  • ECDH-RSA-AES128-GCM-SHA256
  • ECDH-RSA-AES128-SHA256
  • ECDH-RSA-AES256-GCM-SHA384
  • ECDH-RSA-AES256-SHA384

P.S. In case you're wondering: yes, they are listed one by one in the code above, only that I've compressed the cipher list with zlib and encoded it in base 64 to make the code more compact.

P.P.S. Yes, your openssl might not support them all. That's OK, at least some of them should be selected.