/* This file is part of TALER (C) 2018--2020 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-reserve-helper.c * @brief helper functions to check the status of a tipping reserve * @author Christian Grothoff */ #include "platform.h" #include "taler-merchant-httpd_tip-reserve-helper.h" /** * Head of active ctr context DLL. */ static struct TMH_CheckTipReserve *ctr_head; /** * Tail of active ctr context DLL. */ static struct TMH_CheckTipReserve *ctr_tail; /** * Resume connection underlying @a ctr. * * @param ctr what to resume */ static void resume_ctr (struct TMH_CheckTipReserve *ctr) { GNUNET_assert (GNUNET_YES == ctr->suspended); GNUNET_CONTAINER_DLL_remove (ctr_head, ctr_tail, ctr); MHD_resume_connection (ctr->connection); TMH_trigger_daemon (); /* we resumed, kick MHD */ } /** * Resume the given context and send the given response. Stores the response * in the @a ctr and signals MHD to resume the connection. Also ensures MHD * runs immediately. * * @param ctr tip reserve query helper context * @param response_code response code to use * @param response response data to send back */ static void resume_with_response (struct TMH_CheckTipReserve *ctr, unsigned int response_code, struct MHD_Response *response) { ctr->response_code = response_code; ctr->response = response; resume_ctr (ctr); ctr->suspended = GNUNET_NO; } /** * 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 TMH_CheckTipReserve *' * @param hr HTTP response details * @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, const struct TALER_EXCHANGE_HttpResponse *hr, const struct TALER_Amount *balance, unsigned int history_length, const struct TALER_EXCHANGE_ReserveHistory *history) { struct TMH_CheckTipReserve *ctr = cls; ctr->rsh = NULL; ctr->reserve_expiration = GNUNET_TIME_UNIT_ZERO_ABS; if (MHD_HTTP_NOT_FOUND == hr->http_status) { resume_with_response ( ctr, MHD_HTTP_SERVICE_UNAVAILABLE, TALER_MHD_make_json_pack ( "{s:I, s:I, s:s, s:I, s:O}", "code", (json_int_t) TALER_EC_TIP_QUERY_RESERVE_UNKNOWN_TO_EXCHANGE, "exchange_http_status", hr->http_status, "hint", "tipping reserve unknown at exchange", "exchange_code", hr->ec, "exchange_reply", hr->reply)); return; } if (MHD_HTTP_OK != hr->http_status) { GNUNET_break_op (0); resume_with_response ( ctr, MHD_HTTP_FAILED_DEPENDENCY, TALER_MHD_make_json_pack ( "{s:I, s:I, s:s, s:I, s:O}", "code", (json_int_t) TALER_EC_TIP_QUERY_RESERVE_HISTORY_FAILED, "exchange_http_status", hr->http_status, "hint", "exchange failed to provide reserve history", "exchange_code", (json_int_t) hr->ec, "exchange_reply", hr->reply)); return; } if (0 == history_length) { GNUNET_break_op (0); resume_with_response (ctr, MHD_HTTP_FAILED_DEPENDENCY, TALER_MHD_make_error ( TALER_EC_TIP_QUERY_RESERVE_HISTORY_FAILED_EMPTY, "Exchange returned empty reserve history")); return; } { unsigned int found = UINT_MAX; for (unsigned int i = 0; iamount_withdrawn)); } if (GNUNET_YES == ctr->none_authorized) ctr->amount_authorized = ctr->amount_withdrawn; /* aka zero */ ctr->amount_deposited = ctr->amount_withdrawn; /* aka zero */ /* Update DB based on status! */ for (unsigned int i = 0; itype) { case TALER_EXCHANGE_RTT_CREDIT: { enum GNUNET_DB_QueryStatus qs; struct GNUNET_HashCode uuid; struct GNUNET_TIME_Absolute deposit_expiration; if (0 > TALER_amount_add (&ctr->amount_deposited, &ctr->amount_deposited, &hi->amount)) { GNUNET_break_op (0); resume_with_response ( ctr, MHD_HTTP_FAILED_DEPENDENCY, TALER_MHD_make_error ( TALER_EC_TIP_QUERY_RESERVE_HISTORY_ARITHMETIC_ISSUE_DEPOSIT, "Exchange returned invalid reserve history (amount overflow)")); return; } deposit_expiration = GNUNET_TIME_absolute_add ( hi->details.in_details.timestamp, ctr->idle_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. */ ctr->reserve_expiration = GNUNET_TIME_absolute_max ( ctr->reserve_expiration, deposit_expiration); GNUNET_CRYPTO_hash (hi->details.in_details.wire_reference, hi->details.in_details.wire_reference_size, &uuid); db->preflight (db->cls); qs = db->enable_tip_reserve_TR (db->cls, &ctr->reserve_priv, &uuid, &hi->amount, deposit_expiration); if (0 > qs) { /* This is not inherently fatal for the client's request, so we merely log it */ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Database error updating tipping reserve status: %d\n", qs); } } break; case TALER_EXCHANGE_RTT_WITHDRAWAL: if (0 > TALER_amount_add (&ctr->amount_withdrawn, &ctr->amount_withdrawn, &hi->amount)) { GNUNET_break_op (0); resume_with_response ( ctr, MHD_HTTP_FAILED_DEPENDENCY, TALER_MHD_make_error ( TALER_EC_TIP_QUERY_RESERVE_HISTORY_ARITHMETIC_ISSUE_WITHDRAW, "Exchange returned invalid reserve history (amount overflow)")); return; } break; case TALER_EXCHANGE_RTT_RECOUP: { enum GNUNET_DB_QueryStatus qs; struct GNUNET_HashContext *hc; struct GNUNET_HashCode uuid; struct GNUNET_TIME_Absolute deposit_expiration; struct GNUNET_TIME_AbsoluteNBO de; GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Encountered unexpected recoup operation on tipping reserve\n"); /* While unexpected, we can simply count these like deposits. */ if (0 > TALER_amount_add (&ctr->amount_deposited, &ctr->amount_deposited, &hi->amount)) { GNUNET_break_op (0); resume_with_response ( ctr, MHD_HTTP_FAILED_DEPENDENCY, TALER_MHD_make_error ( TALER_EC_TIP_QUERY_RESERVE_HISTORY_ARITHMETIC_ISSUE_RECOUP, "Exchange returned invalid reserve history (amount overflow)")); return; } deposit_expiration = GNUNET_TIME_absolute_add ( hi->details.recoup_details.timestamp, ctr->idle_reserve_expiration_time); ctr->reserve_expiration = GNUNET_TIME_absolute_max ( ctr->reserve_expiration, deposit_expiration); de = GNUNET_TIME_absolute_hton (deposit_expiration); hc = GNUNET_CRYPTO_hash_context_start (); GNUNET_CRYPTO_hash_context_read ( hc, &hi->details.recoup_details.coin_pub, sizeof (struct TALER_CoinSpendPublicKeyP)); GNUNET_CRYPTO_hash_context_read (hc, &de, sizeof (de)); GNUNET_CRYPTO_hash_context_finish (hc, &uuid); db->preflight (db->cls); qs = db->enable_tip_reserve_TR (db->cls, &ctr->reserve_priv, &uuid, &hi->amount, deposit_expiration); if (0 > qs) { /* This is not inherently fatal for the client's request, so we merely log it */ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Database error updating tipping reserve status: %d\n", qs); } } break; case TALER_EXCHANGE_RTT_CLOSE: /* We count 'closing' amounts just like withdrawals */ if (0 > TALER_amount_add (&ctr->amount_withdrawn, &ctr->amount_withdrawn, &hi->amount)) { GNUNET_break_op (0); resume_with_response ( ctr, MHD_HTTP_FAILED_DEPENDENCY, TALER_MHD_make_error ( TALER_EC_TIP_QUERY_RESERVE_HISTORY_ARITHMETIC_ISSUE_CLOSED, "Exchange returned invalid reserve history (amount overflow)")); return; } break; } } /* normal, non-error continuation */ resume_with_response (ctr, 0, NULL); } /** * Function called with the result of a #TMH_EXCHANGES_find_exchange() * operation. Given the exchange handle, we will then interrogate * the exchange about the status of the tipping reserve. * * @param cls closure with a `struct TMH_CheckTipReserve *` * @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 * @param ec error code, #TALER_EC_NONE on success * @param http_status the HTTP status we got from the exchange * @param error_reply the full reply from the exchange, NULL if * the response was NOT in JSON or on success */ static void exchange_cont (void *cls, struct TALER_EXCHANGE_Handle *eh, const struct TALER_Amount *wire_fee, int exchange_trusted, enum TALER_ErrorCode ec, unsigned int http_status, const json_t *error_reply) { struct TMH_CheckTipReserve *ctr = cls; struct TALER_ReservePublicKeyP reserve_pub; const struct TALER_EXCHANGE_Keys *keys; ctr->fo = NULL; if (NULL == eh) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to contact exchange configured for tipping!\n"); resume_with_response (ctr, MHD_HTTP_FAILED_DEPENDENCY, TALER_MHD_make_error ( TALER_EC_TIP_QUERY_RESERVE_STATUS_FAILED_EXCHANGE_DOWN, "Unable to obtain /keys from exchange")); return; } keys = TALER_EXCHANGE_get_keys (eh); GNUNET_assert (NULL != keys); ctr->idle_reserve_expiration_time = keys->reserve_closing_delay; GNUNET_CRYPTO_eddsa_key_get_public (&ctr->reserve_priv.eddsa_priv, &reserve_pub.eddsa_pub); ctr->rsh = TALER_EXCHANGE_reserves_get (eh, &reserve_pub, &handle_status, ctr); } /** * Check the status of the given reserve at the given exchange. * Suspends the MHD connection while this is happening and resumes * processing once we know the reserve status (or once an error * code has been determined). * * @param[in,out] ctr context for checking the reserve status * @param tip_exchange the URL of the exchange to query */ void TMH_check_tip_reserve (struct TMH_CheckTipReserve *ctr, const char *tip_exchange) { MHD_suspend_connection (ctr->connection); ctr->suspended = GNUNET_YES; GNUNET_CONTAINER_DLL_insert (ctr_head, ctr_tail, ctr); db->preflight (db->cls); ctr->fo = TMH_EXCHANGES_find_exchange (tip_exchange, NULL, GNUNET_NO, &exchange_cont, ctr); if (NULL == ctr->fo) { GNUNET_break (0); resume_with_response (ctr, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_MHD_make_error ( TALER_EC_INTERNAL_INVARIANT_FAILURE, "Unable to find exchange handle")); } } /** * Clean up any state that might be left in @a ctr. * * @param[in] context to clean up */ void TMH_check_tip_reserve_cleanup (struct TMH_CheckTipReserve *ctr) { if (NULL != ctr->rsh) { TALER_EXCHANGE_reserves_get_cancel (ctr->rsh); ctr->rsh = NULL; } if (NULL != ctr->fo) { TMH_EXCHANGES_find_exchange_cancel (ctr->fo); ctr->fo = NULL; } if (NULL != ctr->response) { MHD_destroy_response (ctr->response); ctr->response = NULL; } if (MHD_YES == ctr->suspended) { resume_ctr (ctr); ctr->suspended = GNUNET_NO; } } /** * Force all tip reserve helper contexts to be resumed as we are about to shut * down MHD. */ void MH_force_trh_resume () { struct TMH_CheckTipReserve *n; for (struct TMH_CheckTipReserve *ctr = ctr_head; NULL != ctr; ctr = n) { n = ctr->next; resume_ctr (ctr); ctr->suspended = GNUNET_SYSERR; } } /* end of taler-merchant-httpd_tip-reserve-helper.c */