Line data Source code
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 19 : 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 19 : t = NULL;
143 19 : langs = NULL;
144 :
145 19 : 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 19 : a = GNUNET_TIME_absolute_round_down (a,
149 : MAX_TERMS_CACHING);
150 19 : a = GNUNET_TIME_absolute_add (a,
151 : MAX_TERMS_CACHING);
152 19 : m = GNUNET_TIME_absolute_to_timestamp (a);
153 19 : TALER_MHD_get_date_string (m.abs_time,
154 : dat);
155 19 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
156 : "Setting '%s' header to '%s'\n",
157 : MHD_HTTP_HEADER_EXPIRES,
158 : dat);
159 19 : if (NULL == legal)
160 : {
161 8 : t = &none;
162 8 : goto return_t;
163 : }
164 :
165 11 : if (NULL != legal)
166 : {
167 : const char *mime;
168 : const char *lang;
169 11 : double best_mime_q = 0.0;
170 11 : double best_lang_q = 0.0;
171 :
172 11 : mime = MHD_lookup_connection_value (conn,
173 : MHD_HEADER_KIND,
174 : MHD_HTTP_HEADER_ACCEPT);
175 11 : if (NULL == mime)
176 1 : mime = "text/plain";
177 11 : lang = MHD_lookup_connection_value (conn,
178 : MHD_HEADER_KIND,
179 : MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
180 11 : if (NULL == lang)
181 9 : lang = "en";
182 : /* Find best match: must match mime type (if possible), and if
183 : mime type matches, ideally also language */
184 11 : for (struct Terms *p = legal->terms_head;
185 77 : NULL != p;
186 66 : p = p->next)
187 : {
188 : double q;
189 :
190 66 : q = TALER_pattern_matches (mime,
191 : p->mime_type);
192 66 : if (q > best_mime_q)
193 13 : best_mime_q = q;
194 : }
195 11 : for (struct Terms *p = legal->terms_head;
196 77 : NULL != p;
197 66 : p = p->next)
198 : {
199 : double q;
200 :
201 66 : q = TALER_pattern_matches (mime,
202 : p->mime_type);
203 66 : if (q < best_mime_q)
204 38 : continue;
205 28 : q = TALER_pattern_matches (lang,
206 28 : p->language);
207 : /* create 'available-languages' (for this mime-type) */
208 28 : if (NULL == langs)
209 : {
210 11 : langs = GNUNET_strdup (p->language);
211 : }
212 17 : else if (NULL == strstr (langs,
213 17 : p->language))
214 : {
215 7 : char *tmp = langs;
216 :
217 7 : GNUNET_asprintf (&langs,
218 : "%s,%s",
219 : tmp,
220 : p->language);
221 7 : GNUNET_free (tmp);
222 : }
223 28 : if (NULL != t)
224 : {
225 17 : if (q < best_lang_q)
226 9 : 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 8 : if ( (q <= best_lang_q) &&
231 6 : (p->priority <= t->priority) )
232 6 : continue;
233 : }
234 13 : best_lang_q = q;
235 13 : t = p;
236 : }
237 11 : 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 11 : if (NULL != t)
246 : {
247 : const char *etag;
248 :
249 11 : etag = MHD_lookup_connection_value (conn,
250 : MHD_HEADER_KIND,
251 : MHD_HTTP_HEADER_IF_NONE_MATCH);
252 11 : if ( (NULL != etag) &&
253 0 : (NULL != t->terms_etag) &&
254 0 : (0 == strcasecmp (etag,
255 0 : t->terms_etag)) )
256 : {
257 : enum MHD_Result ret;
258 :
259 0 : resp = MHD_create_response_from_buffer (0,
260 : NULL,
261 : MHD_RESPMEM_PERSISTENT);
262 0 : TALER_MHD_add_global_headers (resp,
263 : true);
264 0 : GNUNET_break (MHD_YES ==
265 : MHD_add_response_header (resp,
266 : MHD_HTTP_HEADER_EXPIRES,
267 : dat));
268 0 : GNUNET_break (MHD_YES ==
269 : MHD_add_response_header (resp,
270 : MHD_HTTP_HEADER_ETAG,
271 : t->terms_etag));
272 0 : if (NULL != legal)
273 0 : GNUNET_break (MHD_YES ==
274 : MHD_add_response_header (resp,
275 : TALER_TERMS_VERSION,
276 : legal->terms_version));
277 0 : ret = MHD_queue_response (conn,
278 : MHD_HTTP_NOT_MODIFIED,
279 : resp);
280 0 : GNUNET_break (MHD_YES == ret);
281 0 : MHD_destroy_response (resp);
282 0 : GNUNET_free (langs);
283 0 : return ret;
284 : }
285 : }
286 :
287 11 : if (NULL == t)
288 0 : t = &none; /* 501 if not configured */
289 :
290 11 : return_t:
291 : /* try to compress the response */
292 19 : resp = NULL;
293 19 : if ( (TALER_MHD_CT_DEFLATE ==
294 19 : TALER_MHD_can_compress (conn,
295 0 : TALER_MHD_CT_DEFLATE)) &&
296 0 : (NULL != t->compressed_terms) )
297 : {
298 0 : resp = MHD_create_response_from_buffer (t->compressed_terms_size,
299 : t->compressed_terms,
300 : MHD_RESPMEM_PERSISTENT);
301 0 : if (MHD_NO ==
302 0 : MHD_add_response_header (resp,
303 : MHD_HTTP_HEADER_CONTENT_ENCODING,
304 : "deflate"))
305 : {
306 0 : GNUNET_break (0);
307 0 : MHD_destroy_response (resp);
308 0 : resp = NULL;
309 : }
310 : }
311 19 : if (NULL == resp)
312 : {
313 : /* could not generate compressed response, return uncompressed */
314 19 : resp = MHD_create_response_from_buffer (t->terms_size,
315 : (void *) t->terms,
316 : MHD_RESPMEM_PERSISTENT);
317 : }
318 19 : TALER_MHD_add_global_headers (resp,
319 : true);
320 19 : GNUNET_break (MHD_YES ==
321 : MHD_add_response_header (resp,
322 : MHD_HTTP_HEADER_EXPIRES,
323 : dat));
324 19 : if (NULL != langs)
325 : {
326 11 : GNUNET_break (MHD_YES ==
327 : MHD_add_response_header (resp,
328 : "Avail-Languages",
329 : langs));
330 11 : GNUNET_free (langs);
331 : }
332 : /* Set cache control headers: our response varies depending on these headers */
333 19 : 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 19 : GNUNET_break (MHD_YES ==
341 : MHD_add_response_header (resp,
342 : MHD_HTTP_HEADER_CACHE_CONTROL,
343 : "public,max-age=864000"));
344 19 : if (NULL != t->terms_etag)
345 11 : GNUNET_break (MHD_YES ==
346 : MHD_add_response_header (resp,
347 : MHD_HTTP_HEADER_ETAG,
348 : t->terms_etag));
349 19 : if (NULL != legal)
350 11 : GNUNET_break (MHD_YES ==
351 : MHD_add_response_header (resp,
352 : TALER_TERMS_VERSION,
353 : legal->terms_version));
354 19 : GNUNET_break (MHD_YES ==
355 : MHD_add_response_header (resp,
356 : MHD_HTTP_HEADER_CONTENT_TYPE,
357 : t->mime_type));
358 19 : 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 19 : ret = MHD_queue_response (conn,
366 : t == &none
367 : ? MHD_HTTP_NOT_IMPLEMENTED
368 : : MHD_HTTP_OK,
369 : resp);
370 19 : MHD_destroy_response (resp);
371 19 : 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 6 : 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 6 : const char *ext = strrchr (name, '.');
411 : const char *mime;
412 : unsigned int priority;
413 :
414 6 : if (NULL == ext)
415 : {
416 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
417 : "Unsupported file `%s' in directory `%s/%s': lacks extension\n",
418 : name,
419 : path,
420 : lang);
421 0 : return;
422 : }
423 6 : if ( (NULL == legal->terms_version) ||
424 6 : (0 != strncmp (legal->terms_version,
425 : name,
426 6 : ext - name)) )
427 : {
428 0 : 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 0 : return;
435 : }
436 6 : mime = NULL;
437 18 : for (unsigned int i = 0; NULL != mm[i].ext; i++)
438 18 : if (0 == strcasecmp (mm[i].ext,
439 : ext))
440 : {
441 6 : mime = mm[i].mime;
442 6 : priority = mm[i].priority;
443 6 : break;
444 : }
445 6 : if (NULL == mime)
446 : {
447 0 : 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 0 : 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 6 : GNUNET_asprintf (&fn,
462 : "%s/%s/%s",
463 : path,
464 : lang,
465 : name);
466 6 : fd = open (fn, O_RDONLY);
467 6 : if (-1 == fd)
468 : {
469 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
470 : "open",
471 : fn);
472 0 : GNUNET_free (fn);
473 0 : return;
474 : }
475 6 : if (0 != fstat (fd, &st))
476 : {
477 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
478 : "fstat",
479 : fn);
480 0 : GNUNET_break (0 == close (fd));
481 0 : GNUNET_free (fn);
482 0 : 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 6 : 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 6 : bsize = (size_t) st.st_size;
501 6 : buf = mmap (NULL,
502 : bsize,
503 : PROT_READ,
504 : MAP_SHARED,
505 : fd,
506 : 0);
507 6 : if (MAP_FAILED == buf)
508 : {
509 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
510 : "mmap",
511 : fn);
512 0 : GNUNET_break (0 == close (fd));
513 0 : GNUNET_free (fn);
514 0 : return;
515 : }
516 6 : GNUNET_break (0 == close (fd));
517 6 : 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 6 : GNUNET_CRYPTO_hash (buf,
525 : bsize,
526 : &hc);
527 6 : t = GNUNET_new (struct Terms);
528 6 : t->mime_type = mime;
529 6 : t->terms = buf;
530 6 : t->language = GNUNET_strdup (lang);
531 6 : t->terms_size = bsize;
532 6 : t->priority = priority;
533 : t->terms_etag
534 6 : = GNUNET_STRINGS_data_to_string_alloc (&hc,
535 : sizeof (hc) / 2);
536 6 : buf = GNUNET_memdup (t->terms,
537 : t->terms_size);
538 6 : if (TALER_MHD_body_compress (&buf,
539 : &bsize))
540 : {
541 0 : t->compressed_terms = buf;
542 0 : t->compressed_terms_size = bsize;
543 : }
544 : else
545 : {
546 6 : GNUNET_free (buf);
547 : }
548 : {
549 6 : struct Terms *prev = NULL;
550 :
551 6 : for (struct Terms *pos = legal->terms_head;
552 15 : NULL != pos;
553 9 : pos = pos->next)
554 : {
555 12 : if (pos->priority < priority)
556 3 : break;
557 9 : prev = pos;
558 : }
559 6 : 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 2 : load_language (struct TALER_MHD_Legal *legal,
579 : const char *path,
580 : const char *lang)
581 : {
582 : char *dname;
583 : DIR *d;
584 :
585 2 : GNUNET_asprintf (&dname,
586 : "%s/%s",
587 : path,
588 : lang);
589 2 : d = opendir (dname);
590 2 : if (NULL == d)
591 : {
592 0 : GNUNET_free (dname);
593 0 : return;
594 : }
595 2 : for (struct dirent *de = readdir (d);
596 12 : NULL != de;
597 10 : de = readdir (d))
598 : {
599 10 : const char *fn = de->d_name;
600 :
601 10 : if (fn[0] == '.')
602 4 : continue;
603 6 : load_terms (legal,
604 : path,
605 : lang,
606 : fn);
607 : }
608 2 : GNUNET_break (0 == closedir (d));
609 2 : GNUNET_free (dname);
610 : }
611 :
612 :
613 : struct TALER_MHD_Legal *
614 37 : 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 37 : legal = GNUNET_new (struct TALER_MHD_Legal);
624 37 : if (GNUNET_OK !=
625 37 : GNUNET_CONFIGURATION_get_value_string (cfg,
626 : section,
627 : tagoption,
628 : &legal->terms_version))
629 : {
630 0 : GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
631 : section,
632 : tagoption);
633 0 : GNUNET_free (legal);
634 0 : return NULL;
635 : }
636 37 : if (GNUNET_OK !=
637 37 : GNUNET_CONFIGURATION_get_value_filename (cfg,
638 : section,
639 : diroption,
640 : &path))
641 : {
642 0 : GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
643 : section,
644 : diroption);
645 0 : GNUNET_free (legal->terms_version);
646 0 : GNUNET_free (legal);
647 0 : return NULL;
648 : }
649 37 : d = opendir (path);
650 37 : if (NULL == d)
651 : {
652 36 : GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING,
653 : section,
654 : diroption,
655 : "Could not open directory");
656 36 : GNUNET_free (legal->terms_version);
657 36 : GNUNET_free (legal);
658 36 : GNUNET_free (path);
659 36 : return NULL;
660 : }
661 1 : for (struct dirent *de = readdir (d);
662 5 : NULL != de;
663 4 : de = readdir (d))
664 : {
665 4 : const char *lang = de->d_name;
666 :
667 4 : if (lang[0] == '.')
668 2 : continue;
669 2 : if (0 == strcmp (lang,
670 : "locale"))
671 0 : continue;
672 2 : load_language (legal,
673 : path,
674 : lang);
675 : }
676 1 : GNUNET_break (0 == closedir (d));
677 1 : GNUNET_free (path);
678 1 : return legal;
679 : }
680 :
681 :
682 : void
683 1 : TALER_MHD_legal_free (struct TALER_MHD_Legal *legal)
684 : {
685 : struct Terms *t;
686 1 : if (NULL == legal)
687 0 : return;
688 7 : while (NULL != (t = legal->terms_head))
689 : {
690 6 : GNUNET_free (t->language);
691 6 : GNUNET_free (t->compressed_terms);
692 6 : if (0 != munmap (t->terms, t->terms_size))
693 0 : GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING,
694 : "munmap");
695 6 : GNUNET_CONTAINER_DLL_remove (legal->terms_head,
696 : legal->terms_tail,
697 : t);
698 6 : GNUNET_free (t->terms_etag);
699 6 : GNUNET_free (t);
700 : }
701 1 : GNUNET_free (legal->terms_version);
702 1 : GNUNET_free (legal);
703 : }
|