paivana-httpd_templates.c (45539B)
1 /* 2 This file is part of GNUnet. 3 Copyright (C) 2026 Taler Systems SA 4 5 Paivana is free software; you can redistribute it and/or 6 modify it under the terms of the GNU Affero General Public License 7 as published by the Free Software Foundation; either version 8 3, or (at your option) any later version. 9 10 Paivana is distributed in the hope that it will be useful, 11 but WITHOUT ANY WARRANTY; without even the implied warranty 12 of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 13 the GNU Affero General Public License for more details. 14 15 You should have received a copy of the GNU Affero General Public 16 License along with Paivana; see the file COPYING. If not, 17 write to the Free Software Foundation, Inc., 51 Franklin 18 Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 */ 20 21 /** 22 * @author Christian Grothoff 23 * @file paivana-httpd_templates.c 24 * @brief template functions 25 */ 26 #include "platform.h" 27 #include <curl/curl.h> 28 #include <gnunet/gnunet_util_lib.h> 29 #include <gnunet/gnunet_uri_lib.h> 30 #include <gnunet/gnunet_curl_lib.h> 31 #include "paivana-httpd.h" 32 #include "paivana-httpd_daemon.h" 33 #include "paivana-httpd_helper.h" 34 #include "paivana-httpd_templates.h" 35 #include <taler/taler_mhd_lib.h> 36 #include <taler/taler_templating_lib.h> 37 #include "paivana_pd.h" 38 #include <regex.h> 39 40 41 struct Template; 42 #define TALER_MERCHANT_GET_PRIVATE_TEMPLATE_RESULT_CLOSURE struct Template 43 #include <taler/merchant/get-private-templates-TEMPLATE_ID.h> 44 #include <taler/merchant/get-private-templates.h> 45 46 47 /** 48 * Maximum number of rendered paywall responses we cache per template. 49 * 50 * The key is derived from the client-supplied Accept-Language and 51 * Accept-Encoding headers, so without a bound an attacker could send 52 * unlimited distinct header values and grow the cache without limit 53 * (memory-exhaustion DoS on the cheap pre-payment path). The key is 54 * normalised to what we can actually serve — see cache_key_language() 55 * and TALER_MHD_can_compress() — which is what keeps every request 56 * after the first few a *hit*; this cap only backstops that. On 57 * reaching it we evict the least recently used entry. 58 */ 59 #define MAX_RESPONSE_CACHE_ENTRIES 128 60 61 /** 62 * How long we give the merchant backend to answer our template 63 * queries before abandoning the startup sequence. 64 * 65 * Nothing is served until they are in: #PAIVANA_HTTPD_serve_requests() 66 * — which binds the listen sockets — is only reached from the last of 67 * these callbacks, and neither `TALER_MERCHANT_curl_easy_get_()' nor 68 * anything above it arms CURLOPT_TIMEOUT. A backend that accepts the 69 * TCP connection and then never answers would otherwise leave paivana 70 * neither serving nor exiting, with no log line after the startup 71 * banner; under `SERVE = systemd' the listening socket already 72 * exists, so clients connect successfully and then hang forever with 73 * nothing accepting them. Generous enough for the round-trips a load 74 * takes (GET /private/templates, then one GET per template, issued in 75 * parallel). 76 */ 77 #define TEMPLATE_LOAD_TIMEOUT \ 78 GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 2) 79 80 81 /** 82 * Entry in the cache of responses for a given template. 83 */ 84 struct ResponseCacheEntry 85 { 86 87 /** 88 * Kept in a DLL. 89 */ 90 struct ResponseCacheEntry *next; 91 92 /** 93 * Kept in a DLL. 94 */ 95 struct ResponseCacheEntry *prev; 96 97 /** 98 * Language of the response. 99 */ 100 char *lang; 101 102 /** 103 * True if @e paywall carries a deflate-compressed body. 104 */ 105 bool deflate; 106 107 /** 108 * Paywall response for these request parameters. 109 */ 110 struct MHD_Response *paywall; 111 112 /** 113 * HTTP status to return with @e paywall. 114 */ 115 unsigned int http_status; 116 117 }; 118 119 120 /** 121 * Information about a template in the merchant backend. 122 */ 123 struct Template 124 { 125 126 /** 127 * Kept in a DLL. 128 */ 129 struct Template *next; 130 131 /** 132 * Kept in a DLL. 133 */ 134 struct Template *prev; 135 136 /** 137 * ID of the template. 138 */ 139 char *template_id; 140 141 /** 142 * Summary of the template, NULL if not given. 143 */ 144 char *summary; 145 146 /** 147 * Maximum pickup delay for the pages. 148 */ 149 struct GNUNET_TIME_Relative max_pickup_delay; 150 151 /** 152 * Ways how to pay for the template. 153 */ 154 json_t *choices; 155 156 /** 157 * Regular expression of websites the template is for. 158 */ 159 char *regex; 160 161 /** 162 * Pre-compiled regular expression @e regex. 163 */ 164 regex_t ex; 165 166 /** 167 * Handle used to request more information about the template. 168 */ 169 struct TALER_MERCHANT_GetPrivateTemplateHandle *gt; 170 171 /** 172 * Number of entries in the template cache starting at @e rce_head. 173 */ 174 unsigned int rce_length; 175 176 /** 177 * Kept in a DLL. 178 */ 179 struct ResponseCacheEntry *rce_head; 180 181 /** 182 * Kept in a DLL. 183 */ 184 struct ResponseCacheEntry *rce_tail; 185 186 }; 187 188 189 /** 190 * Kept in a DLL. 191 */ 192 static struct Template *t_head; 193 194 /** 195 * Kept in a DLL. 196 */ 197 static struct Template *t_tail; 198 199 /** 200 * Handle to get all the templates. 201 */ 202 static struct TALER_MERCHANT_GetPrivateTemplatesHandle *gpt; 203 204 /** 205 * Watchdog for #TEMPLATE_LOAD_TIMEOUT, NULL once the templates are in 206 * (or once we have given up on them). 207 */ 208 static struct GNUNET_SCHEDULER_Task *load_timeout_task; 209 210 211 /** 212 * Task run when the merchant backend did not answer our template 213 * queries within #TEMPLATE_LOAD_TIMEOUT. 214 * 215 * Treated exactly like any other failure to load the templates (an 216 * unauthorized or unexpected status from the backend): the daemon 217 * exits with a diagnosis instead of stalling. Whether that policy is 218 * the right one is a separate question — this task only makes sure the 219 * stall is not a third, silent outcome. 220 * 221 * @param cls NULL 222 */ 223 static void 224 load_timeout (void *cls) 225 { 226 (void) cls; 227 load_timeout_task = NULL; 228 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 229 "Merchant backend at `%s' did not answer our template queries" 230 " within %s; giving up instead of never starting to serve\n", 231 PH_merchant_base_url, 232 GNUNET_STRINGS_relative_time_to_string (TEMPLATE_LOAD_TIMEOUT, 233 true)); 234 PH_global_ret = EXIT_FAILURE; 235 GNUNET_SCHEDULER_shutdown (); 236 } 237 238 239 /** 240 * The templates are in: stop the watchdog and open the listen sockets. 241 * 242 * Refuses to start if there is not a single one. Only reachable with 243 * the paywall enabled -- `-n' returns before 244 * #PAIVANA_HTTPD_load_templates() is ever called -- and a paywall with 245 * nothing to sell is not a paywall: PAIVANA_HTTPD_search_templates() 246 * would answer "no template matched" for every URL, which create_response() 247 * reads as "no paywall applies" and forwards. The whole site would be 248 * free, silently, which is exactly the outcome an operator running a 249 * paywall did not ask for. An operator who does want a plain reverse 250 * proxy says so with `-n'. 251 */ 252 static void 253 templates_ready (void) 254 { 255 if (NULL == t_head) 256 { 257 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 258 "The merchant backend at `%s' offers no paivana template;" 259 " refusing to start, as every request would then be" 260 " forwarded for free. Pass -n if serving the site without" 261 " a paywall is what you want.\n", 262 PH_merchant_base_url); 263 PH_global_ret = EXIT_NOTCONFIGURED; 264 GNUNET_SCHEDULER_shutdown (); 265 return; 266 } 267 if (NULL != load_timeout_task) 268 { 269 GNUNET_SCHEDULER_cancel (load_timeout_task); 270 load_timeout_task = NULL; 271 } 272 PAIVANA_HTTPD_serve_requests (); 273 } 274 275 276 /** 277 * Check if two strings are equal, including both being NULL 278 * 279 * @param s1 a string, possibly NULL 280 * @param s2 a string. possibly NULL 281 * @return true if both are equal 282 */ 283 static bool 284 eq (const char *s1, 285 const char *s2) 286 { 287 if (s1 == s2) 288 return true; 289 if (NULL == s1) 290 return false; 291 if (NULL == s2) 292 return false; 293 return (0 == strcmp (s1, 294 s2)); 295 } 296 297 298 /** 299 * Create a taler://pay-template/ URI for the given @a con and @a template_id 300 * and @a instance_id. 301 * 302 * @param merchant_base_url URL to take host and path from; 303 * we cannot take it from the MHD connection as a browser 304 * may have changed 'http' to 'https' and we MUST be consistent 305 * with what the merchant's frontend used initially 306 * @param template_id the template id 307 * @return corresponding taler://pay-template/ URI, or NULL on missing "host" 308 */ 309 static char * 310 make_taler_pay_template_uri (const char *merchant_base_url, 311 const char *template_id) 312 { 313 struct GNUNET_Buffer buf = { 0 }; 314 char *url; 315 struct GNUNET_Uri uri; 316 317 url = GNUNET_strdup (merchant_base_url); 318 if (-1 == GNUNET_uri_parse (&uri, 319 url)) 320 { 321 GNUNET_break (0); 322 GNUNET_free (url); 323 return NULL; 324 } 325 if ( (NULL == uri.scheme) || 326 (NULL == uri.host) ) 327 { 328 GNUNET_break (0); 329 GNUNET_free (url); 330 return NULL; 331 } 332 GNUNET_assert (NULL != template_id); 333 GNUNET_buffer_write_str (&buf, 334 "taler"); 335 if (0 == strcasecmp ("http", 336 uri.scheme)) 337 GNUNET_buffer_write_str (&buf, 338 "+http"); 339 GNUNET_buffer_write_str (&buf, 340 "://pay-template/"); 341 GNUNET_buffer_write_str (&buf, 342 uri.host); 343 if (0 != uri.port) 344 GNUNET_buffer_write_fstr (&buf, 345 ":%u", 346 (unsigned int) uri.port); 347 if (NULL != uri.path) 348 GNUNET_buffer_write_path (&buf, 349 uri.path); 350 GNUNET_buffer_write_path (&buf, 351 template_id); 352 GNUNET_free (url); 353 return GNUNET_buffer_reap_str (&buf); 354 } 355 356 357 /** 358 * Render @a s as a complete, double-quoted JavaScript string literal. 359 * 360 * The paywall page carries its context in `const' declarations inside a 361 * <script> element, and mustache's default escaping is the wrong 362 * escaping there twice over: it escapes exactly '<', '>', '&' and '"' 363 * (mustach-wrap.c), which leaves the apostrophe free to close a 364 * single-quoted literal and let arbitrary JavaScript follow, and the 365 * entity references it does produce are never decoded, because the HTML 366 * parser does not decode them inside a raw-text element -- an '&' in the 367 * merchant base URL reached the script as the literal text "&". 368 * 369 * So the value is escaped for the context it actually lands in, and 370 * interpolated with the unescaped {{{ }}} since it arrives complete with 371 * its quotes. '<', '>' and '&' are still escaped, as \\uXXXX rather 372 * than as entities: without that a value containing "</script>" would 373 * end the element regardless of how well the string literal itself is 374 * quoted. 375 * 376 * @param s string to render, must be valid UTF-8 377 * @return JavaScript literal including the surrounding quotes, 378 * to be freed by the caller 379 */ 380 static char * 381 js_string_literal (const char *s) 382 { 383 struct GNUNET_Buffer buf = { 0 }; 384 385 GNUNET_buffer_write_str (&buf, 386 "\""); 387 for (const unsigned char *p = (const unsigned char *) s; 388 '\0' != *p; 389 p++) 390 { 391 switch (*p) 392 { 393 case '"': 394 GNUNET_buffer_write_str (&buf, 395 "\\\""); 396 break; 397 case '\\': 398 GNUNET_buffer_write_str (&buf, 399 "\\\\"); 400 break; 401 default: 402 if ( (*p < 0x20) || 403 (0x7F == *p) || 404 ('<' == *p) || 405 ('>' == *p) || 406 ('&' == *p) ) 407 GNUNET_buffer_write_fstr (&buf, 408 "\\u%04x", 409 (unsigned int) *p); 410 else 411 GNUNET_buffer_write_fstr (&buf, 412 "%c", 413 (char) *p); 414 break; 415 } 416 } 417 GNUNET_buffer_write_str (&buf, 418 "\""); 419 return GNUNET_buffer_reap_str (&buf); 420 } 421 422 423 /** 424 * Number of installed `paywall.*.must' templates, counted once by 425 * count_paywall_templates(). UINT_MAX until then. 426 */ 427 static unsigned int paywall_template_count = UINT_MAX; 428 429 430 /** 431 * Count the installed paywall templates. 432 * 433 * TALER_TEMPLATING_build() picks among them by matching the client's 434 * `Accept-Language' against the language tag in each file name, and 435 * does not report which one it chose. We do not need to know: if there 436 * is only one to choose from — the shipped configuration, which ships 437 * `paywall.en.must' and nothing else — then every `Accept-Language' 438 * whatsoever produces the same body, and the header can be dropped from 439 * the cache key entirely. 440 * 441 * @param cls unused 442 * @param filename file found in the template directory 443 * @return #GNUNET_OK to continue the scan 444 */ 445 static enum GNUNET_GenericReturnValue 446 count_paywall_template (void *cls, 447 const char *filename) 448 { 449 const char *base; 450 451 (void) cls; 452 base = strrchr (filename, 453 '/'); 454 base = (NULL == base) ? filename : base + 1; 455 if (0 == strncmp (base, 456 "paywall.", 457 strlen ("paywall."))) 458 paywall_template_count++; 459 return GNUNET_OK; 460 } 461 462 463 /** 464 * The `Content-Security-Policy' for the paywall page, built once from 465 * #PH_merchant_base_url. NULL until first needed. 466 */ 467 static char *paywall_csp; 468 469 470 /** 471 * Return the `Content-Security-Policy' for the paywall page. 472 * 473 * The page is entirely self-contained -- every stylesheet, script and 474 * image is inline -- so everything but the merchant backend it polls 475 * can be denied outright. That is the part worth having: with 476 * `default-src' at 'none' and `connect-src' naming exactly two origins, 477 * script that does run cannot reach an attacker's host to report what 478 * it found, and `frame-ancestors' keeps the taler:// link from being 479 * framed and clicked by proxy. 480 * 481 * `script-src' still has to permit inline script: the page carries three 482 * inline <script> elements and one `onclick' attribute, and neither 483 * hashes nor a nonce survive our response cache, which serves one 484 * rendered body to every client for five minutes. Removing 485 * 'unsafe-inline' means moving that handler into paywall.js and hashing 486 * each block after rendering; worth doing, but it is not what makes the 487 * injection this policy backs up impossible -- js_string_literal() is. 488 * 489 * @return the policy, owned by this module, or NULL if the merchant 490 * base URL cannot be parsed 491 */ 492 static const char * 493 get_paywall_csp (void) 494 { 495 struct GNUNET_Buffer buf = { 0 }; 496 struct GNUNET_Uri uri; 497 char *url; 498 499 if (NULL != paywall_csp) 500 return paywall_csp; 501 url = GNUNET_strdup (PH_merchant_base_url); 502 if ( (-1 == GNUNET_uri_parse (&uri, 503 url)) || 504 (NULL == uri.scheme) || 505 (NULL == uri.host) ) 506 { 507 /* Cannot name the backend, and a policy that omits it would break 508 the polling the page exists to do. Serve without one rather than 509 with a broken one; the URL is checked at startup, so this is the 510 unreachable arm. */ 511 GNUNET_break (0); 512 GNUNET_free (url); 513 return NULL; 514 } 515 GNUNET_buffer_write_str (&buf, 516 "default-src 'none'; " 517 "script-src 'unsafe-inline'; " 518 "style-src 'unsafe-inline'; " 519 /* the QR code is drawn to a canvas and 520 handed to an <img> as a data: URL */ 521 "img-src data:; " 522 "connect-src 'self' "); 523 GNUNET_buffer_write_str (&buf, 524 uri.scheme); 525 GNUNET_buffer_write_str (&buf, 526 "://"); 527 GNUNET_buffer_write_str (&buf, 528 uri.host); 529 if (0 != uri.port) 530 GNUNET_buffer_write_fstr (&buf, 531 ":%u", 532 (unsigned int) uri.port); 533 GNUNET_buffer_write_str (&buf, 534 "; frame-ancestors 'none'" 535 "; base-uri 'none'" 536 "; form-action 'none'"); 537 GNUNET_free (url); 538 paywall_csp = GNUNET_buffer_reap_str (&buf); 539 return paywall_csp; 540 } 541 542 543 /** 544 * Return the language component of the render cache key for @a conn. 545 * 546 * With a single installed paywall template there is nothing to 547 * negotiate, so the key does not depend on `Accept-Language' at all and 548 * the whole header — an unauthenticated client's free choice, on the 549 * pre-payment path — stops being able to force a fresh Mustache render 550 * per distinct value. With several installed we cannot tell which one 551 * the templating library picked (it has no accessor for that; the 552 * proper fix belongs there), so we fall back to keying on the raw 553 * header and accept the amplification for that configuration. 554 * 555 * @param conn connection to derive the key component for 556 * @return the key component, or NULL if `Accept-Language' does not 557 * affect the rendered body 558 */ 559 static const char * 560 cache_key_language (struct MHD_Connection *conn) 561 { 562 if (UINT_MAX == paywall_template_count) 563 { 564 char *dir; 565 char *tdir; 566 567 paywall_template_count = 0; 568 dir = GNUNET_OS_installation_get_path (PAIVANA_project_data (), 569 GNUNET_OS_IPK_DATADIR); 570 GNUNET_asprintf (&tdir, 571 "%stemplates", 572 dir); 573 GNUNET_free (dir); 574 if (0 > GNUNET_DISK_directory_scan (tdir, 575 &count_paywall_template, 576 NULL)) 577 { 578 /* Cannot tell; assume the worst and keep the old key. */ 579 GNUNET_break (0); 580 paywall_template_count = 2; 581 } 582 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 583 "%u paywall template(s) installed in `%s'\n", 584 paywall_template_count, 585 tdir); 586 GNUNET_free (tdir); 587 } 588 if (2 > paywall_template_count) 589 return NULL; 590 return MHD_lookup_connection_value (conn, 591 MHD_HEADER_KIND, 592 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 593 } 594 595 596 /** 597 * Try to initialize the paywall response. 598 * 599 * @param conn connection to create the response for 600 * @param t template to create the response for 601 * @return MHD status code to return 602 */ 603 static enum MHD_Result 604 load_paywall (struct MHD_Connection *conn, 605 struct Template *t) 606 { 607 struct MHD_Response *reply; 608 const char *lang; 609 bool deflate; 610 unsigned int http_status = MHD_HTTP_PAYMENT_REQUIRED; 611 612 lang = cache_key_language (conn); 613 /* `Accept-Encoding' reaches the body through exactly this predicate 614 (templating_api.c), so keying on its result rather than on the 615 header text is not an approximation: it is the decision itself, 616 and it has two outcomes instead of unboundedly many. */ 617 deflate = (TALER_MHD_CT_DEFLATE == 618 TALER_MHD_can_compress (conn, 619 TALER_MHD_CT_DEFLATE)); 620 for (struct ResponseCacheEntry *pos = t->rce_head; 621 NULL != pos; 622 pos = pos->next) 623 { 624 if ( (eq (lang, 625 pos->lang)) && 626 (deflate == pos->deflate) ) 627 { 628 if (t->rce_head != pos) 629 { 630 /* Hit, move pos to head of DLL for proper LRU eviction */ 631 GNUNET_CONTAINER_DLL_remove (t->rce_head, 632 t->rce_tail, 633 pos); 634 GNUNET_CONTAINER_DLL_insert (t->rce_head, 635 t->rce_tail, 636 pos); 637 } 638 return MHD_queue_response (conn, 639 pos->http_status, 640 pos->paywall); 641 } 642 } 643 644 { 645 enum GNUNET_GenericReturnValue ret; 646 json_t *data; 647 char *tid_js = js_string_literal (t->template_id); 648 char *mb_js = js_string_literal (PH_merchant_base_url); 649 650 data = GNUNET_JSON_PACK ( 651 GNUNET_JSON_pack_string ( 652 "template_id", 653 t->template_id), 654 /* The `_js' variants are complete JavaScript string literals, 655 quotes included, for the <script> block; the plain ones are for 656 the HTML body, where mustache's own escaping is correct. */ 657 GNUNET_JSON_pack_string ( 658 "template_id_js", 659 tid_js), 660 GNUNET_JSON_pack_string ( 661 "merchant_backend_js", 662 mb_js), 663 GNUNET_JSON_pack_allow_null ( 664 GNUNET_JSON_pack_string ( 665 "summary", 666 t->summary)), 667 GNUNET_JSON_pack_allow_null ( 668 GNUNET_JSON_pack_array_incref ( 669 "choices", 670 t->choices)), 671 GNUNET_JSON_pack_bool ( 672 "has_choices", 673 1 < json_array_size (t->choices)), 674 GNUNET_JSON_pack_allow_null ( 675 GNUNET_JSON_pack_object_incref ( 676 "default_choice", 677 json_array_get (t->choices, 0))), 678 GNUNET_JSON_pack_uint64 ( 679 "max_pickup_delay", 680 /* Note: 'forever' will result in a very large number 681 here, that is intentional, it is equivalent and avoids 682 a special case on the client-side. */ 683 t->max_pickup_delay.rel_value_us / 1000LLU / 1000LLU), 684 GNUNET_JSON_pack_string ( 685 "merchant_backend", 686 PH_merchant_base_url)); 687 GNUNET_free (tid_js); 688 GNUNET_free (mb_js); 689 ret = TALER_TEMPLATING_build ( 690 conn, 691 &http_status, 692 "paywall", 693 NULL /* no instance */, 694 NULL /* no Taler URI (needs dynamic paivana_id!) */, 695 data, 696 &reply); 697 json_decref (data); 698 if (GNUNET_NO == ret) 699 { 700 enum MHD_Result mret; 701 702 /* taler_templating_lib.h: #GNUNET_NO means an *error reply* was 703 built — typically because the paywall template is not 704 installed where TALER_TEMPLATING_init() looked. It is a live 705 MHD_Response and it is ours: returning #MHD_YES without 706 queuing it leaked it once per unauthenticated request and left 707 the client waiting for a status line that never came. */ 708 GNUNET_break (0); 709 mret = MHD_queue_response (conn, 710 http_status, 711 reply); 712 MHD_destroy_response (reply); 713 return mret; 714 } 715 if (GNUNET_OK != ret) 716 { 717 /* #GNUNET_SYSERR: no reply was built, so there is nothing to 718 queue and nothing to free; MHD must close the connection. */ 719 GNUNET_break (0); 720 return MHD_NO; 721 } 722 } 723 724 725 GNUNET_break (MHD_YES == 726 MHD_add_response_header (reply, 727 MHD_HTTP_HEADER_CONTENT_TYPE, 728 "text/html")); 729 /* The paywall body depends on the negotiated language and on 730 whether we deflated it for the client; tell intermediaries to 731 key their cache entries on both. */ 732 GNUNET_break (MHD_YES == 733 MHD_add_response_header (reply, 734 MHD_HTTP_HEADER_VARY, 735 MHD_HTTP_HEADER_ACCEPT_LANGUAGE ", " 736 MHD_HTTP_HEADER_ACCEPT_ENCODING ", " 737 "Cookie")); 738 GNUNET_break (MHD_YES == 739 MHD_add_response_header (reply, 740 MHD_HTTP_HEADER_CACHE_CONTROL, 741 "public, max-age=300")); 742 { 743 const char *csp = get_paywall_csp (); 744 745 if (NULL != csp) 746 GNUNET_break (MHD_YES == 747 MHD_add_response_header (reply, 748 MHD_HTTP_HEADER_CONTENT_SECURITY_POLICY, 749 csp)); 750 } 751 /* frame-ancestors covers this for anything current; X-Frame-Options 752 is for the user agents that do not implement it. */ 753 GNUNET_break (MHD_YES == 754 MHD_add_response_header (reply, 755 MHD_HTTP_HEADER_X_FRAME_OPTIONS, 756 "DENY")); 757 GNUNET_break (MHD_YES == 758 MHD_add_response_header (reply, 759 MHD_HTTP_HEADER_X_CONTENT_TYPE_OPTIONS, 760 "nosniff")); 761 { 762 char *uri; 763 764 uri = make_taler_pay_template_uri (PH_merchant_base_url, 765 t->template_id); 766 if (NULL != uri) 767 { 768 GNUNET_break (MHD_YES == 769 MHD_add_response_header (reply, 770 "Paivana", 771 uri)); 772 GNUNET_free (uri); 773 } 774 } 775 776 { 777 struct ResponseCacheEntry *rce; 778 779 /* '>=', not '>': the insert below is what takes us to the cap, so 780 testing '>' left the steady state one entry above it. */ 781 while (t->rce_length >= MAX_RESPONSE_CACHE_ENTRIES) 782 { 783 /* Evict the least recently used entry; the hit path above 784 promotes to the head, so the tail is the coldest. */ 785 struct ResponseCacheEntry *old = t->rce_tail; 786 787 GNUNET_CONTAINER_DLL_remove (t->rce_head, 788 t->rce_tail, 789 old); 790 GNUNET_assert (t->rce_length > 0); 791 t->rce_length--; 792 MHD_destroy_response (old->paywall); 793 GNUNET_free (old->lang); 794 GNUNET_free (old); 795 } 796 rce = GNUNET_new (struct ResponseCacheEntry); 797 if (NULL != lang) 798 rce->lang = GNUNET_strdup (lang); 799 rce->deflate = deflate; 800 rce->paywall = reply; 801 rce->http_status = http_status; 802 t->rce_length++; 803 GNUNET_CONTAINER_DLL_insert (t->rce_head, 804 t->rce_tail, 805 rce); 806 return MHD_queue_response (conn, 807 rce->http_status, 808 reply); 809 } 810 } 811 812 813 /** 814 * Parse template contract to (mostly) determine the 815 * regex specifying which websites the template applies to. 816 * 817 * @param[in,out] t template to update 818 * @param contract contract to parse 819 * @return true on success, false on failure 820 */ 821 static bool 822 parse_template (struct Template *t, 823 const json_t *contract) 824 { 825 /* An absent website_regex means "every URL". So does an empty one: 826 it is what a merchant UI stores for a template that was never 827 given a restriction, and anchoring it would yield "^()$", which 828 matches only the empty string and therefore no URL at all -- a 829 template that silently paywalls nothing. */ 830 const char *regex = NULL; 831 const char *summary = NULL; 832 const json_t *choices = NULL; 833 struct GNUNET_JSON_Specification spec[] = { 834 GNUNET_JSON_spec_mark_optional ( 835 GNUNET_JSON_spec_string ("website_regex", 836 ®ex), 837 NULL), 838 GNUNET_JSON_spec_mark_optional ( 839 GNUNET_JSON_spec_string ("summary", 840 &summary), 841 NULL), 842 GNUNET_JSON_spec_array_const ("choices", 843 &choices), 844 GNUNET_JSON_spec_mark_optional ( 845 GNUNET_JSON_spec_relative_time ("max_pickup_duration", 846 &t->max_pickup_delay), 847 NULL), 848 GNUNET_JSON_spec_end () 849 }; 850 const char *en; 851 852 if (GNUNET_OK != 853 GNUNET_JSON_parse ((json_t *) contract, 854 spec, 855 &en, 856 NULL)) 857 { 858 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 859 "Invalid template %s at field %s\n", 860 t->template_id, 861 en); 862 return false; 863 } 864 /* GNUNET_JSON_spec_array_const() only establishes that "choices" is 865 an array; what is *in* it is whatever the backend sent. 866 load_paywall() hands element 0 to 867 GNUNET_JSON_pack_object_incref(), which aborts on anything that is 868 not an object — and it does so from an unauthenticated GET of the 869 paywall page, so a backend that disagrees with us about the schema 870 would turn every visitor into a crash loop. We cross a network 871 trust boundary here and must not assert on what comes back. */ 872 { 873 size_t idx; 874 json_t *choice; 875 876 json_array_foreach ((json_t *) choices, idx, choice) 877 { 878 if (json_is_object (choice)) 879 continue; 880 GNUNET_break_op (0); 881 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 882 "Template %s has a non-object choice at index %u\n", 883 t->template_id, 884 (unsigned int) idx); 885 return false; 886 } 887 } 888 if ( (NULL != regex) && 889 ('\0' != regex[0]) ) 890 { 891 char *anchored; 892 regex_t bare; 893 int rc; 894 895 /* Compile the merchant's expression as it stands *before* wrapping 896 it, and refuse the template if that fails. Splicing into a 897 group is not a syntactic no-op: "a)|(b" becomes "^(a)|(b)$", 898 which is "^a" OR "b$" -- each anchored on one side only, so the 899 anchoring below stops being the guarantee the manual states. 900 Every such breakout needs a parenthesis that only balances 901 against the ones we add, which is exactly what a bare regcomp() 902 rejects. The merchant backend compiles the bare expression on 903 POST /private/templates for the same reason; this closes the gap 904 for a template that reached the database some other way. Note 905 that no expression that compiles on its own changes meaning 906 here. */ 907 rc = regcomp (&bare, 908 regex, 909 REG_NOSUB | REG_EXTENDED); 910 if (0 != rc) 911 { 912 GNUNET_break_op (0); 913 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 914 "Invalid regex in template %s: %s\n", 915 t->template_id, 916 regex); 917 return false; 918 } 919 regfree (&bare); 920 /* Anchor the merchant's expression: regexec(3) is unanchored, so 921 an expression like "/premium/" would otherwise put a paywall on 922 every URL merely *containing* it. Wrapping in a group keeps 923 alternations ("a|b") from binding the anchors to only the first 924 and last branch. An expression that already anchors itself is 925 unaffected, as ^ and $ inside still match at string 926 start/end. */ 927 GNUNET_asprintf (&anchored, 928 "^(%s)$", 929 regex); 930 rc = regcomp (&t->ex, 931 anchored, 932 REG_NOSUB | REG_EXTENDED); 933 GNUNET_free (anchored); 934 if (0 != rc) 935 { 936 GNUNET_break_op (0); 937 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 938 "Invalid regex in template %s: %s\n", 939 t->template_id, 940 regex); 941 return false; 942 } 943 t->regex = GNUNET_strdup (regex); 944 } 945 if (NULL != summary) 946 t->summary = GNUNET_strdup (summary); 947 t->choices = json_incref ((json_t *) choices); 948 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 949 "Using payment template %s for `%s'\n", 950 t->template_id, 951 (NULL != t->regex) ? t->regex : "(all URLs)"); 952 return true; 953 } 954 955 956 /** 957 * Is @a contract a template that is ours to serve? 958 * 959 * The merchant's `template_type' discriminator; the literals are 960 * TALER_MERCHANT_template_type_from_string(), which we would call if 961 * it did not live in a library paivana does not otherwise need. Note 962 * that an absent `template_type' is `fixed-order' there, so it is not 963 * ours either. 964 * 965 * @param contract template contract from the backend 966 * @return true if this is a paivana template 967 */ 968 static bool 969 is_paivana_template (const json_t *contract) 970 { 971 const json_t *tt; 972 973 tt = json_object_get (contract, 974 "template_type"); 975 return ( (NULL != tt) && 976 (json_is_string (tt)) && 977 (0 == strcmp ("paivana", 978 json_string_value (tt))) ); 979 } 980 981 982 /** 983 * Remove @a t from the list of templates and free it. 984 * 985 * Only for a template we decided not to use before anything was parsed 986 * into it: @e gt must already be NULL and nothing but the ID 987 * allocated. 988 * 989 * @param[in] t template to drop 990 */ 991 static void 992 drop_template (struct Template *t) 993 { 994 GNUNET_assert (NULL == t->gt); 995 GNUNET_assert (NULL == t->rce_head); 996 GNUNET_CONTAINER_DLL_remove (t_head, 997 t_tail, 998 t); 999 GNUNET_free (t->template_id); 1000 GNUNET_free (t); 1001 } 1002 1003 1004 /** 1005 * Callback for a GET /private/templates/$TEMPLATE_ID request. 1006 * 1007 * @param t template the request was about 1008 * @param tgr response details 1009 */ 1010 static void 1011 setup_template ( 1012 struct Template *t, 1013 const struct TALER_MERCHANT_GetPrivateTemplateResponse *tgr) 1014 { 1015 t->gt = NULL; 1016 switch (tgr->hr.http_status) 1017 { 1018 case MHD_HTTP_OK: 1019 if (! is_paivana_template (tgr->details.ok.template_contract)) 1020 { 1021 /* One merchant instance serves every kind of template, and 1022 `template_type' is what says which of them are ours -- as the 1023 manual promises. Without this check a fixed-order template is 1024 handed to parse_template(), which fails it for want of 1025 `choices' and reports "Invalid template X at field choices": 1026 an error about a template that is simply none of our business, 1027 and (since a parse failure is fatal) one that keeps paivana 1028 from starting at all next to a perfectly good paivana 1029 template. */ 1030 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 1031 "Ignoring template %s: not a paivana template\n", 1032 t->template_id); 1033 drop_template (t); 1034 break; 1035 } 1036 if (! parse_template (t, 1037 tgr->details.ok.template_contract)) 1038 { 1039 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 1040 "Failed to parse template %s, refusing to start\n", 1041 t->template_id); 1042 PH_global_ret = EXIT_FAILURE; 1043 GNUNET_SCHEDULER_shutdown (); 1044 return; 1045 } 1046 break; 1047 default: 1048 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 1049 "Failed to load template %s from backend" 1050 " (HTTP status %u), refusing to start\n", 1051 t->template_id, 1052 tgr->hr.http_status); 1053 PH_global_ret = EXIT_FAILURE; 1054 GNUNET_SCHEDULER_shutdown (); 1055 return; 1056 } 1057 for (struct Template *p = t_head; NULL != p; p = p->next) 1058 if (NULL != p->gt) 1059 return; 1060 /* all templates done, continue with main logic */ 1061 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 1062 "Templates loaded, starting to serve requests\n"); 1063 templates_ready (); 1064 } 1065 1066 1067 /** 1068 * Callback for a GET /private/templates request. 1069 * 1070 * @param cls unused 1071 * @param tgr response details 1072 */ 1073 static void 1074 check_templates ( 1075 void *cls, 1076 const struct TALER_MERCHANT_GetPrivateTemplatesResponse *tgr) 1077 { 1078 (void) cls; 1079 gpt = NULL; 1080 switch (tgr->hr.http_status) 1081 { 1082 case MHD_HTTP_OK: 1083 break; 1084 case MHD_HTTP_UNAUTHORIZED: 1085 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 1086 "Access to templates unauthorized: %s\n", 1087 TALER_ErrorCode_get_hint (tgr->hr.ec)); 1088 PH_global_ret = EXIT_FAILURE; 1089 GNUNET_SCHEDULER_shutdown (); 1090 return; 1091 default: 1092 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 1093 "Unexpected HTTP status code %u on GET /private/templates (%d)\n", 1094 tgr->hr.http_status, 1095 (int) tgr->hr.ec); 1096 PH_global_ret = EXIT_FAILURE; 1097 GNUNET_SCHEDULER_shutdown (); 1098 return; 1099 } 1100 if (0 == tgr->details.ok.templates_length) 1101 { 1102 templates_ready (); 1103 return; 1104 } 1105 1106 for (unsigned int i = 0; i<tgr->details.ok.templates_length; i++) 1107 { 1108 const struct TALER_MERCHANT_GetPrivateTemplatesTemplateEntry *te 1109 = &tgr->details.ok.templates[i]; 1110 struct Template *t; 1111 struct Template *before; 1112 1113 t = GNUNET_new (struct Template); 1114 t->template_id = GNUNET_strdup (te->template_id); 1115 t->max_pickup_delay = GNUNET_TIME_UNIT_FOREVER_REL; 1116 t->gt = TALER_MERCHANT_get_private_template_create (PH_merchant_ctx, 1117 PH_merchant_base_url, 1118 t->template_id); 1119 /* Insert sorted by template ID. Where two expressions both match a 1120 URL, PAIVANA_HTTPD_search_templates() quotes the price of 1121 whichever template it reaches first, so this list's order is a 1122 pricing decision. The backend's array arrives in whatever order 1123 the database returned it -- the SELECT behind 1124 GET /private/templates has no ORDER BY -- so taking it as given 1125 (in either direction) makes that decision depend on nothing an 1126 operator can see or set, and it can differ between two restarts 1127 with no configuration change. Sorting by ID is an arbitrary 1128 rule, but it is a rule, and it is one an operator can act on. */ 1129 before = t_head; 1130 while ( (NULL != before) && 1131 (0 > strcmp (before->template_id, 1132 t->template_id)) ) 1133 before = before->next; 1134 GNUNET_CONTAINER_DLL_insert_before (t_head, 1135 t_tail, 1136 before, 1137 t); 1138 GNUNET_assert ( 1139 TALER_EC_NONE == 1140 TALER_MERCHANT_get_private_template_start (t->gt, 1141 &setup_template, 1142 t)); 1143 } 1144 } 1145 1146 1147 void 1148 PAIVANA_HTTPD_load_templates () 1149 { 1150 GNUNET_assert (NULL == load_timeout_task); 1151 load_timeout_task 1152 = GNUNET_SCHEDULER_add_delayed (TEMPLATE_LOAD_TIMEOUT, 1153 &load_timeout, 1154 NULL); 1155 gpt = TALER_MERCHANT_get_private_templates_create (PH_merchant_ctx, 1156 PH_merchant_base_url); 1157 GNUNET_assert (NULL != gpt); 1158 GNUNET_assert ( 1159 TALER_EC_NONE == 1160 TALER_MERCHANT_get_private_templates_start (gpt, 1161 &check_templates, 1162 NULL)); 1163 } 1164 1165 1166 enum GNUNET_GenericReturnValue 1167 PAIVANA_HTTPD_search_templates (struct MHD_Connection *connection, 1168 const char *website) 1169 { 1170 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 1171 "Searching templates for `%s'\n", 1172 website); 1173 if (PH_MAX_URL_LENGTH < strlen (website)) 1174 { 1175 enum MHD_Result ret; 1176 1177 /* Refuse rather than returning #GNUNET_SYSERR: that would mean 1178 "no paywall applies" and hand the request to the upstream for 1179 free. */ 1180 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 1181 "Refusing to match templates against %llu byte URL\n", 1182 (unsigned long long) strlen (website)); 1183 ret = TALER_MHD_reply_with_error ( 1184 connection, 1185 MHD_HTTP_URI_TOO_LONG, 1186 TALER_EC_GENERIC_URI_TOO_LONG, 1187 NULL); 1188 return (MHD_YES == ret) ? GNUNET_OK : GNUNET_NO; 1189 } 1190 for (struct Template *t = t_head; NULL != t; t = t->next) 1191 { 1192 struct MHD_Response *redirect; 1193 enum MHD_Result ret; 1194 struct GNUNET_Buffer buf = { 0 }; 1195 char *enc = NULL; 1196 char *url; 1197 1198 if (NULL != t->regex) 1199 { 1200 int rc; 1201 1202 rc = regexec (&t->ex, 1203 website, 1204 0, NULL, 1205 0); 1206 if (REG_NOMATCH == rc) 1207 { 1208 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 1209 "Request for %s did not match template %s\n", 1210 website, 1211 t->template_id); 1212 continue; 1213 } 1214 if (0 != rc) 1215 { 1216 char errbuf[128]; 1217 1218 /* Not "did not match": regexec(3) also reports REG_ESPACE, 1219 whose likelihood is a function of the merchant's pattern and 1220 the client's URL. Taking the `continue' would drop this 1221 template from consideration, and if it were the last one the 1222 caller reads #GNUNET_SYSERR as "no paywall applies" and 1223 serves the page for free. Fail closed instead. */ 1224 GNUNET_break (0); 1225 (void) regerror (rc, 1226 &t->ex, 1227 errbuf, 1228 sizeof (errbuf)); 1229 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 1230 "Failed to match template %s against %s: %s\n", 1231 t->template_id, 1232 website, 1233 errbuf); 1234 ret = TALER_MHD_reply_with_error ( 1235 connection, 1236 MHD_HTTP_INTERNAL_SERVER_ERROR, 1237 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 1238 errbuf); 1239 return (MHD_YES == ret) ? GNUNET_OK : GNUNET_NO; 1240 } 1241 } 1242 1243 if (! PAIVANA_HTTPD_get_base_url (connection, 1244 &buf)) 1245 { 1246 GNUNET_break (0); 1247 GNUNET_buffer_clear (&buf); 1248 ret = TALER_MHD_reply_with_error ( 1249 connection, 1250 MHD_HTTP_BAD_REQUEST, 1251 TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, 1252 "Host or X-Forwarded-Host required"); 1253 return (MHD_YES == ret) ? GNUNET_OK : GNUNET_NO; 1254 } 1255 (void) GNUNET_STRINGS_base64url_encode (website, 1256 strlen (website), 1257 &enc); 1258 GNUNET_buffer_write_str (&buf, 1259 "/.well-known/paivana/templates/"); 1260 GNUNET_buffer_write_str (&buf, 1261 t->template_id); 1262 GNUNET_buffer_write_str (&buf, 1263 "#"); 1264 GNUNET_buffer_write_str (&buf, 1265 enc); 1266 GNUNET_free (enc); 1267 url = GNUNET_buffer_reap_str (&buf); 1268 redirect = MHD_create_response_from_buffer_static (0, 1269 NULL); 1270 GNUNET_assert (NULL != redirect); 1271 GNUNET_break (MHD_YES == 1272 MHD_add_response_header (redirect, 1273 MHD_HTTP_HEADER_LOCATION, 1274 url)); 1275 /* Both the Location and the fragment are built from the base URL, 1276 which -- unless BASE_URL pins it -- comes out of the forwarding 1277 headers. A cache sitting between the terminating proxy and us 1278 would otherwise serve one client's redirect to another virtual 1279 host (RFC 9110 section 12.5.5). */ 1280 GNUNET_break (MHD_YES == 1281 MHD_add_response_header (redirect, 1282 MHD_HTTP_HEADER_VARY, 1283 (NULL != PH_base_url) 1284 ? "Cookie" 1285 : "Cookie, " 1286 MHD_HTTP_HEADER_FORWARDED ", " 1287 PH_HEADER_X_FORWARDED_PROTO ", " 1288 PH_HEADER_X_FORWARDED_HOST ", " 1289 PH_HEADER_X_FORWARDED_PORT)); 1290 GNUNET_break (MHD_YES == 1291 MHD_add_response_header (redirect, 1292 MHD_HTTP_HEADER_CACHE_CONTROL, 1293 "public, max-age=60")); 1294 GNUNET_free (url); 1295 ret = MHD_queue_response (connection, 1296 MHD_HTTP_FOUND, 1297 redirect); 1298 MHD_destroy_response (redirect); 1299 return (MHD_YES == ret) ? GNUNET_OK : GNUNET_NO; 1300 } 1301 return GNUNET_SYSERR; 1302 } 1303 1304 1305 /** 1306 * Return the paywall page for the given @a template. 1307 * 1308 * @param connection request to search paywall response for 1309 * @param template template to return paywall page for 1310 * @return MHD status code 1311 */ 1312 enum MHD_Result 1313 PAIVANA_HTTPD_return_template (struct MHD_Connection *connection, 1314 const char *template) 1315 { 1316 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 1317 "Searching template `%s'\n", 1318 template); 1319 for (struct Template *t = t_head; NULL != t; t = t->next) 1320 { 1321 if (0 == strcmp (template, 1322 t->template_id)) 1323 return load_paywall (connection, 1324 t); 1325 } 1326 /* No GNUNET_break_op() here: the ID is whatever the client put in the 1327 path, this runs before any payment, and a stale bookmark or a 1328 crawler would otherwise write an ERROR-level "Assertion failed" per 1329 request. A 404 is the whole answer. */ 1330 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 1331 "No template `%s', returning 404\n", 1332 template); 1333 return TALER_MHD_reply_with_error (connection, 1334 MHD_HTTP_NOT_FOUND, 1335 TALER_EC_PAIVANA_TEMPLATE_UNKNOWN, 1336 template); 1337 } 1338 1339 1340 /** 1341 * Unload all of the template state. 1342 */ 1343 void 1344 PAIVANA_HTTPD_unload_templates () 1345 { 1346 if (NULL != load_timeout_task) 1347 { 1348 GNUNET_SCHEDULER_cancel (load_timeout_task); 1349 load_timeout_task = NULL; 1350 } 1351 while (NULL != t_head) 1352 { 1353 struct Template *t = t_head; 1354 1355 while (NULL != t->rce_head) 1356 { 1357 struct ResponseCacheEntry *rce = t->rce_head; 1358 1359 GNUNET_assert (t->rce_length > 0); 1360 t->rce_length--; 1361 GNUNET_CONTAINER_DLL_remove (t->rce_head, 1362 t->rce_tail, 1363 rce); 1364 MHD_destroy_response (rce->paywall); 1365 GNUNET_free (rce->lang); 1366 GNUNET_free (rce); 1367 } 1368 GNUNET_CONTAINER_DLL_remove (t_head, 1369 t_tail, 1370 t); 1371 if (NULL != t->gt) 1372 TALER_MERCHANT_get_private_template_cancel (t->gt); 1373 if (NULL != t->regex) 1374 { 1375 regfree (&t->ex); 1376 GNUNET_free (t->regex); 1377 } 1378 GNUNET_free (t->template_id); 1379 GNUNET_free (t->summary); 1380 json_decref (t->choices); 1381 GNUNET_free (t); 1382 } 1383 if (NULL != gpt) 1384 { 1385 TALER_MERCHANT_get_private_templates_cancel (gpt); 1386 gpt = NULL; 1387 } 1388 GNUNET_free (paywall_csp); 1389 }