commit 758afecc1bf42cbfa498f10a9a9735e99758b605
parent 0d8f1fa2b4998d6d44164512c8aeb1e861ab0a98
Author: Christian Grothoff <christian@grothoff.org>
Date: Thu, 16 Nov 2023 15:24:24 +0100
add logic to serve AML SPA
Diffstat:
7 files changed, 564 insertions(+), 71 deletions(-)
diff --git a/bootstrap b/bootstrap
@@ -39,4 +39,17 @@ else
echo "Uncrustify not detected, hook not installed. Please install uncrustify if you plan on doing development"
fi
+
+# Generate Makefile.am in contrib/
+cd contrib
+rm -f Makefile.am
+find wallet-core/aml-backoffice/ -type f -printf ' %p \\\n' | sort > Makefile.am.ext
+# Remove extra '\' at the end of the file
+truncate -s -2 Makefile.am.ext
+cat Makefile.am.in Makefile.am.ext >> Makefile.am
+# Prevent accidental editing of the generated Makefile.am
+chmod -w Makefile.am
+cd ..
+
+echo "$0: Running autoreconf"
autoreconf -fi
diff --git a/contrib/Makefile.am b/contrib/Makefile.am
@@ -1,63 +0,0 @@
-# This file is in the public domain.
-
-SUBDIRS = .
-
-tmplpkgdatadir = $(datadir)/taler/exchange/templates/
-dist_tmplpkgdata_DATA = \
- persona-exchange-unauthorized.en.must \
- persona-load-failure.en.must \
- persona-exchange-unpaid.en.must \
- persona-logic-failure.en.must \
- persona-invalid-response.en.must \
- persona-network-timeout.en.must \
- persona-kyc-failed.en.must \
- persona-provider-failure.en.must
-
-termsdir=$(datadir)/taler/terms/
-terms_DATA = \
- exchange-tos-v0.rst \
- exchange-tos-bfh-v0.rst \
- exchange-pp-v0.rst
-
-install-exec-local:
- find locale/ -name "*.po"
- mkdir -p $(DESTDIR)$(datadir)
- cp --parents -r $$(find locale/ -name "*.po") $(DESTDIR)$(datadir)
-
-rdatadir=$(datadir)/taler/exchange
-rdata_DATA = \
- auditor-report.tex.j2
-
-bin_SCRIPTS = \
- taler-auditor-dbconfig \
- taler-exchange-dbconfig \
- taler-terms-generator \
- taler-bank-manage-testing \
- taler-nexus-prepare
-
-edit_script = $(SED) -e 's,%termsdir%,$(termsdir),'g -e 's,%localedir%,$(localedir),'g $(NULL)
-taler-terms-generator: taler-terms-generator.in
- rm -f $@ $@.tmp && \
- $(edit_script) $< >$@.tmp && \
- chmod a-w+x $@.tmp && \
- mv $@.tmp $@
-
-CLEANFILES = \
- taler-terms-generator
-
-EXTRA_DIST = \
- locale/de/LC_MESSAGES/exchange-tos-v0.po \
- taler-bank-manage-testing \
- taler-nexus-prepare \
- taler-terms-generator.in \
- taler-auditor-dbconfig \
- taler-exchange-dbconfig \
- gana-generate.sh \
- gana/gnu-taler-error-codes/registry.rec \
- gana/gnu-taler-error-codes/Makefile \
- $(terms_DATA) \
- $(rdata_DATA) \
- coverage.sh \
- gnunet.tag \
- microhttpd.tag \
- packages
diff --git a/contrib/Makefile.am.in b/contrib/Makefile.am.in
@@ -0,0 +1,71 @@
+# This file is in the public domain.
+
+SUBDIRS = .
+
+tmplpkgdatadir = $(datadir)/taler/exchange/templates/
+dist_tmplpkgdata_DATA = \
+ persona-exchange-unauthorized.en.must \
+ persona-load-failure.en.must \
+ persona-exchange-unpaid.en.must \
+ persona-logic-failure.en.must \
+ persona-invalid-response.en.must \
+ persona-network-timeout.en.must \
+ persona-kyc-failed.en.must \
+ persona-provider-failure.en.must
+
+termsdir=$(datadir)/taler/terms/
+terms_DATA = \
+ exchange-tos-v0.rst \
+ exchange-tos-bfh-v0.rst \
+ exchange-pp-v0.rst
+
+install-exec-local:
+ find locale/ -name "*.po"
+ mkdir -p $(DESTDIR)$(datadir)
+ cp --parents -r $$(find locale/ -name "*.po") $(DESTDIR)$(datadir)
+
+rdatadir=$(datadir)/taler/exchange
+rdata_DATA = \
+ auditor-report.tex.j2
+
+bin_SCRIPTS = \
+ taler-auditor-dbconfig \
+ taler-exchange-dbconfig \
+ taler-terms-generator \
+ taler-bank-manage-testing \
+ taler-nexus-prepare
+
+edit_script = $(SED) -e 's,%termsdir%,$(termsdir),'g -e 's,%localedir%,$(localedir),'g $(NULL)
+taler-terms-generator: taler-terms-generator.in
+ rm -f $@ $@.tmp && \
+ $(edit_script) $< >$@.tmp && \
+ chmod a-w+x $@.tmp && \
+ mv $@.tmp $@
+
+CLEANFILES = \
+ taler-terms-generator
+
+EXTRA_DIST = \
+ locale/de/LC_MESSAGES/exchange-tos-v0.po \
+ taler-bank-manage-testing \
+ taler-nexus-prepare \
+ taler-terms-generator.in \
+ taler-auditor-dbconfig \
+ taler-exchange-dbconfig \
+ gana-generate.sh \
+ gana/gnu-taler-error-codes/registry.rec \
+ gana/gnu-taler-error-codes/Makefile \
+ $(terms_DATA) \
+ $(rdata_DATA) \
+ coverage.sh \
+ gnunet.tag \
+ microhttpd.tag \
+ packages
+
+spapkgdatadir = $(prefix)/share/taler/exchange/spa/
+
+# This is for the single-page-app imported from the wallet-core.git
+# prebuilt branch. This MUST be the last line in the
+# Makefile.am.in, as it will be combined with the
+# actual SPA data by 'bootstrap'!
+dist_spapkgdata_DATA = \
diff --git a/src/exchange/Makefile.am b/src/exchange/Makefile.am
@@ -181,8 +181,9 @@ taler_exchange_httpd_SOURCES = \
taler-exchange-httpd_reserves_open.c taler-exchange-httpd_reserves_open.h \
taler-exchange-httpd_reserves_purse.c taler-exchange-httpd_reserves_purse.h \
taler-exchange-httpd_responses.c taler-exchange-httpd_responses.h \
+ taler-exchange-httpd_spa.c taler-exchange-httpd_spa.h \
taler-exchange-httpd_terms.c taler-exchange-httpd_terms.h \
- taler-exchange-httpd_transfers_get.c taler-exchange-httpd_transfers_get.h
+ taler-exchange-httpd_transfers_get.c taler-exchange-httpd_transfers_get.h
taler_exchange_httpd_LDADD = \
$(LIBGCRYPT_LIBS) \
diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c
@@ -68,6 +68,7 @@
#include "taler-exchange-httpd_reserves_history.h"
#include "taler-exchange-httpd_reserves_open.h"
#include "taler-exchange-httpd_reserves_purse.h"
+#include "taler-exchange-httpd_spa.h"
#include "taler-exchange-httpd_terms.h"
#include "taler-exchange-httpd_transfers_get.h"
#include "taler_exchangedb_lib.h"
@@ -1508,6 +1509,56 @@ handle_post_auditors (struct TEH_RequestContext *rc,
/**
+ * Generates the response for "/", redirecting the
+ * client to the "/webui/" from where we serve the SPA.
+ *
+ * @param rc request context
+ * @param args remaining arguments (should be empty)
+ * @return MHD result code
+ */
+static MHD_RESULT
+spa_redirect (struct TEH_RequestContext *rc,
+ const char *const args[])
+{
+ const char *text = "Redirecting to /webui/";
+ struct MHD_Response *response;
+
+ response = MHD_create_response_from_buffer (strlen (text),
+ (void *) text,
+ MHD_RESPMEM_PERSISTENT);
+ if (NULL == response)
+ {
+ GNUNET_break (0);
+ return MHD_NO;
+ }
+ TALER_MHD_add_global_headers (response);
+ GNUNET_break (MHD_YES ==
+ MHD_add_response_header (response,
+ MHD_HTTP_HEADER_CONTENT_TYPE,
+ "text/plain"));
+ if (MHD_NO ==
+ MHD_add_response_header (response,
+ MHD_HTTP_HEADER_LOCATION,
+ "/webui/"))
+ {
+ GNUNET_break (0);
+ MHD_destroy_response (response);
+ return MHD_NO;
+ }
+
+ {
+ MHD_RESULT ret;
+
+ ret = MHD_queue_response (rc->connection,
+ MHD_HTTP_FOUND,
+ response);
+ MHD_destroy_response (response);
+ return ret;
+ }
+}
+
+
+/**
* Handle incoming HTTP request.
*
* @param cls closure for MHD daemon (unused)
@@ -1540,15 +1591,11 @@ handle_mhd_request (void *cls,
.data = "User-agent: *\nDisallow: /\n",
.response_code = MHD_HTTP_OK
},
- /* Landing page, tell humans to go away. */
+ /* Landing page, redirect to SPA */
{
.url = "",
.method = MHD_HTTP_METHOD_GET,
- .handler.get = TEH_handler_static_response,
- .mime_type = "text/plain",
- .data =
- "Hello, I'm the Taler exchange. This HTTP server is not for humans.\n",
- .response_code = MHD_HTTP_OK
+ .handler.get = &spa_redirect
},
/* AGPL licensing page, redirect to source. As per the AGPL-license, every
deployment is required to offer the user a download of the source of
@@ -1778,7 +1825,13 @@ handle_mhd_request (void *cls,
.handler.post = &handle_post_aml,
.nargs = 2
},
-
+ {
+ .url = "webui",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler.get = &TEH_handler_spa,
+ .nargs = 1,
+ .nargs_is_upper_bound = true
+ },
/* mark end of list */
{
@@ -2543,6 +2596,13 @@ run (void *cls,
return;
}
if (GNUNET_OK !=
+ TEH_spa_init ())
+ {
+ global_ret = EXIT_NOTCONFIGURED;
+ GNUNET_SCHEDULER_shutdown ();
+ return;
+ }
+ if (GNUNET_OK !=
TALER_TEMPLATING_init ("exchange"))
{
global_ret = EXIT_FAILURE;
diff --git a/src/exchange/taler-exchange-httpd_spa.c b/src/exchange/taler-exchange-httpd_spa.c
@@ -0,0 +1,362 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2020, 2023 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU 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 EXCHANGEABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file taler-exchange-httpd_spa.c
+ * @brief logic to load the single page app (/)
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <gnunet/gnunet_util_lib.h>
+#include <taler/taler_util.h>
+#include <taler/taler_mhd_lib.h>
+#include <gnunet/gnunet_mhd_compat.h>
+#include "taler-exchange-httpd.h"
+
+
+/**
+ * Resource from the WebUi.
+ */
+struct WebuiFile
+{
+ /**
+ * Kept in a DLL.
+ */
+ struct WebuiFile *next;
+
+ /**
+ * Kept in a DLL.
+ */
+ struct WebuiFile *prev;
+
+ /**
+ * Path this resource matches.
+ */
+ char *path;
+
+ /**
+ * SPA resource, compressed.
+ */
+ struct MHD_Response *zspa;
+
+ /**
+ * SPA resource, vanilla.
+ */
+ struct MHD_Response *spa;
+
+};
+
+
+/**
+ * Resources of the WebuUI, kept in a DLL.
+ */
+static struct WebuiFile *webui_head;
+
+/**
+ * Resources of the WebuUI, kept in a DLL.
+ */
+static struct WebuiFile *webui_tail;
+
+
+MHD_RESULT
+TEH_handler_spa (struct TEH_RequestContext *rc,
+ const char *const args[])
+{
+ struct WebuiFile *w = NULL;
+ const char *infix = args[0];
+
+ if ( (NULL == infix) ||
+ (0 == strcmp (infix,
+ "")) )
+ infix = "index.html";
+ for (struct WebuiFile *pos = webui_head;
+ NULL != pos;
+ pos = pos->next)
+ if (0 == strcmp (infix,
+ pos->path))
+ {
+ w = pos;
+ break;
+ }
+ if (NULL == w)
+ return TALER_MHD_reply_with_error (rc->connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_GENERIC_ENDPOINT_UNKNOWN,
+ rc->url);
+ if ( (MHD_YES ==
+ TALER_MHD_can_compress (rc->connection)) &&
+ (NULL != w->zspa) )
+ return MHD_queue_response (rc->connection,
+ MHD_HTTP_OK,
+ w->zspa);
+ return MHD_queue_response (rc->connection,
+ MHD_HTTP_OK,
+ w->spa);
+}
+
+
+/**
+ * Function called on each file to load for the WebUI.
+ *
+ * @param cls NULL
+ * @param dn name of the file to load
+ */
+static enum GNUNET_GenericReturnValue
+build_webui (void *cls,
+ const char *dn)
+{
+ static struct
+ {
+ const char *ext;
+ const char *mime;
+ } mime_map[] = {
+ {
+ .ext = "css",
+ .mime = "text/css"
+ },
+ {
+ .ext = "html",
+ .mime = "text/html"
+ },
+ {
+ .ext = "js",
+ .mime = "text/javascript"
+ },
+ {
+ .ext = "jpg",
+ .mime = "image/jpeg"
+ },
+ {
+ .ext = "jpeg",
+ .mime = "image/jpeg"
+ },
+ {
+ .ext = "png",
+ .mime = "image/png"
+ },
+ {
+ .ext = "svg",
+ .mime = "image/svg+xml"
+ },
+ {
+ .ext = NULL,
+ .mime = NULL
+ },
+ };
+ int fd;
+ struct stat sb;
+ struct MHD_Response *zspa = NULL;
+ struct MHD_Response *spa;
+ const char *ext;
+ const char *mime;
+
+ (void) cls;
+ /* finally open template */
+ fd = open (dn,
+ O_RDONLY);
+ if (-1 == fd)
+ {
+ GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
+ "open",
+ dn);
+ return GNUNET_SYSERR;
+ }
+ if (0 !=
+ fstat (fd,
+ &sb))
+ {
+ GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
+ "open",
+ dn);
+ GNUNET_break (0 == close (fd));
+ return GNUNET_SYSERR;
+ }
+
+ mime = NULL;
+ ext = strrchr (dn, '.');
+ if (NULL == ext)
+ {
+ GNUNET_break (0 == close (fd));
+ return GNUNET_OK;
+ }
+ ext++;
+ for (unsigned int i = 0; NULL != mime_map[i].ext; i++)
+ if (0 == strcasecmp (ext,
+ mime_map[i].ext))
+ {
+ mime = mime_map[i].mime;
+ break;
+ }
+
+ {
+ void *in;
+ ssize_t r;
+ size_t csize;
+
+ in = GNUNET_malloc_large (sb.st_size);
+ if (NULL == in)
+ {
+ GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
+ "malloc");
+ GNUNET_break (0 == close (fd));
+ return GNUNET_SYSERR;
+ }
+ r = read (fd,
+ in,
+ sb.st_size);
+ if ( (-1 == r) ||
+ (sb.st_size != (size_t) r) )
+ {
+ GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
+ "read",
+ dn);
+ GNUNET_free (in);
+ GNUNET_break (0 == close (fd));
+ return GNUNET_SYSERR;
+ }
+ csize = (size_t) r;
+ if (MHD_YES ==
+ TALER_MHD_body_compress (&in,
+ &csize))
+ {
+ zspa = MHD_create_response_from_buffer (csize,
+ in,
+ MHD_RESPMEM_MUST_FREE);
+ if (NULL != zspa)
+ {
+ if (MHD_NO ==
+ MHD_add_response_header (zspa,
+ MHD_HTTP_HEADER_CONTENT_ENCODING,
+ "deflate"))
+ {
+ GNUNET_break (0);
+ MHD_destroy_response (zspa);
+ zspa = NULL;
+ }
+ if (NULL != mime)
+ GNUNET_break (MHD_YES ==
+ MHD_add_response_header (zspa,
+ MHD_HTTP_HEADER_CONTENT_TYPE,
+ mime));
+ }
+ }
+ else
+ {
+ GNUNET_free (in);
+ }
+ }
+
+ spa = MHD_create_response_from_fd (sb.st_size,
+ fd);
+ if (NULL == spa)
+ {
+ GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
+ "open",
+ dn);
+ GNUNET_break (0 == close (fd));
+ if (NULL != zspa)
+ {
+ MHD_destroy_response (zspa);
+ zspa = NULL;
+ }
+ return GNUNET_SYSERR;
+ }
+ if (NULL != mime)
+ GNUNET_break (MHD_YES ==
+ MHD_add_response_header (spa,
+ MHD_HTTP_HEADER_CONTENT_TYPE,
+ mime));
+
+ {
+ struct WebuiFile *w;
+ const char *fn;
+
+ fn = strrchr (dn, '/');
+ GNUNET_assert (NULL != fn);
+ w = GNUNET_new (struct WebuiFile);
+ w->path = GNUNET_strdup (fn + 1);
+ w->spa = spa;
+ w->zspa = zspa;
+ GNUNET_CONTAINER_DLL_insert (webui_head,
+ webui_tail,
+ w);
+ }
+ return GNUNET_OK;
+}
+
+
+enum GNUNET_GenericReturnValue
+TEH_spa_init ()
+{
+ char *dn;
+
+ {
+ char *path;
+
+ path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR);
+ if (NULL == path)
+ {
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ GNUNET_asprintf (&dn,
+ "%sexchange/spa/",
+ path);
+ GNUNET_free (path);
+ }
+
+ if (-1 ==
+ GNUNET_DISK_directory_scan (dn,
+ &build_webui,
+ NULL))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to load WebUI from `%s'\n",
+ dn);
+ GNUNET_free (dn);
+ return GNUNET_SYSERR;
+ }
+ GNUNET_free (dn);
+ return GNUNET_OK;
+}
+
+
+/**
+ * Nicely shut down.
+ */
+void __attribute__ ((destructor))
+get_spa_fini ()
+{
+ struct WebuiFile *w;
+
+ while (NULL != (w = webui_head))
+ {
+ GNUNET_CONTAINER_DLL_remove (webui_head,
+ webui_tail,
+ w);
+ if (NULL != w->spa)
+ {
+ MHD_destroy_response (w->spa);
+ w->spa = NULL;
+ }
+ if (NULL != w->zspa)
+ {
+ MHD_destroy_response (w->zspa);
+ w->zspa = NULL;
+ }
+ GNUNET_free (w->path);
+ GNUNET_free (w);
+ }
+}
diff --git a/src/exchange/taler-exchange-httpd_spa.h b/src/exchange/taler-exchange-httpd_spa.h
@@ -0,0 +1,49 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2023 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU 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 EXCHANGEABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file taler-exchange-httpd_spa.h
+ * @brief logic to preload and serve static files
+ * @author Christian Grothoff
+ */
+#ifndef TALER_EXCHANGE_HTTPD_SPA_H
+#define TALER_EXCHANGE_HTTPD_SPA_H
+
+#include <microhttpd.h>
+#include "taler-exchange-httpd.h"
+
+
+/**
+ * Return our single-page-app user interface (see contrib/wallet-core/).
+ *
+ * @param rc context of the handler
+ * @param[in,out] args remaining arguments (ignored)
+ * @return #MHD_YES on success (reply queued), #MHD_NO on error (close connection)
+ */
+MHD_RESULT
+TEH_handler_spa (struct TEH_RequestContext *rc,
+ const char *const args[]);
+
+
+/**
+ * Preload and compress SPA files.
+ *
+ * @return #GNUNET_OK on success
+ */
+enum GNUNET_GenericReturnValue
+TEH_spa_init (void);
+
+
+#endif