async def test_session_resumption(server: Server, configuration: QuicConfiguration): port = server.session_resumption_port or server.port saved_ticket = None def session_ticket_handler(ticket): nonlocal saved_ticket saved_ticket = ticket # connect a first time, receive a ticket async with connect( server.host, port, configuration=configuration, session_ticket_handler=session_ticket_handler, ) as protocol: await protocol.ping() # some servers don't send the ticket immediately await asyncio.sleep(1) # connect a second time, with the ticket if saved_ticket is not None: configuration.session_ticket = saved_ticket async with connect(server.host, port, configuration=configuration) as protocol: await protocol.ping() # check session was resumed if protocol._quic.tls.session_resumed: server.result |= Result.R # check early data was accepted if protocol._quic.tls.early_data_accepted: server.result |= Result.Z
async def test_session_resumption(config, **kwargs): saved_ticket = None def session_ticket_handler(ticket): nonlocal saved_ticket saved_ticket = ticket # connect a first time, receive a ticket async with connect( config.host, config.port, session_ticket_handler=session_ticket_handler, **kwargs ) as connection: await connection.ping() # connect a second time, with the ticket if saved_ticket is not None: async with connect( config.host, config.port, session_ticket=saved_ticket, **kwargs ) as connection: await connection.ping() # check session was resumed if connection._connection.tls.session_resumed: config.result |= Result.R # check early data was accepted if connection._connection.tls.early_data_accepted: config.result |= Result.Z
async def test_http_3(server: Server, configuration: QuicConfiguration): if server.path is None: return configuration.alpn_protocols = H3_ALPN async with connect( server.host, server.port, configuration=configuration, create_protocol=HttpClient, ) as protocol: protocol = cast(HttpClient, protocol) # perform HTTP request events = await protocol.get("https://{}:{}{}".format( server.host, server.port, server.path)) if events and isinstance(events[0], HeadersReceived): server.result |= Result.D server.result |= Result.three # perform more HTTP requests to use QPACK dynamic tables for i in range(2): events = await protocol.get("https://{}:{}{}".format( server.host, server.port, server.path)) if events and isinstance(events[0], HeadersReceived): http = cast(H3Connection, protocol._http) protocol._quic._logger.info( "QPACK decoder bytes RX %d TX %d", http._decoder_bytes_received, http._decoder_bytes_sent, ) protocol._quic._logger.info( "QPACK encoder bytes RX %d TX %d", http._encoder_bytes_received, http._encoder_bytes_sent, ) if (http._decoder_bytes_received and http._decoder_bytes_sent and http._encoder_bytes_received and http._encoder_bytes_sent): server.result |= Result.d # check push support if server.push_path is not None: protocol.pushes.clear() await protocol.get("https://{}:{}{}".format( server.host, server.port, server.push_path)) await asyncio.sleep(0.5) for push_id, events in protocol.pushes.items(): if (len(events) >= 3 and isinstance(events[0], PushPromiseReceived) and isinstance(events[1], HeadersReceived) and isinstance(events[2], DataReceived)): protocol._quic._logger.info( "Push promise %d for %s received (status %s)", push_id, dict(events[0].headers)[b":path"].decode("ascii"), int(dict(events[1].headers)[b":status"]), ) server.result |= Result.p
async def test_handshake_and_close(server: Server, configuration: QuicConfiguration): async with connect( server.host, server.port, configuration=configuration ) as protocol: await protocol.ping() server.result |= Result.H server.result |= Result.C
async def test_migration(server: Server, configuration: QuicConfiguration): async with connect( server.host, server.port, configuration=configuration ) as protocol: # cause some traffic await protocol.ping() # change connection ID and replace transport protocol.change_connection_id() protocol._transport.close() await loop.create_datagram_endpoint(lambda: protocol, local_addr=("::", 0)) # cause more traffic await protocol.ping() # check log dcids = set() for stamp, category, event, data in configuration.quic_logger.to_dict()[ "traces" ][0]["events"]: if ( category == "transport" and event == "packet_received" and data["packet_type"] == "1RTT" ): dcids.add(data["header"]["dcid"]) if len(dcids) == 2: server.result |= Result.M
async def test_quantum_readiness(server: Server, configuration: QuicConfiguration): configuration.quantum_readiness_test = True async with connect( server.host, server.port, configuration=configuration ) as protocol: await protocol.ping() server.result |= Result.Q
async def test_address_mobility(server: Server, configuration: QuicConfiguration): async with connect( server.host, server.port, configuration=configuration ) as protocol: # cause some traffic await protocol.ping() # replace transport protocol._transport.close() await loop.create_datagram_endpoint(lambda: protocol, local_addr=("::", 0)) # change connection ID protocol.change_connection_id() # cause more traffic await protocol.ping() # check log path_challenges = 0 for stamp, category, event, data in configuration.quic_logger.to_dict()[ "traces" ][0]["events"]: if ( category == "transport" and event == "packet_received" and data["packet_type"] == "1RTT" ): for frame in data["frames"]: if frame["frame_type"] == "path_challenge": path_challenges += 1 if not path_challenges: protocol._quic._logger.warning("No PATH_CHALLENGE received") else: server.result |= Result.A
async def test_spin_bit(config, **kwargs): async with connect(config.host, config.port, **kwargs) as connection: spin_bits = set() for i in range(5): await connection.ping() spin_bits.add(connection._connection._spin_bit_peer) if len(spin_bits) == 2: config.result |= Result.P
async def http_client(host, port): async with connect(host, port) as connection: reader, writer = await connection.create_stream() writer.write(b"GET /\r\n") writer.write_eof() response = await reader.read() sys.stdout.buffer.write(response)
async def http_client(host, port): async with connect(host, port) as connection: reader, writer = await connection.create_stream() writer.write(b"GET /\r\n") writer.write_eof() response = await reader.read() print(response.decode("utf8"))
async def test_data_transfer(config, **kwargs): if config.path is None: return async with connect(config.host, config.port, **kwargs) as connection: response1 = await http_request(connection, config.path) response2 = await http_request(connection, config.path) if response1 and response2: config.result |= Result.D
async def http_client(host, port): configuration = QuicConfiguration(alpn_protocols=["hq-23"]) async with connect(host, port, configuration=configuration) as connection: reader, writer = await connection.create_stream() writer.write(b"GET /\r\n") writer.write_eof() response = await reader.read() sys.stdout.buffer.write(response)
async def run(config: QuicConfiguration, host: str, port: int, requested_video: str) -> None: async with connect( host=host, port=port, configuration=config, create_protocol=VideoStreamClientProtocol, ) as client: client = cast(VideoStreamClientProtocol, client) await client.send_request_for_video(requested_video)
async def test_key_update(config, **kwargs): async with connect(config.host, config.port, **kwargs) as connection: # cause some traffic await connection.ping() # request key update connection.request_key_update() # cause more traffic await connection.ping() config.result |= Result.U
async def test_throughput(server: Server, configuration: QuicConfiguration): failures = 0 if server.throughput_path is None: return for size in [5000000, 10000000]: path = server.throughput_path % {"size": size} print("Testing %d bytes download: %s" % (size, path)) # perform HTTP request over TCP start = time.time() response = httpx.get("https://" + server.host + path, verify=False) tcp_octets = len(response.content) tcp_elapsed = time.time() - start assert tcp_octets == size, "HTTP/TCP response size mismatch" # perform HTTP request over QUIC if server.http3: configuration.alpn_protocols = H3_ALPN port = server.http3_port or server.port else: configuration.alpn_protocols = H0_ALPN port = server.port start = time.time() async with connect( server.host, port, configuration=configuration, create_protocol=HttpClient, ) as protocol: protocol = cast(HttpClient, protocol) http_events = await protocol.get("https://{}:{}{}".format( server.host, server.port, path)) quic_elapsed = time.time() - start quic_octets = 0 for http_event in http_events: if isinstance(http_event, DataReceived): quic_octets += len(http_event.data) assert quic_octets == size, "HTTP/QUIC response size mismatch" print(" - HTTP/TCP completed in %.3f s" % tcp_elapsed) print(" - HTTP/QUIC completed in %.3f s" % quic_elapsed) if quic_elapsed > 1.1 * tcp_elapsed: failures += 1 print(" => FAIL") else: print(" => PASS") if failures == 0: server.result |= Result.T
async def test_key_update(server: Server, configuration: QuicConfiguration): async with connect(server.host, server.port, configuration=configuration) as protocol: # cause some traffic await protocol.ping() # request key update protocol.request_key_update() # cause more traffic await protocol.ping() server.result |= Result.U
async def test_stateless_retry(server: Server, configuration: QuicConfiguration): async with connect(server.host, server.retry_port, configuration=configuration) as protocol: await protocol.ping() # check log for stamp, category, event, data in configuration.quic_logger.to_dict( )["traces"][0]["events"]: if (category == "transport" and event == "packet_received" and data["packet_type"] == "retry"): server.result |= Result.S
async def rsocket_connect(host: str, port: int, configuration: QuicConfiguration = None) -> Transport: if configuration is None: configuration = QuicConfiguration( is_client=True ) async with connect( host, port, configuration=configuration, create_protocol=RSocketQuicProtocol, ) as client: yield RSocketQuicTransport(client)
async def test_spin_bit(server: Server, configuration: QuicConfiguration): async with connect(server.host, server.port, configuration=configuration) as protocol: for i in range(5): await protocol.ping() # check log spin_bits = set() for stamp, category, event, data in configuration.quic_logger.to_dict( )["traces"][0]["events"]: if category == "connectivity" and event == "spin_bit_updated": spin_bits.add(data["state"]) if len(spin_bits) == 2: server.result |= Result.P
async def test_server_cid_change(server: Server, configuration: QuicConfiguration): async with connect( server.host, server.port, configuration=configuration ) as protocol: # cause some traffic await protocol.ping() # change connection ID protocol.change_connection_id() # cause more traffic await protocol.ping() server.result |= Result.M
async def test_rebinding(server: Server, configuration: QuicConfiguration): async with connect(server.host, server.port, configuration=configuration) as protocol: # cause some traffic await protocol.ping() # replace transport protocol._transport.close() await loop.create_datagram_endpoint(lambda: protocol, local_addr=("::", 0)) # cause more traffic await protocol.ping() server.result |= Result.B
async def test_version_negotiation(server: Server, configuration: QuicConfiguration): # force version negotiation configuration.supported_versions.insert(0, 0x1A2A3A4A) async with connect(server.host, server.port, configuration=configuration) as protocol: await protocol.ping() # check log for stamp, category, event, data in configuration.quic_logger.to_dict( )["traces"][0]["events"]: if (category == "transport" and event == "packet_received" and data["packet_type"] == "version_negotiation"): server.result |= Result.V
async def run(host, port, path, **kwargs): async with connect(host, port, **kwargs) as connection: # perform HTTP/0.9 request reader, writer = await connection.create_stream() writer.write(("GET %s\r\n" % path).encode("utf8")) writer.write_eof() start = time.time() response = await reader.read() elapsed = time.time() - start print(response.decode("utf8")) octets = len(response) logger.info("Received %d bytes in %.1f s (%.3f Mbps)" % (octets, elapsed, octets * 8 / elapsed / 1000000))
async def test_version_negotiation(server: Server, configuration: QuicConfiguration): configuration.supported_versions = [ 0x1A2A3A4A, QuicProtocolVersion.DRAFT_22 ] async with connect(server.host, server.port, configuration=configuration) as protocol: await protocol.ping() # check log for stamp, category, event, data in configuration.quic_logger.to_dict( )["traces"][0]["events"]: if (category == "TRANSPORT" and event == "PACKET_RECEIVED" and data["packet_type"] == "VERSION_NEGOTIATION"): server.result |= Result.V
async def test_http_0(server: Server, configuration: QuicConfiguration): if server.path is None: return configuration.alpn_protocols = ["hq-22"] async with connect( server.host, server.port, configuration=configuration, create_protocol=HttpClient, ) as protocol: protocol = cast(HttpClient, protocol) # perform HTTP request events = await protocol.get(server.host, server.path) if events and isinstance(events[0], ResponseReceived): server.result |= Result.D
async def test_http_0(server: Server, configuration: QuicConfiguration): if server.path is None: return configuration.alpn_protocols = H0_ALPN async with connect( server.host, server.port, configuration=configuration, create_protocol=HttpClient, ) as protocol: protocol = cast(HttpClient, protocol) # perform HTTP request events = await protocol.get("https://{}:{}{}".format( server.host, server.port, server.path)) if events and isinstance(events[0], HeadersReceived): server.result |= Result.D
async def test_http_3(server: Server, configuration: QuicConfiguration): if server.path is None: return configuration.alpn_protocols = H3_ALPN async with connect( server.host, server.port, configuration=configuration, create_protocol=HttpClient, ) as protocol: protocol = cast(HttpClient, protocol) # perform HTTP request events = await protocol.get("https://{}:{}{}".format( server.host, server.port, server.path)) if events and isinstance(events[0], HeadersReceived): server.result |= Result.D server.result |= Result.three # perform more HTTP requests to use QPACK dynamic tables for i in range(2): events = await protocol.get("https://{}:{}{}".format( server.host, server.port, server.path)) if events and isinstance(events[0], HeadersReceived): http = cast(H3Connection, protocol._http) protocol._quic._logger.info( "QPACK decoder bytes RX %d TX %d", http._decoder_bytes_received, http._decoder_bytes_sent, ) protocol._quic._logger.info( "QPACK encoder bytes RX %d TX %d", http._encoder_bytes_received, http._encoder_bytes_sent, ) if (http._decoder_bytes_received and http._decoder_bytes_sent and http._encoder_bytes_received and http._encoder_bytes_sent): server.result |= Result.d # check push support if protocol.pushes: server.result |= Result.p
async def test_version_negotiation(config, **kwargs): async with connect( config.host, config.port, protocol_version=0x1A2A3A4A, **kwargs ) as connection: if connection._connection._version_negotiation_count == 1: config.result |= Result.V
async def test_handshake_and_close(config, **kwargs): async with connect(config.host, config.port, **kwargs) as connection: config.result |= Result.H if connection._connection._stateless_retry_count == 1: config.result |= Result.S config.result |= Result.C