commit a84144f9dfb1480e46725b76022c109d15255301
parent a700c92d1a6666e7a2b948e3487fac4a3d72536b
Author: Christian Grothoff <grothoff@gnunet.org>
Date: Wed, 18 Sep 2024 00:07:31 +0200
complete PP test logic
Diffstat:
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;
+}