mhd_legal.c (21093B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2019, 2020, 2022, 2024 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file mhd_legal.c 18 * @brief API for returning legal documents based on client language 19 * and content type preferences 20 * @author Christian Grothoff 21 */ 22 #include "platform.h" 23 #include <gnunet/gnunet_util_lib.h> 24 #include <gnunet/gnunet_json_lib.h> 25 #include <jansson.h> 26 #include <microhttpd.h> 27 #include "taler/taler_util.h" 28 #include "taler/taler_mhd_lib.h" 29 30 /** 31 * How long should browsers/proxies cache the "legal" replies? 32 */ 33 #define MAX_TERMS_CACHING GNUNET_TIME_UNIT_DAYS 34 35 /** 36 * HTTP header with the version of the terms of service. 37 */ 38 #define TALER_TERMS_VERSION "Taler-Terms-Version" 39 40 /** 41 * Entry in the terms-of-service array. 42 */ 43 struct Terms 44 { 45 /** 46 * Kept in a DLL. 47 */ 48 struct Terms *prev; 49 50 /** 51 * Kept in a DLL. 52 */ 53 struct Terms *next; 54 55 /** 56 * Mime type of the terms. 57 */ 58 const char *mime_type; 59 60 /** 61 * The terms (NOT 0-terminated!), mmap()'ed. Do not free, 62 * use munmap() instead. 63 */ 64 void *terms; 65 66 /** 67 * The desired language. 68 */ 69 char *language; 70 71 /** 72 * deflated @e terms, to return if client supports deflate compression. 73 * malloc()'ed. NULL if @e terms does not compress. 74 */ 75 void *compressed_terms; 76 77 /** 78 * Etag we use for this response. 79 */ 80 char *terms_etag; 81 82 /** 83 * Number of bytes in @e terms. 84 */ 85 size_t terms_size; 86 87 /** 88 * Number of bytes in @e compressed_terms. 89 */ 90 size_t compressed_terms_size; 91 92 /** 93 * Sorting key by format preference in case 94 * everything else is equal. Higher is preferred. 95 */ 96 unsigned int priority; 97 98 }; 99 100 101 /** 102 * Prepared responses for legal documents 103 * (terms of service, privacy policy). 104 */ 105 struct TALER_MHD_Legal 106 { 107 /** 108 * DLL of terms of service. 109 */ 110 struct Terms *terms_head; 111 112 /** 113 * DLL of terms of service. 114 */ 115 struct Terms *terms_tail; 116 117 /** 118 * Etag to use for the terms of service (= version). 119 */ 120 char *terms_version; 121 }; 122 123 124 /** 125 * Check if the comma-separated list @a langs already contains 126 * the entry @a lang. 127 * 128 * @param langs comma-separated list of languages 129 * @param lang language to look for 130 * @return true if @a lang is an entry of @a langs 131 */ 132 static bool 133 lang_in_list (const char *langs, 134 const char *lang) 135 { 136 size_t len = strlen (lang); 137 const char *pos = langs; 138 139 while (NULL != pos) 140 { 141 const char *end = strchr (pos, 142 ','); 143 size_t elen = (NULL == end) 144 ? strlen (pos) 145 : (size_t) (end - pos); 146 147 if ( (elen == len) && 148 (0 == strncasecmp (pos, 149 lang, 150 len)) ) 151 return true; 152 pos = (NULL == end) 153 ? NULL 154 : end + 1; 155 } 156 return false; 157 } 158 159 160 enum MHD_Result 161 TALER_MHD_reply_legal (struct MHD_Connection *conn, 162 struct TALER_MHD_Legal *legal) 163 { 164 /* Default terms of service if none are configured */ 165 static struct Terms none = { 166 .mime_type = "text/plain", 167 .terms = (void *) "not configured", 168 .language = (void *) "en", 169 .terms_size = strlen ("not configured") 170 }; 171 struct MHD_Response *resp; 172 struct Terms *t; 173 struct GNUNET_TIME_Absolute a; 174 struct GNUNET_TIME_Timestamp m; 175 char dat[128]; 176 char *langs; 177 178 t = NULL; 179 langs = NULL; 180 181 a = GNUNET_TIME_relative_to_absolute (MAX_TERMS_CACHING); 182 /* Round up to next full day to ensure the expiration 183 time does not become a fingerprint! */ 184 a = GNUNET_TIME_absolute_round_down (a, 185 MAX_TERMS_CACHING); 186 a = GNUNET_TIME_absolute_add (a, 187 MAX_TERMS_CACHING); 188 m = GNUNET_TIME_absolute_to_timestamp (a); 189 TALER_MHD_get_date_string (m.abs_time, 190 dat); 191 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 192 "Setting '%s' header to '%s'\n", 193 MHD_HTTP_HEADER_EXPIRES, 194 dat); 195 if (NULL == legal) 196 { 197 t = &none; 198 goto return_t; 199 } 200 201 if (NULL != legal) 202 { 203 const char *mime; 204 const char *lang; 205 double best_mime_q = 0.0; 206 double best_lang_q = 0.0; 207 208 mime = MHD_lookup_connection_value (conn, 209 MHD_HEADER_KIND, 210 MHD_HTTP_HEADER_ACCEPT); 211 if (NULL == mime) 212 mime = "text/plain"; 213 lang = MHD_lookup_connection_value (conn, 214 MHD_HEADER_KIND, 215 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 216 if (NULL == lang) 217 lang = "en"; 218 /* Find best match: must match mime type (if possible), and if 219 mime type matches, ideally also language */ 220 for (struct Terms *p = legal->terms_head; 221 NULL != p; 222 p = p->next) 223 { 224 double q; 225 226 q = TALER_pattern_matches (mime, 227 p->mime_type); 228 if (q > best_mime_q) 229 best_mime_q = q; 230 } 231 for (struct Terms *p = legal->terms_head; 232 NULL != p; 233 p = p->next) 234 { 235 double q; 236 237 q = TALER_pattern_matches (mime, 238 p->mime_type); 239 if (q < best_mime_q) 240 continue; 241 q = TALER_pattern_matches (lang, 242 p->language); 243 /* create 'available-languages' (for this mime-type) */ 244 if (NULL == langs) 245 { 246 langs = GNUNET_strdup (p->language); 247 } 248 else if (! lang_in_list (langs, 249 p->language)) 250 { 251 char *tmp = langs; 252 253 GNUNET_asprintf (&langs, 254 "%s,%s", 255 tmp, 256 p->language); 257 GNUNET_free (tmp); 258 } 259 if (NULL != t) 260 { 261 if (q < best_lang_q) 262 continue; 263 /* Client expressed no preference between these two formats, 264 so fall back to our own ranking instead of letting the 265 last entry in the list win. */ 266 if ( (q <= best_lang_q) && 267 (p->priority <= t->priority) ) 268 continue; 269 } 270 best_lang_q = q; 271 t = p; 272 } 273 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 274 "Best match for %s/%s: %s / %s\n", 275 lang, 276 mime, 277 (NULL != t) ? t->mime_type : "<none>", 278 (NULL != t) ? t->language : "<none>"); 279 } 280 281 if (NULL != t) 282 { 283 const char *etag; 284 285 etag = MHD_lookup_connection_value (conn, 286 MHD_HEADER_KIND, 287 MHD_HTTP_HEADER_IF_NONE_MATCH); 288 if ( (NULL != etag) && 289 (NULL != t->terms_etag) && 290 (0 == strcasecmp (etag, 291 t->terms_etag)) ) 292 { 293 enum MHD_Result ret; 294 295 resp = MHD_create_response_from_buffer (0, 296 NULL, 297 MHD_RESPMEM_PERSISTENT); 298 TALER_MHD_add_global_headers (resp, 299 true); 300 GNUNET_break (MHD_YES == 301 MHD_add_response_header (resp, 302 MHD_HTTP_HEADER_EXPIRES, 303 dat)); 304 GNUNET_break (MHD_YES == 305 MHD_add_response_header (resp, 306 MHD_HTTP_HEADER_ETAG, 307 t->terms_etag)); 308 if (NULL != legal) 309 GNUNET_break (MHD_YES == 310 MHD_add_response_header (resp, 311 TALER_TERMS_VERSION, 312 legal->terms_version)); 313 ret = MHD_queue_response (conn, 314 MHD_HTTP_NOT_MODIFIED, 315 resp); 316 GNUNET_break (MHD_YES == ret); 317 MHD_destroy_response (resp); 318 GNUNET_free (langs); 319 return ret; 320 } 321 } 322 323 if (NULL == t) 324 t = &none; /* 501 if not configured */ 325 326 return_t: 327 /* try to compress the response */ 328 resp = NULL; 329 if ( (TALER_MHD_CT_DEFLATE == 330 TALER_MHD_can_compress (conn, 331 TALER_MHD_CT_DEFLATE)) && 332 (NULL != t->compressed_terms) ) 333 { 334 resp = MHD_create_response_from_buffer (t->compressed_terms_size, 335 t->compressed_terms, 336 MHD_RESPMEM_PERSISTENT); 337 if (MHD_NO == 338 MHD_add_response_header (resp, 339 MHD_HTTP_HEADER_CONTENT_ENCODING, 340 "deflate")) 341 { 342 GNUNET_break (0); 343 MHD_destroy_response (resp); 344 resp = NULL; 345 } 346 } 347 if (NULL == resp) 348 { 349 /* could not generate compressed response, return uncompressed */ 350 resp = MHD_create_response_from_buffer (t->terms_size, 351 (void *) t->terms, 352 MHD_RESPMEM_PERSISTENT); 353 } 354 TALER_MHD_add_global_headers (resp, 355 true); 356 GNUNET_break (MHD_YES == 357 MHD_add_response_header (resp, 358 MHD_HTTP_HEADER_EXPIRES, 359 dat)); 360 if (NULL != langs) 361 { 362 GNUNET_break (MHD_YES == 363 MHD_add_response_header (resp, 364 "Avail-Languages", 365 langs)); 366 GNUNET_free (langs); 367 } 368 /* Set cache control headers: our response varies depending on these headers */ 369 GNUNET_break (MHD_YES == 370 MHD_add_response_header (resp, 371 MHD_HTTP_HEADER_VARY, 372 MHD_HTTP_HEADER_ACCEPT_LANGUAGE "," 373 MHD_HTTP_HEADER_ACCEPT "," 374 MHD_HTTP_HEADER_ACCEPT_ENCODING)); 375 /* Information is always public, revalidate after 10 days */ 376 GNUNET_break (MHD_YES == 377 MHD_add_response_header (resp, 378 MHD_HTTP_HEADER_CACHE_CONTROL, 379 "public,max-age=864000")); 380 if (NULL != t->terms_etag) 381 GNUNET_break (MHD_YES == 382 MHD_add_response_header (resp, 383 MHD_HTTP_HEADER_ETAG, 384 t->terms_etag)); 385 if (NULL != legal) 386 GNUNET_break (MHD_YES == 387 MHD_add_response_header (resp, 388 TALER_TERMS_VERSION, 389 legal->terms_version)); 390 GNUNET_break (MHD_YES == 391 MHD_add_response_header (resp, 392 MHD_HTTP_HEADER_CONTENT_TYPE, 393 t->mime_type)); 394 GNUNET_break (MHD_YES == 395 MHD_add_response_header (resp, 396 MHD_HTTP_HEADER_CONTENT_LANGUAGE, 397 t->language)); 398 { 399 enum MHD_Result ret; 400 401 ret = MHD_queue_response (conn, 402 t == &none 403 ? MHD_HTTP_NOT_IMPLEMENTED 404 : MHD_HTTP_OK, 405 resp); 406 MHD_destroy_response (resp); 407 return ret; 408 } 409 } 410 411 412 /** 413 * Load all the terms of service from @a path under language @a lang 414 * from file @a name 415 * 416 * @param[in,out] legal where to write the result 417 * @param path where the terms are found 418 * @param lang which language directory to crawl 419 * @param name specific file to access 420 */ 421 static void 422 load_terms (struct TALER_MHD_Legal *legal, 423 const char *path, 424 const char *lang, 425 const char *name) 426 { 427 static struct MimeMap 428 { 429 const char *ext; 430 const char *mime; 431 unsigned int priority; 432 } mm[] = { 433 { .ext = ".txt", .mime = "text/plain", .priority = 150 }, 434 { .ext = ".html", .mime = "text/html", .priority = 100 }, 435 { .ext = ".htm", .mime = "text/html", .priority = 99 }, 436 { .ext = ".md", .mime = "text/markdown", .priority = 50 }, 437 { .ext = ".pdf", .mime = "application/pdf", .priority = 25 }, 438 { .ext = ".jpg", .mime = "image/jpeg" }, 439 { .ext = ".jpeg", .mime = "image/jpeg" }, 440 { .ext = ".png", .mime = "image/png" }, 441 { .ext = ".gif", .mime = "image/gif" }, 442 { .ext = ".epub", .mime = "application/epub+zip", .priority = 10 }, 443 { .ext = ".xml", .mime = "text/xml", .priority = 10 }, 444 { .ext = NULL, .mime = NULL } 445 }; 446 const char *ext = strrchr (name, '.'); 447 const char *mime; 448 unsigned int priority; 449 450 if (NULL == ext) 451 { 452 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 453 "Unsupported file `%s' in directory `%s/%s': lacks extension\n", 454 name, 455 path, 456 lang); 457 return; 458 } 459 if ( (NULL == legal->terms_version) || 460 (0 != strncmp (legal->terms_version, 461 name, 462 ext - name)) ) 463 { 464 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 465 "Filename `%s' does not match Etag `%s' in directory `%s/%s'. Ignoring it.\n", 466 name, 467 legal->terms_version, 468 path, 469 lang); 470 return; 471 } 472 mime = NULL; 473 for (unsigned int i = 0; NULL != mm[i].ext; i++) 474 if (0 == strcasecmp (mm[i].ext, 475 ext)) 476 { 477 mime = mm[i].mime; 478 priority = mm[i].priority; 479 break; 480 } 481 if (NULL == mime) 482 { 483 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 484 "Unsupported file extension `%s' of file `%s' in directory `%s/%s'\n", 485 ext, 486 name, 487 path, 488 lang); 489 return; 490 } 491 /* try to read the file with the terms of service */ 492 { 493 struct stat st; 494 char *fn; 495 int fd; 496 497 GNUNET_asprintf (&fn, 498 "%s/%s/%s", 499 path, 500 lang, 501 name); 502 fd = open (fn, O_RDONLY); 503 if (-1 == fd) 504 { 505 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, 506 "open", 507 fn); 508 GNUNET_free (fn); 509 return; 510 } 511 if (0 != fstat (fd, &st)) 512 { 513 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, 514 "fstat", 515 fn); 516 GNUNET_break (0 == close (fd)); 517 GNUNET_free (fn); 518 return; 519 } 520 if (SIZE_MAX < ((unsigned long long) st.st_size)) 521 { 522 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, 523 "fstat-size", 524 fn); 525 GNUNET_break (0 == close (fd)); 526 GNUNET_free (fn); 527 return; 528 } 529 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 530 "Loading legal information from file `%s'\n", 531 fn); 532 { 533 void *buf; 534 size_t bsize; 535 536 bsize = (size_t) st.st_size; 537 buf = mmap (NULL, 538 bsize, 539 PROT_READ, 540 MAP_SHARED, 541 fd, 542 0); 543 if (MAP_FAILED == buf) 544 { 545 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, 546 "mmap", 547 fn); 548 GNUNET_break (0 == close (fd)); 549 GNUNET_free (fn); 550 return; 551 } 552 GNUNET_break (0 == close (fd)); 553 GNUNET_free (fn); 554 555 /* insert into global list of terms of service */ 556 { 557 struct Terms *t; 558 struct GNUNET_HashCode hc; 559 560 GNUNET_CRYPTO_hash (buf, 561 bsize, 562 &hc); 563 t = GNUNET_new (struct Terms); 564 t->mime_type = mime; 565 t->terms = buf; 566 t->language = GNUNET_strdup (lang); 567 t->terms_size = bsize; 568 t->priority = priority; 569 t->terms_etag 570 = GNUNET_STRINGS_data_to_string_alloc (&hc, 571 sizeof (hc) / 2); 572 buf = GNUNET_memdup (t->terms, 573 t->terms_size); 574 if (TALER_MHD_body_compress (&buf, 575 &bsize)) 576 { 577 t->compressed_terms = buf; 578 t->compressed_terms_size = bsize; 579 } 580 else 581 { 582 GNUNET_free (buf); 583 } 584 { 585 struct Terms *prev = NULL; 586 587 for (struct Terms *pos = legal->terms_head; 588 NULL != pos; 589 pos = pos->next) 590 { 591 if (pos->priority < priority) 592 break; 593 prev = pos; 594 } 595 GNUNET_CONTAINER_DLL_insert_after (legal->terms_head, 596 legal->terms_tail, 597 prev, 598 t); 599 } 600 } 601 } 602 } 603 } 604 605 606 /** 607 * Load all the terms of service from @a path under language @a lang. 608 * 609 * @param[in,out] legal where to write the result 610 * @param path where the terms are found 611 * @param lang which language directory to crawl 612 */ 613 static void 614 load_language (struct TALER_MHD_Legal *legal, 615 const char *path, 616 const char *lang) 617 { 618 char *dname; 619 DIR *d; 620 621 GNUNET_asprintf (&dname, 622 "%s/%s", 623 path, 624 lang); 625 d = opendir (dname); 626 if (NULL == d) 627 { 628 GNUNET_free (dname); 629 return; 630 } 631 for (struct dirent *de = readdir (d); 632 NULL != de; 633 de = readdir (d)) 634 { 635 const char *fn = de->d_name; 636 637 if (fn[0] == '.') 638 continue; 639 load_terms (legal, 640 path, 641 lang, 642 fn); 643 } 644 GNUNET_break (0 == closedir (d)); 645 GNUNET_free (dname); 646 } 647 648 649 struct TALER_MHD_Legal * 650 TALER_MHD_legal_load (const struct GNUNET_CONFIGURATION_Handle *cfg, 651 const char *section, 652 const char *diroption, 653 const char *tagoption) 654 { 655 struct TALER_MHD_Legal *legal; 656 char *path; 657 DIR *d; 658 659 legal = GNUNET_new (struct TALER_MHD_Legal); 660 if (GNUNET_OK != 661 GNUNET_CONFIGURATION_get_value_string (cfg, 662 section, 663 tagoption, 664 &legal->terms_version)) 665 { 666 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, 667 section, 668 tagoption); 669 GNUNET_free (legal); 670 return NULL; 671 } 672 if (GNUNET_OK != 673 GNUNET_CONFIGURATION_get_value_filename (cfg, 674 section, 675 diroption, 676 &path)) 677 { 678 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, 679 section, 680 diroption); 681 GNUNET_free (legal->terms_version); 682 GNUNET_free (legal); 683 return NULL; 684 } 685 d = opendir (path); 686 if (NULL == d) 687 { 688 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING, 689 section, 690 diroption, 691 "Could not open directory"); 692 GNUNET_free (legal->terms_version); 693 GNUNET_free (legal); 694 GNUNET_free (path); 695 return NULL; 696 } 697 for (struct dirent *de = readdir (d); 698 NULL != de; 699 de = readdir (d)) 700 { 701 const char *lang = de->d_name; 702 703 if (lang[0] == '.') 704 continue; 705 if (0 == strcmp (lang, 706 "locale")) 707 continue; 708 load_language (legal, 709 path, 710 lang); 711 } 712 GNUNET_break (0 == closedir (d)); 713 GNUNET_free (path); 714 return legal; 715 } 716 717 718 void 719 TALER_MHD_legal_free (struct TALER_MHD_Legal *legal) 720 { 721 struct Terms *t; 722 if (NULL == legal) 723 return; 724 while (NULL != (t = legal->terms_head)) 725 { 726 GNUNET_free (t->language); 727 GNUNET_free (t->compressed_terms); 728 if (0 != munmap (t->terms, t->terms_size)) 729 GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, 730 "munmap"); 731 GNUNET_CONTAINER_DLL_remove (legal->terms_head, 732 legal->terms_tail, 733 t); 734 GNUNET_free (t->terms_etag); 735 GNUNET_free (t); 736 } 737 GNUNET_free (legal->terms_version); 738 GNUNET_free (legal); 739 }