From 75240345d025ce448b7248a4ec693261ecf8a203 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Tue, 10 Dec 2019 21:20:38 +0100 Subject: [PATCH] writing generic tos/pp handler logic --- src/include/taler_mhd_lib.h | 51 ++++ src/mhd/Makefile.am | 1 + src/mhd/mhd_legal.c | 592 ++++++++++++++++++++++++++++++++++++ 3 files changed, 644 insertions(+) create mode 100644 src/mhd/mhd_legal.c diff --git a/src/include/taler_mhd_lib.h b/src/include/taler_mhd_lib.h index cdbc8d290..6aefbd778 100644 --- a/src/include/taler_mhd_lib.h +++ b/src/include/taler_mhd_lib.h @@ -390,4 +390,55 @@ TALER_MHD_bind (const struct GNUNET_CONFIGURATION_Handle *cfg, const char *section, uint16_t *port); + +/** + * Prepared responses for legal documents + * (terms of service, privacy policy). + */ +struct TALER_MHD_Legal; + + +/** + * Load set of legal documents as specified in + * @a cfg in section @a section where the Etag + * is given under the @param tagoption and the + * directory under the @a diroption. + * + * @param cfg configuration to use + * @param section section to load values from + * @param diroption name of the option with the + * path to the legal documents + * @param tagoption name of the files to use + * for the legal documents and the Etag + * @return NULL on error + */ +struct TALER_MHD_Legal * +TALER_MHD_legal_load (const struct GNUNET_CONFIGURATION_Handle *cfg, + const char *section, + const char *diroption, + const char *tagoption); + + +/** + * Free set of legal documents + * + * @param leg legal documents to free + */ +void +TALER_MHD_legal_free (struct TALER_MHD_Legal *legal); + + +/** + * Generate a response with a legal document in + * the format and language of the user's choosing. + * + * @param conn HTTP connection to handle + * @param legal legal document to serve + * @return MHD result code + */ +int +TALER_MHD_reply_legal (struct MHD_Connection *conn, + struct TALER_MHD_Legal *legal); + + #endif diff --git a/src/mhd/Makefile.am b/src/mhd/Makefile.am index cc4c70735..ec0103f4b 100644 --- a/src/mhd/Makefile.am +++ b/src/mhd/Makefile.am @@ -11,6 +11,7 @@ lib_LTLIBRARIES = \ libtalermhd_la_SOURCES = \ mhd_config.c \ + mhd_legal.c \ mhd_parsing.c \ mhd_responses.c libtalermhd_la_LDFLAGS = \ diff --git a/src/mhd/mhd_legal.c b/src/mhd/mhd_legal.c new file mode 100644 index 000000000..0816f38ef --- /dev/null +++ b/src/mhd/mhd_legal.c @@ -0,0 +1,592 @@ +/* + This file is part of TALER + Copyright (C) 2019 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 +*/ +/** + * @file mhd_legal.h + * @brief API for returning legal documents based on client language + * and content type preferences + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include +#include +#include +#include "taler_mhd_lib.h" + + +/** + * Entry in the terms-of-service array. + */ +struct Terms +{ + /** + * Mime type of the terms. + */ + const char *mime_type; + + /** + * The terms (NOT 0-terminated!). + */ + void *terms; + + /** + * The desired language. + */ + char *language; + + /** + * Number of bytes in @e terms. + */ + size_t terms_size; +}; + + +/** + * Prepared responses for legal documents + * (terms of service, privacy policy). + */ +struct TALER_MHD_Legal +{ + /** + * Array of terms of service, terminated by NULL/0 value. + */ + struct Terms *terms; + + /** + * Length of the #terms array. + */ + unsigned int terms_len; + + /** + * Etag to use for the terms of service (= version). + */ + char *terms_etag; +}; + + +/** + * Check if @a mime matches the @a accept_pattern. + * + * @param accept_pattern a mime pattern like text/plain or image/ + * @param mime the mime type to match + * @return true if @a mime matches the @a accept_pattern + */ +static bool +mime_matches (const char *accept_pattern, + const char *mime) +{ + const char *da = strchr (accept_pattern, '/'); + const char *dm = strchr (mime, '/'); + + if ( (NULL == da) || + (NULL == dm) ) + return (0 == strcmp ("*", accept_pattern)); + return + ( ( (1 == da - accept_pattern) && + ('*' == *accept_pattern) ) || + ( (da - accept_pattern == dm - mime) && + (0 == strncasecmp (accept_pattern, + mime, + da - accept_pattern)) ) ) && + ( (0 == strcmp (da, "/*")) || + (0 == strcasecmp (da, + dm)) ); +} + + +/** + * Check if @a lang matches the @a language_pattern, and if so with + * which preference. + * + * @param language_pattern a language preferences string + * like "fr-CH, fr;q=0.9, en;q=0.8, *;q=0.1" + * @param lang the 2-digit language to match + * @return q-weight given for @a lang in @a language_pattern, 1.0 if no weights are given; + * 0 if @a lang is not in @a language_pattern + */ +static double +language_matches (const char *language_pattern, + const char *lang) +{ + char *p = GNUNET_strdup (language_pattern); + char *sptr; + double r = 0.0; + + for (char *tok = strtok_r (p, ", ", &sptr); + NULL != tok; + tok = strtok_r (NULL, ", ", &sptr)) + { + char *sptr2; + char *lp = strtok_r (tok, ";", &sptr2); + char *qp = strtok_r (NULL, ";", &sptr2); + double q = 1.0; + + GNUNET_break_op ( (NULL == qp) || + (1 == sscanf (qp, + "q=%lf", + &q)) ); + if (0 == strcasecmp (lang, + lp)) + r = GNUNET_MAX (r, q); + } + GNUNET_free (p); + return r; +} + + +/** + * Generate a response with a legal document in + * the format and language of the user's choosing. + * + * @param conn HTTP connection to handle + * @param legal legal document to serve + * @return MHD result code + */ +int +TALER_MHD_reply_legal (struct MHD_Connection *conn, + struct TALER_MHD_Legal *legal) +{ + struct MHD_Response *resp; + struct Terms *t; + + if (NULL != legal) + { + const char *etag; + + etag = MHD_lookup_connection_value (conn, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if ( (NULL != etag) && + (NULL != legal->terms_etag) && + (0 == strcasecmp (etag, + legal->terms_etag)) ) + { + int ret; + + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + ret = MHD_queue_response (conn, + MHD_HTTP_NOT_MODIFIED, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } + } + + t = NULL; + if (NULL != legal) + { + const char *mime; + const char *lang; + + mime = MHD_lookup_connection_value (conn, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "text/html"; + lang = MHD_lookup_connection_value (conn, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == lang) + lang = "en"; + /* Find best match: must match mime type (if possible), and if + mime type matches, ideally also language */ + for (unsigned int i = 0; i < legal->terms_len; i++) + { + struct Terms *p = &legal->terms[i]; + + if ( (NULL == t) || + (mime_matches (mime, + p->mime_type)) ) + { + if ( (NULL == t) || + (! mime_matches (mime, + t->mime_type)) || + (language_matches (lang, + p->language) > + language_matches (lang, + t->language) ) ) + t = p; + } + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Best match for %s/%s: %s / %s\n", + lang, + mime, + (NULL != t) ? t->mime_type : "", + (NULL != t) ? t->language : ""); + } + + if (NULL == t) + { + /* Default terms of service if none are configured */ + static struct Terms none = { + .mime_type = "text/plain", + .terms = "not configured", + .language = "en", + .terms_size = strlen ("not configured") + }; + + t = &none; + } + + /* try to compress the response */ + resp = NULL; + if (MHD_YES == + TALER_MHD_can_compress (conn)) + { + void *buf = GNUNET_memdup (t->terms, + t->terms_size); + size_t buf_size = t->terms_size; + + if (TALER_MHD_body_compress (&buf, + &buf_size)) + { + resp = MHD_create_response_from_buffer (buf_size, + buf, + MHD_RESPMEM_MUST_FREE); + if (MHD_NO == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_CONTENT_ENCODING, + "deflate")) + { + GNUNET_break (0); + MHD_destroy_response (resp); + resp = NULL; + } + } + else + { + GNUNET_free (buf); + } + } + if (NULL == resp) + { + /* could not generate compressed response, return uncompressed */ + resp = MHD_create_response_from_buffer (t->terms_size, + (void *) t->terms, + MHD_RESPMEM_PERSISTENT); + } + if (NULL != legal) + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_ETAG, + legal->terms_etag)); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + t->mime_type)); + { + int ret; + + ret = MHD_queue_response (conn, + MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + return ret; + } +} + + +/** + * Load all the terms of service from @a path under language @a lang + * from file @a name + * + * @param legal[in,out] 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_MHD_Legal *legal, + const char *path, + const char *lang, + const char *name) +{ + static struct MimeMap + { + const char *ext; + const char *mime; + } mm[] = { + { .ext = ".html", .mime = "text/html" }, + { .ext = ".htm", .mime = "text/html" }, + { .ext = ".txt", .mime = "text/plain" }, + { .ext = ".pdf", .mime = "application/pdf" }, + { .ext = ".jpg", .mime = "image/jpeg" }, + { .ext = ".jpeg", .mime = "image/jpeg" }, + { .ext = ".png", .mime = "image/png" }, + { .ext = ".gif", .mime = "image/gif" }, + { .ext = NULL, .mime = NULL } + }; + const char *ext = strrchr (name, '.'); + const char *mime; + + 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_etag) || + (0 != strncmp (legal->terms_etag, + name, + ext - name - 1)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Filename `%s' does not match Etag `%s' in directory `%s/%s'. Ignoring it.\n", + name, + legal->terms_etag, + 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; + 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; + } + GNUNET_free (fn); + if (0 != fstat (fd, &st)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, + "fstat", + fn); + (void) 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); + (void) close (fd); + GNUNET_free (fn); + return; + } + { + char *buf; + size_t bsize; + ssize_t ret; + + bsize = (size_t) st.st_size; + buf = GNUNET_malloc_large (bsize); + if (NULL == buf) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, + "malloc"); + (void) close (fd); + GNUNET_free (fn); + return; + } + ret = read (fd, + buf, + bsize); + if ( (ret < 0) || + (bsize != ((size_t) ret)) ) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, + "read", + fn); + (void) close (fd); + GNUNET_free (buf); + GNUNET_free (fn); + return; + } + (void) close (fd); + GNUNET_free (fn); + + /* append to global list of terms of service */ + { + struct Terms t = { + .mime_type = mime, + .terms = buf, + .language = GNUNET_strdup (lang), + .terms_size = bsize + }; + + GNUNET_array_append (legal->terms, + legal->terms_len, + t); + } + } + } +} + + +/** + * Load all the terms of service from @a path under language @a lang. + * + * @param legal[in,out] 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_MHD_Legal *legal, + const char *path, + const char *lang) +{ + char *dname; + DIR *d; + + GNUNET_asprintf (&dname, + "%s/%s", + path, + lang); + d = opendir (dname); + 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); + } + closedir (d); + free (dname); +} + + +/** + * Load set of legal documents as specified in + * @a cfg in section @a section where the Etag + * is given under the @param tagoption and the + * directory under the @a diroption. + * + * @param cfg configuration to use + * @param section section to load values from + * @param diroption name of the option with the + * path to the legal documents + * @param tagoption name of the files to use + * for the legal documents and the Etag + * @return NULL on error + */ +struct TALER_MHD_Legal * +TALER_MHD_legal_load (const struct GNUNET_CONFIGURATION_Handle *cfg, + const char *section, + const char *diroption, + const char *tagoption) +{ + struct TALER_MHD_Legal *legal; + char *path; + DIR *d; + + legal = GNUNET_new (struct TALER_MHD_Legal); + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + tagoption, + &legal->terms_etag)) + { + 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_etag); + GNUNET_free (legal); + return NULL; + } + d = opendir (path); + for (struct dirent *de = readdir (d); + NULL != de; + de = readdir (d)) + { + const char *lang = de->d_name; + + if (lang[0] == '.') + continue; + load_language (legal, path, lang); + } + closedir (d); + free (path); + return legal; +} + + +/** + * Free set of legal documents + * + * @param leg legal documents to free + */ +void +TALER_MHD_legal_free (struct TALER_MHD_Legal *legal) +{ + if (NULL == legal) + return; + for (unsigned int i = 0; iterms_len; i++) + { + struct Terms *t = &legal->terms[i]; + + GNUNET_free (t->language); + GNUNET_free (t->terms); + } + GNUNET_array_grow (legal->terms, + legal->terms_len, + 0); + GNUNET_free (legal->terms_etag); + GNUNET_free (legal); +}