72 lines
4.3 KiB
Python
72 lines
4.3 KiB
Python
"""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())
|