/* This file is part of TALER (C) 2014-2023 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 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 */ /** * @file taler-merchant-httpd_exchanges.c * @brief logic this HTTPD keeps for each exchange we interact with * @author Marcello Stanisci * @author Christian Grothoff */ #include "platform.h" #include #include #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd.h" #include /** * How often do we retry DB transactions with soft errors? */ #define MAX_RETRIES 3 /** * Threshold after which exponential backoff should not increase. */ #define RETRY_BACKOFF_THRESHOLD GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_SECONDS, 60) /** * This is how long /keys long-polls for, so we should * allow this time between requests if there is no * answer. See exchange_api_handle.c. */ #define LONG_POLL_THRESHOLD GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_SECONDS, 120) /** * Information we keep for a pending #MMH_EXCHANGES_keys4exchange() operation. */ struct TMH_EXCHANGES_KeysOperation { /** * Kept in a DLL. */ struct TMH_EXCHANGES_KeysOperation *next; /** * Kept in a DLL. */ struct TMH_EXCHANGES_KeysOperation *prev; /** * Function to call with the result. */ TMH_EXCHANGES_Find2Continuation fc; /** * Closure for @e fc. */ void *fc_cls; /** * Exchange we wait for the /keys for. */ struct TMH_Exchange *my_exchange; /** * Task scheduled to asynchronously return the result to * the find continuation. */ struct GNUNET_SCHEDULER_Task *at; }; /** * Information about wire transfer fees of an exchange, by wire method. */ struct FeesByWireMethod { /** * Kept in a DLL. */ struct FeesByWireMethod *next; /** * Kept in a DLL. */ struct FeesByWireMethod *prev; /** * Wire method these fees are for. */ char *wire_method; /** * Applicable fees, NULL if unknown/error. */ struct TALER_EXCHANGE_WireAggregateFees *af; }; /** * Restriction that applies to an exchange account. */ struct Restriction { /** * Kept in a DLL. */ struct Restriction *next; /** * Kept in a DLL. */ struct Restriction *prev; /** * Type of restriction imposed on the account. */ enum TALER_EXCHANGE_AccountRestrictionType type; /** * Details depending on @e type. */ union { /** * Accounts must match the given regular expression. */ struct { /** * Pre-compiled regex. */ regex_t ex; } regex; } details; }; /** * Information about a bank account of the exchange. */ struct ExchangeAccount { /** * Kept in a DLL. */ struct ExchangeAccount *next; /** * Kept in a DLL. */ struct ExchangeAccount *prev; /** * Wire method of this exchange account. */ char *wire_method; /** * Currency conversion that applies to this account, * NULL if none. */ char *conversion_url; /** * Head of DLL of debit restrictions of this account. */ struct Restriction *d_head; /** * Tail of DLL of debit restrictions of this account. */ struct Restriction *d_tail; }; /** * Internal representation for an exchange */ struct TMH_Exchange { /** * Kept in a DLL. */ struct TMH_Exchange *next; /** * Kept in a DLL. */ struct TMH_Exchange *prev; /** * Head of FOs pending for this exchange. */ struct TMH_EXCHANGES_KeysOperation *keys_head; /** * Tail of FOs pending for this exchange. */ struct TMH_EXCHANGES_KeysOperation *keys_tail; /** * Head of accounts of this exchange. */ struct ExchangeAccount *acc_head; /** * Tail of accounts of this exchange. */ struct ExchangeAccount *acc_tail; /** * (base) URL of the exchange. */ char *url; /** * Currency offered by the exchange according to OUR configuration. */ char *currency; /** * A connection to this exchange */ struct TALER_EXCHANGE_GetKeysHandle *conn; /** * The keys of this exchange */ struct TALER_EXCHANGE_Keys *keys; /** * Head of wire fees from /wire request. */ struct FeesByWireMethod *wire_fees_head; /** * Tail of wire fees from /wire request. */ struct FeesByWireMethod *wire_fees_tail; /** * Master public key of the 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? */ struct GNUNET_TIME_Relative retry_delay; /** * How long should we wait between the next retry for /wire? */ struct GNUNET_TIME_Relative wire_retry_delay; /** * Task where we retry fetching /keys from the exchange. */ struct GNUNET_SCHEDULER_Task *retry_task; /** * What state is this exchange in? */ enum { /** * Downloading /keys failed. */ ESTATE_FAILED = -1, /** * Nothing was ever done. */ ESTATE_INIT = 0, /** * We are actively downloading /keys for the first time. */ ESTATE_DOWNLOADING_FIRST = 1, /** * We finished downloading /keys and the exchange is * ready. */ ESTATE_DOWNLOADED = 2, /** * We are downloading /keys again after a previous * success. */ ESTATE_REDOWNLOADING_SUCCESS = 3, /** * We are downloading /keys again after a previous * failure. */ ESTATE_REDOWNLOADING_FAILURE = 4 } state; /** * true if this exchange is from our configuration and * explicitly trusted, false if we need to check each * key to be sure it is trusted. */ bool trusted; }; /** * Head of exchanges we know about. */ static struct TMH_Exchange *exchange_head; /** * Tail of exchanges we know about. */ static struct TMH_Exchange *exchange_tail; /** * Our event handler listening for /keys downloads * being put into the database. */ static struct GNUNET_DB_EventHandler *keys_eh; /** * How many exchanges do we trust (for our configured * currency) as per our configuration? Used for a * sanity-check on startup. */ static int trusted_exchange_count; const struct TALER_MasterPublicKeyP * TMH_EXCHANGES_get_master_pub ( const struct TMH_Exchange *exchange) { GNUNET_break ( (exchange->trusted) || (NULL != exchange->keys) ); return &exchange->master_pub; } const char * TMH_EXCHANGES_get_currency ( const struct TMH_Exchange *exchange) { return exchange->currency; } /** * Free data structures within @a ea, but not @a ea * pointer itself. * * @param[in] ea data structure to free */ static void free_exchange_account (struct ExchangeAccount *ea) { struct Restriction *r; while (NULL != (r = ea->d_head)) { GNUNET_CONTAINER_DLL_remove (ea->d_head, ea->d_tail, r); switch (r->type) { case TALER_EXCHANGE_AR_INVALID: GNUNET_assert (0); break; case TALER_EXCHANGE_AR_DENY: break; case TALER_EXCHANGE_AR_REGEX: regfree (&r->details.regex.ex); break; } GNUNET_free (r); } GNUNET_free (ea->wire_method); GNUNET_free (ea->conversion_url); } /** * Free list of all accounts in @a exchange. * * @param[in,out] exchange entry to free accounts for */ static void purge_exchange_accounts (struct TMH_Exchange *exchange) { struct ExchangeAccount *acc; while (NULL != (acc = exchange->acc_head)) { GNUNET_CONTAINER_DLL_remove (exchange->acc_head, exchange->acc_tail, acc); free_exchange_account (acc); GNUNET_free (acc); } } /** * Lookup exchange by @a exchange_url. Create one * if it does not exist. * * @param exchange_url base URL to match against * @return fresh entry if exchange was not yet known */ static struct TMH_Exchange * lookup_exchange (const char *exchange_url) { struct TMH_Exchange *exchange; enum GNUNET_DB_QueryStatus qs; for (exchange = exchange_head; NULL != exchange; exchange = exchange->next) if (0 == strcmp (exchange->url, exchange_url)) return exchange; exchange = GNUNET_new (struct TMH_Exchange); exchange->url = GNUNET_strdup (exchange_url); GNUNET_CONTAINER_DLL_insert (exchange_head, exchange_tail, exchange); qs = TMH_db->select_exchange_keys (TMH_db->cls, exchange->url, &exchange->keys); GNUNET_break (qs >= 0); if (qs > 0) exchange->state = ESTATE_DOWNLOADED; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "The exchange `%s' is new (%d)\n", exchange_url, exchange->state); return exchange; } /** * Set the list of accounts of @a exchange. * * @param[in,out] exchange exchange to initialize or update * @param accounts_len length of @a accounts array * @param accounts array of accounts to convert * @return #GNUNET_OK on success */ static enum GNUNET_GenericReturnValue set_exchange_accounts ( struct TMH_Exchange *exchange, unsigned int accounts_len, const struct TALER_EXCHANGE_WireAccount accounts[static accounts_len]) { enum GNUNET_GenericReturnValue ret = GNUNET_OK; purge_exchange_accounts (exchange); for (unsigned int i = 0; iwire_method = TALER_payto_get_method (account->payto_uri); if (NULL != account->conversion_url) acc->conversion_url = GNUNET_strdup (account->conversion_url); GNUNET_CONTAINER_DLL_insert (exchange->acc_head, exchange->acc_tail, acc); for (unsigned int j = 0; jdebit_restrictions_length; j++) { const struct TALER_EXCHANGE_AccountRestriction *ar = &account->debit_restrictions[j]; struct Restriction *r; r = GNUNET_new (struct Restriction); r->type = ar->type; switch (ar->type) { case TALER_EXCHANGE_AR_INVALID: GNUNET_assert (0); break; case TALER_EXCHANGE_AR_DENY: break; case TALER_EXCHANGE_AR_REGEX: if (0 != regcomp (&r->details.regex.ex, ar->details.regex.posix_egrep, REG_NOSUB | REG_EXTENDED)) { GNUNET_break_op (0); GNUNET_free (r); ret = GNUNET_SYSERR; continue; } break; } GNUNET_CONTAINER_DLL_insert (acc->d_head, acc->d_tail, r); } } return ret; } /** * Function called with information about who is auditing * a particular exchange and what key the exchange is using. * * @param cls closure, will be `struct TMH_Exchange` * @param kr response details * @param[in] keys keys object returned */ static void keys_mgmt_cb ( void *cls, const struct TALER_EXCHANGE_KeysResponse *kr, struct TALER_EXCHANGE_Keys *keys); /** * Check if we have any remaining pending requests for the * given @a exchange, and if we have the required data, call * the callback. * * @param exchange the exchange to check for pending find operations */ static void process_find_operations (struct TMH_Exchange *exchange) { struct GNUNET_TIME_Timestamp now; now = GNUNET_TIME_timestamp_get (); for (struct FeesByWireMethod *fbw = exchange->wire_fees_head; NULL != fbw; fbw = fbw->next) { while ( (NULL != fbw->af) && (GNUNET_TIME_timestamp_cmp (fbw->af->end_date, <, now)) ) { struct TALER_EXCHANGE_WireAggregateFees *af = fbw->af; fbw->af = af->next; GNUNET_free (af); } if (NULL == fbw->af) { /* Disagreement on the current time */ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Exchange has no wire fees configured for `%s' wire method\n", fbw->wire_method); } else if (GNUNET_TIME_timestamp_cmp (fbw->af->start_date, >, now)) { /* Disagreement on the current time */ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Exchange's earliest fee is %s ahead of our time. Clock skew issue?\n", GNUNET_TIME_relative2s ( GNUNET_TIME_absolute_get_remaining ( fbw->af->start_date.abs_time), true)); } } /* for all wire methods */ { struct TMH_EXCHANGES_KeysOperation *kon; kon = NULL; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Processing find operations for `%s' (%d)\n", exchange->url, exchange->state); for (struct TMH_EXCHANGES_KeysOperation *ko = exchange->keys_head; NULL != ko; ko = kon) { kon = ko->next; ko->fc (ko->fc_cls, exchange->keys, exchange); TMH_EXCHANGES_keys4exchange_cancel (ko); } } } /** * Function called with information about the wire fees for each wire method. * Stores the wire fees within our internal data structures for later use. * * @param exchange connection to the exchange * @param master_pub public key of the exchange * @param num_methods number of wire methods supported * @param fbm wire fees by method * @return #GNUNET_OK on success */ static enum GNUNET_GenericReturnValue process_wire_fees ( struct TMH_Exchange *exchange, const struct TALER_MasterPublicKeyP *master_pub, unsigned int num_methods, const struct TALER_EXCHANGE_WireFeesByMethod fbm[static num_methods]) { for (unsigned int i = 0; iwire_fees_head; NULL != f; f = f->next) if (0 == strcasecmp (wire_method, f->wire_method)) break; if (NULL == f) { f = GNUNET_new (struct FeesByWireMethod); f->wire_method = GNUNET_strdup (wire_method); GNUNET_CONTAINER_DLL_insert (exchange->wire_fees_head, exchange->wire_fees_tail, f); } endp = f->af; while ( (NULL != endp) && (NULL != endp->next) ) endp = endp->next; while ( (NULL != endp) && (NULL != fees) && (GNUNET_TIME_timestamp_cmp (fees->start_date, <, endp->end_date)) ) fees = fees->next; if ( (NULL != endp) && (NULL != fees) && (GNUNET_TIME_timestamp_cmp (fees->start_date, !=, endp->end_date)) ) { /* Hole or overlap in the fee structure, not allowed! */ GNUNET_break_op (0); return GNUNET_SYSERR; } while (NULL != fees) { struct TALER_EXCHANGE_WireAggregateFees *af; af = GNUNET_new (struct TALER_EXCHANGE_WireAggregateFees); *af = *fees; af->next = NULL; if (NULL == endp) f->af = af; else endp->next = af; endp = af; fees = fees->next; } /* all fees for this method */ } /* for all methods (i) */ return GNUNET_OK; } /** * 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; } /** * Retry getting keys from the given exchange in the closure. * * @param cls the `struct TMH_Exchange *` */ static void retry_exchange (void *cls) { struct TMH_Exchange *exchange = cls; exchange->retry_task = NULL; GNUNET_assert (NULL == exchange->conn); exchange->retry_delay = GNUNET_TIME_randomized_backoff (exchange->retry_delay, RETRY_BACKOFF_THRESHOLD); /* Block for the duration of the long-poller */ exchange->first_retry = GNUNET_TIME_relative_to_absolute (LONG_POLL_THRESHOLD); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Fetching /keys from exchange %s in retry_exchange()\n", exchange->url); switch (exchange->state) { case ESTATE_FAILED: exchange->state = ESTATE_REDOWNLOADING_FAILURE; break; case ESTATE_INIT: exchange->state = ESTATE_DOWNLOADING_FIRST; break; case ESTATE_DOWNLOADING_FIRST: GNUNET_break (0); return; case ESTATE_DOWNLOADED: exchange->state = ESTATE_REDOWNLOADING_SUCCESS; break; case ESTATE_REDOWNLOADING_SUCCESS: GNUNET_break (0); return; case ESTATE_REDOWNLOADING_FAILURE: GNUNET_break (0); return; } exchange->conn = TALER_EXCHANGE_get_keys ( TMH_curl_ctx, exchange->url, exchange->keys, &keys_mgmt_cb, exchange); /* Note: while the API spec says 'returns NULL on error', the implementation actually never returns NULL. */ GNUNET_break (NULL != exchange->conn); } /** * Task to asynchronously return keys operation result to caller. * * @param cls a `struct TMH_EXCHANGES_KeysOperation` */ static void return_keys (void *cls) { struct TMH_EXCHANGES_KeysOperation *fo = cls; struct TMH_Exchange *exchange = fo->my_exchange; fo->at = NULL; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Returning key data for %s instantly\n", exchange->url); process_find_operations (exchange); } struct TMH_EXCHANGES_KeysOperation * TMH_EXCHANGES_keys4exchange ( const char *chosen_exchange, bool force_download, TMH_EXCHANGES_Find2Continuation fc, void *fc_cls) { struct TMH_Exchange *exchange; struct TMH_EXCHANGES_KeysOperation *fo; if (NULL == TMH_curl_ctx) { GNUNET_break (0); return NULL; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Trying to find chosen exchange `%s'\n", chosen_exchange); exchange = lookup_exchange (chosen_exchange); fo = GNUNET_new (struct TMH_EXCHANGES_KeysOperation); fo->fc = fc; fo->fc_cls = fc_cls; fo->my_exchange = exchange; GNUNET_CONTAINER_DLL_insert (exchange->keys_head, exchange->keys_tail, fo); if ( (NULL != exchange->keys) && (! force_download) && (GNUNET_TIME_absolute_is_future ( exchange->keys->key_data_expiration.abs_time)) ) { /* We have a valid reply, immediately return result */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "The exchange `%s' is ready\n", exchange->url); GNUNET_assert (NULL == fo->at); fo->at = GNUNET_SCHEDULER_add_now (&return_keys, fo); return fo; } if ( (NULL == exchange->conn) && ( (ESTATE_FAILED == exchange->state) || (ESTATE_REDOWNLOADING_FAILURE == exchange->state) ) ) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Already waiting for `%skeys' for a while, failing query instantly\n", exchange->url); GNUNET_assert (NULL == fo->at); fo->at = GNUNET_SCHEDULER_add_now (&return_keys, fo); return fo; } if ( (force_download) && (GNUNET_TIME_absolute_is_future (exchange->first_retry)) && (ESTATE_DOWNLOADED == exchange->state) ) { /* Return results immediately. */ fo->at = GNUNET_SCHEDULER_add_now (&return_keys, fo); /* *no* return here, we MAY schedule a 'retry_task' in the next block if there isn't one yet */ } if ( (NULL == exchange->retry_task) && (NULL == exchange->conn) ) exchange->retry_task = GNUNET_SCHEDULER_add_at (exchange->first_retry, &retry_exchange, exchange); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Next %skeys (%d) request scheduled for %s\n", exchange->url, exchange->state, GNUNET_TIME_absolute2s ( exchange->first_retry)); /* No activity to launch, we are already doing so. */ return fo; } void TMH_EXCHANGES_keys4exchange_cancel ( struct TMH_EXCHANGES_KeysOperation *fo) { struct TMH_Exchange *exchange = fo->my_exchange; if (NULL != fo->at) { GNUNET_SCHEDULER_cancel (fo->at); fo->at = NULL; } GNUNET_CONTAINER_DLL_remove (exchange->keys_head, exchange->keys_tail, fo); GNUNET_free (fo); } /** * Obtain applicable fees for @a exchange and @a wire_method. * * @param exchange the exchange to query * @param now current time * @param wire_method the wire method we want the fees for * @return NULL if we do not have fees for this method yet */ static const struct FeesByWireMethod * get_wire_fees (const struct TMH_Exchange *exchange, struct GNUNET_TIME_Timestamp now, const char *wire_method) { for (struct FeesByWireMethod *fbw = exchange->wire_fees_head; NULL != fbw; fbw = fbw->next) { if (0 == strcasecmp (fbw->wire_method, wire_method) ) { struct TALER_EXCHANGE_WireAggregateFees *af; /* Advance through list up to current time */ while ( (NULL != (af = fbw->af)) && (GNUNET_TIME_timestamp_cmp (now, >=, af->end_date)) ) { fbw->af = af->next; GNUNET_free (af); } return fbw; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange supports `%s' as a wire method (but we do not use that one)\n", fbw->wire_method); } return NULL; } /** * Free @a exchange. * * @param[in] exchange entry to free */ static void free_exchange_entry (struct TMH_Exchange *exchange) { struct FeesByWireMethod *f; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Releasing %s exchange %s (%d)\n", exchange->trusted ? "trusted" : "untrusted", exchange->url, exchange->state); GNUNET_CONTAINER_DLL_remove (exchange_head, exchange_tail, exchange); purge_exchange_accounts (exchange); while (NULL != (f = exchange->wire_fees_head)) { struct TALER_EXCHANGE_WireAggregateFees *af; GNUNET_CONTAINER_DLL_remove (exchange->wire_fees_head, exchange->wire_fees_tail, f); while (NULL != (af = f->af)) { f->af = af->next; GNUNET_free (af); } GNUNET_free (f->wire_method); GNUNET_free (f); } if (NULL != exchange->conn) { TALER_EXCHANGE_get_keys_cancel (exchange->conn); exchange->conn = NULL; } TALER_EXCHANGE_keys_decref (exchange->keys); exchange->keys = NULL; if (NULL != exchange->retry_task) { GNUNET_SCHEDULER_cancel (exchange->retry_task); exchange->retry_task = NULL; } GNUNET_assert (NULL == exchange->keys_head); GNUNET_assert (NULL == exchange->keys_tail); GNUNET_free (exchange->currency); GNUNET_free (exchange->url); GNUNET_free (exchange); } /** * We failed downloading /keys from @a exchange. Tell clients * about our failure, abort pending operations and retry later. * * @param exchange exchange that failed */ static void fail_and_retry (struct TMH_Exchange *exchange) { struct TMH_EXCHANGES_KeysOperation *keys; exchange->state = ESTATE_FAILED; while (NULL != (keys = exchange->keys_head)) { keys->fc (keys->fc_cls, NULL, exchange); TMH_EXCHANGES_keys4exchange_cancel (keys); } exchange->retry_delay = GNUNET_TIME_randomized_backoff (exchange->retry_delay, RETRY_BACKOFF_THRESHOLD); GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Failed to fetch /keys from `%s'; retrying in %s\n", exchange->url, GNUNET_STRINGS_relative_time_to_string (exchange->retry_delay, true)); if (NULL != exchange->retry_task) GNUNET_SCHEDULER_cancel (exchange->retry_task); exchange->first_retry = GNUNET_TIME_relative_to_absolute ( exchange->retry_delay); exchange->retry_task = GNUNET_SCHEDULER_add_delayed (exchange->retry_delay, &retry_exchange, exchange); } /** * 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++) { struct TALER_EXCHANGE_SigningPublicKey *sign_key = &keys->sign_keys[i]; enum GNUNET_DB_QueryStatus qs; qs = TMH_db->insert_exchange_signkey ( TMH_db->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 = TMH_db->insert_exchange_keys (TMH_db->cls, keys); if (0 > qs) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); return qs; } qs = TMH_db->delete_exchange_accounts (TMH_db->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])) { TMH_db->rollback (TMH_db->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])) { TMH_db->rollback (TMH_db->cls); GNUNET_break (0); json_decref (debit_restrictions); json_decref (credit_restrictions); return GNUNET_DB_STATUS_HARD_ERROR; } } qs = TMH_db->insert_exchange_account ( TMH_db->cls, &keys->master_pub, account->payto_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 = TMH_db->store_wire_fee_by_exchange (TMH_db->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) }; TMH_db->event_notify (TMH_db->cls, &es, keys->exchange_url, strlen (keys->exchange_url) + 1); } return qs; } static void keys_mgmt_cb (void *cls, const struct TALER_EXCHANGE_KeysResponse *kr, struct TALER_EXCHANGE_Keys *keys) { struct TMH_Exchange *exchange = cls; enum GNUNET_DB_QueryStatus qs; exchange->conn = NULL; if (MHD_HTTP_OK != kr->hr.http_status) { if (GNUNET_TIME_absolute_is_future (exchange->first_retry)) { /* /keys failed *before* the long polling threshold. We apply the exponential back-off from now. */ exchange->first_retry = GNUNET_TIME_relative_to_absolute ( exchange->retry_delay); } fail_and_retry (exchange); TALER_EXCHANGE_keys_decref (keys); return; } if (NULL == exchange->currency) exchange->currency = GNUNET_strdup (keys->currency); if (0 != strcmp (exchange->currency, keys->currency)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "/keys response from `%s' is for currency `%s', but we expected `%s'\n", exchange->url, keys->currency, exchange->currency); fail_and_retry (exchange); TALER_EXCHANGE_keys_decref (keys); return; } exchange->state = ESTATE_DOWNLOADED; TMH_db->preflight (TMH_db->cls); for (unsigned int r = 0; rstart (TMH_db->cls, "update exchange key data")) { TMH_db->rollback (TMH_db->cls); GNUNET_break (0); fail_and_retry (exchange); TALER_EXCHANGE_keys_decref (keys); return; } qs = insert_keys_data (keys); if (qs < 0) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) continue; GNUNET_break (0); fail_and_retry (exchange); TALER_EXCHANGE_keys_decref (keys); return; } qs = TMH_db->commit (TMH_db->cls); if (qs < 0) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) continue; GNUNET_break (0); fail_and_retry (exchange); TALER_EXCHANGE_keys_decref (keys); return; } } /* end of retry loop */ if (qs < 0) { GNUNET_break (0); fail_and_retry (exchange); TALER_EXCHANGE_keys_decref (keys); return; } TALER_EXCHANGE_keys_decref (keys); exchange->retry_delay = GNUNET_TIME_UNIT_ZERO; } enum GNUNET_GenericReturnValue TMH_EXCHANGES_lookup_wire_fee ( const struct TMH_Exchange *exchange, const char *wire_method, struct TALER_Amount *wire_fee) { const struct FeesByWireMethod *fbm; const struct TALER_EXCHANGE_WireAggregateFees *af; fbm = get_wire_fees (exchange, GNUNET_TIME_timestamp_get (), wire_method); if (NULL == fbm) return GNUNET_NO; af = fbm->af; *wire_fee = af->fees.wire; return GNUNET_OK; } enum GNUNET_GenericReturnValue TMH_exchange_check_debit ( const struct TMH_Exchange *exchange, const struct TMH_WireMethod *wm) { if (NULL == exchange->acc_head) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "No accounts available for %s\n", exchange->url); return GNUNET_SYSERR; } for (struct ExchangeAccount *acc = exchange->acc_head; NULL != acc; acc = acc->next) { bool ok = true; if (0 != strcmp (acc->wire_method, wm->wire_method)) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s wire method %s != %s\n", exchange->url, acc->wire_method, wm->wire_method); continue; } if (NULL != acc->conversion_url) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s account requires currency conversion (not supported)\n", exchange->url); continue; /* never use accounts with conversion */ } for (struct Restriction *r = acc->d_head; NULL != r; r = r->next) { switch (r->type) { case TALER_EXCHANGE_AR_INVALID: GNUNET_break (0); ok = false; break; case TALER_EXCHANGE_AR_DENY: ok = false; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s account is disabled\n", exchange->url); break; case TALER_EXCHANGE_AR_REGEX: if (0 != regexec (&r->details.regex.ex, wm->payto_uri, 0, NULL, 0)) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s account regex does not match %s\n", exchange->url, wm->payto_uri); ok = false; } break; } if (! ok) break; } if (ok) return GNUNET_OK; } return GNUNET_NO; } void TMH_exchange_get_trusted (TMH_ExchangeCallback cb, void *cb_cls) { for (const struct TMH_Exchange *exchange = exchange_head; NULL != exchange; exchange = exchange->next) { if (! exchange->trusted) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s not trusted, skipping!\n", exchange->url); continue; } cb (cb_cls, exchange->url, exchange); } } bool TMH_test_exchange_configured_for_currency ( const char *currency) { for (const struct TMH_Exchange *exchange = exchange_head; NULL != exchange; exchange = exchange->next) { if (! exchange->trusted) continue; if (NULL == exchange->currency) continue; if (0 == strcmp (currency, exchange->currency)) return true; } return false; } /** * Function called on each configuration section. Finds sections * about exchanges, parses the entries. * * @param cls closure, with a `const struct GNUNET_CONFIGURATION_Handle *` * @param section name of the section */ static void accept_exchanges (void *cls, const char *section) { const struct GNUNET_CONFIGURATION_Handle *cfg = cls; char *url; char *mks; struct TMH_Exchange *exchange; char *currency; if (GNUNET_SYSERR == trusted_exchange_count) return; 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"); 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); return; } exchange = lookup_exchange (url); GNUNET_free (url); if (NULL == exchange->currency) exchange->currency = currency; else GNUNET_free (currency); if (GNUNET_OK == GNUNET_CONFIGURATION_get_value_string (cfg, section, "MASTER_KEY", &mks)) { if (GNUNET_OK == GNUNET_CRYPTO_eddsa_public_key_from_string ( mks, strlen (mks), &exchange->master_pub.eddsa_pub)) { exchange->trusted = true; trusted_exchange_count++; } else { GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, section, "MASTER_KEY", "malformed EdDSA key"); } GNUNET_free (mks); } else { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "MASTER_KEY missing in section '%s', not trusting exchange\n", section); } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Setup exchange %s as %s\n", exchange->url, exchange->trusted ? "trusted" : "untrusted"); if (NULL != exchange->retry_task) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Exchange at `%s' configured in multiple configuration sections (see `%s')!\n", exchange->url, section); trusted_exchange_count = GNUNET_SYSERR; return; } exchange->retry_task = GNUNET_SCHEDULER_add_now (&retry_exchange, exchange); } /** * Trigger (re)loading of keys from DB. * * @param cls NULL * @param extra base URL of the exchange that changed * @param extra_len number of bytes in @a extra */ static void update_exchange_keys (void *cls, const void *extra, size_t extra_len) { enum GNUNET_DB_QueryStatus qs; const char *url = extra; struct TMH_Exchange *exchange; struct TALER_EXCHANGE_Keys *keys; 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); exchange = lookup_exchange (url); qs = TMH_db->select_exchange_keys (TMH_db->cls, url, &keys); if (qs <= 0) { GNUNET_break (0); return; } if (NULL == exchange->currency) exchange->currency = GNUNET_strdup (keys->currency); if (0 != strcmp (keys->currency, exchange->currency)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "/keys cached in our database are for currency `%s', but we expected `%s'\n", keys->currency, exchange->currency); return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Loaded /keys from database with %u accounts\n", keys->accounts_len); if (GNUNET_OK != set_exchange_accounts (exchange, keys->accounts_len, keys->accounts)) { /* invalid account specification given */ GNUNET_break_op (0); /* but: we can continue anyway, things may just not work, but probably better than to not keep going. */ } if (GNUNET_OK != process_wire_fees (exchange, &keys->master_pub, keys->fees_len, keys->fees)) { /* invalid wire fee specification given */ GNUNET_break_op (0); /* but: we can continue anyway, things may just not work, but probably better than to not keep going. */ return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Reloaded /keys of %s from database\n", url); TALER_EXCHANGE_keys_decref (exchange->keys); exchange->keys = keys; if ( (exchange->trusted) && (0 != GNUNET_memcmp (&exchange->master_pub, &keys->master_pub)) ) { /* master pub differs => do not trust the exchange (without auditor) */ GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Master public key of exchange `%s' differs from our configuration. Not trusting exchange.\n", exchange->url); exchange->trusted = false; } if (! exchange->trusted) { exchange->master_pub = keys->master_pub; for (struct TMH_Exchange *e = exchange_head; NULL != e; e = e->next) { if (e == exchange) continue; if (! e->trusted) continue; if (0 == GNUNET_memcmp (&e->master_pub, &exchange->master_pub)) exchange->trusted = true; /* same exchange, different URL => trust applies */ } } process_find_operations (exchange); } enum GNUNET_GenericReturnValue TMH_EXCHANGES_init (const struct GNUNET_CONFIGURATION_Handle *cfg) { /* get exchanges from the merchant configuration and try to connect to them */ { struct GNUNET_DB_EventHeaderP es = { .size = ntohs (sizeof (es)), .type = ntohs (TALER_DBEVENT_MERCHANT_EXCHANGE_KEYS) }; keys_eh = TMH_db->event_listen (TMH_db->cls, &es, GNUNET_TIME_UNIT_FOREVER_REL, &update_exchange_keys, NULL); } GNUNET_CONFIGURATION_iterate_sections (cfg, &accept_exchanges, (void *) cfg); /* build JSON with list of trusted exchanges (will be included in contracts) */ return trusted_exchange_count; } void TMH_EXCHANGES_done () { if (NULL != keys_eh) { TMH_db->event_listen_cancel (keys_eh); keys_eh = NULL; } while (NULL != exchange_head) free_exchange_entry (exchange_head); } /* end of taler-merchant-httpd_exchanges.c */