paivana-httpd_templates.c (20145B)
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 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 General Public License for more details. 14 15 You should have received a copy of the GNU 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 * Entry in the cache of responses for a given template. 49 */ 50 struct ResponseCacheEntry 51 { 52 53 /** 54 * Kept in a DLL. 55 */ 56 struct ResponseCacheEntry *next; 57 58 /** 59 * Kept in a DLL. 60 */ 61 struct ResponseCacheEntry *prev; 62 63 /** 64 * Language of the response. 65 */ 66 char *lang; 67 68 /** 69 * Accept-Encoding of the response. 70 */ 71 char *ae; 72 73 /** 74 * Paywall response for these request parameters. 75 */ 76 struct MHD_Response *paywall; 77 78 /** 79 * HTTP status to return with @e paywall. 80 */ 81 unsigned int http_status; 82 83 }; 84 85 86 /** 87 * Information about a template in the merchant backend. 88 */ 89 struct Template 90 { 91 92 /** 93 * Kept in a DLL. 94 */ 95 struct Template *next; 96 97 /** 98 * Kept in a DLL. 99 */ 100 struct Template *prev; 101 102 /** 103 * ID of the template. 104 */ 105 char *template_id; 106 107 /** 108 * Summary of the template, NULL if not given. 109 */ 110 char *summary; 111 112 /** 113 * Maximum pickup delay for the pages. 114 */ 115 struct GNUNET_TIME_Relative max_pickup_delay; 116 117 /** 118 * Ways how to pay for the template. 119 */ 120 json_t *choices; 121 122 /** 123 * Regular expression of websites the template is for. 124 */ 125 char *regex; 126 127 /** 128 * Pre-compiled regular expression @e regex. 129 */ 130 regex_t ex; 131 132 /** 133 * Handle used to request more information about the template. 134 */ 135 struct TALER_MERCHANT_GetPrivateTemplateHandle *gt; 136 137 /** 138 * Kept in a DLL. 139 */ 140 struct ResponseCacheEntry *rce_head; 141 142 /** 143 * Kept in a DLL. 144 */ 145 struct ResponseCacheEntry *rce_tail; 146 147 }; 148 149 150 /** 151 * Kept in a DLL. 152 */ 153 static struct Template *t_head; 154 155 /** 156 * Kept in a DLL. 157 */ 158 static struct Template *t_tail; 159 160 /** 161 * Handle to get all the templates. 162 */ 163 static struct TALER_MERCHANT_GetPrivateTemplatesHandle *gpt; 164 165 166 /** 167 * Check if two strings are equal, including both being NULL 168 * 169 * @param s1 a string, possibly NULL 170 * @param s2 a string. possibly NULL 171 * @return true if both are equal 172 */ 173 static bool 174 eq (const char *s1, 175 const char *s2) 176 { 177 if (s1 == s2) 178 return true; 179 if (NULL == s1) 180 return false; 181 if (NULL == s2) 182 return false; 183 return (0 == strcmp (s1, 184 s2)); 185 } 186 187 188 /** 189 * Create a taler://pay-template/ URI for the given @a con and @a template_id 190 * and @a instance_id. 191 * 192 * @param merchant_base_url URL to take host and path from; 193 * we cannot take it from the MHD connection as a browser 194 * may have changed 'http' to 'https' and we MUST be consistent 195 * with what the merchant's frontend used initially 196 * @param template_id the template id 197 * @return corresponding taler://pay-template/ URI, or NULL on missing "host" 198 */ 199 static char * 200 make_taler_pay_template_uri (const char *merchant_base_url, 201 const char *template_id) 202 { 203 struct GNUNET_Buffer buf = { 0 }; 204 char *url; 205 struct GNUNET_Uri uri; 206 207 url = GNUNET_strdup (merchant_base_url); 208 if (-1 == GNUNET_uri_parse (&uri, 209 url)) 210 { 211 GNUNET_break (0); 212 GNUNET_free (url); 213 return NULL; 214 } 215 GNUNET_assert (NULL != template_id); 216 GNUNET_buffer_write_str (&buf, 217 "taler"); 218 if (0 == strcasecmp ("http", 219 uri.scheme)) 220 GNUNET_buffer_write_str (&buf, 221 "+http"); 222 GNUNET_buffer_write_str (&buf, 223 "://pay-template/"); 224 GNUNET_buffer_write_str (&buf, 225 uri.host); 226 if (0 != uri.port) 227 GNUNET_buffer_write_fstr (&buf, 228 ":%u", 229 (unsigned int) uri.port); 230 if (NULL != uri.path) 231 GNUNET_buffer_write_path (&buf, 232 uri.path); 233 GNUNET_buffer_write_path (&buf, 234 template_id); 235 GNUNET_free (url); 236 return GNUNET_buffer_reap_str (&buf); 237 } 238 239 240 /** 241 * Try to initialize the paywall response. 242 * 243 * @param conn connection to create the response for 244 * @param t template template to create the response for 245 * @return MHD status code to return 246 */ 247 static enum MHD_Result 248 load_paywall (struct MHD_Connection *conn, 249 struct Template *t) 250 { 251 struct MHD_Response *reply; 252 const char *lang; 253 const char *ae; 254 unsigned int http_status = MHD_HTTP_PAYMENT_REQUIRED; 255 256 lang = MHD_lookup_connection_value (conn, 257 MHD_HEADER_KIND, 258 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 259 ae = MHD_lookup_connection_value (conn, 260 MHD_HEADER_KIND, 261 MHD_HTTP_HEADER_ACCEPT_ENCODING); 262 for (struct ResponseCacheEntry *pos = t->rce_head; 263 NULL != pos; 264 pos = pos->next) 265 { 266 if ( (eq (lang, 267 pos->lang)) && 268 (eq (ae, 269 pos->ae) ) ) 270 return MHD_queue_response (conn, 271 pos->http_status, 272 pos->paywall); 273 } 274 275 { 276 enum GNUNET_GenericReturnValue ret; 277 json_t *data; 278 279 data = GNUNET_JSON_PACK ( 280 GNUNET_JSON_pack_string ( 281 "template_id", 282 t->template_id), 283 GNUNET_JSON_pack_allow_null ( 284 GNUNET_JSON_pack_string ( 285 "summary", 286 t->summary)), 287 GNUNET_JSON_pack_allow_null ( 288 GNUNET_JSON_pack_array_incref ( 289 "choices", 290 t->choices)), 291 GNUNET_JSON_pack_bool ( 292 "has_choices", 293 1 < json_array_size (t->choices)), 294 GNUNET_JSON_pack_allow_null ( 295 GNUNET_JSON_pack_object_incref ( 296 "default_choice", 297 json_array_get (t->choices, 0))), 298 GNUNET_JSON_pack_uint64 ( 299 "max_pickup_delay", 300 t->max_pickup_delay.rel_value_us / 1000LLU / 1000LLU), 301 GNUNET_JSON_pack_string ( 302 "merchant_backend", 303 PH_merchant_base_url)); 304 ret = TALER_TEMPLATING_build ( 305 conn, 306 &http_status, 307 "paywall", 308 NULL /* no instance */, 309 NULL /* no Taler URI (needs dynamic paivana_id!) */, 310 data, 311 &reply); 312 if (GNUNET_OK != ret) 313 { 314 GNUNET_break (0); 315 json_decref (data); 316 return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; 317 } 318 json_decref (data); 319 } 320 321 322 GNUNET_break (MHD_YES == 323 MHD_add_response_header (reply, 324 MHD_HTTP_HEADER_CONTENT_TYPE, 325 "text/html")); 326 /* The paywall body depends on the negotiated language and on 327 whether we deflated it for the client; tell intermediaries to 328 key their cache entries on both. */ 329 GNUNET_break (MHD_YES == 330 MHD_add_response_header (reply, 331 MHD_HTTP_HEADER_VARY, 332 MHD_HTTP_HEADER_ACCEPT_LANGUAGE ", " 333 MHD_HTTP_HEADER_ACCEPT_ENCODING ", " 334 "Cookie")); 335 GNUNET_break (MHD_YES == 336 MHD_add_response_header (reply, 337 MHD_HTTP_HEADER_CACHE_CONTROL, 338 "public, max-age=300")); 339 { 340 char *uri; 341 342 uri = make_taler_pay_template_uri (PH_merchant_base_url, 343 t->template_id); 344 if (NULL != uri) 345 { 346 GNUNET_assert (MHD_YES == 347 MHD_add_response_header (reply, 348 "Paivana", 349 uri)); 350 GNUNET_free (uri); 351 } 352 } 353 354 { 355 struct ResponseCacheEntry *rce; 356 357 rce = GNUNET_new (struct ResponseCacheEntry); 358 if (NULL != lang) 359 rce->lang = GNUNET_strdup (lang); 360 if (NULL != ae) 361 rce->ae = GNUNET_strdup (ae); 362 rce->paywall = reply; 363 rce->http_status = http_status; 364 GNUNET_CONTAINER_DLL_insert (t->rce_head, 365 t->rce_tail, 366 rce); 367 return MHD_queue_response (conn, 368 rce->http_status, 369 reply); 370 } 371 } 372 373 374 /** 375 * Parse template contract to (mostly) determine the 376 * regex specifying which websites the template applies to. 377 * 378 * @param[in,out] t template to update 379 * @param contract contract to parse 380 * @return true on success, false on failure 381 */ 382 static bool 383 parse_template (struct Template *t, 384 const json_t *contract) 385 { 386 const char *regex = NULL; 387 const char *summary = NULL; 388 const json_t *choices = NULL; 389 struct GNUNET_JSON_Specification spec[] = { 390 GNUNET_JSON_spec_mark_optional ( 391 GNUNET_JSON_spec_string ("website_regex", 392 ®ex), 393 NULL), 394 GNUNET_JSON_spec_mark_optional ( 395 GNUNET_JSON_spec_string ("summary", 396 &summary), 397 NULL), 398 GNUNET_JSON_spec_array_const ("choices", 399 &choices), 400 GNUNET_JSON_spec_mark_optional ( 401 GNUNET_JSON_spec_relative_time ("max_pickup_duration", 402 &t->max_pickup_delay), 403 NULL), 404 GNUNET_JSON_spec_end () 405 }; 406 const char *en; 407 408 if (GNUNET_OK != 409 GNUNET_JSON_parse ((json_t *) contract, 410 spec, 411 &en, 412 NULL)) 413 { 414 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 415 "Invalid template %s at field %s\n", 416 t->template_id, 417 en); 418 return false; 419 } 420 if (NULL != regex) 421 { 422 if (0 != regcomp (&t->ex, 423 regex, 424 REG_NOSUB | REG_EXTENDED)) 425 { 426 GNUNET_break_op (0); 427 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 428 "Invalid regex in template %s: %s\n", 429 t->template_id, 430 regex); 431 return false; 432 } 433 t->regex = GNUNET_strdup (regex); 434 } 435 if (NULL != summary) 436 t->summary = GNUNET_strdup (summary); 437 t->choices = json_incref ((json_t *) choices); 438 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 439 "Using payment template %s for `%s'\n", 440 t->template_id, 441 regex); 442 return true; 443 } 444 445 446 /** 447 * Callback for a GET /private/templates/$TEMPLATE_ID request. 448 * 449 * @param cls closure 450 * @param tgr response details 451 */ 452 static void 453 setup_template ( 454 struct Template *t, 455 const struct TALER_MERCHANT_GetPrivateTemplateResponse *tgr) 456 { 457 t->gt = NULL; 458 switch (tgr->hr.http_status) 459 { 460 case MHD_HTTP_OK: 461 if (! parse_template (t, 462 tgr->details.ok.template_contract)) 463 { 464 GNUNET_free (t->template_id); 465 GNUNET_CONTAINER_DLL_remove (t_head, 466 t_tail, 467 t); 468 GNUNET_free (t); 469 } 470 break; 471 default: 472 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 473 "Failed to load template %s from backend\n", 474 t->template_id); 475 GNUNET_free (t->template_id); 476 GNUNET_CONTAINER_DLL_remove (t_head, 477 t_tail, 478 t); 479 GNUNET_free (t); 480 break; 481 } 482 for (struct Template *p = t_head; NULL != p; p = p->next) 483 if (NULL != p->gt) 484 return; 485 /* all templates done, continue with main logic */ 486 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 487 "Templates loaded, starting to serve requests\n"); 488 PAIVANA_HTTPD_serve_requests (); 489 } 490 491 492 /** 493 * Callback for a GET /private/templates request. 494 * 495 * @param cls closure 496 * @param tgr response details 497 */ 498 static void 499 check_templates ( 500 void *cls, 501 const struct TALER_MERCHANT_GetPrivateTemplatesResponse *tgr) 502 { 503 gpt = NULL; 504 switch (tgr->hr.http_status) 505 { 506 case MHD_HTTP_OK: 507 break; 508 case MHD_HTTP_NO_CONTENT: 509 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 510 "No templates found, starting to serve requests\n"); 511 PAIVANA_HTTPD_serve_requests (); 512 return; 513 case MHD_HTTP_UNAUTHORIZED: 514 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 515 "Access to templates unauthorized: %s\n", 516 TALER_ErrorCode_get_hint (tgr->hr.ec)); 517 PH_global_ret = EXIT_FAILURE; 518 GNUNET_SCHEDULER_shutdown (); 519 return; 520 default: 521 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 522 "Unexpected HTTP status code %u on GET /private/templates (%d)\n", 523 tgr->hr.http_status, 524 (int) tgr->hr.ec); 525 PH_global_ret = EXIT_FAILURE; 526 GNUNET_SCHEDULER_shutdown (); 527 return; 528 } 529 if (0 == tgr->details.ok.templates_length) 530 { 531 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 532 "No templates found, starting to serve requests\n"); 533 PAIVANA_HTTPD_serve_requests (); 534 return; 535 } 536 537 for (unsigned int i = 0; i<tgr->details.ok.templates_length; i++) 538 { 539 const struct TALER_MERCHANT_GetPrivateTemplatesTemplateEntry *te 540 = &tgr->details.ok.templates[i]; 541 struct Template *t; 542 543 t = GNUNET_new (struct Template); 544 t->template_id = GNUNET_strdup (te->template_id); 545 t->max_pickup_delay = GNUNET_TIME_UNIT_FOREVER_REL; 546 t->gt = TALER_MERCHANT_get_private_template_create (PH_ctx, 547 PH_merchant_base_url, 548 t->template_id); 549 GNUNET_CONTAINER_DLL_insert (t_head, 550 t_tail, 551 t); 552 GNUNET_assert ( 553 TALER_EC_NONE == 554 TALER_MERCHANT_get_private_template_start (t->gt, 555 &setup_template, 556 t)); 557 } 558 } 559 560 561 void 562 PAIVANA_HTTPD_load_templates () 563 { 564 gpt = TALER_MERCHANT_get_private_templates_create (PH_ctx, 565 PH_merchant_base_url); 566 GNUNET_assert (NULL != gpt); 567 GNUNET_assert ( 568 TALER_EC_NONE == 569 TALER_MERCHANT_get_private_templates_start (gpt, 570 &check_templates, 571 NULL)); 572 } 573 574 575 enum GNUNET_GenericReturnValue 576 PAIVANA_HTTPD_search_templates (struct MHD_Connection *connection, 577 const char *website) 578 { 579 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 580 "Searching templates for `%s'\n", 581 website); 582 for (struct Template *t = t_head; NULL != t; t = t->next) 583 { 584 struct MHD_Response *redirect; 585 enum MHD_Result ret; 586 struct GNUNET_Buffer buf = { 0 }; 587 char *enc = NULL; 588 char *url; 589 590 if ( (NULL != t->regex) && 591 (0 != regexec (&t->ex, 592 website, 593 0, NULL, 594 0)) ) 595 { 596 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 597 "Request for %s did not match template %s\n", 598 website, 599 t->template_id); 600 continue; 601 } 602 603 if (! PAIVANA_HTTPD_get_base_url (connection, 604 &buf)) 605 { 606 GNUNET_break (0); 607 ret = TALER_MHD_reply_with_error ( 608 connection, 609 MHD_HTTP_BAD_REQUEST, 610 TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, 611 "Host or X-Forwarded-Host required"); 612 return (MHD_YES == ret) ? GNUNET_OK : GNUNET_NO; 613 } 614 (void) GNUNET_STRINGS_base64url_encode (website, 615 strlen (website), 616 &enc); 617 GNUNET_buffer_write_str (&buf, 618 "/.well-known/paivana/templates/"); 619 GNUNET_buffer_write_str (&buf, 620 t->template_id); 621 GNUNET_buffer_write_str (&buf, 622 "#"); 623 GNUNET_buffer_write_str (&buf, 624 enc); 625 GNUNET_free (enc); 626 url = GNUNET_buffer_reap_str (&buf); 627 redirect = MHD_create_response_from_buffer_static (0, 628 NULL); 629 GNUNET_assert (NULL != redirect); 630 GNUNET_break (MHD_YES == 631 MHD_add_response_header (redirect, 632 MHD_HTTP_HEADER_LOCATION, 633 url)); 634 GNUNET_break (MHD_YES == 635 MHD_add_response_header (redirect, 636 MHD_HTTP_HEADER_VARY, 637 "Cookie")); 638 GNUNET_break (MHD_YES == 639 MHD_add_response_header (redirect, 640 MHD_HTTP_HEADER_CACHE_CONTROL, 641 "public, max-age=60")); 642 GNUNET_free (url); 643 ret = MHD_queue_response (connection, 644 MHD_HTTP_FOUND, 645 redirect); 646 MHD_destroy_response (redirect); 647 return (MHD_YES == ret) ? GNUNET_OK : GNUNET_NO; 648 } 649 return GNUNET_SYSERR; 650 } 651 652 653 /** 654 * Return the paywall page for the given @a template. 655 * 656 * @param connection request to search paywall response for 657 * @param id template to return paywall template for 658 * @return MHD status code 659 */ 660 enum MHD_Result 661 PAIVANA_HTTPD_return_template (struct MHD_Connection *connection, 662 const char *template) 663 { 664 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 665 "Searching template `%s'\n", 666 template); 667 for (struct Template *t = t_head; NULL != t; t = t->next) 668 { 669 if (0 == strcmp (template, 670 t->template_id)) 671 return load_paywall (connection, 672 t); 673 } 674 GNUNET_break_op (0); 675 return TALER_MHD_reply_with_error (connection, 676 MHD_HTTP_NOT_FOUND, 677 TALER_EC_PAIVANA_TEMPLATE_UNKNOWN, 678 template); 679 } 680 681 682 /** 683 * Unload all of the template state. 684 */ 685 void 686 PAIVANA_HTTPD_unload_templates () 687 { 688 while (NULL != t_head) 689 { 690 struct Template *t = t_head; 691 692 while (NULL != t->rce_head) 693 { 694 struct ResponseCacheEntry *rce = t->rce_head; 695 696 GNUNET_CONTAINER_DLL_remove (t->rce_head, 697 t->rce_tail, 698 rce); 699 MHD_destroy_response (rce->paywall); 700 GNUNET_free (rce->ae); 701 GNUNET_free (rce->lang); 702 GNUNET_free (rce); 703 } 704 GNUNET_CONTAINER_DLL_remove (t_head, 705 t_tail, 706 t); 707 if (NULL != t->gt) 708 TALER_MERCHANT_get_private_template_cancel (t->gt); 709 if (NULL != t->regex) 710 { 711 regfree (&t->ex); 712 GNUNET_free (t->regex); 713 } 714 GNUNET_free (t->template_id); 715 GNUNET_free (t->summary); 716 json_decref (t->choices); 717 GNUNET_free (t); 718 } 719 if (NULL != gpt) 720 { 721 TALER_MERCHANT_get_private_templates_cancel (gpt); 722 gpt = NULL; 723 } 724 }