diff --git a/src/exchange/Makefile.am b/src/exchange/Makefile.am index 8e1621821..fa359411c 100644 --- a/src/exchange/Makefile.am +++ b/src/exchange/Makefile.am @@ -55,6 +55,7 @@ taler_exchange_httpd_SOURCES = \ taler-exchange-httpd_reserve_status.c taler-exchange-httpd_reserve_status.h \ taler-exchange-httpd_reserve_withdraw.c taler-exchange-httpd_reserve_withdraw.h \ taler-exchange-httpd_responses.c taler-exchange-httpd_responses.h \ + taler-exchange-httpd_terms.c taler-exchange-httpd_terms.h \ taler-exchange-httpd_track_transaction.c taler-exchange-httpd_track_transaction.h \ taler-exchange-httpd_track_transfer.c taler-exchange-httpd_track_transfer.h \ taler-exchange-httpd_wire.c taler-exchange-httpd_wire.h \ diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c index bcac62ce9..e53555d64 100644 --- a/src/exchange/taler-exchange-httpd.c +++ b/src/exchange/taler-exchange-httpd.c @@ -36,6 +36,7 @@ #include "taler-exchange-httpd_refresh_link.h" #include "taler-exchange-httpd_refresh_melt.h" #include "taler-exchange-httpd_refresh_reveal.h" +#include "taler-exchange-httpd_terms.h" #include "taler-exchange-httpd_track_transfer.h" #include "taler-exchange-httpd_track_transaction.h" #include "taler-exchange-httpd_keystate.h" @@ -883,6 +884,7 @@ main (int argc, if (GNUNET_OK != exchange_serve_process_config ()) return 1; + TEH_load_terms (cfg); /* check for systemd-style FD passing */ listen_pid = getenv ("LISTEN_PID"); diff --git a/src/exchange/taler-exchange-httpd_terms.c b/src/exchange/taler-exchange-httpd_terms.c new file mode 100644 index 000000000..dadc1588f --- /dev/null +++ b/src/exchange/taler-exchange-httpd_terms.c @@ -0,0 +1,507 @@ +/* + 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 taler-exchange-httpd_terms.c + * @brief Handle /terms requests to return the terms of service + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include +#include +#include +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Entry in the terms-of-service array. + */ +struct Terms +{ + /** + * Mime type of the terms. + */ + const char *mime_type; + + /** + * The terms (NOT 0-terminated!). + */ + const void *terms; + + /** + * The desired language. + */ + const char *language; + + /** + * Number of bytes in @e terms. + */ + size_t terms_size; +}; + + +/** + * Array of terms of service, terminated by NULL/0 value. + */ +static struct Terms *terms; + +/** + * Length of the #terms array. + */ +static unsigned int terms_len; + +/** + * Etag to use for the terms of service (= version). + */ +static 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; +} + + +/** + * Handle a "/terms" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] connection_cls the connection's closure (can be updated) + * @param upload_data upload data + * @param[in,out] upload_data_size number of bytes (left) in @a upload_data + * @return MHD result code + */ +int +TEH_handler_terms (struct TEH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size) +{ + struct MHD_Response *resp; + struct Terms *t; + + (void) rh; + (void) upload_data; + (void) upload_data_size; + (void) connection_cls; + { + const char *etag; + + etag = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if ( (NULL != etag) && + (0 == strcasecmp (etag, + terms_etag)) ) + { + int ret; + + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + ret = MHD_queue_response (connection, + MHD_HTTP_NOT_MODIFIED, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } + } + + t = NULL; + { + const char *mime; + const char *lang; + + mime = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "text/html"; + lang = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == mime) + mime = "text/html"; + /* Find best match: must match mime type (if possible), and if + mime type matches, ideally also language */ + for (unsigned int i = 0; NULL != terms[i].terms; i++) + { + struct Terms *p = &terms[i]; + + if ( (NULL == t) || + (mime_matches (mime, + p->mime_type)) ) + { + if ( (NULL == t) || + (language_matches (lang, + p->mime_type) > + language_matches (lang, + t->mime_type) ) ) + t = p; + } + } + } + + if (NULL == t) + { + /* Default terms of service if none are configured */ + static struct Terms none = { + .mime_type = "text/plain", + .terms = "Terms of service not configured", + .language = "en", + .terms_size = sizeof ("Terms of service not configured") + }; + t = &none; + } + + /* try to compress the response */ + resp = NULL; + if (MHD_YES == + TALER_MHD_can_compress (connection)) + { + 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); + } + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_ETAG, + 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 (connection, + 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 path where the terms are found + * @param lang which language directory to crawl + * @param name specific file to access + */ +static void +load_terms (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 = 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; + } + 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 < 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 = lang, + .terms_size = bsize + }; + + GNUNET_array_append (terms, + terms_len, + t); + } + } + } +} + + +/** + * Load all the terms of service from @a path under language @a lang. + * + * @param path where the terms are found + * @param lang which language directory to crawl + */ +static void +load_language (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 (path, lang, fn); + } + closedir (d); + free (dname); +} + + +/** + * Load our terms of service as per configuration. + * + * @param cfg configuration to process + */ +void +TEH_load_terms (const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + char *path; + DIR *d; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_filename (cfg, + "exchange", + "terms", + &path)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, + "exchange", + "TERMS"); + + return; + } + 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 (path, lang); + } + closedir (d); + free (path); +} + + +/* end of taler-exchange-httpd_terms.c */ diff --git a/src/exchange/taler-exchange-httpd_terms.h b/src/exchange/taler-exchange-httpd_terms.h new file mode 100644 index 000000000..1cfd8239f --- /dev/null +++ b/src/exchange/taler-exchange-httpd_terms.h @@ -0,0 +1,49 @@ +/* + 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 taler-exchange-httpd_terms.h + * @brief Handle /terms requests to return the terms of service + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_TERMS_H +#define TALER_EXCHANGE_HTTPD_TERMS_H +#include "platform.h" +#include +#include +#include +#include +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * Handle a "/terms" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] connection_cls the connection's closure (can be updated) + * @param upload_data upload data + * @param[in,out] upload_data_size number of bytes (left) in @a upload_data + * @return MHD result code + */ +int +TEH_handler_terms (struct TEH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size); + +#endif