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
# prepare configuration configuration = QuicConfiguration( is_client=True, alpn_protocols=H0_ALPN if args.legacy_http else H3_ALPN) if args.ca_certs: configuration.load_verify_locations(args.ca_certs) if args.insecure: configuration.verify_mode = ssl.CERT_NONE if args.quic_log: configuration.quic_logger = QuicLogger() if args.secrets_log: configuration.secrets_log_file = open(args.secrets_log, "a") if args.session_ticket: try: with open(args.session_ticket, "rb") as fp: configuration.session_ticket = pickle.load(fp) except FileNotFoundError: pass if uvloop is not None: uvloop.install() loop = asyncio.get_event_loop() try: loop.run_until_complete( run( configuration=configuration, url=args.url, data=args.data, parallel=args.parallel, print_response=args.print_response, ))
async def run( configuration: QuicConfiguration, urls: List[str], data: str, include: bool, parallel: int, output_dir: Optional[str], ) -> None: url = urls[0] # parse URL parsed = urlparse(urls[0]) assert parsed.scheme in ( "https", "wss", ), "Only https:// or wss:// URLs are supported." if ":" in parsed.netloc: host, port_str = parsed.netloc.split(":") port = int(port_str) else: host = parsed.netloc port = 443 # we can either do 1rtt and 0rtt back to back (default) # or we can first do 1rtt, store the session ticket to disk # then, in a second call to this programme, do 0rtt only, reading the session ticket attempt_1rtt = True attempt_0rtt = True if args.session_ticket_read: attempt_1rtt = False attempt_0rtt = True elif args.session_ticket_write: attempt_1rtt = True attempt_0rtt = False global session_ticket if attempt_1rtt: async with connect( host, port, configuration=configuration, create_protocol=HttpClient, session_ticket_handler=save_session_ticket, ) as client: client = cast(HttpClient, client) # perform request coros = [ perform_http_request(client=client, url=urls[i], data=data, include=include, output_dir=output_dir, counter=i) for i in range(parallel) ] await asyncio.gather(*coros) client.close() await client.wait_closed() # end attempt_1rtt if attempt_0rtt: try: if args.session_ticket_read: logger.info("Read session ticket (delayed) from %s", args.session_ticket_read) session_ticket = configuration.session_ticket elif session_ticket is not None: # when neither reading nor writing, so doing connections back to back configuration.session_ticket = session_ticket else: logger.info( "----------------------------------------------------") logger.error( "No session ticket received, so not doing 0rtt, sorry") logger.error(session_ticket) return logger.info("------------------------------------------") logger.info("------------------------------------------") logger.info("------------------------------------------") logger.info("ATTEMPTING RESUMPTION WITH SESSION TICKET") logger.info(session_ticket) async with connect(host, port, configuration=configuration, create_protocol=HttpClient, session_ticket_handler=save_session_ticket, wait_connected=False) as client2: client2 = cast(HttpClient, client2) logger.info("Attempting 0RTT, not waiting until connected") if configuration.quic_logger is not None: client2._http._quic_logger.log_event( # this gets the correct trace category="transport", event="session_ticket_used", data={ "not_valid_after": str(session_ticket.not_valid_after), "not_valid_before": str(session_ticket.not_valid_before), "age_add": str(session_ticket.age_add), "server_name": session_ticket.server_name, "resumption_secret": str(session_ticket.resumption_secret), "cipher_suite": str(session_ticket.cipher_suite), "max_early_data_size": str(session_ticket.max_early_data_size), } ) allowance = "sendmemore0rtt_" * 370 # pylsqpack buffer size is 4096 bytes long, string is 15 chars, encodes down to less in utf8, 370 was experimentally defined # when cache busting on facebook (or other cdns), make sure the second url is different from the first if url.find("?buster=") >= 0: url += "nr2for0rtt" # amplification factor 0 = normal 0-RTT # 1 = 3.5 packets of 0-RTT # 2 = 7 packets of 0-RTT, split over 2 requests (because pylsqpack doesn't allow very large headers, so we do 2 requests to get the same result) headers = {} # headers["x-fb-debug"] = "True" # works, but headers are encrypted... so useless if zerortt_amplification_factor > 0: headers[ "x-0rtt-allowance"] = allowance # add a large header to make sure the 0-RTT request spans multiple packets (about 3.5 with the above header size) # ONLY ENABLE TO TEST THIS SCENARIO! # Also change the connection.py to that effect! # SHOULD NOT BE NEEDED FOR ALMOST ALL CASES! firstFlightOnly = False if firstFlightOnly: logger.info( "First flight only: aborting after 10 seconds to prevent stalling" ) futures = [ perform_http_request(client=client2, url=url, data=data, headers=headers, include=include, output_dir=output_dir, counter=0), asyncio.sleep(10) ] await asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED) elif zerortt_amplification_factor < 2: await perform_http_request(client=client2, url=url, data=data, headers=headers, include=include, output_dir=output_dir, counter=0) else: requests2 = [ perform_http_request( client=client2, url=url, data=data, include=include, output_dir=output_dir, counter=i, headers=headers, ) for i in range(zerortt_amplification_factor) ] await asyncio.gather(*requests2) if client2._quic.tls.session_resumed: logger.info("SESSION RESUMED SUCCESSFULLY!") else: logger.error("SESSION NOT RESUMED") if client2._quic.tls.early_data_accepted: logger.info("SESSION EARLY_DATA_ACCEPTED SUCCESSFULLY!") else: logger.error("EARLY_DATA NOT ACCEPTED?!?") client2.close() if not firstFlightOnly: await client2.wait_closed() # with client2 except ConnectionError as ce: logger.error("Connection error encountered") logger.error(ce)
async def run( configuration: QuicConfiguration, urls: List[str], data: str, include: bool, parallel: int, output_dir: Optional[str], ) -> None: url = urls[0] # parse URL parsed = urlparse(urls[0]) assert parsed.scheme in ( "https", "wss", ), "Only https:// or wss:// URLs are supported." if ":" in parsed.netloc: host, port_str = parsed.netloc.split(":") port = int(port_str) else: host = parsed.netloc port = 443 global session_ticket async with connect( host, port, configuration=configuration, create_protocol=HttpClient, session_ticket_handler=save_session_ticket, ) as client: client = cast(HttpClient, client) logger.info("Session ticket already known here?:") logger.info(session_ticket is not None) # perform request coros = [ perform_http_request(client=client, url=urls[i], data=data, include=include, output_dir=output_dir, counter=i) for i in range(parallel) ] await asyncio.gather(*coros) try: if session_ticket is not None: configuration.session_ticket = session_ticket logger.info("------------------------------------------") logger.info("------------------------------------------") logger.info("------------------------------------------") logger.info("ATTEMPTING RESUMPTION WITH SESSION TICKET") async with connect(host, port, configuration=configuration, create_protocol=HttpClient, session_ticket_handler=save_session_ticket, wait_connected=False) as client2: client2 = cast(HttpClient, client2) logger.info("Attempting 0RTT, not waiting until connected") if configuration.quic_logger is not None: client2._http._quic_logger.log_event( # this gets the correct trace category="transport", event="session_ticket_used", data={ "not_valid_after": str(session_ticket.not_valid_after), "not_valid_before": str(session_ticket.not_valid_before), "age_add": str(session_ticket.age_add), "server_name": session_ticket.server_name, "resumption_secret": str(session_ticket.resumption_secret), "cipher_suite": str(session_ticket.cipher_suite), "max_early_data_size": str(session_ticket.max_early_data_size), } ) allowance = "sendmemore0rtt_" * 370 # pylsqpack buffer size is 4096 bytes long, string is 15 chars, encodes down to less in utf8, 370 was experimentally defined # when cache busting on facebook (or other cdns), make sure the second url is different from the first if url.find("?buster=") >= 0: url += "nr2for0rtt" # amplification factor 0 = normal 0-RTT # 1 = 3.5 packets of 0-RTT # 2 = 7 packets of 0-RTT, split over 2 requests (because pylsqpack doesn't allow very large headers, so we do 2 requests to get the same result) headers = {} # headers["x-fb-debug"] = "True" # works, but headers are encrypted... so useless if zerortt_amplification_factor > 0: headers[ "x-0rtt-allowance"] = allowance # add a large header to make sure the 0-RTT request spans multiple packets (about 3.5 with the above header size) if zerortt_amplification_factor < 2: await perform_http_request(client=client2, url=url, data=data, headers=headers, include=include, output_dir=output_dir, counter=0) else: requests2 = [ perform_http_request( client=client2, url=url, data=data, include=include, output_dir=output_dir, counter=i, headers=headers, ) for i in range(zerortt_amplification_factor) ] await asyncio.gather(*requests2) # response = await client2.get( url=url, headers=headers ) # response = await client2.get( "/6000" ) # await client2.wait_connected() # logger.info("DONE GETTING STUFF") # logger.info( response ) # await client2.wait_connected() if client2._quic.tls.session_resumed: logger.info("SESSION RESUMED SUCCESSFULLY!") else: logger.error("SESSION NOT RESUMED") if client2._quic.tls.early_data_accepted: logger.info( "SESSION EARLY_DATA_ACCEPTED SUCCESSFULLY!") else: logger.error("EARLY_DATA NOT ACCEPTED?!?") client2.close() await client2.wait_closed() else: logger.info( "----------------------------------------------------") logger.error( "No session ticket received, so not doing 0rtt, sorry") logger.error(session_ticket) except ConnectionError as ce: logger.error("Connection error encountered") logger.error(ce)
def transaction(topic_name, topic_text, parts_list): async def perform_http_request(client: HttpClient, url: str, data: str, print_response: bool) -> None: # perform request start = time.time() if data is not None: http_events = await client.post( url, data=data.encode(), headers={"content-type": "application/x-www-form-urlencoded"}, ) else: http_events = await client.get(url) elapsed = time.time() - start # print speed octets = 0 for http_event in http_events: if isinstance(http_event, DataReceived): octets += len(http_event.data) logger.info("Received %d bytes in %.1f s (%.3f Mbps)" % (octets, elapsed, octets * 8 / elapsed / 1000000)) # print response if print_response: for http_event in http_events: if isinstance(http_event, HeadersReceived): headers = b"" for k, v in http_event.headers: headers += k + b": " + v + b"\r\n" if headers: sys.stderr.buffer.write(headers + b"\r\n") sys.stderr.buffer.flush() elif isinstance(http_event, DataReceived): sys.stdout.buffer.write(http_event.data) sys.stdout.buffer.flush() def save_session_ticket(ticket): """ Callback which is invoked by the TLS engine when a new session ticket is received. """ logger.info("New session ticket received") if args.session_ticket: with open(args.session_ticket, "wb") as fp: pickle.dump(ticket, fp) async def run( configuration: QuicConfiguration, url: str, data: str, parallel: int, print_response: bool, ) -> None: # parse URL parsed = urlparse(url) assert parsed.scheme in ( "https", "wss", ), "Only https:// or wss:// URLs are supported." if ":" in parsed.netloc: host, port_str = parsed.netloc.split(":") port = int(port_str) else: host = parsed.netloc port = 443 async with connect( host, port, configuration=configuration, create_protocol=HttpClient, session_ticket_handler=save_session_ticket, ) as client: client = cast(HttpClient, client) if parsed.scheme == "wss": ws = await client.websocket(url, subprotocols=["chat", "superchat"]) # print("Hint: To send a message type and press enter.") # ****************************** # *** ASYNCHRONOUS THREADING *** def start_loop(loop): asyncio.set_event_loop(loop) loop.run_forever() new_loop = asyncio.new_event_loop() t = Thread(target=start_loop, args=(new_loop, )) t.start() async def read_user(): # while True: message = add_item(topic_name, topic_text) # message = add_item() await ws.send(message) # CheckLogin() asyncio.run_coroutine_threadsafe(read_user(), new_loop) # *** STAYS IN MAIN LOOP *** while True: messageRec = await ws.recv() print("< " + messageRec) if messageRec == "inserted_item": print("client update list") root = Tk() app = App(root) app.update_text() root.mainloop() # *** ASYNCHRONOUS THREADING *** # ******************************''' await ws.close() else: # perform request coros = [ perform_http_request(client=client, url=url, data=data, print_response=print_response) for i in range(parallel) ] await asyncio.gather(*coros) if __name__ == "__main__": parser = argparse.ArgumentParser(description="HTTP/3 client") parser.add_argument("url", type=str, help="the URL to query (must be HTTPS)") parser.add_argument( "--ca-certs", type=str, help="load CA certificates from the specified file") parser.add_argument("-d", "--data", type=str, help="send the specified data in a POST request") parser.add_argument( "-k", "--insecure", action="store_true", help="do not validate server certificate", ) parser.add_argument("--legacy-http", action="store_true", help="use HTTP/0.9") parser.add_argument("-q", "--quic-log", type=str, help="log QUIC events to a file in QLOG format") parser.add_argument( "-l", "--secrets-log", type=str, help="log secrets to a file, for use with Wireshark", ) parser.add_argument("--parallel", type=int, default=1, help="perform this many requests in parallel") parser.add_argument("--print-response", action="store_true", help="print response headers and body") parser.add_argument( "-s", "--session-ticket", type=str, help="read and write session ticket from the specified file", ) parser.add_argument("-v", "--verbose", action="store_true", help="increase logging verbosity") args = parser.parse_args() logging.basicConfig( format="%(asctime)s %(levelname)s %(name)s %(message)s", level=logging.DEBUG if args.verbose else logging.INFO, ) # prepare configuration configuration = QuicConfiguration( is_client=True, alpn_protocols=H0_ALPN if args.legacy_http else H3_ALPN) if args.ca_certs: configuration.load_verify_locations(args.ca_certs) if args.insecure: configuration.verify_mode = ssl.CERT_NONE if args.quic_log: configuration.quic_logger = QuicLogger() if args.secrets_log: configuration.secrets_log_file = open(args.secrets_log, "a") if args.session_ticket: try: with open(args.session_ticket, "rb") as fp: configuration.session_ticket = pickle.load(fp) except FileNotFoundError: pass if uvloop is not None: uvloop.install() loop = asyncio.get_event_loop() try: loop.run_until_complete( run( configuration=configuration, url=args.url, data=args.data, parallel=args.parallel, print_response=args.print_response, )) finally: if configuration.quic_logger is not None: with open(args.quic_log, "w") as logger_fp: json.dump(configuration.quic_logger.to_dict(), logger_fp, indent=4)