diff options
author | Christian Grothoff <christian@grothoff.org> | 2023-11-16 15:24:24 +0100 |
---|---|---|
committer | Christian Grothoff <christian@grothoff.org> | 2023-11-16 15:24:24 +0100 |
commit | 758afecc1bf42cbfa498f10a9a9735e99758b605 (patch) | |
tree | 0fba1a1b32d5587fe6c8dccea94821f12460aa17 | |
parent | 0d8f1fa2b4998d6d44164512c8aeb1e861ab0a98 (diff) |
add logic to serve AML SPA
-rwxr-xr-x | bootstrap | 13 | ||||
-rw-r--r-- | contrib/Makefile.am.in (renamed from contrib/Makefile.am) | 8 | ||||
-rw-r--r-- | src/exchange/Makefile.am | 3 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd.c | 74 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_spa.c | 362 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_spa.h | 49 |
6 files changed, 501 insertions, 8 deletions
@@ -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.in index 09e1dcf92..64a5a9f54 100644 --- a/contrib/Makefile.am +++ b/contrib/Makefile.am.in @@ -61,3 +61,11 @@ EXTRA_DIST = \ 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 index 12ea34e13..7d0e5a334 100644 --- 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 index 7c7777561..1cde58164 100644 --- 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 new file mode 100644 index 000000000..66b6fdcfa --- /dev/null +++ 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 new file mode 100644 index 000000000..4147a853b --- /dev/null +++ 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 |