diff options
author | Christian Grothoff <christian@grothoff.org> | 2020-04-16 21:02:14 +0200 |
---|---|---|
committer | Christian Grothoff <christian@grothoff.org> | 2020-04-16 21:02:14 +0200 |
commit | 0a327ceebd3126d4adf69916e92702fe3c7a22e2 (patch) | |
tree | 8c41e76fcd0c462c83ad491cb05d55be96ca1d08 /src/backend | |
parent | e28829955091835b7d4b15c04db242cff0accbcc (diff) | |
download | merchant-0a327ceebd3126d4adf69916e92702fe3c7a22e2.tar.gz merchant-0a327ceebd3126d4adf69916e92702fe3c7a22e2.tar.bz2 merchant-0a327ceebd3126d4adf69916e92702fe3c7a22e2.zip |
complete first draft of new SQL schema
Diffstat (limited to 'src/backend')
-rw-r--r-- | src/backend/taler-merchant-httpd_get-orders.c (renamed from src/backend/taler-merchant-httpd_history.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_get-reserves-reserve.c (renamed from src/backend/taler-merchant-httpd_tip-query.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_get-reserves.c (renamed from src/backend/taler-merchant-httpd_tip-reserve-helper.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_get-tips-tip.c (renamed from src/backend/taler-merchant-httpd_tip-pickup_get.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_get-transfers.c (renamed from src/backend/taler-merchant-httpd_track-transfer.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_get.h (renamed from src/backend/taler-merchant-httpd_history.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_abort.h (renamed from src/backend/taler-merchant-httpd_pay.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get.c (renamed from src/backend/taler-merchant-httpd_poll-payment.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get.h (renamed from src/backend/taler-merchant-httpd_poll-payment.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get2.c (renamed from src/backend/taler-merchant-httpd_refund_lookup.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get2.h (renamed from src/backend/taler-merchant-httpd_refund_lookup.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get3.c (renamed from src/backend/taler-merchant-httpd_track-transaction.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get4.c (renamed from src/backend/taler-merchant-httpd_check-payment.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get5.c (renamed from src/backend/taler-merchant-httpd_proposal.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get5.h (renamed from src/backend/taler-merchant-httpd_proposal.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_pay.c (renamed from src/backend/taler-merchant-httpd_pay.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_pay.h | 54 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_refund.c (renamed from src/backend/taler-merchant-httpd_refund_increase.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_refund.h (renamed from src/backend/taler-merchant-httpd_refund_increase.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_post.h (renamed from src/backend/taler-merchant-httpd_order.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_post-orders-order-abort.c | 2255 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_post-orders.c (renamed from src/backend/taler-merchant-httpd_order.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_post-tips-tip-pickup.c (renamed from src/backend/taler-merchant-httpd_tip-pickup.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_post-tips.c (renamed from src/backend/taler-merchant-httpd_tip-authorize.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_post-transfers.c | 1089 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_reserves_get.h (renamed from src/backend/taler-merchant-httpd_tip-reserve-helper.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_reserves_reserve_get.h (renamed from src/backend/taler-merchant-httpd_tip-query.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_responses.c (renamed from src/backend/taler-merchant-httpd_refund.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_tips_post.h (renamed from src/backend/taler-merchant-httpd_tip-authorize.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_tips_tip_pickup.h (renamed from src/backend/taler-merchant-httpd_tip-pickup.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_transfers-post.h (renamed from src/backend/taler-merchant-httpd_track-transfer.h) | 0 |
31 files changed, 3398 insertions, 0 deletions
diff --git a/src/backend/taler-merchant-httpd_history.c b/src/backend/taler-merchant-httpd_get-orders.c index dd353208..dd353208 100644 --- a/src/backend/taler-merchant-httpd_history.c +++ b/src/backend/taler-merchant-httpd_get-orders.c diff --git a/src/backend/taler-merchant-httpd_tip-query.c b/src/backend/taler-merchant-httpd_get-reserves-reserve.c index f7aa0ab0..f7aa0ab0 100644 --- a/src/backend/taler-merchant-httpd_tip-query.c +++ b/src/backend/taler-merchant-httpd_get-reserves-reserve.c diff --git a/src/backend/taler-merchant-httpd_tip-reserve-helper.c b/src/backend/taler-merchant-httpd_get-reserves.c index e104e089..e104e089 100644 --- a/src/backend/taler-merchant-httpd_tip-reserve-helper.c +++ b/src/backend/taler-merchant-httpd_get-reserves.c diff --git a/src/backend/taler-merchant-httpd_tip-pickup_get.c b/src/backend/taler-merchant-httpd_get-tips-tip.c index 42066e3c..42066e3c 100644 --- a/src/backend/taler-merchant-httpd_tip-pickup_get.c +++ b/src/backend/taler-merchant-httpd_get-tips-tip.c diff --git a/src/backend/taler-merchant-httpd_track-transfer.c b/src/backend/taler-merchant-httpd_get-transfers.c index 7f55c917..7f55c917 100644 --- a/src/backend/taler-merchant-httpd_track-transfer.c +++ b/src/backend/taler-merchant-httpd_get-transfers.c diff --git a/src/backend/taler-merchant-httpd_history.h b/src/backend/taler-merchant-httpd_orders_get.h index eac987dd..eac987dd 100644 --- a/src/backend/taler-merchant-httpd_history.h +++ b/src/backend/taler-merchant-httpd_orders_get.h diff --git a/src/backend/taler-merchant-httpd_pay.h b/src/backend/taler-merchant-httpd_orders_order_abort.h index 726a27be..726a27be 100644 --- a/src/backend/taler-merchant-httpd_pay.h +++ b/src/backend/taler-merchant-httpd_orders_order_abort.h diff --git a/src/backend/taler-merchant-httpd_poll-payment.c b/src/backend/taler-merchant-httpd_orders_order_get.c index 6ca4fcce..6ca4fcce 100644 --- a/src/backend/taler-merchant-httpd_poll-payment.c +++ b/src/backend/taler-merchant-httpd_orders_order_get.c diff --git a/src/backend/taler-merchant-httpd_poll-payment.h b/src/backend/taler-merchant-httpd_orders_order_get.h index ac13c4a3..ac13c4a3 100644 --- a/src/backend/taler-merchant-httpd_poll-payment.h +++ b/src/backend/taler-merchant-httpd_orders_order_get.h diff --git a/src/backend/taler-merchant-httpd_refund_lookup.c b/src/backend/taler-merchant-httpd_orders_order_get2.c index e86e4e4b..e86e4e4b 100644 --- a/src/backend/taler-merchant-httpd_refund_lookup.c +++ b/src/backend/taler-merchant-httpd_orders_order_get2.c diff --git a/src/backend/taler-merchant-httpd_refund_lookup.h b/src/backend/taler-merchant-httpd_orders_order_get2.h index 24495daf..24495daf 100644 --- a/src/backend/taler-merchant-httpd_refund_lookup.h +++ b/src/backend/taler-merchant-httpd_orders_order_get2.h diff --git a/src/backend/taler-merchant-httpd_track-transaction.c b/src/backend/taler-merchant-httpd_orders_order_get3.c index 39f8ce9e..39f8ce9e 100644 --- a/src/backend/taler-merchant-httpd_track-transaction.c +++ b/src/backend/taler-merchant-httpd_orders_order_get3.c diff --git a/src/backend/taler-merchant-httpd_check-payment.c b/src/backend/taler-merchant-httpd_orders_order_get4.c index bb5384d1..bb5384d1 100644 --- a/src/backend/taler-merchant-httpd_check-payment.c +++ b/src/backend/taler-merchant-httpd_orders_order_get4.c diff --git a/src/backend/taler-merchant-httpd_proposal.c b/src/backend/taler-merchant-httpd_orders_order_get5.c index 47207131..47207131 100644 --- a/src/backend/taler-merchant-httpd_proposal.c +++ b/src/backend/taler-merchant-httpd_orders_order_get5.c diff --git a/src/backend/taler-merchant-httpd_proposal.h b/src/backend/taler-merchant-httpd_orders_order_get5.h index 677fee0e..677fee0e 100644 --- a/src/backend/taler-merchant-httpd_proposal.h +++ b/src/backend/taler-merchant-httpd_orders_order_get5.h diff --git a/src/backend/taler-merchant-httpd_pay.c b/src/backend/taler-merchant-httpd_orders_order_pay.c index 7a1b7fd8..7a1b7fd8 100644 --- a/src/backend/taler-merchant-httpd_pay.c +++ b/src/backend/taler-merchant-httpd_orders_order_pay.c diff --git a/src/backend/taler-merchant-httpd_orders_order_pay.h b/src/backend/taler-merchant-httpd_orders_order_pay.h new file mode 100644 index 00000000..726a27be --- /dev/null +++ b/src/backend/taler-merchant-httpd_orders_order_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 <http://www.gnu.org/licenses/> +*/ +/** + * @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 <microhttpd.h> +#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_refund_increase.c b/src/backend/taler-merchant-httpd_orders_order_refund.c index 5324c619..5324c619 100644 --- a/src/backend/taler-merchant-httpd_refund_increase.c +++ b/src/backend/taler-merchant-httpd_orders_order_refund.c diff --git a/src/backend/taler-merchant-httpd_refund_increase.h b/src/backend/taler-merchant-httpd_orders_order_refund.h index ff178001..ff178001 100644 --- a/src/backend/taler-merchant-httpd_refund_increase.h +++ b/src/backend/taler-merchant-httpd_orders_order_refund.h diff --git a/src/backend/taler-merchant-httpd_order.h b/src/backend/taler-merchant-httpd_orders_post.h index cf43d1ba..cf43d1ba 100644 --- a/src/backend/taler-merchant-httpd_order.h +++ b/src/backend/taler-merchant-httpd_orders_post.h diff --git a/src/backend/taler-merchant-httpd_post-orders-order-abort.c b/src/backend/taler-merchant-httpd_post-orders-order-abort.c new file mode 100644 index 00000000..7a1b7fd8 --- /dev/null +++ b/src/backend/taler-merchant-httpd_post-orders-order-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 <http://www.gnu.org/licenses/> +*/ + +/** + * @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 <jansson.h> +#include <gnunet/gnunet_util_lib.h> +#include <taler/taler_signatures.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_exchange_service.h> +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_auditors.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_refund.h" + + +/** + * How long to wait before giving up processing with the exchange? + */ +#define PAY_TIMEOUT (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, \ + 30)) + +/** + * 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; i<pc->coins_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; i<pc->coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->dc[i]; + + if (NULL != dc->denom.rsa_public_key) + { + GNUNET_CRYPTO_rsa_public_key_free (dc->denom.rsa_public_key); + dc->denom.rsa_public_key = NULL; + } + if (NULL != dc->ub_sig.rsa_signature) + { + GNUNET_CRYPTO_rsa_signature_free (dc->ub_sig.rsa_signature); + dc->ub_sig.rsa_signature = NULL; + } + GNUNET_free_non_null (dc->exchange_url); + } + GNUNET_free_non_null (pc->dc); + if (NULL != pc->fo) + { + TMH_EXCHANGES_find_exchange_cancel (pc->fo); + pc->fo = NULL; + } + if (NULL != pc->response) + { + MHD_destroy_response (pc->response); + pc->response = NULL; + } + if (NULL != pc->contract_terms) + { + json_decref (pc->contract_terms); + pc->contract_terms = NULL; + } + GNUNET_free_non_null (pc->order_id); + GNUNET_free_non_null (pc->session_id); + GNUNET_free_non_null (pc->fulfillment_url); + GNUNET_CONTAINER_DLL_remove (pc_head, + pc_tail, + pc); + GNUNET_free (pc); +} + + +/** + * 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; i<pc->coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->dc[i]; + + GNUNET_assert (GNUNET_YES == dc->found_in_db); + if ( (0 > + TALER_amount_add (&acc_fee, + &dc->deposit_fee, + &acc_fee)) || + (0 > + TALER_amount_add (&acc_amount, + &dc->amount_with_fee, + &acc_amount)) ) + { + GNUNET_break (0); + /* Overflow in these amounts? Very strange. */ + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_AMOUNT_OVERFLOW, + "Overflow adding up amounts"); + } + if (1 == + TALER_amount_cmp (&dc->deposit_fee, + &dc->amount_with_fee)) + { + GNUNET_break_op (0); + resume_pay_with_error (pc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PAY_FEES_EXCEED_PAYMENT, + "Deposit fees exceed coin's contribution"); + return GNUNET_SYSERR; + } + + /* If exchange differs, add wire fee */ + { + int new_exchange = GNUNET_YES; + + for (unsigned int j = 0; j<i; j++) + if (0 == strcasecmp (dc->exchange_url, + pc->dc[j].exchange_url)) + { + new_exchange = GNUNET_NO; + break; + } + if (GNUNET_YES == new_exchange) + { + if (GNUNET_OK != + TALER_amount_cmp_currency (&total_wire_fee, + &dc->wire_fee)) + { + GNUNET_break_op (0); + resume_pay_with_error (pc, + MHD_HTTP_PRECONDITION_FAILED, + TALER_EC_PAY_WIRE_FEE_CURRENCY_MISMATCH, + "exchange wire in different currency"); + return GNUNET_SYSERR; + } + if (0 > + TALER_amount_add (&total_wire_fee, + &total_wire_fee, + &dc->wire_fee)) + { + GNUNET_break (0); + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED, + "could not add exchange wire fee to total"); + return GNUNET_SYSERR; + } + } + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Amount received from wallet: %s\n", + TALER_amount2s (&acc_amount)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Deposit fee for all coins: %s\n", + TALER_amount2s (&acc_fee)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Total wire fee: %s\n", + TALER_amount2s (&total_wire_fee)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Max wire fee: %s\n", + TALER_amount2s (&pc->max_wire_fee)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Deposit fee limit for merchant: %s\n", + TALER_amount2s (&pc->max_fee)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Total refunded amount: %s\n", + TALER_amount2s (&pc->total_refunded)); + + /* Now compare exchange wire fee compared to + * what we are willing to pay */ + if (GNUNET_YES != + TALER_amount_cmp_currency (&total_wire_fee, + &pc->max_wire_fee)) + { + 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; i<pc->coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->dc[i]; + const struct TALER_EXCHANGE_DenomPublicKey *denom_details; + enum TALER_ErrorCode ec; + unsigned int hc; + + if (NULL != dc->dh) + continue; /* we were here before (can happen due to + tried_force_keys logic), don't go again */ + if (GNUNET_YES == dc->found_in_db) + 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; i<pc->coins_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; i<pc->coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->dc[i]; + + if (GNUNET_YES == dc->found_in_db) + continue; /* processed earlier */ + + /* Get matching coin from results*/ + if ( (0 != GNUNET_memcmp (coin_pub, + &dc->coin_pub)) || + (0 != TALER_amount_cmp (amount_with_fee, + &dc->amount_with_fee)) ) + continue; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Coin (%s) already found in our DB.\n", + TALER_b2s (coin_pub, + sizeof (*coin_pub))); + if (0 > + TALER_amount_add (&pc->total_paid, + &pc->total_paid, + amount_with_fee)) + { + /* We accepted this coin for payment on this contract before, + and now we can't even add the amount!? */ + GNUNET_break (0); + continue; + } + if (0 > + TALER_amount_add (&pc->total_fees_paid, + &pc->total_fees_paid, + deposit_fee)) + { + /* We accepted this coin for payment on this contract before, + and now we can't even add the amount!? */ + GNUNET_break (0); + continue; + } + dc->deposit_fee = *deposit_fee; + dc->refund_fee = *refund_fee; + dc->wire_fee = *wire_fee; + dc->amount_with_fee = *amount_with_fee; + dc->found_in_db = GNUNET_YES; + pc->pending--; + } +} + + +/** + * Try to parse the pay request into the given pay context. + * Schedules an error response in the connection on failure. + * + * @param connection HTTP connection we are receiving payment on + * @param root JSON upload with payment data + * @param pc context we use to handle the payment + * @return #GNUNET_OK on success, + * #GNUNET_NO on failure (response was queued with MHD) + * #GNUNET_SYSERR on hard error (MHD connection must be dropped) + */ +static enum GNUNET_GenericReturnValue +parse_pay (struct MHD_Connection *connection, + const json_t *root, + struct PayContext *pc) +{ + 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; i<pc->coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->dc[i]; + + /* Get matching coin from results*/ + if (0 == GNUNET_memcmp (coin_pub, + &dc->coin_pub)) + { + dc->refunded = GNUNET_YES; + GNUNET_assert (0 <= + TALER_amount_add (&pc->total_refunded, + &pc->total_refunded, + refund_amount)); + } + } +} + + +/** + * 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; i<pc->coins_cnt; i++) + { + struct TALER_MerchantSignatureP msig; + struct TALER_RefundRequestPS rr = { + .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_REFUND), + .purpose.size = htonl (sizeof (rr)), + .h_contract_terms = pc->h_contract_terms, + .coin_pub = pc->dc[i].coin_pub, + .merchant = pc->mi->pubkey, + .rtransaction_id = GNUNET_htonll (0) + }; + + 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_order.c b/src/backend/taler-merchant-httpd_post-orders.c index 317e451b..317e451b 100644 --- a/src/backend/taler-merchant-httpd_order.c +++ b/src/backend/taler-merchant-httpd_post-orders.c diff --git a/src/backend/taler-merchant-httpd_tip-pickup.c b/src/backend/taler-merchant-httpd_post-tips-tip-pickup.c index 51dd8121..51dd8121 100644 --- a/src/backend/taler-merchant-httpd_tip-pickup.c +++ b/src/backend/taler-merchant-httpd_post-tips-tip-pickup.c diff --git a/src/backend/taler-merchant-httpd_tip-authorize.c b/src/backend/taler-merchant-httpd_post-tips.c index 569cf0ab..569cf0ab 100644 --- a/src/backend/taler-merchant-httpd_tip-authorize.c +++ b/src/backend/taler-merchant-httpd_post-tips.c diff --git a/src/backend/taler-merchant-httpd_post-transfers.c b/src/backend/taler-merchant-httpd_post-transfers.c new file mode 100644 index 00000000..7f55c917 --- /dev/null +++ b/src/backend/taler-merchant-httpd_post-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 <http://www.gnu.org/licenses/> +*/ +/** + * @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 <jansson.h> +#include <taler/taler_signatures.h> +#include <taler/taler_json_lib.h> +#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; i<MAX_RETRIES; i++) + { + db->preflight (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; i<details_length; i++) + { + rctx->current_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; r<MAX_RETRIES; r++) + { + db->preflight (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_tip-reserve-helper.h b/src/backend/taler-merchant-httpd_reserves_get.h index f180546d..f180546d 100644 --- a/src/backend/taler-merchant-httpd_tip-reserve-helper.h +++ b/src/backend/taler-merchant-httpd_reserves_get.h diff --git a/src/backend/taler-merchant-httpd_tip-query.h b/src/backend/taler-merchant-httpd_reserves_reserve_get.h index 3123486c..3123486c 100644 --- a/src/backend/taler-merchant-httpd_tip-query.h +++ b/src/backend/taler-merchant-httpd_reserves_reserve_get.h diff --git a/src/backend/taler-merchant-httpd_refund.c b/src/backend/taler-merchant-httpd_responses.c index 62ebf451..62ebf451 100644 --- a/src/backend/taler-merchant-httpd_refund.c +++ b/src/backend/taler-merchant-httpd_responses.c diff --git a/src/backend/taler-merchant-httpd_tip-authorize.h b/src/backend/taler-merchant-httpd_tips_post.h index 1f7f44ea..1f7f44ea 100644 --- a/src/backend/taler-merchant-httpd_tip-authorize.h +++ b/src/backend/taler-merchant-httpd_tips_post.h diff --git a/src/backend/taler-merchant-httpd_tip-pickup.h b/src/backend/taler-merchant-httpd_tips_tip_pickup.h index 6fdba31a..6fdba31a 100644 --- a/src/backend/taler-merchant-httpd_tip-pickup.h +++ b/src/backend/taler-merchant-httpd_tips_tip_pickup.h diff --git a/src/backend/taler-merchant-httpd_track-transfer.h b/src/backend/taler-merchant-httpd_transfers-post.h index 0463295e..0463295e 100644 --- a/src/backend/taler-merchant-httpd_track-transfer.h +++ b/src/backend/taler-merchant-httpd_transfers-post.h |