/*
This file is part of TALER
(C) 2018 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 General Public License for more details.
You should have received a copy of the GNU General Public License along with
TALER; see the file COPYING. If not, see
*/
/**
* @file backend/taler-merchant-httpd_tip-query.c
* @brief implement API for authorizing tips to be paid to visitors
* @author Christian Grothoff
* @author Florian Dold
*/
#include "platform.h"
#include
#include
#include
#include "taler-merchant-httpd.h"
#include "taler-merchant-httpd_mhd.h"
#include "taler-merchant-httpd_parsing.h"
#include "taler-merchant-httpd_exchanges.h"
#include "taler-merchant-httpd_responses.h"
#include "taler-merchant-httpd_tip-query.h"
/**
* 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 reserve_expiration = GNUNET_TIME_UNIT_ZERO_ABS;
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)
tqc->amount_authorized = tqc->amount_withdrawn;
tqc->amount_deposited = tqc->amount_withdrawn;
/* Update DB based on status! */
for (unsigned int i=0;iidle_reserve_expiration_time);
/* We're interested in the latest DEPOSIT timestamp, since this determines the
* reserve's expiration date. Note that the history isn't chronologically ordered. */
reserve_expiration = GNUNET_TIME_absolute_max (reserve_expiration, deposit_expiration);
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,
deposit_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_INTERNAL_SERVER_ERROR,
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_INTERNAL_SERVER_ERROR,
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_INTERNAL_SERVER_ERROR,
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 (reserve_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
* @param[in,out] connection_cls the connection's closure (can be updated)
* @param upload_data upload data
* @param[in,out] upload_data_size number of bytes (left) in @a upload_data
* @return MHD result code
*/
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 TipQueryContext *tqc;
int res;
struct MerchantInstance *mi;
if (NULL == *connection_cls)
{
tqc = GNUNET_new (struct TipQueryContext);
tqc->hc.cc = &cleanup_tqc;
tqc->connection = connection;
*connection_cls = tqc;
}
else
{
tqc = *connection_cls;
}
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;
}
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");
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)
{
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");
}
tqc->reserve_priv = mi->tip_reserve;
db->preflight (db->cls);
{
int qs;
for (unsigned int i=0;iget_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 set amount_authorized to zero later once
// we know the currency
tqc->none_authorized = GNUNET_YES;
}
}
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 */