/* This file is part of TALER Copyright (C) 2023, 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 */ /** * @file taler-merchant-exchangekeyupdate.c * @brief Process that ensures our /keys data for all exchanges is current * @author Christian Grothoff */ #include "platform.h" #include #include #include #include #include "taler_merchant_util.h" #include "taler_merchant_bank_lib.h" #include "taler_merchantdb_lib.h" #include "taler_merchantdb_plugin.h" /** * Maximum frequency for the exchange interaction. */ #define EXCHANGE_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 an exchange. */ struct Exchange { /** * Kept in a DLL. */ struct Exchange *next; /** * Kept in a DLL. */ struct Exchange *prev; /** * Base URL of the exchange are we tracking here. */ char *exchange_url; /** * Expected currency of the exchange. */ char *currency; /** * A /keys request to this exchange, NULL if not active. */ struct TALER_EXCHANGE_GetKeysHandle *conn; /** * The keys of this exchange, NULL if not known. */ struct TALER_EXCHANGE_Keys *keys; /** * Task where we retry fetching /keys from the exchange. */ struct GNUNET_SCHEDULER_Task *retry_task; /** * Master public key expected for this exchange. */ struct TALER_MasterPublicKeyP master_pub; /** * How soon can may we, at the earliest, re-download /keys? */ struct GNUNET_TIME_Absolute first_retry; /** * How long should we wait between the next retry? * Used for exponential back-offs. */ struct GNUNET_TIME_Relative retry_delay; /** * Are we waiting for /keys downloads due to our * hard limit? */ bool limited; }; /** * Head of known exchanges. */ static struct Exchange *e_head; /** * Tail of known exchanges. */ static struct Exchange *e_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 bank. */ 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. * * @param cls a `struct Exchange *` */ 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 Exchange *e = e_head; NULL != e; e = e->next) { if (! e->limited) continue; e->limited = false; /* done synchronously so that the active_inquiries is updated immediately */ download_keys (e); 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. Existing.\n"); GNUNET_SCHEDULER_shutdown (); return; } } /** * Add account restriction @a a to array of @a restrictions. * * @param[in,out] restrictions JSON array to build * @param r restriction to add to @a restrictions * @return #GNUNET_SYSERR if @a r is malformed */ static enum GNUNET_GenericReturnValue add_restriction (json_t *restrictions, const struct TALER_EXCHANGE_AccountRestriction *r) { json_t *jr; jr = NULL; switch (r->type) { case TALER_EXCHANGE_AR_INVALID: GNUNET_break_op (0); return GNUNET_SYSERR; case TALER_EXCHANGE_AR_DENY: jr = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", "deny") ); break; case TALER_EXCHANGE_AR_REGEX: jr = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ( "type", "regex"), GNUNET_JSON_pack_string ( "regex", r->details.regex.posix_egrep), GNUNET_JSON_pack_string ( "human_hint", r->details.regex.human_hint), GNUNET_JSON_pack_object_incref ( "human_hint_i18n", (json_t *) r->details.regex.human_hint_i18n) ); break; } if (NULL == jr) { GNUNET_break_op (0); return GNUNET_SYSERR; } GNUNET_assert (0 == json_array_append_new (restrictions, jr)); return GNUNET_OK; } /** * Update our information in the database about the * /keys of an exchange. Run inside of a database * transaction scope that will re-try and/or commit * depending on the return value. * * @param keys information to persist * @return transaction status */ static enum GNUNET_DB_QueryStatus insert_keys_data (const struct TALER_EXCHANGE_Keys *keys) { enum GNUNET_DB_QueryStatus qs; /* store exchange online signing keys in our DB */ for (unsigned int i = 0; inum_sign_keys; i++) { const struct TALER_EXCHANGE_SigningPublicKey *sign_key = &keys->sign_keys[i]; qs = db_plugin->insert_exchange_signkey ( db_plugin->cls, &keys->master_pub, &sign_key->key, sign_key->valid_from, sign_key->valid_until, sign_key->valid_legal, &sign_key->master_sig); /* 0 is OK, we may already have the key in the DB! */ if (0 > qs) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); return qs; } } qs = db_plugin->insert_exchange_keys (db_plugin->cls, keys); if (0 > qs) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); return qs; } qs = db_plugin->delete_exchange_accounts (db_plugin->cls, &keys->master_pub); if (0 > qs) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); return qs; } for (unsigned int i = 0; iaccounts_len; i++) { const struct TALER_EXCHANGE_WireAccount *account = &keys->accounts[i]; json_t *debit_restrictions; json_t *credit_restrictions; debit_restrictions = json_array (); GNUNET_assert (NULL != debit_restrictions); credit_restrictions = json_array (); GNUNET_assert (NULL != credit_restrictions); for (unsigned int j = 0; jdebit_restrictions_length; j++) { if (GNUNET_OK != add_restriction (debit_restrictions, &account->debit_restrictions[j])) { db_plugin->rollback (db_plugin->cls); GNUNET_break (0); json_decref (debit_restrictions); json_decref (credit_restrictions); return GNUNET_DB_STATUS_HARD_ERROR; } } for (unsigned int j = 0; jcredit_restrictions_length; j++) { if (GNUNET_OK != add_restriction (credit_restrictions, &account->credit_restrictions[j])) { db_plugin->rollback (db_plugin->cls); GNUNET_break (0); json_decref (debit_restrictions); json_decref (credit_restrictions); return GNUNET_DB_STATUS_HARD_ERROR; } } qs = db_plugin->insert_exchange_account ( db_plugin->cls, &keys->master_pub, account->fpayto_uri, account->conversion_url, debit_restrictions, credit_restrictions, &account->master_sig); json_decref (debit_restrictions); json_decref (credit_restrictions); if (qs < 0) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); return qs; } } /* end 'for all accounts' */ for (unsigned int i = 0; ifees_len; i++) { const struct TALER_EXCHANGE_WireFeesByMethod *fbm = &keys->fees[i]; const char *wire_method = fbm->method; const struct TALER_EXCHANGE_WireAggregateFees *fees = fbm->fees_head; while (NULL != fees) { struct GNUNET_HashCode h_wire_method; GNUNET_CRYPTO_hash (wire_method, strlen (wire_method) + 1, &h_wire_method); qs = db_plugin->store_wire_fee_by_exchange ( db_plugin->cls, &keys->master_pub, &h_wire_method, &fees->fees, fees->start_date, fees->end_date, &fees->master_sig); if (0 > qs) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); return qs; } fees = fees->next; } /* all fees for this method */ } /* for all methods (i) */ { struct GNUNET_DB_EventHeaderP es = { .size = ntohs (sizeof (es)), .type = ntohs (TALER_DBEVENT_MERCHANT_EXCHANGE_KEYS) }; db_plugin->event_notify (db_plugin->cls, &es, keys->exchange_url, strlen (keys->exchange_url) + 1); } return qs; } /** * Run database transaction to store the @a keys in * the merchant database (and notify other processes * that may care about them). * * @param keys the keys to store * @return true on success */ static bool store_keys (struct TALER_EXCHANGE_Keys *keys) { enum GNUNET_DB_QueryStatus qs; db_plugin->preflight (db_plugin->cls); for (unsigned int r = 0; rstart (db_plugin->cls, "update exchange key data")) { db_plugin->rollback (db_plugin->cls); GNUNET_break (0); return false; } qs = insert_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; } } /* end of retry loop */ if (qs < 0) { GNUNET_break (0); return false; } return true; } /** * Function called with information about who is auditing * a particular exchange and what keys the exchange is using. * * @param cls closure with a `struct Exchange *` * @param kr response data * @param[in] keys the keys of the exchange */ static void cert_cb ( void *cls, const struct TALER_EXCHANGE_KeysResponse *kr, struct TALER_EXCHANGE_Keys *keys) { struct Exchange *e = cls; struct GNUNET_TIME_Absolute n; e->conn = NULL; switch (kr->hr.http_status) { case MHD_HTTP_OK: TALER_EXCHANGE_keys_decref (e->keys); e->keys = NULL; if (0 != strcmp (e->currency, keys->currency)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "/keys response from `%s' is for currency `%s', but we expected `%s'. Ignoring response.\n", e->exchange_url, keys->currency, e->currency); break; } if (0 != GNUNET_memcmp (&keys->master_pub, &e->master_pub)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Master public key in %skeys response does not match. Ignoring response.\n", e->exchange_url); break; } if (! store_keys (keys)) break; e->keys = TALER_EXCHANGE_keys_incref (keys); /* Reset back-off */ e->retry_delay = EXCHANGE_MAXFREQ; /* limit retry */ e->first_retry = GNUNET_TIME_relative_to_absolute ( EXCHANGE_MAXFREQ); /* Limit by expiration */ n = GNUNET_TIME_absolute_max (e->first_retry, keys->key_data_expiration.abs_time); if (NULL != e->retry_task) GNUNET_SCHEDULER_cancel (e->retry_task); e->retry_task = GNUNET_SCHEDULER_add_at (n, &download_keys, e); end_inquiry (); return; default: break; } /* Try again (soon-ish) */ e->retry_delay = GNUNET_TIME_STD_BACKOFF (e->retry_delay); n = GNUNET_TIME_absolute_max ( e->first_retry, GNUNET_TIME_relative_to_absolute (e->retry_delay)); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Will download %skeys in %s\n", e->exchange_url, GNUNET_TIME_relative2s ( GNUNET_TIME_absolute_get_remaining (n), true)); if (NULL != e->retry_task) GNUNET_SCHEDULER_cancel (e->retry_task); e->retry_task = GNUNET_SCHEDULER_add_at (n, &download_keys, e); end_inquiry (); } static void download_keys (void *cls) { struct Exchange *e = cls; e->retry_task = NULL; GNUNET_break (OPEN_INQUIRY_LIMIT >= active_inquiries); if (OPEN_INQUIRY_LIMIT <= active_inquiries) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Cannot run job: at limit\n"); e->limited = true; at_limit = true; return; } e->retry_delay = GNUNET_TIME_STD_BACKOFF (e->retry_delay); e->conn = TALER_EXCHANGE_get_keys (ctx, e->exchange_url, e->keys, &cert_cb, e); if (NULL != e->conn) { active_inquiries++; } else { struct GNUNET_TIME_Relative n; n = GNUNET_TIME_relative_max (e->retry_delay, EXCHANGE_MAXFREQ); e->retry_task = GNUNET_SCHEDULER_add_delayed (n, &download_keys, e); } } /** * Lookup exchange by @a exchange_url. Create one * if it does not exist. * * @param exchange_url base URL to match against * @return NULL if not found */ static struct Exchange * lookup_exchange (const char *exchange_url) { for (struct Exchange *e = e_head; NULL != e; e = e->next) if (0 == strcmp (e->exchange_url, exchange_url)) return e; GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Got notification about unknown exchange `%s'\n", exchange_url); return NULL; } /** * Force immediate (re)loading of /keys for an exchange. * * @param cls NULL * @param extra base URL of the exchange that changed * @param extra_len number of bytes in @a extra */ static void force_exchange_keys (void *cls, const void *extra, size_t extra_len) { const char *url = extra; struct Exchange *e; 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); e = lookup_exchange (url); if (NULL == e) { GNUNET_break (0); return; } if (NULL != e->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 ( e->first_retry), true)); if (NULL != e->retry_task) GNUNET_SCHEDULER_cancel (e->retry_task); e->retry_task = GNUNET_SCHEDULER_add_at (e->first_retry, &download_keys, e); } /** * Function called on each configuration section. Finds sections * about exchanges, parses the entries. * * @param cls NULL * @param section name of the section */ static void accept_exchanges (void *cls, const char *section) { char *url; char *mks; char *currency; (void) cls; if (0 != strncasecmp (section, "merchant-exchange-", strlen ("merchant-exchange-"))) return; if (GNUNET_YES == GNUNET_CONFIGURATION_get_value_yesno (cfg, section, "DISABLED")) return; if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, section, "EXCHANGE_BASE_URL", &url)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, section, "EXCHANGE_BASE_URL"); global_ret = EXIT_NOTCONFIGURED; GNUNET_SCHEDULER_shutdown (); return; } for (struct Exchange *e = e_head; NULL != e; e = e->next) { if (0 == strcmp (url, e->exchange_url)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Exchange `%s' configured in multiple sections, maybe set DISABLED=YES 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", ¤cy)) { 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 Exchange *e; e = GNUNET_new (struct Exchange); e->exchange_url = url; e->currency = currency; GNUNET_CONTAINER_DLL_insert (e_head, e_tail, e); if (GNUNET_OK != GNUNET_CRYPTO_eddsa_public_key_from_string ( mks, strlen (mks), &e->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 TALER_EXCHANGE_Keys *keys = NULL; qs = db_plugin->select_exchange_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, e->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, e->currency); TALER_EXCHANGE_keys_decref (keys); keys = NULL; } if ( (NULL != keys) && (0 != GNUNET_memcmp (&e->master_pub, &keys->master_pub)) ) { /* master pub differs => fetch keys again */ GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Master public key of exchange `%s' differs from our configuration. Fetching /keys again.\n", e->exchange_url); TALER_EXCHANGE_keys_decref (keys); keys = NULL; } e->keys = keys; if (NULL == keys) { /* done synchronously so that the active_inquiries is updated immediately */ download_keys (e); } else { e->retry_task = GNUNET_SCHEDULER_add_at (keys->key_data_expiration.abs_time, &download_keys, e); } } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange `%s' setup\n", e->exchange_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 != e_head) { struct Exchange *e = e_head; GNUNET_free (e->exchange_url); GNUNET_free (e->currency); if (NULL != e->conn) { TALER_EXCHANGE_get_keys_cancel (e->conn); e->conn = NULL; } if (NULL != e->keys) { TALER_EXCHANGE_keys_decref (e->keys); e->keys = NULL; } if (NULL != e->retry_task) { GNUNET_SCHEDULER_cancel (e->retry_task); e->retry_task = NULL; } GNUNET_CONTAINER_DLL_remove (e_head, e_tail, e); GNUNET_free (e); } 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 == (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. Consider running taler-merchant-dbinit!\n"); GNUNET_SCHEDULER_shutdown (); global_ret = EXIT_FAILURE; return; } { struct GNUNET_DB_EventHeaderP es = { .size = ntohs (sizeof (es)), .type = ntohs (TALER_DBEVENT_MERCHANT_EXCHANGE_FORCE_KEYS) }; eh = db_plugin->event_listen (db_plugin->cls, &es, GNUNET_TIME_UNIT_FOREVER_REL, &force_exchange_keys, NULL); } GNUNET_CONFIGURATION_iterate_sections (cfg, &accept_exchanges, NULL); if ( (0 == active_inquiries) && (test_mode) ) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "No more open inquiries and in test mode. Existing.\n"); GNUNET_SCHEDULER_shutdown (); return; } } /** * The main function of taler-merchant-exchangekeyupdate * * @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; ret = GNUNET_PROGRAM_run ( TALER_MERCHANT_project_data (), argc, argv, "taler-merchant-exchangekeyupdate", gettext_noop ( "background process that ensures our key and configuration data on exchanges is up-to-date"), options, &run, NULL); if (GNUNET_SYSERR == ret) return EXIT_INVALIDARGUMENT; if (GNUNET_NO == ret) return EXIT_SUCCESS; return global_ret; } /* end of taler-merchant-exchangekeyupdate.c */