libmicrohttpd2

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

commit a84144f9dfb1480e46725b76022c109d15255301
parent a700c92d1a6666e7a2b948e3487fac4a3d72536b
Author: Christian Grothoff <grothoff@gnunet.org>
Date:   Wed, 18 Sep 2024 00:07:31 +0200

complete PP test logic

Diffstat:
Msrc/tests/client_server/Makefile.am | 9++++++++-
Msrc/tests/client_server/libtest.h | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/tests/client_server/libtest_convenience_client_request.c | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/tests/client_server/libtest_convenience_server_reply.c | 256+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/tests/client_server/test_postprocessor.c | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 683 insertions(+), 1 deletion(-)

diff --git a/src/tests/client_server/Makefile.am b/src/tests/client_server/Makefile.am @@ -25,7 +25,8 @@ $(top_builddir)/src/mhd2/libmicrohttpd2.la: $(top_builddir)/src/mhd2/Makefile $(am__cd) $(top_builddir)/src/mhd2 && $(MAKE) $(AM_MAKEFLAGS) libmicrohttpd2.la check_PROGRAMS = \ - test_client_server + test_client_server \ + test_postprocessor TESTS = $(check_PROGRAMS) @@ -47,3 +48,9 @@ test_client_server_SOURCES = \ test_client_server.c test_client_server_LDADD = \ libmhdt.la + + +test_postprocessor_SOURCES = \ + test_postprocessor.c +test_postprocessor_LDADD = \ + libmhdt.la diff --git a/src/tests/client_server/libtest.h b/src/tests/client_server/libtest.h @@ -149,6 +149,118 @@ MHDT_client_chunk_data (void *cls, /** + * Information about a result we expect from the PP. + */ +struct MHDT_PostWant +{ + /** + * key for the result + */ + const char *key; + + /** + * Value for the result. + */ + const char *value; + + /** + * Filename attribute for the result, NULL for none. + */ + const char *filename; + + /** + * Content type attribute for the result, NULL for none. + */ + const char *content_type; + + /** + * Number of bytes in @a value, 0 if value is 0-terminated. + */ + size_t value_size; + + /** + * Internal book-keeping for @e incremental processing. + */ + size_t value_off; + + /** + * True if @e value may be transmitted incrementally. + */ + bool incremental; + + /** + * Set to true if a matching record was returned. + */ + bool satisfied; + +}; + + +/** + * Arguments and state for the #MHDT_server_reply_check_post and + * #MHDT_client_do_post() functions. + */ +struct MHDT_PostInstructions +{ + /** + * Encoding to use when decoding. + */ + enum MHD_HTTP_PostEncoding enc; + + /** + * Data to be POSTed to the server. + */ + const char *postdata; + + /** + * HTTP header to set POST content encoding, use + * NULL if you want to set @e request_hdr directly. + */ + const char *postheader; + + /** + * NULL-terminated array of expected POST data for + * the server. + */ + struct MHDT_PostWant *wants; + + /** + * Number of bytes in @e postdata, use 0 for + * 0-terminated @e postdata. + */ + size_t postdata_size; + + /** + * size to use for the buffer. + */ + size_t buffer_size; + + /** + * Size above which we switch to stream processing. + */ + size_t auto_stream_size; +}; + + +/** + * Perform POST request suitable for testing the post processor and expect a + * 204 No Content response. + * + * Note that @a cls cannot be used by multiple commands + * simultaneously, so do not use this in concurrent + * tests aliasing @a cls. + * + * @param cls information what to post of type `struct MHDT_PostInstructions` + * @param pc context for the client + * @return error message, NULL on success + */ +const char * +MHDT_client_do_post ( + void *cls, + const struct MHDT_PhaseContext *pc); + + +/** * A phase defines some server and client-side * behaviors to execute. */ @@ -391,6 +503,37 @@ MHDT_server_reply_check_upload ( /** + * Checks that the client request against the expected + * POST data. If so, returns #MHD_HTTP_STATUS_NO_CONTENT. + * + * Note that @a cls cannot be used by multiple commands + * simultaneously, so do not use this in concurrent + * tests aliasing @a cls. + * + * @param cls a `struct MHD_PostInstructions` + * @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_post ( + 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 diff --git a/src/tests/client_server/libtest_convenience_client_request.c b/src/tests/client_server/libtest_convenience_client_request.c @@ -584,3 +584,95 @@ MHDT_client_chunk_data ( curl_easy_cleanup (c); return NULL; } + + +const char * +MHDT_client_do_post ( + void *cls, + const struct MHDT_PhaseContext *pc) +{ + struct MHDT_PostInstructions *pi = cls; + CURL *c; + struct curl_slist *request_hdr = NULL; + + /* reset wants in case we re-use the array */ + if (NULL != pi->wants) + { + for (unsigned int i = 0; NULL != pi->wants[i].key; i++) + { + pi->wants[i].value_off = 0; + pi->wants[i].satisfied = false; + } + } + 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_POST, + 1L)) + { + curl_easy_cleanup (c); + return "Failed to set POST method for curl request"; + } + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_POSTFIELDS, + pi->postdata)) + { + curl_easy_cleanup (c); + return "Failed to set POSTFIELDS for curl request"; + } + if (0 != pi->postdata_size) + { + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_POSTFIELDSIZE_LARGE, + (curl_off_t) pi->postdata_size)) + { + curl_easy_cleanup (c); + return "Failed to set POSTFIELDS for curl request"; + } + } + if (NULL != pi->postheader) + { + request_hdr = curl_slist_append (request_hdr, + pi->postheader); + } + if (CURLE_OK != + curl_easy_setopt (c, + CURLOPT_HTTPHEADER, + request_hdr)) + { + curl_easy_cleanup (c); + curl_slist_free_all (request_hdr); + return "Failed to set HTTPHEADER for curl request"; + } + PERFORM_REQUEST (c); + CHECK_STATUS (c, + MHD_HTTP_STATUS_NO_CONTENT); + curl_easy_cleanup (c); + curl_slist_free_all (request_hdr); + if (NULL != pi->wants) + { + for (unsigned int i = 0; NULL != pi->wants[i].key; i++) + { + if (! pi->wants[i].satisfied) + { + fprintf (stderr, + "Server did not correctly detect key '%s'\n", + pi->wants[i].key); + return "key-value data not matched by server"; + } + } + } + return NULL; +} diff --git a/src/tests/client_server/libtest_convenience_server_reply.c b/src/tests/client_server/libtest_convenience_server_reply.c @@ -467,3 +467,259 @@ MHDT_server_reply_chunked_text ( cc, &free)); } + + +/** + * Compare two strings, succeed if both are NULL. + * + * @param wants string we want + * @param have string we have + * @return true if what we @a want is what we @a have + */ +static bool +nstrcmp (const char *wants, + const struct MHD_StringNullable *have) +{ + if ( (NULL == wants) && + (NULL == have->cstr) && + (0 == have->len) ) + return true; + if ( (NULL == wants) || + (NULL == have->cstr) ) + return false; + return (0 == strcmp (wants, + have->cstr)); +} + + +/** + * "Stream" reader for POST data. + * This callback is called to incrementally process parsed POST data sent by + * the client. + * + * @param req the request + * @param cls user-specified closure + * @param name the name of the POST field + * @param filename the name of the uploaded file, @a cstr member is NULL if not + * known / not provided + * @param content_type the mime-type of the data, cstr member is NULL if not + * known / not provided + * @param encoding the encoding of the data, cstr member is NULL if not known / + * not provided + * @param size the number of bytes in @a data available, may be zero if + * the @a final_data is #MHD_YES + * @param data the pointer to @a size bytes of data at the specified + * @a off offset, NOT zero-terminated + * @param off the offset of @a data in the overall value, always equal to + * the sum of sizes of previous calls for the same field / file; + * client may provide more than one field with the same name and + * the same filename, the new filed (or file) is indicated by zero + * value of @a off (and the end is indicated by @a final_data) + * @param final_data if set to #MHD_YES then full field data is provided, + * if set to #MHD_NO then more field data may be provided + * @return action specifying how to proceed: + * #MHD_upload_action_continue() if all is well, + * #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 * +post_stream_reader (struct MHD_Request *req, + void *cls, + const struct MHD_String *name, + const struct MHD_StringNullable *filename, + const struct MHD_StringNullable *content_type, + const struct MHD_StringNullable *encoding, + size_t size, + const void *data, + uint_fast64_t off, + enum MHD_Bool final_data) +{ + struct MHDT_PostInstructions *pi = cls; + struct MHDT_PostWant *wants = pi->wants; + + (void) encoding; // TODO: add check + + if (NULL != wants) + { + for (unsigned int i = 0; NULL != wants[i].key; i++) + { + struct MHDT_PostWant *want = &wants[i]; + + if (want->satisfied) + continue; + if (0 != strcmp (want->key, + name->cstr)) + continue; + if (! nstrcmp (want->filename, + filename)) + continue; + if (! nstrcmp (want->content_type, + content_type)) + continue; + if (! want->incremental) + continue; + if (want->value_off != off) + continue; + if (want->value_size < off + size) + continue; + if (0 != memcmp (data, + want->value + off, + size)) + continue; + want->value_off += size; + want->satisfied = (want->value_size == want->value_off) && final_data; + } + } + + return MHD_upload_action_continue (req); +} + + +/** + * Iterator over name-value pairs. This iterator can be used to + * iterate over all of the cookies, headers, or POST-data fields of a + * request, and also to iterate over the headers that have been added + * to a response. + * + * The pointers to the strings in @a nvt are valid until the response + * is queued. If the data is needed beyond this point, it should be copied. + * + * @param cls closure + * @param nvt the name, the value and the kind of the element + * @return #MHD_YES to continue iterating, + * #MHD_NO to abort the iteration + * @ingroup request + */ +static enum MHD_Bool +check_complete_post_value ( + void *cls, + enum MHD_ValueKind kind, + const struct MHD_NameAndValue *nv) +{ + struct MHDT_PostInstructions *pi = cls; + struct MHDT_PostWant *wants = pi->wants; + + if (NULL == wants) + return MHD_NO; + if (MHD_VK_POSTDATA != kind) + return MHD_NO; + for (unsigned int i = 0; NULL != wants[i].key; i++) + { + struct MHDT_PostWant *want = &wants[i]; + + if (want->satisfied) + continue; + if (want->incremental) + continue; + if (0 != strcmp (want->key, + nv->name.cstr)) + continue; + if (NULL == want->value) + { + if (NULL == nv->value.cstr) + want->satisfied = true; + } + else if (NULL == nv->value.cstr) + continue; + else if (0 == want->value_size) + { + if (0 == strcmp (nv->value.cstr, + want->value)) + want->satisfied = true; + } + else + { + if ((want->value_size == nv->value.len) && + (0 == memcmp (nv->value.cstr, + want->value, + want->value_size))) + want->satisfied = true; + } + } + return MHD_YES; +} + + +/** + * The callback to be called when finished with processing + * of the postprocessor upload data. + * @param req the request + * @param cls the closure + * @param parsing_result the result of POST data parsing + * @return the action to proceed + */ +static const struct MHD_UploadAction * +post_stream_done (struct MHD_Request *req, + void *cls, + enum MHD_PostParseResult parsing_result) +{ + struct MHDT_PostInstructions *pi = cls; + struct MHDT_PostWant *wants = pi->wants; + + if (MHD_POST_PARSE_RES_OK != parsing_result) + { + fprintf (stderr, + "POST parsing was not successful. The result: %d\n", + (int) parsing_result); + return MHD_upload_action_abort_request (req); + } + + MHD_request_get_values_cb (req, + MHD_VK_POSTDATA, + &check_complete_post_value, + pi); + if (NULL != wants) + { + for (unsigned int i = 0; NULL != wants[i].key; i++) + { + struct MHDT_PostWant *want = &wants[i]; + + if (want->satisfied) + continue; + fprintf (stderr, + "Expected key-value pair `%s' missing\n", + want->key); + return MHD_upload_action_abort_request (req); + } + } + return MHD_upload_action_from_response ( + req, + MHD_response_from_empty ( + MHD_HTTP_STATUS_NO_CONTENT)); +} + + +const struct MHD_Action * +MHDT_server_reply_check_post ( + 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 MHDT_PostInstructions *pi = cls; + + (void) path; /* Unused */ + (void) upload_size; // TODO: add check + + if (MHD_HTTP_METHOD_POST != method) + { + fprintf (stderr, + "Reported HTTP method other then POST. Reported method: %u\n", + (unsigned) method); + return MHD_action_abort_request (req); + } + + return MHD_action_parse_post (request, + pi->buffer_size, + pi->auto_stream_size, + pi->enc, + &post_stream_reader, + pi, + &post_stream_done, + pi); +} diff --git a/src/tests/client_server/test_postprocessor.c b/src/tests/client_server/test_postprocessor.c @@ -0,0 +1,184 @@ +/* + 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_postprocessor.c + * @brief test with client against server + * @author Christian Grothoff + */ +#include "libtest.h" + + +int +main (int argc, char *argv[]) +{ + struct MHD_DaemonOptionAndValue thread1auto[] = { + MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_AUTO), + MHD_D_OPTION_WM_WORKER_THREADS (1), + 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[] = { + { + .label = "auto-selected mode, single threaded", + .server_setup = &MHDT_server_setup_minimal, + .server_setup_cls = thread1auto, + .server_runner = &MHDT_server_run_minimal, + }, + { + .label = "END" + } + }; +#define MHDT_SOME_BIN_DATA "\x1\x2\x3\x4\x5" + struct MHDT_PostWant simple_wants[] = { + { + .key = "V1", + .value = "One" + }, + { + .key = "V2", + .value = "Two" + }, + { + .key = NULL + } + }; + struct MHDT_PostWant mpart_wants[] = { + { + .key = "username", + .value = "Bob" + }, + { + .key = "password", + .value = "Passwo3d" + }, + { + .key = "file", + .filename = "image.jpg", + .content_type = "image/jpeg", + .value = MHDT_SOME_BIN_DATA, + .value_size = sizeof(MHDT_SOME_BIN_DATA) / sizeof(char) - 1 + }, + { + .key = NULL + } + }; + struct MHDT_PostInstructions simple_pi = { + .enc = MHD_HTTP_POST_ENCODING_FORM_URLENCODED, + .postdata = "V1=One&V2=Two", + .postheader = MHD_HTTP_HEADER_CONTENT_TYPE + ": application/x-www-form-urlencoded", + .buffer_size = 32, + .auto_stream_size = 16, + .wants = simple_wants + }; + struct MHDT_PostInstructions simple_mp = { + .enc = MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA, + .postdata = "--XXXX\r\n" + "Content-Disposition: form-data; name=\"username\"\r\n" + "\r\n" + "Bob\r\n" + "--XXXX\r\n" + "Content-Disposition: form-data; name=\"password\"\r\n" + "\r\n" + "Passwo3d\r\n" + "--XXXX\r\n" + "Content-Disposition: form-data; name=\"file\"; filename=\"image.jpg\"\r\n" + "Content-Type: image/jpeg\r\n" + "\r\n" + MHDT_SOME_BIN_DATA "\r\n" + "--XXXX--\r\n", + .postheader = MHD_HTTP_HEADER_CONTENT_TYPE + ": multipart/form-data; boundary=XXXX", + .buffer_size = 512, + .auto_stream_size = 128, + .wants = mpart_wants + }; + struct MHDT_PostInstructions simple_tp = { + .enc = MHD_HTTP_POST_ENCODING_TEXT_PLAIN, + .postdata = "V1=One\r\nV2=Two\r\n", + .postheader = MHD_HTTP_HEADER_CONTENT_TYPE ": text/plain", + .buffer_size = 32, + .auto_stream_size = 16, + .wants = simple_wants + }; + struct MHDT_Phase phases[] = { + { + .label = "simple post", + .server_cb = &MHDT_server_reply_check_post, + .server_cb_cls = &simple_pi, + .client_cb = &MHDT_client_do_post, + .client_cb_cls = &simple_pi, + .timeout_ms = 2500, + }, + { + .label = "multipart post", + .server_cb = &MHDT_server_reply_check_post, + .server_cb_cls = &simple_mp, + .client_cb = &MHDT_client_do_post, + .client_cb_cls = &simple_mp, + .timeout_ms = 2500, + }, + { + .label = "plain text post", + .server_cb = &MHDT_server_reply_check_post, + .server_cb_cls = &simple_tp, + .client_cb = &MHDT_client_do_post, + .client_cb_cls = &simple_tp, + .timeout_ms = 2500, + }, + { + .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; +}