commit 44f40ec132d377aad0c99c32728eefae742e609b
parent c803eebd067b3e025cfa0da21712caf642ab73bc
Author: Christian Grothoff <christian@grothoff.org>
Date: Sat, 15 Nov 2025 15:01:25 +0100
start work to support HTTP/2 in test suite
Diffstat:
5 files changed, 403 insertions(+), 30 deletions(-)
diff --git a/src/tests/client_server/Makefile.am b/src/tests/client_server/Makefile.am
@@ -37,6 +37,11 @@ check_PROGRAMS += \
test_cert_tls
endif
+if MHD_SUPPORT_HTTP2
+check_PROGRAMS += \
+ test_http2
+endif
+
TESTS = $(check_PROGRAMS)
noinst_LTLIBRARIES = \
@@ -46,6 +51,7 @@ libmhdt_la_SOURCES = \
libtest.c libtest.h \
libtest_convenience.c \
libtest_convenience_client_request.c \
+ libtest_convenience_client2_request.c \
libtest_convenience_server_reply.c
# TODO: fix out-of-tree 'make check'
diff --git a/src/tests/client_server/libtest.h b/src/tests/client_server/libtest.h
@@ -154,6 +154,11 @@ struct MHDT_Phase
bool check_server_cert;
/**
+ * HTTP version to use. 0 = HTTP/1.x, 2 = HTTP/2, 3 = HTTP/3.
+ */
+ unsigned int http_version;
+
+ /**
* Client certificate to present to the server, NULL for none.
*/
const char *client_cert;
diff --git a/src/tests/client_server/libtest_convenience_client_request.c b/src/tests/client_server/libtest_convenience_client_request.c
@@ -354,6 +354,49 @@ set_url (CURL *c,
}
+/**
+ * Create a curl handle for the given @a pc.
+ *
+ * @param pc current phase context with options to use
+ * @return NULL on error
+ */
+static CURL *
+setup_curl (const struct MHDT_PhaseContext *pc)
+{
+ struct MHDT_Phase *p = pc->phase;
+ CURL *c;
+
+ c = curl_easy_init ();
+ if (NULL == c)
+ return NULL;
+ switch (p->http_version)
+ {
+ case 0: /* unset == HTTP/1.x! */
+ case 1:
+ break;
+ case 2: /* HTTP/2 */
+ if (CURLE_OK !=
+ curl_easy_setopt (c,
+ CURLOPT_HTTP_VERSION,
+ CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE))
+ {
+ curl_easy_cleanup (c);
+ fprintf (stderr,
+ "HTTP/2 not supported by curl?\n");
+ return NULL;
+ }
+ break;
+ case 3:
+ abort (); // not yet supported
+ break;
+ default:
+ abort ();
+ break;
+ }
+ return c;
+}
+
+
const char *
MHDT_client_get_host (const void *cls,
struct MHDT_PhaseContext *pc)
@@ -379,7 +422,7 @@ MHDT_client_get_host (const void *cls,
"https://%s%s",
host,
colon);
- c = curl_easy_init ();
+ c = setup_curl (pc);
if (NULL == c)
return "Failed to initialize Curl handle";
err = set_url (c,
@@ -405,7 +448,7 @@ MHDT_client_get_root (
const char *err;
DECLARE_WB (text);
- c = curl_easy_init ();
+ c = setup_curl (pc);
if (NULL == c)
return "Failed to initialize Curl handle";
err = set_url (c,
@@ -442,7 +485,7 @@ MHDT_client_get_with_query (
args,
alen);
u[alen + blen] = '\0';
- c = curl_easy_init ();
+ c = setup_curl (pc);
if (NULL == c)
return "Failed to initialize Curl handle";
err = set_url (c,
@@ -469,7 +512,7 @@ MHDT_client_set_header (
CURLcode res;
struct curl_slist *slist;
- c = curl_easy_init ();
+ c = setup_curl (pc);
if (NULL == c)
return "Failed to initialize Curl handle";
err = set_url (c,
@@ -523,7 +566,7 @@ MHDT_client_expect_header (const void *cls,
hlen);
key[colon - hdr] = '\0';
value = &key[colon - hdr + 1];
- c = curl_easy_init ();
+ c = setup_curl (pc);
if (NULL == c)
return "Failed to initialize Curl handle";
err = set_url (c,
@@ -642,7 +685,7 @@ MHDT_client_put_data (
};
CURL *c;
- c = curl_easy_init ();
+ c = setup_curl (pc);
if (NULL == c)
return "Failed to initialize Curl handle";
err = set_url (c,
@@ -704,7 +747,7 @@ MHDT_client_chunk_data (
};
CURL *c;
- c = curl_easy_init ();
+ c = setup_curl (pc);
if (NULL == c)
return "Failed to initialize Curl handle";
err = set_url (c,
@@ -763,7 +806,7 @@ MHDT_client_do_post (
pi->wants[i].satisfied = false;
}
}
- c = curl_easy_init ();
+ c = setup_curl (pc);
if (NULL == c)
return "Failed to initialize Curl handle";
err = set_url (c,
@@ -858,7 +901,7 @@ send_basic_auth (const char *cred,
user = strndup (cred,
pass - cred);
pass++;
- c = curl_easy_init ();
+ c = setup_curl (pc);
if (NULL == c)
{
free (user);
@@ -969,7 +1012,7 @@ send_digest_auth (const char *cred,
user = strndup (cred,
pass - cred);
pass++;
- c = curl_easy_init ();
+ c = setup_curl (pc);
if (NULL == c)
{
free (user);
diff --git a/src/tests/client_server/libtest_convenience_server_reply.c b/src/tests/client_server/libtest_convenience_server_reply.c
@@ -187,7 +187,7 @@ MHDT_server_reply_check_query (
tok = strtok (NULL, "&"))
{
const char *end;
- const struct MHD_StringNullable *sn;
+ struct MHD_StringNullable sn;
const char *val;
end = strchr (tok, '=');
@@ -208,10 +208,11 @@ MHDT_server_reply_check_query (
tok,
alen);
arg[alen] = '\0';
- sn = MHD_request_get_value (request,
- MHD_VK_URI_QUERY_PARAM,
- arg);
- if (NULL == sn)
+ if (MHD_NO ==
+ MHD_request_get_value (request,
+ MHD_VK_URI_QUERY_PARAM,
+ arg,
+ &sn))
{
fprintf (stderr,
"NULL returned for query key %s\n",
@@ -220,18 +221,18 @@ MHDT_server_reply_check_query (
}
if (NULL == val)
{
- if (NULL != sn->cstr)
+ if (NULL != sn.cstr)
{
fprintf (stderr,
"NULL expected for value for query key %s, got %s\n",
arg,
- sn->cstr);
+ sn.cstr);
return MHD_action_abort_request (request);
}
}
else
{
- if (NULL == sn->cstr)
+ if (NULL == sn.cstr)
{
fprintf (stderr,
"%s expected for value for query key %s, got NULL\n",
@@ -240,13 +241,13 @@ MHDT_server_reply_check_query (
return MHD_action_abort_request (request);
}
if (0 != strcmp (val,
- sn->cstr))
+ sn.cstr))
{
fprintf (stderr,
"%s expected for value for query key %s, got %s\n",
val,
arg,
- sn->cstr);
+ sn.cstr);
return MHD_action_abort_request (request);
}
}
@@ -272,7 +273,7 @@ MHDT_server_reply_check_header (
size_t wlen = strlen (want) + 1;
char key[wlen];
const char *colon = strchr (want, ':');
- const struct MHD_StringNullable *have;
+ struct MHD_StringNullable have;
const char *value;
(void) path; (void) method; (void) upload_size; /* Unused */
@@ -289,10 +290,11 @@ MHDT_server_reply_check_header (
{
value = NULL;
}
- have = MHD_request_get_value (request,
- MHD_VK_HEADER,
- key);
- if (NULL == have)
+ if (MHD_NO ==
+ MHD_request_get_value (request,
+ MHD_VK_HEADER,
+ key,
+ &have))
{
fprintf (stderr,
"Missing client header `%s'\n",
@@ -301,32 +303,32 @@ MHDT_server_reply_check_header (
}
if (NULL == value)
{
- if (NULL != have->cstr)
+ if (NULL != have.cstr)
{
fprintf (stderr,
"Have unexpected client header `%s': `%s'\n",
key,
- have->cstr);
+ have.cstr);
return MHD_action_abort_request (request);
}
}
else
{
- if (NULL == have->cstr)
+ if (NULL == have.cstr)
{
fprintf (stderr,
"Missing value for client header `%s'\n",
want);
return MHD_action_abort_request (request);
}
- if (0 != strcmp (have->cstr,
+ if (0 != strcmp (have.cstr,
value))
{
fprintf (stderr,
"Client HTTP header `%s' was expected to be `%s' but is `%s'\n",
key,
value,
- have->cstr);
+ have.cstr);
return MHD_action_abort_request (request);
}
}
diff --git a/src/tests/client_server/test_http2.c b/src/tests/client_server/test_http2.c
@@ -0,0 +1,317 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later OR (GPL-2.0-or-later WITH eCos-exception-2.0) */
+/*
+ This file is part of GNU libmicrohttpd.
+ Copyright (C) 2016, 2024, 2025 Christian Grothoff & Evgeny Grin (Karlson2k)
+
+ GNU libmicrohttpd is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ GNU libmicrohttpd is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ Alternatively, you can redistribute GNU libmicrohttpd and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 2 of
+ the License, or (at your option) any later version, together
+ with the eCos exception, as follows:
+
+ As a special exception, if other files instantiate templates or
+ use macros or inline functions from this file, or you compile this
+ file and link it with other works to produce a work based on this
+ file, this file does not by itself cause the resulting work to be
+ covered by the GNU General Public License. However the source code
+ for this file must still be made available in accordance with
+ section (3) of the GNU General Public License v2.
+
+ This exception does not invalidate any other reasons why a work
+ based on this file might be covered by the GNU General Public
+ License.
+
+ You should have received copies of the GNU Lesser General Public
+ License and the GNU General Public License along with this library;
+ if not, see <https://www.gnu.org/licenses/>.
+*/
+
+/**
+ * @file test_http2.c
+ * @brief test with client against server
+ * @author Christian Grothoff
+ */
+#include "libtest.h"
+
+
+int
+main (int argc, char *argv[])
+{
+ struct MHD_DaemonOptionAndValue thread1select[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_SELECT),
+ MHD_D_OPTION_WM_WORKER_THREADS (1),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct MHD_DaemonOptionAndValue thread2select[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_SELECT),
+ MHD_D_OPTION_WM_WORKER_THREADS (2),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct MHD_DaemonOptionAndValue thread1poll[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_POLL),
+ MHD_D_OPTION_WM_WORKER_THREADS (1),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct MHD_DaemonOptionAndValue thread2poll[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_POLL),
+ MHD_D_OPTION_WM_WORKER_THREADS (1),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct MHD_DaemonOptionAndValue thread1epoll[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_EPOLL),
+ MHD_D_OPTION_WM_WORKER_THREADS (1),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct MHD_DaemonOptionAndValue thread2epoll[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_EPOLL),
+ MHD_D_OPTION_WM_WORKER_THREADS (1),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct MHD_DaemonOptionAndValue thread1auto[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_AUTO),
+ MHD_D_OPTION_WM_WORKER_THREADS (1),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct MHD_DaemonOptionAndValue external0auto[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_AUTO),
+ MHD_D_OPTION_WM_EXTERNAL_PERIODIC (),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct ServerType
+ {
+ const char *label;
+ MHDT_ServerSetup server_setup;
+ void *server_setup_cls;
+ MHDT_ServerRunner server_runner;
+ void *server_runner_cls;
+ } configs[] = {
+#ifdef MHD_SUPPORT_SELECT
+ {
+ .label = "single threaded select",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = thread1select,
+ .server_runner = &MHDT_server_run_minimal,
+ },
+ {
+ .label = "multi-threaded select",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = thread2select,
+ .server_runner = &MHDT_server_run_minimal,
+ },
+#endif
+#ifdef MHD_SUPPORT_POLL
+ {
+ .label = "single threaded poll",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = thread1poll,
+ .server_runner = &MHDT_server_run_minimal,
+ },
+ {
+ .label = "multi-threaded poll",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = thread2poll,
+ .server_runner = &MHDT_server_run_minimal,
+ },
+#endif
+#if MHD_SUPPORT_EPOLL
+ {
+ .label = "single threaded epoll",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = thread1epoll,
+ .server_runner = &MHDT_server_run_minimal,
+ },
+ {
+ .label = "multi-threaded epoll",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = thread2epoll,
+ .server_runner = &MHDT_server_run_minimal,
+ },
+#endif
+ {
+ .label = "auto-selected mode, single threaded",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = thread1auto,
+ .server_runner = &MHDT_server_run_minimal,
+ },
+#ifdef MHD_SUPPORT_EPOLL
+ {
+ .label = "external events loop mode, no internal threads",
+ .server_setup = &MHDT_server_setup_external,
+ .server_setup_cls = NULL,
+ .server_runner = &MHDT_server_run_external,
+ },
+#endif /* MHD_SUPPORT_EPOLL */
+#if 1
+ /* FIXME: remove once MHD_daemon_process_blocking
+ has been implemented */
+ {
+ .label = "END"
+ },
+#endif
+ {
+ .label = "auto-selected external event loop mode, no threads",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = external0auto,
+ .server_runner = &MHDT_server_run_blocking,
+ },
+ {
+ .label = "END"
+ }
+ };
+ struct MHDT_Phase phases[] = {
+ {
+ .label = "simple get",
+ .server_cb = &MHDT_server_reply_text,
+ .server_cb_cls = (void *) "Hello world",
+ .client_cb = &MHDT_client_get_root,
+ .client_cb_cls = "Hello world",
+ .timeout_ms = 2500,
+ .http_version = 2,
+ },
+ {
+ .label = "GET with sendfile",
+ .server_cb = &MHDT_server_reply_file,
+ .server_cb_cls = (void *) "Hello world",
+ .client_cb = &MHDT_client_get_root,
+ .client_cb_cls = "Hello world",
+ .timeout_ms = 2500,
+ .http_version = 2,
+ },
+ {
+ .label = "client PUT with content-length",
+ .server_cb = &MHDT_server_reply_check_upload,
+ .server_cb_cls = (void *) "simple-upload-value",
+ .client_cb = &MHDT_client_put_data,
+ .client_cb_cls = "simple-upload-value",
+ .timeout_ms = 2500,
+ .http_version = 2,
+ },
+ {
+ .label = "client PUT with 2 chunks",
+ .server_cb = &MHDT_server_reply_check_upload,
+ .server_cb_cls = (void *) "chunky-upload-value",
+ .client_cb = &MHDT_client_chunk_data,
+ .client_cb_cls = "chunky-upload-value",
+ .timeout_ms = 2500,
+ .http_version = 2,
+ },
+ {
+ .label = "client request with custom header",
+ .server_cb = &MHDT_server_reply_check_header,
+ .server_cb_cls = (void *) "C-Header:testvalue",
+ .client_cb = &MHDT_client_set_header,
+ .client_cb_cls = "C-Header:testvalue",
+ .timeout_ms = 2500,
+ .http_version = 2,
+ },
+ {
+ .label = "server response with custom header",
+ .server_cb = &MHDT_server_reply_with_header,
+ .server_cb_cls = (void *) "X-Header:testvalue",
+ .client_cb = &MHDT_client_expect_header,
+ .client_cb_cls = "X-Header:testvalue",
+ .timeout_ms = 2500,
+ .http_version = 2,
+ },
+ {
+ .label = "URL with query parameters 1",
+ .server_cb = &MHDT_server_reply_check_query,
+ .server_cb_cls = (void *) "a=b&c",
+ .client_cb = &MHDT_client_get_with_query,
+ .client_cb_cls = "?a=b&c",
+ .timeout_ms = 5000,
+ .num_clients = 4,
+ .http_version = 2,
+ },
+ {
+ .label = "URL with query parameters 2",
+ .server_cb = &MHDT_server_reply_check_query,
+ .server_cb_cls = (void *) "a=b&c", /* a => b, c => NULL */
+ .client_cb = &MHDT_client_get_with_query,
+ .client_cb_cls = "?c&a=b",
+ .timeout_ms = 5000,
+ .num_clients = 1,
+ .http_version = 2,
+ },
+ {
+ .label = "URL with query parameters 3",
+ .server_cb = &MHDT_server_reply_check_query,
+ .server_cb_cls = (void *) "a=&c", /* a => "", c => NULL */
+ .client_cb = &MHDT_client_get_with_query,
+ .client_cb_cls = "?c&a=",
+ .timeout_ms = 5000,
+ .num_clients = 1,
+ .http_version = 2,
+ },
+ {
+ .label = "URL with query parameters 4",
+ .server_cb = &MHDT_server_reply_check_query,
+ .server_cb_cls = (void *) "a=", /* a => "" */
+ .client_cb = &MHDT_client_get_with_query,
+ .client_cb_cls = "?a=",
+ .timeout_ms = 5000,
+ .num_clients = 1,
+ .http_version = 2,
+ },
+ {
+ .label = "URL with query parameters 5",
+ .server_cb = &MHDT_server_reply_check_query,
+ .server_cb_cls = (void *) "a=b", /* a => "b" */
+ .client_cb = &MHDT_client_get_with_query,
+ .client_cb_cls = "?a=b",
+ .timeout_ms = 5000,
+ .num_clients = 1,
+ .http_version = 2,
+ },
+ {
+ .label = "chunked response get",
+ .server_cb = &MHDT_server_reply_chunked_text,
+ .server_cb_cls = (void *) "Hello world",
+ .client_cb = &MHDT_client_get_root,
+ .client_cb_cls = "Hello world",
+ .timeout_ms = 2500,
+ .http_version = 2,
+ },
+ // TODO: chunked download
+ {
+ .label = NULL,
+ },
+ };
+ unsigned int i;
+
+ (void) argc; /* Unused. Silence compiler warning. */
+ (void) argv; /* Unused. Silence compiler warning. */
+
+ for (i = 0; NULL != configs[i].server_setup; i++)
+ {
+ int ret;
+
+ fprintf (stderr,
+ "Running tests with server setup '%s'\n",
+ configs[i].label);
+ ret = MHDT_test (configs[i].server_setup,
+ configs[i].server_setup_cls,
+ configs[i].server_runner,
+ configs[i].server_runner_cls,
+ phases);
+ if (0 != ret)
+ {
+ fprintf (stderr,
+ "Test failed with server of type '%s' (%u)\n",
+ configs[i].label,
+ i);
+ return ret;
+ }
+ }
+ return 0;
+}