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
Example #2
0
    # 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,
            ))
Example #3
0
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)
Example #4
0
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)
Example #5
0
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)