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