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 }