diff options
Diffstat (limited to 'src/exchange/taler-exchange-httpd_reserves_close.c')
-rw-r--r-- | src/exchange/taler-exchange-httpd_reserves_close.c | 448 |
1 files changed, 448 insertions, 0 deletions
diff --git a/src/exchange/taler-exchange-httpd_reserves_close.c b/src/exchange/taler-exchange-httpd_reserves_close.c new file mode 100644 index 000000000..bbf234428 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_reserves_close.c @@ -0,0 +1,448 @@ +/* + This file is part of TALER + Copyright (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 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-exchange-httpd_reserves_close.c + * @brief Handle /reserves/$RESERVE_PUB/close requests + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#include "taler_kyclogic_lib.h" +#include "taler_mhd_lib.h" +#include "taler_json_lib.h" +#include "taler_dbevents.h" +#include "taler-exchange-httpd_keys.h" +#include "taler-exchange-httpd_reserves_close.h" +#include "taler-exchange-httpd_responses.h" + + +/** + * How far do we allow a client's time to be off when + * checking the request timestamp? + */ +#define TIMESTAMP_TOLERANCE \ + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 15) + + +/** + * Closure for #reserve_close_transaction. + */ +struct ReserveCloseContext +{ + /** + * Public key of the reserve the inquiry is about. + */ + const struct TALER_ReservePublicKeyP *reserve_pub; + + /** + * Timestamp of the request. + */ + struct GNUNET_TIME_Timestamp timestamp; + + /** + * Client signature approving the request. + */ + struct TALER_ReserveSignatureP reserve_sig; + + /** + * Amount that will be wired (after closing fees). + */ + struct TALER_Amount wire_amount; + + /** + * Current balance of the reserve. + */ + struct TALER_Amount balance; + + /** + * Where to wire the funds, may be NULL. + */ + const char *payto_uri; + + /** + * Hash of the @e payto_uri, if given (otherwise zero). + */ + struct TALER_PaytoHashP h_payto; + + /** + * KYC status for the request. + */ + struct TALER_EXCHANGEDB_KycStatus kyc; + + /** + * Hash of the payto-URI that was used for the KYC decision. + */ + struct TALER_PaytoHashP kyc_payto; + + /** + * Query status from the amount_it() helper function. + */ + enum GNUNET_DB_QueryStatus qs; +}; + + +/** + * Send reserve close to client. + * + * @param connection connection to the client + * @param rhc reserve close to return + * @return MHD result code + */ +static MHD_RESULT +reply_reserve_close_success (struct MHD_Connection *connection, + const struct ReserveCloseContext *rhc) +{ + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + TALER_JSON_pack_amount ("wire_amount", + &rhc->wire_amount)); +} + + +/** + * Function called to iterate over KYC-relevant + * transaction amounts for a particular time range. + * Called within a database transaction, so must + * not start a new one. + * + * @param cls closure, identifies the event type and + * account to iterate over events for + * @param limit maximum time-range for which events + * should be fetched (timestamp in the past) + * @param cb function to call on each event found, + * events must be returned in reverse chronological + * order + * @param cb_cls closure for @a cb + */ +static void +amount_it (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_EXCHANGEDB_KycAmountCallback cb, + void *cb_cls) +{ + struct ReserveCloseContext *rcc = cls; + enum GNUNET_GenericReturnValue ret; + + ret = cb (cb_cls, + &rcc->balance, + GNUNET_TIME_absolute_get ()); + GNUNET_break (GNUNET_SYSERR != ret); + if (GNUNET_OK != ret) + return; + rcc->qs + = TEH_plugin->iterate_reserve_close_info ( + TEH_plugin->cls, + &rcc->kyc_payto, + limit, + cb, + cb_cls); +} + + +/** + * Function implementing /reserves/$RID/close transaction. Given the public + * key of a reserve, return the associated transaction close. Runs the + * transaction logic; 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 a `struct ReserveCloseContext *` + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!); unused + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +reserve_close_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct ReserveCloseContext *rcc = cls; + enum GNUNET_DB_QueryStatus qs; + char *payto_uri = NULL; + const struct TALER_WireFeeSet *wf; + + qs = TEH_plugin->select_reserve_close_info ( + TEH_plugin->cls, + rcc->reserve_pub, + &rcc->balance, + &payto_uri); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_reserve_close_info"); + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + return qs; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + + if ( (NULL == rcc->payto_uri) && + (NULL == payto_uri) ) + { + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_RESERVES_CLOSE_NO_TARGET_ACCOUNT, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if ( (NULL != rcc->payto_uri) && + ( (NULL == payto_uri) || + (0 != strcmp (payto_uri, + rcc->payto_uri)) ) ) + { + /* KYC check may be needed: we're not returning + the money to the account that funded the reserve + in the first place. */ + char *kyc_needed; + + TALER_payto_hash (rcc->payto_uri, + &rcc->kyc_payto); + rcc->qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + qs = TALER_KYCLOGIC_kyc_test_required ( + TALER_KYCLOGIC_KYC_TRIGGER_RESERVE_CLOSE, + &rcc->kyc_payto, + TEH_plugin->select_satisfied_kyc_processes, + TEH_plugin->cls, + &amount_it, + rcc, + &kyc_needed); + if (qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "iterate_reserve_close_info"); + return qs; + } + if (rcc->qs < 0) + { + if (GNUNET_DB_STATUS_SOFT_ERROR == rcc->qs) + return rcc->qs; + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "iterate_reserve_close_info"); + return qs; + } + if (NULL != kyc_needed) + { + rcc->kyc.ok = false; + qs = TEH_plugin->insert_kyc_requirement_for_account ( + TEH_plugin->cls, + kyc_needed, + &rcc->kyc_payto, + rcc->reserve_pub, + &rcc->kyc.requirement_row); + GNUNET_free (kyc_needed); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_kyc_requirement_for_account"); + } + return qs; + } + } + + rcc->kyc.ok = true; + if (NULL == rcc->payto_uri) + rcc->payto_uri = payto_uri; + + { + char *method; + + method = TALER_payto_get_method (rcc->payto_uri); + wf = TEH_wire_fees_by_time (rcc->timestamp, + method); + if (NULL == wf) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_WIRE_FEES_NOT_CONFIGURED, + method); + GNUNET_free (method); + return GNUNET_DB_STATUS_HARD_ERROR; + } + GNUNET_free (method); + } + + if (0 > + TALER_amount_subtract (&rcc->wire_amount, + &rcc->balance, + &wf->closing)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client attempted to close reserve with insufficient balance.\n"); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TEH_currency, + &rcc->wire_amount)); + *mhd_ret = reply_reserve_close_success (connection, + rcc); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + qs = TEH_plugin->insert_close_request (TEH_plugin->cls, + rcc->reserve_pub, + payto_uri, + &rcc->reserve_sig, + rcc->timestamp, + &rcc->balance, + &wf->closing); + GNUNET_free (payto_uri); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_break (0); + *mhd_ret + = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "insert_close_request"); + return qs; + } + if (qs <= 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + return qs; +} + + +MHD_RESULT +TEH_handler_reserves_close (struct TEH_RequestContext *rc, + const struct TALER_ReservePublicKeyP *reserve_pub, + const json_t *root) +{ + struct ReserveCloseContext rcc = { + .payto_uri = NULL, + .reserve_pub = reserve_pub + }; + MHD_RESULT mhd_ret; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("request_timestamp", + &rcc.timestamp), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_payto_uri ("payto_uri", + &rcc.payto_uri), + NULL), + GNUNET_JSON_spec_fixed_auto ("reserve_sig", + &rcc.reserve_sig), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (rc->connection, + root, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + } + + { + struct GNUNET_TIME_Timestamp now; + + now = GNUNET_TIME_timestamp_get (); + if (! GNUNET_TIME_absolute_approx_eq (now.abs_time, + rcc.timestamp.abs_time, + TIMESTAMP_TOLERANCE)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_GENERIC_CLOCK_SKEW, + NULL); + } + } + + if (NULL != rcc.payto_uri) + TALER_payto_hash (rcc.payto_uri, + &rcc.h_payto); + if (GNUNET_OK != + TALER_wallet_reserve_close_verify (rcc.timestamp, + &rcc.h_payto, + reserve_pub, + &rcc.reserve_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RESERVES_CLOSE_BAD_SIGNATURE, + NULL); + } + + if (GNUNET_OK != + TEH_DB_run_transaction (rc->connection, + "reserve close", + TEH_MT_REQUEST_OTHER, + &mhd_ret, + &reserve_close_transaction, + &rcc)) + { + return mhd_ret; + } + if (! rcc.kyc.ok) + return TEH_RESPONSE_reply_kyc_required (rc->connection, + &rcc.kyc_payto, + &rcc.kyc); + + return reply_reserve_close_success (rc->connection, + &rcc); +} + + +/* end of taler-exchange-httpd_reserves_close.c */ |