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:
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 */
+}