libmicrohttpd2

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

acme_http_01_files.c (20634B)


      1 /* SPDX-License-Identifier: 0BSD */
      2 /*
      3   This file is part of GNU libmicrohttpd.
      4   Copyright (C) 2026 Evgeny Grin (Karlson2k)
      5 
      6   Permission to use, copy, modify, and/or distribute this software for
      7   any purpose with or without fee is hereby granted.
      8 
      9   THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
     10   WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
     11   OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
     12   FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
     13   DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
     14   AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
     15   OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     16 */
     17 /**
     18  * @file src/examples2/acme/acme_http_01_files.c
     19  * @brief  Serving of the prepared ACME HTTP-01 challenge files
     20  * @author Karlson2k (Evgeny Grin)
     21  */
     22 
     23 /* A minimal HTTP server that serves prepared ACME HTTP-01 challenge files.
     24    Nothing else is served.
     25 
     26    Typically it works together with an ACME client that has written the
     27    files.  The files are looked up in the ".well-known/acme-challenge/"
     28    subdirectory of the directory given by the "--webroot" option, which
     29    is the current directory by default.
     30    The ACME servers always use port 80.  Another port may be given if
     31    the traffic from port 80 is forwarded to it.
     32 
     33    The example can be used as a ready-to-run program, and its code can be
     34    taken into an application as well: the daemon request processing callback
     35    (req_cb()) and the data shared by all the requests
     36    (struct RequestHandlingContextData) do not depend on how the daemon is
     37    created.  Everything else is just the stand-alone program around them.
     38 
     39    Any path other than a challenge path is answered with "404 Not Found",
     40    a challenge path requested with a method other than GET or HEAD with
     41    "405 Method Not Allowed".
     42 
     43    Usage:  acme_http_01_files [--webroot DIR] PORT
     44    The order of the arguments is not significant.                          */
     45 
     46 #include <stdio.h>
     47 #include <stdlib.h>
     48 #include <string.h>
     49 #include <fcntl.h>
     50 #ifdef _WIN32
     51 #  include <io.h> /* for open() */
     52 #endif
     53 #include <microhttpd2.h>
     54 
     55 #ifndef O_BINARY
     56 /* Only W32 distinguishes the binary and the text modes. */
     57 #  define O_BINARY 0
     58 #endif
     59 
     60 /* Check whether a directory name ends exactly where the next path component
     61    starts, so that no separator has to be inserted after it.  On W32 a name
     62    may also end with the drive letter, like "C:", which makes the resulting
     63    path relative to the current directory on that drive. */
     64 #ifndef _WIN32
     65 #  define IS_PATH_END(chr) ('/' == (chr))
     66 #else
     67 #  define IS_PATH_END(chr) \
     68         (('/' == (chr)) || ('\\' == (chr)) || (':' == (chr)))
     69 #endif
     70 
     71 /**
     72  * The maximum length of the path of a challenge file, without
     73  * the terminating null character.
     74  */
     75 #define MAX_PATH_LEN 1023
     76 
     77 /**
     78  * The minimal length of the challenge token.
     79  * The token carries at least 128 bits of entropy, which needs at least
     80  * twenty-two characters of the base64url alphabet.
     81  * See RFC 8555, section 8.3.
     82  */
     83 #define MIN_TOKEN_LEN 22
     84 
     85 /**
     86  * The fixed prefix of the path of the ACME HTTP-01 challenge resources.
     87  */
     88 static const char chlng_prfx[] = MHD_ACME_HTTP_01_CHALLENGE_PATH_PREFIX;
     89 
     90 /**
     91  * The length of #chlng_prfx, without the terminating null character.
     92  */
     93 static const size_t chlng_prfx_len = sizeof(chlng_prfx) / sizeof(char) - 1;
     94 
     95 /**
     96  * The data shared by all requests handled by this example.
     97  */
     98 struct RequestHandlingContextData
     99 {
    100   /**
    101    * The name of the directory with the challenge files, ending with
    102    * the directory separator.
    103    */
    104   const char *challenge_dir;
    105 
    106   /**
    107    * The length of #challenge_dir, without the terminating null character.
    108    */
    109   size_t challenge_dir_len;
    110 
    111   /**
    112    * If not zero, every served challenge is reported to standard error.
    113    */
    114   int log_challenges;
    115 
    116   /**
    117    * The response for every request that is not answered with a challenge
    118    * file: another resource, a missing file or a too long path.
    119    */
    120   struct MHD_Response *resp_not_found;
    121 
    122   /**
    123    * The response for the ACME challenge resources requested with
    124    * an unsupported method.
    125    */
    126   struct MHD_Response *resp_not_allowed;
    127 };
    128 
    129 
    130 /**
    131  * Parse the TCP port number given on the command line.
    132  *
    133  * @param str the string to parse
    134  * @param[out] port_out the resulting port number, set only on success
    135  * @return non-zero if succeed,
    136  *         zero if @p str is not a valid port number
    137  */
    138 static int
    139 parse_port (const char *str,
    140             uint_least16_t *port_out)
    141 {
    142   unsigned long value;
    143   char *endptr;
    144 
    145   if ('\0' == str[0])
    146     return 0;
    147 
    148   value = strtoul (str,
    149                    &endptr,
    150                    10);
    151   if ('\0' != endptr[0])
    152     return 0;
    153   /* A negative number is wrapped by strtoul() to a large value and
    154      is rejected by the range check below. */
    155   if ((1UL > value) || (65535UL < value))
    156     return 0;
    157 
    158   *port_out = (uint_least16_t)value;
    159 
    160   return 1;
    161 }
    162 
    163 
    164 /**
    165  * Check the challenge token taken from the request.
    166  *
    167  * The token is used as a file name, so this check is not optional: it makes
    168  * any directory traversal impossible, as neither '.' nor '/' belongs to the
    169  * base64url alphabet.  See RFC 8555, section 8.3.
    170  *
    171  * @param token the token to check
    172  * @param token_len the length of @p token, without the terminating null
    173  *                  character
    174  * @return non-zero if the token can be used as a file name,
    175  *         zero otherwise
    176  */
    177 static int
    178 check_token (const char *token,
    179              size_t token_len)
    180 {
    181   size_t i;
    182 
    183   if (MIN_TOKEN_LEN > token_len)
    184     return 0;
    185 
    186   for (i = 0; token_len > i; ++i)
    187   {
    188     const unsigned char chr = (unsigned char)token[i];
    189 
    190     if ((('A' > chr) || ('Z' < chr))
    191         && (('a' > chr) || ('z' < chr))
    192         && (('0' > chr) || ('9' < chr))
    193         && ('-' != chr) && ('_' != chr))
    194       return 0;
    195   }
    196 
    197   return 1;
    198 }
    199 
    200 
    201 /**
    202  * Create a re-usable response with a short HTML body.
    203  *
    204  * @param sc the HTTP status code of the response
    205  * @param page_len the length of @p page, without the terminating null
    206  *                 character
    207  * @param page the body of the response, must be valid for the whole
    208  *             lifetime of the response
    209  * @param allow the value of the "Allow:" header,
    210  *              NULL if the header is not needed
    211  * @return the new response,
    212  *         NULL if failed
    213  */
    214 static struct MHD_Response *
    215 make_error_response (enum MHD_HTTP_StatusCode sc,
    216                      size_t page_len,
    217                      const char *page,
    218                      const char *allow)
    219 {
    220   struct MHD_ResponseOptionAndValue reusable;
    221   struct MHD_Response *r;
    222 
    223   /* The option is kept in a variable: the address of the value returned by
    224      the option helper cannot be taken in every supported language mode. */
    225   reusable = MHD_R_OPTION_REUSABLE (MHD_YES);
    226 
    227   r = MHD_response_from_buffer_static (sc,
    228                                        page_len,
    229                                        page);
    230   if (NULL != r)
    231   {
    232     if (MHD_SC_OK ==
    233         MHD_response_add_header (r,
    234                                  MHD_HTTP_HEADER_CONTENT_TYPE,
    235                                  "text/html"))
    236     {
    237       if ((NULL == allow)
    238           || (MHD_SC_OK ==
    239               MHD_response_add_header (r,
    240                                        MHD_HTTP_HEADER_ALLOW,
    241                                        allow)))
    242       {
    243         if (MHD_SC_OK ==
    244             MHD_response_set_option (r,
    245                                      &reusable))
    246           return r; /* Success exit point */
    247       }
    248     }
    249 
    250     /* Below is a clean-up path */
    251     MHD_response_destroy (r);
    252   }
    253 
    254   return NULL; /* Failure exit point */
    255 }
    256 
    257 
    258 /**
    259  * Check whether the request is for an ACME HTTP-01 challenge resource.
    260  * The token is checked as well, so the token of a matching request can be
    261  * used as a file name.
    262  *
    263  * @param path the requested path
    264  * @return non-zero if the request is for a challenge resource,
    265  *         zero otherwise
    266  */
    267 static int
    268 is_req_acme_http_01_challenge (const struct MHD_String *MHD_RESTRICT path)
    269 {
    270   return ((chlng_prfx_len < path->len)
    271           && (0 == memcmp (path->cstr,
    272                            chlng_prfx,
    273                            chlng_prfx_len))
    274           && (check_token (path->cstr + chlng_prfx_len,
    275                            path->len - chlng_prfx_len)));
    276 }
    277 
    278 
    279 /**
    280  * Report the challenge that is being served.
    281  *
    282  * @param method the HTTP method used for the request, GET or HEAD
    283  * @param token the token of the challenge resource
    284  * @param token_len the length of @p token, without the terminating null
    285  *                  character
    286  */
    287 static void
    288 log_challenge (enum MHD_HTTP_Method method,
    289                const char *token,
    290                size_t token_len)
    291 {
    292   /* No other method reaches this point. */
    293   fprintf (stderr,
    294            "Replying to the %s request for challenge %.*s\n",
    295            (MHD_HTTP_METHOD_HEAD == method) ? "HEAD" : "GET",
    296            (int)token_len,
    297            token);
    298 }
    299 
    300 
    301 /**
    302  * Handle the request for an ACME HTTP-01 challenge resource.
    303  *
    304  * @param request the request to handle
    305  * @param path the requested path, must be a challenge resource path
    306  * @param method the HTTP method used for the request
    307  * @param context_data the data shared by all the requests
    308  * @return the action to perform for the @p request
    309  */
    310 static const struct MHD_Action *
    311 handle_acme_http_01_challenge (
    312   struct MHD_Request *MHD_RESTRICT request,
    313   const struct MHD_String *MHD_RESTRICT path,
    314   enum MHD_HTTP_Method method,
    315   const struct RequestHandlingContextData *const context_data)
    316 {
    317   const size_t token_len = path->len - chlng_prfx_len;
    318   char file_name[MAX_PATH_LEN + 1];
    319   int fd;
    320   struct MHD_Response *r;
    321 
    322   /* Only the GET and the HEAD methods are allowed for the challenge
    323      resources. */
    324   if ((MHD_HTTP_METHOD_GET != method)
    325       && (MHD_HTTP_METHOD_HEAD != method))
    326     return MHD_action_from_response (request,
    327                                      context_data->resp_not_allowed);
    328 
    329   /* The name of the file is the challenge directory with the token of
    330      the requested resource appended. */
    331   if (MAX_PATH_LEN - context_data->challenge_dir_len < token_len)
    332     return MHD_action_from_response (request,
    333                                      context_data->resp_not_found);
    334 
    335   memcpy (file_name,
    336           context_data->challenge_dir,
    337           context_data->challenge_dir_len);
    338   memcpy (file_name + context_data->challenge_dir_len,
    339           path->cstr + chlng_prfx_len,
    340           token_len);
    341   file_name[context_data->challenge_dir_len + token_len] = '\0';
    342 
    343   fd = open (file_name,
    344              O_RDONLY | O_BINARY);
    345   if (0 > fd)
    346     return MHD_action_from_response (request,
    347                                      context_data->resp_not_found);
    348 
    349   /* The file is sent whole and unchanged: RFC 8555, section 8.3, requires
    350      the value of the resource to be the ASCII representation of the key
    351      authorization.  The size is not given, so the file is sent to its end.
    352      The descriptor is closed by MHD in any case. */
    353   r = MHD_response_from_fd (MHD_HTTP_STATUS_OK,
    354                             fd,
    355                             0,
    356                             MHD_SIZE_UNKNOWN);
    357   if (NULL != r)
    358   {
    359     if (MHD_SC_OK ==
    360         MHD_response_add_header (r,
    361                                  MHD_HTTP_HEADER_CONTENT_TYPE,
    362                                  "application/octet-stream"))
    363     {
    364       if (context_data->log_challenges)
    365         log_challenge (method,
    366                        path->cstr + chlng_prfx_len,
    367                        token_len);
    368 
    369       return MHD_action_from_response (request,
    370                                        r); /* Success exit point */
    371     }
    372 
    373     /* Below is a clean-up path */
    374     MHD_response_destroy (r);
    375   }
    376   return MHD_action_abort_request (request); /* Failure exit point */
    377 }
    378 
    379 
    380 /**
    381  * The handler of the incoming requests.
    382  *
    383  * @param cls the pointer to the shared #RequestHandlingContextData
    384  * @param request the request to handle
    385  * @param path the requested path
    386  * @param method the HTTP method used for the request
    387  * @param upload_size the size of the request content, unused
    388  * @return the action to perform for the @p request
    389  */
    390 static MHD_FN_PAR_NONNULL_ (2) MHD_FN_PAR_NONNULL_ (3)
    391 const struct MHD_Action *
    392 req_cb (void *cls,
    393         struct MHD_Request *MHD_RESTRICT request,
    394         const struct MHD_String *MHD_RESTRICT path,
    395         enum MHD_HTTP_Method method,
    396         uint_fast64_t upload_size)
    397 {
    398   const struct RequestHandlingContextData *const context_data =
    399     (const struct RequestHandlingContextData *)cls;
    400 
    401   /* If the request is for an ACME HTTP-01 challenge resource, handle it
    402      accordingly. */
    403   if (is_req_acme_http_01_challenge (path))
    404     return handle_acme_http_01_challenge (request,
    405                                           path,
    406                                           method,
    407                                           context_data);
    408 
    409   /* Here other requests can be handled for different needs.
    410      This example just sends 404 (Not Found) replies. */
    411   (void)upload_size;  /* Unused */
    412 
    413   return MHD_action_from_response (request,
    414                                    context_data->resp_not_found);
    415 }
    416 
    417 
    418 /**
    419  * Create the responses used for every request that is not a successfully
    420  * served challenge file.
    421  *
    422  * @param[out] context_data the data to set the responses in
    423  * @return non-zero if succeed,
    424  *         zero if failed
    425  */
    426 static int
    427 init_responses (struct RequestHandlingContextData *context_data)
    428 {
    429   static const char page_not_found[] =
    430     "<html><body>Not found</body></html>";
    431   static const char page_not_allowed[] =
    432     "<html><body>Method not allowed</body></html>";
    433 
    434   context_data->resp_not_found =
    435     make_error_response (MHD_HTTP_STATUS_NOT_FOUND,
    436                          sizeof(page_not_found) / sizeof(char) - 1,
    437                          page_not_found,
    438                          NULL);
    439   if (NULL != context_data->resp_not_found)
    440   {
    441     /* The challenge resource is read-only.  The "Allow:" header is required
    442        for this status code by RFC 9110, section 15.5.6. */
    443     context_data->resp_not_allowed =
    444       make_error_response (MHD_HTTP_STATUS_METHOD_NOT_ALLOWED,
    445                            sizeof(page_not_allowed) / sizeof(char) - 1,
    446                            page_not_allowed,
    447                            "GET, HEAD");
    448     if (NULL != context_data->resp_not_allowed)
    449       return 1; /* Success exit point */
    450 
    451     /* Below is a clean-up path */
    452     MHD_response_destroy (context_data->resp_not_found);
    453   }
    454 
    455   return 0; /* Failure exit point */
    456 }
    457 
    458 
    459 /**
    460  * Destroy the responses created by init_responses().
    461  *
    462  * @param[in] context_data the data with the responses to destroy
    463  */
    464 static void
    465 deinit_responses (struct RequestHandlingContextData *context_data)
    466 {
    467   MHD_response_destroy (context_data->resp_not_allowed);
    468   MHD_response_destroy (context_data->resp_not_found);
    469 }
    470 
    471 
    472 /**
    473  * Create, configure, start and run the daemon until the user presses ENTER.
    474  *
    475  * @param data the data shared by all requests, must be valid until this
    476  *             function returns
    477  * @param port the TCP port to listen on
    478  * @return zero if the daemon has been started and stopped normally,
    479  *         the exit code of the program otherwise
    480  */
    481 static int
    482 run_daemon (struct RequestHandlingContextData *data,
    483             uint_least16_t port)
    484 {
    485   struct MHD_Daemon *d;
    486   int ret;
    487 
    488   if (!init_responses (data))
    489   {
    490     fprintf (stderr,
    491              "Failed to create a response object.\n");
    492     return 4;
    493   }
    494 
    495   d = MHD_daemon_create (&req_cb,
    496                          data);
    497   if (NULL == d)
    498   {
    499     fprintf (stderr,
    500              "Failed to create MHD daemon.\n");
    501     deinit_responses (data);
    502     return 3;
    503   }
    504 
    505   ret = 0;
    506   if (MHD_SC_OK !=
    507       MHD_DAEMON_SET_OPTIONS (
    508         d,
    509         MHD_D_OPTION_WM_WORKER_THREADS (1),
    510         MHD_D_OPTION_BIND_PORT (MHD_AF_AUTO,
    511                                 port),
    512         /* This daemon is exposed to the open Internet and talks to
    513            well-behaving clients only, so the protocol is enforced strictly.
    514            The nearest stricter level is taken if this exact level is not
    515            available in the library build. */
    516         MHD_D_OPTION_PROTOCOL_STRICT_LEVEL (MHD_PSL_STRICT,
    517                                             MHD_USL_THIS_OR_STRICTER),
    518         /* The following three options are an optional optimisation only:
    519            this daemon uses none of these features, so MHD is told not to
    520            spend any resources on them.  Everything works without them. */
    521         MHD_D_OPTION_DISABLE_COOKIES (MHD_YES),
    522         MHD_D_OPTION_DISALLOW_UPGRADE (MHD_YES),
    523         MHD_D_OPTION_DISALLOW_SUSPEND_RESUME (MHD_YES)))
    524   {
    525     fprintf (stderr,
    526              "Failed to set MHD daemon run parameters.\n");
    527     ret = 3;
    528   }
    529   else if (MHD_SC_OK !=
    530            MHD_daemon_start (d))
    531   {
    532     fprintf (stderr,
    533              "Failed to start MHD daemon.\n");
    534     ret = 3;
    535   }
    536   else
    537   {
    538     printf ("The MHD daemon is listening on port %u and serves\n"
    539             "the challenge files from %s\n"
    540             "Press ENTER to stop.\n",
    541             (unsigned int)port,
    542             data->challenge_dir);
    543     (void)fgetc (stdin);
    544   }
    545   printf ("Stopping... ");
    546   fflush (stdout);
    547   MHD_daemon_destroy (d);
    548   printf ("OK\n");
    549   deinit_responses (data);
    550 
    551   return ret;
    552 }
    553 
    554 
    555 /**
    556  * Parse the command line arguments.
    557  *
    558  * The arguments may be given in any order.  All the errors are reported
    559  * by this function.
    560  *
    561  * @param argc the number of the command line arguments
    562  * @param argv the command line arguments
    563  * @param[out] port_out the TCP port to listen on, set only on success
    564  * @param[out] webroot_out the name of the webroot directory, set only
    565  *                         on success
    566  * @return non-zero if succeed,
    567  *         zero if the arguments are not valid
    568  */
    569 static int
    570 parse_cmd_line (int argc,
    571                 char *const *argv,
    572                 uint_least16_t *port_out,
    573                 const char **webroot_out)
    574 {
    575   const char *webroot = "./";
    576   const char *port_str = NULL;
    577   int i;
    578 
    579   for (i = 1; argc > i; ++i)
    580   {
    581     if (0 == strcmp (argv[i],
    582                      "--webroot"))
    583     {
    584       ++i;
    585       if (argc <= i)
    586       {
    587         fprintf (stderr,
    588                  "The \"--webroot\" option requires the directory name.\n");
    589         return 0;
    590       }
    591       webroot = argv[i];
    592     }
    593     else if (NULL == port_str)
    594       port_str = argv[i];
    595     else
    596     {
    597       fprintf (stderr,
    598                "Usage:\n%s [--webroot DIR] PORT\n",
    599                argv[0]);
    600       return 0;
    601     }
    602   }
    603   if (NULL == port_str)
    604   {
    605     fprintf (stderr,
    606              "Usage:\n%s [--webroot DIR] PORT\n",
    607              argv[0]);
    608     return 0;
    609   }
    610   if (!parse_port (port_str,
    611                    port_out))
    612   {
    613     fprintf (stderr,
    614              "The PORT must be a numeric value between 1 and 65535.\n");
    615     return 0;
    616   }
    617   if ('\0' == webroot[0])
    618   {
    619     fprintf (stderr,
    620              "The directory name must not be empty.\n");
    621     return 0;
    622   }
    623 
    624   *webroot_out = webroot;
    625 
    626   return 1;
    627 }
    628 
    629 
    630 /**
    631  * Run the example.
    632  *
    633  * @param argc the number of the command line arguments
    634  * @param argv the command line arguments: the TCP port to listen on and
    635  *             the optional "--webroot" option with the directory name
    636  * @return zero if succeed,
    637  *         non-zero otherwise
    638  */
    639 int
    640 main (int argc,
    641       char *const *argv)
    642 {
    643   struct RequestHandlingContextData data;
    644   char challenge_dir[MAX_PATH_LEN + 1];
    645   const char *webroot;
    646   size_t webroot_len;
    647   size_t skip;
    648   uint_least16_t port;
    649 
    650   if (!parse_cmd_line (argc,
    651                        argv,
    652                        &port,
    653                        &webroot))
    654     return 1;
    655 
    656   webroot_len = strlen (webroot);
    657   /* The prefix starts with the separator, which is not needed if the given
    658      directory name ends where the next path component starts. */
    659   skip = IS_PATH_END (webroot[webroot_len - 1]) ? 1 : 0;
    660   if (MAX_PATH_LEN - (chlng_prfx_len - skip) < webroot_len)
    661   {
    662     fprintf (stderr,
    663              "The directory name is too long.\n");
    664     return 2;
    665   }
    666   memcpy (challenge_dir,
    667           webroot,
    668           webroot_len);
    669   /* The terminating null character is copied together with the prefix. */
    670   memcpy (challenge_dir + webroot_len,
    671           chlng_prfx + skip,
    672           chlng_prfx_len - skip + 1);
    673   data.challenge_dir = challenge_dir;
    674   data.challenge_dir_len = webroot_len + chlng_prfx_len - skip;
    675   /* A real application would take this from its configuration. */
    676   data.log_challenges = !0;
    677 
    678   return run_daemon (&data,
    679                      port);
    680 }