Certificates

Securing the Server

openssl req -x509 -newkey rsa:4096 -sha256 -days 1000 -nodes \
    -keyout server_first_key.pem -out server_first_cert.pem
-----BEGIN CERTIFICATE-----
MIIE+zCCAuOgAwIBAgIUfqV4WLyo+hCSjqfLxt8gxP2SqWMwDQYJKoZIhvcNAQEL
BQAwDTELMAkGA1UEBhMCQ0EwHhcNMjQwMjI1MjI1OTMyWhcNMjYxMTIxMjI1OTMy
WjANMQswCQYDVQQGEwJDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
AL3+7HzDMcRLBUONmN65OSrk23ZitOXenyYaOmbkKH//lM6qMVDRJUA5FRiVuTV8
lx8uw2QKwCfgpcf3y6jk1L3p+eOY333BE38m7GCjysTMc7//aDxdEYu8rkzCeG/G

-----END CERTIFICATE-----
if __name__ == "__main__":
    server_address = ("", 1443)
    sandbox = sys.argv[1]
    certfile = sys.argv[2]
    keyfile = sys.argv[3]

    os.chdir(sandbox)

    # If check_hostname is True, only the hostname that matches the certificate
    # will be accepted
    ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ssl_context.check_hostname = False
    ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile)

    server = HTTPServer(server_address, RequestHandler)
    server.socket = ssl_context.wrap_socket(server.socket, server_side=True)

    print(f"serving at {server_address} in {os.getcwd()}...")
    server.serve_forever()
python src/file_server_secure.py site server_first_cert.pem server_first_key.pem

Securing the Client

ERROR: HTTPSConnectionPool(host='localhost', port=1443):
Max retries exceeded with url: /test.txt
(Caused by SSLError(SSLCertVerificationError(1,
'[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1000)')))
ERROR: HTTPSConnectionPool(host='localhost', port=1443):
Max retries exceeded with url: /test.txt
(Caused by SSLError(SSLCertVerificationError(1,
"[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
Hostname mismatch, certificate is not valid for 'localhost'. (_ssl.c:1000)")))

Starting Over

# create CA key and cert simultaneously
openssl req -x509 -newkey RSA -nodes -keyout CA.key -days 10 -out CA.pem -reqexts \
    v3_ca -subj "/C=CA/ST=ON/L=Toronto/O=Third Bit/OU=x509"

# create server key and cert
openssl req -new -newkey RSA -nodes -keyout server.key -out server.csr -batch \
    -reqexts v3_req -subj "/CN=localhost"

# sign server CSR with CA key
openssl x509 -req -days 10 -in server.csr -CAkey CA.key -CA CA.pem -CAcreateserial \
    -out server.pem -extfile extfile.txt
echo "certificates created"
python src/file_server_secure.py site server.pem server.key
import requests

CA_FILE = "CA.pem"

print(f"trying request with verify={CA_FILE}")
try:
    r = requests.get("https://localhost:1443/motto.txt", verify=CA_FILE)
    print(f"SUCCESS: {r.status_code} {r.text}")
except Exception as exc:
    print(f"FAILURE: {exc}")

This Is Hard

basicConstraints = CA:FALSE
subjectAltName = DNS:localhost
extendedKeyUsage = serverAuth

Running Processes Together

#!/usr/bin/env bash

# Save the process group ID of this script.
pgid=`ps -o pgid=$$`
echo "PGID $pgid"

# Trap a Ctrl-C SIGINT and kill everything running inside this script.
trap "pkill -TERM -g $pgid" INT

# Redirect server stderr to stdout and background the process.
$1 2>&1 &

# Wait one second.
sleep 1

# Redirect client stderr to stdout as well but run in foreground.
$2 2>&1

# Kill this script and its children (client and server) when client finishes.
pkill -TERM -g $pgid
for i in $(seq 1 1 3)
do
    echo "left $i"
    sleep 1
done
$ bash src/run_2_sleep.sh "bash src/run_2_left.sh" "bash src/run_2_right.sh"
PGID  5299
left 1
left 2
right 1
left 3
right 2
right 3

Partial Ordering

Listing Open Files

lsof -n -iTCP | head -n 10 | cut -c -72
COMMAND     PID  USER   FD   TYPE             DEVICE SIZE/OFF NODE
corespeec   585 tut    3u  IPv6 0x5f5fb0f69522963b      0t0  TCP
jetbrains  1609 tut   59u  IPv6 0x5f5fb0f69523863b      0t0  TCP
PowerChim 46752 tut    3u  IPv6 0x5f5fb0f69524ae3b      0t0  TCP
PowerChim 46752 tut    6u  IPv6 0x5f5fb0f695228e3b      0t0  TCP
Google    53670 tut   22u  IPv6 0x5f5fb0f695246e3b      0t0  TCP
Google    53670 tut   24u  IPv6 0x5f5fb0f69523d63b      0t0  TCP
Google    53670 tut   29u  IPv4 0x5f5fb0fb588afdd3      0t0  TCP
Google    53670 tut   33u  IPv6 0x5f5fb0f69524663b      0t0  TCP
Google    53670 tut   34u  IPv6 0x5f5fb0f69523de3b      0t0  TCP

A Better Runner

# assume first arg is a set of TCP ports
# followed by one command per server listening to the port
# followed by client commands
# bash run_and_wait.sh "8000" "server 8000" "client"

PORTS="$1"
shift

CHILDREN=

await_port_free() {
    PORTNUM=$1
    while lsof -n -iTCP:${PORTNUM} ; do
        sleep 0.5
        printf "*"
    done
    printf "\nport $PORTNUM free\n"
}

await_port_listen() {
    PORTNUM=$1
    while ! lsof -n -iTCP:${PORTNUM}|grep -qw LISTEN ; do
        sleep 0.5
        printf "*"
    done
    printf "\nport $PORTNUM in LISTEN state\n"
}

on_exit(){
    # disable trap
    trap - exit int
    # gently kill every child
    kill -INT $CHILDREN &>/dev/null
    sleep 1
    # thorough cleanup
    pkill -TERM -g 0
}

# exiting or ^C runs on_exit
trap on_exit exit int

# server commands

for PORT in $PORTS; do
    await_port_free $PORT

    CMD="$1"
    shift
    $CMD &
    CHILDREN="$CHILDREN $!"

    await_port_listen $PORT
done

# client commands

for CMD in "$@"; do
    $CMD &
    CHILDREN="$CHILDREN $!"
done

# wait until any child process exits
while true; do
    for CHILD in $CHILDREN; do
        if ! kill -0 $CHILD &>/dev/null; then
            exit # to on_exit
        fi
    done
    sleep 0.5
done

Introducing FastAPI

from fastapi import FastAPI
import os
import pandas as pd
import uvicorn
import sys

sandbox, filename = sys.argv[1], sys.argv[2]
os.chdir(sandbox)
df = pd.read_csv(filename)

app = FastAPI()

@app.get("/")
async def get_birds(year: int = None, species: str = None):
    result = df.copy()
    if species is not None:
        result = result[result["species_id"] == species]
    if year is not None:
        result = result[result["year"] == year]
    result["num"].fillna(0, inplace=True)
    return result.to_dict(orient="records")

uvicorn.run(app)

At a Lower Level

import socketserver

CHUNK_SIZE = 4096
HOST = "localhost"
PORT = 8000

RESPONSE = """HTTP/1.1 200 OK
Content-Length: 0
"""

class Handler(socketserver.BaseRequestHandler):

    def handle(self):
        print("server awaiting message")
        self.data = str(self.request.recv(CHUNK_SIZE), "utf-8")
        print(f"From {self.client_address[0]}: {len(self.data)} bytes\n{self.data}")
        self.request.sendall(bytes(RESPONSE, "utf-8"))


if __name__ == "__main__":
    server = socketserver.TCPServer((HOST, PORT), Handler)
    print("server about to handle request")
    server.handle_request()
    server.server_close()
    print("server done")
S:  server about to handle request
S:  server awaiting message
S:  From 127.0.0.1: 154 bytes
S:  GET /motto.txt HTTP/1.1
S:  Host: localhost:8000
S:  User-Agent: python-requests/2.31.0
S:  Accept-Encoding: gzip, deflate
c:  client received: <Response [200]>
S:  Accept: */*
S:  Connection: keep-alive
S:
S:
S:  server done

Sending an HTTP Request

import socket

CHUNK_SIZE = 4096
HOST = "localhost"
PORT = 8000
PATH = "/motto.txt"
MESSAGE = f"GET {PATH} HTTP/1.1\r\nHost: {HOST}\r\n\r\n"
SERVER_ADDRESS = (HOST, 8000)

socket = socket.socket()
socket.connect(SERVER_ADDRESS)
socket.sendall(bytes(MESSAGE, "utf-8"))
print(f"client sent:\n{MESSAGE}")

first = socket.recv(CHUNK_SIZE)
first_str = str(first, "utf-8")
print(f"client received {len(first)} bytes:\n{first_str}")

second = socket.recv(CHUNK_SIZE)
second_str = str(second, "utf-8")
print(f"client received {len(second)} bytes:\n{second_str}")
S:  ::ffff:127.0.0.1 - - [16/Feb/2024 17:53:40] "GET /motto.txt HTTP/1.1" 200 -
c:  client sent:
c:  GET /motto.txt HTTP/1.1
c:  Host: localhost
c:
c:
c:  client received 244 bytes:
c:  HTTP/1.0 200 OK
c:  Server: SimpleHTTP/0.6 Python/3.12.1
c:  Date: Fri, 16 Feb 2024 22:53:40 GMT
c:  Content-type: text/plain
c:  Content-Length: 58
c:  Last-Modified: Thu, 15 Feb 2024 12:58:01 GMT
c:
c:  Start where you are, use what you have, help who you can.
c:
c:  client received 0 bytes:
c:

Secure Sockets

import socket
import ssl
from headers import headers

CHUNK_SIZE = 4096
HOST = "gvwilson.github.io"
PORT = 443
PATH = "/web-tutorial/site/motto.txt"
MESSAGE = f"GET {PATH} HTTP/1.1\r\nHost: {HOST}\r\n\r\n"
SERVER_ADDRESS = (HOST, PORT)

socket = socket.socket()
context = ssl.create_default_context()
connection = context.wrap_socket(socket, server_hostname=HOST)

connection.connect(SERVER_ADDRESS)
connection.sendall(bytes(MESSAGE, "utf-8"))
print(f"client sent:\n{MESSAGE}")

first = connection.recv(CHUNK_SIZE)
first_str = headers(str(first, "utf-8"), "HTTP", "Content-Length", "Content-Type")
print(f"client received {len(first)} bytes:\n{first_str}\n")

second = connection.recv(CHUNK_SIZE)
second_str = str(second, "utf-8")
print(f"client received {len(second)} bytes:\n{second_str}")
client sent:
GET /web-tutorial/site/motto.txt HTTP/1.1
Host: gvwilson.github.io


client received 682 bytes:
HTTP/1.1 200 OK
Content-Length: 58
Content-Type: text/plain; charset=utf-8
plus 22 more lines

client received 58 bytes:
Start where you are, use what you have, help who you can.