From 5da121e9b0ec83f20a1a404f7049f9ff19aca32b Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Mon, 27 Apr 2020 16:32:59 +0200 Subject: implement order claiming --- src/backend/taler-merchant-httpd.c | 2 +- .../taler-merchant-httpd_private-post-transfers.c | 1089 ++++++++++++++++++++ .../taler-merchant-httpd_private-post-transfers.h | 49 + 3 files changed, 1139 insertions(+), 1 deletion(-) create mode 100644 src/backend/taler-merchant-httpd_private-post-transfers.c create mode 100644 src/backend/taler-merchant-httpd_private-post-transfers.h (limited to 'src/backend') diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c index f11d18ca..7d255b9e 100644 --- a/src/backend/taler-merchant-httpd.c +++ b/src/backend/taler-merchant-httpd.c @@ -908,7 +908,7 @@ url_handler (void *cls, (GNUNET_YES != GNUNET_CURL_is_valid_scope_id (correlation_id)) ) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "illegal incoming correlation ID\n"); + "Illegal incoming correlation ID\n"); correlation_id = NULL; } if (NULL != correlation_id) diff --git a/src/backend/taler-merchant-httpd_private-post-transfers.c b/src/backend/taler-merchant-httpd_private-post-transfers.c new file mode 100644 index 00000000..7f55c917 --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-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 +*/ +/** + * @file backend/taler-merchant-httpd_track-transfer.c + * @brief implement API for tracking transfers and wire transfers + * @author Marcello Stanisci + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include +#include +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_mhd.h" +#include "taler-merchant-httpd_auditors.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_track-transfer.h" + + +/** + * How long to wait before giving up processing with the exchange? + */ +#define TRACK_TIMEOUT (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, \ + 30)) + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + +/** + * Context used for handing /track/transfer requests. + */ +struct TrackTransferContext +{ + + /** + * This MUST be first! + */ + struct TM_HandlerContext hc; + + /** + * Handle to the exchange. + */ + struct TALER_EXCHANGE_Handle *eh; + + /** + * Handle for the /wire/transfers request. + */ + struct TALER_EXCHANGE_TransfersGetHandle *wdh; + + /** + * For which merchant instance is this tracking request? + */ + struct MerchantInstance *mi; + + /** + * HTTP connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * Response to return upon resume. + */ + struct MHD_Response *response; + + /** + * Handle for operation to lookup /keys (and auditors) from + * the exchange used for this transaction; NULL if no operation is + * pending. + */ + struct TMH_EXCHANGES_FindOperation *fo; + + /** + * Task run on timeout. + */ + struct GNUNET_SCHEDULER_Task *timeout_task; + + /** + * URL of the exchange. + */ + char *url; + + /** + * Wire method used for the transfer. + */ + char *wire_method; + + /** + * Pointer to the detail that we are currently + * checking in #check_transfer(). + */ + const struct TALER_TrackTransferDetails *current_detail; + + /** + * Argument for the /wire/transfers request. + */ + struct TALER_WireTransferIdentifierRawP wtid; + + /** + * Full original response we are currently processing. + */ + const json_t *original_response; + + /** + * Modified response to return to the frontend. + */ + json_t *deposits_response; + + /** + * Which transaction detail are we currently looking at? + */ + unsigned int current_offset; + + /** + * Response code to return. + */ + unsigned int response_code; + + /** + * #GNUNET_NO if we did not find a matching coin. + * #GNUNET_SYSERR if we found a matching coin, but the amounts do not match. + * #GNUNET_OK if we did find a matching coin. + */ + int check_transfer_result; +}; + + +/** + * Represents an entry in the table used to sum up + * individual deposits for each h_contract_terms. + */ +struct Entry +{ + + /** + * Sum accumulator for deposited value. + */ + struct TALER_Amount deposit_value; + + /** + * Sum accumulator for deposit fee. + */ + struct TALER_Amount deposit_fee; + +}; + + +/** + * Free the @a rctx. + * + * @param rctx data to free + */ +static void +free_transfer_track_context (struct TrackTransferContext *rctx) +{ + if (NULL != rctx->fo) + { + TMH_EXCHANGES_find_exchange_cancel (rctx->fo); + rctx->fo = NULL; + } + if (NULL != rctx->timeout_task) + { + GNUNET_SCHEDULER_cancel (rctx->timeout_task); + rctx->timeout_task = NULL; + } + if (NULL != rctx->wdh) + { + TALER_EXCHANGE_transfers_get_cancel (rctx->wdh); + rctx->wdh = NULL; + } + if (NULL != rctx->url) + { + GNUNET_free (rctx->url); + rctx->url = NULL; + } + if (NULL != rctx->wire_method) + { + GNUNET_free (rctx->wire_method); + rctx->wire_method = NULL; + } + GNUNET_free (rctx); +} + + +/** + * Callback that frees all the elements in the hashmap + * + * @param cls closure, NULL + * @param key current key + * @param value a `struct Entry` + * @return #GNUNET_YES if the iteration should continue, + * #GNUNET_NO otherwise. + */ +static int +hashmap_free (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + struct TALER_Entry *entry = value; + + (void) cls; + (void) key; + GNUNET_free (entry); + return GNUNET_YES; +} + + +/** + * Builds JSON response containing the summed-up amounts + * from individual deposits. + * + * @param cls closure + * @param key map's current key + * @param map's current value + * @return #GNUNET_YES if iteration is to be continued, + * #GNUNET_NO otherwise. + */ +static int +build_deposits_response (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + struct TrackTransferContext *rctx = cls; + struct Entry *entry = value; + json_t *element; + json_t *contract_terms; + json_t *order_id; + + db->preflight (db->cls); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + db->find_contract_terms_from_hash (db->cls, + &contract_terms, + key, + &rctx->mi->pubkey)) + { + GNUNET_break_op (0); + return GNUNET_NO; + } + + order_id = json_object_get (contract_terms, + "order_id"); + if (NULL == order_id) + { + GNUNET_break_op (0); + json_decref (contract_terms); + return GNUNET_NO; + } + element = json_pack ("{s:O, s:o, s:o}", + "order_id", order_id, + "deposit_value", TALER_JSON_from_amount ( + &entry->deposit_value), + "deposit_fee", TALER_JSON_from_amount ( + &entry->deposit_fee)); + json_decref (contract_terms); + if (NULL == element) + { + GNUNET_break_op (0); + return GNUNET_NO; + } + GNUNET_break (0 == + json_array_append_new (rctx->deposits_response, + element)); + return GNUNET_YES; +} + + +/** + * Transform /track/transfer result as gotten from the exchange + * and transforms it in a format liked by the backoffice Web interface. + * + * @param result response from exchange's /track/transfer + * @result pointer to new JSON, or NULL upon errors. + */ +static json_t * +transform_response (const json_t *result, + struct TrackTransferContext *rctx) +{ + json_t *deposits; + json_t *value; + json_t *result_mod = NULL; + size_t index; + const char *key; + struct GNUNET_HashCode h_key; + struct GNUNET_CONTAINER_MultiHashMap *map; + struct TALER_Amount iter_value; + struct TALER_Amount iter_fee; + struct Entry *current_entry; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount ("deposit_value", &iter_value), + TALER_JSON_spec_amount ("deposit_fee", &iter_fee), + GNUNET_JSON_spec_string ("h_contract_terms", &key), + GNUNET_JSON_spec_end () + }; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Transforming /track/transfer response.\n"); + map = GNUNET_CONTAINER_multihashmap_create (1, GNUNET_NO); + deposits = json_object_get (result, + "deposits"); + + json_array_foreach (deposits, index, value) + { + if (GNUNET_OK != + GNUNET_JSON_parse (value, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return NULL; + } + GNUNET_CRYPTO_hash_from_string (key, + &h_key); + + if (NULL != (current_entry = + GNUNET_CONTAINER_multihashmap_get (map, + &h_key))) + { + /* The map already knows this h_contract_terms*/ + if ( (0 > + TALER_amount_add (¤t_entry->deposit_value, + ¤t_entry->deposit_value, + &iter_value)) || + (0 > + TALER_amount_add (¤t_entry->deposit_fee, + ¤t_entry->deposit_fee, + &iter_fee)) ) + { + GNUNET_JSON_parse_free (spec); + goto cleanup; + } + } + else + { + /* First time in the map for this h_contract_terms*/ + current_entry = GNUNET_new (struct Entry); + current_entry->deposit_value = iter_value; + current_entry->deposit_fee = iter_fee; + + if (GNUNET_SYSERR == + GNUNET_CONTAINER_multihashmap_put (map, + &h_key, + current_entry, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)) + { + GNUNET_JSON_parse_free (spec); + goto cleanup; + } + } + GNUNET_JSON_parse_free (spec); + } + rctx->deposits_response = json_array (); + + if (GNUNET_SYSERR == + GNUNET_CONTAINER_multihashmap_iterate (map, + &build_deposits_response, + rctx)) + goto cleanup; + + result_mod = json_copy ((struct json_t *) result); + json_object_del (result_mod, + "deposits"); + json_object_set_new (result_mod, + "deposits_sums", + rctx->deposits_response); + rctx->deposits_response = NULL; +cleanup: + GNUNET_CONTAINER_multihashmap_iterate (map, + &hashmap_free, + NULL); + GNUNET_CONTAINER_multihashmap_destroy (map); + return result_mod; +} + + +/** + * Resume the given /track/transfer operation and send the given response. + * Stores the response in the @a rctx and signals MHD to resume + * the connection. Also ensures MHD runs immediately. + * + * @param rctx transfer tracking context + * @param response_code response code to use + * @param response response data to send back + */ +static void +resume_track_transfer_with_response (struct TrackTransferContext *rctx, + unsigned int response_code, + struct MHD_Response *response) +{ + rctx->response_code = response_code; + rctx->response = response; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resuming /track/transfer handling as exchange interaction is done (%u)\n", + response_code); + if (NULL != rctx->timeout_task) + { + GNUNET_SCHEDULER_cancel (rctx->timeout_task); + rctx->timeout_task = NULL; + } + MHD_resume_connection (rctx->connection); + TMH_trigger_daemon (); /* we resumed, kick MHD */ +} + + +/** + * Custom cleanup routine for a `struct TrackTransferContext`. + * + * @param hc the `struct TrackTransferContext` to clean up. + */ +static void +track_transfer_cleanup (struct TM_HandlerContext *hc) +{ + struct TrackTransferContext *rctx = (struct TrackTransferContext *) hc; + + free_transfer_track_context (rctx); +} + + +/** + * This function checks that the information about the coin which + * was paid back by _this_ wire transfer matches what _we_ (the merchant) + * knew about this coin. + * + * @param cls closure with our `struct TrackTransferContext *` + * @param transaction_id of the contract + * @param coin_pub public key of the coin + * @param exchange_url URL of the exchange that issued @a coin_pub + * @param amount_with_fee amount the exchange will transfer for this coin + * @param deposit_fee fee the exchange will charge for this coin + * @param refund_fee fee the exchange will charge for refunding this coin + * @param exchange_proof proof from exchange that coin was accepted + */ +static void +check_transfer (void *cls, + const struct GNUNET_HashCode *h_contract_terms, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const char *exchange_url, + const struct TALER_Amount *amount_with_fee, + const struct TALER_Amount *deposit_fee, + const struct TALER_Amount *refund_fee, + const struct TALER_Amount *wire_fee, + const json_t *exchange_proof) +{ + struct TrackTransferContext *rctx = cls; + const struct TALER_TrackTransferDetails *ttd = rctx->current_detail; + + if (GNUNET_SYSERR == rctx->check_transfer_result) + return; /* already had a serious issue; odd that we're called more than once as well... */ + if ( (0 != TALER_amount_cmp (amount_with_fee, + &ttd->coin_value)) || + (0 != TALER_amount_cmp (deposit_fee, + &ttd->coin_fee)) ) + { + /* Disagreement between the exchange and us about how much this + coin is worth! */ + GNUNET_break_op (0); + rctx->check_transfer_result = GNUNET_SYSERR; + /* Build the `TrackTransferConflictDetails` */ + rctx->response + = TALER_MHD_make_json_pack ( + "{s:I, s:s, s:o, s:I, s:o, s:o, s:s, s:o, s:o}", + "code", (json_int_t) TALER_EC_TRACK_TRANSFER_CONFLICTING_REPORTS, + "hint", "disagreement about deposit valuation", + "exchange_deposit_proof", exchange_proof, + "conflict_offset", (json_int_t) rctx->current_offset, + "exchange_transfer_proof", rctx->original_response, + "coin_pub", GNUNET_JSON_from_data_auto (coin_pub), + "h_contract_terms", GNUNET_JSON_from_data_auto ( + &ttd->h_contract_terms), + "amount_with_fee", TALER_JSON_from_amount (amount_with_fee), + "deposit_fee", TALER_JSON_from_amount (deposit_fee)); + return; + } + rctx->check_transfer_result = GNUNET_OK; +} + + +/** + * Check that the given @a wire_fee is what the + * @a exchange_pub should charge at the @a execution_time. + * If the fee is correct (according to our database), + * return #GNUNET_OK. If we do not have the fee structure + * in our DB, we just accept it and return #GNUNET_NO; + * if we have proof that the fee is bogus, we respond with + * the proof to the client and return #GNUNET_SYSERR. + * + * @param rctx context of the transfer to respond to + * @param json response from the exchange + * @param execution_time time of the wire transfer + * @param wire_fee fee claimed by the exchange + * @return #GNUNET_SYSERR if we returned hard proof of + * missbehavior from the exchange to the client + */ +static int +check_wire_fee (struct TrackTransferContext *rctx, + const json_t *json, + struct GNUNET_TIME_Absolute execution_time, + const struct TALER_Amount *wire_fee) +{ + const struct TALER_MasterPublicKeyP *master_pub; + struct GNUNET_HashCode h_wire_method; + struct TALER_Amount expected_fee; + struct TALER_Amount closing_fee; + struct TALER_MasterSignatureP master_sig; + struct GNUNET_TIME_Absolute start_date; + struct GNUNET_TIME_Absolute end_date; + enum GNUNET_DB_QueryStatus qs; + const struct TALER_EXCHANGE_Keys *keys; + + keys = TALER_EXCHANGE_get_keys (rctx->eh); + if (NULL == keys) + { + GNUNET_break (0); + return GNUNET_NO; + } + master_pub = &keys->master_pub; + GNUNET_CRYPTO_hash (rctx->wire_method, + strlen (rctx->wire_method) + 1, + &h_wire_method); + db->preflight (db->cls); + qs = db->lookup_wire_fee (db->cls, + master_pub, + &h_wire_method, + execution_time, + &expected_fee, + &closing_fee, + &start_date, + &end_date, + &master_sig); + if (0 >= qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to find wire fee for `%s' and method `%s' at %s in DB, accepting blindly that the fee is %s\n", + TALER_B2S (master_pub), + rctx->wire_method, + GNUNET_STRINGS_absolute_time_to_string (execution_time), + TALER_amount2s (wire_fee)); + return GNUNET_NO; + } + if (0 <= TALER_amount_cmp (&expected_fee, + wire_fee)) + return GNUNET_OK; /* expected_fee >= wire_fee */ + + /* Wire fee check failed, export proof to client */ + resume_track_transfer_with_response ( + rctx, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + "{s:I, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:O}", + "code", (json_int_t) TALER_EC_TRACK_TRANSFER_JSON_BAD_WIRE_FEE, + "wire_fee", TALER_JSON_from_amount (wire_fee), + "execution_time", GNUNET_JSON_from_time_abs (execution_time), + "expected_wire_fee", TALER_JSON_from_amount (&expected_fee), + "expected_closing_fee", TALER_JSON_from_amount (&closing_fee), + "start_date", GNUNET_JSON_from_time_abs (start_date), + "end_date", GNUNET_JSON_from_time_abs (end_date), + "master_sig", GNUNET_JSON_from_data_auto (&master_sig), + "master_pub", GNUNET_JSON_from_data_auto (master_pub), + "json", json)); + return GNUNET_SYSERR; +} + + +/** + * Function called with detailed wire transfer data, including all + * of the coin transactions that were combined into the wire transfer. + * + * @param cls closure + * @param hr HTTP response details + * @param exchange_pub public key of the exchange used to sign @a json + * @param h_wire hash of the wire transfer address the transfer went to, or NULL on error + * @param execution_time time when the exchange claims to have performed the wire transfer + * @param total_amount total amount of the wire transfer, or NULL if the exchange could + * not provide any @a wtid (set only if @a http_status is #MHD_HTTP_OK) + * @param wire_fee wire fee that was charged by the exchange + * @param details_length length of the @a details array + * @param details array with details about the combined transactions + */ +static void +wire_transfer_cb (void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const struct GNUNET_HashCode *h_wire, + struct GNUNET_TIME_Absolute execution_time, + const struct TALER_Amount *total_amount, + const struct TALER_Amount *wire_fee, + unsigned int details_length, + const struct TALER_TrackTransferDetails *details) +{ + struct TrackTransferContext *rctx = cls; + json_t *jresponse; + enum GNUNET_DB_QueryStatus qs; + + rctx->wdh = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got response code %u from exchange for /track/transfer\n", + hr->http_status); + if (MHD_HTTP_OK != hr->http_status) + { + resume_track_transfer_with_response ( + rctx, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + "{s:I, s:I, s:I, s:O}", + "code", (json_int_t) TALER_EC_TRACK_TRANSFER_EXCHANGE_ERROR, + "exchange_code", (json_int_t) hr->ec, + "exchange_http_status", (json_int_t) hr->http_status, + "exchange_reply", hr->reply)); + return; + } + for (unsigned int i = 0; ipreflight (db->cls); + qs = db->store_transfer_to_proof (db->cls, + rctx->url, + &rctx->wtid, + execution_time, + exchange_pub, + hr->reply); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (0 > qs) + { + /* Special report if retries insufficient */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_json_pack ("{s:I, s:s}", + "code", + (json_int_t) + TALER_EC_TRACK_TRANSFER_DB_STORE_TRANSFER_ERROR, + "details", + "failed to store response from exchange to local database")); + return; + } + rctx->original_response = hr->reply; + + if (GNUNET_SYSERR == + check_wire_fee (rctx, + hr->reply, + execution_time, + wire_fee)) + return; + + /* Now we want to double-check that any (Taler coin) deposit + * which is accounted into _this_ wire transfer, does exist + * into _our_ database. This is the rationale: if the + * exchange paid us for it, we must have received it _beforehands_! + * + * details_length is how many (Taler coin) deposits have been + * aggregated into _this_ wire transfer. + */// + for (unsigned int i = 0; icurrent_offset = i; + rctx->current_detail = &details[i]; + /* Set the coin as "never seen" before. */ + rctx->check_transfer_result = GNUNET_NO; + db->preflight (db->cls); + qs = db->find_payments_by_hash_and_coin (db->cls, + &details[i].h_contract_terms, + &rctx->mi->pubkey, + &details[i].coin_pub, + &check_transfer, + rctx); + if (0 > qs) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_json_pack ("{s:I, s:s}", + "code", + (json_int_t) + TALER_EC_TRACK_TRANSFER_DB_FETCH_DEPOSIT_ERROR, + "details", + "failed to obtain deposit data from local database")); + return; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* The exchange says we made this deposit, but WE do not + recall making it (corrupted / unreliable database?)! + Well, let's say thanks and accept the money! */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to find payment data in DB\n"); + rctx->check_transfer_result = GNUNET_OK; + } + if (GNUNET_NO == rctx->check_transfer_result) + { + /* Internal error: how can we have called #check_transfer() + but still have no result? */ + GNUNET_break (0); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_json_pack ("{s:I, s:s, s:I, s:s}", + "code", + (json_int_t) + TALER_EC_TRACK_TRANSFER_DB_INTERNAL_LOGIC_ERROR, + "details", "internal logic error", + "line", (json_int_t) __LINE__, + "file", __FILE__)); + return; + } + if (GNUNET_SYSERR == rctx->check_transfer_result) + { + /* #check_transfer() failed, report conflict! */ + GNUNET_break_op (0); + GNUNET_assert (NULL != rctx->response); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_FAILED_DEPENDENCY, + rctx->response); + rctx->response = NULL; + return; + } + /* Response is consistent with the /deposit we made, + remember it for future reference */ + for (unsigned int r = 0; rpreflight (db->cls); + qs = db->store_coin_to_transfer (db->cls, + &details[i].h_contract_terms, + &details[i].coin_pub, + &rctx->wtid); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (0 > qs) + { + /* Special report if retries insufficient */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_json_pack ("{s:I, s:s}", + "code", + (json_int_t) + TALER_EC_TRACK_TRANSFER_DB_STORE_COIN_ERROR, + "details", + "failed to store response from exchange to local database")); + return; + } + } + rctx->original_response = NULL; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "About to call tracks transformator.\n"); + + if (NULL == (jresponse = + transform_response (hr->reply, + rctx))) + { + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_error (TALER_EC_TRACK_TRANSFER_JSON_RESPONSE_ERROR, + "Fail to elaborate the response.")); + return; + } + + resume_track_transfer_with_response (rctx, + MHD_HTTP_OK, + TALER_MHD_make_json (jresponse)); + json_decref (jresponse); +} + + +/** + * Function called with the result of our exchange lookup. + * + * @param cls the `struct TrackTransferContext` + * @param hr HTTP response details + * @param eh NULL if exchange was not found to be acceptable + * @param wire_fee NULL (we did not specify a wire method) + * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config + */ +static void +process_track_transfer_with_exchange (void *cls, + const struct + TALER_EXCHANGE_HttpResponse *hr, + struct TALER_EXCHANGE_Handle *eh, + const struct TALER_Amount *wire_fee, + int exchange_trusted) +{ + struct TrackTransferContext *rctx = cls; + + rctx->fo = NULL; + if (MHD_HTTP_OK != hr->http_status) + { + /* The request failed somehow */ + GNUNET_break_op (0); + resume_track_transfer_with_response ( + rctx, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + (NULL != hr->reply) + ? "{s:s, s:I, s:I, s:I, s:O}" + : "{s:s, s:I, s:I, s:I}", + "hint", "failed to obtain meta-data from exchange", + "code", (json_int_t) TALER_EC_TRACK_TRANSFER_EXCHANGE_KEYS_FAILURE, + "exchange_http_status", (json_int_t) hr->http_status, + "exchange_code", (json_int_t) hr->ec, + "exchange_reply", hr->reply)); + return; + } + rctx->eh = eh; + rctx->wdh = TALER_EXCHANGE_transfers_get (eh, + &rctx->wtid, + &wire_transfer_cb, + rctx); + if (NULL == rctx->wdh) + { + GNUNET_break (0); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_json_pack ("{s:I, s:s}", + "code", + (json_int_t) + TALER_EC_TRACK_TRANSFER_REQUEST_ERROR, + "error", + "failed to run /transfers/ GET on exchange")); + } +} + + +/** + * Handle a timeout for the processing of the track transfer request. + * + * @param cls closure + */ +static void +handle_track_transfer_timeout (void *cls) +{ + struct TrackTransferContext *rctx = cls; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resuming /track/transfer with error after timeout\n"); + rctx->timeout_task = NULL; + + if (NULL != rctx->fo) + { + TMH_EXCHANGES_find_exchange_cancel (rctx->fo); + rctx->fo = NULL; + } + resume_track_transfer_with_response (rctx, + MHD_HTTP_SERVICE_UNAVAILABLE, + TALER_MHD_make_error ( + TALER_EC_TRACK_TRANSFER_EXCHANGE_TIMEOUT, + "exchange not reachable")); +} + + +/** + * Function called with information about a wire transfer identifier. + * Generate a response based on the given @a proof. + * + * @param cls closure + * @param proof proof from exchange about what the wire transfer was for. + * should match the `TrackTransactionResponse` format + * of the exchange + */ +static void +proof_cb (void *cls, + const json_t *proof) +{ + struct TrackTransferContext *rctx = cls; + json_t *transformed_response; + + if (NULL == (transformed_response = + transform_response (proof, + rctx))) + { + rctx->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + rctx->response + = TALER_MHD_make_error (TALER_EC_TRACK_TRANSFER_JSON_RESPONSE_ERROR, + "Fail to elaborate response."); + return; + } + + rctx->response_code = MHD_HTTP_OK; + rctx->response = TALER_MHD_make_json (transformed_response); + json_decref (transformed_response); +} + + +/** + * Manages a /track/transfer call, thus it calls the /track/wtid + * offered by the exchange in order to return the set of transfers + * (of coins) associated with a given wire transfer. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] connection_cls the connection's closure (can be updated) + * @param upload_data upload data + * @param[in,out] upload_data_size number of bytes (left) in @a upload_data + * @param mi merchant backend instance, never NULL + * @return MHD result code + */ +MHD_RESULT +MH_handler_track_transfer (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi) +{ + struct TrackTransferContext *rctx; + const char *str; + const char *url; + const char *wire_method; + MHD_RESULT ret; + enum GNUNET_DB_QueryStatus qs; + + if (NULL == *connection_cls) + { + rctx = GNUNET_new (struct TrackTransferContext); + rctx->hc.cc = &track_transfer_cleanup; + rctx->connection = connection; + *connection_cls = rctx; + } + else + { + /* not first call, recover state */ + rctx = *connection_cls; + } + + if (0 != rctx->response_code) + { + /* We are *done* processing the request, just queue the response (!) */ + if (UINT_MAX == rctx->response_code) + { + GNUNET_break (0); + return MHD_NO; /* hard error */ + } + ret = MHD_queue_response (connection, + rctx->response_code, + rctx->response); + if (NULL != rctx->response) + { + MHD_destroy_response (rctx->response); + rctx->response = NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Queueing response (%u) for /track/transfer (%s).\n", + (unsigned int) rctx->response_code, + ret ? "OK" : "FAILED"); + return ret; + } + if ( (NULL != rctx->fo) || + (NULL != rctx->eh) ) + { + /* likely old MHD version */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Not sure why we are here, should be suspended\n"); + return MHD_YES; /* still work in progress */ + } + + url = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "exchange"); + if (NULL == url) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "exchange"); + rctx->url = GNUNET_strdup (url); + + /* FIXME: change again: we probably don't want the wire_method + but rather the _account_ (section) here! */ + wire_method = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "wire_method"); + if (NULL == wire_method) + { + if (1) + { + /* temporary work-around until demo is adjusted... */ + GNUNET_break (0); + wire_method = "x-taler-bank"; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Client needs fixing, see API change for #4943!\n"); + } + else + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "wire_method"); + } + rctx->wire_method = GNUNET_strdup (wire_method); + rctx->mi = mi; + str = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "wtid"); + if (NULL == str) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "wtid"); + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (str, + strlen (str), + &rctx->wtid, + sizeof (rctx->wtid))) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MALFORMED, + "wtid"); + } + + /* Check if reply is already in database! */ + db->preflight (db->cls); + qs = db->find_proof_by_wtid (db->cls, + rctx->url, + &rctx->wtid, + &proof_cb, + rctx); + if (0 > qs) + { + /* Simple select queries should not cause serialization issues */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_TRACK_TRANSFER_DB_FETCH_DEPOSIT_ERROR, + "Fail to query database about proofs"); + } + if (0 != rctx->response_code) + { + ret = MHD_queue_response (connection, + rctx->response_code, + rctx->response); + if (NULL != rctx->response) + { + MHD_destroy_response (rctx->response); + rctx->response = NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Queueing response (%u) for /track/transfer (%s).\n", + (unsigned int) rctx->response_code, + ret ? "OK" : "FAILED"); + return ret; + } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Suspending /track/transfer handling while working with the exchange\n"); + MHD_suspend_connection (connection); + rctx->fo = TMH_EXCHANGES_find_exchange (url, + NULL, + GNUNET_NO, + &process_track_transfer_with_exchange, + rctx); + rctx->timeout_task + = GNUNET_SCHEDULER_add_delayed (TRACK_TIMEOUT, + &handle_track_transfer_timeout, + rctx); + return MHD_YES; +} + + +/* end of taler-merchant-httpd_track-transfer.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-transfers.h b/src/backend/taler-merchant-httpd_private-post-transfers.h new file mode 100644 index 00000000..0463295e --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-post-transfers.h @@ -0,0 +1,49 @@ +/* + This file is part of TALER + (C) 2014-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file backend/taler-merchant-httpd_track-transfer.h + * @brief headers for /track/transfer handler + * @author Christian Grothoff + * @author Marcello Stanisci + */ +#ifndef TALER_MERCHANT_HTTPD_TRACK_TRANSFER_H +#define TALER_MERCHANT_HTTPD_TRACK_TRANSFER_H +#include +#include "taler-merchant-httpd.h" + +/** + * Manages a /track/transfer call, thus it calls the /wire/transfer + * offered by the exchange in order to return the set of transfers + * (of coins) associated with a given wire transfer + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] connection_cls the connection's closure (can be updated) + * @param upload_data upload data + * @param[in,out] upload_data_size number of bytes (left) in @a upload_data + * @param mi merchant backend instance, never NULL + * @return MHD result code + */ +MHD_RESULT +MH_handler_track_transfer (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi); + + +#endif -- cgit v1.2.3