From 0bbe6a9d0a74e5f50a9150dfc38079d3b3c966db Mon Sep 17 00:00:00 2001 From: huitema Date: Mon, 19 Aug 2024 16:47:41 -0700 Subject: [PATCH 1/8] First cut at sample doc. --- doc/sample.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 doc/sample.md diff --git a/doc/sample.md b/doc/sample.md new file mode 100644 index 000000000..45bf573da --- /dev/null +++ b/doc/sample.md @@ -0,0 +1,31 @@ +# The picoquic code sample + +The picoquic sample code in the "sample" folder builds a single executable, `picoquic_sample`. +This is a simple file transfer program that can be +instantiated in client or server mode. The program can be instantiated +as either: +~~~ +picoquic_sample client server_name port folder *queried_file +~~~ +or: +~~~ +picoquic_sample server port cert_file private_key_file folder +~~~ +The client opens a quic connection to the server, and then fetches +the listed files. The client opens one bidir client stream for each +file, writes the requested file name in the stream data, and then +marks the stream as finished. The server reads the file name, and +if the named file is present in the server's folder, sends the file +content on the same stream, marking the fin of the stream when all +bytes are sent. If the file is not available, the server resets the +stream. If the client receives the file, it writes its content in the +client's folder. + +Server or client close the connection if it remains inactive for +more than 10 seconds. + +The purpose of the sample is not to provide example of using the +picoquic API to build a simple application. The current code is +limited: it does not use the "configuration" API to set parameters +of the picoquic endpoint, and it does not demonstrate how to run +picoquic in a background thread. \ No newline at end of file From ec1b6922bf57bb9c71eb3798e8c70e00e8116fa1 Mon Sep 17 00:00:00 2001 From: huitema Date: Mon, 26 Aug 2024 11:10:03 -0700 Subject: [PATCH 2/8] Prepare for background option --- sample/sample.c | 13 ++++ sample/sample_client.c | 134 ++++++++++++++++++++++++++++++++++++++++- sample/sample_server.c | 2 + 3 files changed, 147 insertions(+), 2 deletions(-) diff --git a/sample/sample.c b/sample/sample.c index d93dfe322..3c3236ed6 100644 --- a/sample/sample.c +++ b/sample/sample.c @@ -50,6 +50,7 @@ static void usage(char const * sample_name) { fprintf(stderr, "Usage:\n"); fprintf(stderr, " %s client server_name port folder *queried_file\n", sample_name); + fprintf(stderr, " %s background server_name port folder\n", sample_name); fprintf(stderr, "or :\n"); fprintf(stderr, " %s server port cert_file private_key_file folder\n", sample_name); exit(1); @@ -88,6 +89,18 @@ int main(int argc, char** argv) exit_code = picoquic_sample_client(argv[2], server_port, argv[4], nb_files, file_names); } } +#if 0 + else if (strcmp(argv[1], "background") == 0) { + if (argc != 4) { + usage(argv[0]); + } + else { + int server_port = get_port(argv[0], argv[3]); + + exit_code = picoquic_sample_background(argv[2], server_port, argv[4], nb_files, file_names); + } + } +#endif else if (strcmp(argv[1], "server") == 0) { if (argc < 5) { usage(argv[0]); diff --git a/sample/sample_client.c b/sample/sample_client.c index 044355011..bdf65af6f 100644 --- a/sample/sample_client.c +++ b/sample/sample_client.c @@ -88,6 +88,7 @@ typedef struct st_sample_client_stream_ctx_t { } sample_client_stream_ctx_t; typedef struct st_sample_client_ctx_t { + char const* default_dir; char const** file_names; sample_client_stream_ctx_t* first_stream; @@ -430,11 +431,114 @@ static int sample_client_loop_cb(picoquic_quic_t* quic, picoquic_packet_loop_cb_ return ret; } -/* Client: +/* Prepare the context used by both the simple client and the background client.: * - Create the QUIC context. * - Open the sockets * - Find the server's address - * - Create a client context and a client connection. + * - Initialize the client context and create a client connection. + */ +static sample_client_init(char const* server_name, int server_port, char const* default_dir, + char const* ticket_store_filename, char const* token_store_filename, + struct sockaddr_storage * server_address, picoquic_quic_t** quic, picoquic_cnx_t** cnx, sample_client_ctx_t *client_ctx) +{ + int ret = 0; + char const* sni = PICOQUIC_SAMPLE_SNI; + char const* qlog_dir = PICOQUIC_SAMPLE_CLIENT_QLOG_DIR; + uint64_t current_time = picoquic_current_time(); + + *quic = NULL; + *cnx = NULL; + + /* Get the server's address */ + if (ret == 0) { + int is_name = 0; + + ret = picoquic_get_server_address(server_name, server_port, server_address, &is_name); + if (ret != 0) { + fprintf(stderr, "Cannot get the IP address for <%s> port <%d>", server_name, server_port); + } + else if (is_name) { + sni = server_name; + } + } + + /* Create a QUIC context. It could be used for many connections, but in this sample we + * will use it for just one connection. + * The sample code exercises just a small subset of the QUIC context configuration options: + * - use files to store tickets and tokens in order to manage retry and 0-RTT + * - set the congestion control algorithm to BBR + * - enable logging of encryption keys for wireshark debugging. + * - instantiate a binary log option, and log all packets. + */ + if (ret == 0) { + *quic = picoquic_create(1, NULL, NULL, NULL, PICOQUIC_SAMPLE_ALPN, NULL, NULL, + NULL, NULL, NULL, current_time, NULL, + ticket_store_filename, NULL, 0); + + if (*quic == NULL) { + fprintf(stderr, "Could not create quic context\n"); + ret = -1; + } + else { + if (picoquic_load_retry_tokens(*quic, token_store_filename) != 0) { + fprintf(stderr, "No token file present. Will create one as <%s>.\n", token_store_filename); + } + + picoquic_set_default_congestion_algorithm(*quic, picoquic_bbr_algorithm); + + picoquic_set_key_log_file_from_env(*quic); + picoquic_set_qlog(*quic, qlog_dir); + picoquic_set_log_level(*quic, 1); + } + } + /* Initialize the callback context and create the connection context. + * We use minimal options on the client side, keeping the transport + * parameter values set by default for picoquic. This could be fixed later. + */ + + if (ret == 0) { + client_ctx->default_dir = default_dir; + + printf("Starting connection to %s, port %d\n", server_name, server_port); + + /* Create a client connection */ + *cnx = picoquic_create_cnx(*quic, picoquic_null_connection_id, picoquic_null_connection_id, + (struct sockaddr*)server_address, current_time, 0, sni, PICOQUIC_SAMPLE_ALPN, 1); + + if (*cnx == NULL) { + fprintf(stderr, "Could not create connection context\n"); + ret = -1; + } + else { + /* Set the client callback context */ + picoquic_set_callback(*cnx, sample_client_callback, client_ctx); + /* Client connection parameters could be set here, before starting the connection. */ + ret = picoquic_start_client_cnx(*cnx); + if (ret < 0) { + fprintf(stderr, "Could not activate connection\n"); + } + else { + /* Printing out the initial CID, which is used to identify log files */ + picoquic_connection_id_t icid = picoquic_get_initial_cnxid(*cnx); + printf("Initial connection ID: "); + for (uint8_t i = 0; i < icid.id_len; i++) { + printf("%02x", icid.id[i]); + } + printf("\n"); + } + } + } + + return ret; +} + +/* Client: + * - Call the init function to: + * - Create the QUIC context. + * - Open the sockets + * - Find the server's address + * - Create a client context and a client connection. + * - Initialize the list of required files based on the CLI parameters. * - On a forever loop: * - get the next wakeup time * - wait for arrival of message on sockets until that time @@ -448,6 +552,18 @@ int picoquic_sample_client(char const * server_name, int server_port, char const int nb_files, char const ** file_names) { int ret = 0; +#if 1 + struct sockaddr_storage server_address; + picoquic_quic_t* quic = NULL; + picoquic_cnx_t* cnx = NULL; + sample_client_ctx_t client_ctx = { 0 }; + char const* ticket_store_filename = PICOQUIC_SAMPLE_CLIENT_TICKET_STORE; + char const* token_store_filename = PICOQUIC_SAMPLE_CLIENT_TOKEN_STORE; + + ret = sample_client_init(server_name, server_port, default_dir, + ticket_store_filename, token_store_filename, + &server_address, &quic, &cnx, &client_ctx); +#else struct sockaddr_storage server_address; char const* sni = PICOQUIC_SAMPLE_SNI; picoquic_quic_t* quic = NULL; @@ -548,6 +664,20 @@ int picoquic_sample_client(char const * server_name, int server_port, char const } } } +#endif + if (ret == 0) { + /* Initialize all the streams contexts from the list of streams passed on the API. */ + client_ctx.file_names = file_names; + client_ctx.nb_files = nb_files; + + /* Create a stream context for all the files that should be downloaded */ + for (int i = 0; ret == 0 && i < client_ctx.nb_files; i++) { + ret = sample_client_create_stream(cnx, &client_ctx, i); + if (ret < 0) { + fprintf(stderr, "Could not initiate stream for fi\n"); + } + } + } /* Wait for packets */ ret = picoquic_packet_loop(quic, 0, server_address.ss_family, 0, 0, 0, sample_client_loop_cb, &client_ctx); diff --git a/sample/sample_server.c b/sample/sample_server.c index 408e5af84..1bf4058d7 100644 --- a/sample/sample_server.c +++ b/sample/sample_server.c @@ -278,6 +278,7 @@ int sample_server_callback(picoquic_cnx_t* cnx, /* If fin, mark read, check the file, open it. Or reset if there is no such file */ stream_ctx->file_name[stream_ctx->name_length + 1] = 0; stream_ctx->is_name_read = 1; + printf("File requested: <%s>\n", stream_ctx->file_name); stream_ret = sample_server_open_stream(server_ctx, stream_ctx); if (stream_ret == 0) { @@ -390,6 +391,7 @@ int picoquic_sample_server(int server_port, const char* server_cert, const char* default_context.default_dir_len = strlen(default_dir); printf("Starting Picoquic Sample server on port %d\n", server_port); + printf("Serving files from %s\n", default_dir); /* Create the QUIC context for the server */ current_time = picoquic_current_time(); From 5f158f13dc3a689fb92c86222d29bfc591cf8b13 Mon Sep 17 00:00:00 2001 From: huitema Date: Mon, 26 Aug 2024 16:36:07 -0700 Subject: [PATCH 3/8] Memory and copies doc --- doc/managing-memory-copies.md | 87 +++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 doc/managing-memory-copies.md diff --git a/doc/managing-memory-copies.md b/doc/managing-memory-copies.md new file mode 100644 index 000000000..97d4c2600 --- /dev/null +++ b/doc/managing-memory-copies.md @@ -0,0 +1,87 @@ +# Managing Memory Copies + +Picoquic is designed to limit the number of memory copies and +related memory allocations when sending and receiving data. + +## Memory copies when sending data + +Application data is sent as either data frames or stream frames. +The sending process will format packets, send them, and +also keep them in memory for possible repetitions. + +The typical flow would be: + +1. The application makes the data available, either by calling the API `picoquic_add_to_stream` + which allocates memory and keep a copy, or by signalling that data is available on a stream + using `picoquic_mark_active_stream`, or by signalling that datagrams are ready to send + using `picoquic_mark_datagram_ready`. +2. The socket loop wakes up and calls the API `picoquic_prepare_next_packet_ex` + to ask the stack to prepare the next packet in the QUIC context. It provides + a data buffer in which the packet will be copied before being sent. +3. The QUIC context selects the next avalaible connection, and will + call the function `picoquic_prepare_packet_ex` to prepare the next packet for that connection. +4. That function allocates a packet container of type `picoquic_packet_t`, which will + contain the formatted packet. +5. The formatting happens in the functions called from there, which will copy + a set of QUIC frames in the packet, including datagram or stream data frames. +6. The content of the stream frames is either copied from data previously queued + using the `picoquic_add_to_stream` API, or copied directly from the application + memory using a callback `picoquic_callback_prepare_to_send` for streams, or + `picoquic_callback_prepare_datagram` for datagrams. +7. When the packet is ready, the stack encrypt it. The clear text in the `packet` structure + is left untouched, and the encrypted bytes are copied into the "send" buffer + passed in `picoquic_prepare_next_packet_ex` call. That buffer will be sent to + the peer through a socket call. +8. The clear text packet is attached to the retransmission queue, waiting for acknowledgement. + +### Handling packet losses + +In most cases, the clear text packet will be detached from the retransmission queue when the +acknowledgement is received. In some cases, the acknowledgement is not received, and +the data will have to be resent. For stream data, this will involve copying the stream data +from the old copy into a new packet. + +### Recycling packets + +When packets are acknowledged, the `picoquic_packet_t` element is "recycled". It is added +to a queue of empty packets managed in the Quic context, unless that queue has already +reached its maximum size, in which case the packets are freed. New packet structures are +only allocated when no recycled packet is available. + +## Memory copies when receiving data. + +The picoquic stack receives encrypted packets from the network, and delivers +decrypted data to the application. + +The typical flow would be: + +1. A new network packet is received from the socket, and is passed to the + stack through a call to `picoquic_incoming_packet`. +2. The stack allocates a data node container of type `picoquic_stream_data_node_t`. +3. The header is analyzed and the packet is decrypted, with the clear text data + is stored into the data node. +4. The decrypted packet is parsed, and the frames that it contained are processed. + The content of datagram frames is passed to the application through the + callback `picoquic_callback_datagram`. The processing of stream data frames varies, + because stream data must be delivered in order. +5. If the stream data is arriving in order, the data is delivered immediately, + through the callback `picoquic_callback_stream_data` or `picoquic_callback_stream_fin`. +6. If the data cannot be delivered immediately, it needs to be kept in memory + until the holes in the stream have been filled. The processing varies + depending on the number of frames in the packet. +7. If the stream data frame is the last frame in the packet, the data node + structured in queued to the stream. Else, a new data node is allocated, + the stream data frame is copied to it, and that new data node is queued + to the stream. +8. At the end of this process, if the data node was not queued to a stream it + is recycled. + +### Managing out of order delivery + +When stream data frames arrive out of order, one data node is queued for +each incoming frame. When a hole filling frame arrives, the data is delivered +through the callback `picoquic_callback_stream_data` and the data node +is recycled. The data nodes will also be recycled if the stream is reset +or the connection is closed. + + \ No newline at end of file From 8850a50ed12e2a0983059bc4c8f26a60e91e1702 Mon Sep 17 00:00:00 2001 From: huitema Date: Tue, 27 Aug 2024 17:16:08 -0700 Subject: [PATCH 4/8] Add sample with picoquic in background --- CMakeLists.txt | 1 + sample/picoquic_sample.h | 4 + sample/sample.c | 9 +- sample/sample.vcxproj | 1 + sample/sample.vcxproj.filters | 3 + sample/sample_background.c | 717 ++++++++++++++++++++++++++++++++++ sample/sample_client.c | 113 +----- 7 files changed, 737 insertions(+), 111 deletions(-) create mode 100644 sample/sample_background.c diff --git a/CMakeLists.txt b/CMakeLists.txt index f126d16b2..a6a8da30d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -426,6 +426,7 @@ if(BUILD_TESTING AND picoquic_BUILD_TESTS) add_executable(picoquic_sample sample/sample.c + sample/sample_background.c sample/sample_client.c sample/sample_server.c) target_link_libraries(picoquic_sample PRIVATE picoquic-log picoquic-core) diff --git a/sample/picoquic_sample.h b/sample/picoquic_sample.h index 468cd124e..5a4b82159 100644 --- a/sample/picoquic_sample.h +++ b/sample/picoquic_sample.h @@ -43,9 +43,13 @@ extern "C" { #define PICOQUIC_SAMPLE_CLIENT_QLOG_DIR "."; #define PICOQUIC_SAMPLE_SERVER_QLOG_DIR "."; +#define PICOQUIC_SAMPLE_BACKGROUND_MAX_FILES 32 + int picoquic_sample_client(char const* server_name, int server_port, char const* default_dir, int nb_files, char const** file_names); +int picoquic_sample_background(char const* server_name, int server_port, char const* default_dir); + int picoquic_sample_server(int server_port, const char* pem_cert, const char* pem_key, const char * default_dir); #ifdef __cplusplus diff --git a/sample/sample.c b/sample/sample.c index 3c3236ed6..6a6cd066c 100644 --- a/sample/sample.c +++ b/sample/sample.c @@ -66,6 +66,7 @@ int get_port(char const* sample_name, char const* port_arg) return server_port; } + int main(int argc, char** argv) { int exit_code = 0; @@ -89,20 +90,18 @@ int main(int argc, char** argv) exit_code = picoquic_sample_client(argv[2], server_port, argv[4], nb_files, file_names); } } -#if 0 else if (strcmp(argv[1], "background") == 0) { - if (argc != 4) { + if (argc != 5) { usage(argv[0]); } else { int server_port = get_port(argv[0], argv[3]); - exit_code = picoquic_sample_background(argv[2], server_port, argv[4], nb_files, file_names); + exit_code = picoquic_sample_background(argv[2], server_port, argv[4]); } } -#endif else if (strcmp(argv[1], "server") == 0) { - if (argc < 5) { + if (argc != 6) { usage(argv[0]); } else { diff --git a/sample/sample.vcxproj b/sample/sample.vcxproj index 8ac29d235..7b07a1941 100644 --- a/sample/sample.vcxproj +++ b/sample/sample.vcxproj @@ -168,6 +168,7 @@ + diff --git a/sample/sample.vcxproj.filters b/sample/sample.vcxproj.filters index 0d341c5a9..d2812b55c 100644 --- a/sample/sample.vcxproj.filters +++ b/sample/sample.vcxproj.filters @@ -24,5 +24,8 @@ Source Files + + Source Files + \ No newline at end of file diff --git a/sample/sample_background.c b/sample/sample_background.c new file mode 100644 index 000000000..a1170c602 --- /dev/null +++ b/sample/sample_background.c @@ -0,0 +1,717 @@ +/* +* Author: Christian Huitema +* Copyright (c) 2020, Private Octopus, Inc. +* All rights reserved. +* +* Permission to use, copy, modify, and distribute this software for any +* purpose with or without fee is hereby granted, provided that the above +* copyright notice and this permission notice appear in all copies. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL Private Octopus, Inc. BE LIABLE FOR ANY +* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* The "sample" project builds a simple file transfer program that can be + * instantiated in client or server mode. The "sample_client" implements + * the client components of the sample application. + * + * Developing the client requires two main components: + * - the client "callback" that implements the client side of the + * application protocol, managing the client side application context + * for the connection. + * - the client loop, that reads messages on the socket, submits them + * to the Quic context, let the client prepare messages, and send + * them on the appropriate socket. + * + * The Sample Client uses the "qlog" option to produce Quic Logs as defined + * in https://datatracker.ietf.org/doc/draft-marx-qlog-event-definitions-quic-h3/. + * This is an optional feature, which requires linking with the "loglib" library, + * and using the picoquic_set_qlog() API defined in "autoqlog.h". When a connection + * completes, the code saves the log as a file named after the Initial Connection + * ID (in hexa), with the suffix ".client.qlog". + */ + +#include +#include +#include +#include +#include +#include +#include +#include "picoquic_sample.h" + + /* Background thread management: + * + * The "background" sample is an example of application split + * between application threads for the application logic, and a + * background thread for the picoquic stack. + * + * In the "background client" mode, the files are requested from + * the UI thread, but the corresponding streams are created in the + * background thread. The shared state between the two + * threads is kept in the "background context", visible by + * both threads, and all interactions with the stack happen + * in the background thread, because the stack code is not + * thread safe. + * + * The goal of the sample is not to demonstrate distributed programming + * techniques. In a real application, the code running in the background + * would probably send messages and events to the application, but that + * kind of code is very application specific. In our sample, we use a simple + * ad hoc coordination between the two threads: + * - the variable "nb_files" documents the number of files + * that have been documented by the client. It is updated by + * the UI thread when a new file is requested. + * - the variable "nb_files_processed" documents the number + * of files for which a stream has been created. It is + * updated by the UI thread. + * - the flag "is_closing_requested" is set by the application + * when it wants to close the connection. The background thread + * will call the `picoquic_close` API in the background thread, + * set the flag `is_closing` after that, and set the flag + * `is_disconnected` when the stack has completed the closing. + * After the application updates the shared state, it calls the + * API `picoquic_wake_up_network_thread`. This cause the background + * thread to stop waiting for packets or timeout, and to issue + * a loop callback `picoquic_packet_loop_wake_up`, during which the + * application provided call can interact safely with the + * picoquic stack. + */ + +typedef struct st_sample_background_stream_ctx_t { + struct st_sample_background_stream_ctx_t* next_stream; + size_t file_rank; + uint64_t stream_id; + size_t name_length; + size_t name_sent_length; + FILE* F; + size_t bytes_received; + uint64_t remote_error; + unsigned int is_name_sent : 1; + unsigned int is_file_open : 1; + unsigned int is_stream_reset : 1; + unsigned int is_stream_finished : 1; +} sample_background_stream_ctx_t; + +typedef struct st_sample_background_ctx_t { + unsigned int is_closing : 1; + unsigned int is_closing_requested : 1; + picoquic_cnx_t* cnx; + char const* default_dir; + char const** file_names; + sample_background_stream_ctx_t* first_stream; + sample_background_stream_ctx_t* last_stream; + int nb_files; + int nb_files_processed; + int nb_files_received; + int nb_files_failed; + int is_disconnected; +} sample_background_ctx_t; + +static int sample_background_create_stream(picoquic_cnx_t* cnx, + sample_background_ctx_t* client_ctx, int file_rank) +{ + int ret = 0; + sample_background_stream_ctx_t* stream_ctx = (sample_background_stream_ctx_t*) + malloc(sizeof(sample_background_stream_ctx_t)); + + if (stream_ctx == NULL) { + fprintf(stdout, "Memory Error, cannot create stream for file number %d\n", (int)file_rank); + ret = -1; + } + else { + memset(stream_ctx, 0, sizeof(sample_background_stream_ctx_t)); + if (client_ctx->first_stream == NULL) { + client_ctx->first_stream = stream_ctx; + client_ctx->last_stream = stream_ctx; + } + else { + client_ctx->last_stream->next_stream = stream_ctx; + client_ctx->last_stream = stream_ctx; + } + stream_ctx->file_rank = file_rank; + stream_ctx->stream_id = picoquic_get_next_local_stream_id(client_ctx->cnx, 0); + stream_ctx->name_length = strlen(client_ctx->file_names[file_rank]); + + /* Mark the stream as active. The callback will be asked to provide data when + * the connection is ready. */ + ret = picoquic_mark_active_stream(cnx, stream_ctx->stream_id, 1, stream_ctx); + if (ret != 0) { + fprintf(stdout, "Error %d, cannot initialize stream for file number %d\n", ret, (int)file_rank); + } + } + + return ret; +} + +static void sample_background_report(sample_background_ctx_t* client_ctx) +{ + sample_background_stream_ctx_t* stream_ctx = client_ctx->first_stream; + + while (stream_ctx != NULL) { + char const* status; + if (stream_ctx->is_stream_finished) { + status = "complete"; + } + else if (stream_ctx->is_stream_reset) { + status = "reset"; + } + else { + status = "unknown status"; + } + printf("%s: %s, received %zu bytes", client_ctx->file_names[stream_ctx->file_rank], status, stream_ctx->bytes_received); + if (stream_ctx->is_stream_reset && stream_ctx->remote_error != PICOQUIC_SAMPLE_NO_ERROR){ + char const* error_text = "unknown error"; + switch (stream_ctx->remote_error) { + case PICOQUIC_SAMPLE_INTERNAL_ERROR: + error_text = "internal error"; + break; + case PICOQUIC_SAMPLE_NAME_TOO_LONG_ERROR: + error_text = "internal error"; + break; + case PICOQUIC_SAMPLE_NO_SUCH_FILE_ERROR: + error_text = "no such file"; + break; + case PICOQUIC_SAMPLE_FILE_READ_ERROR: + error_text = "file read error"; + break; + case PICOQUIC_SAMPLE_FILE_CANCEL_ERROR: + error_text = "cancelled"; + break; + default: + break; + } + printf(", error 0x%" PRIx64 " -- %s", stream_ctx->remote_error, error_text); + } + printf("\n"); + stream_ctx = stream_ctx->next_stream; + } +} + +static void sample_background_free_context(sample_background_ctx_t* client_ctx) +{ + sample_background_stream_ctx_t* stream_ctx; + + while ((stream_ctx = client_ctx->first_stream) != NULL) { + client_ctx->first_stream = stream_ctx->next_stream; + if (stream_ctx->F != NULL) { + (void)picoquic_file_close(stream_ctx->F); + } + free(stream_ctx); + } + client_ctx->last_stream = NULL; +} + + +int sample_background_callback(picoquic_cnx_t* cnx, + uint64_t stream_id, uint8_t* bytes, size_t length, + picoquic_call_back_event_t fin_or_event, void* callback_ctx, void* v_stream_ctx) +{ + int ret = 0; + sample_background_ctx_t* client_ctx = (sample_background_ctx_t*)callback_ctx; + sample_background_stream_ctx_t* stream_ctx = (sample_background_stream_ctx_t*)v_stream_ctx; + + if (client_ctx == NULL) { + /* This should never happen, because the callback context for the client is initialized + * when creating the client connection. */ + return -1; + } + + if (ret == 0) { + switch (fin_or_event) { + case picoquic_callback_stream_data: + case picoquic_callback_stream_fin: + /* Data arrival on stream #x, maybe with fin mark */ + if (stream_ctx == NULL) { + /* This is unexpected, as all contexts were declared when initializing the + * connection. */ + return -1; + } + else if (!stream_ctx->is_name_sent) { + /* Unexpected: should not receive data before sending the file name to the server */ + return -1; + } + else if (stream_ctx->is_stream_reset || stream_ctx->is_stream_finished) { + /* Unexpected: receive after fin */ + return -1; + } + else + { + if (stream_ctx->F == NULL) { + /* Open the file to receive the data. This is done at the last possible moment, + * to minimize the number of files open simultaneously. + * When formatting the file_path, verify that the directory name is zero-length, + * or terminated by a proper file separator. + */ + char file_path[1024]; + size_t dir_len = strlen(client_ctx->default_dir); + size_t file_name_len = strlen(client_ctx->file_names[stream_ctx->file_rank]); + + if (dir_len > 0 && dir_len < sizeof(file_path)) { + memcpy(file_path, client_ctx->default_dir, dir_len); + if (file_path[dir_len - 1] != PICOQUIC_FILE_SEPARATOR[0]) { + file_path[dir_len] = PICOQUIC_FILE_SEPARATOR[0]; + dir_len++; + } + } + + if (dir_len + file_name_len + 1 >= sizeof(file_path)) { + /* Unexpected: could not format the file name */ + fprintf(stderr, "Could not format the file path.\n"); + ret = -1; + } else { + memcpy(file_path + dir_len, client_ctx->file_names[stream_ctx->file_rank], + file_name_len); + file_path[dir_len + file_name_len] = 0; + stream_ctx->F = picoquic_file_open(file_path, "wb"); + + if (stream_ctx->F == NULL) { + /* Could not open the file */ + fprintf(stderr, "Could not open the file: %s\n", file_path); + ret = -1; + } + } + } + + if (ret == 0 && length > 0) { + /* write the received bytes to the file */ + if (fwrite(bytes, length, 1, stream_ctx->F) != 1) { + /* Could not write file to disk */ + fprintf(stderr, "Could not write data to disk.\n"); + ret = -1; + } + else { + stream_ctx->bytes_received += length; + } + } + + if (ret == 0 && fin_or_event == picoquic_callback_stream_fin) { + stream_ctx->F = picoquic_file_close(stream_ctx->F); + stream_ctx->is_stream_finished = 1; + client_ctx->nb_files_received++; + } + } + break; + case picoquic_callback_stop_sending: /* Should not happen, treated as reset */ + /* Mark stream as abandoned, close the file, etc. */ + picoquic_reset_stream(cnx, stream_id, 0); + /* Fall through */ + case picoquic_callback_stream_reset: /* Server reset stream #x */ + if (stream_ctx == NULL) { + /* This is unexpected, as all contexts were declared when initializing the + * connection. */ + return -1; + } + else if (stream_ctx->is_stream_reset || stream_ctx->is_stream_finished) { + /* Unexpected: receive after fin */ + return -1; + } + else { + stream_ctx->remote_error = picoquic_get_remote_stream_error(cnx, stream_id); + stream_ctx->is_stream_reset = 1; + client_ctx->nb_files_failed++; + } + break; + case picoquic_callback_stateless_reset: + case picoquic_callback_close: /* Received connection close */ + case picoquic_callback_application_close: /* Received application close */ + client_ctx->is_disconnected = 1; + /* Remove the application callback */ + picoquic_set_callback(cnx, NULL, NULL); + break; + case picoquic_callback_version_negotiation: + /* The client did not get the right version. + * TODO: some form of negotiation? + */ + fprintf(stderr, "Received a version negotiation request:"); + for (size_t byte_index = 0; byte_index + 4 <= length; byte_index += 4) { + uint32_t vn = 0; + for (int i = 0; i < 4; i++) { + vn <<= 8; + vn += bytes[byte_index + i]; + } + fprintf(stderr, "%s%08x", (byte_index == 0) ? " " : ", ", vn); + } + fprintf(stdout, "\n"); + break; + case picoquic_callback_stream_gap: + /* This callback is never used. */ + break; + case picoquic_callback_prepare_to_send: + /* Active sending API */ + if (stream_ctx == NULL) { + /* Decidedly unexpected */ + return -1; + } else if (stream_ctx->name_sent_length < stream_ctx->name_length){ + uint8_t* buffer; + size_t available = stream_ctx->name_length - stream_ctx->name_sent_length; + int is_fin = 1; + + /* The length parameter marks the space available in the packet */ + if (available > length) { + available = length; + is_fin = 0; + } + /* Needs to retrieve a pointer to the actual buffer + * the "bytes" parameter points to the sending context + */ + buffer = picoquic_provide_stream_data_buffer(bytes, available, is_fin, !is_fin); + if (buffer != NULL) { + char const* filename = client_ctx->file_names[stream_ctx->file_rank]; + memcpy(buffer, filename + stream_ctx->name_sent_length, available); + stream_ctx->name_sent_length += available; + stream_ctx->is_name_sent = is_fin; + } + else { + fprintf(stderr, "\nError, could not get data buffer.\n"); + ret = -1; + } + } + else { + /* Nothing to send, just return */ + } + break; + case picoquic_callback_almost_ready: + break; + case picoquic_callback_ready: + break; + default: + /* unexpected -- just ignore. */ + break; + } + } + + return ret; +} + +/* Prepare the context used by the background client.: + * - Create the QUIC context. + * - Open the sockets + * - Find the server's address + * - Initialize the client context and create a client connection. + */ +static sample_background_init(char const* server_name, int server_port, char const* default_dir, + char const* ticket_store_filename, char const* token_store_filename, + struct sockaddr_storage * server_address, picoquic_quic_t** quic, picoquic_cnx_t** cnx, sample_background_ctx_t *client_ctx) +{ + int ret = 0; + char const* sni = PICOQUIC_SAMPLE_SNI; + char const* qlog_dir = PICOQUIC_SAMPLE_CLIENT_QLOG_DIR; + uint64_t current_time = picoquic_current_time(); + + *quic = NULL; + *cnx = NULL; + + /* Get the server's address */ + if (ret == 0) { + int is_name = 0; + + ret = picoquic_get_server_address(server_name, server_port, server_address, &is_name); + if (ret != 0) { + fprintf(stderr, "Cannot get the IP address for <%s> port <%d>", server_name, server_port); + } + else if (is_name) { + sni = server_name; + } + } + + /* Create a QUIC context. It could be used for many connections, but in this sample we + * will use it for just one connection. + * The sample code exercises just a small subset of the QUIC context configuration options: + * - use files to store tickets and tokens in order to manage retry and 0-RTT + * - set the congestion control algorithm to BBR + * - enable logging of encryption keys for wireshark debugging. + * - instantiate a binary log option, and log all packets. + */ + if (ret == 0) { + *quic = picoquic_create(1, NULL, NULL, NULL, PICOQUIC_SAMPLE_ALPN, NULL, NULL, + NULL, NULL, NULL, current_time, NULL, + ticket_store_filename, NULL, 0); + + if (*quic == NULL) { + fprintf(stderr, "Could not create quic context\n"); + ret = -1; + } + else { + if (picoquic_load_retry_tokens(*quic, token_store_filename) != 0) { + fprintf(stderr, "No token file present. Will create one as <%s>.\n", token_store_filename); + } + + picoquic_set_default_congestion_algorithm(*quic, picoquic_bbr_algorithm); + + picoquic_set_key_log_file_from_env(*quic); + picoquic_set_qlog(*quic, qlog_dir); + picoquic_set_log_level(*quic, 1); + } + } + /* Initialize the callback context and create the connection context. + * We use minimal options on the client side, keeping the transport + * parameter values set by default for picoquic. This could be fixed later. + */ + + if (ret == 0) { + client_ctx->default_dir = default_dir; + + printf("Starting connection to %s, port %d\n", server_name, server_port); + + /* Create a client connection */ + *cnx = picoquic_create_cnx(*quic, picoquic_null_connection_id, picoquic_null_connection_id, + (struct sockaddr*)server_address, current_time, 0, sni, PICOQUIC_SAMPLE_ALPN, 1); + + if (*cnx == NULL) { + fprintf(stderr, "Could not create connection context\n"); + ret = -1; + } + else { + /* Document connection in client's context */ + client_ctx->cnx = *cnx; + /* Set the client callback context */ + picoquic_set_callback(*cnx, sample_background_callback, client_ctx); + /* Client connection parameters could be set here, before starting the connection. */ + ret = picoquic_start_client_cnx(*cnx); + if (ret < 0) { + fprintf(stderr, "Could not activate connection\n"); + } + else { + /* Printing out the initial CID, which is used to identify log files */ + picoquic_connection_id_t icid = picoquic_get_initial_cnxid(*cnx); + printf("Initial connection ID: "); + for (uint8_t i = 0; i < icid.id_len; i++) { + printf("%02x", icid.id[i]); + } + printf("\n"); + } + } + } + + return ret; +} + +/* Loop callback for the background client. +* +* The main difference with the simpler loop used for the basic client +* is the support for the "wake up" callback, which we use to create +* streams inside the background thread because the picoquic code +* is not generally thread safe. +* +* The support for the wakeup call is declared +* +* +* In the "background client" mode, the files are requested from +* the UI thread, but the corresponding streams are created in the +* background thread. We use a very simple thread coordination +* process to keep the sample simple: +* - the variable "nb_files" documents the number of files +* that have been documented by the client. It is updated by +* the UI thread when a new file is requested. +* - the variable "nb_files_processed" documents the number +* of files for which a stream has been created. It is +* updated by the UI thread. +* This is implemented in the "sample process wakeup" function. +*/ + + +static int sample_background_wakeup(sample_background_ctx_t* client_ctx) +{ + int ret = 0; + + while (client_ctx->nb_files > client_ctx->nb_files_processed) { + ret = sample_background_create_stream(client_ctx->cnx, client_ctx, client_ctx->nb_files_processed); + if (ret < 0) { + fprintf(stderr, "\nCould not initiate stream for file #%d, %s\n", + client_ctx->nb_files_processed, + client_ctx->file_names[client_ctx->nb_files_processed]); + break; + } + client_ctx->nb_files_processed++; + } + + if (client_ctx->is_closing_requested && !client_ctx->is_closing) { + picoquic_close(client_ctx->cnx, 0); + } + + return ret; +} + +static int sample_background_loop_cb(picoquic_quic_t* quic, picoquic_packet_loop_cb_enum cb_mode, + void* callback_ctx, void * callback_arg) +{ + int ret = 0; + sample_background_ctx_t* client_ctx = (sample_background_ctx_t*)callback_ctx; + + if (client_ctx == NULL) { + ret = PICOQUIC_ERROR_UNEXPECTED_ERROR; + } + else { + switch (cb_mode) { + case picoquic_packet_loop_ready: + break; + case picoquic_packet_loop_wake_up: + ret = sample_background_wakeup(client_ctx); + break; + case picoquic_packet_loop_after_receive: + break; + case picoquic_packet_loop_after_send: + if (client_ctx->is_disconnected) { + ret = PICOQUIC_NO_ERROR_TERMINATE_PACKET_LOOP; + } + break; + case picoquic_packet_loop_port_update: + break; + default: + ret = PICOQUIC_ERROR_UNEXPECTED_ERROR; + break; + } + } + return ret; +} + +/* Background client. +*/ +int picoquic_sample_background(char const* server_name, int server_port, char const* default_dir) +{ + int ret = 0; + struct sockaddr_storage server_address; + picoquic_quic_t* quic = NULL; + picoquic_cnx_t* cnx = NULL; + sample_background_ctx_t client_ctx = { 0 }; + char const* ticket_store_filename = PICOQUIC_SAMPLE_CLIENT_TICKET_STORE; + char const* token_store_filename = PICOQUIC_SAMPLE_CLIENT_TOKEN_STORE; + picoquic_network_thread_ctx_t* thread_ctx = NULL; + int thread_ret = 0; + int wait_cycles = 0; + int wait_limit_sec = 1; /* By default, wait 1 second until completion */ + picoquic_packet_loop_param_t param = { 0 }; + char const* file_names[PICOQUIC_SAMPLE_BACKGROUND_MAX_FILES]; + char buf[256]; + + ret = sample_background_init(server_name, server_port, default_dir, + ticket_store_filename, token_store_filename, + &server_address, &quic, &cnx, &client_ctx); + + /* Initialize the files field in the context to an empty vector*/ + client_ctx.file_names = file_names; + + /* set the thread parameters */ + param.local_af = server_address.ss_family; + + /* Start the background thread. */ + thread_ctx = picoquic_start_custom_network_thread(quic, ¶m, + picoquic_internal_thread_create, picoquic_internal_thread_delete, + picoquic_internal_thread_setname,"sample_background", + sample_background_loop_cb, + &client_ctx, &thread_ret); + + /* Loop on the UI until the client calls it quit. */ + while (!client_ctx.is_closing_requested && !client_ctx.is_disconnected && + client_ctx.nb_files < PICOQUIC_SAMPLE_BACKGROUND_MAX_FILES) { + size_t line_size = 0; + printf("\nNext file (or empty line to quit)?\n"); + if (fgets(buf, sizeof(buf), stdin) != NULL) { + /* clean the line feed characters if present */ + line_size = strlen(buf); + while(line_size > 0) { + int c = buf[line_size - 1]; + if (c != '\n' && c != '\r' && c != ' ' && c != '\t') { + break; + } + line_size--; + } + } + if (line_size == 0) { + printf("OK, empty name, closing.\n"); + break; + } + else { + char * f_name = (char*)malloc(line_size + 1); + if (f_name == NULL) { + /* Error! Cannot continue */ + printf("Not enough memory for name, closing.\n"); + client_ctx.is_closing_requested = 1; + } + else { + memcpy(f_name, buf, line_size); + f_name[line_size] = 0; + file_names[client_ctx.nb_files] = f_name; + client_ctx.nb_files++; + } + } + /* wakeup the background thread */ + picoquic_wake_up_network_thread(thread_ctx); + } + + /* Wait until all files have been received. + * If this was not just a sample, we would develop some code + * to wait until the completion of the transport. Here, + * we simply resort to active polling. + */ + while (!thread_ctx->thread_is_closed) { + if (client_ctx.is_disconnected) { + printf("Disconnected.\n"); + break; + } + if ((client_ctx.nb_files_received + client_ctx.nb_files_failed) >= client_ctx.nb_files && + !client_ctx.is_closing_requested) { + client_ctx.is_closing_requested = 1; + picoquic_wake_up_network_thread(thread_ctx); + } + else { +#ifdef _WINDOWS + Sleep(10); +#else + usleep(10000); +#endif + wait_cycles++; + if (wait_cycles > wait_limit_sec * 100) { + printf("\nDo you want to wait longer (y|n, default=y)?\n"); + if (fgets(buf, sizeof(buf), stdin) == NULL) { + break; + } + else { + if (buf[0] != 'n') { + wait_limit_sec *= 2; + } + else { + break; + } + } + } + } + } + + /* close the network thread. */ + picoquic_delete_network_thread(thread_ctx); + thread_ctx = NULL; + + /* Done. At this stage, we could print out statistics, etc. */ + sample_background_report(&client_ctx); + + /* Save tickets and tokens, and free the QUIC context */ + if (quic != NULL) { + if (picoquic_save_session_tickets(quic, ticket_store_filename) != 0) { + fprintf(stderr, "Could not store the saved session tickets.\n"); + } + if (picoquic_save_retry_tokens(quic, token_store_filename) != 0) { + fprintf(stderr, "Could not save tokens to <%s>.\n", token_store_filename); + } + picoquic_free(quic); + } + /* Free the file names */ + for (int i = 0; i < client_ctx.nb_files; i++) { + if (client_ctx.file_names[i] != NULL) { + free((void*)client_ctx.file_names[i]); + } + } + /* Free the Client context */ + sample_background_free_context(&client_ctx); + + return ret; +} \ No newline at end of file diff --git a/sample/sample_client.c b/sample/sample_client.c index bdf65af6f..0fbe73ebf 100644 --- a/sample/sample_client.c +++ b/sample/sample_client.c @@ -88,7 +88,7 @@ typedef struct st_sample_client_stream_ctx_t { } sample_client_stream_ctx_t; typedef struct st_sample_client_ctx_t { - + picoquic_cnx_t* cnx; char const* default_dir; char const** file_names; sample_client_stream_ctx_t* first_stream; @@ -121,7 +121,7 @@ static int sample_client_create_stream(picoquic_cnx_t* cnx, client_ctx->last_stream = stream_ctx; } stream_ctx->file_rank = file_rank; - stream_ctx->stream_id = (uint64_t)4 * file_rank; + stream_ctx->stream_id = picoquic_get_next_local_stream_id(client_ctx->cnx, 0); stream_ctx->name_length = strlen(client_ctx->file_names[file_rank]); /* Mark the stream as active. The callback will be asked to provide data when @@ -370,6 +370,7 @@ int sample_client_callback(picoquic_cnx_t* cnx, stream_ctx->is_name_sent = is_fin; } else { + fprintf(stderr, "\nError, coulfd not get data buffer.\n"); ret = -1; } } @@ -431,7 +432,7 @@ static int sample_client_loop_cb(picoquic_quic_t* quic, picoquic_packet_loop_cb_ return ret; } -/* Prepare the context used by both the simple client and the background client.: +/* Prepare the context used by the simple client: * - Create the QUIC context. * - Open the sockets * - Find the server's address @@ -510,6 +511,8 @@ static sample_client_init(char const* server_name, int server_port, char const* ret = -1; } else { + /* Document connection in client's context */ + client_ctx->cnx = *cnx; /* Set the client callback context */ picoquic_set_callback(*cnx, sample_client_callback, client_ctx); /* Client connection parameters could be set here, before starting the connection. */ @@ -552,7 +555,6 @@ int picoquic_sample_client(char const * server_name, int server_port, char const int nb_files, char const ** file_names) { int ret = 0; -#if 1 struct sockaddr_storage server_address; picoquic_quic_t* quic = NULL; picoquic_cnx_t* cnx = NULL; @@ -563,108 +565,7 @@ int picoquic_sample_client(char const * server_name, int server_port, char const ret = sample_client_init(server_name, server_port, default_dir, ticket_store_filename, token_store_filename, &server_address, &quic, &cnx, &client_ctx); -#else - struct sockaddr_storage server_address; - char const* sni = PICOQUIC_SAMPLE_SNI; - picoquic_quic_t* quic = NULL; - char const* ticket_store_filename = PICOQUIC_SAMPLE_CLIENT_TICKET_STORE; - char const* token_store_filename = PICOQUIC_SAMPLE_CLIENT_TOKEN_STORE; - char const* qlog_dir = PICOQUIC_SAMPLE_CLIENT_QLOG_DIR; - sample_client_ctx_t client_ctx = { 0 }; - picoquic_cnx_t* cnx = NULL; - uint64_t current_time = picoquic_current_time(); - - /* Get the server's address */ - if (ret == 0) { - int is_name = 0; - - ret = picoquic_get_server_address(server_name, server_port, &server_address, &is_name); - if (ret != 0) { - fprintf(stderr, "Cannot get the IP address for <%s> port <%d>", server_name, server_port); - } - else if (is_name) { - sni = server_name; - } - } - - /* Create a QUIC context. It could be used for many connections, but in this sample we - * will use it for just one connection. - * The sample code exercises just a small subset of the QUIC context configuration options: - * - use files to store tickets and tokens in order to manage retry and 0-RTT - * - set the congestion control algorithm to BBR - * - enable logging of encryption keys for wireshark debugging. - * - instantiate a binary log option, and log all packets. - */ - if (ret == 0) { - quic = picoquic_create(1, NULL, NULL, NULL, PICOQUIC_SAMPLE_ALPN, NULL, NULL, - NULL, NULL, NULL, current_time, NULL, - ticket_store_filename, NULL, 0); - - if (quic == NULL) { - fprintf(stderr, "Could not create quic context\n"); - ret = -1; - } - else { - if (picoquic_load_retry_tokens(quic, token_store_filename) != 0) { - fprintf(stderr, "No token file present. Will create one as <%s>.\n", token_store_filename); - } - - picoquic_set_default_congestion_algorithm(quic, picoquic_bbr_algorithm); - - picoquic_set_key_log_file_from_env(quic); - picoquic_set_qlog(quic, qlog_dir); - picoquic_set_log_level(quic, 1); - } - } - - /* Initialize the callback context and create the connection context. - * We use minimal options on the client side, keeping the transport - * parameter values set by default for picoquic. This could be fixed later. - */ - - if (ret == 0) { - client_ctx.default_dir = default_dir; - client_ctx.file_names = file_names; - client_ctx.nb_files = nb_files; - - printf("Starting connection to %s, port %d\n", server_name, server_port); - - /* Create a client connection */ - cnx = picoquic_create_cnx(quic, picoquic_null_connection_id, picoquic_null_connection_id, - (struct sockaddr*) & server_address, current_time, 0, sni, PICOQUIC_SAMPLE_ALPN, 1); - - if (cnx == NULL) { - fprintf(stderr, "Could not create connection context\n"); - ret = -1; - } - else { - - /* Set the client callback context */ - picoquic_set_callback(cnx, sample_client_callback, &client_ctx); - /* Client connection parameters could be set here, before starting the connection. */ - ret = picoquic_start_client_cnx(cnx); - if (ret < 0) { - fprintf(stderr, "Could not activate connection\n"); - } else { - /* Printing out the initial CID, which is used to identify log files */ - picoquic_connection_id_t icid = picoquic_get_initial_cnxid(cnx); - printf("Initial connection ID: "); - for (uint8_t i = 0; i < icid.id_len; i++) { - printf("%02x", icid.id[i]); - } - printf("\n"); - } - } - /* Create a stream context for all the files that should be downloaded */ - for (int i = 0; ret == 0 && i < client_ctx.nb_files; i++) { - ret = sample_client_create_stream(cnx, &client_ctx, i); - if (ret < 0) { - fprintf(stderr, "Could not initiate stream for fi\n"); - } - } - } -#endif if (ret == 0) { /* Initialize all the streams contexts from the list of streams passed on the API. */ client_ctx.file_names = file_names; @@ -700,4 +601,4 @@ int picoquic_sample_client(char const * server_name, int server_port, char const sample_client_free_context(&client_ctx); return ret; -} \ No newline at end of file +} From 133b07fda4a87e50b1fec6b7bcc2cc6f279c1318 Mon Sep 17 00:00:00 2001 From: huitema Date: Tue, 27 Aug 2024 17:26:31 -0700 Subject: [PATCH 5/8] Fix missing int --- sample/sample_background.c | 2 +- sample/sample_client.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sample/sample_background.c b/sample/sample_background.c index a1170c602..6aa1f496b 100644 --- a/sample/sample_background.c +++ b/sample/sample_background.c @@ -398,7 +398,7 @@ int sample_background_callback(picoquic_cnx_t* cnx, * - Find the server's address * - Initialize the client context and create a client connection. */ -static sample_background_init(char const* server_name, int server_port, char const* default_dir, +static int sample_background_init(char const* server_name, int server_port, char const* default_dir, char const* ticket_store_filename, char const* token_store_filename, struct sockaddr_storage * server_address, picoquic_quic_t** quic, picoquic_cnx_t** cnx, sample_background_ctx_t *client_ctx) { diff --git a/sample/sample_client.c b/sample/sample_client.c index 0fbe73ebf..639ff3e56 100644 --- a/sample/sample_client.c +++ b/sample/sample_client.c @@ -438,7 +438,7 @@ static int sample_client_loop_cb(picoquic_quic_t* quic, picoquic_packet_loop_cb_ * - Find the server's address * - Initialize the client context and create a client connection. */ -static sample_client_init(char const* server_name, int server_port, char const* default_dir, +static int sample_client_init(char const* server_name, int server_port, char const* default_dir, char const* ticket_store_filename, char const* token_store_filename, struct sockaddr_storage * server_address, picoquic_quic_t** quic, picoquic_cnx_t** cnx, sample_client_ctx_t *client_ctx) { From 3df1d37fd8247f7277f359161fb99c454fb3e5ec Mon Sep 17 00:00:00 2001 From: huitema Date: Thu, 5 Sep 2024 20:31:36 -0700 Subject: [PATCH 6/8] Enforce limit on offset in crypto packets --- CMakeLists.txt | 2 +- UnitTest1/unittest1.cpp | 7 ++++ picoquic/frames.c | 21 +++++++--- picoquic/picoquic.h | 3 +- picoquic/sender.c | 5 ++- picoquic_t/picoquic_t.c | 1 + picoquictest/edge_cases.c | 79 +++++++++++++++++++++++++++++++++++++ picoquictest/picoquictest.h | 1 + 8 files changed, 109 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a6a8da30d..fb1700386 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ else() endif() project(picoquic - VERSION 1.1.23.1 + VERSION 1.1.24.0 DESCRIPTION "picoquic library" LANGUAGES C CXX) diff --git a/UnitTest1/unittest1.cpp b/UnitTest1/unittest1.cpp index 364bfca32..f060a77f4 100644 --- a/UnitTest1/unittest1.cpp +++ b/UnitTest1/unittest1.cpp @@ -1869,6 +1869,13 @@ namespace UnitTest1 Assert::AreEqual(ret, 0); } + TEST_METHOD(crypto_hs_offset) + { + int ret = crypto_hs_offset_test(); + + Assert::AreEqual(ret, 0); + } + TEST_METHOD(cubic) { int ret = cubic_test(); diff --git a/picoquic/frames.c b/picoquic/frames.c index 9010fa47d..81dfd0b92 100644 --- a/picoquic/frames.c +++ b/picoquic/frames.c @@ -2262,14 +2262,23 @@ const uint8_t* picoquic_decode_crypto_hs_frame(picoquic_cnx_t* cnx, const uint8_ picoquic_connection_error(cnx, PICOQUIC_TRANSPORT_FRAME_FORMAT_ERROR, picoquic_frame_type_crypto_hs); } else { picoquic_stream_head_t* stream = &cnx->tls_stream[epoch]; - int new_data_available; - int ret = picoquic_queue_network_input(cnx->quic, &stream->stream_data_tree, stream->consumed_offset, - offset, data_bytes, (size_t)data_length, picoquic_is_last_stream_frame(bytes+data_length, bytes_max), - received_data, &new_data_available); - if (ret != 0) { - picoquic_connection_error(cnx, (int64_t)ret, picoquic_frame_type_crypto_hs); + + if (stream->consumed_offset < offset && + stream->consumed_offset + PICOQUIC_MAX_CRYPTO_BUFFER_GAP < offset + data_length) { + picoquic_connection_error(cnx, PICOQUIC_TRANSPORT_CRYPTO_BUFFER_EXCEEDED, picoquic_frame_type_crypto_hs); bytes = NULL; } + else { + int new_data_available; + int ret = picoquic_queue_network_input(cnx->quic, &stream->stream_data_tree, stream->consumed_offset, + offset, data_bytes, (size_t)data_length, picoquic_is_last_stream_frame(bytes + data_length, bytes_max), + received_data, &new_data_available); + + if (ret != 0) { + picoquic_connection_error(cnx, (int64_t)ret, picoquic_frame_type_crypto_hs); + bytes = NULL; + } + } } return bytes; diff --git a/picoquic/picoquic.h b/picoquic/picoquic.h index 7f80cba81..cd7e83a42 100644 --- a/picoquic/picoquic.h +++ b/picoquic/picoquic.h @@ -40,7 +40,7 @@ extern "C" { #endif -#define PICOQUIC_VERSION "1.1.23.1" +#define PICOQUIC_VERSION "1.1.24.0" #define PICOQUIC_ERROR_CLASS 0x400 #define PICOQUIC_ERROR_DUPLICATE (PICOQUIC_ERROR_CLASS + 1) #define PICOQUIC_ERROR_AEAD_CHECK (PICOQUIC_ERROR_CLASS + 3) @@ -137,6 +137,7 @@ extern "C" { #define PICOQUIC_RESET_SECRET_SIZE 16 #define PICOQUIC_RESET_PACKET_PAD_SIZE 23 #define PICOQUIC_RESET_PACKET_MIN_SIZE (PICOQUIC_RESET_PACKET_PAD_SIZE + PICOQUIC_RESET_SECRET_SIZE) +#define PICOQUIC_MAX_CRYPTO_BUFFER_GAP 16384 #define PICOQUIC_LOG_PACKET_MAX_SEQUENCE 100 diff --git a/picoquic/sender.c b/picoquic/sender.c index a1a567630..0820d9f53 100644 --- a/picoquic/sender.c +++ b/picoquic/sender.c @@ -2002,6 +2002,7 @@ int picoquic_prepare_packet_client_init(picoquic_cnx_t* cnx, picoquic_path_t * p *is_initial_sent = (packet->ptype == picoquic_packet_initial); } else if (ret == 0 && is_cleartext_mode && tls_ready == 0 + && picoquic_find_first_misc_frame(cnx, pc) != NULL && cnx->first_misc_frame == NULL && !cnx->ack_ctx[pc].act[0].ack_needed && !force_handshake_padding) { /* when in a clear text mode, only send packets if there is * actually something to send, or resend. */ @@ -2026,7 +2027,7 @@ int picoquic_prepare_packet_client_init(picoquic_cnx_t* cnx, picoquic_path_t * p if ((tls_ready == 0 || path_x->cwin <= path_x->bytes_in_transit || cnx->quic->cwin_max <= path_x->bytes_in_transit) && (cnx->cnx_state == picoquic_state_client_almost_ready || picoquic_is_ack_needed(cnx, current_time, next_wake_time, pc, 0) == 0) - && cnx->first_misc_frame == NULL && !force_handshake_padding) { + && picoquic_find_first_misc_frame(cnx, pc) == NULL && !force_handshake_padding) { length = 0; } else { @@ -3169,7 +3170,7 @@ int picoquic_prepare_packet_almost_ready(picoquic_cnx_t* cnx, picoquic_path_t* p * several RTT. */ if (length <= header_length && cnx->client_mode && - cnx->first_misc_frame == NULL && + picoquic_find_first_misc_frame(cnx, pc) == NULL && (length = picoquic_retransmit_needed(cnx, pc, path_x, current_time, next_wake_time, packet, send_buffer_min_max, &header_length)) > 0) { /* Check whether it makes sense to add an ACK at the end of the retransmission */ diff --git a/picoquic_t/picoquic_t.c b/picoquic_t/picoquic_t.c index b0c3d5e99..1d93cdec0 100644 --- a/picoquic_t/picoquic_t.c +++ b/picoquic_t/picoquic_t.c @@ -309,6 +309,7 @@ static const picoquic_test_def_t test_table[] = { { "ready_to_skip", ready_to_skip_test }, { "ready_to_zfin", ready_to_zfin_test }, { "ready_to_zero", ready_to_zero_test }, + { "crypto_hs_offset", crypto_hs_offset_test }, { "cubic", cubic_test }, { "cubic_jitter", cubic_jitter_test }, { "fastcc", fastcc_test }, diff --git a/picoquictest/edge_cases.c b/picoquictest/edge_cases.c index 4f70fd42f..ff8bb311b 100644 --- a/picoquictest/edge_cases.c +++ b/picoquictest/edge_cases.c @@ -1393,5 +1393,84 @@ int initial_pto_test() test_ctx = NULL; } + return ret; +} + +/* Test of out of order crypto packets. +* We test that by injecting a crypto handshake frame with an +* offset of 64K in the crypto context. There will be three tests, +* for initial, handshake and 1 rtt contexts. + */ + + +int crypto_hs_offset_test_one(picoquic_packet_context_enum pc) +{ + int ret = 0; + picoquic_test_tls_api_ctx_t* test_ctx = NULL; + size_t length = 0; + uint64_t simulated_time = 0; + picoquic_connection_id_t initial_cid = { { 0xC0, 0xFF, 0x5E, 0x40, 0, 0, 0, 0}, 8 }; + uint8_t bad_crypto_hs[] = { picoquic_frame_type_crypto_hs, 0x80, 0x01, 0, 0, 4, 1, 2, 3, 4 }; + + initial_cid.id[4] = (uint8_t)pc; + + /* Create a client. */ + ret = tls_api_init_ctx_ex(&test_ctx, PICOQUIC_INTERNAL_TEST_VERSION_1, + PICOQUIC_TEST_SNI, PICOQUIC_TEST_ALPN, &simulated_time, NULL, NULL, 0, 1, 0, &initial_cid); + if (ret != 0) { + DBG_PRINTF("Cannot initialize context, ret = 0x%x", ret); + } + else { + /* Set the binlog */ + picoquic_set_binlog(test_ctx->qserver, "."); + /* start the client connection */ + ret = picoquic_start_client_cnx(test_ctx->cnx_client); + } + + if (ret == 0) { + /* Inject the made up packet */ + ret = picoquic_queue_misc_frame(test_ctx->cnx_client, bad_crypto_hs, sizeof(bad_crypto_hs), 1, pc); + } + + + /* Try to establish the connection */ + if (ret == 0) { + if (wait_client_connection_ready(test_ctx, &simulated_time) == 0) { + if (test_ctx->cnx_server != NULL) { + if (test_ctx->cnx_server->cnx_state != picoquic_state_handshake_failure && + test_ctx->cnx_server->cnx_state < picoquic_state_disconnecting) { + /* Should wait for ready state */ + DBG_PRINTF("Unexpected success, pc=%d\n", pc); + ret = -1; + } + } + } + } + + if (ret == 0 && test_ctx->cnx_client->remote_error != PICOQUIC_TRANSPORT_CRYPTO_BUFFER_EXCEEDED) { + DBG_PRINTF("For pc=%d, expected error 0x%x, got 0x%x\n", pc, + PICOQUIC_TRANSPORT_CRYPTO_BUFFER_EXCEEDED, test_ctx->cnx_client->remote_error); + ret = -1; + } + + if (test_ctx != NULL) { + tls_api_delete_ctx(test_ctx); + test_ctx = NULL; + } + + return ret; +} + +int crypto_hs_offset_test() +{ + picoquic_packet_context_enum pc[] = { picoquic_packet_context_initial, + picoquic_packet_context_handshake, picoquic_packet_context_application }; + size_t nb_pc = sizeof(pc) / sizeof(picoquic_packet_context_enum); + int ret = 0; + + for (size_t i = 0; i < nb_pc && ret == 0; i++) { + ret = crypto_hs_offset_test_one(pc[i]); + } + return ret; } \ No newline at end of file diff --git a/picoquictest/picoquictest.h b/picoquictest/picoquictest.h index 06fbb2b6c..d60ff4376 100644 --- a/picoquictest/picoquictest.h +++ b/picoquictest/picoquictest.h @@ -251,6 +251,7 @@ int ready_to_send_test(); int ready_to_skip_test(); int ready_to_zero_test(); int ready_to_zfin_test(); +int crypto_hs_offset_test(); int cubic_test(); int cubic_jitter_test(); int satellite_basic_test(); From bf10c46c836984f807b2e9be1ce6c093b4acc7f6 Mon Sep 17 00:00:00 2001 From: huitema Date: Thu, 5 Sep 2024 21:08:42 -0700 Subject: [PATCH 7/8] Fix bug in fuzzing test --- picoquic_t/picoquic_t.c | 2 +- picoquictest/stresstest.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/picoquic_t/picoquic_t.c b/picoquic_t/picoquic_t.c index 1d93cdec0..8fe961763 100644 --- a/picoquic_t/picoquic_t.c +++ b/picoquic_t/picoquic_t.c @@ -533,8 +533,8 @@ int usage(char const * argv0) fprintf(stderr, " -C ccc Use nnn stress clients in parallel.\n"); fprintf(stderr, " -c nnn ccc Run connection stress for nnn minutes, ccc connections.\n"); fprintf(stderr, " -d ppp uuu dir Run connection ddoss for ppp packets, uuu usec intervals,\n"); - fprintf(stderr, " -F nnn Run the corrupt file fuzzer nnn times,\n"); fprintf(stderr, " logs in dir. No logs if dir=\"-\""); + fprintf(stderr, " -F nnn Run the corrupt file fuzzer nnn times,\n"); fprintf(stderr, " -n Disable debug prints.\n"); fprintf(stderr, " -r Retry failed tests with debug print enabled.\n"); fprintf(stderr, " -h Print this help message\n"); diff --git a/picoquictest/stresstest.c b/picoquictest/stresstest.c index 7c3a8eb37..ac42c0dd5 100644 --- a/picoquictest/stresstest.c +++ b/picoquictest/stresstest.c @@ -127,7 +127,7 @@ static picosplay_node_t *stress_client_node_create(void * value) void * stress_client_node_value(picosplay_node_t * node) { - return (void*)((char*)node - offsetof(struct st_picoquic_stress_client_t, client_node)); + return (node == NULL)?NULL:(void*)((char*)node - offsetof(struct st_picoquic_stress_client_t, client_node)); } static void stress_client_node_delete(void * tree, picosplay_node_t * node) From 85b6d8497081c3461f44af429c2dc1ce704d719f Mon Sep 17 00:00:00 2001 From: huitema Date: Thu, 5 Sep 2024 21:36:08 -0700 Subject: [PATCH 8/8] Fix incorrect first misc test --- picoquic/sender.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picoquic/sender.c b/picoquic/sender.c index 0820d9f53..29ec10e92 100644 --- a/picoquic/sender.c +++ b/picoquic/sender.c @@ -2002,8 +2002,8 @@ int picoquic_prepare_packet_client_init(picoquic_cnx_t* cnx, picoquic_path_t * p *is_initial_sent = (packet->ptype == picoquic_packet_initial); } else if (ret == 0 && is_cleartext_mode && tls_ready == 0 - && picoquic_find_first_misc_frame(cnx, pc) != NULL - && cnx->first_misc_frame == NULL && !cnx->ack_ctx[pc].act[0].ack_needed && !force_handshake_padding) { + && picoquic_find_first_misc_frame(cnx, pc) == NULL + && !cnx->ack_ctx[pc].act[0].ack_needed && !force_handshake_padding) { /* when in a clear text mode, only send packets if there is * actually something to send, or resend. */