/*
This file is part of TALER
(C) 2014-2022 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.
*/
enum GNUNET_GenericReturnValue check_transfer_result;
/**
* Did we suspend @a connection and are thus in
* the #ptc_head DLL (#GNUNET_YES). Set to
* #GNUNET_NO if we are not suspended, and to
* #GNUNET_SYSERR if we should close the connection
* without a response due to shutdown.
*/
enum GNUNET_GenericReturnValue suspended;
/**
* Should we retry the transaction due to a serialization error?
*/
bool soft_retry;
/**
* Did we just download the exchange reply?
*/
bool downloaded;
};
/**
* 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 = GNUNET_SYSERR;
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 = GNUNET_NO;
MHD_resume_connection (ptc->connection);
TALER_MHD_daemon_trigger (); /* 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 TALER_MerchantWireHashP *h_wire,
struct GNUNET_TIME_Timestamp deposit_timestamp,
struct GNUNET_TIME_Timestamp 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_timestamp ("deposit_timestamp",
deposit_timestamp),
GNUNET_JSON_pack_timestamp ("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 enum GNUNET_GenericReturnValue
check_wire_fee (struct PostTransfersContext *ptc,
struct GNUNET_TIME_Timestamp execution_time,
const struct TALER_Amount *wire_fee)
{
struct TALER_WireFeeSet fees;
struct TALER_MasterSignatureP master_sig;
struct GNUNET_TIME_Timestamp start_date;
struct GNUNET_TIME_Timestamp 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,
&fees,
&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_TIME_timestamp2s (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 (&fees.wire,
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_timestamp ("execution_time",
execution_time),
TALER_JSON_pack_amount ("expected_wire_fee",
&fees.wire),
TALER_JSON_pack_amount ("expected_closing_fee",
&fees.closing),
GNUNET_JSON_pack_timestamp ("start_date",
start_date),
GNUNET_JSON_pack_timestamp ("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 = GNUNET_YES;
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;
}
if (GNUNET_SYSERR == ptc->suspended)
return MHD_NO; /* we are in shutdown */
/* 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 (GNUNET_NO == 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:
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Bank account `%s' is configured at row %llu\n",
ptc->payto_uri,
(unsigned long long) account_serial);
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_timestamp ("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 */