/* This file is part of TALER (C) 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 taler-merchant-httpd_post-orders-ID-refund.c * @brief handling of POST /orders/$ID/refund requests * @author Jonathan Buchanan */ #include "platform.h" #include #include #include #include "taler-merchant-httpd_auditors.h" #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_post-orders-ID-refund.h" /** * 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; /** * Request this operation is part of. */ struct PostRefundData *prd; /** * URL of the exchange for this @e coin_pub. */ char *exchange_url; /** * 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; /** * When did the merchant grant the refund. To be used to group events * in the wallet. */ struct GNUNET_TIME_Absolute execution_time; /** * Coin to refund. */ struct TALER_CoinSpendPublicKeyP coin_pub; /** * Refund transaction ID to use. */ uint64_t rtransaction_id; /** * Unique serial number identifying the refund. */ uint64_t refund_serial; /** * Amount to refund. */ struct TALER_Amount refund_amount; /** * 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; }; /** * Context for the operation. */ struct PostRefundData { /** * Hashed version of contract terms. All zeros if not provided. */ struct GNUNET_HashCode h_contract_terms; /** * DLL of (suspended) requests. */ struct PostRefundData *next; /** * DLL of (suspended) requests. */ struct PostRefundData *prev; /** * Refunds for this order. Head of DLL. */ struct CoinRefund *cr_head; /** * Refunds for this order. Tail of DLL. */ struct CoinRefund *cr_tail; /** * Context of the request. */ struct TMH_HandlerContext *hc; /** * Entry in the #resume_timeout_heap for this check payment, if we are * suspended. */ struct TMH_SuspendedConnection sc; /** * Which merchant instance is this for? */ struct MerchantInstance *mi; /** * order ID for the payment */ const char *order_id; /** * Where to get the contract */ const char *contract_url; /** * fulfillment URL of the contract (valid as long as * @e contract_terms is valid). */ const char *fulfillment_url; /** * session of the client */ const char *session_id; /** * Contract terms of the payment we are checking. NULL when they * are not (yet) known. */ json_t *contract_terms; /** * Total refunds granted for this payment. Only initialized * if @e refunded is set to true. */ struct TALER_Amount refund_amount; /** * Did we suspend @a connection? */ bool suspended; /** * Return code: #TALER_EC_NONE if successful. */ enum TALER_ErrorCode ec; /** * HTTP status to use for the reply, 0 if not yet known. */ unsigned int http_status; /** * Set to true if we are dealing with an unclaimed order * (and thus @e h_contract_terms is not set, and certain * DB queries will not work). */ bool unclaimed; /** * Set to true if this payment has been refunded and * @e refund_amount is initialized. */ bool refunded; /** * Set to true if a refund is still available for the * wallet for this payment. */ bool refund_available; /** * Set to true if the client requested HTML, otherwise * we generate JSON. */ bool generate_html; }; /** * Head of DLL of (suspended) requests. */ static struct PostRefundData *prd_head; /** * Tail of DLL of (suspended) requests. */ static struct PostRefundData *prd_tail; /** * Function called when we are done processing a refund request. * Frees memory associated with @a ctx. * * @param ctx a `struct PostRefundData` */ static void refund_cleanup (void *ctx) { struct PostRefundData *prd = ctx; struct CoinRefund *cr; while (NULL != (cr = prd->cr_head)) { GNUNET_CONTAINER_DLL_remove (prd->cr_head, prd->cr_tail, cr); json_decref (cr->exchange_reply); GNUNET_free (cr->exchange_url); 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; } GNUNET_free (cr); } json_decref (prd->contract_terms); GNUNET_free (prd); } /** * Force resuming all suspended order lookups, needed during shutdown. */ void TMH_force_wallet_refund_order_resume (void) { struct PostRefundData *prd; while (NULL != (prd = prd_head)) { GNUNET_CONTAINER_DLL_remove (prd_head, prd_tail, prd); GNUNET_assert (prd->suspended); prd->suspended = false; MHD_resume_connection (prd->sc.con); } } /** * Check if @a prd has exchange requests still pending. * * @param prd state to check * @return true if activities are still pending */ static bool exchange_operations_pending (struct PostRefundData *prd) { for (struct CoinRefund *cr = prd->cr_head; NULL != cr; cr = cr->next) { if ( (NULL != cr->fo) || (NULL != cr->rh) ) return true; } return false; } /** * 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 PostRefundData *prd) { if ( (TALER_EC_NONE == prd->ec) && exchange_operations_pending (prd) ) return; GNUNET_CONTAINER_DLL_remove (prd_head, prd_tail, prd); GNUNET_assert (prd->suspended); prd->suspended = false; MHD_resume_connection (prd->sc.con); 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 = TMH_db->insert_refund_proof (TMH_db->cls, cr->refund_serial, exchange_sig, exchange_pub); 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 payto_uri payto://-URI of the exchange * @param wire_fee current applicable wire fee for dealing with @a eh, NULL if not available * @param exchange_trusted true 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 char *payto_uri, const struct TALER_Amount *wire_fee, bool exchange_trusted) { struct CoinRefund *cr = cls; struct PostRefundData *prd = cr->prd; (void) payto_uri; (void) wire_fee; (void) exchange_trusted; cr->fo = NULL; if (NULL == hr) { prd->http_status = MHD_HTTP_GATEWAY_TIMEOUT; prd->ec = TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT; check_resume_prd (prd); return; } if (NULL == eh) { prd->http_status = MHD_HTTP_BAD_GATEWAY; prd->ec = TALER_EC_MERCHANT_GENERIC_EXCHANGE_CONNECT_FAILURE; check_resume_prd (prd); return; } cr->rh = TALER_EXCHANGE_refund (eh, &cr->refund_amount, &prd->h_contract_terms, &cr->coin_pub, cr->rtransaction_id, &prd->hc->instance->merchant_priv, &refund_cb, cr); } /** * Function called with information about a refund. * It is responsible for summing up the refund amount. * * @param cls closure * @param refund_serial unique serial number of the refund * @param timestamp time of the refund (for grouping of refunds in the wallet UI) * @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 pending true if the this refund was not yet processed by the wallet/exchange */ static void process_refunds_cb (void *cls, uint64_t refund_serial, struct GNUNET_TIME_Absolute timestamp, const struct TALER_CoinSpendPublicKeyP *coin_pub, const char *exchange_url, uint64_t rtransaction_id, const char *reason, const struct TALER_Amount *refund_amount, bool pending) { struct PostRefundData *prd = cls; struct CoinRefund *cr; for (cr = prd->cr_head; NULL != cr; cr = cr->next) if (cr->refund_serial == refund_serial) return; /* already known */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Found refund of %s for coin %s with reason `%s' in database\n", TALER_amount2s (refund_amount), TALER_B2S (coin_pub), reason); cr = GNUNET_new (struct CoinRefund); cr->refund_serial = refund_serial; 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->execution_time = timestamp; GNUNET_CONTAINER_DLL_insert (prd->cr_head, prd->cr_tail, cr); if (prd->refunded) { GNUNET_assert (0 <= TALER_amount_add (&prd->refund_amount, &prd->refund_amount, refund_amount)); return; } prd->refund_amount = *refund_amount; prd->refunded = true; prd->refund_available |= pending; } /** * Obtain refunds for an order. * * @param rh context of the handler * @param connection the MHD connection to handle * @param[in,out] hc context with further information about the request * @return MHD result code */ MHD_RESULT TMH_post_orders_ID_refund (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { struct PostRefundData *prd = hc->ctx; enum GNUNET_DB_QueryStatus qs; if (NULL == prd) { prd = GNUNET_new (struct PostRefundData); prd->sc.con = connection; prd->hc = hc; prd->order_id = hc->infix; hc->ctx = prd; hc->cc = &refund_cleanup; { enum GNUNET_GenericReturnValue res; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("h_contract", &prd->h_contract_terms), GNUNET_JSON_spec_end () }; res = TALER_MHD_parse_json_data (connection, hc->request_body, spec); if (GNUNET_OK != res) return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } TMH_db->preflight (TMH_db->cls); { json_t *contract_terms; uint64_t order_serial; qs = TMH_db->lookup_contract_terms (TMH_db->cls, hc->instance->settings.id, hc->infix, &contract_terms, &order_serial); 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_GENERIC_DB_FETCH_FAILED, "contract terms"); } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { json_decref (contract_terms); return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, hc->infix); } { struct GNUNET_HashCode h_contract_terms; if (GNUNET_OK != TALER_JSON_contract_hash (contract_terms, &h_contract_terms)) { GNUNET_break (0); json_decref (contract_terms); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, NULL); } json_decref (contract_terms); if (0 != GNUNET_memcmp (&h_contract_terms, &prd->h_contract_terms)) { return TALER_MHD_reply_with_error ( connection, MHD_HTTP_FORBIDDEN, TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, NULL); } } } } if (TALER_EC_NONE != prd->ec) { GNUNET_break (0 != prd->http_status); /* kill pending coin refund operations immediately, just to be extra sure they don't modify 'prd' after we already created a reply (this might not be needed, but feels safer). */ for (struct CoinRefund *cr = prd->cr_head; NULL != cr; cr = cr->next) { 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; } } return TALER_MHD_reply_with_error (connection, prd->http_status, prd->ec, NULL); } { GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TMH_currency, &prd->refund_amount)); qs = TMH_db->lookup_refunds_detailed (TMH_db->cls, hc->instance->settings.id, &prd->h_contract_terms, &process_refunds_cb, prd); if (0 > qs) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "detailed refunds"); } } /* 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 = TMH_db->lookup_refund_proof (TMH_db->cls, cr->refund_serial, &cr->exchange_sig, &cr->exchange_pub); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: case GNUNET_DB_STATUS_SOFT_ERROR: return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "refund proof"); case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: if (NULL == cr->exchange_reply) { /* We need to talk to the exchange */ /* Notify clients waiting for the refund to be obtained. */ TMH_long_poll_resume (hc->infix, hc->instance, &prd->refund_amount, true); cr->fo = TMH_EXCHANGES_find_exchange (cr->exchange_url, NULL, GNUNET_NO, &exchange_found_cb, cr); } break; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: /* We got a reply earlier, set status accordingly */ cr->exchange_status = MHD_HTTP_OK; break; } } /* Check if there are still exchange operations pending */ if (exchange_operations_pending (prd)) { if (! prd->suspended) { prd->suspended = true; MHD_suspend_connection (connection); GNUNET_CONTAINER_DLL_insert (prd_head, prd_tail, prd); } return MHD_YES; /* we're still talking to the exchange */ } { json_t *ra; ra = json_array (); GNUNET_assert (NULL != ra); for (struct CoinRefund *cr = prd->cr_head; NULL != cr; cr = cr->next) { json_t *refund; if (MHD_HTTP_OK != cr->exchange_status) { if (NULL == cr->exchange_reply) { refund = json_pack ("{s:s,s:I,s:I,s:o,s:o,s:o}", "type", "failure", "exchange_status", (json_int_t) cr->exchange_status, "rtransaction_id", (json_int_t) cr->rtransaction_id, "coin_pub", GNUNET_JSON_from_data_auto (&cr->coin_pub), "refund_amount", TALER_JSON_from_amount (&cr->refund_amount), "execution_time", GNUNET_JSON_from_time_abs (cr->execution_time)); } else { refund = json_pack ("{s:s,s:I,s:I,s:O,s:I,s:o,s:o,s:o}", "type", "failure", "exchange_status", (json_int_t) cr->exchange_status, "exchange_code", (json_int_t) cr->exchange_code, "exchange_reply", cr->exchange_reply, "rtransaction_id", (json_int_t) cr->rtransaction_id, "coin_pub", GNUNET_JSON_from_data_auto (&cr->coin_pub), "refund_amount", TALER_JSON_from_amount (&cr->refund_amount), "execution_time", GNUNET_JSON_from_time_abs (cr->execution_time)); } } else { refund = json_pack ("{s:s,s:I,s:o,s:o,s:I,s:o,s:o,s:o}", "type", "success", "exchange_status", (json_int_t) cr->exchange_status, "exchange_sig", GNUNET_JSON_from_data_auto (&cr->exchange_sig), "exchange_pub", GNUNET_JSON_from_data_auto (&cr->exchange_pub), "rtransaction_id", (json_int_t) cr->rtransaction_id, "coin_pub", GNUNET_JSON_from_data_auto (&cr->coin_pub), "refund_amount", TALER_JSON_from_amount (&cr->refund_amount), "execution_time", GNUNET_JSON_from_time_abs (cr->execution_time)); } GNUNET_assert ( 0 == json_array_append_new (ra, refund)); } return TALER_MHD_reply_json_pack ( connection, MHD_HTTP_OK, "{s:o, s:o, s:o}", "refund_amount", TALER_JSON_from_amount (&prd->refund_amount), "refunds", ra, "merchant_pub", GNUNET_JSON_from_data_auto (&hc->instance->merchant_pub)); } return MHD_YES; } /* end of taler-merchant-httpd_post-orders-ID-refund.c */