diff options
author | Christian Grothoff <christian@grothoff.org> | 2020-04-30 21:23:40 +0200 |
---|---|---|
committer | Christian Grothoff <christian@grothoff.org> | 2020-04-30 21:23:40 +0200 |
commit | 577e344b018cf186e179edf8fa56101659e25307 (patch) | |
tree | 5dec709ed9dbdb48346499d64c41d78a447fc48e /src/backend/taler-merchant-httpd_post-orders-ID-abort.c | |
parent | 3f5bc6fa636e278d54f8a17c6f4d4b15cc63608f (diff) | |
download | merchant-577e344b018cf186e179edf8fa56101659e25307.tar.gz merchant-577e344b018cf186e179edf8fa56101659e25307.tar.bz2 merchant-577e344b018cf186e179edf8fa56101659e25307.zip |
work on /pay and /abort processing
Diffstat (limited to 'src/backend/taler-merchant-httpd_post-orders-ID-abort.c')
-rw-r--r-- | src/backend/taler-merchant-httpd_post-orders-ID-abort.c | 2326 |
1 files changed, 581 insertions, 1745 deletions
diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-abort.c b/src/backend/taler-merchant-httpd_post-orders-ID-abort.c index 7a1b7fd8..604490d4 100644 --- a/src/backend/taler-merchant-httpd_post-orders-ID-abort.c +++ b/src/backend/taler-merchant-httpd_post-orders-ID-abort.c @@ -16,31 +16,24 @@ License along with TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - /** - * @file backend/taler-merchant-httpd_pay.c - * @brief handling of /pay requests + * @file backend/taler-merchant-httpd_post-orders-ID-abort.c + * @brief handling of POST /orders/$ID/abort requests * @author Marcello Stanisci * @author Christian Grothoff * @author Florian Dold */ #include "platform.h" -#include <jansson.h> -#include <gnunet/gnunet_util_lib.h> -#include <taler/taler_signatures.h> #include <taler/taler_json_lib.h> #include <taler/taler_exchange_service.h> -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_auditors.h" #include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_refund.h" /** * How long to wait before giving up processing with the exchange? */ -#define PAY_TIMEOUT (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, \ - 30)) +#define ABORT_TIMEOUT (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, \ + 30)) /** * How often do we retry the (complex!) database transaction? @@ -48,26 +41,31 @@ #define MAX_RETRIES 5 /** - * Information we keep for an individual call to the /pay handler. + * Information we keep for an individual call to the /abort handler. */ -struct PayContext; +struct AbortContext; /** - * Information kept during a /pay request for each coin. + * Information kept during a /abort request for each coin. */ -struct DepositConfirmation +struct RefundDetails { /** - * Reference to the main PayContext + * Public key of the coin. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Reference to the main AbortContext */ - struct PayContext *pc; + struct AbortContext *ac; /** - * Handle to the deposit operation we are performing for + * Handle to the refund operation we are performing for * this coin, NULL after the operation is done. */ - struct TALER_EXCHANGE_DepositHandle *dh; + struct TALER_EXCHANGE_RefundHandle *rh; /** * URL of the exchange that issued this coin. @@ -75,9 +73,10 @@ struct DepositConfirmation char *exchange_url; /** - * Denomination of this coin. + * Body of the response from the exchange. Note that the body returned MUST + * be freed (if non-NULL). */ - struct TALER_DenominationPublicKey denom; + json_t *exchange_reply; /** * Amount this coin contributes to the total purchase price. @@ -86,80 +85,59 @@ struct DepositConfirmation 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. + * Offset of this coin into the `rd` array of all coins in the + * @e ac. */ unsigned int index; /** - * #GNUNET_YES if we found this coin in the database. + * HTTP status returned by the exchange (if any). */ - int found_in_db; + unsigned int http_status; /** - * #GNUNET_YES if this coin was refunded. + * Did we try to process this refund yet? */ - int refunded; + boolean processed; }; /** - * Information we keep for an individual call to the /pay handler. + * Information we keep for an individual call to the /abort handler. */ -struct PayContext +struct AbortContext { /** - * This field MUST be first for handle_mhd_completion_callback() to work - * when it treats this struct as a `struct TM_HandlerContext`. + * Hashed contract terms (according to client). */ - struct TM_HandlerContext hc; + struct GNUNET_HashCode h_contract_terms; + + /** + * Context for our operation. + */ + struct TMH_HandlerContext *hc; /** * Stored in a DLL. */ - struct PayContext *next; + struct AbortContext *next; /** * Stored in a DLL. */ - struct PayContext *prev; + struct AbortContext *prev; /** * Array with @e coins_cnt coins we are despositing. */ - struct DepositConfirmation *dc; + struct RefundDetails *rd; /** * MHD connection to return to @@ -167,25 +145,8 @@ struct PayContext struct MHD_Connection *connection; /** - * Instance of the payment's instance (in JSON format) - */ - struct MerchantInstance *mi; - - /** - * What wire method (of the @e mi) was selected by the wallet? - * Set in #parse_pay(). - */ - struct WireMethod *wm; - - /** - * Proposal data for the proposal that is being - * paid for in this context. - */ - json_t *contract_terms; - - /** * Task called when the (suspended) processing for - * the /pay request times out. + * the /abort request times out. * Happens when we don't get a response from the exchange. */ struct GNUNET_SCHEDULER_Task *timeout_task; @@ -196,7 +157,7 @@ struct PayContext struct MHD_Response *response; /** - * Handle to the exchange that we are doing the payment with. + * Handle to the exchange that we are doing the abortment with. * (initially NULL while @e fo is trying to find a exchange). */ struct TALER_EXCHANGE_Handle *mh; @@ -214,124 +175,7 @@ struct PayContext 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. - */ - char *order_id; - - /** - * Fulfillment URL from @e contract_terms. - */ - char *fulfillment_url; - - /** - * 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. + * Number of coins this abort is for. Length of the @e rd array. */ unsigned int coins_cnt; @@ -366,537 +210,220 @@ struct PayContext * #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. + * part of #MH_force_ac_resume during shutdown. */ int suspended; - /** - * #GNUNET_YES if we already tried a forced /keys download. - */ - int tried_force_keys; - - /** - * Which operational mode is the /pay request made in? - */ - enum { PC_MODE_PAY, PC_MODE_ABORT_REFUND } mode; - }; /** - * Head of active pay context DLL. + * Head of active abort context DLL. */ -static struct PayContext *pc_head; +static struct AbortContext *ac_head; /** - * Tail of active pay context DLL. + * Tail of active abort context DLL. */ -static struct PayContext *pc_tail; +static struct AbortContext *ac_tail; /** * Abort all pending /deposit operations. * - * @param pc pay context to abort + * @param ac abort context to abort */ static void -abort_deposit (struct PayContext *pc) +abort_refunds (struct AbortContext *ac) { GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Aborting pending /deposit operations\n"); - for (unsigned int i = 0; i<pc->coins_cnt; i++) + for (unsigned int i = 0; i<ac->coins_cnt; i++) { - struct DepositConfirmation *dci = &pc->dc[i]; + struct RefundDetails *rdi = &ac->rd[i]; - if (NULL != dci->dh) + if (NULL != rdi->dh) { - TALER_EXCHANGE_deposit_cancel (dci->dh); - dci->dh = NULL; + TALER_EXCHANGE_refund_cancel (rdi->rh); + rdi->rh = NULL; } } } /** - * Force all pay contexts to be resumed as we are about + * Force all abort contexts to be resumed as we are about * to shut down MHD. */ void -MH_force_pc_resume () +TMH_force_ac_resume () { - for (struct PayContext *pc = pc_head; - NULL != pc; - pc = pc->next) + for (struct AbortContext *ac = ac_head; + NULL != ac; + ac = ac->next) { - abort_deposit (pc); - if (NULL != pc->timeout_task) + abort_deposit (ac); + if (NULL != ac->timeout_task) { - GNUNET_SCHEDULER_cancel (pc->timeout_task); - pc->timeout_task = NULL; + GNUNET_SCHEDULER_cancel (ac->timeout_task); + ac->timeout_task = NULL; } - if (GNUNET_YES == pc->suspended) + if (GNUNET_YES == ac->suspended) { - pc->suspended = GNUNET_SYSERR; - MHD_resume_connection (pc->connection); + ac->suspended = GNUNET_SYSERR; + MHD_resume_connection (ac->connection); } } } /** - * Resume the given pay context and send the given response. - * Stores the response in the @a pc and signals MHD to resume + * Resume the given abort context and send the given response. + * Stores the response in the @a ac and signals MHD to resume * the connection. Also ensures MHD runs immediately. * - * @param pc payment context + * @param ac abortment 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) +resume_abort_with_response (struct AbortContext *ac, + unsigned int response_code, + struct MHD_Response *response) { - pc->response_code = response_code; - pc->response = response; + abort_refunds (ac); + ac->response_code = response_code; + ac->response = response; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Resuming /pay handling as exchange interaction is done (%u)\n", + "Resuming /abort handling as exchange interaction is done (%u)\n", response_code); - if (NULL != pc->timeout_task) + if (NULL != ac->timeout_task) { - GNUNET_SCHEDULER_cancel (pc->timeout_task); - pc->timeout_task = NULL; + GNUNET_SCHEDULER_cancel (ac->timeout_task); + ac->timeout_task = NULL; } - GNUNET_assert (GNUNET_YES == pc->suspended); - pc->suspended = GNUNET_NO; - MHD_resume_connection (pc->connection); + GNUNET_assert (GNUNET_YES == ac->suspended); + ac->suspended = GNUNET_NO; + MHD_resume_connection (ac->connection); TMH_trigger_daemon (); /* we resumed, kick MHD */ } /** - * Resume payment processing with an error. + * Resume abortment processing with an error. * - * @param pc operation to resume + * @param ac 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_abort_with_error (struct AbortContext *ac, + unsigned int http_status, + enum TALER_ErrorCode ec, + const char *msg) { - resume_pay_with_response (pc, - http_status, - TALER_MHD_make_error (ec, - msg)); + resume_abort_with_response (ac, + http_status, + TALER_MHD_make_error (ec, + msg)); } /** - * Generate a response that indicates payment success. + * Generate a response that indicates abortment success. * - * @param pc payment context + * @param ac abortment context */ static void -generate_success_response (struct PayContext *pc) +generate_success_response (struct AbortContext *ac) { json_t *refunds; - struct GNUNET_CRYPTO_EddsaSignature sig; - /* Check for applicable refunds */ + refunds = json_array (); + if (NULL == refunds) { - enum TALER_ErrorCode ec; - const char *errmsg; - - refunds = TM_get_refund_json (pc->mi, - &pc->h_contract_terms, - &ec, - &errmsg); - /* We would get an EMPTY array back on success if there - are no refunds, but not NULL. So NULL is always an error. */ - if (NULL == refunds) - { - resume_pay_with_error (pc, + GNUNET_break (0); + resume_abort_with_error (ac, MHD_HTTP_INTERNAL_SERVER_ERROR, - ec, - errmsg); - return; - } - } - - /* 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->mi->privkey.eddsa_priv, - &mr, - &sig); + TALER_EC_JSON_ALLOCATION_FAILURE, + "could not create JSON array"); + return; } - - /* Build the response */ + for (unsigned int i = 0; i<ac->coins_cnt; i++) { - json_t *resp; - - resp = json_pack ("{s:O, s:o, s:o, s:o}", - "contract_terms", - pc->contract_terms, - "sig", - GNUNET_JSON_from_data_auto (&sig), - "h_contract_terms", - GNUNET_JSON_from_data (&pc->h_contract_terms, - sizeof (struct GNUNET_HashCode)), - "refund_permissions", - refunds); - if (NULL == resp) + struct RefundDetails *rdi = &ac->rd[i]; + + if (0 != + json_array_append_new ( + refunds, + json_pack ("{s:I, s:O, s:o s:o s:o}", + "exchange_status", + (json_int_t) rdi->exchange_status, + "exchange_body", + rdi->exchange_body))) { + json_decref (refunds); GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_JSON_ALLOCATION_FAILURE, - "could not build final response"); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_JSON_ALLOCATION_FAILURE, + "could not create JSON array"); return; } - resume_pay_with_response (pc, - MHD_HTTP_OK, - TALER_MHD_make_json (resp)); - json_decref (resp); - } -} - - -/** - * Custom cleanup routine for a `struct PayContext`. - * - * @param hc the `struct PayContext` to clean up. - */ -static void -pay_context_cleanup (struct TM_HandlerContext *hc) -{ - struct PayContext *pc = (struct PayContext *) hc; - - if (NULL != pc->timeout_task) - { - GNUNET_SCHEDULER_cancel (pc->timeout_task); - pc->timeout_task = NULL; } - TALER_MHD_parse_post_cleanup_callback (pc->json_parse_context); - abort_deposit (pc); - for (unsigned int i = 0; i<pc->coins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->dc[i]; - if (NULL != dc->denom.rsa_public_key) - { - GNUNET_CRYPTO_rsa_public_key_free (dc->denom.rsa_public_key); - dc->denom.rsa_public_key = NULL; - } - 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_non_null (dc->exchange_url); - } - GNUNET_free_non_null (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; - } - if (NULL != pc->contract_terms) - { - json_decref (pc->contract_terms); - pc->contract_terms = NULL; - } - GNUNET_free_non_null (pc->order_id); - GNUNET_free_non_null (pc->session_id); - GNUNET_free_non_null (pc->fulfillment_url); - GNUNET_CONTAINER_DLL_remove (pc_head, - pc_tail, - pc); - GNUNET_free (pc); + /* Resume and send back the response. */ + resume_abort_with_response (ac, + MHD_HTTP_OK, + TALER_MHD_make_json_pack ("{s:o}", + "refunds", + refunds)); } /** - * Check whether the amount paid is sufficient to cover - * the contract. + * Custom cleanup routine for a `struct AbortContext`. * - * @param pc payment context to check - * @return #GNUNET_OK if the payment is sufficient, #GNUNET_SYSERR if it is - * insufficient + * @param hc the `struct AbortContext` to clean up. */ -static int -check_payment_sufficient (struct PayContext *pc) +static void +abort_context_cleanup (struct TM_HandlerContext *hc) { - 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) - { - GNUNET_break_op (0); - resume_pay_with_error (pc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PAY_PAYMENT_INSUFFICIENT, - "insufficient funds (no coins!)"); - return GNUNET_SYSERR; - } - - 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; i<pc->coins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->dc[i]; - - GNUNET_assert (GNUNET_YES == 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_PAY_AMOUNT_OVERFLOW, - "Overflow adding up amounts"); - } - 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_PAY_FEES_EXCEED_PAYMENT, - "Deposit fees exceed coin's contribution"); - return GNUNET_SYSERR; - } - - /* If exchange differs, add wire fee */ - { - int new_exchange = GNUNET_YES; + struct AbortContext *ac = (struct AbortContext *) hc; - for (unsigned int j = 0; j<i; j++) - if (0 == strcasecmp (dc->exchange_url, - pc->dc[j].exchange_url)) - { - new_exchange = GNUNET_NO; - break; - } - if (GNUNET_YES == new_exchange) - { - if (GNUNET_OK != - TALER_amount_cmp_currency (&total_wire_fee, - &dc->wire_fee)) - { - GNUNET_break_op (0); - resume_pay_with_error (pc, - MHD_HTTP_PRECONDITION_FAILED, - TALER_EC_PAY_WIRE_FEE_CURRENCY_MISMATCH, - "exchange wire in different currency"); - return GNUNET_SYSERR; - } - 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_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED, - "could not add exchange wire fee to total"); - return GNUNET_SYSERR; - } - } - } - } - - 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)) + if (NULL != ac->timeout_task) { - resume_pay_with_error (pc, - MHD_HTTP_PRECONDITION_FAILED, - TALER_EC_PAY_WIRE_FEE_CURRENCY_MISMATCH, - "exchange wire does not match our currency"); - return GNUNET_SYSERR; + GNUNET_SCHEDULER_cancel (ac->timeout_task); + ac->timeout_task = NULL; } - - switch (TALER_amount_subtract (&wire_fee_delta, - &total_wire_fee, - &pc->max_wire_fee)) + abort_refunds (ac); + for (unsigned int i = 0; i<ac->coins_cnt; i++) { - 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); - } + struct RefundDetails *rd = &ac->rd[i]; - /* 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_PAY_AMOUNT_OVERFLOW, - "Overflow adding up amounts"); - return GNUNET_SYSERR; - } - 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)) + if (NULL != rdi->exchange_reply) { - GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_AMOUNT_OVERFLOW, - "Overflow adding up amounts"); - return GNUNET_SYSERR; + json_decref (rdi->exchange_reply); + rdi->reply = NULL; } + GNUNET_free_non_null (rd->exchange_url); } - 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_free_non_null (ac->rd); + if (NULL != ac->fo) { - GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_REFUNDS_EXCEED_PAYMENTS, - "refunded amount exceeds total payments"); - return GNUNET_SYSERR; + TMH_EXCHANGES_find_exchange_cancel (ac->fo); + ac->fo = NULL; } - - if (-1 == TALER_amount_cmp (&final_amount, - &total_needed)) + if (NULL != ac->response) { - /* acc_amount < total_needed */ - if (-1 < TALER_amount_cmp (&acc_amount, - &total_needed)) - { - resume_pay_with_error (pc, - MHD_HTTP_PAYMENT_REQUIRED, - TALER_EC_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_PAY_PAYMENT_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_PAY_PAYMENT_INSUFFICIENT, - "payment insufficient"); - - } - return GNUNET_SYSERR; + MHD_destroy_response (ac->response); + ac->response = NULL; } - - - return GNUNET_OK; + GNUNET_CONTAINER_DLL_remove (ac_head, + ac_tail, + ac); + GNUNET_free (ac); } @@ -904,196 +431,61 @@ check_payment_sufficient (struct PayContext *pc) * Find the exchange we need to talk to for the next * pending deposit permission. * - * @param pc payment context we are processing + * @param ac abortment context we are processing */ static void -find_next_exchange (struct PayContext *pc); +find_next_exchange (struct AbortContext *ac); /** - * Begin of the DB transaction. If required (from - * soft/serialization errors), the transaction can be - * restarted here. + * Function called with the result from the exchange (to be + * passed back to the wallet). * - * @param pc payment context to transact - */ -static void -begin_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 exchange_sig signature from the exchange over the deposit confirmation - * @param sign_key which key did the exchange use to sign the @a proof + * @param cls closure + * @param hr HTTP response data + * @param sign_key exchange key used to sign @a obj, or NULL + * @param signature the actual signature, or NULL on error */ static void -deposit_cb (void *cls, - const struct TALER_EXCHANGE_HttpResponse *hr, - const struct TALER_ExchangeSignatureP *exchange_sig, - const struct TALER_ExchangePublicKeyP *sign_key) +refund_cb (void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr, + const struct TALER_ExchangePublicKeyP *sign_key, + const struct TALER_ExchangeSignatureP *signature) { - struct DepositConfirmation *dc = cls; - struct PayContext *pc = dc->pc; - enum GNUNET_DB_QueryStatus qs; - - 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; stop all other ongoing deposits */ - abort_deposit (pc); - - if (5 == hr->http_status / 100) - { - /* internal server error at exchange */ - resume_pay_with_response (pc, - MHD_HTTP_SERVICE_UNAVAILABLE, - TALER_MHD_make_json_pack ( - "{s:s, s:I, s:I, s:I}", - "hint", - "exchange had an internal server error", - "code", - (json_int_t) TALER_EC_PAY_EXCHANGE_FAILED, - "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_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ( - "{s:s, s:I, s:I, s:I}", - "hint", - "exchange failed, response body not even in JSON", - "code", - (json_int_t) TALER_EC_PAY_EXCHANGE_FAILED, - "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_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", - "exchange failed on deposit of a coin", - "code", - (json_int_t) TALER_EC_PAY_EXCHANGE_FAILED, - "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_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ("{s:s, s:I, s:I, s:I, s:o, s:O}", - "hint", - "exchange failed on deposit of a coin", - "code", - (json_int_t) TALER_EC_PAY_EXCHANGE_FAILED, - "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 for h_contract_terms `%s' and merchant `%s'\n", - GNUNET_h2s (&pc->h_contract_terms), - TALER_B2S (&pc->mi->pubkey)); - /* NOTE: not run in any transaction block, simply as a - transaction by itself! */ - db->preflight (db->cls); - qs = db->store_deposit (db->cls, - &pc->h_contract_terms, - &pc->mi->pubkey, - &dc->coin_pub, - dc->exchange_url, - &dc->amount_with_fee, - &dc->deposit_fee, - &dc->refund_fee, - &dc->wire_fee, - sign_key, - hr->reply); - if (0 > qs) - { - /* Special report if retries insufficient */ - abort_deposit (pc); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - begin_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_PAY_DB_STORE_PAY_ERROR, - "Merchant database error"); - return; - } - dc->found_in_db = GNUNET_YES; - pc->pending--; - - if (0 != pc->pending_at_ce) - return; /* still more to do with current exchange */ - find_next_exchange (pc); + struct RefundDetails *rd = cls; + struct AbortContext *ac = rd->ac; + + (void) sign_key; + (void) signature; + rd->rh = NULL; + rd.http_status = hr->status; + rd.exchange_reply = json_incref ((json_t*) hr->reply); + ac->pending_at_ce--; + if (0 == ac->pending_at_ce) + find_next_exchange (ac); } /** * Function called with the result of our exchange lookup. * - * @param cls the `struct PayContext` + * @param cls the `struct AbortContext` * @param hr HTTP response details - * @param mh NULL if exchange was not found to be acceptable - * @param wire_fee current applicable fee for dealing with @a mh, + * @param exchange_handle NULL if exchange was not found to be acceptable + * @param wire_fee current applicable fee for dealing with @a exchange_handle, * NULL if not available * @param exchange_trusted #GNUNET_YES 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 *mh, - const struct TALER_Amount *wire_fee, - int exchange_trusted) +process_abort_with_exchange (void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr, + struct TALER_EXCHANGE_Handle *exchange_handle, + const struct TALER_Amount *wire_fee, + int exchange_trusted) { - struct PayContext *pc = cls; - const struct TALER_EXCHANGE_Keys *keys; + struct AbortContext *ac = cls; + struct TMH_HandlerContext *hc = pc->hc; pc->fo = NULL; GNUNET_assert (GNUNET_YES == pc->suspended); @@ -1111,7 +503,7 @@ process_pay_with_exchange (void *cls, "hint", "failed to obtain meta-data from exchange", "code", - (json_int_t) TALER_EC_PAY_EXCHANGE_KEYS_FAILURE, + (json_int_t) TALER_EC_ABORT_EXCHANGE_KEYS_FAILURE, "exchange_http_status", (json_int_t) hr->http_status, "exchange_code", @@ -1120,206 +512,93 @@ process_pay_with_exchange (void *cls, hr->reply)); return; } - pc->mh = mh; - keys = TALER_EXCHANGE_get_keys (mh); - if (NULL == keys) - { - GNUNET_break (0); /* should not be possible if HTTP status is #MHD_HTTP_OK */ - resume_pay_with_error (pc, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_EC_PAY_EXCHANGE_KEYS_FAILURE, - "no keys"); - return; - } - - GNUNET_log ( - GNUNET_ERROR_TYPE_DEBUG, - "Found transaction data for proposal `%s' of merchant `%s', initiating deposits\n", - GNUNET_h2s (&pc->h_contract_terms), - TALER_B2S (&pc->mi->pubkey)); - - /* Initiate /deposit operation for all coins of + /* Initiate refund operation for all coins of the current exchange (!) */ - GNUNET_assert (0 == pc->pending_at_ce); - for (unsigned int i = 0; i<pc->coins_cnt; i++) + GNUNET_assert (0 == ac->pending_at_ce); + for (unsigned int i = 0; i<ac->coins_cnt; i++) { - struct DepositConfirmation *dc = &pc->dc[i]; - const struct TALER_EXCHANGE_DenomPublicKey *denom_details; - enum TALER_ErrorCode ec; - unsigned int hc; - - if (NULL != dc->dh) - continue; /* we were here before (can happen due to - tried_force_keys logic), don't go again */ - if (GNUNET_YES == dc->found_in_db) + struct RefundDetails *rdi = &ac->rd[i]; + + if (rdi->processed) continue; - if (0 != strcmp (dc->exchange_url, - pc->current_exchange)) + GNUNET_assert (NULL == rdi->rh); + if (0 != strcmp (rdi->exchange_url, + ac->current_exchange)) continue; - denom_details = TALER_EXCHANGE_get_denomination_key (keys, - &dc->denom); - if (NULL == denom_details) + rdi->processed = true; + ac->pending--; + rdi->rh = TALER_EXCHANGE_refund (exchange_handle, + &rdi->amount_with_fee, + &rdi->refund_fee, + &ac->h_contract_terms, + &rdi->coin_pub, + 0, /* rtransaction_id */ + &ac->hc->instance->merchant_priv, + &refund_Cb, + rdi); + if (NULL == rdi->rh) { - struct GNUNET_HashCode h_denom; - - 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 = GNUNET_YES; - 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 */ - GNUNET_CRYPTO_rsa_public_key_hash (dc->denom.rsa_public_key, - &h_denom); - resume_pay_with_response ( - pc, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ( - "{s:s, s:I, s:o, s:o}", - "hint", "coin's denomination not found", - "code", TALER_EC_PAY_DENOMINATION_KEY_NOT_FOUND, - "h_denom_pub", GNUNET_JSON_from_data_auto (&h_denom), - "exchange_keys", TALER_EXCHANGE_get_keys_raw (mh))); - return; - } - if (GNUNET_OK != - TMH_AUDITORS_check_dk (mh, - denom_details, - exchange_trusted, - &hc, - &ec)) - { - resume_pay_with_response ( - pc, - hc, - TALER_MHD_make_json_pack ("{s:s, s:I, s:o}", - "hint", "denomination not accepted", - "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); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Timing for this payment, wire_deadline: %llu, refund_deadline: %llu\n", - (unsigned long long) pc->wire_transfer_deadline.abs_value_us, - (unsigned long long) pc->refund_deadline.abs_value_us); - db->preflight (db->cls); - dc->dh = TALER_EXCHANGE_deposit (mh, - &dc->amount_with_fee, - pc->wire_transfer_deadline, - pc->wm->j_wire, - &pc->h_contract_terms, - &dc->coin_pub, - &dc->ub_sig, - &dc->denom, - pc->timestamp, - &pc->mi->pubkey, - pc->refund_deadline, - &dc->coin_sig, - &deposit_cb, - dc); - 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, - MHD_HTTP_UNAUTHORIZED, - TALER_MHD_make_json_pack ( - "{s:s, s:I, s:i}", - "hint", "deposit signature invalid", - "code", (json_int_t) TALER_EC_PAY_COIN_SIGNATURE_INVALID, - "coin_idx", i)); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ABORT_EXCHANGE_REFUND_FAILED, + "Failed to start refund with exchange"); return; } - if (TMH_force_audit) - TALER_EXCHANGE_deposit_force_dc (dc->dh); pc->pending_at_ce++; } } /** + * Begin of the DB transaction. If required (from + * soft/serialization errors), the transaction can be + * restarted here. + * + * @param ac abortment context to transact + */ +static void +begin_transaction (struct AbortContext *ac); + + +/** * Find the exchange we need to talk to for the next * pending deposit permission. * - * @param pc payment context we are processing + * @param ac abortment context we are processing */ static void -find_next_exchange (struct PayContext *pc) +find_next_exchange (struct AbortContext *ac) { - for (unsigned int i = 0; i<pc->coins_cnt; i++) + for (unsigned int i = 0; i<ac->coins_cnt; i++) { - struct DepositConfirmation *dc = &pc->dc[i]; + struct RefundDetails *rdi = &ac->rd[i]; - if (GNUNET_YES != dc->found_in_db) + if (! rdi->processed) { - db->preflight (db->cls); - pc->current_exchange = dc->exchange_url; - pc->fo = TMH_EXCHANGES_find_exchange (pc->current_exchange, - pc->wm->wire_method, + ac->current_exchange = rd->exchange_url; + ac->fo = TMH_EXCHANGES_find_exchange (ac->current_exchange, + ac->wm->wire_method, GNUNET_NO, - &process_pay_with_exchange, - pc); - if (NULL == pc->fo) + &process_abort_with_exchange, + ac); + if (NULL == ac->fo) { GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_EXCHANGE_LOOKUP_FAILED, - "Failed to lookup exchange by URL"); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ABORT_EXCHANGE_LOOKUP_FAILED, + "Failed to lookup exchange by URL"); return; } return; } } - pc->current_exchange = NULL; - db->preflight (db->cls); + ac->current_exchange = NULL; + GNUNET_assert (0 == ac->pending); /* We are done with all the HTTP requests, go back and try the 'big' database transaction! (It should work now!) */ - begin_transaction (pc); -} - - -/** - * 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_REQUEST_TIMEOUT, - TALER_EC_PAY_EXCHANGE_TIMEOUT, - "likely the exchange did not reply quickly enough"); + begin_transaction (ac); } @@ -1337,919 +616,476 @@ handle_pay_timeout (void *cls) * @param exchange_proof proof from exchange that coin was accepted */ static void -check_coin_paid (void *cls, - const struct GNUNET_HashCode *h_contract_terms, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const char *exchange_url, - 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, - const json_t *exchange_proof) -{ - struct PayContext *pc = cls; - - if (0 != GNUNET_memcmp (&pc->h_contract_terms, - h_contract_terms)) - { - GNUNET_break (0); - return; - } - for (unsigned int i = 0; i<pc->coins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->dc[i]; - - if (GNUNET_YES == dc->found_in_db) - continue; /* processed earlier */ - - /* Get matching coin from results*/ - if ( (0 != GNUNET_memcmp (coin_pub, - &dc->coin_pub)) || - (0 != TALER_amount_cmp (amount_with_fee, - &dc->amount_with_fee)) ) - continue; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Coin (%s) already found in our DB.\n", - TALER_b2s (coin_pub, - sizeof (*coin_pub))); - if (0 > - TALER_amount_add (&pc->total_paid, - &pc->total_paid, - amount_with_fee)) - { - /* We accepted this coin for payment on this contract before, - and now we can't even add the amount!? */ - GNUNET_break (0); - continue; - } - if (0 > - TALER_amount_add (&pc->total_fees_paid, - &pc->total_fees_paid, - deposit_fee)) - { - /* We accepted this coin for payment on this contract before, - and now we can't even add the amount!? */ - GNUNET_break (0); - continue; - } - 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 = GNUNET_YES; - pc->pending--; - } -} - - -/** - * 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 root JSON upload with payment data - * @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, - const json_t *root, - struct PayContext *pc) +refund_coins (void *cls, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const char *exchange_url, + 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, + const json_t *exchange_proof) { - json_t *coins; - const char *order_id; - const char *mode; - struct TALER_MerchantPublicKeyP merchant_pub; - enum GNUNET_GenericReturnValue res; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("mode", - &mode), - GNUNET_JSON_spec_json ("coins", - &coins), - GNUNET_JSON_spec_string ("order_id", - &order_id), - GNUNET_JSON_spec_fixed_auto ("merchant_pub", - &merchant_pub), - GNUNET_JSON_spec_end () - }; - enum GNUNET_DB_QueryStatus qs; - - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); - return res; - } - - if (0 != GNUNET_memcmp (&merchant_pub, - &pc->mi->pubkey)) - { - GNUNET_JSON_parse_free (spec); - TALER_LOG_INFO ( - "Unknown merchant public key included in payment (usually wrong instance chosen)\n"); - return - (MHD_YES == - TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PAY_WRONG_INSTANCE, - "merchant_pub in contract does not match this instance")) - ? GNUNET_NO - : GNUNET_SYSERR; - } - - { - const char *session_id; - - session_id = json_string_value (json_object_get (root, - "session_id")); - if (NULL != session_id) - pc->session_id = GNUNET_strdup (session_id); - } - GNUNET_assert (NULL == pc->order_id); - pc->order_id = GNUNET_strdup (order_id); - GNUNET_assert (NULL == pc->contract_terms); - qs = db->find_contract_terms (db->cls, - &pc->contract_terms, - order_id, - &merchant_pub); - if (0 > qs) - { - GNUNET_JSON_parse_free (spec); - /* 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_PAY_DB_FETCH_PAY_ERROR, - "Failed to obtain contract terms from DB")) - ? GNUNET_NO - : GNUNET_SYSERR; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_JSON_parse_free (spec); - return - (MHD_YES == - TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_PAY_PROPOSAL_NOT_FOUND, - "Proposal not found")) - ? GNUNET_NO - : GNUNET_SYSERR; - } - - if (GNUNET_OK != - TALER_JSON_hash (pc->contract_terms, - &pc->h_contract_terms)) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return - (MHD_YES == - TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_FAILED_COMPUTE_PROPOSAL_HASH, - "Failed to hash proposal")) - ? GNUNET_NO - : GNUNET_SYSERR; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Handling /pay for order `%s' with contract hash `%s'\n", - order_id, - GNUNET_h2s (&pc->h_contract_terms)); + struct AbortContext *ac = cls; - if (NULL == json_object_get (pc->contract_terms, - "merchant")) + for (unsigned int i = 0; i<ac->coins_cnt; i++) { - /* invalid contract */ - GNUNET_JSON_parse_free (spec); - return - (MHD_YES == - TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_MERCHANT_FIELD_MISSING, - "No merchant field in proposal")) - ? GNUNET_NO - : GNUNET_SYSERR; - } - if (0 != strcasecmp ("abort-refund", - mode)) - pc->mode = PC_MODE_PAY; - else - pc->mode = PC_MODE_ABORT_REFUND; - { - const char *fulfillment_url; - struct GNUNET_JSON_Specification espec[] = { - GNUNET_JSON_spec_absolute_time ("refund_deadline", - &pc->refund_deadline), - GNUNET_JSON_spec_absolute_time ("pay_deadline", - &pc->pay_deadline), - GNUNET_JSON_spec_absolute_time ("wire_transfer_deadline", - &pc->wire_transfer_deadline), - GNUNET_JSON_spec_absolute_time ("timestamp", - &pc->timestamp), - TALER_JSON_spec_amount ("max_fee", - &pc->max_fee), - TALER_JSON_spec_amount ("amount", - &pc->amount), - GNUNET_JSON_spec_string ("fulfillment_url", - &fulfillment_url), - GNUNET_JSON_spec_fixed_auto ("h_wire", - &pc->h_wire), - GNUNET_JSON_spec_end () - }; + struct RefundDetails *rdi = &ac->rd[i]; - res = TALER_MHD_parse_json_data (connection, - pc->contract_terms, - espec); - if (GNUNET_YES != res) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return res; - } + if ( (0 != + GNUNET_memcmp (coin_pub, + &rdi->coin_pub)) || + (0 != + strcmp (exchange_url, + rdi->exchange_url)) ) + continue; /* not in request */ - pc->fulfillment_url = GNUNET_strdup (fulfillment_url); - 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); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE, - "refund deadline after wire transfer deadline"); - } - - if (pc->pay_deadline.abs_value_us < - GNUNET_TIME_absolute_get ().abs_value_us) - { - /* too late */ - GNUNET_JSON_parse_free (spec); - return - (MHD_YES == - TALER_MHD_reply_with_error (connection, - MHD_HTTP_GONE, - TALER_EC_PAY_OFFER_EXPIRED, - "The payment deadline has past and the offer is no longer valid")) - ? GNUNET_NO - : GNUNET_SYSERR; - } - - } - - /* find wire method */ - { - struct WireMethod *wm; - - wm = pc->mi->wm_head; - while (0 != GNUNET_memcmp (&pc->h_wire, - &wm->h_wire)) - wm = wm->next; - if (NULL == wm) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_WIRE_HASH_UNKNOWN, - "Did not find matching wire details"); - } - pc->wm = wm; - } - - /* parse optional details */ - if (NULL != json_object_get (pc->contract_terms, - "max_wire_fee")) - { - struct GNUNET_JSON_Specification espec[] = { - TALER_JSON_spec_amount ("max_wire_fee", - &pc->max_wire_fee), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_json_data (connection, - pc->contract_terms, - espec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); /* invalid input, fail */ - GNUNET_JSON_parse_free (spec); - return res; - } - } - else - { - /* default is we cover no fee */ - GNUNET_assert (GNUNET_OK == - TALER_amount_get_zero (pc->max_fee.currency, - &pc->max_wire_fee)); - } - - if (NULL != json_object_get (pc->contract_terms, - "wire_fee_amortization")) - { - struct GNUNET_JSON_Specification espec[] = { - GNUNET_JSON_spec_uint32 ("wire_fee_amortization", - &pc->wire_fee_amortization), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_json_data (connection, - pc->contract_terms, - espec); - if ( (GNUNET_YES != res) || - (0 == pc->wire_fee_amortization) ) - { - GNUNET_break_op (0); /* invalid input, use default */ - /* default is no amortization */ - pc->wire_fee_amortization = 1; - } - } - else - { - pc->wire_fee_amortization = 1; - } - - pc->coins_cnt = json_array_size (coins); - if (0 == pc->coins_cnt) - { - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PAY_COINS_ARRAY_EMPTY, - "coins"); - } - /* 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) + /* Store refund in DB */ + qs = TMH_db->refund_coin (TMH_db->cls, + ac->hc->instance->settings.id, + &ac->h_contract_terms, + coin_pub, + /* justification */ + "incomplete abortment aborted"); + if (0 > qs) { - struct DepositConfirmation *dc = &pc->dc[coins_index]; - const char *exchange_url; - struct GNUNET_JSON_Specification ispec[] = { - TALER_JSON_spec_denomination_public_key ("denom_pub", - &dc->denom), - TALER_JSON_spec_amount ("contribution", - &dc->amount_with_fee), - GNUNET_JSON_spec_string ("exchange_url", - &exchange_url), - 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 ("coin_sig", - &dc->coin_sig), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_json_data (connection, - coin, - ispec); - if (GNUNET_YES != res) + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { - GNUNET_JSON_parse_free (spec); - GNUNET_break_op (0); - return res; + begin_transaction (ac); + return; } - dc->exchange_url = GNUNET_strdup (exchange_url); - dc->index = coins_index; - dc->pc = pc; - } - } - pc->pending = pc->coins_cnt; - GNUNET_JSON_parse_free (spec); - return GNUNET_OK; -} - - -/** - * 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 exchange_url URL of the exchange that issued @a coin_pub - * @param rtransaction_id identificator of the refund - * @param reason human-readable explanation of the refund - * @param refund_amount refund amount which is being taken from @a coin_pub - * @param refund_fee cost of this refund operation - */ -static void -check_coin_refunded (void *cls, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const char *exchange_url, - uint64_t rtransaction_id, - const char *reason, - const struct TALER_Amount *refund_amount, - const struct TALER_Amount *refund_fee) -{ - struct PayContext *pc = cls; - - (void) exchange_url; - for (unsigned int i = 0; i<pc->coins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->dc[i]; - - /* Get matching coin from results*/ - if (0 == GNUNET_memcmp (coin_pub, - &dc->coin_pub)) - { - dc->refunded = GNUNET_YES; - GNUNET_assert (0 <= - TALER_amount_add (&pc->total_refunded, - &pc->total_refunded, - refund_amount)); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ABORT_DB_STORE_ABORT_ERROR, + "Merchant database error storing abort-refund"); + return; } - } + } /* for all coins */ } /** - * Begin of the DB transaction. If required (from - * soft/serialization errors), the transaction can be - * restarted here. + * Begin of the DB transaction. If required (from soft/serialization errors), + * the transaction can be restarted here. * - * @param pc payment context to transact + * @param ac abortment context to transact */ static void -begin_transaction (struct PayContext *pc) +begin_transaction (struct AbortContext *ac) { enum GNUNET_DB_QueryStatus qs; /* Avoid re-trying transactions on soft errors forever! */ - if (pc->retry_counter++ > MAX_RETRIES) + if (ac->retry_counter++ > MAX_RETRIES) { GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_DB_STORE_TRANSACTION_ERROR, - "Soft merchant database error: retry counter exceeded"); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ABORT_DB_STORE_TRANSACTION_ERROR, + "Soft merchant database error: retry counter exceeded"); return; } - GNUNET_assert (GNUNET_YES == pc->suspended); - - /* Init. some price accumulators. */ - 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)); + GNUNET_assert (GNUNET_YES == ac->suspended); /* First, try to see if we have all we need already done */ - db->preflight (db->cls); + TMH_db->preflight (TMH_db->cls); if (GNUNET_OK != - db->start (db->cls, - "run pay")) + TMH_db->start (TMH_db->cls, + "run abort")) { GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR, - "Merchant database error (could not begin transaction)"); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ABORT_DB_FETCH_TRANSACTION_ERROR, + "Merchant database error (could not begin transaction)"); return; } - /* Check if some of these coins already succeeded for _this_ contract. */ - qs = db->find_payments (db->cls, - &pc->h_contract_terms, - &pc->mi->pubkey, - &check_coin_paid, - pc); + /* check payment was indeed incomplete */ + qs = TMH_db->lookup_paid_order (TMH_db->cls, + ac->hc->instance->settings.id, + &ac->h_contract_terms, + NULL); + if (0 < qs) + { + /* Payment is complete, refuse to abort. */ + json_decref (terms); + TMH_db->rollback (TMH_db->cls); + resume_abort_with_error (ac, + MHD_HTTP_FORBIDDEN, + TALER_EC_ABORT_ABORT_REFUND_REFUSED_ABORTMENT_COMPLETE, + "Payment was complete, refusing to abort"); + return; + } if (0 > qs) { - db->rollback (db->cls); + TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { - begin_transaction (pc); + begin_transaction (ac); 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_PAY_DB_FETCH_TRANSACTION_ERROR, - "Merchant database error"); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ABORT_DB_STORE_ABORT_ERROR, + "Merchant database error"); return; } - /* Check if we refunded some of the coins */ - qs = db->get_refunds_from_contract_terms_hash (db->cls, - &pc->mi->pubkey, - &pc->h_contract_terms, - &check_coin_refunded, - pc); + /* Mark all deposits we have in our database for the order as refunded. */ + qs = TMH_db->lookup_deposits (TMH_db->cls, + ac->hc->instance->settings.id, + &ac->h_contract_terms, + &refund_coins, + ac); if (0 > qs) { - db->rollback (db->cls); + TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { - begin_transaction (pc); + begin_transaction (ac); 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_PAY_DB_FETCH_TRANSACTION_ERROR, - "Merchant database error checking for refunds"); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ABORT_DB_FETCH_TRANSACTION_ERROR, + "Merchant database error"); return; } - /* All the coins known to the database have - * been processed, now delve into specific case - * (pay vs. abort) */ - - if (PC_MODE_ABORT_REFUND == pc->mode) + qs = TMH_db->commit (TMH_db->cls); + if (0 > qs) { - json_t *terms; - - /* The wallet is going for a refund, - (on aborted operation)! */ - - /* check payment was indeed incomplete */ - qs = db->find_paid_contract_terms_from_hash (db->cls, - &terms, - &pc->h_contract_terms, - &pc->mi->pubkey); - if (0 > qs) - { - db->rollback (db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - begin_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_PAY_DB_STORE_PAY_ERROR, - "Merchant database error"); - return; - } - if (0 < qs) + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { - /* Payment had been complete! */ - json_decref (terms); - db->rollback (db->cls); - resume_pay_with_error (pc, - MHD_HTTP_FORBIDDEN, - TALER_EC_PAY_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE, - "Payment complete, refusing to abort"); + begin_transaction (ac); return; } + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ABORT_DB_STORE_ABORT_ERROR, + "Merchant database error: could not commit"); + return; + } - /* Store refund in DB */ - qs = db->increase_refund_for_contract_NT (db->cls, - &pc->h_contract_terms, - &pc->mi->pubkey, - &pc->total_paid, - /* justification */ - "incomplete payment aborted"); - if (0 > qs) + /* At this point, the refund got correctly committed + into the database. Tell exchange about abort/refund. */ + if (ac->pending > 0) + { + find_next_exchange (ac); + return; + } + generate_success_response (ac); +} + + +/** + * Try to parse the abort request into the given abort context. + * Schedules an error response in the connection on failure. + * + * @param connection HTTP connection we are receiving abortment on + * @param root JSON upload with abortment data + * @param ac context we use to handle the abortment + * @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_abort (struct MHD_Connection *connection, + struct TMH_HandlerContext *hc, + struct AbortContext *ac) +{ + { + json_t *coins; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_json ("coins", + &coins), + GNUNET_JSON_spec_fixed_auto ("h_contract", + &ac->h_contract_terms), + + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_YES != res) { - db->rollback (db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - begin_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_PAY_DB_STORE_PAY_ERROR, - "Merchant database error storing abort-refund"); - return; + GNUNET_break_op (0); + return res; } - qs = db->commit (db->cls); - if (0 > qs) + ac->coins_cnt = json_array_size (coins); + if (0 == ac->coins_cnt) { - db->rollback (db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - begin_transaction (pc); - return; - } - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_DB_STORE_PAY_ERROR, - "Merchant database error: could not commit"); - return; + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ABORT_COINS_ARRAY_EMPTY, + "coins"); } - /* At this point, the refund got correctly committed - * into the database. */ + /* note: 1 coin = 1 deposit confirmation expected */ + ac->pending = ac->coins_cnt; + ac->rd = GNUNET_new_array (ac->coins_cnt, + struct RefundDetails); + /* This loop populates the array 'rd' in 'ac' */ { - json_t *refunds; - - refunds = json_array (); - if (NULL == refunds) + unsigned int coins_index; + json_t *coin; + json_array_foreach (coins, coins_index, coin) { - GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_JSON_ALLOCATION_FAILURE, - "could not create JSON array"); - return; - } - for (unsigned int i = 0; i<pc->coins_cnt; i++) - { - struct TALER_MerchantSignatureP msig; - struct TALER_RefundRequestPS rr = { - .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_REFUND), - .purpose.size = htonl (sizeof (rr)), - .h_contract_terms = pc->h_contract_terms, - .coin_pub = pc->dc[i].coin_pub, - .merchant = pc->mi->pubkey, - .rtransaction_id = GNUNET_htonll (0) + struct RefundDetails *rd = &ac->rd[coins_index]; + const char *exchange_url; + struct GNUNET_JSON_Specification ispec[] = { + TALER_JSON_spec_amount ("contribution", + &rd->amount_with_fee), + TALER_JSON_spec_amount ("refund_fee", + &rd->refund_fee), + GNUNET_JSON_spec_string ("exchange_url", + &exchange_url), + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &rd->coin_pub), + GNUNET_JSON_spec_end () }; - if (GNUNET_YES != pc->dc[i].found_in_db) - continue; /* Skip coins not found in DB. */ - TALER_amount_hton (&rr.refund_amount, - &pc->dc[i].amount_with_fee); - TALER_amount_hton (&rr.refund_fee, - &pc->dc[i].refund_fee); - - GNUNET_CRYPTO_eddsa_sign (&pc->mi->privkey.eddsa_priv, - &rr, - &msig.eddsa_sig); - /* Pack refund for i-th coin. */ - if (0 != - json_array_append_new ( - refunds, - json_pack ("{s:I, s:o, s:o s:o s:o}", - "rtransaction_id", - (json_int_t) 0, - "coin_pub", - GNUNET_JSON_from_data_auto (&rr.coin_pub), - "merchant_sig", - GNUNET_JSON_from_data_auto (&msig), - "refund_amount", - TALER_JSON_from_amount_nbo (&rr.refund_amount), - "refund_fee", - TALER_JSON_from_amount_nbo (&rr.refund_fee)))) + res = TALER_MHD_parse_json_data (connection, + coin, + ispec); + if (GNUNET_YES != res) { - json_decref (refunds); - GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_JSON_ALLOCATION_FAILURE, - "could not create JSON array"); - return; + GNUNET_JSON_parse_free (spec); + GNUNET_break_op (0); + return res; } + rd->exchange_url = GNUNET_strdup (exchange_url); + rd->index = coins_index; + rd->ac = ac; } - - /* Resume and send back the response. */ - resume_pay_with_response ( - pc, - MHD_HTTP_OK, - TALER_MHD_make_json_pack ( - "{s:o, s:o, s:o}", - /* Refunds pack. */ - "refund_permissions", refunds, - "merchant_pub", - GNUNET_JSON_from_data_auto (&pc->mi->pubkey), - "h_contract_terms", - GNUNET_JSON_from_data_auto (&pc->h_contract_terms))); } - return; - } /* End of PC_MODE_ABORT_REFUND */ - - /* Default PC_MODE_PAY mode */ + GNUNET_JSON_parse_free (spec); + } - /* Final termination case: all coins already known, just - generate ultimate outcome. */ - if (0 == pc->pending) + /* Check request against contract on file */ { - if (GNUNET_OK != check_payment_sufficient (pc)) + enum GNUNET_DB_QueryStatus qs; + json_t *contract_terms; + + qs = TMH_db->lookup_contract_terms (TMH_db->cls, + hc->instance->settings.id, + hc->infix, + &contract_terms); + if (0 > qs) { - db->rollback (db->cls); - return; + /* 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_ABORT_DB_FETCH_ABORT_ERROR, + "Failed to obtain contract terms from DB")) + ? GNUNET_NO + : GNUNET_SYSERR; } - /* Payment succeeded, save in database */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Contract `%s' was fully paid\n", - GNUNET_h2s (&pc->h_contract_terms)); - qs = db->mark_proposal_paid (db->cls, - &pc->h_contract_terms, - &pc->mi->pubkey); - if (qs < 0) + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { - db->rollback (db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - begin_transaction (pc); - return; - } - resume_pay_with_error ( - pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_DB_STORE_PAYMENTS_ERROR, - "Merchant database error: could not mark proposal as 'paid'"); - return; + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_ABORT_PROPOSAL_NOT_FOUND, + "Order not found")) + ? GNUNET_NO + : GNUNET_SYSERR; } - if ( (NULL != pc->session_id) && - (NULL != pc->fulfillment_url) ) + /* check client provided the right hash and is thus authorized to request aborting */ { - qs = db->insert_session_info (db->cls, - pc->session_id, - pc->fulfillment_url, - pc->order_id, - &pc->mi->pubkey); + struct GNUNET_HashCode h_contract_terms; + + if (GNUNET_OK != + TALER_JSON_hash (ac->contract_terms, + &h_contract_terms)) + { + GNUNET_break (0); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ABORT_FAILED_COMPUTE_PROPOSAL_HASH, + "Failed to hash contract terms")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + if (0 != + GNUNET_memcmp (&ac->contract_terms, + &h_contract_terms)) + { + GNUNET_break_op (0); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ABORT_CONTRACT_HASH_MISSMATCH, + "Provided hash does not match order on file")) + ? GNUNET_NO + : GNUNET_SYSERR; + } } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling /abort for order `%s' with contract hash `%s'\n", + order_id, + GNUNET_h2s (&ac->h_contract_terms)); - /* Now commit! */ - if (0 <= qs) - qs = db->commit (db->cls); - else - db->rollback (db->cls); - if (0 > qs) + /* Check abort is still possible */ { - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + struct GNUNET_TIME_Absolute refund_deadline; + struct GNUNET_TIME_Absolute wire_transfer_deadline; + struct GNUNET_TIME_Absolute now; + struct GNUNET_JSON_Specification espec[] = { + GNUNET_JSON_spec_absolute_time ("refund_deadline", + &refund_deadline), + GNUNET_JSON_spec_absolute_time ("wire_transfer_deadline", + &wire_transfer_deadline), + GNUNET_JSON_spec_end () + }; + + /* FIXME: this is a tad wrong, as IF the contract is + malformed, the routine generates an error that blames + it on the client (400) instead of on us! */ + res = TALER_MHD_parse_json_data (connection, + ac->contract_terms, + espec); + if (GNUNET_YES != res) { - begin_transaction (pc); - return; + GNUNET_break (0); + return res; + } + now = GNUNET_TIME_absolute_get (); + if ( (now.abs_value_us < refund_deadline.abs_value_us) && + ( (0 != refund_deadline.abs_value_us) || + (now.abs_value_us < wire_transfer_deadline.abs_value_us) ) ) + { + /* it is most definitively too late for an abort */ + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_GONE, + TALER_EC_ABORT_TOO_LATE, + "It is too late to abort the payment")) + ? GNUNET_NO + : GNUNET_SYSERR; } - resume_pay_with_error ( - pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_DB_STORE_PAYMENTS_ERROR, - "Merchant database error: could not commit to mark proposal as 'paid'"); - return; } - TMH_long_poll_resume (pc->order_id, - &pc->mi->pubkey, - NULL); - generate_success_response (pc); - return; } - - - /* we made no DB changes, - so we can just rollback */ - db->rollback (db->cls); - - /* Ok, we need to first go to the network. - Do that interaction in *tiny* transactions. */ - find_next_exchange (pc); + return GNUNET_OK; } /** - * Process a payment for a proposal. + * Handle a timeout for the processing of the abort request. * - * @param connection HTTP connection we are receiving payment on - * @param root JSON upload with payment data - * @param pc context we use to handle the payment - * @return value to return to MHD (#MHD_NO to drop connection, - * #MHD_YES to keep handling it) + * @param cls our `struct AbortContext` */ -static MHD_RESULT -handler_pay_json (struct MHD_Connection *connection, - const json_t *root, - struct PayContext *pc) +static void +handle_abort_timeout (void *cls) { - { - enum GNUNET_GenericReturnValue ret; - - ret = parse_pay (connection, - root, - pc); - if (GNUNET_OK != ret) - return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; - } + struct AbortContext *ac = cls; - /* Payment not finished, suspend while we interact with the exchange */ - MHD_suspend_connection (connection); - pc->suspended = GNUNET_YES; + ac->timeout_task = NULL; + GNUNET_assert (GNUNET_YES == ac->suspended); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Suspending /pay handling while working with the exchange\n"); - pc->timeout_task = GNUNET_SCHEDULER_add_delayed (PAY_TIMEOUT, - &handle_pay_timeout, - pc); - begin_transaction (pc); - return MHD_YES; + "Resuming abort with error after timeout\n"); + if (NULL != ac->fo) + { + TMH_EXCHANGES_find_exchange_cancel (ac->fo); + ac->fo = NULL; + } + resume_abort_with_error (ac, + MHD_HTTP_REQUEST_TIMEOUT, + TALER_EC_ABORT_EXCHANGE_TIMEOUT, + "likely the exchange did not reply quickly enough"); } /** - * Process a payment for a proposal. Takes data from the given MHD - * connection. + * Abort payment for a claimed order. * * @param rh context of the handler * @param connection the MHD connection to handle - * @param[in,out] connection_cls the connection's closure - * (can be updated) - * @param upload_data upload data - * @param[in,out] upload_data_size number of bytes (left) in @a - * upload_data - * @param mi merchant backend instance, never NULL + * @param[in,out] hc context with further information about the request * @return MHD result code */ MHD_RESULT -MH_handler_pay (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi) +TMH_post_orders_ID_abort (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) { - struct PayContext *pc; - enum GNUNET_GenericReturnValue res; - MHD_RESULT ret; - json_t *root; + struct AbortContext *ac = hc->ctx; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "In handler for /pay.\n"); if (NULL == *connection_cls) { - pc = GNUNET_new (struct PayContext); - GNUNET_CONTAINER_DLL_insert (pc_head, - pc_tail, - pc); - pc->hc.cc = &pay_context_cleanup; - pc->connection = connection; - *connection_cls = pc; - pc->mi = mi; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "/pay: picked instance %s\n", - mi->id); - } - else - { - /* not the first call, recover state */ - pc = *connection_cls; + ac = GNUNET_new (struct AbortContext); + GNUNET_CONTAINER_DLL_insert (ac_head, + ac_tail, + ac); + ac->connection = connection; + ac->hc = hc; + hc->ctx = ac; + hc->cc = &abort_context_cleanup; } - if (GNUNET_SYSERR == pc->suspended) + if (GNUNET_SYSERR == ac->suspended) return MHD_NO; /* during shutdown, we don't generate any more replies */ - if (0 != pc->response_code) + if (0 != ac->response_code) { + MHD_RESULT res; + /* We are *done* processing the request, just queue the response (!) */ - if (UINT_MAX == pc->response_code) + if (UINT_MAX == ac->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; + ac->response_code, + ac->response); + MHD_destroy_response (ac->response); + ac->response = NULL; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Queueing response (%u) for /pay (%s).\n", - (unsigned int) pc->response_code, + "Queueing response (%u) for /abort (%s).\n", + (unsigned int) ac->response_code, res ? "OK" : "FAILED"); return res; } - res = TALER_MHD_parse_post_json (connection, - &pc->json_parse_context, - upload_data, - upload_data_size, - &root); - if (GNUNET_SYSERR == res) { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_JSON_INVALID, - "could not parse JSON"); + enum GNUNET_GenericReturnValue ret; + + ret = parse_abort (connection, + hc, + ac); + if (GNUNET_OK != ret) + return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; } - if ( (GNUNET_NO == res) || - (NULL == root) ) - return MHD_YES; /* the POST's body has to be further fetched */ - - ret = handler_pay_json (connection, - root, - pc); - json_decref (root); - return ret; + + /* Abort not finished, suspend while we interact with the exchange */ + GNUNET_assert (GNUNET_NO == ac->suspended); + MHD_suspend_connection (connection); + ac->suspended = GNUNET_YES; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Suspending abort handling while working with the exchange\n"); + ac->timeout_task = GNUNET_SCHEDULER_add_delayed (ABORT_TIMEOUT, + &handle_abort_timeout, + ac); + begin_transaction (ac); + return MHD_YES; } -/* end of taler-merchant-httpd_pay.c */ +/* end of taler-merchant-httpd_post-orders-ID-abort.c */ |