/* This file is part of TALER Copyright (C) 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 */ /** * @file taler-exchange-httpd_reserves_open.c * @brief Handle /reserves/$RESERVE_PUB/open requests * @author Christian Grothoff */ #include "platform.h" #include #include #include "taler_mhd_lib.h" #include "taler_json_lib.h" #include "taler_dbevents.h" #include "taler-exchange-httpd_common_deposit.h" #include "taler-exchange-httpd_keys.h" #include "taler-exchange-httpd_reserves_open.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_open_transaction. */ struct ReserveOpenContext { /** * Public key of the reserve the inquiry is about. */ const struct TALER_ReservePublicKeyP *reserve_pub; /** * Desired (minimum) expiration time for the reserve. */ struct GNUNET_TIME_Timestamp desired_expiration; /** * Actual expiration time for the reserve. */ struct GNUNET_TIME_Timestamp reserve_expiration; /** * Timestamp of the request. */ struct GNUNET_TIME_Timestamp timestamp; /** * Client signature approving the request. */ struct TALER_ReserveSignatureP reserve_sig; /** * Global fees applying to the request. */ const struct TEH_GlobalFee *gf; /** * Amount to be paid from the reserve. */ struct TALER_Amount reserve_payment; /** * Actual cost to open the reserve. */ struct TALER_Amount open_cost; /** * Total amount that was deposited. */ struct TALER_Amount total; /** * Information about payments by coin. */ struct TEH_PurseDepositedCoin *payments; /** * Length of the @e payments array. */ unsigned int payments_len; /** * Desired minimum purse limit. */ uint32_t purse_limit; /** * Set to true if the reserve balance is too low * for the operation. */ bool no_funds; }; /** * Send reserve open to client. * * @param connection connection to the client * @param rsc reserve open data to return * @return MHD result code */ static MHD_RESULT reply_reserve_open_success (struct MHD_Connection *connection, const struct ReserveOpenContext *rsc) { struct GNUNET_TIME_Timestamp now; struct GNUNET_TIME_Timestamp re; unsigned int status; status = MHD_HTTP_OK; if (GNUNET_TIME_timestamp_cmp (rsc->reserve_expiration, <, rsc->desired_expiration)) status = MHD_HTTP_PAYMENT_REQUIRED; now = GNUNET_TIME_timestamp_get (); if (GNUNET_TIME_timestamp_cmp (rsc->reserve_expiration, <, now)) re = now; else re = rsc->reserve_expiration; return TALER_MHD_REPLY_JSON_PACK ( connection, status, GNUNET_JSON_pack_timestamp ("reserve_expiration", re), TALER_JSON_pack_amount ("open_cost", &rsc->open_cost)); } /** * Cleans up information in @a rsc, but does not * free @a rsc itself (allocated on the stack!). * * @param[in] rsc struct with information to clean up */ static void cleanup_rsc (struct ReserveOpenContext *rsc) { for (unsigned int i = 0; ipayments_len; i++) { TEH_common_purse_deposit_free_coin (&rsc->payments[i]); } GNUNET_free (rsc->payments); } /** * Function implementing /reserves/$RID/open transaction. Given the public * key of a reserve, return the associated transaction open. 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 ReserveOpenContext *` * @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 reserve_open_transaction (void *cls, struct MHD_Connection *connection, MHD_RESULT *mhd_ret) { struct ReserveOpenContext *rsc = cls; enum GNUNET_DB_QueryStatus qs; for (unsigned int i = 0; ipayments_len; i++) { struct TEH_PurseDepositedCoin *coin = &rsc->payments[i]; bool insufficient_funds = true; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Make coin %u known\n", i); qs = TEH_make_coin_known (&coin->cpi, connection, &coin->known_coin_id, mhd_ret); if (qs < 0) return qs; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Insert open deposit %u known\n", i); qs = TEH_plugin->insert_reserve_open_deposit ( TEH_plugin->cls, &coin->cpi, &coin->coin_sig, coin->known_coin_id, &coin->amount, &rsc->reserve_sig, rsc->reserve_pub, &insufficient_funds); /* 0 == qs is fine, then the coin was already spent for this very operation as identified by reserve_sig! */ 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_STORE_FAILED, "insert_reserve_open_deposit"); return qs; } if (insufficient_funds) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Handle insufficient funds\n"); *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( connection, TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, &coin->cpi.denom_pub_hash, &coin->cpi.coin_pub); return GNUNET_DB_STATUS_HARD_ERROR; } } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Do reserve open with reserve payment of %s\n", TALER_amount2s (&rsc->total)); qs = TEH_plugin->do_reserve_open (TEH_plugin->cls, /* inputs */ rsc->reserve_pub, &rsc->total, &rsc->reserve_payment, rsc->purse_limit, &rsc->reserve_sig, rsc->desired_expiration, rsc->timestamp, &rsc->gf->fees.account, /* outputs */ &rsc->no_funds, &rsc->open_cost, &rsc->reserve_expiration); 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, "do_reserve_open"); return GNUNET_DB_STATUS_HARD_ERROR; 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 (rsc->no_funds) { TEH_plugin->rollback (TEH_plugin->cls); *mhd_ret = TEH_RESPONSE_reply_reserve_insufficient_balance ( connection, TALER_EC_EXCHANGE_RESERVES_OPEN_INSUFFICIENT_FUNDS, &rsc->reserve_payment, rsc->reserve_pub); return GNUNET_DB_STATUS_HARD_ERROR; } return qs; } MHD_RESULT TEH_handler_reserves_open (struct TEH_RequestContext *rc, const struct TALER_ReservePublicKeyP *reserve_pub, const json_t *root) { struct ReserveOpenContext rsc; json_t *payments; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_timestamp ("request_timestamp", &rsc.timestamp), GNUNET_JSON_spec_timestamp ("reserve_expiration", &rsc.desired_expiration), GNUNET_JSON_spec_fixed_auto ("reserve_sig", &rsc.reserve_sig), GNUNET_JSON_spec_uint32 ("purse_limit", &rsc.purse_limit), GNUNET_JSON_spec_json ("payments", &payments), TALER_JSON_spec_amount ("reserve_payment", TEH_currency, &rsc.reserve_payment), GNUNET_JSON_spec_end () }; rsc.reserve_pub = reserve_pub; { 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, rsc.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); } } rsc.payments_len = json_array_size (payments); rsc.payments = GNUNET_new_array (rsc.payments_len, struct TEH_PurseDepositedCoin); rsc.total = rsc.reserve_payment; for (unsigned int i = 0; iconnection, coin, json_array_get (payments, i)); if (GNUNET_SYSERR == res) { GNUNET_break (0); cleanup_rsc (&rsc); return MHD_NO; /* hard failure */ } if (GNUNET_NO == res) { GNUNET_break_op (0); cleanup_rsc (&rsc); return MHD_YES; /* failure */ } if (0 > TALER_amount_add (&rsc.total, &rsc.total, &coin->amount_minus_fee)) { GNUNET_break (0); cleanup_rsc (&rsc); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, NULL); } } { struct TEH_KeyStateHandle *keys; keys = TEH_keys_get_state (); if (NULL == keys) { GNUNET_break (0); GNUNET_JSON_parse_free (spec); cleanup_rsc (&rsc); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, NULL); } rsc.gf = TEH_keys_global_fee_by_time (keys, rsc.timestamp); } if (NULL == rsc.gf) { GNUNET_break (0); cleanup_rsc (&rsc); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, NULL); } if (GNUNET_OK != TALER_wallet_reserve_open_verify (&rsc.reserve_payment, rsc.timestamp, rsc.desired_expiration, rsc.purse_limit, reserve_pub, &rsc.reserve_sig)) { GNUNET_break_op (0); cleanup_rsc (&rsc); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_FORBIDDEN, TALER_EC_EXCHANGE_RESERVES_OPEN_BAD_SIGNATURE, NULL); } { MHD_RESULT mhd_ret; if (GNUNET_OK != TEH_DB_run_transaction (rc->connection, "reserve open", TEH_MT_REQUEST_OTHER, &mhd_ret, &reserve_open_transaction, &rsc)) { cleanup_rsc (&rsc); return mhd_ret; } } { MHD_RESULT mhd_ret; mhd_ret = reply_reserve_open_success (rc->connection, &rsc); cleanup_rsc (&rsc); return mhd_ret; } } /* end of taler-exchange-httpd_reserves_open.c */