/*
This file is part of TALER
(C) 2014-2017 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-authorize.c
* @brief implement API for authorizing tips to be paid to visitors
* @author Christian Grothoff
*/
#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-authorize.h"
struct TipAuthContext
{
/**
* This field MUST be first.
* FIXME: Explain why!
*/
struct TM_HandlerContext hc;
/**
* Placeholder for #TMH_PARSE_post_json() to keep its internal state.
*/
void *json_parse_context;
/**
* HTTP connection we are handling.
*/
struct MHD_Connection *connection;
/**
* Merchant instance to use.
*/
const char *instance;
/**
* Justification to use.
*/
const char *justification;
/**
* Pickup URL to use.
*/
const char *pickup_url;
/**
* URL to navigate to after tip.
*/
const char *next_url;
/**
* JSON request received.
*/
json_t *root;
/**
* 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;
/**
* Private key used by this merchant for the tipping reserve.
*/
struct TALER_ReservePrivateKeyP reserve_priv;
/**
* Flag set to #GNUNET_YES when we have tried /reserve/status of the
* tipping reserve already.
*/
int checked_status;
/**
* Flag set to #GNUNET_YES when we have parsed the incoming JSON already.
*/
int parsed_json;
/**
* Error code witnessing what the Exchange complained about.
*/
enum TALER_ErrorCode exchange_ec;
};
/**
* Custom cleanup routine for a `struct TipAuthContext`.
*
* @param hc the `struct TMH_JsonParseContext` to clean up.
*/
static void
cleanup_tac (struct TM_HandlerContext *hc)
{
struct TipAuthContext *tac = (struct TipAuthContext *) hc;
if (NULL != tac->root)
{
json_decref (tac->root);
tac->root = NULL;
}
if (NULL != tac->rsh)
{
TALER_EXCHANGE_reserve_status_cancel (tac->rsh);
tac->rsh = NULL;
}
if (NULL != tac->fo)
{
TMH_EXCHANGES_find_exchange_cancel (tac->fo);
tac->fo = NULL;
}
TMH_PARSE_post_cleanup_callback (tac->json_parse_context);
GNUNET_free (tac);
}
/**
* 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 TipAuthContext *tac = cls;
tac->rsh = NULL;
if (MHD_HTTP_OK != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("Failed to obtain tipping reserve status from exchange (%u/%d)\n"),
http_status,
ec);
tac->exchange_ec = ec;
MHD_resume_connection (tac->connection);
TMH_trigger_daemon ();
return;
}
/* Update DB based on status! */
for (unsigned int i=0;iidle_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,
&tac->reserve_priv,
&uuid,
&history[i].amount,
expiration);
if (0 > qs)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("Database error updating tipping reserve status: %d\n"),
qs);
}
}
break;
case TALER_EXCHANGE_RTT_WITHDRAWAL:
/* expected */
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;
}
}
/* Finally, resume processing */
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Resuming HTTP processing\n");
MHD_resume_connection (tac->connection);
TMH_trigger_daemon ();
}
/**
* Function called with the result of a #TMH_EXCHANGES_find_exchange()
* operation.
*
* @param cls closure with a `struct TipAuthContext *'
* @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 TipAuthContext *tac = cls;
struct TALER_ReservePublicKeyP reserve_pub;
const struct TALER_EXCHANGE_Keys *keys;
tac->fo = NULL;
if (NULL == eh)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("Failed to contact exchange configured for tipping!\n"));
MHD_resume_connection (tac->connection);
TMH_trigger_daemon ();
return;
}
keys = TALER_EXCHANGE_get_keys (eh);
GNUNET_assert (NULL != keys);
tac->idle_reserve_expiration_time
= keys->reserve_closing_delay;
GNUNET_CRYPTO_eddsa_key_get_public (&tac->reserve_priv.eddsa_priv,
&reserve_pub.eddsa_pub);
tac->rsh = TALER_EXCHANGE_reserve_status (eh,
&reserve_pub,
&handle_status,
tac);
}
/**
* Handle a "/tip-authorize" 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_authorize (struct TMH_RequestHandler *rh,
struct MHD_Connection *connection,
void **connection_cls,
const char *upload_data,
size_t *upload_data_size)
{
struct TipAuthContext *tac;
int res;
struct MerchantInstance *mi;
enum TALER_ErrorCode ec;
struct GNUNET_TIME_Absolute expiration;
struct GNUNET_HashCode tip_id;
if (NULL == *connection_cls)
{
tac = GNUNET_new (struct TipAuthContext);
tac->hc.cc = &cleanup_tac;
tac->connection = connection;
*connection_cls = tac;
}
else
{
tac = *connection_cls;
}
if (GNUNET_NO == tac->parsed_json)
{
struct GNUNET_JSON_Specification spec[] = {
TALER_JSON_spec_amount ("amount", &tac->amount),
GNUNET_JSON_spec_string ("instance", &tac->instance),
GNUNET_JSON_spec_string ("justification", &tac->justification),
GNUNET_JSON_spec_string ("pickup_url", &tac->pickup_url),
GNUNET_JSON_spec_string ("next_url", &tac->next_url),
GNUNET_JSON_spec_end()
};
res = TMH_PARSE_post_json (connection,
&tac->json_parse_context,
upload_data,
upload_data_size,
&tac->root);
if (GNUNET_SYSERR == res)
return MHD_NO;
/* the POST's body has to be further fetched */
if ( (GNUNET_NO == res) ||
(NULL == tac->root) )
return MHD_YES;
if (NULL == json_object_get (tac->root, "pickup_url"))
{
char *pickup_url = TALER_url_absolute_mhd (connection,
"/public/tip-pickup",
NULL);
GNUNET_assert (NULL != pickup_url);
json_object_set_new (tac->root, "pickup_url", json_string (pickup_url));
GNUNET_free (pickup_url);
}
res = TMH_PARSE_json_data (connection,
tac->root,
spec);
if (GNUNET_YES != res)
{
GNUNET_break_op (0);
return (GNUNET_NO == res) ? MHD_YES : MHD_NO;
}
tac->parsed_json = GNUNET_YES;
}
mi = TMH_lookup_instance (tac->instance);
if (NULL == mi)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Instance `%s' not configured\n",
tac->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",
tac->instance);
return TMH_RESPONSE_reply_not_found (connection,
TALER_EC_TIP_AUTHORIZE_INSTANCE_DOES_NOT_TIP,
"exchange for tipping not configured for the instance");
}
tac->reserve_priv = mi->tip_reserve;
db->preflight (db->cls);
ec = db->authorize_tip (db->cls,
tac->justification,
&tac->amount,
&mi->tip_reserve,
mi->tip_exchange,
&expiration,
&tip_id);
/* If we have insufficient funds according to OUR database,
check with exchange to see if the reserve has been topped up
in the meantime (or if tips were not withdrawn yet). */
if ( (TALER_EC_TIP_AUTHORIZE_INSUFFICIENT_FUNDS == ec) &&
(GNUNET_NO == tac->checked_status) )
{
MHD_suspend_connection (connection);
tac->checked_status = GNUNET_YES;
tac->fo = TMH_EXCHANGES_find_exchange (mi->tip_exchange,
NULL,
&exchange_cont,
tac);
return MHD_YES;
}
/* handle irrecoverable errors */
if (TALER_EC_NONE != (ec | tac->exchange_ec))
{
unsigned int rc;
const char *msg;
switch (ec)
{
case TALER_EC_TIP_AUTHORIZE_INSUFFICIENT_FUNDS:
rc = MHD_HTTP_PRECONDITION_FAILED;
msg = "Failed to approve tip: merchant has insufficient tipping funds";
break;
case TALER_EC_TIP_AUTHORIZE_RESERVE_EXPIRED:
msg = "Failed to approve tip: merchant's tipping reserve expired";
rc = MHD_HTTP_PRECONDITION_FAILED;
break;
case TALER_EC_TIP_AUTHORIZE_RESERVE_UNKNOWN:
msg = "Failed to approve tip: merchant's tipping reserve does not exist";
rc = MHD_HTTP_NOT_FOUND;
break;
default:
rc = MHD_HTTP_INTERNAL_SERVER_ERROR;
msg = "Failed to approve tip: internal server error";
break;
}
/* If the exchange complained earlier, we do
* override what the database returned. */
switch (tac->exchange_ec)
{
case TALER_EC_RESERVE_STATUS_UNKNOWN:
rc = MHD_HTTP_NOT_FOUND;
msg = "Exchange does not find any reserve having this key";
/* We override what the DB returned, as an exchange error
* is more important. */
ec = TALER_EC_TIP_AUTHORIZE_RESERVE_UNKNOWN;
break;
default:
/* This makes the compiler silent. */
break;
}
return TMH_RESPONSE_reply_rc (connection,
rc,
ec,
msg);
}
/* generate success response */
{
json_t *tip_token;
char *tip_token_str;
char *tip_redirect_url;
tip_token = json_pack ("{s:o, s:o, s:o, s:s, s:s, s:s}",
"tip_id", GNUNET_JSON_from_data_auto (&tip_id),
"expiration", GNUNET_JSON_from_time_abs (expiration),
"amount", TALER_JSON_from_amount (&tac->amount),
"exchange_url", mi->tip_exchange,
"next_url", tac->next_url,
"pickup_url", tac->pickup_url);
tip_token_str = json_dumps (tip_token, JSON_COMPACT);
GNUNET_assert (NULL != tip_token_str);
tip_redirect_url = TALER_url_absolute_mhd (connection, "public/trigger-pay",
"tip_token", tip_token_str,
NULL);
GNUNET_assert (NULL != tip_redirect_url);
/* FIXME: This is pretty redundant, but we want to support some older
* merchant implementations. Newer ones should only get the
* tip_redirect_url. */
res = TMH_RESPONSE_reply_json_pack (connection,
MHD_HTTP_OK,
"{s:o, s:o, s:s, s:o, s:s}",
"tip_id", GNUNET_JSON_from_data_auto (&tip_id),
"expiration", GNUNET_JSON_from_time_abs (expiration),
"exchange_url", mi->tip_exchange,
"tip_token", tip_token,
"tip_redirect_url", tip_redirect_url);
GNUNET_free (tip_token_str);
GNUNET_free (tip_redirect_url);
return res;
}
}
/* end of taler-merchant-httpd_tip-authorize.c */