libmicrohttpd2

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

commit 3eb3023eb5b4e796da8a797e465f55154ab3c4c5
parent e995c1ce5b5c64d967a328d46151c7ab2b7ad7eb
Author: Evgeny Grin (Karlson2k) <k2k@drgrin.dev>
Date:   Thu, 20 Aug 2026 18:11:14 +0200

Added ACME HTTP-01 redirect example

Diffstat:
Mconfigure.ac | 1+
Msrc/examples2/Makefile.am | 4++++
Asrc/examples2/acme/.gitignore | 1+
Asrc/examples2/acme/Makefile.am | 24++++++++++++++++++++++++
Asrc/examples2/acme/acme_http_01_redirect.c | 484+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 514 insertions(+), 0 deletions(-)

diff --git a/configure.ac b/configure.ac @@ -9905,6 +9905,7 @@ src/tests/upgrade/Makefile src/tests/raw/Makefile src/tests/client_server/Makefile src/examples2/Makefile +src/examples2/acme/Makefile ]) AC_OUTPUT diff --git a/src/examples2/Makefile.am b/src/examples2/Makefile.am @@ -1,6 +1,10 @@ # This Makefile.am is in the public domain SUBDIRS = . +if MHD_SUPPORT_ACME + SUBDIRS += acme +endif + AM_CPPFLAGS = \ -I$(top_srcdir)/src/include \ $(CPPFLAGS_ac) diff --git a/src/examples2/acme/.gitignore b/src/examples2/acme/.gitignore @@ -0,0 +1 @@ +/acme_http_01_redirect diff --git a/src/examples2/acme/Makefile.am b/src/examples2/acme/Makefile.am @@ -0,0 +1,24 @@ +# This Makefile.am is in the public domain + +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/include \ + $(CPPFLAGS_ac) + +AM_CFLAGS = $(CFLAGS_ac) + +AM_LDFLAGS = $(LDFLAGS_ac) + +LDADD = $(top_builddir)/src/mhd2/libmicrohttpd2.la + +if USE_COVERAGE + AM_CFLAGS += --coverage +endif + +$(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 + + +# example programs +noinst_PROGRAMS = \ + acme_http_01_redirect diff --git a/src/examples2/acme/acme_http_01_redirect.c b/src/examples2/acme/acme_http_01_redirect.c @@ -0,0 +1,484 @@ +/* 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_redirect.c + * @brief Redirection of the ACME HTTP-01 challenges to the HTTPS server + * @author Karlson2k (Evgeny Grin) + */ + +/* A minimal HTTP server that redirects requests to another server. + Only the paths suitable for the ACME HTTP-01 challenge are redirected, + that is, the paths starting with "/.well-known/acme-challenge/". + + Typically it works together with an HTTPS server: this example takes the + plain HTTP port (usually port 80) and returns a redirect to the same path + on the HTTPS server. This is handy when the HTTP traffic arrives at one + server, while ACME HTTP-01 challenge responses are provided by the HTTPS + server. + + 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 RedirectData) + do not depend on how the daemon is created, so a single application may + run this daemon and its own HTTPS daemon at the same time. Everything + else is just the stand-alone program around them. + + The HTTPS server does not have to run on the same host. It is addressed + solely by the TARGET_HOST[:TARGET_PORT] argument, so it may be another + machine just as well as the same machine on another port. + + 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_redirect PORT TARGET_HOST[:TARGET_PORT] */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <microhttpd2.h> + +/** + * The scheme and the delimiter starting the redirection target. + * The string is split in two parts as the formatter mangles a macro body + * ending with the two slashes. + */ +#define REDIRECT_SCHEME "https:/" "/" + +/** + * The maximum length of the "Location:" value, without the terminating + * null character. + */ +#define MAX_LOCATION_LEN 511 + +/** + * The data shared by all requests handled by this example. + */ +struct RedirectData +{ + /** + * The target host ("host" or "host:port") of the HTTPS server, + * as given on the command line. + */ + const char *target_host; + + /** + * The length of #target_host, without the terminating null character. + */ + size_t target_host_len; + + /** + * The response for any resource except the ACME challenge resources. + */ + struct MHD_Response *resp_not_found; + + /** + * The response for the ACME challenge resources requested with + * an unsupported method. + */ + struct MHD_Response *resp_not_allowed; +}; + + +/** + * 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; +} + + +/** + * Perform a basic sanity check of the target host ("host" or "host:port") + * string given on the command line. + * + * The string is given by whoever starts this program, so it is trusted. The + * check is a safeguard against an obvious typo, not a parser and not a filter + * of hostile input: it rejects the characters that would break the + * "Location:" header or make the URI invalid, it does not implement the + * "authority" grammar of RFC 3986, section 3.2, and a string that passes it + * may still be a meaningless host name. + * + * @param target_host the string to check + * @param target_host_len the length of @p target_host, without + * the terminating null character + * @return non-zero if the string passed the check, + * zero otherwise + */ +static int +check_target_host (const char *target_host, + size_t target_host_len) +{ + static const char rejected_chars[] = "/?#\\@\"<>^`{|}"; + size_t i; + + if (0 == target_host_len) + return 0; + + for (i = 0; target_host_len > i; ++i) + { + const unsigned char chr = (unsigned char)target_host[i]; + + /* Only the printable US-ASCII characters can be used. */ + if (('!' > chr) || ('~' < chr)) + return 0; + /* Reject URI delimiters and characters unsuitable for "host[:port]". */ + if (NULL != memchr (rejected_chars, + chr, + sizeof(rejected_chars) / sizeof(char) - 1)) + return 0; + } + + return 1; +} + + +/** + * Create a re-usable response with a short HTML body. + * + * @param sc the HTTP status code of the response + * @param page_len the length of @p page, without the terminating null + * character + * @param page the body of the response, must be valid for the whole + * lifetime of the response + * @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_error_response (enum MHD_HTTP_StatusCode sc, + size_t page_len, + const char *page, + 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, + page_len, + page); + if (NULL != r) + { + if (MHD_SC_OK == + MHD_response_add_header (r, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/html")) + { + 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 */ +} + + +/** + * The handler of the incoming requests. + * + * @param cls the pointer to the shared #RedirectData + * @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) +{ + static const size_t prefix_len = + sizeof(MHD_ACME_HTTP_01_CHALLENGE_PATH_PREFIX) / sizeof(char) - 1; + static const size_t scheme_len = + sizeof(REDIRECT_SCHEME) / sizeof(char) - 1; + const struct RedirectData *const data = (const struct RedirectData *)cls; + char location[MAX_LOCATION_LEN + 1]; + size_t pos; + struct MHD_Response *r; + + /* Any request content is discarded by MHD as soon as this callback + provides a response. */ + (void)upload_size; /* Unused */ + + /* The path must start with the challenge prefix and must have at least one + more character, the challenge token. See RFC 8555, section 8.3. */ + if ((prefix_len >= path->len) + || (0 != memcmp (path->cstr, + MHD_ACME_HTTP_01_CHALLENGE_PATH_PREFIX, + prefix_len))) + return MHD_action_from_response (request, + 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. */ + if ((MHD_HTTP_METHOD_GET != method) + && (MHD_HTTP_METHOD_HEAD != method)) + return MHD_action_from_response (request, + data->resp_not_allowed); + + /* A challenge token is short; anything too long to be redirected is not + a challenge resource. */ + if ((MAX_LOCATION_LEN - scheme_len < data->target_host_len) + || (MAX_LOCATION_LEN - scheme_len - data->target_host_len < path->len)) + return MHD_action_from_response (request, + data->resp_not_found); + + /* Build the redirection target: the scheme, the target host given on + the command line and the requested path, unchanged. */ + pos = 0; + memcpy (location + pos, + REDIRECT_SCHEME, + scheme_len); + pos += scheme_len; + memcpy (location + pos, + data->target_host, + data->target_host_len); + pos += data->target_host_len; + memcpy (location + pos, + path->cstr, + path->len); + pos += path->len; + location[pos] = '\0'; + + /* The redirection is temporary and, unlike the "permanent" status codes, + is not cached without the explicit cache directives (RFC 9110, + section 15.1): the next challenge uses a different token. */ + r = MHD_response_from_empty (MHD_HTTP_STATUS_TEMPORARY_REDIRECT); + if (NULL != r) + { + if (MHD_SC_OK == + MHD_response_add_header (r, + MHD_HTTP_HEADER_LOCATION, + location)) + 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 */ +} + + +/** + * 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 RedirectData *data, + uint_least16_t port) +{ + struct MHD_Daemon *d; + int ret; + + d = MHD_daemon_create (&req_cb, + data); + if (NULL == d) + { + fprintf (stderr, + "Failed to create MHD daemon.\n"); + 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 and redirects\n" + "the ACME HTTP-01 challenges to " REDIRECT_SCHEME "%s\n" + "Press ENTER to stop.\n", + (unsigned int)port, + data->target_host); + (void)fgetc (stdin); + } + printf ("Stopping... "); + fflush (stdout); + MHD_daemon_destroy (d); + printf ("OK\n"); + + return ret; +} + + +/** + * Run the example. + * + * @param argc the number of the command line arguments + * @param argv the command line arguments: the TCP port to listen on and + * the target host of the HTTPS server + * @return zero if succeed, + * non-zero otherwise + */ +int +main (int argc, + char *const *argv) +{ + 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>"; + static const size_t min_target_len = + sizeof(REDIRECT_SCHEME) / sizeof(char) - 1 + + sizeof(MHD_ACME_HTTP_01_CHALLENGE_PATH_PREFIX) / sizeof(char) - 1 + + 1; + struct RedirectData data; + uint_least16_t port; + int ret; + + if (3 != argc) + { + fprintf (stderr, + "Usage:\n%s PORT TARGET_HOST[:TARGET_PORT]\n", + argv[0]); + return 1; + } + if (!parse_port (argv[1], + &port)) + { + fprintf (stderr, + "The PORT must be a numeric value between 1 and 65535.\n"); + return 2; + } + + data.target_host = argv[2]; + data.target_host_len = strlen (argv[2]); + /* Leave room for the shortest possible redirection target. */ + if ((MAX_LOCATION_LEN - min_target_len < data.target_host_len) + || (!check_target_host (data.target_host, + data.target_host_len))) + { + fprintf (stderr, + "The TARGET_HOST[:TARGET_PORT] must be a non-empty string of\n" + "printable US-ASCII characters suitable for a URI host.\n"); + return 2; + } + + data.resp_not_found = + make_error_response (MHD_HTTP_STATUS_NOT_FOUND, + sizeof(page_not_found) / sizeof(char) - 1, + page_not_found, + NULL); + if (NULL != data.resp_not_found) + { + data.resp_not_allowed = + make_error_response (MHD_HTTP_STATUS_METHOD_NOT_ALLOWED, + sizeof(page_not_allowed) / sizeof(char) - 1, + page_not_allowed, + "GET, HEAD"); + if (NULL != data.resp_not_allowed) + { + + /* The daemon is started here */ + ret = run_daemon (&data, + port); + + MHD_response_destroy (data.resp_not_allowed); + MHD_response_destroy (data.resp_not_found); + + return ret; /* Success exit point */ + } + + /* Below is a clean-up path */ + MHD_response_destroy (data.resp_not_found); + } + fprintf (stderr, + "Failed to create a response object.\n"); + + return 4; /* Failure exit point */ +}