diff options
author | Christian Grothoff <christian@grothoff.org> | 2017-06-19 16:07:34 +0200 |
---|---|---|
committer | Christian Grothoff <christian@grothoff.org> | 2017-06-19 16:07:34 +0200 |
commit | 92e6744ac032a3c4c4118ac6b251f769c5478aa6 (patch) | |
tree | 9098913719d9b1a70d0ed8d8a0ba8e500c96768a /src/exchange/taler-exchange-httpd_refund.c | |
parent | 3d701e8d2a39e766b8345e242c3ffb501f935a3e (diff) | |
download | exchange-92e6744ac032a3c4c4118ac6b251f769c5478aa6.tar.gz exchange-92e6744ac032a3c4c4118ac6b251f769c5478aa6.tar.bz2 exchange-92e6744ac032a3c4c4118ac6b251f769c5478aa6.zip |
address #5010 for /refund
Diffstat (limited to 'src/exchange/taler-exchange-httpd_refund.c')
-rw-r--r-- | src/exchange/taler-exchange-httpd_refund.c | 357 |
1 files changed, 354 insertions, 3 deletions
diff --git a/src/exchange/taler-exchange-httpd_refund.c b/src/exchange/taler-exchange-httpd_refund.c index a5787eea9..9846c73ba 100644 --- a/src/exchange/taler-exchange-httpd_refund.c +++ b/src/exchange/taler-exchange-httpd_refund.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014, 2015, 2016 Inria and GNUnet e.V. + 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 @@ -37,6 +37,350 @@ /** + * 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); + TEH_KS_sign (&rc.purpose, + &pub, + &sig); + return TEH_RESPONSE_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 TEH_RESPONSE_reply_json_pack (connection, + response_code, + "{s:s, s:I}", + "status", "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 TEH_RESPONSE_reply_json_pack (connection, + MHD_HTTP_CONFLICT, + "{s:s, s:I, s:o}", + "status", "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; + + dep = NULL; + ref = NULL; + tl = NULL; + qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, + session, + &refund->coin.coin_pub, + &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_PAYBACK: + /* Paybacks cannot be refunded, ignore here */ + break; + } + } + /* handle if deposit was NOT found */ + if (GNUNET_NO == deposit_found) + { + TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, + tl); + *mhd_ret = TEH_RESPONSE_reply_transaction_unknown (connection, + TALER_EC_REFUND_DEPOSIT_NOT_FOUND); + 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 */ + // FIXME: DB API... + 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 = TEH_RESPONSE_reply_internal_error (connection, + 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 (); + dki = TEH_KS_denomination_key_lookup (mks, + &dep->coin.denom_pub, + TEH_KS_DKU_DEPOSIT); + 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 = TEH_RESPONSE_reply_internal_error (connection, + TALER_EC_REFUND_DENOMINATION_KEY_NOT_FOUND, + "denomination key not found"); + 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 = TEH_RESPONSE_reply_arg_invalid (connection, + 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 = TEH_RESPONSE_reply_internal_db_error (connection, + TALER_EC_REFUND_STORE_DB_ERROR); + 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 @@ -51,6 +395,7 @@ 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)); @@ -90,8 +435,14 @@ verify_and_execute_refund (struct MHD_Connection *connection, TALER_EC_REFUND_MERCHANT_SIGNATURE_INVALID, "merchant_sig"); } - return TEH_DB_execute_refund (connection, - refund); + if (GNUNET_OK != + TEH_DB_run_transaction (connection, + &mhd_ret, + &refund_transaction, + (void *) refund)) + return mhd_ret; + return reply_refund_success (connection, + refund); } |