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