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 ( (TALER_MHD_CT_DEFLATE ==
284 10 : TALER_MHD_can_compress (conn,
285 0 : TALER_MHD_CT_DEFLATE)) &&
286 0 : (NULL != t->compressed_terms) )
287 : {
288 0 : resp = MHD_create_response_from_buffer (t->compressed_terms_size,
289 : t->compressed_terms,
290 : MHD_RESPMEM_PERSISTENT);
291 0 : if (MHD_NO ==
292 0 : MHD_add_response_header (resp,
293 : MHD_HTTP_HEADER_CONTENT_ENCODING,
294 : "deflate"))
295 : {
296 0 : GNUNET_break (0);
297 0 : MHD_destroy_response (resp);
298 0 : resp = NULL;
299 : }
300 : }
301 10 : if (NULL == resp)
302 : {
303 : /* could not generate compressed response, return uncompressed */
304 10 : resp = MHD_create_response_from_buffer (t->terms_size,
305 : (void *) t->terms,
306 : MHD_RESPMEM_PERSISTENT);
307 : }
308 10 : TALER_MHD_add_global_headers (resp,
309 : true);
310 10 : GNUNET_break (MHD_YES ==
311 : MHD_add_response_header (resp,
312 : MHD_HTTP_HEADER_EXPIRES,
313 : dat));
314 10 : if (NULL != langs)
315 : {
316 0 : GNUNET_break (MHD_YES ==
317 : MHD_add_response_header (resp,
318 : "Avail-Languages",
319 : langs));
320 0 : GNUNET_free (langs);
321 : }
322 : /* Set cache control headers: our response varies depending on these headers */
323 10 : 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 10 : GNUNET_break (MHD_YES ==
331 : MHD_add_response_header (resp,
332 : MHD_HTTP_HEADER_CACHE_CONTROL,
333 : "public,max-age=864000"));
334 10 : if (NULL != t->terms_etag)
335 0 : GNUNET_break (MHD_YES ==
336 : MHD_add_response_header (resp,
337 : MHD_HTTP_HEADER_ETAG,
338 : t->terms_etag));
339 10 : if (NULL != legal)
340 0 : GNUNET_break (MHD_YES ==
341 : MHD_add_response_header (resp,
342 : TALER_TERMS_VERSION,
343 : legal->terms_version));
344 10 : GNUNET_break (MHD_YES ==
345 : MHD_add_response_header (resp,
346 : MHD_HTTP_HEADER_CONTENT_TYPE,
347 : t->mime_type));
348 10 : 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 10 : ret = MHD_queue_response (conn,
356 : t == &none
357 : ? MHD_HTTP_NOT_IMPLEMENTED
358 : : MHD_HTTP_OK,
359 : resp);
360 10 : MHD_destroy_response (resp);
361 10 : 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 0 : 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 0 : const char *ext = strrchr (name, '.');
401 : const char *mime;
402 : unsigned int priority;
403 :
404 0 : if (NULL == ext)
405 : {
406 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
407 : "Unsupported file `%s' in directory `%s/%s': lacks extension\n",
408 : name,
409 : path,
410 : lang);
411 0 : return;
412 : }
413 0 : if ( (NULL == legal->terms_version) ||
414 0 : (0 != strncmp (legal->terms_version,
415 : name,
416 0 : ext - name - 1)) )
417 : {
418 0 : 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 0 : return;
425 : }
426 0 : mime = NULL;
427 0 : for (unsigned int i = 0; NULL != mm[i].ext; i++)
428 0 : if (0 == strcasecmp (mm[i].ext,
429 : ext))
430 : {
431 0 : mime = mm[i].mime;
432 0 : priority = mm[i].priority;
433 0 : break;
434 : }
435 0 : if (NULL == mime)
436 : {
437 0 : 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 0 : 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 0 : GNUNET_asprintf (&fn,
452 : "%s/%s/%s",
453 : path,
454 : lang,
455 : name);
456 0 : fd = open (fn, O_RDONLY);
457 0 : if (-1 == fd)
458 : {
459 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
460 : "open",
461 : fn);
462 0 : GNUNET_free (fn);
463 0 : return;
464 : }
465 0 : if (0 != fstat (fd, &st))
466 : {
467 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
468 : "fstat",
469 : fn);
470 0 : GNUNET_break (0 == close (fd));
471 0 : GNUNET_free (fn);
472 0 : 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 0 : 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 0 : bsize = (size_t) st.st_size;
491 0 : buf = mmap (NULL,
492 : bsize,
493 : PROT_READ,
494 : MAP_SHARED,
495 : fd,
496 : 0);
497 0 : if (MAP_FAILED == buf)
498 : {
499 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
500 : "mmap",
501 : fn);
502 0 : GNUNET_break (0 == close (fd));
503 0 : GNUNET_free (fn);
504 0 : return;
505 : }
506 0 : GNUNET_break (0 == close (fd));
507 0 : 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 0 : GNUNET_CRYPTO_hash (buf,
515 : bsize,
516 : &hc);
517 0 : t = GNUNET_new (struct Terms);
518 0 : t->mime_type = mime;
519 0 : t->terms = buf;
520 0 : t->language = GNUNET_strdup (lang);
521 0 : t->terms_size = bsize;
522 0 : t->priority = priority;
523 : t->terms_etag
524 0 : = GNUNET_STRINGS_data_to_string_alloc (&hc,
525 : sizeof (hc) / 2);
526 0 : buf = GNUNET_memdup (t->terms,
527 : t->terms_size);
528 0 : if (TALER_MHD_body_compress (&buf,
529 : &bsize))
530 : {
531 0 : t->compressed_terms = buf;
532 0 : t->compressed_terms_size = bsize;
533 : }
534 : else
535 : {
536 0 : GNUNET_free (buf);
537 : }
538 : {
539 0 : struct Terms *prev = NULL;
540 :
541 0 : for (struct Terms *pos = legal->terms_head;
542 0 : NULL != pos;
543 0 : pos = pos->next)
544 : {
545 0 : if (pos->priority < priority)
546 0 : break;
547 0 : prev = pos;
548 : }
549 0 : 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 0 : load_language (struct TALER_MHD_Legal *legal,
569 : const char *path,
570 : const char *lang)
571 : {
572 : char *dname;
573 : DIR *d;
574 :
575 0 : GNUNET_asprintf (&dname,
576 : "%s/%s",
577 : path,
578 : lang);
579 0 : d = opendir (dname);
580 0 : if (NULL == d)
581 : {
582 0 : GNUNET_free (dname);
583 0 : return;
584 : }
585 0 : for (struct dirent *de = readdir (d);
586 0 : NULL != de;
587 0 : de = readdir (d))
588 : {
589 0 : const char *fn = de->d_name;
590 :
591 0 : if (fn[0] == '.')
592 0 : continue;
593 0 : load_terms (legal,
594 : path,
595 : lang,
596 : fn);
597 : }
598 0 : GNUNET_break (0 == closedir (d));
599 0 : GNUNET_free (dname);
600 : }
601 :
602 :
603 : struct TALER_MHD_Legal *
604 42 : 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 42 : legal = GNUNET_new (struct TALER_MHD_Legal);
614 42 : if (GNUNET_OK !=
615 42 : GNUNET_CONFIGURATION_get_value_string (cfg,
616 : section,
617 : tagoption,
618 : &legal->terms_version))
619 : {
620 0 : GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
621 : section,
622 : tagoption);
623 0 : GNUNET_free (legal);
624 0 : return NULL;
625 : }
626 42 : if (GNUNET_OK !=
627 42 : GNUNET_CONFIGURATION_get_value_filename (cfg,
628 : section,
629 : diroption,
630 : &path))
631 : {
632 0 : GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
633 : section,
634 : diroption);
635 0 : GNUNET_free (legal->terms_version);
636 0 : GNUNET_free (legal);
637 0 : return NULL;
638 : }
639 42 : d = opendir (path);
640 42 : if (NULL == d)
641 : {
642 42 : GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING,
643 : section,
644 : diroption,
645 : "Could not open directory");
646 42 : GNUNET_free (legal->terms_version);
647 42 : GNUNET_free (legal);
648 42 : GNUNET_free (path);
649 42 : return NULL;
650 : }
651 0 : for (struct dirent *de = readdir (d);
652 0 : NULL != de;
653 0 : de = readdir (d))
654 : {
655 0 : const char *lang = de->d_name;
656 :
657 0 : if (lang[0] == '.')
658 0 : continue;
659 0 : if (0 == strcmp (lang,
660 : "locale"))
661 0 : continue;
662 0 : load_language (legal,
663 : path,
664 : lang);
665 : }
666 0 : GNUNET_break (0 == closedir (d));
667 0 : GNUNET_free (path);
668 0 : return legal;
669 : }
670 :
671 :
672 : void
673 0 : TALER_MHD_legal_free (struct TALER_MHD_Legal *legal)
674 : {
675 : struct Terms *t;
676 0 : if (NULL == legal)
677 0 : return;
678 0 : while (NULL != (t = legal->terms_head))
679 : {
680 0 : GNUNET_free (t->language);
681 0 : GNUNET_free (t->compressed_terms);
682 0 : if (0 != munmap (t->terms, t->terms_size))
683 0 : GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING,
684 : "munmap");
685 0 : GNUNET_CONTAINER_DLL_remove (legal->terms_head,
686 : legal->terms_tail,
687 : t);
688 0 : GNUNET_free (t->terms_etag);
689 0 : GNUNET_free (t);
690 : }
691 0 : GNUNET_free (legal->terms_version);
692 0 : GNUNET_free (legal);
693 : }
|