diff options
author | Florian Dold <florian.dold@gmail.com> | 2018-02-01 03:38:07 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2018-02-01 03:38:07 +0100 |
commit | 77f74fbef5bd6a50f68e61b1642f1701a926abf3 (patch) | |
tree | d2dff0e3f47628d4d2b28d47e39249e51984dac4 /src/backend/taler-merchant-httpd_tip-query.c | |
parent | d0078be494fed82a092952bfc36055d01bfdd60d (diff) | |
download | merchant-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.c | 499 |
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 */ |