merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit 041a0aef99a9d88fcc041cb3dcf628a947c4305c
parent ea6759ed25de830d702963b7798a12baec847995
Author: Bohdan Potuzhnyi <potub1@bfh.ch>
Date:   Fri, 11 Oct 2024 15:16:45 +0000

updated post function to send notification to the donaukeyupdate service

Diffstat:
Msrc/backend/.gitignore | 2++
Asrc/backend/taler-merchant-donaukeyupdate.c | 829+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backend/taler-merchant-httpd_private-post-donau-instance.c | 15++++++++++++++-
3 files changed, 845 insertions(+), 1 deletion(-)

diff --git a/src/backend/.gitignore b/src/backend/.gitignore @@ -4,3 +4,4 @@ taler-merchant-kyccheck taler-merchant-reconciliation taler-merchant-webhook taler-merchant-wirewatch +taler-merchant-donaukeyupdate +\ No newline at end of file diff --git a/src/backend/taler-merchant-donaukeyupdate.c b/src/backend/taler-merchant-donaukeyupdate.c @@ -0,0 +1,829 @@ +/* + This file is part of TALER + Copyright (C) 2024 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-donaukeyupdate.c + * @brief Process that ensures our /keys data for all Donau instances is current + * @author [Your Name] + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include <pthread.h> +#include <taler/taler_dbevents.h> +#include "donau/donau_service.h" +#include "taler_merchantdb_lib.h" +#include "taler_merchantdb_plugin.h" +#include "taler_merchant_bank_lib.h" + +/** + * Maximum frequency for the Donau interaction. + */ +#define DONAU_MAXFREQ GNUNET_TIME_relative_multiply( \ + GNUNET_TIME_UNIT_MINUTES, \ + 5) + +/** + * How many inquiries do we process concurrently at most. + */ +#define OPEN_INQUIRY_LIMIT 1024 + +/** + * How often do we retry after DB serialization errors (at most)? + */ +#define MAX_RETRIES 3 + +/** + * Information about a Donau instance. + */ +struct Donau +{ + /** + * Pointer to the next Donau instance in the doubly linked list. + */ + struct Donau *next; + + /** + * Pointer to the previous Donau instance in the doubly linked list. + */ + struct Donau *prev; + + /** + * Base URL of the Donau instance being tracked. + * This URL is used to query the Donau service for keys and other resources. + */ + char *donau_url; + + /** + * Expected currency of the donau. + */ + char *currency; + + /** + * Pointer to the keys obtained from the Donau instance. + * This structure holds the cryptographic keys for the Donau instance. + */ + struct DONAU_Keys *keys; + + /** + * A handle for an ongoing /keys request to the Donau instance. + * This is NULL when there is no active request. + */ + struct DONAU_GetKeysHandle *conn; + + /** + * Scheduler task for retrying a failed /keys request. + * This task will trigger the next attempt to download the Donau keys if the previous request failed or needs to be retried. + */ + struct GNUNET_SCHEDULER_Task *retry_task; + + /** + * The earliest time at which the Donau instance can attempt another /keys request. + * This is used to manage the timing between requests and ensure compliance with rate-limiting rules. + */ + struct GNUNET_TIME_Absolute first_retry; + + /** + * The delay between the next retry for fetching /keys. + * Used to implement exponential backoff strategies for retries in case of failures. + */ + struct GNUNET_TIME_Relative retry_delay; + + /** + * A flag indicating whether this Donau instance is currently rate-limited. + * If true, the instance is temporarily paused from making further requests due to reaching a limit. + */ + bool limited; +}; + + +/** + * Head of known Donau instances. + */ +static struct Donau *d_head; + +/** + * Tail of known Donau instances. + */ +static struct Donau *d_tail; + +/** + * The merchant's configuration. + */ +static const struct GNUNET_CONFIGURATION_Handle *cfg; + +/** + * Our database plugin. + */ +static struct TALER_MERCHANTDB_Plugin *db_plugin; + +/** + * Our event handler listening for /keys forced downloads. + */ +static struct GNUNET_DB_EventHandler *eh; + +/** + * Handle to the context for interacting with the Donau services. + */ +static struct GNUNET_CURL_Context *ctx; + +/** + * Scheduler context for running the @e ctx. + */ +static struct GNUNET_CURL_RescheduleContext *rc; + +/** + * How many active inquiries do we have right now. + */ +static unsigned int active_inquiries; + +/** + * Value to return from main(). 0 on success, non-zero on errors. + */ +static int global_ret; + +/** + * #GNUNET_YES if we are in test mode and should exit when idle. + */ +static int test_mode; + +/** + * True if the last DB query was limited by the + * #OPEN_INQUIRY_LIMIT and we thus should check again + * as soon as we are substantially below that limit, + * and not only when we get a DB notification. + */ +static bool at_limit; + + +/** + * Function that initiates a /keys download for a Donau instance. + * + * @param cls a `struct Donau *` + */ +static void +download_keys (void *cls); + + +/** + * An inquiry finished, check if we need to start more. + */ +static void +end_inquiry (void) +{ + GNUNET_assert (active_inquiries > 0); + active_inquiries--; + if ( (active_inquiries < OPEN_INQUIRY_LIMIT / 2) && + (at_limit) ) + { + at_limit = false; + for (struct Donau *d = d_head; + NULL != d; + d = d->next) + { + if (!d->limited) + continue; + d->limited = false; + /* done synchronously so that the active_inquiries + is updated immediately */ + download_keys(d); + if (at_limit) + break; + } + } + if ( (! at_limit) && + (0 == active_inquiries) && + (test_mode) ) + { + GNUNET_log(GNUNET_ERROR_TYPE_INFO, + "No more open inquiries and in test mode. Exiting.\n"); + GNUNET_SCHEDULER_shutdown(); + return; + } +} + +/** + * Update Donau keys in the database. + * + * @param keys Donau keys to persist + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +insert_donau_keys_data(const struct DONAU_Keys *keys) +{ + return db_plugin->upsert_donau_keys(db_plugin->cls, keys); +} + +/** + * Store Donau keys in the database and handle retries. + * + * @param keys the keys to store + * @return true on success + */ +static bool +store_donau_keys(struct DONAU_Keys *keys) +{ + enum GNUNET_DB_QueryStatus qs; + + db_plugin->preflight(db_plugin->cls); + for (unsigned int r = 0; r < MAX_RETRIES; r++) + { + if (GNUNET_OK != + db_plugin->start(db_plugin->cls, + "update donau key data")) + { + db_plugin->rollback(db_plugin->cls); + GNUNET_break (0); + return false; + } + + qs = insert_donau_keys_data(keys); + if (qs < 0) + { + db_plugin->rollback(db_plugin->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + continue; + GNUNET_break (0); + return false; + } + + qs = db_plugin->commit(db_plugin->cls); + if (qs < 0) + { + db_plugin->rollback (db_plugin->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + continue; + GNUNET_break (0); + return false; + } + } + if (qs < 0) + { + GNUNET_break (0); + return false; + } + return true; +} + +/** + * Callback after Donau keys are fetched. + * + * @param cls closure with a `struct Donau *` + * @param kr response data + * @param keys the keys of the Donau instance + */ +static void +donau_cert_cb( + void *cls, + const struct DONAU_KeysResponse *kr, + struct DONAU_Keys *keys) +{ + struct Donau *d = cls; + struct GNUNET_TIME_Absolute n; + + d->conn = NULL; + switch (kr->hr.http_status) + { + case MHD_HTTP_OK: + if (! store_donau_keys (keys)) + break; + + d->keys = DONAU_keys_incref(keys); + /* Reset back-off */ + d->retry_delay = DONAU_MAXFREQ; + /* limit retry */ + d->first_retry = + GNUNET_TIME_relative_to_absolute( + DONAU_MAXFREQ); + + //Fixme: hardcoded retry delay + n = GNUNET_TIME_absolute_max (d->first_retry, + GNUNET_TIME_relative_to_absolute( + GNUNET_TIME_relative_multiply( + GNUNET_TIME_UNIT_MONTHS, + 3))); + + if (NULL != d->retry_task) + GNUNET_SCHEDULER_cancel(d->retry_task); + d->retry_task = GNUNET_SCHEDULER_add_at(n, + &download_keys, + d); + + end_inquiry(); + return; + default: + break; + } + + d->retry_delay + = GNUNET_TIME_STD_BACKOFF(d->retry_delay); + n = GNUNET_TIME_absolute_max( + d->first_retry, + GNUNET_TIME_relative_to_absolute(d->retry_delay)); + + if (NULL != d->retry_task) + GNUNET_SCHEDULER_cancel(d->retry_task); + d->retry_task + = GNUNET_SCHEDULER_add_at(n, + &download_keys, + d); + + end_inquiry(); +} + +/** + * Initiate the download of Donau keys. + * + * @param cls closure with a `struct Donau *` + */ +static void +download_keys(void *cls) +{ + struct Donau *d = cls; + + d->retry_task = NULL; + GNUNET_break (OPEN_INQUIRY_LIMIT >= active_inquiries); + if (OPEN_INQUIRY_LIMIT <= active_inquiries) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Cannot start more donaukeys inquiries, already at limit\n"); + d->limited = true; + at_limit = true; + return; + } + d->retry_delay + = GNUNET_TIME_STD_BACKOFF(d->retry_delay); + d->conn = DONAU_get_keys(ctx, + d->donau_url, + &donau_cert_cb, + d); + + if (NULL != d->conn) + { + active_inquiries++; + } + else + { + struct GNUNET_TIME_Relative n; + + n = GNUNET_TIME_relative_max(d->retry_delay, + DONAU_MAXFREQ); + + d->retry_task + = GNUNET_SCHEDULER_add_delayed(n, + &download_keys, + d); + } +} + +/** + * Lookup donau by @a donau_url. Create one + * if it does not exist. + * + * @param donau_url base URL to match against + * @return NULL if not found + */ +static struct Donau * +lookup_donau(const char *donau_url) +{ + for (struct Donau *d = d_head; + NULL != d; + d = d->next) + if (0 == strcmp(d->donau_url, + donau_url)) + return d; + GNUNET_log(GNUNET_ERROR_TYPE_WARNING, + "Got notification about unknown Donau `%s'\n", + donau_url); + return NULL; +} + +/** + * Force immediate (re)loading of /keys for an donau. + * + * @param cls NULL + * @param extra base URL of the donau that changed + * @param extra_len number of bytes in @a extra + */ +static void +force_donau_keys(void *cls, + const void *extra, + size_t extra_len) +{ + const char *url = extra; + struct Donau *d; + + if ( (NULL == extra) || + (0 == extra_len) ) + { + GNUNET_break (0); + return; + } + if ('\0' != url[extra_len - 1]) + { + GNUNET_break (0); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received keys change notification: reload `%s'\n", + url); + + d = lookup_donau(url); + if (NULL == d) + { + GNUNET_break (0); + return; + } + if (NULL != d->conn) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Already downloading %skeys\n", + url); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Will download %skeys in %s\n", + url, + GNUNET_TIME_relative2s ( + GNUNET_TIME_absolute_get_remaining ( + d->first_retry), + true)); + if (NULL != d->retry_task) + GNUNET_SCHEDULER_cancel(d->retry_task); + + d->retry_task + = GNUNET_SCHEDULER_add_at(d->first_retry, + &download_keys, + d); +} + +/** + * Function called on each configuration section. Finds sections + * about donau, parses the entries. + * + * @param cls NULL + * @param section name of the section + */ +static void +accept_donau(void *cls, + const char *section) +{ + char *url; + char *currency; + //char *mks; + + if (0 != + strncasecmp(section, + "merchant-donau-", + strlen ("merchant-donau-"))) + return; + if (GNUNET_YES == + GNUNET_CONFIGURATION_get_value_yesno(cfg, + section, + "DISABLE")) + return; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string(cfg, + section, + "DONAU_URL", + &url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "DONAU_BASE_URL"); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown(); + return; + } + + for (struct Donau *d = d_head; + NULL != d; + d = d->next) + { + if (0 == strcmp (url, + d->donau_url)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Duplicate donau URL `%s', maybe set DISABLED in section `%s'\n", + url, + section); + GNUNET_free (url); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown(); + return; + } + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "CURRENCY", + &currency)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "CURRENCY"); + GNUNET_free (url); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); + return; + } +// if (GNUNET_OK != +// GNUNET_CONFIGURATION_get_value_string (cfg, +// section, +// "MASTER_KEY", +// &mks)) +// { +// GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, +// section, +// "MASTER_KEY"); +// global_ret = EXIT_NOTCONFIGURED; +// GNUNET_SCHEDULER_shutdown (); +// GNUNET_free (currency); +// GNUNET_free (url); +// return; +// } + + + { + struct Donau *d; + + d = GNUNET_new (struct Donau); + d->donau_url = url; + GNUNET_CONTAINER_DLL_insert (d_head, + d_tail, + d); +// if(GNUNET_OK != +// GNUNET_CRYPTO_eddsa_public_key_from_string ( +// mks, +// strlen(mks), +// &d->master_pub.eddsa_pub)) +// { +// GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, +// section, +// "MASTER_KEY", +// "malformed EdDSA key"); +// global_ret = EXIT_NOTCONFIGURED; +// GNUNET_SCHEDULER_shutdown (); +// GNUNET_free (mks); +// return; +// } +// GNUNET_free (mks); + + { + enum GNUNET_DB_QueryStatus qs; + struct DONAU_Keys *keys; + + qs = db_plugin->lookup_donau_keys (db_plugin->cls, + url, + &keys); + + if (qs < 0) + { + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + + if ( (NULL != keys) && + (0 != strcmp (keys->currency, + d->currency)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "/keys cached in our database were for currency `%s', but we expected `%s'. Fetching /keys again.\n", + keys->currency, + d->currency); + DONAU_keys_decref (keys); + keys = NULL; + } +// if ( (NULL != keys) && +// (0 != GNUNET_memcmp (&d->master_pub, +// &keys->master_pub)) ) +// { +// /* master pub differs => fetch keys again */ +// GNUNET_log (GNUNET_ERROR_TYPE_WARNING, +// "Master public key of donau `%s' differs from our configuration. Fetching /keys again.\n", +// d->donau_url); +// DONAU_keys_decref (keys); +// keys = NULL; +// } + d->keys = keys; + if (NULL == keys) + { + /* done synchronously so that the active_inquiries + is updated immediately */ + + download_keys (d); + } + else + { + d->retry_task + = GNUNET_SCHEDULER_add_at (GNUNET_TIME_relative_to_absolute( + GNUNET_TIME_relative_multiply( + GNUNET_TIME_UNIT_MONTHS, + 3)), + &download_keys, + d); + } + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "DONAU `%s' setup\n", + d->donau_url); + } +} + +/** + * We're being aborted with CTRL-C (or SIGTERM). Shut down. + * + * @param cls closure (NULL) + */ +static void +shutdown_task (void *cls) +{ + (void) cls; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Running shutdown\n"); + while (NULL != d_head) + { + struct Donau *d = d_head; + + GNUNET_free (d->donau_url); + GNUNET_free (d->currency); + if (NULL != d->conn) + { + DONAU_get_keys_cancel (d->conn); + d->conn = NULL; + } + if (NULL != d->keys) + { + DONAU_keys_decref (d->keys); + d->keys = NULL; + } + if (NULL != d->retry_task) + { + GNUNET_SCHEDULER_cancel (d->retry_task); + d->retry_task = NULL; + } + GNUNET_CONTAINER_DLL_remove (d_head, + d_tail, + d); + GNUNET_free (d); + } + if (NULL != eh) + { + db_plugin->event_listen_cancel (eh); + eh = NULL; + } + TALER_MERCHANTDB_plugin_unload (db_plugin); + db_plugin = NULL; + cfg = NULL; + if (NULL != ctx) + { + GNUNET_CURL_fini (ctx); + ctx = NULL; + } + if (NULL != rc) + { + GNUNET_CURL_gnunet_rc_destroy (rc); + rc = NULL; + } +} + +/** + * First task. + * + * @param cls closure, NULL + * @param args remaining command-line arguments + * @param cfgfile name of the configuration file used (for saving, can be NULL!) + * @param c configuration + */ +static void +run(void *cls, + char *const *args, + const char *cfgfile, + const struct GNUNET_CONFIGURATION_Handle *c) +{ + (void) args; + (void) cfgfile; + + cfg = c; + GNUNET_SCHEDULER_add_shutdown(&shutdown_task, + NULL); + ctx = GNUNET_CURL_init(&GNUNET_CURL_gnunet_scheduler_reschedule, + &rc); + rc = GNUNET_CURL_gnunet_rc_create(ctx); + if (NULL == ctx) + { + GNUNET_break (0); + GNUNET_SCHEDULER_shutdown (); + global_ret = EXIT_FAILURE; + return; + } + if (NULL == ctx) + { + GNUNET_break (0); + GNUNET_SCHEDULER_shutdown (); + global_ret = EXIT_FAILURE; + return; + } + if( NULL == + (db_plugin = TALER_MERCHANTDB_plugin_load(cfg)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to initialize DB subsystem\n"); + GNUNET_SCHEDULER_shutdown (); + global_ret = EXIT_NOTCONFIGURED; + return; + } + if (GNUNET_OK != + db_plugin->connect (db_plugin->cls)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to connect to database\n"); + GNUNET_SCHEDULER_shutdown (); + global_ret = EXIT_FAILURE; + return; + } + { + struct GNUNET_DB_EventHeaderP es = { + .size = ntohs(sizeof(es)), + .type = ntohs(TALER_DBEVENT_MERCHANT_DONAU_KEYS) + }; + + eh = db_plugin->event_listen(db_plugin->cls, + &es, + GNUNET_TIME_UNIT_FOREVER_REL, + &force_donau_keys, + NULL); + } + GNUNET_CONFIGURATION_iterate_sections(cfg, + &accept_donau, + NULL); + + if ( (0 == active_inquiries) && + (test_mode) ) + { + GNUNET_log(GNUNET_ERROR_TYPE_INFO, + "No donaukeys inquiries to start, exiting.\n"); + GNUNET_SCHEDULER_shutdown(); + return; + } +} + +/** + * The main function of taler-merchant-donaukeyupdate + * + * @param argc number of arguments from the command line + * @param argv command line arguments + * @return 0 ok, 1 on error + */ +int main(int argc, + char *const *argv) +{ + struct GNUNET_GETOPT_CommandLineOption options[] = { + GNUNET_GETOPT_option_timetravel('T', + "timetravel"), + GNUNET_GETOPT_option_flag('t', + "test", + "run in test mode and exit when idle", + &test_mode), + GNUNET_GETOPT_option_version(VERSION "-" VCS_VERSION), + GNUNET_GETOPT_OPTION_END + }; + enum GNUNET_GenericReturnValue ret; + + if (GNUNET_OK != + GNUNET_STRINGS_get_utf8_args(argc, argv, + &argc, &argv)) + return EXIT_INVALIDARGUMENT; + + TALER_OS_init(); + ret = GNUNET_PROGRAM_run( + argc, argv, + "taler-merchant-donaukeyupdate", + gettext_noop( + "background process that ensures our key and configuration data on Donau is up-to-date"), + options, + &run, NULL); + GNUNET_free_nz ((void *) argv); + if (GNUNET_SYSERR == ret) + return EXIT_INVALIDARGUMENT; + if (GNUNET_NO == ret) + return EXIT_SUCCESS; + return global_ret; +} + +/* end of taler-merchant-donaukeyupdate.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-donau-instance.c b/src/backend/taler-merchant-httpd_private-post-donau-instance.c @@ -78,7 +78,20 @@ TMH_private_post_donau_instance (const struct TMH_RequestHandler *rh, switch (qs) { case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return TALER_MHD_reply_static(connection, MHD_HTTP_NO_CONTENT, NULL, NULL, 0); + struct GNUNET_DB_EventHeaderP es = { + .size = ntohs (sizeof (es)), + .type = ntohs (TALER_DBEVENT_MERCHANT_DONAU_KEYS) + }; + + TMH_db->event_notify(TMH_db->cls, + &es, + donau_url, + strlen(donau_url) + 1); + + return TALER_MHD_reply_static(connection, + MHD_HTTP_NO_CONTENT, + NULL, NULL, + 0); case GNUNET_DB_STATUS_HARD_ERROR: return TALER_MHD_reply_with_error(connection, MHD_HTTP_INTERNAL_SERVER_ERROR,