Files
safetynet/scripts/webtransport-receiver.py
T

71 lines
4.3 KiB
Python
Raw Normal View History

"""Finite loopback-only HTTP/3 WebTransport receiver for native security proof."""
import asyncio, datetime, hashlib, ipaddress, json, os, pathlib, sys
from aioquic.asyncio import QuicConnectionProtocol, serve
from aioquic.h3.connection import H3Connection, H3_ALPN
from aioquic.h3.events import HeadersReceived, DatagramReceived, WebTransportStreamDataReceived
from aioquic.quic.configuration import QuicConfiguration
from aioquic.quic.events import ProtocolNegotiated
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import NameOID
facts = {"packets": 0, "connections": 0, "datagrams": 0, "streams": 0, "bytes": 0, "workerBytes": 0, "marker": False}
marker = b"SYNTHETIC_WEBTRANSPORT_DLP_MARKER"
class Receiver(QuicConnectionProtocol):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.http = None
self.worker_sessions = set()
def datagram_received(self, data, addr):
facts["packets"] += 1
super().datagram_received(data, addr)
def quic_event_received(self, event):
if isinstance(event, ProtocolNegotiated):
self.http = H3Connection(self._quic, enable_webtransport=True)
if not self.http:
return
for item in self.http.handle_event(event):
if isinstance(item, HeadersReceived):
headers = dict(item.headers)
if headers.get(b":method") == b"CONNECT" and headers.get(b":protocol") == b"webtransport":
facts["connections"] += 1
if headers.get(b":path", b"").endswith(b"/worker"):
self.worker_sessions.add(item.stream_id)
self.http.send_headers(item.stream_id, [(b":status", b"200"), (b"sec-webtransport-http3-draft", b"draft02")])
self.transmit()
elif isinstance(item, (DatagramReceived, WebTransportStreamDataReceived)):
facts["datagrams" if isinstance(item, DatagramReceived) else "streams"] += 1
facts["bytes"] += len(item.data)
session_id = item.stream_id if isinstance(item, DatagramReceived) else item.session_id
if session_id in self.worker_sessions:
facts["workerBytes"] += len(item.data)
facts["marker"] |= marker in item.data
async def main():
root = pathlib.Path(sys.argv[1]).resolve()
root.mkdir(parents=True, exist_ok=True)
key = ec.generate_private_key(ec.SECP256R1())
now = datetime.datetime.now(datetime.timezone.utc)
name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "127.0.0.1")])
cert = x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key()).serial_number(x509.random_serial_number()).not_valid_before(now-datetime.timedelta(minutes=1)).not_valid_after(now+datetime.timedelta(days=7)).add_extension(x509.SubjectAlternativeName([x509.IPAddress(ipaddress.ip_address("127.0.0.1"))]), critical=False).sign(key, hashes.SHA256())
cert_file, key_file = root / "certificate.pem", root / "key.pem"
cert_file.write_bytes(cert.public_bytes(serialization.Encoding.PEM))
key_file.write_bytes(key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()))
config = QuicConfiguration(is_client=False, alpn_protocols=H3_ALPN, max_datagram_frame_size=65536)
config.load_cert_chain(str(cert_file), str(key_file))
server = await serve("127.0.0.1", 0, configuration=config, create_protocol=Receiver)
async def stats(reader, writer):
await reader.read(8192)
body = json.dumps(facts).encode()
writer.write(b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: " + str(len(body)).encode() + b"\r\nConnection: close\r\n\r\n" + body)
await writer.drain()
writer.close()
control = await asyncio.start_server(stats, "127.0.0.1", 0)
print(json.dumps({"quicPort": server._transport.get_extra_info("sockname")[1], "statsPort": control.sockets[0].getsockname()[1], "hash": hashlib.sha256(cert.public_bytes(serialization.Encoding.DER)).hexdigest(), "aioquic": __import__("aioquic").__version__}), flush=True)
await asyncio.sleep(180)
server.close()
control.close()
if __name__ == "__main__":
asyncio.run(main())