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