/*
This file is part of TALER
(C) 2020-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_post-orders-ID-refund.c
* @brief handling of POST /orders/$ID/refund requests
* @author Jonathan Buchanan
*/
#include "platform.h"
#include
#include
#include
#include
#include "taler-merchant-httpd.h"
#include "taler-merchant-httpd_exchanges.h"
#include "taler-merchant-httpd_post-orders-ID-refund.h"
/**
* Information we keep for each coin to be refunded.
*/
struct CoinRefund
{
/**
* Kept in a DLL.
*/
struct CoinRefund *next;
/**
* Kept in a DLL.
*/
struct CoinRefund *prev;
/**
* Request to connect to the target exchange.
*/
struct TMH_EXCHANGES_KeysOperation *fo;
/**
* Handle for the refund operation with the exchange.
*/
struct TALER_EXCHANGE_RefundHandle *rh;
/**
* Request this operation is part of.
*/
struct PostRefundData *prd;
/**
* URL of the exchange for this @e coin_pub.
*/
char *exchange_url;
/**
* Fully reply from the exchange, only possibly set if
* we got a JSON reply and a non-#MHD_HTTP_OK error code
*/
json_t *exchange_reply;
/**
* When did the merchant grant the refund. To be used to group events
* in the wallet.
*/
struct GNUNET_TIME_Timestamp execution_time;
/**
* Coin to refund.
*/
struct TALER_CoinSpendPublicKeyP coin_pub;
/**
* Refund transaction ID to use.
*/
uint64_t rtransaction_id;
/**
* Unique serial number identifying the refund.
*/
uint64_t refund_serial;
/**
* Amount to refund.
*/
struct TALER_Amount refund_amount;
/**
* Public key of the exchange affirming the refund.
*/
struct TALER_ExchangePublicKeyP exchange_pub;
/**
* Signature of the exchange affirming the refund.
*/
struct TALER_ExchangeSignatureP exchange_sig;
/**
* HTTP status from the exchange, #MHD_HTTP_OK if
* @a exchange_pub and @a exchange_sig are valid.
*/
unsigned int exchange_status;
/**
* HTTP error code from the exchange.
*/
enum TALER_ErrorCode exchange_code;
};
/**
* Context for the operation.
*/
struct PostRefundData
{
/**
* Hashed version of contract terms. All zeros if not provided.
*/
struct TALER_PrivateContractHashP h_contract_terms;
/**
* DLL of (suspended) requests.
*/
struct PostRefundData *next;
/**
* DLL of (suspended) requests.
*/
struct PostRefundData *prev;
/**
* Refunds for this order. Head of DLL.
*/
struct CoinRefund *cr_head;
/**
* Refunds for this order. Tail of DLL.
*/
struct CoinRefund *cr_tail;
/**
* Context of the request.
*/
struct TMH_HandlerContext *hc;
/**
* Entry in the #resume_timeout_heap for this check payment, if we are
* suspended.
*/
struct TMH_SuspendedConnection sc;
/**
* order ID for the payment
*/
const char *order_id;
/**
* Where to get the contract
*/
const char *contract_url;
/**
* fulfillment URL of the contract (valid as long as
* @e contract_terms is valid).
*/
const char *fulfillment_url;
/**
* session of the client
*/
const char *session_id;
/**
* Contract terms of the payment we are checking. NULL when they
* are not (yet) known.
*/
json_t *contract_terms;
/**
* Total refunds granted for this payment. Only initialized
* if @e refunded is set to true.
*/
struct TALER_Amount refund_amount;
/**
* Did we suspend @a connection and are thus in
* the #prd_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;
/**
* Return code: #TALER_EC_NONE if successful.
*/
enum TALER_ErrorCode ec;
/**
* HTTP status to use for the reply, 0 if not yet known.
*/
unsigned int http_status;
/**
* Set to true if we are dealing with an unclaimed order
* (and thus @e h_contract_terms is not set, and certain
* DB queries will not work).
*/
bool unclaimed;
/**
* Set to true if this payment has been refunded and
* @e refund_amount is initialized.
*/
bool refunded;
/**
* Set to true if a refund is still available for the
* wallet for this payment.
*/
bool refund_available;
/**
* Set to true if the client requested HTML, otherwise
* we generate JSON.
*/
bool generate_html;
};
/**
* Head of DLL of (suspended) requests.
*/
static struct PostRefundData *prd_head;
/**
* Tail of DLL of (suspended) requests.
*/
static struct PostRefundData *prd_tail;
/**
* Function called when we are done processing a refund request.
* Frees memory associated with @a ctx.
*
* @param ctx a `struct PostRefundData`
*/
static void
refund_cleanup (void *ctx)
{
struct PostRefundData *prd = ctx;
struct CoinRefund *cr;
while (NULL != (cr = prd->cr_head))
{
GNUNET_CONTAINER_DLL_remove (prd->cr_head,
prd->cr_tail,
cr);
json_decref (cr->exchange_reply);
GNUNET_free (cr->exchange_url);
if (NULL != cr->fo)
{
TMH_EXCHANGES_keys4exchange_cancel (cr->fo);
cr->fo = NULL;
}
if (NULL != cr->rh)
{
TALER_EXCHANGE_refund_cancel (cr->rh);
cr->rh = NULL;
}
GNUNET_free (cr);
}
json_decref (prd->contract_terms);
GNUNET_free (prd);
}
/**
* Force resuming all suspended order lookups, needed during shutdown.
*/
void
TMH_force_wallet_refund_order_resume (void)
{
struct PostRefundData *prd;
while (NULL != (prd = prd_head))
{
GNUNET_CONTAINER_DLL_remove (prd_head,
prd_tail,
prd);
GNUNET_assert (GNUNET_YES == prd->suspended);
prd->suspended = GNUNET_SYSERR;
MHD_resume_connection (prd->sc.con);
}
}
/**
* Check if @a prd has exchange requests still pending.
*
* @param prd state to check
* @return true if activities are still pending
*/
static bool
exchange_operations_pending (struct PostRefundData *prd)
{
for (struct CoinRefund *cr = prd->cr_head;
NULL != cr;
cr = cr->next)
{
if ( (NULL != cr->fo) ||
(NULL != cr->rh) )
return true;
}
return false;
}
/**
* Check if @a prd is ready to be resumed, and if so, do it.
*
* @param prd refund request to be possibly ready
*/
static void
check_resume_prd (struct PostRefundData *prd)
{
if ( (TALER_EC_NONE == prd->ec) &&
exchange_operations_pending (prd) )
return;
GNUNET_CONTAINER_DLL_remove (prd_head,
prd_tail,
prd);
GNUNET_assert (prd->suspended);
prd->suspended = GNUNET_NO;
MHD_resume_connection (prd->sc.con);
TALER_MHD_daemon_trigger ();
}
/**
* Notify applications waiting for a client to obtain
* a refund.
*
* @param prd refund request with the change
*/
static void
notify_refund_obtained (struct PostRefundData *prd)
{
struct TMH_OrderPayEventP refund_eh = {
.header.size = htons (sizeof (refund_eh)),
.header.type = htons (TALER_DBEVENT_MERCHANT_REFUND_OBTAINED),
.merchant_pub = prd->hc->instance->merchant_pub
};
GNUNET_CRYPTO_hash (prd->order_id,
strlen (prd->order_id),
&refund_eh.h_order_id);
TMH_db->event_notify (TMH_db->cls,
&refund_eh.header,
NULL,
0);
}
/**
* Callbacks of this type are used to serve the result of submitting a
* refund request to an exchange.
*
* @param cls a `struct CoinRefund`
* @param rr response data
*/
static void
refund_cb (void *cls,
const struct TALER_EXCHANGE_RefundResponse *rr)
{
struct CoinRefund *cr = cls;
const struct TALER_EXCHANGE_HttpResponse *hr = &rr->hr;
cr->rh = NULL;
cr->exchange_status = hr->http_status;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Exchange refund status for coin %s is %u\n",
TALER_B2S (&cr->coin_pub),
hr->http_status);
if (MHD_HTTP_OK != hr->http_status)
{
cr->exchange_code = hr->ec;
cr->exchange_reply = json_incref ((json_t*) hr->reply);
}
else
{
enum GNUNET_DB_QueryStatus qs;
cr->exchange_pub = rr->details.ok.exchange_pub;
cr->exchange_sig = rr->details.ok.exchange_sig;
qs = TMH_db->insert_refund_proof (TMH_db->cls,
cr->refund_serial,
&rr->details.ok.exchange_sig,
&rr->details.ok.exchange_pub);
if (0 >= qs)
{
/* generally, this is relatively harmless for the merchant, but let's at
least log this. */
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Failed to persist exchange response to /refund in database: %d\n",
qs);
}
else
{
notify_refund_obtained (cr->prd);
}
}
check_resume_prd (cr->prd);
}
/**
* Function called with the result of a
* #TMH_EXCHANGES_keys4exchange()
* operation.
*
* @param cls a `struct CoinRefund *`
* @param keys keys of exchange, NULL on error
* @param exchange representation of the exchange
*/
static void
exchange_found_cb (void *cls,
struct TALER_EXCHANGE_Keys *keys,
struct TMH_Exchange *exchange)
{
struct CoinRefund *cr = cls;
struct PostRefundData *prd = cr->prd;
(void) exchange;
cr->fo = NULL;
if (NULL == keys)
{
prd->http_status = MHD_HTTP_GATEWAY_TIMEOUT;
prd->ec = TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT;
check_resume_prd (prd);
return;
}
cr->rh = TALER_EXCHANGE_refund (
TMH_curl_ctx,
cr->exchange_url,
keys,
&cr->refund_amount,
&prd->h_contract_terms,
&cr->coin_pub,
cr->rtransaction_id,
&prd->hc->instance->merchant_priv,
&refund_cb,
cr);
}
/**
* Function called with information about a refund.
* It is responsible for summing up the refund amount.
*
* @param cls closure
* @param refund_serial unique serial number of the refund
* @param timestamp time of the refund (for grouping of refunds in the wallet UI)
* @param coin_pub public coin from which the refund comes from
* @param exchange_url URL of the exchange that issued @a coin_pub
* @param rtransaction_id identificator of the refund
* @param reason human-readable explanation of the refund
* @param refund_amount refund amount which is being taken from @a coin_pub
* @param pending true if the this refund was not yet processed by the wallet/exchange
*/
static void
process_refunds_cb (void *cls,
uint64_t refund_serial,
struct GNUNET_TIME_Timestamp timestamp,
const struct TALER_CoinSpendPublicKeyP *coin_pub,
const char *exchange_url,
uint64_t rtransaction_id,
const char *reason,
const struct TALER_Amount *refund_amount,
bool pending)
{
struct PostRefundData *prd = cls;
struct CoinRefund *cr;
for (cr = prd->cr_head;
NULL != cr;
cr = cr->next)
if (cr->refund_serial == refund_serial)
return;
/* already known */
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Found refund of %s for coin %s with reason `%s' in database\n",
TALER_amount2s (refund_amount),
TALER_B2S (coin_pub),
reason);
cr = GNUNET_new (struct CoinRefund);
cr->refund_serial = refund_serial;
cr->exchange_url = GNUNET_strdup (exchange_url);
cr->prd = prd;
cr->coin_pub = *coin_pub;
cr->rtransaction_id = rtransaction_id;
cr->refund_amount = *refund_amount;
cr->execution_time = timestamp;
GNUNET_CONTAINER_DLL_insert (prd->cr_head,
prd->cr_tail,
cr);
if (prd->refunded)
{
GNUNET_assert (0 <=
TALER_amount_add (&prd->refund_amount,
&prd->refund_amount,
refund_amount));
return;
}
prd->refund_amount = *refund_amount;
prd->refunded = true;
prd->refund_available |= pending;
}
/**
* Obtain refunds for an order.
*
* @param rh context of the handler
* @param connection the MHD connection to handle
* @param[in,out] hc context with further information about the request
* @return MHD result code
*/
MHD_RESULT
TMH_post_orders_ID_refund (const struct TMH_RequestHandler *rh,
struct MHD_Connection *connection,
struct TMH_HandlerContext *hc)
{
struct PostRefundData *prd = hc->ctx;
enum GNUNET_DB_QueryStatus qs;
if (NULL == prd)
{
prd = GNUNET_new (struct PostRefundData);
prd->sc.con = connection;
prd->hc = hc;
prd->order_id = hc->infix;
hc->ctx = prd;
hc->cc = &refund_cleanup;
{
enum GNUNET_GenericReturnValue res;
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_fixed_auto ("h_contract",
&prd->h_contract_terms),
GNUNET_JSON_spec_end ()
};
res = TALER_MHD_parse_json_data (connection,
hc->request_body,
spec);
if (GNUNET_OK != res)
return (GNUNET_NO == res)
? MHD_YES
: MHD_NO;
}
TMH_db->preflight (TMH_db->cls);
{
json_t *contract_terms;
uint64_t order_serial;
qs = TMH_db->lookup_contract_terms (TMH_db->cls,
hc->instance->settings.id,
hc->infix,
&contract_terms,
&order_serial,
NULL);
if (0 > qs)
{
/* single, read-only SQL statements should never cause
serialization problems */
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
/* Always report on hard error as well to enable diagnostics */
GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"contract terms");
}
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
{
json_decref (contract_terms);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_NOT_FOUND,
TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
hc->infix);
}
{
struct TALER_PrivateContractHashP h_contract_terms;
if (GNUNET_OK !=
TALER_JSON_contract_hash (contract_terms,
&h_contract_terms))
{
GNUNET_break (0);
json_decref (contract_terms);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
NULL);
}
json_decref (contract_terms);
if (0 != GNUNET_memcmp (&h_contract_terms,
&prd->h_contract_terms))
{
return TALER_MHD_reply_with_error (
connection,
MHD_HTTP_FORBIDDEN,
TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER,
NULL);
}
}
}
}
if (GNUNET_SYSERR == prd->suspended)
return MHD_NO; /* we are in shutdown */
if (TALER_EC_NONE != prd->ec)
{
GNUNET_break (0 != prd->http_status);
/* kill pending coin refund operations immediately, just to be
extra sure they don't modify 'prd' after we already created
a reply (this might not be needed, but feels safer). */
for (struct CoinRefund *cr = prd->cr_head;
NULL != cr;
cr = cr->next)
{
if (NULL != cr->fo)
{
TMH_EXCHANGES_keys4exchange_cancel (cr->fo);
cr->fo = NULL;
}
if (NULL != cr->rh)
{
TALER_EXCHANGE_refund_cancel (cr->rh);
cr->rh = NULL;
}
}
return TALER_MHD_reply_with_error (connection,
prd->http_status,
prd->ec,
NULL);
}
{
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (TMH_currency,
&prd->refund_amount));
qs = TMH_db->lookup_refunds_detailed (TMH_db->cls,
hc->instance->settings.id,
&prd->h_contract_terms,
&process_refunds_cb,
prd);
if (0 > qs)
{
GNUNET_break (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"detailed refunds");
}
}
/* Now launch exchange interactions, unless we already have the
response in the database! */
for (struct CoinRefund *cr = prd->cr_head;
NULL != cr;
cr = cr->next)
{
enum GNUNET_DB_QueryStatus qs;
qs = TMH_db->lookup_refund_proof (TMH_db->cls,
cr->refund_serial,
&cr->exchange_sig,
&cr->exchange_pub);
switch (qs)
{
case GNUNET_DB_STATUS_HARD_ERROR:
case GNUNET_DB_STATUS_SOFT_ERROR:
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"refund proof");
case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
if (NULL == cr->exchange_reply)
{
/* We need to talk to the exchange */
cr->fo = TMH_EXCHANGES_keys4exchange (cr->exchange_url,
false,
&exchange_found_cb,
cr);
}
break;
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
/* We got a reply earlier, set status accordingly */
cr->exchange_status = MHD_HTTP_OK;
break;
}
}
/* Check if there are still exchange operations pending */
if (exchange_operations_pending (prd))
{
if (GNUNET_NO == prd->suspended)
{
prd->suspended = GNUNET_YES;
MHD_suspend_connection (connection);
GNUNET_CONTAINER_DLL_insert (prd_head,
prd_tail,
prd);
}
return MHD_YES; /* we're still talking to the exchange */
}
{
json_t *ra;
ra = json_array ();
GNUNET_assert (NULL != ra);
for (struct CoinRefund *cr = prd->cr_head;
NULL != cr;
cr = cr->next)
{
json_t *refund;
if (MHD_HTTP_OK != cr->exchange_status)
{
if (NULL == cr->exchange_reply)
{
refund = GNUNET_JSON_PACK (
GNUNET_JSON_pack_string ("type",
"failure"),
GNUNET_JSON_pack_uint64 ("exchange_status",
cr->exchange_status),
GNUNET_JSON_pack_uint64 ("rtransaction_id",
cr->rtransaction_id),
GNUNET_JSON_pack_data_auto ("coin_pub",
&cr->coin_pub),
TALER_JSON_pack_amount ("refund_amount",
&cr->refund_amount),
GNUNET_JSON_pack_timestamp ("execution_time",
cr->execution_time));
}
else
{
refund = GNUNET_JSON_PACK (
GNUNET_JSON_pack_string ("type",
"failure"),
GNUNET_JSON_pack_uint64 ("exchange_status",
cr->exchange_status),
GNUNET_JSON_pack_uint64 ("exchange_code",
cr->exchange_code),
GNUNET_JSON_pack_object_incref ("exchange_reply",
cr->exchange_reply),
GNUNET_JSON_pack_uint64 ("rtransaction_id",
cr->rtransaction_id),
GNUNET_JSON_pack_data_auto ("coin_pub",
&cr->coin_pub),
TALER_JSON_pack_amount ("refund_amount",
&cr->refund_amount),
GNUNET_JSON_pack_timestamp ("execution_time",
cr->execution_time));
}
}
else
{
refund = GNUNET_JSON_PACK (
GNUNET_JSON_pack_string ("type",
"success"),
GNUNET_JSON_pack_uint64 ("exchange_status",
cr->exchange_status),
GNUNET_JSON_pack_data_auto ("exchange_sig",
&cr->exchange_sig),
GNUNET_JSON_pack_data_auto ("exchange_pub",
&cr->exchange_pub),
GNUNET_JSON_pack_uint64 ("rtransaction_id",
cr->rtransaction_id),
GNUNET_JSON_pack_data_auto ("coin_pub",
&cr->coin_pub),
TALER_JSON_pack_amount ("refund_amount",
&cr->refund_amount),
GNUNET_JSON_pack_timestamp ("execution_time",
cr->execution_time));
}
GNUNET_assert (
0 ==
json_array_append_new (ra,
refund));
}
return TALER_MHD_REPLY_JSON_PACK (
connection,
MHD_HTTP_OK,
TALER_JSON_pack_amount ("refund_amount",
&prd->refund_amount),
GNUNET_JSON_pack_array_steal ("refunds",
ra),
GNUNET_JSON_pack_data_auto ("merchant_pub",
&hc->instance->merchant_pub));
}
return MHD_YES;
}
/* end of taler-merchant-httpd_post-orders-ID-refund.c */