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 | |
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')
-rw-r--r-- | src/backend/taler-merchant-httpd_tip-authorize.c | 1 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_tip-query.c | 499 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_tip-query.h | 4 | ||||
-rw-r--r-- | src/backenddb/plugin_merchantdb_postgres.c | 175 | ||||
-rw-r--r-- | src/include/taler_merchant_service.h | 68 | ||||
-rw-r--r-- | src/include/taler_merchantdb_plugin.h | 16 | ||||
-rw-r--r-- | src/lib/Makefile.am | 3 | ||||
-rw-r--r-- | src/lib/merchant_api_tip_query.c | 244 | ||||
-rw-r--r-- | src/lib/test_merchant_api.c | 169 |
9 files changed, 1112 insertions, 67 deletions
diff --git a/src/backend/taler-merchant-httpd_tip-authorize.c b/src/backend/taler-merchant-httpd_tip-authorize.c index ea2c7064..71938619 100644 --- a/src/backend/taler-merchant-httpd_tip-authorize.c +++ b/src/backend/taler-merchant-httpd_tip-authorize.c @@ -207,6 +207,7 @@ handle_status (void *cls, qs); } } + break; case TALER_EXCHANGE_RTT_WITHDRAWAL: /* expected */ break; 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 */ diff --git a/src/backend/taler-merchant-httpd_tip-query.h b/src/backend/taler-merchant-httpd_tip-query.h index ec8358d3..f3a9ebff 100644 --- a/src/backend/taler-merchant-httpd_tip-query.h +++ b/src/backend/taler-merchant-httpd_tip-query.h @@ -16,7 +16,6 @@ /** * @file backend/taler-merchant-httpd_tip-query.h * @brief headers for /tip-query handler - * @author Christian Grothoff * @author Florian Dold */ #ifndef TALER_MERCHANT_HTTPD_TIP_QUERY_H @@ -25,8 +24,7 @@ #include "taler-merchant-httpd.h" /** - * Manages a /tip-query call, checking if a tip authorization - * exists and, if so, returning its details. + * Manages a /tip-query call. * * @param rh context of the handler * @param connection the MHD connection to handle diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c index 0a3ce5f5..0ee4aa05 100644 --- a/src/backenddb/plugin_merchantdb_postgres.c +++ b/src/backenddb/plugin_merchantdb_postgres.c @@ -214,7 +214,7 @@ postgres_initialize (void *cls) /* table where we remember when tipping reserves where established / enabled */ GNUNET_PQ_make_execute ("CREATE TABLE IF NOT EXISTS merchant_tip_reserve_credits (" " reserve_priv BYTEA NOT NULL CHECK (LENGTH(reserve_priv)=32)" - ",credit_uuid BYTEA NOT NULL CHECK (LENGTH(credit_uuid)=64)" + ",credit_uuid BYTEA UNIQUE NOT NULL CHECK (LENGTH(credit_uuid)=64)" ",timestamp INT8 NOT NULL" ",amount_val INT8 NOT NULL" ",amount_frac INT4 NOT NULL" @@ -585,6 +585,16 @@ postgres_initialize (void *cls) " FROM merchant_tip_reserves" " WHERE reserve_priv=$1", 1), + GNUNET_PQ_make_prepare ("find_tip_authorizations", + "SELECT" + " amount_val" + ",amount_frac" + ",amount_curr" + ",justification" + ",tip_id" + " FROM merchant_tips" + " WHERE reserve_priv=$1", + 1), GNUNET_PQ_make_prepare ("update_tip_reserve_balance", "UPDATE merchant_tip_reserves SET" " expiration=$2" @@ -675,6 +685,11 @@ postgres_initialize (void *cls) " VALUES " "($1, $2, $3, $4, $5, $6)", 6), + GNUNET_PQ_make_prepare ("lookup_tip_credit_uuid", + "SELECT 1 " + "FROM merchant_tip_reserve_credits " + "WHERE credit_uuid=$1 AND reserve_priv=$2", + 2), GNUNET_PQ_PREPARED_STATEMENT_END }; @@ -1425,6 +1440,128 @@ postgres_find_contract_terms_by_date_and_range (void *cls, /** + * Closure for #find_tip_authorizations_cb(). + */ +struct GetAuthorizedTipAmountContext +{ + /** + * Total authorized amount. + */ + struct TALER_Amount authorized_amount; + + /** + * Transaction status code to set. + */ + enum GNUNET_DB_QueryStatus qs; + +}; + + + + +/** + * Function to be called with the results of a SELECT statement + * that has returned @a num_results results. + * + * @param cls of type `struct GetAuthorizedTipAmountContext *` + * @param result the postgres result + * @param num_result the number of results in @a result + */ +static void +find_tip_authorizations_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct GetAuthorizedTipAmountContext *ctx = cls; + unsigned int i; + + for (i = 0; i < num_results; i++) + { + struct TALER_Amount amount; + char *just; + struct GNUNET_HashCode h; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_string ("justification", &just), + GNUNET_PQ_result_spec_auto_from_type ("tip_id", &h), + TALER_PQ_result_spec_amount ("amount", + &amount), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + ctx->qs = GNUNET_DB_STATUS_HARD_ERROR; + return; + } + + if (0 == i) + { + memcpy (&ctx->authorized_amount, &amount, sizeof (struct TALER_Amount)); + } + else if (GNUNET_OK != + TALER_amount_add (&ctx->authorized_amount, + &ctx->authorized_amount, &amount)) + { + GNUNET_break (0); + ctx->qs = GNUNET_DB_STATUS_HARD_ERROR; + return; + } + } + + if (0 == i) + { + ctx->qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + else + { + /* one aggregated result */ + ctx->qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + } +} + + +/** + * Get the total amount of authorized tips for a tipping reserve. + * + * @param cls closure, typically a connection to the db + * @param reserve_priv which reserve to check + * @param[out] authorzed_amount amount we've authorized so far for tips + * @return transaction status, usually + * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT for success + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the reserve_priv + * does not identify a known tipping reserve + */ +enum GNUNET_DB_QueryStatus +postgres_get_authorized_tip_amount (void *cls, + const struct TALER_ReservePrivateKeyP *reserve_priv, + struct TALER_Amount *authorized_amount) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (reserve_priv), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + struct GetAuthorizedTipAmountContext ctx = { 0 }; + + check_connection (pg); + qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, + "find_tip_authorizations", + params, + &find_tip_authorizations_cb, + &ctx); + if (0 >= qs) + return qs; + memcpy (authorized_amount, &ctx.authorized_amount, sizeof (struct TALER_Amount)); + return ctx.qs; +} + + +/** * Return proposals whose timestamp are older than `date`. * The rows are sorted having the youngest first. * @@ -2830,6 +2967,35 @@ postgres_enable_tip_reserve (void *cls, /* ensure that credit_uuid is new/unique */ { + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (credit_uuid), + GNUNET_PQ_query_param_auto_from_type (reserve_priv), + GNUNET_PQ_query_param_end + }; + + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_end + }; + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "lookup_tip_credit_uuid", + params, + rs); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + postgres_rollback (pg); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + goto RETRY; + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs) + { + /* UUID already exists, we are done! */ + return qs; + } + } + + { struct GNUNET_TIME_Absolute now; struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (reserve_priv), @@ -2852,12 +3018,6 @@ postgres_enable_tip_reserve (void *cls, goto RETRY; return qs; } - /* UUID already exists, we are done! */ - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - postgres_rollback (pg); - return qs; - } } /* Obtain existing reserve balance */ @@ -3405,6 +3565,7 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) plugin->find_contract_terms = &postgres_find_contract_terms; plugin->find_contract_terms_history = &postgres_find_contract_terms_history; plugin->find_contract_terms_by_date = &postgres_find_contract_terms_by_date; + plugin->get_authorized_tip_amount = &postgres_get_authorized_tip_amount; plugin->find_contract_terms_by_date_and_range = &postgres_find_contract_terms_by_date_and_range; plugin->find_contract_terms_from_hash = &postgres_find_contract_terms_from_hash; plugin->find_paid_contract_terms_from_hash = &postgres_find_paid_contract_terms_from_hash; diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h index 3462288e..2dd0b202 100644 --- a/src/include/taler_merchant_service.h +++ b/src/include/taler_merchant_service.h @@ -922,7 +922,7 @@ TALER_MERCHANT_tip_pickup_cancel (struct TALER_MERCHANT_TipPickupOperation *tp); /** - * Handle for a /tip-pickup operation. + * Handle for a /check-payment operation. */ struct TALER_MERCHANT_CheckPaymentOperation; @@ -980,14 +980,76 @@ TALER_MERCHANT_check_payment (struct GNUNET_CURL_Context *ctx, TALER_MERCHANT_CheckPaymentCallback check_payment_cb, void *check_payment_cls); - /** * Cancel a GET /check-payment request. * + * @param cpo handle to the request to be canceled + */ +void +TALER_MERCHANT_check_payment_cancel (struct TALER_MERCHANT_CheckPaymentOperation *cpo); + + +/* ********************** /tip-query ************************* */ + +/** + * Handle for a /tip-query operation. + */ +struct TALER_MERCHANT_TipQueryOperation; + + +/** + * Callback to process a GET /tip-query request + * + * @param cls closure + * @param http_status HTTP status code for this request + * @param ec Taler-specific error code + * @param raw raw response body + */ +typedef void +(*TALER_MERCHANT_TipQueryCallback) (void *cls, + unsigned int http_status, + enum TALER_ErrorCode ec, + const json_t *raw, + struct GNUNET_TIME_Absolute reserve_expiration, + struct TALER_ReservePublicKeyP *reserve_pub, + struct TALER_Amount *amount_authorized, + struct TALER_Amount *amount_available, + struct TALER_Amount *amount_picked_up); + + +/** + * Cancel a GET /tip-query request. + * * @param cph handle to the request to be canceled */ void -TALER_MERCHANT_check_payment_cancel (struct TALER_MERCHANT_CheckPaymentOperation *cph); +TALER_MERCHANT_tip_query_cancel (struct TALER_MERCHANT_TipQueryOperation *tqo); + + +/** + * Issue a /tip-query request to the backend. Informs the backend + * that a customer wants to pick up a tip. + * + * @param ctx execution context + * @param backend_url base URL of the merchant backend + * @param instance instance to query + * @return handle for this operation, NULL upon errors + */ +struct TALER_MERCHANT_TipQueryOperation * +TALER_MERCHANT_tip_query (struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *instance, + TALER_MERCHANT_TipQueryCallback query_cb, + void *query_cb_cls); + + +/** + * Cancel a GET /tip-query request. + * + * @param tqo handle to the request to be canceled + */ +void +TALER_MERCHANT_tip_query_cancel (struct TALER_MERCHANT_TipQueryOperation *tqh); #endif /* _TALER_MERCHANT_SERVICE_H */ diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h index 0bb1e335..1005e6c5 100644 --- a/src/include/taler_merchantdb_plugin.h +++ b/src/include/taler_merchantdb_plugin.h @@ -738,6 +738,22 @@ struct TALER_MERCHANTDB_Plugin struct GNUNET_TIME_Absolute *expiration, struct GNUNET_HashCode *tip_id); + /** + * Get the total amount of authorized tips for a tipping reserve. + * + * @param cls closure, typically a connection to the db + * @param reserve_priv which reserve to check + * @param[out] authorzed_amount amount we've authorized so far for tips + * @return transaction status, usually + * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT for success + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the reserve_priv + * does not identify a known tipping reserve + */ + enum GNUNET_DB_QueryStatus + (*get_authorized_tip_amount)(void *cls, + const struct TALER_ReservePrivateKeyP *reserve_priv, + struct TALER_Amount *authorized_amount); + /** * Find out tip authorization details associated with @a tip_id diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am index 79f26749..d953081f 100644 --- a/src/lib/Makefile.am +++ b/src/lib/Makefile.am @@ -22,7 +22,8 @@ libtalermerchant_la_SOURCES = \ merchant_api_track_transfer.c \ merchant_api_history.c \ merchant_api_refund.c \ - merchant_api_check_payment.c + merchant_api_check_payment.c \ + merchant_api_tip_query.c libtalermerchant_la_LIBADD = \ -ltalerexchange \ diff --git a/src/lib/merchant_api_tip_query.c b/src/lib/merchant_api_tip_query.c new file mode 100644 index 00000000..b0b2c194 --- /dev/null +++ b/src/lib/merchant_api_tip_query.c @@ -0,0 +1,244 @@ +/* + This file is part of TALER + Copyright (C) 2014-2018 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 2.1, 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with + TALER; see the file COPYING.LGPL. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/merchant_api_tip_query.c + * @brief Implementation of the /tip-query request of the merchant's HTTP API + * @author Florian Dold + */ +#include "platform.h" +#include <curl/curl.h> +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include "taler_merchant_service.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_signatures.h> + + +/** + * @brief A handle for tracking /tip-pickup operations + */ +struct TALER_MERCHANT_TipQueryOperation +{ + /** + * The url for this request. + */ + char *url; + + /** + * JSON encoding of the request to POST. + */ + char *json_enc; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_MERCHANT_TipQueryCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Expected number of planchets. + */ + unsigned int num_planchets; +}; + + +/** + * We got a 200 response back from the exchange (or the merchant). + * Now we need to parse the response and if it is well-formed, + * call the callback (and set it to NULL afterwards). + * + * @param tqo handle of the original operation + * @param json cryptographic proof returned by the exchange/merchant + * @return #GNUNET_OK if response is valid + */ +static int +check_ok (struct TALER_MERCHANT_TipQueryOperation *tqo, + const json_t *json) +{ + struct GNUNET_TIME_Absolute reserve_expiration; + struct TALER_Amount amount_authorized; + struct TALER_Amount amount_available; + struct TALER_Amount amount_picked_up; + struct TALER_ReservePublicKeyP reserve_pub; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("reserve_pub", &reserve_pub), + GNUNET_JSON_spec_absolute_time ("reserve_expiration", &reserve_expiration), + TALER_JSON_spec_amount ("amount_authorized", &amount_authorized), + TALER_JSON_spec_amount ("amount_available", &amount_available), + TALER_JSON_spec_amount ("amount_picked_up", &amount_picked_up), + GNUNET_JSON_spec_end() + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + tqo->cb (tqo->cb_cls, + MHD_HTTP_OK, + TALER_JSON_get_error_code (json), + json, + reserve_expiration, + &reserve_pub, + &amount_authorized, + &amount_available, + &amount_picked_up); + return GNUNET_OK; +} + + +/** + * Function called when we're done processing the + * HTTP /track/transaction request. + * + * @param cls the `struct TALER_MERCHANT_TipQueryOperation` + * @param response_code HTTP response code, 0 on error + * @param json response body, NULL if not in JSON + */ +static void +handle_tip_query_finished (void *cls, + long response_code, + const json_t *json) +{ + struct TALER_MERCHANT_TipQueryOperation *tqo = cls; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Got /tip-query response with status code %u\n", + (unsigned int) response_code); + + tqo->job = NULL; + switch (response_code) + { + case MHD_HTTP_OK: + if (GNUNET_OK != check_ok (tqo, + json)) + { + GNUNET_break_op (0); + response_code = 0; + } + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + case MHD_HTTP_NOT_FOUND: + /* legal, can happen if instance or tip reserve is unknown */ + break; + default: + /* unexpected response code */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u\n", + (unsigned int) response_code); + GNUNET_break (0); + response_code = 0; + break; + } + if (MHD_HTTP_OK != response_code) + tqo->cb (tqo->cb_cls, + response_code, + TALER_JSON_get_error_code (json), + json, + GNUNET_TIME_UNIT_ZERO_ABS, + NULL, + NULL, + NULL, + NULL); + TALER_MERCHANT_tip_query_cancel (tqo); +} + + +/** + * Issue a /tip-query request to the backend. Informs the backend + * that a customer wants to pick up a tip. + * + * @param ctx execution context + * @param backend_url base URL of the merchant backend + * @param instance instance to query + * @return handle for this operation, NULL upon errors + */ +struct TALER_MERCHANT_TipQueryOperation * +TALER_MERCHANT_tip_query (struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *instance, + TALER_MERCHANT_TipQueryCallback query_cb, + void *query_cb_cls) +{ + struct TALER_MERCHANT_TipQueryOperation *tqo; + CURL *eh; + + tqo = GNUNET_new (struct TALER_MERCHANT_TipQueryOperation); + tqo->ctx = ctx; + tqo->cb = query_cb; + tqo->cb_cls = query_cb_cls; + tqo->url = TALER_url_join (backend_url, "/tip-query", + "instance", instance, + NULL); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Requesting URL '%s'\n", + tqo->url); + eh = curl_easy_init (); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_URL, + tqo->url)); + tqo->job = GNUNET_CURL_job_add (ctx, + eh, + GNUNET_YES, + &handle_tip_query_finished, + tqo); + return tqo; +} + + +/** + * Cancel a /tip-query request. This function cannot be used + * on a request handle if a response is already served for it. + * + * @param tqo handle to the operation being cancelled + */ +void +TALER_MERCHANT_tip_query_cancel (struct TALER_MERCHANT_TipQueryOperation *tqo) +{ + if (NULL != tqo->job) + { + GNUNET_CURL_job_cancel (tqo->job); + tqo->job = NULL; + } + GNUNET_free (tqo->url); + GNUNET_free (tqo); +} + +/* end of merchant_api_tip_query.c */ diff --git a/src/lib/test_merchant_api.c b/src/lib/test_merchant_api.c index dc81b9a7..5a92e9cc 100644 --- a/src/lib/test_merchant_api.c +++ b/src/lib/test_merchant_api.c @@ -248,6 +248,11 @@ enum OpCode */ OC_CHECK_PAYMENT, + /** + * Query tip stats. + */ + OC_TIP_QUERY, + }; @@ -962,6 +967,37 @@ struct Command } tip_pickup; struct { + /** + * Expected available amount (in string format). + * NULL to skip check. + */ + char *expected_amount_available; + + /** + * Expected picked up amount (in string format). + * NULL to skip check. + */ + char *expected_amount_picked_up; + + /** + * Expected authorized amount (in string format). + * NULL to skip check. + */ + char *expected_amount_authorized; + + /** + * Handle for the ongoing operation. + */ + struct TALER_MERCHANT_TipQueryOperation *tqo; + + /** + * Merchant instance to use for tipping. + */ + char *instance; + + } tip_query; + + struct { /** * Reference for the contract we want to check. @@ -2114,6 +2150,77 @@ check_payment_cb (void *cls, /** + * Callback to process a GET /tip-query request + * + * @param cls closure + * @param http_status HTTP status code for this request + * @param ec Taler-specific error code + * @param raw raw response body + */ +static void +tip_query_cb (void *cls, + unsigned int http_status, + enum TALER_ErrorCode ec, + const json_t *raw, + struct GNUNET_TIME_Absolute reserve_expiration, + struct TALER_ReservePublicKeyP *reserve_pub, + struct TALER_Amount *amount_authorized, + struct TALER_Amount *amount_available, + struct TALER_Amount *amount_picked_up) +{ + struct InterpreterState *is = cls; + struct Command *cmd = &is->commands[is->ip]; + struct TALER_Amount a; + + cmd->details.tip_query.tqo = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Tip query callback at command %u/%s(%u)\n", + is->ip, + cmd->label, + cmd->oc); + + GNUNET_assert (NULL != reserve_pub); + GNUNET_assert (NULL != amount_authorized); + GNUNET_assert (NULL != amount_available); + GNUNET_assert (NULL != amount_picked_up); + + if (cmd->details.tip_query.expected_amount_available) + { + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (cmd->details.tip_query.expected_amount_available, &a)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, "expected available %s, actual %s\n", + TALER_amount_to_string (&a), + TALER_amount_to_string (amount_available)); + GNUNET_assert (0 == TALER_amount_cmp (amount_available, &a)); + } + + if (cmd->details.tip_query.expected_amount_authorized) + { + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (cmd->details.tip_query.expected_amount_authorized, &a)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, "expected authorized %s, actual %s\n", + TALER_amount_to_string (&a), + TALER_amount_to_string (amount_authorized)); + GNUNET_assert (0 == TALER_amount_cmp (amount_authorized, &a)); + } + + if (cmd->details.tip_query.expected_amount_picked_up) + { + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (cmd->details.tip_query.expected_amount_picked_up, &a)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, "expected picked_up %s, actual %s\n", + TALER_amount_to_string (&a), + TALER_amount_to_string (amount_picked_up)); + GNUNET_assert (0 == TALER_amount_cmp (amount_picked_up, &a)); + } + + if (cmd->expected_response_code != http_status) + fail (is); + next_command (is); +} + + +/** * Function called with detailed wire transfer data. * * @param cls closure @@ -2684,6 +2791,17 @@ cleanup_state (struct InterpreterState *is) cmd->details.check_payment.cpo = NULL; } break; + case OC_TIP_QUERY: + if (NULL != cmd->details.tip_query.tqo) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command %u (%s) did not complete\n", + i, + cmd->label); + TALER_MERCHANT_tip_query_cancel (cmd->details.tip_query.tqo); + cmd->details.tip_query.tqo = NULL; + } + break; default: GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Shutdown: unknown instruction %d at %u (%s)\n", @@ -2902,7 +3020,7 @@ interpreter_run (void *cls) fail (is); return; } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Interpreter runs command %u/%s(%u)\n", is->ip, cmd->label, @@ -2985,6 +3103,29 @@ interpreter_run (void *cls) } } break; + case OC_TIP_QUERY: + { + if (instance_idx != 0) + { + // We check /tip-query only for the first instance, + // since for tipping we use a dedicated instance. + // On repeated runs, the expected authorized amounts wouldn't + // match up (they would all accumulate!) + next_command (is); + break; + } + if (NULL == (cmd->details.tip_query.tqo + = TALER_MERCHANT_tip_query (ctx, + MERCHANT_URL, + cmd->details.tip_query.instance, + tip_query_cb, + is))) + { + GNUNET_break (0); + fail (is); + } + } + break; case OC_ADMIN_ADD_INCOMING: { char *subject; @@ -4414,17 +4555,43 @@ run (void *cls) .details.tip_authorize.instance = "tip", .details.tip_authorize.justification = "tip 2", .details.tip_authorize.amount = "EUR:5.01" }, + /* Check tip status */ + { .oc = OC_TIP_QUERY, + .label = "query-tip-1", + .expected_response_code = MHD_HTTP_OK, + .details.tip_query.instance = "tip" }, + { .oc = OC_TIP_QUERY, + .label = "query-tip-2", + .expected_response_code = MHD_HTTP_OK, + .details.tip_query.instance = "tip", + .details.tip_query.expected_amount_authorized = "EUR:10.02", + .details.tip_query.expected_amount_picked_up = "EUR:0.0", + .details.tip_query.expected_amount_available = "EUR:20.04" }, /* Withdraw tip */ { .oc = OC_TIP_PICKUP, .label = "pickup-tip-1", .expected_response_code = MHD_HTTP_OK, .details.tip_pickup.authorize_ref = "authorize-tip-1", .details.tip_pickup.amounts = pickup_amounts_1 }, + /* Check tip status again */ + { .oc = OC_TIP_QUERY, + .label = "query-tip-3", + .expected_response_code = MHD_HTTP_OK, + .details.tip_query.instance = "tip", + .details.tip_query.expected_amount_picked_up = "EUR:5.01", + .details.tip_query.expected_amount_available = "EUR:15.03" }, { .oc = OC_TIP_PICKUP, .label = "pickup-tip-2", .expected_response_code = MHD_HTTP_OK, .details.tip_pickup.authorize_ref = "authorize-tip-2", .details.tip_pickup.amounts = pickup_amounts_1 }, + { .oc = OC_TIP_QUERY, + .label = "query-tip-4", + .expected_response_code = MHD_HTTP_OK, + .details.tip_query.instance = "tip", + .details.tip_query.expected_amount_picked_up = "EUR:10.02", + .details.tip_query.expected_amount_available = "EUR:10.02", + .details.tip_query.expected_amount_authorized = "EUR:10.02" }, { .oc = OC_TIP_PICKUP, .label = "pickup-tip-2b", .expected_response_code = MHD_HTTP_OK, |