Commit 58ef864189 for openssl.org

commit 58ef864189a97b147986013a98c6694dd1f20ba1
Author: Neil Horman <nhorman@openssl.org>
Date:   Mon Feb 2 10:24:56 2026 -0500

    Update ossl-http3-demo to support multiple requests

    In order to use our http3 demo to do interop testing, said demo needs to
    be able to handle multiple requests and responses written to specific
    output files.

    Add that code here, allowing us to specify optionally a list of requests
    on the command line to send to the server, as well as a download
    directory, so that requests made get written locally to the same name as
    the request in the specified download directory.

    while we're at it, also clean up the code infrastructure to use SSL_poll
    to do read-ready checking, rather than iterating/mutating the internal
    hash table, which is questionable to do (i.e. we shouldn't be removing
    elements from the hash table while iterating over it).

    Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
    Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
    MergeDate: Fri Feb  6 12:46:24 2026
    (Merged from https://github.com/openssl/openssl/pull/29922)

diff --git a/demos/http3/ossl-nghttp3-demo.c b/demos/http3/ossl-nghttp3-demo.c
index a8d5cebbf2..e4f98f515a 100644
--- a/demos/http3/ossl-nghttp3-demo.c
+++ b/demos/http3/ossl-nghttp3-demo.c
@@ -14,6 +14,19 @@

 static int done;

+/**
+ * @brief variable to record output basename path
+ */
+static char *dlpath = NULL;
+
+/**
+ * @brief struct to hold user data for async http3 writes
+ */
+struct stream_user_data {
+    char *outpath;
+    FILE *fp;
+};
+
 static void make_nv(nghttp3_nv *nv, const char *name, const char *value)
 {
     nv->name = (uint8_t *)name;
@@ -54,58 +67,94 @@ static int on_end_headers(nghttp3_conn *h3conn, int64_t stream_id,

 static int on_recv_data(nghttp3_conn *h3conn, int64_t stream_id,
     const uint8_t *data, size_t datalen,
-    void *conn_user_data, void *stream_user_data)
+    void *conn_user_data, void *user_data)
 {
+    FILE *outfp;
     size_t wr;
+    struct stream_user_data *sdata = OSSL_DEMO_H3_STREAM_get_user_data((const OSSL_DEMO_H3_STREAM *)user_data);

-    /* HTTP response body data - write it to stdout. */
+    if (dlpath == NULL) {
+        outfp = stdout;
+    } else {
+        if (sdata->fp == NULL) {
+            sdata->fp = fopen(sdata->outpath, "w+");
+        }
+        if (sdata->fp == NULL) {
+            fprintf(stderr, "Failed to open %s for writing\n", sdata->outpath);
+            return 1;
+        }
+        outfp = sdata->fp;
+    }
+    /* HTTP response body data - write it. */
     while (datalen > 0) {
-        wr = fwrite(data, 1, datalen, stdout);
-        if (ferror(stdout))
+        fprintf(stderr, "writing %lu bytes to %s\n", datalen, sdata->outpath);
+        wr = fwrite(data, 1, datalen, outfp);
+        if (ferror(outfp))
             return 1;

         data += wr;
         datalen -= wr;
     }
-
     return 0;
 }

 static int on_end_stream(nghttp3_conn *h3conn, int64_t stream_id,
-    void *conn_user_data, void *stream_user_data)
+    void *conn_user_data, void *user_data)
 {
     /* HTTP transaction is done - set done flag so that we stop looping. */
     done = 1;
     return 0;
 }

-static int try_conn(OSSL_DEMO_H3_CONN *conn, const char *bare_hostname)
+static int try_conn(OSSL_DEMO_H3_CONN *conn, const char *bare_hostname, const char *path)
 {
     nghttp3_nv nva[16];
     size_t num_nv = 0;
+    struct stream_user_data *sdata;
+    size_t needed_size;
+    int pathsize;

     /* Build HTTP headers. */
     make_nv(&nva[num_nv++], ":method", "GET");
     make_nv(&nva[num_nv++], ":scheme", "https");
     make_nv(&nva[num_nv++], ":authority", bare_hostname);
-    make_nv(&nva[num_nv++], ":path", "/");
+    make_nv(&nva[num_nv++], ":path", path);
     make_nv(&nva[num_nv++], "user-agent", "OpenSSL-Demo/nghttp3");

+    needed_size = sizeof(struct stream_user_data);
+    pathsize = snprintf(NULL, 0, "%s/%s", dlpath, path);
+    if (pathsize < 0) {
+        fprintf(stderr, "Unable to format path string\n");
+        return 0;
+    }
+    needed_size += pathsize;
+
+    sdata = malloc(needed_size + 1);
+    if (sdata == NULL)
+        return 0;
+    sdata->outpath = (char *)(sdata + 1);
+    sprintf(sdata->outpath, "%s/%s", dlpath, path);
+    sdata->fp = NULL;
+    fprintf(stderr, "Requesting %s\n", sdata->outpath);
     /* Submit request. */
-    if (!OSSL_DEMO_H3_CONN_submit_request(conn, nva, num_nv, NULL, NULL)) {
-        ERR_raise_data(ERR_LIB_USER, ERR_R_OPERATION_FAIL,
-            "cannot submit HTTP/3 request");
+    if (!OSSL_DEMO_H3_CONN_submit_request(conn, nva, num_nv, NULL, sdata)) {
+        fprintf(stderr, "Cannot submit HTTP/3 request\n");
         return 0;
     }

     /* Wait for request to complete. */
     done = 0;
-    while (!done)
+    while (!done) {
         if (!OSSL_DEMO_H3_CONN_handle_events(conn)) {
-            ERR_raise_data(ERR_LIB_USER, ERR_R_OPERATION_FAIL,
-                "cannot handle events");
+            fprintf(stderr, "Cannot handle events\n");
             return 0;
         }
+    }
+    if (sdata->fp != NULL) {
+        fclose(sdata->fp);
+        fprintf(stderr, "Closing local FILE pointer for %s\n", sdata->outpath);
+    }
+    free(sdata);
     return 1;
 }

@@ -120,13 +169,53 @@ int main(int argc, char **argv)
     char *hostname, *service;
     BIO_ADDRINFO *bai = NULL;
     const BIO_ADDRINFO *bai_walk;
+    FILE *req_fp = NULL;
+    size_t req_count = 0;
+    char **req_array = NULL;
+    size_t i;
+    char buffer[PATH_MAX];

     /* Check arguments. */
     if (argc < 2) {
-        fprintf(stderr, "usage: %s <host:port>\n", argv[0]);
+        fprintf(stderr, "usage: %s <host:port> [requestfile.txt <download_dir>]\n", argv[0]);
         goto err;
     }

+    /*
+     * If we have more than two arguments, then we are accepting both a request file
+     * which is a newline separated list of paths to request from the server
+     * as well as a download location relative to the current working directory
+     */
+    if (argc >= 4) {
+        dlpath = argv[3];
+        fprintf(stderr, "setting download path to %s, and reading %s\n", dlpath, argv[2]);
+
+        /*
+         * Read in our request file, one path per line
+         */
+        req_fp = fopen(argv[2], "r");
+        if (req_fp == NULL) {
+            fprintf(stderr, "Unable to open request file\n");
+            goto err;
+        }
+        while (!feof(req_fp)) {
+            req_count++;
+            req_array = realloc(req_array, sizeof(char *) * req_count);
+            req_array[req_count - 1] = NULL;
+            if (fscanf(req_fp, "%s", buffer) != 1) {
+                req_count--;
+                if (feof(req_fp))
+                    break;
+                fprintf(stderr, "Failed to read request file at index %lu\n", req_count);
+                fclose(req_fp);
+                goto err;
+            }
+            req_array[req_count - 1] = calloc(1, strlen(buffer) + 1);
+            memcpy(req_array[req_count - 1], buffer, strlen(buffer));
+        }
+        fclose(req_fp);
+    }
+
     addr = argv[1];

     hostname = NULL;
@@ -156,7 +245,7 @@ int main(int argc, char **argv)
     if ((ctx = SSL_CTX_new(OSSL_QUIC_client_method())) == NULL)
         goto err;

-    SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
+    SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);

     if (SSL_CTX_set_default_verify_paths(ctx) == 0)
         goto err;
@@ -183,23 +272,26 @@ int main(int argc, char **argv)
         conn = OSSL_DEMO_H3_CONN_new_for_addr(ctx, bai_walk, hostname,
             &callbacks, NULL, NULL);
         if (conn != NULL) {
-            if (try_conn(conn, addr) == 0) {
-                /*
-                 * Failure, try the next address.
-                 */
-                OSSL_DEMO_H3_CONN_free(conn);
-                conn = NULL;
-                ret = 1;
-            } else {
-                /*
-                 * Success, done.
-                 */
-                ret = 0;
-                break;
+            for (i = 0; i < req_count; i++) {
+                if (try_conn(conn, addr, req_array[i]) == 0) {
+                    /*
+                     * Failure, bail out.
+                     */
+                    OSSL_DEMO_H3_CONN_free(conn);
+                    conn = NULL;
+                    ret = 1;
+                    goto err;
+                }
             }
+            fprintf(stderr, "all requests complete\n");
+            ret = 0;
         }
     }
 err:
+    for (i = 0; i < req_count; i++)
+        free(req_array[i]);
+    free(req_array);
+
     if (ret != 0)
         ERR_print_errors_fp(stderr);

diff --git a/demos/http3/ossl-nghttp3.c b/demos/http3/ossl-nghttp3.c
index 050254d928..5be7b1a52c 100644
--- a/demos/http3/ossl-nghttp3.c
+++ b/demos/http3/ossl-nghttp3.c
@@ -126,19 +126,6 @@ err:
     return NULL;
 }

-static OSSL_DEMO_H3_STREAM *h3_conn_accept_stream(OSSL_DEMO_H3_CONN *conn, SSL *qstream)
-{
-    OSSL_DEMO_H3_STREAM *s;
-
-    if ((s = OPENSSL_zalloc(sizeof(OSSL_DEMO_H3_STREAM))) == NULL)
-        return NULL;
-
-    s->id = SSL_get_stream_id(qstream);
-    s->s = qstream;
-    lh_OSSL_DEMO_H3_STREAM_insert(conn->streams, s);
-    return s;
-}
-
 static void h3_conn_remove_stream(OSSL_DEMO_H3_CONN *conn, OSSL_DEMO_H3_STREAM *s)
 {
     if (s == NULL)
@@ -459,6 +446,25 @@ SSL *OSSL_DEMO_H3_CONN_get0_connection(const OSSL_DEMO_H3_CONN *conn)
     return conn->qconn;
 }

+typedef struct ossl_demo_h3_poll_list {
+    SSL_POLL_ITEM *poll_list;
+    OSSL_DEMO_H3_STREAM **h3_streams;
+    OSSL_DEMO_H3_CONN *conn;
+    size_t poll_count;
+    size_t idx;
+} OSSL_DEMO_H3_POLL_LIST;
+
+static void h3_conn_collect_streams(OSSL_DEMO_H3_STREAM *s, void *list)
+{
+    OSSL_DEMO_H3_POLL_LIST *pollist = list;
+
+    pollist->poll_list[pollist->idx].desc = SSL_as_poll_descriptor(s->s);
+    pollist->poll_list[pollist->idx].revents = 0;
+    pollist->poll_list[pollist->idx].events = SSL_POLL_EVENT_R;
+    pollist->h3_streams[pollist->idx] = s;
+    pollist->idx++;
+}
+
 /* Pumps received data to the HTTP/3 stack for a single stream. */
 static void h3_conn_pump_stream(OSSL_DEMO_H3_STREAM *s, void *conn_)
 {
@@ -578,6 +584,10 @@ int OSSL_DEMO_H3_CONN_handle_events(OSSL_DEMO_H3_CONN *conn)
     nghttp3_vec vecs[8] = { 0 };
     OSSL_DEMO_H3_STREAM key, *s;
     SSL *snew;
+    OSSL_DEMO_H3_POLL_LIST *pollist = NULL;
+    size_t poll_num;
+    struct timeval poll_timeout;
+    size_t result_count;

     if (conn == NULL)
         return 0;
@@ -594,15 +604,6 @@ int OSSL_DEMO_H3_CONN_handle_events(OSSL_DEMO_H3_CONN *conn)
     for (;;) {
         if ((snew = SSL_accept_stream(conn->qconn, SSL_ACCEPT_STREAM_NO_BLOCK)) == NULL)
             break;
-
-        /*
-         * Each new incoming stream gets wrapped into an OSSL_DEMO_H3_STREAM object and
-         * added into our stream ID map.
-         */
-        if (h3_conn_accept_stream(conn, snew) == NULL) {
-            SSL_free(snew);
-            return 0;
-        }
     }

     /* 2. Pump outgoing data from HTTP/3 engine to QUIC. */
@@ -704,9 +705,36 @@ int OSSL_DEMO_H3_CONN_handle_events(OSSL_DEMO_H3_CONN *conn)
         }
     }

-    /* 3. Pump incoming data from QUIC to HTTP/3 engine. */
+    /* 3. Build a list of streams to poll on */
     conn->pump_res = 1; /* cleared in below call if an error occurs */
-    lh_OSSL_DEMO_H3_STREAM_doall_arg(conn->streams, h3_conn_pump_stream, conn);
+    poll_num = lh_OSSL_DEMO_H3_STREAM_num_items(conn->streams);
+    pollist = OPENSSL_malloc(sizeof(OSSL_DEMO_H3_POLL_LIST) + (sizeof(SSL_POLL_ITEM) * poll_num) + (sizeof(OSSL_DEMO_H3_STREAM *) * poll_num));
+    pollist->poll_count = poll_num;
+    pollist->poll_list = (SSL_POLL_ITEM *)(pollist + 1);
+    pollist->h3_streams = (OSSL_DEMO_H3_STREAM **)(pollist->poll_list + poll_num);
+    pollist->conn = conn;
+    pollist->idx = 0;
+    lh_OSSL_DEMO_H3_STREAM_doall_arg(conn->streams, h3_conn_collect_streams, pollist);
+    poll_timeout.tv_sec = 0;
+    poll_timeout.tv_usec = 0;
+    result_count = 0;
+
+    /* 4. poll the list built above, looking for streams that are ready to read */
+    if (!SSL_poll(pollist->poll_list, pollist->idx, sizeof(SSL_POLL_ITEM),
+            &poll_timeout, 0, &result_count)) {
+        fprintf(stderr, "Failed to poll\n");
+        goto end;
+    }
+
+    /* 5. Pump incoming data from QUIC to HTTP/3 engine. */
+    for (i = 0; result_count != 0; i++) {
+        if (pollist->poll_list[i].revents == SSL_POLL_EVENT_R) {
+            result_count--;
+            h3_conn_pump_stream(pollist->h3_streams[i], pollist->conn);
+        }
+    }
+end:
+    OPENSSL_free(pollist);
     if (!conn->pump_res)
         return 0;