commit d659e359b1a8141e23fe691d5e8a435a5d55f916
parent 13e058a902a3dbee9d7fe327030b88c2d126675b
Author: Christian Grothoff <christian@grothoff.org>
Date: Sun, 2 Mar 2025 01:22:13 +0100
port mhd_legal.c
Diffstat:
2 files changed, 689 insertions(+), 0 deletions(-)
diff --git a/src/mhd/Makefile.am b/src/mhd/Makefile.am
@@ -37,6 +37,7 @@ lib_LTLIBRARIES += \
libtalermhd2_la_SOURCES = \
mhd_config.c \
+ mhd2_legal.c \
mhd2_responses.c \
mhd2_run.c
libtalermhd2_la_LDFLAGS = \
diff --git a/src/mhd/mhd2_legal.c b/src/mhd/mhd2_legal.c
@@ -0,0 +1,688 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2019--2025 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file mhd2_legal.c
+ * @brief API for returning legal documents based on client language
+ * and content type preferences
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <jansson.h>
+#include <microhttpd2.h>
+#include "taler_util.h"
+#include "taler_mhd2_lib.h"
+
+/**
+ * How long should browsers/proxies cache the "legal" replies?
+ */
+#define MAX_TERMS_CACHING GNUNET_TIME_UNIT_DAYS
+
+/**
+ * HTTP header with the version of the terms of service.
+ */
+#define TALER_TERMS_VERSION "Taler-Terms-Version"
+
+/**
+ * Entry in the terms-of-service array.
+ */
+struct Terms
+{
+ /**
+ * Kept in a DLL.
+ */
+ struct Terms *prev;
+
+ /**
+ * Kept in a DLL.
+ */
+ struct Terms *next;
+
+ /**
+ * Mime type of the terms.
+ */
+ const char *mime_type;
+
+ /**
+ * The terms (NOT 0-terminated!), mmap()'ed. Do not free,
+ * use munmap() instead.
+ */
+ void *terms;
+
+ /**
+ * The desired language.
+ */
+ char *language;
+
+ /**
+ * deflated @e terms, to return if client supports deflate compression.
+ * malloc()'ed. NULL if @e terms does not compress.
+ */
+ void *compressed_terms;
+
+ /**
+ * Etag we use for this response.
+ */
+ char *terms_etag;
+
+ /**
+ * Number of bytes in @e terms.
+ */
+ size_t terms_size;
+
+ /**
+ * Number of bytes in @e compressed_terms.
+ */
+ size_t compressed_terms_size;
+
+ /**
+ * Sorting key by format preference in case
+ * everything else is equal. Higher is preferred.
+ */
+ unsigned int priority;
+
+};
+
+
+/**
+ * Prepared responses for legal documents
+ * (terms of service, privacy policy).
+ */
+struct TALER_MHD2_Legal
+{
+ /**
+ * DLL of terms of service.
+ */
+ struct Terms *terms_head;
+
+ /**
+ * DLL of terms of service.
+ */
+ struct Terms *terms_tail;
+
+ /**
+ * Etag to use for the terms of service (= version).
+ */
+ char *terms_version;
+};
+
+
+const struct MHD_Action *
+TALER_MHD2_reply_legal (struct MHD_Request *request,
+ struct TALER_MHD2_Legal *legal)
+{
+ /* Default terms of service if none are configured */
+ static struct Terms none = {
+ .mime_type = "text/plain",
+ .terms = (void *) "not configured",
+ .language = (void *) "en",
+ .terms_size = strlen ("not configured")
+ };
+ struct MHD_Response *resp;
+ struct Terms *t;
+ struct GNUNET_TIME_Absolute a;
+ struct GNUNET_TIME_Timestamp m;
+ char dat[128];
+ char *langs;
+
+ t = NULL;
+ langs = NULL;
+ a = GNUNET_TIME_relative_to_absolute (MAX_TERMS_CACHING);
+ m = GNUNET_TIME_absolute_to_timestamp (a);
+ /* Round up to next full day to ensure the expiration
+ time does not become a fingerprint! */
+ a = GNUNET_TIME_absolute_round_down (a,
+ MAX_TERMS_CACHING);
+ a = GNUNET_TIME_absolute_add (a,
+ MAX_TERMS_CACHING);
+ TALER_MHD2_get_date_string (m.abs_time,
+ dat);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Setting '%s' header to '%s'\n",
+ MHD_HTTP_HEADER_EXPIRES,
+ dat);
+ if (NULL == legal)
+ {
+ t = &none;
+ goto return_t;
+ }
+
+ if (NULL != legal)
+ {
+ const struct MHD_StringNullable *mimen;
+ const struct MHD_StringNullable *langn;
+ const char *mime;
+ const char *lang;
+ double best_mime_q = 0.0;
+ double best_lang_q = 0.0;
+
+ mimen = MHD_request_get_value (request,
+ MHD_VK_HEADER,
+ MHD_HTTP_HEADER_ACCEPT);
+ if ( (NULL == mimen) ||
+ (NULL == mimen->cstr) )
+ mime = "text/plain";
+ else
+ mime = mimen->cstr;
+ langn = MHD_request_get_value (request,
+ MHD_VK_HEADER,
+ MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
+ if ( (NULL == langn) ||
+ (NULL == langn->cstr) )
+ lang = "en";
+ else
+ lang = langn->cstr;
+ /* Find best match: must match mime type (if possible), and if
+ mime type matches, ideally also language */
+ for (struct Terms *p = legal->terms_head;
+ NULL != p;
+ p = p->next)
+ {
+ double q;
+
+ q = TALER_pattern_matches (mime,
+ p->mime_type);
+ if (q > best_mime_q)
+ best_mime_q = q;
+ }
+ for (struct Terms *p = legal->terms_head;
+ NULL != p;
+ p = p->next)
+ {
+ double q;
+
+ q = TALER_pattern_matches (mime,
+ p->mime_type);
+ if (q < best_mime_q)
+ continue;
+ if (NULL == langs)
+ {
+ langs = GNUNET_strdup (p->language);
+ }
+ else if (NULL == strstr (langs,
+ p->language))
+ {
+ char *tmp = langs;
+
+ GNUNET_asprintf (&langs,
+ "%s,%s",
+ tmp,
+ p->language);
+ GNUNET_free (tmp);
+ }
+ q = TALER_pattern_matches (langs,
+ p->language);
+ if (q < best_lang_q)
+ continue;
+ best_lang_q = q;
+ t = p;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Best match for %s/%s: %s / %s\n",
+ lang,
+ mime,
+ (NULL != t) ? t->mime_type : "<none>",
+ (NULL != t) ? t->language : "<none>");
+ }
+
+ if (NULL != t)
+ {
+ const struct MHD_StringNullable *etag;
+
+ etag = MHD_request_get_value (request,
+ MHD_VK_HEADER,
+ MHD_HTTP_HEADER_IF_NONE_MATCH);
+ if ( (NULL != etag) &&
+ (NULL != etag->cstr) &&
+ (NULL != t->terms_etag) &&
+ (0 == strcasecmp (etag->cstr,
+ t->terms_etag)) )
+ {
+ resp = MHD_response_from_empty (MHD_HTTP_STATUS_NOT_MODIFIED);
+ GNUNET_break (MHD_SC_OK ==
+ MHD_RESPONSE_SET_OPTIONS (
+ resp,
+ MHD_R_OPTION_HEAD_ONLY_RESPONSE (true)));
+ TALER_MHD2_add_global_headers (resp);
+ GNUNET_break (MHD_SC_OK ==
+ MHD_response_add_header (resp,
+ MHD_HTTP_HEADER_EXPIRES,
+ dat));
+ GNUNET_break (MHD_SC_OK ==
+ MHD_response_add_header (resp,
+ MHD_HTTP_HEADER_ETAG,
+ t->terms_etag));
+ if (NULL != legal)
+ GNUNET_break (MHD_SC_OK ==
+ MHD_response_add_header (resp,
+ TALER_TERMS_VERSION,
+ legal->terms_version));
+ return MHD_action_from_response (request,
+ resp);
+ }
+ }
+
+ if (NULL == t)
+ t = &none; /* 501 if not configured */
+
+return_t:
+ /* try to compress the response */
+ resp = NULL;
+ if ( (MHD_YES ==
+ TALER_MHD2_can_compress (request)) &&
+ (NULL != t->compressed_terms) )
+ {
+ resp = MHD_response_from_buffer_static (t == &none
+ ? MHD_HTTP_STATUS_NOT_IMPLEMENTED
+ : MHD_HTTP_STATUS_OK,
+ t->compressed_terms_size,
+ t->compressed_terms);
+ if (MHD_SC_OK ==
+ MHD_response_add_header (resp,
+ MHD_HTTP_HEADER_CONTENT_ENCODING,
+ "deflate"))
+ {
+ GNUNET_break (0);
+ MHD_response_destroy (resp);
+ resp = NULL;
+ }
+ }
+ if (NULL == resp)
+ {
+ /* could not generate compressed response, return uncompressed */
+ resp = MHD_response_from_buffer_static (t == &none
+ ? MHD_HTTP_STATUS_NOT_IMPLEMENTED
+ : MHD_HTTP_STATUS_OK,
+ t->terms_size,
+ (void *) t->terms);
+ }
+ TALER_MHD2_add_global_headers (resp);
+ GNUNET_break (MHD_SC_OK ==
+ MHD_response_add_header (resp,
+ MHD_HTTP_HEADER_EXPIRES,
+ dat));
+ if (NULL != langs)
+ {
+ GNUNET_break (MHD_SC_OK ==
+ MHD_response_add_header (resp,
+ "Avail-Languages",
+ langs));
+ GNUNET_free (langs);
+ }
+ /* Set cache control headers: our response varies depending on these headers */
+ GNUNET_break (MHD_SC_OK ==
+ MHD_response_add_header (resp,
+ MHD_HTTP_HEADER_VARY,
+ MHD_HTTP_HEADER_ACCEPT_LANGUAGE ","
+ MHD_HTTP_HEADER_ACCEPT ","
+ MHD_HTTP_HEADER_ACCEPT_ENCODING));
+ /* Information is always public, revalidate after 10 days */
+ GNUNET_break (MHD_SC_OK ==
+ MHD_response_add_header (resp,
+ MHD_HTTP_HEADER_CACHE_CONTROL,
+ "public,max-age=864000"));
+ if (NULL != t->terms_etag)
+ GNUNET_break (MHD_SC_OK ==
+ MHD_response_add_header (resp,
+ MHD_HTTP_HEADER_ETAG,
+ t->terms_etag));
+ if (NULL != legal)
+ GNUNET_break (MHD_SC_OK ==
+ MHD_response_add_header (resp,
+ TALER_TERMS_VERSION,
+ legal->terms_version));
+ GNUNET_break (MHD_SC_OK ==
+ MHD_response_add_header (resp,
+ MHD_HTTP_HEADER_CONTENT_TYPE,
+ t->mime_type));
+ GNUNET_break (MHD_SC_OK ==
+ MHD_response_add_header (resp,
+ MHD_HTTP_HEADER_CONTENT_LANGUAGE,
+ t->language));
+ return MHD_action_from_response (request,
+ resp);
+}
+
+
+/**
+ * Load all the terms of service from @a path under language @a lang
+ * from file @a name
+ *
+ * @param[in,out] legal where to write the result
+ * @param path where the terms are found
+ * @param lang which language directory to crawl
+ * @param name specific file to access
+ */
+static void
+load_terms (struct TALER_MHD2_Legal *legal,
+ const char *path,
+ const char *lang,
+ const char *name)
+{
+ static struct MimeMap
+ {
+ const char *ext;
+ const char *mime;
+ unsigned int priority;
+ } mm[] = {
+ { .ext = ".txt", .mime = "text/plain", .priority = 150 },
+ { .ext = ".html", .mime = "text/html", .priority = 100 },
+ { .ext = ".htm", .mime = "text/html", .priority = 99 },
+ { .ext = ".md", .mime = "text/markdown", .priority = 50 },
+ { .ext = ".pdf", .mime = "application/pdf", .priority = 25 },
+ { .ext = ".jpg", .mime = "image/jpeg" },
+ { .ext = ".jpeg", .mime = "image/jpeg" },
+ { .ext = ".png", .mime = "image/png" },
+ { .ext = ".gif", .mime = "image/gif" },
+ { .ext = ".epub", .mime = "application/epub+zip", .priority = 10 },
+ { .ext = ".xml", .mime = "text/xml", .priority = 10 },
+ { .ext = NULL, .mime = NULL }
+ };
+ const char *ext = strrchr (name, '.');
+ const char *mime;
+ unsigned int priority;
+
+ if (NULL == ext)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Unsupported file `%s' in directory `%s/%s': lacks extension\n",
+ name,
+ path,
+ lang);
+ return;
+ }
+ if ( (NULL == legal->terms_version) ||
+ (0 != strncmp (legal->terms_version,
+ name,
+ ext - name - 1)) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Filename `%s' does not match Etag `%s' in directory `%s/%s'. Ignoring it.\n",
+ name,
+ legal->terms_version,
+ path,
+ lang);
+ return;
+ }
+ mime = NULL;
+ for (unsigned int i = 0; NULL != mm[i].ext; i++)
+ if (0 == strcasecmp (mm[i].ext,
+ ext))
+ {
+ mime = mm[i].mime;
+ priority = mm[i].priority;
+ break;
+ }
+ if (NULL == mime)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Unsupported file extension `%s' of file `%s' in directory `%s/%s'\n",
+ ext,
+ name,
+ path,
+ lang);
+ return;
+ }
+ /* try to read the file with the terms of service */
+ {
+ struct stat st;
+ char *fn;
+ int fd;
+
+ GNUNET_asprintf (&fn,
+ "%s/%s/%s",
+ path,
+ lang,
+ name);
+ fd = open (fn, O_RDONLY);
+ if (-1 == fd)
+ {
+ GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
+ "open",
+ fn);
+ GNUNET_free (fn);
+ return;
+ }
+ if (0 != fstat (fd, &st))
+ {
+ GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
+ "fstat",
+ fn);
+ GNUNET_break (0 == close (fd));
+ GNUNET_free (fn);
+ return;
+ }
+ if (SIZE_MAX < ((unsigned long long) st.st_size))
+ {
+ GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
+ "fstat-size",
+ fn);
+ GNUNET_break (0 == close (fd));
+ GNUNET_free (fn);
+ return;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Loading legal information from file `%s'\n",
+ fn);
+ {
+ void *buf;
+ size_t bsize;
+
+ bsize = (size_t) st.st_size;
+ buf = mmap (NULL,
+ bsize,
+ PROT_READ,
+ MAP_SHARED,
+ fd,
+ 0);
+ if (MAP_FAILED == buf)
+ {
+ GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
+ "mmap",
+ fn);
+ GNUNET_break (0 == close (fd));
+ GNUNET_free (fn);
+ return;
+ }
+ GNUNET_break (0 == close (fd));
+ GNUNET_free (fn);
+
+ /* insert into global list of terms of service */
+ {
+ struct Terms *t;
+ struct GNUNET_HashCode hc;
+
+ GNUNET_CRYPTO_hash (buf,
+ bsize,
+ &hc);
+ t = GNUNET_new (struct Terms);
+ t->mime_type = mime;
+ t->terms = buf;
+ t->language = GNUNET_strdup (lang);
+ t->terms_size = bsize;
+ t->priority = priority;
+ t->terms_etag
+ = GNUNET_STRINGS_data_to_string_alloc (&hc,
+ sizeof (hc) / 2);
+ buf = GNUNET_memdup (t->terms,
+ t->terms_size);
+ if (TALER_MHD2_body_compress (&buf,
+ &bsize))
+ {
+ t->compressed_terms = buf;
+ t->compressed_terms_size = bsize;
+ }
+ else
+ {
+ GNUNET_free (buf);
+ }
+ {
+ struct Terms *prev = NULL;
+
+ for (struct Terms *pos = legal->terms_head;
+ NULL != pos;
+ pos = pos->next)
+ {
+ if (pos->priority < priority)
+ break;
+ prev = pos;
+ }
+ GNUNET_CONTAINER_DLL_insert_after (legal->terms_head,
+ legal->terms_tail,
+ prev,
+ t);
+ }
+ }
+ }
+ }
+}
+
+
+/**
+ * Load all the terms of service from @a path under language @a lang.
+ *
+ * @param[in,out] legal where to write the result
+ * @param path where the terms are found
+ * @param lang which language directory to crawl
+ */
+static void
+load_language (struct TALER_MHD2_Legal *legal,
+ const char *path,
+ const char *lang)
+{
+ char *dname;
+ DIR *d;
+
+ GNUNET_asprintf (&dname,
+ "%s/%s",
+ path,
+ lang);
+ d = opendir (dname);
+ if (NULL == d)
+ {
+ GNUNET_free (dname);
+ return;
+ }
+ for (struct dirent *de = readdir (d);
+ NULL != de;
+ de = readdir (d))
+ {
+ const char *fn = de->d_name;
+
+ if (fn[0] == '.')
+ continue;
+ load_terms (legal,
+ path,
+ lang,
+ fn);
+ }
+ GNUNET_break (0 == closedir (d));
+ GNUNET_free (dname);
+}
+
+
+struct TALER_MHD2_Legal *
+TALER_MHD2_legal_load (const struct GNUNET_CONFIGURATION_Handle *cfg,
+ const char *section,
+ const char *diroption,
+ const char *tagoption)
+{
+ struct TALER_MHD2_Legal *legal;
+ char *path;
+ DIR *d;
+
+ legal = GNUNET_new (struct TALER_MHD2_Legal);
+ if (GNUNET_OK !=
+ GNUNET_CONFIGURATION_get_value_string (cfg,
+ section,
+ tagoption,
+ &legal->terms_version))
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
+ section,
+ tagoption);
+ GNUNET_free (legal);
+ return NULL;
+ }
+ if (GNUNET_OK !=
+ GNUNET_CONFIGURATION_get_value_filename (cfg,
+ section,
+ diroption,
+ &path))
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
+ section,
+ diroption);
+ GNUNET_free (legal->terms_version);
+ GNUNET_free (legal);
+ return NULL;
+ }
+ d = opendir (path);
+ if (NULL == d)
+ {
+ GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING,
+ section,
+ diroption,
+ "Could not open directory");
+ GNUNET_free (legal->terms_version);
+ GNUNET_free (legal);
+ GNUNET_free (path);
+ return NULL;
+ }
+ for (struct dirent *de = readdir (d);
+ NULL != de;
+ de = readdir (d))
+ {
+ const char *lang = de->d_name;
+
+ if (lang[0] == '.')
+ continue;
+ if (0 == strcmp (lang,
+ "locale"))
+ continue;
+ load_language (legal,
+ path,
+ lang);
+ }
+ GNUNET_break (0 == closedir (d));
+ GNUNET_free (path);
+ return legal;
+}
+
+
+void
+TALER_MHD2_legal_free (struct TALER_MHD2_Legal *legal)
+{
+ struct Terms *t;
+ if (NULL == legal)
+ return;
+ while (NULL != (t = legal->terms_head))
+ {
+ GNUNET_free (t->language);
+ GNUNET_free (t->compressed_terms);
+ if (0 != munmap (t->terms, t->terms_size))
+ GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING,
+ "munmap");
+ GNUNET_CONTAINER_DLL_remove (legal->terms_head,
+ legal->terms_tail,
+ t);
+ GNUNET_free (t->terms_etag);
+ GNUNET_free (t);
+ }
+ GNUNET_free (legal->terms_version);
+ GNUNET_free (legal);
+}