From 5f97f9112746afa308d6b281cbefad4b6e166370 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Mon, 27 Nov 2023 13:10:30 +0900 Subject: major /pay handling cleanup --- .../taler-merchant-httpd_post-orders-ID-pay.c | 1502 +++++++++++--------- 1 file changed, 833 insertions(+), 669 deletions(-) (limited to 'src/backend') diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c index c942847f..e263538a 100644 --- a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c +++ b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -58,6 +58,63 @@ struct PayContext; +/** + * Different phases of processing the /pay request. + */ +enum PayPhase +{ + /** + * Initial phase where the request is parsed. + */ + PP_INIT = 0, + + /** + * Check database state for the given order. + */ + PP_CHECK_CONTRACT, + + /** + * Contract has been paid. + */ + PP_CONTRACT_PAID, + + /** + * Execute payment transaction. + */ + PP_PAY_TRANSACTION, + + /** + * Notify other processes about successful payment. + */ + PP_PAYMENT_NOTIFICATION, + + /** + * Create final success response. + */ + PP_SUCCESS_RESPONSE, + + /** + * Perform batch deposits with exchange(s). + */ + PP_BATCH_DEPOSITS, + + /** + * Return response in payment context. + */ + PP_RETURN_RESPONSE, + + /** + * Return #MHD_YES to end processing. + */ + PP_END_YES, + + /** + * Return #MHD_NO to end processing. + */ + PP_END_NO +}; + + /** * Information kept during a pay request for each coin. */ @@ -228,7 +285,7 @@ struct PayContext /** * What wire method (of the @e mi) was selected by the wallet? - * Set in #parse_pay(). + * Set in #phase_parse_pay(). */ struct TMH_WireMethod *wm; @@ -391,6 +448,11 @@ struct PayContext */ unsigned int response_code; + /** + * Payment processing phase we are in. + */ + enum PayPhase phase; + /** * #GNUNET_NO if the @e connection was not suspended, * #GNUNET_YES if the @e connection was suspended, @@ -404,13 +466,13 @@ struct PayContext * does not match the contract currency. */ bool deposit_currency_mismatch; - + /** * Set to true if the database contains a (bogus) * refund for a different currency. */ bool refund_currency_mismatch; - + }; @@ -526,29 +588,6 @@ destroy_kc (struct KycContext *kc) } -/** - * 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; -} - - void TMH_force_pc_resume () { @@ -579,6 +618,21 @@ TMH_force_pc_resume () } +/** + * Resume payment processing. + * + * @param[in,out] pc payment process to resume + */ +static void +pay_resume (struct PayContext *pc) +{ + GNUNET_assert (GNUNET_YES == pc->suspended); + pc->suspended = GNUNET_NO; + MHD_resume_connection (pc->connection); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ +} + + /** * Resume the given pay context and send the given response. * Stores the response in the @a pc and signals MHD to resume @@ -603,10 +657,8 @@ resume_pay_with_response (struct PayContext *pc, 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); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ + pc->phase = PP_RETURN_RESPONSE; + pay_resume (pc); } @@ -631,66 +683,44 @@ resume_pay_with_error (struct PayContext *pc, /** - * Custom cleanup routine for a `struct PayContext`. + * Conclude payment processing for @a pc with the + * given @a res MHD status code. * - * @param cls the `struct PayContext` to clean up. + * @param[in,out] pc payment context for final state transition + * @param res MHD return code to end with */ static void -pay_context_cleanup (void *cls) +pay_end (struct PayContext *pc, + MHD_RESULT res) { - struct PayContext *pc = cls; - - if (NULL != pc->timeout_task) - { - GNUNET_SCHEDULER_cancel (pc->timeout_task); - pc->timeout_task = NULL; - } - if (NULL != pc->contract_terms) - { - json_decref (pc->contract_terms); - pc->contract_terms = NULL; - } - for (unsigned int i = 0; icoins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->dc[i]; - - TALER_denom_sig_free (&dc->cdd.denom_sig); - GNUNET_free (dc->exchange_url); - } - GNUNET_free (pc->dc); - for (unsigned int i = 0; inum_exchanges; i++) - { - struct ExchangeGroup *eg = pc->egs[i]; - - if (NULL != eg->fo) - TMH_EXCHANGES_keys4exchange_cancel (eg->fo); - GNUNET_free (eg); - } - GNUNET_free (pc->egs); - if (NULL != pc->response) - { - MHD_destroy_response (pc->response); - pc->response = NULL; - } - GNUNET_free (pc->fulfillment_url); - GNUNET_free (pc->session_id); - GNUNET_CONTAINER_DLL_remove (pc_head, - pc_tail, - pc); - GNUNET_free (pc->pos_key); - GNUNET_free (pc); + pc->phase = (MHD_YES == res) + ? PP_END_YES + : PP_END_NO; } /** - * Execute the DB transaction. If required (from - * soft/serialization errors), the transaction can be - * restarted here. + * Return response stored in @a pc. * - * @param pc payment context to transact + * @param[in,out] pc payment context we are processing */ static void -execute_pay_transaction (struct PayContext *pc); +phase_return_response (struct PayContext *pc) +{ + GNUNET_assert (0 != pc->response_code); + /* We are *done* processing the request, just queue the response (!) */ + if (UINT_MAX == pc->response_code) + { + GNUNET_break (0); + pay_end (pc, + MHD_NO); /* hard error */ + return; + } + pay_end (pc, + MHD_queue_response (pc->connection, + pc->response_code, + pc->response)); +} /** @@ -1113,7 +1143,10 @@ batch_deposit_cb ( handle_batch_deposit_ok (eg, dr); if (0 == pc->pending_at_eg) - execute_pay_transaction (eg->pc); + { + pc->phase = PP_PAY_TRANSACTION; + pay_resume (pc); + } return; default: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, @@ -1232,23 +1265,7 @@ process_pay_with_keys ( NULL); return; } - eg->tried_force_keys = true; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Forcing /keys download (once) as wire method seems unsupported for debit\n"); - eg->fo = TMH_EXCHANGES_keys4exchange ( - eg->exchange_url, - true, - &process_pay_with_keys, - eg); - if (NULL == eg->fo) - { - GNUNET_break (0); - pc->pending_at_eg--; - resume_pay_with_error (pc, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED, - "Failed to lookup exchange by URL"); - return; - } + force_keys (eg); return; } @@ -1421,7 +1438,11 @@ AGE_FAIL: "Group size zero, %u batch transactions remain pending\n", pc->pending_at_eg); if (0 == pc->pending_at_eg) - execute_pay_transaction (pc); + { + pc->phase = PP_PAY_TRANSACTION; + pay_resume (pc); + return; + } return; } @@ -1509,14 +1530,57 @@ force_keys (struct ExchangeGroup *eg) } +/** + * Handle a timeout for the processing of the pay request. + * + * @param cls our `struct PayContext` + */ +static void +handle_pay_timeout (void *cls) +{ + struct PayContext *pc = cls; + + pc->timeout_task = NULL; + GNUNET_assert (GNUNET_YES == pc->suspended); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming pay with error after timeout\n"); + resume_pay_with_error (pc, + TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, + NULL); +} + + +/** + * Compute the timeout for a /pay request based on the number of coins + * involved. + * + * @param num_coins number of coins + * @returns timeout for the /pay request + */ +static struct GNUNET_TIME_Relative +get_pay_timeout (unsigned int num_coins) +{ + struct GNUNET_TIME_Relative t; + + /* FIXME: Do some benchmarking to come up with a better timeout. + * We've increased this value so the wallet integration test passes again + * on my (Florian) machine. + */ + t = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, + 15 * (1 + (num_coins / 5))); + + return t; +} + + /** * Start batch deposits for all exchanges involved * in this payment. * - * @param pc payment context we are processing + * @param[in,out] pc payment context we are processing */ static void -start_batch_deposits (struct PayContext *pc) +phase_batch_deposits (struct PayContext *pc) { for (unsigned int i = 0; inum_exchanges; i++) { @@ -1548,45 +1612,150 @@ start_batch_deposits (struct PayContext *pc) if (NULL == eg->fo) { GNUNET_break (0); - resume_pay_with_error (pc, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED, - "Failed to lookup exchange by URL"); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED, + "Failed to lookup exchange by URL")); return; } pc->pending_at_eg++; } if (0 == pc->pending_at_eg) - execute_pay_transaction (pc); + { + pc->phase = PP_PAY_TRANSACTION; + pay_resume (pc); + return; + } + /* Suspend while we interact with the exchange */ + MHD_suspend_connection (pc->connection); + pc->suspended = GNUNET_YES; + GNUNET_assert (NULL == pc->timeout_task); + pc->timeout_task + = GNUNET_SCHEDULER_add_delayed (get_pay_timeout (pc->coins_cnt), + &handle_pay_timeout, + pc); } /** - * Function called with information about a coin that was deposited. + * Generate response (payment successful) * - * @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[in,out] pc payment context where the payment was successful */ 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) +phase_success_response (struct PayContext *pc) { - struct PayContext *pc = cls; + struct GNUNET_CRYPTO_EddsaSignature sig; + char *pos_confirmation; - for (unsigned int i = 0; icoins_cnt; i++) + /* Sign on our end (as the payment did go through, even if it may + have been refunded already) */ + TALER_merchant_pay_sign (&pc->h_contract_terms, + &pc->hc->instance->merchant_priv, + &sig); + /* Build the response */ + pos_confirmation = (NULL == pc->pos_key) + ? NULL + : TALER_build_pos_confirmation (pc->pos_key, + pc->pos_alg, + &pc->amount, + pc->timestamp); + pay_end (pc, + TALER_MHD_REPLY_JSON_PACK ( + pc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("pos_confirmation", + pos_confirmation)), + GNUNET_JSON_pack_data_auto ("sig", + &sig))); + GNUNET_free (pos_confirmation); +} + + +/** + * Use database to notify other clients about the + * payment being completed. + * + * @param[in,out] pc context to trigger notification for + */ +static void +phase_payment_notification (struct PayContext *pc) +{ { - struct DepositConfirmation *dc = &pc->dc[i]; + struct TMH_OrderPayEventP pay_eh = { + .header.size = htons (sizeof (pay_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID), + .merchant_pub = pc->hc->instance->merchant_pub + }; - if (dc->found_in_db) - continue; /* processed earlier, skip "expensive" memcmp() */ - /* Get matching coin from results*/ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying clients about payment of order %s\n", + pc->order_id); + GNUNET_CRYPTO_hash (pc->order_id, + strlen (pc->order_id), + &pay_eh.h_order_id); + TMH_db->event_notify (TMH_db->cls, + &pay_eh.header, + NULL, + 0); + } + if ( (NULL != pc->session_id) && + (NULL != pc->fulfillment_url) ) + { + struct TMH_SessionEventP session_eh = { + .header.size = htons (sizeof (session_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED), + .merchant_pub = pc->hc->instance->merchant_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying clients about session change to %s for %s\n", + pc->session_id, + pc->fulfillment_url); + GNUNET_CRYPTO_hash (pc->session_id, + strlen (pc->session_id), + &session_eh.h_session_id); + GNUNET_CRYPTO_hash (pc->fulfillment_url, + strlen (pc->fulfillment_url), + &session_eh.h_fulfillment_url); + TMH_db->event_notify (TMH_db->cls, + &session_eh.header, + NULL, + 0); + } + pc->phase = PP_SUCCESS_RESPONSE; +} + + +/** + * Function called with information about a coin that was deposited. + * + * @param cls closure + * @param exchange_url exchange where @a coin_pub was deposited + * @param coin_pub public key of the coin + * @param amount_with_fee amount the exchange will deposit for this coin + * @param deposit_fee fee the exchange will charge for this coin + * @param refund_fee fee the exchange will charge for refunding this coin + */ +static void +check_coin_paid (void *cls, + const char *exchange_url, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_Amount *amount_with_fee, + const struct TALER_Amount *deposit_fee, + const struct TALER_Amount *refund_fee) +{ + struct PayContext *pc = cls; + + for (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->cdd.coin_pub)) || (0 != @@ -1707,9 +1876,11 @@ check_payment_sufficient (struct PayContext *pc) &pc->egs[i]->wire_fee)) { GNUNET_break_op (0); - resume_pay_with_error (pc, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - total_wire_fee.currency); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + total_wire_fee.currency)); return false; } if (0 > @@ -1718,9 +1889,12 @@ check_payment_sufficient (struct PayContext *pc) &pc->egs[i]->wire_fee)) { GNUNET_break (0); - resume_pay_with_error (pc, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED, - "could not add exchange wire fee to total"); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED, + "could not add exchange wire fee to total")); return false; } } @@ -1748,9 +1922,12 @@ check_payment_sufficient (struct PayContext *pc) &dc->cdd.amount)) ) { GNUNET_break_op (0); - resume_pay_with_error (pc, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - dc->deposit_fee.currency); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + dc->deposit_fee.currency)); return false; } if ( (0 > @@ -1764,9 +1941,12 @@ check_payment_sufficient (struct PayContext *pc) { GNUNET_break (0); /* Overflow in these amounts? Very strange. */ - resume_pay_with_error (pc, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, - "Overflow adding up amounts"); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, + "Overflow adding up amounts")); return false; } if (1 == @@ -1774,13 +1954,15 @@ check_payment_sufficient (struct PayContext *pc) &dc->cdd.amount)) { GNUNET_break_op (0); - resume_pay_with_error (pc, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT, - "Deposit fees exceed coin's contribution"); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT, + "Deposit fees exceed coin's contribution")); return false; } - - } /* deposit loop */ + } /* end deposit loop */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Amount received from wallet: %s\n", @@ -1805,9 +1987,12 @@ check_payment_sufficient (struct PayContext *pc) &acc_fee)) { GNUNET_break (0); - resume_pay_with_error (pc, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - total_wire_fee.currency); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + total_wire_fee.currency)); return false; } @@ -1818,9 +2003,12 @@ check_payment_sufficient (struct PayContext *pc) &total_wire_fee)) { GNUNET_break (0); - resume_pay_with_error (pc, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, - "Overflow adding up amounts"); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, + "Overflow adding up amounts")); return false; } if (-1 == TALER_amount_cmp (&pc->max_fee, @@ -1845,9 +2033,12 @@ check_payment_sufficient (struct PayContext *pc) &pc->amount)) { GNUNET_break (0); - resume_pay_with_error (pc, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, - "Overflow adding up amounts"); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, + "Overflow adding up amounts")); return false; } } @@ -1868,9 +2059,12 @@ check_payment_sufficient (struct PayContext *pc) &pc->total_refunded)) { GNUNET_break (0); - resume_pay_with_error (pc, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUNDS_EXCEED_PAYMENTS, - "refunded amount exceeds total payments"); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUNDS_EXCEED_PAYMENTS, + "refunded amount exceeds total payments")); return false; } @@ -1882,25 +2076,33 @@ check_payment_sufficient (struct PayContext *pc) &total_needed)) { GNUNET_break_op (0); - resume_pay_with_error (pc, - 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, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES, - "contract not paid up due to fees (client may have calculated them badly)"); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_PAYMENT_REQUIRED, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUNDED, + "contract not paid up due to refunds")); + return false; } - else + if (-1 < TALER_amount_cmp (&acc_amount, + &pc->amount)) { GNUNET_break_op (0); - resume_pay_with_error (pc, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT, - "payment insufficient"); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + 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)")); + return false; } + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_NOT_ACCEPTABLE, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT, + "payment insufficient")); return false; } return true; @@ -1908,97 +2110,14 @@ check_payment_sufficient (struct PayContext *pc) /** - * Use database to notify other clients about the - * payment being completed. - * - * @param pc context to trigger notification for - */ -static void -trigger_payment_notification (struct PayContext *pc) -{ - { - struct TMH_OrderPayEventP pay_eh = { - .header.size = htons (sizeof (pay_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID), - .merchant_pub = pc->hc->instance->merchant_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Notifying clients about payment of order %s\n", - pc->order_id); - GNUNET_CRYPTO_hash (pc->order_id, - strlen (pc->order_id), - &pay_eh.h_order_id); - TMH_db->event_notify (TMH_db->cls, - &pay_eh.header, - NULL, - 0); - } - if ( (NULL != pc->session_id) && - (NULL != pc->fulfillment_url) ) - { - struct TMH_SessionEventP session_eh = { - .header.size = htons (sizeof (session_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED), - .merchant_pub = pc->hc->instance->merchant_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Notifying clients about session change to %s for %s\n", - pc->session_id, - pc->fulfillment_url); - GNUNET_CRYPTO_hash (pc->session_id, - strlen (pc->session_id), - &session_eh.h_session_id); - GNUNET_CRYPTO_hash (pc->fulfillment_url, - strlen (pc->fulfillment_url), - &session_eh.h_fulfillment_url); - TMH_db->event_notify (TMH_db->cls, - &session_eh.header, - NULL, - 0); - } -} - - -/** - * Generate response (payment successful) + * Execute the DB transaction. If required (from + * soft/serialization errors), the transaction can be + * restarted here. * - * @param[in,out] pc payment context where the payment was successful + * @param[in,out] pc payment context to transact */ static void -generate_success_response (struct PayContext *pc) -{ - struct GNUNET_CRYPTO_EddsaSignature sig; - char *pos_confirmation; - - /* Sign on our end (as the payment did go through, even if it may - have been refunded already) */ - TALER_merchant_pay_sign (&pc->h_contract_terms, - &pc->hc->instance->merchant_priv, - &sig); - /* Build the response */ - pos_confirmation = (NULL == pc->pos_key) - ? NULL - : TALER_build_pos_confirmation (pc->pos_key, - pc->pos_alg, - &pc->amount, - pc->timestamp); - resume_pay_with_response ( - pc, - MHD_HTTP_OK, - TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("pos_confirmation", - pos_confirmation)), - GNUNET_JSON_pack_data_auto ("sig", - &sig))); - GNUNET_free (pos_confirmation); -} - - -static void -execute_pay_transaction (struct PayContext *pc) +phase_execute_pay_transaction (struct PayContext *pc) { struct TMH_HandlerContext *hc = pc->hc; const char *instance_id = hc->instance->settings.id; @@ -2007,12 +2126,13 @@ execute_pay_transaction (struct PayContext *pc) if (pc->retry_counter++ > MAX_RETRIES) { GNUNET_break (0); - resume_pay_with_error (pc, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + 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() @@ -2037,9 +2157,11 @@ execute_pay_transaction (struct PayContext *pc) "run pay")) { GNUNET_break (0); - resume_pay_with_error (pc, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + NULL)); return; } @@ -2056,28 +2178,28 @@ execute_pay_transaction (struct PayContext *pc) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - execute_pay_transaction (pc); - return; - } + return; /* do it again */ /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - resume_pay_with_error (pc, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup deposits"); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup deposits")); return; } if (pc->deposit_currency_mismatch) { GNUNET_break_op (0); - resume_pay_with_error (pc, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - pc->amount.currency); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + pc->amount.currency)); return; } } - { enum GNUNET_DB_QueryStatus qs; @@ -2091,23 +2213,24 @@ execute_pay_transaction (struct PayContext *pc) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - execute_pay_transaction (pc); - return; - } + return; /* do it again */ /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - resume_pay_with_error (pc, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup refunds"); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup refunds")); return; } if (pc->refund_currency_mismatch) { TMH_db->rollback (TMH_db->cls); - resume_pay_with_error (pc, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "refund currency in database does not match order currency"); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "refund currency in database does not match order currency")); return; } } @@ -2117,11 +2240,10 @@ execute_pay_transaction (struct PayContext *pc) { /* 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). */ - start_batch_deposits (pc); + pc->phase = PP_BATCH_DEPOSITS; return; } @@ -2149,14 +2271,13 @@ execute_pay_transaction (struct PayContext *pc) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - execute_pay_transaction (pc); - return; - } + return; /* do it again */ GNUNET_break (0); - resume_pay_with_error (pc, - TALER_EC_GENERIC_DB_STORE_FAILED, - "mark contract paid"); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "mark contract paid")); return; } } @@ -2184,14 +2305,13 @@ execute_pay_transaction (struct PayContext *pc) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - execute_pay_transaction (pc); - return; - } + return; /* do it again */ GNUNET_break (0); - resume_pay_with_error (pc, - TALER_EC_GENERIC_DB_STORE_FAILED, - "failed to trigger webhooks"); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "failed to trigger webhooks")); return; } } @@ -2205,250 +2325,66 @@ execute_pay_transaction (struct PayContext *pc) /* commit failed */ TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - execute_pay_transaction (pc); - return; - } + return; /* do it again */ GNUNET_break (0); - resume_pay_with_error (pc, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL)); return; } - trigger_payment_notification (pc); } - generate_success_response (pc); + pc->phase = PP_PAYMENT_NOTIFICATION; } /** - * Try to parse the pay request into the given pay context. - * Schedules an error response in the connection on failure. + * Function called with information about a coin that was deposited. + * Checks if this coin is in our list of deposits as well. * - * @param[in,out] 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) + * @param cls closure with our `struct PayContext *` + * @param deposit_serial which deposit operation is this about + * @param exchange_url URL of the exchange that issued the coin + * @param amount_with_fee amount the exchange will deposit for this coin + * @param deposit_fee fee the exchange will charge for this coin + * @param h_wire hash of merchant's wire details + * @param coin_pub public key of the coin */ -static enum GNUNET_GenericReturnValue -parse_pay (struct PayContext *pc) +static void +deposit_paid_check ( + void *cls, + uint64_t deposit_serial, + const char *exchange_url, + const struct TALER_MerchantWireHashP *h_wire, + const struct TALER_Amount *amount_with_fee, + const struct TALER_Amount *deposit_fee, + const struct TALER_CoinSpendPublicKeyP *coin_pub) { - const char *session_id = NULL; - const json_t *coins; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("coins", - &coins), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("session_id", - &session_id), - NULL), - GNUNET_JSON_spec_end () - }; + struct PayContext *pc = cls; + for (unsigned int i = 0; icoins_cnt; i++) { - enum GNUNET_GenericReturnValue res; + struct DepositConfirmation *dci = &pc->dc[i]; - res = TALER_MHD_parse_json_data (pc->connection, - pc->hc->request_body, - spec); - if (GNUNET_YES != res) + if ( (0 == + GNUNET_memcmp (&dci->cdd.coin_pub, + coin_pub)) && + (0 == + strcmp (dci->exchange_url, + exchange_url)) && + (GNUNET_YES == + TALER_amount_cmp_currency (&dci->cdd.amount, + amount_with_fee)) && + (0 == + TALER_amount_cmp (&dci->cdd.amount, + amount_with_fee)) ) { - GNUNET_break_op (0); - return res; + dci->matched_in_db = true; + break; } } - - /* copy session ID (if set) */ - if (NULL != session_id) - { - pc->session_id = GNUNET_strdup (session_id); - } - else - { - /* use empty string as default if client didn't specify it */ - pc->session_id = GNUNET_strdup (""); - } - pc->coins_cnt = json_array_size (coins); - if (pc->coins_cnt > MAX_COIN_ALLOWED_COINS) - { - GNUNET_break_op (0); - return (MHD_YES == - TALER_MHD_reply_with_error ( - pc->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->cdd.coin_sig), - GNUNET_JSON_spec_fixed_auto ("coin_pub", - &dc->cdd.coin_pub), - TALER_JSON_spec_denom_sig ("ub_sig", - &dc->cdd.denom_sig), - GNUNET_JSON_spec_fixed_auto ("h_denom", - &dc->cdd.h_denom_pub), - TALER_JSON_spec_amount_any ("contribution", - &dc->cdd.amount), - TALER_JSON_spec_web_url ("exchange_url", - &exchange_url), - /* if a minimum age was required, the minimum_age_sig and - * age_commitment must be provided */ - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("minimum_age_sig", - &dc->minimum_age_sig), - &dc->no_minimum_age_sig), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_age_commitment ("age_commitment", - &dc->age_commitment), - &dc->no_age_commitment), - /* if minimum age was not required, but coin with age restriction set - * was used, h_age_commitment must be provided. */ - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("h_age_commitment", - &dc->cdd.h_age_commitment), - &dc->no_h_age_commitment), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - bool have_eg = false; - - res = TALER_MHD_parse_json_data (pc->connection, - coin, - ispec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); - return res; - } - for (unsigned int j = 0; jcdd.coin_pub, - &pc->dc[j].cdd.coin_pub)) - { - GNUNET_break_op (0); - return (MHD_YES == - TALER_MHD_reply_with_error (pc->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; - - /* Check the consistency of the (potential) age restriction - * information. */ - if (dc->no_age_commitment != dc->no_minimum_age_sig) - { - GNUNET_break_op (0); - return (MHD_YES == - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "inconsistent: 'age_commitment' vs. 'minimum_age_sig'" - ) - ) - ? GNUNET_NO - : GNUNET_SYSERR; - } - - /* Setup exchange group */ - for (unsigned int i = 0; inum_exchanges; i++) - { - if (0 == - strcmp (pc->egs[i]->exchange_url, - exchange_url)) - { - have_eg = true; - break; - } - } - if (! have_eg) - { - struct ExchangeGroup *eg; - - eg = GNUNET_new (struct ExchangeGroup); - eg->pc = pc; - eg->exchange_url = dc->exchange_url; - GNUNET_array_append (pc->egs, - pc->num_exchanges, - eg); - } - } - } - return GNUNET_OK; -} - - -/** - * Function called with information about a coin that was deposited. - * Checks if this coin is in our list of deposits as well. - * - * @param cls closure with our `struct PayContext *` - * @param deposit_serial which deposit operation is this about - * @param exchange_url URL of the exchange that issued the coin - * @param amount_with_fee amount the exchange will deposit for this coin - * @param deposit_fee fee the exchange will charge for this coin - * @param h_wire hash of merchant's wire details - * @param coin_pub public key of the coin - */ -static void -deposit_paid_check ( - void *cls, - uint64_t deposit_serial, - const char *exchange_url, - const struct TALER_MerchantWireHashP *h_wire, - const struct TALER_Amount *amount_with_fee, - const struct TALER_Amount *deposit_fee, - const struct TALER_CoinSpendPublicKeyP *coin_pub) -{ - struct PayContext *pc = cls; - - for (unsigned int i = 0; icoins_cnt; i++) - { - struct DepositConfirmation *dci = &pc->dc[i]; - - if ( (0 == - GNUNET_memcmp (&dci->cdd.coin_pub, - coin_pub)) && - (0 == - strcmp (dci->exchange_url, - exchange_url)) && - (GNUNET_YES == - TALER_amount_cmp_currency (&dci->cdd.amount, - amount_with_fee)) && - (0 == - TALER_amount_cmp (&dci->cdd.amount, - amount_with_fee)) ) - { - dci->matched_in_db = true; - break; - } - } -} +} /** @@ -2456,11 +2392,9 @@ deposit_paid_check ( * the payment is idempotent, or refunds the excess payment. * * @param[in,out] pc context we use to handle the payment - * @return #GNUNET_NO if response was queued with MHD - * #GNUNET_SYSERR on hard error (MHD connection must be dropped) */ -static enum GNUNET_GenericReturnValue -handle_contract_paid (struct PayContext *pc) +static void +phase_contract_paid (struct PayContext *pc) { enum GNUNET_DB_QueryStatus qs; bool unmatched = false; @@ -2473,13 +2407,12 @@ handle_contract_paid (struct PayContext *pc) if (qs <= 0) { GNUNET_break (0); - return (MHD_YES == - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_deposits_by_order")) - ? GNUNET_NO - : GNUNET_SYSERR; + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_deposits_by_order")); + return; } for (unsigned int i = 0; icoins_cnt; i++) { @@ -2499,14 +2432,13 @@ handle_contract_paid (struct PayContext *pc) TALER_merchant_pay_sign (&pc->h_contract_terms, &pc->hc->instance->merchant_priv, &sig); - return (MHD_YES == - TALER_MHD_REPLY_JSON_PACK ( - pc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ("sig", - &sig))) - ? GNUNET_NO - : GNUNET_SYSERR; + pay_end (pc, + TALER_MHD_REPLY_JSON_PACK ( + pc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ("sig", + &sig))); + return; } /* Conflict, double-payment detected! */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -2543,29 +2475,25 @@ handle_contract_paid (struct PayContext *pc) GNUNET_JSON_pack_uint64 ("rtransaction_id", 0)))); } - return (MHD_YES == - TALER_MHD_REPLY_JSON_PACK ( - pc->connection, - MHD_HTTP_CONFLICT, - TALER_MHD_PACK_EC ( - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID), - GNUNET_JSON_pack_array_steal ("refunds", - refunds))) - ? GNUNET_NO - : GNUNET_SYSERR; + pay_end (pc, + TALER_MHD_REPLY_JSON_PACK ( + pc->connection, + MHD_HTTP_CONFLICT, + TALER_MHD_PACK_EC ( + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID), + GNUNET_JSON_pack_array_steal ("refunds", + refunds))); } /** - * Check the database state for the given order. * Schedules an error response in the connection on failure. + * Check the database state for the given order. + * Schedules an error response in the connection on failure. * - * @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) + * @param[in,out] pc context we use to handle the payment */ -static enum GNUNET_GenericReturnValue -check_contract (struct PayContext *pc) +static void +phase_check_contract (struct PayContext *pc) { /* obtain contract terms */ enum GNUNET_DB_QueryStatus qs; @@ -2592,23 +2520,21 @@ check_contract (struct PayContext *pc) 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 (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "contract terms")) - ? GNUNET_NO - : GNUNET_SYSERR; + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "contract terms")); + return; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { - return (MHD_YES == - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, - pc->order_id)) - ? GNUNET_NO - : GNUNET_SYSERR; + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, + pc->order_id)); + return; } /* hash contract (needed later) */ if (GNUNET_OK != @@ -2616,20 +2542,20 @@ check_contract (struct PayContext *pc) &pc->h_contract_terms)) { GNUNET_break (0); - return (MHD_YES == - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - NULL)) - ? GNUNET_NO - : GNUNET_SYSERR; + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + NULL)); + return; } if (paid) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Order `%s' paid, checking for double-payment\n", pc->order_id); - return handle_contract_paid (pc); + pc->phase = PP_CONTRACT_PAID; + return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Handling payment for order `%s' with contract hash `%s'\n", @@ -2642,13 +2568,12 @@ check_contract (struct PayContext *pc) { /* invalid contract */ GNUNET_break (0); - return (MHD_YES == - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING, - NULL)) - ? GNUNET_NO - : GNUNET_SYSERR; + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING, + NULL)); + return; } /* Get details from contract and check fundamentals */ @@ -2691,7 +2616,11 @@ check_contract (struct PayContext *pc) if (GNUNET_YES != res) { GNUNET_break (0); - return res; + pay_end (pc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); + return; } } @@ -2700,10 +2629,12 @@ check_contract (struct PayContext *pc) &pc->amount)) { GNUNET_break (0); - return TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "'max_fee' in database does not match currency of contract price"); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "'max_fee' in database does not match currency of contract price")); + return; } for (unsigned int i=0;icoins_cnt;i++) @@ -2715,13 +2646,12 @@ check_contract (struct PayContext *pc) &pc->amount)) { GNUNET_break_op (0); - return (MHD_YES == - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - pc->amount.currency)) - ? GNUNET_NO - : GNUNET_SYSERR; + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + pc->amount.currency)); + return; } } @@ -2731,21 +2661,22 @@ check_contract (struct PayContext *pc) { /* This should already have been checked when creating the order! */ GNUNET_break (0); - return TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE, - NULL); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE, + NULL)); + return; } if (GNUNET_TIME_absolute_is_past (pc->pay_deadline.abs_time)) { /* too late */ - return (MHD_YES == - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_GONE, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED, - NULL)) - ? GNUNET_NO - : GNUNET_SYSERR; + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_GONE, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED, + NULL)); + return; } /* Make sure wire method (still) exists for this instance */ @@ -2759,35 +2690,251 @@ check_contract (struct PayContext *pc) if (NULL == wm) { GNUNET_break (0); - return TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN, - NULL); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN, + NULL)); + return; } pc->wm = wm; } + pc->phase = PP_PAY_TRANSACTION; +} + + +/** + * Try to parse the pay request into the given pay context. + * Schedules an error response in the connection on failure. + * + * @param[in,out] pc context we use to handle the payment + */ +static void +phase_parse_pay (struct PayContext *pc) +{ + const char *session_id = NULL; + const json_t *coins; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("coins", + &coins), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("session_id", + &session_id), + NULL), + GNUNET_JSON_spec_end () + }; + + GNUNET_assert (PP_INIT == pc->phase); + { + enum GNUNET_GenericReturnValue res; - return GNUNET_OK; + res = TALER_MHD_parse_json_data (pc->connection, + pc->hc->request_body, + spec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + pay_end (pc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); + return; + } + } + + /* copy session ID (if set) */ + if (NULL != session_id) + { + pc->session_id = GNUNET_strdup (session_id); + } + else + { + /* use empty string as default if client didn't specify it */ + pc->session_id = GNUNET_strdup (""); + } + pc->coins_cnt = json_array_size (coins); + if (pc->coins_cnt > MAX_COIN_ALLOWED_COINS) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "'coins' array too long")); + return; + } + + /* note: 1 coin = 1 deposit confirmation expected */ + pc->dc = GNUNET_new_array (pc->coins_cnt, + struct DepositConfirmation); + + /* This loop populates the array 'dc' in 'pc' */ + { + unsigned int coins_index; + json_t *coin; + + json_array_foreach (coins, coins_index, coin) + { + struct DepositConfirmation *dc = &pc->dc[coins_index]; + const char *exchange_url; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &dc->cdd.coin_sig), + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &dc->cdd.coin_pub), + TALER_JSON_spec_denom_sig ("ub_sig", + &dc->cdd.denom_sig), + GNUNET_JSON_spec_fixed_auto ("h_denom", + &dc->cdd.h_denom_pub), + TALER_JSON_spec_amount_any ("contribution", + &dc->cdd.amount), + TALER_JSON_spec_web_url ("exchange_url", + &exchange_url), + /* if a minimum age was required, the minimum_age_sig and + * age_commitment must be provided */ + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("minimum_age_sig", + &dc->minimum_age_sig), + &dc->no_minimum_age_sig), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_age_commitment ("age_commitment", + &dc->age_commitment), + &dc->no_age_commitment), + /* if minimum age was not required, but coin with age restriction set + * was used, h_age_commitment must be provided. */ + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("h_age_commitment", + &dc->cdd.h_age_commitment), + &dc->no_h_age_commitment), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + bool have_eg = false; + + res = TALER_MHD_parse_json_data (pc->connection, + coin, + ispec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + pay_end (pc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); + } + for (unsigned int j = 0; jcdd.coin_pub, + &pc->dc[j].cdd.coin_pub)) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "duplicate coin in list")); + return; + } + } + + dc->exchange_url = GNUNET_strdup (exchange_url); + dc->index = coins_index; + dc->pc = pc; + + /* Check the consistency of the (potential) age restriction + * information. */ + if (dc->no_age_commitment != dc->no_minimum_age_sig) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inconsistent: 'age_commitment' vs. 'minimum_age_sig'" + )); + return; + } + + /* Setup exchange group */ + for (unsigned int i = 0; inum_exchanges; i++) + { + if (0 == + strcmp (pc->egs[i]->exchange_url, + exchange_url)) + { + have_eg = true; + break; + } + } + if (! have_eg) + { + struct ExchangeGroup *eg; + + eg = GNUNET_new (struct ExchangeGroup); + eg->pc = pc; + eg->exchange_url = dc->exchange_url; + GNUNET_array_append (pc->egs, + pc->num_exchanges, + eg); + } + } + } + pc->phase = PP_CHECK_CONTRACT; } /** - * Handle a timeout for the processing of the pay request. + * Custom cleanup routine for a `struct PayContext`. * - * @param cls our `struct PayContext` + * @param cls the `struct PayContext` to clean up. */ static void -handle_pay_timeout (void *cls) +pay_context_cleanup (void *cls) { struct PayContext *pc = cls; - pc->timeout_task = NULL; - GNUNET_assert (GNUNET_YES == pc->suspended); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming pay with error after timeout\n"); - resume_pay_with_error (pc, - TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, - NULL); + if (NULL != pc->timeout_task) + { + GNUNET_SCHEDULER_cancel (pc->timeout_task); + pc->timeout_task = NULL; + } + if (NULL != pc->contract_terms) + { + json_decref (pc->contract_terms); + pc->contract_terms = NULL; + } + for (unsigned int i = 0; icoins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->dc[i]; + + TALER_denom_sig_free (&dc->cdd.denom_sig); + GNUNET_free (dc->exchange_url); + } + GNUNET_free (pc->dc); + for (unsigned int i = 0; inum_exchanges; i++) + { + struct ExchangeGroup *eg = pc->egs[i]; + + if (NULL != eg->fo) + TMH_EXCHANGES_keys4exchange_cancel (eg->fo); + GNUNET_free (eg); + } + GNUNET_free (pc->egs); + if (NULL != pc->response) + { + MHD_destroy_response (pc->response); + pc->response = NULL; + } + GNUNET_free (pc->fulfillment_url); + GNUNET_free (pc->session_id); + GNUNET_CONTAINER_DLL_remove (pc_head, + pc_tail, + pc); + GNUNET_free (pc->pos_key); + GNUNET_free (pc); } @@ -2797,59 +2944,76 @@ TMH_post_orders_ID_pay (const struct TMH_RequestHandler *rh, struct TMH_HandlerContext *hc) { struct PayContext *pc = hc->ctx; - enum GNUNET_GenericReturnValue ret; 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; - ret = parse_pay (pc); - if (GNUNET_OK != ret) - return (GNUNET_NO == ret) - ? MHD_YES - : MHD_NO; - } - 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) - { - /* We are *done* processing the request, just queue the response (!) */ - if (UINT_MAX == pc->response_code) + GNUNET_CONTAINER_DLL_insert (pc_head, + pc_tail, + pc); + } + while (1) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Processing /pay in phase %d\n", + (int) pc->phase); + switch (pc->phase) { - GNUNET_break (0); - return MHD_NO; /* hard error */ + case PP_INIT: + phase_parse_pay (pc); + break; + case PP_CHECK_CONTRACT: + phase_check_contract (pc); + break; + case PP_CONTRACT_PAID: + phase_contract_paid (pc); + break; + case PP_PAY_TRANSACTION: + phase_execute_pay_transaction (pc); + break; + case PP_BATCH_DEPOSITS: + phase_batch_deposits (pc); + break; + case PP_PAYMENT_NOTIFICATION: + phase_payment_notification (pc); + break; + case PP_SUCCESS_RESPONSE: + phase_success_response (pc); + break; + case PP_RETURN_RESPONSE: + phase_return_response (pc); + break; + case PP_END_YES: + return MHD_YES; + case PP_END_NO: + return MHD_NO; + } + switch (pc->suspended) + { + case GNUNET_SYSERR: + /* during shutdown, we don't generate any more replies */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Processing /pay ends due to shutdown in phase %d\n", + (int) pc->phase); + return MHD_NO; + case GNUNET_NO: + /* continue to next phase */ + break; + case GNUNET_YES: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Processing /pay suspended in phase %d\n", + (int) pc->phase); + return MHD_YES; } - return MHD_queue_response (connection, - pc->response_code, - pc->response); } - ret = check_contract (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); - GNUNET_assert (NULL != pc->wm); - execute_pay_transaction (pc); + /* impossible to get here */ + GNUNET_assert (0); return MHD_YES; } -- cgit v1.2.3