mhd2_legal.c (20153B)
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 "taler/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 (langs, 230 p->language); 231 if (q < best_lang_q) 232 continue; 233 best_lang_q = q; 234 t = p; 235 } 236 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 237 "Best match for %s/%s: %s / %s\n", 238 lang, 239 mime, 240 (NULL != t) ? t->mime_type : "<none>", 241 (NULL != t) ? t->language : "<none>"); 242 } 243 244 if (NULL != t) 245 { 246 struct MHD_StringNullable etag; 247 248 if ( (MHD_NO != 249 MHD_request_get_value (request, 250 MHD_VK_HEADER, 251 MHD_HTTP_HEADER_IF_NONE_MATCH, 252 &etag)) && 253 (NULL != etag.cstr) && 254 (NULL != t->terms_etag) && 255 (0 == strcasecmp (etag.cstr, 256 t->terms_etag)) ) 257 { 258 resp = MHD_response_from_empty (MHD_HTTP_STATUS_NOT_MODIFIED); 259 GNUNET_break (MHD_SC_OK == 260 MHD_RESPONSE_SET_OPTIONS ( 261 resp, 262 MHD_R_OPTION_HEAD_ONLY_RESPONSE (true))); 263 TALER_MHD2_add_global_headers (resp, 264 true); 265 GNUNET_break (MHD_SC_OK == 266 MHD_response_add_header (resp, 267 MHD_HTTP_HEADER_EXPIRES, 268 dat)); 269 GNUNET_break (MHD_SC_OK == 270 MHD_response_add_header (resp, 271 MHD_HTTP_HEADER_ETAG, 272 t->terms_etag)); 273 if (NULL != legal) 274 GNUNET_break (MHD_SC_OK == 275 MHD_response_add_header (resp, 276 TALER_TERMS_VERSION, 277 legal->terms_version)); 278 return MHD_action_from_response (request, 279 resp); 280 } 281 } 282 283 if (NULL == t) 284 t = &none; /* 501 if not configured */ 285 286 return_t: 287 /* try to compress the response */ 288 resp = NULL; 289 if ( (MHD_YES == 290 TALER_MHD2_can_compress (request)) && 291 (NULL != t->compressed_terms) ) 292 { 293 resp = MHD_response_from_buffer_static (t == &none 294 ? MHD_HTTP_STATUS_NOT_IMPLEMENTED 295 : MHD_HTTP_STATUS_OK, 296 t->compressed_terms_size, 297 t->compressed_terms); 298 if (MHD_SC_OK == 299 MHD_response_add_header (resp, 300 MHD_HTTP_HEADER_CONTENT_ENCODING, 301 "deflate")) 302 { 303 GNUNET_break (0); 304 MHD_response_destroy (resp); 305 resp = NULL; 306 } 307 } 308 if (NULL == resp) 309 { 310 /* could not generate compressed response, return uncompressed */ 311 resp = MHD_response_from_buffer_static (t == &none 312 ? MHD_HTTP_STATUS_NOT_IMPLEMENTED 313 : MHD_HTTP_STATUS_OK, 314 t->terms_size, 315 (void *) t->terms); 316 } 317 TALER_MHD2_add_global_headers (resp, 318 true); 319 GNUNET_break (MHD_SC_OK == 320 MHD_response_add_header (resp, 321 MHD_HTTP_HEADER_EXPIRES, 322 dat)); 323 if (NULL != langs) 324 { 325 GNUNET_break (MHD_SC_OK == 326 MHD_response_add_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_SC_OK == 333 MHD_response_add_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_SC_OK == 340 MHD_response_add_header (resp, 341 MHD_HTTP_HEADER_CACHE_CONTROL, 342 "public,max-age=864000")); 343 if (NULL != t->terms_etag) 344 GNUNET_break (MHD_SC_OK == 345 MHD_response_add_header (resp, 346 MHD_HTTP_HEADER_ETAG, 347 t->terms_etag)); 348 if (NULL != legal) 349 GNUNET_break (MHD_SC_OK == 350 MHD_response_add_header (resp, 351 TALER_TERMS_VERSION, 352 legal->terms_version)); 353 GNUNET_break (MHD_SC_OK == 354 MHD_response_add_header (resp, 355 MHD_HTTP_HEADER_CONTENT_TYPE, 356 t->mime_type)); 357 GNUNET_break (MHD_SC_OK == 358 MHD_response_add_header (resp, 359 MHD_HTTP_HEADER_CONTENT_LANGUAGE, 360 t->language)); 361 return MHD_action_from_response (request, 362 resp); 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_MHD2_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_MHD2_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_MHD2_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_MHD2_Legal * 604 TALER_MHD2_legal_load (const struct GNUNET_CONFIGURATION_Handle *cfg, 605 const char *section, 606 const char *diroption, 607 const char *tagoption) 608 { 609 struct TALER_MHD2_Legal *legal; 610 char *path; 611 DIR *d; 612 613 legal = GNUNET_new (struct TALER_MHD2_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_MHD2_legal_free (struct TALER_MHD2_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 }