summaryrefslogtreecommitdiff
path: root/src/exchange/taler-exchange-httpd_purses_merge.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/exchange/taler-exchange-httpd_purses_merge.c')
-rw-r--r--src/exchange/taler-exchange-httpd_purses_merge.c696
1 files changed, 696 insertions, 0 deletions
diff --git a/src/exchange/taler-exchange-httpd_purses_merge.c b/src/exchange/taler-exchange-httpd_purses_merge.c
new file mode 100644
index 000000000..fb5ce4d90
--- /dev/null
+++ b/src/exchange/taler-exchange-httpd_purses_merge.c
@@ -0,0 +1,696 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file taler-exchange-httpd_purses_merge.c
+ * @brief Handle /purses/$PID/merge requests; parses the POST and JSON and
+ * verifies the reserve signature before handing things off
+ * to the database.
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <jansson.h>
+#include <microhttpd.h>
+#include <pthread.h>
+#include "taler_dbevents.h"
+#include "taler_json_lib.h"
+#include "taler_kyclogic_lib.h"
+#include "taler_mhd_lib.h"
+#include "taler-exchange-httpd_purses_merge.h"
+#include "taler-exchange-httpd_responses.h"
+#include "taler_exchangedb_lib.h"
+#include "taler-exchange-httpd_keys.h"
+
+
+/**
+ * Closure for #merge_transaction.
+ */
+struct PurseMergeContext
+{
+ /**
+ * Public key of the purse we are creating.
+ */
+ const struct TALER_PurseContractPublicKeyP *purse_pub;
+
+ /**
+ * Total amount to be put into the purse.
+ */
+ struct TALER_Amount target_amount;
+
+ /**
+ * Current amount in the purse.
+ */
+ struct TALER_Amount balance;
+
+ /**
+ * When should the purse expire.
+ */
+ struct GNUNET_TIME_Timestamp purse_expiration;
+
+ /**
+ * When the client signed the merge.
+ */
+ struct GNUNET_TIME_Timestamp merge_timestamp;
+
+ /**
+ * Our current time.
+ */
+ struct GNUNET_TIME_Timestamp exchange_timestamp;
+
+ /**
+ * Merge key for the purse.
+ */
+ struct TALER_PurseMergePublicKeyP merge_pub;
+
+ /**
+ * Signature of the reservce affiming this request.
+ */
+ struct TALER_ReserveSignatureP reserve_sig;
+
+ /**
+ * Signature of the client affiming the merge.
+ */
+ struct TALER_PurseMergeSignatureP merge_sig;
+
+ /**
+ * Public key of the reserve, as extracted from @e payto_uri.
+ */
+ struct TALER_ReservePublicKeyP reserve_pub;
+
+ /**
+ * Hash of the contract terms of the purse.
+ */
+ struct TALER_PrivateContractHashP h_contract_terms;
+
+ /**
+ * Fees that apply to this operation.
+ */
+ const struct TALER_WireFeeSet *wf;
+
+ /**
+ * URI of the account the purse is to be merged into.
+ * Must be of the form 'payto://taler-reserve/$EXCHANGE_URL/RESERVE_PUB'.
+ */
+ const char *payto_uri;
+
+ /**
+ * Hash of the @e payto_uri.
+ */
+ struct TALER_PaytoHashP h_payto;
+
+ /**
+ * KYC status of the operation.
+ */
+ struct TALER_EXCHANGEDB_KycStatus kyc;
+
+ /**
+ * Base URL of the exchange provider hosting the reserve.
+ */
+ char *provider_url;
+
+ /**
+ * Minimum age for deposits into this purse.
+ */
+ uint32_t min_age;
+};
+
+
+/**
+ * Send confirmation of purse creation success to client.
+ *
+ * @param connection connection to the client
+ * @param pcc details about the request that succeeded
+ * @return MHD result code
+ */
+static MHD_RESULT
+reply_merge_success (struct MHD_Connection *connection,
+ const struct PurseMergeContext *pcc)
+{
+ struct TALER_ExchangePublicKeyP pub;
+ struct TALER_ExchangeSignatureP sig;
+ enum TALER_ErrorCode ec;
+ struct TALER_Amount merge_amount;
+
+ if (0 <
+ TALER_amount_cmp (&pcc->balance,
+ &pcc->target_amount))
+ {
+ GNUNET_break (0);
+ return TALER_MHD_REPLY_JSON_PACK (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_JSON_pack_amount ("balance",
+ &pcc->balance),
+ TALER_JSON_pack_amount ("target_amount",
+ &pcc->target_amount));
+ }
+ if ( (NULL == pcc->provider_url) ||
+ (0 == strcmp (pcc->provider_url,
+ TEH_base_url)) )
+ {
+ /* wad fee is always zero if we stay at our own exchange */
+ merge_amount = pcc->target_amount;
+ }
+ else
+ {
+#if WAD_NOT_IMPLEMENTED
+ /* FIXME: figure out partner, lookup wad fee by partner! #7271 */
+ if (0 >
+ TALER_amount_subtract (&merge_amount,
+ &pcc->target_amount,
+ &wad_fee))
+ {
+ GNUNET_assert (GNUNET_OK ==
+ TALER_amount_set_zero (TEH_currency,
+ &merge_amount));
+ }
+#else
+ merge_amount = pcc->target_amount;
+#endif
+ }
+ if (TALER_EC_NONE !=
+ (ec = TALER_exchange_online_purse_merged_sign (
+ &TEH_keys_exchange_sign_,
+ pcc->exchange_timestamp,
+ pcc->purse_expiration,
+ &merge_amount,
+ pcc->purse_pub,
+ &pcc->h_contract_terms,
+ &pcc->reserve_pub,
+ (NULL != pcc->provider_url)
+ ? pcc->provider_url
+ : TEH_base_url,
+ &pub,
+ &sig)))
+ {
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_ec (connection,
+ ec,
+ NULL);
+ }
+ return TALER_MHD_REPLY_JSON_PACK (
+ connection,
+ MHD_HTTP_OK,
+ TALER_JSON_pack_amount ("merge_amount",
+ &merge_amount),
+ GNUNET_JSON_pack_timestamp ("exchange_timestamp",
+ pcc->exchange_timestamp),
+ GNUNET_JSON_pack_data_auto ("exchange_sig",
+ &sig),
+ GNUNET_JSON_pack_data_auto ("exchange_pub",
+ &pub));
+}
+
+
+/**
+ * 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 a `struct PurseMergeContext`
+ * @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_iterator (void *cls,
+ struct GNUNET_TIME_Absolute limit,
+ TALER_EXCHANGEDB_KycAmountCallback cb,
+ void *cb_cls)
+{
+ struct PurseMergeContext *pcc = cls;
+ enum GNUNET_DB_QueryStatus qs;
+
+ cb (cb_cls,
+ &pcc->target_amount,
+ GNUNET_TIME_absolute_get ());
+ qs = TEH_plugin->select_merge_amounts_for_kyc_check (
+ TEH_plugin->cls,
+ &pcc->h_payto,
+ limit,
+ cb,
+ cb_cls);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Got %d additional transactions for this merge and limit %llu\n",
+ qs,
+ (unsigned long long) limit.abs_value_us);
+ GNUNET_break (qs >= 0);
+}
+
+
+/**
+ * Execute database transaction for /purses/$PID/merge. 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 PurseMergeContext`
+ * @param connection MHD request context
+ * @param[out] mhd_ret set to MHD status on error
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+merge_transaction (void *cls,
+ struct MHD_Connection *connection,
+ MHD_RESULT *mhd_ret)
+{
+ struct PurseMergeContext *pcc = cls;
+ enum GNUNET_DB_QueryStatus qs;
+ bool in_conflict = true;
+ bool no_balance = true;
+ bool no_partner = true;
+ char *required;
+
+ qs = TALER_KYCLOGIC_kyc_test_required (
+ TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE,
+ &pcc->h_payto,
+ TEH_plugin->select_satisfied_kyc_processes,
+ TEH_plugin->cls,
+ &amount_iterator,
+ pcc,
+ &required);
+ 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,
+ "kyc_test_required");
+ return qs;
+ }
+ if (NULL != required)
+ {
+ pcc->kyc.ok = false;
+ qs = TEH_plugin->insert_kyc_requirement_for_account (
+ TEH_plugin->cls,
+ required,
+ &pcc->h_payto,
+ &pcc->reserve_pub,
+ &pcc->kyc.requirement_row);
+ GNUNET_free (required);
+ 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;
+ }
+ pcc->kyc.ok = true;
+ qs = TEH_plugin->do_purse_merge (
+ TEH_plugin->cls,
+ pcc->purse_pub,
+ &pcc->merge_sig,
+ pcc->merge_timestamp,
+ &pcc->reserve_sig,
+ pcc->provider_url,
+ &pcc->reserve_pub,
+ &no_partner,
+ &no_balance,
+ &in_conflict);
+ 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,
+ "purse merge");
+ return qs;
+ }
+ if (no_partner)
+ {
+ *mhd_ret =
+ TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_EXCHANGE_MERGE_PURSE_PARTNER_UNKNOWN,
+ pcc->provider_url);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ if (no_balance)
+ {
+ *mhd_ret =
+ TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_PAYMENT_REQUIRED,
+ TALER_EC_EXCHANGE_PURSE_NOT_FULL,
+ NULL);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ if (in_conflict)
+ {
+ struct TALER_PurseMergeSignatureP merge_sig;
+ struct GNUNET_TIME_Timestamp merge_timestamp;
+ char *partner_url = NULL;
+ struct TALER_ReservePublicKeyP reserve_pub;
+ bool refunded;
+
+ qs = TEH_plugin->select_purse_merge (TEH_plugin->cls,
+ pcc->purse_pub,
+ &merge_sig,
+ &merge_timestamp,
+ &partner_url,
+ &reserve_pub,
+ &refunded);
+ if (qs <= 0)
+ {
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
+ return qs;
+ TALER_LOG_WARNING (
+ "Failed to fetch merge purse information from database\n");
+ *mhd_ret =
+ TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "select purse merge");
+ return qs;
+ }
+ if (refunded)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Purse was already refunded\n");
+ *mhd_ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_GONE,
+ TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED,
+ NULL);
+ GNUNET_free (partner_url);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ if (0 !=
+ GNUNET_memcmp (&merge_sig,
+ &pcc->merge_sig))
+ {
+ *mhd_ret = TALER_MHD_REPLY_JSON_PACK (
+ connection,
+ MHD_HTTP_CONFLICT,
+ GNUNET_JSON_pack_timestamp ("merge_timestamp",
+ merge_timestamp),
+ GNUNET_JSON_pack_data_auto ("merge_sig",
+ &merge_sig),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_string ("partner_url",
+ partner_url)),
+ GNUNET_JSON_pack_data_auto ("reserve_pub",
+ &reserve_pub));
+ GNUNET_free (partner_url);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ /* idempotent! */
+ *mhd_ret = reply_merge_success (connection,
+ pcc);
+ GNUNET_free (partner_url);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+
+ return qs;
+}
+
+
+MHD_RESULT
+TEH_handler_purses_merge (
+ struct MHD_Connection *connection,
+ const struct TALER_PurseContractPublicKeyP *purse_pub,
+ const json_t *root)
+{
+ struct PurseMergeContext pcc = {
+ .purse_pub = purse_pub,
+ .exchange_timestamp = GNUNET_TIME_timestamp_get ()
+ };
+ struct GNUNET_JSON_Specification spec[] = {
+ TALER_JSON_spec_payto_uri ("payto_uri",
+ &pcc.payto_uri),
+ GNUNET_JSON_spec_fixed_auto ("reserve_sig",
+ &pcc.reserve_sig),
+ GNUNET_JSON_spec_fixed_auto ("merge_sig",
+ &pcc.merge_sig),
+ GNUNET_JSON_spec_timestamp ("merge_timestamp",
+ &pcc.merge_timestamp),
+ GNUNET_JSON_spec_end ()
+ };
+ struct TALER_PurseContractSignatureP purse_sig;
+ enum GNUNET_DB_QueryStatus qs;
+ bool http;
+
+ {
+ enum GNUNET_GenericReturnValue res;
+
+ res = TALER_MHD_parse_json_data (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 */
+ }
+ }
+
+ /* Fetch purse details */
+ qs = TEH_plugin->get_purse_request (TEH_plugin->cls,
+ pcc.purse_pub,
+ &pcc.merge_pub,
+ &pcc.purse_expiration,
+ &pcc.h_contract_terms,
+ &pcc.min_age,
+ &pcc.target_amount,
+ &pcc.balance,
+ &purse_sig);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "select purse request");
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "select purse request");
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN,
+ NULL);
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ /* continued below */
+ break;
+ }
+ /* parse 'payto_uri' into pcc.reserve_pub and provider_url */
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Received payto: `%s'\n",
+ pcc.payto_uri);
+ if ( (0 != strncmp (pcc.payto_uri,
+ "payto://taler-reserve/",
+ strlen ("payto://taler-reserve/"))) &&
+ (0 != strncmp (pcc.payto_uri,
+ "payto://taler-reserve-http/",
+ strlen ("payto://taler-reserve+http/"))) )
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "payto_uri");
+ }
+ http = (0 == strncmp (pcc.payto_uri,
+ "payto://taler-reserve-http/",
+ strlen ("payto://taler-reserve-http/")));
+
+ {
+ const char *host = &pcc.payto_uri[http
+ ? strlen ("payto://taler-reserve-http/")
+ : strlen ("payto://taler-reserve/")];
+ const char *slash = strchr (host,
+ '/');
+
+ if (NULL == slash)
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "payto_uri");
+ }
+ GNUNET_asprintf (&pcc.provider_url,
+ "%s://%.*s/",
+ http ? "http" : "https",
+ (int) (slash - host),
+ host);
+ slash++;
+ if (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (slash,
+ strlen (slash),
+ &pcc.reserve_pub,
+ sizeof (pcc.reserve_pub)))
+ {
+ GNUNET_break_op (0);
+ GNUNET_free (pcc.provider_url);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "payto_uri");
+ }
+ slash++;
+ }
+ TALER_payto_hash (pcc.payto_uri,
+ &pcc.h_payto);
+ if (0 == strcmp (pcc.provider_url,
+ TEH_base_url))
+ {
+ /* we use NULL to represent 'self' as the provider */
+ GNUNET_free (pcc.provider_url);
+ }
+ else
+ {
+ char *method = GNUNET_strdup ("FIXME-WAD #7271");
+
+ /* FIXME-#7271: lookup wire method by pcc.provider_url! */
+ pcc.wf = TEH_wire_fees_by_time (pcc.exchange_timestamp,
+ method);
+ if (NULL == pcc.wf)
+ {
+ MHD_RESULT res;
+
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Cannot merge purse: wire fees not configured!\n");
+ res = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_EXCHANGE_GENERIC_WIRE_FEES_MISSING,
+ method);
+ GNUNET_free (method);
+ return res;
+ }
+ GNUNET_free (method);
+ }
+ /* check signatures */
+ if (GNUNET_OK !=
+ TALER_wallet_purse_merge_verify (
+ pcc.payto_uri,
+ pcc.merge_timestamp,
+ pcc.purse_pub,
+ &pcc.merge_pub,
+ &pcc.merge_sig))
+ {
+ GNUNET_break_op (0);
+ GNUNET_free (pcc.provider_url);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_FORBIDDEN,
+ TALER_EC_EXCHANGE_PURSE_MERGE_INVALID_MERGE_SIGNATURE,
+ NULL);
+ }
+ {
+ struct TALER_Amount zero_purse_fee;
+
+ GNUNET_assert (GNUNET_OK ==
+ TALER_amount_set_zero (pcc.target_amount.currency,
+ &zero_purse_fee));
+ if (GNUNET_OK !=
+ TALER_wallet_account_merge_verify (
+ pcc.merge_timestamp,
+ pcc.purse_pub,
+ pcc.purse_expiration,
+ &pcc.h_contract_terms,
+ &pcc.target_amount,
+ &zero_purse_fee,
+ pcc.min_age,
+ TALER_WAMF_MODE_MERGE_FULLY_PAID_PURSE,
+ &pcc.reserve_pub,
+ &pcc.reserve_sig))
+ {
+ GNUNET_break_op (0);
+ GNUNET_free (pcc.provider_url);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_FORBIDDEN,
+ TALER_EC_EXCHANGE_PURSE_MERGE_INVALID_RESERVE_SIGNATURE,
+ NULL);
+ }
+ }
+
+ /* execute transaction */
+ {
+ MHD_RESULT mhd_ret;
+
+ if (GNUNET_OK !=
+ TEH_DB_run_transaction (connection,
+ "execute purse merge",
+ TEH_MT_REQUEST_PURSE_MERGE,
+ &mhd_ret,
+ &merge_transaction,
+ &pcc))
+ {
+ GNUNET_free (pcc.provider_url);
+ return mhd_ret;
+ }
+ }
+
+
+ GNUNET_free (pcc.provider_url);
+ if (! pcc.kyc.ok)
+ return TEH_RESPONSE_reply_kyc_required (connection,
+ &pcc.h_payto,
+ &pcc.kyc);
+
+ {
+ struct TALER_PurseEventP rep = {
+ .header.size = htons (sizeof (rep)),
+ .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_MERGED),
+ .purse_pub = *pcc.purse_pub
+ };
+
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Notifying about purse merge\n");
+ TEH_plugin->event_notify (TEH_plugin->cls,
+ &rep.header,
+ NULL,
+ 0);
+ }
+
+ /* generate regular response */
+ return reply_merge_success (connection,
+ &pcc);
+}
+
+
+/* end of taler-exchange-httpd_purses_merge.c */