mhd_legal.c (20257B)
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 m = GNUNET_TIME_absolute_to_timestamp (a); 147 /* Round up to next full day to ensure the expiration 148 time does not become a fingerprint! */ 149 a = GNUNET_TIME_absolute_round_down (a, 150 MAX_TERMS_CACHING); 151 a = GNUNET_TIME_absolute_add (a, 152 MAX_TERMS_CACHING); 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 return ret; 283 } 284 } 285 286 if (NULL == t) 287 t = &none; /* 501 if not configured */ 288 289 return_t: 290 /* try to compress the response */ 291 resp = NULL; 292 if ( (TALER_MHD_CT_DEFLATE == 293 TALER_MHD_can_compress (conn, 294 TALER_MHD_CT_DEFLATE)) && 295 (NULL != t->compressed_terms) ) 296 { 297 resp = MHD_create_response_from_buffer (t->compressed_terms_size, 298 t->compressed_terms, 299 MHD_RESPMEM_PERSISTENT); 300 if (MHD_NO == 301 MHD_add_response_header (resp, 302 MHD_HTTP_HEADER_CONTENT_ENCODING, 303 "deflate")) 304 { 305 GNUNET_break (0); 306 MHD_destroy_response (resp); 307 resp = NULL; 308 } 309 } 310 if (NULL == resp) 311 { 312 /* could not generate compressed response, return uncompressed */ 313 resp = MHD_create_response_from_buffer (t->terms_size, 314 (void *) t->terms, 315 MHD_RESPMEM_PERSISTENT); 316 } 317 TALER_MHD_add_global_headers (resp, 318 true); 319 GNUNET_break (MHD_YES == 320 MHD_add_response_header (resp, 321 MHD_HTTP_HEADER_EXPIRES, 322 dat)); 323 if (NULL != langs) 324 { 325 GNUNET_break (MHD_YES == 326 MHD_add_response_header (resp, 327 "Avail-Languages", 328 langs)); 329 GNUNET_free (langs); 330 } 331 /* Set cache control headers: our response varies depending on these headers */ 332 GNUNET_break (MHD_YES == 333 MHD_add_response_header (resp, 334 MHD_HTTP_HEADER_VARY, 335 MHD_HTTP_HEADER_ACCEPT_LANGUAGE "," 336 MHD_HTTP_HEADER_ACCEPT "," 337 MHD_HTTP_HEADER_ACCEPT_ENCODING)); 338 /* Information is always public, revalidate after 10 days */ 339 GNUNET_break (MHD_YES == 340 MHD_add_response_header (resp, 341 MHD_HTTP_HEADER_CACHE_CONTROL, 342 "public,max-age=864000")); 343 if (NULL != t->terms_etag) 344 GNUNET_break (MHD_YES == 345 MHD_add_response_header (resp, 346 MHD_HTTP_HEADER_ETAG, 347 t->terms_etag)); 348 if (NULL != legal) 349 GNUNET_break (MHD_YES == 350 MHD_add_response_header (resp, 351 TALER_TERMS_VERSION, 352 legal->terms_version)); 353 GNUNET_break (MHD_YES == 354 MHD_add_response_header (resp, 355 MHD_HTTP_HEADER_CONTENT_TYPE, 356 t->mime_type)); 357 GNUNET_break (MHD_YES == 358 MHD_add_response_header (resp, 359 MHD_HTTP_HEADER_CONTENT_LANGUAGE, 360 t->language)); 361 { 362 enum MHD_Result ret; 363 364 ret = MHD_queue_response (conn, 365 t == &none 366 ? MHD_HTTP_NOT_IMPLEMENTED 367 : MHD_HTTP_OK, 368 resp); 369 MHD_destroy_response (resp); 370 return ret; 371 } 372 } 373 374 375 /** 376 * Load all the terms of service from @a path under language @a lang 377 * from file @a name 378 * 379 * @param[in,out] legal where to write the result 380 * @param path where the terms are found 381 * @param lang which language directory to crawl 382 * @param name specific file to access 383 */ 384 static void 385 load_terms (struct TALER_MHD_Legal *legal, 386 const char *path, 387 const char *lang, 388 const char *name) 389 { 390 static struct MimeMap 391 { 392 const char *ext; 393 const char *mime; 394 unsigned int priority; 395 } mm[] = { 396 { .ext = ".txt", .mime = "text/plain", .priority = 150 }, 397 { .ext = ".html", .mime = "text/html", .priority = 100 }, 398 { .ext = ".htm", .mime = "text/html", .priority = 99 }, 399 { .ext = ".md", .mime = "text/markdown", .priority = 50 }, 400 { .ext = ".pdf", .mime = "application/pdf", .priority = 25 }, 401 { .ext = ".jpg", .mime = "image/jpeg" }, 402 { .ext = ".jpeg", .mime = "image/jpeg" }, 403 { .ext = ".png", .mime = "image/png" }, 404 { .ext = ".gif", .mime = "image/gif" }, 405 { .ext = ".epub", .mime = "application/epub+zip", .priority = 10 }, 406 { .ext = ".xml", .mime = "text/xml", .priority = 10 }, 407 { .ext = NULL, .mime = NULL } 408 }; 409 const char *ext = strrchr (name, '.'); 410 const char *mime; 411 unsigned int priority; 412 413 if (NULL == ext) 414 { 415 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 416 "Unsupported file `%s' in directory `%s/%s': lacks extension\n", 417 name, 418 path, 419 lang); 420 return; 421 } 422 if ( (NULL == legal->terms_version) || 423 (0 != strncmp (legal->terms_version, 424 name, 425 ext - name)) ) 426 { 427 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 428 "Filename `%s' does not match Etag `%s' in directory `%s/%s'. Ignoring it.\n", 429 name, 430 legal->terms_version, 431 path, 432 lang); 433 return; 434 } 435 mime = NULL; 436 for (unsigned int i = 0; NULL != mm[i].ext; i++) 437 if (0 == strcasecmp (mm[i].ext, 438 ext)) 439 { 440 mime = mm[i].mime; 441 priority = mm[i].priority; 442 break; 443 } 444 if (NULL == mime) 445 { 446 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 447 "Unsupported file extension `%s' of file `%s' in directory `%s/%s'\n", 448 ext, 449 name, 450 path, 451 lang); 452 return; 453 } 454 /* try to read the file with the terms of service */ 455 { 456 struct stat st; 457 char *fn; 458 int fd; 459 460 GNUNET_asprintf (&fn, 461 "%s/%s/%s", 462 path, 463 lang, 464 name); 465 fd = open (fn, O_RDONLY); 466 if (-1 == fd) 467 { 468 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, 469 "open", 470 fn); 471 GNUNET_free (fn); 472 return; 473 } 474 if (0 != fstat (fd, &st)) 475 { 476 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, 477 "fstat", 478 fn); 479 GNUNET_break (0 == close (fd)); 480 GNUNET_free (fn); 481 return; 482 } 483 if (SIZE_MAX < ((unsigned long long) st.st_size)) 484 { 485 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, 486 "fstat-size", 487 fn); 488 GNUNET_break (0 == close (fd)); 489 GNUNET_free (fn); 490 return; 491 } 492 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 493 "Loading legal information from file `%s'\n", 494 fn); 495 { 496 void *buf; 497 size_t bsize; 498 499 bsize = (size_t) st.st_size; 500 buf = mmap (NULL, 501 bsize, 502 PROT_READ, 503 MAP_SHARED, 504 fd, 505 0); 506 if (MAP_FAILED == buf) 507 { 508 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, 509 "mmap", 510 fn); 511 GNUNET_break (0 == close (fd)); 512 GNUNET_free (fn); 513 return; 514 } 515 GNUNET_break (0 == close (fd)); 516 GNUNET_free (fn); 517 518 /* insert into global list of terms of service */ 519 { 520 struct Terms *t; 521 struct GNUNET_HashCode hc; 522 523 GNUNET_CRYPTO_hash (buf, 524 bsize, 525 &hc); 526 t = GNUNET_new (struct Terms); 527 t->mime_type = mime; 528 t->terms = buf; 529 t->language = GNUNET_strdup (lang); 530 t->terms_size = bsize; 531 t->priority = priority; 532 t->terms_etag 533 = GNUNET_STRINGS_data_to_string_alloc (&hc, 534 sizeof (hc) / 2); 535 buf = GNUNET_memdup (t->terms, 536 t->terms_size); 537 if (TALER_MHD_body_compress (&buf, 538 &bsize)) 539 { 540 t->compressed_terms = buf; 541 t->compressed_terms_size = bsize; 542 } 543 else 544 { 545 GNUNET_free (buf); 546 } 547 { 548 struct Terms *prev = NULL; 549 550 for (struct Terms *pos = legal->terms_head; 551 NULL != pos; 552 pos = pos->next) 553 { 554 if (pos->priority < priority) 555 break; 556 prev = pos; 557 } 558 GNUNET_CONTAINER_DLL_insert_after (legal->terms_head, 559 legal->terms_tail, 560 prev, 561 t); 562 } 563 } 564 } 565 } 566 } 567 568 569 /** 570 * Load all the terms of service from @a path under language @a lang. 571 * 572 * @param[in,out] legal where to write the result 573 * @param path where the terms are found 574 * @param lang which language directory to crawl 575 */ 576 static void 577 load_language (struct TALER_MHD_Legal *legal, 578 const char *path, 579 const char *lang) 580 { 581 char *dname; 582 DIR *d; 583 584 GNUNET_asprintf (&dname, 585 "%s/%s", 586 path, 587 lang); 588 d = opendir (dname); 589 if (NULL == d) 590 { 591 GNUNET_free (dname); 592 return; 593 } 594 for (struct dirent *de = readdir (d); 595 NULL != de; 596 de = readdir (d)) 597 { 598 const char *fn = de->d_name; 599 600 if (fn[0] == '.') 601 continue; 602 load_terms (legal, 603 path, 604 lang, 605 fn); 606 } 607 GNUNET_break (0 == closedir (d)); 608 GNUNET_free (dname); 609 } 610 611 612 struct TALER_MHD_Legal * 613 TALER_MHD_legal_load (const struct GNUNET_CONFIGURATION_Handle *cfg, 614 const char *section, 615 const char *diroption, 616 const char *tagoption) 617 { 618 struct TALER_MHD_Legal *legal; 619 char *path; 620 DIR *d; 621 622 legal = GNUNET_new (struct TALER_MHD_Legal); 623 if (GNUNET_OK != 624 GNUNET_CONFIGURATION_get_value_string (cfg, 625 section, 626 tagoption, 627 &legal->terms_version)) 628 { 629 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, 630 section, 631 tagoption); 632 GNUNET_free (legal); 633 return NULL; 634 } 635 if (GNUNET_OK != 636 GNUNET_CONFIGURATION_get_value_filename (cfg, 637 section, 638 diroption, 639 &path)) 640 { 641 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, 642 section, 643 diroption); 644 GNUNET_free (legal->terms_version); 645 GNUNET_free (legal); 646 return NULL; 647 } 648 d = opendir (path); 649 if (NULL == d) 650 { 651 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING, 652 section, 653 diroption, 654 "Could not open directory"); 655 GNUNET_free (legal->terms_version); 656 GNUNET_free (legal); 657 GNUNET_free (path); 658 return NULL; 659 } 660 for (struct dirent *de = readdir (d); 661 NULL != de; 662 de = readdir (d)) 663 { 664 const char *lang = de->d_name; 665 666 if (lang[0] == '.') 667 continue; 668 if (0 == strcmp (lang, 669 "locale")) 670 continue; 671 load_language (legal, 672 path, 673 lang); 674 } 675 GNUNET_break (0 == closedir (d)); 676 GNUNET_free (path); 677 return legal; 678 } 679 680 681 void 682 TALER_MHD_legal_free (struct TALER_MHD_Legal *legal) 683 { 684 struct Terms *t; 685 if (NULL == legal) 686 return; 687 while (NULL != (t = legal->terms_head)) 688 { 689 GNUNET_free (t->language); 690 GNUNET_free (t->compressed_terms); 691 if (0 != munmap (t->terms, t->terms_size)) 692 GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, 693 "munmap"); 694 GNUNET_CONTAINER_DLL_remove (legal->terms_head, 695 legal->terms_tail, 696 t); 697 GNUNET_free (t->terms_etag); 698 GNUNET_free (t); 699 } 700 GNUNET_free (legal->terms_version); 701 GNUNET_free (legal); 702 }