libmicrohttpd2

HTTP server C library (MHD 2.x, alpha)
Log | Files | Refs | README | LICENSE

commit 9873cb011aa46b45ea25c0930cce85cdd1afd8a0
parent c744d067ebcec0677cf7fb6c769b5bd72a679143
Author: Christian Grothoff <grothoff@gnunet.org>
Date:   Mon, 29 Jul 2024 14:51:58 +0200

test framework + tests against test framework

Diffstat:
Mconfigure.ac | 1+
Msrc/tests/Makefile.am | 2+-
Asrc/tests/client_server/Makefile.am | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/tests/client_server/libtest.c | 616+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/tests/client_server/libtest.h | 478+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/tests/client_server/libtest_convenience.c | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/tests/client_server/libtest_convenience_client_request.c | 568+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/tests/client_server/libtest_convenience_server_reply.c | 450+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/tests/client_server/test_client_server.c | 281+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 2594 insertions(+), 1 deletion(-)

diff --git a/configure.ac b/configure.ac @@ -7206,6 +7206,7 @@ src/include/Makefile src/mhd2/Makefile src/tests/Makefile src/tests/basic/Makefile +src/tests/client_server/Makefile src/examples2/Makefile ]) AC_OUTPUT diff --git a/src/tests/Makefile.am b/src/tests/Makefile.am @@ -1,5 +1,5 @@ # This Makefile.am is in the public domain -SUBDIRS = basic +SUBDIRS = basic client_server .NOTPARALLEL: diff --git a/src/tests/client_server/Makefile.am b/src/tests/client_server/Makefile.am @@ -0,0 +1,49 @@ +# This Makefile.am is in the public domain +EMPTY_ITEM = + +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/include \ + -I$(top_srcdir)/src/mhd2 \ + -DMHD_CPU_COUNT=$(CPU_COUNT) \ + $(CPPFLAGS_ac) + +AM_CFLAGS = $(CFLAGS_ac) + +AM_LDFLAGS = $(LDFLAGS_ac) + +AM_TESTS_ENVIRONMENT = $(TESTS_ENVIRONMENT_ac) + +if USE_COVERAGE + AM_CFLAGS += -fprofile-arcs -ftest-coverage +endif + +LDADD = $(top_builddir)/src/mhd2/libmicrohttpd2.la +LIBADD = $(top_builddir)/src/mhd2/libmicrohttpd2.la + +$(top_builddir)/src/mhd2/libmicrohttpd2.la: $(top_builddir)/src/mhd2/Makefile + @echo ' cd $(top_builddir)/src/mhd2 && $(MAKE) $(AM_MAKEFLAGS) libmicrohttpd2.la'; \ + $(am__cd) $(top_builddir)/src/mhd2 && $(MAKE) $(AM_MAKEFLAGS) libmicrohttpd2.la + +check_PROGRAMS = \ + test_client_server + +TESTS = $(check_PROGRAMS) + + +noinst_LTLIBRARIES = \ + libmhdt.la + +libmhdt_la_SOURCES = \ + libtest.c libtest.h \ + libtest_convenience.c \ + libtest_convenience_client_request.c \ + libtest_convenience_server_reply.c +libmhdt_la_LIBADD = \ + -lpthread \ + -lcurl \ + $(LIBADD) + +test_client_server_SOURCES = \ + test_client_server.c +test_client_server_LDADD = \ + libmhdt.la diff --git a/src/tests/client_server/libtest.c b/src/tests/client_server/libtest.c @@ -0,0 +1,616 @@ +/* + This file is part of GNU libmicrohttpd + Copyright (C) 2024 Christian Grothoff + + 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. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +/** + * @file libtest.c + * @brief testing harness with clients against server + * @author Christian Grothoff + */ +#include <pthread.h> +#include <stdbool.h> +#include <fcntl.h> +#include <unistd.h> +#include <errno.h> +#include "microhttpd2.h" +#include "libtest.h" + +/** + * A semaphore. + */ +struct Semaphore +{ + /** + * Mutex for the semaphore. + */ + pthread_mutex_t mutex; + + /** + * Condition variable for the semaphore. + */ + pthread_cond_t cv; + + /** + * Counter of the semaphore. + */ + unsigned int ctr; +}; + + +/** + * Check that @a cond is true, otherwise abort(). + * + * @param cond condition to check + * @param filename filename to log + * @param line line number to log + */ +static void +test_check_ (bool cond, + const char *filename, + unsigned int line) +{ + if (! cond) + { + fprintf (stderr, + "Assertion failed at %s:%u\n", + filename, + line); + abort (); + } +} + + +/** + * Checks that @a cond is true and otherwise aborts. + * + * @param cond condition to check + */ +#define test_check(cond) \ + test_check_ (cond, __FILE__, __LINE__) + + +/** + * Initialize a semaphore @a sem with a value of @a val. + * + * @param[out] sem semaphore to initialize + * @param val initial value of the semaphore + */ +static void +semaphore_create (struct Semaphore *sem, + unsigned int val) +{ + test_check (0 == + pthread_mutex_init (&sem->mutex, + NULL)); + test_check (0 == + pthread_cond_init (&sem->cv, + NULL)); + sem->ctr = val; +} + + +/** + * Decrement semaphore, blocks until this is possible. + * + * @param[in,out] sem semaphore to decrement + */ +static void +semaphore_down (struct Semaphore *sem) +{ + test_check (0 == pthread_mutex_lock (&sem->mutex)); + while (0 == sem->ctr) + { + pthread_cond_wait (&sem->cv, + &sem->mutex); + } + sem->ctr--; + test_check (0 == pthread_mutex_unlock (&sem->mutex)); +} + + +/** + * Increment semaphore, blocks until this is possible. + * + * @param[in,out] sem semaphore to decrement + */ +static void +semaphore_up (struct Semaphore *sem) +{ + test_check (0 == pthread_mutex_lock (&sem->mutex)); + sem->ctr++; + test_check (0 == pthread_mutex_unlock (&sem->mutex)); + pthread_cond_signal (&sem->cv); +} + + +/** + * Release resources used by @a sem. + * + * @param[in] sem semaphore to release (except the memory itself) + */ +static void +semaphore_destroy (struct Semaphore *sem) +{ + test_check (0 == pthread_cond_destroy (&sem->cv)); + test_check (0 == pthread_mutex_destroy (&sem->mutex)); +} + + +/** + * Context for the implementation of the HTTP server. + */ +struct ServerContext +{ + /** + * Semaphore the client raises when it goes into the + * next phase. + */ + struct Semaphore client_sem; + + /** + * Semaphore the server raises when it goes into the + * next phase. + */ + struct Semaphore server_sem; + + /** + * Current phase of the server. + */ + const struct MHDT_Phase *phase; + + /** + * Main function to run the server. + */ + MHDT_ServerRunner run_cb; + + /** + * Closure for @e run_cb. + */ + void *run_cb_cls; + + /** + * The daemon we are running. + */ + struct MHD_Daemon *d; + + /** + * Signal for server termination. + */ + int finsig; +}; + + +/** + * A client has requested the given url using the given method + * (#MHD_HTTP_METHOD_GET, #MHD_HTTP_METHOD_PUT, + * #MHD_HTTP_METHOD_DELETE, #MHD_HTTP_METHOD_POST, etc). + * If @a upload_size is not zero and response action is provided by this + * callback, then upload will be discarded and the stream (the connection for + * HTTP/1.1) will be closed after sending the response. + * + * @param cls argument given together with the function + * pointer when the handler was registered with MHD + * @param request the request object + * @param path the requested uri (without arguments after "?") + * @param method the HTTP method used (#MHD_HTTP_METHOD_GET, + * #MHD_HTTP_METHOD_PUT, etc.) + * @param upload_size the size of the message upload content payload, + * #MHD_SIZE_UNKNOWN for chunked uploads (if the + * final chunk has not been processed yet) + * @return action how to proceed, NULL + * if the request must be aborted due to a serious + * error while handling the request (implies closure + * of underling data stream, for HTTP/1.1 it means + * socket closure). + */ +static const struct MHD_Action * +server_req_cb (void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size) +{ + struct ServerContext *sc = cls; + + if (NULL == sc->phase->label) + return NULL; + return sc->phase->server_cb (sc->phase->server_cb_cls, + request, + path, + method, + upload_size); +} + + +/** + * Closure for run_single_client() + */ +struct ClientContext +{ + /** + * Test phase to run. + */ + const struct MHDT_Phase *phase; + + /** + * Phase and client specific context. + */ + struct MHDT_PhaseContext pc; + + /** + * Pipe to use to signal that the thread has + * finished. + */ + int p2; + + /** + * Set to true on success. + */ + bool status; +}; + + +/** + * Runs the logic for a single client in a thread. + * + * @param cls a `struct ClientContext` + * @return NULL + */ +static void * +run_single_client (void *cls) +{ + struct ClientContext *cc = cls; + const char *err; + + fprintf (stderr, + "Client %u started in phase `%s'\n", + cc->pc.client_id, + cc->phase->label); + err = cc->phase->client_cb (cc->phase->client_cb_cls, + &cc->pc); + if (NULL != err) + { + fprintf (stderr, + "Client %u failed in phase `%s': %s\n", + cc->pc.client_id, + cc->phase->label, + err); + /* This is a blocking write, thus must succeed */ + test_check (1 == + write (cc->p2, + "e", + 1)); + return NULL; + } + cc->status = true; + /* This is a blocking write, thus must succeed */ + test_check (1 == + write (cc->p2, + "s", + 1)); + fprintf (stderr, + "Client %u finished in phase `%s'\n", + cc->pc.client_id, + cc->phase->label); + return NULL; +} + + +/** + * Creates a pipe with a non-blocking read end. + * + * @param p pipe to initialize + */ +static void +make_pipe (int p[2]) +{ + int flags; + + test_check (0 == + pipe (p)); + flags = fcntl (p[0], + F_GETFL); + flags |= O_NONBLOCK; + test_check (0 == + fcntl (p[0], + F_SETFL, + flags)); +} + + +/** + * Run client processes for the given test @a phase + * + * @param phase test phase to run + * @param pc context to give to clients + */ +static bool +run_client_phase (const struct MHDT_Phase *phase, + const struct MHDT_PhaseContext *pc) +{ + unsigned int num_clients + = (0 == phase->num_clients) + ? 1 + : phase->num_clients; + unsigned int clients_left = 0; + struct ClientContext cctxs[num_clients]; + pthread_t clients[num_clients]; + int p[2]; + unsigned int i; + bool ret = true; + + make_pipe (p); + fprintf (stderr, + "Starting phase `%s'\n", + phase->label); + for (i = 0; i<num_clients; i++) + { + cctxs[i].phase = phase; + cctxs[i].pc = *pc; + cctxs[i].pc.client_id = i; + cctxs[i].p2 = p[1]; + cctxs[i].status = false; + if (0 != + pthread_create (&clients[i], + NULL, + &run_single_client, + &cctxs[i])) + goto cleanup; + clients_left++; + } + + /* 0 for timeout_ms means no timeout, we deliberately + underflow to MAX_UINT in this case... */ + for (i = phase->timeout_ms - 1; i>0; i--) + { + struct timespec ms = { + .tv_nsec = 1000 * 1000 + }; + struct timespec rem; + char c; + + if (0 != nanosleep (&ms, + &rem)) + { + fprintf (stderr, + "nanosleep() interrupted (%s), trying again\n", + strerror (errno)); + i++; + } + /* This is a non-blocking read */ + while (1 == read (p[0], + &c, + 1)) + clients_left--; + if (0 == clients_left) + break; + } + if (0 != clients_left) + { + fprintf (stderr, + "Timeout (%u ms) in phase `%s': %u clients still running\n", + phase->timeout_ms, + phase->label, + clients_left); + exit (1); + } +cleanup: + for (i = 0; i<num_clients; i++) + { + void *res; + + test_check (0 == + pthread_join (clients[i], + &res)); + if (! cctxs[i].status) + ret = false; + } + test_check (0 == close (p[0])); + test_check (0 == close (p[1])); + fprintf (stderr, + "Finished phase `%s' with %s\n", + phase->label, + ret ? "success" : "FAILURE"); + return ret; +} + + +/** + * Thread that switches the server to the next phase + * as needed. + * + * @param cls a `struct ServerContext` + * @return NULL + */ +static void * +server_phase_logic (void *cls) +{ + struct ServerContext *ctx = cls; + unsigned int i; + + for (i = 0; NULL != ctx->phase->label; i++) + { + fprintf (stderr, + "Running server phase `%s'\n", + ctx->phase->label); + semaphore_down (&ctx->client_sem); + ctx->phase++; + semaphore_up (&ctx->server_sem); + } + fprintf (stderr, + "Server terminating\n"); + return NULL; +} + + +/** + * Thread that runs the MHD daemon. + * + * @param cls a `struct ServerContext` + * @return NULL + */ +static void * +server_run_logic (void *cls) +{ + struct ServerContext *ctx = cls; + + ctx->run_cb (ctx->run_cb_cls, + ctx->finsig, + ctx->d); + return NULL; +} + + +int +MHDT_test (MHDT_ServerSetup ss_cb, + void *ss_cb_cls, + MHDT_ServerRunner run_cb, + void *run_cb_cls, + const struct MHDT_Phase *phases) +{ + struct ServerContext ctx = { + .run_cb = run_cb, + .run_cb_cls = run_cb_cls, + .phase = &phases[0] + }; + struct MHD_Daemon *d; + int res; + const char *err; + pthread_t server_phase_thr; + pthread_t server_run_thr; + struct MHDT_PhaseContext pc; + char base_url[128]; + unsigned int i; + int p[2]; + + make_pipe (p); + semaphore_create (&ctx.server_sem, + 0); + semaphore_create (&ctx.client_sem, + 0); + d = MHD_daemon_create (&server_req_cb, + &ctx); + if (NULL == d) + exit (77); + err = ss_cb (ss_cb_cls, + d); + if (NULL != err) + { + fprintf (stderr, + "Failed to setup server: %s\n", + err); + return 1; + } + { + enum MHD_StatusCode sc; + + sc = MHD_daemon_start (d); + if (MHD_SC_OK != sc) + { + fprintf (stderr, + "Failed to start server: %s\n", + err); + return 1; + } + } + { + union MHD_DaemonInfoFixedData info; + enum MHD_StatusCode sc; + + sc = MHD_daemon_get_info_fixed ( + d, + MHD_DAEMON_INFO_FIXED_BIND_PORT, + &info); + test_check (MHD_SC_OK == sc); + snprintf (base_url, + sizeof (base_url), + "http://localhost:%u/", + (unsigned int) info.v_port); + pc.base_url = base_url; + } + if (0 != pthread_create (&server_phase_thr, + NULL, + &server_phase_logic, + &ctx)) + { + fprintf (stderr, + "Failed to start server phase thread: %s\n", + strerror (errno)); + return 77; + } + ctx.finsig = p[0]; + ctx.d = d; + if (0 != pthread_create (&server_run_thr, + NULL, + &server_run_logic, + &ctx)) + { + fprintf (stderr, + "Failed to start server run thread: %s\n", + strerror (errno)); + return 77; + } + for (i = 0; NULL != phases[i].label; i++) + { + fprintf (stderr, + "Running test phase `%s'\n", + phases[i].label); + if (! run_client_phase (&phases[i], + &pc)) + { + res = 1; + goto cleanup; + } + /* client is done with phase */ + semaphore_up (&ctx.client_sem); + /* wait for server to have moved to new phase */ + semaphore_down (&ctx.server_sem); + } + res = 0; +cleanup: + /* stop thread that runs the actual server */ + { + void *pres; + + test_check (1 == + write (p[1], + "e", + 1)); + test_check (0 == + pthread_join (server_run_thr, + &pres)); + } + { + void *pres; + + /* Unblock the #server_phase_logic() even if we had + an error */ + for (i = 0; NULL != phases[i].label; i++) + semaphore_up (&ctx.client_sem); + test_check (0 == + pthread_join (server_phase_thr, + &pres)); + } + MHD_daemon_destroy (d); + semaphore_destroy (&ctx.client_sem); + semaphore_destroy (&ctx.server_sem); + test_check (0 == close (p[0])); + test_check (0 == close (p[1])); + return res; +} diff --git a/src/tests/client_server/libtest.h b/src/tests/client_server/libtest.h @@ -0,0 +1,478 @@ +/* + This file is part of GNU libmicrohttpd + Copyright (C) 2024 Christian Grothoff + + 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. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +/** + * @file libtest.h + * @brief testing harness with clients against server + * @author Christian Grothoff + */ +#ifndef LIBTEST_H +#define LIBTEST_H + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <microhttpd2.h> + +/** + * Information about the current phase. + */ +struct MHDT_PhaseContext +{ + /** + * Base URL of the server + */ + const char *base_url; + + /** + * Specific client we are running. + */ + unsigned int client_id; +}; + + +/** + * Function called to run some client logic against + * the server. + * + * @param cls closure + * @param pc context for the client + * @return error message, NULL on success + */ +typedef const char * +(*MHDT_ClientLogic)(void *cls, + const struct MHDT_PhaseContext *pc); + + +/** + * Run request against the base URL and expect the + * string in @a cls to be returned + * + * @param cls closure with text string to be returned + * @param pc context for the client + * @return error message, NULL on success + */ +const char * +MHDT_client_get_root (void *cls, + const struct MHDT_PhaseContext *pc); + + +/** + * Run request against the base URL with the + * query arguments from @a cls appended to it. + * Expect the server to return a 200 OK response. + * + * @param cls closure with query parameters to append + * to the base URL of the server + * @param pc context for the client + * @return error message, NULL on success + */ +const char * +MHDT_client_get_with_query (void *cls, + const struct MHDT_PhaseContext *pc); + + +/** + * Run request against the base URL with the + * custom header from @a cls set. + * Expect the server to return a 204 No content response. + * + * @param cls closure with custom header to set + * @param pc context for the client + * @return error message, NULL on success + */ +const char * +MHDT_client_set_header (void *cls, + const struct MHDT_PhaseContext *pc); + + +/** + * Run request against the base URL and expect the header from @a cls to be + * set in the 204 No content response. + * + * @param cls closure with custom header to set, + * must be of the format "$KEY:$VALUE" + * without space before the "$VALUE". + * @param pc context for the client + * @return error message, NULL on success + */ +const char * +MHDT_client_expect_header (void *cls, + const struct MHDT_PhaseContext *pc); + + +/** + * Run simple upload against the base URL and expect a + * 204 No Content response. + * + * @param cls 0-terminated string with data to PUT + * @param pc context for the client + * @return error message, NULL on success + */ +const char * +MHDT_client_put_data (void *cls, + const struct MHDT_PhaseContext *pc); + + +/** + * Run chunked upload against the base URL and expect a + * 204 No Content response. + * + * @param cls 0-terminated string with data to PUT + * @param pc context for the client + * @return error message, NULL on success + */ +const char * +MHDT_client_chunk_data (void *cls, + const struct MHDT_PhaseContext *pc); + + +/** + * A phase defines some server and client-side + * behaviors to execute. + */ +struct MHDT_Phase +{ + + /** + * Name of the phase, for debugging/logging. + */ + const char *label; + + /** + * Logic for the MHD server for this phase. + */ + MHD_RequestCallback server_cb; + + /** + * Closure for @e server_cb. + */ + void *server_cb_cls; + + /** + * Logic for the CURL client for this phase. + */ + MHDT_ClientLogic client_cb; + + /** + * Closure for @e client_cb. + */ + void *client_cb_cls; + + /** + * How long is the phase allowed to run at most before + * timing out. 0 for no timeout. + */ + unsigned int timeout_ms; + + /** + * How many clients should be run in parallel. + * 0 to run just one client. + */ + unsigned int num_clients; +}; + + +/** + * Returns the text from @a cls as the response to any + * request. + * + * @param cls argument given together with the function + * pointer when the handler was registered with MHD + * @param request the request object + * @param path the requested uri (without arguments after "?") + * @param method the HTTP method used (#MHD_HTTP_METHOD_GET, + * #MHD_HTTP_METHOD_PUT, etc.) + * @param upload_size the size of the message upload content payload, + * #MHD_SIZE_UNKNOWN for chunked uploads (if the + * final chunk has not been processed yet) + * @return action how to proceed, NULL + * if the request must be aborted due to a serious + * error while handling the request (implies closure + * of underling data stream, for HTTP/1.1 it means + * socket closure). + */ +const struct MHD_Action * +MHDT_server_reply_text ( + void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size); + + +/** + * Returns the text from @a cls as the response to any + * request, but using chunks by returning @a cls + * word-wise (breaking into chunks at spaces). + * + * @param cls argument given together with the function + * pointer when the handler was registered with MHD + * @param request the request object + * @param path the requested uri (without arguments after "?") + * @param method the HTTP method used (#MHD_HTTP_METHOD_GET, + * #MHD_HTTP_METHOD_PUT, etc.) + * @param upload_size the size of the message upload content payload, + * #MHD_SIZE_UNKNOWN for chunked uploads (if the + * final chunk has not been processed yet) + * @return action how to proceed, NULL + * if the request must be aborted due to a serious + * error while handling the request (implies closure + * of underling data stream, for HTTP/1.1 it means + * socket closure). + */ +const struct MHD_Action * +MHDT_server_reply_chunked_text ( + void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size); + + +/** + * Returns writes text from @a cls to a temporary file + * and then uses the file descriptor to serve the + * content to the client. + * + * @param cls argument given together with the function + * pointer when the handler was registered with MHD + * @param request the request object + * @param path the requested uri (without arguments after "?") + * @param method the HTTP method used (#MHD_HTTP_METHOD_GET, + * #MHD_HTTP_METHOD_PUT, etc.) + * @param upload_size the size of the message upload content payload, + * #MHD_SIZE_UNKNOWN for chunked uploads (if the + * final chunk has not been processed yet) + * @return action how to proceed, NULL + * if the request must be aborted due to a serious + * error while handling the request (implies closure + * of underling data stream, for HTTP/1.1 it means + * socket closure). + */ +const struct MHD_Action * +MHDT_server_reply_file ( + void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size); + + +/** + * Returns an emtpy response with a custom header + * set from @a cls and the #MHD_HTTP_STATUS_NO_CONTENT. + * + * @param cls header in the format "$NAME:$VALUE" + * without a space before "$VALUE". + * @param request the request object + * @param path the requested uri (without arguments after "?") + * @param method the HTTP method used (#MHD_HTTP_METHOD_GET, + * #MHD_HTTP_METHOD_PUT, etc.) + * @param upload_size the size of the message upload content payload, + * #MHD_SIZE_UNKNOWN for chunked uploads (if the + * final chunk has not been processed yet) + * @return action how to proceed, NULL + * if the request must be aborted due to a serious + * error while handling the request (implies closure + * of underling data stream, for HTTP/1.1 it means + * socket closure). + */ +const struct MHD_Action * +MHDT_server_reply_with_header ( + void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size); + + +/** + * Checks that the request query arguments match the + * arguments given in @a cls. + * request. + * + * @param cls string with expected arguments separated by '&' and '='. URI encoding is NOT supported. + * @param request the request object + * @param path the requested uri (without arguments after "?") + * @param method the HTTP method used (#MHD_HTTP_METHOD_GET, + * #MHD_HTTP_METHOD_PUT, etc.) + * @param upload_size the size of the message upload content payload, + * #MHD_SIZE_UNKNOWN for chunked uploads (if the + * final chunk has not been processed yet) + * @return action how to proceed, NULL + * if the request must be aborted due to a serious + * error while handling the request (implies closure + * of underling data stream, for HTTP/1.1 it means + * socket closure). + */ +const struct MHD_Action * +MHDT_server_reply_check_query ( + void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size); + + +/** + * Checks that the client request includes the given + * custom header. If so, returns #MHD_HTTP_STATUS_NO_CONTENT. + * + * @param cls expected header with "$NAME:$VALUE" format. + * @param request the request object + * @param path the requested uri (without arguments after "?") + * @param method the HTTP method used (#MHD_HTTP_METHOD_GET, + * #MHD_HTTP_METHOD_PUT, etc.) + * @param upload_size the size of the message upload content payload, + * #MHD_SIZE_UNKNOWN for chunked uploads (if the + * final chunk has not been processed yet) + * @return action how to proceed, NULL + * if the request must be aborted due to a serious + * error while handling the request (implies closure + * of underling data stream, for HTTP/1.1 it means + * socket closure). + */ +const struct MHD_Action * +MHDT_server_reply_check_header ( + void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size); + + +/** + * Checks that the client request includes the given + * upload. If so, returns #MHD_HTTP_STATUS_NO_CONTENT. + * + * @param cls expected upload data as a 0-terminated string. + * @param request the request object + * @param path the requested uri (without arguments after "?") + * @param method the HTTP method used (#MHD_HTTP_METHOD_GET, + * #MHD_HTTP_METHOD_PUT, etc.) + * @param upload_size the size of the message upload content payload, + * #MHD_SIZE_UNKNOWN for chunked uploads (if the + * final chunk has not been processed yet) + * @return action how to proceed, NULL + * if the request must be aborted due to a serious + * error while handling the request (implies closure + * of underling data stream, for HTTP/1.1 it means + * socket closure). + */ +const struct MHD_Action * +MHDT_server_reply_check_upload ( + void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size); + + +/** + * Initialize options for an MHD daemon for a test. + * + * @param cls closure + * @param[in,out] d daemon to initialize + * @return error message, NULL on success + */ +typedef const char * +(*MHDT_ServerSetup)(void *cls, + struct MHD_Daemon *d); + + +/** + * Initialize MHD daemon without any special + * options, binding to any free port. + * + * @param cls closure + * @param[in,out] d daemon to initialize + * @return error message, NULL on success + */ +const char * +MHDT_server_setup_minimal (void *cls, + struct MHD_Daemon *d); + + +/** + * Function that runs an MHD daemon until + * a read() against @a finsig succeeds. + * + * @param cls closure + * @param finsig fd to read from to detect termination request + * @param[in,out] d daemon to run + */ +typedef void +(*MHDT_ServerRunner)(void *cls, + int finsig, + struct MHD_Daemon *d); + + +/** + * Function that starts an MHD daemon with the + * simple #MHD_daemon_start() method until + * a read() against @a finsig succeeds. + * + * @param cls closure, pass a NULL-terminated (!) + * array of `struct MHD_DaemonOptionAndValue` with the + * the threading mode to use + * @param finsig fd to read from to detect termination request + * @param[in,out] d daemon to run + */ +void +MHDT_server_run_minimal (void *cls, + int finsig, + struct MHD_Daemon *d); + + +/** + * Function that runs an MHD daemon in blocking mode until + * a read() against @a finsig succeeds. + * + * @param cls closure + * @param finsig fd to read from to detect termination request + * @param[in,out] d daemon to run + */ +void +MHDT_server_run_blocking (void *cls, + int finsig, + struct MHD_Daemon *d); + + +/** + * Run test suite with @a phases for a daemon initialized + * using @a ss_cb on the local machine. + * + * @param ss_cb setup logic for the daemon + * @param ss_cb_cls closure for @a ss_cb + * @param run_cb runs the daemon + * @param run_cb_cls closure for @a run_cb + * @param phases test phases to run in child processes + * @return 0 on success, 77 if test was skipped, + * error code otherwise + */ +int +MHDT_test (MHDT_ServerSetup ss_cb, + void *ss_cb_cls, + MHDT_ServerRunner run_cb, + void *run_cb_cls, + const struct MHDT_Phase *phases); + +#endif diff --git a/src/tests/client_server/libtest_convenience.c b/src/tests/client_server/libtest_convenience.c @@ -0,0 +1,150 @@ +/* + This file is part of GNU libmicrohttpd + Copyright (C) 2024 Christian Grothoff + + 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. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +/** + * @file libtest_convenience.c + * @brief convenience functions for libtest users + * @author Christian Grothoff + */ +#include <pthread.h> +#include <stdbool.h> +#include <fcntl.h> +#include <unistd.h> +#include <errno.h> +#include "microhttpd2.h" +#include "libtest.h" +#include <curl/curl.h> + + +const char * +MHDT_server_setup_minimal (void *cls, + struct MHD_Daemon *d) +{ + const struct MHD_DaemonOptionAndValue *options = cls; + + if (MHD_SC_OK != + MHD_daemon_set_options ( + d, + options, + MHD_OPTIONS_ARRAY_MAX_SIZE)) + return "Failed to configure threading mode!"; + if (MHD_SC_OK != + MHD_DAEMON_SET_OPTIONS ( + d, + MHD_D_OPTION_BIND_PORT (MHD_AF_AUTO, + 0))) + return "Failed to bind to port 0!"; + return NULL; +} + + +void +MHDT_server_run_minimal (void *cls, + int finsig, + struct MHD_Daemon *d) +{ + fd_set r; + char c; + + FD_ZERO (&r); + FD_SET (finsig, &r); + while (1) + { + if ( (-1 == + select (finsig + 1, + &r, + NULL, + NULL, + NULL)) && + (EAGAIN != errno) ) + { + fprintf (stderr, + "Failure waiting on termination signal: %s\n", + strerror (errno)); + break; + } + if (FD_ISSET (finsig, + &r)) + break; + } + if ( (FD_ISSET (finsig, + &r)) && + (1 != read (finsig, + &c, + 1)) ) + { + fprintf (stderr, + "Failed to drain termination signal\n"); + } +} + + +void +MHDT_server_run_blocking (void *cls, + int finsig, + struct MHD_Daemon *d) +{ + fd_set r; + char c; + + FD_ZERO (&r); + FD_SET (finsig, &r); + while (1) + { + struct timeval timeout = { + .tv_usec = 1000 /* 1000 microseconds */ + }; + + if ( (-1 == + select (finsig + 1, + &r, + NULL, + NULL, + &timeout)) && + (EAGAIN != errno) ) + { + fprintf (stderr, + "Failure waiting on termination signal: %s\n", + strerror (errno)); + break; + } +#if FIXME + if (MHD_SC_OK != + MHD_daemon_process_blocking (d, + 1000)) + { + fprintf (stderr, + "Failure running MHD_daemon_process_blocking()\n"); + break; + } +#else + abort (); +#endif + } + if ( (FD_ISSET (finsig, + &r)) && + (1 != read (finsig, + &c, + 1)) ) + { + fprintf (stderr, + "Failed to drain termination signal\n"); + } +} diff --git a/src/tests/client_server/libtest_convenience_client_request.c b/src/tests/client_server/libtest_convenience_client_request.c @@ -0,0 +1,568 @@ +/* + This file is part of GNU libmicrohttpd + Copyright (C) 2024 Christian Grothoff + + 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. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +/** + * @file libtest_convenience.c + * @brief convenience functions implementing clients making requests for libtest users + * @author Christian Grothoff + */ +#include <pthread.h> +#include <stdbool.h> +#include <fcntl.h> +#include <unistd.h> +#include <errno.h> +#include "microhttpd2.h" +#include "libtest.h" +#include <curl/curl.h> + +/** + * Closure for the write_cb(). + */ +struct WriteBuffer +{ + /** + * Where to store the response. + */ + char *buf; + + /** + * Number of bytes in @e buf. + */ + size_t len; + + /** + * Current write offset in @e buf. + */ + size_t pos; + + /** + * Set to non-zero on errors (buffer full). + */ + int err; +}; + + +/** + * Callback for CURLOPT_WRITEFUNCTION processing + * data downloaded from the HTTP server. + * + * @param ptr data uploaded + * @param size size of a member + * @param nmemb number of members + * @param stream must be a `struct WriteBuffer` + * @return bytes processed (size*nmemb) or error + */ +static size_t +write_cb (void *ptr, + size_t size, + size_t nmemb, + void *stream) +{ + struct WriteBuffer *wb = stream; + size_t prod = size * nmemb; + + if ( (prod / size != nmemb) || + (wb->pos + prod < wb->pos) || + (wb->pos + prod > wb->len) ) + { + wb->err = 1; + return CURLE_WRITE_ERROR; + } + memcpy (wb->buf + wb->pos, + ptr, + prod); + wb->pos += prod; + return prod; +} + + +/** + * Declare variables needed to check a download. + * + * @param text text data we expect to receive + */ +#define DECLARE_WB(text) \ + size_t wb_tlen = strlen (text); \ + char wb_buf[wb_tlen]; \ + struct WriteBuffer wb = { \ + .buf = wb_buf, \ + .len = wb_tlen \ + } + + +/** + * Set CURL options to the write_cb() and wb buffer + * to check a download. + * + * @param c CURL handle + */ +#define SETUP_WB(c) do { \ + if (CURLE_OK != \ + curl_easy_setopt (c, \ + CURLOPT_WRITEFUNCTION, \ + &write_cb)) \ + { \ + curl_easy_cleanup (c); \ + return "Failed to set write callback for curl request"; \ + } \ + if (CURLE_OK != \ + curl_easy_setopt (c, \ + CURLOPT_WRITEDATA, \ + &wb)) \ + { \ + curl_easy_cleanup (c); \ + return "Failed to set write buffer for curl request"; \ + } \ +} while (0) + +/** + * Check that we received the expected text. + * + * @param text text we expect to have downloaded + */ +#define CHECK_WB(text) do { \ + if ( (wb_tlen != wb.pos) || \ + (0 != wb.err) || \ + (0 != memcmp (text, \ + wb_buf, \ + wb_tlen)) ) \ + return "Downloaded data does not match expectations"; \ +} while (0) + + +/** + * Perform the curl request @a c and cleanup and + * return an error if the request failed. + * + * @param c request to perform + */ +#define PERFORM_REQUEST(c) do { \ + CURLcode res; \ + res = curl_easy_perform (c); \ + if (CURLE_OK != res) \ + { \ + curl_easy_cleanup (c); \ + return "Failed to fetch URL"; \ + } \ +} while (0) + +/** + * Check that the curl request @a c completed + * with the @a want status code. + * Return an error if the status does not match. + * + * @param c request to check + * @param want desired HTTP status code + */ +#define CHECK_STATUS(c,want) do { \ + if (! check_status (c, want)) \ + { \ + curl_easy_cleanup (c); \ + return "Unexpected HTTP status"; \ + } \ +} while (0) + +/** + * Chec that the HTTP status of @a c matches @a expected_status + * + * @param a completed CURL request + * @param expected_status the expected HTTP response code + * @return true if the status matches + */ +static bool +check_status (CURL *c, + unsigned int expected_status) +{ + long status; + + if (CURLE_OK != + curl_easy_getinfo (c, + CURLINFO_RESPONSE_CODE, + &status)) + { + fprintf (stderr, + "Failed to get HTTP status"); + return false; + } + if (status != expected_status) + { + fprintf (stderr, + "Expected HTTP status %u, got %ld\n", + expected_status, + status); + return false; + } + return true; +} + + +const char * +MHDT_client_get_root ( + void *cls, + const struct MHDT_PhaseContext *pc) +{ + const char *text = cls; + CURL *c; + DECLARE_WB (text); + + c = curl_easy_init (); + if (NULL == c) + return "Failed to initialize Curl handle"; + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_URL, + pc->base_url)) + { + curl_easy_cleanup (c); + return "Failed to set URL for curl request"; + } + SETUP_WB (c); + PERFORM_REQUEST (c); + CHECK_STATUS (c, MHD_HTTP_STATUS_OK); + curl_easy_cleanup (c); + CHECK_WB (text); + return NULL; +} + + +const char * +MHDT_client_get_with_query ( + void *cls, + const struct MHDT_PhaseContext *pc) +{ + const char *args = cls; + size_t alen = strlen (args); + CURL *c; + size_t blen = strlen (pc->base_url); + char u[alen + blen + 1]; + + memcpy (u, + pc->base_url, + blen); + memcpy (u + blen, + args, + alen); + u[alen + blen] = '\0'; + c = curl_easy_init (); + if (NULL == c) + return "Failed to initialize Curl handle"; + + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_URL, + u)) + { + curl_easy_cleanup (c); + return "Failed to set URL for curl request"; + } + PERFORM_REQUEST (c); + CHECK_STATUS (c, + MHD_HTTP_STATUS_NO_CONTENT); + curl_easy_cleanup (c); + return NULL; +} + + +const char * +MHDT_client_set_header (void *cls, + const struct MHDT_PhaseContext *pc) +{ + const char *hdr = cls; + CURL *c; + CURLcode res; + struct curl_slist *slist; + + c = curl_easy_init (); + if (NULL == c) + return "Failed to initialize Curl handle"; + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_URL, + pc->base_url)) + { + curl_easy_cleanup (c); + return "Failed to set URL for curl request"; + } + slist = curl_slist_append (NULL, + hdr); + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_HTTPHEADER, + slist)) + { + curl_easy_cleanup (c); + curl_slist_free_all (slist); + return "Failed to set custom header for curl request"; + } + res = curl_easy_perform (c); + curl_slist_free_all (slist); + if (CURLE_OK != res) + { + curl_easy_cleanup (c); + return "Failed to fetch URL"; + } + CHECK_STATUS (c, + MHD_HTTP_STATUS_NO_CONTENT); + curl_easy_cleanup (c); + return NULL; +} + + +const char * +MHDT_client_expect_header (void *cls, + const struct MHDT_PhaseContext *pc) +{ + const char *hdr = cls; + size_t hlen = strlen (hdr) + 1; + char key[hlen]; + const char *colon = strchr (hdr, ':'); + const char *value; + CURL *c; + bool found = false; + + if (NULL == colon) + return "Invalid expected header passed"; + memcpy (key, + hdr, + hlen); + key[colon - hdr] = '\0'; + value = &key[colon - hdr + 1]; + c = curl_easy_init (); + if (NULL == c) + return "Failed to initialize Curl handle"; + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_URL, + pc->base_url)) + { + curl_easy_cleanup (c); + return "Failed to set URL for curl request"; + } + PERFORM_REQUEST (c); + CHECK_STATUS (c, + MHD_HTTP_STATUS_NO_CONTENT); + for (size_t index = 0; ! found; index++) + { + CURLHcode rval; + struct curl_header *hout; + + rval = curl_easy_header (c, + key, + index, + CURLH_HEADER, + -1 /* last request */, + &hout); + if (CURLHE_BADINDEX == rval) + break; + found = (0 == strcmp (value, + hout->value)); + } + if (! found) + { + curl_easy_cleanup (c); + return "Expected HTTP response header not found"; + } + curl_easy_cleanup (c); + return NULL; +} + + +/** + * Closure for the read_cb(). + */ +struct ReadBuffer +{ + /** + * Origin of data to upload. + */ + const char *buf; + + /** + * Number of bytes in @e buf. + */ + size_t len; + + /** + * Current read offset in @e buf. + */ + size_t pos; + + /** + * Number of chunks to user when sending. + */ + unsigned int chunks; + +}; + + +/** + * Callback for CURLOPT_READFUNCTION for uploading + * data to the HTTP server. + * + * @param ptr data uploaded + * @param size size of a member + * @param nmemb number of members + * @param stream must be a `struct ReadBuffer` + * @return bytes processed (size*nmemb) or error + */ +static size_t +read_cb (void *ptr, + size_t size, + size_t nmemb, + void *stream) +{ + struct ReadBuffer *rb = stream; + size_t limit = size * nmemb; + + if (limit / size != nmemb) + return CURLE_WRITE_ERROR; + if (limit > rb->len - rb->pos) + limit = rb->len - rb->pos; + if ( (rb->chunks > 1) && + (limit > 1) ) + { + limit /= rb->chunks; + rb->chunks--; + } + memcpy (ptr, + rb->buf + rb->pos, + limit); + rb->pos += limit; + return limit; +} + + +const char * +MHDT_client_put_data ( + void *cls, + const struct MHDT_PhaseContext *pc) +{ + const char *text = cls; + struct ReadBuffer rb = { + .buf = text, + .len = strlen (text) + }; + CURL *c; + + c = curl_easy_init (); + if (NULL == c) + return "Failed to initialize Curl handle"; + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_URL, + pc->base_url)) + { + curl_easy_cleanup (c); + return "Failed to set URL for curl request"; + } + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_UPLOAD, + 1L)) + { + curl_easy_cleanup (c); + return "Failed to set PUT method for curl request"; + } + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_READFUNCTION, + &read_cb)) + { + curl_easy_cleanup (c); + return "Failed to set READFUNCTION for curl request"; + } + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_READDATA, + &rb)) + { + curl_easy_cleanup (c); + return "Failed to set READFUNCTION for curl request"; + } + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_INFILESIZE_LARGE, + (curl_off_t) rb.len)) + { + curl_easy_cleanup (c); + return "Failed to set INFILESIZE_LARGE for curl request"; + } + PERFORM_REQUEST (c); + CHECK_STATUS (c, + MHD_HTTP_STATUS_NO_CONTENT); + curl_easy_cleanup (c); + return NULL; +} + + +const char * +MHDT_client_chunk_data ( + void *cls, + const struct MHDT_PhaseContext *pc) +{ + const char *text = cls; + struct ReadBuffer rb = { + .buf = text, + .len = strlen (text), + .chunks = 2 + }; + CURL *c; + + c = curl_easy_init (); + if (NULL == c) + return "Failed to initialize Curl handle"; + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_URL, + pc->base_url)) + { + curl_easy_cleanup (c); + return "Failed to set URL for curl request"; + } + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_UPLOAD, + 1L)) + { + curl_easy_cleanup (c); + return "Failed to set PUT method for curl request"; + } + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_READFUNCTION, + &read_cb)) + { + curl_easy_cleanup (c); + return "Failed to set READFUNCTION for curl request"; + } + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_READDATA, + &rb)) + { + curl_easy_cleanup (c); + return "Failed to set READFUNCTION for curl request"; + } + PERFORM_REQUEST (c); + CHECK_STATUS (c, + MHD_HTTP_STATUS_NO_CONTENT); + curl_easy_cleanup (c); + return NULL; +} diff --git a/src/tests/client_server/libtest_convenience_server_reply.c b/src/tests/client_server/libtest_convenience_server_reply.c @@ -0,0 +1,450 @@ +/* + This file is part of GNU libmicrohttpd + Copyright (C) 2024 Christian Grothoff + + 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. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +/** + * @file libtest_convenience_server_reply.c + * @brief convenience functions that generate + * replies from the server for libtest users + * @author Christian Grothoff + */ +#include <pthread.h> +#include <stdbool.h> +#include <fcntl.h> +#include <stdio.h> +#include <unistd.h> +#include <errno.h> +#include "microhttpd2.h" +#include "libtest.h" +#include <curl/curl.h> + + +const struct MHD_Action * +MHDT_server_reply_text ( + void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size) +{ + const char *text = cls; + + return MHD_action_from_response ( + request, + MHD_response_from_buffer_static (MHD_HTTP_STATUS_OK, + strlen (text), + text)); +} + + +const struct MHD_Action * +MHDT_server_reply_file ( + void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size) +{ + const char *text = cls; + size_t tlen = strlen (text); + char fn[] = "/tmp/mhd-test-XXXXXX"; + int fd; + + fd = mkstemp (fn); + if (-1 == fd) + { + fprintf (stderr, + "Failed to mkstemp() temporary file\n"); + return NULL; + } + if (tlen != write (fd, text, tlen)) + { + fprintf (stderr, + "Failed to write() temporary file in one go: %s\n", + strerror (errno)); + return NULL; + } + fsync (fd); + if (0 != remove (fn)) + { + fprintf (stderr, + "Failed to remove() temporary file %s: %s\n", + fn, + strerror (errno)); + } + return MHD_action_from_response ( + request, + MHD_response_from_fd (MHD_HTTP_STATUS_OK, + fd, + 0 /* offset */, + tlen)); +} + + +const struct MHD_Action * +MHDT_server_reply_with_header ( + void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size) +{ + const char *header = cls; + size_t hlen = strlen (header) + 1; + char name[hlen]; + const char *colon = strchr (header, ':'); + const char *value; + struct MHD_Response *resp; + + memcpy (name, + header, + hlen); + name[colon - header] = '\0'; + value = &name[colon - header + 1]; + + resp = MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT); + if (MHD_SC_OK != + MHD_response_add_header (resp, + name, + value)) + return NULL; + return MHD_action_from_response ( + request, + resp); +} + + +const struct MHD_Action * +MHDT_server_reply_check_query ( + void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size) +{ + const char *equery = cls; + size_t qlen = strlen (equery) + 1; + char qc[qlen]; + + memcpy (qc, + equery, + qlen); + for (const char *tok = strtok (qc, "&"); + NULL != tok; + tok = strtok (NULL, "&")) + { + const char *end; + const struct MHD_StringNullable *sn; + const char *val; + + end = strchr (tok, '='); + if (NULL == end) + { + end = &tok[strlen (tok)]; + val = NULL; + } + else + { + val = end + 1; + } + { + size_t alen = end - tok; + char arg[alen + 1]; + + memcpy (arg, + tok, + alen); + arg[alen] = '\0'; + sn = MHD_request_get_value (request, + MHD_VK_GET_ARGUMENT, + arg); + if (NULL == sn) + { + fprintf (stderr, + "NULL returned for query key %s\n", + arg); + return NULL; + } + if (NULL == val) + { + if (NULL != sn->cstr) + { + fprintf (stderr, + "NULL expected for value for query key %s, got %s\n", + arg, + sn->cstr); + return NULL; + } + } + else + { + if (NULL == sn->cstr) + { + fprintf (stderr, + "%s expected for value for query key %s, got NULL\n", + val, + arg); + return NULL; + } + if (0 != strcmp (val, + sn->cstr)) + { + fprintf (stderr, + "%s expected for value for query key %s, got %s\n", + val, + arg, + sn->cstr); + return NULL; + } + } + } + } + + return MHD_action_from_response ( + request, + MHD_response_from_empty ( + MHD_HTTP_STATUS_NO_CONTENT)); +} + + +const struct MHD_Action * +MHDT_server_reply_check_header ( + void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size) +{ + const char *want = cls; + size_t wlen = strlen (want) + 1; + char key[wlen]; + const char *colon = strchr (want, ':'); + const struct MHD_StringNullable *have; + const char *value; + + memcpy (key, + want, + wlen); + if (NULL != colon) + { + key[colon - want] = '\0'; + value = &key[colon - want + 1]; + } + else + { + value = NULL; + } + have = MHD_request_get_value (request, + MHD_VK_HEADER, + key); + if (NULL == have) + { + fprintf (stderr, + "Missing client header `%s'\n", + want); + return NULL; + } + if (NULL == value) + { + if (NULL != have->cstr) + { + fprintf (stderr, + "Have unexpected client header `%s': `%s'\n", + key, + have->cstr); + return NULL; + } + } + else + { + if (NULL == have->cstr) + { + fprintf (stderr, + "Missing value for client header `%s'\n", + want); + return NULL; + } + 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); + return NULL; + } + } + return MHD_action_from_response ( + request, + MHD_response_from_empty ( + MHD_HTTP_STATUS_NO_CONTENT)); +} + + +/** + * Function to process data uploaded by a client. + * + * @param cls the payload we expect to be uploaded as a 0-terminated string + * @param request the request is being processed + * @param content_data_size the size of the @a content_data, + * zero when all data have been processed + * @param[in] content_data the uploaded content data, + * may be modified in the callback, + * valid only until return from the callback, + * NULL when all data have been processed + * @return action specifying how to proceed: + * #MHD_upload_action_continue() to continue upload (for incremental + * upload processing only), + * #MHD_upload_action_suspend() to stop reading the upload until + * the request is resumed, + * #MHD_upload_action_abort_request() to close the socket, + * or a response to discard the rest of the upload and transmit + * the response + * @ingroup action + */ +static const struct MHD_UploadAction * +check_upload_cb (void *cls, + struct MHD_Request *request, + size_t content_data_size, + void *content_data) +{ + const char *want = cls; + size_t wlen = strlen (want); + + if (content_data_size != wlen) + { + fprintf (stderr, + "Invalid body size given to full upload callback\n"); + return NULL; + } + if (0 != memcmp (want, + content_data, + wlen)) + { + fprintf (stderr, + "Invalid body data given to full upload callback\n"); + return NULL; + } + /* success! */ + return MHD_upload_action_from_response ( + request, + MHD_response_from_empty ( + MHD_HTTP_STATUS_NO_CONTENT)); +} + + +const struct MHD_Action * +MHDT_server_reply_check_upload ( + void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size) +{ + const char *want = cls; + size_t wlen = strlen (want); + + return MHD_action_process_upload_full (request, + wlen, + &check_upload_cb, + (void *) want); +} + + +/** + * Closure for #chunk_return. + */ +struct ChunkContext +{ + /** + * Where we are in the buffer. + */ + const char *pos; +}; + + +/** + * Function that returns a string in chunks. + * + * @param dyn_cont_cls must be a `struct ChunkContext` + * @param ctx the context to produce the action to return, + * the pointer is only valid until the callback returns + * @param pos position in the datastream to access; + * note that if a `struct MHD_Response` object is re-used, + * it is possible for the same content reader to + * be queried multiple times for the same data; + * however, if a `struct MHD_Response` is not re-used, + * libmicrohttpd guarantees that "pos" will be + * the sum of all data sizes provided by this callback + * @param[out] buf where to copy the data + * @param max maximum number of bytes to copy to @a buf (size of @a buf) + * @return action to use, + * NULL in case of any error (the response will be aborted) + */ +static const struct MHD_DynamicContentCreatorAction * +chunk_return (void *cls, + struct MHD_DynamicContentCreatorContext *ctx, + uint_fast64_t pos, + void *buf, + size_t max) +{ + struct ChunkContext *cc = cls; + size_t imax = strlen (cc->pos); + const char *space = strchr (cc->pos, ' '); + + if (0 == imax) + return MHD_DCC_action_finish (ctx); + if (NULL != space) + imax = space - cc->pos + 1; + if (imax > max) + imax = max; + memcpy (buf, + cc->pos, + imax); + cc->pos += imax; + return MHD_DCC_action_continue (ctx, + imax); +} + + +const struct MHD_Action * +MHDT_server_reply_chunked_text ( + void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size) +{ + const char *text = cls; + struct ChunkContext *cc; + + cc = malloc (sizeof (struct ChunkContext)); + if (NULL == cc) + return NULL; + cc->pos = text; + + return MHD_action_from_response ( + request, + MHD_response_from_callback (MHD_HTTP_STATUS_OK, + MHD_SIZE_UNKNOWN, + &chunk_return, + cc, + &free)); +} diff --git a/src/tests/client_server/test_client_server.c b/src/tests/client_server/test_client_server.c @@ -0,0 +1,281 @@ +/* + This file is part of GNU libmicrohttpd + Copyright (C) 2016, 2024 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. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +/** + * @file test_client_server.c + * @brief test with client against server + * @author Christian Grothoff + */ +#include <microhttpd2.h> +#include "mhd_config.h" +#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_USE_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_USE_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_USE_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, + }, +#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 = "Hello world", + .client_cb = &MHDT_client_get_root, + .client_cb_cls = "Hello world", + .timeout_ms = 2500, + }, + { + .label = "GET with sendfile", + .server_cb = &MHDT_server_reply_file, + .server_cb_cls = "Hello world", + .client_cb = &MHDT_client_get_root, + .client_cb_cls = "Hello world", + .timeout_ms = 2500, + }, + { + .label = "client PUT with content-length", + .server_cb = &MHDT_server_reply_check_upload, + .server_cb_cls = "simple-upload-value", + .client_cb = &MHDT_client_put_data, + .client_cb_cls = "simple-upload-value", + .timeout_ms = 2500, + }, + { + .label = "client PUT with 2 chunks", + .server_cb = &MHDT_server_reply_check_upload, + .server_cb_cls = "chunky-upload-value", + .client_cb = &MHDT_client_chunk_data, + .client_cb_cls = "chunky-upload-value", + .timeout_ms = 2500, + }, + { + .label = "client request with custom header", + .server_cb = &MHDT_server_reply_check_header, + .server_cb_cls = "C-Header:testvalue", + .client_cb = &MHDT_client_set_header, + .client_cb_cls = "C-Header:testvalue", + .timeout_ms = 2500, + }, + { + .label = "server response with custom header", + .server_cb = &MHDT_server_reply_with_header, + .server_cb_cls = "X-Header:testvalue", + .client_cb = &MHDT_client_expect_header, + .client_cb_cls = "X-Header:testvalue", + .timeout_ms = 2500, + }, + { + .label = "URL with query parameters 1", + .server_cb = &MHDT_server_reply_check_query, + .server_cb_cls = "a=b&c", + .client_cb = &MHDT_client_get_with_query, + .client_cb_cls = "?a=b&c", + .timeout_ms = 5000, + .num_clients = 4 + }, + { + .label = "URL with query parameters 2", + .server_cb = &MHDT_server_reply_check_query, + .server_cb_cls = "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 + }, + { + .label = "URL with query parameters 3", + .server_cb = &MHDT_server_reply_check_query, + .server_cb_cls = "a=&c", /* a => "", c => NULL */ + .client_cb = &MHDT_client_get_with_query, + .client_cb_cls = "?c&a=", + .timeout_ms = 5000, + .num_clients = 1 + }, + { + .label = "URL with query parameters 4", + .server_cb = &MHDT_server_reply_check_query, + .server_cb_cls = "a=", /* a => "" */ + .client_cb = &MHDT_client_get_with_query, + .client_cb_cls = "?a=", + .timeout_ms = 5000, + .num_clients = 1 + }, + { + .label = "URL with query parameters 5", + .server_cb = &MHDT_server_reply_check_query, + .server_cb_cls = "a=b", /* a => "b" */ + .client_cb = &MHDT_client_get_with_query, + .client_cb_cls = "?a=b", + .timeout_ms = 5000, + .num_clients = 1 + }, + { + .label = "chunked response get", + .server_cb = &MHDT_server_reply_chunked_text, + .server_cb_cls = "Hello world", + .client_cb = &MHDT_client_get_root, + .client_cb_cls = "Hello world", + .timeout_ms = 2500, + }, + // 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; +}