diff options
author | Christian Grothoff <christian@grothoff.org> | 2024-08-07 00:06:09 +0200 |
---|---|---|
committer | Christian Grothoff <christian@grothoff.org> | 2024-08-07 00:06:09 +0200 |
commit | 93c5ae8e9e3497b16ff52c50bb9d3cd3cc4d35d6 (patch) | |
tree | dd3303426e010466e675dcd14dc874494fe74935 | |
parent | d2babb0ff1118c39ccf153523ad8ccb0f4f45d1b (diff) |
major async transformation for skip-check AML program helper execution; breaks p2p tests (reserve not found); not sure why
24 files changed, 3482 insertions, 1878 deletions
diff --git a/contrib/gana b/contrib/gana -Subproject 3ae4bcc5c40d63d91c5248311b93419e4faabf8 +Subproject a2057ac0e3c43950dcfc5024cc0abe6f362e45b diff --git a/src/exchange/Makefile.am b/src/exchange/Makefile.am index 9edbbc914..ee85bab1f 100644 --- a/src/exchange/Makefile.am +++ b/src/exchange/Makefile.am @@ -188,8 +188,7 @@ taler_exchange_httpd_SOURCES = \ 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_withdraw.c taler-exchange-httpd_withdraw.h + taler-exchange-httpd_transfers_get.c taler-exchange-httpd_transfers_get.h taler_exchange_httpd_LDADD = \ diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c index bec6cc87b..b04a56429 100644 --- a/src/exchange/taler-exchange-httpd.c +++ b/src/exchange/taler-exchange-httpd.c @@ -877,13 +877,13 @@ handle_get_reserves (struct TEH_RequestContext *rc, /** * Signature of functions that handle operations on purses. * - * @param connection HTTP request handle + * @param rc request handle * @param purse_pub the public key of the purse * @param root uploaded JSON data * @return MHD result code */ typedef MHD_RESULT -(*PurseOpHandler)(struct MHD_Connection *connection, +(*PurseOpHandler)(struct TEH_RequestContext *rc, const struct TALER_PurseContractPublicKeyP *purse_pub, const json_t *root); @@ -949,7 +949,7 @@ handle_post_purses (struct TEH_RequestContext *rc, for (unsigned int i = 0; NULL != h[i].op; i++) if (0 == strcmp (h[i].op, args[1])) - return h[i].handler (rc->connection, + return h[i].handler (rc, &purse_pub, root); return r404 (rc->connection, @@ -2570,6 +2570,12 @@ do_shutdown (void *cls) mhd = TALER_MHD_daemon_stop (); TEH_resume_keys_requests (true); + TEH_age_withdraw_cleanup (); + TEH_batch_withdraw_cleanup (); + TEH_reserves_close_cleanup (); + TEH_reserves_purse_cleanup (); + TEH_purses_merge_cleanup (); + TEH_kyc_wallet_cleanup (); TEH_kyc_upload_cleanup (); TEH_deposits_get_cleanup (); TEH_reserves_get_cleanup (); diff --git a/src/exchange/taler-exchange-httpd_age-withdraw.c b/src/exchange/taler-exchange-httpd_age-withdraw.c index dba4b9bef..1d692a2d7 100644 --- a/src/exchange/taler-exchange-httpd_age-withdraw.c +++ b/src/exchange/taler-exchange-httpd_age-withdraw.c @@ -20,6 +20,7 @@ * @file taler-exchange-httpd_age-withdraw.c * @brief Handle /reserves/$RESERVE_PUB/age-withdraw requests * @author Özgür Kesim + * @author Christian Grothoff */ #include "platform.h" #include <gnunet/gnunet_common.h> @@ -33,7 +34,7 @@ #include "taler_kyclogic_lib.h" #include "taler_mhd_lib.h" #include "taler-exchange-httpd_age-withdraw.h" -#include "taler-exchange-httpd_withdraw.h" +#include "taler-exchange-httpd_common_kyc.h" #include "taler-exchange-httpd_responses.h" #include "taler-exchange-httpd_keys.h" #include "taler_util.h" @@ -44,6 +45,37 @@ */ struct AgeWithdrawContext { + + /** + * Kept in a DLL. + */ + struct AgeWithdrawContext *next; + + /** + * Kept in a DLL. + */ + struct AgeWithdrawContext *prev; + + /** + * Handle for the legitimization check. + */ + struct TEH_LegitimizationCheckHandle *lch; + + /** + * request context + */ + const struct TEH_RequestContext *rc; + + /** + * Response to return, if set. + */ + struct MHD_Response *response; + + /** + * Public key of the reserve. + */ + struct TALER_ReservePublicKeyP reserve_pub; + /** * KYC status for the operation. */ @@ -56,6 +88,11 @@ struct AgeWithdrawContext struct TALER_PaytoHashP h_payto; /** + * value the client committed to + */ + struct TALER_AgeWithdrawCommitmentHashP ach; + + /** * Timestamp */ struct GNUNET_TIME_Timestamp now; @@ -66,9 +103,14 @@ struct AgeWithdrawContext struct TALER_EXCHANGEDB_AgeWithdraw commitment; /** + * HTTP status to return with @e response, or 0. + */ + unsigned int http_status; + + /** * Number of coins/denonations in the reveal */ - uint32_t num_coins; + unsigned int num_coins; /** * #num_coins * #kappa hashes of blinded coin planchets. @@ -81,465 +123,67 @@ struct AgeWithdrawContext */ struct TALER_DenominationHashP *denom_hs; -}; - -/* - * @brief Free the resources within a AgeWithdrawContext - * - * @param awc the context to free - */ -static void -free_age_withdraw_context_resources (struct AgeWithdrawContext *awc) -{ - GNUNET_free (awc->denom_hs); - for (unsigned int i = 0; i<awc->num_coins; i++) - { - for (unsigned int kappa = 0; kappa<TALER_CNC_KAPPA; kappa++) - { - TALER_blinded_planchet_free (&awc->coin_evs[i][kappa]); - } - } - GNUNET_free (awc->coin_evs); - GNUNET_free (awc->commitment.denom_serials); - /* - * Note: - * awc->commitment.denom_sigs and .h_coin_evs were stack allocated and - * .denom_pub_hashes is NULL for this context. + /** + * Current processing phase we are in. */ -} - - -/** - * Parse the denominations and blinded coin data of an '/age-withdraw' request. - * - * @param connection The MHD connection to handle - * @param j_denom_hs Array of n hashes of the denominations for the withdrawal, in JSON format - * @param j_blinded_coin_evs Array of n arrays of kappa blinded envelopes of in JSON format for the coins. - * @param[out] awc The context of the operation, only partially built at call time - * @param[out] mhd_ret The result if a reply is queued for MHD - * @return true on success, false on failure, with a reply already queued for MHD - */ -static enum GNUNET_GenericReturnValue -parse_age_withdraw_json ( - struct MHD_Connection *connection, - const json_t *j_denom_hs, - const json_t *j_blinded_coin_evs, - struct AgeWithdrawContext *awc, - MHD_RESULT *mhd_ret) -{ - char buf[256] = {0}; - const char *error = NULL; - unsigned int idx = 0; - json_t *value = NULL; - struct GNUNET_HashContext *hash_context; - - - /* The age value MUST be on the beginning of an age group */ - if (awc->commitment.max_age != - TALER_get_lowest_age (&TEH_age_restriction_config.mask, - awc->commitment.max_age)) + enum { - error = "max_age must be the lower edge of an age group"; - goto EXIT; - } - - /* Verify JSON-structure consistency */ - { - uint32_t num_coins = json_array_size (j_denom_hs); - - if (! json_is_array (j_denom_hs)) - error = "denoms_h must be an array"; - else if (! json_is_array (j_blinded_coin_evs)) - error = "coin_evs must be an array"; - else if (num_coins == 0) - error = "denoms_h must not be empty"; - else if (num_coins != json_array_size (j_blinded_coin_evs)) - error = "denoms_h and coins_evs must be arrays of the same size"; - else if (num_coins > TALER_MAX_FRESH_COINS) - /** - * The wallet had committed to more than the maximum coins allowed, the - * reserve has been charged, but now the user can not withdraw any money - * from it. Note that the user can't get their money back in this case! - **/ - error = "maximum number of coins that can be withdrawn has been exceeded"; - - _Static_assert ((TALER_MAX_FRESH_COINS < INT_MAX / TALER_CNC_KAPPA), - "TALER_MAX_FRESH_COINS too large"); - - if (NULL != error) - goto EXIT; - - awc->num_coins = num_coins; - awc->commitment.num_coins = num_coins; - } + AWC_PHASE_CHECK_KEYS = 1, + AWC_PHASE_CHECK_RESERVE_SIGNATURE, + AWC_PHASE_RUN_LEGI_CHECK, + AWC_PHASE_SUSPENDED, + AWC_PHASE_CHECK_KYC_RESULT, + AWC_PHASE_PREPARE_TRANSACTION, + AWC_PHASE_RUN_TRANSACTION, + AWC_PHASE_GENERATE_REPLY_SUCCESS, + AWC_PHASE_GENERATE_REPLY_FAILURE, + AWC_PHASE_RETURN_YES, + AWC_PHASE_RETURN_NO + } phase; - /* Continue parsing the parts */ - - /* Parse denomination keys */ - awc->denom_hs = GNUNET_new_array (awc->num_coins, - struct TALER_DenominationHashP); - - json_array_foreach (j_denom_hs, idx, value) { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto (NULL, &awc->denom_hs[idx]), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (value, spec, NULL, NULL)) - { - GNUNET_snprintf (buf, - sizeof(buf), - "couldn't parse entry no. %d in array denoms_h", - idx + 1); - error = buf; - goto EXIT; - } - }; - - { - typedef struct TALER_BlindedPlanchet - _array_of_kappa_planchets[TALER_CNC_KAPPA]; - - awc->coin_evs = GNUNET_new_array (awc->num_coins, - _array_of_kappa_planchets); - } - - hash_context = GNUNET_CRYPTO_hash_context_start (); - GNUNET_assert (NULL != hash_context); - - /* Parse blinded envelopes. */ - json_array_foreach (j_blinded_coin_evs, idx, value) { - const json_t *j_kappa_coin_evs = value; - - if (! json_is_array (j_kappa_coin_evs)) - { - GNUNET_snprintf (buf, - sizeof(buf), - "enxtry %d in array blinded_coin_evs is not an array", - idx + 1); - error = buf; - goto EXIT; - } - else if (TALER_CNC_KAPPA != json_array_size (j_kappa_coin_evs)) - { - GNUNET_snprintf (buf, - sizeof(buf), - "array no. %d in coin_evs not of correct size", - idx + 1); - error = buf; - goto EXIT; - } - - /* Now parse the individual kappa envelopes and calculate the hash of - * the commitment along the way. */ - { - unsigned int kappa = 0; - - json_array_foreach (j_kappa_coin_evs, kappa, value) { - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_blinded_planchet (NULL, - &awc->coin_evs[idx][kappa]), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (value, - spec, - NULL, - NULL)) - { - GNUNET_snprintf (buf, - sizeof(buf), - "couldn't parse array no. %d in blinded_coin_evs[%d]", - kappa + 1, - idx + 1); - error = buf; - goto EXIT; - } - - /* Continue to hash of the coin candidates */ - { - struct TALER_BlindedCoinHashP bch; - - TALER_coin_ev_hash (&awc->coin_evs[idx][kappa], - &awc->denom_hs[idx], - &bch); - GNUNET_CRYPTO_hash_context_read (hash_context, - &bch, - sizeof(bch)); - } - - /* Check for duplicate planchets. Technically a bug on - * the client side that is harmless for us, but still - * not allowed per protocol */ - for (unsigned int i = 0; i < idx; i++) - { - if (0 == TALER_blinded_planchet_cmp (&awc->coin_evs[idx][kappa], - &awc->coin_evs[i][kappa])) - { - GNUNET_JSON_parse_free (spec); - error = "duplicate planchet"; - goto EXIT; - } - } - } - } - }; /* json_array_foreach over j_blinded_coin_evs */ - - /* Finally, calculate the h_commitment from all blinded envelopes */ - GNUNET_CRYPTO_hash_context_finish (hash_context, - &awc->commitment.h_commitment.hash); - - GNUNET_assert (NULL == error); - - -EXIT: - if (NULL != error) - { - /* Note: resources are freed in caller */ - - *mhd_ret = TALER_MHD_reply_with_ec ( - connection, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - error); - return GNUNET_SYSERR; - } - - return GNUNET_OK; -} +}; /** - * Check if the given denomination is still or already valid, has not been - * revoked and supports age restriction. - * - * @param connection HTTP-connection to the client - * @param ksh The handle to the current state of (denomination) keys in the exchange - * @param denom_h Hash of the denomination key to check - * @param[out] pdk On success, will contain the denomination key details - * @param[out] result On failure, an MHD-response will be queued and result will be set to accordingly - * @return true on success (denomination valid), false otherwise + * Kept in a DLL. */ -static bool -denomination_is_valid ( - struct MHD_Connection *connection, - struct TEH_KeyStateHandle *ksh, - const struct TALER_DenominationHashP *denom_h, - struct TEH_DenominationKey **pdk, - MHD_RESULT *result) -{ - struct TEH_DenominationKey *dk; - dk = TEH_keys_denomination_by_hash_from_state (ksh, - denom_h, - connection, - result); - if (NULL == dk) - { - /* The denomination doesn't exist */ - /* Note: a HTTP-response has been queued and result has been set by - * TEH_keys_denominations_by_hash_from_state */ - return false; - } - - if (GNUNET_TIME_absolute_is_past (dk->meta.expire_withdraw.abs_time)) - { - /* This denomination is past the expiration time for withdraws */ - /* FIXME[oec]: add idempotency check */ - *result = TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - denom_h, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, - "age-withdraw_reveal"); - return false; - } - - if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) - { - /* This denomination is not yet valid */ - *result = TEH_RESPONSE_reply_expired_denom_pub_hash ( - connection, - denom_h, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, - "age-withdraw_reveal"); - return false; - } - - if (dk->recoup_possible) - { - /* This denomination has been revoked */ - *result = TALER_MHD_reply_with_ec ( - connection, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, - NULL); - return false; - } - - if (0 == dk->denom_pub.age_mask.bits) - { - /* This denomation does not support age restriction */ - char msg[256] = {0}; - GNUNET_snprintf (msg, - sizeof(msg), - "denomination %s does not support age restriction", - GNUNET_h2s (&denom_h->hash)); - - *result = TALER_MHD_reply_with_ec ( - connection, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, - msg); - return false; - } - - *pdk = dk; - return true; -} - +static struct AgeWithdrawContext *awc_head; /** - * Check if the given array of hashes of denomination_keys a) belong - * to valid denominations and b) those are marked as age restricted. - * Also, calculate the total amount of the denominations including fees - * for withdraw. - * - * @param connection The HTTP connection to the client - * @param len The lengths of the array @a denoms_h - * @param denom_hs array of hashes of denomination public keys - * @param coin_evs array of blinded coin planchet candidates - * @param[out] denom_serials On success, will be filled with the serial-id's of the denomination keys. Caller must deallocate. - * @param[out] amount_with_fee On success, will contain the committed amount including fees - * @param[out] result In the error cases, a response will be queued with MHD and this will be the result. - * @return #GNUNET_OK if the denominations are valid and support age-restriction - * #GNUNET_SYSERR otherwise + * Kept in a DLL. */ -static enum GNUNET_GenericReturnValue -are_denominations_valid ( - struct MHD_Connection *connection, - uint32_t len, - const struct TALER_DenominationHashP *denom_hs, - const struct TALER_BlindedPlanchet (*coin_evs) [ TALER_CNC_KAPPA], - uint64_t **denom_serials, - struct TALER_Amount *amount_with_fee, - MHD_RESULT *result) -{ - struct TALER_Amount total_amount; - struct TALER_Amount total_fee; - struct TEH_KeyStateHandle *ksh; - uint64_t *serials; +static struct AgeWithdrawContext *awc_tail; - ksh = TEH_keys_get_state (); - if (NULL == ksh) - { - *result = TALER_MHD_reply_with_ec (connection, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL); - return GNUNET_SYSERR; - } - *denom_serials = - serials = GNUNET_new_array (len, uint64_t); - - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &total_amount)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &total_fee)); +void +TEH_age_withdraw_cleanup () +{ + struct AgeWithdrawContext *awc; - for (uint32_t i = 0; i < len; i++) + while (NULL != (awc = awc_head)) { - struct TEH_DenominationKey *dk; - if (! denomination_is_valid (connection, - ksh, - &denom_hs[i], - &dk, - result)) - /* FIXME[oec]: add idempotency check */ - return GNUNET_SYSERR; - - /* Ensure the ciphers from the planchets match the denominations' */ - for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) - { - if (dk->denom_pub.bsign_pub_key->cipher != - coin_evs[i][k].blinded_message->cipher) - { - GNUNET_break_op (0); - *result = TALER_MHD_reply_with_ec (connection, - TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, - NULL); - return GNUNET_SYSERR; - } - } - - /* Accumulate the values */ - if (0 > TALER_amount_add (&total_amount, - &total_amount, - &dk->meta.value)) - { - GNUNET_break_op (0); - *result = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW, - "amount"); - return GNUNET_SYSERR; - } - - /* Accumulate the withdraw fees */ - if (0 > TALER_amount_add (&total_fee, - &total_fee, - &dk->meta.fees.withdraw)) - { - GNUNET_break_op (0); - *result = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW, - "fee"); - return GNUNET_SYSERR; - } - - serials[i] = dk->meta.serial; + GNUNET_CONTAINER_DLL_remove (awc_head, + awc_tail, + awc); + MHD_resume_connection (awc->rc->connection); } - - /* Save the total amount including fees */ - GNUNET_assert (0 < TALER_amount_add (amount_with_fee, - &total_amount, - &total_fee)); - - return GNUNET_OK; } /** - * @brief Verify the signature of the request body with the reserve key + * Terminate the main loop by returning the final + * result. * - * @param connection the connection to the client - * @param commitment the age withdraw commitment - * @param mhd_ret the response to fill in the error case - * @return GNUNET_OK on success + * @param[in,out] awc context to update phase for + * @param mres MHD status to return */ -static enum GNUNET_GenericReturnValue -verify_reserve_signature ( - struct MHD_Connection *connection, - const struct TALER_EXCHANGEDB_AgeWithdraw *commitment, - enum MHD_Result *mhd_ret) +static void +finish_loop (struct AgeWithdrawContext *awc, + MHD_RESULT mres) { - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_age_withdraw_verify (&commitment->h_commitment, - &commitment->amount_with_fee, - &TEH_age_restriction_config.mask, - commitment->max_age, - &commitment->reserve_pub, - &commitment->reserve_sig)) - { - GNUNET_break_op (0); - *mhd_ret = TALER_MHD_reply_with_ec (connection, - TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID, - NULL); - return GNUNET_SYSERR; - } - - return GNUNET_OK; + awc->phase = (MHD_YES == mres) + ? AWC_PHASE_RETURN_YES + : AWC_PHASE_RETURN_NO; } @@ -549,14 +193,17 @@ verify_reserve_signature ( * @param connection the connection to send the response to * @param ach value the client committed to * @param noreveal_index which index will the client not have to reveal - * @return a MHD status code */ -static MHD_RESULT +static void reply_age_withdraw_success ( - struct MHD_Connection *connection, - const struct TALER_AgeWithdrawCommitmentHashP *ach, - uint32_t noreveal_index) + struct AgeWithdrawContext *awc) { + struct MHD_Connection *connection + = awc->rc->connection; + const struct TALER_AgeWithdrawCommitmentHashP *ach + = &awc->commitment.h_commitment; + uint32_t noreveal_index + = awc->commitment.noreveal_index; struct TALER_ExchangePublicKeyP pub; struct TALER_ExchangeSignatureP sig; enum TALER_ErrorCode ec; @@ -568,17 +215,23 @@ reply_age_withdraw_success ( &pub, &sig); if (TALER_EC_NONE != ec) - return TALER_MHD_reply_with_ec (connection, - ec, - NULL); - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_uint64 ("noreveal_index", - noreveal_index), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &pub)); + { + finish_loop (awc, + TALER_MHD_reply_with_ec (connection, + ec, + NULL)); + return; + } + finish_loop (awc, + TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_uint64 ("noreveal_index", + noreveal_index), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &pub))); } @@ -587,42 +240,40 @@ reply_age_withdraw_success ( * answer. If so, replay the existing answer and return the * HTTP response. * - * @param con connection to the client * @param[in,out] awc parsed request data - * @param[out] mret HTTP status, set if we return true * @return true if the request is idempotent with an existing request * false if we did not find the request in the DB and did not set @a mret */ static bool -request_is_idempotent (struct MHD_Connection *con, - struct AgeWithdrawContext *awc, - MHD_RESULT *mret) +check_request_idempotent ( + struct AgeWithdrawContext *awc) { enum GNUNET_DB_QueryStatus qs; struct TALER_EXCHANGEDB_AgeWithdraw commitment; - qs = TEH_plugin->get_age_withdraw (TEH_plugin->cls, - &awc->commitment.reserve_pub, - &awc->commitment.h_commitment, - &commitment); + qs = TEH_plugin->get_age_withdraw ( + TEH_plugin->cls, + &awc->commitment.reserve_pub, + &awc->commitment.h_commitment, + &commitment); if (0 > qs) { + /* FIXME: soft error not handled correctly! */ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); if (GNUNET_DB_STATUS_HARD_ERROR == qs) - *mret = TALER_MHD_reply_with_ec (con, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_age_withdraw"); - return true; /* Well, kind-of. At least we have set mret. */ + finish_loop (awc, + TALER_MHD_reply_with_ec ( + awc->rc->connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_age_withdraw")); + return true; /* Well, kind-of. */ } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) return false; /* Generate idempotent reply */ TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_AGE_WITHDRAW]++; - *mret = reply_age_withdraw_success (con, - &commitment.h_commitment, - commitment.noreveal_index); + awc->phase = AWC_PHASE_GENERATE_REPLY_SUCCESS; return true; } @@ -642,9 +293,10 @@ request_is_idempotent (struct MHD_Connection *con, * @return transaction status */ static enum GNUNET_DB_QueryStatus -age_withdraw_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) +age_withdraw_transaction ( + void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) { struct AgeWithdrawContext *awc = cls; enum GNUNET_DB_QueryStatus qs; @@ -656,82 +308,73 @@ age_withdraw_transaction (void *cls, uint32_t reserve_birthday = 0; struct TALER_Amount reserve_balance; - qs = TEH_withdraw_kyc_check (&awc->kyc, - &awc->h_payto, - connection, - mhd_ret, - &awc->commitment.reserve_pub, - &awc->commitment.amount_with_fee, - awc->now); - if ( (qs < 0) || - (! awc->kyc.ok) ) - return qs; - qs = TEH_plugin->do_age_withdraw (TEH_plugin->cls, - &awc->commitment, - awc->now, - &found, - &balance_ok, - &reserve_balance, - &age_ok, - &allowed_maximum_age, - &reserve_birthday, - &conflict); + qs = TEH_plugin->do_age_withdraw ( + TEH_plugin->cls, + &awc->commitment, + awc->now, + &found, + &balance_ok, + &reserve_balance, + &age_ok, + &allowed_maximum_age, + &reserve_birthday, + &conflict); if (0 > qs) { if (GNUNET_DB_STATUS_HARD_ERROR == qs) - *mhd_ret = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "do_age_withdraw"); + finish_loop (awc, + TALER_MHD_reply_with_ec ( + awc->rc->connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "do_age_withdraw")); return qs; } if (! found) { - *mhd_ret = TALER_MHD_reply_with_ec (connection, - TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, - NULL); + finish_loop (awc, + TALER_MHD_reply_with_ec ( + awc->rc->connection, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL)); return GNUNET_DB_STATUS_HARD_ERROR; } if (! age_ok) { - enum TALER_ErrorCode ec = - TALER_EC_EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE; - - *mhd_ret = - TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_CONFLICT, - TALER_MHD_PACK_EC (ec), - GNUNET_JSON_pack_uint64 ("allowed_maximum_age", - allowed_maximum_age), - GNUNET_JSON_pack_uint64 ("reserve_birthday", - reserve_birthday)); - + finish_loop (awc, + TALER_MHD_REPLY_JSON_PACK ( + awc->rc->connection, + MHD_HTTP_CONFLICT, + TALER_MHD_PACK_EC ( + TALER_EC_EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE), + GNUNET_JSON_pack_uint64 ( + "allowed_maximum_age", + allowed_maximum_age), + GNUNET_JSON_pack_uint64 ( + "reserve_birthday", + reserve_birthday))); return GNUNET_DB_STATUS_HARD_ERROR; } if (! balance_ok) { TEH_plugin->rollback (TEH_plugin->cls); - - *mhd_ret = TEH_RESPONSE_reply_reserve_insufficient_balance ( - connection, - TALER_EC_EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS, - &reserve_balance, - &awc->commitment.amount_with_fee, - &awc->commitment.reserve_pub); - + finish_loop (awc, + TEH_RESPONSE_reply_reserve_insufficient_balance ( + awc->rc->connection, + TALER_EC_EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS, + &reserve_balance, + &awc->commitment.amount_with_fee, + &awc->commitment.reserve_pub)); return GNUNET_DB_STATUS_HARD_ERROR; } if (conflict) { /* do_age_withdraw signaled a conflict, so there MUST be an entry * in the DB. Put that into the response */ - bool ok = request_is_idempotent (connection, - awc, - mhd_ret); - GNUNET_assert (ok); + if (check_request_idempotent (awc)) + return GNUNET_DB_STATUS_HARD_ERROR; + GNUNET_break (0); return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } - *mhd_ret = -1; if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) TEH_METRICS_num_success[TEH_MT_SUCCESS_AGE_WITHDRAW]++; return qs; @@ -739,39 +382,63 @@ age_withdraw_transaction (void *cls, /** - * @brief Sign the chosen blinded coins, debit the reserve and persist - * the commitment. + * @brief Persist the commitment. * * On conflict, the noreveal_index from the previous, existing * commitment is returned to the client, returning success. * * On error (like, insufficient funds), the client is notified. * - * Note that on success, there are two possible states: - * 1.) KYC is required (awc.kyc.ok == false) or - * 2.) age withdraw was successful. + * @param awc The context for the current age withdraw request + */ +static void +run_transaction ( + struct AgeWithdrawContext *awc) +{ + MHD_RESULT mhd_ret; + + GNUNET_assert (AWC_PHASE_RUN_TRANSACTION == + awc->phase); + if (GNUNET_OK != + TEH_DB_run_transaction (awc->rc->connection, + "run age withdraw", + TEH_MT_REQUEST_AGE_WITHDRAW, + &mhd_ret, + &age_withdraw_transaction, + awc)) + { + if (AWC_PHASE_RUN_TRANSACTION == awc->phase) + finish_loop (awc, + mhd_ret); + return; + } + awc->phase++; +} + + +/** + * @brief Sign the chosen blinded coins. * - * @param connection HTTP-connection to the client * @param awc The context for the current age withdraw request - * @param[out] result On error, a HTTP-response will be queued and result set accordingly - * @return #GNUNET_OK on success, #GNUNET_SYSERR otherwise */ -static enum GNUNET_GenericReturnValue -sign_and_do_age_withdraw ( - struct MHD_Connection *connection, - struct AgeWithdrawContext *awc, - MHD_RESULT *result) +static void +prepare_transaction ( + struct AgeWithdrawContext *awc) { - enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; - struct TALER_BlindedCoinHashP h_coin_evs[awc->num_coins]; - struct TALER_BlindedDenominationSignature denom_sigs[awc->num_coins]; uint8_t noreveal_index; + awc->commitment.denom_sigs + = GNUNET_new_array ( + awc->num_coins, + struct TALER_BlindedDenominationSignature); + awc->commitment.h_coin_evs + = GNUNET_new_array ( + awc->num_coins, + struct TALER_BlindedCoinHashP); /* Pick the challenge */ noreveal_index = GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG, TALER_CNC_KAPPA); - awc->commitment.noreveal_index = noreveal_index; /* Choose and sign the coins */ @@ -782,157 +449,727 @@ sign_and_do_age_withdraw ( /* Pick the chosen blinded coins */ for (uint32_t i = 0; i<awc->num_coins; i++) { - csds[i].bp = &awc->coin_evs[i][noreveal_index]; - csds[i].h_denom_pub = &awc->denom_hs[i]; + struct TEH_CoinSignData *csdsi = &csds[i]; + + csdsi->bp = &awc->coin_evs[i][noreveal_index]; + csdsi->h_denom_pub = &awc->denom_hs[i]; } - ec = TEH_keys_denomination_batch_sign (awc->num_coins, - csds, - false, - denom_sigs); + ec = TEH_keys_denomination_batch_sign ( + awc->num_coins, + csds, + false, + awc->commitment.denom_sigs); if (TALER_EC_NONE != ec) { GNUNET_break (0); - *result = TALER_MHD_reply_with_ec (connection, - ec, - NULL); - return GNUNET_SYSERR; + finish_loop (awc, + TALER_MHD_reply_with_ec ( + awc->rc->connection, + ec, + NULL)); + return; } } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Signatures ready, starting DB interaction\n"); - /* Prepare the hashes of the coins for insertion */ for (uint32_t i = 0; i<awc->num_coins; i++) { TALER_coin_ev_hash (&awc->coin_evs[i][noreveal_index], &awc->denom_hs[i], - &h_coin_evs[i]); + &awc->commitment.h_coin_evs[i]); + } + awc->phase++; +} + + +/** + * Check the KYC result. + * + * @param awc storage for request processing + */ +static void +check_kyc_result (struct AgeWithdrawContext *awc) +{ + /* return final positive response */ + if (! awc->kyc.ok) + { + if (check_request_idempotent (awc)) + return; + /* KYC required */ + finish_loop (awc, + TEH_RESPONSE_reply_kyc_required ( + awc->rc->connection, + &awc->h_payto, + &awc->kyc)); + return; + } + awc->phase++; +} + + +/** + * Function called with the result of a legitimization + * check. + * + * @param cls closure + * @param lcr legitimization check result + */ +static void +withdraw_legi_cb ( + void *cls, + const struct TEH_LegitimizationCheckResult *lcr) +{ + struct AgeWithdrawContext *awc = cls; + + awc->lch = NULL; + GNUNET_assert (AWC_PHASE_SUSPENDED == + awc->phase); + MHD_resume_connection (awc->rc->connection); + GNUNET_CONTAINER_DLL_remove (awc_head, + awc_tail, + awc); + TALER_MHD_daemon_trigger (); + if (NULL != lcr->response) + { + awc->response = lcr->response; + awc->http_status = lcr->http_status; + awc->phase = AWC_PHASE_GENERATE_REPLY_FAILURE; + return; } + awc->kyc = lcr->kyc; + awc->phase = AWC_PHASE_CHECK_KYC_RESULT; +} + + +/** + * Function called to iterate over KYC-relevant transaction amounts for a + * particular time range. Called within a database transaction, so must + * not start a new one. + * + * @param cls closure, identifies the event type and account to iterate + * over events for + * @param limit maximum time-range for which events should be fetched + * (timestamp in the past) + * @param cb function to call on each event found, events must be returned + * in reverse chronological order + * @param cb_cls closure for @a cb, of type struct AgeWithdrawContext + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +withdraw_amount_cb ( + void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct AgeWithdrawContext *awc = cls; + enum GNUNET_GenericReturnValue ret; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Signaling amount %s for KYC check during age-withdrawal\n", + TALER_amount2s (&awc->commitment.amount_with_fee)); + ret = cb (cb_cls, + &awc->commitment.amount_with_fee, + awc->now.abs_time); + GNUNET_break (GNUNET_SYSERR != ret); + if (GNUNET_OK != ret) + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + qs = TEH_plugin->select_withdraw_amounts_for_kyc_check ( + TEH_plugin->cls, + &awc->h_payto, + limit, + cb, + cb_cls); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got %d additional transactions for this age-withdrawal and limit %llu\n", + qs, + (unsigned long long) limit.abs_value_us); + GNUNET_break (qs >= 0); + return qs; +} + + +/** + * Do legitimization check. + * + * @param awc operation context + */ +static void +run_legi_check (struct AgeWithdrawContext *awc) +{ + enum GNUNET_DB_QueryStatus qs; + char *payto_uri; + + /* Check if the money came from a wire transfer */ + qs = TEH_plugin->reserves_get_origin ( + TEH_plugin->cls, + &awc->commitment.reserve_pub, + &awc->h_payto, + &payto_uri); + if (qs < 0) + { + finish_loop (awc, + TALER_MHD_reply_with_error ( + awc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "reserves_get_origin")); + return; + } + /* If _no_ results, reserve was created by merge, + in which case no KYC check is required as the + merge already did that. */ + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + awc->phase = AWC_PHASE_PREPARE_TRANSACTION; + return; + } + + awc->lch = TEH_legitimization_check ( + &awc->rc->async_scope_id, + TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW, + payto_uri, + &awc->h_payto, + NULL, + &withdraw_amount_cb, + awc, + &withdraw_legi_cb, + awc); + GNUNET_assert (NULL != awc->lch); + GNUNET_free (payto_uri); + GNUNET_CONTAINER_DLL_insert (awc_head, + awc_tail, + awc); + MHD_suspend_connection (awc->rc->connection); + awc->phase = AWC_PHASE_SUSPENDED; +} + + +/** + * Check that the client signature authorizing the + * withdrawal is valid. + * + * @param[in,out] awc request context to check + */ +static void +check_reserve_signature ( + struct AgeWithdrawContext *awc) +{ + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_age_withdraw_verify ( + &awc->commitment.h_commitment, + &awc->commitment.amount_with_fee, + &TEH_age_restriction_config.mask, + awc->commitment.max_age, + &awc->commitment.reserve_pub, + &awc->commitment.reserve_sig)) + { + GNUNET_break_op (0); + finish_loop (awc, + TALER_MHD_reply_with_ec ( + awc->rc->connection, + TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID, + NULL)); + return; + } + awc->phase++; +} + + +/** + * Check if the given denomination is still or already valid, has not been + * revoked and supports age restriction. + * + * @param connection HTTP-connection to the client + * @param ksh The handle to the current state of (denomination) keys in the exchange + * @param denom_h Hash of the denomination key to check + * @return NULL on failure (denomination invalid) + */ +static struct TEH_DenominationKey * +denomination_is_valid ( + struct AgeWithdrawContext *awc, + struct TEH_KeyStateHandle *ksh, + const struct TALER_DenominationHashP *denom_h) +{ + struct MHD_Connection *connection = awc->rc->connection; + struct TEH_DenominationKey *dk; + MHD_RESULT result; + + dk = TEH_keys_denomination_by_hash_from_state ( + ksh, + denom_h, + connection, + &result); + if (NULL == dk) + { + /* The denomination doesn't exist */ + /* Note: a HTTP-response has been queued and result has been set by + * TEH_keys_denominations_by_hash_from_state */ + /* FIXME-Oec: lacks idempotency check... */ + finish_loop (awc, + result); + return NULL; + } + + if (GNUNET_TIME_absolute_is_past (dk->meta.expire_withdraw.abs_time)) + { + /* This denomination is past the expiration time for withdrawc */ + /* FIXME[oec]: add idempotency check */ + finish_loop (awc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "age-withdraw_reveal")); + return NULL; + } + + if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + { + /* This denomination is not yet valid */ + finish_loop (awc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + denom_h, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "age-withdraw_reveal")); + return NULL; + } + + if (dk->recoup_possible) + { + /* This denomination has been revoked */ + finish_loop (awc, + TALER_MHD_reply_with_ec ( + connection, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, + NULL)); + return NULL; + } + + if (0 == dk->denom_pub.age_mask.bits) + { + /* This denomation does not support age restriction */ + char msg[256]; + + GNUNET_snprintf (msg, + sizeof(msg), + "denomination %s does not support age restriction", + GNUNET_h2s (&denom_h->hash)); + + finish_loop (awc, + TALER_MHD_reply_with_ec ( + connection, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN, + msg)); + return NULL; + } + + return dk; +} + + +/** + * Check if the given array of hashes of denomination_keys a) belong + * to valid denominations and b) those are marked as age restricted. + * Also, calculate the total amount of the denominations including fees + * for withdraw. + * + * @param awc context to check keys for + */ +static void +check_keys ( + struct AgeWithdrawContext *awc) +{ + struct MHD_Connection *connection + = awc->rc->connection; + unsigned int len + = awc->num_coins; + struct TALER_Amount total_amount; + struct TALER_Amount total_fee; + struct TEH_KeyStateHandle *ksh; + + ksh = TEH_keys_get_state (); + if (NULL == ksh) + { + finish_loop (awc, + TALER_MHD_reply_with_ec ( + connection, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL)); + return; + } + + awc->commitment.denom_serials + = GNUNET_new_array (len, + uint64_t); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &total_amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &total_fee)); + for (unsigned int i = 0; i < len; i++) + { + struct TEH_DenominationKey *dk; + + dk = denomination_is_valid (awc, + ksh, + &awc->denom_hs[i]); + if (NULL == dk) + /* FIXME[oec]: add idempotency check */ + return; + + /* Ensure the ciphers from the planchets match the denominations' */ + for (uint8_t k = 0; k < TALER_CNC_KAPPA; k++) + { + if (dk->denom_pub.bsign_pub_key->cipher != + awc->coin_evs[i][k].blinded_message->cipher) + { + GNUNET_break_op (0); + finish_loop (awc, + TALER_MHD_reply_with_ec ( + connection, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + NULL)); + return; + } + } + + /* Accumulate the values */ + if (0 > TALER_amount_add (&total_amount, + &total_amount, + &dk->meta.value)) + { + GNUNET_break_op (0); + finish_loop (awc, + TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW, + "amount")); + return; + } + + /* Accumulate the withdraw fees */ + if (0 > TALER_amount_add (&total_fee, + &total_fee, + &dk->meta.fees.withdraw)) + { + GNUNET_break_op (0); + finish_loop (awc, + TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW, + "fee")); + return; + } + awc->commitment.denom_serials[i] = dk->meta.serial; + } + + /* Save the total amount including fees */ + GNUNET_assert (0 < + TALER_amount_add ( + &awc->commitment.amount_with_fee, + &total_amount, + &total_fee)); + awc->phase++; +} + + +/** + * Age-withdraw-specific cleanup routine. Function called + * upon completion of the request that should + * clean up @a rh_ctx. Can be NULL. + * + * @param rc request context to clean up + */ +static void +clean_age_withdraw_rc (struct TEH_RequestContext *rc) +{ + struct AgeWithdrawContext *awc = rc->rh_ctx; - /* Run the transaction */ - awc->commitment.h_coin_evs = h_coin_evs; - awc->commitment.denom_sigs = denom_sigs; - ret = TEH_DB_run_transaction (connection, - "run age withdraw", - TEH_MT_REQUEST_AGE_WITHDRAW, - result, - &age_withdraw_transaction, - awc); - /* Free resources */ for (unsigned int i = 0; i<awc->num_coins; i++) - TALER_blinded_denom_sig_free (&denom_sigs[i]); - awc->commitment.h_coin_evs = NULL; - awc->commitment.denom_sigs = NULL; - return ret; + { + for (unsigned int kappa = 0; kappa<TALER_CNC_KAPPA; kappa++) + { + TALER_blinded_planchet_free (&awc->coin_evs[i][kappa]); + } + } + for (unsigned int i = 0; i<awc->num_coins; i++) + { + TALER_blinded_denom_sig_free (&awc->commitment.denom_sigs[i]); + } + GNUNET_free (awc->commitment.h_coin_evs); + GNUNET_free (awc->commitment.denom_sigs); + GNUNET_free (awc->denom_hs); + GNUNET_free (awc->coin_evs); + GNUNET_free (awc->commitment.denom_serials); + GNUNET_free (awc); } MHD_RESULT -TEH_handler_age_withdraw (struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root) +TEH_handler_age_withdraw ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root) { - MHD_RESULT mhd_ret; - const json_t *j_denom_hs; - const json_t *j_blinded_coin_evs; - struct AgeWithdrawContext awc = { - .commitment.reserve_pub = *reserve_pub, - .now = GNUNET_TIME_timestamp_get () - }; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("denom_hs", - &j_denom_hs), - GNUNET_JSON_spec_array_const ("blinded_coin_evs", - &j_blinded_coin_evs), - GNUNET_JSON_spec_uint16 ("max_age", - &awc.commitment.max_age), - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &awc.commitment.reserve_sig), - GNUNET_JSON_spec_end () - }; - - /* Parse the JSON body */ + struct AgeWithdrawContext *awc = rc->rh_ctx; + + if (NULL == awc) { - enum GNUNET_GenericReturnValue res; + awc = GNUNET_new (struct AgeWithdrawContext); + rc->rh_ctx = awc; + rc->rh_cleaner = &clean_age_withdraw_rc; + awc->rc = rc; + awc->commitment.reserve_pub = *reserve_pub; + awc->now = GNUNET_TIME_timestamp_get (); - res = TALER_MHD_parse_json_data (rc->connection, - root, - spec); - if (GNUNET_OK != res) - return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; - } + { + const json_t *j_denom_hs; + const json_t *j_blinded_coin_evs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("denom_hs", + &j_denom_hs), + GNUNET_JSON_spec_array_const ("blinded_coin_evs", + &j_blinded_coin_evs), + GNUNET_JSON_spec_uint16 ("max_age", + &awc->commitment.max_age), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &awc->commitment.reserve_sig), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_OK != res) + return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; + + /* The age value MUST be on the beginning of an age group */ + if (awc->commitment.max_age != + TALER_get_lowest_age (&TEH_age_restriction_config.mask, + awc->commitment.max_age)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_ec ( + rc->connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "max_age must be the lower edge of an age group"); + } - do { - /* Note: If we break the statement here at any point, - * a response to the client MUST have been populated - * with an appropriate answer and mhd_ret MUST have - * been set accordingly. - */ - - /* Parse denoms_h and blinded_coins_evs, partially fill awc */ - if (GNUNET_OK != - parse_age_withdraw_json (rc->connection, - j_denom_hs, - j_blinded_coin_evs, - &awc, - &mhd_ret)) - break; + { + size_t num_coins = json_array_size (j_denom_hs); + const char *error = NULL; + + if (0 == num_coins) + error = "denoms_h must not be empty"; + else if (num_coins != json_array_size (j_blinded_coin_evs)) + error = "denoms_h and coins_evs must be arrays of the same size"; + else if (num_coins > TALER_MAX_FRESH_COINS) + /** + * The wallet had committed to more than the maximum coins allowed, the + * reserve has been charged, but now the user can not withdraw any money + * from it. Note that the user can't get their money back in this case! + */ + error = + "maximum number of coins that can be withdrawn has been exceeded"; + + _Static_assert ((TALER_MAX_FRESH_COINS < INT_MAX / TALER_CNC_KAPPA), + "TALER_MAX_FRESH_COINS too large"); + + if (NULL != error) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_ec ( + rc->connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + error); + } + awc->num_coins = (unsigned int) num_coins; + awc->commitment.num_coins = (unsigned int) num_coins; + } - /* Ensure validity of denoms and calculate amounts and fees */ - if (GNUNET_OK != - are_denominations_valid (rc->connection, - awc.num_coins, - awc.denom_hs, - awc.coin_evs, - &awc.commitment.denom_serials, - &awc.commitment.amount_with_fee, - &mhd_ret)) - break; + awc->denom_hs + = GNUNET_new_array (awc->num_coins, + struct TALER_DenominationHashP); + { + size_t idx; + json_t *value; + + json_array_foreach (j_denom_hs, idx, value) { + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto (NULL, + &awc->denom_hs[idx]), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (rc->connection, + value, + ispec); + if (GNUNET_OK != res) + return (GNUNET_SYSERR == res) + ? MHD_NO + : MHD_YES; + } + } + { + typedef struct TALER_BlindedPlanchet + _array_of_kappa_planchets[TALER_CNC_KAPPA]; - /* Now that amount_with_fee is calculated, verify the signature of - * the request body with the reserve key. - */ - if (GNUNET_OK != - verify_reserve_signature (rc->connection, - &awc.commitment, - &mhd_ret)) - break; + awc->coin_evs = GNUNET_new_array (awc->num_coins, + _array_of_kappa_planchets); + } + { + struct GNUNET_HashContext *hash_context; - /* Sign the chosen blinded coins, persist the commitment and - * charge the reserve. - * On error (like, insufficient funds), the client is notified. - * On conflict, the noreveal_index from the previous, existing - * commitment is returned to the client, returning success. - * Note that on success, there are two possible states: - * KYC is required (awc.kyc.ok == false) or - * age withdraw was successful. - */ - if (GNUNET_OK != - sign_and_do_age_withdraw (rc->connection, - &awc, - &mhd_ret)) - break; + hash_context = GNUNET_CRYPTO_hash_context_start (); + GNUNET_assert (NULL != hash_context); - /* Send back final response, depending on the outcome of - * the DB-transaction */ - if (! awc.kyc.ok) - mhd_ret = TEH_RESPONSE_reply_kyc_required ( - rc->connection, - &awc.h_payto, - &awc.kyc); - else - mhd_ret = reply_age_withdraw_success ( - rc->connection, - &awc.commitment.h_commitment, - awc.commitment.noreveal_index); - - } while (0); - - GNUNET_JSON_parse_free (spec); - free_age_withdraw_context_resources (&awc); - return mhd_ret; + /* Parse blinded envelopes. */ + { + json_t *j_kappa_coin_evs; + size_t idx; + + json_array_foreach (j_blinded_coin_evs, idx, j_kappa_coin_evs) { + if (! json_is_array (j_kappa_coin_evs)) + { + char buf[256]; + + GNUNET_snprintf ( + buf, + sizeof(buf), + "entry %u in array blinded_coin_evs must be an array", + (unsigned int) (idx + 1)); + GNUNET_break_op (0); + return TALER_MHD_reply_with_ec ( + rc->connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + buf); + } + if (TALER_CNC_KAPPA != json_array_size (j_kappa_coin_evs)) + { + char buf[256]; + + GNUNET_snprintf (buf, + sizeof(buf), + "array no. %u in coin_evs must have length %u", + (unsigned int) (idx + 1), + (unsigned int) TALER_CNC_KAPPA); + GNUNET_break_op (0); + return TALER_MHD_reply_with_ec ( + rc->connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + buf); + } + + /* Now parse the individual kappa envelopes and calculate the hash of + * the commitment along the way. */ + { + size_t kappa; + json_t *kvalue; + + json_array_foreach (j_kappa_coin_evs, kappa, kvalue) { + struct GNUNET_JSON_Specification kspec[] = { + TALER_JSON_spec_blinded_planchet (NULL, + &awc->coin_evs[idx][kappa]), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (rc->connection, + kvalue, + kspec); + if (GNUNET_OK != res) + return (GNUNET_SYSERR == res) + ? MHD_NO + : MHD_YES; + /* Continue to hash of the coin candidates */ + { + struct TALER_BlindedCoinHashP bch; + + TALER_coin_ev_hash (&awc->coin_evs[idx][kappa], + &awc->denom_hs[idx], + &bch); + GNUNET_CRYPTO_hash_context_read (hash_context, + &bch, + sizeof(bch)); + } + + /* Check for duplicate planchets. Technically a bug on + * the client side that is harmless for us, but still + * not allowed per protocol */ + for (unsigned int i = 0; i < idx; i++) + { + if (0 == + TALER_blinded_planchet_cmp ( + &awc->coin_evs[idx][kappa], + &awc->coin_evs[i][kappa])) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_ec ( + rc->connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "duplicate planchet"); + } + } /* end duplicate check */ + } /* json_array_foreach over j_kappa_coin_evs */ + } /* scope of kappa/kvalue */ + } /* json_array_foreach over j_blinded_coin_evs */ + } /* scope of j_kappa_coin_evs, idx */ + + /* Finally, calculate the h_commitment from all blinded envelopes */ + GNUNET_CRYPTO_hash_context_finish (hash_context, + &awc->commitment.h_commitment.hash); + + } /* scope of hash_context */ + } /* scope of j_denom_hs, j_blinded_coin_evs */ + + awc->phase = AWC_PHASE_CHECK_KEYS; + } /* end of if NULL == awc */ + + while (true) + { + switch (awc->phase) + { + case AWC_PHASE_CHECK_KEYS: + check_keys (awc); + break; + case AWC_PHASE_CHECK_RESERVE_SIGNATURE: + check_reserve_signature (awc); + break; + case AWC_PHASE_RUN_LEGI_CHECK: + run_legi_check (awc); + break; + case AWC_PHASE_SUSPENDED: + return MHD_YES; + case AWC_PHASE_CHECK_KYC_RESULT: + check_kyc_result (awc); + break; + case AWC_PHASE_PREPARE_TRANSACTION: + prepare_transaction (awc); + break; + case AWC_PHASE_RUN_TRANSACTION: + run_transaction (awc); + break; + case AWC_PHASE_GENERATE_REPLY_SUCCESS: + reply_age_withdraw_success (awc); + break; + case AWC_PHASE_GENERATE_REPLY_FAILURE: + return MHD_queue_response (rc->connection, + awc->http_status, + awc->response); + case AWC_PHASE_RETURN_YES: + return MHD_YES; + case AWC_PHASE_RETURN_NO: + return MHD_NO; + } + } } diff --git a/src/exchange/taler-exchange-httpd_age-withdraw.h b/src/exchange/taler-exchange-httpd_age-withdraw.h index a76779190..23d09d298 100644 --- a/src/exchange/taler-exchange-httpd_age-withdraw.h +++ b/src/exchange/taler-exchange-httpd_age-withdraw.h @@ -26,6 +26,13 @@ /** + * Resume suspended connections, we are shutting down. + */ +void +TEH_age_withdraw_cleanup (void); + + +/** * Handle a "/reserves/$RESERVE_PUB/age-withdraw" request. * * Parses the batch of commitments to withdraw age restricted coins, and checks @@ -40,8 +47,9 @@ * @return MHD result code */ MHD_RESULT -TEH_handler_age_withdraw (struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root); +TEH_handler_age_withdraw ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root); #endif diff --git a/src/exchange/taler-exchange-httpd_batch-withdraw.c b/src/exchange/taler-exchange-httpd_batch-withdraw.c index 4cb847832..6fb3c5447 100644 --- a/src/exchange/taler-exchange-httpd_batch-withdraw.c +++ b/src/exchange/taler-exchange-httpd_batch-withdraw.c @@ -31,7 +31,7 @@ #include "taler_kyclogic_lib.h" #include "taler_mhd_lib.h" #include "taler-exchange-httpd_batch-withdraw.h" -#include "taler-exchange-httpd_withdraw.h" +#include "taler-exchange-httpd_common_kyc.h" #include "taler-exchange-httpd_responses.h" #include "taler-exchange-httpd_keys.h" #include "taler_util.h" @@ -71,9 +71,19 @@ struct BatchWithdrawContext { /** - * Public key of the reserve. + * Kept in a DLL. + */ + struct BatchWithdrawContext *prev; + + /** + * Kept in a DLL. + */ + struct BatchWithdrawContext *next; + + /** + * Handle for the legitimization check. */ - const struct TALER_ReservePublicKeyP *reserve_pub; + struct TEH_LegitimizationCheckHandle *lch; /** * request context @@ -81,6 +91,16 @@ struct BatchWithdrawContext const struct TEH_RequestContext *rc; /** + * Response to return, if set. + */ + struct MHD_Response *response; + + /** + * Public key of the reserve. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** * KYC status of the reserve used for the operation. */ struct TALER_EXCHANGEDB_KycStatus kyc; @@ -112,27 +132,90 @@ struct BatchWithdrawContext */ unsigned int planchets_length; + /** + * HTTP status to return with @e response, or 0. + */ + unsigned int http_status; + + /** + * Processing phase we are in. + */ + enum + { + BWC_PHASE_CHECK_KEYS, + BWC_PHASE_RUN_LEGI_CHECK, + BWC_PHASE_SUSPENDED, + BWC_PHASE_CHECK_KYC_RESULT, + BWC_PHASE_PREPARE_TRANSACTION, + BWC_PHASE_RUN_TRANSACTION, + BWC_PHASE_GENERATE_REPLY_SUCCESS, + BWC_PHASE_GENERATE_REPLY_FAILURE, + BWC_PHASE_RETURN_YES, + BWC_PHASE_RETURN_NO + } phase; + }; /** + * Kept in a DLL. + */ +static struct BatchWithdrawContext *bwc_head; + +/** + * Kept in a DLL. + */ +static struct BatchWithdrawContext *bwc_tail; + + +void +TEH_batch_withdraw_cleanup () +{ + struct BatchWithdrawContext *bwc; + + while (NULL != (bwc = bwc_head)) + { + GNUNET_CONTAINER_DLL_remove (bwc_head, + bwc_tail, + bwc); + MHD_resume_connection (bwc->rc->connection); + } +} + + +/** + * Terminate the main loop by returning the final + * result. + * + * @param[in,out] bwc context to update phase for + * @param mres MHD status to return + */ +static void +finish_loop (struct BatchWithdrawContext *bwc, + MHD_RESULT mres) +{ + bwc->phase = (MHD_YES == mres) + ? BWC_PHASE_RETURN_YES + : BWC_PHASE_RETURN_NO; +} + + +/** * Generates our final (successful) response. * - * @param rc request context - * @param wc operation context - * @return MHD queue status + * @param bwc operation context */ -static MHD_RESULT -generate_reply_success (const struct TEH_RequestContext *rc, - const struct BatchWithdrawContext *wc) +static void +generate_reply_success (struct BatchWithdrawContext *bwc) { + const struct TEH_RequestContext *rc = bwc->rc; json_t *sigs; sigs = json_array (); GNUNET_assert (NULL != sigs); - for (unsigned int i = 0; i<wc->planchets_length; i++) + for (unsigned int i = 0; i<bwc->planchets_length; i++) { - struct PlanchetContext *pc = &wc->planchets[i]; + struct PlanchetContext *pc = &bwc->planchets[i]; GNUNET_assert ( 0 == @@ -143,48 +226,52 @@ generate_reply_success (const struct TEH_RequestContext *rc, "ev_sig", &pc->collectable.sig)))); } - TEH_METRICS_batch_withdraw_num_coins += wc->planchets_length; - return TALER_MHD_REPLY_JSON_PACK ( - rc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("ev_sigs", - sigs)); + TEH_METRICS_batch_withdraw_num_coins += bwc->planchets_length; + finish_loop (bwc, + TALER_MHD_REPLY_JSON_PACK ( + rc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("ev_sigs", + sigs))); } /** - * Check if the @a wc is replayed and we already have an + * Check if the @a bwc is replayed and we already have an * answer. If so, replay the existing answer and return the * HTTP response. * - * @param wc parsed request data - * @param[out] mret HTTP status, set if we return true + * @param bwc parsed request data * @return true if the request is idempotent with an existing request * false if we did not find the request in the DB and did not set @a mret */ static bool -check_request_idempotent (const struct BatchWithdrawContext *wc, - MHD_RESULT *mret) +check_request_idempotent ( + struct BatchWithdrawContext *bwc) { - const struct TEH_RequestContext *rc = wc->rc; + const struct TEH_RequestContext *rc = bwc->rc; - for (unsigned int i = 0; i<wc->planchets_length; i++) + for (unsigned int i = 0; i<bwc->planchets_length; i++) { - struct PlanchetContext *pc = &wc->planchets[i]; + struct PlanchetContext *pc = &bwc->planchets[i]; enum GNUNET_DB_QueryStatus qs; struct TALER_EXCHANGEDB_CollectableBlindcoin collectable; - qs = TEH_plugin->get_withdraw_info (TEH_plugin->cls, - &pc->collectable.h_coin_envelope, - &collectable); + qs = TEH_plugin->get_withdraw_info ( + TEH_plugin->cls, + &pc->collectable.h_coin_envelope, + &collectable); if (0 > qs) { + /* FIXME: soft error not handled correctly! */ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - *mret = TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_withdraw_info"); - return true; /* well, kind-of */ + finish_loop (bwc, + TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_withdraw_info")); + return true; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) return false; @@ -192,8 +279,7 @@ check_request_idempotent (const struct BatchWithdrawContext *wc, } /* generate idempotent reply */ TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_BATCH_WITHDRAW]++; - *mret = generate_reply_success (rc, - wc); + bwc->phase = BWC_PHASE_GENERATE_REPLY_SUCCESS; return true; } @@ -206,7 +292,7 @@ check_request_idempotent (const struct BatchWithdrawContext *wc, * IF it returns the soft error code, the function MAY be called again * to retry and MUST not queue a MHD response. * - * Note that "wc->collectable.sig" is set before entering this function as we + * Note that "bwc->collectable.sig" is set before entering this function as we * signed before entering the transaction. * * @param cls a `struct BatchWithdrawContext *` @@ -216,11 +302,12 @@ check_request_idempotent (const struct BatchWithdrawContext *wc, * @return transaction status */ static enum GNUNET_DB_QueryStatus -batch_withdraw_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) +batch_withdraw_transaction ( + void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) { - struct BatchWithdrawContext *wc = cls; + struct BatchWithdrawContext *bwc = cls; uint64_t ruuid; enum GNUNET_DB_QueryStatus qs; bool found = false; @@ -229,45 +316,41 @@ batch_withdraw_transaction (void *cls, uint16_t allowed_maximum_age = 0; struct TALER_Amount reserve_balance; - qs = TEH_withdraw_kyc_check (&wc->kyc, - &wc->h_payto, - connection, - mhd_ret, - wc->reserve_pub, - &wc->batch_total, - wc->now); - if ( (qs < 0) || - (! wc->kyc.ok) ) - return qs; - qs = TEH_plugin->do_batch_withdraw (TEH_plugin->cls, - wc->now, - wc->reserve_pub, - &wc->batch_total, - TEH_age_restriction_enabled, - &found, - &balance_ok, - &reserve_balance, - &age_ok, - &allowed_maximum_age, - &ruuid); + qs = TEH_plugin->do_batch_withdraw ( + TEH_plugin->cls, + bwc->now, + &bwc->reserve_pub, + &bwc->batch_total, + TEH_age_restriction_enabled, + &found, + &balance_ok, + &reserve_balance, + &age_ok, + &allowed_maximum_age, + &ruuid); if (0 > qs) { if (GNUNET_DB_STATUS_HARD_ERROR == qs) { GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "update_reserve_batch_withdraw"); + finish_loop (bwc, + TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "update_reserve_batch_withdraw")); + return qs; } return qs; } if (! found) { - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, - NULL); + finish_loop (bwc, + TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL)); return GNUNET_DB_STATUS_HARD_ERROR; } @@ -279,33 +362,31 @@ batch_withdraw_transaction (void *cls, &TEH_age_restriction_config.mask, allowed_maximum_age); - *mhd_ret = TEH_RESPONSE_reply_reserve_age_restriction_required ( - connection, - lowest_age); + finish_loop (bwc, + TEH_RESPONSE_reply_reserve_age_restriction_required ( + connection, + lowest_age)); return GNUNET_DB_STATUS_HARD_ERROR; } if (! balance_ok) { - if (check_request_idempotent (wc, - mhd_ret)) - { + if (check_request_idempotent (bwc)) return GNUNET_DB_STATUS_HARD_ERROR; - } - - *mhd_ret = TEH_RESPONSE_reply_reserve_insufficient_balance ( - connection, - TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS, - &reserve_balance, - &wc->batch_total, - wc->reserve_pub); + finish_loop (bwc, + TEH_RESPONSE_reply_reserve_insufficient_balance ( + connection, + TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS, + &reserve_balance, + &bwc->batch_total, + &bwc->reserve_pub)); return GNUNET_DB_STATUS_HARD_ERROR; } /* Add information about each planchet in the batch */ - for (unsigned int i = 0; i<wc->planchets_length; i++) + for (unsigned int i = 0; i<bwc->planchets_length; i++) { - struct PlanchetContext *pc = &wc->planchets[i]; + struct PlanchetContext *pc = &bwc->planchets[i]; const struct TALER_BlindedPlanchet *bp = &pc->blinded_planchet; const union GNUNET_CRYPTO_BlindSessionNonce *nonce = NULL; bool denom_unknown = true; @@ -323,56 +404,63 @@ batch_withdraw_transaction (void *cls, &bp->blinded_message->details.cs_blinded_message.nonce; break; } - qs = TEH_plugin->do_batch_withdraw_insert (TEH_plugin->cls, - nonce, - &pc->collectable, - wc->now, - ruuid, - &denom_unknown, - &conflict, - &nonce_reuse); + qs = TEH_plugin->do_batch_withdraw_insert ( + TEH_plugin->cls, + nonce, + &pc->collectable, + bwc->now, + ruuid, + &denom_unknown, + &conflict, + &nonce_reuse); if (0 > qs) { if (GNUNET_DB_STATUS_HARD_ERROR == qs) - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "do_batch_withdraw_insert"); + finish_loop (bwc, + TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "do_batch_withdraw_insert")); return qs; } if (denom_unknown) { GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - NULL); + finish_loop (bwc, + TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + NULL)); return GNUNET_DB_STATUS_HARD_ERROR; } if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || (conflict) ) { - if (! check_request_idempotent (wc, - mhd_ret)) - { - /* We do not support *some* of the coins of the request being + if (check_request_idempotent (bwc)) + return GNUNET_DB_STATUS_HARD_ERROR; + /* We do not support *some* of the coins of the request being idempotent while others being fresh. */ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Idempotent coin in batch, not allowed. Aborting.\n"); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_WITHDRAW_BATCH_IDEMPOTENT_PLANCHET, - NULL); - } + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Idempotent coin in batch, not allowed. Aborting.\n"); + finish_loop (bwc, + TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_WITHDRAW_BATCH_IDEMPOTENT_PLANCHET, + NULL)); return GNUNET_DB_STATUS_HARD_ERROR; } if (nonce_reuse) { GNUNET_break_op (0); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_WITHDRAW_NONCE_REUSE, - NULL); + finish_loop (bwc, + TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_WITHDRAW_NONCE_REUSE, + NULL)); return GNUNET_DB_STATUS_HARD_ERROR; } } @@ -382,162 +470,285 @@ batch_withdraw_transaction (void *cls, /** + * The request was prepared successfully. Run + * the main DB transaction. + * + * @param bwc storage for request processing + */ +static void +run_transaction (struct BatchWithdrawContext *bwc) +{ + MHD_RESULT mhd_ret; + + GNUNET_assert (BWC_PHASE_RUN_TRANSACTION == + bwc->phase); + if (GNUNET_OK != + TEH_DB_run_transaction (bwc->rc->connection, + "run batch withdraw", + TEH_MT_REQUEST_WITHDRAW, + &mhd_ret, + &batch_withdraw_transaction, + bwc)) + { + if (BWC_PHASE_RUN_TRANSACTION == bwc->phase) + finish_loop (bwc, + mhd_ret); + return; + } + bwc->phase++; +} + + +/** * The request was parsed successfully. Prepare * our side for the main DB transaction. * - * @param rc request details - * @param wc storage for request processing - * @return MHD result for the @a rc + * @param bwc storage for request processing */ -static MHD_RESULT -prepare_transaction (const struct TEH_RequestContext *rc, - struct BatchWithdrawContext *wc) +static void +prepare_transaction (struct BatchWithdrawContext *bwc) { - struct TEH_CoinSignData csds[wc->planchets_length]; - struct TALER_BlindedDenominationSignature bss[wc->planchets_length]; + const struct TEH_RequestContext *rc = bwc->rc; + struct TALER_BlindedDenominationSignature bss[bwc->planchets_length]; + struct TEH_CoinSignData csds[bwc->planchets_length]; - for (unsigned int i = 0; i<wc->planchets_length; i++) + for (unsigned int i = 0; i<bwc->planchets_length; i++) { - struct PlanchetContext *pc = &wc->planchets[i]; + struct PlanchetContext *pc = &bwc->planchets[i]; + struct TEH_CoinSignData *csdsi = &csds[i]; - csds[i].h_denom_pub = &pc->collectable.denom_pub_hash; - csds[i].bp = &pc->blinded_planchet; + csdsi->h_denom_pub = &pc->collectable.denom_pub_hash; + csdsi->bp = &pc->blinded_planchet; } { enum TALER_ErrorCode ec; ec = TEH_keys_denomination_batch_sign ( - wc->planchets_length, + bwc->planchets_length, csds, false, bss); if (TALER_EC_NONE != ec) { GNUNET_break (0); - return TALER_MHD_reply_with_ec (rc->connection, - ec, - NULL); + finish_loop (bwc, + TALER_MHD_reply_with_ec ( + rc->connection, + ec, + NULL)); + return; } } - for (unsigned int i = 0; i<wc->planchets_length; i++) + for (unsigned int i = 0; i<bwc->planchets_length; i++) { - struct PlanchetContext *pc = &wc->planchets[i]; + struct PlanchetContext *pc = &bwc->planchets[i]; pc->collectable.sig = bss[i]; } + bwc->phase++; +} - /* run transaction */ - { - MHD_RESULT mhd_ret; - if (GNUNET_OK != - TEH_DB_run_transaction (rc->connection, - "run batch withdraw", - TEH_MT_REQUEST_WITHDRAW, - &mhd_ret, - &batch_withdraw_transaction, - wc)) +/** + * Check the KYC result. + * + * @param bwc storage for request processing + */ +static void +check_kyc_result (struct BatchWithdrawContext *bwc) +{ + /* return final positive response */ + if (! bwc->kyc.ok) + { + if (check_request_idempotent (bwc)) { - return mhd_ret; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Request is idempotent!\n"); + return; } + /* KYC required */ + finish_loop (bwc, + TEH_RESPONSE_reply_kyc_required ( + bwc->rc->connection, + &bwc->h_payto, + &bwc->kyc)); + return; } - /* return final positive response */ - if (! wc->kyc.ok) - { - MHD_RESULT mhd_ret; + bwc->phase++; +} - if (check_request_idempotent (wc, - &mhd_ret)) - return mhd_ret; - /* KYC required */ - return TEH_RESPONSE_reply_kyc_required ( - rc->connection, - &wc->h_payto, - &wc->kyc); + +/** + * Function called with the result of a legitimization + * check. + * + * @param cls closure + * @param lcr legitimization check result + */ +static void +withdraw_legi_cb ( + void *cls, + const struct TEH_LegitimizationCheckResult *lcr) +{ + struct BatchWithdrawContext *bwc = cls; + + bwc->lch = NULL; + GNUNET_assert (BWC_PHASE_SUSPENDED == + bwc->phase); + MHD_resume_connection (bwc->rc->connection); + GNUNET_CONTAINER_DLL_remove (bwc_head, + bwc_tail, + bwc); + TALER_MHD_daemon_trigger (); + if (NULL != lcr->response) + { + bwc->response = lcr->response; + bwc->http_status = lcr->http_status; + bwc->phase = BWC_PHASE_GENERATE_REPLY_FAILURE; + return; } - return generate_reply_success (rc, - wc); + bwc->kyc = lcr->kyc; + bwc->phase = BWC_PHASE_CHECK_KYC_RESULT; } /** - * Continue processing the request @a rc by parsing the - * @a planchets and then running the transaction. + * Function called to iterate over KYC-relevant transaction amounts for a + * particular time range. Called within a database transaction, so must + * not start a new one. * - * @param rc request details - * @param wc storage for request processing - * @param planchets array of planchets to parse - * @return MHD result for the @a rc + * @param cls closure, identifies the event type and account to iterate + * over events for + * @param limit maximum time-range for which events should be fetched + * (timestamp in the past) + * @param cb function to call on each event found, events must be returned + * in reverse chronological order + * @param cb_cls closure for @a cb, of type struct AgeWithdrawContext + * @return transaction status */ -static MHD_RESULT -parse_planchets (const struct TEH_RequestContext *rc, - struct BatchWithdrawContext *wc, - const json_t *planchets) +static enum GNUNET_DB_QueryStatus +withdraw_amount_cb ( + void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) { - struct TEH_KeyStateHandle *ksh; + struct BatchWithdrawContext *bwc = cls; + enum GNUNET_GenericReturnValue ret; + enum GNUNET_DB_QueryStatus qs; - for (unsigned int i = 0; i<wc->planchets_length; i++) - { - struct PlanchetContext *pc = &wc->planchets[i]; - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &pc->collectable.reserve_sig), - GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", - &pc->collectable.denom_pub_hash), - TALER_JSON_spec_blinded_planchet ("coin_ev", - &pc->blinded_planchet), - GNUNET_JSON_spec_end () - }; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Signaling amount %s for KYC check during age-withdrawal\n", + TALER_amount2s (&bwc->batch_total)); + ret = cb (cb_cls, + &bwc->batch_total, + bwc->now.abs_time); + GNUNET_break (GNUNET_SYSERR != ret); + if (GNUNET_OK != ret) + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + qs = TEH_plugin->select_withdraw_amounts_for_kyc_check ( + TEH_plugin->cls, + &bwc->h_payto, + limit, + cb, + cb_cls); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got %d additional transactions for this age-withdrawal and limit %llu\n", + qs, + (unsigned long long) limit.abs_value_us); + GNUNET_break (qs >= 0); + return qs; +} - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (rc->connection, - json_array_get (planchets, - i), - ispec); - if (GNUNET_OK != res) - return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; - } - pc->collectable.reserve_pub = *wc->reserve_pub; - for (unsigned int k = 0; k<i; k++) - { - const struct PlanchetContext *kpc = &wc->planchets[k]; - if (0 == - TALER_blinded_planchet_cmp (&kpc->blinded_planchet, - &pc->blinded_planchet)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "duplicate planchet"); - } - } +/** + * Do legitimization check. + * + * @param bwc operation context + */ +static void +run_legi_check (struct BatchWithdrawContext *bwc) +{ + enum GNUNET_DB_QueryStatus qs; + char *payto_uri; + + /* Check if the money came from a wire transfer */ + qs = TEH_plugin->reserves_get_origin ( + TEH_plugin->cls, + &bwc->reserve_pub, + &bwc->h_payto, + &payto_uri); + if (qs < 0) + { + finish_loop (bwc, + TALER_MHD_reply_with_error ( + bwc->rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "reserves_get_origin")); + return; } + /* If _no_ results, reserve was created by merge, + in which case no KYC check is required as the + merge already did that. */ + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + bwc->phase = BWC_PHASE_PREPARE_TRANSACTION; + return; + } + + bwc->lch = TEH_legitimization_check ( + &bwc->rc->async_scope_id, + TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW, + payto_uri, + &bwc->h_payto, + NULL, + &withdraw_amount_cb, + bwc, + &withdraw_legi_cb, + bwc); + GNUNET_assert (NULL != bwc->lch); + GNUNET_free (payto_uri); + GNUNET_CONTAINER_DLL_insert (bwc_head, + bwc_tail, + bwc); + MHD_suspend_connection (bwc->rc->connection); + bwc->phase = BWC_PHASE_SUSPENDED; +} + + +/** + * Check if the keys in the request are valid for + * withdrawing. + * + * @param[in,out] bwc storage for request processing + */ +static void +check_keys (struct BatchWithdrawContext *bwc) +{ + const struct TEH_RequestContext *rc = bwc->rc; + struct TEH_KeyStateHandle *ksh; ksh = TEH_keys_get_state (); if (NULL == ksh) { - MHD_RESULT mret; - - if (! check_request_idempotent (wc, - &mret)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL); - } - return mret; + if (check_request_idempotent (bwc)) + return; + GNUNET_break (0); + finish_loop (bwc, + TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL)); + return; } - for (unsigned int i = 0; i<wc->planchets_length; i++) + for (unsigned int i = 0; i<bwc->planchets_length; i++) { - struct PlanchetContext *pc = &wc->planchets[i]; + struct PlanchetContext *pc = &bwc->planchets[i]; struct TEH_DenominationKey *dk; + dk = TEH_keys_denomination_by_hash_from_state ( ksh, &pc->collectable.denom_pub_hash, @@ -545,72 +756,68 @@ parse_planchets (const struct TEH_RequestContext *rc, NULL); if (NULL == dk) { - MHD_RESULT mret; - - if (! check_request_idempotent (wc, - &mret)) - { - GNUNET_break_op (0); - return TEH_RESPONSE_reply_unknown_denom_pub_hash ( - rc->connection, - &pc->collectable.denom_pub_hash); - } - return mret; + if (check_request_idempotent (bwc)) + return; + GNUNET_break_op (0); + finish_loop (bwc, + TEH_RESPONSE_reply_unknown_denom_pub_hash ( + rc->connection, + &pc->collectable.denom_pub_hash)); + return; } - if (GNUNET_TIME_absolute_is_past (dk->meta.expire_withdraw.abs_time)) + if (GNUNET_TIME_absolute_is_past ( + dk->meta.expire_withdraw.abs_time)) { - MHD_RESULT mret; - /* This denomination is past the expiration time for withdraws */ - if (! check_request_idempotent (wc, - &mret)) - { - GNUNET_break_op (0); - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - rc->connection, - &pc->collectable.denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, - "WITHDRAW"); - } - return mret; + if (check_request_idempotent (bwc)) + return; + GNUNET_break_op (0); + finish_loop (bwc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + &pc->collectable.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "WITHDRAW")); + return; } - if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + if (GNUNET_TIME_absolute_is_future ( + dk->meta.start.abs_time)) { /* This denomination is not yet valid, no need to check for idempotency! */ GNUNET_break_op (0); - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - rc->connection, - &pc->collectable.denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, - "WITHDRAW"); + finish_loop (bwc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + &pc->collectable.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "WITHDRAW")); } if (dk->recoup_possible) { - MHD_RESULT mret; - /* This denomination has been revoked */ - if (! check_request_idempotent (wc, - &mret)) - { - GNUNET_break_op (0); - return TEH_RESPONSE_reply_expired_denom_pub_hash ( - rc->connection, - &pc->collectable.denom_pub_hash, - TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, - "WITHDRAW"); - } - return mret; + if (check_request_idempotent (bwc)) + return; + GNUNET_break_op (0); + finish_loop (bwc, + TEH_RESPONSE_reply_expired_denom_pub_hash ( + rc->connection, + &pc->collectable.denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, + "WITHDRAW")); + return; } if (dk->denom_pub.bsign_pub_key->cipher != pc->blinded_planchet.blinded_message->cipher) { /* denomination cipher and blinded planchet cipher not the same */ GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, - NULL); + finish_loop (bwc, + TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_CIPHER_MISMATCH, + NULL)); } if (0 > TALER_amount_add (&pc->collectable.amount_with_fee, @@ -618,21 +825,26 @@ parse_planchets (const struct TEH_RequestContext *rc, &dk->meta.fees.withdraw)) { GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW, - NULL); + finish_loop (bwc, + TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW, + NULL)); + return; } if (0 > - TALER_amount_add (&wc->batch_total, - &wc->batch_total, + TALER_amount_add (&bwc->batch_total, + &bwc->batch_total, &pc->collectable.amount_with_fee)) { GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW, - NULL); + finish_loop (bwc, + TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW, + NULL)); + return; } TALER_coin_ev_hash (&pc->blinded_planchet, @@ -641,91 +853,213 @@ parse_planchets (const struct TEH_RequestContext *rc, TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != - TALER_wallet_withdraw_verify (&pc->collectable.denom_pub_hash, - &pc->collectable.amount_with_fee, - &pc->collectable.h_coin_envelope, - &pc->collectable.reserve_pub, - &pc->collectable.reserve_sig)) + TALER_wallet_withdraw_verify ( + &pc->collectable.denom_pub_hash, + &pc->collectable.amount_with_fee, + &pc->collectable.h_coin_envelope, + &pc->collectable.reserve_pub, + &pc->collectable.reserve_sig)) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID, - NULL); + finish_loop (bwc, + TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID, + NULL)); + return; } } + bwc->phase++; /* everything parsed */ - return prepare_transaction (rc, - wc); } -MHD_RESULT -TEH_handler_batch_withdraw (struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root) +/** + * Batch-withdraw-specific cleanup routine. Function called + * upon completion of the request that should + * clean up @a rh_ctx. Can be NULL. + * + * @param rc request context to clean up + */ +static void +clean_batch_withdraw_rc (struct TEH_RequestContext *rc) { - struct BatchWithdrawContext wc = { - .reserve_pub = reserve_pub, - .rc = rc, - .now = GNUNET_TIME_timestamp_get () - }; - const json_t *planchets; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("planchets", - &planchets), - GNUNET_JSON_spec_end () - }; - - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &wc.batch_total)); - { - enum GNUNET_GenericReturnValue res; + struct BatchWithdrawContext *bwc = rc->rh_ctx; - res = TALER_MHD_parse_json_data (rc->connection, - root, - spec); - if (GNUNET_OK != res) - return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; + if (NULL != bwc->lch) + { + TEH_legitimization_check_cancel (bwc->lch); + bwc->lch = NULL; } - wc.planchets_length = json_array_size (planchets); - if (0 == wc.planchets_length) + for (unsigned int i = 0; i<bwc->planchets_length; i++) { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "planchets"); + struct PlanchetContext *pc = &bwc->planchets[i]; + + TALER_blinded_planchet_free (&pc->blinded_planchet); + TALER_blinded_denom_sig_free (&pc->collectable.sig); } - if (wc.planchets_length > TALER_MAX_FRESH_COINS) + GNUNET_free (bwc->planchets); + if (NULL != bwc->response) { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "too many planchets"); + MHD_destroy_response (bwc->response); + bwc->response = NULL; } + GNUNET_free (bwc); +} + + +MHD_RESULT +TEH_handler_batch_withdraw ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root) +{ + struct BatchWithdrawContext *bwc = rc->rh_ctx; + + if (NULL == bwc) { - struct PlanchetContext splanchets[wc.planchets_length]; - MHD_RESULT ret; - - memset (splanchets, - 0, - sizeof (splanchets)); - wc.planchets = splanchets; - ret = parse_planchets (rc, - &wc, - planchets); - /* Clean up */ - for (unsigned int i = 0; i<wc.planchets_length; i++) + const json_t *planchets; + + bwc = GNUNET_new (struct BatchWithdrawContext); + rc->rh_ctx = bwc; + rc->rh_cleaner = &clean_batch_withdraw_rc; + bwc->rc = rc; + bwc->reserve_pub = *reserve_pub; + bwc->now = GNUNET_TIME_timestamp_get (); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &bwc->batch_total)); + { - struct PlanchetContext *pc = &wc.planchets[i]; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("planchets", + &planchets), + GNUNET_JSON_spec_end () + }; - TALER_blinded_planchet_free (&pc->blinded_planchet); - TALER_blinded_denom_sig_free (&pc->collectable.sig); + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_OK != res) + return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES; + } + } + + bwc->planchets_length = json_array_size (planchets); + if (0 == bwc->planchets_length) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "planchets"); + } + if (bwc->planchets_length > TALER_MAX_FRESH_COINS) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "too many planchets"); + } + + bwc->planchets + = GNUNET_new_array (bwc->planchets_length, + struct PlanchetContext); + + for (unsigned int i = 0; i<bwc->planchets_length; i++) + { + struct PlanchetContext *pc = &bwc->planchets[i]; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto ( + "reserve_sig", + &pc->collectable.reserve_sig), + GNUNET_JSON_spec_fixed_auto ( + "denom_pub_hash", + &pc->collectable.denom_pub_hash), + TALER_JSON_spec_blinded_planchet ( + "coin_ev", + &pc->blinded_planchet), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data ( + rc->connection, + json_array_get (planchets, + i), + ispec); + if (GNUNET_OK != res) + return (GNUNET_SYSERR == res) + ? MHD_NO + : MHD_YES; + } + pc->collectable.reserve_pub = bwc->reserve_pub; + for (unsigned int k = 0; k<i; k++) + { + const struct PlanchetContext *kpc = &bwc->planchets[k]; + + if (0 == + TALER_blinded_planchet_cmp ( + &kpc->blinded_planchet, + &pc->blinded_planchet)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "duplicate planchet"); + } + } + } + bwc->phase = BWC_PHASE_CHECK_KEYS; + } + + while (true) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Batch withdraw processing in phase %d\n", + bwc->phase); + switch (bwc->phase) + { + case BWC_PHASE_CHECK_KEYS: + check_keys (bwc); + break; + case BWC_PHASE_RUN_LEGI_CHECK: + run_legi_check (bwc); + break; + case BWC_PHASE_SUSPENDED: + return MHD_YES; + case BWC_PHASE_CHECK_KYC_RESULT: + check_kyc_result (bwc); + break; + case BWC_PHASE_PREPARE_TRANSACTION: + prepare_transaction (bwc); + break; + case BWC_PHASE_RUN_TRANSACTION: + run_transaction (bwc); + break; + case BWC_PHASE_GENERATE_REPLY_SUCCESS: + generate_reply_success (bwc); + break; + case BWC_PHASE_GENERATE_REPLY_FAILURE: + return MHD_queue_response (rc->connection, + bwc->http_status, + bwc->response); + case BWC_PHASE_RETURN_YES: + return MHD_YES; + case BWC_PHASE_RETURN_NO: + return MHD_NO; } - return ret; } } diff --git a/src/exchange/taler-exchange-httpd_batch-withdraw.h b/src/exchange/taler-exchange-httpd_batch-withdraw.h index dfc6e5ad8..913f77f27 100644 --- a/src/exchange/taler-exchange-httpd_batch-withdraw.h +++ b/src/exchange/taler-exchange-httpd_batch-withdraw.h @@ -28,6 +28,12 @@ /** + * Resume suspended connections, we are shutting down. + */ +void +TEH_batch_withdraw_cleanup (void); + +/** * Handle a "/reserves/$RESERVE_PUB/batch-withdraw" request. Parses the batch of * requested "denom_pub" which specifies the key/value of the coin to be * withdrawn, and checks that the signature "reserve_sig" makes this a valid diff --git a/src/exchange/taler-exchange-httpd_common_kyc.c b/src/exchange/taler-exchange-httpd_common_kyc.c index aea27f261..a3239fdea 100644 --- a/src/exchange/taler-exchange-httpd_common_kyc.c +++ b/src/exchange/taler-exchange-httpd_common_kyc.c @@ -27,6 +27,14 @@ #include "taler_exchangedb_plugin.h" #include <gnunet/gnunet_common.h> +/** + * How often do we allow a legitimization rule to + * automatically trigger the next rule before bailing + * out? + */ +#define MAX_LEGI_LOOPS 5 + + struct TEH_KycAmlTrigger { @@ -320,9 +328,12 @@ kyc_aml_finished ( GNUNET_break (0); if (NULL != kat->response) MHD_destroy_response (kat->response); - kat->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - kat->response = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, - "do_insert_kyc_attributes"); + kat->http_status + = MHD_HTTP_INTERNAL_SERVER_ERROR; + kat->response + = TALER_MHD_make_error ( + TALER_EC_GENERIC_DB_STORE_FAILED, + "do_insert_kyc_attributes"); /* Continued below to return the response */ } RETURN_RESULT: @@ -467,10 +478,37 @@ add_kyc_history_entry ( } +/** + * We have finished a KYC process and obtained new + * @a attributes for a given @a account_id. + * Check with the KYC-AML trigger to see if we need + * to initiate an AML process, and store the attributes + * in the database. Then call @a cb. + * + * @param scope the HTTP request logging scope + * @param process_row legitimization process the data provided is about, + * or must be 0 if instant_ms is given + * @param instant_ms instant measure to run, used if @a process_row is 0, + * otherwise must be NULL + * @param account_id account the webhook was about + * @param provider_name name of the provider with the logic that was run + * @param provider_user_id set to user ID at the provider, or + * NULL if not supported or unknown + * @param provider_legitimization_id set to legitimization process ID at the provider, + * or NULL if not supported or unknown + * @param expiration until when is the KYC check valid + * @param attributes user attributes returned by the provider + * @param http_status HTTP status code of @a response + * @param[in] response to return to the HTTP client, can be NULL + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle to cancel the operation + */ struct TEH_KycAmlTrigger * -TEH_kyc_finished ( +TEH_kyc_finished2 ( const struct GNUNET_AsyncScopeId *scope, uint64_t process_row, + const struct TALER_KYCLOGIC_Measure *instant_ms, const struct TALER_PaytoHashP *account_id, const char *provider_name, const char *provider_user_id, @@ -503,24 +541,27 @@ TEH_kyc_finished ( kat->response = response; kat->cb = cb; kat->cb_cls = cb_cls; - qs = TEH_plugin->lookup_active_legitimization ( - TEH_plugin->cls, - process_row, - &kat->measure_index, - &kat->jmeasures); - switch (qs) + if (NULL == instant_ms) { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - TEH_kyc_finished_cancel (kat); - return NULL; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - TEH_kyc_finished_cancel (kat); - return NULL; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; + qs = TEH_plugin->lookup_active_legitimization ( + TEH_plugin->cls, + process_row, + &kat->measure_index, + &kat->jmeasures); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + TEH_kyc_finished_cancel (kat); + return NULL; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + TEH_kyc_finished_cancel (kat); + return NULL; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } } kat->aml_history = json_array (); kat->kyc_history = json_array (); @@ -560,14 +601,29 @@ TEH_kyc_finished ( case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } - kat->kyc_aml - = TALER_KYCLOGIC_run_aml_program (kat->attributes, - kat->aml_history, - kat->kyc_history, - kat->jmeasures, - kat->measure_index, - &kyc_aml_finished, - kat); + if (NULL == instant_ms) + { + kat->kyc_aml + = TALER_KYCLOGIC_run_aml_program ( + kat->attributes, + kat->aml_history, + kat->kyc_history, + kat->jmeasures, + kat->measure_index, + &kyc_aml_finished, + kat); + } + else + { + kat->kyc_aml + = TALER_KYCLOGIC_run_aml_program3 ( + instant_ms, + kat->attributes, + kat->aml_history, + kat->kyc_history, + &kyc_aml_finished, + kat); + } if (NULL == kat->kyc_aml) { GNUNET_break (0); @@ -578,6 +634,37 @@ TEH_kyc_finished ( } +struct TEH_KycAmlTrigger * +TEH_kyc_finished ( + const struct GNUNET_AsyncScopeId *scope, + uint64_t process_row, + const struct TALER_PaytoHashP *account_id, + const char *provider_name, + const char *provider_user_id, + const char *provider_legitimization_id, + struct GNUNET_TIME_Absolute expiration, + const json_t *attributes, + unsigned int http_status, + struct MHD_Response *response, + TEH_KycAmlTriggerCallback cb, + void *cb_cls) +{ + return TEH_kyc_finished2 (scope, + process_row, + NULL, + account_id, + provider_name, + provider_user_id, + provider_legitimization_id, + expiration, + attributes, + http_status, + response, + cb, + cb_cls); +} + + void TEH_kyc_finished_cancel (struct TEH_KycAmlTrigger *kat) { @@ -903,38 +990,236 @@ TEH_kyc_failed ( } -enum GNUNET_DB_QueryStatus +struct TEH_LegitimizationCheckHandle +{ + /** + * Function to call with the result. + */ + TEH_LegitimizationCheckCallback result_cb; + + /** + * Closure for @e result_cb. + */ + void *result_cb_cls; + + /** + * Task scheduled to return a result asynchronously. + */ + struct GNUNET_SCHEDULER_Task *async_task; + + /** + * Handle to asynchronously running instant measure. + */ + struct TEH_KycAmlTrigger *kat; + + /** + * Our request scope for logging. + */ + struct GNUNET_AsyncScopeId scope; + + /** + * Legitimization result we have been building and + * should return. + */ + struct TEH_LegitimizationCheckResult lcr; + + /** + * Event we were triggered for. + */ + enum TALER_KYCLOGIC_KycTriggerEvent et; + + /** + * Payto-URI of the account. + */ + char *payto_uri; + + /** + * Hash of @e payto_uri. + */ + struct TALER_PaytoHashP h_payto; + + /** + * Public key of the account. + */ + union TALER_AccountPublicKeyP account_pub; + + /** + * Amount iterator to call to check for amounts. + */ + TALER_KYCLOGIC_KycAmountIterator ai; + + /** + * Closure for @e ai. + */ + void *ai_cls; + + /** + * Number of instant rule triggers we have experienced + * in this check already. + */ + unsigned int rerun; + + /** + * Do we have @e account_pub? + */ + bool have_account_pub; +}; + + +/** + * Helper task that asynchronously calls the result + * callback and then cleans up. + * + * @param[in] cls a `struct TEH_LegitimizationCheckHandle *` + */ +static void +async_return_legi_result (void *cls) +{ + struct TEH_LegitimizationCheckHandle *lch = cls; + + lch->async_task = NULL; + // FIXME: enter (+exit) lch->scope... + lch->result_cb (lch->result_cb_cls, + &lch->lcr); + lch->lcr.response = NULL; + TEH_legitimization_check_cancel (lch); +} + + +/** + * The legitimization process failed, return an error + * response. + * + * @param[in,out] lch legitimization check that failed + * @param ec error code to return + * @param details error details to return (can be NULL) + */ +static void +legi_fail (struct TEH_LegitimizationCheckHandle *lch, + enum TALER_ErrorCode ec, + const char *details) +{ + lch->lcr.http_status + = TALER_ErrorCode_get_http_status (ec); + lch->lcr.response + = TALER_MHD_make_error ( + ec, + details); + lch->async_task + = GNUNET_SCHEDULER_add_now ( + &async_return_legi_result, + lch); +} + + +/** + * Actually (re)-run the legitimization check @a lch. + * + * @param[in,out] lch legitimization check to run + */ +static void +legitimization_check_run ( + struct TEH_LegitimizationCheckHandle *lch); + + +/** + * Function called after the KYC-AML trigger is done. + * + * @param cls must be a `struct TEH_LegitimizationCheckHandle *` + * @param http_status final HTTP status to return + * @param[in] response final HTTP ro return + */ +static void +legi_check_aml_trigger_cb ( + void *cls, + unsigned int http_status, + struct MHD_Response *response) +{ + struct TEH_LegitimizationCheckHandle *lch = cls; + + lch->kat = NULL; + if (NULL != response) + { + lch->lcr.http_status = http_status; + lch->lcr.response = response; + lch->async_task + = GNUNET_SCHEDULER_add_now ( + &async_return_legi_result, + lch); + return; + } + /* re-run the check, we got new rules! */ + if (lch->rerun > MAX_LEGI_LOOPS) + { + /* deep recursion not allowed, abort! */ + GNUNET_break (0); + legi_fail (lch, + TALER_EC_EXCHANGE_KYC_RECURSIVE_RULE_DETECTED, + NULL); + return; + } + lch->rerun++; + legitimization_check_run (lch); +} + + +struct TEH_LegitimizationCheckHandle * TEH_legitimization_check ( - struct TALER_EXCHANGEDB_KycStatus *kyc, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret, + const struct GNUNET_AsyncScopeId *scope, enum TALER_KYCLOGIC_KycTriggerEvent et, const char *payto_uri, const struct TALER_PaytoHashP *h_payto, const union TALER_AccountPublicKeyP *account_pub, TALER_KYCLOGIC_KycAmountIterator ai, - void *ai_cls) + void *ai_cls, + TEH_LegitimizationCheckCallback result_cb, + void *result_cb_cls) +{ + struct TEH_LegitimizationCheckHandle *lch; + + lch = GNUNET_new (struct TEH_LegitimizationCheckHandle); + lch->scope = *scope; + lch->et = et; + lch->payto_uri = GNUNET_strdup (payto_uri); + lch->h_payto = *h_payto; + if (NULL != account_pub) + { + lch->account_pub = *account_pub; + lch->have_account_pub = true; + } + lch->ai = ai; + lch->ai_cls = ai_cls; + lch->result_cb = result_cb; + lch->result_cb_cls = result_cb_cls; + legitimization_check_run (lch); + return lch; +} + + +static void +legitimization_check_run ( + struct TEH_LegitimizationCheckHandle *lch) { struct TALER_KYCLOGIC_LegitimizationRuleSet *lrs = NULL; const struct TALER_KYCLOGIC_KycRule *requirement; enum GNUNET_DB_QueryStatus qs; + const struct TALER_KYCLOGIC_Measure *instant_ms; + // FIXME: add global flag to disable legitimizations! + // FIXME: enter (+exit) lch->scope! { json_t *jrules; qs = TEH_plugin->get_kyc_rules (TEH_plugin->cls, - h_payto, + &lch->h_payto, &jrules); if (qs < 0) { - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_kyc_rules"); - } - return qs; + GNUNET_break (0); + legi_fail (lch, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_kyc_rules"); + return; } if (qs > 0) { @@ -946,66 +1231,137 @@ TEH_legitimization_check ( } qs = TALER_KYCLOGIC_kyc_test_required ( - et, + lch->et, lrs, - ai, - ai_cls, + lch->ai, + lch->ai_cls, &requirement); if (qs < 0) { TALER_KYCLOGIC_rules_free (lrs); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_ec ( - connection, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "kyc_test_required"); - } - return qs; + legi_fail (lch, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "kyc_test_required"); + return; } if (NULL == requirement) { TALER_KYCLOGIC_rules_free (lrs); - kyc->ok = true; - return qs; + lch->lcr.kyc.ok = true; + /* return success! */ + lch->async_task + = GNUNET_SCHEDULER_add_now ( + &async_return_legi_result, + lch); + return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "KYC requirement is %s\n", TALER_KYCLOGIC_rule2s (requirement)); - kyc->ok = false; + + instant_ms + = TALER_KYCLOGIC_rule_get_instant_measure ( + requirement); + if (NULL != instant_ms) + { + /* We have an 'instant' measure which means we must run the + AML program immediately instead of waiting for the account owner + to select some measure and contribute their KYC data. */ + json_t *attributes + = json_object (); /* instant: empty attributes */ + + GNUNET_assert (NULL != attributes); + lch->kat + = TEH_kyc_finished2 ( + &lch->scope, + 0LL, + instant_ms, + &lch->h_payto, + "SKIP", /* provider */ + NULL, + NULL, + GNUNET_TIME_UNIT_FOREVER_ABS, + attributes, + 0, /* http status */ + NULL, /* MHD_Response */ + &legi_check_aml_trigger_cb, + lch); + json_decref (attributes); + if (NULL == lch->kat) + { + GNUNET_break (0); + legi_fail (lch, + TALER_EC_EXCHANGE_KYC_AML_PROGRAM_FAILURE, + NULL); + return; + } + return; + } + + /* No instant measure, store all measures in the database and + wait for the user to select one (via /kyc-info) and to then + provide the data. */ + lch->lcr.kyc.ok = false; { json_t *jmeasures; jmeasures = TALER_KYCLOGIC_rule_to_measures (requirement); qs = TEH_plugin->trigger_kyc_rule_for_account ( TEH_plugin->cls, - payto_uri, - h_payto, - account_pub, + lch->payto_uri, + &lch->h_payto, + lch->have_account_pub ? &lch->account_pub : NULL, jmeasures, TALER_KYCLOGIC_rule2priority (requirement), - &kyc->requirement_row); + &lch->lcr.kyc.requirement_row); json_decref (jmeasures); } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_ec ( - connection, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "trigger_kyc_rule_for_account"); - return GNUNET_DB_STATUS_HARD_ERROR; + legi_fail (lch, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "trigger_kyc_rule_for_account"); + return; } + TALER_KYCLOGIC_rules_free (lrs); if (GNUNET_DB_STATUS_HARD_ERROR == qs) { GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_STORE_FAILED, - "trigger_kyc_rule_for_account"); + legi_fail (lch, + TALER_EC_GENERIC_DB_STORE_FAILED, + "trigger_kyc_rule_for_account"); + return; } - TALER_KYCLOGIC_rules_free (lrs); - return qs; + /* return success! */ + lch->async_task + = GNUNET_SCHEDULER_add_now ( + &async_return_legi_result, + lch); +} + + +void +TEH_legitimization_check_cancel ( + struct TEH_LegitimizationCheckHandle *lch) +{ + if (NULL != lch->async_task) + { + GNUNET_SCHEDULER_cancel (lch->async_task); + lch->async_task = NULL; + } + if (NULL != lch->kat) + { + TEH_kyc_finished_cancel (lch->kat); + lch->kat = NULL; + } + if (NULL != lch->lcr.response) + { + MHD_destroy_response (lch->lcr.response); + lch->lcr.response = NULL; + } + GNUNET_free (lch->payto_uri); + GNUNET_free (lch); } diff --git a/src/exchange/taler-exchange-httpd_common_kyc.h b/src/exchange/taler-exchange-httpd_common_kyc.h index b866e867e..68fc31768 100644 --- a/src/exchange/taler-exchange-httpd_common_kyc.h +++ b/src/exchange/taler-exchange-httpd_common_kyc.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2023 Taler Systems SA + 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 @@ -185,12 +185,52 @@ TEH_kyc_failed ( /** + * Result from a legitimization check. + */ +struct TEH_LegitimizationCheckResult +{ + /** + * KYC status for the account + */ + struct TALER_EXCHANGEDB_KycStatus kyc; + + /** + * HTTP status code for @a response, or 0 + */ + unsigned int http_status; + + /** + * Response to return. Note that the response must + * be queued or destroyed by the callee. NULL + * if the legitimization check was successful and the handler should return + * a handler-specific result. + */ + struct MHD_Response *response; +}; + + +/** + * Function called with the result of a legitimization + * check. + * + * @param cls closure + * @param lcr legitimization check result + */ +typedef void +(*TEH_LegitimizationCheckCallback)( + void *cls, + const struct TEH_LegitimizationCheckResult *lcr); + +/** + * Handle for a legitimization check. + */ +struct TEH_LegitimizationCheckHandle; + + +/** * Do legitimization check. * - * @param[out] kyc set to kyc status - * @param[in,out] connection used to return hard errors - * @param[out] mhd_ret set if errors were returned - * (only on hard error) + * @param scope scope for logging * @param et type of event we are checking * @param payto_uri account we are checking for * @param h_payto hash of @a payto_uri @@ -198,20 +238,30 @@ TEH_kyc_failed ( * KYC authorization, NULL if not known * @param ai callback to get amounts involved historically * @param ai_cls closure for @a ai - * @return transaction status, error will have been - * queued if transaction status is set to hard error + * @param result_cb function to call with the result + * @param result_cb_cls closure for @a result_cb + * @return handle for the operation */ -enum GNUNET_DB_QueryStatus +struct TEH_LegitimizationCheckHandle * TEH_legitimization_check ( - struct TALER_EXCHANGEDB_KycStatus *kyc, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret, + const struct GNUNET_AsyncScopeId *scope, enum TALER_KYCLOGIC_KycTriggerEvent et, const char *payto_uri, const struct TALER_PaytoHashP *h_payto, const union TALER_AccountPublicKeyP *account_pub, TALER_KYCLOGIC_KycAmountIterator ai, - void *ai_cls); + void *ai_cls, + TEH_LegitimizationCheckCallback result_cb, + void *result_cb_cls); +/** + * Cancel legitimization check. + * + * @param[in] lch handle of the check to cancel + */ +void +TEH_legitimization_check_cancel ( + struct TEH_LegitimizationCheckHandle *lch); + #endif diff --git a/src/exchange/taler-exchange-httpd_kyc-wallet.c b/src/exchange/taler-exchange-httpd_kyc-wallet.c index 21730b5ed..a74f6eca6 100644 --- a/src/exchange/taler-exchange-httpd_kyc-wallet.c +++ b/src/exchange/taler-exchange-httpd_kyc-wallet.c @@ -27,6 +27,7 @@ #include "taler_json_lib.h" #include "taler_mhd_lib.h" #include "taler_kyclogic_lib.h" +#include "taler-exchange-httpd_common_kyc.h" #include "taler-exchange-httpd_kyc-wallet.h" #include "taler-exchange-httpd_responses.h" #include "taler-exchange-httpd_withdraw.h" @@ -37,6 +38,40 @@ */ struct KycRequestContext { + + /** + * Kept in a DLL. + */ + struct KycRequestContext *next; + + /** + * Kept in a DLL. + */ + struct KycRequestContext *prev; + + /** + * Handle for legitimization check. + */ + struct TEH_LegitimizationCheckHandle *lch; + + /** + * Payto URI of the reserve. + */ + char *payto_uri; + + /** + * Request context. + */ + struct TEH_RequestContext *rc; + + /** + * Response to return. Note that the response must + * be queued or destroyed by the callee. NULL + * if the legitimization check was successful and the handler should return + * a handler-specific result. + */ + struct MHD_Response *response; + /** * Public key of the reserve/wallet this is about. */ @@ -48,24 +83,50 @@ struct KycRequestContext union TALER_AccountPublicKeyP wallet_pub; /** - * KYC status, with row with the legitimization requirement. + * Balance threshold crossed by the wallet. */ - struct TALER_EXCHANGEDB_KycStatus kyc; + struct TALER_Amount balance; /** - * Balance threshold crossed by the wallet. + * KYC status, with row with the legitimization requirement. */ - struct TALER_Amount balance; + struct TALER_EXCHANGEDB_KycStatus kyc; /** - * Payto URI of the reserve. + * HTTP status code for @a response, or 0 */ - char *payto_uri; + unsigned int http_status; }; /** + * Kept in a DLL. + */ +static struct KycRequestContext *krc_head; + +/** + * Kept in a DLL. + */ +static struct KycRequestContext *krc_tail; + + +void +TEH_kyc_wallet_cleanup () +{ + struct KycRequestContext *krc; + + while (NULL != (krc = krc_head)) + { + GNUNET_CONTAINER_DLL_remove (krc_head, + krc_tail, + krc); + MHD_resume_connection (krc->rc->connection); + } +} + + +/** * Function called to iterate over KYC-relevant * transaction amounts for a particular time range. * Returns the wallet balance. @@ -99,36 +160,48 @@ balance_iterator (void *cls, /** - * Function implementing database transaction to check wallet's KYC status. - * Runs the transaction logic; IF it returns a non-error code, the transaction - * logic MUST NOT queue a MHD response. IF it returns an hard error, the - * transaction logic MUST queue a MHD response and set @a mhd_ret. IF it - * returns the soft error code, the function MAY be called again to retry and - * MUST not queue a MHD response. + * Function called with the result of a legitimization + * check. * - * @param cls closure with a `struct KycRequestContext *` - * @param connection MHD request which triggered the transaction - * @param[out] mhd_ret set to MHD response status for @a connection, - * if transaction failed (!) - * @return transaction status + * @param cls must be a `struct KycRequestContext *` + * @param lcr legitimization check result */ -static enum GNUNET_DB_QueryStatus -wallet_kyc_check (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) +static void +legi_result_cb ( + void *cls, + const struct TEH_LegitimizationCheckResult *lcr) { struct KycRequestContext *krc = cls; - return TEH_legitimization_check ( - &krc->kyc, - connection, - mhd_ret, - TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE, - krc->payto_uri, - &krc->h_payto, - &krc->wallet_pub, - &balance_iterator, - krc); + krc->lch = NULL; + krc->http_status = lcr->http_status; + krc->response = lcr->response; + krc->kyc = lcr->kyc; + GNUNET_CONTAINER_DLL_remove (krc_head, + krc_tail, + krc); + MHD_resume_connection (krc->rc->connection); + TALER_MHD_daemon_trigger (); +} + + +/** + * Function to clean up our rh_ctx in @a rc + * + * @param[in,out] rc context to clean up + */ +static void +krc_cleaner (struct TEH_RequestContext *rc) +{ + struct KycRequestContext *krc = rc->rh_ctx; + + if (NULL != krc->lch) + { + TEH_legitimization_check_cancel (krc->lch); + krc->lch = NULL; + } + GNUNET_free (krc->payto_uri); + GNUNET_free (krc); } @@ -138,63 +211,83 @@ TEH_handler_kyc_wallet ( const json_t *root, const char *const args[]) { - struct TALER_ReserveSignatureP reserve_sig; - struct KycRequestContext krc; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &reserve_sig), - GNUNET_JSON_spec_fixed_auto ("reserve_pub", - &krc.wallet_pub.reserve_pub), - TALER_JSON_spec_amount ("balance", - TEH_currency, - &krc.balance), - GNUNET_JSON_spec_end () - }; - MHD_RESULT res; - enum GNUNET_GenericReturnValue ret; + struct KycRequestContext *krc = rc->rh_ctx; - (void) args; - ret = TALER_MHD_parse_json_data (rc->connection, - root, - spec); - if (GNUNET_SYSERR == ret) - return MHD_NO; /* hard failure */ - if (GNUNET_NO == ret) - return MHD_YES; /* failure */ - - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_account_setup_verify ( - &krc.wallet_pub.reserve_pub, - &krc.balance, - &reserve_sig)) + if (NULL == krc) { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_KYC_WALLET_SIGNATURE_INVALID, - NULL); + krc = GNUNET_new (struct KycRequestContext); + krc->rc = rc; + rc->rh_ctx = krc; + rc->rh_cleaner = &krc_cleaner; + { + struct TALER_ReserveSignatureP reserve_sig; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &reserve_sig), + GNUNET_JSON_spec_fixed_auto ("reserve_pub", + &krc->wallet_pub.reserve_pub), + TALER_JSON_spec_amount ("balance", + TEH_currency, + &krc->balance), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue ret; + + (void) args; + ret = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_SYSERR == ret) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == ret) + return MHD_YES; /* failure */ + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_account_setup_verify ( + &krc->wallet_pub.reserve_pub, + &krc->balance, + &reserve_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_KYC_WALLET_SIGNATURE_INVALID, + NULL); + } + } + krc->payto_uri + = TALER_reserve_make_payto (TEH_base_url, + &krc->wallet_pub.reserve_pub); + TALER_payto_hash (krc->payto_uri, + &krc->h_payto); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "h_payto of wallet %s is %s\n", + krc->payto_uri, + TALER_B2S (&krc->h_payto)); + krc->lch = TEH_legitimization_check ( + &rc->async_scope_id, + TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE, + krc->payto_uri, + &krc->h_payto, + &krc->wallet_pub, + &balance_iterator, + krc, + &legi_result_cb, + krc); + GNUNET_assert (NULL != krc->lch); + MHD_suspend_connection (rc->connection); + GNUNET_CONTAINER_DLL_insert (krc_head, + krc_tail, + krc); + return MHD_YES; } - krc.payto_uri - = TALER_reserve_make_payto (TEH_base_url, - &krc.wallet_pub.reserve_pub); - TALER_payto_hash (krc.payto_uri, - &krc.h_payto); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "h_payto of wallet %s is %s\n", - krc.payto_uri, - TALER_B2S (&krc.h_payto)); - ret = TEH_DB_run_transaction (rc->connection, - "check wallet kyc", - TEH_MT_REQUEST_OTHER, - &res, - &wallet_kyc_check, - &krc); - GNUNET_free (krc.payto_uri); - if (GNUNET_SYSERR == ret) - return res; - if (krc.kyc.ok) + if (NULL != krc->response) + return MHD_queue_response (rc->connection, + krc->http_status, + krc->response); + if (krc->kyc.ok) { /* KYC not required or already satisfied */ return TALER_MHD_reply_static ( @@ -205,8 +298,8 @@ TEH_handler_kyc_wallet ( 0); } return TEH_RESPONSE_reply_kyc_required (rc->connection, - &krc.h_payto, - &krc.kyc); + &krc->h_payto, + &krc->kyc); } diff --git a/src/exchange/taler-exchange-httpd_kyc-wallet.h b/src/exchange/taler-exchange-httpd_kyc-wallet.h index bd8ae1b08..6539358e8 100644 --- a/src/exchange/taler-exchange-httpd_kyc-wallet.h +++ b/src/exchange/taler-exchange-httpd_kyc-wallet.h @@ -26,6 +26,13 @@ /** + * Resume suspended connections, we are shutting down. + */ +void +TEH_kyc_wallet_cleanup (void); + + +/** * Handle a "/kyc-wallet" request. Parses the "reserve_pub" EdDSA key of the * reserve and the signature "reserve_sig" which affirms the operation. If OK, * a KYC record is created (if missing) and the KYC status returned. diff --git a/src/exchange/taler-exchange-httpd_purses_create.c b/src/exchange/taler-exchange-httpd_purses_create.c index f6db221ec..dae5ea600 100644 --- a/src/exchange/taler-exchange-httpd_purses_create.c +++ b/src/exchange/taler-exchange-httpd_purses_create.c @@ -422,10 +422,11 @@ parse_coin (struct MHD_Connection *connection, MHD_RESULT TEH_handler_purses_create ( - struct MHD_Connection *connection, + struct TEH_RequestContext *rc, const struct TALER_PurseContractPublicKeyP *purse_pub, const json_t *root) { + struct MHD_Connection *connection = rc->connection; struct PurseCreateContext pcc = { .pd.purse_pub = *purse_pub, .exchange_timestamp = GNUNET_TIME_timestamp_get () diff --git a/src/exchange/taler-exchange-httpd_purses_create.h b/src/exchange/taler-exchange-httpd_purses_create.h index 7ccba1446..bec4c5c64 100644 --- a/src/exchange/taler-exchange-httpd_purses_create.h +++ b/src/exchange/taler-exchange-httpd_purses_create.h @@ -32,14 +32,14 @@ * the details of the operation specified. If everything checks out, this * will ultimately lead to the "purses create" being executed, or rejected. * - * @param connection the MHD connection to handle + * @param rc connection to handle * @param purse_pub public key of the purse * @param root uploaded JSON data * @return MHD result code */ MHD_RESULT TEH_handler_purses_create ( - struct MHD_Connection *connection, + struct TEH_RequestContext *rc, const struct TALER_PurseContractPublicKeyP *purse_pub, const json_t *root); diff --git a/src/exchange/taler-exchange-httpd_purses_deposit.c b/src/exchange/taler-exchange-httpd_purses_deposit.c index 8e4d5e41a..d518e6250 100644 --- a/src/exchange/taler-exchange-httpd_purses_deposit.c +++ b/src/exchange/taler-exchange-httpd_purses_deposit.c @@ -321,10 +321,11 @@ parse_coin (struct MHD_Connection *connection, MHD_RESULT TEH_handler_purses_deposit ( - struct MHD_Connection *connection, + struct TEH_RequestContext *rc, const struct TALER_PurseContractPublicKeyP *purse_pub, const json_t *root) { + struct MHD_Connection *connection = rc->connection; struct PurseDepositContext pcc = { .purse_pub = purse_pub, .exchange_timestamp = GNUNET_TIME_timestamp_get () diff --git a/src/exchange/taler-exchange-httpd_purses_deposit.h b/src/exchange/taler-exchange-httpd_purses_deposit.h index fa587e977..9e300c3c0 100644 --- a/src/exchange/taler-exchange-httpd_purses_deposit.h +++ b/src/exchange/taler-exchange-httpd_purses_deposit.h @@ -32,14 +32,14 @@ * the details of the operation specified. If everything checks out, this * will ultimately lead to the "purses deposit" being executed, or rejected. * - * @param connection the MHD connection to handle + * @param rc request to handle * @param purse_pub public key of the purse * @param root uploaded JSON data * @return MHD result code */ MHD_RESULT TEH_handler_purses_deposit ( - struct MHD_Connection *connection, + struct TEH_RequestContext *rc, const struct TALER_PurseContractPublicKeyP *purse_pub, const json_t *root); diff --git a/src/exchange/taler-exchange-httpd_purses_merge.c b/src/exchange/taler-exchange-httpd_purses_merge.c index d6b0348ba..5a62b7a7d 100644 --- a/src/exchange/taler-exchange-httpd_purses_merge.c +++ b/src/exchange/taler-exchange-httpd_purses_merge.c @@ -30,6 +30,7 @@ #include "taler_json_lib.h" #include "taler_kyclogic_lib.h" #include "taler_mhd_lib.h" +#include "taler-exchange-httpd_common_kyc.h" #include "taler-exchange-httpd_purses_merge.h" #include "taler-exchange-httpd_responses.h" #include "taler-exchange-httpd_withdraw.h" @@ -42,10 +43,52 @@ */ struct PurseMergeContext { + + /** + * Kept in a DLL. + */ + struct PurseMergeContext *next; + + /** + * Kept in a DLL. + */ + struct PurseMergeContext *prev; + + /** + * Our request. + */ + struct TEH_RequestContext *rc; + + /** + * Handle for the legitimization check. + */ + struct TEH_LegitimizationCheckHandle *lch; + + /** + * Fees that apply to this operation. + */ + const struct TALER_WireFeeSet *wf; + + /** + * Base URL of the exchange provider hosting the reserve. + */ + char *provider_url; + + /** + * URI of the account the purse is to be merged into. + * Must be of the form 'payto://taler-reserve/$EXCHANGE_URL/RESERVE_PUB'. + */ + const char *payto_uri; + + /** + * Response to return, if set. + */ + struct MHD_Response *response; + /** * Public key of the purse we are creating. */ - const struct TALER_PurseContractPublicKeyP *purse_pub; + struct TALER_PurseContractPublicKeyP purse_pub; /** * Total amount to be put into the purse. @@ -98,17 +141,6 @@ struct PurseMergeContext struct TALER_PrivateContractHashP h_contract_terms; /** - * Fees that apply to this operation. - */ - const struct TALER_WireFeeSet *wf; - - /** - * URI of the account the purse is to be merged into. - * Must be of the form 'payto://taler-reserve/$EXCHANGE_URL/RESERVE_PUB'. - */ - const char *payto_uri; - - /** * Hash of the @e payto_uri. */ struct TALER_PaytoHashP h_payto; @@ -119,52 +151,112 @@ struct PurseMergeContext struct TALER_EXCHANGEDB_KycStatus kyc; /** - * Base URL of the exchange provider hosting the reserve. + * HTTP status to return with @e response, or 0. */ - char *provider_url; + unsigned int http_status; /** * Minimum age for deposits into this purse. */ uint32_t min_age; + + /** + * Set to true if this request was suspended. + */ + bool suspended; }; /** + * Kept in a DLL. + */ +static struct PurseMergeContext *pmc_head; + +/** + * Kept in a DLL. + */ +static struct PurseMergeContext *pmc_tail; + + +void +TEH_purses_merge_cleanup () +{ + struct PurseMergeContext *pmc; + + while (NULL != (pmc = pmc_head)) + { + GNUNET_CONTAINER_DLL_remove (pmc_head, + pmc_tail, + pmc); + MHD_resume_connection (pmc->rc->connection); + } +} + + +/** + * Function called with the result of a legitimization + * check. + * + * @param cls closure + * @param lcr legitimization check result + */ +static void +legi_result_cb ( + void *cls, + const struct TEH_LegitimizationCheckResult *lcr) +{ + struct PurseMergeContext *pmc = cls; + + pmc->lch = NULL; + MHD_resume_connection (pmc->rc->connection); + GNUNET_CONTAINER_DLL_remove (pmc_head, + pmc_tail, + pmc); + TALER_MHD_daemon_trigger (); + if (NULL != lcr->response) + { + pmc->response = lcr->response; + pmc->http_status = lcr->http_status; + return; + } + pmc->kyc = lcr->kyc; +} + + +/** * Send confirmation of purse creation success to client. * - * @param connection connection to the client * @param pcc details about the request that succeeded * @return MHD result code */ static MHD_RESULT -reply_merge_success (struct MHD_Connection *connection, - const struct PurseMergeContext *pcc) +reply_merge_success (const struct PurseMergeContext *pmc) { + struct MHD_Connection *connection = pmc->rc->connection; struct TALER_ExchangePublicKeyP pub; struct TALER_ExchangeSignatureP sig; enum TALER_ErrorCode ec; struct TALER_Amount merge_amount; if (0 < - TALER_amount_cmp (&pcc->balance, - &pcc->target_amount)) + TALER_amount_cmp (&pmc->balance, + &pmc->target_amount)) { GNUNET_break (0); return TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_JSON_pack_amount ("balance", - &pcc->balance), + &pmc->balance), TALER_JSON_pack_amount ("target_amount", - &pcc->target_amount)); + &pmc->target_amount)); } - if ( (NULL == pcc->provider_url) || - (0 == strcmp (pcc->provider_url, + if ( (NULL == pmc->provider_url) || + (0 == strcmp (pmc->provider_url, TEH_base_url)) ) { /* wad fee is always zero if we stay at our own exchange */ - merge_amount = pcc->target_amount; + merge_amount = pmc->target_amount; } else { @@ -172,7 +264,7 @@ reply_merge_success (struct MHD_Connection *connection, /* FIXME: figure out partner, lookup wad fee by partner! #7271 */ if (0 > TALER_amount_subtract (&merge_amount, - &pcc->target_amount, + &pmc->target_amount, &wad_fee)) { GNUNET_assert (GNUNET_OK == @@ -180,20 +272,20 @@ reply_merge_success (struct MHD_Connection *connection, &merge_amount)); } #else - merge_amount = pcc->target_amount; + merge_amount = pmc->target_amount; #endif } if (TALER_EC_NONE != (ec = TALER_exchange_online_purse_merged_sign ( &TEH_keys_exchange_sign_, - pcc->exchange_timestamp, - pcc->purse_expiration, + pmc->exchange_timestamp, + pmc->purse_expiration, &merge_amount, - pcc->purse_pub, - &pcc->h_contract_terms, - &pcc->account_pub.reserve_pub, - (NULL != pcc->provider_url) - ? pcc->provider_url + &pmc->purse_pub, + &pmc->h_contract_terms, + &pmc->account_pub.reserve_pub, + (NULL != pmc->provider_url) + ? pmc->provider_url : TEH_base_url, &pub, &sig))) @@ -209,7 +301,7 @@ reply_merge_success (struct MHD_Connection *connection, TALER_JSON_pack_amount ("merge_amount", &merge_amount), GNUNET_JSON_pack_timestamp ("exchange_timestamp", - pcc->exchange_timestamp), + pmc->exchange_timestamp), GNUNET_JSON_pack_data_auto ("exchange_sig", &sig), GNUNET_JSON_pack_data_auto ("exchange_pub", @@ -238,19 +330,19 @@ amount_iterator (void *cls, TALER_EXCHANGEDB_KycAmountCallback cb, void *cb_cls) { - struct PurseMergeContext *pcc = cls; + struct PurseMergeContext *pmc = cls; enum GNUNET_GenericReturnValue ret; enum GNUNET_DB_QueryStatus qs; ret = cb (cb_cls, - &pcc->target_amount, + &pmc->target_amount, GNUNET_TIME_absolute_get ()); GNUNET_break (GNUNET_SYSERR != ret); if (GNUNET_OK != ret) return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; qs = TEH_plugin->select_merge_amounts_for_kyc_check ( TEH_plugin->cls, - &pcc->h_payto, + &pmc->h_payto, limit, cb, cb_cls); @@ -281,33 +373,20 @@ merge_transaction (void *cls, struct MHD_Connection *connection, MHD_RESULT *mhd_ret) { - struct PurseMergeContext *pcc = cls; + struct PurseMergeContext *pmc = cls; enum GNUNET_DB_QueryStatus qs; bool in_conflict = true; bool no_balance = true; bool no_partner = true; - qs = TEH_legitimization_check ( - &pcc->kyc, - connection, - mhd_ret, - TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE, - pcc->payto_uri, - &pcc->h_payto, - &pcc->account_pub, - &amount_iterator, - pcc); - if ( (qs < 0) || - (! pcc->kyc.ok) ) - return qs; qs = TEH_plugin->do_purse_merge ( TEH_plugin->cls, - pcc->purse_pub, - &pcc->merge_sig, - pcc->merge_timestamp, - &pcc->reserve_sig, - pcc->provider_url, - &pcc->account_pub.reserve_pub, + &pmc->purse_pub, + &pmc->merge_sig, + pmc->merge_timestamp, + &pmc->reserve_sig, + pmc->provider_url, + &pmc->account_pub.reserve_pub, &no_partner, &no_balance, &in_conflict); @@ -329,7 +408,7 @@ merge_transaction (void *cls, TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_EXCHANGE_MERGE_PURSE_PARTNER_UNKNOWN, - pcc->provider_url); + pmc->provider_url); return GNUNET_DB_STATUS_HARD_ERROR; } if (no_balance) @@ -350,7 +429,7 @@ merge_transaction (void *cls, bool refunded; qs = TEH_plugin->select_purse_merge (TEH_plugin->cls, - pcc->purse_pub, + &pmc->purse_pub, &merge_sig, &merge_timestamp, &partner_url, @@ -382,7 +461,7 @@ merge_transaction (void *cls, } if (0 != GNUNET_memcmp (&merge_sig, - &pcc->merge_sig)) + &pmc->merge_sig)) { *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( connection, @@ -400,8 +479,7 @@ merge_transaction (void *cls, return GNUNET_DB_STATUS_HARD_ERROR; } /* idempotent! */ - *mhd_ret = reply_merge_success (connection, - pcc); + *mhd_ret = reply_merge_success (pmc); GNUNET_free (partner_url); return GNUNET_DB_STATUS_HARD_ERROR; } @@ -410,251 +488,306 @@ merge_transaction (void *cls, } +/** + * Purse-merge-specific cleanup routine. Function called + * upon completion of the request that should + * clean up @a rh_ctx. Can be NULL. + * + * @param rc request context to clean up + */ +static void +clean_purse_merge_rc (struct TEH_RequestContext *rc) +{ + struct PurseMergeContext *pmc = rc->rh_ctx; + + if (NULL != pmc->lch) + { + TEH_legitimization_check_cancel (pmc->lch); + pmc->lch = NULL; + } + GNUNET_free (pmc->provider_url); + GNUNET_free (pmc); +} + + MHD_RESULT TEH_handler_purses_merge ( - struct MHD_Connection *connection, + struct TEH_RequestContext *rc, const struct TALER_PurseContractPublicKeyP *purse_pub, const json_t *root) { - struct PurseMergeContext pcc = { - .purse_pub = purse_pub, - .exchange_timestamp = GNUNET_TIME_timestamp_get () - }; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_payto_uri ("payto_uri", - &pcc.payto_uri), - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &pcc.reserve_sig), - GNUNET_JSON_spec_fixed_auto ("merge_sig", - &pcc.merge_sig), - GNUNET_JSON_spec_timestamp ("merge_timestamp", - &pcc.merge_timestamp), - GNUNET_JSON_spec_end () - }; - struct TALER_PurseContractSignatureP purse_sig; - enum GNUNET_DB_QueryStatus qs; - bool http; + struct PurseMergeContext *pmc = rc->rh_ctx; + if (NULL == pmc) { - enum GNUNET_GenericReturnValue res; + pmc = GNUNET_new (struct PurseMergeContext); + rc->rh_ctx = pmc; + rc->rh_cleaner = &clean_purse_merge_rc; + pmc->rc = rc; + pmc->purse_pub = *purse_pub; + pmc->exchange_timestamp + = GNUNET_TIME_timestamp_get (); - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == res) { - GNUNET_break (0); - return MHD_NO; /* hard failure */ + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_payto_uri ("payto_uri", + &pmc->payto_uri), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &pmc->reserve_sig), + GNUNET_JSON_spec_fixed_auto ("merge_sig", + &pmc->merge_sig), + GNUNET_JSON_spec_timestamp ("merge_timestamp", + &pmc->merge_timestamp), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } } - if (GNUNET_NO == res) + { - GNUNET_break_op (0); - return MHD_YES; /* failure */ + struct TALER_PurseContractSignatureP purse_sig; + enum GNUNET_DB_QueryStatus qs; + + /* Fetch purse details */ + qs = TEH_plugin->get_purse_request ( + TEH_plugin->cls, + &pmc->purse_pub, + &pmc->merge_pub, + &pmc->purse_expiration, + &pmc->h_contract_terms, + &pmc->min_age, + &pmc->target_amount, + &pmc->balance, + &purse_sig); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse request"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse request"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* continued below */ + break; + } } - } - - /* Fetch purse details */ - qs = TEH_plugin->get_purse_request (TEH_plugin->cls, - pcc.purse_pub, - &pcc.merge_pub, - &pcc.purse_expiration, - &pcc.h_contract_terms, - &pcc.min_age, - &pcc.target_amount, - &pcc.balance, - &purse_sig); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select purse request"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select purse request"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, - NULL); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* continued below */ - break; - } - /* parse 'payto_uri' into pcc.account_pub and provider_url */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received payto: `%s'\n", - pcc.payto_uri); - if ( (0 != strncmp (pcc.payto_uri, - "payto://taler-reserve/", - strlen ("payto://taler-reserve/"))) && - (0 != strncmp (pcc.payto_uri, - "payto://taler-reserve-http/", - strlen ("payto://taler-reserve+http/"))) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "payto_uri"); - } - http = (0 == strncmp (pcc.payto_uri, - "payto://taler-reserve-http/", - strlen ("payto://taler-reserve-http/"))); - { - const char *host = &pcc.payto_uri[http - ? strlen ("payto://taler-reserve-http/") - : strlen ("payto://taler-reserve/")]; - const char *slash = strchr (host, - '/'); - - if (NULL == slash) + /* check signatures */ + if (GNUNET_OK != + TALER_wallet_purse_merge_verify ( + pmc->payto_uri, + pmc->merge_timestamp, + &pmc->purse_pub, + &pmc->merge_pub, + &pmc->merge_sig)) { GNUNET_break_op (0); return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "payto_uri"); + rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_MERGE_INVALID_MERGE_SIGNATURE, + NULL); } - GNUNET_asprintf (&pcc.provider_url, - "%s://%.*s/", - http ? "http" : "https", - (int) (slash - host), - host); - slash++; - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data ( - slash, - strlen (slash), - &pcc.account_pub.reserve_pub, - sizeof (pcc.account_pub.reserve_pub))) + + /* parse 'payto_uri' into pmc->account_pub and provider_url */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received payto: `%s'\n", + pmc->payto_uri); + if ( (0 != strncmp (pmc->payto_uri, + "payto://taler-reserve/", + strlen ("payto://taler-reserve/"))) && + (0 != strncmp (pmc->payto_uri, + "payto://taler-reserve-http/", + strlen ("payto://taler-reserve+http/"))) ) { GNUNET_break_op (0); - GNUNET_free (pcc.provider_url); return TALER_MHD_reply_with_error ( - connection, + rc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "payto_uri"); } - slash++; - } - TALER_payto_hash (pcc.payto_uri, - &pcc.h_payto); - if (0 == strcmp (pcc.provider_url, - TEH_base_url)) - { - /* we use NULL to represent 'self' as the provider */ - GNUNET_free (pcc.provider_url); - } - else - { - char *method = GNUNET_strdup ("FIXME-WAD #7271"); - /* FIXME-#7271: lookup wire method by pcc.provider_url! */ - pcc.wf = TEH_wire_fees_by_time (pcc.exchange_timestamp, - method); - if (NULL == pcc.wf) { - MHD_RESULT res; - - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Cannot merge purse: wire fees not configured!\n"); - res = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_WIRE_FEES_MISSING, - method); + bool http; + const char *host; + const char *slash; + + http = (0 == strncmp (pmc->payto_uri, + "payto://taler-reserve-http/", + strlen ("payto://taler-reserve-http/"))); + host = &pmc->payto_uri[http + ? strlen ("payto://taler-reserve-http/") + : strlen ("payto://taler-reserve/")]; + slash = strchr (host, + '/'); + if (NULL == slash) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "payto_uri"); + } + GNUNET_asprintf (&pmc->provider_url, + "%s://%.*s/", + http ? "http" : "https", + (int) (slash - host), + host); + slash++; + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + slash, + strlen (slash), + &pmc->account_pub.reserve_pub, + sizeof (pmc->account_pub.reserve_pub))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "payto_uri"); + } + } + TALER_payto_hash (pmc->payto_uri, + &pmc->h_payto); + if (0 == strcmp (pmc->provider_url, + TEH_base_url)) + { + /* we use NULL to represent 'self' as the provider */ + GNUNET_free (pmc->provider_url); + } + else + { + char *method = GNUNET_strdup ("FIXME-WAD #7271"); + + /* FIXME-#7271: lookup wire method by pmc.provider_url! */ + pmc->wf = TEH_wire_fees_by_time (pmc->exchange_timestamp, + method); + if (NULL == pmc->wf) + { + MHD_RESULT res; + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Cannot merge purse: wire fees not configured!\n"); + res = TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_WIRE_FEES_MISSING, + method); + GNUNET_free (method); + return res; + } GNUNET_free (method); - return res; } - GNUNET_free (method); - } - /* check signatures */ - if (GNUNET_OK != - TALER_wallet_purse_merge_verify ( - pcc.payto_uri, - pcc.merge_timestamp, - pcc.purse_pub, - &pcc.merge_pub, - &pcc.merge_sig)) - { - GNUNET_break_op (0); - GNUNET_free (pcc.provider_url); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_PURSE_MERGE_INVALID_MERGE_SIGNATURE, - NULL); - } - { - struct TALER_Amount zero_purse_fee; - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (pcc.target_amount.currency, - &zero_purse_fee)); - if (GNUNET_OK != - TALER_wallet_account_merge_verify ( - pcc.merge_timestamp, - pcc.purse_pub, - pcc.purse_expiration, - &pcc.h_contract_terms, - &pcc.target_amount, - &zero_purse_fee, - pcc.min_age, - TALER_WAMF_MODE_MERGE_FULLY_PAID_PURSE, - &pcc.account_pub.reserve_pub, - &pcc.reserve_sig)) { - GNUNET_break_op (0); - GNUNET_free (pcc.provider_url); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_PURSE_MERGE_INVALID_RESERVE_SIGNATURE, - NULL); + struct TALER_Amount zero_purse_fee; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero ( + pmc->target_amount.currency, + &zero_purse_fee)); + if (GNUNET_OK != + TALER_wallet_account_merge_verify ( + pmc->merge_timestamp, + &pmc->purse_pub, + pmc->purse_expiration, + &pmc->h_contract_terms, + &pmc->target_amount, + &zero_purse_fee, + pmc->min_age, + TALER_WAMF_MODE_MERGE_FULLY_PAID_PURSE, + &pmc->account_pub.reserve_pub, + &pmc->reserve_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_MERGE_INVALID_RESERVE_SIGNATURE, + NULL); + } } + pmc->lch = TEH_legitimization_check ( + &rc->async_scope_id, + TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE, + pmc->payto_uri, + &pmc->h_payto, + &pmc->account_pub, + &amount_iterator, + pmc, + &legi_result_cb, + pmc); + GNUNET_assert (NULL != pmc->lch); + MHD_suspend_connection (rc->connection); + GNUNET_CONTAINER_DLL_insert (pmc_head, + pmc_tail, + pmc); + return MHD_YES; } + if (NULL != pmc->response) + { + return MHD_queue_response (rc->connection, + pmc->http_status, + pmc->response); + } + if (! pmc->kyc.ok) + return TEH_RESPONSE_reply_kyc_required (rc->connection, + &pmc->h_payto, + &pmc->kyc); - /* execute transaction */ + /* execute merge transaction */ { MHD_RESULT mhd_ret; if (GNUNET_OK != - TEH_DB_run_transaction (connection, + TEH_DB_run_transaction (rc->connection, "execute purse merge", TEH_MT_REQUEST_PURSE_MERGE, &mhd_ret, &merge_transaction, - &pcc)) + pmc)) { - GNUNET_free (pcc.provider_url); return mhd_ret; } } - - GNUNET_free (pcc.provider_url); - if (! pcc.kyc.ok) - return TEH_RESPONSE_reply_kyc_required (connection, - &pcc.h_payto, - &pcc.kyc); - { struct TALER_PurseEventP rep = { .header.size = htons (sizeof (rep)), .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_MERGED), - .purse_pub = *pcc.purse_pub + .purse_pub = pmc->purse_pub }; GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -666,8 +799,7 @@ TEH_handler_purses_merge ( } /* generate regular response */ - return reply_merge_success (connection, - &pcc); + return reply_merge_success (pmc); } diff --git a/src/exchange/taler-exchange-httpd_purses_merge.h b/src/exchange/taler-exchange-httpd_purses_merge.h index 3bc6e1696..9355ae1e4 100644 --- a/src/exchange/taler-exchange-httpd_purses_merge.h +++ b/src/exchange/taler-exchange-httpd_purses_merge.h @@ -27,20 +27,28 @@ /** + * Resume suspended connections, we are shutting down. + */ +void +TEH_purses_merge_cleanup (void); + + +/** * Handle a "/purses/$PURSE_PUB/merge" request. Parses the JSON, and, if * successful, passes the JSON data to #merge_transaction() to further check * the details of the operation specified. If everything checks out, this * will ultimately lead to the "purses merge" being executed, or rejected. * - * @param connection the MHD connection to handle + * @param rc request to handle * @param purse_pub public key of the purse * @param root uploaded JSON data * @return MHD result code */ MHD_RESULT -TEH_handler_purses_merge (struct MHD_Connection *connection, - const struct TALER_PurseContractPublicKeyP *purse_pub, - const json_t *root); +TEH_handler_purses_merge ( + struct TEH_RequestContext *rc, + const struct TALER_PurseContractPublicKeyP *purse_pub, + const json_t *root); #endif diff --git a/src/exchange/taler-exchange-httpd_reserves_close.c b/src/exchange/taler-exchange-httpd_reserves_close.c index 7b5f14c13..bd048e0c2 100644 --- a/src/exchange/taler-exchange-httpd_reserves_close.c +++ b/src/exchange/taler-exchange-httpd_reserves_close.c @@ -27,6 +27,7 @@ #include "taler_mhd_lib.h" #include "taler_json_lib.h" #include "taler_dbevents.h" +#include "taler-exchange-httpd_common_kyc.h" #include "taler-exchange-httpd_keys.h" #include "taler-exchange-httpd_reserves_close.h" #include "taler-exchange-httpd_withdraw.h" @@ -46,10 +47,44 @@ */ struct ReserveCloseContext { + + /** + * Kept in a DLL. + */ + struct ReserveCloseContext *next; + + /** + * Kept in a DLL. + */ + struct ReserveCloseContext *prev; + + /** + * Our request context. + */ + struct TEH_RequestContext *rc; + + /** + * Handle for legitimization check. + */ + struct TEH_LegitimizationCheckHandle *lch; + + /** + * Where to wire the funds, may be NULL. + */ + const char *payto_uri; + + /** + * Response to return. Note that the response must + * be queued or destroyed by the callee. NULL + * if the legitimization check was successful and the handler should return + * a handler-specific result. + */ + struct MHD_Response *response; + /** * Public key of the reserve the inquiry is about. */ - const struct TALER_ReservePublicKeyP *reserve_pub; + struct TALER_ReservePublicKeyP reserve_pub; /** * Timestamp of the request. @@ -72,11 +107,6 @@ struct ReserveCloseContext struct TALER_Amount balance; /** - * Where to wire the funds, may be NULL. - */ - const char *payto_uri; - - /** * Hash of the @e payto_uri, if given (otherwise zero). */ struct TALER_PaytoHashP h_payto; @@ -95,20 +125,61 @@ struct ReserveCloseContext * Query status from the amount_it() helper function. */ enum GNUNET_DB_QueryStatus qs; + + /** + * HTTP status code for @a response, or 0 + */ + unsigned int http_status; + + /** + * Set to true if the request was suspended. + */ + bool suspended; + + /** + * Set to true if the request was suspended. + */ + bool resumed; }; /** + * Kept in a DLL. + */ +static struct ReserveCloseContext *rcc_head; + +/** + * Kept in a DLL. + */ +static struct ReserveCloseContext *rcc_tail; + + +void +TEH_reserves_close_cleanup () +{ + struct ReserveCloseContext *rcc; + + while (NULL != (rcc = rcc_head)) + { + GNUNET_CONTAINER_DLL_remove (rcc_head, + rcc_tail, + rcc); + MHD_resume_connection (rcc->rc->connection); + } +} + + +/** * Send reserve close to client. * - * @param connection connection to the client * @param rhc reserve close to return * @return MHD result code */ static MHD_RESULT -reply_reserve_close_success (struct MHD_Connection *connection, - const struct ReserveCloseContext *rhc) +reply_reserve_close_success ( + const struct ReserveCloseContext *rhc) { + struct MHD_Connection *connection = rhc->rc->connection; return TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_OK, @@ -118,6 +189,34 @@ reply_reserve_close_success (struct MHD_Connection *connection, /** + * Function called with the result of a legitimization + * check. + * + * @param cls closure + * @param lcr legitimization check result + */ +static void +reserve_close_legi_cb ( + void *cls, + const struct TEH_LegitimizationCheckResult *lcr) +{ + struct ReserveCloseContext *rcc = cls; + + rcc->lch = NULL; + rcc->http_status = lcr->http_status; + rcc->response = lcr->response; + rcc->kyc = lcr->kyc; + GNUNET_CONTAINER_DLL_remove (rcc_head, + rcc_tail, + rcc); + MHD_resume_connection (rcc->rc->connection); + rcc->resumed = true; + rcc->suspended = false; + TALER_MHD_daemon_trigger (); +} + + +/** * Function called to iterate over KYC-relevant * transaction amounts for a particular time range. * Called within a database transaction, so must @@ -175,9 +274,10 @@ amount_it (void *cls, * @return transaction status */ static enum GNUNET_DB_QueryStatus -reserve_close_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) +reserve_close_transaction ( + void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) { struct ReserveCloseContext *rcc = cls; enum GNUNET_DB_QueryStatus qs; @@ -186,7 +286,7 @@ reserve_close_transaction (void *cls, qs = TEH_plugin->select_reserve_close_info ( TEH_plugin->cls, - rcc->reserve_pub, + &rcc->reserve_pub, &rcc->balance, &payto_uri); switch (qs) @@ -194,19 +294,21 @@ reserve_close_transaction (void *cls, case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); *mhd_ret - = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_reserve_close_info"); + = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_reserve_close_info"); return qs; case GNUNET_DB_STATUS_SOFT_ERROR: return qs; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: *mhd_ret - = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, - NULL); + = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL); return GNUNET_DB_STATUS_HARD_ERROR; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; @@ -216,14 +318,16 @@ reserve_close_transaction (void *cls, (NULL == payto_uri) ) { *mhd_ret - = TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_RESERVES_CLOSE_NO_TARGET_ACCOUNT, - NULL); + = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_RESERVES_CLOSE_NO_TARGET_ACCOUNT, + NULL); return GNUNET_DB_STATUS_HARD_ERROR; } - if ( (NULL != rcc->payto_uri) && + if ( (! rcc->resumed) && + (NULL != rcc->payto_uri) && ( (NULL == payto_uri) || (0 != strcmp (payto_uri, rcc->payto_uri)) ) ) @@ -234,28 +338,25 @@ reserve_close_transaction (void *cls, TALER_payto_hash (rcc->payto_uri, &rcc->kyc_payto); - rcc->qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - qs = TEH_legitimization_check ( - &rcc->kyc, - connection, - mhd_ret, + rcc->lch = TEH_legitimization_check ( + &rcc->rc->async_scope_id, TALER_KYCLOGIC_KYC_TRIGGER_RESERVE_CLOSE, rcc->payto_uri, &rcc->kyc_payto, - NULL, + NULL, /* no account_pub */ &amount_it, + rcc, + &reserve_close_legi_cb, rcc); - if ( (qs < 0) || - (! rcc->kyc.ok) ) - { - GNUNET_free (payto_uri); - return qs; - } - } - else - { - rcc->kyc.ok = true; + GNUNET_assert (NULL != rcc->lch); + GNUNET_CONTAINER_DLL_insert (rcc_head, + rcc_tail, + rcc); + MHD_suspend_connection (rcc->rc->connection); + rcc->suspended = true; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; } + rcc->kyc.ok = true; if (NULL == rcc->payto_uri) rcc->payto_uri = payto_uri; @@ -268,10 +369,11 @@ reserve_close_transaction (void *cls, if (NULL == wf) { GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_WIRE_FEES_NOT_CONFIGURED, - method); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_WIRE_FEES_NOT_CONFIGURED, + method); GNUNET_free (method); return GNUNET_DB_STATUS_HARD_ERROR; } @@ -288,18 +390,18 @@ reserve_close_transaction (void *cls, GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TEH_currency, &rcc->wire_amount)); - *mhd_ret = reply_reserve_close_success (connection, - rcc); + *mhd_ret = reply_reserve_close_success (rcc); return GNUNET_DB_STATUS_HARD_ERROR; } - qs = TEH_plugin->insert_close_request (TEH_plugin->cls, - rcc->reserve_pub, - payto_uri, - &rcc->reserve_sig, - rcc->timestamp, - &rcc->balance, - &wf->closing); + qs = TEH_plugin->insert_close_request ( + TEH_plugin->cls, + &rcc->reserve_pub, + payto_uri, + &rcc->reserve_sig, + rcc->timestamp, + &rcc->balance, + &wf->closing); GNUNET_free (payto_uri); rcc->payto_uri = NULL; if (GNUNET_DB_STATUS_HARD_ERROR == qs) @@ -321,76 +423,131 @@ reserve_close_transaction (void *cls, } +/** + * Cleanup routine. Function called + * upon completion of the request that should + * clean up @a rh_ctx. Can be NULL. + * + * @param rc request to clean up context for + */ +static void +reserve_close_cleanup (struct TEH_RequestContext *rc) +{ + struct ReserveCloseContext *rcc = rc->rh_ctx; + + if (NULL != rcc->lch) + { + TEH_legitimization_check_cancel (rcc->lch); + rcc->lch = NULL; + } + GNUNET_free (rcc); +} + + MHD_RESULT -TEH_handler_reserves_close (struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root) +TEH_handler_reserves_close ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root) { - struct ReserveCloseContext rcc = { - .payto_uri = NULL, - .reserve_pub = reserve_pub - }; + struct ReserveCloseContext *rcc = rc->rh_ctx; MHD_RESULT mhd_ret; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_timestamp ("request_timestamp", - &rcc.timestamp), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_payto_uri ("payto_uri", - &rcc.payto_uri), - NULL), - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &rcc.reserve_sig), - GNUNET_JSON_spec_end () - }; + if (NULL == rcc) { - enum GNUNET_GenericReturnValue res; + rcc = GNUNET_new (struct ReserveCloseContext); + rc->rh_ctx = rcc; + rc->rh_cleaner = &reserve_close_cleanup; + rcc->reserve_pub = *reserve_pub; - res = TALER_MHD_parse_json_data (rc->connection, - root, - spec); - if (GNUNET_SYSERR == res) { - GNUNET_break (0); - return MHD_NO; /* hard failure */ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("request_timestamp", + &rcc->timestamp), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_payto_uri ("payto_uri", + &rcc->payto_uri), + NULL), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &rcc->reserve_sig), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + } } - if (GNUNET_NO == res) + { - GNUNET_break_op (0); - return MHD_YES; /* failure */ + struct GNUNET_TIME_Timestamp now; + + now = GNUNET_TIME_timestamp_get (); + if (! GNUNET_TIME_absolute_approx_eq ( + now.abs_time, + rcc->timestamp.abs_time, + TIMESTAMP_TOLERANCE)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_CLOCK_SKEW, + NULL); + } } - } - { - struct GNUNET_TIME_Timestamp now; - - now = GNUNET_TIME_timestamp_get (); - if (! GNUNET_TIME_absolute_approx_eq (now.abs_time, - rcc.timestamp.abs_time, - TIMESTAMP_TOLERANCE)) + if (NULL != rcc->payto_uri) + TALER_payto_hash (rcc->payto_uri, + &rcc->h_payto); + if (GNUNET_OK != + TALER_wallet_reserve_close_verify ( + rcc->timestamp, + &rcc->h_payto, + &rcc->reserve_pub, + &rcc->reserve_sig)) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_GENERIC_CLOCK_SKEW, - NULL); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVES_CLOSE_BAD_SIGNATURE, + NULL); } } - - if (NULL != rcc.payto_uri) - TALER_payto_hash (rcc.payto_uri, - &rcc.h_payto); - if (GNUNET_OK != - TALER_wallet_reserve_close_verify (rcc.timestamp, - &rcc.h_payto, - reserve_pub, - &rcc.reserve_sig)) + if (NULL != rcc->response) + return MHD_queue_response (rc->connection, + rcc->http_status, + rcc->response); + if (rcc->resumed && + (! rcc->kyc.ok) ) { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_RESERVES_CLOSE_BAD_SIGNATURE, - NULL); + if (0 == rcc->kyc.requirement_row) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "requirement row not set"); + } + return TEH_RESPONSE_reply_kyc_required ( + rc->connection, + &rcc->kyc_payto, + &rcc->kyc); } if (GNUNET_OK != @@ -403,24 +560,9 @@ TEH_handler_reserves_close (struct TEH_RequestContext *rc, { return mhd_ret; } - if (! rcc.kyc.ok) - { - if (0 == rcc.kyc.requirement_row) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "requirement row not set"); - } - return TEH_RESPONSE_reply_kyc_required ( - rc->connection, - &rcc.kyc_payto, - &rcc.kyc); - } - return reply_reserve_close_success (rc->connection, - &rcc); + if (rcc->suspended) + return MHD_YES; + return reply_reserve_close_success (rcc); } diff --git a/src/exchange/taler-exchange-httpd_reserves_close.h b/src/exchange/taler-exchange-httpd_reserves_close.h index 4c70b17cb..f4ca07c4f 100644 --- a/src/exchange/taler-exchange-httpd_reserves_close.h +++ b/src/exchange/taler-exchange-httpd_reserves_close.h @@ -26,6 +26,12 @@ /** + * Resume suspended connections, we are shutting down. + */ +void +TEH_reserves_close_cleanup (void); + +/** * Handle a POST "/reserves/$RID/close" request. * * @param rc request context @@ -34,8 +40,9 @@ * @return MHD result code */ MHD_RESULT -TEH_handler_reserves_close (struct TEH_RequestContext *rc, - const struct TALER_ReservePublicKeyP *reserve_pub, - const json_t *root); +TEH_handler_reserves_close ( + struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root); #endif diff --git a/src/exchange/taler-exchange-httpd_reserves_purse.c b/src/exchange/taler-exchange-httpd_reserves_purse.c index 6186afe4e..5774e10ea 100644 --- a/src/exchange/taler-exchange-httpd_reserves_purse.c +++ b/src/exchange/taler-exchange-httpd_reserves_purse.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022-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 @@ -29,6 +29,7 @@ #include "taler_json_lib.h" #include "taler_kyclogic_lib.h" #include "taler_mhd_lib.h" +#include "taler-exchange-httpd_common_kyc.h" #include "taler-exchange-httpd_reserves_purse.h" #include "taler-exchange-httpd_responses.h" #include "taler-exchange-httpd_withdraw.h" @@ -43,14 +44,42 @@ struct ReservePurseContext { /** - * Public key of the account (reserve) we are creating a purse for. + * Kept in a DLL. + */ + struct ReservePurseContext *next; + + /** + * Kept in a DLL. + */ + struct ReservePurseContext *prev; + + /** + * Our request context. + */ + struct TEH_RequestContext *rc; + + /** + * Handle for legitimization check. */ - const union TALER_AccountPublicKeyP account_pub; + struct TEH_LegitimizationCheckHandle *lch; /** - * Fees for the operation. + * Response to return. Note that the response must + * be queued or destroyed by the callee. NULL + * if the legitimization check was successful and the handler should return + * a handler-specific result. */ - const struct TEH_GlobalFee *gf; + struct MHD_Response *response; + + /** + * Payto URI for the reserve. + */ + char *payto_uri; + + /** + * Public key of the account (reserve) we are creating a purse for. + */ + union TALER_AccountPublicKeyP account_pub; /** * Signature of the reserve affirming the merge. @@ -108,11 +137,6 @@ struct ReservePurseContext struct TALER_PaytoHashP h_payto; /** - * Payto URI for the reserve. - */ - char *payto_uri; - - /** * KYC status of the operation. */ struct TALER_EXCHANGEDB_KycStatus kyc; @@ -128,12 +152,47 @@ struct ReservePurseContext enum TALER_WalletAccountMergeFlags flags; /** + * HTTP status code for @a response, or 0 + */ + unsigned int http_status; + + /** * Do we lack an @e econtract? */ bool no_econtract; + /** + * Set to true if the purse_fee was not given in the REST request. + */ + bool no_purse_fee; + }; +/** + * Kept in a DLL. + */ +static struct ReservePurseContext *rpc_head; + +/** + * Kept in a DLL. + */ +static struct ReservePurseContext *rpc_tail; + + +void +TEH_reserves_purse_cleanup () +{ + struct ReservePurseContext *rpc; + + while (NULL != (rpc = rpc_head)) + { + GNUNET_CONTAINER_DLL_remove (rpc_head, + rpc_tail, + rpc); + MHD_resume_connection (rpc->rc->connection); + } +} + /** * Function called to iterate over KYC-relevant @@ -182,6 +241,32 @@ amount_iterator (void *cls, /** + * Function called with the result of a legitimization + * check. + * + * @param cls closure + * @param lcr legitimization check result + */ +static void +reserve_purse_legi_cb ( + void *cls, + const struct TEH_LegitimizationCheckResult *lcr) +{ + struct ReservePurseContext *rpc = cls; + + rpc->lch = NULL; + rpc->http_status = lcr->http_status; + rpc->response = lcr->response; + rpc->kyc = lcr->kyc; + GNUNET_CONTAINER_DLL_remove (rpc_head, + rpc_tail, + rpc); + MHD_resume_connection (rpc->rc->connection); + TALER_MHD_daemon_trigger (); +} + + +/** * Execute database transaction for /reserves/$PID/purse. Runs the transaction * logic; IF it returns a non-error code, the transaction logic MUST NOT queue * a MHD response. IF it returns an hard error, the transaction logic MUST @@ -202,20 +287,6 @@ purse_transaction (void *cls, struct ReservePurseContext *rpc = cls; enum GNUNET_DB_QueryStatus qs; - qs = TEH_legitimization_check ( - &rpc->kyc, - connection, - mhd_ret, - TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE, - rpc->payto_uri, - &rpc->h_payto, - &rpc->account_pub, - &amount_iterator, - rpc); - if ( (qs < 0) || - (! rpc->kyc.ok) ) - return qs; - { bool in_conflict = true; @@ -238,10 +309,11 @@ purse_transaction (void *cls, return qs; GNUNET_break (0); *mhd_ret = - TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert purse request"); + TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert purse request"); return GNUNET_DB_STATUS_HARD_ERROR; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) @@ -272,10 +344,11 @@ purse_transaction (void *cls, GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); GNUNET_break (0 != qs); TALER_LOG_WARNING ("Failed to fetch purse information from database\n"); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select purse request"); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse request"); return GNUNET_DB_STATUS_HARD_ERROR; } *mhd_ret @@ -319,7 +392,7 @@ purse_transaction (void *cls, (TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA == rpc->flags) ? NULL - : &rpc->gf->fees.purse, + : &rpc->purse_fee, &rpc->account_pub.reserve_pub, &in_conflict, &no_reserve, @@ -331,10 +404,11 @@ purse_transaction (void *cls, TALER_LOG_WARNING ( "Failed to store purse merge information in database\n"); *mhd_ret = - TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "do reserve purse"); + TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "do reserve purse"); return GNUNET_DB_STATUS_HARD_ERROR; } if (in_conflict) @@ -362,10 +436,11 @@ purse_transaction (void *cls, GNUNET_break (0 != qs); TALER_LOG_WARNING ( "Failed to fetch purse merge information from database\n"); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select purse merge"); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select purse merge"); return GNUNET_DB_STATUS_HARD_ERROR; } if (refunded) @@ -373,10 +448,11 @@ purse_transaction (void *cls, /* This is a bit of a strange case ... */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Purse was already refunded\n"); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_GONE, - TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED, - NULL); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_GONE, + TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED, + NULL); GNUNET_free (partner_url); return GNUNET_DB_STATUS_HARD_ERROR; } @@ -402,7 +478,7 @@ purse_transaction (void *cls, if ( (no_reserve) && ( (TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA == rpc->flags) || - (! TALER_amount_is_zero (&rpc->gf->fees.purse)) ) ) + (! TALER_amount_is_zero (&rpc->purse_fee)) ) ) { *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( @@ -437,10 +513,11 @@ purse_transaction (void *cls, if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return qs; TALER_LOG_WARNING ("Failed to store purse information in database\n"); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "purse purse contract"); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "purse purse contract"); return GNUNET_DB_STATUS_HARD_ERROR; } if (in_conflict) @@ -459,10 +536,11 @@ purse_transaction (void *cls, GNUNET_break (0 != qs); TALER_LOG_WARNING ( "Failed to store fetch contract information from database\n"); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select contract"); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select contract"); return GNUNET_DB_STATUS_HARD_ERROR; } GNUNET_CRYPTO_hash (econtract.econtract, @@ -480,7 +558,6 @@ purse_transaction (void *cls, &econtract.econtract_sig), GNUNET_JSON_pack_data_auto ("contract_pub", &econtract.contract_pub)); - GNUNET_free (econtract.econtract); return GNUNET_DB_STATUS_HARD_ERROR; } } @@ -488,277 +565,320 @@ purse_transaction (void *cls, } +/** + * Function to clean up our rh_ctx in @a rc + * + * @param[in,out] rc context to clean up + */ +static void +rpc_cleaner (struct TEH_RequestContext *rc) +{ + struct ReservePurseContext *rpc = rc->rh_ctx; + + if (NULL != rpc->lch) + { + TEH_legitimization_check_cancel (rpc->lch); + rpc->lch = NULL; + } + GNUNET_free (rpc->econtract.econtract); + GNUNET_free (rpc->payto_uri); + GNUNET_free (rpc); +} + + MHD_RESULT TEH_handler_reserves_purse ( struct TEH_RequestContext *rc, const struct TALER_ReservePublicKeyP *reserve_pub, const json_t *root) { + struct ReservePurseContext *rpc = rc->rh_ctx; struct MHD_Connection *connection = rc->connection; - struct ReservePurseContext rpc = { - .account_pub.reserve_pub = *reserve_pub, - .exchange_timestamp = GNUNET_TIME_timestamp_get () - }; - bool no_purse_fee = true; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount ("purse_value", - TEH_currency, - &rpc.pd.target_amount), - GNUNET_JSON_spec_uint32 ("min_age", - &rpc.min_age), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount ("purse_fee", - TEH_currency, - &rpc.purse_fee), - &no_purse_fee), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_econtract ("econtract", - &rpc.econtract), - &rpc.no_econtract), - GNUNET_JSON_spec_fixed_auto ("merge_pub", - &rpc.merge_pub), - GNUNET_JSON_spec_fixed_auto ("merge_sig", - &rpc.merge_sig), - GNUNET_JSON_spec_fixed_auto ("reserve_sig", - &rpc.reserve_sig), - GNUNET_JSON_spec_fixed_auto ("purse_pub", - &rpc.pd.purse_pub), - GNUNET_JSON_spec_fixed_auto ("purse_sig", - &rpc.purse_sig), - GNUNET_JSON_spec_fixed_auto ("h_contract_terms", - &rpc.pd.h_contract_terms), - GNUNET_JSON_spec_timestamp ("merge_timestamp", - &rpc.merge_timestamp), - GNUNET_JSON_spec_timestamp ("purse_expiration", - &rpc.pd.purse_expiration), - GNUNET_JSON_spec_end () - }; + if (NULL == rpc) { - enum GNUNET_GenericReturnValue res; + rpc = GNUNET_new (struct ReservePurseContext); + rc->rh_ctx = rpc; + rc->rh_cleaner = &rpc_cleaner; + rpc->rc = rc; + rpc->account_pub.reserve_pub = *reserve_pub; + rpc->exchange_timestamp + = GNUNET_TIME_timestamp_get (); + rpc->no_purse_fee = true; + - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_SYSERR == res) { - GNUNET_break (0); - return MHD_NO; /* hard failure */ + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount ("purse_value", + TEH_currency, + &rpc->pd.target_amount), + GNUNET_JSON_spec_uint32 ("min_age", + &rpc->min_age), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount ("purse_fee", + TEH_currency, + &rpc->purse_fee), + &rpc->no_purse_fee), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_econtract ("econtract", + &rpc->econtract), + &rpc->no_econtract), + GNUNET_JSON_spec_fixed_auto ("merge_pub", + &rpc->merge_pub), + GNUNET_JSON_spec_fixed_auto ("merge_sig", + &rpc->merge_sig), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &rpc->reserve_sig), + GNUNET_JSON_spec_fixed_auto ("purse_pub", + &rpc->pd.purse_pub), + GNUNET_JSON_spec_fixed_auto ("purse_sig", + &rpc->purse_sig), + GNUNET_JSON_spec_fixed_auto ("h_contract_terms", + &rpc->pd.h_contract_terms), + GNUNET_JSON_spec_timestamp ("merge_timestamp", + &rpc->merge_timestamp), + GNUNET_JSON_spec_timestamp ("purse_expiration", + &rpc->pd.purse_expiration), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + } } - if (GNUNET_NO == res) + + rpc->payto_uri + = TALER_reserve_make_payto (TEH_base_url, + &rpc->account_pub.reserve_pub); + TALER_payto_hash (rpc->payto_uri, + &rpc->h_payto); + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + + + if (GNUNET_OK != + TALER_wallet_purse_merge_verify (rpc->payto_uri, + rpc->merge_timestamp, + &rpc->pd.purse_pub, + &rpc->merge_pub, + &rpc->merge_sig)) { + MHD_RESULT ret; + GNUNET_break_op (0); - return MHD_YES; /* failure */ + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVES_PURSE_MERGE_SIGNATURE_INVALID, + rpc->payto_uri); + GNUNET_free (rpc->payto_uri); + return ret; + } + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &rpc->deposit_total)); + if (GNUNET_TIME_timestamp_cmp (rpc->pd.purse_expiration, + <, + rpc->exchange_timestamp)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_RESERVES_PURSE_EXPIRATION_BEFORE_NOW, + NULL); + } + if (GNUNET_TIME_absolute_is_never ( + rpc->pd.purse_expiration.abs_time)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_RESERVES_PURSE_EXPIRATION_IS_NEVER, + NULL); } - } - rpc.payto_uri - = TALER_reserve_make_payto (TEH_base_url, - &rpc.account_pub.reserve_pub); - TALER_payto_hash (rpc.payto_uri, - &rpc.h_payto); - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_purse_merge_verify (rpc.payto_uri, - rpc.merge_timestamp, - &rpc.pd.purse_pub, - &rpc.merge_pub, - &rpc.merge_sig)) - { - MHD_RESULT ret; - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_RESERVES_PURSE_MERGE_SIGNATURE_INVALID, - rpc.payto_uri); - GNUNET_free (rpc.payto_uri); - return ret; - } - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &rpc.deposit_total)); - if (GNUNET_TIME_timestamp_cmp (rpc.pd.purse_expiration, - <, - rpc.exchange_timestamp)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (rpc.payto_uri); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_RESERVES_PURSE_EXPIRATION_BEFORE_NOW, - NULL); - } - if (GNUNET_TIME_absolute_is_never (rpc.pd.purse_expiration.abs_time)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (rpc.payto_uri); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_RESERVES_PURSE_EXPIRATION_IS_NEVER, - NULL); - } - { - struct TEH_KeyStateHandle *keys; + { + struct TEH_KeyStateHandle *keys; + const struct TEH_GlobalFee *gf; - keys = TEH_keys_get_state (); - if (NULL == keys) + keys = TEH_keys_get_state (); + if (NULL == keys) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + NULL); + } + gf = TEH_keys_global_fee_by_time (keys, + rpc->exchange_timestamp); + if (NULL == gf) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Cannot create purse: global fees not configured!\n"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_GENERIC_GLOBAL_FEES_MISSING, + NULL); + } + if (rpc->no_purse_fee) + { + rpc->flags = TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA; + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &rpc->purse_fee)); + } + else + { + rpc->flags = TALER_WAMF_MODE_CREATE_WITH_PURSE_FEE; + if (-1 == + TALER_amount_cmp (&rpc->purse_fee, + &gf->fees.purse)) + { + /* rpc->purse_fee is below gf.fees.purse! */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_RESERVES_PURSE_FEE_TOO_LOW, + TALER_amount2s (&gf->fees.purse)); + } + } + } + + TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; + if (GNUNET_OK != + TALER_wallet_purse_create_verify ( + rpc->pd.purse_expiration, + &rpc->pd.h_contract_terms, + &rpc->merge_pub, + rpc->min_age, + &rpc->pd.target_amount, + &rpc->pd.purse_pub, + &rpc->purse_sig)) { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (rpc.payto_uri); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, - NULL); + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID, + NULL); } - rpc.gf = TEH_keys_global_fee_by_time (keys, - rpc.exchange_timestamp); - } - if (NULL == rpc.gf) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Cannot purse purse: global fees not configured!\n"); - GNUNET_JSON_parse_free (spec); - GNUNET_free (rpc.payto_uri); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_GLOBAL_FEES_MISSING, - NULL); - } - if (no_purse_fee) - { - rpc.flags = TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA; - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &rpc.purse_fee)); - } - else - { - rpc.flags = TALER_WAMF_MODE_CREATE_WITH_PURSE_FEE; - if (-1 == - TALER_amount_cmp (&rpc.purse_fee, - &rpc.gf->fees.purse)) + if (GNUNET_OK != + TALER_wallet_account_merge_verify ( + rpc->merge_timestamp, + &rpc->pd.purse_pub, + rpc->pd.purse_expiration, + &rpc->pd.h_contract_terms, + &rpc->pd.target_amount, + &rpc->purse_fee, + rpc->min_age, + rpc->flags, + &rpc->account_pub.reserve_pub, + &rpc->reserve_sig)) { - /* rpc.purse_fee is below gf.fees.purse! */ GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (rpc.payto_uri); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVES_RESERVE_MERGE_SIGNATURE_INVALID, + NULL); + } + if ( (! rpc->no_econtract) && + (GNUNET_OK != + TALER_wallet_econtract_upload_verify ( + rpc->econtract.econtract, + rpc->econtract.econtract_size, + &rpc->econtract.contract_pub, + &rpc->pd.purse_pub, + &rpc->econtract.econtract_sig)) + ) + { + TALER_LOG_WARNING ("Invalid signature on /reserves/$PID/purse request\n"); return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_RESERVES_PURSE_FEE_TOO_LOW, - TALER_amount2s (&rpc.gf->fees.purse)); + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID, + NULL); } - } - TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; - if (GNUNET_OK != - TALER_wallet_purse_create_verify (rpc.pd.purse_expiration, - &rpc.pd.h_contract_terms, - &rpc.merge_pub, - rpc.min_age, - &rpc.pd.target_amount, - &rpc.pd.purse_pub, - &rpc.purse_sig)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (rpc.payto_uri); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID, - NULL); - } - if (GNUNET_OK != - TALER_wallet_account_merge_verify ( - rpc.merge_timestamp, - &rpc.pd.purse_pub, - rpc.pd.purse_expiration, - &rpc.pd.h_contract_terms, - &rpc.pd.target_amount, - &rpc.purse_fee, - rpc.min_age, - rpc.flags, - &rpc.account_pub.reserve_pub, - &rpc.reserve_sig)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (rpc.payto_uri); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_RESERVES_RESERVE_MERGE_SIGNATURE_INVALID, - NULL); - } - if ( (! rpc.no_econtract) && - (GNUNET_OK != - TALER_wallet_econtract_upload_verify (rpc.econtract.econtract, - rpc.econtract.econtract_size, - &rpc.econtract.contract_pub, - &rpc.pd.purse_pub, - &rpc.econtract.econtract_sig)) ) - { - TALER_LOG_WARNING ("Invalid signature on /reserves/$PID/purse request\n"); - GNUNET_JSON_parse_free (spec); - GNUNET_free (rpc.payto_uri); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID, - NULL); - } - - if (GNUNET_SYSERR == - TEH_plugin->preflight (TEH_plugin->cls)) + rpc->lch = TEH_legitimization_check ( + &rpc->rc->async_scope_id, + TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE, + rpc->payto_uri, + &rpc->h_payto, + &rpc->account_pub, + &amount_iterator, + rpc, + &reserve_purse_legi_cb, + rpc); + GNUNET_assert (NULL != rpc->lch); + MHD_suspend_connection (rc->connection); + GNUNET_CONTAINER_DLL_insert (rpc_head, + rpc_tail, + rpc); + return MHD_YES; + } + if (NULL != rpc->response) + return MHD_queue_response (connection, + rpc->http_status, + rpc->response); + if (! rpc->kyc.ok) { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (rpc.payto_uri); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - "preflight failure"); + if (0 == rpc->kyc.requirement_row) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "requirement row not set"); + } + return TEH_RESPONSE_reply_kyc_required (connection, + &rpc->h_payto, + &rpc->kyc); } - /* execute transaction */ { MHD_RESULT mhd_ret; if (GNUNET_OK != TEH_DB_run_transaction (connection, - "execute purse purse", + "execute reserve purse", TEH_MT_REQUEST_RESERVE_PURSE, &mhd_ret, &purse_transaction, - &rpc)) + rpc)) { - GNUNET_JSON_parse_free (spec); - GNUNET_free (rpc.payto_uri); return mhd_ret; } } - GNUNET_free (rpc.payto_uri); - if (! rpc.kyc.ok) - { - GNUNET_JSON_parse_free (spec); - return TEH_RESPONSE_reply_kyc_required (connection, - &rpc.h_payto, - &rpc.kyc); - } /* generate regular response */ { MHD_RESULT res; - res = TEH_RESPONSE_reply_purse_created (connection, - rpc.exchange_timestamp, - &rpc.deposit_total, - &rpc.pd); - GNUNET_JSON_parse_free (spec); + res = TEH_RESPONSE_reply_purse_created ( + connection, + rpc->exchange_timestamp, + &rpc->deposit_total, + &rpc->pd); return res; } } diff --git a/src/exchange/taler-exchange-httpd_reserves_purse.h b/src/exchange/taler-exchange-httpd_reserves_purse.h index 017e357d2..1c19f5e51 100644 --- a/src/exchange/taler-exchange-httpd_reserves_purse.h +++ b/src/exchange/taler-exchange-httpd_reserves_purse.h @@ -27,6 +27,13 @@ /** + * Resume suspended connections, we are shutting down. + */ +void +TEH_reserves_purse_cleanup (void); + + +/** * Handle a "/reserves/$RESERVE_PUB/purse" request. Parses the JSON, and, if * successful, passes the JSON data to #create_transaction() to further check * the details of the operation specified. If everything checks out, this diff --git a/src/exchangedb/pg_select_reserve_close_info.c b/src/exchangedb/pg_select_reserve_close_info.c index eccba8e4c..fff07944b 100644 --- a/src/exchangedb/pg_select_reserve_close_info.c +++ b/src/exchangedb/pg_select_reserve_close_info.c @@ -53,11 +53,14 @@ TEH_PG_select_reserve_close_info ( " r.current_balance" ",wt.payto_uri" " FROM reserves r" - " LEFT JOIN reserves_in ri USING (reserve_pub)" - " LEFT JOIN wire_targets wt ON (ri.wire_source_h_payto = wt.wire_target_h_payto)" + " LEFT JOIN reserves_in ri" + " USING (reserve_pub)" + " LEFT JOIN wire_targets wt" + " ON (ri.wire_source_h_payto = wt.wire_target_h_payto)" " WHERE reserve_pub=$1;"); - return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "select_reserve_close_info", - params, - rs); + return GNUNET_PQ_eval_prepared_singleton_select ( + pg->conn, + "select_reserve_close_info", + params, + rs); } diff --git a/src/include/taler_kyclogic_lib.h b/src/include/taler_kyclogic_lib.h index c04553ea5..987cee4e2 100644 --- a/src/include/taler_kyclogic_lib.h +++ b/src/include/taler_kyclogic_lib.h @@ -691,6 +691,20 @@ TALER_KYCLOGIC_get_measure_configuration ( /** + * Check if there is a measure triggered by the + * KYC rule @a r that has a check name of "SKIP" and + * thus should be immediately executed. If such a + * measure exists, return it. + * + * @param r rule to check for instant measures + * @return NULL if there is no instant measure + */ +const struct TALER_KYCLOGIC_Measure * +TALER_KYCLOGIC_rule_get_instant_measure ( + const struct TALER_KYCLOGIC_KycRule *r); + + +/** * Handle to manage a running AML program. */ struct TALER_KYCLOGIC_AmlProgramRunnerHandle; @@ -847,6 +861,30 @@ TALER_KYCLOGIC_run_aml_program2 ( /** + * Run AML program specified by the given + * measure. + * + * @param measure measure with program name and context + * to run + * @param attributes attributes to run with + * @param aml_history AML history of the account + * @param kyc_history KYC history of the account + * @param aprc function to call with the result + * @param aprc_cls closure for @a aprc + * @return NULL if @a jmeasures is invalid for the + * selected @a measure_index or @a attributes + */ +struct TALER_KYCLOGIC_AmlProgramRunnerHandle * +TALER_KYCLOGIC_run_aml_program3 ( + const struct TALER_KYCLOGIC_Measure *measure, + const json_t *attributes, + const json_t *aml_history, + const json_t *kyc_history, + TALER_KYCLOGIC_AmlProgramResultCallback aprc, + void *aprc_cls); + + +/** * Cancel running AML program. * * @param[in] aprh handle of program to cancel diff --git a/src/kyclogic/kyclogic_api.c b/src/kyclogic/kyclogic_api.c index d29a6ac34..af801a55c 100644 --- a/src/kyclogic/kyclogic_api.c +++ b/src/kyclogic/kyclogic_api.c @@ -885,6 +885,35 @@ find_measure (const struct TALER_KYCLOGIC_LegitimizationRuleSet *lrs, } +const struct TALER_KYCLOGIC_Measure * +TALER_KYCLOGIC_rule_get_instant_measure ( + const struct TALER_KYCLOGIC_KycRule *r) +{ + const struct TALER_KYCLOGIC_LegitimizationRuleSet *lrs + = r->lrs; + + if (r->verboten) + return NULL; + for (unsigned int i = 0; i<r->num_measures; i++) + { + const char *measure_name = r->next_measures[i]; + const struct TALER_KYCLOGIC_Measure *ms; + + ms = find_measure (lrs, + measure_name); + if (NULL == ms) + { + GNUNET_break (0); + return NULL; + } + if (0 == strcasecmp (ms->check_name, + "SKIP")) + return ms; + } + return NULL; +} + + json_t * TALER_KYCLOGIC_rule_to_measures (const struct TALER_KYCLOGIC_KycRule *r) { @@ -3463,6 +3492,26 @@ TALER_KYCLOGIC_run_aml_program2 ( } +struct TALER_KYCLOGIC_AmlProgramRunnerHandle * +TALER_KYCLOGIC_run_aml_program3 ( + const struct TALER_KYCLOGIC_Measure *measure, + const json_t *attributes, + const json_t *aml_history, + const json_t *kyc_history, + TALER_KYCLOGIC_AmlProgramResultCallback aprc, + void *aprc_cls) +{ + return TALER_KYCLOGIC_run_aml_program2 ( + measure->prog_name, + attributes, + aml_history, + kyc_history, + measure->context, + aprc, + aprc_cls); +} + + void TALER_KYCLOGIC_run_aml_program_cancel ( struct TALER_KYCLOGIC_AmlProgramRunnerHandle *aprh) |