Simple HTTPS server in python: a one-liner
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 HTTPS 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 to0.0.0.0
, it's the IP onto which the server should listen[port]
: defaults to8443
, the server listens on this port[timeout]
: defaults to15
, 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.