e7aeec04f4
The current "/recoup" API does not have clear idempotency semantics, as we've discussed on the phone. This is already bad by itself, as it makes it hard to write down what the API does other than "whatever the implementation does". However, it actually breaks correctness in this (admittedly kinda contrived, but not impossible) case: Say that we have a coin A obtained via withdrawal and a coin B obtained via refreshing coin A. Now the denominations of A gets revoked.. The wallet does a recoup of A for EUR:1. Now the denomination of B also gets revoked. The wallet recoups B (incidentally also for EUR:1) and now A can be recouped again for EUR:1. But now the exchange is in a state where it will refuse a legitimate recoup request for A because the detection for an idempotent request kicks in. This is IMHO bad API design, and the exchange should simply always recoup the maximum amount. Furthermore, we usually follow the principle of "API calls that take up DB space are paid". With the current recoup API, I can do many tiny recoup requests which the exchange then has to store, right? I guess it would not be a big change to remove the "amount" value from the recoup/recoup-refresh request bodies, right? - Florian
400 lines
12 KiB
C
400 lines
12 KiB
C
/*
|
|
This file is part of TALER
|
|
Copyright (C) 2017-2021 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
|
|
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
|
|
<http://www.gnu.org/licenses/>
|
|
*/
|
|
/**
|
|
* @file lib/exchange_api_recoup_refresh.c
|
|
* @brief Implementation of the /recoup-refresh request of the exchange's HTTP API
|
|
* @author Christian Grothoff
|
|
*/
|
|
#include "platform.h"
|
|
#include <jansson.h>
|
|
#include <microhttpd.h> /* just for HTTP status codes */
|
|
#include <gnunet/gnunet_util_lib.h>
|
|
#include <gnunet/gnunet_json_lib.h>
|
|
#include <gnunet/gnunet_curl_lib.h>
|
|
#include "taler_json_lib.h"
|
|
#include "taler_exchange_service.h"
|
|
#include "exchange_api_handle.h"
|
|
#include "taler_signatures.h"
|
|
#include "exchange_api_curl_defaults.h"
|
|
|
|
|
|
/**
|
|
* @brief A Recoup Handle
|
|
*/
|
|
struct TALER_EXCHANGE_RecoupRefreshHandle
|
|
{
|
|
|
|
/**
|
|
* The connection to exchange this request handle will use
|
|
*/
|
|
struct TALER_EXCHANGE_Handle *exchange;
|
|
|
|
/**
|
|
* The url for this request.
|
|
*/
|
|
char *url;
|
|
|
|
/**
|
|
* Context for #TEH_curl_easy_post(). Keeps the data that must
|
|
* persist for Curl to make the upload.
|
|
*/
|
|
struct TALER_CURL_PostContext ctx;
|
|
|
|
/**
|
|
* Denomination key of the coin.
|
|
*/
|
|
struct TALER_EXCHANGE_DenomPublicKey pk;
|
|
|
|
/**
|
|
* Handle for the request.
|
|
*/
|
|
struct GNUNET_CURL_Job *job;
|
|
|
|
/**
|
|
* Function to call with the result.
|
|
*/
|
|
TALER_EXCHANGE_RecoupRefreshResultCallback cb;
|
|
|
|
/**
|
|
* Closure for @a cb.
|
|
*/
|
|
void *cb_cls;
|
|
|
|
/**
|
|
* Public key of the coin we are trying to get paid back.
|
|
*/
|
|
struct TALER_CoinSpendPublicKeyP coin_pub;
|
|
|
|
};
|
|
|
|
|
|
/**
|
|
* Parse a recoup-refresh response. If it is valid, call the callback.
|
|
*
|
|
* @param ph recoup handle
|
|
* @param json json reply with the signature
|
|
* @return #GNUNET_OK if the signature is valid and we called the callback;
|
|
* #GNUNET_SYSERR if not (callback must still be called)
|
|
*/
|
|
static enum GNUNET_GenericReturnValue
|
|
process_recoup_response (
|
|
const struct TALER_EXCHANGE_RecoupRefreshHandle *ph,
|
|
const json_t *json)
|
|
{
|
|
struct TALER_CoinSpendPublicKeyP old_coin_pub;
|
|
struct GNUNET_JSON_Specification spec_refresh[] = {
|
|
GNUNET_JSON_spec_fixed_auto ("old_coin_pub",
|
|
&old_coin_pub),
|
|
GNUNET_JSON_spec_end ()
|
|
};
|
|
struct TALER_EXCHANGE_HttpResponse hr = {
|
|
.reply = json,
|
|
.http_status = MHD_HTTP_OK
|
|
};
|
|
|
|
if (GNUNET_OK !=
|
|
GNUNET_JSON_parse (json,
|
|
spec_refresh,
|
|
NULL, NULL))
|
|
{
|
|
GNUNET_break_op (0);
|
|
return GNUNET_SYSERR;
|
|
}
|
|
ph->cb (ph->cb_cls,
|
|
&hr,
|
|
&old_coin_pub);
|
|
return GNUNET_OK;
|
|
}
|
|
|
|
|
|
/**
|
|
* Function called when we're done processing the
|
|
* HTTP /recoup-refresh request.
|
|
*
|
|
* @param cls the `struct TALER_EXCHANGE_RecoupRefreshHandle`
|
|
* @param response_code HTTP response code, 0 on error
|
|
* @param response parsed JSON result, NULL on error
|
|
*/
|
|
static void
|
|
handle_recoup_refresh_finished (void *cls,
|
|
long response_code,
|
|
const void *response)
|
|
{
|
|
struct TALER_EXCHANGE_RecoupRefreshHandle *ph = cls;
|
|
const json_t *j = response;
|
|
struct TALER_EXCHANGE_HttpResponse hr = {
|
|
.reply = j,
|
|
.http_status = (unsigned int) response_code
|
|
};
|
|
|
|
ph->job = NULL;
|
|
switch (response_code)
|
|
{
|
|
case 0:
|
|
hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
|
|
break;
|
|
case MHD_HTTP_OK:
|
|
if (GNUNET_OK !=
|
|
process_recoup_response (ph,
|
|
j))
|
|
{
|
|
GNUNET_break_op (0);
|
|
hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
|
|
hr.http_status = 0;
|
|
break;
|
|
}
|
|
TALER_EXCHANGE_recoup_refresh_cancel (ph);
|
|
return;
|
|
case MHD_HTTP_BAD_REQUEST:
|
|
/* This should never happen, either us or the exchange is buggy
|
|
(or API version conflict); just pass JSON reply to the application */
|
|
hr.ec = TALER_JSON_get_error_code (j);
|
|
hr.hint = TALER_JSON_get_error_hint (j);
|
|
break;
|
|
case MHD_HTTP_CONFLICT:
|
|
{
|
|
/* Insufficient funds, proof attached */
|
|
json_t *history;
|
|
struct TALER_Amount total;
|
|
struct TALER_DenominationHash h_denom_pub;
|
|
const struct TALER_EXCHANGE_DenomPublicKey *dki;
|
|
enum TALER_ErrorCode ec;
|
|
|
|
dki = &ph->pk;
|
|
history = json_object_get (j,
|
|
"history");
|
|
if (GNUNET_OK !=
|
|
TALER_EXCHANGE_verify_coin_history (dki,
|
|
dki->fee_deposit.currency,
|
|
&ph->coin_pub,
|
|
history,
|
|
&h_denom_pub,
|
|
&total))
|
|
{
|
|
GNUNET_break_op (0);
|
|
hr.http_status = 0;
|
|
hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
|
|
}
|
|
else
|
|
{
|
|
hr.ec = TALER_JSON_get_error_code (j);
|
|
hr.hint = TALER_JSON_get_error_hint (j);
|
|
}
|
|
ec = TALER_JSON_get_error_code (j);
|
|
switch (ec)
|
|
{
|
|
case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS:
|
|
if (0 > TALER_amount_cmp (&total,
|
|
&dki->value))
|
|
{
|
|
/* recoup MAY have still been possible */
|
|
/* FIXME: This code may falsely complain, as we do not
|
|
know that the smallest denomination offered by the
|
|
exchange is here. We should look at the key
|
|
structure of ph->exchange, and find the smallest
|
|
_currently withdrawable_ denomination and check
|
|
if the value remaining would suffice... */
|
|
GNUNET_break_op (0);
|
|
hr.http_status = 0;
|
|
hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
|
|
break;
|
|
}
|
|
break;
|
|
case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY:
|
|
if (0 == GNUNET_memcmp (&ph->pk.h_key,
|
|
&h_denom_pub))
|
|
{
|
|
/* invalid proof provided */
|
|
GNUNET_break_op (0);
|
|
hr.http_status = 0;
|
|
hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
|
|
break;
|
|
}
|
|
/* valid error from exchange */
|
|
break;
|
|
default:
|
|
GNUNET_break_op (0);
|
|
hr.http_status = 0;
|
|
hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
|
|
break;
|
|
}
|
|
ph->cb (ph->cb_cls,
|
|
&hr,
|
|
NULL);
|
|
TALER_EXCHANGE_recoup_refresh_cancel (ph);
|
|
return;
|
|
}
|
|
case MHD_HTTP_FORBIDDEN:
|
|
/* Nothing really to verify, exchange says one of the signatures is
|
|
invalid; as we checked them, this should never happen, we
|
|
should pass the JSON reply to the application */
|
|
hr.ec = TALER_JSON_get_error_code (j);
|
|
hr.hint = TALER_JSON_get_error_hint (j);
|
|
break;
|
|
case MHD_HTTP_NOT_FOUND:
|
|
/* Nothing really to verify, this should never
|
|
happen, we should pass the JSON reply to the application */
|
|
hr.ec = TALER_JSON_get_error_code (j);
|
|
hr.hint = TALER_JSON_get_error_hint (j);
|
|
break;
|
|
case MHD_HTTP_GONE:
|
|
/* Kind of normal: the money was already sent to the merchant
|
|
(it was too late for the refund). */
|
|
hr.ec = TALER_JSON_get_error_code (j);
|
|
hr.hint = TALER_JSON_get_error_hint (j);
|
|
break;
|
|
case MHD_HTTP_INTERNAL_SERVER_ERROR:
|
|
/* Server had an internal issue; we should retry, but this API
|
|
leaves this to the application */
|
|
hr.ec = TALER_JSON_get_error_code (j);
|
|
hr.hint = TALER_JSON_get_error_hint (j);
|
|
break;
|
|
default:
|
|
/* unexpected response code */
|
|
hr.ec = TALER_JSON_get_error_code (j);
|
|
hr.hint = TALER_JSON_get_error_hint (j);
|
|
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
|
|
"Unexpected response code %u/%d for exchange recoup\n",
|
|
(unsigned int) response_code,
|
|
(int) hr.ec);
|
|
GNUNET_break (0);
|
|
break;
|
|
}
|
|
ph->cb (ph->cb_cls,
|
|
&hr,
|
|
NULL);
|
|
TALER_EXCHANGE_recoup_refresh_cancel (ph);
|
|
}
|
|
|
|
|
|
struct TALER_EXCHANGE_RecoupRefreshHandle *
|
|
TALER_EXCHANGE_recoup_refresh (
|
|
struct TALER_EXCHANGE_Handle *exchange,
|
|
const struct TALER_EXCHANGE_DenomPublicKey *pk,
|
|
const struct TALER_DenominationSignature *denom_sig,
|
|
const struct TALER_PlanchetSecretsP *ps,
|
|
TALER_EXCHANGE_RecoupRefreshResultCallback recoup_cb,
|
|
void *recoup_cb_cls)
|
|
{
|
|
struct TALER_EXCHANGE_RecoupRefreshHandle *ph;
|
|
struct GNUNET_CURL_Context *ctx;
|
|
struct TALER_CoinSpendSignatureP coin_sig;
|
|
struct TALER_CoinSpendPublicKeyP coin_pub;
|
|
struct TALER_DenominationHash h_denom_pub;
|
|
json_t *recoup_obj;
|
|
CURL *eh;
|
|
char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 32];
|
|
|
|
GNUNET_assert (GNUNET_YES ==
|
|
TEAH_handle_is_ready (exchange));
|
|
GNUNET_CRYPTO_eddsa_key_get_public (&ps->coin_priv.eddsa_priv,
|
|
&coin_pub.eddsa_pub);
|
|
TALER_denom_pub_hash (&pk->key,
|
|
&h_denom_pub);
|
|
TALER_wallet_recoup_refresh_sign (&h_denom_pub,
|
|
&ps->blinding_key,
|
|
&ps->coin_priv,
|
|
&coin_sig);
|
|
recoup_obj = GNUNET_JSON_PACK (
|
|
GNUNET_JSON_pack_data_auto ("denom_pub_hash",
|
|
&h_denom_pub),
|
|
TALER_JSON_pack_denom_sig ("denom_sig",
|
|
denom_sig),
|
|
GNUNET_JSON_pack_data_auto ("coin_sig",
|
|
&coin_sig),
|
|
GNUNET_JSON_pack_data_auto ("coin_blind_key_secret",
|
|
&ps->blinding_key));
|
|
|
|
{
|
|
char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2];
|
|
char *end;
|
|
|
|
end = GNUNET_STRINGS_data_to_string (
|
|
&coin_pub,
|
|
sizeof (struct TALER_CoinSpendPublicKeyP),
|
|
pub_str,
|
|
sizeof (pub_str));
|
|
*end = '\0';
|
|
GNUNET_snprintf (arg_str,
|
|
sizeof (arg_str),
|
|
"/coins/%s/recoup-refresh",
|
|
pub_str);
|
|
}
|
|
|
|
ph = GNUNET_new (struct TALER_EXCHANGE_RecoupRefreshHandle);
|
|
ph->coin_pub = coin_pub;
|
|
ph->exchange = exchange;
|
|
ph->pk = *pk;
|
|
memset (&ph->pk.key,
|
|
0,
|
|
sizeof (ph->pk.key)); /* zero out, as lifetime cannot be warranted */
|
|
ph->cb = recoup_cb;
|
|
ph->cb_cls = recoup_cb_cls;
|
|
ph->url = TEAH_path_to_url (exchange,
|
|
arg_str);
|
|
if (NULL == ph->url)
|
|
{
|
|
json_decref (recoup_obj);
|
|
GNUNET_free (ph);
|
|
return NULL;
|
|
}
|
|
eh = TALER_EXCHANGE_curl_easy_get_ (ph->url);
|
|
if ( (NULL == eh) ||
|
|
(GNUNET_OK !=
|
|
TALER_curl_easy_post (&ph->ctx,
|
|
eh,
|
|
recoup_obj)) )
|
|
{
|
|
GNUNET_break (0);
|
|
if (NULL != eh)
|
|
curl_easy_cleanup (eh);
|
|
json_decref (recoup_obj);
|
|
GNUNET_free (ph->url);
|
|
GNUNET_free (ph);
|
|
return NULL;
|
|
}
|
|
json_decref (recoup_obj);
|
|
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
|
|
"URL for recoup-refresh: `%s'\n",
|
|
ph->url);
|
|
ctx = TEAH_handle_to_context (exchange);
|
|
ph->job = GNUNET_CURL_job_add2 (ctx,
|
|
eh,
|
|
ph->ctx.headers,
|
|
&handle_recoup_refresh_finished,
|
|
ph);
|
|
return ph;
|
|
}
|
|
|
|
|
|
void
|
|
TALER_EXCHANGE_recoup_refresh_cancel (
|
|
struct TALER_EXCHANGE_RecoupRefreshHandle *ph)
|
|
{
|
|
if (NULL != ph->job)
|
|
{
|
|
GNUNET_CURL_job_cancel (ph->job);
|
|
ph->job = NULL;
|
|
}
|
|
GNUNET_free (ph->url);
|
|
TALER_curl_easy_post_finished (&ph->ctx);
|
|
GNUNET_free (ph);
|
|
}
|
|
|
|
|
|
/* end of exchange_api_recoup_refresh.c */
|