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