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