summaryrefslogtreecommitdiff
path: root/src/backend/taler-merchant-httpd_tip-query.c
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2018-02-01 03:38:07 +0100
committerFlorian Dold <florian.dold@gmail.com>2018-02-01 03:38:07 +0100
commit77f74fbef5bd6a50f68e61b1642f1701a926abf3 (patch)
treed2dff0e3f47628d4d2b28d47e39249e51984dac4 /src/backend/taler-merchant-httpd_tip-query.c
parentd0078be494fed82a092952bfc36055d01bfdd60d (diff)
downloadmerchant-77f74fbef5bd6a50f68e61b1642f1701a926abf3.tar.gz
merchant-77f74fbef5bd6a50f68e61b1642f1701a926abf3.tar.bz2
merchant-77f74fbef5bd6a50f68e61b1642f1701a926abf3.zip
implement /tip-query and fix tip db issues (uniqueness)
Diffstat (limited to 'src/backend/taler-merchant-httpd_tip-query.c')
-rw-r--r--src/backend/taler-merchant-httpd_tip-query.c499
1 files changed, 447 insertions, 52 deletions
diff --git a/src/backend/taler-merchant-httpd_tip-query.c b/src/backend/taler-merchant-httpd_tip-query.c
index 6381c8aa..7139b3f1 100644
--- a/src/backend/taler-merchant-httpd_tip-query.c
+++ b/src/backend/taler-merchant-httpd_tip-query.c
@@ -1,9 +1,9 @@
/*
This file is part of TALER
- (C) 2017 Taler Systems SA
+ (C) 2018 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
+ 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
@@ -15,15 +15,14 @@
*/
/**
* @file backend/taler-merchant-httpd_tip-query.c
- * @brief implementation of /tip-query handler
+ * @brief implement API for authorizing tips to be paid to visitors
* @author Christian Grothoff
* @author Florian Dold
*/
#include "platform.h"
-#include <microhttpd.h>
#include <jansson.h>
+#include <taler/taler_util.h>
#include <taler/taler_json_lib.h>
-#include <taler/taler_signatures.h>
#include "taler-merchant-httpd.h"
#include "taler-merchant-httpd_mhd.h"
#include "taler-merchant-httpd_parsing.h"
@@ -33,8 +32,346 @@
/**
- * Manages a /tip-query call, checking if a tip authorization
- * exists and, if so, returning its details.
+ * Maximum number of retries for database operations.
+ */
+#define MAX_RETRIES 5
+
+
+struct TipQueryContext
+{
+ /**
+ * This field MUST be first.
+ * FIXME: Explain why!
+ */
+ struct TM_HandlerContext hc;
+
+ /**
+ * HTTP connection we are handling.
+ */
+ struct MHD_Connection *connection;
+
+ /**
+ * Merchant instance to use.
+ */
+ const char *instance;
+
+ /**
+ * Handle to pending /reserve/status request.
+ */
+ struct TALER_EXCHANGE_ReserveStatusHandle *rsh;
+
+ /**
+ * Handle for operation to obtain exchange handle.
+ */
+ struct TMH_EXCHANGES_FindOperation *fo;
+
+ /**
+ * Reserve expiration time as provided by the exchange.
+ * Set in #exchange_cont.
+ */
+ struct GNUNET_TIME_Relative idle_reserve_expiration_time;
+
+ /**
+ * Tip amount requested.
+ */
+ struct TALER_Amount amount_deposited;
+
+ /**
+ * Tip amount requested.
+ */
+ struct TALER_Amount amount_withdrawn;
+
+ /**
+ * Amount authorized.
+ */
+ struct TALER_Amount amount_authorized;
+
+ /**
+ * Private key used by this merchant for the tipping reserve.
+ */
+ struct TALER_ReservePrivateKeyP reserve_priv;
+
+ /**
+ * No tips were authorized yet.
+ */
+ int none_authorized;
+
+ /**
+ * Response to return, NULL if we don't have one yet.
+ */
+ struct MHD_Response *response;
+
+ /**
+ * HTTP status code to use for the reply, i.e 200 for "OK".
+ * Special value UINT_MAX is used to indicate hard errors
+ * (no reply, return #MHD_NO).
+ */
+ unsigned int response_code;
+
+ /**
+ * #GNUNET_NO if the @e connection was not suspended,
+ * #GNUNET_YES if the @e connection was suspended,
+ * #GNUNET_SYSERR if @e connection was resumed to as
+ * part of #MH_force_pc_resume during shutdown.
+ */
+ int suspended;
+};
+
+
+/**
+ * Custom cleanup routine for a `struct TipQueryContext`.
+ *
+ * @param hc the `struct TMH_JsonParseContext` to clean up.
+ */
+static void
+cleanup_tqc (struct TM_HandlerContext *hc)
+{
+ struct TipQueryContext *tqc = (struct TipQueryContext *) hc;
+
+ if (NULL != tqc->rsh)
+ {
+ TALER_EXCHANGE_reserve_status_cancel (tqc->rsh);
+ tqc->rsh = NULL;
+ }
+ if (NULL != tqc->fo)
+ {
+ TMH_EXCHANGES_find_exchange_cancel (tqc->fo);
+ tqc->fo = NULL;
+ }
+ GNUNET_free (tqc);
+}
+
+
+/**
+ * Resume the given context and send the given response. Stores the response
+ * in the @a pc and signals MHD to resume the connection. Also ensures MHD
+ * runs immediately.
+ *
+ * @param pc payment context
+ * @param response_code response code to use
+ * @param response response data to send back
+ */
+static void
+resume_with_response (struct TipQueryContext *tqc,
+ unsigned int response_code,
+ struct MHD_Response *response)
+{
+ tqc->response_code = response_code;
+ tqc->response = response;
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Resuming /tip-query response (%u)\n",
+ response_code);
+ GNUNET_assert (GNUNET_YES == tqc->suspended);
+ tqc->suspended = GNUNET_NO;
+ MHD_resume_connection (tqc->connection);
+ TMH_trigger_daemon (); /* we resumed, kick MHD */
+}
+
+
+/**
+ * Function called with the result of the /reserve/status request
+ * for the tipping reserve. Update our database balance with the
+ * result.
+ *
+ * @param cls closure with a `struct TipAuthContext *'
+ * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request
+ * 0 if the exchange's reply is bogus (fails to follow the protocol)
+ * @param ec taler-specific error code, #TALER_EC_NONE on success
+ * @param[in] json original response in JSON format (useful only for diagnostics)
+ * @param balance current balance in the reserve, NULL on error
+ * @param history_length number of entries in the transaction history, 0 on error
+ * @param history detailed transaction history, NULL on error
+ */
+static void
+handle_status (void *cls,
+ unsigned int http_status,
+ enum TALER_ErrorCode ec,
+ const json_t *json,
+ const struct TALER_Amount *balance,
+ unsigned int history_length,
+ const struct TALER_EXCHANGE_ReserveHistory *history)
+{
+ struct TipQueryContext *tqc = cls;
+ struct GNUNET_TIME_Absolute expiration;
+
+ tqc->rsh = NULL;
+ if (MHD_HTTP_OK != http_status)
+ {
+ GNUNET_break_op (0);
+ resume_with_response (tqc, MHD_HTTP_SERVICE_UNAVAILABLE,
+ TMH_RESPONSE_make_error (TALER_EC_NONE /* FIXME */,
+ "Unable to obtain reserve status from exchange"));
+ return;
+ }
+
+ if (0 == history_length)
+ {
+ GNUNET_break_op (0);
+ resume_with_response (tqc, MHD_HTTP_SERVICE_UNAVAILABLE,
+ TMH_RESPONSE_make_error (TALER_EC_NONE /* FIXME */,
+ "Exchange returned empty reserve history"));
+ return;
+ }
+
+ if (TALER_EXCHANGE_RTT_DEPOSIT != history[0].type)
+ {
+ GNUNET_break_op (0);
+ resume_with_response (tqc, MHD_HTTP_SERVICE_UNAVAILABLE,
+ TMH_RESPONSE_make_error (TALER_EC_NONE /* FIXME */,
+ "Exchange returned invalid reserve history"));
+ return;
+ }
+
+ if (GNUNET_OK != TALER_amount_get_zero (history[0].amount.currency, &tqc->amount_withdrawn))
+ {
+ GNUNET_break_op (0);
+ resume_with_response (tqc, MHD_HTTP_SERVICE_UNAVAILABLE,
+ TMH_RESPONSE_make_error (TALER_EC_NONE /* FIXME */,
+ "Exchange returned invalid reserve history"));
+ return;
+ }
+
+ if (GNUNET_YES == tqc->none_authorized)
+ memcpy (&tqc->amount_authorized, &tqc->amount_withdrawn, sizeof (struct TALER_Amount));
+ memcpy (&tqc->amount_deposited, &tqc->amount_withdrawn, sizeof (struct TALER_Amount));
+
+ /* Update DB based on status! */
+ for (unsigned int i=0;i<history_length;i++)
+ {
+ switch (history[i].type)
+ {
+ case TALER_EXCHANGE_RTT_DEPOSIT:
+ {
+ enum GNUNET_DB_QueryStatus qs;
+ struct GNUNET_HashCode uuid;
+
+ expiration = GNUNET_TIME_absolute_add (history[i].details.in_details.timestamp,
+ tqc->idle_reserve_expiration_time);
+ GNUNET_CRYPTO_hash (history[i].details.in_details.wire_reference,
+ history[i].details.in_details.wire_reference_size,
+ &uuid);
+ qs = db->enable_tip_reserve (db->cls,
+ &tqc->reserve_priv,
+ &uuid,
+ &history[i].amount,
+ expiration);
+ if (GNUNET_OK != TALER_amount_add (&tqc->amount_deposited,
+ &tqc->amount_deposited,
+ &history[i].amount))
+ {
+ GNUNET_break_op (0);
+ resume_with_response (tqc, MHD_HTTP_SERVICE_UNAVAILABLE,
+ TMH_RESPONSE_make_error (TALER_EC_NONE /* FIXME */,
+ "Exchange returned invalid reserve history (amount overflow)"));
+ return;
+ }
+
+ if (0 > qs)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ _("Database error updating tipping reserve status: %d\n"),
+ qs);
+ }
+ }
+ break;
+ case TALER_EXCHANGE_RTT_WITHDRAWAL:
+ if (GNUNET_OK != TALER_amount_add (&tqc->amount_withdrawn,
+ &tqc->amount_withdrawn,
+ &history[i].amount))
+ {
+ GNUNET_break_op (0);
+ resume_with_response (tqc, MHD_HTTP_SERVICE_UNAVAILABLE,
+ TMH_RESPONSE_make_error (TALER_EC_NONE /* FIXME */,
+ "Exchange returned invalid reserve history (amount overflow)"));
+ return;
+ }
+ break;
+ case TALER_EXCHANGE_RTT_PAYBACK:
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ _("Encountered unsupported /payback operation on tipping reserve\n"));
+ break;
+ case TALER_EXCHANGE_RTT_CLOSE:
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ _("Exchange closed reserve (due to expiration), balance calulation is likely wrong. Please create a fresh reserve.\n"));
+ break;
+ }
+ }
+
+ {
+ struct GNUNET_CRYPTO_EddsaPublicKey reserve_pub;
+ struct TALER_Amount amount_available;
+ GNUNET_CRYPTO_eddsa_key_get_public (&tqc->reserve_priv.eddsa_priv,
+ &reserve_pub);
+ if (GNUNET_SYSERR == TALER_amount_subtract (&amount_available, &tqc->amount_deposited, &tqc->amount_withdrawn))
+ {
+ GNUNET_break_op (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "amount overflow, deposited %s but withdrawn %s\n",
+ TALER_amount_to_string (&tqc->amount_deposited),
+ TALER_amount_to_string (&tqc->amount_withdrawn));
+
+ resume_with_response (tqc, MHD_HTTP_SERVICE_UNAVAILABLE,
+ TMH_RESPONSE_make_error (TALER_EC_NONE /* FIXME */,
+ "Exchange returned invalid reserve history (amount overflow)"));
+ }
+ resume_with_response (tqc, MHD_HTTP_OK,
+ TMH_RESPONSE_make_json_pack ("{s:o, s:o, s:o, s:o, s:o}",
+ "reserve_pub",
+ GNUNET_JSON_from_data_auto (&reserve_pub),
+ "reserve_expiration",
+ GNUNET_JSON_from_time_abs (expiration),
+ "amount_authorized",
+ TALER_JSON_from_amount (&tqc->amount_authorized),
+ "amount_picked_up",
+ TALER_JSON_from_amount (&tqc->amount_withdrawn),
+ "amount_available",
+ TALER_JSON_from_amount (&amount_available)));
+ }
+}
+
+
+/**
+ * Function called with the result of a #TMH_EXCHANGES_find_exchange()
+ * operation.
+ *
+ * @param cls closure with a `struct TipQueryContext *'
+ * @param eh handle to the exchange context
+ * @param wire_fee current applicable wire fee for dealing with @a eh, NULL if not available
+ * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config
+ */
+static void
+exchange_cont (void *cls,
+ struct TALER_EXCHANGE_Handle *eh,
+ const struct TALER_Amount *wire_fee,
+ int exchange_trusted)
+{
+ struct TipQueryContext *tqc = cls;
+ struct TALER_ReservePublicKeyP reserve_pub;
+ const struct TALER_EXCHANGE_Keys *keys;
+
+ tqc->fo = NULL;
+ if (NULL == eh)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ _("Failed to contact exchange configured for tipping!\n"));
+ MHD_resume_connection (tqc->connection);
+ TMH_trigger_daemon ();
+ return;
+ }
+ keys = TALER_EXCHANGE_get_keys (eh);
+ GNUNET_assert (NULL != keys);
+ tqc->idle_reserve_expiration_time
+ = keys->reserve_closing_delay;
+ GNUNET_CRYPTO_eddsa_key_get_public (&tqc->reserve_priv.eddsa_priv,
+ &reserve_pub.eddsa_pub);
+ tqc->rsh = TALER_EXCHANGE_reserve_status (eh,
+ &reserve_pub,
+ &handle_status,
+ tqc);
+}
+
+
+/**
+ * Handle a "/tip-query" request.
*
* @param rh context of the handler
* @param connection the MHD connection to handle
@@ -45,58 +382,116 @@
*/
int
MH_handler_tip_query (struct TMH_RequestHandler *rh,
- struct MHD_Connection *connection,
- void **connection_cls,
- const char *upload_data,
- size_t *upload_data_size)
+ struct MHD_Connection *connection,
+ void **connection_cls,
+ const char *upload_data,
+ size_t *upload_data_size)
{
- const char *tip_id_str;
- struct GNUNET_HashCode tip_id;
-
- tip_id_str = MHD_lookup_connection_value (connection,
- MHD_GET_ARGUMENT_KIND,
- "tip_id");
- if (NULL == tip_id_str)
- return TMH_RESPONSE_reply_arg_missing (connection,
- TALER_EC_PARAMETER_MISSING,
- "tip_id");
+ struct TipQueryContext *tqc;
+ int res;
+ struct MerchantInstance *mi;
- if (GNUNET_OK != GNUNET_STRINGS_string_to_data (tip_id_str,
- strlen (tip_id_str), &tip_id,
- sizeof (struct GNUNET_HashCode)))
- return TMH_RESPONSE_reply_arg_invalid (connection,
- TALER_EC_PARAMETER_MISSING,
- "tip_id");
+ if (NULL == *connection_cls)
+ {
+ tqc = GNUNET_new (struct TipQueryContext);
+ tqc->hc.cc = &cleanup_tqc;
+ tqc->connection = connection;
+ *connection_cls = tqc;
+ }
+ else
+ {
+ tqc = *connection_cls;
+ }
- enum GNUNET_DB_QueryStatus qs;
- struct TALER_Amount tip_amount;
- struct GNUNET_TIME_Absolute tip_timestamp;
- char *tip_exchange_url;
+ if (0 != tqc->response_code)
+ {
+ /* We are *done* processing the request, just queue the response (!) */
+ if (UINT_MAX == tqc->response_code)
+ {
+ GNUNET_break (0);
+ return MHD_NO; /* hard error */
+ }
+ res = MHD_queue_response (connection,
+ tqc->response_code,
+ tqc->response);
+ MHD_destroy_response (tqc->response);
+ tqc->response = NULL;
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Queueing response (%u) for /tip-query (%s).\n",
+ (unsigned int) tqc->response_code,
+ res ? "OK" : "FAILED");
+ return res;
+ }
- qs = db->lookup_tip_by_id (db->cls,
- &tip_id,
- &tip_exchange_url,
- &tip_amount,
- &tip_timestamp);
+ tqc->instance = MHD_lookup_connection_value (connection,
+ MHD_GET_ARGUMENT_KIND,
+ "instance");
+ if (NULL == tqc->instance)
+ return TMH_RESPONSE_reply_arg_missing (connection,
+ TALER_EC_PARAMETER_MISSING,
+ "instance");
- if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ mi = TMH_lookup_instance (tqc->instance);
+ if (NULL == mi)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Instance `%s' not configured\n",
+ tqc->instance);
+ return TMH_RESPONSE_reply_not_found (connection,
+ TALER_EC_TIP_AUTHORIZE_INSTANCE_UNKNOWN,
+ "unknown instance");
+ }
+ if (NULL == mi->tip_exchange)
{
- return TMH_RESPONSE_reply_rc (connection,
- MHD_HTTP_NOT_FOUND,
- TALER_EC_TIP_QUERY_TIP_ID_UNKNOWN,
- "tip id not found");
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Instance `%s' not configured for tipping\n",
+ tqc->instance);
+ return TMH_RESPONSE_reply_not_found (connection,
+ TALER_EC_TIP_AUTHORIZE_INSTANCE_DOES_NOT_TIP,
+ "exchange for tipping not configured for the instance");
}
- else if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
+ tqc->reserve_priv = mi->tip_reserve;
+
{
- return TMH_RESPONSE_reply_json_pack (connection,
- MHD_HTTP_OK,
- "{s:s, s:s}",
- "exchange", tip_exchange_url,
- "timestamp", GNUNET_JSON_from_time_abs (tip_timestamp),
- "amount", TALER_JSON_from_amount (&tip_amount));
+ int qs;
+ for (unsigned int i=0;i<MAX_RETRIES;i++)
+ {
+ qs = db->get_authorized_tip_amount (db->cls,
+ &tqc->reserve_priv,
+ &tqc->amount_authorized);
+ if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
+ break;
+ }
+ if (0 > qs)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Database hard error on get_authorized_tip_amount\n");
+ return TMH_RESPONSE_reply_internal_error (connection,
+ TALER_EC_NONE /* FIXME */,
+ "Merchant database error");
+ }
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ {
+ // We'll amount_authorized to zero later once
+ // we know the currency
+ tqc->none_authorized = GNUNET_YES;
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "DB::::: authorized amount: NONE\n");
+ }
+ else
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "DB::::: authorized amount: %s\n", TALER_amount_to_string (&tqc->amount_authorized));
+ }
}
- return TMH_RESPONSE_reply_rc (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_INTERNAL_INVARIANT_FAILURE,
- "tip lookup failure");
+
+
+ MHD_suspend_connection (connection);
+ tqc->suspended = GNUNET_YES;
+
+ tqc->fo = TMH_EXCHANGES_find_exchange (mi->tip_exchange,
+ NULL,
+ &exchange_cont,
+ tqc);
+ return MHD_YES;
}
+
+/* end of taler-merchant-httpd_tip-query.c */