mhd_legal.c (20284B)
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 enum MHD_Result 125 TALER_MHD_reply_legal (struct MHD_Connection *conn, 126 struct TALER_MHD_Legal *legal) 127 { 128 /* Default terms of service if none are configured */ 129 static struct Terms none = { 130 .mime_type = "text/plain", 131 .terms = (void *) "not configured", 132 .language = (void *) "en", 133 .terms_size = strlen ("not configured") 134 }; 135 struct MHD_Response *resp; 136 struct Terms *t; 137 struct GNUNET_TIME_Absolute a; 138 struct GNUNET_TIME_Timestamp m; 139 char dat[128]; 140 char *langs; 141 142 t = NULL; 143 langs = NULL; 144 145 a = GNUNET_TIME_relative_to_absolute (MAX_TERMS_CACHING); 146 /* Round up to next full day to ensure the expiration 147 time does not become a fingerprint! */ 148 a = GNUNET_TIME_absolute_round_down (a, 149 MAX_TERMS_CACHING); 150 a = GNUNET_TIME_absolute_add (a, 151 MAX_TERMS_CACHING); 152 m = GNUNET_TIME_absolute_to_timestamp (a); 153 TALER_MHD_get_date_string (m.abs_time, 154 dat); 155 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 156 "Setting '%s' header to '%s'\n", 157 MHD_HTTP_HEADER_EXPIRES, 158 dat); 159 if (NULL == legal) 160 { 161 t = &none; 162 goto return_t; 163 } 164 165 if (NULL != legal) 166 { 167 const char *mime; 168 const char *lang; 169 double best_mime_q = 0.0; 170 double best_lang_q = 0.0; 171 172 mime = MHD_lookup_connection_value (conn, 173 MHD_HEADER_KIND, 174 MHD_HTTP_HEADER_ACCEPT); 175 if (NULL == mime) 176 mime = "text/plain"; 177 lang = MHD_lookup_connection_value (conn, 178 MHD_HEADER_KIND, 179 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 180 if (NULL == lang) 181 lang = "en"; 182 /* Find best match: must match mime type (if possible), and if 183 mime type matches, ideally also language */ 184 for (struct Terms *p = legal->terms_head; 185 NULL != p; 186 p = p->next) 187 { 188 double q; 189 190 q = TALER_pattern_matches (mime, 191 p->mime_type); 192 if (q > best_mime_q) 193 best_mime_q = q; 194 } 195 for (struct Terms *p = legal->terms_head; 196 NULL != p; 197 p = p->next) 198 { 199 double q; 200 201 q = TALER_pattern_matches (mime, 202 p->mime_type); 203 if (q < best_mime_q) 204 continue; 205 q = TALER_pattern_matches (lang, 206 p->language); 207 /* create 'available-languages' (for this mime-type) */ 208 if (NULL == langs) 209 { 210 langs = GNUNET_strdup (p->language); 211 } 212 else if (NULL == strstr (langs, 213 p->language)) 214 { 215 char *tmp = langs; 216 217 GNUNET_asprintf (&langs, 218 "%s,%s", 219 tmp, 220 p->language); 221 GNUNET_free (tmp); 222 } 223 if (NULL != t) 224 { 225 if (q < best_lang_q) 226 continue; 227 /* Client expressed no preference between these two formats, 228 so fall back to our own ranking instead of letting the 229 last entry in the list win. */ 230 if ( (q <= best_lang_q) && 231 (p->priority <= t->priority) ) 232 continue; 233 } 234 best_lang_q = q; 235 t = p; 236 } 237 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 238 "Best match for %s/%s: %s / %s\n", 239 lang, 240 mime, 241 (NULL != t) ? t->mime_type : "<none>", 242 (NULL != t) ? t->language : "<none>"); 243 } 244 245 if (NULL != t) 246 { 247 const char *etag; 248 249 etag = MHD_lookup_connection_value (conn, 250 MHD_HEADER_KIND, 251 MHD_HTTP_HEADER_IF_NONE_MATCH); 252 if ( (NULL != etag) && 253 (NULL != t->terms_etag) && 254 (0 == strcasecmp (etag, 255 t->terms_etag)) ) 256 { 257 enum MHD_Result ret; 258 259 resp = MHD_create_response_from_buffer (0, 260 NULL, 261 MHD_RESPMEM_PERSISTENT); 262 TALER_MHD_add_global_headers (resp, 263 true); 264 GNUNET_break (MHD_YES == 265 MHD_add_response_header (resp, 266 MHD_HTTP_HEADER_EXPIRES, 267 dat)); 268 GNUNET_break (MHD_YES == 269 MHD_add_response_header (resp, 270 MHD_HTTP_HEADER_ETAG, 271 t->terms_etag)); 272 if (NULL != legal) 273 GNUNET_break (MHD_YES == 274 MHD_add_response_header (resp, 275 TALER_TERMS_VERSION, 276 legal->terms_version)); 277 ret = MHD_queue_response (conn, 278 MHD_HTTP_NOT_MODIFIED, 279 resp); 280 GNUNET_break (MHD_YES == ret); 281 MHD_destroy_response (resp); 282 GNUNET_free (langs); 283 return ret; 284 } 285 } 286 287 if (NULL == t) 288 t = &none; /* 501 if not configured */ 289 290 return_t: 291 /* try to compress the response */ 292 resp = NULL; 293 if ( (TALER_MHD_CT_DEFLATE == 294 TALER_MHD_can_compress (conn, 295 TALER_MHD_CT_DEFLATE)) && 296 (NULL != t->compressed_terms) ) 297 { 298 resp = MHD_create_response_from_buffer (t->compressed_terms_size, 299 t->compressed_terms, 300 MHD_RESPMEM_PERSISTENT); 301 if (MHD_NO == 302 MHD_add_response_header (resp, 303 MHD_HTTP_HEADER_CONTENT_ENCODING, 304 "deflate")) 305 { 306 GNUNET_break (0); 307 MHD_destroy_response (resp); 308 resp = NULL; 309 } 310 } 311 if (NULL == resp) 312 { 313 /* could not generate compressed response, return uncompressed */ 314 resp = MHD_create_response_from_buffer (t->terms_size, 315 (void *) t->terms, 316 MHD_RESPMEM_PERSISTENT); 317 } 318 TALER_MHD_add_global_headers (resp, 319 true); 320 GNUNET_break (MHD_YES == 321 MHD_add_response_header (resp, 322 MHD_HTTP_HEADER_EXPIRES, 323 dat)); 324 if (NULL != langs) 325 { 326 GNUNET_break (MHD_YES == 327 MHD_add_response_header (resp, 328 "Avail-Languages", 329 langs)); 330 GNUNET_free (langs); 331 } 332 /* Set cache control headers: our response varies depending on these headers */ 333 GNUNET_break (MHD_YES == 334 MHD_add_response_header (resp, 335 MHD_HTTP_HEADER_VARY, 336 MHD_HTTP_HEADER_ACCEPT_LANGUAGE "," 337 MHD_HTTP_HEADER_ACCEPT "," 338 MHD_HTTP_HEADER_ACCEPT_ENCODING)); 339 /* Information is always public, revalidate after 10 days */ 340 GNUNET_break (MHD_YES == 341 MHD_add_response_header (resp, 342 MHD_HTTP_HEADER_CACHE_CONTROL, 343 "public,max-age=864000")); 344 if (NULL != t->terms_etag) 345 GNUNET_break (MHD_YES == 346 MHD_add_response_header (resp, 347 MHD_HTTP_HEADER_ETAG, 348 t->terms_etag)); 349 if (NULL != legal) 350 GNUNET_break (MHD_YES == 351 MHD_add_response_header (resp, 352 TALER_TERMS_VERSION, 353 legal->terms_version)); 354 GNUNET_break (MHD_YES == 355 MHD_add_response_header (resp, 356 MHD_HTTP_HEADER_CONTENT_TYPE, 357 t->mime_type)); 358 GNUNET_break (MHD_YES == 359 MHD_add_response_header (resp, 360 MHD_HTTP_HEADER_CONTENT_LANGUAGE, 361 t->language)); 362 { 363 enum MHD_Result ret; 364 365 ret = MHD_queue_response (conn, 366 t == &none 367 ? MHD_HTTP_NOT_IMPLEMENTED 368 : MHD_HTTP_OK, 369 resp); 370 MHD_destroy_response (resp); 371 return ret; 372 } 373 } 374 375 376 /** 377 * Load all the terms of service from @a path under language @a lang 378 * from file @a name 379 * 380 * @param[in,out] legal where to write the result 381 * @param path where the terms are found 382 * @param lang which language directory to crawl 383 * @param name specific file to access 384 */ 385 static void 386 load_terms (struct TALER_MHD_Legal *legal, 387 const char *path, 388 const char *lang, 389 const char *name) 390 { 391 static struct MimeMap 392 { 393 const char *ext; 394 const char *mime; 395 unsigned int priority; 396 } mm[] = { 397 { .ext = ".txt", .mime = "text/plain", .priority = 150 }, 398 { .ext = ".html", .mime = "text/html", .priority = 100 }, 399 { .ext = ".htm", .mime = "text/html", .priority = 99 }, 400 { .ext = ".md", .mime = "text/markdown", .priority = 50 }, 401 { .ext = ".pdf", .mime = "application/pdf", .priority = 25 }, 402 { .ext = ".jpg", .mime = "image/jpeg" }, 403 { .ext = ".jpeg", .mime = "image/jpeg" }, 404 { .ext = ".png", .mime = "image/png" }, 405 { .ext = ".gif", .mime = "image/gif" }, 406 { .ext = ".epub", .mime = "application/epub+zip", .priority = 10 }, 407 { .ext = ".xml", .mime = "text/xml", .priority = 10 }, 408 { .ext = NULL, .mime = NULL } 409 }; 410 const char *ext = strrchr (name, '.'); 411 const char *mime; 412 unsigned int priority; 413 414 if (NULL == ext) 415 { 416 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 417 "Unsupported file `%s' in directory `%s/%s': lacks extension\n", 418 name, 419 path, 420 lang); 421 return; 422 } 423 if ( (NULL == legal->terms_version) || 424 (0 != strncmp (legal->terms_version, 425 name, 426 ext - name)) ) 427 { 428 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 429 "Filename `%s' does not match Etag `%s' in directory `%s/%s'. Ignoring it.\n", 430 name, 431 legal->terms_version, 432 path, 433 lang); 434 return; 435 } 436 mime = NULL; 437 for (unsigned int i = 0; NULL != mm[i].ext; i++) 438 if (0 == strcasecmp (mm[i].ext, 439 ext)) 440 { 441 mime = mm[i].mime; 442 priority = mm[i].priority; 443 break; 444 } 445 if (NULL == mime) 446 { 447 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 448 "Unsupported file extension `%s' of file `%s' in directory `%s/%s'\n", 449 ext, 450 name, 451 path, 452 lang); 453 return; 454 } 455 /* try to read the file with the terms of service */ 456 { 457 struct stat st; 458 char *fn; 459 int fd; 460 461 GNUNET_asprintf (&fn, 462 "%s/%s/%s", 463 path, 464 lang, 465 name); 466 fd = open (fn, O_RDONLY); 467 if (-1 == fd) 468 { 469 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, 470 "open", 471 fn); 472 GNUNET_free (fn); 473 return; 474 } 475 if (0 != fstat (fd, &st)) 476 { 477 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, 478 "fstat", 479 fn); 480 GNUNET_break (0 == close (fd)); 481 GNUNET_free (fn); 482 return; 483 } 484 if (SIZE_MAX < ((unsigned long long) st.st_size)) 485 { 486 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, 487 "fstat-size", 488 fn); 489 GNUNET_break (0 == close (fd)); 490 GNUNET_free (fn); 491 return; 492 } 493 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 494 "Loading legal information from file `%s'\n", 495 fn); 496 { 497 void *buf; 498 size_t bsize; 499 500 bsize = (size_t) st.st_size; 501 buf = mmap (NULL, 502 bsize, 503 PROT_READ, 504 MAP_SHARED, 505 fd, 506 0); 507 if (MAP_FAILED == buf) 508 { 509 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, 510 "mmap", 511 fn); 512 GNUNET_break (0 == close (fd)); 513 GNUNET_free (fn); 514 return; 515 } 516 GNUNET_break (0 == close (fd)); 517 GNUNET_free (fn); 518 519 /* insert into global list of terms of service */ 520 { 521 struct Terms *t; 522 struct GNUNET_HashCode hc; 523 524 GNUNET_CRYPTO_hash (buf, 525 bsize, 526 &hc); 527 t = GNUNET_new (struct Terms); 528 t->mime_type = mime; 529 t->terms = buf; 530 t->language = GNUNET_strdup (lang); 531 t->terms_size = bsize; 532 t->priority = priority; 533 t->terms_etag 534 = GNUNET_STRINGS_data_to_string_alloc (&hc, 535 sizeof (hc) / 2); 536 buf = GNUNET_memdup (t->terms, 537 t->terms_size); 538 if (TALER_MHD_body_compress (&buf, 539 &bsize)) 540 { 541 t->compressed_terms = buf; 542 t->compressed_terms_size = bsize; 543 } 544 else 545 { 546 GNUNET_free (buf); 547 } 548 { 549 struct Terms *prev = NULL; 550 551 for (struct Terms *pos = legal->terms_head; 552 NULL != pos; 553 pos = pos->next) 554 { 555 if (pos->priority < priority) 556 break; 557 prev = pos; 558 } 559 GNUNET_CONTAINER_DLL_insert_after (legal->terms_head, 560 legal->terms_tail, 561 prev, 562 t); 563 } 564 } 565 } 566 } 567 } 568 569 570 /** 571 * Load all the terms of service from @a path under language @a lang. 572 * 573 * @param[in,out] legal where to write the result 574 * @param path where the terms are found 575 * @param lang which language directory to crawl 576 */ 577 static void 578 load_language (struct TALER_MHD_Legal *legal, 579 const char *path, 580 const char *lang) 581 { 582 char *dname; 583 DIR *d; 584 585 GNUNET_asprintf (&dname, 586 "%s/%s", 587 path, 588 lang); 589 d = opendir (dname); 590 if (NULL == d) 591 { 592 GNUNET_free (dname); 593 return; 594 } 595 for (struct dirent *de = readdir (d); 596 NULL != de; 597 de = readdir (d)) 598 { 599 const char *fn = de->d_name; 600 601 if (fn[0] == '.') 602 continue; 603 load_terms (legal, 604 path, 605 lang, 606 fn); 607 } 608 GNUNET_break (0 == closedir (d)); 609 GNUNET_free (dname); 610 } 611 612 613 struct TALER_MHD_Legal * 614 TALER_MHD_legal_load (const struct GNUNET_CONFIGURATION_Handle *cfg, 615 const char *section, 616 const char *diroption, 617 const char *tagoption) 618 { 619 struct TALER_MHD_Legal *legal; 620 char *path; 621 DIR *d; 622 623 legal = GNUNET_new (struct TALER_MHD_Legal); 624 if (GNUNET_OK != 625 GNUNET_CONFIGURATION_get_value_string (cfg, 626 section, 627 tagoption, 628 &legal->terms_version)) 629 { 630 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, 631 section, 632 tagoption); 633 GNUNET_free (legal); 634 return NULL; 635 } 636 if (GNUNET_OK != 637 GNUNET_CONFIGURATION_get_value_filename (cfg, 638 section, 639 diroption, 640 &path)) 641 { 642 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, 643 section, 644 diroption); 645 GNUNET_free (legal->terms_version); 646 GNUNET_free (legal); 647 return NULL; 648 } 649 d = opendir (path); 650 if (NULL == d) 651 { 652 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING, 653 section, 654 diroption, 655 "Could not open directory"); 656 GNUNET_free (legal->terms_version); 657 GNUNET_free (legal); 658 GNUNET_free (path); 659 return NULL; 660 } 661 for (struct dirent *de = readdir (d); 662 NULL != de; 663 de = readdir (d)) 664 { 665 const char *lang = de->d_name; 666 667 if (lang[0] == '.') 668 continue; 669 if (0 == strcmp (lang, 670 "locale")) 671 continue; 672 load_language (legal, 673 path, 674 lang); 675 } 676 GNUNET_break (0 == closedir (d)); 677 GNUNET_free (path); 678 return legal; 679 } 680 681 682 void 683 TALER_MHD_legal_free (struct TALER_MHD_Legal *legal) 684 { 685 struct Terms *t; 686 if (NULL == legal) 687 return; 688 while (NULL != (t = legal->terms_head)) 689 { 690 GNUNET_free (t->language); 691 GNUNET_free (t->compressed_terms); 692 if (0 != munmap (t->terms, t->terms_size)) 693 GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, 694 "munmap"); 695 GNUNET_CONTAINER_DLL_remove (legal->terms_head, 696 legal->terms_tail, 697 t); 698 GNUNET_free (t->terms_etag); 699 GNUNET_free (t); 700 } 701 GNUNET_free (legal->terms_version); 702 GNUNET_free (legal); 703 }