/*
This file is part of TALER
Copyright (C) 2014-2017 Inria and GNUnet e.V.
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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with
TALER; see the file COPYING. If not, see
*/
/**
* @file taler-exchange-httpd_refund.c
* @brief Handle /refund requests; parses the POST and JSON and
* verifies the coin signature before handing things off
* to the database.
* @author Florian Dold
* @author Benedikt Mueller
* @author Christian Grothoff
*/
#include "platform.h"
#include
#include
#include
#include
#include
#include "taler_json_lib.h"
#include "taler_mhd_lib.h"
#include "taler-exchange-httpd_refund.h"
#include "taler-exchange-httpd_responses.h"
#include "taler-exchange-httpd_keystate.h"
#include "taler-exchange-httpd_validation.h"
/**
* Generate successful refund confirmation message.
*
* @param connection connection to the client
* @param refund details about the successful refund
* @return MHD result code
*/
static int
reply_refund_success (struct MHD_Connection *connection,
const struct TALER_EXCHANGEDB_Refund *refund)
{
struct TALER_RefundConfirmationPS rc;
struct TALER_ExchangePublicKeyP pub;
struct TALER_ExchangeSignatureP sig;
rc.purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND);
rc.purpose.size = htonl (sizeof (struct TALER_RefundConfirmationPS));
rc.h_contract_terms = refund->h_contract_terms;
rc.coin_pub = refund->coin.coin_pub;
rc.merchant = refund->merchant_pub;
rc.rtransaction_id = GNUNET_htonll (refund->rtransaction_id);
TALER_amount_hton (&rc.refund_amount,
&refund->refund_amount);
TALER_amount_hton (&rc.refund_fee,
&refund->refund_fee);
if (GNUNET_OK !=
TEH_KS_sign (&rc.purpose,
&pub,
&sig))
{
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_EXCHANGE_BAD_CONFIGURATION,
"no keys");
}
return TALER_MHD_reply_json_pack (connection,
MHD_HTTP_OK,
"{s:s, s:o, s:o}",
"status", "REFUND_OK",
"sig", GNUNET_JSON_from_data_auto (&sig),
"pub", GNUNET_JSON_from_data_auto (&pub));
}
/**
* Generate generic refund failure message. All the details
* are in the @a response_code. The body can be empty.
*
* @param connection connection to the client
* @param response_code response code to generate
* @param ec taler error code to include
* @return MHD result code
*/
static int
reply_refund_failure (struct MHD_Connection *connection,
unsigned int response_code,
enum TALER_ErrorCode ec)
{
return TALER_MHD_reply_json_pack (connection,
response_code,
"{s:s, s:I}",
"hint", "refund failure",
"code", (json_int_t) ec);
}
/**
* Generate refund conflict failure message. Returns the
* transaction list @a tl with the details about the conflict.
*
* @param connection connection to the client
* @param tl transaction list showing the conflict
* @return MHD result code
*/
static int
reply_refund_conflict (struct MHD_Connection *connection,
const struct TALER_EXCHANGEDB_TransactionList *tl)
{
return TALER_MHD_reply_json_pack (connection,
MHD_HTTP_CONFLICT,
"{s:s, s:I, s:o}",
"hint", "conflicting refund",
"code",
(json_int_t) TALER_EC_REFUND_CONFLICT,
"history",
TEH_RESPONSE_compile_transaction_history (
tl));
}
/**
* Execute a "/refund" transaction. Returns a confirmation that the
* refund was successful, or a failure if we are not aware of a
* matching /deposit or if it is too late to do the refund.
*
* IF it returns a non-error code, the transaction logic MUST
* NOT queue a MHD response. IF it returns an hard error, the
* transaction logic MUST queue a MHD response and set @a mhd_ret. IF
* it returns the soft error code, the function MAY be called again to
* retry and MUST not queue a MHD response.
*
* @param cls closure with a `const struct TALER_EXCHANGEDB_Refund *`
* @param connection MHD request which triggered the transaction
* @param session database session to use
* @param[out] mhd_ret set to MHD response status for @a connection,
* if transaction failed (!)
* @return transaction status
*/
static enum GNUNET_DB_QueryStatus
refund_transaction (void *cls,
struct MHD_Connection *connection,
struct TALER_EXCHANGEDB_Session *session,
int *mhd_ret)
{
const struct TALER_EXCHANGEDB_Refund *refund = cls;
struct TALER_EXCHANGEDB_TransactionList *tl;
const struct TALER_EXCHANGEDB_Deposit *dep;
const struct TALER_EXCHANGEDB_Refund *ref;
struct TEH_KS_StateHandle *mks;
struct TALER_EXCHANGEDB_DenominationKeyIssueInformation *dki;
struct TALER_Amount expect_fee;
enum GNUNET_DB_QueryStatus qs;
int deposit_found;
int refund_found;
int fee_cmp;
unsigned int hc;
enum TALER_ErrorCode ec;
dep = NULL;
ref = NULL;
tl = NULL;
qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls,
session,
&refund->coin.coin_pub,
GNUNET_NO,
&tl);
if (0 > qs)
{
if (GNUNET_DB_STATUS_HARD_ERROR == qs)
*mhd_ret = reply_refund_failure (connection,
MHD_HTTP_NOT_FOUND,
TALER_EC_REFUND_COIN_NOT_FOUND);
return qs;
}
deposit_found = GNUNET_NO;
refund_found = GNUNET_NO;
for (struct TALER_EXCHANGEDB_TransactionList *tlp = tl;
NULL != tlp;
tlp = tlp->next)
{
switch (tlp->type)
{
case TALER_EXCHANGEDB_TT_DEPOSIT:
if (GNUNET_NO == deposit_found)
{
if ( (0 == memcmp (&tlp->details.deposit->merchant_pub,
&refund->merchant_pub,
sizeof (struct TALER_MerchantPublicKeyP))) &&
(0 == memcmp (&tlp->details.deposit->h_contract_terms,
&refund->h_contract_terms,
sizeof (struct GNUNET_HashCode))) )
{
dep = tlp->details.deposit;
deposit_found = GNUNET_YES;
break;
}
}
break;
case TALER_EXCHANGEDB_TT_REFRESH_MELT:
/* Melts cannot be refunded, ignore here */
break;
case TALER_EXCHANGEDB_TT_REFUND:
if (GNUNET_NO == refund_found)
{
/* First, check if existing refund request is identical */
if ( (0 == memcmp (&tlp->details.refund->merchant_pub,
&refund->merchant_pub,
sizeof (struct TALER_MerchantPublicKeyP))) &&
(0 == memcmp (&tlp->details.refund->h_contract_terms,
&refund->h_contract_terms,
sizeof (struct GNUNET_HashCode))) &&
(tlp->details.refund->rtransaction_id == refund->rtransaction_id) )
{
ref = tlp->details.refund;
refund_found = GNUNET_YES;
break;
}
/* Second, check if existing refund request conflicts */
if ( (0 == memcmp (&tlp->details.refund->merchant_pub,
&refund->merchant_pub,
sizeof (struct TALER_MerchantPublicKeyP))) &&
(0 == memcmp (&tlp->details.refund->h_contract_terms,
&refund->h_contract_terms,
sizeof (struct GNUNET_HashCode))) &&
(tlp->details.refund->rtransaction_id != refund->rtransaction_id) )
{
GNUNET_break_op (0); /* conflicting refund found */
refund_found = GNUNET_SYSERR;
/* NOTE: Alternatively we could total up all existing
refunds and check if the sum still permits the
refund requested (thus allowing multiple, partial
refunds). Fow now, we keep it simple. */
break;
}
}
break;
case TALER_EXCHANGEDB_TT_OLD_COIN_PAYBACK:
/* Paybacks cannot be refunded, ignore here */
break;
case TALER_EXCHANGEDB_TT_PAYBACK:
/* Paybacks cannot be refunded, ignore here */
break;
case TALER_EXCHANGEDB_TT_PAYBACK_REFRESH:
/* Paybacks cannot be refunded, ignore here */
break;
}
}
/* handle if deposit was NOT found */
if (GNUNET_NO == deposit_found)
{
TALER_LOG_WARNING ("Deposit to /refund was not found\n");
TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
tl);
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_NOT_FOUND,
TALER_EC_REFUND_DEPOSIT_NOT_FOUND,
"deposit unknown");
return GNUNET_DB_STATUS_HARD_ERROR;
}
/* handle if conflicting refund found */
if (GNUNET_SYSERR == refund_found)
{
*mhd_ret = reply_refund_conflict (connection,
tl);
TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
tl);
return GNUNET_DB_STATUS_HARD_ERROR;
}
/* handle if identical refund found */
if (GNUNET_YES == refund_found)
{
/* /refund already done, simply re-transmit confirmation */
*mhd_ret = reply_refund_success (connection,
ref);
TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
tl);
return GNUNET_DB_STATUS_HARD_ERROR;
}
/* check currency is compatible */
if ( (GNUNET_YES !=
TALER_amount_cmp_currency (&refund->refund_amount,
&dep->amount_with_fee)) ||
(GNUNET_YES !=
TALER_amount_cmp_currency (&refund->refund_fee,
&dep->deposit_fee)) )
{
GNUNET_break_op (0); /* currency missmatch */
*mhd_ret = reply_refund_failure (connection,
MHD_HTTP_PRECONDITION_FAILED,
TALER_EC_REFUND_CURRENCY_MISSMATCH);
return GNUNET_DB_STATUS_HARD_ERROR;
}
/* check if we already send the money for the /deposit */
qs = TEH_plugin->test_deposit_done (TEH_plugin->cls,
session,
dep);
if (GNUNET_DB_STATUS_HARD_ERROR == qs)
{
/* Internal error, we first had the deposit in the history,
but now it is gone? */
GNUNET_break (0);
TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
tl);
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_REFUND_DB_INCONSISTENT,
"database inconsistent");
return qs;
}
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
return qs; /* go and retry */
if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
{
/* money was already transferred to merchant, can no longer refund */
TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
tl);
*mhd_ret = reply_refund_failure (connection,
MHD_HTTP_GONE,
TALER_EC_REFUND_MERCHANT_ALREADY_PAID);
return GNUNET_DB_STATUS_HARD_ERROR;
}
/* check refund amount is sufficiently low */
if (1 == TALER_amount_cmp (&refund->refund_amount,
&dep->amount_with_fee) )
{
GNUNET_break_op (0); /* cannot refund more than original value */
TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
tl);
*mhd_ret = reply_refund_failure (connection,
MHD_HTTP_PRECONDITION_FAILED,
TALER_EC_REFUND_INSUFFICIENT_FUNDS);
return GNUNET_DB_STATUS_HARD_ERROR;
}
// FIXME: do this outside of transaction function?
/* Check refund fee matches fee of denomination key! */
mks = TEH_KS_acquire (GNUNET_TIME_absolute_get ());
if (NULL == mks)
{
TALER_LOG_ERROR ("Lacking keys to operate\n");
TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
tl);
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_EXCHANGE_BAD_CONFIGURATION,
"no keys");
return GNUNET_DB_STATUS_HARD_ERROR;
}
dki = TEH_KS_denomination_key_lookup_by_hash (mks,
&dep->coin.denom_pub_hash,
TEH_KS_DKU_DEPOSIT,
&ec,
&hc);
if (NULL == dki)
{
/* DKI not found, but we do have a coin with this DK in our database;
not good... */
GNUNET_break (0);
TEH_KS_release (mks);
TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
tl);
*mhd_ret = TALER_MHD_reply_with_error (connection,
hc,
ec,
"denomination not found, but coin known");
return GNUNET_DB_STATUS_HARD_ERROR;
}
TALER_amount_ntoh (&expect_fee,
&dki->issue.properties.fee_refund);
fee_cmp = TALER_amount_cmp (&refund->refund_fee,
&expect_fee);
TEH_KS_release (mks);
if (-1 == fee_cmp)
{
TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
tl);
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_REFUND_FEE_TOO_LOW,
"refund_fee");
return GNUNET_DB_STATUS_HARD_ERROR;
}
if (1 == fee_cmp)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Refund fee proposed by merchant is higher than necessary.\n");
}
TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
tl);
/* Finally, store new refund data */
qs = TEH_plugin->insert_refund (TEH_plugin->cls,
session,
refund);
if (GNUNET_DB_STATUS_HARD_ERROR == qs)
{
TALER_LOG_WARNING ("Failed to store /refund information in database\n");
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_REFUND_STORE_DB_ERROR,
"could not persist store information");
return qs;
}
/* Success or soft failure */
return qs;
}
/**
* We have parsed the JSON information about the refund, do some basic
* sanity checks (especially that the signature on the coin is valid)
* and then execute the refund. Note that we need the DB to check
* the fee structure, so this is not done here.
*
* @param connection the MHD connection to handle
* @param refund information about the refund
* @return MHD result code
*/
static int
verify_and_execute_refund (struct MHD_Connection *connection,
const struct TALER_EXCHANGEDB_Refund *refund)
{
struct TALER_RefundRequestPS rr;
int mhd_ret;
rr.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_REFUND);
rr.purpose.size = htonl (sizeof (struct TALER_RefundRequestPS));
rr.h_contract_terms = refund->h_contract_terms;
rr.coin_pub = refund->coin.coin_pub;
rr.merchant = refund->merchant_pub;
rr.rtransaction_id = GNUNET_htonll (refund->rtransaction_id);
TALER_amount_hton (&rr.refund_amount,
&refund->refund_amount);
TALER_amount_hton (&rr.refund_fee,
&refund->refund_fee);
if (GNUNET_YES !=
TALER_amount_cmp_currency (&refund->refund_amount,
&refund->refund_fee) )
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_REFUND_FEE_CURRENCY_MISSMATCH,
"refund_fee");
}
if (-1 == TALER_amount_cmp (&refund->refund_amount,
&refund->refund_fee) )
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_REFUND_FEE_ABOVE_AMOUNT,
"refund_amount");
}
if (GNUNET_OK !=
GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_MERCHANT_REFUND,
&rr.purpose,
&refund->merchant_sig.eddsa_sig,
&refund->merchant_pub.eddsa_pub))
{
TALER_LOG_WARNING ("Invalid signature on /refund request\n");
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_FORBIDDEN,
TALER_EC_REFUND_MERCHANT_SIGNATURE_INVALID,
"merchant_sig");
}
if (GNUNET_OK !=
TEH_DB_run_transaction (connection,
"run refund",
&mhd_ret,
&refund_transaction,
(void *) refund))
return mhd_ret;
return reply_refund_success (connection,
refund);
}
/**
* Handle a "/refund" request. Parses the JSON, and, if successful,
* passes the JSON data to #verify_and_execute_refund() to
* further check the details of the operation specified. If
* everything checks out, this will ultimately lead to the "/refund"
* being executed, or rejected.
*
* @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
* @return MHD result code
*/
int
TEH_REFUND_handler_refund (struct TEH_RequestHandler *rh,
struct MHD_Connection *connection,
void **connection_cls,
const char *upload_data,
size_t *upload_data_size)
{
json_t *json;
int res;
struct TALER_EXCHANGEDB_Refund refund;
struct GNUNET_JSON_Specification spec[] = {
TALER_JSON_spec_amount ("refund_amount", &refund.refund_amount),
TALER_JSON_spec_amount ("refund_fee", &refund.refund_fee),
GNUNET_JSON_spec_fixed_auto ("h_contract_terms", &refund.h_contract_terms),
GNUNET_JSON_spec_fixed_auto ("coin_pub", &refund.coin.coin_pub),
GNUNET_JSON_spec_fixed_auto ("merchant_pub", &refund.merchant_pub),
GNUNET_JSON_spec_uint64 ("rtransaction_id", &refund.rtransaction_id),
GNUNET_JSON_spec_fixed_auto ("merchant_sig", &refund.merchant_sig),
GNUNET_JSON_spec_end ()
};
(void) rh;
res = TALER_MHD_parse_post_json (connection,
connection_cls,
upload_data,
upload_data_size,
&json);
if (GNUNET_SYSERR == res)
return MHD_NO;
if ( (GNUNET_NO == res) || (NULL == json) )
return MHD_YES;
res = TALER_MHD_parse_json_data (connection,
json,
spec);
json_decref (json);
if (GNUNET_SYSERR == res)
return MHD_NO; /* hard failure */
if (GNUNET_NO == res)
return MHD_YES; /* failure */
res = verify_and_execute_refund (connection,
&refund);
GNUNET_JSON_parse_free (spec);
return res;
}
/* end of taler-exchange-httpd_refund.c */