/* This file is part of TALER (C) 2014-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_refund_lookup.c * @brief refund handling logic * @author Marcello Stanisci */ #include "platform.h" #include #include #include #include #include "taler-merchant-httpd.h" #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_refund.h" /** * How often do we retry DB transactions on serialization failures? */ #define MAX_RETRIES 5 /** * Information we keep for each coin to be refunded. */ struct CoinRefund { /** * Kept in a DLL. */ struct CoinRefund *next; /** * Kept in a DLL. */ struct CoinRefund *prev; /** * Request to connect to the target exchange. */ struct TMH_EXCHANGES_FindOperation *fo; /** * Handle for the refund operation with the exchange. */ struct TALER_EXCHANGE_RefundHandle *rh; /** * PRD this operation is part of. */ struct ProcessRefundData *prd; /** * URL of the exchange for this @e coin_pub. */ char *exchange_url; /** * Coin to refund. */ struct TALER_CoinSpendPublicKeyP coin_pub; /** * Refund transaction ID to use. */ uint64_t rtransaction_id; /** * Amount to refund. */ struct TALER_Amount refund_amount; /** * Applicable refund transaction fee. */ struct TALER_Amount refund_fee; /** * Public key of the exchange affirming the refund. */ struct TALER_ExchangePublicKeyP exchange_pub; /** * Signature of the exchange affirming the refund. */ struct TALER_ExchangeSignatureP exchange_sig; /** * HTTP status from the exchange, #MHD_HTTP_OK if * @a exchange_pub and @a exchange_sig are valid. */ unsigned int exchange_status; /** * HTTP error code from the exchange. */ enum TALER_ErrorCode exchange_code; /** * Fully reply from the exchange, only possibly set if * we got a JSON reply and a non-#MHD_HTTP_OK error code */ json_t *exchange_reply; }; /** * Closure for #process_refunds_cb. */ struct ProcessRefundData { /** * Must be first for #handle_mhd_completion_callback() cleanup * logic to work. */ struct TM_HandlerContext hc; /** * Hashed version of contract terms. */ struct GNUNET_HashCode h_contract_terms; /** * DLL of (suspended) requests. */ struct ProcessRefundData *next; /** * DLL of (suspended) requests. */ struct ProcessRefundData *prev; /** * Head of DLL of coin refunds for this request. */ struct CoinRefund *cr_head; /** * Tail of DLL of coin refunds for this request. */ struct CoinRefund *cr_tail; /** * Both public and private key are needed by the callback */ const struct MerchantInstance *merchant; /** * Connection we are handling. */ struct MHD_Connection *connection; /** * Did we suspend @a connection? */ int suspended; /** * Return code: #TALER_EC_NONE if successful. */ enum TALER_ErrorCode ec; }; /** * HEad of DLL of (suspended) requests. */ static struct ProcessRefundData *prd_head; /** * Tail of DLL of (suspended) requests. */ static struct ProcessRefundData *prd_tail; /** * Clean up memory in @a cls, the connection was closed. * * @param cls a `struct ProcessRefundData` to clean up. */ static void cleanup_prd (struct TM_HandlerContext *cls) { struct ProcessRefundData *prd = (struct ProcessRefundData *) cls; struct CoinRefund *cr; while (NULL != (cr = prd->cr_head)) { GNUNET_CONTAINER_DLL_remove (prd->cr_head, prd->cr_tail, cr); if (NULL != cr->fo) { TMH_EXCHANGES_find_exchange_cancel (cr->fo); cr->fo = NULL; } if (NULL != cr->rh) { TALER_EXCHANGE_refund_cancel (cr->rh); cr->rh = NULL; } if (NULL != cr->exchange_reply) { json_decref (cr->exchange_reply); cr->exchange_reply = NULL; } GNUNET_free (cr->exchange_url); GNUNET_free (cr); } GNUNET_free (prd); } /** * Check if @a prd has sub-activities still pending. * * @param prd request to check * @return #GNUNET_YES if activities are still pending */ static int prd_pending (struct ProcessRefundData *prd) { int pending = GNUNET_NO; for (struct CoinRefund *cr = prd->cr_head; NULL != cr; cr = cr->next) { if ( (NULL != cr->fo) || (NULL != cr->rh) ) { pending = GNUNET_YES; break; } } return pending; } /** * Check if @a prd is ready to be resumed, and if so, do it. * * @param prd refund request to be possibly ready */ static void check_resume_prd (struct ProcessRefundData *prd) { if (prd_pending (prd)) return; GNUNET_CONTAINER_DLL_remove (prd_head, prd_tail, prd); GNUNET_assert (prd->suspended); prd->suspended = GNUNET_NO; MHD_resume_connection (prd->connection); TMH_trigger_daemon (); } /** * Callbacks of this type are used to serve the result of submitting a * refund request to an exchange. * * @param cls a `struct CoinRefund` * @param hr HTTP response data * @param exchange_pub exchange key used to sign refund confirmation * @param exchange_sig exchange's signature over refund */ static void refund_cb (void *cls, const struct TALER_EXCHANGE_HttpResponse *hr, const struct TALER_ExchangePublicKeyP *exchange_pub, const struct TALER_ExchangeSignatureP *exchange_sig) { struct CoinRefund *cr = cls; cr->rh = NULL; cr->exchange_status = hr->http_status; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Exchange refund status for coin %s is %u\n", TALER_B2S (&cr->coin_pub), hr->http_status); if (MHD_HTTP_OK != hr->http_status) { cr->exchange_code = hr->ec; cr->exchange_reply = json_incref ((json_t*) hr->reply); } else { enum GNUNET_DB_QueryStatus qs; cr->exchange_pub = *exchange_pub; cr->exchange_sig = *exchange_sig; qs = db->put_refund_proof (db->cls, &cr->prd->merchant->pubkey, &cr->prd->h_contract_terms, &cr->coin_pub, cr->rtransaction_id, exchange_pub, exchange_sig); if (0 >= qs) { /* generally, this is relatively harmless for the merchant, but let's at least log this. */ GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Failed to persist exchange response to /refund in database: %d\n", qs); } } check_resume_prd (cr->prd); } /** * Function called with the result of a #TMH_EXCHANGES_find_exchange() * operation. * * @param cls a `struct CoinRefund *` * @param hr HTTP response details * @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_found_cb (void *cls, const struct TALER_EXCHANGE_HttpResponse *hr, struct TALER_EXCHANGE_Handle *eh, const struct TALER_Amount *wire_fee, int exchange_trusted) { struct CoinRefund *cr = cls; cr->fo = NULL; if (TALER_EC_NONE == hr->ec) { cr->rh = TALER_EXCHANGE_refund (eh, &cr->refund_amount, &cr->refund_fee, &cr->prd->h_contract_terms, &cr->coin_pub, cr->rtransaction_id, &cr->prd->merchant->privkey, &refund_cb, cr); return; } cr->exchange_status = hr->http_status; cr->exchange_code = hr->ec; cr->exchange_reply = json_incref ((json_t*) hr->reply); check_resume_prd (cr->prd); } /** * Function called with information about a refund. * It is responsible for packing up the data to return. * * @param cls closure * @param coin_pub public coin from which the refund comes from * @param exchange_url URL of the exchange that issued @a coin_pub * @param rtransaction_id identificator of the refund * @param reason human-readable explanation of the refund * @param refund_amount refund amount which is being taken from @a coin_pub * @param refund_fee cost of this refund operation */ static void process_refunds_cb (void *cls, const struct TALER_CoinSpendPublicKeyP *coin_pub, const char *exchange_url, uint64_t rtransaction_id, const char *reason, const struct TALER_Amount *refund_amount, const struct TALER_Amount *refund_fee) { struct ProcessRefundData *prd = cls; struct CoinRefund *cr; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Found refund of %s for coin %s with reason `%s' in database\n", TALER_B2S (coin_pub), TALER_amount2s (refund_amount), reason); cr = GNUNET_new (struct CoinRefund); cr->exchange_url = GNUNET_strdup (exchange_url); cr->prd = prd; cr->coin_pub = *coin_pub; cr->rtransaction_id = rtransaction_id; cr->refund_amount = *refund_amount; cr->refund_fee = *refund_fee; GNUNET_CONTAINER_DLL_insert (prd->cr_head, prd->cr_tail, cr); } /** * Force resuming all suspended refund lookups, needed during shutdown. */ void MH_force_refund_resume (void) { struct ProcessRefundData *prd; while (NULL != (prd = prd_head)) { GNUNET_CONTAINER_DLL_remove (prd_head, prd_tail, prd); GNUNET_assert (prd->suspended); prd->suspended = GNUNET_NO; MHD_resume_connection (prd->connection); } } /** * Return refund situation about a contract. * * @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 * @param mi merchant backend instance, never NULL * @return MHD result code */ MHD_RESULT MH_handler_refund_lookup (struct TMH_RequestHandler *rh, struct MHD_Connection *connection, void **connection_cls, const char *upload_data, size_t *upload_data_size, struct MerchantInstance *mi) { struct ProcessRefundData *prd; const char *order_id; json_t *contract_terms; enum GNUNET_DB_QueryStatus qs; prd = *connection_cls; if (NULL == prd) { order_id = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "order_id"); if (NULL == order_id) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_PARAMETER_MISSING, "order_id"); } /* Convert order id to h_contract_terms */ contract_terms = NULL; db->preflight (db->cls); qs = db->find_contract_terms (db->cls, &contract_terms, order_id, &mi->pubkey); if (0 > qs) { /* single, read-only SQL statements should never cause serialization problems */ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_REFUND_LOOKUP_DB_ERROR, "database error looking up order_id from merchant_contract_terms table"); } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Unknown order id given: `%s'\n", order_id); return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_REFUND_ORDER_ID_UNKNOWN, "order_id not found in database"); } prd = GNUNET_new (struct ProcessRefundData); if (GNUNET_OK != TALER_JSON_hash (contract_terms, &prd->h_contract_terms)) { GNUNET_break (0); json_decref (contract_terms); GNUNET_free (prd); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_INTERNAL_LOGIC_ERROR, "Could not hash contract terms"); } json_decref (contract_terms); prd->hc.cc = &cleanup_prd; prd->merchant = mi; prd->ec = TALER_EC_NONE; prd->connection = connection; *connection_cls = prd; for (unsigned int i = 0; iget_refunds_from_contract_terms_hash (db->cls, &mi->pubkey, &prd->h_contract_terms, &process_refunds_cb, prd); if (GNUNET_DB_STATUS_SOFT_ERROR != qs) break; } if (0 > qs) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Database hard error on refunds_from_contract_terms_hash lookup: %s\n", GNUNET_h2s (&prd->h_contract_terms)); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_REFUND_LOOKUP_DB_ERROR, "Failed to lookup refunds for contract"); } /* Now launch exchange interactions, unless we already have the response in the database! */ for (struct CoinRefund *cr = prd->cr_head; NULL != cr; cr = cr->next) { enum GNUNET_DB_QueryStatus qs; qs = db->get_refund_proof (db->cls, &cr->prd->merchant->pubkey, &cr->prd->h_contract_terms, &cr->coin_pub, cr->rtransaction_id, &cr->exchange_pub, &cr->exchange_sig); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) { /* We need to talk to the exchange */ cr->fo = TMH_EXCHANGES_find_exchange (cr->exchange_url, NULL, GNUNET_NO, &exchange_found_cb, cr); } } } /* Check if there are still exchange operations pending */ if (GNUNET_YES == prd_pending (prd)) { if (! prd->suspended) { prd->suspended = GNUNET_YES; MHD_suspend_connection (connection); GNUNET_CONTAINER_DLL_insert (prd_head, prd_tail, prd); } return MHD_YES; /* we're still talking to the exchange */ } /* All operations done, build final response */ if (NULL == prd->cr_head) { /* There ARE no refunds scheduled, bitch */ return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_REFUND_LOOKUP_NO_REFUND, "This contract is not currently eligible for refunds"); } { json_t *ra; ra = json_array (); GNUNET_assert (NULL != ra); for (struct CoinRefund *cr = prd->cr_head; NULL != cr; cr = cr->next) { GNUNET_assert ( 0 == json_array_append_new ( ra, (MHD_HTTP_OK != cr->exchange_status) ? json_pack ((NULL != cr->exchange_reply) ? "{s:o,s:o,s:o,s:I,s:I,s:I,s:O}" : "{s:o,s:o,s:o,s:I,s:I:s:I}", "coin_pub", GNUNET_JSON_from_data_auto (&cr->coin_pub), "refund_amount", TALER_JSON_from_amount (&cr->refund_amount), "refund_fee", TALER_JSON_from_amount (&cr->refund_fee), "exchange_http_status", (json_int_t) cr->exchange_status, "rtransaction_id", (json_int_t) cr->rtransaction_id, "exchange_code", (json_int_t) cr->exchange_code, "exchange_reply", cr->exchange_reply) : json_pack ("{s:o,s:o,s:o,s:I,s:I,s:o,s:o}", "coin_pub", GNUNET_JSON_from_data_auto (&cr->coin_pub), "refund_amount", TALER_JSON_from_amount (&cr->refund_amount), "refund_fee", TALER_JSON_from_amount (&cr->refund_fee), "exchange_http_status", (json_int_t) cr->exchange_status, "rtransaction_id", (json_int_t) cr->rtransaction_id, "exchange_pub", GNUNET_JSON_from_data_auto (&cr->exchange_pub), "exchange_sig", GNUNET_JSON_from_data_auto (&cr->exchange_sig) ))); } return TALER_MHD_reply_json_pack ( connection, MHD_HTTP_OK, "{s:o, s:o, s:o}", "refunds", ra, "merchant_pub", GNUNET_JSON_from_data_auto (&mi->pubkey), "h_contract_terms", GNUNET_JSON_from_data_auto (&prd->h_contract_terms)); } } /* end of taler-merchant-httpd_refund_lookup.c */