paivana-httpd.c (36002B)
1 /* 2 This file is part of GNU Taler 3 Copyright (C) 2012-2014 GNUnet e.V. 4 Copyright (C) 2018, 2025, 2026 Taler Systems SA 5 6 GNU Taler is free software; you can redistribute it and/or 7 modify it under the terms of the GNU Affero General Public License 8 as published by the Free Software Foundation; either version 9 3, or (at your option) any later version. 10 11 GNU Taler is distributed in the hope that it will be useful, but 12 WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public 17 License along with GNU Taler; see the file COPYING. If not, 18 write to the Free Software Foundation, Inc., 51 Franklin 19 Street, Fifth Floor, Boston, MA 02110-1301, USA. 20 */ 21 22 /** 23 * @author Martin Schanzenbach 24 * @author Christian Grothoff 25 * @author Marcello Stanisci 26 * @file src/backend/paivana-httpd.c 27 * @brief HTTP proxy that acts as a GNU Taler paywall 28 */ 29 #include "platform.h" 30 #include <curl/curl.h> 31 #include <gnunet/gnunet_util_lib.h> 32 #include <gnunet/gnunet_curl_lib.h> 33 #include <taler/taler_mhd_lib.h> 34 #include <taler/taler_templating_lib.h> 35 #include <taler/merchant/common.h> 36 #include "paivana-httpd.h" 37 #include "paivana-httpd_cookie.h" 38 #include "paivana-httpd_daemon.h" 39 #include "paivana-httpd_helper.h" 40 #include "paivana-httpd_pay.h" 41 #include "paivana-httpd_reverse.h" 42 #include "paivana-httpd_templates.h" 43 #include "paivana_pd.h" 44 45 46 char *PH_target_server_base_url; 47 48 char *PH_target_server_unixpath; 49 50 char *PH_merchant_base_url; 51 52 char *PH_base_url; 53 54 struct GNUNET_CURL_Context *PH_merchant_ctx; 55 56 struct GNUNET_CURL_Context *PH_proxy_ctx; 57 58 int PH_no_check; 59 60 int PH_respect_forwarded_headers; 61 62 struct GNUNET_STRINGS_IPv4NetworkPolicy *PH_trusted_proxies4; 63 64 struct GNUNET_STRINGS_IPv6NetworkPolicy *PH_trusted_proxies6; 65 66 bool PH_have_trusted_proxies; 67 68 /** 69 * Compiled-in default for #PH_request_buffer_max. Named because 70 * `run()` has to be able to tell it apart from a value the operator 71 * wrote, which is what decides whether #PH_max_request_size inherits 72 * it. 73 */ 74 #define PH_DEFAULT_REQUEST_BUFFER_MAX (256 * 1024) 75 76 unsigned long long PH_request_buffer_max = PH_DEFAULT_REQUEST_BUFFER_MAX; 77 78 unsigned long long PH_response_buffer_max = 256 * 1024; 79 80 unsigned long long PH_max_request_size = 1024 * 1024; 81 82 struct GNUNET_TIME_Relative PH_upstream_timeout; 83 84 struct GNUNET_TIME_Relative PH_upstream_stall_timeout; 85 86 unsigned int PH_connection_limit = 512; 87 88 unsigned int PH_per_ip_connection_limit = 32; 89 90 int PH_global_ret; 91 92 int PH_global_cookie; 93 94 regex_t PH_whitelist_ex; 95 96 bool PH_have_whitelist_ex; 97 98 /** 99 * Our configuration. 100 */ 101 const struct GNUNET_CONFIGURATION_Handle *PH_cfg; 102 103 104 /** 105 * Closure for #GNUNET_CURL_gnunet_scheduler_reschedule() of 106 * #PH_merchant_ctx. 107 */ 108 static struct GNUNET_CURL_RescheduleContext *merchant_ctx_rc; 109 110 /** 111 * Closure for #GNUNET_CURL_gnunet_scheduler_reschedule() of 112 * #PH_proxy_ctx. 113 */ 114 static struct GNUNET_CURL_RescheduleContext *proxy_ctx_rc; 115 116 117 /* *************** General / main code *************** */ 118 119 120 /** 121 * Load one of the `TRUSTED_PROXIES` options. 122 * 123 * The GNUnet policy parsers are lenient in ways that matter here, so 124 * a non-NULL return is not on its own evidence that anything was 125 * understood: 126 * 127 * - the list is terminated by an all-zero entry, so a network of 128 * 0.0.0.0/0 or ::/0 *is* the terminator and silently truncates 129 * everything after it. "Trust everyone" is therefore inexpressible 130 * — and would be a strange thing to write anyway; 131 * - the v4 parser accepts a value whose last entry is not terminated 132 * by ';' -- the operator wrote the separators but not the 133 * terminator -- and drops that entry without a word, along with 134 * anything else after the final ';'. (Its v6 counterpart refuses 135 * such a value outright, so this one is the v4 side's alone.) 136 * 137 * A genuinely malformed entry, including a v6 network handed to the 138 * v4 parser, does come back as NULL rather than as a shorter list; it 139 * is the only one of these that announces itself. 140 * 141 * The first comes out as "parsed, but nothing usable", which we reject 142 * along with NULL: quietly trusting nobody would send every visitor to 143 * the socket address, and the operator would have no hint why. The 144 * second is worse, because it succeeds: a single missing ';' would 145 * leave the clients behind an unlisted proxy sharing that proxy's 146 * address as their identity, i.e. sharing one paid cookie. So the 147 * entries that came back are counted against the ';' that went in, and 148 * the value has to end in one. 149 * 150 * Not checked for here: an entry may also carry a port policy 151 * ("10.0.0.0/8:80;"), which parses into a perfectly usable entry whose 152 * port range nothing in paivana ever consults -- it reads as a 153 * restriction on the trust and is not one. 154 * 155 * @param c configuration to read from 156 * @param option name of the option 157 * @param[out] count set to the number of usable entries 158 * @return false if the option is present but unusable 159 */ 160 static bool 161 load_trusted_proxies (const struct GNUNET_CONFIGURATION_Handle *c, 162 const char *option, 163 unsigned int *count) 164 { 165 char *opt; 166 bool v6 = (0 != strcmp (option, 167 "TRUSTED_PROXIES")); 168 unsigned int want = 0; 169 size_t len; 170 171 *count = 0; 172 if (GNUNET_OK != 173 GNUNET_CONFIGURATION_get_value_string (c, 174 "paivana", 175 option, 176 &opt)) 177 return true; /* not configured at all: fine */ 178 len = strlen (opt); 179 while ( (len > 0) && 180 ( (' ' == opt[len - 1]) || 181 ('\t' == opt[len - 1]) ) ) 182 opt[--len] = '\0'; 183 for (const char *p = strchr (opt, ';'); NULL != p; p = strchr (p + 1, ';')) 184 want++; 185 if (v6) 186 { 187 PH_trusted_proxies6 = GNUNET_STRINGS_parse_ipv6_policy (opt); 188 if (NULL != PH_trusted_proxies6) 189 while (! GNUNET_is_zero (&PH_trusted_proxies6[*count].network)) 190 (*count)++; 191 } 192 else 193 { 194 PH_trusted_proxies4 = GNUNET_STRINGS_parse_ipv4_policy (opt); 195 if (NULL != PH_trusted_proxies4) 196 while (0 != PH_trusted_proxies4[*count].network.s_addr) 197 (*count)++; 198 } 199 if ( (0 == *count) || 200 (*count != want) || 201 (0 == len) || 202 (';' != opt[len - 1]) ) 203 { 204 if ( (0 != *count) && 205 (*count != want) ) 206 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 207 "Only %u of the %u entries of `%s' were understood;" 208 " refusing to trust a prefix of the list\n", 209 *count, 210 want, 211 option); 212 else if (0 != *count) 213 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 214 "`%s' does not end in the ';' that terminates the last" 215 " entry; anything after the final one is dropped\n", 216 option); 217 GNUNET_log_config_invalid ( 218 GNUNET_ERROR_TYPE_ERROR, 219 "paivana", 220 option, 221 v6 222 ? "not a usable IPv6 network list; entries are separated *and*" 223 " terminated by ';' and must not contain spaces, e.g." 224 " \"2001:db8::/32;fe80::/10;\" (note that ::/0 is indistinguishable" 225 " from the end of the list and cannot be used)" 226 : "not a usable IPv4 network list; entries are separated *and*" 227 " terminated by ';', e.g. \"10.0.0.0/8;192.168.0.0/16;\"" 228 " (note that 0.0.0.0/0 is indistinguishable from the end of the" 229 " list and cannot be used)"); 230 GNUNET_free (opt); 231 return false; 232 } 233 GNUNET_free (opt); 234 PH_have_trusted_proxies = true; 235 return true; 236 } 237 238 239 /** 240 * Task run on shutdown 241 * 242 * @param cls closure 243 */ 244 static void 245 do_shutdown (void *cls) 246 { 247 (void) cls; 248 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 249 "Shutting down...\n"); 250 TALER_MHD_daemons_halt (); 251 PAIVANA_HTTPD_payment_shutdown (); 252 PAIVANA_HTTPD_reverse_shutdown (); 253 TALER_MHD_daemons_destroy (); 254 PAIVANA_HTTPD_unload_templates (); 255 TALER_TEMPLATING_done (); 256 GNUNET_free (PH_target_server_base_url); 257 GNUNET_free (PH_target_server_unixpath); 258 GNUNET_free (PH_trusted_proxies4); 259 GNUNET_free (PH_trusted_proxies6); 260 GNUNET_free (PH_merchant_base_url); 261 GNUNET_free (PH_base_url); 262 if (PH_have_whitelist_ex) 263 { 264 regfree (&PH_whitelist_ex); 265 PH_have_whitelist_ex = false; 266 } 267 if (NULL != PH_merchant_ctx) 268 { 269 GNUNET_CURL_fini (PH_merchant_ctx); 270 PH_merchant_ctx = NULL; 271 } 272 if (NULL != merchant_ctx_rc) 273 { 274 GNUNET_CURL_gnunet_rc_destroy (merchant_ctx_rc); 275 merchant_ctx_rc = NULL; 276 } 277 if (NULL != PH_proxy_ctx) 278 { 279 GNUNET_CURL_fini (PH_proxy_ctx); 280 PH_proxy_ctx = NULL; 281 } 282 if (NULL != proxy_ctx_rc) 283 { 284 GNUNET_CURL_gnunet_rc_destroy (proxy_ctx_rc); 285 proxy_ctx_rc = NULL; 286 } 287 } 288 289 290 /** 291 * Remove trailing slashes from the web URL @a url, in place. 292 * 293 * Our configuration syntax prefers base URLs to be written with a 294 * trailing '/', while everything we append to one -- a request path, 295 * a "/.well-known/..." endpoint -- brings a leading '/' of its own. 296 * Dropping them here is what keeps the concatenation from yielding 297 * "//", which would otherwise reach the upstream verbatim and, for 298 * BASE_URL, end up in the string the access cookie is keyed on and in 299 * the URL the templates' regular expressions are matched against. 300 * 301 * Never strips below "scheme://h", so that a URL consisting of 302 * nothing but a scheme and a host keeps its host. 303 * 304 * Refuses a URL carrying a query or a fragment instead of mangling it: 305 * the trailing '/' would then be part of the query ("http://h/?p=a/") 306 * and stripping it changes what the query says. Neither has a sensible 307 * reading in a base URL we concatenate a request path onto anyway -- 308 * "http://h/?p=a" + "/x" is not a request for /x -- so the operator is 309 * better told than quietly corrected. 310 * 311 * @param[in,out] url URL to canonicalize; must have passed 312 * TALER_is_web_url() 313 * @return false if @a url carries a query or a fragment, in which case 314 * it is left untouched 315 */ 316 static bool 317 strip_trailing_slashes (char *url) 318 { 319 size_t len = strlen (url); 320 const char *sep; 321 size_t min_len; 322 323 if ( (NULL != strchr (url, 324 '?')) || 325 (NULL != strchr (url, 326 '#')) ) 327 return false; 328 sep = strstr (url, 329 "://"); 330 GNUNET_assert (NULL != sep); /* was a web URL after all! */ 331 min_len = (size_t) (sep - url) + strlen ("://") + 1; 332 while ( (len > min_len) && 333 ('/' == url[len - 1]) ) 334 url[--len] = '\0'; 335 return true; 336 } 337 338 339 /** 340 * Main function that will be run. Main tasks are (1) init. the 341 * curl infrastructure (curl_global_init() / curl_multi_init()), 342 * then fetch the HTTP port where its Web service should listen at, 343 * and finally start MHD on that port. 344 * 345 * @param cls closure 346 * @param args remaining command-line arguments 347 * @param cfgfile name of the configuration file used (for saving, can be NULL!) 348 * @param c configuration 349 */ 350 static void 351 run (void *cls, 352 char *const *args, 353 const char *cfgfile, 354 const struct GNUNET_CONFIGURATION_Handle *c) 355 { 356 char *secret; 357 bool buffer_max_explicit; 358 359 (void) cls; 360 (void) args; 361 (void) cfgfile; 362 PH_cfg = c; 363 PH_upstream_timeout 364 = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 365 60); 366 PH_upstream_stall_timeout 367 = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 368 60); 369 GNUNET_SCHEDULER_add_shutdown (&do_shutdown, 370 NULL); 371 if ( (0 == PH_request_buffer_max) || 372 (PH_request_buffer_max > GNUNET_MAX_MALLOC_CHECKED) ) 373 { 374 /* 0 would leave no room to move a body through at all; above the 375 hard allocation cap the buffer simply cannot be made. A negative 376 argument ends up here too: GNUnet parses the option with 377 sscanf("%llu"), which reads "-1" as ULLONG_MAX rather than 378 complaining. */ 379 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 380 "-u/--max-upload must be between 1 and %llu:" 381 " it sizes the buffer a request body is relayed through\n", 382 (unsigned long long) GNUNET_MAX_MALLOC_CHECKED); 383 PH_global_ret = EXIT_INVALIDARGUMENT; 384 GNUNET_SCHEDULER_shutdown (); 385 return; 386 } 387 /* `-u' is applied by GNUNET_PROGRAM_run() before we are called, so a 388 value differing from the compiled-in default is one the operator 389 wrote. See PH_max_request_size for why that has to be visible. */ 390 buffer_max_explicit = (PH_DEFAULT_REQUEST_BUFFER_MAX 391 != PH_request_buffer_max); 392 if (! PH_no_check) 393 { 394 if (GNUNET_OK != 395 TALER_TEMPLATING_init (PAIVANA_project_data ())) 396 { 397 /* Almost always a missing or unreadable $PREFIX/share/paivana/ 398 templates/ -- an installation problem, not a bug, so say so 399 instead of adding a second "Assertion failed" to the one 400 TALER_TEMPLATING_init() already logged. */ 401 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 402 "Failed to load the paywall templates; is paivana" 403 " installed, and does PAIVANA_PREFIX point at the" 404 " installation?\n"); 405 PH_global_ret = EXIT_NOTINSTALLED; 406 GNUNET_SCHEDULER_shutdown (); 407 return; 408 } 409 } 410 if (! PAIVANA_HTTPD_reverse_init ()) 411 { 412 GNUNET_break (0); 413 PH_global_ret = EXIT_FAILURE; 414 GNUNET_SCHEDULER_shutdown (); 415 return; 416 } 417 418 if (GNUNET_OK != 419 GNUNET_CONFIGURATION_get_value_string ( 420 c, 421 "paivana", 422 "DESTINATION_BASE_URL", 423 &PH_target_server_base_url)) 424 { 425 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 426 "paivana", 427 "DESTINATION_BASE_URL"); 428 PH_global_ret = EXIT_NOTCONFIGURED; 429 GNUNET_SCHEDULER_shutdown (); 430 return; 431 } 432 if (! TALER_is_web_url (PH_target_server_base_url)) 433 { 434 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, 435 "paivana", 436 "DESTINATION_BASE_URL", 437 "not a web url"); 438 PH_global_ret = EXIT_NOTCONFIGURED; 439 GNUNET_SCHEDULER_shutdown (); 440 return; 441 } 442 { 443 unsigned long long v; 444 445 if (GNUNET_OK == 446 GNUNET_CONFIGURATION_get_value_number (c, 447 "paivana", 448 "CONNECTION_LIMIT", 449 &v)) 450 { 451 if ( (0 == v) || 452 (v > UINT_MAX) ) 453 { 454 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, 455 "paivana", 456 "CONNECTION_LIMIT", 457 "must be between 1 and UINT_MAX"); 458 PH_global_ret = EXIT_NOTCONFIGURED; 459 GNUNET_SCHEDULER_shutdown (); 460 return; 461 } 462 PH_connection_limit = (unsigned int) v; 463 } 464 if (GNUNET_OK == 465 GNUNET_CONFIGURATION_get_value_number (c, 466 "paivana", 467 "PER_IP_CONNECTION_LIMIT", 468 &v)) 469 { 470 if (v > UINT_MAX) 471 { 472 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, 473 "paivana", 474 "PER_IP_CONNECTION_LIMIT", 475 "must not exceed UINT_MAX"); 476 PH_global_ret = EXIT_NOTCONFIGURED; 477 GNUNET_SCHEDULER_shutdown (); 478 return; 479 } 480 PH_per_ip_connection_limit = (unsigned int) v; 481 } 482 if (GNUNET_OK == 483 GNUNET_CONFIGURATION_get_value_number (c, 484 "paivana", 485 "REQUEST_BUFFER_MAX", 486 &v)) 487 { 488 if ( (0 == v) || 489 (v > GNUNET_MAX_MALLOC_CHECKED) ) 490 { 491 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, 492 "paivana", 493 "REQUEST_BUFFER_MAX", 494 "must be between 1 and 40 MiB"); 495 PH_global_ret = EXIT_NOTCONFIGURED; 496 GNUNET_SCHEDULER_shutdown (); 497 return; 498 } 499 PH_request_buffer_max = v; 500 buffer_max_explicit = true; 501 } 502 if (GNUNET_OK == 503 GNUNET_CONFIGURATION_get_value_number (c, 504 "paivana", 505 "RESPONSE_BUFFER_MAX", 506 &v)) 507 { 508 if ( (0 == v) || 509 (v > GNUNET_MAX_MALLOC_CHECKED) ) 510 { 511 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, 512 "paivana", 513 "RESPONSE_BUFFER_MAX", 514 "must be between 1 and 40 MiB"); 515 PH_global_ret = EXIT_NOTCONFIGURED; 516 GNUNET_SCHEDULER_shutdown (); 517 return; 518 } 519 PH_response_buffer_max = v; 520 } 521 if (GNUNET_OK == 522 GNUNET_CONFIGURATION_get_value_number (c, 523 "paivana", 524 "MAX_REQUEST_SIZE", 525 &v)) 526 { 527 PH_max_request_size = v; 528 } 529 else if (buffer_max_explicit) 530 { 531 /* Before streaming these were one number: the buffer a body was 532 assembled in *was* the largest body we would accept. An 533 operator who raised it to permit large uploads meant the limit, 534 so honour that reading rather than silently tightening their 535 configuration back down to the 1 MiB default. */ 536 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 537 "Taking MAX_REQUEST_SIZE from the configured request" 538 " buffer size (%llu bytes); set it explicitly to say" 539 " otherwise\n", 540 PH_request_buffer_max); 541 PH_max_request_size = PH_request_buffer_max; 542 } 543 } 544 { 545 struct GNUNET_TIME_Relative st; 546 547 if (GNUNET_OK == 548 GNUNET_CONFIGURATION_get_value_time (c, 549 "paivana", 550 "UPSTREAM_TIMEOUT", 551 &st)) 552 { 553 if (0 == st.rel_value_us) 554 { 555 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, 556 "paivana", 557 "UPSTREAM_TIMEOUT", 558 "must not be zero"); 559 PH_global_ret = EXIT_NOTCONFIGURED; 560 GNUNET_SCHEDULER_shutdown (); 561 return; 562 } 563 PH_upstream_timeout = st; 564 } 565 if (GNUNET_OK == 566 GNUNET_CONFIGURATION_get_value_time (c, 567 "paivana", 568 "UPSTREAM_STALL_TIMEOUT", 569 &st)) 570 { 571 if (0 == st.rel_value_us) 572 { 573 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, 574 "paivana", 575 "UPSTREAM_STALL_TIMEOUT", 576 "must not be zero"); 577 PH_global_ret = EXIT_NOTCONFIGURED; 578 GNUNET_SCHEDULER_shutdown (); 579 return; 580 } 581 PH_upstream_stall_timeout = st; 582 } 583 } 584 { 585 unsigned int n4; 586 unsigned int n6; 587 588 if ( (! load_trusted_proxies (c, 589 "TRUSTED_PROXIES", 590 &n4)) || 591 (! load_trusted_proxies (c, 592 "TRUSTED_PROXIES6", 593 &n6)) ) 594 { 595 PH_global_ret = EXIT_NOTCONFIGURED; 596 GNUNET_SCHEDULER_shutdown (); 597 return; 598 } 599 if (PH_have_trusted_proxies) 600 { 601 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 602 "Trusting %u IPv4 and %u IPv6 network(s) as reverse proxies\n", 603 n4, 604 n6); 605 if (! PH_respect_forwarded_headers) 606 { 607 /* The policy says which proxies may speak for a client; it is 608 the -f flag that says we listen at all. Configuring one 609 without the other is a mistake in either direction, but only 610 this one leaves the policy inert. */ 611 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 612 "TRUSTED_PROXIES configured but -f/--respect-forwarded-headers" 613 " is not set; forwarded headers are ignored entirely\n"); 614 } 615 } 616 else if (PH_respect_forwarded_headers) 617 { 618 /* Not the same mistake in reverse: a single proxy in front, one 619 that writes the forwarding headers itself, needs no policy at 620 all. PAIVANA_HTTPD_resolve_forwarding() walks the chain from 621 the right and stops at the first hop it does not trust, so 622 with no policy it stops on its first step -- at the element 623 our peer wrote, which nothing the client prepends can displace. 624 What the policy adds is the ability to keep stepping leftwards 625 through hops that are named there, which is what a chain of 626 more than one proxy needs. The hazard worth warning about is 627 narrower, and it is upstream of us. */ 628 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 629 "-f/--respect-forwarded-headers is set without TRUSTED_PROXIES:" 630 " the client address is taken from the rightmost element of the" 631 " forwarding chain, which is the client's own address only if" 632 " the server in front sets or appends these headers itself." 633 " One that passes the client's `%s' through unchanged -- as" 634 " nginx does, and `%s' is the header we prefer over `%s' --" 635 " leaves the client choosing the element we believe." 636 " TRUSTED_PROXIES is needed only for a chain of more than one" 637 " proxy\n", 638 MHD_HTTP_HEADER_FORWARDED, 639 MHD_HTTP_HEADER_FORWARDED, 640 PH_HEADER_X_FORWARDED_FOR); 641 } 642 } 643 /* No need to check the return value. If given, we take it, 644 * otherwise it stays NULL. */ 645 GNUNET_CONFIGURATION_get_value_filename ( 646 c, 647 "paivana", 648 "DESTINATION_UNIXPATH", 649 &PH_target_server_unixpath); 650 if (! strip_trailing_slashes (PH_target_server_base_url)) 651 { 652 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, 653 "paivana", 654 "DESTINATION_BASE_URL", 655 "must not carry a query or a fragment"); 656 PH_global_ret = EXIT_NOTCONFIGURED; 657 GNUNET_SCHEDULER_shutdown (); 658 return; 659 } 660 if (! PH_no_check) 661 { 662 if (GNUNET_OK != 663 GNUNET_CONFIGURATION_get_value_string ( 664 c, 665 "paivana", 666 "MERCHANT_BACKEND_URL", 667 &PH_merchant_base_url)) 668 { 669 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 670 "paivana", 671 "MERCHANT_BACKEND_URL"); 672 PH_global_ret = EXIT_NOTCONFIGURED; 673 GNUNET_SCHEDULER_shutdown (); 674 return; 675 } 676 if (! TALER_is_web_url (PH_merchant_base_url)) 677 { 678 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, 679 "paivana", 680 "MERCHANT_BACKEND_URL", 681 "not a web url"); 682 PH_global_ret = EXIT_NOTCONFIGURED; 683 GNUNET_SCHEDULER_shutdown (); 684 return; 685 } 686 /* Deliberately *not* run through strip_trailing_slashes(): a Taler 687 merchant API base URL is joined with relative paths, so it wants 688 the '/' the other base URLs above shed. TALER_url_join() insists 689 on it and returns NULL without it, which surfaces much later as 690 an assertion failure on the handle built from the joined URL -- 691 so check for it here, where we can still name the option that is 692 wrong. */ 693 if ('/' != PH_merchant_base_url[strlen (PH_merchant_base_url) - 1]) 694 { 695 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, 696 "paivana", 697 "MERCHANT_BACKEND_URL", 698 "must end with a '/'"); 699 PH_global_ret = EXIT_NOTCONFIGURED; 700 GNUNET_SCHEDULER_shutdown (); 701 return; 702 } 703 } 704 { 705 char *merchant_unix_path; 706 707 if (GNUNET_OK == 708 GNUNET_CONFIGURATION_get_value_string ( 709 c, 710 "paivana", 711 "MERCHANT_BACKEND_UNIX_PATH", 712 &merchant_unix_path)) 713 { 714 if (! TALER_MERCHANT_global_set_unixpath (merchant_unix_path)) 715 { 716 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING, 717 "paivana", 718 "MERCHANT_BACKEND_UNIX_PATH", 719 "invalid path; ignoring the setting"); 720 } 721 GNUNET_free (merchant_unix_path); 722 } 723 } 724 { 725 char *whitelist; 726 727 if (GNUNET_OK == 728 GNUNET_CONFIGURATION_get_value_string ( 729 c, 730 "paivana", 731 "WHITELIST", 732 &whitelist)) 733 { 734 regex_t bare; 735 int rc; 736 737 /* Compile the expression exactly as written first, and refuse 738 the configuration if that fails. The wrapping below is a 739 textual splice, and a value like "a)|(b" splices into 740 "^(a)|(b)$" -- which compiles, but means "^a" OR "b$", each 741 anchored on one side only. Getting out of the group that way 742 needs parentheses that do not balance on their own, and that 743 is precisely what a bare regcomp() rejects; an expression that 744 is valid by itself is unaffected and keeps matching what it 745 always did. (The merchant backend validates the website_regex 746 of a template the same way.) */ 747 rc = regcomp (&bare, 748 whitelist, 749 REG_NOSUB | REG_EXTENDED); 750 if (0 == rc) 751 { 752 char *anchored; 753 754 regfree (&bare); 755 /* Anchor the expression: regexec(3) is unanchored, so a 756 WHITELIST of "/free/" would otherwise waive payment for every 757 URL merely *containing* it -- including one an attacker 758 appends to a path they want for free. Wrapping in a group 759 keeps alternations ("a|b") from binding the anchors to only 760 the first and last branch. An expression that already 761 anchors itself is unaffected, as ^ and $ inside still match 762 at string start/end. */ 763 GNUNET_asprintf (&anchored, 764 "^(%s)$", 765 whitelist); 766 rc = regcomp (&PH_whitelist_ex, 767 anchored, 768 REG_NOSUB | REG_EXTENDED); 769 GNUNET_free (anchored); 770 } 771 if (0 != rc) 772 { 773 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, 774 "paivana", 775 "WHITELIST", 776 "Invalid regular expression"); 777 GNUNET_free (whitelist); 778 PH_global_ret = EXIT_NOTCONFIGURED; 779 GNUNET_SCHEDULER_shutdown (); 780 return; 781 } 782 PH_have_whitelist_ex = true; 783 GNUNET_free (whitelist); 784 } 785 } 786 787 if (GNUNET_OK != 788 GNUNET_CONFIGURATION_get_value_string ( 789 c, 790 "paivana", 791 "BASE_URL", 792 &PH_base_url)) 793 { 794 /* Without BASE_URL we reconstruct our own URL from the request, and 795 the only thing that makes that safe is a reverse proxy in front 796 of us that enforced a correct Host: -- which is exactly what -f 797 asserts. Talking to clients directly, Host: is whatever the 798 client typed, and it decides both the string the access cookie is 799 keyed on and the string the templates' website_regex is matched 800 against: a client sending "Host: anything.invalid" would match no 801 template and be served for free. The scheme has the same 802 problem in reverse: direct connections have no X-Forwarded-Proto 803 to consult, so a site reached over https would generate http:// 804 URLs and again match no template. */ 805 if ( (! PH_respect_forwarded_headers) && 806 (! PH_no_check) ) 807 { 808 /* -n has no paywall, hence no access decision that the site's 809 own identity could be got wrong for; the base URL is then only 810 cosmetic and the "pure reverse proxy" mode stays configurable 811 with nothing but DESTINATION_BASE_URL. */ 812 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 813 "paivana", 814 "BASE_URL"); 815 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 816 "BASE_URL is required unless -f is given: without a" 817 " reverse proxy vouching for it, the client's Host" 818 " header cannot be used to identify this site\n"); 819 PH_global_ret = EXIT_NOTCONFIGURED; 820 GNUNET_SCHEDULER_shutdown (); 821 return; 822 } 823 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_INFO, 824 "paivana", 825 "BASE_URL"); 826 } 827 if (NULL != PH_base_url) 828 { 829 if (! TALER_is_web_url (PH_base_url)) 830 { 831 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, 832 "paivana", 833 "BASE_URL", 834 "not a web url"); 835 PH_global_ret = EXIT_NOTCONFIGURED; 836 GNUNET_SCHEDULER_shutdown (); 837 return; 838 } 839 if (! strip_trailing_slashes (PH_base_url)) 840 { 841 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, 842 "paivana", 843 "BASE_URL", 844 "must not carry a query or a fragment"); 845 PH_global_ret = EXIT_NOTCONFIGURED; 846 GNUNET_SCHEDULER_shutdown (); 847 return; 848 } 849 } 850 851 if (GNUNET_OK != 852 GNUNET_CONFIGURATION_get_value_string ( 853 c, 854 "paivana", 855 "SECRET", 856 &secret)) 857 { 858 if (! PH_no_check) 859 { 860 /* The key is the only input to the access-cookie MAC that the 861 client does not supply, so a fresh random one invalidates 862 every access anyone has paid for. That used to be a warning 863 and a random key, which is survivable only if restarts are 864 rare -- and the shipped unit restarts hourly (RuntimeMaxSec), 865 so a customer paying at 10:59 was shown the paywall again at 866 11:01. Refuse to start instead: the packaging generates one 867 at install time, and an operator configuring by hand needs to 868 be told rather than silently sold a paywall that forgets. `-n' 869 mints no cookies at all and so needs no key. */ 870 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 871 "paivana", 872 "SECRET"); 873 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 874 "Refusing to start without `SECRET': every restart" 875 " would invalidate all access already paid for." 876 " Generate one with `gpg --gen-random 0 32 | base64'" 877 " (or pass -n to serve without a paywall).\n"); 878 PH_global_ret = EXIT_NOTCONFIGURED; 879 GNUNET_SCHEDULER_shutdown (); 880 return; 881 } 882 GNUNET_CRYPTO_random_block (&paivana_secret, 883 sizeof (paivana_secret)); 884 } 885 else 886 { 887 GNUNET_CRYPTO_hash (secret, 888 strlen (secret), 889 &paivana_secret); 890 GNUNET_free (secret); 891 } 892 PH_proxy_ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, 893 &proxy_ctx_rc); 894 GNUNET_assert (NULL != PH_proxy_ctx); 895 proxy_ctx_rc = GNUNET_CURL_gnunet_rc_create (PH_proxy_ctx); 896 if (! PH_no_check) 897 { 898 char *merchant_access_token; 899 char *auth_header; 900 901 if (GNUNET_OK != 902 GNUNET_CONFIGURATION_get_value_string ( 903 c, 904 "paivana", 905 "MERCHANT_ACCESS_TOKEN", 906 &merchant_access_token)) 907 { 908 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 909 "paivana", 910 "MERCHANT_ACCESS_TOKEN"); 911 PH_global_ret = EXIT_NOTCONFIGURED; 912 GNUNET_SCHEDULER_shutdown (); 913 return; 914 } 915 if ('\0' == merchant_access_token[0]) 916 { 917 /* An empty value builds "Authorization: Bearer " and the backend 918 answers 401 to every request we make with it, i.e. no template 919 ever loads and no payment is ever confirmed -- and nothing 920 says why. The option being present but blank is the shape a 921 half-finished install has. */ 922 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, 923 "paivana", 924 "MERCHANT_ACCESS_TOKEN", 925 "must not be empty"); 926 GNUNET_free (merchant_access_token); 927 PH_global_ret = EXIT_NOTCONFIGURED; 928 GNUNET_SCHEDULER_shutdown (); 929 return; 930 } 931 /* A second context, because the credential below is appended to 932 *every* request the context makes: on a shared context we would 933 hand our merchant bearer token to the origin server (and to 934 whoever it redirects to) on each forwarded request. */ 935 PH_merchant_ctx 936 = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, 937 &merchant_ctx_rc); 938 GNUNET_assert (NULL != PH_merchant_ctx); 939 merchant_ctx_rc = GNUNET_CURL_gnunet_rc_create (PH_merchant_ctx); 940 GNUNET_asprintf (&auth_header, 941 "%s: Bearer %s", 942 MHD_HTTP_HEADER_AUTHORIZATION, 943 merchant_access_token); 944 GNUNET_free (merchant_access_token); 945 GNUNET_assert (GNUNET_OK == 946 GNUNET_CURL_append_header (PH_merchant_ctx, 947 auth_header)); 948 GNUNET_free (auth_header); 949 } 950 /* Once templates are done loading, this will 951 start the daemon as well. In -n (no-payment) mode we skip 952 the merchant round-trip entirely. */ 953 if (PH_no_check) 954 { 955 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 956 "Paywall disabled (-n), skipping template load\n"); 957 PAIVANA_HTTPD_serve_requests (); 958 return; 959 } 960 PAIVANA_HTTPD_load_templates (); 961 } 962 963 964 /** 965 * Main function. 966 */ 967 int 968 main (int argc, 969 char *const *argv) 970 { 971 struct GNUNET_GETOPT_CommandLineOption options[] = { 972 GNUNET_GETOPT_option_flag ( 973 'f', 974 "respect-forwarded-headers", 975 gettext_noop ( 976 "trust X-Forwarded-For for the client address (only safe behind a trusted reverse proxy)"), 977 &PH_respect_forwarded_headers), 978 GNUNET_GETOPT_option_flag ( 979 'g', 980 "global-payment", 981 gettext_noop ( 982 "disables per-page payment, useful if a single payment should grant access to the entire site"), 983 &PH_global_cookie), 984 GNUNET_GETOPT_option_flag ( 985 'n', 986 "no-payment", 987 gettext_noop ( 988 "disables payment, useful for testing reverse-proxy only"), 989 &PH_no_check), 990 GNUNET_GETOPT_option_ulong ( 991 'u', 992 "max-upload", 993 "BYTES", 994 gettext_noop ( 995 "bytes of a request body to hold in memory while relaying it upstream (default: 262144); the largest body accepted is MAX_REQUEST_SIZE"), 996 &PH_request_buffer_max), 997 GNUNET_GETOPT_OPTION_END 998 }; 999 enum GNUNET_GenericReturnValue ret; 1000 1001 ret = GNUNET_PROGRAM_run ( 1002 PAIVANA_project_data (), 1003 argc, 1004 argv, 1005 "paivana-httpd", 1006 "reverse proxy requesting Taler payment", 1007 options, 1008 &run, NULL); 1009 if (GNUNET_SYSERR == ret) 1010 return EXIT_INVALIDARGUMENT; 1011 if (GNUNET_NO == ret) 1012 return EXIT_SUCCESS; 1013 return PH_global_ret; 1014 } 1015 1016 1017 /* end of paivana-httpd.c */