Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2019, 2020 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 : /**
37 : * Entry in the terms-of-service array.
38 : */
39 : struct Terms
40 : {
41 : /**
42 : * Kept in a DLL.
43 : */
44 : struct Terms *prev;
45 :
46 : /**
47 : * Kept in a DLL.
48 : */
49 : struct Terms *next;
50 :
51 : /**
52 : * Mime type of the terms.
53 : */
54 : const char *mime_type;
55 :
56 : /**
57 : * The terms (NOT 0-terminated!), mmap()'ed. Do not free,
58 : * use munmap() instead.
59 : */
60 : void *terms;
61 :
62 : /**
63 : * The desired language.
64 : */
65 : char *language;
66 :
67 : /**
68 : * deflated @e terms, to return if client supports deflate compression.
69 : * malloc()'ed. NULL if @e terms does not compress.
70 : */
71 : void *compressed_terms;
72 :
73 : /**
74 : * Number of bytes in @e terms.
75 : */
76 : size_t terms_size;
77 :
78 : /**
79 : * Number of bytes in @e compressed_terms.
80 : */
81 : size_t compressed_terms_size;
82 :
83 : /**
84 : * Sorting key by format preference in case
85 : * everything else is equal. Higher is preferred.
86 : */
87 : unsigned int priority;
88 :
89 : };
90 :
91 :
92 : /**
93 : * Prepared responses for legal documents
94 : * (terms of service, privacy policy).
95 : */
96 : struct TALER_MHD_Legal
97 : {
98 : /**
99 : * DLL of terms of service.
100 : */
101 : struct Terms *terms_head;
102 :
103 : /**
104 : * DLL of terms of service.
105 : */
106 : struct Terms *terms_tail;
107 :
108 : /**
109 : * Etag to use for the terms of service (= version).
110 : */
111 : char *terms_etag;
112 : };
113 :
114 :
115 : /**
116 : * Check if @a mime matches the @a accept_pattern.
117 : *
118 : * @param accept_pattern a mime pattern like "text/plain"
119 : * or "image/STAR"
120 : * @param mime the mime type to match
121 : * @return true if @a mime matches the @a accept_pattern
122 : */
123 : static bool
124 0 : mime_matches (const char *accept_pattern,
125 : const char *mime)
126 : {
127 0 : const char *da = strchr (accept_pattern, '/');
128 0 : const char *dm = strchr (mime, '/');
129 :
130 0 : if ( (NULL == da) ||
131 : (NULL == dm) )
132 0 : return (0 == strcmp ("*", accept_pattern));
133 : return
134 0 : ( ( (1 == da - accept_pattern) &&
135 0 : ('*' == *accept_pattern) ) ||
136 0 : ( (da - accept_pattern == dm - mime) &&
137 0 : (0 == strncasecmp (accept_pattern,
138 : mime,
139 0 : da - accept_pattern)) ) ) &&
140 0 : ( (0 == strcmp (da, "/*")) ||
141 0 : (0 == strcasecmp (da,
142 : dm)) );
143 : }
144 :
145 :
146 : bool
147 0 : TALER_MHD_xmime_matches (const char *accept_pattern,
148 : const char *mime)
149 : {
150 0 : char *ap = GNUNET_strdup (accept_pattern);
151 : char *sptr;
152 :
153 0 : for (const char *tok = strtok_r (ap, ";", &sptr);
154 : NULL != tok;
155 0 : tok = strtok_r (NULL, ";", &sptr))
156 : {
157 0 : if (mime_matches (tok,
158 : mime))
159 : {
160 0 : GNUNET_free (ap);
161 0 : return true;
162 : }
163 : }
164 0 : GNUNET_free (ap);
165 0 : return false;
166 : }
167 :
168 :
169 : MHD_RESULT
170 0 : TALER_MHD_reply_legal (struct MHD_Connection *conn,
171 : struct TALER_MHD_Legal *legal)
172 : {
173 : struct MHD_Response *resp;
174 : struct Terms *t;
175 : struct GNUNET_TIME_Absolute a;
176 : struct GNUNET_TIME_Timestamp m;
177 : char dat[128];
178 :
179 0 : a = GNUNET_TIME_relative_to_absolute (MAX_TERMS_CACHING);
180 0 : m = GNUNET_TIME_absolute_to_timestamp (a);
181 0 : TALER_MHD_get_date_string (m.abs_time,
182 : dat);
183 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
184 : "Setting 'Expires' header to '%s'\n",
185 : dat);
186 0 : if (NULL != legal)
187 : {
188 : const char *etag;
189 :
190 0 : etag = MHD_lookup_connection_value (conn,
191 : MHD_HEADER_KIND,
192 : MHD_HTTP_HEADER_IF_NONE_MATCH);
193 0 : if ( (NULL != etag) &&
194 0 : (NULL != legal->terms_etag) &&
195 0 : (0 == strcasecmp (etag,
196 0 : legal->terms_etag)) )
197 : {
198 : MHD_RESULT ret;
199 :
200 0 : resp = MHD_create_response_from_buffer (0,
201 : NULL,
202 : MHD_RESPMEM_PERSISTENT);
203 0 : TALER_MHD_add_global_headers (resp);
204 0 : GNUNET_break (MHD_YES ==
205 : MHD_add_response_header (resp,
206 : MHD_HTTP_HEADER_EXPIRES,
207 : dat));
208 :
209 0 : GNUNET_break (MHD_YES ==
210 : MHD_add_response_header (resp,
211 : MHD_HTTP_HEADER_ETAG,
212 : legal->terms_etag));
213 0 : ret = MHD_queue_response (conn,
214 : MHD_HTTP_NOT_MODIFIED,
215 : resp);
216 0 : GNUNET_break (MHD_YES == ret);
217 0 : MHD_destroy_response (resp);
218 0 : return ret;
219 : }
220 : }
221 :
222 0 : t = NULL;
223 0 : if (NULL != legal)
224 : {
225 : const char *mime;
226 : const char *lang;
227 :
228 0 : mime = MHD_lookup_connection_value (conn,
229 : MHD_HEADER_KIND,
230 : MHD_HTTP_HEADER_ACCEPT);
231 0 : if (NULL == mime)
232 0 : mime = "text/html";
233 0 : lang = MHD_lookup_connection_value (conn,
234 : MHD_HEADER_KIND,
235 : MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
236 0 : if (NULL == lang)
237 0 : lang = "en";
238 : /* Find best match: must match mime type (if possible), and if
239 : mime type matches, ideally also language */
240 0 : for (struct Terms *p = legal->terms_head;
241 : NULL != p;
242 0 : p = p->next)
243 : {
244 0 : if ( (NULL == t) ||
245 0 : (TALER_MHD_xmime_matches (mime,
246 : p->mime_type)) )
247 : {
248 0 : if ( (NULL == t) ||
249 0 : (! TALER_MHD_xmime_matches (mime,
250 0 : t->mime_type)) ||
251 0 : (TALER_language_matches (lang,
252 0 : p->language) >
253 0 : TALER_language_matches (lang,
254 0 : t->language) ) )
255 0 : t = p;
256 : }
257 : }
258 0 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
259 : "Best match for %s/%s: %s / %s\n",
260 : lang,
261 : mime,
262 : (NULL != t) ? t->mime_type : "<none>",
263 : (NULL != t) ? t->language : "<none>");
264 : }
265 :
266 0 : if (NULL == t)
267 : {
268 : /* Default terms of service if none are configured */
269 : static struct Terms none = {
270 : .mime_type = "text/plain",
271 : .terms = "not configured",
272 : .language = "en",
273 : .terms_size = strlen ("not configured")
274 : };
275 :
276 0 : t = &none;
277 : }
278 :
279 : /* try to compress the response */
280 0 : resp = NULL;
281 0 : if (MHD_YES ==
282 0 : TALER_MHD_can_compress (conn))
283 : {
284 0 : resp = MHD_create_response_from_buffer (t->compressed_terms_size,
285 : t->compressed_terms,
286 : MHD_RESPMEM_PERSISTENT);
287 0 : if (MHD_NO ==
288 0 : MHD_add_response_header (resp,
289 : MHD_HTTP_HEADER_CONTENT_ENCODING,
290 : "deflate"))
291 : {
292 0 : GNUNET_break (0);
293 0 : MHD_destroy_response (resp);
294 0 : resp = NULL;
295 : }
296 : }
297 0 : if (NULL == resp)
298 : {
299 : /* could not generate compressed response, return uncompressed */
300 0 : resp = MHD_create_response_from_buffer (t->terms_size,
301 : (void *) t->terms,
302 : MHD_RESPMEM_PERSISTENT);
303 : }
304 0 : TALER_MHD_add_global_headers (resp);
305 0 : GNUNET_break (MHD_YES ==
306 : MHD_add_response_header (resp,
307 : MHD_HTTP_HEADER_EXPIRES,
308 : dat));
309 : /* Set cache control headers: our response varies depending on these headers */
310 0 : GNUNET_break (MHD_YES ==
311 : MHD_add_response_header (resp,
312 : MHD_HTTP_HEADER_VARY,
313 : MHD_HTTP_HEADER_ACCEPT_LANGUAGE ","
314 : MHD_HTTP_HEADER_ACCEPT ","
315 : MHD_HTTP_HEADER_ACCEPT_ENCODING));
316 : /* Information is always public, revalidate after 10 days */
317 0 : GNUNET_break (MHD_YES ==
318 : MHD_add_response_header (resp,
319 : MHD_HTTP_HEADER_CACHE_CONTROL,
320 : "public max-age=864000"));
321 0 : if (NULL != legal)
322 0 : GNUNET_break (MHD_YES ==
323 : MHD_add_response_header (resp,
324 : MHD_HTTP_HEADER_ETAG,
325 : legal->terms_etag));
326 0 : GNUNET_break (MHD_YES ==
327 : MHD_add_response_header (resp,
328 : MHD_HTTP_HEADER_CONTENT_TYPE,
329 : t->mime_type));
330 : {
331 : MHD_RESULT ret;
332 :
333 0 : ret = MHD_queue_response (conn,
334 : MHD_HTTP_OK,
335 : resp);
336 0 : MHD_destroy_response (resp);
337 0 : return ret;
338 : }
339 : }
340 :
341 :
342 : /**
343 : * Load all the terms of service from @a path under language @a lang
344 : * from file @a name
345 : *
346 : * @param[in,out] legal where to write the result
347 : * @param path where the terms are found
348 : * @param lang which language directory to crawl
349 : * @param name specific file to access
350 : */
351 : static void
352 0 : load_terms (struct TALER_MHD_Legal *legal,
353 : const char *path,
354 : const char *lang,
355 : const char *name)
356 : {
357 : static struct MimeMap
358 : {
359 : const char *ext;
360 : const char *mime;
361 : unsigned int priority;
362 : } mm[] = {
363 : { .ext = ".html", .mime = "text/html", .priority = 100 },
364 : { .ext = ".htm", .mime = "text/html", .priority = 99 },
365 : { .ext = ".txt", .mime = "text/plain", .priority = 50 },
366 : { .ext = ".pdf", .mime = "application/pdf", .priority = 25 },
367 : { .ext = ".jpg", .mime = "image/jpeg" },
368 : { .ext = ".jpeg", .mime = "image/jpeg" },
369 : { .ext = ".png", .mime = "image/png" },
370 : { .ext = ".gif", .mime = "image/gif" },
371 : { .ext = ".epub", .mime = "application/epub+zip", .priority = 10 },
372 : { .ext = ".xml", .mime = "text/xml", .priority = 10 },
373 : { .ext = NULL, .mime = NULL }
374 : };
375 0 : const char *ext = strrchr (name, '.');
376 : const char *mime;
377 : unsigned int priority;
378 :
379 0 : if (NULL == ext)
380 : {
381 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
382 : "Unsupported file `%s' in directory `%s/%s': lacks extension\n",
383 : name,
384 : path,
385 : lang);
386 0 : return;
387 : }
388 0 : if ( (NULL == legal->terms_etag) ||
389 0 : (0 != strncmp (legal->terms_etag,
390 : name,
391 0 : ext - name - 1)) )
392 : {
393 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
394 : "Filename `%s' does not match Etag `%s' in directory `%s/%s'. Ignoring it.\n",
395 : name,
396 : legal->terms_etag,
397 : path,
398 : lang);
399 0 : return;
400 : }
401 0 : mime = NULL;
402 0 : for (unsigned int i = 0; NULL != mm[i].ext; i++)
403 0 : if (0 == strcasecmp (mm[i].ext,
404 : ext))
405 : {
406 0 : mime = mm[i].mime;
407 0 : priority = mm[i].priority;
408 0 : break;
409 : }
410 0 : if (NULL == mime)
411 : {
412 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
413 : "Unsupported file extension `%s' of file `%s' in directory `%s/%s'\n",
414 : ext,
415 : name,
416 : path,
417 : lang);
418 0 : return;
419 : }
420 : /* try to read the file with the terms of service */
421 : {
422 : struct stat st;
423 : char *fn;
424 : int fd;
425 :
426 0 : GNUNET_asprintf (&fn,
427 : "%s/%s/%s",
428 : path,
429 : lang,
430 : name);
431 0 : fd = open (fn, O_RDONLY);
432 0 : if (-1 == fd)
433 : {
434 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
435 : "open",
436 : fn);
437 0 : GNUNET_free (fn);
438 0 : return;
439 : }
440 0 : if (0 != fstat (fd, &st))
441 : {
442 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
443 : "fstat",
444 : fn);
445 0 : GNUNET_break (0 == close (fd));
446 0 : GNUNET_free (fn);
447 0 : return;
448 : }
449 : if (SIZE_MAX < ((unsigned long long) st.st_size))
450 : {
451 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
452 : "fstat-size",
453 : fn);
454 : GNUNET_break (0 == close (fd));
455 : GNUNET_free (fn);
456 : return;
457 : }
458 : {
459 : void *buf;
460 : size_t bsize;
461 :
462 0 : bsize = (size_t) st.st_size;
463 0 : buf = mmap (NULL,
464 : bsize,
465 : PROT_READ,
466 : MAP_SHARED,
467 : fd,
468 : 0);
469 0 : if (MAP_FAILED == buf)
470 : {
471 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
472 : "mmap",
473 : fn);
474 0 : GNUNET_break (0 == close (fd));
475 0 : GNUNET_free (fn);
476 0 : return;
477 : }
478 0 : GNUNET_break (0 == close (fd));
479 0 : GNUNET_free (fn);
480 :
481 : /* insert into global list of terms of service */
482 : {
483 : struct Terms *t;
484 :
485 0 : t = GNUNET_new (struct Terms);
486 0 : t->mime_type = mime;
487 0 : t->terms = buf;
488 0 : t->language = GNUNET_strdup (lang);
489 0 : t->terms_size = bsize;
490 0 : t->priority = priority;
491 0 : buf = GNUNET_memdup (t->terms,
492 : t->terms_size);
493 0 : if (TALER_MHD_body_compress (&buf,
494 : &bsize))
495 : {
496 0 : t->compressed_terms = buf;
497 0 : t->compressed_terms_size = bsize;
498 : }
499 : else
500 : {
501 0 : GNUNET_free (buf);
502 : }
503 : {
504 0 : struct Terms *prev = NULL;
505 :
506 0 : for (struct Terms *pos = legal->terms_head;
507 : NULL != pos;
508 0 : pos = pos->next)
509 : {
510 0 : if (pos->priority < priority)
511 0 : break;
512 0 : prev = pos;
513 : }
514 0 : GNUNET_CONTAINER_DLL_insert_after (legal->terms_head,
515 : legal->terms_tail,
516 : prev,
517 : t);
518 : }
519 : }
520 : }
521 : }
522 : }
523 :
524 :
525 : /**
526 : * Load all the terms of service from @a path under language @a lang.
527 : *
528 : * @param[in,out] legal where to write the result
529 : * @param path where the terms are found
530 : * @param lang which language directory to crawl
531 : */
532 : static void
533 0 : load_language (struct TALER_MHD_Legal *legal,
534 : const char *path,
535 : const char *lang)
536 : {
537 : char *dname;
538 : DIR *d;
539 :
540 0 : GNUNET_asprintf (&dname,
541 : "%s/%s",
542 : path,
543 : lang);
544 0 : d = opendir (dname);
545 0 : if (NULL == d)
546 : {
547 0 : GNUNET_free (dname);
548 0 : return;
549 : }
550 0 : for (struct dirent *de = readdir (d);
551 : NULL != de;
552 0 : de = readdir (d))
553 : {
554 0 : const char *fn = de->d_name;
555 :
556 0 : if (fn[0] == '.')
557 0 : continue;
558 0 : load_terms (legal, path, lang, fn);
559 : }
560 0 : GNUNET_break (0 == closedir (d));
561 0 : GNUNET_free (dname);
562 : }
563 :
564 :
565 : struct TALER_MHD_Legal *
566 0 : TALER_MHD_legal_load (const struct GNUNET_CONFIGURATION_Handle *cfg,
567 : const char *section,
568 : const char *diroption,
569 : const char *tagoption)
570 : {
571 : struct TALER_MHD_Legal *legal;
572 : char *path;
573 : DIR *d;
574 :
575 0 : legal = GNUNET_new (struct TALER_MHD_Legal);
576 0 : if (GNUNET_OK !=
577 0 : GNUNET_CONFIGURATION_get_value_string (cfg,
578 : section,
579 : tagoption,
580 : &legal->terms_etag))
581 : {
582 0 : GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
583 : section,
584 : tagoption);
585 0 : GNUNET_free (legal);
586 0 : return NULL;
587 : }
588 0 : if (GNUNET_OK !=
589 0 : GNUNET_CONFIGURATION_get_value_filename (cfg,
590 : section,
591 : diroption,
592 : &path))
593 : {
594 0 : GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
595 : section,
596 : diroption);
597 0 : GNUNET_free (legal->terms_etag);
598 0 : GNUNET_free (legal);
599 0 : return NULL;
600 : }
601 0 : d = opendir (path);
602 0 : if (NULL == d)
603 : {
604 0 : GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING,
605 : section,
606 : diroption,
607 : "Could not open directory");
608 0 : GNUNET_free (legal->terms_etag);
609 0 : GNUNET_free (legal);
610 0 : GNUNET_free (path);
611 0 : return NULL;
612 : }
613 0 : for (struct dirent *de = readdir (d);
614 : NULL != de;
615 0 : de = readdir (d))
616 : {
617 0 : const char *lang = de->d_name;
618 :
619 0 : if (lang[0] == '.')
620 0 : continue;
621 0 : load_language (legal, path, lang);
622 : }
623 0 : GNUNET_break (0 == closedir (d));
624 0 : GNUNET_free (path);
625 0 : return legal;
626 : }
627 :
628 :
629 : void
630 0 : TALER_MHD_legal_free (struct TALER_MHD_Legal *legal)
631 : {
632 : struct Terms *t;
633 0 : if (NULL == legal)
634 0 : return;
635 0 : while (NULL != (t = legal->terms_head))
636 : {
637 0 : GNUNET_free (t->language);
638 0 : GNUNET_free (t->compressed_terms);
639 0 : if (0 != munmap (t->terms, t->terms_size))
640 0 : GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING,
641 : "munmap");
642 0 : GNUNET_CONTAINER_DLL_remove (legal->terms_head,
643 : legal->terms_tail,
644 : t);
645 0 : GNUNET_free (t);
646 : }
647 0 : GNUNET_free (legal->terms_etag);
648 0 : GNUNET_free (legal);
649 : }
|