diff options
author | Jonathan Buchanan <jonathan.russ.buchanan@gmail.com> | 2020-08-16 02:42:03 -0400 |
---|---|---|
committer | Jonathan Buchanan <jonathan.russ.buchanan@gmail.com> | 2020-08-16 02:42:49 -0400 |
commit | 84d79e5c8eda85d4dc2af6528de19a35350ad60e (patch) | |
tree | 38d109bda1d7f1b9fed8ca44a7834aa6fc205bcf | |
parent | 5e9a041c084f70c7bb80d13b960402d30cd5e6fe (diff) |
early stages of implementing POST /orders/$ORDER_ID/refund
-rw-r--r-- | src/backend/Makefile.am | 2 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd.c | 12 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_post-orders-ID-refund.c | 704 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_post-orders-ID-refund.h | 40 |
4 files changed, 758 insertions, 0 deletions
diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am index 8e99993f..af86034b 100644 --- a/src/backend/Makefile.am +++ b/src/backend/Makefile.am @@ -87,6 +87,8 @@ taler_merchant_httpd_SOURCES = \ taler-merchant-httpd_post-orders-ID-pay.h \ taler-merchant-httpd_post-orders-ID-paid.c \ taler-merchant-httpd_post-orders-ID-paid.h \ + taler-merchant-httpd_post-orders-ID-refund.c \ + taler-merchant-httpd_post-orders-ID-refund.h \ taler-merchant-httpd_post-tips-ID-pickup.c \ taler-merchant-httpd_post-tips-ID-pickup.h \ taler-merchant-httpd_qr.c \ diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c index cf149d27..f65b4292 100644 --- a/src/backend/taler-merchant-httpd.c +++ b/src/backend/taler-merchant-httpd.c @@ -60,6 +60,7 @@ #include "taler-merchant-httpd_post-orders-ID-claim.h" #include "taler-merchant-httpd_post-orders-ID-paid.h" #include "taler-merchant-httpd_post-orders-ID-pay.h" +#include "taler-merchant-httpd_post-orders-ID-refund.h" #include "taler-merchant-httpd_post-tips-ID-pickup.h" #include "taler-merchant-httpd_reserves.h" #include "taler-merchant-httpd_templating.h" @@ -1067,6 +1068,17 @@ url_handler (void *cls, to set a conservative bound for sane wallets */ .max_upload = 1024 * 1024 }, + /* POST /orders/$ID/refund: */ + { + .url_prefix = "/orders/", + .have_id_segment = true, + .url_suffix = "refund", + .method = MHD_HTTP_METHOD_POST, + .handler = &TMH_post_orders_ID_refund, + /* the body should be pretty small, allow 1 MB of upload + to set a conservative bound for sane wallets */ + .max_upload = 1024 * 1024 + }, /* GET /orders/$ID: */ { .url_prefix = "/orders/", diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-refund.c b/src/backend/taler-merchant-httpd_post-orders-ID-refund.c new file mode 100644 index 00000000..bfdb6ca2 --- /dev/null +++ b/src/backend/taler-merchant-httpd_post-orders-ID-refund.c @@ -0,0 +1,704 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ + +/** + * @file backend/taler-merchant-httpd_post-orders-ID-refund.c + * @brief handling of POST /orders/$ID/refund requests + * @author Jonathan Buchanan + */ +#include "platform.h" +#include <taler/taler_signatures.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_exchange_service.h> +#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; + + /** + * 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; + + +/* FIXME: Handle shutdown and other events that require ending all requests */ + + +/** + * 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 (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; + + (void) payto_uri; + cr->fo = NULL; + if (TALER_EC_NONE == hr->ec) + { + cr->rh = TALER_EXCHANGE_refund (eh, + &cr->refund_amount, + &cr->prd->h_contract_terms, + &cr->coin_pub, + cr->rtransaction_id, + &cr->prd->hc->instance->merchant_priv, + &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 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 timestamp when was the refund made + * @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; + + 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; + { + 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_GET_ORDERS_DB_FETCH_CONTRACT_TERMS_ERROR, + "db error fetching 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_GET_ORDERS_ORDER_NOT_FOUND, + "Did not find contract terms for order in DB"); + } + { + 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_GET_ORDERS_FAILED_COMPUTE_PROPOSAL_HASH, + "Failed to hash contract terms"); + } + 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_GET_ORDERS_FAILED_COMPUTE_PROPOSAL_HASH, + ""); + } + } + } + } + { + 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_GET_ORDERS_DB_LOOKUP_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 = 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_GET_ORDERS_DB_LOOKUP_ERROR, + "Merchant database error"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* We need to talk to the exchange */ + /* FIXME: notify clients polling for this to happen */ + 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 */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-refund.h b/src/backend/taler-merchant-httpd_post-orders-ID-refund.h new file mode 100644 index 00000000..8bf0a6ee --- /dev/null +++ b/src/backend/taler-merchant-httpd_post-orders-ID-refund.h @@ -0,0 +1,40 @@ +/* + 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 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 backend/taler-merchant-httpd_post-orders-ID-refund.h + * @brief headers for POST /orders/$ID/refund handler + * @author Jonathan Buchanan + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_REFUND_H +#define TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_REFUND_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + + +/** + * 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); + +#endif |