mhd2_legal.c (20459B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2019--2025 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 mhd2_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 <microhttpd2.h> 27 #include "taler/taler_util.h" 28 #include "taler/taler_mhd2_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_MHD2_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 const struct MHD_Action * 125 TALER_MHD2_reply_legal (struct MHD_Request *request, 126 struct TALER_MHD2_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 a = GNUNET_TIME_relative_to_absolute (MAX_TERMS_CACHING); 145 m = GNUNET_TIME_absolute_to_timestamp (a); 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 TALER_MHD2_get_date_string (m.abs_time, 153 dat); 154 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 155 "Setting '%s' header to '%s'\n", 156 MHD_HTTP_HEADER_EXPIRES, 157 dat); 158 if (NULL == legal) 159 { 160 t = &none; 161 goto return_t; 162 } 163 164 if (NULL != legal) 165 { 166 struct MHD_StringNullable mimen; 167 struct MHD_StringNullable langn; 168 const char *mime; 169 const char *lang; 170 double best_mime_q = 0.0; 171 double best_lang_q = 0.0; 172 173 if ( (MHD_NO == 174 MHD_request_get_value (request, 175 MHD_VK_HEADER, 176 MHD_HTTP_HEADER_ACCEPT, 177 &mimen)) || 178 (NULL == mimen.cstr) ) 179 mime = "text/plain"; 180 else 181 mime = mimen.cstr; 182 if ( (MHD_NO == 183 MHD_request_get_value (request, 184 MHD_VK_HEADER, 185 MHD_HTTP_HEADER_ACCEPT_LANGUAGE, 186 &langn)) || 187 (NULL == langn.cstr) ) 188 lang = "en"; 189 else 190 lang = langn.cstr; 191 /* Find best match: must match mime type (if possible), and if 192 mime type matches, ideally also language */ 193 for (struct Terms *p = legal->terms_head; 194 NULL != p; 195 p = p->next) 196 { 197 double q; 198 199 q = TALER_pattern_matches (mime, 200 p->mime_type); 201 if (q > best_mime_q) 202 best_mime_q = q; 203 } 204 for (struct Terms *p = legal->terms_head; 205 NULL != p; 206 p = p->next) 207 { 208 double q; 209 210 q = TALER_pattern_matches (mime, 211 p->mime_type); 212 if (q < best_mime_q) 213 continue; 214 if (NULL == langs) 215 { 216 langs = GNUNET_strdup (p->language); 217 } 218 else if (NULL == strstr (langs, 219 p->language)) 220 { 221 char *tmp = langs; 222 223 GNUNET_asprintf (&langs, 224 "%s,%s", 225 tmp, 226 p->language); 227 GNUNET_free (tmp); 228 } 229 q = TALER_pattern_matches (lang, 230 p->language); 231 if (NULL != t) 232 { 233 if (q < best_lang_q) 234 continue; 235 /* Client expressed no preference between these two formats, 236 so fall back to our own ranking instead of letting the 237 last entry in the list win. */ 238 if ( (q == best_lang_q) && 239 (p->priority <= t->priority) ) 240 continue; 241 } 242 best_lang_q = q; 243 t = p; 244 } 245 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 246 "Best match for %s/%s: %s / %s\n", 247 lang, 248 mime, 249 (NULL != t) ? t->mime_type : "<none>", 250 (NULL != t) ? t->language : "<none>"); 251 } 252 253 if (NULL != t) 254 { 255 struct MHD_StringNullable etag; 256 257 if ( (MHD_NO != 258 MHD_request_get_value (request, 259 MHD_VK_HEADER, 260 MHD_HTTP_HEADER_IF_NONE_MATCH, 261 &etag)) && 262 (NULL != etag.cstr) && 263 (NULL != t->terms_etag) && 264 (0 == strcasecmp (etag.cstr, 265 t->terms_etag)) ) 266 { 267 resp = MHD_response_from_empty (MHD_HTTP_STATUS_NOT_MODIFIED); 268 GNUNET_break (MHD_SC_OK == 269 MHD_RESPONSE_SET_OPTIONS ( 270 resp, 271 MHD_R_OPTION_HEAD_ONLY_RESPONSE (true))); 272 TALER_MHD2_add_global_headers (resp, 273 true); 274 GNUNET_break (MHD_SC_OK == 275 MHD_response_add_header (resp, 276 MHD_HTTP_HEADER_EXPIRES, 277 dat)); 278 GNUNET_break (MHD_SC_OK == 279 MHD_response_add_header (resp, 280 MHD_HTTP_HEADER_ETAG, 281 t->terms_etag)); 282 if (NULL != legal) 283 GNUNET_break (MHD_SC_OK == 284 MHD_response_add_header (resp, 285 TALER_TERMS_VERSION, 286 legal->terms_version)); 287 return MHD_action_from_response (request, 288 resp); 289 } 290 } 291 292 if (NULL == t) 293 t = &none; /* 501 if not configured */ 294 295 return_t: 296 /* try to compress the response */ 297 resp = NULL; 298 if ( (MHD_YES == 299 TALER_MHD2_can_compress (request)) && 300 (NULL != t->compressed_terms) ) 301 { 302 resp = MHD_response_from_buffer_static (t == &none 303 ? MHD_HTTP_STATUS_NOT_IMPLEMENTED 304 : MHD_HTTP_STATUS_OK, 305 t->compressed_terms_size, 306 t->compressed_terms); 307 if (MHD_SC_OK != 308 MHD_response_add_header (resp, 309 MHD_HTTP_HEADER_CONTENT_ENCODING, 310 "deflate")) 311 { 312 GNUNET_break (0); 313 MHD_response_destroy (resp); 314 resp = NULL; 315 } 316 } 317 if (NULL == resp) 318 { 319 /* could not generate compressed response, return uncompressed */ 320 resp = MHD_response_from_buffer_static (t == &none 321 ? MHD_HTTP_STATUS_NOT_IMPLEMENTED 322 : MHD_HTTP_STATUS_OK, 323 t->terms_size, 324 (void *) t->terms); 325 } 326 TALER_MHD2_add_global_headers (resp, 327 true); 328 GNUNET_break (MHD_SC_OK == 329 MHD_response_add_header (resp, 330 MHD_HTTP_HEADER_EXPIRES, 331 dat)); 332 if (NULL != langs) 333 { 334 GNUNET_break (MHD_SC_OK == 335 MHD_response_add_header (resp, 336 "Avail-Languages", 337 langs)); 338 GNUNET_free (langs); 339 } 340 /* Set cache control headers: our response varies depending on these headers */ 341 GNUNET_break (MHD_SC_OK == 342 MHD_response_add_header (resp, 343 MHD_HTTP_HEADER_VARY, 344 MHD_HTTP_HEADER_ACCEPT_LANGUAGE "," 345 MHD_HTTP_HEADER_ACCEPT "," 346 MHD_HTTP_HEADER_ACCEPT_ENCODING)); 347 /* Information is always public, revalidate after 10 days */ 348 GNUNET_break (MHD_SC_OK == 349 MHD_response_add_header (resp, 350 MHD_HTTP_HEADER_CACHE_CONTROL, 351 "public,max-age=864000")); 352 if (NULL != t->terms_etag) 353 GNUNET_break (MHD_SC_OK == 354 MHD_response_add_header (resp, 355 MHD_HTTP_HEADER_ETAG, 356 t->terms_etag)); 357 if (NULL != legal) 358 GNUNET_break (MHD_SC_OK == 359 MHD_response_add_header (resp, 360 TALER_TERMS_VERSION, 361 legal->terms_version)); 362 GNUNET_break (MHD_SC_OK == 363 MHD_response_add_header (resp, 364 MHD_HTTP_HEADER_CONTENT_TYPE, 365 t->mime_type)); 366 GNUNET_break (MHD_SC_OK == 367 MHD_response_add_header (resp, 368 MHD_HTTP_HEADER_CONTENT_LANGUAGE, 369 t->language)); 370 return MHD_action_from_response (request, 371 resp); 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_MHD2_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_MHD2_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_MHD2_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_MHD2_Legal * 613 TALER_MHD2_legal_load (const struct GNUNET_CONFIGURATION_Handle *cfg, 614 const char *section, 615 const char *diroption, 616 const char *tagoption) 617 { 618 struct TALER_MHD2_Legal *legal; 619 char *path; 620 DIR *d; 621 622 legal = GNUNET_new (struct TALER_MHD2_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_MHD2_legal_free (struct TALER_MHD2_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 }