/* This file is part of TALER (C) 2014-2021 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 "taler-merchant-httpd_auditors.h" #include "taler-merchant-httpd_exchanges.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 /** * Information we keep for an individual call to the pay handler. */ struct PayContext; /** * Information kept during a pay request for each coin. */ struct DepositConfirmation { /** * Reference to the main PayContext */ struct PayContext *pc; /** * Handle to the deposit operation we are performing for * this coin, NULL after the operation is done. */ struct TALER_EXCHANGE_DepositHandle *dh; /** * URL of the exchange that issued this coin. */ char *exchange_url; /** * Hash of the denomination of this coin. */ struct GNUNET_HashCode h_denom; /** * Amount this coin contributes to the total purchase price. * This amount includes the deposit fee. */ struct TALER_Amount amount_with_fee; /** * 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; /** * Wire fee charged by the exchange of this coin. */ struct TALER_Amount wire_fee; /** * Public key of the coin. */ struct TALER_CoinSpendPublicKeyP coin_pub; /** * Signature using the @e denom key over the @e coin_pub. */ struct TALER_DenominationSignature ub_sig; /** * Signature of the coin's private key over the contract. */ struct TALER_CoinSpendSignatureP coin_sig; /** * Offset of this coin into the `dc` array of all coins in the * @e pc. */ unsigned int index; /** * true if we found this coin in the database. */ bool found_in_db; }; /** * 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 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 #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; /** * Handle for operation to lookup /keys (and auditors) from * the exchange used for this transaction; NULL if no operation is * pending. */ struct TMH_EXCHANGES_FindOperation *fo; /** * URL of the exchange used for the last @e fo. */ const char *current_exchange; /** * 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 GNUNET_HashCode h_contract_terms; /** * "h_wire" from @e contract_terms. Used to identify * the instance's wire transfer method. */ struct GNUNET_HashCode 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; /** * Maximum wire 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 amorized difference. Wire fees are charged over an * aggregate of several translations, hence unlike the deposit * fees, they are amortized over several customer's transactions. * The contract specifies under @e wire_fee_amortization how many * customer's transactions he expects the wire fees to be amortized * over on average. Thus, if the wire fees are larger than * @e max_wire_fee, each customer is expected to contribute * $\frac{actual-wire-fee - max_wire_fee}{wire_fee_amortization}$. * The customer's contribution may be further reduced by the * difference between @e max_fee and the sum of the deposit fees. * * Default is that the merchant is unwilling to pay any wire fees. */ struct TALER_Amount max_wire_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_Absolute wire_transfer_deadline; /** * Timestamp from @e contract_terms. */ struct GNUNET_TIME_Absolute timestamp; /** * Refund deadline from @e contract_terms. */ struct GNUNET_TIME_Absolute refund_deadline; /** * Deadline for the customer to pay for this proposal. */ struct GNUNET_TIME_Absolute pay_deadline; /** * Number of transactions that the wire fees are expected to be * amortized over. Never zero, defaults (conservateively) to 1. * May be higher if merchants expect many small transactions to * be aggregated and thus wire fees to be reasonably amortized * due to aggregation. */ uint32_t wire_fee_amortization; /** * Number of coins this payment is made of. Length * of the @e dc array. */ unsigned int coins_cnt; /** * How often have we retried the 'main' transaction? */ unsigned int retry_counter; /** * Number of transactions still pending. Initially set to * @e coins_cnt, decremented on each transaction that * successfully finished. */ unsigned int pending; /** * Number of transactions still pending for the currently selected * exchange. Initially set to the number of coins started at the * exchange, decremented on each transaction that successfully * finished. Once it hits zero, we pick the next exchange. */ unsigned int pending_at_ce; /** * 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; /** * #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; /** * true if we already tried a forced /keys download. */ bool tried_force_keys; }; /** * Head of active pay context DLL. */ static struct PayContext *pc_head; /** * Tail of active pay context DLL. */ static struct PayContext *pc_tail; /** * 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: 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; } /** * Abort all pending /deposit operations. * * @param pc pay context to abort */ static void abort_deposit (struct PayContext *pc) { GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Aborting pending /deposit operations\n"); for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dci = &pc->dc[i]; if (NULL != dci->dh) { TALER_EXCHANGE_deposit_cancel (dci->dh); dci->dh = NULL; } } } /** * Force all pay contexts to be resumed as we are about * to shut down MHD. */ void TMH_force_pc_resume () { for (struct PayContext *pc = pc_head; NULL != pc; pc = pc->next) { abort_deposit (pc); 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 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) { abort_deposit (pc); pc->response_code = response_code; pc->response = response; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming /pay handling as exchange interaction is done (%u)\n", response_code); if (NULL != pc->timeout_task) { GNUNET_SCHEDULER_cancel (pc->timeout_task); pc->timeout_task = NULL; } GNUNET_assert (GNUNET_YES == pc->suspended); pc->suspended = GNUNET_NO; MHD_resume_connection (pc->connection); TMH_trigger_daemon (); /* we resumed, kick MHD */ } /** * Resume payment processing with an error. * * @param pc operation to resume * @param http_status http status code to return * @param ec taler error code to return * @param msg human readable error message */ static void resume_pay_with_error (struct PayContext *pc, unsigned int http_status, enum TALER_ErrorCode ec, const char *msg) { resume_pay_with_response (pc, http_status, TALER_MHD_make_error (ec, msg)); } /** * 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; } abort_deposit (pc); for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; if (NULL != dc->ub_sig.rsa_signature) { GNUNET_CRYPTO_rsa_signature_free (dc->ub_sig.rsa_signature); dc->ub_sig.rsa_signature = NULL; } GNUNET_free (dc->exchange_url); } GNUNET_free (pc->dc); if (NULL != pc->fo) { TMH_EXCHANGES_find_exchange_cancel (pc->fo); pc->fo = NULL; } 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); } /** * Find the exchange we need to talk to for the next * pending deposit permission. * * @param pc payment context we are processing */ static void find_next_exchange (struct PayContext *pc); /** * Execute the DB transaction. If required (from * soft/serialization errors), the transaction can be * restarted here. * * @param pc payment context to transact */ static void execute_pay_transaction (struct PayContext *pc); /** * Callback to handle a deposit permission's response. * * @param cls a `struct DepositConfirmation` (i.e. a pointer * into the global array of confirmations and an index for this call * in that array). That way, the last executed callback can detect * that no other confirmations are on the way, and can pack a response * for the wallet * @param hr HTTP response code details * @param deposit_timestamp time when the exchange generated the deposit confirmation * @param exchange_sig signature from the exchange over the deposit confirmation * @param exchange_pub which key did the exchange use to create the @a exchange_sig */ static void deposit_cb (void *cls, const struct TALER_EXCHANGE_HttpResponse *hr, struct GNUNET_TIME_Absolute deposit_timestamp, const struct TALER_ExchangeSignatureP *exchange_sig, const struct TALER_ExchangePublicKeyP *exchange_pub) { struct DepositConfirmation *dc = cls; struct PayContext *pc = dc->pc; dc->dh = NULL; GNUNET_assert (GNUNET_YES == pc->suspended); pc->pending_at_ce--; if (MHD_HTTP_OK != hr->http_status) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Deposit operation failed with HTTP code %u/%d\n", hr->http_status, (int) hr->ec); /* Transaction failed */ if (5 == hr->http_status / 100) { /* internal server error at exchange */ resume_pay_with_response (pc, MHD_HTTP_BAD_GATEWAY, TALER_MHD_make_json_pack ( "{s:s, s:I, s:I, s:I}", "hint", TALER_ErrorCode_get_hint ( TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS), "code", (json_int_t) TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS, "exchange_code", (json_int_t) hr->ec, "exchange_http_status", (json_int_t) hr->http_status)); } else if (NULL == 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 ( "{s:s, s:I, s:I, s:I}", "hint", TALER_ErrorCode_get_hint ( TALER_EC_MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED), "code", (json_int_t) TALER_EC_MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED, "exchange_code", (json_int_t) hr->ec, "exchange_http_status", (json_int_t) hr->http_status)); } else { /* Forward error, adding the "coin_pub" for which the error was being generated */ if (TALER_EC_EXCHANGE_DEPOSIT_INSUFFICIENT_FUNDS == hr->ec) { resume_pay_with_response ( pc, MHD_HTTP_CONFLICT, TALER_MHD_make_json_pack ("{s:s, s:I, s:I, s:I, s:o, s:O}", "hint", TALER_ErrorCode_get_hint ( TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS), "code", (json_int_t) TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS, "exchange_code", (json_int_t) hr->ec, "exchange_http_status", (json_int_t) hr->http_status, "coin_pub", GNUNET_JSON_from_data_auto (&dc->coin_pub), "exchange_reply", hr->reply)); } else { resume_pay_with_response ( pc, MHD_HTTP_BAD_GATEWAY, TALER_MHD_make_json_pack ("{s:s, s:I, s:I, s:I, s:o, s:O}", "hint", TALER_ErrorCode_get_hint ( TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS), "code", (json_int_t) TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS, "exchange_code", (json_int_t) hr->ec, "exchange_http_status", (json_int_t) hr->http_status, "coin_pub", GNUNET_JSON_from_data_auto (&dc->coin_pub), "exchange_reply", hr->reply)); } } return; } /* 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), pc->hc->instance->settings.id); TMH_db->preflight (TMH_db->cls); { enum GNUNET_DB_QueryStatus qs; qs = TMH_db->insert_deposit (TMH_db->cls, pc->hc->instance->settings.id, deposit_timestamp, &pc->h_contract_terms, &dc->coin_pub, dc->exchange_url, &dc->amount_with_fee, &dc->deposit_fee, &dc->refund_fee, &dc->wire_fee, &pc->wm->h_wire, exchange_sig, exchange_pub); if (0 > qs) { /* Special report if retries insufficient */ if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { execute_pay_transaction (pc); return; } /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); /* Forward error including 'proof' for the body */ resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "deposit"); return; } } dc->found_in_db = true; /* well, at least NOW it'd be true ;-) */ pc->pending--; if (0 != pc->pending_at_ce) return; /* still more to do with current exchange */ find_next_exchange (pc); } /** * Function called with the result of our exchange lookup. * * @param cls the `struct PayContext` * @param hr HTTP response details * @param exchange_handle NULL if exchange was not found to be acceptable * @param payto_uri payto://-URI of the exchange * @param wire_fee current applicable fee for dealing with @a exchange_handle, * NULL if not available * @param exchange_trusted true if this exchange is * trusted by config */ static void process_pay_with_exchange (void *cls, const struct TALER_EXCHANGE_HttpResponse *hr, struct TALER_EXCHANGE_Handle *exchange_handle, const char *payto_uri, const struct TALER_Amount *wire_fee, bool exchange_trusted) { struct PayContext *pc = cls; struct TMH_HandlerContext *hc = pc->hc; const struct TALER_EXCHANGE_Keys *keys; (void) payto_uri; pc->fo = NULL; GNUNET_assert (GNUNET_YES == pc->suspended); if (NULL == hr) { resume_pay_with_response ( pc, MHD_HTTP_GATEWAY_TIMEOUT, TALER_MHD_make_json_pack ( "{s:s, s:I}", "hint", TALER_ErrorCode_get_hint ( TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT), "code", (json_int_t) TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT)); return; } if ( (MHD_HTTP_OK != hr->http_status) || (NULL == exchange_handle) ) { resume_pay_with_response ( pc, MHD_HTTP_BAD_GATEWAY, TALER_MHD_make_json_pack ( "{s:s, s:I, s:I, s:I, s:O?}", "hint", TALER_ErrorCode_get_hint ( TALER_EC_MERCHANT_GENERIC_EXCHANGE_CONNECT_FAILURE), "code", (json_int_t) TALER_EC_MERCHANT_GENERIC_EXCHANGE_CONNECT_FAILURE, "exchange_http_status", (json_int_t) hr->http_status, "exchange_code", (json_int_t) hr->ec, "exchange_reply", hr->reply)); return; } keys = TALER_EXCHANGE_get_keys (exchange_handle); if (NULL == keys) { GNUNET_break (0); /* should not be possible if HTTP status is #MHD_HTTP_OK */ resume_pay_with_error (pc, MHD_HTTP_BAD_GATEWAY, TALER_EC_MERCHANT_GENERIC_EXCHANGE_KEYS_FAILURE, NULL); return; } /* Initiate /deposit operation for all coins of the current exchange (!) */ GNUNET_assert (0 == pc->pending_at_ce); for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; const struct TALER_EXCHANGE_DenomPublicKey *denom_details; enum TALER_ErrorCode ec; unsigned int http_status; if (NULL != dc->dh) continue; /* we were here before (can happen due to tried_force_keys logic), don't go again */ if (dc->found_in_db) continue; if (0 != strcmp (dc->exchange_url, pc->current_exchange)) continue; denom_details = TALER_EXCHANGE_get_denomination_key_by_hash (keys, &dc->h_denom); if (NULL == denom_details) { if (! pc->tried_force_keys) { /* let's try *forcing* a re-download of /keys from the exchange. Maybe the wallet has seen /keys that we missed. */ pc->tried_force_keys = true; pc->fo = TMH_EXCHANGES_find_exchange (pc->current_exchange, pc->wm->wire_method, GNUNET_YES, &process_pay_with_exchange, pc); if (NULL != pc->fo) return; } /* Forcing failed or we already did it, give up */ resume_pay_with_response ( pc, MHD_HTTP_BAD_REQUEST, TALER_MHD_make_json_pack ( "{s:s, s:I, s:o, s:o?}", "hint", TALER_ErrorCode_get_hint ( TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND), "code", TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND, "h_denom_pub", GNUNET_JSON_from_data_auto (&dc->h_denom), "exchange_keys", TALER_EXCHANGE_get_keys_raw (exchange_handle))); return; } if (GNUNET_OK != TMH_AUDITORS_check_dk (exchange_handle, denom_details, exchange_trusted, &http_status, &ec)) { if (! pc->tried_force_keys) { /* let's try *forcing* a re-download of /keys from the exchange. Maybe the wallet has seen auditors that we missed. */ pc->tried_force_keys = true; pc->fo = TMH_EXCHANGES_find_exchange (pc->current_exchange, pc->wm->wire_method, GNUNET_YES, &process_pay_with_exchange, pc); if (NULL != pc->fo) return; } resume_pay_with_response ( pc, http_status, TALER_MHD_make_json_pack ( "{s:s, s:I, s:o}", "hint", TALER_ErrorCode_get_hint (ec), "code", (json_int_t) ec, "h_denom_pub", GNUNET_JSON_from_data_auto (&denom_details->h_key))); return; } dc->deposit_fee = denom_details->fee_deposit; dc->refund_fee = denom_details->fee_refund; dc->wire_fee = *wire_fee; GNUNET_assert (NULL != pc->wm); GNUNET_assert (NULL != pc->wm->j_wire); TMH_db->preflight (TMH_db->cls); dc->dh = TALER_EXCHANGE_deposit (exchange_handle, &dc->amount_with_fee, pc->wire_transfer_deadline, pc->wm->j_wire, &pc->h_contract_terms, &dc->coin_pub, &dc->ub_sig, &denom_details->key, pc->timestamp, &hc->instance->merchant_pub, pc->refund_deadline, &dc->coin_sig, &deposit_cb, dc, &ec); if (NULL == dc->dh) { /* 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 ( "{s:s, s:I, s:i}", "hint", TALER_ErrorCode_get_hint (ec), "code", (json_int_t) ec, "coin_idx", i)); return; } if (TMH_force_audit) TALER_EXCHANGE_deposit_force_dc (dc->dh); pc->pending_at_ce++; } } /** * Find the exchange we need to talk to for the next * pending deposit permission. * * @param pc payment context we are processing */ static void find_next_exchange (struct PayContext *pc) { GNUNET_assert (0 == pc->pending_at_ce); for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; if (dc->found_in_db) continue; pc->current_exchange = dc->exchange_url; pc->fo = TMH_EXCHANGES_find_exchange (pc->current_exchange, pc->wm->wire_method, GNUNET_NO, &process_pay_with_exchange, pc); if (NULL == pc->fo) { GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED, "Failed to lookup exchange by URL"); return; } return; } pc->current_exchange = NULL; /* We are done with all the HTTP requests, go back and try the 'big' database transaction! (It should work now!) */ GNUNET_assert (0 == pc->pending); execute_pay_transaction (pc); } /** * 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 * @param wire_fee wire fee the exchange of this coin charges */ 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, const struct TALER_Amount *wire_fee) { struct PayContext *pc = cls; for (unsigned int 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->coin_pub)) || (0 != strcmp (exchange_url, dc->exchange_url)) || (0 != TALER_amount_cmp (amount_with_fee, &dc->amount_with_fee)) ) continue; /* does not match, skip */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Deposit of coin `%s' already in our DB.\n", TALER_B2S (coin_pub)); 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->wire_fee = *wire_fee; dc->amount_with_fee = *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 (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; /* Get matching coin from results*/ if (0 != GNUNET_memcmp (coin_pub, &dc->coin_pub)) continue; 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 wire_fee_delta; struct TALER_Amount wire_fee_customer_contribution; struct TALER_Amount total_wire_fee; struct TALER_Amount total_needed; if (0 == pc->coins_cnt) { return ((0 == pc->amount.value) && (0 == pc->amount.fraction)); } acc_fee = pc->dc[0].deposit_fee; total_wire_fee = pc->dc[0].wire_fee; acc_amount = pc->dc[0].amount_with_fee; /** * This loops calculates what are the deposit fee / total * amount with fee / and wire fee, for all the coins. */ for (unsigned int i = 1; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; GNUNET_assert (dc->found_in_db); if ( (0 > TALER_amount_add (&acc_fee, &dc->deposit_fee, &acc_fee)) || (0 > TALER_amount_add (&acc_amount, &dc->amount_with_fee, &acc_amount)) ) { GNUNET_break (0); /* Overflow in these amounts? Very strange. */ resume_pay_with_error (pc, 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->amount_with_fee)) { GNUNET_break_op (0); resume_pay_with_error (pc, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT, "Deposit fees exceed coin's contribution"); return false; } /* If exchange differs, add wire fee */ { bool new_exchange = true; for (unsigned int j = 0; jexchange_url, pc->dc[j].exchange_url)) { new_exchange = false; break; } if (! new_exchange) continue; if (GNUNET_OK != TALER_amount_cmp_currency (&total_wire_fee, &dc->wire_fee)) { GNUNET_break_op (0); resume_pay_with_error (pc, MHD_HTTP_CONFLICT, TALER_EC_GENERIC_CURRENCY_MISMATCH, total_wire_fee.currency); return false; } if (0 > TALER_amount_add (&total_wire_fee, &total_wire_fee, &dc->wire_fee)) { GNUNET_break (0); resume_pay_with_error (pc, 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; } } } /* 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, "Max wire fee: %s\n", TALER_amount2s (&pc->max_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, &pc->max_wire_fee)) { GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_CONFLICT, TALER_EC_GENERIC_CURRENCY_MISMATCH, total_wire_fee.currency); return false; } switch (TALER_amount_subtract (&wire_fee_delta, &total_wire_fee, &pc->max_wire_fee)) { case TALER_AAR_RESULT_POSITIVE: /* Actual wire fee is indeed higher than our maximum, compute how much the customer is expected to cover! */ TALER_amount_divide (&wire_fee_customer_contribution, &wire_fee_delta, pc->wire_fee_amortization); break; case TALER_AAR_RESULT_ZERO: case TALER_AAR_INVALID_NEGATIVE_RESULT: /* Wire fee threshold is still above the wire fee amount. Customer is not going to contribute on this. */ GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (total_wire_fee.currency, &wire_fee_customer_contribution)); break; default: GNUNET_assert (0); } /* add wire fee contribution to the total fees */ if (0 > TALER_amount_add (&acc_fee, &acc_fee, &wire_fee_customer_contribution)) { GNUNET_break (0); resume_pay_with_error (pc, 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); resume_pay_with_error (pc, 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); resume_pay_with_error (pc, 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); resume_pay_with_error (pc, MHD_HTTP_PAYMENT_REQUIRED, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUNDED, "contract not paid up due to refunds"); } else if (-1 < TALER_amount_cmp (&acc_amount, &pc->amount)) { GNUNET_break_op (0); resume_pay_with_error (pc, MHD_HTTP_NOT_ACCEPTABLE, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES, "contract not paid up due to fees (client may have calculated them badly)"); } else { GNUNET_break_op (0); resume_pay_with_error (pc, MHD_HTTP_NOT_ACCEPTABLE, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT, "payment insufficient"); } return false; } return true; } /** * * */ static void execute_pay_transaction (struct PayContext *pc) { struct TMH_HandlerContext *hc = pc->hc; const char *instance_id = hc->instance->settings.id; bool refunded; /* Avoid re-trying transactions on soft errors forever! */ if (pc->retry_counter++ > MAX_RETRIES) { GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_SOFT_FAILURE, NULL); return; } GNUNET_assert (GNUNET_YES == pc->suspended); /* Initialize some amount accumulators (used in check_coin_paid(), check_coin_refunded() and check_payment_sufficient()). */ GNUNET_break (GNUNET_OK == TALER_amount_get_zero (pc->amount.currency, &pc->total_paid)); GNUNET_break (GNUNET_OK == TALER_amount_get_zero (pc->amount.currency, &pc->total_fees_paid)); GNUNET_break (GNUNET_OK == TALER_amount_get_zero (pc->amount.currency, &pc->total_refunded)); for (unsigned int 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); resume_pay_with_error (pc, 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) { execute_pay_transaction (pc); return; } /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "lookup deposits"); 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) { execute_pay_transaction (pc); return; } /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "lookup refunds"); return; } refunded = (qs > 0); } /* 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). */ find_next_exchange (pc); 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)); { 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) { execute_pay_transaction (pc); return; } GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "mark contract paid"); 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) { execute_pay_transaction (pc); return; } GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_COMMIT_FAILED, NULL); return; } } /* Notify clients that have been waiting for the payment to succeed */ if ( (NULL != pc->session_id) && (NULL != pc->fulfillment_url) ) TMH_long_poll_resume2 (pc->session_id, pc->fulfillment_url); TMH_long_poll_resume (pc->order_id, hc->instance, NULL, false); TMH_notify_order_change (hc->instance, pc->order_id, true, /* paid */ refunded, false, /* wired */ pc->timestamp, pc->order_serial); /* Generate response (payment successful) */ { struct GNUNET_CRYPTO_EddsaSignature sig; /* Sign on our end (as the payment did go through, even if it may have been refunded already) */ { struct PaymentResponsePS mr = { .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_PAYMENT_OK), .purpose.size = htonl (sizeof (mr)), .h_contract_terms = pc->h_contract_terms }; GNUNET_CRYPTO_eddsa_sign (&pc->hc->instance->merchant_priv.eddsa_priv, &mr, &sig); } /* Build the response */ { json_t *resp; resp = json_pack ("{s:o}", "sig", GNUNET_JSON_from_data_auto (&sig)); GNUNET_assert (NULL != resp); resume_pay_with_response (pc, MHD_HTTP_OK, TALER_MHD_make_json (resp)); json_decref (resp); } } } /** * Try to parse the pay request into the given pay context. * Schedules an error response in the connection on failure. * * @param connection HTTP connection we are receiving payment on * @param[in,out] hc context with further information about the request * @param pc context we use to handle the payment * @return #GNUNET_OK on success, * #GNUNET_NO on failure (response was queued with MHD) * #GNUNET_SYSERR on hard error (MHD connection must be dropped) */ static enum GNUNET_GenericReturnValue parse_pay (struct MHD_Connection *connection, struct TMH_HandlerContext *hc, struct PayContext *pc) { /* First, parse request */ { const char *session_id = NULL; json_t *coins; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_json ("coins", &coins), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("session_id", &session_id)), GNUNET_JSON_spec_end () }; { enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_data (connection, hc->request_body, spec); if (GNUNET_YES != res) { GNUNET_break_op (0); return res; } } /* 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 (""); } if (! json_is_array (coins)) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); return (MHD_YES == TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MISSING, "'coins' must be an array")) ? GNUNET_NO : GNUNET_SYSERR; } pc->coins_cnt = json_array_size (coins); if (pc->coins_cnt > MAX_COIN_ALLOWED_COINS) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); return (MHD_YES == TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "'coins' array too long")) ? GNUNET_NO : GNUNET_SYSERR; } /* 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->coin_sig), GNUNET_JSON_spec_fixed_auto ("coin_pub", &dc->coin_pub), TALER_JSON_spec_denomination_signature ("ub_sig", &dc->ub_sig), GNUNET_JSON_spec_fixed_auto ("h_denom", &dc->h_denom), TALER_JSON_spec_amount ("contribution", &dc->amount_with_fee), GNUNET_JSON_spec_string ("exchange_url", &exchange_url), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_data (connection, coin, ispec); if (GNUNET_YES != res) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); return res; } for (unsigned int j = 0; jcoin_pub, &pc->dc[j].coin_pub)) { GNUNET_break_op (0); return (MHD_YES == TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "duplicate coin in list")) ? GNUNET_NO : GNUNET_SYSERR; } } dc->exchange_url = GNUNET_strdup (exchange_url); dc->index = coins_index; dc->pc = pc; if (0 != strcasecmp (dc->amount_with_fee.currency, TMH_currency)) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_CONFLICT, TALER_EC_GENERIC_CURRENCY_MISMATCH, TMH_currency); } } } GNUNET_JSON_parse_free (spec); } /* obtain contract terms */ { enum GNUNET_DB_QueryStatus qs; json_t *contract_terms = NULL; qs = TMH_db->lookup_contract_terms (TMH_db->cls, hc->instance->settings.id, pc->order_id, &contract_terms, &pc->order_serial); 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); return (MHD_YES == TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "contract terms")) ? GNUNET_NO : GNUNET_SYSERR; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { return (MHD_YES == TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, pc->order_id)) ? GNUNET_NO : GNUNET_SYSERR; } /* hash contract (needed later) */ if (GNUNET_OK != TALER_JSON_contract_hash (contract_terms, &pc->h_contract_terms)) { GNUNET_break (0); json_decref (contract_terms); return (MHD_YES == TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, NULL)) ? GNUNET_NO : GNUNET_SYSERR; } 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)); /* basic sanity check on the contract */ if (NULL == json_object_get (contract_terms, "merchant")) { /* invalid contract */ GNUNET_break (0); json_decref (contract_terms); return (MHD_YES == TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING, NULL)) ? GNUNET_NO : GNUNET_SYSERR; } /* Get details from contract and check fundamentals */ { const char *fulfillment_url = NULL; struct GNUNET_JSON_Specification espec[] = { TALER_JSON_spec_amount ("amount", &pc->amount), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("fulfillment_url", &fulfillment_url)), TALER_JSON_spec_amount ("max_fee", &pc->max_fee), TALER_JSON_spec_amount ("max_wire_fee", &pc->max_wire_fee), GNUNET_JSON_spec_uint32 ("wire_fee_amortization", &pc->wire_fee_amortization), TALER_JSON_spec_absolute_time ("timestamp", &pc->timestamp), TALER_JSON_spec_absolute_time ("refund_deadline", &pc->refund_deadline), TALER_JSON_spec_absolute_time ("pay_deadline", &pc->pay_deadline), TALER_JSON_spec_absolute_time ("wire_transfer_deadline", &pc->wire_transfer_deadline), GNUNET_JSON_spec_fixed_auto ("h_wire", &pc->h_wire), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_internal_json_data (connection, contract_terms, espec); if (NULL != fulfillment_url) pc->fulfillment_url = GNUNET_strdup (fulfillment_url); json_decref (contract_terms); if (GNUNET_YES != res) { GNUNET_break (0); return res; } if ((0 != strcasecmp (pc->amount.currency, TMH_currency)) || ((0 != strcasecmp (pc->max_fee.currency, TMH_currency)) || (0 != strcasecmp (pc->max_wire_fee.currency, TMH_currency)))) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_CURRENCY_MISMATCH, TMH_currency); } } if (pc->wire_transfer_deadline.abs_value_us < pc->refund_deadline.abs_value_us) { /* This should already have been checked when creating the order! */ GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE, NULL); } if (0 == GNUNET_TIME_absolute_get_remaining (pc->pay_deadline).rel_value_us) { /* too late */ return (MHD_YES == TALER_MHD_reply_with_error (connection, MHD_HTTP_GONE, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED, NULL)) ? GNUNET_NO : GNUNET_SYSERR; } } /* Make sure wire method (still) exists for this instance */ { struct TMH_WireMethod *wm; wm = hc->instance->wm_head; while (0 != GNUNET_memcmp (&pc->h_wire, &wm->h_wire)) wm = wm->next; if (NULL == wm) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN, NULL); } pc->wm = wm; } return GNUNET_OK; } /** * 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_DEBUG, "Resuming pay with error after timeout\n"); if (NULL != pc->fo) { TMH_EXCHANGES_find_exchange_cancel (pc->fo); pc->fo = NULL; } resume_pay_with_error (pc, MHD_HTTP_GATEWAY_TIMEOUT, TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, NULL); } 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); GNUNET_CONTAINER_DLL_insert (pc_head, pc_tail, pc); pc->connection = connection; pc->hc = hc; pc->order_id = hc->infix; hc->ctx = pc; hc->cc = &pay_context_cleanup; } if (GNUNET_SYSERR == pc->suspended) return MHD_NO; /* during shutdown, we don't generate any more replies */ GNUNET_assert (GNUNET_NO == pc->suspended); if (0 != pc->response_code) { MHD_RESULT res; /* We are *done* processing the request, just queue the response (!) */ if (UINT_MAX == pc->response_code) { GNUNET_break (0); return MHD_NO; /* hard error */ } res = MHD_queue_response (connection, pc->response_code, pc->response); MHD_destroy_response (pc->response); pc->response = NULL; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Queueing response (%u) for POST /orders/$ID/pay (%s).\n", (unsigned int) pc->response_code, res ? "OK" : "FAILED"); return res; } { enum GNUNET_GenericReturnValue ret; ret = parse_pay (connection, hc, pc); if (GNUNET_OK != ret) return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; } /* Payment not finished, suspend while we interact with the exchange */ MHD_suspend_connection (connection); pc->suspended = GNUNET_YES; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Suspending pay handling while working with the exchange\n"); GNUNET_assert (NULL == pc->timeout_task); pc->timeout_task = GNUNET_SCHEDULER_add_delayed (get_pay_timeout (pc->coins_cnt), &handle_pay_timeout, pc); execute_pay_transaction (pc); return MHD_YES; } /* end of taler-merchant-httpd_post-orders-ID-pay.c */