From 20853863335ee4cd6a43044bfa1c8b38361cbc1f Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Mon, 27 Apr 2020 04:53:17 +0200 Subject: rename fest to match new structure --- src/backend/taler-merchant-httpd_check-payment.h | 48 - src/backend/taler-merchant-httpd_get-orders-ID-2.c | 582 +++++ src/backend/taler-merchant-httpd_get-orders-ID-2.h | 47 + src/backend/taler-merchant-httpd_get-orders-ID.c | 654 ++++++ src/backend/taler-merchant-httpd_get-orders-ID.h | 48 + src/backend/taler-merchant-httpd_get-orders.c | 299 --- .../taler-merchant-httpd_get-reserves-reserve.c | 248 --- src/backend/taler-merchant-httpd_get-reserves.c | 462 ---- src/backend/taler-merchant-httpd_get-tips-tip.c | 146 -- src/backend/taler-merchant-httpd_get-transfers.c | 1089 ---------- src/backend/taler-merchant-httpd_orders_get.h | 49 - .../taler-merchant-httpd_orders_order_abort.h | 54 - .../taler-merchant-httpd_orders_order_get.c | 582 ----- .../taler-merchant-httpd_orders_order_get.h | 47 - .../taler-merchant-httpd_orders_order_get2.c | 654 ------ .../taler-merchant-httpd_orders_order_get2.h | 48 - .../taler-merchant-httpd_orders_order_get3.c | 1250 ----------- .../taler-merchant-httpd_orders_order_get4.c | 591 ----- .../taler-merchant-httpd_orders_order_get5.c | 228 -- .../taler-merchant-httpd_orders_order_get5.h | 47 - .../taler-merchant-httpd_orders_order_pay.c | 2255 -------------------- .../taler-merchant-httpd_orders_order_pay.h | 54 - .../taler-merchant-httpd_orders_order_refund.c | 379 ---- .../taler-merchant-httpd_orders_order_refund.h | 49 - .../taler-merchant-httpd_post-orders-ID-abort.c | 2255 ++++++++++++++++++++ .../taler-merchant-httpd_post-orders-ID-abort.h | 54 + .../taler-merchant-httpd_post-orders-ID-claim.c | 228 ++ .../taler-merchant-httpd_post-orders-ID-claim.h | 47 + .../taler-merchant-httpd_post-orders-ID-pay.c | 2255 ++++++++++++++++++++ .../taler-merchant-httpd_post-orders-ID-pay.h | 54 + .../taler-merchant-httpd_post-orders-order-abort.c | 2255 -------------------- .../taler-merchant-httpd_post-tips-ID-pickup.c | 146 ++ .../taler-merchant-httpd_post-tips-ID-pickup.h | 75 + src/backend/taler-merchant-httpd_post-tips.c | 306 --- src/backend/taler-merchant-httpd_post-transfers.c | 1089 ---------- .../taler-merchant-httpd_private-get-orders-ID.c | 591 +++++ .../taler-merchant-httpd_private-get-orders-ID.h | 48 + .../taler-merchant-httpd_private-get-reserves-ID.c | 248 +++ .../taler-merchant-httpd_private-get-reserves-ID.h | 45 + .../taler-merchant-httpd_private-get-transfers.c | 1089 ++++++++++ .../taler-merchant-httpd_private-get-transfers.h | 49 + ...-merchant-httpd_private-post-orders-ID-refund.c | 379 ++++ ...-merchant-httpd_private-post-orders-ID-refund.h | 49 + ...ant-httpd_private-post-orders-ID-track-UNSPEC.c | 1250 +++++++++++ ...ant-httpd_private-post-orders-ID-track-UNSPEC.h | 47 + ...-httpd_private-post-reserves-ID-authorize-tip.c | 306 +++ ...-httpd_private-post-reserves-ID-authorize-tip.h | 46 + src/backend/taler-merchant-httpd_refund.h | 71 - src/backend/taler-merchant-httpd_reserves_get.h | 153 -- .../taler-merchant-httpd_reserves_reserve_get.h | 45 - src/backend/taler-merchant-httpd_tips_post.h | 46 - src/backend/taler-merchant-httpd_tips_tip_pickup.h | 75 - .../taler-merchant-httpd_track-transaction.h | 47 - src/backend/taler-merchant-httpd_transfers-get.h | 49 - src/backend/taler-merchant-httpd_transfers-post.h | 49 - 55 files changed, 10592 insertions(+), 12764 deletions(-) delete mode 100644 src/backend/taler-merchant-httpd_check-payment.h create mode 100644 src/backend/taler-merchant-httpd_get-orders-ID-2.c create mode 100644 src/backend/taler-merchant-httpd_get-orders-ID-2.h create mode 100644 src/backend/taler-merchant-httpd_get-orders-ID.c create mode 100644 src/backend/taler-merchant-httpd_get-orders-ID.h delete mode 100644 src/backend/taler-merchant-httpd_get-orders.c delete mode 100644 src/backend/taler-merchant-httpd_get-reserves-reserve.c delete mode 100644 src/backend/taler-merchant-httpd_get-reserves.c delete mode 100644 src/backend/taler-merchant-httpd_get-tips-tip.c delete mode 100644 src/backend/taler-merchant-httpd_get-transfers.c delete mode 100644 src/backend/taler-merchant-httpd_orders_get.h delete mode 100644 src/backend/taler-merchant-httpd_orders_order_abort.h delete mode 100644 src/backend/taler-merchant-httpd_orders_order_get.c delete mode 100644 src/backend/taler-merchant-httpd_orders_order_get.h delete mode 100644 src/backend/taler-merchant-httpd_orders_order_get2.c delete mode 100644 src/backend/taler-merchant-httpd_orders_order_get2.h delete mode 100644 src/backend/taler-merchant-httpd_orders_order_get3.c delete mode 100644 src/backend/taler-merchant-httpd_orders_order_get4.c delete mode 100644 src/backend/taler-merchant-httpd_orders_order_get5.c delete mode 100644 src/backend/taler-merchant-httpd_orders_order_get5.h delete mode 100644 src/backend/taler-merchant-httpd_orders_order_pay.c delete mode 100644 src/backend/taler-merchant-httpd_orders_order_pay.h delete mode 100644 src/backend/taler-merchant-httpd_orders_order_refund.c delete mode 100644 src/backend/taler-merchant-httpd_orders_order_refund.h create mode 100644 src/backend/taler-merchant-httpd_post-orders-ID-abort.c create mode 100644 src/backend/taler-merchant-httpd_post-orders-ID-abort.h create mode 100644 src/backend/taler-merchant-httpd_post-orders-ID-claim.c create mode 100644 src/backend/taler-merchant-httpd_post-orders-ID-claim.h create mode 100644 src/backend/taler-merchant-httpd_post-orders-ID-pay.c create mode 100644 src/backend/taler-merchant-httpd_post-orders-ID-pay.h delete mode 100644 src/backend/taler-merchant-httpd_post-orders-order-abort.c create mode 100644 src/backend/taler-merchant-httpd_post-tips-ID-pickup.c create mode 100644 src/backend/taler-merchant-httpd_post-tips-ID-pickup.h delete mode 100644 src/backend/taler-merchant-httpd_post-tips.c delete mode 100644 src/backend/taler-merchant-httpd_post-transfers.c create mode 100644 src/backend/taler-merchant-httpd_private-get-orders-ID.c create mode 100644 src/backend/taler-merchant-httpd_private-get-orders-ID.h create mode 100644 src/backend/taler-merchant-httpd_private-get-reserves-ID.c create mode 100644 src/backend/taler-merchant-httpd_private-get-reserves-ID.h create mode 100644 src/backend/taler-merchant-httpd_private-get-transfers.c create mode 100644 src/backend/taler-merchant-httpd_private-get-transfers.h create mode 100644 src/backend/taler-merchant-httpd_private-post-orders-ID-refund.c create mode 100644 src/backend/taler-merchant-httpd_private-post-orders-ID-refund.h create mode 100644 src/backend/taler-merchant-httpd_private-post-orders-ID-track-UNSPEC.c create mode 100644 src/backend/taler-merchant-httpd_private-post-orders-ID-track-UNSPEC.h create mode 100644 src/backend/taler-merchant-httpd_private-post-reserves-ID-authorize-tip.c create mode 100644 src/backend/taler-merchant-httpd_private-post-reserves-ID-authorize-tip.h delete mode 100644 src/backend/taler-merchant-httpd_refund.h delete mode 100644 src/backend/taler-merchant-httpd_reserves_get.h delete mode 100644 src/backend/taler-merchant-httpd_reserves_reserve_get.h delete mode 100644 src/backend/taler-merchant-httpd_tips_post.h delete mode 100644 src/backend/taler-merchant-httpd_tips_tip_pickup.h delete mode 100644 src/backend/taler-merchant-httpd_track-transaction.h delete mode 100644 src/backend/taler-merchant-httpd_transfers-get.h delete mode 100644 src/backend/taler-merchant-httpd_transfers-post.h (limited to 'src/backend') diff --git a/src/backend/taler-merchant-httpd_check-payment.h b/src/backend/taler-merchant-httpd_check-payment.h deleted file mode 100644 index e94645df..00000000 --- a/src/backend/taler-merchant-httpd_check-payment.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - This file is part of TALER - (C) 2017 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_tip-query.h - * @brief headers for /tip-query handler - * @author Christian Grothoff - * @author Florian Dold - */ -#ifndef TALER_MERCHANT_HTTPD_CHECK_PAYMENT_H -#define TALER_MERCHANT_HTTPD_CHECK_PAYMENT_H -#include -#include "taler-merchant-httpd.h" - -/** - * Manages a /check-payment call, checking the status - * of a payment and, if necessary, constructing the URL - * for a payment redirect URL. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_check_payment (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi); - -#endif diff --git a/src/backend/taler-merchant-httpd_get-orders-ID-2.c b/src/backend/taler-merchant-httpd_get-orders-ID-2.c new file mode 100644 index 00000000..6ca4fcce --- /dev/null +++ b/src/backend/taler-merchant-httpd_get-orders-ID-2.c @@ -0,0 +1,582 @@ +/* + This file is part of TALER + (C) 2017, 2019 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_poll-payment.c + * @brief implementation of /public/poll-payment handler + * @author Florian Dold + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include +#include +#include +#include +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_mhd.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_poll-payment.h" + +/** + * Maximum number of retries for database operations. + */ +#define MAX_RETRIES 5 + + +/** + * Data structure we keep for a check payment request. + */ +struct PollPaymentRequestContext +{ + /** + * Must be first for #handle_mhd_completion_callback. + */ + struct TM_HandlerContext hc; + + /** + * Entry in the #resume_timeout_heap for this check payment, if we are + * suspended. + */ + struct TMH_SuspendedConnection sc; + + /** + * Which merchant instance is this for? + */ + struct MerchantInstance *mi; + + /** + * URL where the final contract can be found for this payment. + */ + char *final_contract_url; + + /** + * order ID for the payment + */ + const char *order_id; + + /** + * Where to get the contract + */ + const char *contract_url; + + /** + * fulfillment URL of the contract (valid as long as + * @e contract_terms is valid). + */ + const char *fulfillment_url; + + /** + * session of the client + */ + const char *session_id; + + /** + * Contract terms of the payment we are checking. NULL when they + * are not (yet) known. + */ + json_t *contract_terms; + + /** + * Hash of @e contract_terms, set only once @e contract_terms + * is available. + */ + struct GNUNET_HashCode h_contract_terms; + + /** + * Total refunds granted for this payment. Only initialized + * if @e refunded is set to #GNUNET_YES. + */ + struct TALER_Amount refund_amount; + + /** + * Minimum refund amount the client would like to poll for. + * Only initialized if + * @e awaiting_refund is set to #GNUNET_YES. + */ + struct TALER_Amount min_refund; + + /** + * Set to #GNUNET_YES if this payment has been refunded and + * @e refund_amount is initialized. + */ + int refunded; + + /** + * Set to #GNUNET_YES if this client is waiting for a refund. + */ + int awaiting_refund; + + /** + * Initially #GNUNET_SYSERR. If we queued a response, set to the + * result code (i.e. #MHD_YES or #MHD_NO). FIXME: fix type! + */ + int ret; + +}; + + +/** + * Clean up the session state for a check payment request. + * + * @param hc must be a `struct PollPaymentRequestContext *` + */ +static void +pprc_cleanup (struct TM_HandlerContext *hc) +{ + struct PollPaymentRequestContext *pprc + = (struct PollPaymentRequestContext *) hc; + + if (NULL != pprc->contract_terms) + json_decref (pprc->contract_terms); + GNUNET_free_non_null (pprc->final_contract_url); + GNUNET_free (pprc); +} + + +/** + * Function called with information about a refund. + * It is responsible for summing up the refund amount. + * + * @param cls closure + * @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 +process_refunds_cb (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 PollPaymentRequestContext *pprc = cls; + + if (pprc->refunded) + { + GNUNET_assert (0 <= + TALER_amount_add (&pprc->refund_amount, + &pprc->refund_amount, + refund_amount)); + return; + } + pprc->refund_amount = *refund_amount; + pprc->refunded = GNUNET_YES; +} + + +/** + * Suspend this @a pprc until the trigger is satisfied. + * + * @param ppr + */ +static void +suspend_pprc (struct PollPaymentRequestContext *pprc) +{ + TMH_compute_pay_key (pprc->order_id, + &pprc->mi->pubkey, + &pprc->sc.key); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending /poll-payment on key %s\n", + GNUNET_h2s (&pprc->sc.key)); + TMH_long_poll_suspend (&pprc->sc, + (pprc->awaiting_refund) + ? &pprc->min_refund + : NULL); +} + + +/** + * The client did not yet pay, send it the payment request. + * + * @param pprc check pay request context + * @return #MHD_YES on success + */ +static MHD_RESULT +send_pay_request (struct PollPaymentRequestContext *pprc) +{ + MHD_RESULT ret; + char *already_paid_order_id = NULL; + char *taler_pay_uri; + struct GNUNET_TIME_Relative remaining; + + remaining = GNUNET_TIME_absolute_get_remaining (pprc->sc.long_poll_timeout); + if (0 != remaining.rel_value_us) + { + /* long polling: do not queue a response, suspend connection instead */ + suspend_pprc (pprc); + return MHD_YES; + } + + /* Check if resource_id has been paid for in the same session + * with another order_id. + */ + if ( (NULL != pprc->session_id) && + (NULL != pprc->fulfillment_url) ) + { + enum GNUNET_DB_QueryStatus qs; + + qs = db->find_session_info (db->cls, + &already_paid_order_id, + pprc->session_id, + pprc->fulfillment_url, + &pprc->mi->pubkey); + if (qs < 0) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (pprc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_CHECK_PAYMENT_DB_FETCH_ORDER_ERROR, + "db error fetching pay session info"); + } + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Sending payment request in /poll-payment\n"); + taler_pay_uri = TMH_make_taler_pay_uri (pprc->sc.con, + pprc->order_id, + pprc->session_id, + pprc->mi->id); + ret = TALER_MHD_reply_json_pack (pprc->sc.con, + MHD_HTTP_OK, + "{s:s, s:s, s:b, s:s?}", + "taler_pay_uri", taler_pay_uri, + "contract_url", pprc->final_contract_url, + "paid", 0, + "already_paid_order_id", + already_paid_order_id); + GNUNET_free (taler_pay_uri); + GNUNET_free_non_null (already_paid_order_id); + return ret; +} + + +/** + * Manages a /public/poll-payment call, checking the status + * of a payment and, if necessary, constructing the URL + * for a payment redirect URL. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_poll_payment (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi) +{ + struct PollPaymentRequestContext *pprc = *connection_cls; + enum GNUNET_DB_QueryStatus qs; + MHD_RESULT ret; + + if (NULL == pprc) + { + /* First time here, parse request and check order is known */ + const char *long_poll_timeout_s; + const char *cts; + const char *min_refund; + + pprc = GNUNET_new (struct PollPaymentRequestContext); + pprc->hc.cc = &pprc_cleanup; + pprc->ret = GNUNET_SYSERR; + pprc->sc.con = connection; + pprc->mi = mi; + *connection_cls = pprc; + + pprc->order_id = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "order_id"); + if (NULL == pprc->order_id) + { + /* order_id is required but missing */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "order_id required"); + } + cts = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "h_contract"); + if (NULL == cts) + { + /* h_contract required but missing */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "h_contract required"); + } + if (GNUNET_OK != + GNUNET_CRYPTO_hash_from_string (cts, + &pprc->h_contract_terms)) + { + /* cts has wrong encoding */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MALFORMED, + "h_contract malformed"); + } + long_poll_timeout_s = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "timeout"); + if (NULL != long_poll_timeout_s) + { + unsigned int timeout; + + if (1 != sscanf (long_poll_timeout_s, + "%u", + &timeout)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MALFORMED, + "timeout must be non-negative number"); + } + pprc->sc.long_poll_timeout + = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply ( + GNUNET_TIME_UNIT_SECONDS, + timeout)); + } + else + { + pprc->sc.long_poll_timeout = GNUNET_TIME_UNIT_ZERO_ABS; + } + + min_refund = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "refund"); + if (NULL != min_refund) + { + if ( (GNUNET_OK != + TALER_string_to_amount (min_refund, + &pprc->min_refund)) || + (0 != strcasecmp (pprc->min_refund.currency, + TMH_currency) ) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MALFORMED, + "invalid amount given for refund argument"); + } + pprc->awaiting_refund = GNUNET_YES; + } + + pprc->contract_url = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "contract_url"); + if (NULL == pprc->contract_url) + { + pprc->final_contract_url = TALER_url_absolute_mhd (connection, + "/public/proposal", + "instance", mi->id, + "order_id", + pprc->order_id, + NULL); + GNUNET_assert (NULL != pprc->final_contract_url); + } + else + { + pprc->final_contract_url = GNUNET_strdup (pprc->contract_url); + } + pprc->session_id = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "session_id"); + + /* obtain contract terms, indirectly checking that the client's contract + terms hash is actually valid and known. */ + db->preflight (db->cls); + qs = db->find_contract_terms_from_hash (db->cls, + &pprc->contract_terms, + &pprc->h_contract_terms, + &mi->pubkey); + if (0 > qs) + { + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR, + "Merchant database error"); + } + if (0 == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_POLL_PAYMENT_CONTRACT_NOT_FOUND, + "Given order_id doesn't map to any proposal"); + } + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); + + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("fulfillment_url", + &pprc->fulfillment_url), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (pprc->contract_terms, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_CHECK_PAYMENT_DB_FETCH_CONTRACT_TERMS_ERROR, + "Merchant database error (contract terms corrupted)"); + } + } + } /* end of first-time initialization / sanity checks */ + + db->preflight (db->cls); + + /* Check if the order has been paid for. */ + if (NULL != pprc->session_id) + { + /* Check if paid within a session. */ + char *already_paid_order_id = NULL; + + qs = db->find_session_info (db->cls, + &already_paid_order_id, + pprc->session_id, + pprc->fulfillment_url, + &mi->pubkey); + if (qs < 0) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_CHECK_PAYMENT_DB_FETCH_ORDER_ERROR, + "db error fetching pay session info"); + } + else if (0 == qs) + { + ret = send_pay_request (pprc); + GNUNET_free_non_null (already_paid_order_id); + return ret; + } + GNUNET_break (1 == qs); + GNUNET_break (0 == strcmp (pprc->order_id, + already_paid_order_id)); + GNUNET_free_non_null (already_paid_order_id); + } + else + { + /* Check if paid regardless of session. */ + json_t *xcontract_terms = NULL; + + qs = db->find_paid_contract_terms_from_hash (db->cls, + &xcontract_terms, + &pprc->h_contract_terms, + &mi->pubkey); + if (0 > qs) + { + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR, + "Merchant database error"); + } + if (0 == qs) + { + return send_pay_request (pprc); + } + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); + GNUNET_assert (NULL != xcontract_terms); + json_decref (xcontract_terms); + } + + /* Accumulate refunds, if any. */ + for (unsigned int i = 0; irefunded = GNUNET_NO; + qs = db->get_refunds_from_contract_terms_hash (db->cls, + &mi->pubkey, + &pprc->h_contract_terms, + &process_refunds_cb, + pprc); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (0 > qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database hard error on refunds_from_contract_terms_hash lookup: %s\n", + GNUNET_h2s (&pprc->h_contract_terms)); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR, + "Merchant database error"); + } + if ( (pprc->awaiting_refund) && + ( (! pprc->refunded) || + (1 != TALER_amount_cmp (&pprc->refund_amount, + &pprc->min_refund)) ) ) + { + /* Client is waiting for a refund larger than what we have, suspend + until timeout */ + struct GNUNET_TIME_Relative remaining; + + remaining = GNUNET_TIME_absolute_get_remaining (pprc->sc.long_poll_timeout); + if (0 != remaining.rel_value_us) + { + /* yes, indeed suspend */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Awaiting refund exceeding %s\n", + TALER_amount2s (&pprc->min_refund)); + suspend_pprc (pprc); + return MHD_YES; + } + } + + if (pprc->refunded) + return TALER_MHD_reply_json_pack (connection, + MHD_HTTP_OK, + "{s:b, s:b, s:o}", + "paid", 1, + "refunded", pprc->refunded, + "refund_amount", + TALER_JSON_from_amount ( + &pprc->refund_amount)); + return TALER_MHD_reply_json_pack (connection, + MHD_HTTP_OK, + "{s:b, s:b }", + "paid", 1, + "refunded", 0); +} diff --git a/src/backend/taler-merchant-httpd_get-orders-ID-2.h b/src/backend/taler-merchant-httpd_get-orders-ID-2.h new file mode 100644 index 00000000..ac13c4a3 --- /dev/null +++ b/src/backend/taler-merchant-httpd_get-orders-ID-2.h @@ -0,0 +1,47 @@ +/* + This file is part of TALER + (C) 2017 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_poll-payment.h + * @brief headers for /public/poll-payment handler + * @author Christian Grothoff + * @author Florian Dold + */ +#ifndef TALER_MERCHANT_HTTPD_POLL_PAYMENT_H +#define TALER_MERCHANT_HTTPD_POLL_PAYMENT_H +#include +#include "taler-merchant-httpd.h" + +/** + * Manages a /public/poll-payment call, checking the status + * of a payment. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_poll_payment (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi); + +#endif diff --git a/src/backend/taler-merchant-httpd_get-orders-ID.c b/src/backend/taler-merchant-httpd_get-orders-ID.c new file mode 100644 index 00000000..e86e4e4b --- /dev/null +++ b/src/backend/taler-merchant-httpd_get-orders-ID.c @@ -0,0 +1,654 @@ +/* + This file is part of TALER + (C) 2014-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_refund_lookup.c + * @brief refund handling logic + * @author Marcello Stanisci + */ +#include "platform.h" +#include +#include +#include +#include +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_refund.h" + +/** + * How often do we retry DB transactions on serialization failures? + */ +#define MAX_RETRIES 5 + +/** + * Information we keep for each coin to be refunded. + */ +struct CoinRefund +{ + + /** + * Kept in a DLL. + */ + struct CoinRefund *next; + + /** + * Kept in a DLL. + */ + struct CoinRefund *prev; + + /** + * Request to connect to the target exchange. + */ + struct TMH_EXCHANGES_FindOperation *fo; + + /** + * Handle for the refund operation with the exchange. + */ + struct TALER_EXCHANGE_RefundHandle *rh; + + /** + * PRD this operation is part of. + */ + struct ProcessRefundData *prd; + + /** + * URL of the exchange for this @e coin_pub. + */ + char *exchange_url; + + /** + * Coin to refund. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Refund transaction ID to use. + */ + uint64_t rtransaction_id; + + /** + * Amount to refund. + */ + struct TALER_Amount refund_amount; + + /** + * Applicable refund transaction fee. + */ + struct TALER_Amount refund_fee; + + /** + * Public key of the exchange affirming the refund. + */ + struct TALER_ExchangePublicKeyP exchange_pub; + + /** + * Signature of the exchange affirming the refund. + */ + struct TALER_ExchangeSignatureP exchange_sig; + + /** + * HTTP status from the exchange, #MHD_HTTP_OK if + * @a exchange_pub and @a exchange_sig are valid. + */ + unsigned int exchange_status; + + /** + * HTTP error code from the exchange. + */ + enum TALER_ErrorCode exchange_code; + + /** + * Fully reply from the exchange, only possibly set if + * we got a JSON reply and a non-#MHD_HTTP_OK error code + */ + json_t *exchange_reply; + +}; + + +/** + * Closure for #process_refunds_cb. + */ +struct ProcessRefundData +{ + /** + * Must be first for #handle_mhd_completion_callback() cleanup + * logic to work. + */ + struct TM_HandlerContext hc; + + /** + * Hashed version of contract terms. + */ + struct GNUNET_HashCode h_contract_terms; + + /** + * DLL of (suspended) requests. + */ + struct ProcessRefundData *next; + + /** + * DLL of (suspended) requests. + */ + struct ProcessRefundData *prev; + + /** + * Head of DLL of coin refunds for this request. + */ + struct CoinRefund *cr_head; + + /** + * Tail of DLL of coin refunds for this request. + */ + struct CoinRefund *cr_tail; + + /** + * Both public and private key are needed by the callback + */ + const struct MerchantInstance *merchant; + + /** + * Connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * Did we suspend @a connection? + */ + int suspended; + + /** + * Return code: #TALER_EC_NONE if successful. + */ + enum TALER_ErrorCode ec; +}; + + +/** + * HEad of DLL of (suspended) requests. + */ +static struct ProcessRefundData *prd_head; + +/** + * Tail of DLL of (suspended) requests. + */ +static struct ProcessRefundData *prd_tail; + + +/** + * Clean up memory in @a cls, the connection was closed. + * + * @param cls a `struct ProcessRefundData` to clean up. + */ +static void +cleanup_prd (struct TM_HandlerContext *cls) +{ + struct ProcessRefundData *prd = (struct ProcessRefundData *) cls; + struct CoinRefund *cr; + + while (NULL != (cr = prd->cr_head)) + { + GNUNET_CONTAINER_DLL_remove (prd->cr_head, + prd->cr_tail, + cr); + if (NULL != cr->fo) + { + TMH_EXCHANGES_find_exchange_cancel (cr->fo); + cr->fo = NULL; + } + if (NULL != cr->rh) + { + TALER_EXCHANGE_refund_cancel (cr->rh); + cr->rh = NULL; + } + if (NULL != cr->exchange_reply) + { + json_decref (cr->exchange_reply); + cr->exchange_reply = NULL; + } + GNUNET_free (cr->exchange_url); + GNUNET_free (cr); + } + GNUNET_free (prd); +} + + +/** + * Check if @a prd has sub-activities still pending. + * + * @param prd request to check + * @return #GNUNET_YES if activities are still pending + */ +static int +prd_pending (struct ProcessRefundData *prd) +{ + int pending = GNUNET_NO; + + for (struct CoinRefund *cr = prd->cr_head; + NULL != cr; + cr = cr->next) + { + if ( (NULL != cr->fo) || + (NULL != cr->rh) ) + { + pending = GNUNET_YES; + break; + } + } + return pending; +} + + +/** + * Check if @a prd is ready to be resumed, and if so, do it. + * + * @param prd refund request to be possibly ready + */ +static void +check_resume_prd (struct ProcessRefundData *prd) +{ + if (prd_pending (prd)) + return; + GNUNET_CONTAINER_DLL_remove (prd_head, + prd_tail, + prd); + GNUNET_assert (prd->suspended); + prd->suspended = GNUNET_NO; + MHD_resume_connection (prd->connection); + TMH_trigger_daemon (); +} + + +/** + * Callbacks of this type are used to serve the result of submitting a + * refund request to an exchange. + * + * @param cls a `struct CoinRefund` + * @param hr HTTP response data + * @param exchange_pub exchange key used to sign refund confirmation + * @param exchange_sig exchange's signature over refund + */ +static void +refund_cb (void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const struct TALER_ExchangeSignatureP *exchange_sig) +{ + struct CoinRefund *cr = cls; + + cr->rh = NULL; + cr->exchange_status = hr->http_status; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Exchange refund status for coin %s is %u\n", + TALER_B2S (&cr->coin_pub), + hr->http_status); + if (MHD_HTTP_OK != hr->http_status) + { + cr->exchange_code = hr->ec; + cr->exchange_reply = json_incref ((json_t*) hr->reply); + } + else + { + enum GNUNET_DB_QueryStatus qs; + + cr->exchange_pub = *exchange_pub; + cr->exchange_sig = *exchange_sig; + qs = db->put_refund_proof (db->cls, + &cr->prd->merchant->pubkey, + &cr->prd->h_contract_terms, + &cr->coin_pub, + cr->rtransaction_id, + exchange_pub, + exchange_sig); + if (0 >= qs) + { + /* generally, this is relatively harmless for the merchant, but let's at + least log this. */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to persist exchange response to /refund in database: %d\n", + qs); + } + } + check_resume_prd (cr->prd); +} + + +/** + * Function called with the result of a #TMH_EXCHANGES_find_exchange() + * operation. + * + * @param cls a `struct CoinRefund *` + * @param hr HTTP response details + * @param eh handle to the exchange context + * @param wire_fee current applicable wire fee for dealing with @a eh, NULL if not available + * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config + */ +static void +exchange_found_cb (void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr, + struct TALER_EXCHANGE_Handle *eh, + const struct TALER_Amount *wire_fee, + int exchange_trusted) +{ + struct CoinRefund *cr = cls; + + cr->fo = NULL; + if (TALER_EC_NONE == hr->ec) + { + cr->rh = TALER_EXCHANGE_refund (eh, + &cr->refund_amount, + &cr->refund_fee, + &cr->prd->h_contract_terms, + &cr->coin_pub, + cr->rtransaction_id, + &cr->prd->merchant->privkey, + &refund_cb, + cr); + return; + } + cr->exchange_status = hr->http_status; + cr->exchange_code = hr->ec; + cr->exchange_reply = json_incref ((json_t*) hr->reply); + check_resume_prd (cr->prd); +} + + +/** + * Function called with information about a refund. + * It is responsible for packing up the data to return. + * + * @param cls closure + * @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 +process_refunds_cb (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 ProcessRefundData *prd = cls; + struct CoinRefund *cr; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Found refund of %s for coin %s with reason `%s' in database\n", + TALER_B2S (coin_pub), + TALER_amount2s (refund_amount), + reason); + cr = GNUNET_new (struct CoinRefund); + cr->exchange_url = GNUNET_strdup (exchange_url); + cr->prd = prd; + cr->coin_pub = *coin_pub; + cr->rtransaction_id = rtransaction_id; + cr->refund_amount = *refund_amount; + cr->refund_fee = *refund_fee; + GNUNET_CONTAINER_DLL_insert (prd->cr_head, + prd->cr_tail, + cr); +} + + +/** + * Force resuming all suspended refund lookups, needed during shutdown. + */ +void +MH_force_refund_resume (void) +{ + struct ProcessRefundData *prd; + + while (NULL != (prd = prd_head)) + { + GNUNET_CONTAINER_DLL_remove (prd_head, + prd_tail, + prd); + GNUNET_assert (prd->suspended); + prd->suspended = GNUNET_NO; + MHD_resume_connection (prd->connection); + } +} + + +/** + * Return refund situation about a contract. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_refund_lookup (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi) +{ + struct ProcessRefundData *prd; + const char *order_id; + json_t *contract_terms; + enum GNUNET_DB_QueryStatus qs; + + prd = *connection_cls; + if (NULL == prd) + { + order_id = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "order_id"); + if (NULL == order_id) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "order_id"); + } + + /* Convert order id to h_contract_terms */ + contract_terms = NULL; + db->preflight (db->cls); + qs = db->find_contract_terms (db->cls, + &contract_terms, + order_id, + &mi->pubkey); + if (0 > qs) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_REFUND_LOOKUP_DB_ERROR, + "database error looking up order_id from merchant_contract_terms table"); + } + + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Unknown order id given: `%s'\n", + order_id); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_REFUND_ORDER_ID_UNKNOWN, + "order_id not found in database"); + } + + prd = GNUNET_new (struct ProcessRefundData); + if (GNUNET_OK != + TALER_JSON_hash (contract_terms, + &prd->h_contract_terms)) + { + GNUNET_break (0); + json_decref (contract_terms); + GNUNET_free (prd); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_INTERNAL_LOGIC_ERROR, + "Could not hash contract terms"); + } + json_decref (contract_terms); + prd->hc.cc = &cleanup_prd; + prd->merchant = mi; + prd->ec = TALER_EC_NONE; + prd->connection = connection; + *connection_cls = prd; + + for (unsigned int i = 0; iget_refunds_from_contract_terms_hash (db->cls, + &mi->pubkey, + &prd->h_contract_terms, + &process_refunds_cb, + prd); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (0 > qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database hard error on refunds_from_contract_terms_hash lookup: %s\n", + GNUNET_h2s (&prd->h_contract_terms)); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_REFUND_LOOKUP_DB_ERROR, + "Failed to lookup refunds for contract"); + } + + /* Now launch exchange interactions, unless we already have the + response in the database! */ + for (struct CoinRefund *cr = prd->cr_head; + NULL != cr; + cr = cr->next) + { + enum GNUNET_DB_QueryStatus qs; + + qs = db->get_refund_proof (db->cls, + &cr->prd->merchant->pubkey, + &cr->prd->h_contract_terms, + &cr->coin_pub, + cr->rtransaction_id, + &cr->exchange_pub, + &cr->exchange_sig); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + /* We need to talk to the exchange */ + cr->fo = TMH_EXCHANGES_find_exchange (cr->exchange_url, + NULL, + GNUNET_NO, + &exchange_found_cb, + cr); + } + } + } + + /* Check if there are still exchange operations pending */ + if (GNUNET_YES == prd_pending (prd)) + { + if (! prd->suspended) + { + prd->suspended = GNUNET_YES; + MHD_suspend_connection (connection); + GNUNET_CONTAINER_DLL_insert (prd_head, + prd_tail, + prd); + } + return MHD_YES; /* we're still talking to the exchange */ + } + + /* All operations done, build final response */ + if (NULL == prd->cr_head) + { + /* There ARE no refunds scheduled, bitch */ + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_REFUND_LOOKUP_NO_REFUND, + "This contract is not currently eligible for refunds"); + } + + { + json_t *ra; + + ra = json_array (); + GNUNET_assert (NULL != ra); + for (struct CoinRefund *cr = prd->cr_head; + NULL != cr; + cr = cr->next) + { + GNUNET_assert ( + 0 == + json_array_append_new ( + ra, + (MHD_HTTP_OK != cr->exchange_status) + ? json_pack ((NULL != cr->exchange_reply) + ? "{s:o,s:o,s:o,s:I,s:I,s:I,s:O}" + : "{s:o,s:o,s:o,s:I,s:I:s:I}", + "coin_pub", + GNUNET_JSON_from_data_auto (&cr->coin_pub), + "refund_amount", + TALER_JSON_from_amount (&cr->refund_amount), + "refund_fee", + TALER_JSON_from_amount (&cr->refund_fee), + "exchange_http_status", + (json_int_t) cr->exchange_status, + "rtransaction_id", + (json_int_t) cr->rtransaction_id, + "exchange_code", + (json_int_t) cr->exchange_code, + "exchange_reply", + cr->exchange_reply) + : json_pack ("{s:o,s:o,s:o,s:I,s:I,s:o,s:o}", + "coin_pub", + GNUNET_JSON_from_data_auto (&cr->coin_pub), + "refund_amount", + TALER_JSON_from_amount (&cr->refund_amount), + "refund_fee", + TALER_JSON_from_amount (&cr->refund_fee), + "exchange_http_status", + (json_int_t) cr->exchange_status, + "rtransaction_id", + (json_int_t) cr->rtransaction_id, + "exchange_pub", + GNUNET_JSON_from_data_auto (&cr->exchange_pub), + "exchange_sig", + GNUNET_JSON_from_data_auto (&cr->exchange_sig) + ))); + } + return TALER_MHD_reply_json_pack ( + connection, + MHD_HTTP_OK, + "{s:o, s:o, s:o}", + "refunds", + ra, + "merchant_pub", + GNUNET_JSON_from_data_auto (&mi->pubkey), + "h_contract_terms", + GNUNET_JSON_from_data_auto (&prd->h_contract_terms)); + } +} + + +/* end of taler-merchant-httpd_refund_lookup.c */ diff --git a/src/backend/taler-merchant-httpd_get-orders-ID.h b/src/backend/taler-merchant-httpd_get-orders-ID.h new file mode 100644 index 00000000..24495daf --- /dev/null +++ b/src/backend/taler-merchant-httpd_get-orders-ID.h @@ -0,0 +1,48 @@ +/* + This file is part of TALER + (C) 2014, 2015, 2016, 2017 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ + +/** + * @file backend/taler-merchant-httpd_refund_lookup.h + * @brief + * @author Marcello Stanisci + */ + +#ifndef TALER_MERCHANT_HTTPD_REFUND_LOOKUP_H +#define TALER_MERCHANT_HTTPD_REFUND_LOOKUP_H +#include +#include "taler-merchant-httpd.h" + + +/** + * Return refund situation about a contract. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_refund_lookup (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi); + +#endif diff --git a/src/backend/taler-merchant-httpd_get-orders.c b/src/backend/taler-merchant-httpd_get-orders.c deleted file mode 100644 index dd353208..00000000 --- a/src/backend/taler-merchant-httpd_get-orders.c +++ /dev/null @@ -1,299 +0,0 @@ -/* - This file is part of TALER - (C) 2014, 2015, 2016, 2017 INRIA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_history.c - * @brief HTTP serving layer mainly intended to communicate with the frontend - * @author Marcello Stanisci - */ -#include "platform.h" -#include -#include -#include -#include "taler-merchant-httpd.h" - - -/** - * Closure for #pd_cb. - */ -struct ProcessContractClosure -{ - - /** - * Updated by #pd_cb to build the response. - */ - json_t *response; - - /** - * Set to #GNUNET_SYSERR if the database returned a contract - * that was not well-formed. - */ - int failure; - -}; - - -/** - * Function called with information about a transaction. - * - * @param cls closure of type `struct ProcessContractClosure` - * @param order_id transaction's order ID. - * @param row_id serial numer of the transaction in the table, - * used as index by the frontend to skip previous results. - */ -static void -pd_cb (void *cls, - const char *order_id, - uint64_t row_id, - const json_t *contract_terms) -{ - struct ProcessContractClosure *pcc = cls; - json_t *entry; - json_t *amount; - json_t *timestamp; - json_t *instance; - json_t *summary; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "/history's row_id: %llu\n", - (unsigned long long) row_id); - summary = NULL; - if (-1 == json_unpack ((json_t *) contract_terms, - "{s:o, s:o, s:{s:o}, s?:o}", - "amount", &amount, - "timestamp", ×tamp, - "merchant", "instance", &instance, - "summary", &summary)) - { - GNUNET_break (0); - pcc->failure = GNUNET_SYSERR; - return; - } - - /* summary is optional, but we need something, so we use - the order ID if it is not given. */ - if (NULL == summary) - summary = json_string (order_id); - - if (NULL == (entry = - json_pack ("{s:I, s:s, s:O, s:O, s:O, s:O}", - "row_id", row_id, - "order_id", order_id, - "amount", amount, - "timestamp", timestamp, - "instance", instance, - "summary", summary))) - { - GNUNET_break (0); - pcc->failure = GNUNET_SYSERR; - return; - } - if (0 != - json_array_append_new (pcc->response, - entry)) - { - GNUNET_break (0); - pcc->failure = GNUNET_SYSERR; - json_decref (entry); - return; - } -} - - -/** - * Manage a /history request. Query the db and returns transactions - * younger than the date given as parameter - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_history (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi) -{ - #define LOG_INFO(...) GNUNET_log (GNUNET_ERROR_TYPE_INFO, __VA_ARGS__) - const char *str; - struct GNUNET_TIME_Absolute date; - json_t *response; - MHD_RESULT ret; - unsigned int ascending = GNUNET_NO; - unsigned long long seconds; - unsigned long long start = INT64_MAX; - long long delta = -20; - enum GNUNET_DB_QueryStatus qs; - struct ProcessContractClosure pcc; - - LOG_INFO ("Serving /history\n"); - response = json_array (); - str = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "date"); - date = GNUNET_TIME_absolute_get (); - (void) GNUNET_TIME_round_abs (&date); - if (NULL != str) - { - if (1 != sscanf (str, - "%llu", - &seconds)) - { - json_decref (response); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MALFORMED, - "date"); - } - date.abs_value_us = seconds * 1000LL * 1000LL; - if (date.abs_value_us / 1000LL / 1000LL != seconds) - { - json_decref (response); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_HISTORY_TIMESTAMP_OVERFLOW, - "Timestamp overflowed"); - } - } - - /* Sanity check that we don't have some odd stale transaction running */ - db->preflight (db->cls); - - /* Here goes the cherry-picking logic */ - str = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "order_id"); - if (NULL != str) - { - pcc.response = response; - pcc.failure = GNUNET_NO; - qs = db->find_contract_terms_history (db->cls, - str, - &mi->pubkey, - &pd_cb, - &pcc); - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - if ( (0 > qs) || - (GNUNET_SYSERR == pcc.failure) ) - { - json_decref (response); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_HISTORY_DB_FETCH_ERROR, - "db error to get history"); - } - ret = TALER_MHD_reply_json (connection, - response, - MHD_HTTP_OK); - json_decref (response); - return ret; - } - - str = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "start"); - if (NULL != str) - { - TALER_LOG_DEBUG ("'start' argument given ('%s')\n", - str); - if (1 != sscanf (str, - "%llu", - &start)) - { - json_decref (response); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MALFORMED, - "start"); - } - } - - str = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "delta"); - - if (NULL != str) - { - if (1 != sscanf (str, - "%lld", - &delta)) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MALFORMED, - "delta"); - } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Querying history back to %s, start: %llu, delta: %lld\n", - GNUNET_STRINGS_absolute_time_to_string (date), - start, - delta); - - pcc.response = response; - pcc.failure = GNUNET_NO; - - str = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "ordering"); - if ( (NULL != str) && - (0 == strcmp ("ascending", - str)) ) - ascending = GNUNET_YES; - - qs = db->find_contract_terms_by_date_and_range (db->cls, - date, - &mi->pubkey, - start, - llabs (delta), - (delta < 0) ? GNUNET_YES : - GNUNET_NO, - ascending, - &pd_cb, - &pcc); - if ( (0 > qs) || - (GNUNET_SYSERR == pcc.failure) ) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - json_decref (response); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_HISTORY_DB_FETCH_ERROR, - "db error to get history"); - } - ret = TALER_MHD_reply_json_pack (connection, - MHD_HTTP_OK, - "{ s:o }", - "history", - response /* consumes 'response' */); - LOG_INFO ("/history, http code: %d\n", - MHD_HTTP_OK); - return ret; -} - - -/* end of taler-merchant-httpd_history.c */ diff --git a/src/backend/taler-merchant-httpd_get-reserves-reserve.c b/src/backend/taler-merchant-httpd_get-reserves-reserve.c deleted file mode 100644 index f7aa0ab0..00000000 --- a/src/backend/taler-merchant-httpd_get-reserves-reserve.c +++ /dev/null @@ -1,248 +0,0 @@ -/* - This file is part of TALER - (C) 2018 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_tip-query.c - * @brief implement API for authorizing tips to be paid to visitors - * @author Christian Grothoff - * @author Florian Dold - */ -#include "platform.h" -#include -#include -#include -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_mhd.h" -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_tip-query.h" -#include "taler-merchant-httpd_tip-reserve-helper.h" - - -/** - * Maximum number of retries for database operations. - */ -#define MAX_RETRIES 5 - - -/** - * Internal per-request state for processing tip queries. - */ -struct TipQueryContext -{ - /** - * This field MUST be first for handle_mhd_completion_callback() to work - * when it treats this struct as a `struct TM_HandlerContext`. - */ - struct TM_HandlerContext hc; - - /** - * Merchant instance to use. - */ - const char *instance; - - /** - * Context for checking the tipping reserve's status. - */ - struct TMH_CheckTipReserve ctr; - - /** - * #GNUNET_YES if the tip query has already been processed - * and we can queue the response. - */ - int processed; - -}; - - -/** - * Custom cleanup routine for a `struct TipQueryContext`. - * - * @param hc the `struct TMH_JsonParseContext` to clean up. - */ -static void -cleanup_tqc (struct TM_HandlerContext *hc) -{ - struct TipQueryContext *tqc = (struct TipQueryContext *) hc; - - TMH_check_tip_reserve_cleanup (&tqc->ctr); - GNUNET_free (tqc); -} - - -/** - * We've been resumed after processing the reserve data from the - * exchange without error. Generate the final response. - * - * @param tqc context for which to generate the response. - */ -static int -generate_final_response (struct TipQueryContext *tqc) -{ - struct GNUNET_CRYPTO_EddsaPublicKey reserve_pub; - struct TALER_Amount amount_available; - - GNUNET_CRYPTO_eddsa_key_get_public (&tqc->ctr.reserve_priv.eddsa_priv, - &reserve_pub); - if (0 > - TALER_amount_subtract (&amount_available, - &tqc->ctr.amount_deposited, - &tqc->ctr.amount_withdrawn)) - { - char *a1; - char *a2; - - GNUNET_break_op (0); - a1 = TALER_amount_to_string (&tqc->ctr.amount_deposited); - a2 = TALER_amount_to_string (&tqc->ctr.amount_withdrawn); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "amount overflow, deposited %s but withdrawn %s\n", - a1, - a2); - GNUNET_free (a2); - GNUNET_free (a1); - return TALER_MHD_reply_with_error ( - tqc->ctr.connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_TIP_QUERY_RESERVE_HISTORY_ARITHMETIC_ISSUE_INCONSISTENT, - "Exchange returned invalid reserve history (amount overflow)"); - } - return TALER_MHD_reply_json_pack ( - tqc->ctr.connection, - MHD_HTTP_OK, - "{s:o, s:o, s:o, s:o, s:o}", - "reserve_pub", - GNUNET_JSON_from_data_auto (&reserve_pub), - "reserve_expiration", - GNUNET_JSON_from_time_abs (tqc->ctr.reserve_expiration), - "amount_authorized", - TALER_JSON_from_amount (&tqc->ctr.amount_authorized), - "amount_picked_up", - TALER_JSON_from_amount (&tqc->ctr.amount_withdrawn), - "amount_available", - TALER_JSON_from_amount (&amount_available)); -} - - -/** - * Handle a "/tip-query" request. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_tip_query (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi) -{ - struct TipQueryContext *tqc; - - if (NULL == *connection_cls) - { - tqc = GNUNET_new (struct TipQueryContext); - tqc->hc.cc = &cleanup_tqc; - tqc->ctr.connection = connection; - *connection_cls = tqc; - } - else - { - tqc = *connection_cls; - } - - if (0 != tqc->ctr.response_code) - { - MHD_RESULT res; - - /* We are *done* processing the request, just queue the response (!) */ - if (UINT_MAX == tqc->ctr.response_code) - { - GNUNET_break (0); - return MHD_NO; /* hard error */ - } - res = MHD_queue_response (connection, - tqc->ctr.response_code, - tqc->ctr.response); - MHD_destroy_response (tqc->ctr.response); - tqc->ctr.response = NULL; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Queueing response (%u) for /tip-query (%s).\n", - (unsigned int) tqc->ctr.response_code, - res ? "OK" : "FAILED"); - return res; - } - - if (GNUNET_YES == tqc->processed) - { - /* We've been here before, so TMH_check_tip_reserve() must have - finished and left the result for us. Finish processing. */ - return generate_final_response (tqc); - } - - if (NULL == mi->tip_exchange) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Instance `%s' not configured for tipping\n", - mi->id); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_PRECONDITION_FAILED, - TALER_EC_TIP_QUERY_INSTANCE_DOES_NOT_TIP, - "exchange for tipping not configured for the instance"); - } - tqc->ctr.reserve_priv = mi->tip_reserve; - - { - enum GNUNET_DB_QueryStatus qs; - - for (unsigned int i = 0; ipreflight (db->cls); - qs = db->get_authorized_tip_amount (db->cls, - &tqc->ctr.reserve_priv, - &tqc->ctr.amount_authorized); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_TIP_QUERY_DB_ERROR, - "Merchant database error"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - /* we'll set amount_authorized to zero later once - we know the currency */ - tqc->ctr.none_authorized = GNUNET_YES; - } - } - - tqc->processed = GNUNET_YES; - TMH_check_tip_reserve (&tqc->ctr, - mi->tip_exchange); - return MHD_YES; -} - - -/* end of taler-merchant-httpd_tip-query.c */ diff --git a/src/backend/taler-merchant-httpd_get-reserves.c b/src/backend/taler-merchant-httpd_get-reserves.c deleted file mode 100644 index e104e089..00000000 --- a/src/backend/taler-merchant-httpd_get-reserves.c +++ /dev/null @@ -1,462 +0,0 @@ -/* - This file is part of TALER - (C) 2018--2020 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_tip-reserve-helper.c - * @brief helper functions to check the status of a tipping reserve - * @author Christian Grothoff - */ -#include "platform.h" -#include "taler-merchant-httpd_tip-reserve-helper.h" - - -/** - * Head of active ctr context DLL. - */ -static struct TMH_CheckTipReserve *ctr_head; - -/** - * Tail of active ctr context DLL. - */ -static struct TMH_CheckTipReserve *ctr_tail; - - -/** - * Resume connection underlying @a ctr. - * - * @param ctr what to resume - */ -static void -resume_ctr (struct TMH_CheckTipReserve *ctr) -{ - GNUNET_assert (GNUNET_YES == ctr->suspended); - GNUNET_CONTAINER_DLL_remove (ctr_head, - ctr_tail, - ctr); - MHD_resume_connection (ctr->connection); - TMH_trigger_daemon (); /* we resumed, kick MHD */ -} - - -/** - * Resume the given context and send the given response. Stores the response - * in the @a ctr and signals MHD to resume the connection. Also ensures MHD - * runs immediately. - * - * @param ctr tip reserve query helper context - * @param response_code response code to use - * @param response response data to send back - */ -static void -resume_with_response (struct TMH_CheckTipReserve *ctr, - unsigned int response_code, - struct MHD_Response *response) -{ - ctr->response_code = response_code; - ctr->response = response; - resume_ctr (ctr); - ctr->suspended = GNUNET_NO; -} - - -/** - * Function called with the result of the /reserve/status request - * for the tipping reserve. Update our database balance with the - * result. - * - * @param cls closure with a `struct TMH_CheckTipReserve *' - * @param hr HTTP response details - * @param balance current balance in the reserve, NULL on error - * @param history_length number of entries in the transaction history, 0 on error - * @param history detailed transaction history, NULL on error - */ -static void -handle_status (void *cls, - const struct TALER_EXCHANGE_HttpResponse *hr, - const struct TALER_Amount *balance, - unsigned int history_length, - const struct TALER_EXCHANGE_ReserveHistory *history) -{ - struct TMH_CheckTipReserve *ctr = cls; - - ctr->rsh = NULL; - ctr->reserve_expiration = GNUNET_TIME_UNIT_ZERO_ABS; - if (MHD_HTTP_NOT_FOUND == hr->http_status) - { - resume_with_response ( - ctr, - MHD_HTTP_SERVICE_UNAVAILABLE, - TALER_MHD_make_json_pack ( - "{s:I, s:I, s:s, s:I, s:O}", - "code", (json_int_t) TALER_EC_TIP_QUERY_RESERVE_UNKNOWN_TO_EXCHANGE, - "exchange_http_status", hr->http_status, - "hint", "tipping reserve unknown at exchange", - "exchange_code", hr->ec, - "exchange_reply", hr->reply)); - return; - } - if (MHD_HTTP_OK != hr->http_status) - { - GNUNET_break_op (0); - resume_with_response ( - ctr, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ( - "{s:I, s:I, s:s, s:I, s:O}", - "code", (json_int_t) TALER_EC_TIP_QUERY_RESERVE_HISTORY_FAILED, - "exchange_http_status", hr->http_status, - "hint", "exchange failed to provide reserve history", - "exchange_code", (json_int_t) hr->ec, - "exchange_reply", hr->reply)); - return; - } - - if (0 == history_length) - { - GNUNET_break_op (0); - resume_with_response (ctr, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_error ( - TALER_EC_TIP_QUERY_RESERVE_HISTORY_FAILED_EMPTY, - "Exchange returned empty reserve history")); - return; - } - - { - unsigned int found = UINT_MAX; - - for (unsigned int i = 0; iamount_withdrawn)); - } - - if (GNUNET_YES == ctr->none_authorized) - ctr->amount_authorized = ctr->amount_withdrawn; /* aka zero */ - ctr->amount_deposited = ctr->amount_withdrawn; /* aka zero */ - - /* Update DB based on status! */ - for (unsigned int i = 0; itype) - { - case TALER_EXCHANGE_RTT_CREDIT: - { - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_HashCode uuid; - struct GNUNET_TIME_Absolute deposit_expiration; - - if (0 > - TALER_amount_add (&ctr->amount_deposited, - &ctr->amount_deposited, - &hi->amount)) - { - GNUNET_break_op (0); - resume_with_response ( - ctr, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_error ( - TALER_EC_TIP_QUERY_RESERVE_HISTORY_ARITHMETIC_ISSUE_DEPOSIT, - "Exchange returned invalid reserve history (amount overflow)")); - return; - } - deposit_expiration = GNUNET_TIME_absolute_add ( - hi->details.in_details.timestamp, - ctr->idle_reserve_expiration_time); - /* We're interested in the latest DEPOSIT timestamp, since this determines the - * reserve's expiration date. Note that the history isn't chronologically ordered. */ - ctr->reserve_expiration = GNUNET_TIME_absolute_max ( - ctr->reserve_expiration, - deposit_expiration); - GNUNET_CRYPTO_hash (hi->details.in_details.wire_reference, - hi->details.in_details.wire_reference_size, - &uuid); - db->preflight (db->cls); - qs = db->enable_tip_reserve_TR (db->cls, - &ctr->reserve_priv, - &uuid, - &hi->amount, - deposit_expiration); - - if (0 > qs) - { - /* This is not inherently fatal for the client's request, so we merely log it */ - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Database error updating tipping reserve status: %d\n", - qs); - } - } - break; - case TALER_EXCHANGE_RTT_WITHDRAWAL: - if (0 > - TALER_amount_add (&ctr->amount_withdrawn, - &ctr->amount_withdrawn, - &hi->amount)) - { - GNUNET_break_op (0); - resume_with_response ( - ctr, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_error ( - TALER_EC_TIP_QUERY_RESERVE_HISTORY_ARITHMETIC_ISSUE_WITHDRAW, - "Exchange returned invalid reserve history (amount overflow)")); - return; - } - break; - case TALER_EXCHANGE_RTT_RECOUP: - { - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_HashContext *hc; - struct GNUNET_HashCode uuid; - struct GNUNET_TIME_Absolute deposit_expiration; - struct GNUNET_TIME_AbsoluteNBO de; - - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Encountered unexpected recoup operation on tipping reserve\n"); - /* While unexpected, we can simply count these like deposits. */ - if (0 > - TALER_amount_add (&ctr->amount_deposited, - &ctr->amount_deposited, - &hi->amount)) - { - GNUNET_break_op (0); - resume_with_response ( - ctr, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_error ( - TALER_EC_TIP_QUERY_RESERVE_HISTORY_ARITHMETIC_ISSUE_RECOUP, - "Exchange returned invalid reserve history (amount overflow)")); - return; - } - deposit_expiration = GNUNET_TIME_absolute_add ( - hi->details.recoup_details.timestamp, - ctr->idle_reserve_expiration_time); - ctr->reserve_expiration = GNUNET_TIME_absolute_max ( - ctr->reserve_expiration, - deposit_expiration); - de = GNUNET_TIME_absolute_hton (deposit_expiration); - hc = GNUNET_CRYPTO_hash_context_start (); - GNUNET_CRYPTO_hash_context_read ( - hc, - &hi->details.recoup_details.coin_pub, - sizeof (struct TALER_CoinSpendPublicKeyP)); - GNUNET_CRYPTO_hash_context_read (hc, - &de, - sizeof (de)); - GNUNET_CRYPTO_hash_context_finish (hc, - &uuid); - db->preflight (db->cls); - qs = db->enable_tip_reserve_TR (db->cls, - &ctr->reserve_priv, - &uuid, - &hi->amount, - deposit_expiration); - if (0 > qs) - { - /* This is not inherently fatal for the client's request, so we merely log it */ - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Database error updating tipping reserve status: %d\n", - qs); - } - } - break; - case TALER_EXCHANGE_RTT_CLOSE: - /* We count 'closing' amounts just like withdrawals */ - if (0 > - TALER_amount_add (&ctr->amount_withdrawn, - &ctr->amount_withdrawn, - &hi->amount)) - { - GNUNET_break_op (0); - resume_with_response ( - ctr, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_error ( - TALER_EC_TIP_QUERY_RESERVE_HISTORY_ARITHMETIC_ISSUE_CLOSED, - "Exchange returned invalid reserve history (amount overflow)")); - return; - } - break; - } - } - - /* normal, non-error continuation */ - resume_with_response (ctr, - 0, - NULL); -} - - -/** - * Function called with the result of a #TMH_EXCHANGES_find_exchange() - * operation. Given the exchange handle, we will then interrogate - * the exchange about the status of the tipping reserve. - * - * @param cls closure with a `struct TMH_CheckTipReserve *` - * @param hr HTTP response details - * @param eh handle to the exchange context - * @param wire_fee current applicable wire fee for dealing with @a eh, NULL if not available - * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config - */ -static void -exchange_cont (void *cls, - const struct TALER_EXCHANGE_HttpResponse *hr, - struct TALER_EXCHANGE_Handle *eh, - const struct TALER_Amount *wire_fee, - int exchange_trusted) -{ - struct TMH_CheckTipReserve *ctr = cls; - struct TALER_ReservePublicKeyP reserve_pub; - const struct TALER_EXCHANGE_Keys *keys; - - ctr->fo = NULL; - if (NULL == eh) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to contact exchange configured for tipping!\n"); - resume_with_response (ctr, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_error ( - TALER_EC_TIP_QUERY_RESERVE_STATUS_FAILED_EXCHANGE_DOWN, - "Unable to obtain /keys from exchange")); - return; - } - keys = TALER_EXCHANGE_get_keys (eh); - GNUNET_assert (NULL != keys); - ctr->idle_reserve_expiration_time - = keys->reserve_closing_delay; - GNUNET_CRYPTO_eddsa_key_get_public (&ctr->reserve_priv.eddsa_priv, - &reserve_pub.eddsa_pub); - ctr->rsh = TALER_EXCHANGE_reserves_get (eh, - &reserve_pub, - &handle_status, - ctr); -} - - -/** - * Check the status of the given reserve at the given exchange. - * Suspends the MHD connection while this is happening and resumes - * processing once we know the reserve status (or once an error - * code has been determined). - * - * @param[in,out] ctr context for checking the reserve status - * @param tip_exchange the URL of the exchange to query - */ -void -TMH_check_tip_reserve (struct TMH_CheckTipReserve *ctr, - const char *tip_exchange) -{ - MHD_suspend_connection (ctr->connection); - ctr->suspended = GNUNET_YES; - GNUNET_CONTAINER_DLL_insert (ctr_head, - ctr_tail, - ctr); - db->preflight (db->cls); - ctr->fo = TMH_EXCHANGES_find_exchange (tip_exchange, - NULL, - GNUNET_NO, - &exchange_cont, - ctr); - if (NULL == ctr->fo) - { - GNUNET_break (0); - resume_with_response (ctr, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_MHD_make_error ( - TALER_EC_INTERNAL_INVARIANT_FAILURE, - "Unable to find exchange handle")); - } -} - - -/** - * Clean up any state that might be left in @a ctr. - * - * @param[in] context to clean up - */ -void -TMH_check_tip_reserve_cleanup (struct TMH_CheckTipReserve *ctr) -{ - if (NULL != ctr->rsh) - { - TALER_EXCHANGE_reserves_get_cancel (ctr->rsh); - ctr->rsh = NULL; - } - if (NULL != ctr->fo) - { - TMH_EXCHANGES_find_exchange_cancel (ctr->fo); - ctr->fo = NULL; - } - if (NULL != ctr->response) - { - MHD_destroy_response (ctr->response); - ctr->response = NULL; - } - if (MHD_YES == ctr->suspended) - { - resume_ctr (ctr); - ctr->suspended = GNUNET_NO; - } -} - - -/** - * Force all tip reserve helper contexts to be resumed as we are about to shut - * down MHD. - */ -void -MH_force_trh_resume () -{ - struct TMH_CheckTipReserve *n; - - for (struct TMH_CheckTipReserve *ctr = ctr_head; - NULL != ctr; - ctr = n) - { - n = ctr->next; - resume_ctr (ctr); - ctr->suspended = GNUNET_SYSERR; - } -} - - -/* end of taler-merchant-httpd_tip-reserve-helper.c */ diff --git a/src/backend/taler-merchant-httpd_get-tips-tip.c b/src/backend/taler-merchant-httpd_get-tips-tip.c deleted file mode 100644 index 42066e3c..00000000 --- a/src/backend/taler-merchant-httpd_get-tips-tip.c +++ /dev/null @@ -1,146 +0,0 @@ -/* - This file is part of TALER - (C) 2017-2020 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_tip-pickup.c - * @brief implementation of /tip-pickup handler - * @author Christian Grothoff - */ -#include "platform.h" -#include -#include -#include -#include -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_mhd.h" -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_tip-pickup.h" - - -/** - * Manages a GET /tip-pickup call, checking that the tip is authorized, - * and if so, returning the withdrawal permissions. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_tip_pickup_get (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi) -{ - const char *tip_id_str; - char *exchange_url; - json_t *extra; - struct GNUNET_HashCode tip_id; - struct TALER_Amount tip_amount; - struct TALER_Amount tip_amount_left; - struct GNUNET_TIME_Absolute timestamp; - struct GNUNET_TIME_Absolute timestamp_expire; - MHD_RESULT ret; - enum GNUNET_DB_QueryStatus qs; - - tip_id_str = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "tip_id"); - - if (NULL == tip_id_str) - { - /* tip_id is required but missing */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MISSING, - "tip_id required"); - } - - if (GNUNET_OK != - GNUNET_CRYPTO_hash_from_string (tip_id_str, - &tip_id)) - { - /* tip_id has wrong encoding */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MALFORMED, - "tip_id malformed"); - } - - db->preflight (db->cls); - qs = db->lookup_tip_by_id (db->cls, - &tip_id, - &exchange_url, - &extra, - &tip_amount, - &tip_amount_left, - ×tamp); - - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) - { - unsigned int response_code; - enum TALER_ErrorCode ec; - - switch (qs) - { - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - ec = TALER_EC_TIP_PICKUP_TIP_ID_UNKNOWN; - response_code = MHD_HTTP_NOT_FOUND; - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - ec = TALER_EC_TIP_PICKUP_DB_ERROR_SOFT; - response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; - break; - case GNUNET_DB_STATUS_HARD_ERROR: - ec = TALER_EC_TIP_PICKUP_DB_ERROR_HARD; - response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; - break; - default: - GNUNET_break (0); - ec = TALER_EC_INTERNAL_LOGIC_ERROR; - response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; - break; - } - return TALER_MHD_reply_with_error (connection, - response_code, - ec, - "Could not determine exchange URL for the given tip id"); - } - - timestamp_expire = GNUNET_TIME_absolute_add (timestamp, - GNUNET_TIME_UNIT_DAYS); - - ret = TALER_MHD_reply_json_pack ( - connection, - MHD_HTTP_OK, - "{s:s, s:o, s:o, s:o, s:o, s:o}", - "exchange_url", exchange_url, - "amount", TALER_JSON_from_amount (&tip_amount), - "amount_left", TALER_JSON_from_amount (&tip_amount_left), - "stamp_created", GNUNET_JSON_from_time_abs (timestamp), - "stamp_expire", GNUNET_JSON_from_time_abs (timestamp_expire), - "extra", extra); - - GNUNET_free (exchange_url); - json_decref (extra); - return ret; -} diff --git a/src/backend/taler-merchant-httpd_get-transfers.c b/src/backend/taler-merchant-httpd_get-transfers.c deleted file mode 100644 index 7f55c917..00000000 --- a/src/backend/taler-merchant-httpd_get-transfers.c +++ /dev/null @@ -1,1089 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2020 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_track-transfer.c - * @brief implement API for tracking transfers and wire transfers - * @author Marcello Stanisci - * @author Christian Grothoff - */ -#include "platform.h" -#include -#include -#include -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_mhd.h" -#include "taler-merchant-httpd_auditors.h" -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_track-transfer.h" - - -/** - * How long to wait before giving up processing with the exchange? - */ -#define TRACK_TIMEOUT (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, \ - 30)) - -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - -/** - * Context used for handing /track/transfer requests. - */ -struct TrackTransferContext -{ - - /** - * This MUST be first! - */ - struct TM_HandlerContext hc; - - /** - * Handle to the exchange. - */ - struct TALER_EXCHANGE_Handle *eh; - - /** - * Handle for the /wire/transfers request. - */ - struct TALER_EXCHANGE_TransfersGetHandle *wdh; - - /** - * For which merchant instance is this tracking request? - */ - struct MerchantInstance *mi; - - /** - * HTTP connection we are handling. - */ - struct MHD_Connection *connection; - - /** - * Response to return upon resume. - */ - struct MHD_Response *response; - - /** - * Handle for operation to lookup /keys (and auditors) from - * the exchange used for this transaction; NULL if no operation is - * pending. - */ - struct TMH_EXCHANGES_FindOperation *fo; - - /** - * Task run on timeout. - */ - struct GNUNET_SCHEDULER_Task *timeout_task; - - /** - * URL of the exchange. - */ - char *url; - - /** - * Wire method used for the transfer. - */ - char *wire_method; - - /** - * Pointer to the detail that we are currently - * checking in #check_transfer(). - */ - const struct TALER_TrackTransferDetails *current_detail; - - /** - * Argument for the /wire/transfers request. - */ - struct TALER_WireTransferIdentifierRawP wtid; - - /** - * Full original response we are currently processing. - */ - const json_t *original_response; - - /** - * Modified response to return to the frontend. - */ - json_t *deposits_response; - - /** - * Which transaction detail are we currently looking at? - */ - unsigned int current_offset; - - /** - * Response code to return. - */ - unsigned int response_code; - - /** - * #GNUNET_NO if we did not find a matching coin. - * #GNUNET_SYSERR if we found a matching coin, but the amounts do not match. - * #GNUNET_OK if we did find a matching coin. - */ - int check_transfer_result; -}; - - -/** - * Represents an entry in the table used to sum up - * individual deposits for each h_contract_terms. - */ -struct Entry -{ - - /** - * Sum accumulator for deposited value. - */ - struct TALER_Amount deposit_value; - - /** - * Sum accumulator for deposit fee. - */ - struct TALER_Amount deposit_fee; - -}; - - -/** - * Free the @a rctx. - * - * @param rctx data to free - */ -static void -free_transfer_track_context (struct TrackTransferContext *rctx) -{ - if (NULL != rctx->fo) - { - TMH_EXCHANGES_find_exchange_cancel (rctx->fo); - rctx->fo = NULL; - } - if (NULL != rctx->timeout_task) - { - GNUNET_SCHEDULER_cancel (rctx->timeout_task); - rctx->timeout_task = NULL; - } - if (NULL != rctx->wdh) - { - TALER_EXCHANGE_transfers_get_cancel (rctx->wdh); - rctx->wdh = NULL; - } - if (NULL != rctx->url) - { - GNUNET_free (rctx->url); - rctx->url = NULL; - } - if (NULL != rctx->wire_method) - { - GNUNET_free (rctx->wire_method); - rctx->wire_method = NULL; - } - GNUNET_free (rctx); -} - - -/** - * Callback that frees all the elements in the hashmap - * - * @param cls closure, NULL - * @param key current key - * @param value a `struct Entry` - * @return #GNUNET_YES if the iteration should continue, - * #GNUNET_NO otherwise. - */ -static int -hashmap_free (void *cls, - const struct GNUNET_HashCode *key, - void *value) -{ - struct TALER_Entry *entry = value; - - (void) cls; - (void) key; - GNUNET_free (entry); - return GNUNET_YES; -} - - -/** - * Builds JSON response containing the summed-up amounts - * from individual deposits. - * - * @param cls closure - * @param key map's current key - * @param map's current value - * @return #GNUNET_YES if iteration is to be continued, - * #GNUNET_NO otherwise. - */ -static int -build_deposits_response (void *cls, - const struct GNUNET_HashCode *key, - void *value) -{ - struct TrackTransferContext *rctx = cls; - struct Entry *entry = value; - json_t *element; - json_t *contract_terms; - json_t *order_id; - - db->preflight (db->cls); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != - db->find_contract_terms_from_hash (db->cls, - &contract_terms, - key, - &rctx->mi->pubkey)) - { - GNUNET_break_op (0); - return GNUNET_NO; - } - - order_id = json_object_get (contract_terms, - "order_id"); - if (NULL == order_id) - { - GNUNET_break_op (0); - json_decref (contract_terms); - return GNUNET_NO; - } - element = json_pack ("{s:O, s:o, s:o}", - "order_id", order_id, - "deposit_value", TALER_JSON_from_amount ( - &entry->deposit_value), - "deposit_fee", TALER_JSON_from_amount ( - &entry->deposit_fee)); - json_decref (contract_terms); - if (NULL == element) - { - GNUNET_break_op (0); - return GNUNET_NO; - } - GNUNET_break (0 == - json_array_append_new (rctx->deposits_response, - element)); - return GNUNET_YES; -} - - -/** - * Transform /track/transfer result as gotten from the exchange - * and transforms it in a format liked by the backoffice Web interface. - * - * @param result response from exchange's /track/transfer - * @result pointer to new JSON, or NULL upon errors. - */ -static json_t * -transform_response (const json_t *result, - struct TrackTransferContext *rctx) -{ - json_t *deposits; - json_t *value; - json_t *result_mod = NULL; - size_t index; - const char *key; - struct GNUNET_HashCode h_key; - struct GNUNET_CONTAINER_MultiHashMap *map; - struct TALER_Amount iter_value; - struct TALER_Amount iter_fee; - struct Entry *current_entry; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount ("deposit_value", &iter_value), - TALER_JSON_spec_amount ("deposit_fee", &iter_fee), - GNUNET_JSON_spec_string ("h_contract_terms", &key), - GNUNET_JSON_spec_end () - }; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Transforming /track/transfer response.\n"); - map = GNUNET_CONTAINER_multihashmap_create (1, GNUNET_NO); - deposits = json_object_get (result, - "deposits"); - - json_array_foreach (deposits, index, value) - { - if (GNUNET_OK != - GNUNET_JSON_parse (value, - spec, - NULL, - NULL)) - { - GNUNET_break_op (0); - return NULL; - } - GNUNET_CRYPTO_hash_from_string (key, - &h_key); - - if (NULL != (current_entry = - GNUNET_CONTAINER_multihashmap_get (map, - &h_key))) - { - /* The map already knows this h_contract_terms*/ - if ( (0 > - TALER_amount_add (¤t_entry->deposit_value, - ¤t_entry->deposit_value, - &iter_value)) || - (0 > - TALER_amount_add (¤t_entry->deposit_fee, - ¤t_entry->deposit_fee, - &iter_fee)) ) - { - GNUNET_JSON_parse_free (spec); - goto cleanup; - } - } - else - { - /* First time in the map for this h_contract_terms*/ - current_entry = GNUNET_new (struct Entry); - current_entry->deposit_value = iter_value; - current_entry->deposit_fee = iter_fee; - - if (GNUNET_SYSERR == - GNUNET_CONTAINER_multihashmap_put (map, - &h_key, - current_entry, - GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)) - { - GNUNET_JSON_parse_free (spec); - goto cleanup; - } - } - GNUNET_JSON_parse_free (spec); - } - rctx->deposits_response = json_array (); - - if (GNUNET_SYSERR == - GNUNET_CONTAINER_multihashmap_iterate (map, - &build_deposits_response, - rctx)) - goto cleanup; - - result_mod = json_copy ((struct json_t *) result); - json_object_del (result_mod, - "deposits"); - json_object_set_new (result_mod, - "deposits_sums", - rctx->deposits_response); - rctx->deposits_response = NULL; -cleanup: - GNUNET_CONTAINER_multihashmap_iterate (map, - &hashmap_free, - NULL); - GNUNET_CONTAINER_multihashmap_destroy (map); - return result_mod; -} - - -/** - * Resume the given /track/transfer operation and send the given response. - * Stores the response in the @a rctx and signals MHD to resume - * the connection. Also ensures MHD runs immediately. - * - * @param rctx transfer tracking context - * @param response_code response code to use - * @param response response data to send back - */ -static void -resume_track_transfer_with_response (struct TrackTransferContext *rctx, - unsigned int response_code, - struct MHD_Response *response) -{ - rctx->response_code = response_code; - rctx->response = response; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Resuming /track/transfer handling as exchange interaction is done (%u)\n", - response_code); - if (NULL != rctx->timeout_task) - { - GNUNET_SCHEDULER_cancel (rctx->timeout_task); - rctx->timeout_task = NULL; - } - MHD_resume_connection (rctx->connection); - TMH_trigger_daemon (); /* we resumed, kick MHD */ -} - - -/** - * Custom cleanup routine for a `struct TrackTransferContext`. - * - * @param hc the `struct TrackTransferContext` to clean up. - */ -static void -track_transfer_cleanup (struct TM_HandlerContext *hc) -{ - struct TrackTransferContext *rctx = (struct TrackTransferContext *) hc; - - free_transfer_track_context (rctx); -} - - -/** - * This function checks that the information about the coin which - * was paid back by _this_ wire transfer matches what _we_ (the merchant) - * knew about this coin. - * - * @param cls closure with our `struct TrackTransferContext *` - * @param transaction_id of the contract - * @param coin_pub public key of the coin - * @param exchange_url URL of the exchange that issued @a coin_pub - * @param amount_with_fee amount the exchange will transfer 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 exchange_proof proof from exchange that coin was accepted - */ -static void -check_transfer (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 TrackTransferContext *rctx = cls; - const struct TALER_TrackTransferDetails *ttd = rctx->current_detail; - - if (GNUNET_SYSERR == rctx->check_transfer_result) - return; /* already had a serious issue; odd that we're called more than once as well... */ - if ( (0 != TALER_amount_cmp (amount_with_fee, - &ttd->coin_value)) || - (0 != TALER_amount_cmp (deposit_fee, - &ttd->coin_fee)) ) - { - /* Disagreement between the exchange and us about how much this - coin is worth! */ - GNUNET_break_op (0); - rctx->check_transfer_result = GNUNET_SYSERR; - /* Build the `TrackTransferConflictDetails` */ - rctx->response - = TALER_MHD_make_json_pack ( - "{s:I, s:s, s:o, s:I, s:o, s:o, s:s, s:o, s:o}", - "code", (json_int_t) TALER_EC_TRACK_TRANSFER_CONFLICTING_REPORTS, - "hint", "disagreement about deposit valuation", - "exchange_deposit_proof", exchange_proof, - "conflict_offset", (json_int_t) rctx->current_offset, - "exchange_transfer_proof", rctx->original_response, - "coin_pub", GNUNET_JSON_from_data_auto (coin_pub), - "h_contract_terms", GNUNET_JSON_from_data_auto ( - &ttd->h_contract_terms), - "amount_with_fee", TALER_JSON_from_amount (amount_with_fee), - "deposit_fee", TALER_JSON_from_amount (deposit_fee)); - return; - } - rctx->check_transfer_result = GNUNET_OK; -} - - -/** - * Check that the given @a wire_fee is what the - * @a exchange_pub should charge at the @a execution_time. - * If the fee is correct (according to our database), - * return #GNUNET_OK. If we do not have the fee structure - * in our DB, we just accept it and return #GNUNET_NO; - * if we have proof that the fee is bogus, we respond with - * the proof to the client and return #GNUNET_SYSERR. - * - * @param rctx context of the transfer to respond to - * @param json response from the exchange - * @param execution_time time of the wire transfer - * @param wire_fee fee claimed by the exchange - * @return #GNUNET_SYSERR if we returned hard proof of - * missbehavior from the exchange to the client - */ -static int -check_wire_fee (struct TrackTransferContext *rctx, - const json_t *json, - struct GNUNET_TIME_Absolute execution_time, - const struct TALER_Amount *wire_fee) -{ - const struct TALER_MasterPublicKeyP *master_pub; - struct GNUNET_HashCode h_wire_method; - struct TALER_Amount expected_fee; - struct TALER_Amount closing_fee; - struct TALER_MasterSignatureP master_sig; - struct GNUNET_TIME_Absolute start_date; - struct GNUNET_TIME_Absolute end_date; - enum GNUNET_DB_QueryStatus qs; - const struct TALER_EXCHANGE_Keys *keys; - - keys = TALER_EXCHANGE_get_keys (rctx->eh); - if (NULL == keys) - { - GNUNET_break (0); - return GNUNET_NO; - } - master_pub = &keys->master_pub; - GNUNET_CRYPTO_hash (rctx->wire_method, - strlen (rctx->wire_method) + 1, - &h_wire_method); - db->preflight (db->cls); - qs = db->lookup_wire_fee (db->cls, - master_pub, - &h_wire_method, - execution_time, - &expected_fee, - &closing_fee, - &start_date, - &end_date, - &master_sig); - if (0 >= qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to find wire fee for `%s' and method `%s' at %s in DB, accepting blindly that the fee is %s\n", - TALER_B2S (master_pub), - rctx->wire_method, - GNUNET_STRINGS_absolute_time_to_string (execution_time), - TALER_amount2s (wire_fee)); - return GNUNET_NO; - } - if (0 <= TALER_amount_cmp (&expected_fee, - wire_fee)) - return GNUNET_OK; /* expected_fee >= wire_fee */ - - /* Wire fee check failed, export proof to client */ - resume_track_transfer_with_response ( - rctx, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ( - "{s:I, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:O}", - "code", (json_int_t) TALER_EC_TRACK_TRANSFER_JSON_BAD_WIRE_FEE, - "wire_fee", TALER_JSON_from_amount (wire_fee), - "execution_time", GNUNET_JSON_from_time_abs (execution_time), - "expected_wire_fee", TALER_JSON_from_amount (&expected_fee), - "expected_closing_fee", TALER_JSON_from_amount (&closing_fee), - "start_date", GNUNET_JSON_from_time_abs (start_date), - "end_date", GNUNET_JSON_from_time_abs (end_date), - "master_sig", GNUNET_JSON_from_data_auto (&master_sig), - "master_pub", GNUNET_JSON_from_data_auto (master_pub), - "json", json)); - return GNUNET_SYSERR; -} - - -/** - * Function called with detailed wire transfer data, including all - * of the coin transactions that were combined into the wire transfer. - * - * @param cls closure - * @param hr HTTP response details - * @param exchange_pub public key of the exchange used to sign @a json - * @param h_wire hash of the wire transfer address the transfer went to, or NULL on error - * @param execution_time time when the exchange claims to have performed the wire transfer - * @param total_amount total amount of the wire transfer, or NULL if the exchange could - * not provide any @a wtid (set only if @a http_status is #MHD_HTTP_OK) - * @param wire_fee wire fee that was charged by the exchange - * @param details_length length of the @a details array - * @param details array with details about the combined transactions - */ -static void -wire_transfer_cb (void *cls, - const struct TALER_EXCHANGE_HttpResponse *hr, - const struct TALER_ExchangePublicKeyP *exchange_pub, - const struct GNUNET_HashCode *h_wire, - struct GNUNET_TIME_Absolute execution_time, - const struct TALER_Amount *total_amount, - const struct TALER_Amount *wire_fee, - unsigned int details_length, - const struct TALER_TrackTransferDetails *details) -{ - struct TrackTransferContext *rctx = cls; - json_t *jresponse; - enum GNUNET_DB_QueryStatus qs; - - rctx->wdh = NULL; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Got response code %u from exchange for /track/transfer\n", - hr->http_status); - if (MHD_HTTP_OK != hr->http_status) - { - resume_track_transfer_with_response ( - rctx, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ( - "{s:I, s:I, s:I, s:O}", - "code", (json_int_t) TALER_EC_TRACK_TRANSFER_EXCHANGE_ERROR, - "exchange_code", (json_int_t) hr->ec, - "exchange_http_status", (json_int_t) hr->http_status, - "exchange_reply", hr->reply)); - return; - } - for (unsigned int i = 0; ipreflight (db->cls); - qs = db->store_transfer_to_proof (db->cls, - rctx->url, - &rctx->wtid, - execution_time, - exchange_pub, - hr->reply); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - if (0 > qs) - { - /* Special report if retries insufficient */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - resume_track_transfer_with_response - (rctx, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_MHD_make_json_pack ("{s:I, s:s}", - "code", - (json_int_t) - TALER_EC_TRACK_TRANSFER_DB_STORE_TRANSFER_ERROR, - "details", - "failed to store response from exchange to local database")); - return; - } - rctx->original_response = hr->reply; - - if (GNUNET_SYSERR == - check_wire_fee (rctx, - hr->reply, - execution_time, - wire_fee)) - return; - - /* Now we want to double-check that any (Taler coin) deposit - * which is accounted into _this_ wire transfer, does exist - * into _our_ database. This is the rationale: if the - * exchange paid us for it, we must have received it _beforehands_! - * - * details_length is how many (Taler coin) deposits have been - * aggregated into _this_ wire transfer. - */// - for (unsigned int i = 0; icurrent_offset = i; - rctx->current_detail = &details[i]; - /* Set the coin as "never seen" before. */ - rctx->check_transfer_result = GNUNET_NO; - db->preflight (db->cls); - qs = db->find_payments_by_hash_and_coin (db->cls, - &details[i].h_contract_terms, - &rctx->mi->pubkey, - &details[i].coin_pub, - &check_transfer, - rctx); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - resume_track_transfer_with_response - (rctx, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_MHD_make_json_pack ("{s:I, s:s}", - "code", - (json_int_t) - TALER_EC_TRACK_TRANSFER_DB_FETCH_DEPOSIT_ERROR, - "details", - "failed to obtain deposit data from local database")); - return; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - /* The exchange says we made this deposit, but WE do not - recall making it (corrupted / unreliable database?)! - Well, let's say thanks and accept the money! */ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to find payment data in DB\n"); - rctx->check_transfer_result = GNUNET_OK; - } - if (GNUNET_NO == rctx->check_transfer_result) - { - /* Internal error: how can we have called #check_transfer() - but still have no result? */ - GNUNET_break (0); - resume_track_transfer_with_response - (rctx, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_MHD_make_json_pack ("{s:I, s:s, s:I, s:s}", - "code", - (json_int_t) - TALER_EC_TRACK_TRANSFER_DB_INTERNAL_LOGIC_ERROR, - "details", "internal logic error", - "line", (json_int_t) __LINE__, - "file", __FILE__)); - return; - } - if (GNUNET_SYSERR == rctx->check_transfer_result) - { - /* #check_transfer() failed, report conflict! */ - GNUNET_break_op (0); - GNUNET_assert (NULL != rctx->response); - resume_track_transfer_with_response - (rctx, - MHD_HTTP_FAILED_DEPENDENCY, - rctx->response); - rctx->response = NULL; - return; - } - /* Response is consistent with the /deposit we made, - remember it for future reference */ - for (unsigned int r = 0; rpreflight (db->cls); - qs = db->store_coin_to_transfer (db->cls, - &details[i].h_contract_terms, - &details[i].coin_pub, - &rctx->wtid); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - if (0 > qs) - { - /* Special report if retries insufficient */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - resume_track_transfer_with_response - (rctx, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_MHD_make_json_pack ("{s:I, s:s}", - "code", - (json_int_t) - TALER_EC_TRACK_TRANSFER_DB_STORE_COIN_ERROR, - "details", - "failed to store response from exchange to local database")); - return; - } - } - rctx->original_response = NULL; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "About to call tracks transformator.\n"); - - if (NULL == (jresponse = - transform_response (hr->reply, - rctx))) - { - resume_track_transfer_with_response - (rctx, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_MHD_make_error (TALER_EC_TRACK_TRANSFER_JSON_RESPONSE_ERROR, - "Fail to elaborate the response.")); - return; - } - - resume_track_transfer_with_response (rctx, - MHD_HTTP_OK, - TALER_MHD_make_json (jresponse)); - json_decref (jresponse); -} - - -/** - * Function called with the result of our exchange lookup. - * - * @param cls the `struct TrackTransferContext` - * @param hr HTTP response details - * @param eh NULL if exchange was not found to be acceptable - * @param wire_fee NULL (we did not specify a wire method) - * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config - */ -static void -process_track_transfer_with_exchange (void *cls, - const struct - TALER_EXCHANGE_HttpResponse *hr, - struct TALER_EXCHANGE_Handle *eh, - const struct TALER_Amount *wire_fee, - int exchange_trusted) -{ - struct TrackTransferContext *rctx = cls; - - rctx->fo = NULL; - if (MHD_HTTP_OK != hr->http_status) - { - /* The request failed somehow */ - GNUNET_break_op (0); - resume_track_transfer_with_response ( - rctx, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ( - (NULL != hr->reply) - ? "{s:s, s:I, s:I, s:I, s:O}" - : "{s:s, s:I, s:I, s:I}", - "hint", "failed to obtain meta-data from exchange", - "code", (json_int_t) TALER_EC_TRACK_TRANSFER_EXCHANGE_KEYS_FAILURE, - "exchange_http_status", (json_int_t) hr->http_status, - "exchange_code", (json_int_t) hr->ec, - "exchange_reply", hr->reply)); - return; - } - rctx->eh = eh; - rctx->wdh = TALER_EXCHANGE_transfers_get (eh, - &rctx->wtid, - &wire_transfer_cb, - rctx); - if (NULL == rctx->wdh) - { - GNUNET_break (0); - resume_track_transfer_with_response - (rctx, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_MHD_make_json_pack ("{s:I, s:s}", - "code", - (json_int_t) - TALER_EC_TRACK_TRANSFER_REQUEST_ERROR, - "error", - "failed to run /transfers/ GET on exchange")); - } -} - - -/** - * Handle a timeout for the processing of the track transfer request. - * - * @param cls closure - */ -static void -handle_track_transfer_timeout (void *cls) -{ - struct TrackTransferContext *rctx = cls; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Resuming /track/transfer with error after timeout\n"); - rctx->timeout_task = NULL; - - if (NULL != rctx->fo) - { - TMH_EXCHANGES_find_exchange_cancel (rctx->fo); - rctx->fo = NULL; - } - resume_track_transfer_with_response (rctx, - MHD_HTTP_SERVICE_UNAVAILABLE, - TALER_MHD_make_error ( - TALER_EC_TRACK_TRANSFER_EXCHANGE_TIMEOUT, - "exchange not reachable")); -} - - -/** - * Function called with information about a wire transfer identifier. - * Generate a response based on the given @a proof. - * - * @param cls closure - * @param proof proof from exchange about what the wire transfer was for. - * should match the `TrackTransactionResponse` format - * of the exchange - */ -static void -proof_cb (void *cls, - const json_t *proof) -{ - struct TrackTransferContext *rctx = cls; - json_t *transformed_response; - - if (NULL == (transformed_response = - transform_response (proof, - rctx))) - { - rctx->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; - rctx->response - = TALER_MHD_make_error (TALER_EC_TRACK_TRANSFER_JSON_RESPONSE_ERROR, - "Fail to elaborate response."); - return; - } - - rctx->response_code = MHD_HTTP_OK; - rctx->response = TALER_MHD_make_json (transformed_response); - json_decref (transformed_response); -} - - -/** - * Manages a /track/transfer call, thus it calls the /track/wtid - * offered by the exchange in order to return the set of transfers - * (of coins) associated with a given wire transfer. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_track_transfer (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi) -{ - struct TrackTransferContext *rctx; - const char *str; - const char *url; - const char *wire_method; - MHD_RESULT ret; - enum GNUNET_DB_QueryStatus qs; - - if (NULL == *connection_cls) - { - rctx = GNUNET_new (struct TrackTransferContext); - rctx->hc.cc = &track_transfer_cleanup; - rctx->connection = connection; - *connection_cls = rctx; - } - else - { - /* not first call, recover state */ - rctx = *connection_cls; - } - - if (0 != rctx->response_code) - { - /* We are *done* processing the request, just queue the response (!) */ - if (UINT_MAX == rctx->response_code) - { - GNUNET_break (0); - return MHD_NO; /* hard error */ - } - ret = MHD_queue_response (connection, - rctx->response_code, - rctx->response); - if (NULL != rctx->response) - { - MHD_destroy_response (rctx->response); - rctx->response = NULL; - } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Queueing response (%u) for /track/transfer (%s).\n", - (unsigned int) rctx->response_code, - ret ? "OK" : "FAILED"); - return ret; - } - if ( (NULL != rctx->fo) || - (NULL != rctx->eh) ) - { - /* likely old MHD version */ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Not sure why we are here, should be suspended\n"); - return MHD_YES; /* still work in progress */ - } - - url = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "exchange"); - if (NULL == url) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MISSING, - "exchange"); - rctx->url = GNUNET_strdup (url); - - /* FIXME: change again: we probably don't want the wire_method - but rather the _account_ (section) here! */ - wire_method = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "wire_method"); - if (NULL == wire_method) - { - if (1) - { - /* temporary work-around until demo is adjusted... */ - GNUNET_break (0); - wire_method = "x-taler-bank"; - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Client needs fixing, see API change for #4943!\n"); - } - else - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MISSING, - "wire_method"); - } - rctx->wire_method = GNUNET_strdup (wire_method); - rctx->mi = mi; - str = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "wtid"); - if (NULL == str) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MISSING, - "wtid"); - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (str, - strlen (str), - &rctx->wtid, - sizeof (rctx->wtid))) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MALFORMED, - "wtid"); - } - - /* Check if reply is already in database! */ - db->preflight (db->cls); - qs = db->find_proof_by_wtid (db->cls, - rctx->url, - &rctx->wtid, - &proof_cb, - rctx); - if (0 > qs) - { - /* Simple select queries should not cause serialization issues */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_TRACK_TRANSFER_DB_FETCH_DEPOSIT_ERROR, - "Fail to query database about proofs"); - } - if (0 != rctx->response_code) - { - ret = MHD_queue_response (connection, - rctx->response_code, - rctx->response); - if (NULL != rctx->response) - { - MHD_destroy_response (rctx->response); - rctx->response = NULL; - } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Queueing response (%u) for /track/transfer (%s).\n", - (unsigned int) rctx->response_code, - ret ? "OK" : "FAILED"); - return ret; - } - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Suspending /track/transfer handling while working with the exchange\n"); - MHD_suspend_connection (connection); - rctx->fo = TMH_EXCHANGES_find_exchange (url, - NULL, - GNUNET_NO, - &process_track_transfer_with_exchange, - rctx); - rctx->timeout_task - = GNUNET_SCHEDULER_add_delayed (TRACK_TIMEOUT, - &handle_track_transfer_timeout, - rctx); - return MHD_YES; -} - - -/* end of taler-merchant-httpd_track-transfer.c */ diff --git a/src/backend/taler-merchant-httpd_orders_get.h b/src/backend/taler-merchant-httpd_orders_get.h deleted file mode 100644 index eac987dd..00000000 --- a/src/backend/taler-merchant-httpd_orders_get.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - This file is part of TALER - (C) 2014, 2015, 2016 INRIA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_history.c - * @brief HTTP serving layer mainly intended to communicate with the frontend - * @author Marcello Stanisci - */ - -#ifndef TALER_MERCHANT_HTTPD_HISTORY_H -#define TALER_MERCHANT_HTTPD_HISTORY_H -#include -#include "taler-merchant-httpd.h" - - -/** - * Manage a /history request. Query the db and returns transactions - * younger than the date given as parameter - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_history (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi); - -/* end of taler-merchant-httpd_history.c */ -#endif diff --git a/src/backend/taler-merchant-httpd_orders_order_abort.h b/src/backend/taler-merchant-httpd_orders_order_abort.h deleted file mode 100644 index 726a27be..00000000 --- a/src/backend/taler-merchant-httpd_orders_order_abort.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2017 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_pay.h - * @brief headers for /pay handler - * @author Marcello Stanisci - */ -#ifndef TALER_EXCHANGE_HTTPD_PAY_H -#define TALER_EXCHANGE_HTTPD_PAY_H -#include -#include "taler-merchant-httpd.h" - - -/** - * Force all pay contexts to be resumed as we are about - * to shut down MHD. - */ -void -MH_force_pc_resume (void); - - -/** - * Manage a payment - * - * @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 - * @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); - -#endif diff --git a/src/backend/taler-merchant-httpd_orders_order_get.c b/src/backend/taler-merchant-httpd_orders_order_get.c deleted file mode 100644 index 6ca4fcce..00000000 --- a/src/backend/taler-merchant-httpd_orders_order_get.c +++ /dev/null @@ -1,582 +0,0 @@ -/* - This file is part of TALER - (C) 2017, 2019 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_poll-payment.c - * @brief implementation of /public/poll-payment handler - * @author Florian Dold - * @author Christian Grothoff - */ -#include "platform.h" -#include -#include -#include -#include -#include -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_mhd.h" -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_poll-payment.h" - -/** - * Maximum number of retries for database operations. - */ -#define MAX_RETRIES 5 - - -/** - * Data structure we keep for a check payment request. - */ -struct PollPaymentRequestContext -{ - /** - * Must be first for #handle_mhd_completion_callback. - */ - struct TM_HandlerContext hc; - - /** - * Entry in the #resume_timeout_heap for this check payment, if we are - * suspended. - */ - struct TMH_SuspendedConnection sc; - - /** - * Which merchant instance is this for? - */ - struct MerchantInstance *mi; - - /** - * URL where the final contract can be found for this payment. - */ - char *final_contract_url; - - /** - * order ID for the payment - */ - const char *order_id; - - /** - * Where to get the contract - */ - const char *contract_url; - - /** - * fulfillment URL of the contract (valid as long as - * @e contract_terms is valid). - */ - const char *fulfillment_url; - - /** - * session of the client - */ - const char *session_id; - - /** - * Contract terms of the payment we are checking. NULL when they - * are not (yet) known. - */ - json_t *contract_terms; - - /** - * Hash of @e contract_terms, set only once @e contract_terms - * is available. - */ - struct GNUNET_HashCode h_contract_terms; - - /** - * Total refunds granted for this payment. Only initialized - * if @e refunded is set to #GNUNET_YES. - */ - struct TALER_Amount refund_amount; - - /** - * Minimum refund amount the client would like to poll for. - * Only initialized if - * @e awaiting_refund is set to #GNUNET_YES. - */ - struct TALER_Amount min_refund; - - /** - * Set to #GNUNET_YES if this payment has been refunded and - * @e refund_amount is initialized. - */ - int refunded; - - /** - * Set to #GNUNET_YES if this client is waiting for a refund. - */ - int awaiting_refund; - - /** - * Initially #GNUNET_SYSERR. If we queued a response, set to the - * result code (i.e. #MHD_YES or #MHD_NO). FIXME: fix type! - */ - int ret; - -}; - - -/** - * Clean up the session state for a check payment request. - * - * @param hc must be a `struct PollPaymentRequestContext *` - */ -static void -pprc_cleanup (struct TM_HandlerContext *hc) -{ - struct PollPaymentRequestContext *pprc - = (struct PollPaymentRequestContext *) hc; - - if (NULL != pprc->contract_terms) - json_decref (pprc->contract_terms); - GNUNET_free_non_null (pprc->final_contract_url); - GNUNET_free (pprc); -} - - -/** - * Function called with information about a refund. - * It is responsible for summing up the refund amount. - * - * @param cls closure - * @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 -process_refunds_cb (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 PollPaymentRequestContext *pprc = cls; - - if (pprc->refunded) - { - GNUNET_assert (0 <= - TALER_amount_add (&pprc->refund_amount, - &pprc->refund_amount, - refund_amount)); - return; - } - pprc->refund_amount = *refund_amount; - pprc->refunded = GNUNET_YES; -} - - -/** - * Suspend this @a pprc until the trigger is satisfied. - * - * @param ppr - */ -static void -suspend_pprc (struct PollPaymentRequestContext *pprc) -{ - TMH_compute_pay_key (pprc->order_id, - &pprc->mi->pubkey, - &pprc->sc.key); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Suspending /poll-payment on key %s\n", - GNUNET_h2s (&pprc->sc.key)); - TMH_long_poll_suspend (&pprc->sc, - (pprc->awaiting_refund) - ? &pprc->min_refund - : NULL); -} - - -/** - * The client did not yet pay, send it the payment request. - * - * @param pprc check pay request context - * @return #MHD_YES on success - */ -static MHD_RESULT -send_pay_request (struct PollPaymentRequestContext *pprc) -{ - MHD_RESULT ret; - char *already_paid_order_id = NULL; - char *taler_pay_uri; - struct GNUNET_TIME_Relative remaining; - - remaining = GNUNET_TIME_absolute_get_remaining (pprc->sc.long_poll_timeout); - if (0 != remaining.rel_value_us) - { - /* long polling: do not queue a response, suspend connection instead */ - suspend_pprc (pprc); - return MHD_YES; - } - - /* Check if resource_id has been paid for in the same session - * with another order_id. - */ - if ( (NULL != pprc->session_id) && - (NULL != pprc->fulfillment_url) ) - { - enum GNUNET_DB_QueryStatus qs; - - qs = db->find_session_info (db->cls, - &already_paid_order_id, - pprc->session_id, - pprc->fulfillment_url, - &pprc->mi->pubkey); - if (qs < 0) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (pprc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_CHECK_PAYMENT_DB_FETCH_ORDER_ERROR, - "db error fetching pay session info"); - } - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Sending payment request in /poll-payment\n"); - taler_pay_uri = TMH_make_taler_pay_uri (pprc->sc.con, - pprc->order_id, - pprc->session_id, - pprc->mi->id); - ret = TALER_MHD_reply_json_pack (pprc->sc.con, - MHD_HTTP_OK, - "{s:s, s:s, s:b, s:s?}", - "taler_pay_uri", taler_pay_uri, - "contract_url", pprc->final_contract_url, - "paid", 0, - "already_paid_order_id", - already_paid_order_id); - GNUNET_free (taler_pay_uri); - GNUNET_free_non_null (already_paid_order_id); - return ret; -} - - -/** - * Manages a /public/poll-payment call, checking the status - * of a payment and, if necessary, constructing the URL - * for a payment redirect URL. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_poll_payment (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi) -{ - struct PollPaymentRequestContext *pprc = *connection_cls; - enum GNUNET_DB_QueryStatus qs; - MHD_RESULT ret; - - if (NULL == pprc) - { - /* First time here, parse request and check order is known */ - const char *long_poll_timeout_s; - const char *cts; - const char *min_refund; - - pprc = GNUNET_new (struct PollPaymentRequestContext); - pprc->hc.cc = &pprc_cleanup; - pprc->ret = GNUNET_SYSERR; - pprc->sc.con = connection; - pprc->mi = mi; - *connection_cls = pprc; - - pprc->order_id = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "order_id"); - if (NULL == pprc->order_id) - { - /* order_id is required but missing */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MISSING, - "order_id required"); - } - cts = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "h_contract"); - if (NULL == cts) - { - /* h_contract required but missing */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MISSING, - "h_contract required"); - } - if (GNUNET_OK != - GNUNET_CRYPTO_hash_from_string (cts, - &pprc->h_contract_terms)) - { - /* cts has wrong encoding */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MALFORMED, - "h_contract malformed"); - } - long_poll_timeout_s = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "timeout"); - if (NULL != long_poll_timeout_s) - { - unsigned int timeout; - - if (1 != sscanf (long_poll_timeout_s, - "%u", - &timeout)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MALFORMED, - "timeout must be non-negative number"); - } - pprc->sc.long_poll_timeout - = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply ( - GNUNET_TIME_UNIT_SECONDS, - timeout)); - } - else - { - pprc->sc.long_poll_timeout = GNUNET_TIME_UNIT_ZERO_ABS; - } - - min_refund = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "refund"); - if (NULL != min_refund) - { - if ( (GNUNET_OK != - TALER_string_to_amount (min_refund, - &pprc->min_refund)) || - (0 != strcasecmp (pprc->min_refund.currency, - TMH_currency) ) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MALFORMED, - "invalid amount given for refund argument"); - } - pprc->awaiting_refund = GNUNET_YES; - } - - pprc->contract_url = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "contract_url"); - if (NULL == pprc->contract_url) - { - pprc->final_contract_url = TALER_url_absolute_mhd (connection, - "/public/proposal", - "instance", mi->id, - "order_id", - pprc->order_id, - NULL); - GNUNET_assert (NULL != pprc->final_contract_url); - } - else - { - pprc->final_contract_url = GNUNET_strdup (pprc->contract_url); - } - pprc->session_id = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "session_id"); - - /* obtain contract terms, indirectly checking that the client's contract - terms hash is actually valid and known. */ - db->preflight (db->cls); - qs = db->find_contract_terms_from_hash (db->cls, - &pprc->contract_terms, - &pprc->h_contract_terms, - &mi->pubkey); - if (0 > qs) - { - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR, - "Merchant database error"); - } - if (0 == qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_POLL_PAYMENT_CONTRACT_NOT_FOUND, - "Given order_id doesn't map to any proposal"); - } - GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); - - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("fulfillment_url", - &pprc->fulfillment_url), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (pprc->contract_terms, - spec, - NULL, NULL)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_CHECK_PAYMENT_DB_FETCH_CONTRACT_TERMS_ERROR, - "Merchant database error (contract terms corrupted)"); - } - } - } /* end of first-time initialization / sanity checks */ - - db->preflight (db->cls); - - /* Check if the order has been paid for. */ - if (NULL != pprc->session_id) - { - /* Check if paid within a session. */ - char *already_paid_order_id = NULL; - - qs = db->find_session_info (db->cls, - &already_paid_order_id, - pprc->session_id, - pprc->fulfillment_url, - &mi->pubkey); - if (qs < 0) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_CHECK_PAYMENT_DB_FETCH_ORDER_ERROR, - "db error fetching pay session info"); - } - else if (0 == qs) - { - ret = send_pay_request (pprc); - GNUNET_free_non_null (already_paid_order_id); - return ret; - } - GNUNET_break (1 == qs); - GNUNET_break (0 == strcmp (pprc->order_id, - already_paid_order_id)); - GNUNET_free_non_null (already_paid_order_id); - } - else - { - /* Check if paid regardless of session. */ - json_t *xcontract_terms = NULL; - - qs = db->find_paid_contract_terms_from_hash (db->cls, - &xcontract_terms, - &pprc->h_contract_terms, - &mi->pubkey); - if (0 > qs) - { - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR, - "Merchant database error"); - } - if (0 == qs) - { - return send_pay_request (pprc); - } - GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); - GNUNET_assert (NULL != xcontract_terms); - json_decref (xcontract_terms); - } - - /* Accumulate refunds, if any. */ - for (unsigned int i = 0; irefunded = GNUNET_NO; - qs = db->get_refunds_from_contract_terms_hash (db->cls, - &mi->pubkey, - &pprc->h_contract_terms, - &process_refunds_cb, - pprc); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - if (0 > qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Database hard error on refunds_from_contract_terms_hash lookup: %s\n", - GNUNET_h2s (&pprc->h_contract_terms)); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR, - "Merchant database error"); - } - if ( (pprc->awaiting_refund) && - ( (! pprc->refunded) || - (1 != TALER_amount_cmp (&pprc->refund_amount, - &pprc->min_refund)) ) ) - { - /* Client is waiting for a refund larger than what we have, suspend - until timeout */ - struct GNUNET_TIME_Relative remaining; - - remaining = GNUNET_TIME_absolute_get_remaining (pprc->sc.long_poll_timeout); - if (0 != remaining.rel_value_us) - { - /* yes, indeed suspend */ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Awaiting refund exceeding %s\n", - TALER_amount2s (&pprc->min_refund)); - suspend_pprc (pprc); - return MHD_YES; - } - } - - if (pprc->refunded) - return TALER_MHD_reply_json_pack (connection, - MHD_HTTP_OK, - "{s:b, s:b, s:o}", - "paid", 1, - "refunded", pprc->refunded, - "refund_amount", - TALER_JSON_from_amount ( - &pprc->refund_amount)); - return TALER_MHD_reply_json_pack (connection, - MHD_HTTP_OK, - "{s:b, s:b }", - "paid", 1, - "refunded", 0); -} diff --git a/src/backend/taler-merchant-httpd_orders_order_get.h b/src/backend/taler-merchant-httpd_orders_order_get.h deleted file mode 100644 index ac13c4a3..00000000 --- a/src/backend/taler-merchant-httpd_orders_order_get.h +++ /dev/null @@ -1,47 +0,0 @@ -/* - This file is part of TALER - (C) 2017 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_poll-payment.h - * @brief headers for /public/poll-payment handler - * @author Christian Grothoff - * @author Florian Dold - */ -#ifndef TALER_MERCHANT_HTTPD_POLL_PAYMENT_H -#define TALER_MERCHANT_HTTPD_POLL_PAYMENT_H -#include -#include "taler-merchant-httpd.h" - -/** - * Manages a /public/poll-payment call, checking the status - * of a payment. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_poll_payment (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi); - -#endif diff --git a/src/backend/taler-merchant-httpd_orders_order_get2.c b/src/backend/taler-merchant-httpd_orders_order_get2.c deleted file mode 100644 index e86e4e4b..00000000 --- a/src/backend/taler-merchant-httpd_orders_order_get2.c +++ /dev/null @@ -1,654 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2020 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_refund_lookup.c - * @brief refund handling logic - * @author Marcello Stanisci - */ -#include "platform.h" -#include -#include -#include -#include -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_refund.h" - -/** - * How often do we retry DB transactions on serialization failures? - */ -#define MAX_RETRIES 5 - -/** - * Information we keep for each coin to be refunded. - */ -struct CoinRefund -{ - - /** - * Kept in a DLL. - */ - struct CoinRefund *next; - - /** - * Kept in a DLL. - */ - struct CoinRefund *prev; - - /** - * Request to connect to the target exchange. - */ - struct TMH_EXCHANGES_FindOperation *fo; - - /** - * Handle for the refund operation with the exchange. - */ - struct TALER_EXCHANGE_RefundHandle *rh; - - /** - * PRD this operation is part of. - */ - struct ProcessRefundData *prd; - - /** - * URL of the exchange for this @e coin_pub. - */ - char *exchange_url; - - /** - * Coin to refund. - */ - struct TALER_CoinSpendPublicKeyP coin_pub; - - /** - * Refund transaction ID to use. - */ - uint64_t rtransaction_id; - - /** - * Amount to refund. - */ - struct TALER_Amount refund_amount; - - /** - * Applicable refund transaction fee. - */ - struct TALER_Amount refund_fee; - - /** - * Public key of the exchange affirming the refund. - */ - struct TALER_ExchangePublicKeyP exchange_pub; - - /** - * Signature of the exchange affirming the refund. - */ - struct TALER_ExchangeSignatureP exchange_sig; - - /** - * HTTP status from the exchange, #MHD_HTTP_OK if - * @a exchange_pub and @a exchange_sig are valid. - */ - unsigned int exchange_status; - - /** - * HTTP error code from the exchange. - */ - enum TALER_ErrorCode exchange_code; - - /** - * Fully reply from the exchange, only possibly set if - * we got a JSON reply and a non-#MHD_HTTP_OK error code - */ - json_t *exchange_reply; - -}; - - -/** - * Closure for #process_refunds_cb. - */ -struct ProcessRefundData -{ - /** - * Must be first for #handle_mhd_completion_callback() cleanup - * logic to work. - */ - struct TM_HandlerContext hc; - - /** - * Hashed version of contract terms. - */ - struct GNUNET_HashCode h_contract_terms; - - /** - * DLL of (suspended) requests. - */ - struct ProcessRefundData *next; - - /** - * DLL of (suspended) requests. - */ - struct ProcessRefundData *prev; - - /** - * Head of DLL of coin refunds for this request. - */ - struct CoinRefund *cr_head; - - /** - * Tail of DLL of coin refunds for this request. - */ - struct CoinRefund *cr_tail; - - /** - * Both public and private key are needed by the callback - */ - const struct MerchantInstance *merchant; - - /** - * Connection we are handling. - */ - struct MHD_Connection *connection; - - /** - * Did we suspend @a connection? - */ - int suspended; - - /** - * Return code: #TALER_EC_NONE if successful. - */ - enum TALER_ErrorCode ec; -}; - - -/** - * HEad of DLL of (suspended) requests. - */ -static struct ProcessRefundData *prd_head; - -/** - * Tail of DLL of (suspended) requests. - */ -static struct ProcessRefundData *prd_tail; - - -/** - * Clean up memory in @a cls, the connection was closed. - * - * @param cls a `struct ProcessRefundData` to clean up. - */ -static void -cleanup_prd (struct TM_HandlerContext *cls) -{ - struct ProcessRefundData *prd = (struct ProcessRefundData *) cls; - struct CoinRefund *cr; - - while (NULL != (cr = prd->cr_head)) - { - GNUNET_CONTAINER_DLL_remove (prd->cr_head, - prd->cr_tail, - cr); - if (NULL != cr->fo) - { - TMH_EXCHANGES_find_exchange_cancel (cr->fo); - cr->fo = NULL; - } - if (NULL != cr->rh) - { - TALER_EXCHANGE_refund_cancel (cr->rh); - cr->rh = NULL; - } - if (NULL != cr->exchange_reply) - { - json_decref (cr->exchange_reply); - cr->exchange_reply = NULL; - } - GNUNET_free (cr->exchange_url); - GNUNET_free (cr); - } - GNUNET_free (prd); -} - - -/** - * Check if @a prd has sub-activities still pending. - * - * @param prd request to check - * @return #GNUNET_YES if activities are still pending - */ -static int -prd_pending (struct ProcessRefundData *prd) -{ - int pending = GNUNET_NO; - - for (struct CoinRefund *cr = prd->cr_head; - NULL != cr; - cr = cr->next) - { - if ( (NULL != cr->fo) || - (NULL != cr->rh) ) - { - pending = GNUNET_YES; - break; - } - } - return pending; -} - - -/** - * Check if @a prd is ready to be resumed, and if so, do it. - * - * @param prd refund request to be possibly ready - */ -static void -check_resume_prd (struct ProcessRefundData *prd) -{ - if (prd_pending (prd)) - return; - GNUNET_CONTAINER_DLL_remove (prd_head, - prd_tail, - prd); - GNUNET_assert (prd->suspended); - prd->suspended = GNUNET_NO; - MHD_resume_connection (prd->connection); - TMH_trigger_daemon (); -} - - -/** - * Callbacks of this type are used to serve the result of submitting a - * refund request to an exchange. - * - * @param cls a `struct CoinRefund` - * @param hr HTTP response data - * @param exchange_pub exchange key used to sign refund confirmation - * @param exchange_sig exchange's signature over refund - */ -static void -refund_cb (void *cls, - const struct TALER_EXCHANGE_HttpResponse *hr, - const struct TALER_ExchangePublicKeyP *exchange_pub, - const struct TALER_ExchangeSignatureP *exchange_sig) -{ - struct CoinRefund *cr = cls; - - cr->rh = NULL; - cr->exchange_status = hr->http_status; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Exchange refund status for coin %s is %u\n", - TALER_B2S (&cr->coin_pub), - hr->http_status); - if (MHD_HTTP_OK != hr->http_status) - { - cr->exchange_code = hr->ec; - cr->exchange_reply = json_incref ((json_t*) hr->reply); - } - else - { - enum GNUNET_DB_QueryStatus qs; - - cr->exchange_pub = *exchange_pub; - cr->exchange_sig = *exchange_sig; - qs = db->put_refund_proof (db->cls, - &cr->prd->merchant->pubkey, - &cr->prd->h_contract_terms, - &cr->coin_pub, - cr->rtransaction_id, - exchange_pub, - exchange_sig); - if (0 >= qs) - { - /* generally, this is relatively harmless for the merchant, but let's at - least log this. */ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to persist exchange response to /refund in database: %d\n", - qs); - } - } - check_resume_prd (cr->prd); -} - - -/** - * Function called with the result of a #TMH_EXCHANGES_find_exchange() - * operation. - * - * @param cls a `struct CoinRefund *` - * @param hr HTTP response details - * @param eh handle to the exchange context - * @param wire_fee current applicable wire fee for dealing with @a eh, NULL if not available - * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config - */ -static void -exchange_found_cb (void *cls, - const struct TALER_EXCHANGE_HttpResponse *hr, - struct TALER_EXCHANGE_Handle *eh, - const struct TALER_Amount *wire_fee, - int exchange_trusted) -{ - struct CoinRefund *cr = cls; - - cr->fo = NULL; - if (TALER_EC_NONE == hr->ec) - { - cr->rh = TALER_EXCHANGE_refund (eh, - &cr->refund_amount, - &cr->refund_fee, - &cr->prd->h_contract_terms, - &cr->coin_pub, - cr->rtransaction_id, - &cr->prd->merchant->privkey, - &refund_cb, - cr); - return; - } - cr->exchange_status = hr->http_status; - cr->exchange_code = hr->ec; - cr->exchange_reply = json_incref ((json_t*) hr->reply); - check_resume_prd (cr->prd); -} - - -/** - * Function called with information about a refund. - * It is responsible for packing up the data to return. - * - * @param cls closure - * @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 -process_refunds_cb (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 ProcessRefundData *prd = cls; - struct CoinRefund *cr; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Found refund of %s for coin %s with reason `%s' in database\n", - TALER_B2S (coin_pub), - TALER_amount2s (refund_amount), - reason); - cr = GNUNET_new (struct CoinRefund); - cr->exchange_url = GNUNET_strdup (exchange_url); - cr->prd = prd; - cr->coin_pub = *coin_pub; - cr->rtransaction_id = rtransaction_id; - cr->refund_amount = *refund_amount; - cr->refund_fee = *refund_fee; - GNUNET_CONTAINER_DLL_insert (prd->cr_head, - prd->cr_tail, - cr); -} - - -/** - * Force resuming all suspended refund lookups, needed during shutdown. - */ -void -MH_force_refund_resume (void) -{ - struct ProcessRefundData *prd; - - while (NULL != (prd = prd_head)) - { - GNUNET_CONTAINER_DLL_remove (prd_head, - prd_tail, - prd); - GNUNET_assert (prd->suspended); - prd->suspended = GNUNET_NO; - MHD_resume_connection (prd->connection); - } -} - - -/** - * Return refund situation about a contract. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_refund_lookup (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi) -{ - struct ProcessRefundData *prd; - const char *order_id; - json_t *contract_terms; - enum GNUNET_DB_QueryStatus qs; - - prd = *connection_cls; - if (NULL == prd) - { - order_id = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "order_id"); - if (NULL == order_id) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MISSING, - "order_id"); - } - - /* Convert order id to h_contract_terms */ - contract_terms = NULL; - db->preflight (db->cls); - qs = db->find_contract_terms (db->cls, - &contract_terms, - order_id, - &mi->pubkey); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_REFUND_LOOKUP_DB_ERROR, - "database error looking up order_id from merchant_contract_terms table"); - } - - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Unknown order id given: `%s'\n", - order_id); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_REFUND_ORDER_ID_UNKNOWN, - "order_id not found in database"); - } - - prd = GNUNET_new (struct ProcessRefundData); - if (GNUNET_OK != - TALER_JSON_hash (contract_terms, - &prd->h_contract_terms)) - { - GNUNET_break (0); - json_decref (contract_terms); - GNUNET_free (prd); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_INTERNAL_LOGIC_ERROR, - "Could not hash contract terms"); - } - json_decref (contract_terms); - prd->hc.cc = &cleanup_prd; - prd->merchant = mi; - prd->ec = TALER_EC_NONE; - prd->connection = connection; - *connection_cls = prd; - - for (unsigned int i = 0; iget_refunds_from_contract_terms_hash (db->cls, - &mi->pubkey, - &prd->h_contract_terms, - &process_refunds_cb, - prd); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - if (0 > qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Database hard error on refunds_from_contract_terms_hash lookup: %s\n", - GNUNET_h2s (&prd->h_contract_terms)); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_REFUND_LOOKUP_DB_ERROR, - "Failed to lookup refunds for contract"); - } - - /* Now launch exchange interactions, unless we already have the - response in the database! */ - for (struct CoinRefund *cr = prd->cr_head; - NULL != cr; - cr = cr->next) - { - enum GNUNET_DB_QueryStatus qs; - - qs = db->get_refund_proof (db->cls, - &cr->prd->merchant->pubkey, - &cr->prd->h_contract_terms, - &cr->coin_pub, - cr->rtransaction_id, - &cr->exchange_pub, - &cr->exchange_sig); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) - { - /* We need to talk to the exchange */ - cr->fo = TMH_EXCHANGES_find_exchange (cr->exchange_url, - NULL, - GNUNET_NO, - &exchange_found_cb, - cr); - } - } - } - - /* Check if there are still exchange operations pending */ - if (GNUNET_YES == prd_pending (prd)) - { - if (! prd->suspended) - { - prd->suspended = GNUNET_YES; - MHD_suspend_connection (connection); - GNUNET_CONTAINER_DLL_insert (prd_head, - prd_tail, - prd); - } - return MHD_YES; /* we're still talking to the exchange */ - } - - /* All operations done, build final response */ - if (NULL == prd->cr_head) - { - /* There ARE no refunds scheduled, bitch */ - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_REFUND_LOOKUP_NO_REFUND, - "This contract is not currently eligible for refunds"); - } - - { - json_t *ra; - - ra = json_array (); - GNUNET_assert (NULL != ra); - for (struct CoinRefund *cr = prd->cr_head; - NULL != cr; - cr = cr->next) - { - GNUNET_assert ( - 0 == - json_array_append_new ( - ra, - (MHD_HTTP_OK != cr->exchange_status) - ? json_pack ((NULL != cr->exchange_reply) - ? "{s:o,s:o,s:o,s:I,s:I,s:I,s:O}" - : "{s:o,s:o,s:o,s:I,s:I:s:I}", - "coin_pub", - GNUNET_JSON_from_data_auto (&cr->coin_pub), - "refund_amount", - TALER_JSON_from_amount (&cr->refund_amount), - "refund_fee", - TALER_JSON_from_amount (&cr->refund_fee), - "exchange_http_status", - (json_int_t) cr->exchange_status, - "rtransaction_id", - (json_int_t) cr->rtransaction_id, - "exchange_code", - (json_int_t) cr->exchange_code, - "exchange_reply", - cr->exchange_reply) - : json_pack ("{s:o,s:o,s:o,s:I,s:I,s:o,s:o}", - "coin_pub", - GNUNET_JSON_from_data_auto (&cr->coin_pub), - "refund_amount", - TALER_JSON_from_amount (&cr->refund_amount), - "refund_fee", - TALER_JSON_from_amount (&cr->refund_fee), - "exchange_http_status", - (json_int_t) cr->exchange_status, - "rtransaction_id", - (json_int_t) cr->rtransaction_id, - "exchange_pub", - GNUNET_JSON_from_data_auto (&cr->exchange_pub), - "exchange_sig", - GNUNET_JSON_from_data_auto (&cr->exchange_sig) - ))); - } - return TALER_MHD_reply_json_pack ( - connection, - MHD_HTTP_OK, - "{s:o, s:o, s:o}", - "refunds", - ra, - "merchant_pub", - GNUNET_JSON_from_data_auto (&mi->pubkey), - "h_contract_terms", - GNUNET_JSON_from_data_auto (&prd->h_contract_terms)); - } -} - - -/* end of taler-merchant-httpd_refund_lookup.c */ diff --git a/src/backend/taler-merchant-httpd_orders_order_get2.h b/src/backend/taler-merchant-httpd_orders_order_get2.h deleted file mode 100644 index 24495daf..00000000 --- a/src/backend/taler-merchant-httpd_orders_order_get2.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - This file is part of TALER - (C) 2014, 2015, 2016, 2017 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ - -/** - * @file backend/taler-merchant-httpd_refund_lookup.h - * @brief - * @author Marcello Stanisci - */ - -#ifndef TALER_MERCHANT_HTTPD_REFUND_LOOKUP_H -#define TALER_MERCHANT_HTTPD_REFUND_LOOKUP_H -#include -#include "taler-merchant-httpd.h" - - -/** - * Return refund situation about a contract. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_refund_lookup (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi); - -#endif diff --git a/src/backend/taler-merchant-httpd_orders_order_get3.c b/src/backend/taler-merchant-httpd_orders_order_get3.c deleted file mode 100644 index 39f8ce9e..00000000 --- a/src/backend/taler-merchant-httpd_orders_order_get3.c +++ /dev/null @@ -1,1250 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2020 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_track-transaction.c - * @brief implement API for tracking deposits and wire transfers - * @author Marcello Stanisci - * @author Christian Grothoff - */ -#include "platform.h" -#include -#include -#include -#include "taler-merchant-httpd.h" -#include "taler_merchant_service.h" -#include "taler-merchant-httpd_mhd.h" -#include "taler-merchant-httpd_auditors.h" -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_track-transaction.h" - - -/** - * Information about a wire transfer for a /track/transaction response. - */ -struct TransactionWireTransfer -{ - - /** - * Wire transfer identifier this struct is about. - */ - struct TALER_WireTransferIdentifierRawP wtid; - - /** - * When was this wire transfer executed? - */ - struct GNUNET_TIME_Absolute execution_time; - - /** - * Number of coins of the selected transaction that - * is covered by this wire transfer. - */ - unsigned int num_coins; - - /** - * Information about the coins of the selected transaction - * that are part of the wire transfer. - */ - struct TALER_MERCHANT_CoinWireTransfer *coins; - - /** - * URL of the exchange that executed the wire transfer. - */ - char *exchange_url; -}; - - -/** - * Map containing all the known merchant instances - */ -extern struct GNUNET_CONTAINER_MultiHashMap *by_id_map; - -/** - * How long to wait before giving up processing with the exchange? - */ -#define TRACK_TIMEOUT (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, \ - 30)) - -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - - -/** - * Generate /track/transaction response. - * - * @param num_transfers how many wire transfers make up the transaction - * @param transfers data on each wire transfer - * @return MHD response object - */ -static struct MHD_Response * -make_track_transaction_ok (unsigned int num_transfers, - const struct TransactionWireTransfer *transfers) -{ - json_t *j_transfers; - - j_transfers = json_array (); - for (unsigned int i = 0; icoins[0].amount_with_fee; - for (unsigned int j = 1; jnum_coins; j++) - { - const struct TALER_MERCHANT_CoinWireTransfer *coin = &transfer->coins[j]; - - GNUNET_assert (0 <= - TALER_amount_add (&sum, - &sum, - &coin->amount_with_fee)); - } - - GNUNET_assert (0 == - json_array_append_new ( - j_transfers, - json_pack ( - "{s:s, s:o, s:o, s:o}", - "exchange", - transfer->exchange_url, - "wtid", - GNUNET_JSON_from_data_auto (&transfer->wtid), - "execution_time", - GNUNET_JSON_from_time_abs (transfer->execution_time), - "amount", - TALER_JSON_from_amount (&sum)))); - } - { - struct MHD_Response *ret; - - ret = TALER_MHD_make_json (j_transfers); - json_decref (j_transfers); - return ret; - } -} - - -/** - * Context for a /track/transaction operation. - */ -struct TrackTransactionContext; - -/** - * Merchant instance being tracked - */ -struct MerchantInstance; - -/** - * Information we keep for each coin in a /track/transaction operation. - */ -struct TrackCoinContext -{ - /** - * Kept in a DLL. - */ - struct TrackCoinContext *next; - - /** - * Kept in a DLL. - */ - struct TrackCoinContext *prev; - - /** - * Our context for a /track/transaction operation. - */ - struct TrackTransactionContext *tctx; - - /** - * Public key of the coin. - */ - struct TALER_CoinSpendPublicKeyP coin_pub; - - /** - * Exchange that was used for the transaction. - */ - char *exchange_url; - - /** - * Handle for the request to resolve the WTID for this coin. - */ - struct TALER_EXCHANGE_DepositGetHandle *dwh; - - /** - * Wire transfer identifier for this coin. - */ - struct TALER_WireTransferIdentifierRawP wtid; - - /** - * Execution time of the wire transfer @e wtid. - */ - struct GNUNET_TIME_Absolute execution_time; - - /** - * Value of the coin including deposit fee. - */ - struct TALER_Amount amount_with_fee; - - /** - * Deposit fee for the coin. - */ - struct TALER_Amount deposit_fee; - - /** - * Have we obtained the WTID for this coin yet? - */ - int have_wtid; - -}; - - -/** - * Context for a /track/transaction operation. - */ -struct TrackTransactionContext -{ - - /** - * This field MUST be first. - */ - struct TM_HandlerContext hc; - - /** - * HTTP request we are handling. - */ - struct MHD_Connection *connection; - - /** - * Kept in a DLL. - */ - struct TrackCoinContext *tcc_head; - - /** - * Kept in a DLL. - */ - struct TrackCoinContext *tcc_tail; - - /** - * Task run on timeout. - */ - struct GNUNET_SCHEDULER_Task *timeout_task; - - /** - * Handle for operation to lookup /keys (and auditors) from - * the exchange used for this transaction; NULL if no operation is - * pending. - */ - struct TMH_EXCHANGES_FindOperation *fo; - - /** - * Handle to our exchange, once we found it. - */ - struct TALER_EXCHANGE_Handle *eh; - - /** - * URL of the exchange we currently have in @e eh. - */ - const char *current_exchange; - - /** - * Handle we use to resolve transactions for a given WTID. - */ - struct TALER_EXCHANGE_TransfersGetHandle *wdh; - - /** - * Response to return upon resume. - */ - struct MHD_Response *response; - - /** - * Wire transfer identifier we are currently looking up in @e wdh. - */ - struct TALER_WireTransferIdentifierRawP current_wtid; - - /** - * Execution time of the wire transfer we are currently looking up in @e wdh. - */ - struct GNUNET_TIME_Absolute current_execution_time; - - /** - * Hash of wire details for the transaction. - */ - struct GNUNET_HashCode h_wire; - - /** - * Timestamp of the transaction. - */ - struct GNUNET_TIME_Absolute timestamp; - - /** - * Refund deadline for the transaction. - */ - struct GNUNET_TIME_Absolute refund_deadline; - - /** - * Total value of the transaction. - */ - struct TALER_Amount total_amount; - - /** - * Transaction this request is about. - */ - const char *transaction_id; - - /** - * Proposal's hashcode. - */ - struct GNUNET_HashCode h_contract_terms; - - /** - * Response code to return upon resume. - */ - unsigned int response_code; - - /** - * Which merchant instance is being tracked - */ - struct MerchantInstance *mi; - - /** - * Set to negative values in #coin_cb() if we encounter - * a database problem. - */ - enum GNUNET_DB_QueryStatus qs; - -}; - - -/** - * Free the @a tctx. - * - * @param tctx data to free - */ -static void -free_tctx (struct TrackTransactionContext *tctx) -{ - struct TrackCoinContext *tcc; - - while (NULL != (tcc = tctx->tcc_head)) - { - GNUNET_CONTAINER_DLL_remove (tctx->tcc_head, - tctx->tcc_tail, - tcc); - if (NULL != tcc->dwh) - { - TALER_EXCHANGE_deposits_get_cancel (tcc->dwh); - tcc->dwh = NULL; - } - if (NULL != tcc->exchange_url) - { - GNUNET_free (tcc->exchange_url); - tcc->exchange_url = NULL; - } - GNUNET_free (tcc); - } - if (NULL != tctx->wdh) - { - TALER_EXCHANGE_transfers_get_cancel (tctx->wdh); - tctx->wdh = NULL; - } - if (NULL != tctx->fo) - { - TMH_EXCHANGES_find_exchange_cancel (tctx->fo); - tctx->fo = NULL; - } - if (NULL != tctx->timeout_task) - { - GNUNET_SCHEDULER_cancel (tctx->timeout_task); - tctx->timeout_task = NULL; - } - GNUNET_free (tctx); -} - - -/** - * Custom cleanup routine for a `struct TrackTransactionContext`. - * - * @param hc the `struct PayContext` to clean up. - */ -static void -track_transaction_cleanup (struct TM_HandlerContext *hc) -{ - struct TrackTransactionContext *tctx = (struct TrackTransactionContext *) hc; - - free_tctx (tctx); -} - - -/** - * Resume the given /track/transaction operation and send the given - * response. Stores the response in the @a tctx and signals MHD to - * resume the connection. Also ensures MHD runs immediately. - * - * @param tctx transaction tracking context - * @param response_code response code to use - * @param response response data to send back - */ -static void -resume_track_transaction_with_response (struct TrackTransactionContext *tctx, - unsigned int response_code, - struct MHD_Response *response) -{ - tctx->response_code = response_code; - tctx->response = response; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Resuming /track/transaction handling as exchange interaction is done (%u)\n", - response_code); - if (NULL != tctx->timeout_task) - { - GNUNET_SCHEDULER_cancel (tctx->timeout_task); - tctx->timeout_task = NULL; - } - MHD_resume_connection (tctx->connection); - TMH_trigger_daemon (); /* we resumed, kick MHD */ -} - - -/** - * This function is called to trace the wire transfers for - * all of the coins of the transaction of the @a tctx. Once - * we have traced all coins, we build the response. - * - * @param tctx track context with established connection to exchange - */ -static void -trace_coins (struct TrackTransactionContext *tctx); - - -/** - * Function called with detailed wire transfer data, including all - * of the coin transactions that were combined into the wire transfer. - * - * We now store this information. Then we check if we still have - * any coins of the original wire transfer not taken care of. - * - * @param cls closure - * @param hr HTTP response details - * @param exchange_pub public key of the exchange used for signing - * @param execution_time time when the exchange claims to have performed the wire transfer - * @param wtid extracted wire transfer identifier, or NULL if the exchange could - * not provide any (set only if @a http_status is #MHD_HTTP_OK) - * @param total_amount total amount of the wire transfer, or NULL if the exchange could - * not provide any @a wtid (set only if @a http_status is #MHD_HTTP_OK) - * @param wire_fee wire fee that was charged by the exchange - * @param details_length length of the @a details array - * @param details array with details about the combined transactions - */ -static void -wire_deposits_cb (void *cls, - const struct TALER_EXCHANGE_HttpResponse *hr, - const struct TALER_ExchangePublicKeyP *exchange_pub, - const struct GNUNET_HashCode *h_wire, - struct GNUNET_TIME_Absolute execution_time, - const struct TALER_Amount *total_amount, - const struct TALER_Amount *wire_fee, - unsigned int details_length, - const struct TALER_TrackTransferDetails *details) -{ - struct TrackTransactionContext *tctx = cls; - enum GNUNET_DB_QueryStatus qs; - - tctx->wdh = NULL; - if (MHD_HTTP_OK != hr->http_status) - { - GNUNET_break_op (0); - resume_track_transaction_with_response ( - tctx, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ( - "{s:I, s:I, s:I, s:O}", - "code", - (json_int_t) TALER_EC_TRACK_TRANSACTION_WIRE_TRANSFER_TRACE_ERROR, - "exchange_http_status", - (json_int_t) hr->http_status, - "exchange_code", - (json_int_t) hr->ec, - "exchange_reply", - hr->reply)); - return; - } - for (unsigned int i = 0; ipreflight (db->cls); - qs = db->store_transfer_to_proof (db->cls, - tctx->current_exchange, - &tctx->current_wtid, - tctx->current_execution_time, - exchange_pub, - hr->reply); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - if (0 > qs) - { - /* Not good, but not fatal either, log error and continue */ - /* Special report if retries insufficient */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to store transfer-to-proof mapping in DB\n"); - } - for (struct TrackCoinContext *tcc = tctx->tcc_head; - NULL != tcc; - tcc = tcc->next) - { - if (GNUNET_YES == tcc->have_wtid) - continue; - for (unsigned int d = 0; dcoin_pub)) - { - tcc->wtid = tctx->current_wtid; - tcc->execution_time = tctx->current_execution_time; - tcc->have_wtid = GNUNET_YES; - } - - for (unsigned int i = 0; ipreflight (db->cls); - qs = db->store_coin_to_transfer (db->cls, - &details[d].h_contract_terms, - &details[d].coin_pub, - &tctx->current_wtid); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - if (0 > qs) - { - /* Not good, but not fatal either, log error and continue */ - /* Special report if retries insufficient */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to store coin-to-transfer mapping in DB\n"); - } - } - } - /* Continue tracing (will also handle case that we are done) */ - trace_coins (tctx); -} - - -/** - * Closure for #proof_cb(). - */ -struct ProofCheckContext -{ - /** - * Proof returned from #proof_cb. The reference counter was - * increased for this reference and it must thus be freed. - * NULL if we did not find any proof. The JSON should - * match the `TrackTransferResponse` of the exchange API - * (https://api.taler.net/api-exchange.html#tracktransferresponse) - */ - json_t *p_ret; - -}; - - -/** - * Function called with information about a wire transfer identifier. - * We actually never expect this to be called. - * - * @param cls closure with a `struct ProofCheckContext` - * @param proof proof from exchange about what the wire transfer was for - */ -static void -proof_cb (void *cls, - const json_t *proof) -{ - struct ProofCheckContext *pcc = cls; - - GNUNET_break (NULL == pcc->p_ret); - pcc->p_ret = json_incref ((json_t *) proof); -} - - -/** - * This function takes the wtid from the coin being tracked - * and _track_ it against the exchange. This way, we know - * all the other coins which were aggregated together with - * this one. This way we save further HTTP requests to track - * the other coins. - * - * @param cls closure with a `struct TrackCoinContext` - * @param hr HTTP response details - * @param exchange_pub public key of the exchange used for signing @a json - * @param wtid wire transfer identifier used by the exchange, NULL if exchange did not - * yet execute the transaction - * @param execution_time actual or planned execution time for the wire transfer - * @param coin_contribution contribution to the @a total_amount of the deposited coin (may be NULL) - */ -static void -wtid_cb (void *cls, - const struct TALER_EXCHANGE_HttpResponse *hr, - const struct TALER_ExchangePublicKeyP *exchange_pub, - const struct TALER_WireTransferIdentifierRawP *wtid, - struct GNUNET_TIME_Absolute execution_time, - const struct TALER_Amount *coin_contribution) -{ - struct TrackCoinContext *tcc = cls; - struct TrackTransactionContext *tctx = tcc->tctx; - struct ProofCheckContext pcc; - enum GNUNET_DB_QueryStatus qs; - - tcc->dwh = NULL; - if (MHD_HTTP_OK != hr->http_status) - { - if (MHD_HTTP_ACCEPTED == hr->http_status) - { - resume_track_transaction_with_response ( - tcc->tctx, - MHD_HTTP_ACCEPTED, - /* Return verbatim what the exchange said. */ - TALER_MHD_make_json (hr->reply)); - return; - } - - /* Transaction not resolved for one of the - coins, report error! */ - resume_track_transaction_with_response ( - tcc->tctx, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ( - "{s:I, s:I, s:I, s:O}", - "code", - (json_int_t) TALER_EC_TRACK_TRANSACTION_COIN_TRACE_ERROR, - "exchange_http_status", - (json_int_t) hr->http_status, - "exchange_code", - (json_int_t) hr->ec, - "exchange_reply", - hr->reply)); - return; - } - tctx->current_wtid = *wtid; - tctx->current_execution_time = execution_time; - - pcc.p_ret = NULL; - /* attempt to find this wtid's track from our database, - Will make pcc.p_ret point to a "proof", if one exists. */ - db->preflight (db->cls); - qs = db->find_proof_by_wtid (db->cls, - tctx->current_exchange, - wtid, - &proof_cb, - &pcc); - if (0 > qs) - { - /* Simple select queries should not - cause serialization issues */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - resume_track_transaction_with_response ( - tcc->tctx, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_MHD_make_error ( - TALER_EC_TRACK_TRANSACTION_DB_FETCH_TRANSACTION_ERROR, - "Fail to query database about proofs")); - return; - } - - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - { - /* How come this wtid was already stored into the - database and _not all_ of its coins were already - tracked? Inconsistent state (! At least regarding - what the exchange tells us) */ - GNUNET_break_op (0); - resume_track_transaction_with_response ( - tcc->tctx, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ( - "{s:I, s:s, s:O, s:o, s:o}", - "code", (json_int_t) TALER_EC_TRACK_TRANSACTION_CONFLICTING_REPORTS, - "hint", "conflicting transfer data from exchange", - "transaction_tracking_claim", hr->reply, - "wtid_tracking_claim", pcc.p_ret, - "coin_pub", GNUNET_JSON_from_data_auto (&tcc->coin_pub))); - return; - } - - tctx->wdh = TALER_EXCHANGE_transfers_get (tctx->eh, - wtid, - &wire_deposits_cb, - tctx); -} - - -/** - * We have obtained all WTIDs, now prepare the response - * - * @param tctx handle for the operation - */ -static void -generate_response (struct TrackTransactionContext *tctx) -{ - unsigned int num_wtid = 0; - - /* count how many disjoint wire transfer identifiers there are; - note that there should only usually be one, so while this - is worst-case O(n^2), in pracitce this is O(n) */ - for (struct TrackCoinContext *tcc = tctx->tcc_head; - NULL != tcc; - tcc = tcc->next) - { - int found = GNUNET_NO; - - for (struct TrackCoinContext *tcc2 = tctx->tcc_head; - tcc2 != tcc; - tcc2 = tcc2->next) - { - if (0 == GNUNET_memcmp (&tcc->wtid, - &tcc2->wtid)) - { - found = GNUNET_YES; - break; - } - } - if (GNUNET_NO == found) - num_wtid++; - } - - { - /* on-stack allocation is fine, as the number of coins and the - number of wire-transfers per-transaction is expected to be tiny. */ - struct TransactionWireTransfer wts[num_wtid]; - unsigned int wtid_off; - - wtid_off = 0; - for (struct TrackCoinContext *tcc = tctx->tcc_head; - NULL != tcc; - tcc = tcc->next) - { - int found = GNUNET_NO; - - for (struct TrackCoinContext *tcc2 = tctx->tcc_head; - tcc2 != tcc; - tcc2 = tcc2->next) - { - if (0 == GNUNET_memcmp (&tcc->wtid, - &tcc2->wtid)) - { - found = GNUNET_YES; - break; - } - } - if (GNUNET_NO == found) - { - unsigned int num_coins; - struct TransactionWireTransfer *wt; - - wt = &wts[wtid_off++]; - wt->wtid = tcc->wtid; - wt->exchange_url = tcc->exchange_url; - wt->execution_time = tcc->execution_time; - /* count number of coins with this wtid */ - num_coins = 0; - for (struct TrackCoinContext *tcc2 = tctx->tcc_head; - NULL != tcc2; - tcc2 = tcc2->next) - { - if (0 == GNUNET_memcmp (&wt->wtid, - &tcc2->wtid)) - num_coins++; - } - /* initialize coins array */ - wt->num_coins = num_coins; - wt->coins = GNUNET_new_array (num_coins, - struct TALER_MERCHANT_CoinWireTransfer); - num_coins = 0; - for (struct TrackCoinContext *tcc2 = tctx->tcc_head; - NULL != tcc2; - tcc2 = tcc2->next) - { - if (0 == GNUNET_memcmp (&wt->wtid, - &tcc2->wtid)) - { - struct TALER_MERCHANT_CoinWireTransfer *coin = - &wt->coins[num_coins++]; - - coin->coin_pub = tcc2->coin_pub; - coin->amount_with_fee = tcc2->amount_with_fee; - coin->deposit_fee = tcc2->deposit_fee; - } - } - } /* GNUNET_NO == found */ - } /* for all tcc */ - GNUNET_assert (wtid_off == num_wtid); - - { - struct MHD_Response *resp; - - resp = make_track_transaction_ok (num_wtid, - wts); - for (wtid_off = 0; wtid_off < num_wtid; wtid_off++) - GNUNET_free (wts[wtid_off].coins); - resume_track_transaction_with_response (tctx, - MHD_HTTP_OK, - resp); - } - } /* end of scope for 'wts' and 'resp' */ -} - - -/** - * Find the exchange to trace the next coin(s). - * - * @param tctx operation context - */ -static void -find_exchange (struct TrackTransactionContext *tctx); - - -/** - * This function is called to 'trace the wire transfers' - * (true?) for all of the coins of the transaction of the @a tctx. - * Once we have traced all coins, we build the response. - * - * @param tctx track context with established connection to exchange - */ -static void -trace_coins (struct TrackTransactionContext *tctx) -{ - struct TrackCoinContext *tcc; - - /* Make sure we are connected to the exchange. */ - GNUNET_assert (NULL != tctx->eh); - - for (tcc = tctx->tcc_head; NULL != tcc; tcc = tcc->next) - - /* How come one doesn't have wtid? */ - if (GNUNET_YES != tcc->have_wtid) - break; - - if (NULL != tcc) - { - if (0 != strcmp (tcc->exchange_url, - tctx->current_exchange)) - { - /* exchange changed, find matching one first! */ - tctx->eh = NULL; - tctx->current_exchange = NULL; - find_exchange (tctx); - return; - } - /* we are not done requesting WTIDs from the current - exchange; do the next one */ - tcc->dwh = TALER_EXCHANGE_deposits_get (tctx->eh, - &tctx->mi->privkey, - &tctx->h_wire, - &tctx->h_contract_terms, - &tcc->coin_pub, - &wtid_cb, - tcc); - return; - } - tctx->current_exchange = NULL; - tctx->eh = NULL; - generate_response (tctx); -} - - -/** - * Function called with the result of our exchange lookup. - * Merely provide the execution context to the routine actually - * tracking the coin. - * - * @param cls the `struct TrackTransactionContext` - * @param hr HTTP response details - * @param eh NULL if exchange was not found to be acceptable - * @param wire_fee NULL (we did not specify a wire method) - * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config - */ -static void -process_track_transaction_with_exchange (void *cls, - const struct - TALER_EXCHANGE_HttpResponse *hr, - struct TALER_EXCHANGE_Handle *eh, - const struct TALER_Amount *wire_fee, - int exchange_trusted) -{ - struct TrackTransactionContext *tctx = cls; - - tctx->fo = NULL; - if (MHD_HTTP_OK != hr->http_status) - { - /* The request failed somehow */ - GNUNET_break_op (0); - resume_track_transaction_with_response ( - tctx, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ( - (NULL != hr->reply) - ? "{s:s, s:I, s:I, s:I, s:O}" - : "{s:s, s:I, s:I, s:I}", - "hint", "failed to obtain meta-data from exchange", - "code", (json_int_t) TALER_EC_TRACK_TRANSACTION_EXCHANGE_KEYS_FAILURE, - "exchange_http_status", (json_int_t) hr->http_status, - "exchange_code", (json_int_t) hr->ec, - "exchange_reply", hr->reply)); - return; - } - tctx->eh = eh; - trace_coins (tctx); -} - - -/** - * Handle a timeout for the processing of the track transaction request. - * - * @param cls closure - */ -static void -handle_track_transaction_timeout (void *cls) -{ - struct TrackTransactionContext *tctx = cls; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Resuming /track/transaction with error after timeout\n"); - tctx->timeout_task = NULL; - if (NULL != tctx->fo) - { - TMH_EXCHANGES_find_exchange_cancel (tctx->fo); - tctx->fo = NULL; - } - resume_track_transaction_with_response (tctx, - MHD_HTTP_SERVICE_UNAVAILABLE, - TALER_MHD_make_error ( - TALER_EC_PAY_EXCHANGE_TIMEOUT, - "exchange not reachable")); -} - - -/** - * Information about the wire transfer corresponding to - * a deposit operation. Note that it is in theory possible - * that we have a @a transaction_id and @a coin_pub in the - * result that do not match a deposit that we know about, - * for example because someone else deposited funds into - * our account. - * - * @param cls closure - * @param transaction_id ID of the contract - * @param coin_pub public key of the coin - * @param wtid identifier of the wire transfer in which the exchange - * send us the money for the coin deposit - * @param execution_time when was the wire transfer executed? - * @param exchange_proof proof from exchange about what the deposit was for - * NULL if we have not asked for this signature - */ -static void -transfer_cb (void *cls, - const struct GNUNET_HashCode *h_contract_terms, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_WireTransferIdentifierRawP *wtid, - struct GNUNET_TIME_Absolute execution_time, - const json_t *exchange_proof) -{ - struct TrackCoinContext *tcc = cls; - - if (0 != GNUNET_memcmp (coin_pub, - &tcc->coin_pub)) - return; - tcc->wtid = *wtid; - tcc->execution_time = execution_time; - tcc->have_wtid = GNUNET_YES; -} - - -/** - * Responsible to get the current coin wtid and store it into its state. - * - * @param cls closure - * @param transaction_id of the contract - * @param coin_pub public key of the coin - * @param exchange_url URL of exchange that issued @a coin_pub - * @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 exchange_proof proof from exchange that coin was accepted - */ -static void -coin_cb (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 TrackTransactionContext *tctx = cls; - struct TrackCoinContext *tcc; - - tcc = GNUNET_new (struct TrackCoinContext); - tcc->tctx = tctx; - tcc->coin_pub = *coin_pub; - tcc->exchange_url = GNUNET_strdup (exchange_url); - tcc->amount_with_fee = *amount_with_fee; - tcc->deposit_fee = *deposit_fee; - GNUNET_CONTAINER_DLL_insert (tctx->tcc_head, - tctx->tcc_tail, - tcc); - - /* find all those pairs associated to - this contract term's hash code. The callback - will then set the wtid for the "current coin" - context. */ - { - enum GNUNET_DB_QueryStatus qs; - - qs = db->find_transfers_by_hash (db->cls, - h_contract_terms, - &transfer_cb, - tcc); - if (0 > qs) - { - GNUNET_break (0); - tctx->qs = qs; - } - } -} - - -/** - * Find the exchange to trace the next coin(s). - * - * @param tctx operation context - */ -static void -find_exchange (struct TrackTransactionContext *tctx) -{ - struct TrackCoinContext *tcc = tctx->tcc_head; - - while ( (NULL != tcc) && - (GNUNET_YES == tcc->have_wtid) ) - tcc = tcc->next; - if (NULL != tcc) - { - tctx->current_exchange = tcc->exchange_url; - tctx->fo = TMH_EXCHANGES_find_exchange ( - tctx->current_exchange, - NULL, - GNUNET_NO, - &process_track_transaction_with_exchange, - tctx); - - } - else - { - generate_response (tctx); - } -} - - -/** - * Handle a "/track/transaction" request. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_track_transaction (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi) -{ - struct TrackTransactionContext *tctx; - const char *order_id; - enum GNUNET_DB_QueryStatus qs; - struct json_t *contract_terms; - - if (NULL == *connection_cls) - { - tctx = GNUNET_new (struct TrackTransactionContext); - tctx->hc.cc = &track_transaction_cleanup; - tctx->connection = connection; - *connection_cls = tctx; - } - else - { - /* not first call, recover state */ - tctx = *connection_cls; - } - - if (0 != tctx->response_code) - { - MHD_RESULT ret; - - /* We are *done* processing the request, just queue the response (!) */ - if (UINT_MAX == tctx->response_code) - { - GNUNET_break (0); - return MHD_NO; /* hard error */ - } - ret = MHD_queue_response (connection, - tctx->response_code, - tctx->response); - if (NULL != tctx->response) - { - MHD_destroy_response (tctx->response); - tctx->response = NULL; - } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Queueing response (%u) for /track/transaction (%s).\n", - (unsigned int) tctx->response_code, - ret ? "OK" : "FAILED"); - return ret; - } - if ( (NULL != tctx->fo) || - (NULL != tctx->eh) ) - { - /* likely old MHD version */ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Not sure why we are here, should be suspended\n"); - return MHD_YES; /* still work in progress */ - } - order_id = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "order_id"); - if (NULL == order_id) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MISSING, - "order_id"); - - tctx->mi = mi; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Tracking on behalf of instance '%s'\n", - mi->id); - - - /* Map order id to contract terms; the objective is to get - the contract term's hashcode so as to retrieve all the - coins which have been deposited for it. */ - db->preflight (db->cls); - qs = db->find_contract_terms (db->cls, - &contract_terms, - order_id, - &tctx->mi->pubkey); - if (0 > qs) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_TRACK_TRANSACTION_DB_FETCH_TRANSACTION_ERROR, - "Database error finding contract terms"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_PROPOSAL_LOOKUP_NOT_FOUND, - "Given order_id doesn't map to any proposal"); - - if (GNUNET_OK != - TALER_JSON_hash (contract_terms, - &tctx->h_contract_terms)) - { - json_decref (contract_terms); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_INTERNAL_LOGIC_ERROR, - "Failed to hash contract terms"); - } - - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_absolute_time ("refund_deadline", - &tctx->refund_deadline), - GNUNET_JSON_spec_absolute_time ("timestamp", - &tctx->timestamp), - TALER_JSON_spec_amount ("amount", - &tctx->total_amount), - GNUNET_JSON_spec_fixed_auto ("h_wire", - &tctx->h_wire), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_YES != - GNUNET_JSON_parse (contract_terms, - spec, - NULL, NULL)) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - json_decref (contract_terms); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_INTERNAL_LOGIC_ERROR, - "Failed to parse contract terms from DB"); - } - json_decref (contract_terms); - } - - tctx->qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - db->preflight (db->cls); - - /* Find coins which have been deposited for this contract, - and retrieve the wtid for each one. */ - qs = db->find_payments (db->cls, - &tctx->h_contract_terms, - &tctx->mi->pubkey, - &coin_cb, - tctx); - if ( (0 > qs) || - (0 > tctx->qs) ) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != tctx->qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_TRACK_TRANSACTION_DB_FETCH_PAYMENT_ERROR, - "Database error: failed to find payment data"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_TRACK_TRANSACTION_DB_NO_DEPOSITS_ERROR, - "deposit data not found"); - } - *connection_cls = tctx; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Suspending /track/transaction handling while working with the exchange\n"); - MHD_suspend_connection (connection); - tctx->timeout_task - = GNUNET_SCHEDULER_add_delayed (TRACK_TIMEOUT, - &handle_track_transaction_timeout, - tctx); - find_exchange (tctx); - return MHD_YES; -} - - -/* end of taler-merchant-httpd_track-transaction.c */ diff --git a/src/backend/taler-merchant-httpd_orders_order_get4.c b/src/backend/taler-merchant-httpd_orders_order_get4.c deleted file mode 100644 index bb5384d1..00000000 --- a/src/backend/taler-merchant-httpd_orders_order_get4.c +++ /dev/null @@ -1,591 +0,0 @@ -/* - This file is part of TALER - (C) 2017, 2019 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_check-payment.c - * @brief implementation of /check-payment handler - * @author Florian Dold - * @author Christian Grothoff - */ -#include "platform.h" -#include -#include -#include -#include -#include -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_mhd.h" -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_check-payment.h" - -/** - * Maximum number of retries for database operations. - */ -#define MAX_RETRIES 5 - - -/** - * Data structure we keep for a check payment request. - */ -struct CheckPaymentRequestContext -{ - /** - * Must be first for #handle_mhd_completion_callback. - */ - struct TM_HandlerContext hc; - - /** - * Entry in the #resume_timeout_heap for this check payment, if we are - * suspended. - */ - struct TMH_SuspendedConnection sc; - - /** - * Which merchant instance is this for? - */ - struct MerchantInstance *mi; - - /** - * URL where the final contract can be found for this payment. - */ - char *final_contract_url; - - /** - * order ID for the payment - */ - const char *order_id; - - /** - * Where to get the contract - */ - const char *contract_url; - - /** - * session of the client - */ - const char *session_id; - - /** - * fulfillment URL of the contract (valid as long as - * @e contract_terms is valid). - */ - const char *fulfillment_url; - - /** - * Contract terms of the payment we are checking. NULL when they - * are not (yet) known. - */ - json_t *contract_terms; - - /** - * Hash of @e contract_terms, set only once @e contract_terms - * is available. - */ - struct GNUNET_HashCode h_contract_terms; - - /** - * Total refunds granted for this payment. Only initialized - * if @e refunded is set to #GNUNET_YES. - */ - struct TALER_Amount refund_amount; - - /** - * Set to #GNUNET_YES if this payment has been refunded and - * @e refund_amount is initialized. - */ - int refunded; - - /** - * Initially #GNUNET_SYSERR. If we queued a response, set to the - * result code (i.e. #MHD_YES or #MHD_NO). - */ - int ret; - -}; - - -/** - * Clean up the session state for a check payment request. - * - * @param hc must be a `struct CheckPaymentRequestContext *` - */ -static void -cprc_cleanup (struct TM_HandlerContext *hc) -{ - struct CheckPaymentRequestContext *cprc = (struct - CheckPaymentRequestContext *) hc; - - if (NULL != cprc->contract_terms) - json_decref (cprc->contract_terms); - GNUNET_free_non_null (cprc->final_contract_url); - GNUNET_free (cprc); -} - - -/** - * Function called with information about a refund. - * It is responsible for summing up the refund amount. - * - * @param cls closure - * @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 -process_refunds_cb (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 CheckPaymentRequestContext *cprc = cls; - - if (cprc->refunded) - { - GNUNET_assert (0 <= - TALER_amount_add (&cprc->refund_amount, - &cprc->refund_amount, - refund_amount)); - return; - } - cprc->refund_amount = *refund_amount; - cprc->refunded = GNUNET_YES; -} - - -/** - * The client did not yet pay, send it the payment request. - * - * @param cprc check pay request context - * @return #MHD_YES on success - */ -static int -send_pay_request (struct CheckPaymentRequestContext *cprc) -{ - int ret; - char *already_paid_order_id = NULL; - char *taler_pay_uri; - struct GNUNET_TIME_Relative remaining; - - remaining = GNUNET_TIME_absolute_get_remaining (cprc->sc.long_poll_timeout); - if (0 != remaining.rel_value_us) - { - /* long polling: do not queue a response, suspend connection instead */ - TMH_compute_pay_key (cprc->order_id, - &cprc->mi->pubkey, - &cprc->sc.key); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Suspending /check-payment on key %s\n", - GNUNET_h2s (&cprc->sc.key)); - TMH_long_poll_suspend (&cprc->sc, - NULL); - return MHD_YES; - } - - /* Check if resource_id has been paid for in the same session - * with another order_id. - */ - if ( (NULL != cprc->session_id) && - (NULL != cprc->fulfillment_url) ) - { - enum GNUNET_DB_QueryStatus qs; - - qs = db->find_session_info (db->cls, - &already_paid_order_id, - cprc->session_id, - cprc->fulfillment_url, - &cprc->mi->pubkey); - if (qs < 0) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (cprc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_CHECK_PAYMENT_DB_FETCH_ORDER_ERROR, - "db error fetching pay session info"); - } - } - taler_pay_uri = TMH_make_taler_pay_uri (cprc->sc.con, - cprc->order_id, - cprc->session_id, - cprc->mi->id); - ret = TALER_MHD_reply_json_pack (cprc->sc.con, - MHD_HTTP_OK, - "{s:s, s:s, s:b, s:s?}", - "taler_pay_uri", taler_pay_uri, - "contract_url", cprc->final_contract_url, - "paid", 0, - "already_paid_order_id", - already_paid_order_id); - GNUNET_free (taler_pay_uri); - GNUNET_free_non_null (already_paid_order_id); - return ret; -} - - -/** - * Parse the "contract_terms" in @a cprc and set the - * "fulfillment_url" and the "h_contract_terms" in @a cprc - * accordingly. - * - * On errors, the response is being queued and the status - * code set in @cprc "ret". - * - * @param cprc[in,out] context to process - * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure - */ -static int -parse_contract_terms (struct CheckPaymentRequestContext *cprc) -{ - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("fulfillment_url", - &cprc->fulfillment_url), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (cprc->contract_terms, - spec, - NULL, NULL)) - { - GNUNET_break (0); - cprc->ret - = TALER_MHD_reply_with_error (cprc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_CHECK_PAYMENT_DB_FETCH_CONTRACT_TERMS_ERROR, - "Merchant database error (contract terms corrupted)"); - return GNUNET_SYSERR; - } - if (GNUNET_OK != - TALER_JSON_hash (cprc->contract_terms, - &cprc->h_contract_terms)) - { - GNUNET_break (0); - cprc->ret - = TALER_MHD_reply_with_error (cprc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_CHECK_PAYMENT_FAILED_COMPUTE_PROPOSAL_HASH, - "Failed to hash proposal"); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Check that we are aware of @a order_id and if so request the payment, - * otherwise generate an error response. - * - * @param cprc session state - * @return status code to return to MHD for @a connection - */ -static int -check_order_and_request_payment (struct CheckPaymentRequestContext *cprc) -{ - enum GNUNET_DB_QueryStatus qs; - - if (NULL != cprc->contract_terms) - { - /* This should never happen. */ - GNUNET_break (0); - json_decref (cprc->contract_terms); - cprc->contract_terms = NULL; - } - qs = db->find_order (db->cls, - &cprc->contract_terms, - cprc->order_id, - &cprc->mi->pubkey); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (cprc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_CHECK_PAYMENT_DB_FETCH_ORDER_ERROR, - "db error fetching order"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - return TALER_MHD_reply_with_error (cprc->sc.con, - MHD_HTTP_NOT_FOUND, - TALER_EC_CHECK_PAYMENT_ORDER_ID_UNKNOWN, - "unknown order_id"); - } - - if (GNUNET_OK != - parse_contract_terms (cprc)) - return cprc->ret; - /* Offer was not picked up yet, but we ensured that it exists */ - return send_pay_request (cprc); -} - - -/** - * Manages a /check-payment call, checking the status - * of a payment and, if necessary, constructing the URL - * for a payment redirect URL. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_check_payment (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi) -{ - struct CheckPaymentRequestContext *cprc = *connection_cls; - enum GNUNET_DB_QueryStatus qs; - MHD_RESULT ret; - - if (NULL == cprc) - { - const char *long_poll_timeout_s; - - /* First time here, parse request and check order is known */ - cprc = GNUNET_new (struct CheckPaymentRequestContext); - cprc->hc.cc = &cprc_cleanup; - cprc->ret = GNUNET_SYSERR; - cprc->sc.con = connection; - cprc->mi = mi; - *connection_cls = cprc; - - cprc->order_id = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "order_id"); - if (NULL == cprc->order_id) - { - /* order_id is required but missing */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MISSING, - "order_id required"); - } - cprc->contract_url = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "contract_url"); - if (NULL == cprc->contract_url) - { - cprc->final_contract_url = TALER_url_absolute_mhd (connection, - "/public/proposal", - "instance", mi->id, - "order_id", - cprc->order_id, - NULL); - GNUNET_assert (NULL != cprc->final_contract_url); - } - else - { - cprc->final_contract_url = GNUNET_strdup (cprc->contract_url); - } - cprc->session_id = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "session_id"); - long_poll_timeout_s = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "timeout"); - if (NULL != long_poll_timeout_s) - { - unsigned int timeout; - - if (1 != sscanf (long_poll_timeout_s, - "%u", - &timeout)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MALFORMED, - "timeout must be non-negative number"); - } - cprc->sc.long_poll_timeout - = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply ( - GNUNET_TIME_UNIT_SECONDS, - timeout)); - } - else - { - cprc->sc.long_poll_timeout = GNUNET_TIME_UNIT_ZERO_ABS; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Starting /check-payment processing with timeout %s\n", - GNUNET_STRINGS_absolute_time_to_string ( - cprc->sc.long_poll_timeout)); - } - if (NULL != cprc->contract_terms) - { - json_decref (cprc->contract_terms); - cprc->contract_terms = NULL; - } - db->preflight (db->cls); - qs = db->find_contract_terms (db->cls, - &cprc->contract_terms, - cprc->order_id, - &mi->pubkey); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_CHECK_PAYMENT_DB_FETCH_CONTRACT_TERMS_ERROR, - "db error fetching contract terms"); - } - - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - /* Check that we're at least aware of the order */ - return check_order_and_request_payment (cprc); - } - GNUNET_assert (NULL != cprc->contract_terms); - - if (GNUNET_OK != - parse_contract_terms (cprc)) - return cprc->ret; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Checkig payment status for order `%s' with contract %s\n", - cprc->order_id, - GNUNET_h2s (&cprc->h_contract_terms)); - GNUNET_assert (NULL != cprc->contract_terms); - - /* Check if the order has been paid for. */ - if (NULL != cprc->session_id) - { - /* Check if paid within a session. */ - char *already_paid_order_id = NULL; - - qs = db->find_session_info (db->cls, - &already_paid_order_id, - cprc->session_id, - cprc->fulfillment_url, - &mi->pubkey); - if (qs < 0) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_CHECK_PAYMENT_DB_FETCH_ORDER_ERROR, - "db error fetching pay session info"); - } - else if (0 == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order `%s' was not yet paid for session `%s'\n", - cprc->order_id, - cprc->session_id); - ret = send_pay_request (cprc); - GNUNET_free_non_null (already_paid_order_id); - return ret; - } - GNUNET_break (1 == qs); - GNUNET_break (0 == strcmp (cprc->order_id, - already_paid_order_id)); - GNUNET_free_non_null (already_paid_order_id); - } - else - { - /* Check if paid regardless of session. */ - json_t *xcontract_terms = NULL; - - qs = db->find_paid_contract_terms_from_hash (db->cls, - &xcontract_terms, - &cprc->h_contract_terms, - &mi->pubkey); - if (0 > qs) - { - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR, - "Merchant database error"); - } - if (0 == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order `%s' (contract `%s') was not yet paid\n", - cprc->order_id, - GNUNET_h2s (&cprc->h_contract_terms)); - return send_pay_request (cprc); - } - GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); - GNUNET_assert (NULL != xcontract_terms); - json_decref (xcontract_terms); - } - - /* Accumulate refunds, if any. */ - for (unsigned int i = 0; iget_refunds_from_contract_terms_hash (db->cls, - &mi->pubkey, - &cprc->h_contract_terms, - &process_refunds_cb, - cprc); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - if (0 > qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Database hard error on refunds_from_contract_terms_hash lookup: %s\n", - GNUNET_h2s (&cprc->h_contract_terms)); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR, - "Merchant database error"); - } - if (cprc->refunded) - return TALER_MHD_reply_json_pack (connection, - MHD_HTTP_OK, - "{s:O, s:b, s:b, s:o}", - "contract_terms", cprc->contract_terms, - "paid", 1, - "refunded", cprc->refunded, - "refund_amount", - TALER_JSON_from_amount ( - &cprc->refund_amount)); - return TALER_MHD_reply_json_pack (connection, - MHD_HTTP_OK, - "{s:O, s:b, s:b }", - "contract_terms", cprc->contract_terms, - "paid", 1, - "refunded", 0); -} diff --git a/src/backend/taler-merchant-httpd_orders_order_get5.c b/src/backend/taler-merchant-httpd_orders_order_get5.c deleted file mode 100644 index 47207131..00000000 --- a/src/backend/taler-merchant-httpd_orders_order_get5.c +++ /dev/null @@ -1,228 +0,0 @@ -/* - This file is part of TALER - (C) 2014, 2015, 2016, 2018 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation; either version 3, - or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public - License along with TALER; see the file COPYING. If not, - see -*/ - -/** - * @file backend/taler-merchant-httpd_proposal.c - * @brief HTTP serving layer mainly intended to communicate - * with the frontend - * @author Marcello Stanisci - */ -#include "platform.h" -#include -#include -#include -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_auditors.h" -#include "taler-merchant-httpd_exchanges.h" - - -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - -/** - * Manage a GET /proposal request. Query the db and returns the - * proposal's data related to the transaction id given as the URL's - * parameter. - * - * Binds the proposal to a nonce. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_proposal_lookup (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi) -{ - const char *order_id; - const char *nonce; - enum GNUNET_DB_QueryStatus qs; - json_t *contract_terms; - struct GNUNET_CRYPTO_EddsaSignature merchant_sig; - const char *stored_nonce; - - order_id = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "order_id"); - if (NULL == order_id) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MISSING, - "order_id"); - nonce = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "nonce"); - if (NULL == nonce) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MISSING, - "nonce"); - db->preflight (db->cls); - qs = db->find_contract_terms (db->cls, - &contract_terms, - order_id, - &mi->pubkey); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PROPOSAL_LOOKUP_DB_ERROR, - "An error occurred while retrieving proposal data from db"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - struct GNUNET_TIME_Absolute timestamp; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_absolute_time ("timestamp", ×tamp), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - db->preflight (db->cls); - qs = db->find_order (db->cls, - &contract_terms, - order_id, - &mi->pubkey); - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_PROPOSAL_LOOKUP_NOT_FOUND, - "unknown order id"); - } - GNUNET_assert (NULL != contract_terms); - json_object_set_new (contract_terms, - "nonce", - json_string (nonce)); - - /* extract fields we need to sign separately */ - res = TALER_MHD_parse_json_data (connection, - contract_terms, - spec); - if (GNUNET_NO == res) - { - return MHD_YES; - } - if (GNUNET_SYSERR == res) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PROPOSAL_ORDER_PARSE_ERROR, - "Impossible to parse the order"); - } - - for (unsigned int i = 0; ipreflight (db->cls); - qs = db->insert_contract_terms (db->cls, - order_id, - &mi->pubkey, - timestamp, - contract_terms); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - if (0 > qs) - { - /* Special report if retries insufficient */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PROPOSAL_STORE_DB_ERROR, - "db error: could not store this proposal's data into db"); - } - // FIXME: now we can delete (merchant_pub, order_id) from the merchant_orders table - } - - GNUNET_assert (NULL != contract_terms); - - stored_nonce - = json_string_value (json_object_get (contract_terms, - "nonce")); - - if (NULL == stored_nonce) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PROPOSAL_ORDER_PARSE_ERROR, - "existing proposal has no nonce"); - } - - if (0 != strcmp (stored_nonce, - nonce)) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PROPOSAL_LOOKUP_NOT_FOUND, - "mismatched nonce"); - } - - - /* create proposal signature */ - { - struct TALER_ProposalDataPS pdps = { - .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_CONTRACT), - .purpose.size = htonl (sizeof (pdps)) - }; - - if (GNUNET_OK != - TALER_JSON_hash (contract_terms, - &pdps.hash)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_INTERNAL_LOGIC_ERROR, - "Could not hash order"); - } - - GNUNET_CRYPTO_eddsa_sign (&mi->privkey.eddsa_priv, - &pdps, - &merchant_sig); - } - return TALER_MHD_reply_json_pack (connection, - MHD_HTTP_OK, - "{ s:o, s:o }", - "contract_terms", - contract_terms, - "sig", - GNUNET_JSON_from_data_auto ( - &merchant_sig)); -} - - -/* end of taler-merchant-httpd_proposal.c */ diff --git a/src/backend/taler-merchant-httpd_orders_order_get5.h b/src/backend/taler-merchant-httpd_orders_order_get5.h deleted file mode 100644 index 677fee0e..00000000 --- a/src/backend/taler-merchant-httpd_orders_order_get5.h +++ /dev/null @@ -1,47 +0,0 @@ -/* - This file is part of TALER - (C) 2014, 2015 INRIA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_propose.h - * @brief headers for /contract handler - * @author Marcello Stanisci - */ -#ifndef TALER_MERCHANT_HTTPD_CONTRACT_H -#define TALER_MERCHANT_HTTPD_CONTRACT_H -#include -#include "taler-merchant-httpd.h" - -/** - * Manage a GET /proposal request. Query the db and returns the - * proposal's data related to the transaction id given as the URL's - * parameter. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_proposal_lookup (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi); - -#endif diff --git a/src/backend/taler-merchant-httpd_orders_order_pay.c b/src/backend/taler-merchant-httpd_orders_order_pay.c deleted file mode 100644 index 7a1b7fd8..00000000 --- a/src/backend/taler-merchant-httpd_orders_order_pay.c +++ /dev/null @@ -1,2255 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2020 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation; either version 3, - or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public - License along with TALER; see the file COPYING. If not, - see -*/ - -/** - * @file backend/taler-merchant-httpd_pay.c - * @brief handling of /pay requests - * @author Marcello Stanisci - * @author Christian Grothoff - * @author Florian Dold - */ -#include "platform.h" -#include -#include -#include -#include -#include -#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)) - -/** - * How often do we retry the (complex!) database transaction? - */ -#define MAX_RETRIES 5 - -/** - * Information we keep for an individual call to the /pay handler. - */ -struct PayContext; - -/** - * Information kept during a /pay request for each coin. - */ -struct DepositConfirmation -{ - - /** - * Reference to the main PayContext - */ - struct PayContext *pc; - - /** - * Handle to the deposit operation we are performing for - * this coin, NULL after the operation is done. - */ - struct TALER_EXCHANGE_DepositHandle *dh; - - /** - * URL of the exchange that issued this coin. - */ - char *exchange_url; - - /** - * Denomination of this coin. - */ - struct TALER_DenominationPublicKey denom; - - /** - * Amount this coin contributes to the total purchase price. - * This amount includes the deposit fee. - */ - struct TALER_Amount amount_with_fee; - - /** - * Fee charged by the exchange for the deposit operation of this coin. - */ - struct TALER_Amount deposit_fee; - - /** - * Fee charged by the exchange for the refund operation of this coin. - */ - struct TALER_Amount refund_fee; - - /** - * Wire fee charged by the exchange of this coin. - */ - struct TALER_Amount wire_fee; - - /** - * Public key of the coin. - */ - struct TALER_CoinSpendPublicKeyP coin_pub; - - /** - * Signature using the @e denom key over the @e coin_pub. - */ - struct TALER_DenominationSignature ub_sig; - - /** - * Signature of the coin's private key over the contract. - */ - struct TALER_CoinSpendSignatureP coin_sig; - - /** - * Offset of this coin into the `dc` array of all coins in the - * @e pc. - */ - unsigned int index; - - /** - * #GNUNET_YES if we found this coin in the database. - */ - int found_in_db; - - /** - * #GNUNET_YES if this coin was refunded. - */ - int refunded; - -}; - - -/** - * Information we keep for an individual call to the /pay handler. - */ -struct PayContext -{ - - /** - * This field MUST be first for handle_mhd_completion_callback() to work - * when it treats this struct as a `struct TM_HandlerContext`. - */ - struct TM_HandlerContext hc; - - /** - * Stored in a DLL. - */ - struct PayContext *next; - - /** - * Stored in a DLL. - */ - struct PayContext *prev; - - /** - * Array with @e coins_cnt coins we are despositing. - */ - struct DepositConfirmation *dc; - - /** - * MHD connection to return to - */ - struct MHD_Connection *connection; - - /** - * 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. - * Happens when we don't get a response from the exchange. - */ - struct GNUNET_SCHEDULER_Task *timeout_task; - - /** - * Response to return, NULL if we don't have one yet. - */ - struct MHD_Response *response; - - /** - * Handle to the exchange that we are doing the payment with. - * (initially NULL while @e fo is trying to find a exchange). - */ - struct TALER_EXCHANGE_Handle *mh; - - /** - * Handle for operation to lookup /keys (and auditors) from - * the exchange used for this transaction; NULL if no operation is - * pending. - */ - struct TMH_EXCHANGES_FindOperation *fo; - - /** - * URL of the exchange used for the last @e fo. - */ - const char *current_exchange; - - /** - * Placeholder for #TALER_MHD_parse_post_json() to keep its internal state. - */ - void *json_parse_context; - - /** - * Optional session id given in @e root. - * NULL if not given. - */ - char *session_id; - - /** - * Transaction ID given in @e root. - */ - 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. - */ - unsigned int coins_cnt; - - /** - * How often have we retried the 'main' transaction? - */ - unsigned int retry_counter; - - /** - * Number of transactions still pending. Initially set to - * @e coins_cnt, decremented on each transaction that - * successfully finished. - */ - unsigned int pending; - - /** - * Number of transactions still pending for the currently selected - * exchange. Initially set to the number of coins started at the - * exchange, decremented on each transaction that successfully - * finished. Once it hits zero, we pick the next exchange. - */ - unsigned int pending_at_ce; - - /** - * HTTP status code to use for the reply, i.e 200 for "OK". - * Special value UINT_MAX is used to indicate hard errors - * (no reply, return #MHD_NO). - */ - unsigned int response_code; - - /** - * #GNUNET_NO if the @e connection was not suspended, - * #GNUNET_YES if the @e connection was suspended, - * #GNUNET_SYSERR if @e connection was resumed to as - * part of #MH_force_pc_resume during shutdown. - */ - 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. - */ -static struct PayContext *pc_head; - -/** - * Tail of active pay context DLL. - */ -static struct PayContext *pc_tail; - - -/** - * Abort all pending /deposit operations. - * - * @param pc pay context to abort - */ -static void -abort_deposit (struct PayContext *pc) -{ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Aborting pending /deposit operations\n"); - for (unsigned int i = 0; icoins_cnt; i++) - { - struct DepositConfirmation *dci = &pc->dc[i]; - - if (NULL != dci->dh) - { - TALER_EXCHANGE_deposit_cancel (dci->dh); - dci->dh = NULL; - } - } -} - - -/** - * Force all pay contexts to be resumed as we are about - * to shut down MHD. - */ -void -MH_force_pc_resume () -{ - for (struct PayContext *pc = pc_head; - NULL != pc; - pc = pc->next) - { - abort_deposit (pc); - if (NULL != pc->timeout_task) - { - GNUNET_SCHEDULER_cancel (pc->timeout_task); - pc->timeout_task = NULL; - } - if (GNUNET_YES == pc->suspended) - { - pc->suspended = GNUNET_SYSERR; - MHD_resume_connection (pc->connection); - } - } -} - - -/** - * Resume the given pay context and send the given response. - * Stores the response in the @a pc and signals MHD to resume - * the connection. Also ensures MHD runs immediately. - * - * @param pc payment context - * @param response_code response code to use - * @param response response data to send back - */ -static void -resume_pay_with_response (struct PayContext *pc, - unsigned int response_code, - struct MHD_Response *response) -{ - pc->response_code = response_code; - pc->response = response; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Resuming /pay handling as exchange interaction is done (%u)\n", - response_code); - if (NULL != pc->timeout_task) - { - GNUNET_SCHEDULER_cancel (pc->timeout_task); - pc->timeout_task = NULL; - } - GNUNET_assert (GNUNET_YES == pc->suspended); - pc->suspended = GNUNET_NO; - MHD_resume_connection (pc->connection); - TMH_trigger_daemon (); /* we resumed, kick MHD */ -} - - -/** - * Resume payment processing with an error. - * - * @param pc operation to resume - * @param http_status http status code to return - * @param ec taler error code to return - * @param msg human readable error message - */ -static void -resume_pay_with_error (struct PayContext *pc, - unsigned int http_status, - enum TALER_ErrorCode ec, - const char *msg) -{ - resume_pay_with_response (pc, - http_status, - TALER_MHD_make_error (ec, - msg)); -} - - -/** - * Generate a response that indicates payment success. - * - * @param pc payment context - */ -static void -generate_success_response (struct PayContext *pc) -{ - json_t *refunds; - struct GNUNET_CRYPTO_EddsaSignature sig; - - /* Check for applicable 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, - 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); - } - - /* Build the response */ - { - 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) - { - GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_JSON_ALLOCATION_FAILURE, - "could not build final response"); - 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; icoins_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); -} - - -/** - * Check whether the amount paid is sufficient to cover - * the contract. - * - * @param pc payment context to check - * @return #GNUNET_OK if the payment is sufficient, #GNUNET_SYSERR if it is - * insufficient - */ -static int -check_payment_sufficient (struct PayContext *pc) -{ - struct TALER_Amount acc_fee; - struct TALER_Amount acc_amount; - struct TALER_Amount final_amount; - struct TALER_Amount wire_fee_delta; - struct TALER_Amount wire_fee_customer_contribution; - struct TALER_Amount total_wire_fee; - struct TALER_Amount total_needed; - - if (0 == pc->coins_cnt) - { - 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; icoins_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; - - for (unsigned int j = 0; jexchange_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)) - { - 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; - } - - switch (TALER_amount_subtract (&wire_fee_delta, - &total_wire_fee, - &pc->max_wire_fee)) - { - case TALER_AAR_RESULT_POSITIVE: - /* Actual wire fee is indeed higher than our maximum, - compute how much the customer is expected to cover! */ - TALER_amount_divide (&wire_fee_customer_contribution, - &wire_fee_delta, - pc->wire_fee_amortization); - break; - case TALER_AAR_RESULT_ZERO: - case TALER_AAR_INVALID_NEGATIVE_RESULT: - /* Wire fee threshold is still above the wire fee amount. - Customer is not going to contribute on this. */ - GNUNET_assert (GNUNET_OK == - TALER_amount_get_zero (total_wire_fee.currency, - &wire_fee_customer_contribution)); - break; - default: - GNUNET_assert (0); - } - - /* add wire fee contribution to the total fees */ - if (0 > - TALER_amount_add (&acc_fee, - &acc_fee, - &wire_fee_customer_contribution)) - { - GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_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)) - { - 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; - } - } - else - { - /* Fees are fully covered by the merchant, all we require - is that the total payment is not below the contract's amount */ - total_needed = pc->amount; - } - - /* Do not count refunds towards the payment */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Subtracting total refunds from paid amount: %s\n", - TALER_amount2s (&pc->total_refunded)); - if (0 > - TALER_amount_subtract (&final_amount, - &acc_amount, - &pc->total_refunded)) - { - GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_REFUNDS_EXCEED_PAYMENTS, - "refunded amount exceeds total payments"); - return GNUNET_SYSERR; - } - - if (-1 == TALER_amount_cmp (&final_amount, - &total_needed)) - { - /* 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; - } - - - return GNUNET_OK; -} - - -/** - * Find the exchange we need to talk to for the next - * pending deposit permission. - * - * @param pc payment context we are processing - */ -static void -find_next_exchange (struct PayContext *pc); - - -/** - * Begin of the DB transaction. If required (from - * soft/serialization errors), the transaction can be - * restarted here. - * - * @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 - */ -static void -deposit_cb (void *cls, - const struct TALER_EXCHANGE_HttpResponse *hr, - const struct TALER_ExchangeSignatureP *exchange_sig, - const struct TALER_ExchangePublicKeyP *sign_key) -{ - 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); -} - - -/** - * Function called with the result of our exchange lookup. - * - * @param cls the `struct PayContext` - * @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, - * 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) -{ - struct PayContext *pc = cls; - const struct TALER_EXCHANGE_Keys *keys; - - pc->fo = NULL; - GNUNET_assert (GNUNET_YES == pc->suspended); - if (MHD_HTTP_OK != hr->http_status) - { - /* The request failed somehow */ - GNUNET_break_op (0); - resume_pay_with_response ( - pc, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ( - (NULL != hr->reply) - ? "{s:s, s:I, s:I, s:I, s:O}" - : "{s:s, s:I, s:I, s:I}", - "hint", - "failed to obtain meta-data from exchange", - "code", - (json_int_t) TALER_EC_PAY_EXCHANGE_KEYS_FAILURE, - "exchange_http_status", - (json_int_t) hr->http_status, - "exchange_code", - (json_int_t) hr->ec, - "exchange_reply", - 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 - the current exchange (!) */ - GNUNET_assert (0 == pc->pending_at_ce); - for (unsigned int i = 0; icoins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->dc[i]; - const struct TALER_EXCHANGE_DenomPublicKey *denom_details; - enum TALER_ErrorCode ec; - unsigned int 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) - continue; - if (0 != strcmp (dc->exchange_url, - pc->current_exchange)) - continue; - denom_details = TALER_EXCHANGE_get_denomination_key (keys, - &dc->denom); - if (NULL == denom_details) - { - 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)); - return; - } - if (TMH_force_audit) - TALER_EXCHANGE_deposit_force_dc (dc->dh); - pc->pending_at_ce++; - } -} - - -/** - * Find the exchange we need to talk to for the next - * pending deposit permission. - * - * @param pc payment context we are processing - */ -static void -find_next_exchange (struct PayContext *pc) -{ - for (unsigned int i = 0; icoins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->dc[i]; - - if (GNUNET_YES != dc->found_in_db) - { - db->preflight (db->cls); - pc->current_exchange = dc->exchange_url; - pc->fo = TMH_EXCHANGES_find_exchange (pc->current_exchange, - pc->wm->wire_method, - GNUNET_NO, - &process_pay_with_exchange, - pc); - if (NULL == pc->fo) - { - GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_EXCHANGE_LOOKUP_FAILED, - "Failed to lookup exchange by URL"); - return; - } - return; - } - } - pc->current_exchange = NULL; - db->preflight (db->cls); - /* 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"); -} - - -/** - * Function called with information about a coin that was deposited. - * - * @param cls closure - * @param h_contract_terms hashed proposal data - * @param coin_pub public key of the coin - * @param exchange_url URL of the exchange that issued @a coin_pub - * @param amount_with_fee amount the exchange will deposit for this coin - * @param deposit_fee fee the exchange will charge for this coin - * @param refund_fee fee the exchange will charge for refunding this coin - * @param wire_fee wire fee the exchange of this coin charges - * @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; icoins_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) -{ - 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)); - - if (NULL == json_object_get (pc->contract_terms, - "merchant")) - { - /* 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 () - }; - - 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; - } - - 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) - { - 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) - { - GNUNET_JSON_parse_free (spec); - GNUNET_break_op (0); - return res; - } - 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; icoins_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)); - } - } -} - - -/** - * Begin of the DB transaction. If required (from - * soft/serialization errors), the transaction can be - * restarted here. - * - * @param pc payment context to transact - */ -static void -begin_transaction (struct PayContext *pc) -{ - enum GNUNET_DB_QueryStatus qs; - - /* Avoid re-trying transactions on soft errors forever! */ - if (pc->retry_counter++ > MAX_RETRIES) - { - GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_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)); - - /* First, try to see if we have all we need already done */ - db->preflight (db->cls); - if (GNUNET_OK != - db->start (db->cls, - "run pay")) - { - 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)"); - 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); - 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_FETCH_TRANSACTION_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); - 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_FETCH_TRANSACTION_ERROR, - "Merchant database error checking for refunds"); - 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) - { - 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) - { - /* 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"); - 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) - { - 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; - } - qs = db->commit (db->cls); - if (0 > 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_PAY_ERROR, - "Merchant database error: could not commit"); - return; - } - /* At this point, the refund got correctly committed - * into the database. */ - { - json_t *refunds; - - refunds = json_array (); - if (NULL == 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; - } - for (unsigned int i = 0; icoins_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) - }; - - 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)))) - { - 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; - } - } - - /* 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 */ - - /* Final termination case: all coins already known, just - generate ultimate outcome. */ - if (0 == pc->pending) - { - if (GNUNET_OK != check_payment_sufficient (pc)) - { - db->rollback (db->cls); - return; - } - /* 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) - { - 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; - } - - if ( (NULL != pc->session_id) && - (NULL != pc->fulfillment_url) ) - { - qs = db->insert_session_info (db->cls, - pc->session_id, - pc->fulfillment_url, - pc->order_id, - &pc->mi->pubkey); - } - - /* Now commit! */ - if (0 <= qs) - qs = db->commit (db->cls); - else - db->rollback (db->cls); - if (0 > qs) - { - 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 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); -} - - -/** - * Process a payment for a proposal. - * - * @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) - */ -static MHD_RESULT -handler_pay_json (struct MHD_Connection *connection, - const json_t *root, - struct PayContext *pc) -{ - { - enum GNUNET_GenericReturnValue ret; - - ret = parse_pay (connection, - root, - 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"); - pc->timeout_task = GNUNET_SCHEDULER_add_delayed (PAY_TIMEOUT, - &handle_pay_timeout, - pc); - begin_transaction (pc); - return MHD_YES; -} - - -/** - * Process a payment for a proposal. Takes data from the given MHD - * connection. - * - * @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 - * @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) -{ - struct PayContext *pc; - enum GNUNET_GenericReturnValue res; - MHD_RESULT ret; - json_t *root; - - 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; - } - if (GNUNET_SYSERR == pc->suspended) - return MHD_NO; /* during shutdown, we don't generate any more replies */ - if (0 != pc->response_code) - { - /* We are *done* processing the request, - just queue the response (!) */ - if (UINT_MAX == pc->response_code) - { - GNUNET_break (0); - return MHD_NO; /* hard error */ - } - res = MHD_queue_response (connection, - pc->response_code, - pc->response); - MHD_destroy_response (pc->response); - pc->response = NULL; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Queueing response (%u) for /pay (%s).\n", - (unsigned int) pc->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"); - } - 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; -} - - -/* end of taler-merchant-httpd_pay.c */ diff --git a/src/backend/taler-merchant-httpd_orders_order_pay.h b/src/backend/taler-merchant-httpd_orders_order_pay.h deleted file mode 100644 index 726a27be..00000000 --- a/src/backend/taler-merchant-httpd_orders_order_pay.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2017 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_pay.h - * @brief headers for /pay handler - * @author Marcello Stanisci - */ -#ifndef TALER_EXCHANGE_HTTPD_PAY_H -#define TALER_EXCHANGE_HTTPD_PAY_H -#include -#include "taler-merchant-httpd.h" - - -/** - * Force all pay contexts to be resumed as we are about - * to shut down MHD. - */ -void -MH_force_pc_resume (void); - - -/** - * Manage a payment - * - * @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 - * @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); - -#endif diff --git a/src/backend/taler-merchant-httpd_orders_order_refund.c b/src/backend/taler-merchant-httpd_orders_order_refund.c deleted file mode 100644 index 5324c619..00000000 --- a/src/backend/taler-merchant-httpd_orders_order_refund.c +++ /dev/null @@ -1,379 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2020 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_refund_increase.c - * @brief Handle request to increase the refund for an order - * @author Marcello Stanisci - */ -#include "platform.h" -#include -#include -#include -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_refund.h" - -/** - * How often do we retry the non-trivial refund INSERT database - * transaction? - */ -#define MAX_RETRIES 5 - - -/** - * Information we keep for individual calls - * to requests that parse JSON, but keep no other state. - */ -struct TMH_JsonParseContext -{ - - /** - * This field MUST be first for handle_mhd_completion_callback() to work - * when it treats this struct as a `struct TM_HandlerContext`. - */ - struct TM_HandlerContext hc; - - /** - * Placeholder for #TALER_MHD_parse_post_json() to keep its internal state. - */ - void *json_parse_context; -}; - - -/** - * Make a taler://refund URI - * - * @param connection MHD connection to take host and path from - * @param instance_id merchant's instance ID, must not be NULL - * @param order_id order ID to show a refund for, must not be NULL - * @returns the URI, must be freed with #GNUNET_free - */ -static char * -make_taler_refund_uri (struct MHD_Connection *connection, - const char *instance_id, - const char *order_id) -{ - const char *host; - const char *forwarded_host; - const char *uri_path; - const char *uri_instance_id; - const char *query; - char *result; - - GNUNET_assert (NULL != instance_id); - GNUNET_assert (NULL != order_id); - host = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_HOST); - forwarded_host = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - "X-Forwarded-Host"); - if (NULL != forwarded_host) - host = forwarded_host; - if (NULL == host) - { - /* Should never happen, at least the host header should be defined */ - GNUNET_break (0); - return NULL; - } - uri_path = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - "X-Forwarded-Prefix"); - if (NULL == uri_path) - uri_path = "-"; - if (0 == strcmp (instance_id, - "default")) - uri_instance_id = "-"; - else - uri_instance_id = instance_id; - if (GNUNET_YES == TALER_mhd_is_https (connection)) - query = ""; - else - query = "?insecure=1"; - GNUNET_assert (0 < GNUNET_asprintf (&result, - "taler://refund/%s/%s/%s/%s%s", - host, - uri_path, - uri_instance_id, - order_id, - query)); - return result; -} - - -/** - * Custom cleanup routine for a `struct TMH_JsonParseContext`. - * - * @param hc the `struct TMH_JsonParseContext` to clean up. - */ -static void -json_parse_cleanup (struct TM_HandlerContext *hc) -{ - struct TMH_JsonParseContext *jpc = (struct TMH_JsonParseContext *) hc; - - TALER_MHD_parse_post_cleanup_callback (jpc->json_parse_context); - GNUNET_free (jpc); -} - - -/** - * Process a refund request. - * - * @param connection HTTP client connection - * @param mi merchant instance doing the processing - * @param refund amount to be refunded - * @param order_id for which order is the refund - * @param reason reason for the refund - * @return MHD result code - */ -static MHD_RESULT -process_refund (struct MHD_Connection *connection, - struct MerchantInstance *mi, - const struct TALER_Amount *refund, - const char *order_id, - const char *reason) -{ - json_t *contract_terms; - enum GNUNET_DB_QueryStatus qs; - enum GNUNET_DB_QueryStatus qsx; - struct GNUNET_HashCode h_contract_terms; - - db->preflight (db->cls); - /* Convert order id to h_contract_terms */ - qs = db->find_contract_terms (db->cls, - &contract_terms, - order_id, - &mi->pubkey); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_REFUND_LOOKUP_DB_ERROR, - "An error occurred while retrieving payment data from db"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Unknown order id given: `%s'\n", - order_id); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_REFUND_ORDER_ID_UNKNOWN, - "order_id not found in database"); - } - - if (GNUNET_OK != - TALER_JSON_hash (contract_terms, - &h_contract_terms)) - { - GNUNET_break (0); - json_decref (contract_terms); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_INTERNAL_LOGIC_ERROR, - "Could not hash contract terms"); - } - json_decref (contract_terms); - for (unsigned int i = 0; istart (db->cls, - "increase refund")) - { - GNUNET_break (0); - return GNUNET_DB_STATUS_HARD_ERROR; - } - qs = db->increase_refund_for_contract_NT (db->cls, - &h_contract_terms, - &mi->pubkey, - refund, - reason); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "increase refund returned %d\n", - qs); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - GNUNET_break (0); - db->rollback (db->cls); - break; - } - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - db->rollback (db->cls); - continue; - } - /* Got one or more deposits */ - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) - { - db->rollback (db->cls); - break; - } - qsx = db->commit (db->cls); - if (GNUNET_DB_STATUS_HARD_ERROR == qsx) - { - GNUNET_break (0); - qs = qsx; - break; - } - if (GNUNET_DB_STATUS_SOFT_ERROR != qsx) - break; - } - if (0 > qs) - { - /* Special report if retries insufficient */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_REFUND_MERCHANT_DB_COMMIT_ERROR, - "Internal database error or refund amount too big"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Refusing refund amount %s that is larger than original payment\n", - TALER_amount2s (refund)); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_REFUND_INCONSISTENT_AMOUNT, - "Amount above payment"); - } - - /* Resume /public/poll-payments clients that may wait for this refund */ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Awakeing clients on %s waiting for refund of less than %s\n", - order_id, - TALER_amount2s (refund)); - - TMH_long_poll_resume (order_id, - &mi->pubkey, - refund); - - { - MHD_RESULT ret; - char *taler_refund_uri; - - taler_refund_uri = make_taler_refund_uri (connection, - mi->id, - order_id); - ret = TALER_MHD_reply_json_pack ( - connection, - MHD_HTTP_OK, - "{s:o, s:s}", - "h_contract_terms", - GNUNET_JSON_from_data_auto (&h_contract_terms), - "taler_refund_url", - taler_refund_uri); - GNUNET_free (taler_refund_uri); - return ret; - } -} - - -/** - * Handle request for increasing the refund associated with - * a contract. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_refund_increase (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi) -{ - enum GNUNET_GenericReturnValue res; - struct TMH_JsonParseContext *ctx; - struct TALER_Amount refund; - const char *order_id; - const char *reason; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount ("refund", &refund), - GNUNET_JSON_spec_string ("order_id", &order_id), - GNUNET_JSON_spec_string ("reason", &reason), - GNUNET_JSON_spec_end () - }; - json_t *root; - - if (NULL == *connection_cls) - { - ctx = GNUNET_new (struct TMH_JsonParseContext); - ctx->hc.cc = &json_parse_cleanup; - *connection_cls = ctx; - } - else - { - ctx = *connection_cls; - } - - res = TALER_MHD_parse_post_json (connection, - &ctx->json_parse_context, - upload_data, - upload_data_size, - &root); - if (GNUNET_SYSERR == res) - return MHD_NO; - /* the POST's body has to be further fetched */ - if ( (GNUNET_NO == res) || - (NULL == root) ) - return MHD_YES; - - res = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_NO == res) - { - GNUNET_break_op (0); - json_decref (root); - return MHD_YES; - } - if (GNUNET_SYSERR == res) - { - GNUNET_break_op (0); - json_decref (root); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_JSON_INVALID, - "Request body does not match specification"); - } - { - MHD_RESULT ret; - - ret = process_refund (connection, - mi, - &refund, - order_id, - reason); - GNUNET_JSON_parse_free (spec); - json_decref (root); - return ret; - } -} - - -/* end of taler-merchant-httpd_refund_increase.c */ diff --git a/src/backend/taler-merchant-httpd_orders_order_refund.h b/src/backend/taler-merchant-httpd_orders_order_refund.h deleted file mode 100644 index ff178001..00000000 --- a/src/backend/taler-merchant-httpd_orders_order_refund.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - This file is part of TALER - (C) 2014, 2015, 2016, 2017 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ - -/** - * @file backend/taler-merchant-httpd_refund_increase.h - * @brief HTTP serving layer mainly intended to communicate with the frontend - * @author Marcello Stanisci - */ - -#ifndef TALER_MERCHANT_HTTPD_REFUND_INCREASE_H -#define TALER_MERCHANT_HTTPD_REFUND_INCREASE_H -#include -#include "taler-merchant-httpd.h" - - -/** - * Handle request for increasing the refund associated with - * a contract. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_refund_increase (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi); - - -#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-abort.c b/src/backend/taler-merchant-httpd_post-orders-ID-abort.c new file mode 100644 index 00000000..7a1b7fd8 --- /dev/null +++ b/src/backend/taler-merchant-httpd_post-orders-ID-abort.c @@ -0,0 +1,2255 @@ +/* + This file is part of TALER + (C) 2014-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation; either version 3, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, + see +*/ + +/** + * @file backend/taler-merchant-httpd_pay.c + * @brief handling of /pay requests + * @author Marcello Stanisci + * @author Christian Grothoff + * @author Florian Dold + */ +#include "platform.h" +#include +#include +#include +#include +#include +#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)) + +/** + * How often do we retry the (complex!) database transaction? + */ +#define MAX_RETRIES 5 + +/** + * Information we keep for an individual call to the /pay handler. + */ +struct PayContext; + +/** + * Information kept during a /pay request for each coin. + */ +struct DepositConfirmation +{ + + /** + * Reference to the main PayContext + */ + struct PayContext *pc; + + /** + * Handle to the deposit operation we are performing for + * this coin, NULL after the operation is done. + */ + struct TALER_EXCHANGE_DepositHandle *dh; + + /** + * URL of the exchange that issued this coin. + */ + char *exchange_url; + + /** + * Denomination of this coin. + */ + struct TALER_DenominationPublicKey denom; + + /** + * Amount this coin contributes to the total purchase price. + * This amount includes the deposit fee. + */ + struct TALER_Amount amount_with_fee; + + /** + * Fee charged by the exchange for the deposit operation of this coin. + */ + struct TALER_Amount deposit_fee; + + /** + * Fee charged by the exchange for the refund operation of this coin. + */ + struct TALER_Amount refund_fee; + + /** + * Wire fee charged by the exchange of this coin. + */ + struct TALER_Amount wire_fee; + + /** + * Public key of the coin. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Signature using the @e denom key over the @e coin_pub. + */ + struct TALER_DenominationSignature ub_sig; + + /** + * Signature of the coin's private key over the contract. + */ + struct TALER_CoinSpendSignatureP coin_sig; + + /** + * Offset of this coin into the `dc` array of all coins in the + * @e pc. + */ + unsigned int index; + + /** + * #GNUNET_YES if we found this coin in the database. + */ + int found_in_db; + + /** + * #GNUNET_YES if this coin was refunded. + */ + int refunded; + +}; + + +/** + * Information we keep for an individual call to the /pay handler. + */ +struct PayContext +{ + + /** + * This field MUST be first for handle_mhd_completion_callback() to work + * when it treats this struct as a `struct TM_HandlerContext`. + */ + struct TM_HandlerContext hc; + + /** + * Stored in a DLL. + */ + struct PayContext *next; + + /** + * Stored in a DLL. + */ + struct PayContext *prev; + + /** + * Array with @e coins_cnt coins we are despositing. + */ + struct DepositConfirmation *dc; + + /** + * MHD connection to return to + */ + struct MHD_Connection *connection; + + /** + * 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. + * Happens when we don't get a response from the exchange. + */ + struct GNUNET_SCHEDULER_Task *timeout_task; + + /** + * Response to return, NULL if we don't have one yet. + */ + struct MHD_Response *response; + + /** + * Handle to the exchange that we are doing the payment with. + * (initially NULL while @e fo is trying to find a exchange). + */ + struct TALER_EXCHANGE_Handle *mh; + + /** + * Handle for operation to lookup /keys (and auditors) from + * the exchange used for this transaction; NULL if no operation is + * pending. + */ + struct TMH_EXCHANGES_FindOperation *fo; + + /** + * URL of the exchange used for the last @e fo. + */ + const char *current_exchange; + + /** + * Placeholder for #TALER_MHD_parse_post_json() to keep its internal state. + */ + void *json_parse_context; + + /** + * Optional session id given in @e root. + * NULL if not given. + */ + char *session_id; + + /** + * Transaction ID given in @e root. + */ + 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. + */ + unsigned int coins_cnt; + + /** + * How often have we retried the 'main' transaction? + */ + unsigned int retry_counter; + + /** + * Number of transactions still pending. Initially set to + * @e coins_cnt, decremented on each transaction that + * successfully finished. + */ + unsigned int pending; + + /** + * Number of transactions still pending for the currently selected + * exchange. Initially set to the number of coins started at the + * exchange, decremented on each transaction that successfully + * finished. Once it hits zero, we pick the next exchange. + */ + unsigned int pending_at_ce; + + /** + * HTTP status code to use for the reply, i.e 200 for "OK". + * Special value UINT_MAX is used to indicate hard errors + * (no reply, return #MHD_NO). + */ + unsigned int response_code; + + /** + * #GNUNET_NO if the @e connection was not suspended, + * #GNUNET_YES if the @e connection was suspended, + * #GNUNET_SYSERR if @e connection was resumed to as + * part of #MH_force_pc_resume during shutdown. + */ + 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. + */ +static struct PayContext *pc_head; + +/** + * Tail of active pay context DLL. + */ +static struct PayContext *pc_tail; + + +/** + * Abort all pending /deposit operations. + * + * @param pc pay context to abort + */ +static void +abort_deposit (struct PayContext *pc) +{ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Aborting pending /deposit operations\n"); + for (unsigned int i = 0; icoins_cnt; i++) + { + struct DepositConfirmation *dci = &pc->dc[i]; + + if (NULL != dci->dh) + { + TALER_EXCHANGE_deposit_cancel (dci->dh); + dci->dh = NULL; + } + } +} + + +/** + * Force all pay contexts to be resumed as we are about + * to shut down MHD. + */ +void +MH_force_pc_resume () +{ + for (struct PayContext *pc = pc_head; + NULL != pc; + pc = pc->next) + { + abort_deposit (pc); + if (NULL != pc->timeout_task) + { + GNUNET_SCHEDULER_cancel (pc->timeout_task); + pc->timeout_task = NULL; + } + if (GNUNET_YES == pc->suspended) + { + pc->suspended = GNUNET_SYSERR; + MHD_resume_connection (pc->connection); + } + } +} + + +/** + * Resume the given pay context and send the given response. + * Stores the response in the @a pc and signals MHD to resume + * the connection. Also ensures MHD runs immediately. + * + * @param pc payment context + * @param response_code response code to use + * @param response response data to send back + */ +static void +resume_pay_with_response (struct PayContext *pc, + unsigned int response_code, + struct MHD_Response *response) +{ + pc->response_code = response_code; + pc->response = response; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resuming /pay handling as exchange interaction is done (%u)\n", + response_code); + if (NULL != pc->timeout_task) + { + GNUNET_SCHEDULER_cancel (pc->timeout_task); + pc->timeout_task = NULL; + } + GNUNET_assert (GNUNET_YES == pc->suspended); + pc->suspended = GNUNET_NO; + MHD_resume_connection (pc->connection); + TMH_trigger_daemon (); /* we resumed, kick MHD */ +} + + +/** + * Resume payment processing with an error. + * + * @param pc operation to resume + * @param http_status http status code to return + * @param ec taler error code to return + * @param msg human readable error message + */ +static void +resume_pay_with_error (struct PayContext *pc, + unsigned int http_status, + enum TALER_ErrorCode ec, + const char *msg) +{ + resume_pay_with_response (pc, + http_status, + TALER_MHD_make_error (ec, + msg)); +} + + +/** + * Generate a response that indicates payment success. + * + * @param pc payment context + */ +static void +generate_success_response (struct PayContext *pc) +{ + json_t *refunds; + struct GNUNET_CRYPTO_EddsaSignature sig; + + /* Check for applicable 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, + 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); + } + + /* Build the response */ + { + 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) + { + GNUNET_break (0); + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_JSON_ALLOCATION_FAILURE, + "could not build final response"); + 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; icoins_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); +} + + +/** + * Check whether the amount paid is sufficient to cover + * the contract. + * + * @param pc payment context to check + * @return #GNUNET_OK if the payment is sufficient, #GNUNET_SYSERR if it is + * insufficient + */ +static int +check_payment_sufficient (struct PayContext *pc) +{ + struct TALER_Amount acc_fee; + struct TALER_Amount acc_amount; + struct TALER_Amount final_amount; + struct TALER_Amount wire_fee_delta; + struct TALER_Amount wire_fee_customer_contribution; + struct TALER_Amount total_wire_fee; + struct TALER_Amount total_needed; + + if (0 == pc->coins_cnt) + { + 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; icoins_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; + + for (unsigned int j = 0; jexchange_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)) + { + 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; + } + + switch (TALER_amount_subtract (&wire_fee_delta, + &total_wire_fee, + &pc->max_wire_fee)) + { + case TALER_AAR_RESULT_POSITIVE: + /* Actual wire fee is indeed higher than our maximum, + compute how much the customer is expected to cover! */ + TALER_amount_divide (&wire_fee_customer_contribution, + &wire_fee_delta, + pc->wire_fee_amortization); + break; + case TALER_AAR_RESULT_ZERO: + case TALER_AAR_INVALID_NEGATIVE_RESULT: + /* Wire fee threshold is still above the wire fee amount. + Customer is not going to contribute on this. */ + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (total_wire_fee.currency, + &wire_fee_customer_contribution)); + break; + default: + GNUNET_assert (0); + } + + /* add wire fee contribution to the total fees */ + if (0 > + TALER_amount_add (&acc_fee, + &acc_fee, + &wire_fee_customer_contribution)) + { + GNUNET_break (0); + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_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)) + { + 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; + } + } + else + { + /* Fees are fully covered by the merchant, all we require + is that the total payment is not below the contract's amount */ + total_needed = pc->amount; + } + + /* Do not count refunds towards the payment */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Subtracting total refunds from paid amount: %s\n", + TALER_amount2s (&pc->total_refunded)); + if (0 > + TALER_amount_subtract (&final_amount, + &acc_amount, + &pc->total_refunded)) + { + GNUNET_break (0); + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_REFUNDS_EXCEED_PAYMENTS, + "refunded amount exceeds total payments"); + return GNUNET_SYSERR; + } + + if (-1 == TALER_amount_cmp (&final_amount, + &total_needed)) + { + /* 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; + } + + + return GNUNET_OK; +} + + +/** + * Find the exchange we need to talk to for the next + * pending deposit permission. + * + * @param pc payment context we are processing + */ +static void +find_next_exchange (struct PayContext *pc); + + +/** + * Begin of the DB transaction. If required (from + * soft/serialization errors), the transaction can be + * restarted here. + * + * @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 + */ +static void +deposit_cb (void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr, + const struct TALER_ExchangeSignatureP *exchange_sig, + const struct TALER_ExchangePublicKeyP *sign_key) +{ + 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); +} + + +/** + * Function called with the result of our exchange lookup. + * + * @param cls the `struct PayContext` + * @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, + * 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) +{ + struct PayContext *pc = cls; + const struct TALER_EXCHANGE_Keys *keys; + + pc->fo = NULL; + GNUNET_assert (GNUNET_YES == pc->suspended); + if (MHD_HTTP_OK != hr->http_status) + { + /* The request failed somehow */ + GNUNET_break_op (0); + resume_pay_with_response ( + pc, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + (NULL != hr->reply) + ? "{s:s, s:I, s:I, s:I, s:O}" + : "{s:s, s:I, s:I, s:I}", + "hint", + "failed to obtain meta-data from exchange", + "code", + (json_int_t) TALER_EC_PAY_EXCHANGE_KEYS_FAILURE, + "exchange_http_status", + (json_int_t) hr->http_status, + "exchange_code", + (json_int_t) hr->ec, + "exchange_reply", + 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 + the current exchange (!) */ + GNUNET_assert (0 == pc->pending_at_ce); + for (unsigned int i = 0; icoins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->dc[i]; + const struct TALER_EXCHANGE_DenomPublicKey *denom_details; + enum TALER_ErrorCode ec; + unsigned int 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) + continue; + if (0 != strcmp (dc->exchange_url, + pc->current_exchange)) + continue; + denom_details = TALER_EXCHANGE_get_denomination_key (keys, + &dc->denom); + if (NULL == denom_details) + { + 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)); + return; + } + if (TMH_force_audit) + TALER_EXCHANGE_deposit_force_dc (dc->dh); + pc->pending_at_ce++; + } +} + + +/** + * Find the exchange we need to talk to for the next + * pending deposit permission. + * + * @param pc payment context we are processing + */ +static void +find_next_exchange (struct PayContext *pc) +{ + for (unsigned int i = 0; icoins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->dc[i]; + + if (GNUNET_YES != dc->found_in_db) + { + db->preflight (db->cls); + pc->current_exchange = dc->exchange_url; + pc->fo = TMH_EXCHANGES_find_exchange (pc->current_exchange, + pc->wm->wire_method, + GNUNET_NO, + &process_pay_with_exchange, + pc); + if (NULL == pc->fo) + { + GNUNET_break (0); + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_EXCHANGE_LOOKUP_FAILED, + "Failed to lookup exchange by URL"); + return; + } + return; + } + } + pc->current_exchange = NULL; + db->preflight (db->cls); + /* 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"); +} + + +/** + * Function called with information about a coin that was deposited. + * + * @param cls closure + * @param h_contract_terms hashed proposal data + * @param coin_pub public key of the coin + * @param exchange_url URL of the exchange that issued @a coin_pub + * @param amount_with_fee amount the exchange will deposit for this coin + * @param deposit_fee fee the exchange will charge for this coin + * @param refund_fee fee the exchange will charge for refunding this coin + * @param wire_fee wire fee the exchange of this coin charges + * @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; icoins_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) +{ + 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)); + + if (NULL == json_object_get (pc->contract_terms, + "merchant")) + { + /* 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 () + }; + + 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; + } + + 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) + { + 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) + { + GNUNET_JSON_parse_free (spec); + GNUNET_break_op (0); + return res; + } + 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; icoins_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)); + } + } +} + + +/** + * Begin of the DB transaction. If required (from + * soft/serialization errors), the transaction can be + * restarted here. + * + * @param pc payment context to transact + */ +static void +begin_transaction (struct PayContext *pc) +{ + enum GNUNET_DB_QueryStatus qs; + + /* Avoid re-trying transactions on soft errors forever! */ + if (pc->retry_counter++ > MAX_RETRIES) + { + GNUNET_break (0); + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_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)); + + /* First, try to see if we have all we need already done */ + db->preflight (db->cls); + if (GNUNET_OK != + db->start (db->cls, + "run pay")) + { + 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)"); + 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); + 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_FETCH_TRANSACTION_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); + 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_FETCH_TRANSACTION_ERROR, + "Merchant database error checking for refunds"); + 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) + { + 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) + { + /* 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"); + 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) + { + 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; + } + qs = db->commit (db->cls); + if (0 > 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_PAY_ERROR, + "Merchant database error: could not commit"); + return; + } + /* At this point, the refund got correctly committed + * into the database. */ + { + json_t *refunds; + + refunds = json_array (); + if (NULL == 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; + } + for (unsigned int i = 0; icoins_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) + }; + + 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)))) + { + 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; + } + } + + /* 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 */ + + /* Final termination case: all coins already known, just + generate ultimate outcome. */ + if (0 == pc->pending) + { + if (GNUNET_OK != check_payment_sufficient (pc)) + { + db->rollback (db->cls); + return; + } + /* 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) + { + 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; + } + + if ( (NULL != pc->session_id) && + (NULL != pc->fulfillment_url) ) + { + qs = db->insert_session_info (db->cls, + pc->session_id, + pc->fulfillment_url, + pc->order_id, + &pc->mi->pubkey); + } + + /* Now commit! */ + if (0 <= qs) + qs = db->commit (db->cls); + else + db->rollback (db->cls); + if (0 > qs) + { + 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 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); +} + + +/** + * Process a payment for a proposal. + * + * @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) + */ +static MHD_RESULT +handler_pay_json (struct MHD_Connection *connection, + const json_t *root, + struct PayContext *pc) +{ + { + enum GNUNET_GenericReturnValue ret; + + ret = parse_pay (connection, + root, + 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"); + pc->timeout_task = GNUNET_SCHEDULER_add_delayed (PAY_TIMEOUT, + &handle_pay_timeout, + pc); + begin_transaction (pc); + return MHD_YES; +} + + +/** + * Process a payment for a proposal. Takes data from the given MHD + * connection. + * + * @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 + * @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) +{ + struct PayContext *pc; + enum GNUNET_GenericReturnValue res; + MHD_RESULT ret; + json_t *root; + + 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; + } + if (GNUNET_SYSERR == pc->suspended) + return MHD_NO; /* during shutdown, we don't generate any more replies */ + if (0 != pc->response_code) + { + /* We are *done* processing the request, + just queue the response (!) */ + if (UINT_MAX == pc->response_code) + { + GNUNET_break (0); + return MHD_NO; /* hard error */ + } + res = MHD_queue_response (connection, + pc->response_code, + pc->response); + MHD_destroy_response (pc->response); + pc->response = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Queueing response (%u) for /pay (%s).\n", + (unsigned int) pc->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"); + } + 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; +} + + +/* end of taler-merchant-httpd_pay.c */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-abort.h b/src/backend/taler-merchant-httpd_post-orders-ID-abort.h new file mode 100644 index 00000000..726a27be --- /dev/null +++ b/src/backend/taler-merchant-httpd_post-orders-ID-abort.h @@ -0,0 +1,54 @@ +/* + This file is part of TALER + (C) 2014-2017 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_pay.h + * @brief headers for /pay handler + * @author Marcello Stanisci + */ +#ifndef TALER_EXCHANGE_HTTPD_PAY_H +#define TALER_EXCHANGE_HTTPD_PAY_H +#include +#include "taler-merchant-httpd.h" + + +/** + * Force all pay contexts to be resumed as we are about + * to shut down MHD. + */ +void +MH_force_pc_resume (void); + + +/** + * Manage a payment + * + * @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 + * @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); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-claim.c b/src/backend/taler-merchant-httpd_post-orders-ID-claim.c new file mode 100644 index 00000000..47207131 --- /dev/null +++ b/src/backend/taler-merchant-httpd_post-orders-ID-claim.c @@ -0,0 +1,228 @@ +/* + This file is part of TALER + (C) 2014, 2015, 2016, 2018 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation; either version 3, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, + see +*/ + +/** + * @file backend/taler-merchant-httpd_proposal.c + * @brief HTTP serving layer mainly intended to communicate + * with the frontend + * @author Marcello Stanisci + */ +#include "platform.h" +#include +#include +#include +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_auditors.h" +#include "taler-merchant-httpd_exchanges.h" + + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + +/** + * Manage a GET /proposal request. Query the db and returns the + * proposal's data related to the transaction id given as the URL's + * parameter. + * + * Binds the proposal to a nonce. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_proposal_lookup (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi) +{ + const char *order_id; + const char *nonce; + enum GNUNET_DB_QueryStatus qs; + json_t *contract_terms; + struct GNUNET_CRYPTO_EddsaSignature merchant_sig; + const char *stored_nonce; + + order_id = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "order_id"); + if (NULL == order_id) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "order_id"); + nonce = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "nonce"); + if (NULL == nonce) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "nonce"); + db->preflight (db->cls); + qs = db->find_contract_terms (db->cls, + &contract_terms, + order_id, + &mi->pubkey); + if (0 > qs) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PROPOSAL_LOOKUP_DB_ERROR, + "An error occurred while retrieving proposal data from db"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + struct GNUNET_TIME_Absolute timestamp; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_absolute_time ("timestamp", ×tamp), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + db->preflight (db->cls); + qs = db->find_order (db->cls, + &contract_terms, + order_id, + &mi->pubkey); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_PROPOSAL_LOOKUP_NOT_FOUND, + "unknown order id"); + } + GNUNET_assert (NULL != contract_terms); + json_object_set_new (contract_terms, + "nonce", + json_string (nonce)); + + /* extract fields we need to sign separately */ + res = TALER_MHD_parse_json_data (connection, + contract_terms, + spec); + if (GNUNET_NO == res) + { + return MHD_YES; + } + if (GNUNET_SYSERR == res) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PROPOSAL_ORDER_PARSE_ERROR, + "Impossible to parse the order"); + } + + for (unsigned int i = 0; ipreflight (db->cls); + qs = db->insert_contract_terms (db->cls, + order_id, + &mi->pubkey, + timestamp, + contract_terms); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (0 > qs) + { + /* Special report if retries insufficient */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PROPOSAL_STORE_DB_ERROR, + "db error: could not store this proposal's data into db"); + } + // FIXME: now we can delete (merchant_pub, order_id) from the merchant_orders table + } + + GNUNET_assert (NULL != contract_terms); + + stored_nonce + = json_string_value (json_object_get (contract_terms, + "nonce")); + + if (NULL == stored_nonce) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PROPOSAL_ORDER_PARSE_ERROR, + "existing proposal has no nonce"); + } + + if (0 != strcmp (stored_nonce, + nonce)) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PROPOSAL_LOOKUP_NOT_FOUND, + "mismatched nonce"); + } + + + /* create proposal signature */ + { + struct TALER_ProposalDataPS pdps = { + .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_CONTRACT), + .purpose.size = htonl (sizeof (pdps)) + }; + + if (GNUNET_OK != + TALER_JSON_hash (contract_terms, + &pdps.hash)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_INTERNAL_LOGIC_ERROR, + "Could not hash order"); + } + + GNUNET_CRYPTO_eddsa_sign (&mi->privkey.eddsa_priv, + &pdps, + &merchant_sig); + } + return TALER_MHD_reply_json_pack (connection, + MHD_HTTP_OK, + "{ s:o, s:o }", + "contract_terms", + contract_terms, + "sig", + GNUNET_JSON_from_data_auto ( + &merchant_sig)); +} + + +/* end of taler-merchant-httpd_proposal.c */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-claim.h b/src/backend/taler-merchant-httpd_post-orders-ID-claim.h new file mode 100644 index 00000000..677fee0e --- /dev/null +++ b/src/backend/taler-merchant-httpd_post-orders-ID-claim.h @@ -0,0 +1,47 @@ +/* + This file is part of TALER + (C) 2014, 2015 INRIA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_propose.h + * @brief headers for /contract handler + * @author Marcello Stanisci + */ +#ifndef TALER_MERCHANT_HTTPD_CONTRACT_H +#define TALER_MERCHANT_HTTPD_CONTRACT_H +#include +#include "taler-merchant-httpd.h" + +/** + * Manage a GET /proposal request. Query the db and returns the + * proposal's data related to the transaction id given as the URL's + * parameter. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_proposal_lookup (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c new file mode 100644 index 00000000..7a1b7fd8 --- /dev/null +++ b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -0,0 +1,2255 @@ +/* + This file is part of TALER + (C) 2014-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation; either version 3, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, + see +*/ + +/** + * @file backend/taler-merchant-httpd_pay.c + * @brief handling of /pay requests + * @author Marcello Stanisci + * @author Christian Grothoff + * @author Florian Dold + */ +#include "platform.h" +#include +#include +#include +#include +#include +#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)) + +/** + * How often do we retry the (complex!) database transaction? + */ +#define MAX_RETRIES 5 + +/** + * Information we keep for an individual call to the /pay handler. + */ +struct PayContext; + +/** + * Information kept during a /pay request for each coin. + */ +struct DepositConfirmation +{ + + /** + * Reference to the main PayContext + */ + struct PayContext *pc; + + /** + * Handle to the deposit operation we are performing for + * this coin, NULL after the operation is done. + */ + struct TALER_EXCHANGE_DepositHandle *dh; + + /** + * URL of the exchange that issued this coin. + */ + char *exchange_url; + + /** + * Denomination of this coin. + */ + struct TALER_DenominationPublicKey denom; + + /** + * Amount this coin contributes to the total purchase price. + * This amount includes the deposit fee. + */ + struct TALER_Amount amount_with_fee; + + /** + * Fee charged by the exchange for the deposit operation of this coin. + */ + struct TALER_Amount deposit_fee; + + /** + * Fee charged by the exchange for the refund operation of this coin. + */ + struct TALER_Amount refund_fee; + + /** + * Wire fee charged by the exchange of this coin. + */ + struct TALER_Amount wire_fee; + + /** + * Public key of the coin. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Signature using the @e denom key over the @e coin_pub. + */ + struct TALER_DenominationSignature ub_sig; + + /** + * Signature of the coin's private key over the contract. + */ + struct TALER_CoinSpendSignatureP coin_sig; + + /** + * Offset of this coin into the `dc` array of all coins in the + * @e pc. + */ + unsigned int index; + + /** + * #GNUNET_YES if we found this coin in the database. + */ + int found_in_db; + + /** + * #GNUNET_YES if this coin was refunded. + */ + int refunded; + +}; + + +/** + * Information we keep for an individual call to the /pay handler. + */ +struct PayContext +{ + + /** + * This field MUST be first for handle_mhd_completion_callback() to work + * when it treats this struct as a `struct TM_HandlerContext`. + */ + struct TM_HandlerContext hc; + + /** + * Stored in a DLL. + */ + struct PayContext *next; + + /** + * Stored in a DLL. + */ + struct PayContext *prev; + + /** + * Array with @e coins_cnt coins we are despositing. + */ + struct DepositConfirmation *dc; + + /** + * MHD connection to return to + */ + struct MHD_Connection *connection; + + /** + * 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. + * Happens when we don't get a response from the exchange. + */ + struct GNUNET_SCHEDULER_Task *timeout_task; + + /** + * Response to return, NULL if we don't have one yet. + */ + struct MHD_Response *response; + + /** + * Handle to the exchange that we are doing the payment with. + * (initially NULL while @e fo is trying to find a exchange). + */ + struct TALER_EXCHANGE_Handle *mh; + + /** + * Handle for operation to lookup /keys (and auditors) from + * the exchange used for this transaction; NULL if no operation is + * pending. + */ + struct TMH_EXCHANGES_FindOperation *fo; + + /** + * URL of the exchange used for the last @e fo. + */ + const char *current_exchange; + + /** + * Placeholder for #TALER_MHD_parse_post_json() to keep its internal state. + */ + void *json_parse_context; + + /** + * Optional session id given in @e root. + * NULL if not given. + */ + char *session_id; + + /** + * Transaction ID given in @e root. + */ + 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. + */ + unsigned int coins_cnt; + + /** + * How often have we retried the 'main' transaction? + */ + unsigned int retry_counter; + + /** + * Number of transactions still pending. Initially set to + * @e coins_cnt, decremented on each transaction that + * successfully finished. + */ + unsigned int pending; + + /** + * Number of transactions still pending for the currently selected + * exchange. Initially set to the number of coins started at the + * exchange, decremented on each transaction that successfully + * finished. Once it hits zero, we pick the next exchange. + */ + unsigned int pending_at_ce; + + /** + * HTTP status code to use for the reply, i.e 200 for "OK". + * Special value UINT_MAX is used to indicate hard errors + * (no reply, return #MHD_NO). + */ + unsigned int response_code; + + /** + * #GNUNET_NO if the @e connection was not suspended, + * #GNUNET_YES if the @e connection was suspended, + * #GNUNET_SYSERR if @e connection was resumed to as + * part of #MH_force_pc_resume during shutdown. + */ + 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. + */ +static struct PayContext *pc_head; + +/** + * Tail of active pay context DLL. + */ +static struct PayContext *pc_tail; + + +/** + * Abort all pending /deposit operations. + * + * @param pc pay context to abort + */ +static void +abort_deposit (struct PayContext *pc) +{ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Aborting pending /deposit operations\n"); + for (unsigned int i = 0; icoins_cnt; i++) + { + struct DepositConfirmation *dci = &pc->dc[i]; + + if (NULL != dci->dh) + { + TALER_EXCHANGE_deposit_cancel (dci->dh); + dci->dh = NULL; + } + } +} + + +/** + * Force all pay contexts to be resumed as we are about + * to shut down MHD. + */ +void +MH_force_pc_resume () +{ + for (struct PayContext *pc = pc_head; + NULL != pc; + pc = pc->next) + { + abort_deposit (pc); + if (NULL != pc->timeout_task) + { + GNUNET_SCHEDULER_cancel (pc->timeout_task); + pc->timeout_task = NULL; + } + if (GNUNET_YES == pc->suspended) + { + pc->suspended = GNUNET_SYSERR; + MHD_resume_connection (pc->connection); + } + } +} + + +/** + * Resume the given pay context and send the given response. + * Stores the response in the @a pc and signals MHD to resume + * the connection. Also ensures MHD runs immediately. + * + * @param pc payment context + * @param response_code response code to use + * @param response response data to send back + */ +static void +resume_pay_with_response (struct PayContext *pc, + unsigned int response_code, + struct MHD_Response *response) +{ + pc->response_code = response_code; + pc->response = response; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resuming /pay handling as exchange interaction is done (%u)\n", + response_code); + if (NULL != pc->timeout_task) + { + GNUNET_SCHEDULER_cancel (pc->timeout_task); + pc->timeout_task = NULL; + } + GNUNET_assert (GNUNET_YES == pc->suspended); + pc->suspended = GNUNET_NO; + MHD_resume_connection (pc->connection); + TMH_trigger_daemon (); /* we resumed, kick MHD */ +} + + +/** + * Resume payment processing with an error. + * + * @param pc operation to resume + * @param http_status http status code to return + * @param ec taler error code to return + * @param msg human readable error message + */ +static void +resume_pay_with_error (struct PayContext *pc, + unsigned int http_status, + enum TALER_ErrorCode ec, + const char *msg) +{ + resume_pay_with_response (pc, + http_status, + TALER_MHD_make_error (ec, + msg)); +} + + +/** + * Generate a response that indicates payment success. + * + * @param pc payment context + */ +static void +generate_success_response (struct PayContext *pc) +{ + json_t *refunds; + struct GNUNET_CRYPTO_EddsaSignature sig; + + /* Check for applicable 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, + 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); + } + + /* Build the response */ + { + 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) + { + GNUNET_break (0); + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_JSON_ALLOCATION_FAILURE, + "could not build final response"); + 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; icoins_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); +} + + +/** + * Check whether the amount paid is sufficient to cover + * the contract. + * + * @param pc payment context to check + * @return #GNUNET_OK if the payment is sufficient, #GNUNET_SYSERR if it is + * insufficient + */ +static int +check_payment_sufficient (struct PayContext *pc) +{ + struct TALER_Amount acc_fee; + struct TALER_Amount acc_amount; + struct TALER_Amount final_amount; + struct TALER_Amount wire_fee_delta; + struct TALER_Amount wire_fee_customer_contribution; + struct TALER_Amount total_wire_fee; + struct TALER_Amount total_needed; + + if (0 == pc->coins_cnt) + { + 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; icoins_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; + + for (unsigned int j = 0; jexchange_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)) + { + 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; + } + + switch (TALER_amount_subtract (&wire_fee_delta, + &total_wire_fee, + &pc->max_wire_fee)) + { + case TALER_AAR_RESULT_POSITIVE: + /* Actual wire fee is indeed higher than our maximum, + compute how much the customer is expected to cover! */ + TALER_amount_divide (&wire_fee_customer_contribution, + &wire_fee_delta, + pc->wire_fee_amortization); + break; + case TALER_AAR_RESULT_ZERO: + case TALER_AAR_INVALID_NEGATIVE_RESULT: + /* Wire fee threshold is still above the wire fee amount. + Customer is not going to contribute on this. */ + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (total_wire_fee.currency, + &wire_fee_customer_contribution)); + break; + default: + GNUNET_assert (0); + } + + /* add wire fee contribution to the total fees */ + if (0 > + TALER_amount_add (&acc_fee, + &acc_fee, + &wire_fee_customer_contribution)) + { + GNUNET_break (0); + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_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)) + { + 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; + } + } + else + { + /* Fees are fully covered by the merchant, all we require + is that the total payment is not below the contract's amount */ + total_needed = pc->amount; + } + + /* Do not count refunds towards the payment */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Subtracting total refunds from paid amount: %s\n", + TALER_amount2s (&pc->total_refunded)); + if (0 > + TALER_amount_subtract (&final_amount, + &acc_amount, + &pc->total_refunded)) + { + GNUNET_break (0); + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_REFUNDS_EXCEED_PAYMENTS, + "refunded amount exceeds total payments"); + return GNUNET_SYSERR; + } + + if (-1 == TALER_amount_cmp (&final_amount, + &total_needed)) + { + /* 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; + } + + + return GNUNET_OK; +} + + +/** + * Find the exchange we need to talk to for the next + * pending deposit permission. + * + * @param pc payment context we are processing + */ +static void +find_next_exchange (struct PayContext *pc); + + +/** + * Begin of the DB transaction. If required (from + * soft/serialization errors), the transaction can be + * restarted here. + * + * @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 + */ +static void +deposit_cb (void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr, + const struct TALER_ExchangeSignatureP *exchange_sig, + const struct TALER_ExchangePublicKeyP *sign_key) +{ + 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); +} + + +/** + * Function called with the result of our exchange lookup. + * + * @param cls the `struct PayContext` + * @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, + * 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) +{ + struct PayContext *pc = cls; + const struct TALER_EXCHANGE_Keys *keys; + + pc->fo = NULL; + GNUNET_assert (GNUNET_YES == pc->suspended); + if (MHD_HTTP_OK != hr->http_status) + { + /* The request failed somehow */ + GNUNET_break_op (0); + resume_pay_with_response ( + pc, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + (NULL != hr->reply) + ? "{s:s, s:I, s:I, s:I, s:O}" + : "{s:s, s:I, s:I, s:I}", + "hint", + "failed to obtain meta-data from exchange", + "code", + (json_int_t) TALER_EC_PAY_EXCHANGE_KEYS_FAILURE, + "exchange_http_status", + (json_int_t) hr->http_status, + "exchange_code", + (json_int_t) hr->ec, + "exchange_reply", + 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 + the current exchange (!) */ + GNUNET_assert (0 == pc->pending_at_ce); + for (unsigned int i = 0; icoins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->dc[i]; + const struct TALER_EXCHANGE_DenomPublicKey *denom_details; + enum TALER_ErrorCode ec; + unsigned int 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) + continue; + if (0 != strcmp (dc->exchange_url, + pc->current_exchange)) + continue; + denom_details = TALER_EXCHANGE_get_denomination_key (keys, + &dc->denom); + if (NULL == denom_details) + { + 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)); + return; + } + if (TMH_force_audit) + TALER_EXCHANGE_deposit_force_dc (dc->dh); + pc->pending_at_ce++; + } +} + + +/** + * Find the exchange we need to talk to for the next + * pending deposit permission. + * + * @param pc payment context we are processing + */ +static void +find_next_exchange (struct PayContext *pc) +{ + for (unsigned int i = 0; icoins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->dc[i]; + + if (GNUNET_YES != dc->found_in_db) + { + db->preflight (db->cls); + pc->current_exchange = dc->exchange_url; + pc->fo = TMH_EXCHANGES_find_exchange (pc->current_exchange, + pc->wm->wire_method, + GNUNET_NO, + &process_pay_with_exchange, + pc); + if (NULL == pc->fo) + { + GNUNET_break (0); + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_EXCHANGE_LOOKUP_FAILED, + "Failed to lookup exchange by URL"); + return; + } + return; + } + } + pc->current_exchange = NULL; + db->preflight (db->cls); + /* 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"); +} + + +/** + * Function called with information about a coin that was deposited. + * + * @param cls closure + * @param h_contract_terms hashed proposal data + * @param coin_pub public key of the coin + * @param exchange_url URL of the exchange that issued @a coin_pub + * @param amount_with_fee amount the exchange will deposit for this coin + * @param deposit_fee fee the exchange will charge for this coin + * @param refund_fee fee the exchange will charge for refunding this coin + * @param wire_fee wire fee the exchange of this coin charges + * @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; icoins_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) +{ + 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)); + + if (NULL == json_object_get (pc->contract_terms, + "merchant")) + { + /* 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 () + }; + + 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; + } + + 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) + { + 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) + { + GNUNET_JSON_parse_free (spec); + GNUNET_break_op (0); + return res; + } + 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; icoins_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)); + } + } +} + + +/** + * Begin of the DB transaction. If required (from + * soft/serialization errors), the transaction can be + * restarted here. + * + * @param pc payment context to transact + */ +static void +begin_transaction (struct PayContext *pc) +{ + enum GNUNET_DB_QueryStatus qs; + + /* Avoid re-trying transactions on soft errors forever! */ + if (pc->retry_counter++ > MAX_RETRIES) + { + GNUNET_break (0); + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_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)); + + /* First, try to see if we have all we need already done */ + db->preflight (db->cls); + if (GNUNET_OK != + db->start (db->cls, + "run pay")) + { + 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)"); + 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); + 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_FETCH_TRANSACTION_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); + 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_FETCH_TRANSACTION_ERROR, + "Merchant database error checking for refunds"); + 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) + { + 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) + { + /* 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"); + 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) + { + 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; + } + qs = db->commit (db->cls); + if (0 > 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_PAY_ERROR, + "Merchant database error: could not commit"); + return; + } + /* At this point, the refund got correctly committed + * into the database. */ + { + json_t *refunds; + + refunds = json_array (); + if (NULL == 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; + } + for (unsigned int i = 0; icoins_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) + }; + + 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)))) + { + 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; + } + } + + /* 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 */ + + /* Final termination case: all coins already known, just + generate ultimate outcome. */ + if (0 == pc->pending) + { + if (GNUNET_OK != check_payment_sufficient (pc)) + { + db->rollback (db->cls); + return; + } + /* 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) + { + 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; + } + + if ( (NULL != pc->session_id) && + (NULL != pc->fulfillment_url) ) + { + qs = db->insert_session_info (db->cls, + pc->session_id, + pc->fulfillment_url, + pc->order_id, + &pc->mi->pubkey); + } + + /* Now commit! */ + if (0 <= qs) + qs = db->commit (db->cls); + else + db->rollback (db->cls); + if (0 > qs) + { + 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 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); +} + + +/** + * Process a payment for a proposal. + * + * @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) + */ +static MHD_RESULT +handler_pay_json (struct MHD_Connection *connection, + const json_t *root, + struct PayContext *pc) +{ + { + enum GNUNET_GenericReturnValue ret; + + ret = parse_pay (connection, + root, + 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"); + pc->timeout_task = GNUNET_SCHEDULER_add_delayed (PAY_TIMEOUT, + &handle_pay_timeout, + pc); + begin_transaction (pc); + return MHD_YES; +} + + +/** + * Process a payment for a proposal. Takes data from the given MHD + * connection. + * + * @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 + * @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) +{ + struct PayContext *pc; + enum GNUNET_GenericReturnValue res; + MHD_RESULT ret; + json_t *root; + + 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; + } + if (GNUNET_SYSERR == pc->suspended) + return MHD_NO; /* during shutdown, we don't generate any more replies */ + if (0 != pc->response_code) + { + /* We are *done* processing the request, + just queue the response (!) */ + if (UINT_MAX == pc->response_code) + { + GNUNET_break (0); + return MHD_NO; /* hard error */ + } + res = MHD_queue_response (connection, + pc->response_code, + pc->response); + MHD_destroy_response (pc->response); + pc->response = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Queueing response (%u) for /pay (%s).\n", + (unsigned int) pc->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"); + } + 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; +} + + +/* end of taler-merchant-httpd_pay.c */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.h b/src/backend/taler-merchant-httpd_post-orders-ID-pay.h new file mode 100644 index 00000000..726a27be --- /dev/null +++ b/src/backend/taler-merchant-httpd_post-orders-ID-pay.h @@ -0,0 +1,54 @@ +/* + This file is part of TALER + (C) 2014-2017 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_pay.h + * @brief headers for /pay handler + * @author Marcello Stanisci + */ +#ifndef TALER_EXCHANGE_HTTPD_PAY_H +#define TALER_EXCHANGE_HTTPD_PAY_H +#include +#include "taler-merchant-httpd.h" + + +/** + * Force all pay contexts to be resumed as we are about + * to shut down MHD. + */ +void +MH_force_pc_resume (void); + + +/** + * Manage a payment + * + * @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 + * @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); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-order-abort.c b/src/backend/taler-merchant-httpd_post-orders-order-abort.c deleted file mode 100644 index 7a1b7fd8..00000000 --- a/src/backend/taler-merchant-httpd_post-orders-order-abort.c +++ /dev/null @@ -1,2255 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2020 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation; either version 3, - or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public - License along with TALER; see the file COPYING. If not, - see -*/ - -/** - * @file backend/taler-merchant-httpd_pay.c - * @brief handling of /pay requests - * @author Marcello Stanisci - * @author Christian Grothoff - * @author Florian Dold - */ -#include "platform.h" -#include -#include -#include -#include -#include -#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)) - -/** - * How often do we retry the (complex!) database transaction? - */ -#define MAX_RETRIES 5 - -/** - * Information we keep for an individual call to the /pay handler. - */ -struct PayContext; - -/** - * Information kept during a /pay request for each coin. - */ -struct DepositConfirmation -{ - - /** - * Reference to the main PayContext - */ - struct PayContext *pc; - - /** - * Handle to the deposit operation we are performing for - * this coin, NULL after the operation is done. - */ - struct TALER_EXCHANGE_DepositHandle *dh; - - /** - * URL of the exchange that issued this coin. - */ - char *exchange_url; - - /** - * Denomination of this coin. - */ - struct TALER_DenominationPublicKey denom; - - /** - * Amount this coin contributes to the total purchase price. - * This amount includes the deposit fee. - */ - struct TALER_Amount amount_with_fee; - - /** - * Fee charged by the exchange for the deposit operation of this coin. - */ - struct TALER_Amount deposit_fee; - - /** - * Fee charged by the exchange for the refund operation of this coin. - */ - struct TALER_Amount refund_fee; - - /** - * Wire fee charged by the exchange of this coin. - */ - struct TALER_Amount wire_fee; - - /** - * Public key of the coin. - */ - struct TALER_CoinSpendPublicKeyP coin_pub; - - /** - * Signature using the @e denom key over the @e coin_pub. - */ - struct TALER_DenominationSignature ub_sig; - - /** - * Signature of the coin's private key over the contract. - */ - struct TALER_CoinSpendSignatureP coin_sig; - - /** - * Offset of this coin into the `dc` array of all coins in the - * @e pc. - */ - unsigned int index; - - /** - * #GNUNET_YES if we found this coin in the database. - */ - int found_in_db; - - /** - * #GNUNET_YES if this coin was refunded. - */ - int refunded; - -}; - - -/** - * Information we keep for an individual call to the /pay handler. - */ -struct PayContext -{ - - /** - * This field MUST be first for handle_mhd_completion_callback() to work - * when it treats this struct as a `struct TM_HandlerContext`. - */ - struct TM_HandlerContext hc; - - /** - * Stored in a DLL. - */ - struct PayContext *next; - - /** - * Stored in a DLL. - */ - struct PayContext *prev; - - /** - * Array with @e coins_cnt coins we are despositing. - */ - struct DepositConfirmation *dc; - - /** - * MHD connection to return to - */ - struct MHD_Connection *connection; - - /** - * 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. - * Happens when we don't get a response from the exchange. - */ - struct GNUNET_SCHEDULER_Task *timeout_task; - - /** - * Response to return, NULL if we don't have one yet. - */ - struct MHD_Response *response; - - /** - * Handle to the exchange that we are doing the payment with. - * (initially NULL while @e fo is trying to find a exchange). - */ - struct TALER_EXCHANGE_Handle *mh; - - /** - * Handle for operation to lookup /keys (and auditors) from - * the exchange used for this transaction; NULL if no operation is - * pending. - */ - struct TMH_EXCHANGES_FindOperation *fo; - - /** - * URL of the exchange used for the last @e fo. - */ - const char *current_exchange; - - /** - * Placeholder for #TALER_MHD_parse_post_json() to keep its internal state. - */ - void *json_parse_context; - - /** - * Optional session id given in @e root. - * NULL if not given. - */ - char *session_id; - - /** - * Transaction ID given in @e root. - */ - 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. - */ - unsigned int coins_cnt; - - /** - * How often have we retried the 'main' transaction? - */ - unsigned int retry_counter; - - /** - * Number of transactions still pending. Initially set to - * @e coins_cnt, decremented on each transaction that - * successfully finished. - */ - unsigned int pending; - - /** - * Number of transactions still pending for the currently selected - * exchange. Initially set to the number of coins started at the - * exchange, decremented on each transaction that successfully - * finished. Once it hits zero, we pick the next exchange. - */ - unsigned int pending_at_ce; - - /** - * HTTP status code to use for the reply, i.e 200 for "OK". - * Special value UINT_MAX is used to indicate hard errors - * (no reply, return #MHD_NO). - */ - unsigned int response_code; - - /** - * #GNUNET_NO if the @e connection was not suspended, - * #GNUNET_YES if the @e connection was suspended, - * #GNUNET_SYSERR if @e connection was resumed to as - * part of #MH_force_pc_resume during shutdown. - */ - 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. - */ -static struct PayContext *pc_head; - -/** - * Tail of active pay context DLL. - */ -static struct PayContext *pc_tail; - - -/** - * Abort all pending /deposit operations. - * - * @param pc pay context to abort - */ -static void -abort_deposit (struct PayContext *pc) -{ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Aborting pending /deposit operations\n"); - for (unsigned int i = 0; icoins_cnt; i++) - { - struct DepositConfirmation *dci = &pc->dc[i]; - - if (NULL != dci->dh) - { - TALER_EXCHANGE_deposit_cancel (dci->dh); - dci->dh = NULL; - } - } -} - - -/** - * Force all pay contexts to be resumed as we are about - * to shut down MHD. - */ -void -MH_force_pc_resume () -{ - for (struct PayContext *pc = pc_head; - NULL != pc; - pc = pc->next) - { - abort_deposit (pc); - if (NULL != pc->timeout_task) - { - GNUNET_SCHEDULER_cancel (pc->timeout_task); - pc->timeout_task = NULL; - } - if (GNUNET_YES == pc->suspended) - { - pc->suspended = GNUNET_SYSERR; - MHD_resume_connection (pc->connection); - } - } -} - - -/** - * Resume the given pay context and send the given response. - * Stores the response in the @a pc and signals MHD to resume - * the connection. Also ensures MHD runs immediately. - * - * @param pc payment context - * @param response_code response code to use - * @param response response data to send back - */ -static void -resume_pay_with_response (struct PayContext *pc, - unsigned int response_code, - struct MHD_Response *response) -{ - pc->response_code = response_code; - pc->response = response; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Resuming /pay handling as exchange interaction is done (%u)\n", - response_code); - if (NULL != pc->timeout_task) - { - GNUNET_SCHEDULER_cancel (pc->timeout_task); - pc->timeout_task = NULL; - } - GNUNET_assert (GNUNET_YES == pc->suspended); - pc->suspended = GNUNET_NO; - MHD_resume_connection (pc->connection); - TMH_trigger_daemon (); /* we resumed, kick MHD */ -} - - -/** - * Resume payment processing with an error. - * - * @param pc operation to resume - * @param http_status http status code to return - * @param ec taler error code to return - * @param msg human readable error message - */ -static void -resume_pay_with_error (struct PayContext *pc, - unsigned int http_status, - enum TALER_ErrorCode ec, - const char *msg) -{ - resume_pay_with_response (pc, - http_status, - TALER_MHD_make_error (ec, - msg)); -} - - -/** - * Generate a response that indicates payment success. - * - * @param pc payment context - */ -static void -generate_success_response (struct PayContext *pc) -{ - json_t *refunds; - struct GNUNET_CRYPTO_EddsaSignature sig; - - /* Check for applicable 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, - 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); - } - - /* Build the response */ - { - 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) - { - GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_JSON_ALLOCATION_FAILURE, - "could not build final response"); - 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; icoins_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); -} - - -/** - * Check whether the amount paid is sufficient to cover - * the contract. - * - * @param pc payment context to check - * @return #GNUNET_OK if the payment is sufficient, #GNUNET_SYSERR if it is - * insufficient - */ -static int -check_payment_sufficient (struct PayContext *pc) -{ - struct TALER_Amount acc_fee; - struct TALER_Amount acc_amount; - struct TALER_Amount final_amount; - struct TALER_Amount wire_fee_delta; - struct TALER_Amount wire_fee_customer_contribution; - struct TALER_Amount total_wire_fee; - struct TALER_Amount total_needed; - - if (0 == pc->coins_cnt) - { - 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; icoins_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; - - for (unsigned int j = 0; jexchange_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)) - { - 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; - } - - switch (TALER_amount_subtract (&wire_fee_delta, - &total_wire_fee, - &pc->max_wire_fee)) - { - case TALER_AAR_RESULT_POSITIVE: - /* Actual wire fee is indeed higher than our maximum, - compute how much the customer is expected to cover! */ - TALER_amount_divide (&wire_fee_customer_contribution, - &wire_fee_delta, - pc->wire_fee_amortization); - break; - case TALER_AAR_RESULT_ZERO: - case TALER_AAR_INVALID_NEGATIVE_RESULT: - /* Wire fee threshold is still above the wire fee amount. - Customer is not going to contribute on this. */ - GNUNET_assert (GNUNET_OK == - TALER_amount_get_zero (total_wire_fee.currency, - &wire_fee_customer_contribution)); - break; - default: - GNUNET_assert (0); - } - - /* add wire fee contribution to the total fees */ - if (0 > - TALER_amount_add (&acc_fee, - &acc_fee, - &wire_fee_customer_contribution)) - { - GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_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)) - { - 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; - } - } - else - { - /* Fees are fully covered by the merchant, all we require - is that the total payment is not below the contract's amount */ - total_needed = pc->amount; - } - - /* Do not count refunds towards the payment */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Subtracting total refunds from paid amount: %s\n", - TALER_amount2s (&pc->total_refunded)); - if (0 > - TALER_amount_subtract (&final_amount, - &acc_amount, - &pc->total_refunded)) - { - GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_REFUNDS_EXCEED_PAYMENTS, - "refunded amount exceeds total payments"); - return GNUNET_SYSERR; - } - - if (-1 == TALER_amount_cmp (&final_amount, - &total_needed)) - { - /* 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; - } - - - return GNUNET_OK; -} - - -/** - * Find the exchange we need to talk to for the next - * pending deposit permission. - * - * @param pc payment context we are processing - */ -static void -find_next_exchange (struct PayContext *pc); - - -/** - * Begin of the DB transaction. If required (from - * soft/serialization errors), the transaction can be - * restarted here. - * - * @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 - */ -static void -deposit_cb (void *cls, - const struct TALER_EXCHANGE_HttpResponse *hr, - const struct TALER_ExchangeSignatureP *exchange_sig, - const struct TALER_ExchangePublicKeyP *sign_key) -{ - 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); -} - - -/** - * Function called with the result of our exchange lookup. - * - * @param cls the `struct PayContext` - * @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, - * 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) -{ - struct PayContext *pc = cls; - const struct TALER_EXCHANGE_Keys *keys; - - pc->fo = NULL; - GNUNET_assert (GNUNET_YES == pc->suspended); - if (MHD_HTTP_OK != hr->http_status) - { - /* The request failed somehow */ - GNUNET_break_op (0); - resume_pay_with_response ( - pc, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ( - (NULL != hr->reply) - ? "{s:s, s:I, s:I, s:I, s:O}" - : "{s:s, s:I, s:I, s:I}", - "hint", - "failed to obtain meta-data from exchange", - "code", - (json_int_t) TALER_EC_PAY_EXCHANGE_KEYS_FAILURE, - "exchange_http_status", - (json_int_t) hr->http_status, - "exchange_code", - (json_int_t) hr->ec, - "exchange_reply", - 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 - the current exchange (!) */ - GNUNET_assert (0 == pc->pending_at_ce); - for (unsigned int i = 0; icoins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->dc[i]; - const struct TALER_EXCHANGE_DenomPublicKey *denom_details; - enum TALER_ErrorCode ec; - unsigned int 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) - continue; - if (0 != strcmp (dc->exchange_url, - pc->current_exchange)) - continue; - denom_details = TALER_EXCHANGE_get_denomination_key (keys, - &dc->denom); - if (NULL == denom_details) - { - 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)); - return; - } - if (TMH_force_audit) - TALER_EXCHANGE_deposit_force_dc (dc->dh); - pc->pending_at_ce++; - } -} - - -/** - * Find the exchange we need to talk to for the next - * pending deposit permission. - * - * @param pc payment context we are processing - */ -static void -find_next_exchange (struct PayContext *pc) -{ - for (unsigned int i = 0; icoins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->dc[i]; - - if (GNUNET_YES != dc->found_in_db) - { - db->preflight (db->cls); - pc->current_exchange = dc->exchange_url; - pc->fo = TMH_EXCHANGES_find_exchange (pc->current_exchange, - pc->wm->wire_method, - GNUNET_NO, - &process_pay_with_exchange, - pc); - if (NULL == pc->fo) - { - GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_EXCHANGE_LOOKUP_FAILED, - "Failed to lookup exchange by URL"); - return; - } - return; - } - } - pc->current_exchange = NULL; - db->preflight (db->cls); - /* 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"); -} - - -/** - * Function called with information about a coin that was deposited. - * - * @param cls closure - * @param h_contract_terms hashed proposal data - * @param coin_pub public key of the coin - * @param exchange_url URL of the exchange that issued @a coin_pub - * @param amount_with_fee amount the exchange will deposit for this coin - * @param deposit_fee fee the exchange will charge for this coin - * @param refund_fee fee the exchange will charge for refunding this coin - * @param wire_fee wire fee the exchange of this coin charges - * @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; icoins_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) -{ - 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)); - - if (NULL == json_object_get (pc->contract_terms, - "merchant")) - { - /* 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 () - }; - - 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; - } - - 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) - { - 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) - { - GNUNET_JSON_parse_free (spec); - GNUNET_break_op (0); - return res; - } - 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; icoins_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)); - } - } -} - - -/** - * Begin of the DB transaction. If required (from - * soft/serialization errors), the transaction can be - * restarted here. - * - * @param pc payment context to transact - */ -static void -begin_transaction (struct PayContext *pc) -{ - enum GNUNET_DB_QueryStatus qs; - - /* Avoid re-trying transactions on soft errors forever! */ - if (pc->retry_counter++ > MAX_RETRIES) - { - GNUNET_break (0); - resume_pay_with_error (pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_PAY_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)); - - /* First, try to see if we have all we need already done */ - db->preflight (db->cls); - if (GNUNET_OK != - db->start (db->cls, - "run pay")) - { - 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)"); - 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); - 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_FETCH_TRANSACTION_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); - 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_FETCH_TRANSACTION_ERROR, - "Merchant database error checking for refunds"); - 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) - { - 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) - { - /* 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"); - 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) - { - 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; - } - qs = db->commit (db->cls); - if (0 > 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_PAY_ERROR, - "Merchant database error: could not commit"); - return; - } - /* At this point, the refund got correctly committed - * into the database. */ - { - json_t *refunds; - - refunds = json_array (); - if (NULL == 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; - } - for (unsigned int i = 0; icoins_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) - }; - - 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)))) - { - 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; - } - } - - /* 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 */ - - /* Final termination case: all coins already known, just - generate ultimate outcome. */ - if (0 == pc->pending) - { - if (GNUNET_OK != check_payment_sufficient (pc)) - { - db->rollback (db->cls); - return; - } - /* 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) - { - 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; - } - - if ( (NULL != pc->session_id) && - (NULL != pc->fulfillment_url) ) - { - qs = db->insert_session_info (db->cls, - pc->session_id, - pc->fulfillment_url, - pc->order_id, - &pc->mi->pubkey); - } - - /* Now commit! */ - if (0 <= qs) - qs = db->commit (db->cls); - else - db->rollback (db->cls); - if (0 > qs) - { - 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 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); -} - - -/** - * Process a payment for a proposal. - * - * @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) - */ -static MHD_RESULT -handler_pay_json (struct MHD_Connection *connection, - const json_t *root, - struct PayContext *pc) -{ - { - enum GNUNET_GenericReturnValue ret; - - ret = parse_pay (connection, - root, - 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"); - pc->timeout_task = GNUNET_SCHEDULER_add_delayed (PAY_TIMEOUT, - &handle_pay_timeout, - pc); - begin_transaction (pc); - return MHD_YES; -} - - -/** - * Process a payment for a proposal. Takes data from the given MHD - * connection. - * - * @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 - * @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) -{ - struct PayContext *pc; - enum GNUNET_GenericReturnValue res; - MHD_RESULT ret; - json_t *root; - - 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; - } - if (GNUNET_SYSERR == pc->suspended) - return MHD_NO; /* during shutdown, we don't generate any more replies */ - if (0 != pc->response_code) - { - /* We are *done* processing the request, - just queue the response (!) */ - if (UINT_MAX == pc->response_code) - { - GNUNET_break (0); - return MHD_NO; /* hard error */ - } - res = MHD_queue_response (connection, - pc->response_code, - pc->response); - MHD_destroy_response (pc->response); - pc->response = NULL; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Queueing response (%u) for /pay (%s).\n", - (unsigned int) pc->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"); - } - 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; -} - - -/* end of taler-merchant-httpd_pay.c */ diff --git a/src/backend/taler-merchant-httpd_post-tips-ID-pickup.c b/src/backend/taler-merchant-httpd_post-tips-ID-pickup.c new file mode 100644 index 00000000..42066e3c --- /dev/null +++ b/src/backend/taler-merchant-httpd_post-tips-ID-pickup.c @@ -0,0 +1,146 @@ +/* + This file is part of TALER + (C) 2017-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_tip-pickup.c + * @brief implementation of /tip-pickup handler + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include +#include +#include +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_mhd.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_tip-pickup.h" + + +/** + * Manages a GET /tip-pickup call, checking that the tip is authorized, + * and if so, returning the withdrawal permissions. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_tip_pickup_get (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi) +{ + const char *tip_id_str; + char *exchange_url; + json_t *extra; + struct GNUNET_HashCode tip_id; + struct TALER_Amount tip_amount; + struct TALER_Amount tip_amount_left; + struct GNUNET_TIME_Absolute timestamp; + struct GNUNET_TIME_Absolute timestamp_expire; + MHD_RESULT ret; + enum GNUNET_DB_QueryStatus qs; + + tip_id_str = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "tip_id"); + + if (NULL == tip_id_str) + { + /* tip_id is required but missing */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "tip_id required"); + } + + if (GNUNET_OK != + GNUNET_CRYPTO_hash_from_string (tip_id_str, + &tip_id)) + { + /* tip_id has wrong encoding */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MALFORMED, + "tip_id malformed"); + } + + db->preflight (db->cls); + qs = db->lookup_tip_by_id (db->cls, + &tip_id, + &exchange_url, + &extra, + &tip_amount, + &tip_amount_left, + ×tamp); + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + unsigned int response_code; + enum TALER_ErrorCode ec; + + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + ec = TALER_EC_TIP_PICKUP_TIP_ID_UNKNOWN; + response_code = MHD_HTTP_NOT_FOUND; + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + ec = TALER_EC_TIP_PICKUP_DB_ERROR_SOFT; + response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + break; + case GNUNET_DB_STATUS_HARD_ERROR: + ec = TALER_EC_TIP_PICKUP_DB_ERROR_HARD; + response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + break; + default: + GNUNET_break (0); + ec = TALER_EC_INTERNAL_LOGIC_ERROR; + response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + break; + } + return TALER_MHD_reply_with_error (connection, + response_code, + ec, + "Could not determine exchange URL for the given tip id"); + } + + timestamp_expire = GNUNET_TIME_absolute_add (timestamp, + GNUNET_TIME_UNIT_DAYS); + + ret = TALER_MHD_reply_json_pack ( + connection, + MHD_HTTP_OK, + "{s:s, s:o, s:o, s:o, s:o, s:o}", + "exchange_url", exchange_url, + "amount", TALER_JSON_from_amount (&tip_amount), + "amount_left", TALER_JSON_from_amount (&tip_amount_left), + "stamp_created", GNUNET_JSON_from_time_abs (timestamp), + "stamp_expire", GNUNET_JSON_from_time_abs (timestamp_expire), + "extra", extra); + + GNUNET_free (exchange_url); + json_decref (extra); + return ret; +} diff --git a/src/backend/taler-merchant-httpd_post-tips-ID-pickup.h b/src/backend/taler-merchant-httpd_post-tips-ID-pickup.h new file mode 100644 index 00000000..6fdba31a --- /dev/null +++ b/src/backend/taler-merchant-httpd_post-tips-ID-pickup.h @@ -0,0 +1,75 @@ +/* + This file is part of TALER + (C) 2017 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_tip-pickup.h + * @brief headers for /tip-pickup handler + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_TIP_PICKUP_H +#define TALER_MERCHANT_HTTPD_TIP_PICKUP_H +#include +#include "taler-merchant-httpd.h" + + +/** + * We are shutting down, force resuming all suspended pickup operations. + */ +void +MH_force_tip_pickup_resume (void); + + +/** + * Manages a POST /tip-pickup call, checking that the tip is authorized, + * and if so, returning the withdrawal permissions. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_tip_pickup (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi); + + +/** + * Manages a GET /tip-pickup call. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_tip_pickup_get (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi); + + +#endif diff --git a/src/backend/taler-merchant-httpd_post-tips.c b/src/backend/taler-merchant-httpd_post-tips.c deleted file mode 100644 index 569cf0ab..00000000 --- a/src/backend/taler-merchant-httpd_post-tips.c +++ /dev/null @@ -1,306 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2017 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_tip-authorize.c - * @brief implement API for authorizing tips to be paid to visitors - * @author Christian Grothoff - */ -#include "platform.h" -#include -#include -#include -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_mhd.h" -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_tip-authorize.h" -#include "taler-merchant-httpd_tip-reserve-helper.h" - - -struct TipAuthContext -{ - /** - * This field MUST be first for handle_mhd_completion_callback() to work - * when it treats this struct as a `struct TM_HandlerContext`. - */ - struct TM_HandlerContext hc; - - /** - * Placeholder for #TALER_MHD_parse_post_json() to keep its internal state. - */ - void *json_parse_context; - - /** - * Justification to use. - */ - const char *justification; - - /** - * JSON request received. - */ - json_t *root; - - /** - * Context for checking the tipping reserve's status. - */ - struct TMH_CheckTipReserve ctr; - - /** - * Tip amount requested. - */ - struct TALER_Amount amount; - - /** - * Flag set to #GNUNET_YES when we have tried /reserve/status of the - * tipping reserve already. - */ - int checked_status; - - /** - * Flag set to #GNUNET_YES when we have parsed the incoming JSON already. - */ - int parsed_json; - -}; - - -/** - * Custom cleanup routine for a `struct TipAuthContext`. - * - * @param hc the `struct TMH_JsonParseContext` to clean up. - */ -static void -cleanup_tac (struct TM_HandlerContext *hc) -{ - struct TipAuthContext *tac = (struct TipAuthContext *) hc; - - if (NULL != tac->root) - { - json_decref (tac->root); - tac->root = NULL; - } - TMH_check_tip_reserve_cleanup (&tac->ctr); - TALER_MHD_parse_post_cleanup_callback (tac->json_parse_context); - GNUNET_free (tac); -} - - -/** - * Handle a "/tip-authorize" request. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_tip_authorize (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi) -{ - struct TipAuthContext *tac; - enum GNUNET_GenericReturnValue res; - enum TALER_ErrorCode ec; - struct GNUNET_TIME_Absolute expiration; - struct GNUNET_HashCode tip_id; - json_t *extra; - - if (NULL == *connection_cls) - { - tac = GNUNET_new (struct TipAuthContext); - tac->hc.cc = &cleanup_tac; - tac->ctr.connection = connection; - *connection_cls = tac; - } - else - { - tac = *connection_cls; - } - if (NULL != tac->ctr.response) - { - MHD_RESULT ret; - - ret = MHD_queue_response (connection, - tac->ctr.response_code, - tac->ctr.response); - MHD_destroy_response (tac->ctr.response); - tac->ctr.response = NULL; - return ret; - } - if (GNUNET_NO == tac->parsed_json) - { - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount ("amount", &tac->amount), - GNUNET_JSON_spec_string ("justification", &tac->justification), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_post_json (connection, - &tac->json_parse_context, - upload_data, - upload_data_size, - &tac->root); - if (GNUNET_SYSERR == res) - return MHD_NO; - /* the POST's body has to be further fetched */ - if ( (GNUNET_NO == res) || - (NULL == tac->root) ) - return MHD_YES; - - res = TALER_MHD_parse_json_data (connection, - tac->root, - spec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); - return (GNUNET_NO == res) ? MHD_YES : MHD_NO; - } - tac->parsed_json = GNUNET_YES; - } - - if (NULL == mi->tip_exchange) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Instance `%s' not configured for tipping\n", - mi->id); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_PRECONDITION_FAILED, - TALER_EC_TIP_AUTHORIZE_INSTANCE_DOES_NOT_TIP, - "exchange for tipping not configured for the instance"); - } - tac->ctr.reserve_priv = mi->tip_reserve; - extra = json_object_get (tac->root, "extra"); - if (NULL == extra) - extra = json_object (); - else - json_incref (extra); - - - db->preflight (db->cls); - ec = db->authorize_tip_TR (db->cls, - tac->justification, - extra, - &tac->amount, - &mi->tip_reserve, - mi->tip_exchange, - &expiration, - &tip_id); - json_decref (extra); - /* If we have insufficient funds according to OUR database, - check with exchange to see if the reserve has been topped up - in the meantime (or if tips were not withdrawn yet). */ - if ( (TALER_EC_TIP_AUTHORIZE_INSUFFICIENT_FUNDS == ec) && - (GNUNET_NO == tac->checked_status) ) - { - tac->checked_status = GNUNET_YES; - tac->ctr.none_authorized = GNUNET_YES; - TMH_check_tip_reserve (&tac->ctr, - mi->tip_exchange); - return MHD_YES; - } - - /* handle irrecoverable errors */ - if (TALER_EC_NONE != ec) - { - unsigned int rc; - const char *msg; - - switch (ec) - { - case TALER_EC_TIP_AUTHORIZE_INSUFFICIENT_FUNDS: - rc = MHD_HTTP_PRECONDITION_FAILED; - msg = "Failed to approve tip: merchant has insufficient tipping funds"; - break; - case TALER_EC_TIP_AUTHORIZE_RESERVE_EXPIRED: - msg = "Failed to approve tip: merchant's tipping reserve expired"; - rc = MHD_HTTP_PRECONDITION_FAILED; - break; - case TALER_EC_TIP_AUTHORIZE_RESERVE_UNKNOWN: - msg = "Failed to approve tip: merchant's tipping reserve does not exist"; - rc = MHD_HTTP_SERVICE_UNAVAILABLE; - break; - default: - rc = MHD_HTTP_INTERNAL_SERVER_ERROR; - msg = "Failed to approve tip: internal server error"; - break; - } - - return TALER_MHD_reply_with_error (connection, - rc, - ec, - msg); - } - - /* generate success response */ - { - char *taler_tip_uri; - const char *host; - const char *forwarded_host; - const char *uri_path; - const char *uri_instance_id; - struct GNUNET_CRYPTO_HashAsciiEncoded hash_enc; - - host = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, "Host"); - forwarded_host = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, - "X-Forwarded-Host"); - - uri_path = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, - "X-Forwarded-Prefix"); - if (NULL == uri_path) - uri_path = "-"; - - if (NULL != forwarded_host) - host = forwarded_host; - - if (NULL == host) - { - /* Should never happen, at last the host header should be defined */ - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_INTERNAL_INVARIANT_FAILURE, - "unable to identify backend host"); - } - - if (0 == strcmp (mi->id, "default")) - uri_instance_id = "-"; - else - uri_instance_id = mi->id; - - GNUNET_CRYPTO_hash_to_enc (&tip_id, &hash_enc); - - GNUNET_assert (0 < GNUNET_asprintf (&taler_tip_uri, - "taler://tip/%s/%s/%s/%s", - host, - uri_path, - uri_instance_id, - hash_enc.encoding)); - - return TALER_MHD_reply_json_pack (connection, - MHD_HTTP_OK, - "{s:s, s:s}", - "taler_tip_uri", taler_tip_uri, - "tip_id", hash_enc.encoding); - } -} - - -/* end of taler-merchant-httpd_tip-authorize.c */ diff --git a/src/backend/taler-merchant-httpd_post-transfers.c b/src/backend/taler-merchant-httpd_post-transfers.c deleted file mode 100644 index 7f55c917..00000000 --- a/src/backend/taler-merchant-httpd_post-transfers.c +++ /dev/null @@ -1,1089 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2020 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_track-transfer.c - * @brief implement API for tracking transfers and wire transfers - * @author Marcello Stanisci - * @author Christian Grothoff - */ -#include "platform.h" -#include -#include -#include -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_mhd.h" -#include "taler-merchant-httpd_auditors.h" -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_track-transfer.h" - - -/** - * How long to wait before giving up processing with the exchange? - */ -#define TRACK_TIMEOUT (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, \ - 30)) - -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - -/** - * Context used for handing /track/transfer requests. - */ -struct TrackTransferContext -{ - - /** - * This MUST be first! - */ - struct TM_HandlerContext hc; - - /** - * Handle to the exchange. - */ - struct TALER_EXCHANGE_Handle *eh; - - /** - * Handle for the /wire/transfers request. - */ - struct TALER_EXCHANGE_TransfersGetHandle *wdh; - - /** - * For which merchant instance is this tracking request? - */ - struct MerchantInstance *mi; - - /** - * HTTP connection we are handling. - */ - struct MHD_Connection *connection; - - /** - * Response to return upon resume. - */ - struct MHD_Response *response; - - /** - * Handle for operation to lookup /keys (and auditors) from - * the exchange used for this transaction; NULL if no operation is - * pending. - */ - struct TMH_EXCHANGES_FindOperation *fo; - - /** - * Task run on timeout. - */ - struct GNUNET_SCHEDULER_Task *timeout_task; - - /** - * URL of the exchange. - */ - char *url; - - /** - * Wire method used for the transfer. - */ - char *wire_method; - - /** - * Pointer to the detail that we are currently - * checking in #check_transfer(). - */ - const struct TALER_TrackTransferDetails *current_detail; - - /** - * Argument for the /wire/transfers request. - */ - struct TALER_WireTransferIdentifierRawP wtid; - - /** - * Full original response we are currently processing. - */ - const json_t *original_response; - - /** - * Modified response to return to the frontend. - */ - json_t *deposits_response; - - /** - * Which transaction detail are we currently looking at? - */ - unsigned int current_offset; - - /** - * Response code to return. - */ - unsigned int response_code; - - /** - * #GNUNET_NO if we did not find a matching coin. - * #GNUNET_SYSERR if we found a matching coin, but the amounts do not match. - * #GNUNET_OK if we did find a matching coin. - */ - int check_transfer_result; -}; - - -/** - * Represents an entry in the table used to sum up - * individual deposits for each h_contract_terms. - */ -struct Entry -{ - - /** - * Sum accumulator for deposited value. - */ - struct TALER_Amount deposit_value; - - /** - * Sum accumulator for deposit fee. - */ - struct TALER_Amount deposit_fee; - -}; - - -/** - * Free the @a rctx. - * - * @param rctx data to free - */ -static void -free_transfer_track_context (struct TrackTransferContext *rctx) -{ - if (NULL != rctx->fo) - { - TMH_EXCHANGES_find_exchange_cancel (rctx->fo); - rctx->fo = NULL; - } - if (NULL != rctx->timeout_task) - { - GNUNET_SCHEDULER_cancel (rctx->timeout_task); - rctx->timeout_task = NULL; - } - if (NULL != rctx->wdh) - { - TALER_EXCHANGE_transfers_get_cancel (rctx->wdh); - rctx->wdh = NULL; - } - if (NULL != rctx->url) - { - GNUNET_free (rctx->url); - rctx->url = NULL; - } - if (NULL != rctx->wire_method) - { - GNUNET_free (rctx->wire_method); - rctx->wire_method = NULL; - } - GNUNET_free (rctx); -} - - -/** - * Callback that frees all the elements in the hashmap - * - * @param cls closure, NULL - * @param key current key - * @param value a `struct Entry` - * @return #GNUNET_YES if the iteration should continue, - * #GNUNET_NO otherwise. - */ -static int -hashmap_free (void *cls, - const struct GNUNET_HashCode *key, - void *value) -{ - struct TALER_Entry *entry = value; - - (void) cls; - (void) key; - GNUNET_free (entry); - return GNUNET_YES; -} - - -/** - * Builds JSON response containing the summed-up amounts - * from individual deposits. - * - * @param cls closure - * @param key map's current key - * @param map's current value - * @return #GNUNET_YES if iteration is to be continued, - * #GNUNET_NO otherwise. - */ -static int -build_deposits_response (void *cls, - const struct GNUNET_HashCode *key, - void *value) -{ - struct TrackTransferContext *rctx = cls; - struct Entry *entry = value; - json_t *element; - json_t *contract_terms; - json_t *order_id; - - db->preflight (db->cls); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != - db->find_contract_terms_from_hash (db->cls, - &contract_terms, - key, - &rctx->mi->pubkey)) - { - GNUNET_break_op (0); - return GNUNET_NO; - } - - order_id = json_object_get (contract_terms, - "order_id"); - if (NULL == order_id) - { - GNUNET_break_op (0); - json_decref (contract_terms); - return GNUNET_NO; - } - element = json_pack ("{s:O, s:o, s:o}", - "order_id", order_id, - "deposit_value", TALER_JSON_from_amount ( - &entry->deposit_value), - "deposit_fee", TALER_JSON_from_amount ( - &entry->deposit_fee)); - json_decref (contract_terms); - if (NULL == element) - { - GNUNET_break_op (0); - return GNUNET_NO; - } - GNUNET_break (0 == - json_array_append_new (rctx->deposits_response, - element)); - return GNUNET_YES; -} - - -/** - * Transform /track/transfer result as gotten from the exchange - * and transforms it in a format liked by the backoffice Web interface. - * - * @param result response from exchange's /track/transfer - * @result pointer to new JSON, or NULL upon errors. - */ -static json_t * -transform_response (const json_t *result, - struct TrackTransferContext *rctx) -{ - json_t *deposits; - json_t *value; - json_t *result_mod = NULL; - size_t index; - const char *key; - struct GNUNET_HashCode h_key; - struct GNUNET_CONTAINER_MultiHashMap *map; - struct TALER_Amount iter_value; - struct TALER_Amount iter_fee; - struct Entry *current_entry; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount ("deposit_value", &iter_value), - TALER_JSON_spec_amount ("deposit_fee", &iter_fee), - GNUNET_JSON_spec_string ("h_contract_terms", &key), - GNUNET_JSON_spec_end () - }; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Transforming /track/transfer response.\n"); - map = GNUNET_CONTAINER_multihashmap_create (1, GNUNET_NO); - deposits = json_object_get (result, - "deposits"); - - json_array_foreach (deposits, index, value) - { - if (GNUNET_OK != - GNUNET_JSON_parse (value, - spec, - NULL, - NULL)) - { - GNUNET_break_op (0); - return NULL; - } - GNUNET_CRYPTO_hash_from_string (key, - &h_key); - - if (NULL != (current_entry = - GNUNET_CONTAINER_multihashmap_get (map, - &h_key))) - { - /* The map already knows this h_contract_terms*/ - if ( (0 > - TALER_amount_add (¤t_entry->deposit_value, - ¤t_entry->deposit_value, - &iter_value)) || - (0 > - TALER_amount_add (¤t_entry->deposit_fee, - ¤t_entry->deposit_fee, - &iter_fee)) ) - { - GNUNET_JSON_parse_free (spec); - goto cleanup; - } - } - else - { - /* First time in the map for this h_contract_terms*/ - current_entry = GNUNET_new (struct Entry); - current_entry->deposit_value = iter_value; - current_entry->deposit_fee = iter_fee; - - if (GNUNET_SYSERR == - GNUNET_CONTAINER_multihashmap_put (map, - &h_key, - current_entry, - GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)) - { - GNUNET_JSON_parse_free (spec); - goto cleanup; - } - } - GNUNET_JSON_parse_free (spec); - } - rctx->deposits_response = json_array (); - - if (GNUNET_SYSERR == - GNUNET_CONTAINER_multihashmap_iterate (map, - &build_deposits_response, - rctx)) - goto cleanup; - - result_mod = json_copy ((struct json_t *) result); - json_object_del (result_mod, - "deposits"); - json_object_set_new (result_mod, - "deposits_sums", - rctx->deposits_response); - rctx->deposits_response = NULL; -cleanup: - GNUNET_CONTAINER_multihashmap_iterate (map, - &hashmap_free, - NULL); - GNUNET_CONTAINER_multihashmap_destroy (map); - return result_mod; -} - - -/** - * Resume the given /track/transfer operation and send the given response. - * Stores the response in the @a rctx and signals MHD to resume - * the connection. Also ensures MHD runs immediately. - * - * @param rctx transfer tracking context - * @param response_code response code to use - * @param response response data to send back - */ -static void -resume_track_transfer_with_response (struct TrackTransferContext *rctx, - unsigned int response_code, - struct MHD_Response *response) -{ - rctx->response_code = response_code; - rctx->response = response; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Resuming /track/transfer handling as exchange interaction is done (%u)\n", - response_code); - if (NULL != rctx->timeout_task) - { - GNUNET_SCHEDULER_cancel (rctx->timeout_task); - rctx->timeout_task = NULL; - } - MHD_resume_connection (rctx->connection); - TMH_trigger_daemon (); /* we resumed, kick MHD */ -} - - -/** - * Custom cleanup routine for a `struct TrackTransferContext`. - * - * @param hc the `struct TrackTransferContext` to clean up. - */ -static void -track_transfer_cleanup (struct TM_HandlerContext *hc) -{ - struct TrackTransferContext *rctx = (struct TrackTransferContext *) hc; - - free_transfer_track_context (rctx); -} - - -/** - * This function checks that the information about the coin which - * was paid back by _this_ wire transfer matches what _we_ (the merchant) - * knew about this coin. - * - * @param cls closure with our `struct TrackTransferContext *` - * @param transaction_id of the contract - * @param coin_pub public key of the coin - * @param exchange_url URL of the exchange that issued @a coin_pub - * @param amount_with_fee amount the exchange will transfer 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 exchange_proof proof from exchange that coin was accepted - */ -static void -check_transfer (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 TrackTransferContext *rctx = cls; - const struct TALER_TrackTransferDetails *ttd = rctx->current_detail; - - if (GNUNET_SYSERR == rctx->check_transfer_result) - return; /* already had a serious issue; odd that we're called more than once as well... */ - if ( (0 != TALER_amount_cmp (amount_with_fee, - &ttd->coin_value)) || - (0 != TALER_amount_cmp (deposit_fee, - &ttd->coin_fee)) ) - { - /* Disagreement between the exchange and us about how much this - coin is worth! */ - GNUNET_break_op (0); - rctx->check_transfer_result = GNUNET_SYSERR; - /* Build the `TrackTransferConflictDetails` */ - rctx->response - = TALER_MHD_make_json_pack ( - "{s:I, s:s, s:o, s:I, s:o, s:o, s:s, s:o, s:o}", - "code", (json_int_t) TALER_EC_TRACK_TRANSFER_CONFLICTING_REPORTS, - "hint", "disagreement about deposit valuation", - "exchange_deposit_proof", exchange_proof, - "conflict_offset", (json_int_t) rctx->current_offset, - "exchange_transfer_proof", rctx->original_response, - "coin_pub", GNUNET_JSON_from_data_auto (coin_pub), - "h_contract_terms", GNUNET_JSON_from_data_auto ( - &ttd->h_contract_terms), - "amount_with_fee", TALER_JSON_from_amount (amount_with_fee), - "deposit_fee", TALER_JSON_from_amount (deposit_fee)); - return; - } - rctx->check_transfer_result = GNUNET_OK; -} - - -/** - * Check that the given @a wire_fee is what the - * @a exchange_pub should charge at the @a execution_time. - * If the fee is correct (according to our database), - * return #GNUNET_OK. If we do not have the fee structure - * in our DB, we just accept it and return #GNUNET_NO; - * if we have proof that the fee is bogus, we respond with - * the proof to the client and return #GNUNET_SYSERR. - * - * @param rctx context of the transfer to respond to - * @param json response from the exchange - * @param execution_time time of the wire transfer - * @param wire_fee fee claimed by the exchange - * @return #GNUNET_SYSERR if we returned hard proof of - * missbehavior from the exchange to the client - */ -static int -check_wire_fee (struct TrackTransferContext *rctx, - const json_t *json, - struct GNUNET_TIME_Absolute execution_time, - const struct TALER_Amount *wire_fee) -{ - const struct TALER_MasterPublicKeyP *master_pub; - struct GNUNET_HashCode h_wire_method; - struct TALER_Amount expected_fee; - struct TALER_Amount closing_fee; - struct TALER_MasterSignatureP master_sig; - struct GNUNET_TIME_Absolute start_date; - struct GNUNET_TIME_Absolute end_date; - enum GNUNET_DB_QueryStatus qs; - const struct TALER_EXCHANGE_Keys *keys; - - keys = TALER_EXCHANGE_get_keys (rctx->eh); - if (NULL == keys) - { - GNUNET_break (0); - return GNUNET_NO; - } - master_pub = &keys->master_pub; - GNUNET_CRYPTO_hash (rctx->wire_method, - strlen (rctx->wire_method) + 1, - &h_wire_method); - db->preflight (db->cls); - qs = db->lookup_wire_fee (db->cls, - master_pub, - &h_wire_method, - execution_time, - &expected_fee, - &closing_fee, - &start_date, - &end_date, - &master_sig); - if (0 >= qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to find wire fee for `%s' and method `%s' at %s in DB, accepting blindly that the fee is %s\n", - TALER_B2S (master_pub), - rctx->wire_method, - GNUNET_STRINGS_absolute_time_to_string (execution_time), - TALER_amount2s (wire_fee)); - return GNUNET_NO; - } - if (0 <= TALER_amount_cmp (&expected_fee, - wire_fee)) - return GNUNET_OK; /* expected_fee >= wire_fee */ - - /* Wire fee check failed, export proof to client */ - resume_track_transfer_with_response ( - rctx, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ( - "{s:I, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:O}", - "code", (json_int_t) TALER_EC_TRACK_TRANSFER_JSON_BAD_WIRE_FEE, - "wire_fee", TALER_JSON_from_amount (wire_fee), - "execution_time", GNUNET_JSON_from_time_abs (execution_time), - "expected_wire_fee", TALER_JSON_from_amount (&expected_fee), - "expected_closing_fee", TALER_JSON_from_amount (&closing_fee), - "start_date", GNUNET_JSON_from_time_abs (start_date), - "end_date", GNUNET_JSON_from_time_abs (end_date), - "master_sig", GNUNET_JSON_from_data_auto (&master_sig), - "master_pub", GNUNET_JSON_from_data_auto (master_pub), - "json", json)); - return GNUNET_SYSERR; -} - - -/** - * Function called with detailed wire transfer data, including all - * of the coin transactions that were combined into the wire transfer. - * - * @param cls closure - * @param hr HTTP response details - * @param exchange_pub public key of the exchange used to sign @a json - * @param h_wire hash of the wire transfer address the transfer went to, or NULL on error - * @param execution_time time when the exchange claims to have performed the wire transfer - * @param total_amount total amount of the wire transfer, or NULL if the exchange could - * not provide any @a wtid (set only if @a http_status is #MHD_HTTP_OK) - * @param wire_fee wire fee that was charged by the exchange - * @param details_length length of the @a details array - * @param details array with details about the combined transactions - */ -static void -wire_transfer_cb (void *cls, - const struct TALER_EXCHANGE_HttpResponse *hr, - const struct TALER_ExchangePublicKeyP *exchange_pub, - const struct GNUNET_HashCode *h_wire, - struct GNUNET_TIME_Absolute execution_time, - const struct TALER_Amount *total_amount, - const struct TALER_Amount *wire_fee, - unsigned int details_length, - const struct TALER_TrackTransferDetails *details) -{ - struct TrackTransferContext *rctx = cls; - json_t *jresponse; - enum GNUNET_DB_QueryStatus qs; - - rctx->wdh = NULL; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Got response code %u from exchange for /track/transfer\n", - hr->http_status); - if (MHD_HTTP_OK != hr->http_status) - { - resume_track_transfer_with_response ( - rctx, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ( - "{s:I, s:I, s:I, s:O}", - "code", (json_int_t) TALER_EC_TRACK_TRANSFER_EXCHANGE_ERROR, - "exchange_code", (json_int_t) hr->ec, - "exchange_http_status", (json_int_t) hr->http_status, - "exchange_reply", hr->reply)); - return; - } - for (unsigned int i = 0; ipreflight (db->cls); - qs = db->store_transfer_to_proof (db->cls, - rctx->url, - &rctx->wtid, - execution_time, - exchange_pub, - hr->reply); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - if (0 > qs) - { - /* Special report if retries insufficient */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - resume_track_transfer_with_response - (rctx, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_MHD_make_json_pack ("{s:I, s:s}", - "code", - (json_int_t) - TALER_EC_TRACK_TRANSFER_DB_STORE_TRANSFER_ERROR, - "details", - "failed to store response from exchange to local database")); - return; - } - rctx->original_response = hr->reply; - - if (GNUNET_SYSERR == - check_wire_fee (rctx, - hr->reply, - execution_time, - wire_fee)) - return; - - /* Now we want to double-check that any (Taler coin) deposit - * which is accounted into _this_ wire transfer, does exist - * into _our_ database. This is the rationale: if the - * exchange paid us for it, we must have received it _beforehands_! - * - * details_length is how many (Taler coin) deposits have been - * aggregated into _this_ wire transfer. - */// - for (unsigned int i = 0; icurrent_offset = i; - rctx->current_detail = &details[i]; - /* Set the coin as "never seen" before. */ - rctx->check_transfer_result = GNUNET_NO; - db->preflight (db->cls); - qs = db->find_payments_by_hash_and_coin (db->cls, - &details[i].h_contract_terms, - &rctx->mi->pubkey, - &details[i].coin_pub, - &check_transfer, - rctx); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - resume_track_transfer_with_response - (rctx, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_MHD_make_json_pack ("{s:I, s:s}", - "code", - (json_int_t) - TALER_EC_TRACK_TRANSFER_DB_FETCH_DEPOSIT_ERROR, - "details", - "failed to obtain deposit data from local database")); - return; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - /* The exchange says we made this deposit, but WE do not - recall making it (corrupted / unreliable database?)! - Well, let's say thanks and accept the money! */ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to find payment data in DB\n"); - rctx->check_transfer_result = GNUNET_OK; - } - if (GNUNET_NO == rctx->check_transfer_result) - { - /* Internal error: how can we have called #check_transfer() - but still have no result? */ - GNUNET_break (0); - resume_track_transfer_with_response - (rctx, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_MHD_make_json_pack ("{s:I, s:s, s:I, s:s}", - "code", - (json_int_t) - TALER_EC_TRACK_TRANSFER_DB_INTERNAL_LOGIC_ERROR, - "details", "internal logic error", - "line", (json_int_t) __LINE__, - "file", __FILE__)); - return; - } - if (GNUNET_SYSERR == rctx->check_transfer_result) - { - /* #check_transfer() failed, report conflict! */ - GNUNET_break_op (0); - GNUNET_assert (NULL != rctx->response); - resume_track_transfer_with_response - (rctx, - MHD_HTTP_FAILED_DEPENDENCY, - rctx->response); - rctx->response = NULL; - return; - } - /* Response is consistent with the /deposit we made, - remember it for future reference */ - for (unsigned int r = 0; rpreflight (db->cls); - qs = db->store_coin_to_transfer (db->cls, - &details[i].h_contract_terms, - &details[i].coin_pub, - &rctx->wtid); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - if (0 > qs) - { - /* Special report if retries insufficient */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - resume_track_transfer_with_response - (rctx, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_MHD_make_json_pack ("{s:I, s:s}", - "code", - (json_int_t) - TALER_EC_TRACK_TRANSFER_DB_STORE_COIN_ERROR, - "details", - "failed to store response from exchange to local database")); - return; - } - } - rctx->original_response = NULL; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "About to call tracks transformator.\n"); - - if (NULL == (jresponse = - transform_response (hr->reply, - rctx))) - { - resume_track_transfer_with_response - (rctx, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_MHD_make_error (TALER_EC_TRACK_TRANSFER_JSON_RESPONSE_ERROR, - "Fail to elaborate the response.")); - return; - } - - resume_track_transfer_with_response (rctx, - MHD_HTTP_OK, - TALER_MHD_make_json (jresponse)); - json_decref (jresponse); -} - - -/** - * Function called with the result of our exchange lookup. - * - * @param cls the `struct TrackTransferContext` - * @param hr HTTP response details - * @param eh NULL if exchange was not found to be acceptable - * @param wire_fee NULL (we did not specify a wire method) - * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config - */ -static void -process_track_transfer_with_exchange (void *cls, - const struct - TALER_EXCHANGE_HttpResponse *hr, - struct TALER_EXCHANGE_Handle *eh, - const struct TALER_Amount *wire_fee, - int exchange_trusted) -{ - struct TrackTransferContext *rctx = cls; - - rctx->fo = NULL; - if (MHD_HTTP_OK != hr->http_status) - { - /* The request failed somehow */ - GNUNET_break_op (0); - resume_track_transfer_with_response ( - rctx, - MHD_HTTP_FAILED_DEPENDENCY, - TALER_MHD_make_json_pack ( - (NULL != hr->reply) - ? "{s:s, s:I, s:I, s:I, s:O}" - : "{s:s, s:I, s:I, s:I}", - "hint", "failed to obtain meta-data from exchange", - "code", (json_int_t) TALER_EC_TRACK_TRANSFER_EXCHANGE_KEYS_FAILURE, - "exchange_http_status", (json_int_t) hr->http_status, - "exchange_code", (json_int_t) hr->ec, - "exchange_reply", hr->reply)); - return; - } - rctx->eh = eh; - rctx->wdh = TALER_EXCHANGE_transfers_get (eh, - &rctx->wtid, - &wire_transfer_cb, - rctx); - if (NULL == rctx->wdh) - { - GNUNET_break (0); - resume_track_transfer_with_response - (rctx, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_MHD_make_json_pack ("{s:I, s:s}", - "code", - (json_int_t) - TALER_EC_TRACK_TRANSFER_REQUEST_ERROR, - "error", - "failed to run /transfers/ GET on exchange")); - } -} - - -/** - * Handle a timeout for the processing of the track transfer request. - * - * @param cls closure - */ -static void -handle_track_transfer_timeout (void *cls) -{ - struct TrackTransferContext *rctx = cls; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Resuming /track/transfer with error after timeout\n"); - rctx->timeout_task = NULL; - - if (NULL != rctx->fo) - { - TMH_EXCHANGES_find_exchange_cancel (rctx->fo); - rctx->fo = NULL; - } - resume_track_transfer_with_response (rctx, - MHD_HTTP_SERVICE_UNAVAILABLE, - TALER_MHD_make_error ( - TALER_EC_TRACK_TRANSFER_EXCHANGE_TIMEOUT, - "exchange not reachable")); -} - - -/** - * Function called with information about a wire transfer identifier. - * Generate a response based on the given @a proof. - * - * @param cls closure - * @param proof proof from exchange about what the wire transfer was for. - * should match the `TrackTransactionResponse` format - * of the exchange - */ -static void -proof_cb (void *cls, - const json_t *proof) -{ - struct TrackTransferContext *rctx = cls; - json_t *transformed_response; - - if (NULL == (transformed_response = - transform_response (proof, - rctx))) - { - rctx->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; - rctx->response - = TALER_MHD_make_error (TALER_EC_TRACK_TRANSFER_JSON_RESPONSE_ERROR, - "Fail to elaborate response."); - return; - } - - rctx->response_code = MHD_HTTP_OK; - rctx->response = TALER_MHD_make_json (transformed_response); - json_decref (transformed_response); -} - - -/** - * Manages a /track/transfer call, thus it calls the /track/wtid - * offered by the exchange in order to return the set of transfers - * (of coins) associated with a given wire transfer. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_track_transfer (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi) -{ - struct TrackTransferContext *rctx; - const char *str; - const char *url; - const char *wire_method; - MHD_RESULT ret; - enum GNUNET_DB_QueryStatus qs; - - if (NULL == *connection_cls) - { - rctx = GNUNET_new (struct TrackTransferContext); - rctx->hc.cc = &track_transfer_cleanup; - rctx->connection = connection; - *connection_cls = rctx; - } - else - { - /* not first call, recover state */ - rctx = *connection_cls; - } - - if (0 != rctx->response_code) - { - /* We are *done* processing the request, just queue the response (!) */ - if (UINT_MAX == rctx->response_code) - { - GNUNET_break (0); - return MHD_NO; /* hard error */ - } - ret = MHD_queue_response (connection, - rctx->response_code, - rctx->response); - if (NULL != rctx->response) - { - MHD_destroy_response (rctx->response); - rctx->response = NULL; - } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Queueing response (%u) for /track/transfer (%s).\n", - (unsigned int) rctx->response_code, - ret ? "OK" : "FAILED"); - return ret; - } - if ( (NULL != rctx->fo) || - (NULL != rctx->eh) ) - { - /* likely old MHD version */ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Not sure why we are here, should be suspended\n"); - return MHD_YES; /* still work in progress */ - } - - url = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "exchange"); - if (NULL == url) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MISSING, - "exchange"); - rctx->url = GNUNET_strdup (url); - - /* FIXME: change again: we probably don't want the wire_method - but rather the _account_ (section) here! */ - wire_method = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "wire_method"); - if (NULL == wire_method) - { - if (1) - { - /* temporary work-around until demo is adjusted... */ - GNUNET_break (0); - wire_method = "x-taler-bank"; - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Client needs fixing, see API change for #4943!\n"); - } - else - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MISSING, - "wire_method"); - } - rctx->wire_method = GNUNET_strdup (wire_method); - rctx->mi = mi; - str = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "wtid"); - if (NULL == str) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MISSING, - "wtid"); - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (str, - strlen (str), - &rctx->wtid, - sizeof (rctx->wtid))) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_PARAMETER_MALFORMED, - "wtid"); - } - - /* Check if reply is already in database! */ - db->preflight (db->cls); - qs = db->find_proof_by_wtid (db->cls, - rctx->url, - &rctx->wtid, - &proof_cb, - rctx); - if (0 > qs) - { - /* Simple select queries should not cause serialization issues */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_TRACK_TRANSFER_DB_FETCH_DEPOSIT_ERROR, - "Fail to query database about proofs"); - } - if (0 != rctx->response_code) - { - ret = MHD_queue_response (connection, - rctx->response_code, - rctx->response); - if (NULL != rctx->response) - { - MHD_destroy_response (rctx->response); - rctx->response = NULL; - } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Queueing response (%u) for /track/transfer (%s).\n", - (unsigned int) rctx->response_code, - ret ? "OK" : "FAILED"); - return ret; - } - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Suspending /track/transfer handling while working with the exchange\n"); - MHD_suspend_connection (connection); - rctx->fo = TMH_EXCHANGES_find_exchange (url, - NULL, - GNUNET_NO, - &process_track_transfer_with_exchange, - rctx); - rctx->timeout_task - = GNUNET_SCHEDULER_add_delayed (TRACK_TIMEOUT, - &handle_track_transfer_timeout, - rctx); - return MHD_YES; -} - - -/* end of taler-merchant-httpd_track-transfer.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.c b/src/backend/taler-merchant-httpd_private-get-orders-ID.c new file mode 100644 index 00000000..bb5384d1 --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-get-orders-ID.c @@ -0,0 +1,591 @@ +/* + This file is part of TALER + (C) 2017, 2019 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_check-payment.c + * @brief implementation of /check-payment handler + * @author Florian Dold + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include +#include +#include +#include +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_mhd.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_check-payment.h" + +/** + * Maximum number of retries for database operations. + */ +#define MAX_RETRIES 5 + + +/** + * Data structure we keep for a check payment request. + */ +struct CheckPaymentRequestContext +{ + /** + * Must be first for #handle_mhd_completion_callback. + */ + struct TM_HandlerContext hc; + + /** + * Entry in the #resume_timeout_heap for this check payment, if we are + * suspended. + */ + struct TMH_SuspendedConnection sc; + + /** + * Which merchant instance is this for? + */ + struct MerchantInstance *mi; + + /** + * URL where the final contract can be found for this payment. + */ + char *final_contract_url; + + /** + * order ID for the payment + */ + const char *order_id; + + /** + * Where to get the contract + */ + const char *contract_url; + + /** + * session of the client + */ + const char *session_id; + + /** + * fulfillment URL of the contract (valid as long as + * @e contract_terms is valid). + */ + const char *fulfillment_url; + + /** + * Contract terms of the payment we are checking. NULL when they + * are not (yet) known. + */ + json_t *contract_terms; + + /** + * Hash of @e contract_terms, set only once @e contract_terms + * is available. + */ + struct GNUNET_HashCode h_contract_terms; + + /** + * Total refunds granted for this payment. Only initialized + * if @e refunded is set to #GNUNET_YES. + */ + struct TALER_Amount refund_amount; + + /** + * Set to #GNUNET_YES if this payment has been refunded and + * @e refund_amount is initialized. + */ + int refunded; + + /** + * Initially #GNUNET_SYSERR. If we queued a response, set to the + * result code (i.e. #MHD_YES or #MHD_NO). + */ + int ret; + +}; + + +/** + * Clean up the session state for a check payment request. + * + * @param hc must be a `struct CheckPaymentRequestContext *` + */ +static void +cprc_cleanup (struct TM_HandlerContext *hc) +{ + struct CheckPaymentRequestContext *cprc = (struct + CheckPaymentRequestContext *) hc; + + if (NULL != cprc->contract_terms) + json_decref (cprc->contract_terms); + GNUNET_free_non_null (cprc->final_contract_url); + GNUNET_free (cprc); +} + + +/** + * Function called with information about a refund. + * It is responsible for summing up the refund amount. + * + * @param cls closure + * @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 +process_refunds_cb (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 CheckPaymentRequestContext *cprc = cls; + + if (cprc->refunded) + { + GNUNET_assert (0 <= + TALER_amount_add (&cprc->refund_amount, + &cprc->refund_amount, + refund_amount)); + return; + } + cprc->refund_amount = *refund_amount; + cprc->refunded = GNUNET_YES; +} + + +/** + * The client did not yet pay, send it the payment request. + * + * @param cprc check pay request context + * @return #MHD_YES on success + */ +static int +send_pay_request (struct CheckPaymentRequestContext *cprc) +{ + int ret; + char *already_paid_order_id = NULL; + char *taler_pay_uri; + struct GNUNET_TIME_Relative remaining; + + remaining = GNUNET_TIME_absolute_get_remaining (cprc->sc.long_poll_timeout); + if (0 != remaining.rel_value_us) + { + /* long polling: do not queue a response, suspend connection instead */ + TMH_compute_pay_key (cprc->order_id, + &cprc->mi->pubkey, + &cprc->sc.key); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending /check-payment on key %s\n", + GNUNET_h2s (&cprc->sc.key)); + TMH_long_poll_suspend (&cprc->sc, + NULL); + return MHD_YES; + } + + /* Check if resource_id has been paid for in the same session + * with another order_id. + */ + if ( (NULL != cprc->session_id) && + (NULL != cprc->fulfillment_url) ) + { + enum GNUNET_DB_QueryStatus qs; + + qs = db->find_session_info (db->cls, + &already_paid_order_id, + cprc->session_id, + cprc->fulfillment_url, + &cprc->mi->pubkey); + if (qs < 0) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (cprc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_CHECK_PAYMENT_DB_FETCH_ORDER_ERROR, + "db error fetching pay session info"); + } + } + taler_pay_uri = TMH_make_taler_pay_uri (cprc->sc.con, + cprc->order_id, + cprc->session_id, + cprc->mi->id); + ret = TALER_MHD_reply_json_pack (cprc->sc.con, + MHD_HTTP_OK, + "{s:s, s:s, s:b, s:s?}", + "taler_pay_uri", taler_pay_uri, + "contract_url", cprc->final_contract_url, + "paid", 0, + "already_paid_order_id", + already_paid_order_id); + GNUNET_free (taler_pay_uri); + GNUNET_free_non_null (already_paid_order_id); + return ret; +} + + +/** + * Parse the "contract_terms" in @a cprc and set the + * "fulfillment_url" and the "h_contract_terms" in @a cprc + * accordingly. + * + * On errors, the response is being queued and the status + * code set in @cprc "ret". + * + * @param cprc[in,out] context to process + * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure + */ +static int +parse_contract_terms (struct CheckPaymentRequestContext *cprc) +{ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("fulfillment_url", + &cprc->fulfillment_url), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (cprc->contract_terms, + spec, + NULL, NULL)) + { + GNUNET_break (0); + cprc->ret + = TALER_MHD_reply_with_error (cprc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_CHECK_PAYMENT_DB_FETCH_CONTRACT_TERMS_ERROR, + "Merchant database error (contract terms corrupted)"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_JSON_hash (cprc->contract_terms, + &cprc->h_contract_terms)) + { + GNUNET_break (0); + cprc->ret + = TALER_MHD_reply_with_error (cprc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_CHECK_PAYMENT_FAILED_COMPUTE_PROPOSAL_HASH, + "Failed to hash proposal"); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Check that we are aware of @a order_id and if so request the payment, + * otherwise generate an error response. + * + * @param cprc session state + * @return status code to return to MHD for @a connection + */ +static int +check_order_and_request_payment (struct CheckPaymentRequestContext *cprc) +{ + enum GNUNET_DB_QueryStatus qs; + + if (NULL != cprc->contract_terms) + { + /* This should never happen. */ + GNUNET_break (0); + json_decref (cprc->contract_terms); + cprc->contract_terms = NULL; + } + qs = db->find_order (db->cls, + &cprc->contract_terms, + cprc->order_id, + &cprc->mi->pubkey); + if (0 > qs) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (cprc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_CHECK_PAYMENT_DB_FETCH_ORDER_ERROR, + "db error fetching order"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error (cprc->sc.con, + MHD_HTTP_NOT_FOUND, + TALER_EC_CHECK_PAYMENT_ORDER_ID_UNKNOWN, + "unknown order_id"); + } + + if (GNUNET_OK != + parse_contract_terms (cprc)) + return cprc->ret; + /* Offer was not picked up yet, but we ensured that it exists */ + return send_pay_request (cprc); +} + + +/** + * Manages a /check-payment call, checking the status + * of a payment and, if necessary, constructing the URL + * for a payment redirect URL. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_check_payment (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi) +{ + struct CheckPaymentRequestContext *cprc = *connection_cls; + enum GNUNET_DB_QueryStatus qs; + MHD_RESULT ret; + + if (NULL == cprc) + { + const char *long_poll_timeout_s; + + /* First time here, parse request and check order is known */ + cprc = GNUNET_new (struct CheckPaymentRequestContext); + cprc->hc.cc = &cprc_cleanup; + cprc->ret = GNUNET_SYSERR; + cprc->sc.con = connection; + cprc->mi = mi; + *connection_cls = cprc; + + cprc->order_id = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "order_id"); + if (NULL == cprc->order_id) + { + /* order_id is required but missing */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "order_id required"); + } + cprc->contract_url = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "contract_url"); + if (NULL == cprc->contract_url) + { + cprc->final_contract_url = TALER_url_absolute_mhd (connection, + "/public/proposal", + "instance", mi->id, + "order_id", + cprc->order_id, + NULL); + GNUNET_assert (NULL != cprc->final_contract_url); + } + else + { + cprc->final_contract_url = GNUNET_strdup (cprc->contract_url); + } + cprc->session_id = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "session_id"); + long_poll_timeout_s = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "timeout"); + if (NULL != long_poll_timeout_s) + { + unsigned int timeout; + + if (1 != sscanf (long_poll_timeout_s, + "%u", + &timeout)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MALFORMED, + "timeout must be non-negative number"); + } + cprc->sc.long_poll_timeout + = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply ( + GNUNET_TIME_UNIT_SECONDS, + timeout)); + } + else + { + cprc->sc.long_poll_timeout = GNUNET_TIME_UNIT_ZERO_ABS; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting /check-payment processing with timeout %s\n", + GNUNET_STRINGS_absolute_time_to_string ( + cprc->sc.long_poll_timeout)); + } + if (NULL != cprc->contract_terms) + { + json_decref (cprc->contract_terms); + cprc->contract_terms = NULL; + } + db->preflight (db->cls); + qs = db->find_contract_terms (db->cls, + &cprc->contract_terms, + cprc->order_id, + &mi->pubkey); + if (0 > qs) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_CHECK_PAYMENT_DB_FETCH_CONTRACT_TERMS_ERROR, + "db error fetching contract terms"); + } + + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* Check that we're at least aware of the order */ + return check_order_and_request_payment (cprc); + } + GNUNET_assert (NULL != cprc->contract_terms); + + if (GNUNET_OK != + parse_contract_terms (cprc)) + return cprc->ret; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Checkig payment status for order `%s' with contract %s\n", + cprc->order_id, + GNUNET_h2s (&cprc->h_contract_terms)); + GNUNET_assert (NULL != cprc->contract_terms); + + /* Check if the order has been paid for. */ + if (NULL != cprc->session_id) + { + /* Check if paid within a session. */ + char *already_paid_order_id = NULL; + + qs = db->find_session_info (db->cls, + &already_paid_order_id, + cprc->session_id, + cprc->fulfillment_url, + &mi->pubkey); + if (qs < 0) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_CHECK_PAYMENT_DB_FETCH_ORDER_ERROR, + "db error fetching pay session info"); + } + else if (0 == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order `%s' was not yet paid for session `%s'\n", + cprc->order_id, + cprc->session_id); + ret = send_pay_request (cprc); + GNUNET_free_non_null (already_paid_order_id); + return ret; + } + GNUNET_break (1 == qs); + GNUNET_break (0 == strcmp (cprc->order_id, + already_paid_order_id)); + GNUNET_free_non_null (already_paid_order_id); + } + else + { + /* Check if paid regardless of session. */ + json_t *xcontract_terms = NULL; + + qs = db->find_paid_contract_terms_from_hash (db->cls, + &xcontract_terms, + &cprc->h_contract_terms, + &mi->pubkey); + if (0 > qs) + { + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR, + "Merchant database error"); + } + if (0 == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order `%s' (contract `%s') was not yet paid\n", + cprc->order_id, + GNUNET_h2s (&cprc->h_contract_terms)); + return send_pay_request (cprc); + } + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); + GNUNET_assert (NULL != xcontract_terms); + json_decref (xcontract_terms); + } + + /* Accumulate refunds, if any. */ + for (unsigned int i = 0; iget_refunds_from_contract_terms_hash (db->cls, + &mi->pubkey, + &cprc->h_contract_terms, + &process_refunds_cb, + cprc); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (0 > qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database hard error on refunds_from_contract_terms_hash lookup: %s\n", + GNUNET_h2s (&cprc->h_contract_terms)); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR, + "Merchant database error"); + } + if (cprc->refunded) + return TALER_MHD_reply_json_pack (connection, + MHD_HTTP_OK, + "{s:O, s:b, s:b, s:o}", + "contract_terms", cprc->contract_terms, + "paid", 1, + "refunded", cprc->refunded, + "refund_amount", + TALER_JSON_from_amount ( + &cprc->refund_amount)); + return TALER_MHD_reply_json_pack (connection, + MHD_HTTP_OK, + "{s:O, s:b, s:b }", + "contract_terms", cprc->contract_terms, + "paid", 1, + "refunded", 0); +} diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.h b/src/backend/taler-merchant-httpd_private-get-orders-ID.h new file mode 100644 index 00000000..e94645df --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-get-orders-ID.h @@ -0,0 +1,48 @@ +/* + This file is part of TALER + (C) 2017 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_tip-query.h + * @brief headers for /tip-query handler + * @author Christian Grothoff + * @author Florian Dold + */ +#ifndef TALER_MERCHANT_HTTPD_CHECK_PAYMENT_H +#define TALER_MERCHANT_HTTPD_CHECK_PAYMENT_H +#include +#include "taler-merchant-httpd.h" + +/** + * Manages a /check-payment call, checking the status + * of a payment and, if necessary, constructing the URL + * for a payment redirect URL. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_check_payment (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi); + +#endif diff --git a/src/backend/taler-merchant-httpd_private-get-reserves-ID.c b/src/backend/taler-merchant-httpd_private-get-reserves-ID.c new file mode 100644 index 00000000..f7aa0ab0 --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-get-reserves-ID.c @@ -0,0 +1,248 @@ +/* + This file is part of TALER + (C) 2018 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_tip-query.c + * @brief implement API for authorizing tips to be paid to visitors + * @author Christian Grothoff + * @author Florian Dold + */ +#include "platform.h" +#include +#include +#include +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_mhd.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_tip-query.h" +#include "taler-merchant-httpd_tip-reserve-helper.h" + + +/** + * Maximum number of retries for database operations. + */ +#define MAX_RETRIES 5 + + +/** + * Internal per-request state for processing tip queries. + */ +struct TipQueryContext +{ + /** + * This field MUST be first for handle_mhd_completion_callback() to work + * when it treats this struct as a `struct TM_HandlerContext`. + */ + struct TM_HandlerContext hc; + + /** + * Merchant instance to use. + */ + const char *instance; + + /** + * Context for checking the tipping reserve's status. + */ + struct TMH_CheckTipReserve ctr; + + /** + * #GNUNET_YES if the tip query has already been processed + * and we can queue the response. + */ + int processed; + +}; + + +/** + * Custom cleanup routine for a `struct TipQueryContext`. + * + * @param hc the `struct TMH_JsonParseContext` to clean up. + */ +static void +cleanup_tqc (struct TM_HandlerContext *hc) +{ + struct TipQueryContext *tqc = (struct TipQueryContext *) hc; + + TMH_check_tip_reserve_cleanup (&tqc->ctr); + GNUNET_free (tqc); +} + + +/** + * We've been resumed after processing the reserve data from the + * exchange without error. Generate the final response. + * + * @param tqc context for which to generate the response. + */ +static int +generate_final_response (struct TipQueryContext *tqc) +{ + struct GNUNET_CRYPTO_EddsaPublicKey reserve_pub; + struct TALER_Amount amount_available; + + GNUNET_CRYPTO_eddsa_key_get_public (&tqc->ctr.reserve_priv.eddsa_priv, + &reserve_pub); + if (0 > + TALER_amount_subtract (&amount_available, + &tqc->ctr.amount_deposited, + &tqc->ctr.amount_withdrawn)) + { + char *a1; + char *a2; + + GNUNET_break_op (0); + a1 = TALER_amount_to_string (&tqc->ctr.amount_deposited); + a2 = TALER_amount_to_string (&tqc->ctr.amount_withdrawn); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "amount overflow, deposited %s but withdrawn %s\n", + a1, + a2); + GNUNET_free (a2); + GNUNET_free (a1); + return TALER_MHD_reply_with_error ( + tqc->ctr.connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_TIP_QUERY_RESERVE_HISTORY_ARITHMETIC_ISSUE_INCONSISTENT, + "Exchange returned invalid reserve history (amount overflow)"); + } + return TALER_MHD_reply_json_pack ( + tqc->ctr.connection, + MHD_HTTP_OK, + "{s:o, s:o, s:o, s:o, s:o}", + "reserve_pub", + GNUNET_JSON_from_data_auto (&reserve_pub), + "reserve_expiration", + GNUNET_JSON_from_time_abs (tqc->ctr.reserve_expiration), + "amount_authorized", + TALER_JSON_from_amount (&tqc->ctr.amount_authorized), + "amount_picked_up", + TALER_JSON_from_amount (&tqc->ctr.amount_withdrawn), + "amount_available", + TALER_JSON_from_amount (&amount_available)); +} + + +/** + * Handle a "/tip-query" request. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_tip_query (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi) +{ + struct TipQueryContext *tqc; + + if (NULL == *connection_cls) + { + tqc = GNUNET_new (struct TipQueryContext); + tqc->hc.cc = &cleanup_tqc; + tqc->ctr.connection = connection; + *connection_cls = tqc; + } + else + { + tqc = *connection_cls; + } + + if (0 != tqc->ctr.response_code) + { + MHD_RESULT res; + + /* We are *done* processing the request, just queue the response (!) */ + if (UINT_MAX == tqc->ctr.response_code) + { + GNUNET_break (0); + return MHD_NO; /* hard error */ + } + res = MHD_queue_response (connection, + tqc->ctr.response_code, + tqc->ctr.response); + MHD_destroy_response (tqc->ctr.response); + tqc->ctr.response = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Queueing response (%u) for /tip-query (%s).\n", + (unsigned int) tqc->ctr.response_code, + res ? "OK" : "FAILED"); + return res; + } + + if (GNUNET_YES == tqc->processed) + { + /* We've been here before, so TMH_check_tip_reserve() must have + finished and left the result for us. Finish processing. */ + return generate_final_response (tqc); + } + + if (NULL == mi->tip_exchange) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Instance `%s' not configured for tipping\n", + mi->id); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_PRECONDITION_FAILED, + TALER_EC_TIP_QUERY_INSTANCE_DOES_NOT_TIP, + "exchange for tipping not configured for the instance"); + } + tqc->ctr.reserve_priv = mi->tip_reserve; + + { + enum GNUNET_DB_QueryStatus qs; + + for (unsigned int i = 0; ipreflight (db->cls); + qs = db->get_authorized_tip_amount (db->cls, + &tqc->ctr.reserve_priv, + &tqc->ctr.amount_authorized); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_TIP_QUERY_DB_ERROR, + "Merchant database error"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* we'll set amount_authorized to zero later once + we know the currency */ + tqc->ctr.none_authorized = GNUNET_YES; + } + } + + tqc->processed = GNUNET_YES; + TMH_check_tip_reserve (&tqc->ctr, + mi->tip_exchange); + return MHD_YES; +} + + +/* end of taler-merchant-httpd_tip-query.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-reserves-ID.h b/src/backend/taler-merchant-httpd_private-get-reserves-ID.h new file mode 100644 index 00000000..3123486c --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-get-reserves-ID.h @@ -0,0 +1,45 @@ +/* + This file is part of TALER + (C) 2017 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_tip-query.h + * @brief headers for /tip-query handler + * @author Florian Dold + */ +#ifndef TALER_MERCHANT_HTTPD_TIP_QUERY_H +#define TALER_MERCHANT_HTTPD_TIP_QUERY_H +#include +#include "taler-merchant-httpd.h" + +/** + * Manages a /tip-query call. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_tip_query (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi); + +#endif diff --git a/src/backend/taler-merchant-httpd_private-get-transfers.c b/src/backend/taler-merchant-httpd_private-get-transfers.c new file mode 100644 index 00000000..7f55c917 --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-get-transfers.c @@ -0,0 +1,1089 @@ +/* + This file is part of TALER + (C) 2014-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_track-transfer.c + * @brief implement API for tracking transfers and wire transfers + * @author Marcello Stanisci + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include +#include +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_mhd.h" +#include "taler-merchant-httpd_auditors.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_track-transfer.h" + + +/** + * How long to wait before giving up processing with the exchange? + */ +#define TRACK_TIMEOUT (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, \ + 30)) + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + +/** + * Context used for handing /track/transfer requests. + */ +struct TrackTransferContext +{ + + /** + * This MUST be first! + */ + struct TM_HandlerContext hc; + + /** + * Handle to the exchange. + */ + struct TALER_EXCHANGE_Handle *eh; + + /** + * Handle for the /wire/transfers request. + */ + struct TALER_EXCHANGE_TransfersGetHandle *wdh; + + /** + * For which merchant instance is this tracking request? + */ + struct MerchantInstance *mi; + + /** + * HTTP connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * Response to return upon resume. + */ + struct MHD_Response *response; + + /** + * Handle for operation to lookup /keys (and auditors) from + * the exchange used for this transaction; NULL if no operation is + * pending. + */ + struct TMH_EXCHANGES_FindOperation *fo; + + /** + * Task run on timeout. + */ + struct GNUNET_SCHEDULER_Task *timeout_task; + + /** + * URL of the exchange. + */ + char *url; + + /** + * Wire method used for the transfer. + */ + char *wire_method; + + /** + * Pointer to the detail that we are currently + * checking in #check_transfer(). + */ + const struct TALER_TrackTransferDetails *current_detail; + + /** + * Argument for the /wire/transfers request. + */ + struct TALER_WireTransferIdentifierRawP wtid; + + /** + * Full original response we are currently processing. + */ + const json_t *original_response; + + /** + * Modified response to return to the frontend. + */ + json_t *deposits_response; + + /** + * Which transaction detail are we currently looking at? + */ + unsigned int current_offset; + + /** + * Response code to return. + */ + unsigned int response_code; + + /** + * #GNUNET_NO if we did not find a matching coin. + * #GNUNET_SYSERR if we found a matching coin, but the amounts do not match. + * #GNUNET_OK if we did find a matching coin. + */ + int check_transfer_result; +}; + + +/** + * Represents an entry in the table used to sum up + * individual deposits for each h_contract_terms. + */ +struct Entry +{ + + /** + * Sum accumulator for deposited value. + */ + struct TALER_Amount deposit_value; + + /** + * Sum accumulator for deposit fee. + */ + struct TALER_Amount deposit_fee; + +}; + + +/** + * Free the @a rctx. + * + * @param rctx data to free + */ +static void +free_transfer_track_context (struct TrackTransferContext *rctx) +{ + if (NULL != rctx->fo) + { + TMH_EXCHANGES_find_exchange_cancel (rctx->fo); + rctx->fo = NULL; + } + if (NULL != rctx->timeout_task) + { + GNUNET_SCHEDULER_cancel (rctx->timeout_task); + rctx->timeout_task = NULL; + } + if (NULL != rctx->wdh) + { + TALER_EXCHANGE_transfers_get_cancel (rctx->wdh); + rctx->wdh = NULL; + } + if (NULL != rctx->url) + { + GNUNET_free (rctx->url); + rctx->url = NULL; + } + if (NULL != rctx->wire_method) + { + GNUNET_free (rctx->wire_method); + rctx->wire_method = NULL; + } + GNUNET_free (rctx); +} + + +/** + * Callback that frees all the elements in the hashmap + * + * @param cls closure, NULL + * @param key current key + * @param value a `struct Entry` + * @return #GNUNET_YES if the iteration should continue, + * #GNUNET_NO otherwise. + */ +static int +hashmap_free (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + struct TALER_Entry *entry = value; + + (void) cls; + (void) key; + GNUNET_free (entry); + return GNUNET_YES; +} + + +/** + * Builds JSON response containing the summed-up amounts + * from individual deposits. + * + * @param cls closure + * @param key map's current key + * @param map's current value + * @return #GNUNET_YES if iteration is to be continued, + * #GNUNET_NO otherwise. + */ +static int +build_deposits_response (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + struct TrackTransferContext *rctx = cls; + struct Entry *entry = value; + json_t *element; + json_t *contract_terms; + json_t *order_id; + + db->preflight (db->cls); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + db->find_contract_terms_from_hash (db->cls, + &contract_terms, + key, + &rctx->mi->pubkey)) + { + GNUNET_break_op (0); + return GNUNET_NO; + } + + order_id = json_object_get (contract_terms, + "order_id"); + if (NULL == order_id) + { + GNUNET_break_op (0); + json_decref (contract_terms); + return GNUNET_NO; + } + element = json_pack ("{s:O, s:o, s:o}", + "order_id", order_id, + "deposit_value", TALER_JSON_from_amount ( + &entry->deposit_value), + "deposit_fee", TALER_JSON_from_amount ( + &entry->deposit_fee)); + json_decref (contract_terms); + if (NULL == element) + { + GNUNET_break_op (0); + return GNUNET_NO; + } + GNUNET_break (0 == + json_array_append_new (rctx->deposits_response, + element)); + return GNUNET_YES; +} + + +/** + * Transform /track/transfer result as gotten from the exchange + * and transforms it in a format liked by the backoffice Web interface. + * + * @param result response from exchange's /track/transfer + * @result pointer to new JSON, or NULL upon errors. + */ +static json_t * +transform_response (const json_t *result, + struct TrackTransferContext *rctx) +{ + json_t *deposits; + json_t *value; + json_t *result_mod = NULL; + size_t index; + const char *key; + struct GNUNET_HashCode h_key; + struct GNUNET_CONTAINER_MultiHashMap *map; + struct TALER_Amount iter_value; + struct TALER_Amount iter_fee; + struct Entry *current_entry; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount ("deposit_value", &iter_value), + TALER_JSON_spec_amount ("deposit_fee", &iter_fee), + GNUNET_JSON_spec_string ("h_contract_terms", &key), + GNUNET_JSON_spec_end () + }; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Transforming /track/transfer response.\n"); + map = GNUNET_CONTAINER_multihashmap_create (1, GNUNET_NO); + deposits = json_object_get (result, + "deposits"); + + json_array_foreach (deposits, index, value) + { + if (GNUNET_OK != + GNUNET_JSON_parse (value, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return NULL; + } + GNUNET_CRYPTO_hash_from_string (key, + &h_key); + + if (NULL != (current_entry = + GNUNET_CONTAINER_multihashmap_get (map, + &h_key))) + { + /* The map already knows this h_contract_terms*/ + if ( (0 > + TALER_amount_add (¤t_entry->deposit_value, + ¤t_entry->deposit_value, + &iter_value)) || + (0 > + TALER_amount_add (¤t_entry->deposit_fee, + ¤t_entry->deposit_fee, + &iter_fee)) ) + { + GNUNET_JSON_parse_free (spec); + goto cleanup; + } + } + else + { + /* First time in the map for this h_contract_terms*/ + current_entry = GNUNET_new (struct Entry); + current_entry->deposit_value = iter_value; + current_entry->deposit_fee = iter_fee; + + if (GNUNET_SYSERR == + GNUNET_CONTAINER_multihashmap_put (map, + &h_key, + current_entry, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)) + { + GNUNET_JSON_parse_free (spec); + goto cleanup; + } + } + GNUNET_JSON_parse_free (spec); + } + rctx->deposits_response = json_array (); + + if (GNUNET_SYSERR == + GNUNET_CONTAINER_multihashmap_iterate (map, + &build_deposits_response, + rctx)) + goto cleanup; + + result_mod = json_copy ((struct json_t *) result); + json_object_del (result_mod, + "deposits"); + json_object_set_new (result_mod, + "deposits_sums", + rctx->deposits_response); + rctx->deposits_response = NULL; +cleanup: + GNUNET_CONTAINER_multihashmap_iterate (map, + &hashmap_free, + NULL); + GNUNET_CONTAINER_multihashmap_destroy (map); + return result_mod; +} + + +/** + * Resume the given /track/transfer operation and send the given response. + * Stores the response in the @a rctx and signals MHD to resume + * the connection. Also ensures MHD runs immediately. + * + * @param rctx transfer tracking context + * @param response_code response code to use + * @param response response data to send back + */ +static void +resume_track_transfer_with_response (struct TrackTransferContext *rctx, + unsigned int response_code, + struct MHD_Response *response) +{ + rctx->response_code = response_code; + rctx->response = response; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resuming /track/transfer handling as exchange interaction is done (%u)\n", + response_code); + if (NULL != rctx->timeout_task) + { + GNUNET_SCHEDULER_cancel (rctx->timeout_task); + rctx->timeout_task = NULL; + } + MHD_resume_connection (rctx->connection); + TMH_trigger_daemon (); /* we resumed, kick MHD */ +} + + +/** + * Custom cleanup routine for a `struct TrackTransferContext`. + * + * @param hc the `struct TrackTransferContext` to clean up. + */ +static void +track_transfer_cleanup (struct TM_HandlerContext *hc) +{ + struct TrackTransferContext *rctx = (struct TrackTransferContext *) hc; + + free_transfer_track_context (rctx); +} + + +/** + * This function checks that the information about the coin which + * was paid back by _this_ wire transfer matches what _we_ (the merchant) + * knew about this coin. + * + * @param cls closure with our `struct TrackTransferContext *` + * @param transaction_id of the contract + * @param coin_pub public key of the coin + * @param exchange_url URL of the exchange that issued @a coin_pub + * @param amount_with_fee amount the exchange will transfer 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 exchange_proof proof from exchange that coin was accepted + */ +static void +check_transfer (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 TrackTransferContext *rctx = cls; + const struct TALER_TrackTransferDetails *ttd = rctx->current_detail; + + if (GNUNET_SYSERR == rctx->check_transfer_result) + return; /* already had a serious issue; odd that we're called more than once as well... */ + if ( (0 != TALER_amount_cmp (amount_with_fee, + &ttd->coin_value)) || + (0 != TALER_amount_cmp (deposit_fee, + &ttd->coin_fee)) ) + { + /* Disagreement between the exchange and us about how much this + coin is worth! */ + GNUNET_break_op (0); + rctx->check_transfer_result = GNUNET_SYSERR; + /* Build the `TrackTransferConflictDetails` */ + rctx->response + = TALER_MHD_make_json_pack ( + "{s:I, s:s, s:o, s:I, s:o, s:o, s:s, s:o, s:o}", + "code", (json_int_t) TALER_EC_TRACK_TRANSFER_CONFLICTING_REPORTS, + "hint", "disagreement about deposit valuation", + "exchange_deposit_proof", exchange_proof, + "conflict_offset", (json_int_t) rctx->current_offset, + "exchange_transfer_proof", rctx->original_response, + "coin_pub", GNUNET_JSON_from_data_auto (coin_pub), + "h_contract_terms", GNUNET_JSON_from_data_auto ( + &ttd->h_contract_terms), + "amount_with_fee", TALER_JSON_from_amount (amount_with_fee), + "deposit_fee", TALER_JSON_from_amount (deposit_fee)); + return; + } + rctx->check_transfer_result = GNUNET_OK; +} + + +/** + * Check that the given @a wire_fee is what the + * @a exchange_pub should charge at the @a execution_time. + * If the fee is correct (according to our database), + * return #GNUNET_OK. If we do not have the fee structure + * in our DB, we just accept it and return #GNUNET_NO; + * if we have proof that the fee is bogus, we respond with + * the proof to the client and return #GNUNET_SYSERR. + * + * @param rctx context of the transfer to respond to + * @param json response from the exchange + * @param execution_time time of the wire transfer + * @param wire_fee fee claimed by the exchange + * @return #GNUNET_SYSERR if we returned hard proof of + * missbehavior from the exchange to the client + */ +static int +check_wire_fee (struct TrackTransferContext *rctx, + const json_t *json, + struct GNUNET_TIME_Absolute execution_time, + const struct TALER_Amount *wire_fee) +{ + const struct TALER_MasterPublicKeyP *master_pub; + struct GNUNET_HashCode h_wire_method; + struct TALER_Amount expected_fee; + struct TALER_Amount closing_fee; + struct TALER_MasterSignatureP master_sig; + struct GNUNET_TIME_Absolute start_date; + struct GNUNET_TIME_Absolute end_date; + enum GNUNET_DB_QueryStatus qs; + const struct TALER_EXCHANGE_Keys *keys; + + keys = TALER_EXCHANGE_get_keys (rctx->eh); + if (NULL == keys) + { + GNUNET_break (0); + return GNUNET_NO; + } + master_pub = &keys->master_pub; + GNUNET_CRYPTO_hash (rctx->wire_method, + strlen (rctx->wire_method) + 1, + &h_wire_method); + db->preflight (db->cls); + qs = db->lookup_wire_fee (db->cls, + master_pub, + &h_wire_method, + execution_time, + &expected_fee, + &closing_fee, + &start_date, + &end_date, + &master_sig); + if (0 >= qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to find wire fee for `%s' and method `%s' at %s in DB, accepting blindly that the fee is %s\n", + TALER_B2S (master_pub), + rctx->wire_method, + GNUNET_STRINGS_absolute_time_to_string (execution_time), + TALER_amount2s (wire_fee)); + return GNUNET_NO; + } + if (0 <= TALER_amount_cmp (&expected_fee, + wire_fee)) + return GNUNET_OK; /* expected_fee >= wire_fee */ + + /* Wire fee check failed, export proof to client */ + resume_track_transfer_with_response ( + rctx, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + "{s:I, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:O}", + "code", (json_int_t) TALER_EC_TRACK_TRANSFER_JSON_BAD_WIRE_FEE, + "wire_fee", TALER_JSON_from_amount (wire_fee), + "execution_time", GNUNET_JSON_from_time_abs (execution_time), + "expected_wire_fee", TALER_JSON_from_amount (&expected_fee), + "expected_closing_fee", TALER_JSON_from_amount (&closing_fee), + "start_date", GNUNET_JSON_from_time_abs (start_date), + "end_date", GNUNET_JSON_from_time_abs (end_date), + "master_sig", GNUNET_JSON_from_data_auto (&master_sig), + "master_pub", GNUNET_JSON_from_data_auto (master_pub), + "json", json)); + return GNUNET_SYSERR; +} + + +/** + * Function called with detailed wire transfer data, including all + * of the coin transactions that were combined into the wire transfer. + * + * @param cls closure + * @param hr HTTP response details + * @param exchange_pub public key of the exchange used to sign @a json + * @param h_wire hash of the wire transfer address the transfer went to, or NULL on error + * @param execution_time time when the exchange claims to have performed the wire transfer + * @param total_amount total amount of the wire transfer, or NULL if the exchange could + * not provide any @a wtid (set only if @a http_status is #MHD_HTTP_OK) + * @param wire_fee wire fee that was charged by the exchange + * @param details_length length of the @a details array + * @param details array with details about the combined transactions + */ +static void +wire_transfer_cb (void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const struct GNUNET_HashCode *h_wire, + struct GNUNET_TIME_Absolute execution_time, + const struct TALER_Amount *total_amount, + const struct TALER_Amount *wire_fee, + unsigned int details_length, + const struct TALER_TrackTransferDetails *details) +{ + struct TrackTransferContext *rctx = cls; + json_t *jresponse; + enum GNUNET_DB_QueryStatus qs; + + rctx->wdh = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got response code %u from exchange for /track/transfer\n", + hr->http_status); + if (MHD_HTTP_OK != hr->http_status) + { + resume_track_transfer_with_response ( + rctx, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + "{s:I, s:I, s:I, s:O}", + "code", (json_int_t) TALER_EC_TRACK_TRANSFER_EXCHANGE_ERROR, + "exchange_code", (json_int_t) hr->ec, + "exchange_http_status", (json_int_t) hr->http_status, + "exchange_reply", hr->reply)); + return; + } + for (unsigned int i = 0; ipreflight (db->cls); + qs = db->store_transfer_to_proof (db->cls, + rctx->url, + &rctx->wtid, + execution_time, + exchange_pub, + hr->reply); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (0 > qs) + { + /* Special report if retries insufficient */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_json_pack ("{s:I, s:s}", + "code", + (json_int_t) + TALER_EC_TRACK_TRANSFER_DB_STORE_TRANSFER_ERROR, + "details", + "failed to store response from exchange to local database")); + return; + } + rctx->original_response = hr->reply; + + if (GNUNET_SYSERR == + check_wire_fee (rctx, + hr->reply, + execution_time, + wire_fee)) + return; + + /* Now we want to double-check that any (Taler coin) deposit + * which is accounted into _this_ wire transfer, does exist + * into _our_ database. This is the rationale: if the + * exchange paid us for it, we must have received it _beforehands_! + * + * details_length is how many (Taler coin) deposits have been + * aggregated into _this_ wire transfer. + */// + for (unsigned int i = 0; icurrent_offset = i; + rctx->current_detail = &details[i]; + /* Set the coin as "never seen" before. */ + rctx->check_transfer_result = GNUNET_NO; + db->preflight (db->cls); + qs = db->find_payments_by_hash_and_coin (db->cls, + &details[i].h_contract_terms, + &rctx->mi->pubkey, + &details[i].coin_pub, + &check_transfer, + rctx); + if (0 > qs) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_json_pack ("{s:I, s:s}", + "code", + (json_int_t) + TALER_EC_TRACK_TRANSFER_DB_FETCH_DEPOSIT_ERROR, + "details", + "failed to obtain deposit data from local database")); + return; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* The exchange says we made this deposit, but WE do not + recall making it (corrupted / unreliable database?)! + Well, let's say thanks and accept the money! */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to find payment data in DB\n"); + rctx->check_transfer_result = GNUNET_OK; + } + if (GNUNET_NO == rctx->check_transfer_result) + { + /* Internal error: how can we have called #check_transfer() + but still have no result? */ + GNUNET_break (0); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_json_pack ("{s:I, s:s, s:I, s:s}", + "code", + (json_int_t) + TALER_EC_TRACK_TRANSFER_DB_INTERNAL_LOGIC_ERROR, + "details", "internal logic error", + "line", (json_int_t) __LINE__, + "file", __FILE__)); + return; + } + if (GNUNET_SYSERR == rctx->check_transfer_result) + { + /* #check_transfer() failed, report conflict! */ + GNUNET_break_op (0); + GNUNET_assert (NULL != rctx->response); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_FAILED_DEPENDENCY, + rctx->response); + rctx->response = NULL; + return; + } + /* Response is consistent with the /deposit we made, + remember it for future reference */ + for (unsigned int r = 0; rpreflight (db->cls); + qs = db->store_coin_to_transfer (db->cls, + &details[i].h_contract_terms, + &details[i].coin_pub, + &rctx->wtid); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (0 > qs) + { + /* Special report if retries insufficient */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_json_pack ("{s:I, s:s}", + "code", + (json_int_t) + TALER_EC_TRACK_TRANSFER_DB_STORE_COIN_ERROR, + "details", + "failed to store response from exchange to local database")); + return; + } + } + rctx->original_response = NULL; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "About to call tracks transformator.\n"); + + if (NULL == (jresponse = + transform_response (hr->reply, + rctx))) + { + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_error (TALER_EC_TRACK_TRANSFER_JSON_RESPONSE_ERROR, + "Fail to elaborate the response.")); + return; + } + + resume_track_transfer_with_response (rctx, + MHD_HTTP_OK, + TALER_MHD_make_json (jresponse)); + json_decref (jresponse); +} + + +/** + * Function called with the result of our exchange lookup. + * + * @param cls the `struct TrackTransferContext` + * @param hr HTTP response details + * @param eh NULL if exchange was not found to be acceptable + * @param wire_fee NULL (we did not specify a wire method) + * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config + */ +static void +process_track_transfer_with_exchange (void *cls, + const struct + TALER_EXCHANGE_HttpResponse *hr, + struct TALER_EXCHANGE_Handle *eh, + const struct TALER_Amount *wire_fee, + int exchange_trusted) +{ + struct TrackTransferContext *rctx = cls; + + rctx->fo = NULL; + if (MHD_HTTP_OK != hr->http_status) + { + /* The request failed somehow */ + GNUNET_break_op (0); + resume_track_transfer_with_response ( + rctx, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + (NULL != hr->reply) + ? "{s:s, s:I, s:I, s:I, s:O}" + : "{s:s, s:I, s:I, s:I}", + "hint", "failed to obtain meta-data from exchange", + "code", (json_int_t) TALER_EC_TRACK_TRANSFER_EXCHANGE_KEYS_FAILURE, + "exchange_http_status", (json_int_t) hr->http_status, + "exchange_code", (json_int_t) hr->ec, + "exchange_reply", hr->reply)); + return; + } + rctx->eh = eh; + rctx->wdh = TALER_EXCHANGE_transfers_get (eh, + &rctx->wtid, + &wire_transfer_cb, + rctx); + if (NULL == rctx->wdh) + { + GNUNET_break (0); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_json_pack ("{s:I, s:s}", + "code", + (json_int_t) + TALER_EC_TRACK_TRANSFER_REQUEST_ERROR, + "error", + "failed to run /transfers/ GET on exchange")); + } +} + + +/** + * Handle a timeout for the processing of the track transfer request. + * + * @param cls closure + */ +static void +handle_track_transfer_timeout (void *cls) +{ + struct TrackTransferContext *rctx = cls; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resuming /track/transfer with error after timeout\n"); + rctx->timeout_task = NULL; + + if (NULL != rctx->fo) + { + TMH_EXCHANGES_find_exchange_cancel (rctx->fo); + rctx->fo = NULL; + } + resume_track_transfer_with_response (rctx, + MHD_HTTP_SERVICE_UNAVAILABLE, + TALER_MHD_make_error ( + TALER_EC_TRACK_TRANSFER_EXCHANGE_TIMEOUT, + "exchange not reachable")); +} + + +/** + * Function called with information about a wire transfer identifier. + * Generate a response based on the given @a proof. + * + * @param cls closure + * @param proof proof from exchange about what the wire transfer was for. + * should match the `TrackTransactionResponse` format + * of the exchange + */ +static void +proof_cb (void *cls, + const json_t *proof) +{ + struct TrackTransferContext *rctx = cls; + json_t *transformed_response; + + if (NULL == (transformed_response = + transform_response (proof, + rctx))) + { + rctx->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + rctx->response + = TALER_MHD_make_error (TALER_EC_TRACK_TRANSFER_JSON_RESPONSE_ERROR, + "Fail to elaborate response."); + return; + } + + rctx->response_code = MHD_HTTP_OK; + rctx->response = TALER_MHD_make_json (transformed_response); + json_decref (transformed_response); +} + + +/** + * Manages a /track/transfer call, thus it calls the /track/wtid + * offered by the exchange in order to return the set of transfers + * (of coins) associated with a given wire transfer. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_track_transfer (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi) +{ + struct TrackTransferContext *rctx; + const char *str; + const char *url; + const char *wire_method; + MHD_RESULT ret; + enum GNUNET_DB_QueryStatus qs; + + if (NULL == *connection_cls) + { + rctx = GNUNET_new (struct TrackTransferContext); + rctx->hc.cc = &track_transfer_cleanup; + rctx->connection = connection; + *connection_cls = rctx; + } + else + { + /* not first call, recover state */ + rctx = *connection_cls; + } + + if (0 != rctx->response_code) + { + /* We are *done* processing the request, just queue the response (!) */ + if (UINT_MAX == rctx->response_code) + { + GNUNET_break (0); + return MHD_NO; /* hard error */ + } + ret = MHD_queue_response (connection, + rctx->response_code, + rctx->response); + if (NULL != rctx->response) + { + MHD_destroy_response (rctx->response); + rctx->response = NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Queueing response (%u) for /track/transfer (%s).\n", + (unsigned int) rctx->response_code, + ret ? "OK" : "FAILED"); + return ret; + } + if ( (NULL != rctx->fo) || + (NULL != rctx->eh) ) + { + /* likely old MHD version */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Not sure why we are here, should be suspended\n"); + return MHD_YES; /* still work in progress */ + } + + url = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "exchange"); + if (NULL == url) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "exchange"); + rctx->url = GNUNET_strdup (url); + + /* FIXME: change again: we probably don't want the wire_method + but rather the _account_ (section) here! */ + wire_method = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "wire_method"); + if (NULL == wire_method) + { + if (1) + { + /* temporary work-around until demo is adjusted... */ + GNUNET_break (0); + wire_method = "x-taler-bank"; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Client needs fixing, see API change for #4943!\n"); + } + else + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "wire_method"); + } + rctx->wire_method = GNUNET_strdup (wire_method); + rctx->mi = mi; + str = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "wtid"); + if (NULL == str) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "wtid"); + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (str, + strlen (str), + &rctx->wtid, + sizeof (rctx->wtid))) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MALFORMED, + "wtid"); + } + + /* Check if reply is already in database! */ + db->preflight (db->cls); + qs = db->find_proof_by_wtid (db->cls, + rctx->url, + &rctx->wtid, + &proof_cb, + rctx); + if (0 > qs) + { + /* Simple select queries should not cause serialization issues */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_TRACK_TRANSFER_DB_FETCH_DEPOSIT_ERROR, + "Fail to query database about proofs"); + } + if (0 != rctx->response_code) + { + ret = MHD_queue_response (connection, + rctx->response_code, + rctx->response); + if (NULL != rctx->response) + { + MHD_destroy_response (rctx->response); + rctx->response = NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Queueing response (%u) for /track/transfer (%s).\n", + (unsigned int) rctx->response_code, + ret ? "OK" : "FAILED"); + return ret; + } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Suspending /track/transfer handling while working with the exchange\n"); + MHD_suspend_connection (connection); + rctx->fo = TMH_EXCHANGES_find_exchange (url, + NULL, + GNUNET_NO, + &process_track_transfer_with_exchange, + rctx); + rctx->timeout_task + = GNUNET_SCHEDULER_add_delayed (TRACK_TIMEOUT, + &handle_track_transfer_timeout, + rctx); + return MHD_YES; +} + + +/* end of taler-merchant-httpd_track-transfer.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-transfers.h b/src/backend/taler-merchant-httpd_private-get-transfers.h new file mode 100644 index 00000000..0463295e --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-get-transfers.h @@ -0,0 +1,49 @@ +/* + This file is part of TALER + (C) 2014-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_track-transfer.h + * @brief headers for /track/transfer handler + * @author Christian Grothoff + * @author Marcello Stanisci + */ +#ifndef TALER_MERCHANT_HTTPD_TRACK_TRANSFER_H +#define TALER_MERCHANT_HTTPD_TRACK_TRANSFER_H +#include +#include "taler-merchant-httpd.h" + +/** + * Manages a /track/transfer call, thus it calls the /wire/transfer + * offered by the exchange in order to return the set of transfers + * (of coins) associated with a given wire transfer + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_track_transfer (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi); + + +#endif diff --git a/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.c b/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.c new file mode 100644 index 00000000..5324c619 --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.c @@ -0,0 +1,379 @@ +/* + This file is part of TALER + (C) 2014-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_refund_increase.c + * @brief Handle request to increase the refund for an order + * @author Marcello Stanisci + */ +#include "platform.h" +#include +#include +#include +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_refund.h" + +/** + * How often do we retry the non-trivial refund INSERT database + * transaction? + */ +#define MAX_RETRIES 5 + + +/** + * Information we keep for individual calls + * to requests that parse JSON, but keep no other state. + */ +struct TMH_JsonParseContext +{ + + /** + * This field MUST be first for handle_mhd_completion_callback() to work + * when it treats this struct as a `struct TM_HandlerContext`. + */ + struct TM_HandlerContext hc; + + /** + * Placeholder for #TALER_MHD_parse_post_json() to keep its internal state. + */ + void *json_parse_context; +}; + + +/** + * Make a taler://refund URI + * + * @param connection MHD connection to take host and path from + * @param instance_id merchant's instance ID, must not be NULL + * @param order_id order ID to show a refund for, must not be NULL + * @returns the URI, must be freed with #GNUNET_free + */ +static char * +make_taler_refund_uri (struct MHD_Connection *connection, + const char *instance_id, + const char *order_id) +{ + const char *host; + const char *forwarded_host; + const char *uri_path; + const char *uri_instance_id; + const char *query; + char *result; + + GNUNET_assert (NULL != instance_id); + GNUNET_assert (NULL != order_id); + host = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_HOST); + forwarded_host = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + "X-Forwarded-Host"); + if (NULL != forwarded_host) + host = forwarded_host; + if (NULL == host) + { + /* Should never happen, at least the host header should be defined */ + GNUNET_break (0); + return NULL; + } + uri_path = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + "X-Forwarded-Prefix"); + if (NULL == uri_path) + uri_path = "-"; + if (0 == strcmp (instance_id, + "default")) + uri_instance_id = "-"; + else + uri_instance_id = instance_id; + if (GNUNET_YES == TALER_mhd_is_https (connection)) + query = ""; + else + query = "?insecure=1"; + GNUNET_assert (0 < GNUNET_asprintf (&result, + "taler://refund/%s/%s/%s/%s%s", + host, + uri_path, + uri_instance_id, + order_id, + query)); + return result; +} + + +/** + * Custom cleanup routine for a `struct TMH_JsonParseContext`. + * + * @param hc the `struct TMH_JsonParseContext` to clean up. + */ +static void +json_parse_cleanup (struct TM_HandlerContext *hc) +{ + struct TMH_JsonParseContext *jpc = (struct TMH_JsonParseContext *) hc; + + TALER_MHD_parse_post_cleanup_callback (jpc->json_parse_context); + GNUNET_free (jpc); +} + + +/** + * Process a refund request. + * + * @param connection HTTP client connection + * @param mi merchant instance doing the processing + * @param refund amount to be refunded + * @param order_id for which order is the refund + * @param reason reason for the refund + * @return MHD result code + */ +static MHD_RESULT +process_refund (struct MHD_Connection *connection, + struct MerchantInstance *mi, + const struct TALER_Amount *refund, + const char *order_id, + const char *reason) +{ + json_t *contract_terms; + enum GNUNET_DB_QueryStatus qs; + enum GNUNET_DB_QueryStatus qsx; + struct GNUNET_HashCode h_contract_terms; + + db->preflight (db->cls); + /* Convert order id to h_contract_terms */ + qs = db->find_contract_terms (db->cls, + &contract_terms, + order_id, + &mi->pubkey); + if (0 > qs) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_REFUND_LOOKUP_DB_ERROR, + "An error occurred while retrieving payment data from db"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Unknown order id given: `%s'\n", + order_id); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_REFUND_ORDER_ID_UNKNOWN, + "order_id not found in database"); + } + + if (GNUNET_OK != + TALER_JSON_hash (contract_terms, + &h_contract_terms)) + { + GNUNET_break (0); + json_decref (contract_terms); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_INTERNAL_LOGIC_ERROR, + "Could not hash contract terms"); + } + json_decref (contract_terms); + for (unsigned int i = 0; istart (db->cls, + "increase refund")) + { + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + qs = db->increase_refund_for_contract_NT (db->cls, + &h_contract_terms, + &mi->pubkey, + refund, + reason); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "increase refund returned %d\n", + qs); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_break (0); + db->rollback (db->cls); + break; + } + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + { + db->rollback (db->cls); + continue; + } + /* Got one or more deposits */ + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + db->rollback (db->cls); + break; + } + qsx = db->commit (db->cls); + if (GNUNET_DB_STATUS_HARD_ERROR == qsx) + { + GNUNET_break (0); + qs = qsx; + break; + } + if (GNUNET_DB_STATUS_SOFT_ERROR != qsx) + break; + } + if (0 > qs) + { + /* Special report if retries insufficient */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_REFUND_MERCHANT_DB_COMMIT_ERROR, + "Internal database error or refund amount too big"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Refusing refund amount %s that is larger than original payment\n", + TALER_amount2s (refund)); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_REFUND_INCONSISTENT_AMOUNT, + "Amount above payment"); + } + + /* Resume /public/poll-payments clients that may wait for this refund */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Awakeing clients on %s waiting for refund of less than %s\n", + order_id, + TALER_amount2s (refund)); + + TMH_long_poll_resume (order_id, + &mi->pubkey, + refund); + + { + MHD_RESULT ret; + char *taler_refund_uri; + + taler_refund_uri = make_taler_refund_uri (connection, + mi->id, + order_id); + ret = TALER_MHD_reply_json_pack ( + connection, + MHD_HTTP_OK, + "{s:o, s:s}", + "h_contract_terms", + GNUNET_JSON_from_data_auto (&h_contract_terms), + "taler_refund_url", + taler_refund_uri); + GNUNET_free (taler_refund_uri); + return ret; + } +} + + +/** + * Handle request for increasing the refund associated with + * a contract. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_refund_increase (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi) +{ + enum GNUNET_GenericReturnValue res; + struct TMH_JsonParseContext *ctx; + struct TALER_Amount refund; + const char *order_id; + const char *reason; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount ("refund", &refund), + GNUNET_JSON_spec_string ("order_id", &order_id), + GNUNET_JSON_spec_string ("reason", &reason), + GNUNET_JSON_spec_end () + }; + json_t *root; + + if (NULL == *connection_cls) + { + ctx = GNUNET_new (struct TMH_JsonParseContext); + ctx->hc.cc = &json_parse_cleanup; + *connection_cls = ctx; + } + else + { + ctx = *connection_cls; + } + + res = TALER_MHD_parse_post_json (connection, + &ctx->json_parse_context, + upload_data, + upload_data_size, + &root); + if (GNUNET_SYSERR == res) + return MHD_NO; + /* the POST's body has to be further fetched */ + if ( (GNUNET_NO == res) || + (NULL == root) ) + return MHD_YES; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + json_decref (root); + return MHD_YES; + } + if (GNUNET_SYSERR == res) + { + GNUNET_break_op (0); + json_decref (root); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_JSON_INVALID, + "Request body does not match specification"); + } + { + MHD_RESULT ret; + + ret = process_refund (connection, + mi, + &refund, + order_id, + reason); + GNUNET_JSON_parse_free (spec); + json_decref (root); + return ret; + } +} + + +/* end of taler-merchant-httpd_refund_increase.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.h b/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.h new file mode 100644 index 00000000..ff178001 --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.h @@ -0,0 +1,49 @@ +/* + This file is part of TALER + (C) 2014, 2015, 2016, 2017 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ + +/** + * @file backend/taler-merchant-httpd_refund_increase.h + * @brief HTTP serving layer mainly intended to communicate with the frontend + * @author Marcello Stanisci + */ + +#ifndef TALER_MERCHANT_HTTPD_REFUND_INCREASE_H +#define TALER_MERCHANT_HTTPD_REFUND_INCREASE_H +#include +#include "taler-merchant-httpd.h" + + +/** + * Handle request for increasing the refund associated with + * a contract. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_refund_increase (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi); + + +#endif diff --git a/src/backend/taler-merchant-httpd_private-post-orders-ID-track-UNSPEC.c b/src/backend/taler-merchant-httpd_private-post-orders-ID-track-UNSPEC.c new file mode 100644 index 00000000..39f8ce9e --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-post-orders-ID-track-UNSPEC.c @@ -0,0 +1,1250 @@ +/* + This file is part of TALER + (C) 2014-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_track-transaction.c + * @brief implement API for tracking deposits and wire transfers + * @author Marcello Stanisci + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include +#include +#include "taler-merchant-httpd.h" +#include "taler_merchant_service.h" +#include "taler-merchant-httpd_mhd.h" +#include "taler-merchant-httpd_auditors.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_track-transaction.h" + + +/** + * Information about a wire transfer for a /track/transaction response. + */ +struct TransactionWireTransfer +{ + + /** + * Wire transfer identifier this struct is about. + */ + struct TALER_WireTransferIdentifierRawP wtid; + + /** + * When was this wire transfer executed? + */ + struct GNUNET_TIME_Absolute execution_time; + + /** + * Number of coins of the selected transaction that + * is covered by this wire transfer. + */ + unsigned int num_coins; + + /** + * Information about the coins of the selected transaction + * that are part of the wire transfer. + */ + struct TALER_MERCHANT_CoinWireTransfer *coins; + + /** + * URL of the exchange that executed the wire transfer. + */ + char *exchange_url; +}; + + +/** + * Map containing all the known merchant instances + */ +extern struct GNUNET_CONTAINER_MultiHashMap *by_id_map; + +/** + * How long to wait before giving up processing with the exchange? + */ +#define TRACK_TIMEOUT (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, \ + 30)) + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + + +/** + * Generate /track/transaction response. + * + * @param num_transfers how many wire transfers make up the transaction + * @param transfers data on each wire transfer + * @return MHD response object + */ +static struct MHD_Response * +make_track_transaction_ok (unsigned int num_transfers, + const struct TransactionWireTransfer *transfers) +{ + json_t *j_transfers; + + j_transfers = json_array (); + for (unsigned int i = 0; icoins[0].amount_with_fee; + for (unsigned int j = 1; jnum_coins; j++) + { + const struct TALER_MERCHANT_CoinWireTransfer *coin = &transfer->coins[j]; + + GNUNET_assert (0 <= + TALER_amount_add (&sum, + &sum, + &coin->amount_with_fee)); + } + + GNUNET_assert (0 == + json_array_append_new ( + j_transfers, + json_pack ( + "{s:s, s:o, s:o, s:o}", + "exchange", + transfer->exchange_url, + "wtid", + GNUNET_JSON_from_data_auto (&transfer->wtid), + "execution_time", + GNUNET_JSON_from_time_abs (transfer->execution_time), + "amount", + TALER_JSON_from_amount (&sum)))); + } + { + struct MHD_Response *ret; + + ret = TALER_MHD_make_json (j_transfers); + json_decref (j_transfers); + return ret; + } +} + + +/** + * Context for a /track/transaction operation. + */ +struct TrackTransactionContext; + +/** + * Merchant instance being tracked + */ +struct MerchantInstance; + +/** + * Information we keep for each coin in a /track/transaction operation. + */ +struct TrackCoinContext +{ + /** + * Kept in a DLL. + */ + struct TrackCoinContext *next; + + /** + * Kept in a DLL. + */ + struct TrackCoinContext *prev; + + /** + * Our context for a /track/transaction operation. + */ + struct TrackTransactionContext *tctx; + + /** + * Public key of the coin. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Exchange that was used for the transaction. + */ + char *exchange_url; + + /** + * Handle for the request to resolve the WTID for this coin. + */ + struct TALER_EXCHANGE_DepositGetHandle *dwh; + + /** + * Wire transfer identifier for this coin. + */ + struct TALER_WireTransferIdentifierRawP wtid; + + /** + * Execution time of the wire transfer @e wtid. + */ + struct GNUNET_TIME_Absolute execution_time; + + /** + * Value of the coin including deposit fee. + */ + struct TALER_Amount amount_with_fee; + + /** + * Deposit fee for the coin. + */ + struct TALER_Amount deposit_fee; + + /** + * Have we obtained the WTID for this coin yet? + */ + int have_wtid; + +}; + + +/** + * Context for a /track/transaction operation. + */ +struct TrackTransactionContext +{ + + /** + * This field MUST be first. + */ + struct TM_HandlerContext hc; + + /** + * HTTP request we are handling. + */ + struct MHD_Connection *connection; + + /** + * Kept in a DLL. + */ + struct TrackCoinContext *tcc_head; + + /** + * Kept in a DLL. + */ + struct TrackCoinContext *tcc_tail; + + /** + * Task run on timeout. + */ + struct GNUNET_SCHEDULER_Task *timeout_task; + + /** + * Handle for operation to lookup /keys (and auditors) from + * the exchange used for this transaction; NULL if no operation is + * pending. + */ + struct TMH_EXCHANGES_FindOperation *fo; + + /** + * Handle to our exchange, once we found it. + */ + struct TALER_EXCHANGE_Handle *eh; + + /** + * URL of the exchange we currently have in @e eh. + */ + const char *current_exchange; + + /** + * Handle we use to resolve transactions for a given WTID. + */ + struct TALER_EXCHANGE_TransfersGetHandle *wdh; + + /** + * Response to return upon resume. + */ + struct MHD_Response *response; + + /** + * Wire transfer identifier we are currently looking up in @e wdh. + */ + struct TALER_WireTransferIdentifierRawP current_wtid; + + /** + * Execution time of the wire transfer we are currently looking up in @e wdh. + */ + struct GNUNET_TIME_Absolute current_execution_time; + + /** + * Hash of wire details for the transaction. + */ + struct GNUNET_HashCode h_wire; + + /** + * Timestamp of the transaction. + */ + struct GNUNET_TIME_Absolute timestamp; + + /** + * Refund deadline for the transaction. + */ + struct GNUNET_TIME_Absolute refund_deadline; + + /** + * Total value of the transaction. + */ + struct TALER_Amount total_amount; + + /** + * Transaction this request is about. + */ + const char *transaction_id; + + /** + * Proposal's hashcode. + */ + struct GNUNET_HashCode h_contract_terms; + + /** + * Response code to return upon resume. + */ + unsigned int response_code; + + /** + * Which merchant instance is being tracked + */ + struct MerchantInstance *mi; + + /** + * Set to negative values in #coin_cb() if we encounter + * a database problem. + */ + enum GNUNET_DB_QueryStatus qs; + +}; + + +/** + * Free the @a tctx. + * + * @param tctx data to free + */ +static void +free_tctx (struct TrackTransactionContext *tctx) +{ + struct TrackCoinContext *tcc; + + while (NULL != (tcc = tctx->tcc_head)) + { + GNUNET_CONTAINER_DLL_remove (tctx->tcc_head, + tctx->tcc_tail, + tcc); + if (NULL != tcc->dwh) + { + TALER_EXCHANGE_deposits_get_cancel (tcc->dwh); + tcc->dwh = NULL; + } + if (NULL != tcc->exchange_url) + { + GNUNET_free (tcc->exchange_url); + tcc->exchange_url = NULL; + } + GNUNET_free (tcc); + } + if (NULL != tctx->wdh) + { + TALER_EXCHANGE_transfers_get_cancel (tctx->wdh); + tctx->wdh = NULL; + } + if (NULL != tctx->fo) + { + TMH_EXCHANGES_find_exchange_cancel (tctx->fo); + tctx->fo = NULL; + } + if (NULL != tctx->timeout_task) + { + GNUNET_SCHEDULER_cancel (tctx->timeout_task); + tctx->timeout_task = NULL; + } + GNUNET_free (tctx); +} + + +/** + * Custom cleanup routine for a `struct TrackTransactionContext`. + * + * @param hc the `struct PayContext` to clean up. + */ +static void +track_transaction_cleanup (struct TM_HandlerContext *hc) +{ + struct TrackTransactionContext *tctx = (struct TrackTransactionContext *) hc; + + free_tctx (tctx); +} + + +/** + * Resume the given /track/transaction operation and send the given + * response. Stores the response in the @a tctx and signals MHD to + * resume the connection. Also ensures MHD runs immediately. + * + * @param tctx transaction tracking context + * @param response_code response code to use + * @param response response data to send back + */ +static void +resume_track_transaction_with_response (struct TrackTransactionContext *tctx, + unsigned int response_code, + struct MHD_Response *response) +{ + tctx->response_code = response_code; + tctx->response = response; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resuming /track/transaction handling as exchange interaction is done (%u)\n", + response_code); + if (NULL != tctx->timeout_task) + { + GNUNET_SCHEDULER_cancel (tctx->timeout_task); + tctx->timeout_task = NULL; + } + MHD_resume_connection (tctx->connection); + TMH_trigger_daemon (); /* we resumed, kick MHD */ +} + + +/** + * This function is called to trace the wire transfers for + * all of the coins of the transaction of the @a tctx. Once + * we have traced all coins, we build the response. + * + * @param tctx track context with established connection to exchange + */ +static void +trace_coins (struct TrackTransactionContext *tctx); + + +/** + * Function called with detailed wire transfer data, including all + * of the coin transactions that were combined into the wire transfer. + * + * We now store this information. Then we check if we still have + * any coins of the original wire transfer not taken care of. + * + * @param cls closure + * @param hr HTTP response details + * @param exchange_pub public key of the exchange used for signing + * @param execution_time time when the exchange claims to have performed the wire transfer + * @param wtid extracted wire transfer identifier, or NULL if the exchange could + * not provide any (set only if @a http_status is #MHD_HTTP_OK) + * @param total_amount total amount of the wire transfer, or NULL if the exchange could + * not provide any @a wtid (set only if @a http_status is #MHD_HTTP_OK) + * @param wire_fee wire fee that was charged by the exchange + * @param details_length length of the @a details array + * @param details array with details about the combined transactions + */ +static void +wire_deposits_cb (void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const struct GNUNET_HashCode *h_wire, + struct GNUNET_TIME_Absolute execution_time, + const struct TALER_Amount *total_amount, + const struct TALER_Amount *wire_fee, + unsigned int details_length, + const struct TALER_TrackTransferDetails *details) +{ + struct TrackTransactionContext *tctx = cls; + enum GNUNET_DB_QueryStatus qs; + + tctx->wdh = NULL; + if (MHD_HTTP_OK != hr->http_status) + { + GNUNET_break_op (0); + resume_track_transaction_with_response ( + tctx, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + "{s:I, s:I, s:I, s:O}", + "code", + (json_int_t) TALER_EC_TRACK_TRANSACTION_WIRE_TRANSFER_TRACE_ERROR, + "exchange_http_status", + (json_int_t) hr->http_status, + "exchange_code", + (json_int_t) hr->ec, + "exchange_reply", + hr->reply)); + return; + } + for (unsigned int i = 0; ipreflight (db->cls); + qs = db->store_transfer_to_proof (db->cls, + tctx->current_exchange, + &tctx->current_wtid, + tctx->current_execution_time, + exchange_pub, + hr->reply); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (0 > qs) + { + /* Not good, but not fatal either, log error and continue */ + /* Special report if retries insufficient */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to store transfer-to-proof mapping in DB\n"); + } + for (struct TrackCoinContext *tcc = tctx->tcc_head; + NULL != tcc; + tcc = tcc->next) + { + if (GNUNET_YES == tcc->have_wtid) + continue; + for (unsigned int d = 0; dcoin_pub)) + { + tcc->wtid = tctx->current_wtid; + tcc->execution_time = tctx->current_execution_time; + tcc->have_wtid = GNUNET_YES; + } + + for (unsigned int i = 0; ipreflight (db->cls); + qs = db->store_coin_to_transfer (db->cls, + &details[d].h_contract_terms, + &details[d].coin_pub, + &tctx->current_wtid); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (0 > qs) + { + /* Not good, but not fatal either, log error and continue */ + /* Special report if retries insufficient */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to store coin-to-transfer mapping in DB\n"); + } + } + } + /* Continue tracing (will also handle case that we are done) */ + trace_coins (tctx); +} + + +/** + * Closure for #proof_cb(). + */ +struct ProofCheckContext +{ + /** + * Proof returned from #proof_cb. The reference counter was + * increased for this reference and it must thus be freed. + * NULL if we did not find any proof. The JSON should + * match the `TrackTransferResponse` of the exchange API + * (https://api.taler.net/api-exchange.html#tracktransferresponse) + */ + json_t *p_ret; + +}; + + +/** + * Function called with information about a wire transfer identifier. + * We actually never expect this to be called. + * + * @param cls closure with a `struct ProofCheckContext` + * @param proof proof from exchange about what the wire transfer was for + */ +static void +proof_cb (void *cls, + const json_t *proof) +{ + struct ProofCheckContext *pcc = cls; + + GNUNET_break (NULL == pcc->p_ret); + pcc->p_ret = json_incref ((json_t *) proof); +} + + +/** + * This function takes the wtid from the coin being tracked + * and _track_ it against the exchange. This way, we know + * all the other coins which were aggregated together with + * this one. This way we save further HTTP requests to track + * the other coins. + * + * @param cls closure with a `struct TrackCoinContext` + * @param hr HTTP response details + * @param exchange_pub public key of the exchange used for signing @a json + * @param wtid wire transfer identifier used by the exchange, NULL if exchange did not + * yet execute the transaction + * @param execution_time actual or planned execution time for the wire transfer + * @param coin_contribution contribution to the @a total_amount of the deposited coin (may be NULL) + */ +static void +wtid_cb (void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const struct TALER_WireTransferIdentifierRawP *wtid, + struct GNUNET_TIME_Absolute execution_time, + const struct TALER_Amount *coin_contribution) +{ + struct TrackCoinContext *tcc = cls; + struct TrackTransactionContext *tctx = tcc->tctx; + struct ProofCheckContext pcc; + enum GNUNET_DB_QueryStatus qs; + + tcc->dwh = NULL; + if (MHD_HTTP_OK != hr->http_status) + { + if (MHD_HTTP_ACCEPTED == hr->http_status) + { + resume_track_transaction_with_response ( + tcc->tctx, + MHD_HTTP_ACCEPTED, + /* Return verbatim what the exchange said. */ + TALER_MHD_make_json (hr->reply)); + return; + } + + /* Transaction not resolved for one of the + coins, report error! */ + resume_track_transaction_with_response ( + tcc->tctx, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + "{s:I, s:I, s:I, s:O}", + "code", + (json_int_t) TALER_EC_TRACK_TRANSACTION_COIN_TRACE_ERROR, + "exchange_http_status", + (json_int_t) hr->http_status, + "exchange_code", + (json_int_t) hr->ec, + "exchange_reply", + hr->reply)); + return; + } + tctx->current_wtid = *wtid; + tctx->current_execution_time = execution_time; + + pcc.p_ret = NULL; + /* attempt to find this wtid's track from our database, + Will make pcc.p_ret point to a "proof", if one exists. */ + db->preflight (db->cls); + qs = db->find_proof_by_wtid (db->cls, + tctx->current_exchange, + wtid, + &proof_cb, + &pcc); + if (0 > qs) + { + /* Simple select queries should not + cause serialization issues */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + resume_track_transaction_with_response ( + tcc->tctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_error ( + TALER_EC_TRACK_TRANSACTION_DB_FETCH_TRANSACTION_ERROR, + "Fail to query database about proofs")); + return; + } + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + /* How come this wtid was already stored into the + database and _not all_ of its coins were already + tracked? Inconsistent state (! At least regarding + what the exchange tells us) */ + GNUNET_break_op (0); + resume_track_transaction_with_response ( + tcc->tctx, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + "{s:I, s:s, s:O, s:o, s:o}", + "code", (json_int_t) TALER_EC_TRACK_TRANSACTION_CONFLICTING_REPORTS, + "hint", "conflicting transfer data from exchange", + "transaction_tracking_claim", hr->reply, + "wtid_tracking_claim", pcc.p_ret, + "coin_pub", GNUNET_JSON_from_data_auto (&tcc->coin_pub))); + return; + } + + tctx->wdh = TALER_EXCHANGE_transfers_get (tctx->eh, + wtid, + &wire_deposits_cb, + tctx); +} + + +/** + * We have obtained all WTIDs, now prepare the response + * + * @param tctx handle for the operation + */ +static void +generate_response (struct TrackTransactionContext *tctx) +{ + unsigned int num_wtid = 0; + + /* count how many disjoint wire transfer identifiers there are; + note that there should only usually be one, so while this + is worst-case O(n^2), in pracitce this is O(n) */ + for (struct TrackCoinContext *tcc = tctx->tcc_head; + NULL != tcc; + tcc = tcc->next) + { + int found = GNUNET_NO; + + for (struct TrackCoinContext *tcc2 = tctx->tcc_head; + tcc2 != tcc; + tcc2 = tcc2->next) + { + if (0 == GNUNET_memcmp (&tcc->wtid, + &tcc2->wtid)) + { + found = GNUNET_YES; + break; + } + } + if (GNUNET_NO == found) + num_wtid++; + } + + { + /* on-stack allocation is fine, as the number of coins and the + number of wire-transfers per-transaction is expected to be tiny. */ + struct TransactionWireTransfer wts[num_wtid]; + unsigned int wtid_off; + + wtid_off = 0; + for (struct TrackCoinContext *tcc = tctx->tcc_head; + NULL != tcc; + tcc = tcc->next) + { + int found = GNUNET_NO; + + for (struct TrackCoinContext *tcc2 = tctx->tcc_head; + tcc2 != tcc; + tcc2 = tcc2->next) + { + if (0 == GNUNET_memcmp (&tcc->wtid, + &tcc2->wtid)) + { + found = GNUNET_YES; + break; + } + } + if (GNUNET_NO == found) + { + unsigned int num_coins; + struct TransactionWireTransfer *wt; + + wt = &wts[wtid_off++]; + wt->wtid = tcc->wtid; + wt->exchange_url = tcc->exchange_url; + wt->execution_time = tcc->execution_time; + /* count number of coins with this wtid */ + num_coins = 0; + for (struct TrackCoinContext *tcc2 = tctx->tcc_head; + NULL != tcc2; + tcc2 = tcc2->next) + { + if (0 == GNUNET_memcmp (&wt->wtid, + &tcc2->wtid)) + num_coins++; + } + /* initialize coins array */ + wt->num_coins = num_coins; + wt->coins = GNUNET_new_array (num_coins, + struct TALER_MERCHANT_CoinWireTransfer); + num_coins = 0; + for (struct TrackCoinContext *tcc2 = tctx->tcc_head; + NULL != tcc2; + tcc2 = tcc2->next) + { + if (0 == GNUNET_memcmp (&wt->wtid, + &tcc2->wtid)) + { + struct TALER_MERCHANT_CoinWireTransfer *coin = + &wt->coins[num_coins++]; + + coin->coin_pub = tcc2->coin_pub; + coin->amount_with_fee = tcc2->amount_with_fee; + coin->deposit_fee = tcc2->deposit_fee; + } + } + } /* GNUNET_NO == found */ + } /* for all tcc */ + GNUNET_assert (wtid_off == num_wtid); + + { + struct MHD_Response *resp; + + resp = make_track_transaction_ok (num_wtid, + wts); + for (wtid_off = 0; wtid_off < num_wtid; wtid_off++) + GNUNET_free (wts[wtid_off].coins); + resume_track_transaction_with_response (tctx, + MHD_HTTP_OK, + resp); + } + } /* end of scope for 'wts' and 'resp' */ +} + + +/** + * Find the exchange to trace the next coin(s). + * + * @param tctx operation context + */ +static void +find_exchange (struct TrackTransactionContext *tctx); + + +/** + * This function is called to 'trace the wire transfers' + * (true?) for all of the coins of the transaction of the @a tctx. + * Once we have traced all coins, we build the response. + * + * @param tctx track context with established connection to exchange + */ +static void +trace_coins (struct TrackTransactionContext *tctx) +{ + struct TrackCoinContext *tcc; + + /* Make sure we are connected to the exchange. */ + GNUNET_assert (NULL != tctx->eh); + + for (tcc = tctx->tcc_head; NULL != tcc; tcc = tcc->next) + + /* How come one doesn't have wtid? */ + if (GNUNET_YES != tcc->have_wtid) + break; + + if (NULL != tcc) + { + if (0 != strcmp (tcc->exchange_url, + tctx->current_exchange)) + { + /* exchange changed, find matching one first! */ + tctx->eh = NULL; + tctx->current_exchange = NULL; + find_exchange (tctx); + return; + } + /* we are not done requesting WTIDs from the current + exchange; do the next one */ + tcc->dwh = TALER_EXCHANGE_deposits_get (tctx->eh, + &tctx->mi->privkey, + &tctx->h_wire, + &tctx->h_contract_terms, + &tcc->coin_pub, + &wtid_cb, + tcc); + return; + } + tctx->current_exchange = NULL; + tctx->eh = NULL; + generate_response (tctx); +} + + +/** + * Function called with the result of our exchange lookup. + * Merely provide the execution context to the routine actually + * tracking the coin. + * + * @param cls the `struct TrackTransactionContext` + * @param hr HTTP response details + * @param eh NULL if exchange was not found to be acceptable + * @param wire_fee NULL (we did not specify a wire method) + * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config + */ +static void +process_track_transaction_with_exchange (void *cls, + const struct + TALER_EXCHANGE_HttpResponse *hr, + struct TALER_EXCHANGE_Handle *eh, + const struct TALER_Amount *wire_fee, + int exchange_trusted) +{ + struct TrackTransactionContext *tctx = cls; + + tctx->fo = NULL; + if (MHD_HTTP_OK != hr->http_status) + { + /* The request failed somehow */ + GNUNET_break_op (0); + resume_track_transaction_with_response ( + tctx, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + (NULL != hr->reply) + ? "{s:s, s:I, s:I, s:I, s:O}" + : "{s:s, s:I, s:I, s:I}", + "hint", "failed to obtain meta-data from exchange", + "code", (json_int_t) TALER_EC_TRACK_TRANSACTION_EXCHANGE_KEYS_FAILURE, + "exchange_http_status", (json_int_t) hr->http_status, + "exchange_code", (json_int_t) hr->ec, + "exchange_reply", hr->reply)); + return; + } + tctx->eh = eh; + trace_coins (tctx); +} + + +/** + * Handle a timeout for the processing of the track transaction request. + * + * @param cls closure + */ +static void +handle_track_transaction_timeout (void *cls) +{ + struct TrackTransactionContext *tctx = cls; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resuming /track/transaction with error after timeout\n"); + tctx->timeout_task = NULL; + if (NULL != tctx->fo) + { + TMH_EXCHANGES_find_exchange_cancel (tctx->fo); + tctx->fo = NULL; + } + resume_track_transaction_with_response (tctx, + MHD_HTTP_SERVICE_UNAVAILABLE, + TALER_MHD_make_error ( + TALER_EC_PAY_EXCHANGE_TIMEOUT, + "exchange not reachable")); +} + + +/** + * Information about the wire transfer corresponding to + * a deposit operation. Note that it is in theory possible + * that we have a @a transaction_id and @a coin_pub in the + * result that do not match a deposit that we know about, + * for example because someone else deposited funds into + * our account. + * + * @param cls closure + * @param transaction_id ID of the contract + * @param coin_pub public key of the coin + * @param wtid identifier of the wire transfer in which the exchange + * send us the money for the coin deposit + * @param execution_time when was the wire transfer executed? + * @param exchange_proof proof from exchange about what the deposit was for + * NULL if we have not asked for this signature + */ +static void +transfer_cb (void *cls, + const struct GNUNET_HashCode *h_contract_terms, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_WireTransferIdentifierRawP *wtid, + struct GNUNET_TIME_Absolute execution_time, + const json_t *exchange_proof) +{ + struct TrackCoinContext *tcc = cls; + + if (0 != GNUNET_memcmp (coin_pub, + &tcc->coin_pub)) + return; + tcc->wtid = *wtid; + tcc->execution_time = execution_time; + tcc->have_wtid = GNUNET_YES; +} + + +/** + * Responsible to get the current coin wtid and store it into its state. + * + * @param cls closure + * @param transaction_id of the contract + * @param coin_pub public key of the coin + * @param exchange_url URL of exchange that issued @a coin_pub + * @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 exchange_proof proof from exchange that coin was accepted + */ +static void +coin_cb (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 TrackTransactionContext *tctx = cls; + struct TrackCoinContext *tcc; + + tcc = GNUNET_new (struct TrackCoinContext); + tcc->tctx = tctx; + tcc->coin_pub = *coin_pub; + tcc->exchange_url = GNUNET_strdup (exchange_url); + tcc->amount_with_fee = *amount_with_fee; + tcc->deposit_fee = *deposit_fee; + GNUNET_CONTAINER_DLL_insert (tctx->tcc_head, + tctx->tcc_tail, + tcc); + + /* find all those pairs associated to + this contract term's hash code. The callback + will then set the wtid for the "current coin" + context. */ + { + enum GNUNET_DB_QueryStatus qs; + + qs = db->find_transfers_by_hash (db->cls, + h_contract_terms, + &transfer_cb, + tcc); + if (0 > qs) + { + GNUNET_break (0); + tctx->qs = qs; + } + } +} + + +/** + * Find the exchange to trace the next coin(s). + * + * @param tctx operation context + */ +static void +find_exchange (struct TrackTransactionContext *tctx) +{ + struct TrackCoinContext *tcc = tctx->tcc_head; + + while ( (NULL != tcc) && + (GNUNET_YES == tcc->have_wtid) ) + tcc = tcc->next; + if (NULL != tcc) + { + tctx->current_exchange = tcc->exchange_url; + tctx->fo = TMH_EXCHANGES_find_exchange ( + tctx->current_exchange, + NULL, + GNUNET_NO, + &process_track_transaction_with_exchange, + tctx); + + } + else + { + generate_response (tctx); + } +} + + +/** + * Handle a "/track/transaction" request. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_track_transaction (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi) +{ + struct TrackTransactionContext *tctx; + const char *order_id; + enum GNUNET_DB_QueryStatus qs; + struct json_t *contract_terms; + + if (NULL == *connection_cls) + { + tctx = GNUNET_new (struct TrackTransactionContext); + tctx->hc.cc = &track_transaction_cleanup; + tctx->connection = connection; + *connection_cls = tctx; + } + else + { + /* not first call, recover state */ + tctx = *connection_cls; + } + + if (0 != tctx->response_code) + { + MHD_RESULT ret; + + /* We are *done* processing the request, just queue the response (!) */ + if (UINT_MAX == tctx->response_code) + { + GNUNET_break (0); + return MHD_NO; /* hard error */ + } + ret = MHD_queue_response (connection, + tctx->response_code, + tctx->response); + if (NULL != tctx->response) + { + MHD_destroy_response (tctx->response); + tctx->response = NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Queueing response (%u) for /track/transaction (%s).\n", + (unsigned int) tctx->response_code, + ret ? "OK" : "FAILED"); + return ret; + } + if ( (NULL != tctx->fo) || + (NULL != tctx->eh) ) + { + /* likely old MHD version */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Not sure why we are here, should be suspended\n"); + return MHD_YES; /* still work in progress */ + } + order_id = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "order_id"); + if (NULL == order_id) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "order_id"); + + tctx->mi = mi; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Tracking on behalf of instance '%s'\n", + mi->id); + + + /* Map order id to contract terms; the objective is to get + the contract term's hashcode so as to retrieve all the + coins which have been deposited for it. */ + db->preflight (db->cls); + qs = db->find_contract_terms (db->cls, + &contract_terms, + order_id, + &tctx->mi->pubkey); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_TRACK_TRANSACTION_DB_FETCH_TRANSACTION_ERROR, + "Database error finding contract terms"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_PROPOSAL_LOOKUP_NOT_FOUND, + "Given order_id doesn't map to any proposal"); + + if (GNUNET_OK != + TALER_JSON_hash (contract_terms, + &tctx->h_contract_terms)) + { + json_decref (contract_terms); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_INTERNAL_LOGIC_ERROR, + "Failed to hash contract terms"); + } + + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_absolute_time ("refund_deadline", + &tctx->refund_deadline), + GNUNET_JSON_spec_absolute_time ("timestamp", + &tctx->timestamp), + TALER_JSON_spec_amount ("amount", + &tctx->total_amount), + GNUNET_JSON_spec_fixed_auto ("h_wire", + &tctx->h_wire), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_YES != + GNUNET_JSON_parse (contract_terms, + spec, + NULL, NULL)) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + json_decref (contract_terms); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_INTERNAL_LOGIC_ERROR, + "Failed to parse contract terms from DB"); + } + json_decref (contract_terms); + } + + tctx->qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + db->preflight (db->cls); + + /* Find coins which have been deposited for this contract, + and retrieve the wtid for each one. */ + qs = db->find_payments (db->cls, + &tctx->h_contract_terms, + &tctx->mi->pubkey, + &coin_cb, + tctx); + if ( (0 > qs) || + (0 > tctx->qs) ) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != tctx->qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_TRACK_TRANSACTION_DB_FETCH_PAYMENT_ERROR, + "Database error: failed to find payment data"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_TRACK_TRANSACTION_DB_NO_DEPOSITS_ERROR, + "deposit data not found"); + } + *connection_cls = tctx; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Suspending /track/transaction handling while working with the exchange\n"); + MHD_suspend_connection (connection); + tctx->timeout_task + = GNUNET_SCHEDULER_add_delayed (TRACK_TIMEOUT, + &handle_track_transaction_timeout, + tctx); + find_exchange (tctx); + return MHD_YES; +} + + +/* end of taler-merchant-httpd_track-transaction.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-orders-ID-track-UNSPEC.h b/src/backend/taler-merchant-httpd_private-post-orders-ID-track-UNSPEC.h new file mode 100644 index 00000000..d6201107 --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-post-orders-ID-track-UNSPEC.h @@ -0,0 +1,47 @@ +/* + This file is part of TALER + (C) 2014-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_track-transaction.h + * @brief headers for /track/transaction handler + * @author Christian Grothoff + * @author Marcello Stanisci + */ +#ifndef TALER_MERCHANT_HTTPD_TRACK_TRANSACTION_H +#define TALER_MERCHANT_HTTPD_TRACK_TRANSACTION_H +#include +#include "taler-merchant-httpd.h" + +/** + * Handle a "/track/transaction" request. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_track_transaction (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi); + + +#endif diff --git a/src/backend/taler-merchant-httpd_private-post-reserves-ID-authorize-tip.c b/src/backend/taler-merchant-httpd_private-post-reserves-ID-authorize-tip.c new file mode 100644 index 00000000..569cf0ab --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-post-reserves-ID-authorize-tip.c @@ -0,0 +1,306 @@ +/* + This file is part of TALER + (C) 2014-2017 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_tip-authorize.c + * @brief implement API for authorizing tips to be paid to visitors + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include +#include +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_mhd.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_tip-authorize.h" +#include "taler-merchant-httpd_tip-reserve-helper.h" + + +struct TipAuthContext +{ + /** + * This field MUST be first for handle_mhd_completion_callback() to work + * when it treats this struct as a `struct TM_HandlerContext`. + */ + struct TM_HandlerContext hc; + + /** + * Placeholder for #TALER_MHD_parse_post_json() to keep its internal state. + */ + void *json_parse_context; + + /** + * Justification to use. + */ + const char *justification; + + /** + * JSON request received. + */ + json_t *root; + + /** + * Context for checking the tipping reserve's status. + */ + struct TMH_CheckTipReserve ctr; + + /** + * Tip amount requested. + */ + struct TALER_Amount amount; + + /** + * Flag set to #GNUNET_YES when we have tried /reserve/status of the + * tipping reserve already. + */ + int checked_status; + + /** + * Flag set to #GNUNET_YES when we have parsed the incoming JSON already. + */ + int parsed_json; + +}; + + +/** + * Custom cleanup routine for a `struct TipAuthContext`. + * + * @param hc the `struct TMH_JsonParseContext` to clean up. + */ +static void +cleanup_tac (struct TM_HandlerContext *hc) +{ + struct TipAuthContext *tac = (struct TipAuthContext *) hc; + + if (NULL != tac->root) + { + json_decref (tac->root); + tac->root = NULL; + } + TMH_check_tip_reserve_cleanup (&tac->ctr); + TALER_MHD_parse_post_cleanup_callback (tac->json_parse_context); + GNUNET_free (tac); +} + + +/** + * Handle a "/tip-authorize" request. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_tip_authorize (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi) +{ + struct TipAuthContext *tac; + enum GNUNET_GenericReturnValue res; + enum TALER_ErrorCode ec; + struct GNUNET_TIME_Absolute expiration; + struct GNUNET_HashCode tip_id; + json_t *extra; + + if (NULL == *connection_cls) + { + tac = GNUNET_new (struct TipAuthContext); + tac->hc.cc = &cleanup_tac; + tac->ctr.connection = connection; + *connection_cls = tac; + } + else + { + tac = *connection_cls; + } + if (NULL != tac->ctr.response) + { + MHD_RESULT ret; + + ret = MHD_queue_response (connection, + tac->ctr.response_code, + tac->ctr.response); + MHD_destroy_response (tac->ctr.response); + tac->ctr.response = NULL; + return ret; + } + if (GNUNET_NO == tac->parsed_json) + { + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount ("amount", &tac->amount), + GNUNET_JSON_spec_string ("justification", &tac->justification), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_post_json (connection, + &tac->json_parse_context, + upload_data, + upload_data_size, + &tac->root); + if (GNUNET_SYSERR == res) + return MHD_NO; + /* the POST's body has to be further fetched */ + if ( (GNUNET_NO == res) || + (NULL == tac->root) ) + return MHD_YES; + + res = TALER_MHD_parse_json_data (connection, + tac->root, + spec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + } + tac->parsed_json = GNUNET_YES; + } + + if (NULL == mi->tip_exchange) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Instance `%s' not configured for tipping\n", + mi->id); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_PRECONDITION_FAILED, + TALER_EC_TIP_AUTHORIZE_INSTANCE_DOES_NOT_TIP, + "exchange for tipping not configured for the instance"); + } + tac->ctr.reserve_priv = mi->tip_reserve; + extra = json_object_get (tac->root, "extra"); + if (NULL == extra) + extra = json_object (); + else + json_incref (extra); + + + db->preflight (db->cls); + ec = db->authorize_tip_TR (db->cls, + tac->justification, + extra, + &tac->amount, + &mi->tip_reserve, + mi->tip_exchange, + &expiration, + &tip_id); + json_decref (extra); + /* If we have insufficient funds according to OUR database, + check with exchange to see if the reserve has been topped up + in the meantime (or if tips were not withdrawn yet). */ + if ( (TALER_EC_TIP_AUTHORIZE_INSUFFICIENT_FUNDS == ec) && + (GNUNET_NO == tac->checked_status) ) + { + tac->checked_status = GNUNET_YES; + tac->ctr.none_authorized = GNUNET_YES; + TMH_check_tip_reserve (&tac->ctr, + mi->tip_exchange); + return MHD_YES; + } + + /* handle irrecoverable errors */ + if (TALER_EC_NONE != ec) + { + unsigned int rc; + const char *msg; + + switch (ec) + { + case TALER_EC_TIP_AUTHORIZE_INSUFFICIENT_FUNDS: + rc = MHD_HTTP_PRECONDITION_FAILED; + msg = "Failed to approve tip: merchant has insufficient tipping funds"; + break; + case TALER_EC_TIP_AUTHORIZE_RESERVE_EXPIRED: + msg = "Failed to approve tip: merchant's tipping reserve expired"; + rc = MHD_HTTP_PRECONDITION_FAILED; + break; + case TALER_EC_TIP_AUTHORIZE_RESERVE_UNKNOWN: + msg = "Failed to approve tip: merchant's tipping reserve does not exist"; + rc = MHD_HTTP_SERVICE_UNAVAILABLE; + break; + default: + rc = MHD_HTTP_INTERNAL_SERVER_ERROR; + msg = "Failed to approve tip: internal server error"; + break; + } + + return TALER_MHD_reply_with_error (connection, + rc, + ec, + msg); + } + + /* generate success response */ + { + char *taler_tip_uri; + const char *host; + const char *forwarded_host; + const char *uri_path; + const char *uri_instance_id; + struct GNUNET_CRYPTO_HashAsciiEncoded hash_enc; + + host = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, "Host"); + forwarded_host = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, + "X-Forwarded-Host"); + + uri_path = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, + "X-Forwarded-Prefix"); + if (NULL == uri_path) + uri_path = "-"; + + if (NULL != forwarded_host) + host = forwarded_host; + + if (NULL == host) + { + /* Should never happen, at last the host header should be defined */ + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_INTERNAL_INVARIANT_FAILURE, + "unable to identify backend host"); + } + + if (0 == strcmp (mi->id, "default")) + uri_instance_id = "-"; + else + uri_instance_id = mi->id; + + GNUNET_CRYPTO_hash_to_enc (&tip_id, &hash_enc); + + GNUNET_assert (0 < GNUNET_asprintf (&taler_tip_uri, + "taler://tip/%s/%s/%s/%s", + host, + uri_path, + uri_instance_id, + hash_enc.encoding)); + + return TALER_MHD_reply_json_pack (connection, + MHD_HTTP_OK, + "{s:s, s:s}", + "taler_tip_uri", taler_tip_uri, + "tip_id", hash_enc.encoding); + } +} + + +/* end of taler-merchant-httpd_tip-authorize.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-reserves-ID-authorize-tip.h b/src/backend/taler-merchant-httpd_private-post-reserves-ID-authorize-tip.h new file mode 100644 index 00000000..1f7f44ea --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-post-reserves-ID-authorize-tip.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + (C) 2017 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_tip-authorize.h + * @brief headers for /tip-authorize handler + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_TIP_AUTHORIZE_H +#define TALER_MERCHANT_HTTPD_TIP_AUTHORIZE_H +#include +#include "taler-merchant-httpd.h" + +/** + * Manages a /tip-authorize call, creating a TIP ID and storing the + * authorization in our DB. + * + * @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 + * @return MHD result code + */ +MHD_RESULT +MH_handler_tip_authorize (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi); + +#endif diff --git a/src/backend/taler-merchant-httpd_refund.h b/src/backend/taler-merchant-httpd_refund.h deleted file mode 100644 index f0fb44dd..00000000 --- a/src/backend/taler-merchant-httpd_refund.h +++ /dev/null @@ -1,71 +0,0 @@ -/* - This file is part of TALER - (C) 2014, 2015, 2016, 2017 INRIA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ - -/** - * @file backend/taler-merchant-httpd_refund.c - * @brief HTTP serving layer mainly intended to communicate with the frontend - * @author Marcello Stanisci - */ - -#ifndef TALER_MERCHANT_HTTPD_REFUND_H -#define TALER_MERCHANT_HTTPD_REFUND_H -#include -#include "taler-merchant-httpd.h" - - -/** - * Return refund situation about a contract. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_refund_lookup (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi); - - -/** - * Force resuming all suspended refund lookups, needed during shutdown. - */ -void -MH_force_refund_resume (void); - - -/** - * Get the JSON representation of a refund. - * - * @param merchant_pub the merchant's public key - * @param mi merchant instance - * @param ret_ec where to store error code - * @param ret_errmsg where to store error message - * @return NULL on error, JSON array with refunds on success - */ -json_t * -TM_get_refund_json (const struct MerchantInstance *mi, - const struct GNUNET_HashCode *h_contract_terms, - enum TALER_ErrorCode *ret_ec, - const char **ret_errmsg); - -#endif diff --git a/src/backend/taler-merchant-httpd_reserves_get.h b/src/backend/taler-merchant-httpd_reserves_get.h deleted file mode 100644 index f180546d..00000000 --- a/src/backend/taler-merchant-httpd_reserves_get.h +++ /dev/null @@ -1,153 +0,0 @@ -/* - This file is part of TALER - (C) 2018--2019 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_tip-reserve-helper.h - * @brief helper functions to check the status of a tipping reserve - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_TIP_RESERVE_HELPER_H -#define TALER_MERCHANT_HTTPD_TIP_RESERVE_HELPER_H -#include -#include -#include -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_mhd.h" -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_tip-reserve-helper.h" - - -/** - * Context with input, output and internal state for - * #TMH_check_tip_reserve() and #TMH_check_tip_reserve_cleanup(). - */ -struct TMH_CheckTipReserve -{ - /** - * Input: MHD connection we should resume when finished - */ - struct MHD_Connection *connection; - - /** - * Input: private key of the reserve. - */ - struct TALER_ReservePrivateKeyP reserve_priv; - - /** - * Output: Set to delay after which the reserve will expire if idle. - */ - struct GNUNET_TIME_Relative idle_reserve_expiration_time; - - /** - * Internal: exchange find operation. - */ - struct TMH_EXCHANGES_FindOperation *fo; - - /** - * Internal: reserve status operation. - */ - struct TALER_EXCHANGE_ReservesGetHandle *rsh; - - /** - * Internal: DLL for resumption on shutdown. - */ - struct TMH_CheckTipReserve *next; - - /** - * Internal: DLL for resumption on shutdown. - */ - struct TMH_CheckTipReserve *prev; - - /** - * Output: response object to return (on error only) - */ - struct MHD_Response *response; - - /** - * Output: Total amount deposited into the reserve. - */ - struct TALER_Amount amount_deposited; - - /** - * Output: total tip amount requested. - */ - struct TALER_Amount amount_withdrawn; - - /** - * Input: total amount authorized. - */ - struct TALER_Amount amount_authorized; - - /** - * Output: set to the time when the reserve will expire - */ - struct GNUNET_TIME_Absolute reserve_expiration; - - /** - * Output: HTTP status code to return (on error only) - */ - unsigned int response_code; - - /** - * Input: Set to #GNUNET_NO if no tips were authorized yet. - * Used to know that @e amount_authorized is not yet initialized - * and in that case the helper will set it to zero (once we know - * the currency). - */ - int none_authorized; - - /** - * Internal: Is the @e connection currently suspended? - * #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. - */ - int suspended; - -}; - - -/** - * Check the status of the given reserve at the given exchange. - * Suspends the MHD connection while this is happening and resumes - * processing once we know the reserve status (or once an error - * code has been determined). - * - * @param[in,out] ctr context for checking the reserve status - * @param tip_exchange the URL of the exchange to query - */ -void -TMH_check_tip_reserve (struct TMH_CheckTipReserve *ctr, - const char *tip_exchange); - - -/** - * Clean up any state that might be left in @a ctr. - * - * @param[in] context to clean up - */ -void -TMH_check_tip_reserve_cleanup (struct TMH_CheckTipReserve *ctr); - -/** - * Force all tip reserve helper contexts to be resumed as we are about to shut - * down MHD. - */ -void -MH_force_trh_resume (void); - - -#endif diff --git a/src/backend/taler-merchant-httpd_reserves_reserve_get.h b/src/backend/taler-merchant-httpd_reserves_reserve_get.h deleted file mode 100644 index 3123486c..00000000 --- a/src/backend/taler-merchant-httpd_reserves_reserve_get.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - This file is part of TALER - (C) 2017 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_tip-query.h - * @brief headers for /tip-query handler - * @author Florian Dold - */ -#ifndef TALER_MERCHANT_HTTPD_TIP_QUERY_H -#define TALER_MERCHANT_HTTPD_TIP_QUERY_H -#include -#include "taler-merchant-httpd.h" - -/** - * Manages a /tip-query call. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_tip_query (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi); - -#endif diff --git a/src/backend/taler-merchant-httpd_tips_post.h b/src/backend/taler-merchant-httpd_tips_post.h deleted file mode 100644 index 1f7f44ea..00000000 --- a/src/backend/taler-merchant-httpd_tips_post.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - This file is part of TALER - (C) 2017 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_tip-authorize.h - * @brief headers for /tip-authorize handler - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_TIP_AUTHORIZE_H -#define TALER_MERCHANT_HTTPD_TIP_AUTHORIZE_H -#include -#include "taler-merchant-httpd.h" - -/** - * Manages a /tip-authorize call, creating a TIP ID and storing the - * authorization in our DB. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_tip_authorize (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi); - -#endif diff --git a/src/backend/taler-merchant-httpd_tips_tip_pickup.h b/src/backend/taler-merchant-httpd_tips_tip_pickup.h deleted file mode 100644 index 6fdba31a..00000000 --- a/src/backend/taler-merchant-httpd_tips_tip_pickup.h +++ /dev/null @@ -1,75 +0,0 @@ -/* - This file is part of TALER - (C) 2017 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_tip-pickup.h - * @brief headers for /tip-pickup handler - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_TIP_PICKUP_H -#define TALER_MERCHANT_HTTPD_TIP_PICKUP_H -#include -#include "taler-merchant-httpd.h" - - -/** - * We are shutting down, force resuming all suspended pickup operations. - */ -void -MH_force_tip_pickup_resume (void); - - -/** - * Manages a POST /tip-pickup call, checking that the tip is authorized, - * and if so, returning the withdrawal permissions. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_tip_pickup (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi); - - -/** - * Manages a GET /tip-pickup call. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_tip_pickup_get (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi); - - -#endif diff --git a/src/backend/taler-merchant-httpd_track-transaction.h b/src/backend/taler-merchant-httpd_track-transaction.h deleted file mode 100644 index d6201107..00000000 --- a/src/backend/taler-merchant-httpd_track-transaction.h +++ /dev/null @@ -1,47 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2020 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_track-transaction.h - * @brief headers for /track/transaction handler - * @author Christian Grothoff - * @author Marcello Stanisci - */ -#ifndef TALER_MERCHANT_HTTPD_TRACK_TRANSACTION_H -#define TALER_MERCHANT_HTTPD_TRACK_TRANSACTION_H -#include -#include "taler-merchant-httpd.h" - -/** - * Handle a "/track/transaction" request. - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_track_transaction (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi); - - -#endif diff --git a/src/backend/taler-merchant-httpd_transfers-get.h b/src/backend/taler-merchant-httpd_transfers-get.h deleted file mode 100644 index 0463295e..00000000 --- a/src/backend/taler-merchant-httpd_transfers-get.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2020 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_track-transfer.h - * @brief headers for /track/transfer handler - * @author Christian Grothoff - * @author Marcello Stanisci - */ -#ifndef TALER_MERCHANT_HTTPD_TRACK_TRANSFER_H -#define TALER_MERCHANT_HTTPD_TRACK_TRANSFER_H -#include -#include "taler-merchant-httpd.h" - -/** - * Manages a /track/transfer call, thus it calls the /wire/transfer - * offered by the exchange in order to return the set of transfers - * (of coins) associated with a given wire transfer - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_track_transfer (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi); - - -#endif diff --git a/src/backend/taler-merchant-httpd_transfers-post.h b/src/backend/taler-merchant-httpd_transfers-post.h deleted file mode 100644 index 0463295e..00000000 --- a/src/backend/taler-merchant-httpd_transfers-post.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2020 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see -*/ -/** - * @file backend/taler-merchant-httpd_track-transfer.h - * @brief headers for /track/transfer handler - * @author Christian Grothoff - * @author Marcello Stanisci - */ -#ifndef TALER_MERCHANT_HTTPD_TRACK_TRANSFER_H -#define TALER_MERCHANT_HTTPD_TRACK_TRANSFER_H -#include -#include "taler-merchant-httpd.h" - -/** - * Manages a /track/transfer call, thus it calls the /wire/transfer - * offered by the exchange in order to return the set of transfers - * (of coins) associated with a given wire transfer - * - * @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 - * @return MHD result code - */ -MHD_RESULT -MH_handler_track_transfer (struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size, - struct MerchantInstance *mi); - - -#endif -- cgit v1.2.3