libmicrohttpd2

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

commit 2432d969f8dc7372d2a130ef66ee0f83865394ce
parent 3319b8ab9364242670218517ccdfec6e9ab9a6bf
Author: Evgeny Grin (Karlson2k) <k2k@drgrin.dev>
Date:   Sat, 22 Aug 2026 17:48:25 +0200

Added ACME HTTP-01 token response example

Diffstat:
Msrc/examples2/acme/.gitignore | 1+
Msrc/examples2/acme/Makefile.am | 3++-
Asrc/examples2/acme/acme_http_01_token.c | 756+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 759 insertions(+), 1 deletion(-)

diff --git a/src/examples2/acme/.gitignore b/src/examples2/acme/.gitignore @@ -1,2 +1,3 @@ /acme_http_01_redirect /acme_http_01_files +/acme_http_01_token diff --git a/src/examples2/acme/Makefile.am b/src/examples2/acme/Makefile.am @@ -22,4 +22,5 @@ $(top_builddir)/src/mhd2/libmicrohttpd2.la: $(top_builddir)/src/mhd2/Makefile # example programs noinst_PROGRAMS = \ acme_http_01_redirect \ - acme_http_01_files + acme_http_01_files \ + acme_http_01_token diff --git a/src/examples2/acme/acme_http_01_token.c b/src/examples2/acme/acme_http_01_token.c @@ -0,0 +1,756 @@ +/* SPDX-License-Identifier: 0BSD */ +/* + This file is part of GNU libmicrohttpd. + Copyright (C) 2026 Evgeny Grin (Karlson2k) + + Permission to use, copy, modify, and/or distribute this software for + any purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL + WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE + FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY + DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT + OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +/** + * @file src/examples2/acme/acme_http_01_token.c + * @brief Answering the ACME HTTP-01 challenge given on the command line + * @author Karlson2k (Evgeny Grin) + */ + +/* A minimal HTTP server that answers a single ACME HTTP-01 challenge given + on the command line. Nothing else is served. + + Unlike the file-based example, nothing is read from the disk: the key + authorization is given directly, so the server is fully described by its + arguments. The ACME servers always use port 80. Another port may be + given if the traffic from port 80 is forwarded to it. + + The example can be used as a ready-to-run program, and its code can be + taken into an application as well: the daemon request processing callback + (req_cb()) and the data shared by all the requests + (struct RequestHandlingContextData) do not depend on how the daemon is + created. Everything else is just the stand-alone program around them. + + Any path other than a challenge path is answered with "404 Not Found", + a challenge path requested with a method other than GET or HEAD with + "405 Method Not Allowed". + + Usage: acme_http_01_token [--naive] PORT KEY_AUTHORIZATION + The arguments are told apart by their length, so their order is not + significant. With "--naive" the account thumbprint is given instead of + the key authorization; see the note at print_naive_warning(). */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <microhttpd2.h> + +/** + * The minimal length of the challenge token. + * The token carries at least 128 bits of entropy, which needs at least + * twenty-two characters of the base64url alphabet. + * See RFC 8555, section 8.3. + */ +#define MIN_TOKEN_LEN 22 + +/** + * The maximal length of the port number given on the command line. + * An argument of at least #MIN_TOKEN_LEN characters is the key + * authorization or the thumbprint; the lengths in between are rejected. + */ +#define MAX_PORT_LEN 5 + +/** + * The maximal length of the key authorization built in the "naive" mode, + * without the terminating null character. The other mode sends the string + * given on the command line and needs no buffer at all. + */ +#define MAX_KEY_AUTH_LEN 511 + +/** + * The fixed prefix of the path of the ACME HTTP-01 challenge resources. + */ +static const char chlng_prfx[] = MHD_ACME_HTTP_01_CHALLENGE_PATH_PREFIX; + +/** + * The length of #chlng_prfx, without the terminating null character. + */ +static const size_t chlng_prfx_len = sizeof(chlng_prfx) / sizeof(char) - 1; + +/** + * The data shared by all requests handled by this example. + */ +struct RequestHandlingContextData +{ + /** + * The key authorization to send, NULL in the "naive" mode. + */ + const char *key_auth; + + /** + * The length of #key_auth, without the terminating null character. + */ + size_t key_auth_len; + + /** + * The length of the token part of #key_auth, zero in the "naive" mode. + * The token is the part of the key authorization before the first dot, + * so it is also the prefix of #key_auth. See RFC 8555, section 8.1. + */ + size_t token_len; + + /** + * The account thumbprint, NULL unless the "naive" mode is used. + */ + const char *thumbprint; + + /** + * The length of #thumbprint, without the terminating null character. + */ + size_t thumbprint_len; + + /** + * The ready response with the key authorization, used for every matching + * request. NULL in the "naive" mode. + */ + struct MHD_Response *resp_challenge; + + /** + * The response for every request that is not answered with the key + * authorization: another resource, another token or a too long token. + */ + struct MHD_Response *resp_not_found; + + /** + * The response for the ACME challenge resources requested with + * an unsupported method. + */ + struct MHD_Response *resp_not_allowed; + + /** + * If not zero, every served challenge is reported to standard error. + */ + int log_challenges; +}; + + +/** + * Parse the TCP port number given on the command line. + * + * @param str the string to parse + * @param[out] port_out the resulting port number, set only on success + * @return non-zero if succeed, + * zero if @p str is not a valid port number + */ +static int +parse_port (const char *str, + uint_least16_t *port_out) +{ + unsigned long value; + char *endptr; + + if ('\0' == str[0]) + return 0; + + value = strtoul (str, + &endptr, + 10); + if ('\0' != endptr[0]) + return 0; + /* A negative number is wrapped by strtoul() to a large value and + is rejected by the range check below. */ + if ((1UL > value) || (65535UL < value)) + return 0; + + *port_out = (uint_least16_t)value; + + return 1; +} + + +/** + * Check the challenge token taken from the request. + * + * In the "naive" mode the token is copied from the request to the reply, + * which makes this check mandatory: RFC 8555, section 8.3, requires such + * a client to validate the token syntax. + * + * @param token the token to check + * @param token_len the length of @p token, without the terminating null + * character + * @return non-zero if the token is syntactically valid, + * zero otherwise + */ +static int +check_token (const char *token, + size_t token_len) +{ + size_t i; + + if (MIN_TOKEN_LEN > token_len) + return 0; + + for (i = 0; token_len > i; ++i) + { + const unsigned char chr = (unsigned char)token[i]; + + if ((('A' > chr) || ('Z' < chr)) + && (('a' > chr) || ('z' < chr)) + && (('0' > chr) || ('9' < chr)) + && ('-' != chr) && ('_' != chr)) + return 0; + } + + return 1; +} + + +/** + * Report the "naive" mode to the user. + * + * In this mode the reply is built from the token of the request instead of + * being compared with the configured one, so any syntactically valid token + * is answered. RFC 8555, section 11.3, requires the entropy of the token + * partly to make such servers harder to write. + */ +static void +print_naive_warning (void) +{ + fprintf (stderr, + "WARNING: the \"naive\" mode (also known as \"stateless\")" + " answers every syntactically valid token with the account" + " thumbprint, thus confirming any challenge that is asked for.\n" + "It is not recommended by RFC 8555, section 8.3, " + "and is less secure.\n"); +} + + +/** + * Create a re-usable response with the given body. + * + * @param sc the HTTP status code of the response + * @param body_len the length of @p body, without the terminating null + * character + * @param body the body of the response, must be valid for the whole + * lifetime of the response + * @param content_type the value of the "Content-Type:" header + * @param allow the value of the "Allow:" header, + * NULL if the header is not needed + * @return the new response, + * NULL if failed + */ +static struct MHD_Response * +make_static_response (enum MHD_HTTP_StatusCode sc, + size_t body_len, + const char *body, + const char *content_type, + const char *allow) +{ + struct MHD_ResponseOptionAndValue reusable; + struct MHD_Response *r; + + /* The option is kept in a variable: the address of the value returned by + the option helper cannot be taken in every supported language mode. */ + reusable = MHD_R_OPTION_REUSABLE (MHD_YES); + + r = MHD_response_from_buffer_static (sc, + body_len, + body); + if (NULL != r) + { + if (MHD_SC_OK == + MHD_response_add_header (r, + MHD_HTTP_HEADER_CONTENT_TYPE, + content_type)) + { + if ((NULL == allow) + || (MHD_SC_OK == + MHD_response_add_header (r, + MHD_HTTP_HEADER_ALLOW, + allow))) + { + if (MHD_SC_OK == + MHD_response_set_option (r, + &reusable)) + return r; /* Success exit point */ + } + } + + /* Below is a clean-up path */ + MHD_response_destroy (r); + } + + return NULL; /* Failure exit point */ +} + + +/** + * Report the challenge that is being served. + * + * @param method the HTTP method used for the request, GET or HEAD + * @param token the token of the challenge resource + * @param token_len the length of @p token, without the terminating null + * character + */ +static void +log_challenge (enum MHD_HTTP_Method method, + const char *token, + size_t token_len) +{ + /* No other method reaches this point. */ + fprintf (stderr, + "Replying to the %s request for challenge %.*s\n", + (MHD_HTTP_METHOD_HEAD == method) ? "HEAD" : "GET", + (int)token_len, + token); +} + + +/** + * Check whether the request is for an ACME HTTP-01 challenge resource. + * The token is checked as well, so the token of a matching request is + * syntactically valid. + * + * @param path the requested path + * @return non-zero if the request is for a challenge resource, + * zero otherwise + */ +static int +is_req_acme_http_01_challenge (const struct MHD_String *MHD_RESTRICT path) +{ + return ((chlng_prfx_len < path->len) + && (0 == memcmp (path->cstr, + chlng_prfx, + chlng_prfx_len)) + && (check_token (path->cstr + chlng_prfx_len, + path->len - chlng_prfx_len))); +} + + +/** + * Handle the request for an ACME HTTP-01 challenge resource. + * + * @param request the request to handle + * @param path the requested path, must be a challenge resource path + * @param method the HTTP method used for the request + * @param context_data the data shared by all the requests + * @return the action to perform for the @p request + */ +static const struct MHD_Action * +handle_acme_http_01_challenge ( + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + const struct RequestHandlingContextData *const context_data) +{ + const char *const token = path->cstr + chlng_prfx_len; + const size_t token_len = path->len - chlng_prfx_len; + char key_auth[MAX_KEY_AUTH_LEN + 1]; + struct MHD_Response *r; + + /* Only the GET and the HEAD methods are allowed for the challenge + resources. */ + if ((MHD_HTTP_METHOD_GET != method) + && (MHD_HTTP_METHOD_HEAD != method)) + return MHD_action_from_response (request, + context_data->resp_not_allowed); + + if (NULL != context_data->key_auth) + { + /* The key authorization is known, so only the configured token is + answered. The token is the prefix of the key authorization. */ + if ((token_len != context_data->token_len) + || (0 != memcmp (token, + context_data->key_auth, + token_len))) + return MHD_action_from_response (request, + context_data->resp_not_found); + + if (context_data->log_challenges) + log_challenge (method, + token, + token_len); + + /* There is only one key authorization, so the reply never changes: + the ready response is used and nothing has to be built here. */ + return MHD_action_from_response (request, + context_data->resp_challenge); + } + + /* The "naive" mode: the key authorization is not known in advance, so it + is built for every request from the token of the request and the + configured account thumbprint, joined with a dot. + See RFC 8555, section 8.1. */ + if (MAX_KEY_AUTH_LEN - context_data->thumbprint_len - 1 < token_len) + return MHD_action_from_response (request, + context_data->resp_not_found); + + memcpy (key_auth, + token, + token_len); + key_auth[token_len] = '.'; + memcpy (key_auth + token_len + 1, + context_data->thumbprint, + context_data->thumbprint_len); + + r = MHD_response_from_buffer_copy (MHD_HTTP_STATUS_OK, + token_len + 1 + + context_data->thumbprint_len, + key_auth); + if (NULL != r) + { + if (MHD_SC_OK == + MHD_response_add_header (r, + MHD_HTTP_HEADER_CONTENT_TYPE, + "application/octet-stream")) + { + if (context_data->log_challenges) + log_challenge (method, + token, + token_len); + + return MHD_action_from_response (request, + r); /* Success exit point */ + } + + /* Below is a clean-up path */ + MHD_response_destroy (r); + } + + return MHD_action_abort_request (request); /* Failure exit point */ +} + + +/** + * The handler of the incoming requests. + * + * @param cls the pointer to the shared #RequestHandlingContextData + * @param request the request to handle + * @param path the requested path + * @param method the HTTP method used for the request + * @param upload_size the size of the request content, unused + * @return the action to perform for the @p request + */ +static MHD_FN_PAR_NONNULL_ (2) MHD_FN_PAR_NONNULL_ (3) +const struct MHD_Action * +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) +{ + const struct RequestHandlingContextData *const context_data = + (const struct RequestHandlingContextData *)cls; + + /* If the request is for an ACME HTTP-01 challenge resource, handle it + accordingly. */ + if (is_req_acme_http_01_challenge (path)) + return handle_acme_http_01_challenge (request, + path, + method, + context_data); + + /* Here other requests can be handled for different needs. + This example just sends 404 (Not Found) replies. */ + (void)upload_size; /* Unused */ + + return MHD_action_from_response (request, + context_data->resp_not_found); +} + + +/** + * Create the responses that do not change from request to request. + * + * @param[in,out] context_data the data to set the responses in + * @return non-zero if succeed, + * zero if failed + */ +static int +init_responses (struct RequestHandlingContextData *context_data) +{ + static const char page_not_found[] = + "<html><body>Not found</body></html>"; + static const char page_not_allowed[] = + "<html><body>Method not allowed</body></html>"; + + context_data->resp_challenge = NULL; + if (NULL != context_data->key_auth) + { + /* The key authorization never changes, so the response for it is built + once here instead of being built for every request. */ + context_data->resp_challenge = + make_static_response (MHD_HTTP_STATUS_OK, + context_data->key_auth_len, + context_data->key_auth, + "application/octet-stream", + NULL); + if (NULL == context_data->resp_challenge) + return 0; /* Failure exit point */ + } + + context_data->resp_not_found = + make_static_response (MHD_HTTP_STATUS_NOT_FOUND, + sizeof(page_not_found) / sizeof(char) - 1, + page_not_found, + "text/html", + NULL); + if (NULL != context_data->resp_not_found) + { + /* The challenge resource is read-only. The "Allow:" header is required + for this status code by RFC 9110, section 15.5.6. */ + context_data->resp_not_allowed = + make_static_response (MHD_HTTP_STATUS_METHOD_NOT_ALLOWED, + sizeof(page_not_allowed) / sizeof(char) - 1, + page_not_allowed, + "text/html", + "GET, HEAD"); + if (NULL != context_data->resp_not_allowed) + return 1; /* Success exit point */ + + /* Below is a clean-up path */ + MHD_response_destroy (context_data->resp_not_found); + } + if (NULL != context_data->resp_challenge) + MHD_response_destroy (context_data->resp_challenge); + + return 0; /* Failure exit point */ +} + + +/** + * Destroy the responses created by init_responses(). + * + * @param[in] context_data the data with the responses to destroy + */ +static void +deinit_responses (struct RequestHandlingContextData *context_data) +{ + MHD_response_destroy (context_data->resp_not_allowed); + MHD_response_destroy (context_data->resp_not_found); + if (NULL != context_data->resp_challenge) + MHD_response_destroy (context_data->resp_challenge); +} + + +/** + * Create, configure, start and run the daemon until the user presses ENTER. + * + * @param data the data shared by all requests, must be valid until this + * function returns + * @param port the TCP port to listen on + * @return zero if the daemon has been started and stopped normally, + * the exit code of the program otherwise + */ +static int +run_daemon (struct RequestHandlingContextData *data, + uint_least16_t port) +{ + struct MHD_Daemon *d; + int ret; + + if (!init_responses (data)) + { + fprintf (stderr, + "Failed to create a response object.\n"); + return 4; + } + + d = MHD_daemon_create (&req_cb, + data); + if (NULL == d) + { + fprintf (stderr, + "Failed to create MHD daemon.\n"); + deinit_responses (data); + return 3; + } + + ret = 0; + if (MHD_SC_OK != + MHD_DAEMON_SET_OPTIONS ( + d, + MHD_D_OPTION_WM_WORKER_THREADS (1), + MHD_D_OPTION_BIND_PORT (MHD_AF_AUTO, + port), + /* This daemon is exposed to the open Internet and talks to + well-behaving clients only, so the protocol is enforced strictly. + The nearest stricter level is taken if this exact level is not + available in the library build. */ + MHD_D_OPTION_PROTOCOL_STRICT_LEVEL (MHD_PSL_STRICT, + MHD_USL_THIS_OR_STRICTER), + /* The following three options are an optional optimisation only: + this daemon uses none of these features, so MHD is told not to + spend any resources on them. Everything works without them. */ + MHD_D_OPTION_DISABLE_COOKIES (MHD_YES), + MHD_D_OPTION_DISALLOW_UPGRADE (MHD_YES), + MHD_D_OPTION_DISALLOW_SUSPEND_RESUME (MHD_YES))) + { + fprintf (stderr, + "Failed to set MHD daemon run parameters.\n"); + ret = 3; + } + else if (MHD_SC_OK != + MHD_daemon_start (d)) + { + fprintf (stderr, + "Failed to start MHD daemon.\n"); + ret = 3; + } + else + { + printf ("The MHD daemon is listening on port %u\n" + "Press ENTER to stop.\n", + (unsigned int)port); + (void)fgetc (stdin); + } + printf ("Stopping... "); + fflush (stdout); + MHD_daemon_destroy (d); + printf ("OK\n"); + deinit_responses (data); + + return ret; +} + + +/** + * Parse the command line arguments. + * + * The port number and the key authorization are told apart by their length, + * so the arguments may be given in any order. All the errors are reported + * by this function. + * + * @param argc the number of the command line arguments + * @param argv the command line arguments + * @param[out] port_out the TCP port to listen on, set only on success + * @param[out] auth_out the key authorization, or the account thumbprint if + * the "naive" mode is requested, set only on success + * @param[out] naive_out set to non-zero if the "naive" mode is requested + * @return non-zero if succeed, + * zero if the arguments are not valid + */ +static int +parse_cmd_line (int argc, + char *const *argv, + uint_least16_t *port_out, + const char **auth_out, + int *naive_out) +{ + const char *port_str = NULL; + const char *auth = NULL; + int i; + + *naive_out = 0; + for (i = 1; argc > i; ++i) + { + const size_t arg_len = strlen (argv[i]); + + if ((0 == strcmp (argv[i], + "--naive")) + || (0 == strcmp (argv[i], + "--stateless"))) + *naive_out = !0; + else if ((MAX_PORT_LEN >= arg_len) && (NULL == port_str)) + port_str = argv[i]; + else if ((MIN_TOKEN_LEN <= arg_len) && (NULL == auth)) + auth = argv[i]; + else + { + fprintf (stderr, + "Unexpected argument \"%s\": wrong length or given twice.\n", + argv[i]); + return 0; + } + } + if ((NULL == port_str) || (NULL == auth)) + { + fprintf (stderr, + "Usage:\n%s [--naive] PORT KEY_AUTHORIZATION\n", + argv[0]); + return 0; + } + if (!parse_port (port_str, + port_out)) + { + fprintf (stderr, + "The PORT must be a numeric value between 1 and 65535.\n"); + return 0; + } + + *auth_out = auth; + + return 1; +} + + +/** + * Run the example. + * + * @param argc the number of the command line arguments + * @param argv the command line arguments: the TCP port to listen on, the key + * authorization and the optional "--naive" option + * @return zero if succeed, + * non-zero otherwise + */ +int +main (int argc, + char *const *argv) +{ + struct RequestHandlingContextData data; + const char *auth; + const char *dot; + uint_least16_t port; + int naive; + + if (!parse_cmd_line (argc, + argv, + &port, + &auth, + &naive)) + return 1; + + data.key_auth = NULL; + data.key_auth_len = 0; + data.token_len = 0; + data.thumbprint = NULL; + data.thumbprint_len = 0; + if (naive) + { + print_naive_warning (); + data.thumbprint = auth; + data.thumbprint_len = strlen (auth); + if (MAX_KEY_AUTH_LEN - MIN_TOKEN_LEN - 1 < data.thumbprint_len) + { + fprintf (stderr, + "The thumbprint is too long.\n"); + return 2; + } + } + else + { + /* The token is the part of the key authorization before the first dot. + See RFC 8555, section 8.1. */ + dot = strchr (auth, '.'); + if ((NULL == dot) + || (!check_token (auth, + (size_t)(dot - auth)))) + { + fprintf (stderr, + "The KEY_AUTHORIZATION must be the token, a dot and" + " the account thumbprint.\n"); + return 2; + } + data.key_auth = auth; + data.key_auth_len = strlen (auth); + data.token_len = (size_t)(dot - auth); + } + /* A real application would take this from its configuration. */ + data.log_challenges = !0; + + return run_daemon (&data, + port); +}