AtlatestRepositorysigil-http
sigil-http / tree / test / integrationtimeout-servers.py
1
#!/usr/bin/env python32
"""Servers for the live timeout controls.4
Two healthy servers, deliberately boring: they answer correctly and promptly.5
Their whole job is to prove that the new bounds do NOT break a request that6
should work, which no stall rig can show.8
Routes:9
/ 200, {"status":"ok"}10
/echo-length 200, the request body's byte length as text11
/bytes 200, exactly 65536 bytes of application/octet-stream13
And one DRIP peer, which is the control that a silent stall rig cannot14
provide. See its docstring: it is the difference between a bound that is15
per-read and one that is per-handshake, and a rig made only of peers that16
send NOTHING cannot tell those apart.18
Usage: timeout-servers.py <http-port> <tls-port> <certfile> <keyfile> <drip-port>19
"""20
import http.server21
import socket22
import ssl23
import sys24
import threading25
import time27
BYTES_LEN = 6553629
# One byte per interval. Long enough that a per-read bound of a comparable30
# size resets on every byte, which is exactly the defect being tested for.31
DRIP_INTERVAL_S = 0.633
# A well-formed TLS record header: handshake (0x16), TLS 1.2 (0x0303), then a34
# declared body length of 0x4000 = 16384 bytes. The client will therefore sit35
# in mbedtls_ssl_fetch_input waiting for 16384 bytes that arrive one at a36
# time. The header must be VALID or the client rejects it immediately and the37
# result is "handshake-failed" rather than a timeout, which would test38
# nothing.39
TLS_RECORD_HEADER = bytes([0x16, 0x03, 0x03, 0x40, 0x00])42
def drip_server(port):43
"""Accept, send a valid record header, then trickle body bytes forever.45
A peer that goes silent and a peer that DRIPS are different failure46
geometries, and a handshake bound can be correct against the first while47
being useless against the second: mbedtls_ssl_handshake does not return48
between steps and passes the full configured read timeout to every49
partial read, so each dribbled byte restarts the clock. Measured against50
an earlier build, a 1000 ms bound took 13.06 s to fire here, and never51
fired at all while the drip continued.52
"""53
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)54
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)55
srv.bind(("127.0.0.1", port))56
srv.listen(16)58
def handle(conn):59
try:60
conn.sendall(TLS_RECORD_HEADER)61
while True:62
time.sleep(DRIP_INTERVAL_S)63
conn.sendall(b"\x00")64
except OSError:65
pass66
finally:67
try:68
conn.close()69
except OSError:70
pass72
while True:73
client, _ = srv.accept()74
threading.Thread(target=handle, args=(client,), daemon=True).start()77
class Handler(http.server.BaseHTTPRequestHandler):78
protocol_version = "HTTP/1.1"80
def _send(self, body, ctype="text/plain"):81
self.send_response(200)82
self.send_header("Content-Type", ctype)83
self.send_header("Content-Length", str(len(body)))84
self.end_headers()85
self.wfile.write(body)87
def do_GET(self):88
if self.path == "/bytes":89
self._send(bytes(i % 256 for i in range(BYTES_LEN)),90
"application/octet-stream")91
else:92
self._send(b'{"status":"ok"}', "application/json")94
def do_POST(self):95
length = int(self.headers.get("Content-Length", "0"))96
read = 097
while read < length:98
chunk = self.rfile.read(min(65536, length - read))99
if not chunk:100
break101
read += len(chunk)102
if self.path == "/echo-length":103
self._send(str(read).encode())104
else:105
self._send(b'{"status":"ok"}', "application/json")107
def log_message(self, *args):108
pass111
def serve(port, certfile=None, keyfile=None):112
srv = http.server.ThreadingHTTPServer(("127.0.0.1", port), Handler)113
if certfile:114
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)115
ctx.load_cert_chain(certfile, keyfile)116
srv.socket = ctx.wrap_socket(srv.socket, server_side=True)117
srv.serve_forever()120
def main():121
http_port = int(sys.argv[1])122
tls_port = int(sys.argv[2])123
certfile = sys.argv[3]124
keyfile = sys.argv[4]125
drip_port = int(sys.argv[5])127
threading.Thread(target=serve, args=(http_port,), daemon=True).start()128
threading.Thread(target=serve, args=(tls_port, certfile, keyfile),129
daemon=True).start()130
threading.Thread(target=drip_server, args=(drip_port,), daemon=True).start()132
# The runner waits for this line before starting the tests.133
print("SERVERS READY http=%d tls=%d drip=%d"134
% (http_port, tls_port, drip_port), flush=True)135
threading.Event().wait()138
if __name__ == "__main__":139
main()