/* This file is part of TALER (C) 2014-2024 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * @file taler-merchant-httpd_post-orders-ID-pay.c * @brief handling of POST /orders/$ID/pay requests * @author Marcello Stanisci * @author Christian Grothoff * @author Florian Dold */ #include "platform.h" #include #include #include #include #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_post-orders-ID-pay.h" #include "taler-merchant-httpd_private-get-orders.h" /** * How often do we retry the (complex!) database transaction? */ #define MAX_RETRIES 5 /** * Maximum number of coins that we allow per transaction */ #define MAX_COIN_ALLOWED_COINS 1024 /** * How often do we ask the exchange again about our * KYC status? Very rarely, as if the user actively * changes it, we should usually notice anyway. */ #define KYC_RETRY_FREQUENCY GNUNET_TIME_UNIT_WEEKS /** * Information we keep for an individual call to the pay handler. */ struct PayContext; /** * Different phases of processing the /pay request. */ enum PayPhase { /** * Initial phase where the request is parsed. */ PP_INIT = 0, /** * Check database state for the given order. */ PP_CHECK_CONTRACT, /** * Contract has been paid. */ PP_CONTRACT_PAID, /** * Execute payment transaction. */ PP_PAY_TRANSACTION, /** * Notify other processes about successful payment. */ PP_PAYMENT_NOTIFICATION, /** * Create final success response. */ PP_SUCCESS_RESPONSE, /** * Perform batch deposits with exchange(s). */ PP_BATCH_DEPOSITS, /** * Return response in payment context. */ PP_RETURN_RESPONSE, /** * Return #MHD_YES to end processing. */ PP_END_YES, /** * Return #MHD_NO to end processing. */ PP_END_NO }; /** * Information kept during a pay request for each coin. */ struct DepositConfirmation { /** * Reference to the main PayContext */ struct PayContext *pc; /** * URL of the exchange that issued this coin. */ char *exchange_url; /** * Details about the coin being deposited. */ struct TALER_EXCHANGE_CoinDepositDetail cdd; /** * Fee charged by the exchange for the deposit operation of this coin. */ struct TALER_Amount deposit_fee; /** * Fee charged by the exchange for the refund operation of this coin. */ struct TALER_Amount refund_fee; /** * If a minimum age was required (i. e. pc->minimum_age is large enough), * this is the signature of the minimum age (as a single uint8_t), using the * private key to the corresponding age group. Might be all zeroes for no * age attestation. */ struct TALER_AgeAttestation minimum_age_sig; /** * If a minimum age was required (i. e. pc->minimum_age is large enough), * this is the age commitment (i. e. age mask and vector of EdDSA public * keys, one per age group) that went into the mining of the coin. The * SHA256 hash of the mask and the vector of public keys was bound to the * key. */ struct TALER_AgeCommitment age_commitment; /** * Age mask in the denomination that defines the age groups. Only * applicable, if minimum age was required. */ struct TALER_AgeMask age_mask; /** * Offset of this coin into the `dc` array of all coins in the * @e pc. */ unsigned int index; /** * true, if no field "age_commitment" was found in the JSON blob */ bool no_age_commitment; /** * True, if no field "minimum_age_sig" was found in the JSON blob */ bool no_minimum_age_sig; /** * true, if no field "h_age_commitment" was found in the JSON blob */ bool no_h_age_commitment; /** * true if we found this coin in the database. */ bool found_in_db; /** * true if we #deposit_paid_check() matched this coin in the database. */ bool matched_in_db; }; /** * Information kept during a pay request for each exchange. */ struct ExchangeGroup { /** * Payment context this group is part of. */ struct PayContext *pc; /** * Handle to the batch deposit operation we are performing for this * exchange, NULL after the operation is done. */ struct TALER_EXCHANGE_BatchDepositHandle *bdh; /** * Handle for operation to lookup /keys (and auditors) from * the exchange used for this transaction; NULL if no operation is * pending. */ struct TMH_EXCHANGES_KeysOperation *fo; /** * URL of the exchange that issued this coin. Aliases * the exchange URL of one of the coins, do not free! */ const char *exchange_url; /** * Wire fee that applies to this exchange for the * given payment context's wire method. */ struct TALER_Amount wire_fee; /** * true if we already tried a forced /keys download. */ bool tried_force_keys; }; /** * Information we keep for an individual call to the /pay handler. */ struct PayContext { /** * Stored in a DLL. */ struct PayContext *next; /** * Stored in a DLL. */ struct PayContext *prev; /** * Array with @e num_exchange exchanges we are depositing * coins into. */ struct ExchangeGroup **egs; /** * Array with @e coins_cnt coins we are despositing. */ struct DepositConfirmation *dc; /** * MHD connection to return to */ struct MHD_Connection *connection; /** * Details about the client's request. */ struct TMH_HandlerContext *hc; /** * What wire method (of the @e mi) was selected by the wallet? * Set in #phase_parse_pay(). */ struct TMH_WireMethod *wm; /** * Task called when the (suspended) processing for * the /pay request times out. * Happens when we don't get a response from the exchange. */ struct GNUNET_SCHEDULER_Task *timeout_task; /** * Response to return, NULL if we don't have one yet. */ struct MHD_Response *response; /** * Our contract (or NULL if not available). */ json_t *contract_terms; /** * Placeholder for #TALER_MHD_parse_post_json() to keep its internal state. */ void *json_parse_context; /** * Optional session id given in @e root. * NULL if not given. */ char *session_id; /** * Transaction ID given in @e root. */ const char *order_id; /** * Fulfillment URL from the contract, or NULL if we don't have one. */ char *fulfillment_url; /** * Serial number of this order in the database (set once we did the lookup). */ uint64_t order_serial; /** * Hashed proposal. */ struct TALER_PrivateContractHashP h_contract_terms; /** * "h_wire" from @e contract_terms. Used to identify * the instance's wire transfer method. */ struct TALER_MerchantWireHashP h_wire; /** * Maximum fee the merchant is willing to pay, from @e root. * Note that IF the total fee of the exchange is higher, that is * acceptable to the merchant if the customer is willing to * pay the difference * (i.e. amount - max_fee <= actual_amount - actual_fee). */ struct TALER_Amount max_fee; /** * Amount from @e root. This is the amount the merchant expects * to make, minus @e max_fee. */ struct TALER_Amount amount; /** * Considering all the coins with the "found_in_db" flag * set, what is the total amount we were so far paid on * this contract? */ struct TALER_Amount total_paid; /** * Considering all the coins with the "found_in_db" flag * set, what is the total amount we had to pay in deposit * fees so far on this contract? */ struct TALER_Amount total_fees_paid; /** * Considering all the coins with the "found_in_db" flag * set, what is the total amount we already refunded? */ struct TALER_Amount total_refunded; /** * Wire transfer deadline. How soon would the merchant like the * wire transfer to be executed? */ struct GNUNET_TIME_Timestamp wire_transfer_deadline; /** * Timestamp from @e contract_terms. */ struct GNUNET_TIME_Timestamp timestamp; /** * Refund deadline from @e contract_terms. */ struct GNUNET_TIME_Timestamp refund_deadline; /** * Deadline for the customer to pay for this proposal. */ struct GNUNET_TIME_Timestamp pay_deadline; /** * Set to the POS key, if applicable for this order. */ char *pos_key; /** * Algorithm chosen for generating the confirmation code. */ enum TALER_MerchantConfirmationAlgorithm pos_alg; /** * Minimum age required for this purchase. */ unsigned int minimum_age; /** * Number of coins this payment is made of. Length * of the @e dc array. */ size_t coins_cnt; /** * Number of exchanges involved in the payment. Length * of the @e eg array. */ unsigned int num_exchanges; /** * How often have we retried the 'main' transaction? */ unsigned int retry_counter; /** * Number of batch transactions pending. */ unsigned int pending_at_eg; /** * Number of coin deposits pending. */ unsigned int pending; /** * HTTP status code to use for the reply, i.e 200 for "OK". * Special value UINT_MAX is used to indicate hard errors * (no reply, return #MHD_NO). */ unsigned int response_code; /** * Payment processing phase we are in. */ enum PayPhase phase; /** * #GNUNET_NO if the @e connection was not suspended, * #GNUNET_YES if the @e connection was suspended, * #GNUNET_SYSERR if @e connection was resumed to as * part of #MH_force_pc_resume during shutdown. */ enum GNUNET_GenericReturnValue suspended; /** * Set to true if the deposit currency of a coin * does not match the contract currency. */ bool deposit_currency_mismatch; /** * Set to true if the database contains a (bogus) * refund for a different currency. */ bool refund_currency_mismatch; }; /** * Head of active pay context DLL. */ static struct PayContext *pc_head; /** * Tail of active pay context DLL. */ static struct PayContext *pc_tail; void TMH_force_pc_resume () { for (struct PayContext *pc = pc_head; NULL != pc; pc = pc->next) { if (NULL != pc->timeout_task) { GNUNET_SCHEDULER_cancel (pc->timeout_task); pc->timeout_task = NULL; } if (GNUNET_YES == pc->suspended) { pc->suspended = GNUNET_SYSERR; MHD_resume_connection (pc->connection); } } } /** * Resume payment processing. * * @param[in,out] pc payment process to resume */ static void pay_resume (struct PayContext *pc) { GNUNET_assert (GNUNET_YES == pc->suspended); pc->suspended = GNUNET_NO; MHD_resume_connection (pc->connection); TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ } /** * Resume the given pay context and send the given response. * Stores the response in the @a pc and signals MHD to resume * the connection. Also ensures MHD runs immediately. * * @param pc payment context * @param response_code response code to use * @param response response data to send back */ static void resume_pay_with_response (struct PayContext *pc, unsigned int response_code, struct MHD_Response *response) { pc->response_code = response_code; pc->response = response; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming /pay handling. HTTP status for our reply is %u.\n", response_code); for (unsigned int i = 0; inum_exchanges; i++) { struct ExchangeGroup *eg = pc->egs[i]; if (NULL != eg->fo) { TMH_EXCHANGES_keys4exchange_cancel (eg->fo); eg->fo = NULL; pc->pending_at_eg--; } if (NULL != eg->bdh) { TALER_EXCHANGE_batch_deposit_cancel (eg->bdh); eg->bdh = NULL; pc->pending_at_eg--; } } GNUNET_assert (0 == pc->pending_at_eg); if (NULL != pc->timeout_task) { GNUNET_SCHEDULER_cancel (pc->timeout_task); pc->timeout_task = NULL; } pc->phase = PP_RETURN_RESPONSE; pay_resume (pc); } /** * Resume payment processing with an error. * * @param pc operation to resume * @param ec taler error code to return * @param msg human readable error message */ static void resume_pay_with_error (struct PayContext *pc, enum TALER_ErrorCode ec, const char *msg) { resume_pay_with_response ( pc, TALER_ErrorCode_get_http_status_safe (ec), TALER_MHD_make_error (ec, msg)); } /** * Conclude payment processing for @a pc with the * given @a res MHD status code. * * @param[in,out] pc payment context for final state transition * @param res MHD return code to end with */ static void pay_end (struct PayContext *pc, MHD_RESULT res) { pc->phase = (MHD_YES == res) ? PP_END_YES : PP_END_NO; } /** * Return response stored in @a pc. * * @param[in,out] pc payment context we are processing */ static void phase_return_response (struct PayContext *pc) { GNUNET_assert (0 != pc->response_code); /* We are *done* processing the request, just queue the response (!) */ if (UINT_MAX == pc->response_code) { GNUNET_break (0); pay_end (pc, MHD_NO); /* hard error */ return; } pay_end (pc, MHD_queue_response (pc->connection, pc->response_code, pc->response)); } /** * Do database transaction for a completed batch deposit. * * @param eg group that completed * @param dr response from the server * @return transaction status */ static enum GNUNET_DB_QueryStatus batch_deposit_transaction (const struct ExchangeGroup *eg, const struct TALER_EXCHANGE_BatchDepositResult *dr) { const struct PayContext *pc = eg->pc; enum GNUNET_DB_QueryStatus qs; struct TALER_Amount total_without_fees; uint64_t b_dep_serial; uint32_t off = 0; GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (pc->amount.currency, &total_without_fees)); for (size_t i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; struct TALER_Amount amount_without_fees; /* might want to group deposits by batch more explicitly ... */ if (0 != strcmp (eg->exchange_url, dc->exchange_url)) continue; if (dc->found_in_db) continue; GNUNET_assert (0 <= TALER_amount_subtract (&amount_without_fees, &dc->cdd.amount, &dc->deposit_fee)); GNUNET_assert (0 <= TALER_amount_add (&total_without_fees, &total_without_fees, &amount_without_fees)); } qs = TMH_db->insert_deposit_confirmation ( TMH_db->cls, pc->hc->instance->settings.id, dr->details.ok.deposit_timestamp, &pc->h_contract_terms, eg->exchange_url, pc->wire_transfer_deadline, &total_without_fees, &eg->wire_fee, &pc->wm->h_wire, dr->details.ok.exchange_sig, dr->details.ok.exchange_pub, &b_dep_serial); if (qs <= 0) return qs; /* Entire batch already known or failure, we're done */ for (size_t i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; /* might want to group deposits by batch more explicitly ... */ if (0 != strcmp (eg->exchange_url, dc->exchange_url)) continue; if (dc->found_in_db) continue; /* NOTE: We might want to check if the order was fully paid concurrently by some other wallet here, and if so, issue an auto-refund. Right now, it is possible to over-pay if two wallets literally make a concurrent payment, as the earlier check for 'paid' is not in the same transaction scope as this 'insert' operation. */ qs = TMH_db->insert_deposit ( TMH_db->cls, off++, /* might want to group deposits by batch more explicitly ... */ b_dep_serial, &dc->cdd.coin_pub, &dc->cdd.coin_sig, &dc->cdd.amount, &dc->deposit_fee, &dc->refund_fee); if (qs < 0) return qs; GNUNET_break (qs > 0); } return qs; } /** * Handle case where the batch deposit completed * with a status of #MHD_HTTP_OK. * * @param eg group that completed * @param dr response from the server */ static void handle_batch_deposit_ok (struct ExchangeGroup *eg, const struct TALER_EXCHANGE_BatchDepositResult *dr) { struct PayContext *pc = eg->pc; enum GNUNET_DB_QueryStatus qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; /* store result to DB */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Storing successful payment %s (%s) at instance `%s'\n", pc->hc->infix, GNUNET_h2s (&pc->h_contract_terms.hash), pc->hc->instance->settings.id); for (unsigned int r = 0; rpreflight (TMH_db->cls); if (GNUNET_OK != TMH_db->start (TMH_db->cls, "batch-deposit-insert-confirmation")) { resume_pay_with_response ( pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_GENERIC_DB_START_FAILED), TMH_pack_exchange_reply (&dr->hr))); return; } qs = batch_deposit_transaction (eg, dr); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { TMH_db->rollback (TMH_db->cls); continue; } if (GNUNET_DB_STATUS_HARD_ERROR == qs) { GNUNET_break (0); resume_pay_with_error (pc, TALER_EC_GENERIC_DB_COMMIT_FAILED, "batch_deposit_transaction"); } qs = TMH_db->commit (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { TMH_db->rollback (TMH_db->cls); continue; } if (GNUNET_DB_STATUS_HARD_ERROR == qs) { GNUNET_break (0); resume_pay_with_error (pc, TALER_EC_GENERIC_DB_COMMIT_FAILED, "insert_deposit"); } break; /* DB transaction succeeded */ } if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { resume_pay_with_error (pc, TALER_EC_GENERIC_DB_SOFT_FAILURE, "insert_deposit"); return; } /* Transaction is done, mark affected coins as complete as well. */ for (size_t i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; if (0 != strcmp (eg->exchange_url, pc->dc[i].exchange_url)) continue; if (dc->found_in_db) continue; dc->found_in_db = true; /* well, at least NOW it'd be true ;-) */ pc->pending--; } } /** * Callback to handle a batch deposit permission's response. * * @param cls a `struct ExchangeGroup` * @param dr HTTP response code details */ static void batch_deposit_cb ( void *cls, const struct TALER_EXCHANGE_BatchDepositResult *dr) { struct ExchangeGroup *eg = cls; struct PayContext *pc = eg->pc; eg->bdh = NULL; pc->pending_at_eg--; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Batch deposit completed with status %u\n", dr->hr.http_status); GNUNET_assert (GNUNET_YES == pc->suspended); switch (dr->hr.http_status) { case MHD_HTTP_OK: handle_batch_deposit_ok (eg, dr); if (0 == pc->pending_at_eg) { pc->phase = PP_PAY_TRANSACTION; pay_resume (pc); } return; default: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Deposit operation failed with HTTP code %u/%d\n", dr->hr.http_status, (int) dr->hr.ec); /* Transaction failed */ if (5 == dr->hr.http_status / 100) { /* internal server error at exchange */ resume_pay_with_response (pc, MHD_HTTP_BAD_GATEWAY, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS), TMH_pack_exchange_reply (&dr->hr))); return; } if (NULL == dr->hr.reply) { /* We can't do anything meaningful here, the exchange did something wrong */ resume_pay_with_response ( pc, MHD_HTTP_BAD_GATEWAY, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED), TMH_pack_exchange_reply (&dr->hr))); return; } /* Forward error, adding the "exchange_url" for which the error was being generated */ if (TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS == dr->hr.ec) { resume_pay_with_response ( pc, MHD_HTTP_CONFLICT, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS), TMH_pack_exchange_reply (&dr->hr), GNUNET_JSON_pack_string ("exchange_url", eg->exchange_url))); return; } resume_pay_with_response ( pc, MHD_HTTP_BAD_GATEWAY, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS), TMH_pack_exchange_reply (&dr->hr), GNUNET_JSON_pack_string ("exchange_url", eg->exchange_url))); return; } /* end switch */ } /** * Force re-downloading keys for @a eg. * * @param[in,out] eg group to re-download keys for */ static void force_keys (struct ExchangeGroup *eg); /** * Function called with the result of our exchange keys lookup. * * @param cls the `struct ExchangeGroup` * @param keys the keys of the exchange * @param exchange representation of the exchange */ static void process_pay_with_keys ( void *cls, struct TALER_EXCHANGE_Keys *keys, struct TMH_Exchange *exchange) { struct ExchangeGroup *eg = cls; struct PayContext *pc = eg->pc; struct TMH_HandlerContext *hc = pc->hc; unsigned int group_size; eg->fo = NULL; pc->pending_at_eg--; GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Processing payment with exchange %s\n", eg->exchange_url); GNUNET_assert (GNUNET_YES == pc->suspended); if (NULL == keys) { GNUNET_break_op (0); resume_pay_with_error ( pc, TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, NULL); return; } if (GNUNET_OK != TMH_exchange_check_debit (exchange, pc->wm)) { if (eg->tried_force_keys) { GNUNET_break_op (0); resume_pay_with_error ( pc, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED, NULL); return; } force_keys (eg); return; } if (GNUNET_OK != TMH_EXCHANGES_lookup_wire_fee (exchange, pc->wm->wire_method, &eg->wire_fee)) { if (eg->tried_force_keys) { GNUNET_break_op (0); resume_pay_with_error ( pc, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED, pc->wm->wire_method); return; } force_keys (eg); return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Got wire data for %s\n", eg->exchange_url); /* Initiate /batch-deposit operation for all coins of the current exchange (!) */ group_size = 0; for (size_t i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; const struct TALER_EXCHANGE_DenomPublicKey *denom_details; bool is_age_restricted_denom = false; if (0 != strcmp (eg->exchange_url, pc->dc[i].exchange_url)) continue; if (dc->found_in_db) continue; denom_details = TALER_EXCHANGE_get_denomination_key_by_hash (keys, &dc->cdd.h_denom_pub); if (NULL == denom_details) { if (eg->tried_force_keys) { GNUNET_break_op (0); resume_pay_with_response ( pc, MHD_HTTP_BAD_REQUEST, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND), GNUNET_JSON_pack_data_auto ("h_denom_pub", &dc->cdd.h_denom_pub), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_steal ( "exchange_keys", TALER_EXCHANGE_keys_to_json (keys))))); return; } force_keys (eg); return; } dc->deposit_fee = denom_details->fees.deposit; dc->refund_fee = denom_details->fees.refund; if (GNUNET_TIME_absolute_is_past ( denom_details->expire_deposit.abs_time)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Denomination key offered by client has expired for deposits\n"); resume_pay_with_response ( pc, MHD_HTTP_GONE, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_DEPOSIT_EXPIRED), GNUNET_JSON_pack_data_auto ("h_denom_pub", &denom_details->h_key))); return; } /* Now that we have the details about the denomination, we can verify age * restriction requirements, if applicable. Note that denominations with an * age_mask equal to zero always pass the age verification. */ is_age_restricted_denom = (0 != denom_details->key.age_mask.bits); if (is_age_restricted_denom && (0 < pc->minimum_age)) { /* Minimum age given and restricted coin provided: We need to verify the * minimum age */ unsigned int code = 0; if (dc->no_age_commitment) { GNUNET_break_op (0); code = TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_MISSING; goto AGE_FAIL; } dc->age_commitment.mask = denom_details->key.age_mask; if (((int) (dc->age_commitment.num + 1)) != __builtin_popcount (dc->age_commitment.mask.bits)) { GNUNET_break_op (0); code = TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_SIZE_MISMATCH; goto AGE_FAIL; } if (GNUNET_OK != TALER_age_commitment_verify ( &dc->age_commitment, pc->minimum_age, &dc->minimum_age_sig)) code = TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED; AGE_FAIL: if (0 < code) { GNUNET_break_op (0); GNUNET_free (dc->age_commitment.keys); resume_pay_with_response ( pc, MHD_HTTP_BAD_REQUEST, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec (code), GNUNET_JSON_pack_data_auto ("h_denom_pub", &denom_details->h_key))); return; } /* Age restriction successfully verified! * Calculate the hash of the age commitment. */ TALER_age_commitment_hash (&dc->age_commitment, &dc->cdd.h_age_commitment); GNUNET_free (dc->age_commitment.keys); } else if (is_age_restricted_denom && dc->no_h_age_commitment) { /* The contract did not ask for a minimum_age but the client paid * with a coin that has age restriction enabled. We lack the hash * of the age commitment in this case in order to verify the coin * and to deposit it with the exchange. */ GNUNET_break_op (0); resume_pay_with_response ( pc, MHD_HTTP_BAD_REQUEST, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_HASH_MISSING), GNUNET_JSON_pack_data_auto ("h_denom_pub", &denom_details->h_key))); return; } group_size++; } if (0 == group_size) { GNUNET_break (0); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Group size zero, %u batch transactions remain pending\n", pc->pending_at_eg); if (0 == pc->pending_at_eg) { pc->phase = PP_PAY_TRANSACTION; pay_resume (pc); return; } return; } { struct TALER_EXCHANGE_CoinDepositDetail cdds[group_size]; struct TALER_EXCHANGE_DepositContractDetail dcd = { .wire_deadline = pc->wire_transfer_deadline, .merchant_payto_uri = pc->wm->payto_uri, .wire_salt = pc->wm->wire_salt, .h_contract_terms = pc->h_contract_terms, .wallet_timestamp = pc->timestamp, .merchant_pub = hc->instance->merchant_pub, .refund_deadline = pc->refund_deadline }; enum TALER_ErrorCode ec; size_t off = 0; for (size_t i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; if (dc->found_in_db) continue; if (0 != strcmp (dc->exchange_url, eg->exchange_url)) continue; GNUNET_assert (off < group_size); cdds[off++] = dc->cdd; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Initiating batch deposit with %u coins\n", group_size); eg->bdh = TALER_EXCHANGE_batch_deposit ( TMH_curl_ctx, eg->exchange_url, keys, &dcd, group_size, cdds, &batch_deposit_cb, eg, &ec); if (NULL == eg->bdh) { /* Signature was invalid or some other constraint was not satisfied. If the exchange was unavailable, we'd get that information in the callback. */ GNUNET_break_op (0); resume_pay_with_response ( pc, TALER_ErrorCode_get_http_status_safe (ec), TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec (ec), GNUNET_JSON_pack_string ("exchange_url", eg->exchange_url))); return; } pc->pending_at_eg++; if (TMH_force_audit) TALER_EXCHANGE_batch_deposit_force_dc (eg->bdh); } } static void force_keys (struct ExchangeGroup *eg) { struct PayContext *pc = eg->pc; eg->tried_force_keys = true; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Forcing /keys download (once) as wire fees are unknown\n"); eg->fo = TMH_EXCHANGES_keys4exchange ( eg->exchange_url, true, &process_pay_with_keys, eg); if (NULL == eg->fo) { GNUNET_break (0); resume_pay_with_error (pc, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED, "Failed to lookup exchange by URL"); return; } pc->pending_at_eg++; } /** * Handle a timeout for the processing of the pay request. * * @param cls our `struct PayContext` */ static void handle_pay_timeout (void *cls) { struct PayContext *pc = cls; pc->timeout_task = NULL; GNUNET_assert (GNUNET_YES == pc->suspended); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Resuming pay with error after timeout\n"); resume_pay_with_error (pc, TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, NULL); } /** * Compute the timeout for a /pay request based on the number of coins * involved. * * @param num_coins number of coins * @returns timeout for the /pay request */ static struct GNUNET_TIME_Relative get_pay_timeout (unsigned int num_coins) { struct GNUNET_TIME_Relative t; /* FIXME-Performance-Optimization: Do some benchmarking to come up with a * better timeout. We've increased this value so the wallet integration * test passes again on my (Florian) machine. */ t = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 15 * (1 + (num_coins / 5))); return t; } /** * Start batch deposits for all exchanges involved * in this payment. * * @param[in,out] pc payment context we are processing */ static void phase_batch_deposits (struct PayContext *pc) { for (unsigned int i = 0; inum_exchanges; i++) { struct ExchangeGroup *eg = pc->egs[i]; bool have_coins = false; for (size_t j = 0; jcoins_cnt; j++) { struct DepositConfirmation *dc = &pc->dc[j]; if (0 != strcmp (eg->exchange_url, pc->dc[j].exchange_url)) continue; if (dc->found_in_db) continue; have_coins = true; break; } if (! have_coins) continue; /* no coins left to deposit at this exchange */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Getting /keys for %s\n", eg->exchange_url); eg->fo = TMH_EXCHANGES_keys4exchange ( eg->exchange_url, false, &process_pay_with_keys, eg); if (NULL == eg->fo) { GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED, "Failed to lookup exchange by URL")); return; } pc->pending_at_eg++; } if (0 == pc->pending_at_eg) { pc->phase = PP_PAY_TRANSACTION; pay_resume (pc); return; } /* Suspend while we interact with the exchange */ MHD_suspend_connection (pc->connection); pc->suspended = GNUNET_YES; GNUNET_assert (NULL == pc->timeout_task); pc->timeout_task = GNUNET_SCHEDULER_add_delayed (get_pay_timeout (pc->coins_cnt), &handle_pay_timeout, pc); } /** * Generate response (payment successful) * * @param[in,out] pc payment context where the payment was successful */ static void phase_success_response (struct PayContext *pc) { struct TALER_MerchantSignatureP sig; char *pos_confirmation; /* Sign on our end (as the payment did go through, even if it may have been refunded already) */ TALER_merchant_pay_sign (&pc->h_contract_terms, &pc->hc->instance->merchant_priv, &sig); /* Build the response */ pos_confirmation = (NULL == pc->pos_key) ? NULL : TALER_build_pos_confirmation (pc->pos_key, pc->pos_alg, &pc->amount, pc->timestamp); pay_end (pc, TALER_MHD_REPLY_JSON_PACK ( pc->connection, MHD_HTTP_OK, GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("pos_confirmation", pos_confirmation)), GNUNET_JSON_pack_data_auto ("sig", &sig))); GNUNET_free (pos_confirmation); } /** * Use database to notify other clients about the * payment being completed. * * @param[in,out] pc context to trigger notification for */ static void phase_payment_notification (struct PayContext *pc) { { struct TMH_OrderPayEventP pay_eh = { .header.size = htons (sizeof (pay_eh)), .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID), .merchant_pub = pc->hc->instance->merchant_pub }; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Notifying clients about payment of order %s\n", pc->order_id); GNUNET_CRYPTO_hash (pc->order_id, strlen (pc->order_id), &pay_eh.h_order_id); TMH_db->event_notify (TMH_db->cls, &pay_eh.header, NULL, 0); } if ( (NULL != pc->session_id) && (NULL != pc->fulfillment_url) ) { struct TMH_SessionEventP session_eh = { .header.size = htons (sizeof (session_eh)), .header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED), .merchant_pub = pc->hc->instance->merchant_pub }; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Notifying clients about session change to %s for %s\n", pc->session_id, pc->fulfillment_url); GNUNET_CRYPTO_hash (pc->session_id, strlen (pc->session_id), &session_eh.h_session_id); GNUNET_CRYPTO_hash (pc->fulfillment_url, strlen (pc->fulfillment_url), &session_eh.h_fulfillment_url); TMH_db->event_notify (TMH_db->cls, &session_eh.header, NULL, 0); } pc->phase = PP_SUCCESS_RESPONSE; } /** * Function called with information about a coin that was deposited. * * @param cls closure * @param exchange_url exchange where @a coin_pub was deposited * @param coin_pub public key of the coin * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin * @param refund_fee fee the exchange will charge for refunding this coin */ static void check_coin_paid (void *cls, const char *exchange_url, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, const struct TALER_Amount *refund_fee) { struct PayContext *pc = cls; for (size_t i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; if (dc->found_in_db) continue; /* processed earlier, skip "expensive" memcmp() */ /* Get matching coin from results*/ if ( (0 != GNUNET_memcmp (coin_pub, &dc->cdd.coin_pub)) || (0 != strcmp (exchange_url, dc->exchange_url)) || (GNUNET_OK != TALER_amount_cmp_currency (amount_with_fee, &dc->cdd.amount)) || (0 != TALER_amount_cmp (amount_with_fee, &dc->cdd.amount)) ) continue; /* does not match, skip */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Deposit of coin `%s' already in our DB.\n", TALER_B2S (coin_pub)); if ( (GNUNET_OK != TALER_amount_cmp_currency (&pc->total_paid, amount_with_fee)) || (GNUNET_OK != TALER_amount_cmp_currency (&pc->total_fees_paid, deposit_fee)) ) { GNUNET_break_op (0); pc->deposit_currency_mismatch = true; break; } GNUNET_assert (0 <= TALER_amount_add (&pc->total_paid, &pc->total_paid, amount_with_fee)); GNUNET_assert (0 <= TALER_amount_add (&pc->total_fees_paid, &pc->total_fees_paid, deposit_fee)); dc->deposit_fee = *deposit_fee; dc->refund_fee = *refund_fee; dc->cdd.amount = *amount_with_fee; dc->found_in_db = true; pc->pending--; } } /** * Function called with information about a refund. Check if this coin was * claimed by the wallet for the transaction, and if so add the refunded * amount to the pc's "total_refunded" amount. * * @param cls closure with a `struct PayContext` * @param coin_pub public coin from which the refund comes from * @param refund_amount refund amount which is being taken from @a coin_pub */ static void check_coin_refunded (void *cls, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_Amount *refund_amount) { struct PayContext *pc = cls; /* We look at refunds here that apply to the coins that the customer is currently trying to pay us with. Such refunds are not "normal" refunds, but abort-pay refunds, which are given in the case that the wallet aborts the payment. In the case the wallet then decides to complete the payment *after* doing an abort-pay refund (an unusual but possible case), we need to make sure that existing refunds are accounted for. */ for (size_t i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; /* Get matching coins from results. */ if (0 != GNUNET_memcmp (coin_pub, &dc->cdd.coin_pub)) continue; if (GNUNET_OK != TALER_amount_cmp_currency (&pc->total_refunded, refund_amount)) { GNUNET_break (0); pc->refund_currency_mismatch = true; break; } GNUNET_assert (0 <= TALER_amount_add (&pc->total_refunded, &pc->total_refunded, refund_amount)); break; } } /** * Check whether the amount paid is sufficient to cover the price. * * @param pc payment context to check * @return true if the payment is sufficient, false if it is * insufficient */ static bool check_payment_sufficient (struct PayContext *pc) { struct TALER_Amount acc_fee; struct TALER_Amount acc_amount; struct TALER_Amount final_amount; struct TALER_Amount total_wire_fee; struct TALER_Amount total_needed; if (0 == pc->coins_cnt) return TALER_amount_is_zero (&pc->amount); GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (pc->amount.currency, &total_wire_fee)); for (unsigned int i = 0; i < pc->num_exchanges; i++) { if (GNUNET_OK != TALER_amount_cmp_currency (&total_wire_fee, &pc->egs[i]->wire_fee)) { GNUNET_break_op (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_CURRENCY_MISMATCH, total_wire_fee.currency)); return false; } if (0 > TALER_amount_add (&total_wire_fee, &total_wire_fee, &pc->egs[i]->wire_fee)) { GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED, "could not add exchange wire fee to total")); return false; } } /** * This loops calculates what are the deposit fee / total * amount with fee / and wire fee, for all the coins. */ GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (pc->amount.currency, &acc_fee)); GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (pc->amount.currency, &acc_amount)); for (size_t i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; GNUNET_assert (dc->found_in_db); if ( (GNUNET_OK != TALER_amount_cmp_currency (&acc_fee, &dc->deposit_fee)) || (GNUNET_OK != TALER_amount_cmp_currency (&acc_amount, &dc->cdd.amount)) ) { GNUNET_break_op (0); pay_end (pc, TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_CURRENCY_MISMATCH, dc->deposit_fee.currency)); return false; } if ( (0 > TALER_amount_add (&acc_fee, &dc->deposit_fee, &acc_fee)) || (0 > TALER_amount_add (&acc_amount, &dc->cdd.amount, &acc_amount)) ) { GNUNET_break (0); /* Overflow in these amounts? Very strange. */ pay_end (pc, TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, "Overflow adding up amounts")); return false; } if (1 == TALER_amount_cmp (&dc->deposit_fee, &dc->cdd.amount)) { GNUNET_break_op (0); pay_end (pc, TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT, "Deposit fees exceed coin's contribution")); return false; } } /* end deposit loop */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Amount received from wallet: %s\n", TALER_amount2s (&acc_amount)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Deposit fee for all coins: %s\n", TALER_amount2s (&acc_fee)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Total wire fee: %s\n", TALER_amount2s (&total_wire_fee)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Deposit fee limit for merchant: %s\n", TALER_amount2s (&pc->max_fee)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Total refunded amount: %s\n", TALER_amount2s (&pc->total_refunded)); /* Now compare exchange wire fee compared to * what we are willing to pay */ if (GNUNET_YES != TALER_amount_cmp_currency (&total_wire_fee, &acc_fee)) { GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_CURRENCY_MISMATCH, total_wire_fee.currency)); return false; } /* add wire fee to the total fees */ if (0 > TALER_amount_add (&acc_fee, &acc_fee, &total_wire_fee)) { GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, "Overflow adding up amounts")); return false; } if (-1 == TALER_amount_cmp (&pc->max_fee, &acc_fee)) { /** * Sum of fees of *all* the different exchanges of all the coins are * higher than the fixed limit that the merchant is willing to pay. The * difference must be paid by the customer. */ struct TALER_Amount excess_fee; /* compute fee amount to be covered by customer */ GNUNET_assert (TALER_AAR_RESULT_POSITIVE == TALER_amount_subtract (&excess_fee, &acc_fee, &pc->max_fee)); /* add that to the total */ if (0 > TALER_amount_add (&total_needed, &excess_fee, &pc->amount)) { GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, "Overflow adding up amounts")); return false; } } else { /* Fees are fully covered by the merchant, all we require is that the total payment is not below the contract's amount */ total_needed = pc->amount; } /* Do not count refunds towards the payment */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Subtracting total refunds from paid amount: %s\n", TALER_amount2s (&pc->total_refunded)); if (0 > TALER_amount_subtract (&final_amount, &acc_amount, &pc->total_refunded)) { GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUNDS_EXCEED_PAYMENTS, "refunded amount exceeds total payments")); return false; } if (-1 == TALER_amount_cmp (&final_amount, &total_needed)) { /* acc_amount < total_needed */ if (-1 < TALER_amount_cmp (&acc_amount, &total_needed)) { GNUNET_break_op (0); pay_end (pc, TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_PAYMENT_REQUIRED, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUNDED, "contract not paid up due to refunds")); return false; } if (-1 < TALER_amount_cmp (&acc_amount, &pc->amount)) { GNUNET_break_op (0); pay_end (pc, TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES, "contract not paid up due to fees (client may have calculated them badly)")); return false; } GNUNET_break_op (0); pay_end (pc, TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT, "payment insufficient")); return false; } return true; } /** * Execute the DB transaction. If required (from * soft/serialization errors), the transaction can be * restarted here. * * @param[in,out] pc payment context to transact */ static void phase_execute_pay_transaction (struct PayContext *pc) { struct TMH_HandlerContext *hc = pc->hc; const char *instance_id = hc->instance->settings.id; /* Avoid re-trying transactions on soft errors forever! */ if (pc->retry_counter++ > MAX_RETRIES) { GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_SOFT_FAILURE, NULL)); return; } /* Initialize some amount accumulators (used in check_coin_paid(), check_coin_refunded() and check_payment_sufficient()). */ GNUNET_break (GNUNET_OK == TALER_amount_set_zero (pc->amount.currency, &pc->total_paid)); GNUNET_break (GNUNET_OK == TALER_amount_set_zero (pc->amount.currency, &pc->total_fees_paid)); GNUNET_break (GNUNET_OK == TALER_amount_set_zero (pc->amount.currency, &pc->total_refunded)); for (size_t i = 0; icoins_cnt; i++) pc->dc[i].found_in_db = false; pc->pending = pc->coins_cnt; /* First, try to see if we have all we need already done */ TMH_db->preflight (TMH_db->cls); if (GNUNET_OK != TMH_db->start (TMH_db->cls, "run pay")) { GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_START_FAILED, NULL)); return; } { enum GNUNET_DB_QueryStatus qs; /* Check if some of these coins already succeeded for _this_ contract. */ qs = TMH_db->lookup_deposits (TMH_db->cls, instance_id, &pc->h_contract_terms, &check_coin_paid, pc); if (0 > qs) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return; /* do it again */ /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "lookup deposits")); return; } if (pc->deposit_currency_mismatch) { GNUNET_break_op (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, pc->amount.currency)); return; } } { enum GNUNET_DB_QueryStatus qs; /* Check if we refunded some of the coins */ qs = TMH_db->lookup_refunds (TMH_db->cls, instance_id, &pc->h_contract_terms, &check_coin_refunded, pc); if (0 > qs) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return; /* do it again */ /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "lookup refunds")); return; } if (pc->refund_currency_mismatch) { TMH_db->rollback (TMH_db->cls); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "refund currency in database does not match order currency")); return; } } /* Check if there are coins that still need to be processed */ if (0 != pc->pending) { /* we made no DB changes, so we can just rollback */ TMH_db->rollback (TMH_db->cls); /* Ok, we need to first go to the network to process more coins. We that interaction in *tiny* transactions (hence the rollback above). */ pc->phase = PP_BATCH_DEPOSITS; return; } /* 0 == pc->pending: all coins processed, let's see if that was enough */ if (! check_payment_sufficient (pc)) { /* check_payment_sufficient() will have queued an error already. We need to still abort the transaction. */ TMH_db->rollback (TMH_db->cls); return; } /* Payment succeeded, save in database */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Order `%s' (%s) was fully paid\n", pc->order_id, GNUNET_h2s (&pc->h_contract_terms.hash)); { enum GNUNET_DB_QueryStatus qs; qs = TMH_db->mark_contract_paid (TMH_db->cls, instance_id, &pc->h_contract_terms, pc->session_id); if (qs < 0) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return; /* do it again */ GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "mark contract paid")); return; } } TMH_notify_order_change (hc->instance, TMH_OSF_CLAIMED | TMH_OSF_PAID, pc->timestamp, pc->order_serial); { enum GNUNET_DB_QueryStatus qs; json_t *jhook; jhook = GNUNET_JSON_PACK ( GNUNET_JSON_pack_object_incref ("contract_terms", pc->contract_terms), GNUNET_JSON_pack_string ("order_id", pc->order_id) ); GNUNET_assert (NULL != jhook); qs = TMH_trigger_webhook (pc->hc->instance->settings.id, "pay", jhook); json_decref (jhook); if (qs < 0) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return; /* do it again */ GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "failed to trigger webhooks")); return; } } { enum GNUNET_DB_QueryStatus qs; /* Now commit! */ qs = TMH_db->commit (TMH_db->cls); if (0 > qs) { /* commit failed */ TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return; /* do it again */ GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_COMMIT_FAILED, NULL)); return; } } pc->phase = PP_PAYMENT_NOTIFICATION; } /** * Function called with information about a coin that was deposited. * Checks if this coin is in our list of deposits as well. * * @param cls closure with our `struct PayContext *` * @param deposit_serial which deposit operation is this about * @param exchange_url URL of the exchange that issued the coin * @param h_wire hash of merchant's wire details * @param deposit_timestamp when was the deposit made * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin * @param coin_pub public key of the coin */ static void deposit_paid_check ( void *cls, uint64_t deposit_serial, const char *exchange_url, const struct TALER_MerchantWireHashP *h_wire, struct GNUNET_TIME_Timestamp deposit_timestamp, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, const struct TALER_CoinSpendPublicKeyP *coin_pub) { struct PayContext *pc = cls; for (size_t i = 0; icoins_cnt; i++) { struct DepositConfirmation *dci = &pc->dc[i]; if ( (0 == GNUNET_memcmp (&dci->cdd.coin_pub, coin_pub)) && (0 == strcmp (dci->exchange_url, exchange_url)) && (GNUNET_YES == TALER_amount_cmp_currency (&dci->cdd.amount, amount_with_fee)) && (0 == TALER_amount_cmp (&dci->cdd.amount, amount_with_fee)) ) { dci->matched_in_db = true; break; } } } /** * Handle case where contract was already paid. Either decides * the payment is idempotent, or refunds the excess payment. * * @param[in,out] pc context we use to handle the payment */ static void phase_contract_paid (struct PayContext *pc) { enum GNUNET_DB_QueryStatus qs; bool unmatched = false; json_t *refunds; qs = TMH_db->lookup_deposits_by_order (TMH_db->cls, pc->order_serial, &deposit_paid_check, pc); if (qs <= 0) { GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "lookup_deposits_by_order")); return; } for (size_t i = 0; icoins_cnt; i++) { struct DepositConfirmation *dci = &pc->dc[i]; if (! dci->matched_in_db) unmatched = true; } if (! unmatched) { /* Everything fine, idempotent request */ struct TALER_MerchantSignatureP sig; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Idempotent pay request for order `%s', signing again\n", pc->order_id); TALER_merchant_pay_sign (&pc->h_contract_terms, &pc->hc->instance->merchant_priv, &sig); pay_end (pc, TALER_MHD_REPLY_JSON_PACK ( pc->connection, MHD_HTTP_OK, GNUNET_JSON_pack_data_auto ("sig", &sig))); return; } /* Conflict, double-payment detected! */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Client attempted to pay extra for already paid order `%s'\n", pc->order_id); refunds = json_array (); GNUNET_assert (NULL != refunds); for (size_t i = 0; icoins_cnt; i++) { struct DepositConfirmation *dci = &pc->dc[i]; struct TALER_MerchantSignatureP merchant_sig; if (dci->matched_in_db) continue; TALER_merchant_refund_sign (&dci->cdd.coin_pub, &pc->h_contract_terms, 0, /* rtransaction id */ &dci->cdd.amount, &pc->hc->instance->merchant_priv, &merchant_sig); GNUNET_assert ( 0 == json_array_append_new ( refunds, GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ( "coin_pub", &dci->cdd.coin_pub), GNUNET_JSON_pack_data_auto ( "merchant_sig", &merchant_sig), TALER_JSON_pack_amount ("amount", &dci->cdd.amount), GNUNET_JSON_pack_uint64 ("rtransaction_id", 0)))); } pay_end (pc, TALER_MHD_REPLY_JSON_PACK ( pc->connection, MHD_HTTP_CONFLICT, TALER_MHD_PACK_EC ( TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID), GNUNET_JSON_pack_array_steal ("refunds", refunds))); } /** * Check the database state for the given order. * Schedules an error response in the connection on failure. * * @param[in,out] pc context we use to handle the payment */ static void phase_check_contract (struct PayContext *pc) { /* obtain contract terms */ enum GNUNET_DB_QueryStatus qs; bool paid = false; if (NULL != pc->contract_terms) { json_decref (pc->contract_terms); pc->contract_terms = NULL; } qs = TMH_db->lookup_contract_terms2 (TMH_db->cls, pc->hc->instance->settings.id, pc->order_id, &pc->contract_terms, &pc->order_serial, &paid, NULL, &pc->pos_key, &pc->pos_alg); if (0 > qs) { /* single, read-only SQL statements should never cause serialization problems */ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); /* Always report on hard error to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "contract terms")); return; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_NOT_FOUND, TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, pc->order_id)); return; } /* hash contract (needed later) */ json_dumpf (pc->contract_terms, stderr, JSON_INDENT (2)); if (GNUNET_OK != TALER_JSON_contract_hash (pc->contract_terms, &pc->h_contract_terms)) { GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, NULL)); return; } if (paid) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Order `%s' paid, checking for double-payment\n", pc->order_id); pc->phase = PP_CONTRACT_PAID; return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Handling payment for order `%s' with contract hash `%s'\n", pc->order_id, GNUNET_h2s (&pc->h_contract_terms.hash)); /* basic sanity check on the contract */ if (NULL == json_object_get (pc->contract_terms, "merchant")) { /* invalid contract */ GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING, NULL)); return; } /* Get details from contract and check fundamentals */ { const char *fulfillment_url = NULL; struct GNUNET_JSON_Specification espec[] = { TALER_JSON_spec_amount_any ("amount", &pc->amount), GNUNET_JSON_spec_mark_optional ( /* This one does not have to be a Web URL */ GNUNET_JSON_spec_string ("fulfillment_url", &fulfillment_url), NULL), TALER_JSON_spec_amount_any ("max_fee", &pc->max_fee), GNUNET_JSON_spec_timestamp ("timestamp", &pc->timestamp), GNUNET_JSON_spec_timestamp ("refund_deadline", &pc->refund_deadline), GNUNET_JSON_spec_timestamp ("pay_deadline", &pc->pay_deadline), GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", &pc->wire_transfer_deadline), GNUNET_JSON_spec_fixed_auto ("h_wire", &pc->h_wire), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_uint32 ("minimum_age", &pc->minimum_age), NULL), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; pc->minimum_age = 0; res = TALER_MHD_parse_internal_json_data (pc->connection, pc->contract_terms, espec); if (NULL != fulfillment_url) pc->fulfillment_url = GNUNET_strdup (fulfillment_url); if (GNUNET_YES != res) { GNUNET_break (0); pay_end (pc, (GNUNET_NO == res) ? MHD_YES : MHD_NO); return; } } if (GNUNET_OK != TALER_amount_cmp_currency (&pc->max_fee, &pc->amount)) { GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "'max_fee' in database does not match currency of contract price")); return; } for (size_t i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; if (GNUNET_OK != TALER_amount_cmp_currency (&dc->cdd.amount, &pc->amount)) { GNUNET_break_op (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, pc->amount.currency)); return; } } if (GNUNET_TIME_timestamp_cmp (pc->wire_transfer_deadline, <, pc->refund_deadline)) { /* This should already have been checked when creating the order! */ GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE, NULL)); return; } if (GNUNET_TIME_absolute_is_past (pc->pay_deadline.abs_time)) { /* too late */ pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_GONE, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED, NULL)); return; } /* Make sure wire method (still) exists for this instance */ { struct TMH_WireMethod *wm; wm = pc->hc->instance->wm_head; while (0 != GNUNET_memcmp (&pc->h_wire, &wm->h_wire)) wm = wm->next; if (NULL == wm) { GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN, NULL)); return; } pc->wm = wm; } pc->phase = PP_PAY_TRANSACTION; } /** * Try to parse the pay request into the given pay context. * Schedules an error response in the connection on failure. * * @param[in,out] pc context we use to handle the payment */ static void phase_parse_pay (struct PayContext *pc) { const char *session_id = NULL; const json_t *coins; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_array_const ("coins", &coins), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("session_id", &session_id), NULL), GNUNET_JSON_spec_end () }; GNUNET_assert (PP_INIT == pc->phase); { enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_data (pc->connection, pc->hc->request_body, spec); if (GNUNET_YES != res) { GNUNET_break_op (0); pay_end (pc, (GNUNET_NO == res) ? MHD_YES : MHD_NO); return; } } /* copy session ID (if set) */ if (NULL != session_id) { pc->session_id = GNUNET_strdup (session_id); } else { /* use empty string as default if client didn't specify it */ pc->session_id = GNUNET_strdup (""); } pc->coins_cnt = json_array_size (coins); if (pc->coins_cnt > MAX_COIN_ALLOWED_COINS) { GNUNET_break_op (0); pay_end (pc, TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "'coins' array too long")); return; } /* note: 1 coin = 1 deposit confirmation expected */ pc->dc = GNUNET_new_array (pc->coins_cnt, struct DepositConfirmation); /* This loop populates the array 'dc' in 'pc' */ { unsigned int coins_index; json_t *coin; json_array_foreach (coins, coins_index, coin) { struct DepositConfirmation *dc = &pc->dc[coins_index]; const char *exchange_url; struct GNUNET_JSON_Specification ispec[] = { GNUNET_JSON_spec_fixed_auto ("coin_sig", &dc->cdd.coin_sig), GNUNET_JSON_spec_fixed_auto ("coin_pub", &dc->cdd.coin_pub), TALER_JSON_spec_denom_sig ("ub_sig", &dc->cdd.denom_sig), GNUNET_JSON_spec_fixed_auto ("h_denom", &dc->cdd.h_denom_pub), TALER_JSON_spec_amount_any ("contribution", &dc->cdd.amount), TALER_JSON_spec_web_url ("exchange_url", &exchange_url), /* if a minimum age was required, the minimum_age_sig and * age_commitment must be provided */ GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_fixed_auto ("minimum_age_sig", &dc->minimum_age_sig), &dc->no_minimum_age_sig), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_age_commitment ("age_commitment", &dc->age_commitment), &dc->no_age_commitment), /* if minimum age was not required, but coin with age restriction set * was used, h_age_commitment must be provided. */ GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_fixed_auto ("h_age_commitment", &dc->cdd.h_age_commitment), &dc->no_h_age_commitment), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; bool have_eg = false; res = TALER_MHD_parse_json_data (pc->connection, coin, ispec); if (GNUNET_YES != res) { GNUNET_break_op (0); pay_end (pc, (GNUNET_NO == res) ? MHD_YES : MHD_NO); return; } for (unsigned int j = 0; jcdd.coin_pub, &pc->dc[j].cdd.coin_pub)) { GNUNET_break_op (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "duplicate coin in list")); return; } } dc->exchange_url = GNUNET_strdup (exchange_url); dc->index = coins_index; dc->pc = pc; /* Check the consistency of the (potential) age restriction * information. */ if (dc->no_age_commitment != dc->no_minimum_age_sig) { GNUNET_break_op (0); pay_end (pc, TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "inconsistent: 'age_commitment' vs. 'minimum_age_sig'" )); return; } /* Setup exchange group */ for (unsigned int i = 0; inum_exchanges; i++) { if (0 == strcmp (pc->egs[i]->exchange_url, exchange_url)) { have_eg = true; break; } } if (! have_eg) { struct ExchangeGroup *eg; eg = GNUNET_new (struct ExchangeGroup); eg->pc = pc; eg->exchange_url = dc->exchange_url; GNUNET_array_append (pc->egs, pc->num_exchanges, eg); } } } pc->phase = PP_CHECK_CONTRACT; } /** * Custom cleanup routine for a `struct PayContext`. * * @param cls the `struct PayContext` to clean up. */ static void pay_context_cleanup (void *cls) { struct PayContext *pc = cls; if (NULL != pc->timeout_task) { GNUNET_SCHEDULER_cancel (pc->timeout_task); pc->timeout_task = NULL; } if (NULL != pc->contract_terms) { json_decref (pc->contract_terms); pc->contract_terms = NULL; } for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; TALER_denom_sig_free (&dc->cdd.denom_sig); GNUNET_free (dc->exchange_url); } GNUNET_free (pc->dc); for (unsigned int i = 0; inum_exchanges; i++) { struct ExchangeGroup *eg = pc->egs[i]; if (NULL != eg->fo) TMH_EXCHANGES_keys4exchange_cancel (eg->fo); GNUNET_free (eg); } GNUNET_free (pc->egs); if (NULL != pc->response) { MHD_destroy_response (pc->response); pc->response = NULL; } GNUNET_free (pc->fulfillment_url); GNUNET_free (pc->session_id); GNUNET_CONTAINER_DLL_remove (pc_head, pc_tail, pc); GNUNET_free (pc->pos_key); GNUNET_free (pc); } MHD_RESULT TMH_post_orders_ID_pay (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { struct PayContext *pc = hc->ctx; GNUNET_assert (NULL != hc->infix); if (NULL == pc) { pc = GNUNET_new (struct PayContext); pc->connection = connection; pc->hc = hc; pc->order_id = hc->infix; hc->ctx = pc; hc->cc = &pay_context_cleanup; GNUNET_CONTAINER_DLL_insert (pc_head, pc_tail, pc); } while (1) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Processing /pay in phase %d\n", (int) pc->phase); switch (pc->phase) { case PP_INIT: phase_parse_pay (pc); break; case PP_CHECK_CONTRACT: phase_check_contract (pc); break; case PP_CONTRACT_PAID: phase_contract_paid (pc); break; case PP_PAY_TRANSACTION: phase_execute_pay_transaction (pc); break; case PP_BATCH_DEPOSITS: phase_batch_deposits (pc); break; case PP_PAYMENT_NOTIFICATION: phase_payment_notification (pc); break; case PP_SUCCESS_RESPONSE: phase_success_response (pc); break; case PP_RETURN_RESPONSE: phase_return_response (pc); break; case PP_END_YES: return MHD_YES; case PP_END_NO: return MHD_NO; } switch (pc->suspended) { case GNUNET_SYSERR: /* during shutdown, we don't generate any more replies */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Processing /pay ends due to shutdown in phase %d\n", (int) pc->phase); return MHD_NO; case GNUNET_NO: /* continue to next phase */ break; case GNUNET_YES: GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Processing /pay suspended in phase %d\n", (int) pc->phase); return MHD_YES; } } /* impossible to get here */ GNUNET_assert (0); return MHD_YES; } /* end of taler-merchant-httpd_post-orders-ID-pay.c */