/* This file is part of TALER (C) 2014-2021 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 taler-merchant-httpd_private-post-transfers.c * @brief implement API for registering wire transfers * @author Marcello Stanisci * @author Christian Grothoff */ #include "platform.h" #include #include #include #include "taler-merchant-httpd_auditors.h" #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_private-post-transfers.h" /** * How long to wait before giving up processing with the exchange? */ #define TRANSFER_GENERIC_TIMEOUT (GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_SECONDS, \ 15)) /** * How often do we retry the simple INSERT database transaction? */ #define MAX_RETRIES 3 /** * Context used for handing POST /private/transfers requests. */ struct PostTransfersContext { /** * Kept in a DLL. */ struct PostTransfersContext *next; /** * Kept in a DLL. */ struct PostTransfersContext *prev; /** * Argument for the /wire/transfers request. */ struct TALER_WireTransferIdentifierRawP wtid; /** * Amount of the wire transfer. */ struct TALER_Amount amount; /** * URL of the exchange. */ const char *exchange_url; /** * payto:// URI used for the transfer. */ const char *payto_uri; /** * Master public key of the exchange at @e exchange_url. */ struct TALER_MasterPublicKeyP master_pub; /** * Handle for the /wire/transfers request. */ struct TALER_EXCHANGE_TransfersGetHandle *wdh; /** * For which merchant instance is this tracking request? */ struct TMH_HandlerContext *hc; /** * 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; /** * Pointer to the detail that we are currently * checking in #check_transfer(). */ const struct TALER_TrackTransferDetails *current_detail; /** * 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; /** * Should we retry the transaction due to a serialization error? */ bool soft_retry; /** * Did we just download the exchange reply? */ bool downloaded; /** * Are we currently suspended? */ bool suspended; }; /** * Head of list of suspended requests. */ static struct PostTransfersContext *ptc_head; /** * Tail of list of suspended requests. */ static struct PostTransfersContext *ptc_tail; void TMH_force_post_transfers_resume () { struct PostTransfersContext *ptc; while (NULL != (ptc = ptc_head)) { GNUNET_CONTAINER_DLL_remove (ptc_head, ptc_tail, ptc); ptc->suspended = false; MHD_resume_connection (ptc->connection); if (NULL != ptc->timeout_task) { GNUNET_SCHEDULER_cancel (ptc->timeout_task); ptc->timeout_task = NULL; } } } /** * Resume the given /track/transfer operation and send the given response. * Stores the response in the @a ptc and signals MHD to resume * the connection. Also ensures MHD runs immediately. * * @param ptc transfer tracking context * @param response_code response code to use * @param response response data to send back */ static void resume_transfer_with_response (struct PostTransfersContext *ptc, unsigned int response_code, struct MHD_Response *response) { ptc->response_code = response_code; ptc->response = response; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming POST /transfers handling as exchange interaction is done (%u)\n", response_code); if (NULL != ptc->timeout_task) { GNUNET_SCHEDULER_cancel (ptc->timeout_task); ptc->timeout_task = NULL; } GNUNET_CONTAINER_DLL_remove (ptc_head, ptc_tail, ptc); ptc->suspended = false; MHD_resume_connection (ptc->connection); TMH_trigger_daemon (); /* we resumed, kick MHD */ } /** * Resume the given POST /transfers operation with an error. * * @param ptc transfer tracking context * @param response_code response code to use * @param ec error code to use * @param hint hint text to provide */ static void resume_transfer_with_error (struct PostTransfersContext *ptc, unsigned int response_code, enum TALER_ErrorCode ec, const char *hint) { resume_transfer_with_response (ptc, response_code, TALER_MHD_make_error (ec, hint)); } /** * Custom cleanup routine for a `struct PostTransfersContext`. * * @param cls the `struct PostTransfersContext` to clean up. */ static void transfer_cleanup (void *cls) { struct PostTransfersContext *ptc = cls; if (NULL != ptc->fo) { TMH_EXCHANGES_find_exchange_cancel (ptc->fo); ptc->fo = NULL; } if (NULL != ptc->timeout_task) { GNUNET_SCHEDULER_cancel (ptc->timeout_task); ptc->timeout_task = NULL; } if (NULL != ptc->wdh) { TALER_EXCHANGE_transfers_get_cancel (ptc->wdh); ptc->wdh = NULL; } if (NULL != ptc->response) { MHD_destroy_response (ptc->response); ptc->response = NULL; } GNUNET_free (ptc); } /** * 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 PostTransfersContext *` * @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 wire_fee paid wire fee * @param h_wire hash of merchant's wire details * @param deposit_timestamp when did the exchange receive the deposit * @param refund_deadline until when are refunds allowed * @param exchange_sig signature by the exchange * @param exchange_pub exchange signing key used for @a exchange_sig */ static void check_transfer (void *cls, 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 struct GNUNET_HashCode *h_wire, struct GNUNET_TIME_Absolute deposit_timestamp, struct GNUNET_TIME_Absolute refund_deadline, const struct TALER_ExchangeSignatureP *exchange_sig, const struct TALER_ExchangePublicKeyP *exchange_pub) { struct PostTransfersContext *ptc = cls; const struct TALER_TrackTransferDetails *ttd = ptc->current_detail; if (GNUNET_SYSERR == ptc->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); ptc->check_transfer_result = GNUNET_SYSERR; /* Build the `TrackTransferConflictDetails` */ ptc->response_code = MHD_HTTP_ACCEPTED; ptc->response = TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_REPORTS), GNUNET_JSON_pack_string ("exchange_url", exchange_url), GNUNET_JSON_pack_time_abs ("deposit_timestamp", deposit_timestamp), GNUNET_JSON_pack_time_abs ("refund_deadline", refund_deadline), GNUNET_JSON_pack_uint64 ("conflict_offset", ptc->current_offset), GNUNET_JSON_pack_data_auto ("coin_pub", &ttd->coin_pub), GNUNET_JSON_pack_data_auto ("h_wire", h_wire), GNUNET_JSON_pack_data_auto ("deposit_exchange_sig", exchange_sig), GNUNET_JSON_pack_data_auto ("deposit_exchange_pub", exchange_pub), GNUNET_JSON_pack_data_auto ("h_contract_terms", &ttd->h_contract_terms), TALER_JSON_pack_amount ("amount_with_fee", amount_with_fee), TALER_JSON_pack_amount ("coin_value", &ttd->coin_value), TALER_JSON_pack_amount ("coin_fee", &ttd->coin_fee), TALER_JSON_pack_amount ("deposit_fee", deposit_fee)); return; } ptc->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 ptc context of the transfer to respond to * @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 PostTransfersContext *ptc, struct GNUNET_TIME_Absolute execution_time, const struct TALER_Amount *wire_fee) { 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; char *wire_method; wire_method = TALER_payto_get_method (ptc->payto_uri); qs = TMH_db->lookup_wire_fee (TMH_db->cls, &ptc->master_pub, wire_method, execution_time, &expected_fee, &closing_fee, &start_date, &end_date, &master_sig); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); ptc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; ptc->response = TALER_MHD_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED, "lookup_wire_fee"); return GNUNET_SYSERR; case GNUNET_DB_STATUS_SOFT_ERROR: ptc->soft_retry = true; return GNUNET_NO; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 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 (&ptc->master_pub), wire_method, GNUNET_STRINGS_absolute_time_to_string (execution_time), TALER_amount2s (wire_fee)); GNUNET_free (wire_method); return GNUNET_NO; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } if (0 <= TALER_amount_cmp (&expected_fee, wire_fee)) { GNUNET_free (wire_method); return GNUNET_OK; /* expected_fee >= wire_fee */ } /* Wire fee check failed, export proof to client */ ptc->response_code = MHD_HTTP_ACCEPTED; ptc->response = TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_BAD_WIRE_FEE), TALER_JSON_pack_amount ("wire_fee", wire_fee), GNUNET_JSON_pack_time_abs ("execution_time", execution_time), TALER_JSON_pack_amount ("expected_wire_fee", &expected_fee), TALER_JSON_pack_amount ("expected_closing_fee", &closing_fee), GNUNET_JSON_pack_time_abs ("start_date", start_date), GNUNET_JSON_pack_time_abs ("end_date", end_date), GNUNET_JSON_pack_data_auto ("master_sig", &master_sig), GNUNET_JSON_pack_data_auto ("master_pub", &ptc->master_pub)); GNUNET_free (wire_method); 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 td transfer data */ static void wire_transfer_cb (void *cls, const struct TALER_EXCHANGE_HttpResponse *hr, const struct TALER_EXCHANGE_TransferData *td) { struct PostTransfersContext *ptc = cls; const char *instance_id = ptc->hc->instance->settings.id; enum GNUNET_DB_QueryStatus qs; ptc->wdh = NULL; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Got response code %u from exchange for GET /transfers/$WTID\n", hr->http_status); switch (hr->http_status) { case MHD_HTTP_OK: break; case MHD_HTTP_NOT_FOUND: resume_transfer_with_response ( ptc, MHD_HTTP_BAD_GATEWAY, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_EXCHANGE_UNKNOWN), TMH_pack_exchange_reply (hr))); return; default: resume_transfer_with_response ( ptc, MHD_HTTP_BAD_GATEWAY, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS), TMH_pack_exchange_reply (hr))); return; } TMH_db->preflight (TMH_db->cls); /* Ok, exchange answer is acceptable, store it */ qs = TMH_db->insert_transfer_details (TMH_db->cls, instance_id, ptc->exchange_url, ptc->payto_uri, &ptc->wtid, td); if (0 > qs) { /* Always report on DB error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); resume_transfer_with_error ( ptc, MHD_HTTP_INTERNAL_SERVER_ERROR, (GNUNET_DB_STATUS_HARD_ERROR == qs) ? TALER_EC_GENERIC_DB_COMMIT_FAILED : TALER_EC_GENERIC_DB_SOFT_FAILURE, NULL); return; } if (0 == qs) { GNUNET_break (0); resume_transfer_with_error ( ptc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "insert-transfer-details"); return; } if (0 != TALER_amount_cmp (&td->total_amount, &ptc->amount)) { resume_transfer_with_error ( ptc, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_TRANSFERS, NULL); return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Transfer details inserted, resuming request...\n"); /* resume processing, main function will build the response */ resume_transfer_with_response (ptc, 0, NULL); } /** * Function called with the result of our exchange lookup. * * @param cls the `struct PostTransfersContext` * @param hr HTTP response details * @param eh NULL if exchange was not found to be acceptable * @param payto_uri payto://-URI of the exchange * @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_transfer_with_exchange (void *cls, const struct TALER_EXCHANGE_HttpResponse *hr, struct TALER_EXCHANGE_Handle *eh, const char *payto_uri, const struct TALER_Amount *wire_fee, bool exchange_trusted) { struct PostTransfersContext *ptc = cls; (void) payto_uri; (void) exchange_trusted; ptc->fo = NULL; if (NULL == hr) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange failed to respond!\n"); resume_transfer_with_response ( ptc, MHD_HTTP_GATEWAY_TIMEOUT, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec (TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT))); return; } if (NULL == eh) { /* The request failed somehow */ GNUNET_break_op (0); resume_transfer_with_response ( ptc, MHD_HTTP_BAD_GATEWAY, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_GENERIC_EXCHANGE_CONNECT_FAILURE), TMH_pack_exchange_reply (hr))); return; } /* keep master key for later */ { const struct TALER_EXCHANGE_Keys *keys; keys = TALER_EXCHANGE_get_keys (eh); if (NULL == keys) { GNUNET_break (0); resume_transfer_with_error (ptc, MHD_HTTP_BAD_GATEWAY, TALER_EC_MERCHANT_GENERIC_EXCHANGE_KEYS_FAILURE, NULL); return; } ptc->master_pub = keys->master_pub; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Requesting transfer details from exchange\n"); ptc->wdh = TALER_EXCHANGE_transfers_get (eh, &ptc->wtid, &wire_transfer_cb, ptc); if (NULL == ptc->wdh) { GNUNET_break (0); resume_transfer_with_error (ptc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_REQUEST_ERROR, "failed to run GET /transfers/ on exchange"); } } /** * 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_! * * @param cls a `struct PostTransfersContext` * @param current_offset at which offset in the exchange's reply are the @a ttd * @param ttd details about an aggregated transfer (to check) */ static void verify_exchange_claim_cb (void *cls, unsigned int current_offset, const struct TALER_TrackTransferDetails *ttd) { struct PostTransfersContext *ptc = cls; enum GNUNET_DB_QueryStatus qs; if (0 != ptc->response_code) return; /* already encountered an error */ if (ptc->soft_retry) return; /* already encountered an error */ ptc->current_offset = current_offset; ptc->current_detail = ttd; /* Set the coin as "never seen" before. */ ptc->check_transfer_result = GNUNET_NO; qs = TMH_db->lookup_deposits_by_contract_and_coin ( TMH_db->cls, ptc->hc->instance->settings.id, &ttd->h_contract_terms, &ttd->coin_pub, &check_transfer, ptc); switch (qs) { case GNUNET_DB_STATUS_SOFT_ERROR: ptc->soft_retry = true; return; case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); ptc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; ptc->response = TALER_MHD_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED, "deposit by contract and coin"); return; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: /* 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"); ptc->check_transfer_result = GNUNET_OK; break; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } switch (ptc->check_transfer_result) { case GNUNET_NO: /* Internal error: how can we have called #check_transfer() but still have no result? */ GNUNET_break (0); ptc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; ptc->response = TALER_MHD_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, "check_transfer_result must not be GNUNET_NO"); return; case GNUNET_SYSERR: /* #check_transfer() failed, report conflict! */ GNUNET_break_op (0); GNUNET_assert (NULL != ptc->response); return; case GNUNET_OK: break; } } /** * Represents an entry in the table used to sum up * individual deposits for each h_contract_terms/order_id * (as the exchange gives us per coin, and we return * per order). */ struct Entry { /** * Order of the entry. */ char *order_id; /** * Sum accumulator for deposited value. */ struct TALER_Amount deposit_value; /** * Sum accumulator for deposit fee. */ struct TALER_Amount deposit_fee; }; /** * Function called with information about a wire transfer identifier. * Generate a response array based on the given information. * * @param cls closure, a hashmap to update * @param order_id the order to which the deposits belong * @param deposit_value the amount deposited under @a order_id * @param deposit_fee the fee charged for @a deposit_value */ static void transfer_summary_cb (void *cls, const char *order_id, const struct TALER_Amount *deposit_value, const struct TALER_Amount *deposit_fee) { struct GNUNET_CONTAINER_MultiHashMap *map = cls; struct Entry *current_entry; struct GNUNET_HashCode h_key; GNUNET_CRYPTO_hash (order_id, strlen (order_id), &h_key); current_entry = GNUNET_CONTAINER_multihashmap_get (map, &h_key); if (NULL != current_entry) { /* The map already knows this order, do aggregation */ GNUNET_assert ( (0 <= TALER_amount_add (¤t_entry->deposit_value, ¤t_entry->deposit_value, deposit_value)) && (0 <= TALER_amount_add (¤t_entry->deposit_fee, ¤t_entry->deposit_fee, deposit_fee)) ); } else { /* First time in the map for this h_contract_terms*/ current_entry = GNUNET_new (struct Entry); current_entry->deposit_value = *deposit_value; current_entry->deposit_fee = *deposit_fee; current_entry->order_id = GNUNET_strdup (order_id); GNUNET_assert (GNUNET_SYSERR != GNUNET_CONTAINER_multihashmap_put (map, &h_key, current_entry, GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); } } /** * Callback that frees all the elements in the hashmap, and @a cls * is non-NULL, appends them as JSON to the array * * @param cls closure, NULL or a `json_t *` array * @param key current key * @param value a `struct Entry` * @return #GNUNET_YES if the iteration should continue, * #GNUNET_NO otherwise. */ static int hashmap_update_and_free (void *cls, const struct GNUNET_HashCode *key, void *value) { json_t *ja = cls; struct Entry *entry = value; (void) key; if (NULL != ja) { GNUNET_assert ( 0 == json_array_append_new ( ja, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("order_id", entry->order_id), TALER_JSON_pack_amount ("deposit_value", &entry->deposit_value), TALER_JSON_pack_amount ("deposit_fee", &entry->deposit_fee)))); } GNUNET_free (entry->order_id); GNUNET_free (entry); return GNUNET_YES; } /** * Handle a timeout for the processing of the track transfer request. * * @param cls closure */ static void handle_transfer_timeout (void *cls) { struct PostTransfersContext *ptc = cls; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming POST /private/transfers with error after timeout\n"); ptc->timeout_task = NULL; if (NULL != ptc->fo) { TMH_EXCHANGES_find_exchange_cancel (ptc->fo); ptc->fo = NULL; } if (NULL != ptc->wdh) { TALER_EXCHANGE_transfers_get_cancel (ptc->wdh); ptc->wdh = NULL; } resume_transfer_with_error (ptc, MHD_HTTP_GATEWAY_TIMEOUT, TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, NULL); } /** * We are *done* processing the request, just queue the response (!) * * @param ptc request context */ static MHD_RESULT queue (struct PostTransfersContext *ptc) { MHD_RESULT ret; GNUNET_assert (0 != ptc->response_code); if (UINT_MAX == ptc->response_code) { GNUNET_break (0); return MHD_NO; /* hard error */ } ret = MHD_queue_response (ptc->connection, ptc->response_code, ptc->response); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Queueing response (%u) for POST /private/transfers (%s).\n", (unsigned int) ptc->response_code, ret ? "OK" : "FAILED"); return ret; } /** * Download transfer data from the exchange. * * @param ptc request context */ static void download (struct PostTransfersContext *ptc) { ptc->downloaded = true; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Suspending POST /private/transfers handling while working with exchange\n"); MHD_suspend_connection (ptc->connection); ptc->suspended = true; GNUNET_CONTAINER_DLL_insert (ptc_head, ptc_tail, ptc); ptc->fo = TMH_EXCHANGES_find_exchange (ptc->exchange_url, NULL, GNUNET_NO, &process_transfer_with_exchange, ptc); ptc->timeout_task = GNUNET_SCHEDULER_add_delayed (TRANSFER_GENERIC_TIMEOUT, &handle_transfer_timeout, ptc); } MHD_RESULT TMH_private_post_transfers (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { struct PostTransfersContext *ptc = hc->ctx; enum GNUNET_DB_QueryStatus qs; if (NULL == ptc) { ptc = GNUNET_new (struct PostTransfersContext); ptc->connection = connection; ptc->hc = hc; hc->ctx = ptc; hc->cc = &transfer_cleanup; } /* resume logic: did we get resumed after a reply was built? */ if (0 != ptc->response_code) return queue (ptc); if ( (NULL != ptc->fo) || (NULL != ptc->wdh) ) { /* likely old MHD version causing spurious wake-up */ GNUNET_break (ptc->suspended); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Not sure why we are here, should be suspended\n"); return MHD_YES; /* still work in progress */ } if (NULL == ptc->exchange_url) { /* First request, parse it! */ struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_amount ("credit_amount", TMH_currency, &ptc->amount), GNUNET_JSON_spec_fixed_auto ("wtid", &ptc->wtid), GNUNET_JSON_spec_string ("payto_uri", &ptc->payto_uri), GNUNET_JSON_spec_string ("exchange_url", &ptc->exchange_url), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_data (connection, hc->request_body, spec); if (GNUNET_OK != res) return (GNUNET_NO == res) ? MHD_YES : MHD_NO; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "New inbound wire transfer over %s to %s from %s\n", TALER_amount2s (&ptc->amount), ptc->payto_uri, ptc->exchange_url); } /* Check if transfer data is in database, if not, add it. */ for (unsigned int retry = 0; retrypreflight (TMH_db->cls); if (GNUNET_OK != TMH_db->start (TMH_db->cls, "post-transfers")) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_START_FAILED, "transfer"); } qs = TMH_db->lookup_transfer (TMH_db->cls, ptc->hc->instance->settings.id, ptc->exchange_url, &ptc->wtid, &total_amount, &wire_fee, &exchange_amount, &execution_time, &have_exchange_sig, &verified); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "transfer"); break; case GNUNET_DB_STATUS_SOFT_ERROR: TMH_db->rollback (TMH_db->cls); continue; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: /* Transfer so far unknown; try to persist the wire transfer information we have received in the database (it is not yet present). Upon success, try to download the transfer details from the exchange. */ { uint64_t account_serial; /* Make sure the bank account is configured. */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Transfer is not yet known\n"); qs = TMH_db->lookup_account (TMH_db->cls, ptc->hc->instance->settings.id, ptc->payto_uri, &account_serial); switch (qs) { case GNUNET_DB_STATUS_SOFT_ERROR: TMH_db->rollback (TMH_db->cls); continue; case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "lookup_account"); case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Bank account `%s' not configured for instance `%s'\n", ptc->payto_uri, ptc->hc->instance->settings.id); TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_ACCOUNT_NOT_FOUND, ptc->payto_uri); case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Inserting new transfer\n"); qs = TMH_db->insert_transfer (TMH_db->cls, ptc->hc->instance->settings.id, ptc->exchange_url, &ptc->wtid, &ptc->amount, ptc->payto_uri, true /* confirmed! */); switch (qs) { case GNUNET_DB_STATUS_SOFT_ERROR: TMH_db->rollback (TMH_db->cls); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Soft error, retrying...\n"); continue; case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "transfer"); case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: TMH_db->rollback (TMH_db->cls); /* Should not happen: we checked earlier! */ GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_CONFLICT, TALER_EC_GENERIC_DB_STORE_FAILED, "not unique"); case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } qs = TMH_db->commit (TMH_db->cls); switch (qs) { case GNUNET_DB_STATUS_SOFT_ERROR: TMH_db->rollback (TMH_db->cls); continue; case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_COMMIT_FAILED, NULL); case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: GNUNET_log (GNUNET_ERROR_TYPE_INFO, "post-transfer committed successfully\n"); break; } download (ptc); return MHD_YES; /* download() always suspends */ } case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: /* Transfer exists */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Transfer exists in DB (verified: %s, exchange signature: %s)\n", verified ? "true" : "false", have_exchange_sig ? "true" : "false"); if (! verified) { if ( (! ptc->downloaded) && (! have_exchange_sig) ) { /* We may have previously attempted and failed to download the exchange data, do it again! */ TMH_db->rollback (TMH_db->cls); download (ptc); return MHD_YES; /* download always suspends */ } if (! have_exchange_sig) { /* We tried to download and still failed to get an exchange signture. Still, that should have been handled there. */ TMH_db->rollback (TMH_db->cls); GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, "download but no exchange signature and no error"); } /* verify */ if (GNUNET_SYSERR == check_wire_fee (ptc, execution_time, &wire_fee)) { TMH_db->rollback (TMH_db->cls); return queue (ptc); /* generate error */ } if (ptc->soft_retry) { /* DB serialization failure */ ptc->soft_retry = false; TMH_db->rollback (TMH_db->cls); continue; } qs = TMH_db->lookup_transfer_details (TMH_db->cls, ptc->exchange_url, &ptc->wtid, &verify_exchange_claim_cb, ptc); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "lookup_transfer_details"); case GNUNET_DB_STATUS_SOFT_ERROR: TMH_db->rollback (TMH_db->cls); continue; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: default: break; } if (0 != ptc->response_code) { TMH_db->rollback (TMH_db->cls); return queue (ptc); /* generate error */ } if (ptc->soft_retry) { /* DB serialization failure */ ptc->soft_retry = false; TMH_db->rollback (TMH_db->cls); continue; } { struct TALER_Amount delta; if (0 > TALER_amount_subtract (&delta, &total_amount, &wire_fee)) { GNUNET_break (0); TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, NULL); } if (0 != TALER_amount_cmp (&exchange_amount, &delta)) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Amount of expected was %s\n", TALER_amount2s (&delta)); TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_TRANSFERS, TALER_amount2s (&exchange_amount)); } if ( (GNUNET_OK != TALER_amount_cmp_currency (&ptc->amount, &delta)) || (0 != TALER_amount_cmp (&ptc->amount, &delta)) ) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Amount submitted was %s\n", TALER_amount2s (&ptc->amount)); TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_SUBMISSION, TALER_amount2s (&exchange_amount)); } } verified = true; qs = TMH_db->set_transfer_status_to_verified (TMH_db->cls, ptc->exchange_url, &ptc->wtid); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "set_transfer_status_to_verified"); case GNUNET_DB_STATUS_SOFT_ERROR: TMH_db->rollback (TMH_db->cls); continue; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: GNUNET_assert (0); break; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } } /* end of 'if (! verified)' */ /* Short version: we verified that the exchange reply and our own accounting match; generate the summary response */ GNUNET_assert (verified); { struct GNUNET_CONTAINER_MultiHashMap *map; json_t *deposit_sums; map = GNUNET_CONTAINER_multihashmap_create (16, GNUNET_NO); qs = TMH_db->lookup_transfer_summary (TMH_db->cls, ptc->exchange_url, &ptc->wtid, &transfer_summary_cb, map); switch (qs) { case GNUNET_DB_STATUS_SOFT_ERROR: TMH_db->rollback (TMH_db->cls); continue; case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); TMH_db->rollback (TMH_db->cls); GNUNET_CONTAINER_multihashmap_iterate (map, &hashmap_update_and_free, NULL); GNUNET_CONTAINER_multihashmap_destroy (map); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "transfer summary"); case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: default: break; } qs = TMH_db->commit (TMH_db->cls); switch (qs) { case GNUNET_DB_STATUS_SOFT_ERROR: TMH_db->rollback (TMH_db->cls); continue; case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_COMMIT_FAILED, NULL); case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: GNUNET_log (GNUNET_ERROR_TYPE_INFO, "post-transfer committed uselessly\n"); break; } deposit_sums = json_array (); GNUNET_assert (NULL != deposit_sums); GNUNET_CONTAINER_multihashmap_iterate (map, &hashmap_update_and_free, deposit_sums); GNUNET_CONTAINER_multihashmap_destroy (map); return TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_OK, TALER_JSON_pack_amount ("total", &total_amount), TALER_JSON_pack_amount ("wire_fee", &wire_fee), GNUNET_JSON_pack_time_abs ("execution_time", execution_time), GNUNET_JSON_pack_array_steal ("deposit_sums", deposit_sums)); } /* end of 'verified == true' (not an 'if'!) */ } /* end of 'switch (qs)' */ GNUNET_assert (0); } /* end of 'for(retries...) */ return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_SOFT_FAILURE, "post-transfers"); } /* end of taler-merchant-httpd_private-post-transfers.c */