/* This file is part of TALER Copyright (C) 2014-2023 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with TALER; see the file COPYING. If not, see */ /** * @file taler-exchange-httpd_coins_get.c * @brief Handle GET /coins/$COIN_PUB/history requests * @author Christian Grothoff */ #include "platform.h" #include #include #include "taler_mhd_lib.h" #include "taler_json_lib.h" #include "taler_dbevents.h" #include "taler-exchange-httpd_keys.h" #include "taler-exchange-httpd_coins_get.h" #include "taler-exchange-httpd_responses.h" /** * Add the headers we want to set for every response. * * @param cls the key state to use * @param[in,out] response the response to modify */ static void add_response_headers (void *cls, struct MHD_Response *response) { (void) cls; TALER_MHD_add_global_headers (response); GNUNET_break (MHD_YES == MHD_add_response_header (response, MHD_HTTP_HEADER_CACHE_CONTROL, "no-cache")); } /** * Compile the transaction history of a coin into a JSON object. * * @param coin_pub public key of the coin * @param tl transaction history to JSON-ify * @return json representation of the @a rh, NULL on error */ static json_t * compile_transaction_history ( const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_EXCHANGEDB_TransactionList *tl) { json_t *history; history = json_array (); if (NULL == history) { GNUNET_break (0); /* out of memory!? */ return NULL; } for (const struct TALER_EXCHANGEDB_TransactionList *pos = tl; NULL != pos; pos = pos->next) { switch (pos->type) { case TALER_EXCHANGEDB_TT_DEPOSIT: { const struct TALER_EXCHANGEDB_DepositListEntry *deposit = pos->details.deposit; struct TALER_MerchantWireHashP h_wire; TALER_merchant_wire_signature_hash (deposit->receiver_wire_account, &deposit->wire_salt, &h_wire); #if ENABLE_SANITY_CHECKS /* internal sanity check before we hand out a bogus sig... */ TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_wallet_deposit_verify ( &deposit->amount_with_fee, &deposit->deposit_fee, &h_wire, &deposit->h_contract_terms, deposit->no_wallet_data_hash ? NULL : &deposit->wallet_data_hash, deposit->no_age_commitment ? NULL : &deposit->h_age_commitment, &deposit->h_policy, &deposit->h_denom_pub, deposit->timestamp, &deposit->merchant_pub, deposit->refund_deadline, coin_pub, &deposit->csig)) { GNUNET_break (0); json_decref (history); return NULL; } #endif if (0 != json_array_append_new ( history, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", "DEPOSIT"), TALER_JSON_pack_amount ("amount", &deposit->amount_with_fee), TALER_JSON_pack_amount ("deposit_fee", &deposit->deposit_fee), GNUNET_JSON_pack_timestamp ("timestamp", deposit->timestamp), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_timestamp ("refund_deadline", deposit->refund_deadline)), GNUNET_JSON_pack_data_auto ("merchant_pub", &deposit->merchant_pub), GNUNET_JSON_pack_data_auto ("h_contract_terms", &deposit->h_contract_terms), GNUNET_JSON_pack_data_auto ("h_wire", &h_wire), GNUNET_JSON_pack_allow_null ( deposit->no_age_commitment ? GNUNET_JSON_pack_string ( "h_age_commitment", NULL) : GNUNET_JSON_pack_data_auto ("h_age_commitment", &deposit->h_age_commitment)), GNUNET_JSON_pack_data_auto ("coin_sig", &deposit->csig)))) { GNUNET_break (0); json_decref (history); return NULL; } break; } case TALER_EXCHANGEDB_TT_MELT: { const struct TALER_EXCHANGEDB_MeltListEntry *melt = pos->details.melt; const struct TALER_AgeCommitmentHash *phac = NULL; #if ENABLE_SANITY_CHECKS TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_wallet_melt_verify ( &melt->amount_with_fee, &melt->melt_fee, &melt->rc, &melt->h_denom_pub, &melt->h_age_commitment, coin_pub, &melt->coin_sig)) { GNUNET_break (0); json_decref (history); return NULL; } #endif /* Age restriction is optional. We communicate a NULL value to * JSON_PACK below */ if (! melt->no_age_commitment) phac = &melt->h_age_commitment; if (0 != json_array_append_new ( history, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", "MELT"), TALER_JSON_pack_amount ("amount", &melt->amount_with_fee), TALER_JSON_pack_amount ("melt_fee", &melt->melt_fee), GNUNET_JSON_pack_data_auto ("rc", &melt->rc), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_data_auto ("h_age_commitment", phac)), GNUNET_JSON_pack_data_auto ("coin_sig", &melt->coin_sig)))) { GNUNET_break (0); json_decref (history); return NULL; } } break; case TALER_EXCHANGEDB_TT_REFUND: { const struct TALER_EXCHANGEDB_RefundListEntry *refund = pos->details.refund; struct TALER_Amount value; #if ENABLE_SANITY_CHECKS TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; if (GNUNET_OK != TALER_merchant_refund_verify ( coin_pub, &refund->h_contract_terms, refund->rtransaction_id, &refund->refund_amount, &refund->merchant_pub, &refund->merchant_sig)) { GNUNET_break (0); json_decref (history); return NULL; } #endif if (0 > TALER_amount_subtract (&value, &refund->refund_amount, &refund->refund_fee)) { GNUNET_break (0); json_decref (history); return NULL; } if (0 != json_array_append_new ( history, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", "REFUND"), TALER_JSON_pack_amount ("amount", &value), TALER_JSON_pack_amount ("refund_fee", &refund->refund_fee), GNUNET_JSON_pack_data_auto ("h_contract_terms", &refund->h_contract_terms), GNUNET_JSON_pack_data_auto ("merchant_pub", &refund->merchant_pub), GNUNET_JSON_pack_uint64 ("rtransaction_id", refund->rtransaction_id), GNUNET_JSON_pack_data_auto ("merchant_sig", &refund->merchant_sig)))) { GNUNET_break (0); json_decref (history); return NULL; } } break; case TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP: { struct TALER_EXCHANGEDB_RecoupRefreshListEntry *pr = pos->details.old_coin_recoup; struct TALER_ExchangePublicKeyP epub; struct TALER_ExchangeSignatureP esig; if (TALER_EC_NONE != TALER_exchange_online_confirm_recoup_refresh_sign ( &TEH_keys_exchange_sign_, pr->timestamp, &pr->value, &pr->coin.coin_pub, &pr->old_coin_pub, &epub, &esig)) { GNUNET_break (0); json_decref (history); return NULL; } /* NOTE: we could also provide coin_pub's coin_sig, denomination key hash and the denomination key's RSA signature over coin_pub, but as the wallet should really already have this information (and cannot check or do anything with it anyway if it doesn't), it seems strictly unnecessary. */ if (0 != json_array_append_new ( history, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", "OLD-COIN-RECOUP"), TALER_JSON_pack_amount ("amount", &pr->value), GNUNET_JSON_pack_data_auto ("exchange_sig", &esig), GNUNET_JSON_pack_data_auto ("exchange_pub", &epub), GNUNET_JSON_pack_data_auto ("coin_pub", &pr->coin.coin_pub), GNUNET_JSON_pack_timestamp ("timestamp", pr->timestamp)))) { GNUNET_break (0); json_decref (history); return NULL; } break; } case TALER_EXCHANGEDB_TT_RECOUP: { const struct TALER_EXCHANGEDB_RecoupListEntry *recoup = pos->details.recoup; struct TALER_ExchangePublicKeyP epub; struct TALER_ExchangeSignatureP esig; if (TALER_EC_NONE != TALER_exchange_online_confirm_recoup_sign ( &TEH_keys_exchange_sign_, recoup->timestamp, &recoup->value, coin_pub, &recoup->reserve_pub, &epub, &esig)) { GNUNET_break (0); json_decref (history); return NULL; } if (0 != json_array_append_new ( history, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", "RECOUP"), TALER_JSON_pack_amount ("amount", &recoup->value), GNUNET_JSON_pack_data_auto ("exchange_sig", &esig), GNUNET_JSON_pack_data_auto ("exchange_pub", &epub), GNUNET_JSON_pack_data_auto ("reserve_pub", &recoup->reserve_pub), GNUNET_JSON_pack_data_auto ("coin_sig", &recoup->coin_sig), GNUNET_JSON_pack_data_auto ("coin_blind", &recoup->coin_blind), GNUNET_JSON_pack_data_auto ("reserve_pub", &recoup->reserve_pub), GNUNET_JSON_pack_timestamp ("timestamp", recoup->timestamp)))) { GNUNET_break (0); json_decref (history); return NULL; } } break; case TALER_EXCHANGEDB_TT_RECOUP_REFRESH: { struct TALER_EXCHANGEDB_RecoupRefreshListEntry *pr = pos->details.recoup_refresh; struct TALER_ExchangePublicKeyP epub; struct TALER_ExchangeSignatureP esig; if (TALER_EC_NONE != TALER_exchange_online_confirm_recoup_refresh_sign ( &TEH_keys_exchange_sign_, pr->timestamp, &pr->value, coin_pub, &pr->old_coin_pub, &epub, &esig)) { GNUNET_break (0); json_decref (history); return NULL; } /* NOTE: we could also provide coin_pub's coin_sig, denomination key hash and the denomination key's RSA signature over coin_pub, but as the wallet should really already have this information (and cannot check or do anything with it anyway if it doesn't), it seems strictly unnecessary. */ if (0 != json_array_append_new ( history, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", "RECOUP-REFRESH"), TALER_JSON_pack_amount ("amount", &pr->value), GNUNET_JSON_pack_data_auto ("exchange_sig", &esig), GNUNET_JSON_pack_data_auto ("exchange_pub", &epub), GNUNET_JSON_pack_data_auto ("old_coin_pub", &pr->old_coin_pub), GNUNET_JSON_pack_data_auto ("coin_sig", &pr->coin_sig), GNUNET_JSON_pack_data_auto ("coin_blind", &pr->coin_blind), GNUNET_JSON_pack_timestamp ("timestamp", pr->timestamp)))) { GNUNET_break (0); json_decref (history); return NULL; } break; } case TALER_EXCHANGEDB_TT_PURSE_DEPOSIT: { struct TALER_EXCHANGEDB_PurseDepositListEntry *pd = pos->details.purse_deposit; const struct TALER_AgeCommitmentHash *phac = NULL; if (! pd->no_age_commitment) phac = &pd->h_age_commitment; if (0 != json_array_append_new ( history, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", "PURSE-DEPOSIT"), TALER_JSON_pack_amount ("amount", &pd->amount), GNUNET_JSON_pack_string ("exchange_base_url", NULL == pd->exchange_base_url ? TEH_base_url : pd->exchange_base_url), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_data_auto ("h_age_commitment", phac)), GNUNET_JSON_pack_data_auto ("purse_pub", &pd->purse_pub), GNUNET_JSON_pack_bool ("refunded", pd->refunded), GNUNET_JSON_pack_data_auto ("coin_sig", &pd->coin_sig)))) { GNUNET_break (0); json_decref (history); return NULL; } break; } case TALER_EXCHANGEDB_TT_PURSE_REFUND: { const struct TALER_EXCHANGEDB_PurseRefundListEntry *prefund = pos->details.purse_refund; struct TALER_Amount value; enum TALER_ErrorCode ec; struct TALER_ExchangePublicKeyP epub; struct TALER_ExchangeSignatureP esig; if (0 > TALER_amount_subtract (&value, &prefund->refund_amount, &prefund->refund_fee)) { GNUNET_break (0); json_decref (history); return NULL; } ec = TALER_exchange_online_purse_refund_sign ( &TEH_keys_exchange_sign_, &value, &prefund->refund_fee, coin_pub, &prefund->purse_pub, &epub, &esig); if (TALER_EC_NONE != ec) { GNUNET_break (0); json_decref (history); return NULL; } if (0 != json_array_append_new ( history, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", "PURSE-REFUND"), TALER_JSON_pack_amount ("amount", &value), TALER_JSON_pack_amount ("refund_fee", &prefund->refund_fee), GNUNET_JSON_pack_data_auto ("exchange_sig", &esig), GNUNET_JSON_pack_data_auto ("exchange_pub", &epub), GNUNET_JSON_pack_data_auto ("purse_pub", &prefund->purse_pub)))) { GNUNET_break (0); json_decref (history); return NULL; } } break; case TALER_EXCHANGEDB_TT_RESERVE_OPEN: { struct TALER_EXCHANGEDB_ReserveOpenListEntry *role = pos->details.reserve_open; if (0 != json_array_append_new ( history, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", "RESERVE-OPEN-DEPOSIT"), TALER_JSON_pack_amount ("coin_contribution", &role->coin_contribution), GNUNET_JSON_pack_data_auto ("reserve_sig", &role->reserve_sig), GNUNET_JSON_pack_data_auto ("coin_sig", &role->coin_sig)))) { GNUNET_break (0); json_decref (history); return NULL; } break; } } } return history; } MHD_RESULT TEH_handler_coins_get (struct TEH_RequestContext *rc, const struct TALER_CoinSpendPublicKeyP *coin_pub) { struct TALER_EXCHANGEDB_TransactionList *tl = NULL; uint64_t start_off = 0; uint64_t etag_in; uint64_t etag_out; char etagp[24]; struct MHD_Response *resp; unsigned int http_status; struct TALER_DenominationHashP h_denom_pub; struct TALER_Amount balance; TALER_MHD_parse_request_number (rc->connection, "start", &start_off); /* Check signature */ { struct TALER_CoinSpendSignatureP coin_sig; bool required = true; TALER_MHD_parse_request_header_auto (rc->connection, TALER_COIN_HISTORY_SIGNATURE_HEADER, &coin_sig, required); if (GNUNET_OK != TALER_wallet_coin_history_verify (start_off, coin_pub, &coin_sig)) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_FORBIDDEN, TALER_EC_EXCHANGE_COIN_HISTORY_BAD_SIGNATURE, NULL); } } /* Get etag */ { const char *etags; etags = MHD_lookup_connection_value (rc->connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_NONE_MATCH); if (NULL != etags) { char dummy; unsigned long long ev; if (1 != sscanf (etags, "\"%llu\"%c", &ev, &dummy)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Client send malformed `If-None-Match' header `%s'\n", etags); etag_in = start_off; } else { etag_in = (uint64_t) ev; } } else { etag_in = start_off; } } /* Get history from DB between etag and now */ { enum GNUNET_DB_QueryStatus qs; qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, coin_pub, start_off, etag_in, &etag_out, &balance, &h_denom_pub, &tl); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "get_coin_history"); case GNUNET_DB_STATUS_SOFT_ERROR: return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_SOFT_FAILURE, "get_coin_history"); case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_NOT_FOUND, TALER_EC_EXCHANGE_GENERIC_COIN_UNKNOWN, NULL); case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: /* Handled below */ break; } } GNUNET_snprintf (etagp, sizeof (etagp), "\"%llu\"", (unsigned long long) etag_out); if (etag_in == etag_out) { return TEH_RESPONSE_reply_not_modified (rc->connection, etagp, &add_response_headers, NULL); } if (NULL == tl) { /* 204: empty history */ resp = MHD_create_response_from_buffer (0, "", MHD_RESPMEM_PERSISTENT); http_status = MHD_HTTP_NO_CONTENT; } else { /* 200: regular history */ json_t *history; history = compile_transaction_history (coin_pub, tl); TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, tl); tl = NULL; if (NULL == history) { GNUNET_break (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, "Failed to compile coin history"); } resp = TALER_MHD_MAKE_JSON_PACK ( GNUNET_JSON_pack_data_auto ("h_denom_pub", &h_denom_pub), TALER_JSON_pack_amount ("balance", &balance), GNUNET_JSON_pack_array_steal ("history", history)); http_status = MHD_HTTP_OK; } add_response_headers (NULL, resp); GNUNET_break (MHD_YES == MHD_add_response_header (resp, MHD_HTTP_HEADER_ETAG, etagp)); { MHD_RESULT ret; ret = MHD_queue_response (rc->connection, http_status, resp); GNUNET_break (MHD_YES == ret); MHD_destroy_response (resp); return ret; } } /* end of taler-exchange-httpd_coins_get.c */