/* This file is part of TALER Copyright (C) 2014-2020 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 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_keys.h" /** * Generate successful refund confirmation message. * * @param connection connection to the client * @param coin_pub public key of the coin * @param refund details about the successful refund * @return MHD result code */ static MHD_RESULT reply_refund_success (struct MHD_Connection *connection, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_EXCHANGEDB_RefundListEntry *refund) { struct TALER_ExchangePublicKeyP pub; struct TALER_ExchangeSignatureP sig; enum TALER_ErrorCode ec; if (TALER_EC_NONE != (ec = TALER_exchange_online_refund_confirmation_sign ( &TEH_keys_exchange_sign_, &refund->h_contract_terms, coin_pub, &refund->merchant_pub, refund->rtransaction_id, &refund->refund_amount, &pub, &sig))) { return TALER_MHD_reply_with_ec (connection, ec, NULL); } return TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_OK, GNUNET_JSON_pack_data_auto ("exchange_sig", &sig), GNUNET_JSON_pack_data_auto ("exchange_pub", &pub)); } /** * Closure for refund_transaction(). */ struct RefundContext { /** * Details about the deposit operation. */ const struct TALER_EXCHANGEDB_Refund *refund; /** * Deposit fee of the coin. */ struct TALER_Amount deposit_fee; /** * Unique ID of the coin in known_coins. */ uint64_t known_coin_id; }; /** * 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[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, MHD_RESULT *mhd_ret) { struct RefundContext *rctx = cls; const struct TALER_EXCHANGEDB_Refund *refund = rctx->refund; enum GNUNET_DB_QueryStatus qs; bool not_found; bool refund_ok; bool conflict; bool gone; /* Finally, store new refund data */ qs = TEH_plugin->do_refund (TEH_plugin->cls, refund, &rctx->deposit_fee, rctx->known_coin_id, ¬_found, &refund_ok, &gone, &conflict); if (0 > qs) { if (GNUNET_DB_STATUS_HARD_ERROR == qs) *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "do refund"); return qs; } if (gone) { *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_GONE, TALER_EC_EXCHANGE_REFUND_MERCHANT_ALREADY_PAID, NULL); return GNUNET_DB_STATUS_HARD_ERROR; } if (conflict) { GNUNET_break_op (0); *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( connection, TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT, &refund->coin.denom_pub_hash, &refund->coin.coin_pub); return GNUNET_DB_STATUS_HARD_ERROR; } if (not_found) { *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_EXCHANGE_REFUND_DEPOSIT_NOT_FOUND, NULL); return GNUNET_DB_STATUS_HARD_ERROR; } if (! refund_ok) { *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( connection, TALER_EC_EXCHANGE_REFUND_CONFLICT_DEPOSIT_INSUFFICIENT, &refund->coin.denom_pub_hash, &refund->coin.coin_pub); return GNUNET_DB_STATUS_HARD_ERROR; } 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[in,out] refund information about the refund * @return MHD result code */ static MHD_RESULT verify_and_execute_refund (struct MHD_Connection *connection, struct TALER_EXCHANGEDB_Refund *refund) { struct RefundContext rctx = { .refund = refund }; TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_merchant_refund_verify (&refund->coin.coin_pub, &refund->details.h_contract_terms, refund->details.rtransaction_id, &refund->details.refund_amount, &refund->details.merchant_pub, &refund->details.merchant_sig)) { TALER_LOG_WARNING ("Invalid signature on refund request\n"); return TALER_MHD_reply_with_error (connection, MHD_HTTP_FORBIDDEN, TALER_EC_EXCHANGE_REFUND_MERCHANT_SIGNATURE_INVALID, NULL); } /* Fetch the coin's denomination (hash) */ { enum GNUNET_DB_QueryStatus qs; qs = TEH_plugin->get_coin_denomination (TEH_plugin->cls, &refund->coin.coin_pub, &rctx.known_coin_id, &refund->coin.denom_pub_hash); if (0 > qs) { MHD_RESULT res; char *dhs; GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); dhs = GNUNET_STRINGS_data_to_string_alloc ( &refund->coin.denom_pub_hash, sizeof (refund->coin.denom_pub_hash)); res = TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_EXCHANGE_REFUND_COIN_NOT_FOUND, dhs); GNUNET_free (dhs); return res; } } { /* Obtain information about the coin's denomination! */ struct TEH_DenominationKey *dk; MHD_RESULT mret; dk = TEH_keys_denomination_by_hash (&refund->coin.denom_pub_hash, connection, &mret); if (NULL == dk) { /* DKI not found, but we do have a coin with this DK in our database; not good... */ GNUNET_break (0); return mret; } refund->details.refund_fee = dk->meta.fees.refund; rctx.deposit_fee = dk->meta.fees.deposit; } /* Finally run the actual transaction logic */ { MHD_RESULT mhd_ret; if (GNUNET_OK != TEH_DB_run_transaction (connection, "run refund", TEH_MT_REQUEST_OTHER, &mhd_ret, &refund_transaction, &rctx)) { return mhd_ret; } } return reply_refund_success (connection, &refund->coin.coin_pub, &refund->details); } /** * Handle a "/coins/$COIN_PUB/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 connection the MHD connection to handle * @param coin_pub public key of the coin * @param root uploaded JSON data * @return MHD result code */ MHD_RESULT TEH_handler_refund (struct MHD_Connection *connection, const struct TALER_CoinSpendPublicKeyP *coin_pub, const json_t *root) { struct TALER_EXCHANGEDB_Refund refund = { .details.refund_fee.currency = {0} /* set to invalid, just to be sure */ }; struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_amount ("refund_amount", TEH_currency, &refund.details.refund_amount), GNUNET_JSON_spec_fixed_auto ("h_contract_terms", &refund.details.h_contract_terms), GNUNET_JSON_spec_fixed_auto ("merchant_pub", &refund.details.merchant_pub), GNUNET_JSON_spec_uint64 ("rtransaction_id", &refund.details.rtransaction_id), GNUNET_JSON_spec_fixed_auto ("merchant_sig", &refund.details.merchant_sig), GNUNET_JSON_spec_end () }; refund.coin.coin_pub = *coin_pub; { enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_data (connection, root, spec); if (GNUNET_SYSERR == res) return MHD_NO; /* hard failure */ if (GNUNET_NO == res) return MHD_YES; /* failure */ } { MHD_RESULT res; res = verify_and_execute_refund (connection, &refund); GNUNET_JSON_parse_free (spec); return res; } } /* end of taler-exchange-httpd_refund.c */