paivana-httpd_templates.c (19333B)
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_array_incref ( 288 "choices", 289 t->choices), 290 GNUNET_JSON_pack_bool ( 291 "has_choices", 292 1 < json_array_size(t->choices)), 293 GNUNET_JSON_pack_object_steal ( 294 "default_choice", 295 json_array_get(t->choices, 0)), 296 GNUNET_JSON_pack_uint64 ( 297 "max_pickup_delay", 298 t->max_pickup_delay.rel_value_us / 1000LLU / 1000LLU), 299 GNUNET_JSON_pack_string ( 300 "merchant_backend", 301 PH_merchant_base_url)); 302 ret = TALER_TEMPLATING_build ( 303 conn, 304 &http_status, 305 "paywall", 306 NULL /* no instance */, 307 NULL /* no Taler URI (needs dynamic paivana_id!) */, 308 data, 309 &reply); 310 if (GNUNET_OK != ret) 311 { 312 GNUNET_break (0); 313 json_decref (data); 314 return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; 315 } 316 json_decref (data); 317 } 318 319 320 GNUNET_break (MHD_YES == 321 MHD_add_response_header (reply, 322 MHD_HTTP_HEADER_CONTENT_TYPE, 323 "text/html")); 324 /* The paywall body depends on the negotiated language and on 325 whether we deflated it for the client; tell intermediaries to 326 key their cache entries on both. */ 327 GNUNET_break (MHD_YES == 328 MHD_add_response_header (reply, 329 MHD_HTTP_HEADER_VARY, 330 MHD_HTTP_HEADER_ACCEPT_LANGUAGE ", " 331 MHD_HTTP_HEADER_ACCEPT_ENCODING ", " 332 "Cookie")); 333 GNUNET_break (MHD_YES == 334 MHD_add_response_header (reply, 335 MHD_HTTP_HEADER_CACHE_CONTROL, 336 "public, max-age=300")); 337 { 338 char *uri; 339 340 uri = make_taler_pay_template_uri (PH_merchant_base_url, 341 t->template_id); 342 GNUNET_assert (MHD_YES == 343 MHD_add_response_header (reply, 344 "Paivana", 345 uri)); 346 GNUNET_free (uri); 347 } 348 349 { 350 struct ResponseCacheEntry *rce; 351 352 rce = GNUNET_new (struct ResponseCacheEntry); 353 if (NULL != lang) 354 rce->lang = GNUNET_strdup (lang); 355 if (NULL != ae) 356 rce->ae = GNUNET_strdup (ae); 357 rce->paywall = reply; 358 rce->http_status = http_status; 359 GNUNET_CONTAINER_DLL_insert (t->rce_head, 360 t->rce_tail, 361 rce); 362 return MHD_queue_response (conn, 363 rce->http_status, 364 reply); 365 } 366 } 367 368 369 /** 370 * Parse template contract to (mostly) determine the 371 * regex specifying which websites the template applies to. 372 * 373 * @param[in,out] t template to update 374 * @param contract contract to parse 375 */ 376 static void 377 parse_template (struct Template *t, 378 const json_t *contract) 379 { 380 const char *regex = NULL; 381 const char *summary = NULL; 382 const json_t *choices = NULL; 383 struct GNUNET_JSON_Specification spec[] = { 384 GNUNET_JSON_spec_mark_optional ( 385 GNUNET_JSON_spec_string ("website_regex", 386 ®ex), 387 NULL), 388 GNUNET_JSON_spec_mark_optional ( 389 GNUNET_JSON_spec_string ("summary", 390 &summary), 391 NULL), 392 GNUNET_JSON_spec_array_const ("choices", 393 &choices), 394 GNUNET_JSON_spec_mark_optional ( 395 GNUNET_JSON_spec_relative_time ("max_pickup_duration", 396 &t->max_pickup_delay), 397 NULL), 398 GNUNET_JSON_spec_end () 399 }; 400 const char *en; 401 402 if (GNUNET_OK != 403 GNUNET_JSON_parse ((json_t *) contract, 404 spec, 405 &en, 406 NULL)) 407 { 408 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 409 "Invalid template %s at field %s\n", 410 t->template_id, 411 en); 412 return; 413 } 414 if (NULL != regex) 415 { 416 if (0 != regcomp (&t->ex, 417 regex, 418 REG_NOSUB | REG_EXTENDED)) 419 { 420 GNUNET_break_op (0); 421 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 422 "Invalid regex in template %s: %s\n", 423 t->template_id, 424 regex); 425 return; 426 } 427 t->regex = GNUNET_strdup (regex); 428 } 429 if (NULL != summary) 430 t->summary = GNUNET_strdup (summary); 431 t->choices = json_incref ((json_t *) choices); 432 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 433 "Using payment template %s for `%s'\n", 434 t->template_id, 435 regex); 436 } 437 438 439 /** 440 * Callback for a GET /private/templates/$TEMPLATE_ID request. 441 * 442 * @param cls closure 443 * @param tgr response details 444 */ 445 static void 446 setup_template ( 447 struct Template *t, 448 const struct TALER_MERCHANT_GetPrivateTemplateResponse *tgr) 449 { 450 t->gt = NULL; 451 switch (tgr->hr.http_status) 452 { 453 case MHD_HTTP_OK: 454 parse_template (t, 455 tgr->details.ok.template_contract); 456 break; 457 default: 458 GNUNET_break (0); 459 break; 460 } 461 for (struct Template *p = t_head; NULL != p; p = p->next) 462 if (NULL != p->gt) 463 return; 464 /* all templates done, continue with main logic */ 465 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 466 "Templates loaded, starting to serve requests\n"); 467 PAIVANA_HTTPD_serve_requests (); 468 } 469 470 471 /** 472 * Callback for a GET /private/templates request. 473 * 474 * @param cls closure 475 * @param tgr response details 476 */ 477 static void 478 check_templates ( 479 void *cls, 480 const struct TALER_MERCHANT_GetPrivateTemplatesResponse *tgr) 481 { 482 gpt = NULL; 483 switch (tgr->hr.http_status) 484 { 485 case MHD_HTTP_OK: 486 break; 487 case MHD_HTTP_NO_CONTENT: 488 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 489 "No templates found, starting to serve requests\n"); 490 PAIVANA_HTTPD_serve_requests (); 491 return; 492 case MHD_HTTP_UNAUTHORIZED: 493 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 494 "Access to templates unauthorized: %s\n", 495 TALER_ErrorCode_get_hint (tgr->hr.ec)); 496 PH_global_ret = EXIT_FAILURE; 497 GNUNET_SCHEDULER_shutdown (); 498 return; 499 default: 500 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 501 "Unexpected HTTP status code %u on GET /private/templates (%d)\n", 502 tgr->hr.http_status, 503 (int) tgr->hr.ec); 504 PH_global_ret = EXIT_FAILURE; 505 GNUNET_SCHEDULER_shutdown (); 506 return; 507 } 508 if (0 == tgr->details.ok.templates_length) 509 { 510 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 511 "No templates found, starting to serve requests\n"); 512 PAIVANA_HTTPD_serve_requests (); 513 return; 514 } 515 516 for (unsigned int i = 0; i<tgr->details.ok.templates_length; i++) 517 { 518 const struct TALER_MERCHANT_GetPrivateTemplatesTemplateEntry *te 519 = &tgr->details.ok.templates[i]; 520 struct Template *t; 521 522 t = GNUNET_new (struct Template); 523 t->template_id = GNUNET_strdup (te->template_id); 524 t->max_pickup_delay = GNUNET_TIME_UNIT_FOREVER_REL; 525 t->gt = TALER_MERCHANT_get_private_template_create (PH_ctx, 526 PH_merchant_base_url, 527 t->template_id); 528 GNUNET_CONTAINER_DLL_insert (t_head, 529 t_tail, 530 t); 531 GNUNET_assert ( 532 TALER_EC_NONE == 533 TALER_MERCHANT_get_private_template_start (t->gt, 534 &setup_template, 535 t)); 536 } 537 } 538 539 540 void 541 PAIVANA_HTTPD_load_templates () 542 { 543 gpt = TALER_MERCHANT_get_private_templates_create (PH_ctx, 544 PH_merchant_base_url); 545 GNUNET_assert (NULL != gpt); 546 GNUNET_assert ( 547 TALER_EC_NONE == 548 TALER_MERCHANT_get_private_templates_start (gpt, 549 &check_templates, 550 NULL)); 551 } 552 553 554 enum GNUNET_GenericReturnValue 555 PAIVANA_HTTPD_search_templates (struct MHD_Connection *connection, 556 const char *website) 557 { 558 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 559 "Searching templates for `%s'\n", 560 website); 561 for (struct Template *t = t_head; NULL != t; t = t->next) 562 { 563 struct MHD_Response *redirect; 564 enum MHD_Result ret; 565 struct GNUNET_Buffer buf = { 0 }; 566 char *enc = NULL; 567 char *url; 568 569 if ( (NULL != t->regex) && 570 (0 != regexec (&t->ex, 571 website, 572 0, NULL, 573 0)) ) 574 { 575 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 576 "Request for %s did not match template %s\n", 577 website, 578 t->template_id); 579 continue; 580 } 581 582 redirect = MHD_create_response_from_buffer_static (0, 583 NULL); 584 if (! PAIVANA_HTTPD_get_base_url (connection, 585 &buf)) 586 { 587 GNUNET_break (0); 588 return TALER_MHD_reply_with_error ( 589 connection, 590 MHD_HTTP_BAD_REQUEST, 591 TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, 592 "Host or X-Forwarded-Host required"); 593 } 594 GNUNET_STRINGS_base64url_encode (website, 595 strlen (website), 596 &enc); 597 GNUNET_buffer_write_str (&buf, 598 "/.well-known/paivana/templates/"); 599 GNUNET_buffer_write_str (&buf, 600 t->template_id); 601 GNUNET_buffer_write_str (&buf, 602 "#"); 603 GNUNET_buffer_write_str (&buf, 604 enc); 605 GNUNET_free (enc); 606 url = GNUNET_buffer_reap_str (&buf); 607 GNUNET_break (MHD_YES == 608 MHD_add_response_header (redirect, 609 MHD_HTTP_HEADER_LOCATION, 610 url)); 611 GNUNET_break (MHD_YES == 612 MHD_add_response_header (redirect, 613 MHD_HTTP_HEADER_VARY, 614 "Cookie")); 615 GNUNET_break (MHD_YES == 616 MHD_add_response_header (redirect, 617 MHD_HTTP_HEADER_CACHE_CONTROL, 618 "public, max-age=60")); 619 GNUNET_free (url); 620 ret = MHD_queue_response (connection, 621 MHD_HTTP_FOUND, 622 redirect); 623 MHD_destroy_response (redirect); 624 return (MHD_YES == ret) ? GNUNET_OK : GNUNET_NO; 625 } 626 return GNUNET_SYSERR; 627 } 628 629 630 /** 631 * Return the paywall page for the given @a template. 632 * 633 * @param connection request to search paywall response for 634 * @param id template to return paywall template for 635 * @return MHD status code 636 */ 637 enum MHD_Result 638 PAIVANA_HTTPD_return_template (struct MHD_Connection *connection, 639 const char *template) 640 { 641 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 642 "Searching template `%s'\n", 643 template); 644 for (struct Template *t = t_head; NULL != t; t = t->next) 645 { 646 if (0 == strcmp (template, 647 t->template_id)) 648 return load_paywall (connection, 649 t); 650 } 651 GNUNET_break_op (0); 652 return TALER_MHD_reply_with_error (connection, 653 MHD_HTTP_NOT_FOUND, 654 TALER_EC_PAIVANA_TEMPLATE_UNKNOWN, 655 template); 656 } 657 658 659 /** 660 * Unload all of the template state. 661 */ 662 void 663 PAIVANA_HTTPD_unload_templates () 664 { 665 while (NULL != t_head) 666 { 667 struct Template *t = t_head; 668 669 while (NULL != t->rce_head) 670 { 671 struct ResponseCacheEntry *rce = t->rce_head; 672 673 GNUNET_CONTAINER_DLL_remove (t->rce_head, 674 t->rce_tail, 675 rce); 676 MHD_destroy_response (rce->paywall); 677 GNUNET_free (rce->ae); 678 GNUNET_free (rce->lang); 679 GNUNET_free (rce); 680 } 681 GNUNET_CONTAINER_DLL_remove (t_head, 682 t_tail, 683 t); 684 if (NULL != t->gt) 685 TALER_MERCHANT_get_private_template_cancel (t->gt); 686 if (NULL != t->regex) 687 { 688 regfree (&t->ex); 689 GNUNET_free (t->regex); 690 } 691 GNUNET_free (t->template_id); 692 GNUNET_free (t->summary); 693 json_decref (t->choices); 694 GNUNET_free (t); 695 } 696 if (NULL != gpt) 697 { 698 TALER_MERCHANT_get_private_templates_cancel (gpt); 699 gpt = NULL; 700 } 701 }