From 20853863335ee4cd6a43044bfa1c8b38361cbc1f Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Mon, 27 Apr 2020 04:53:17 +0200 Subject: rename fest to match new structure --- .../taler-merchant-httpd_private-get-orders-ID.c | 591 +++++++++++++++++++++ 1 file changed, 591 insertions(+) create mode 100644 src/backend/taler-merchant-httpd_private-get-orders-ID.c (limited to 'src/backend/taler-merchant-httpd_private-get-orders-ID.c') diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.c b/src/backend/taler-merchant-httpd_private-get-orders-ID.c new file mode 100644 index 00000000..bb5384d1 --- /dev/null +++ b/src/backend/taler-merchant-httpd_private-get-orders-ID.c @@ -0,0 +1,591 @@ +/* + This file is part of TALER + (C) 2017, 2019 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 +*/ +/** + * @file backend/taler-merchant-httpd_check-payment.c + * @brief implementation of /check-payment handler + * @author Florian Dold + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include +#include +#include +#include +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_mhd.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_check-payment.h" + +/** + * Maximum number of retries for database operations. + */ +#define MAX_RETRIES 5 + + +/** + * Data structure we keep for a check payment request. + */ +struct CheckPaymentRequestContext +{ + /** + * Must be first for #handle_mhd_completion_callback. + */ + struct TM_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; + + /** + * URL where the final contract can be found for this payment. + */ + char *final_contract_url; + + /** + * order ID for the payment + */ + const char *order_id; + + /** + * Where to get the contract + */ + const char *contract_url; + + /** + * session of the client + */ + const char *session_id; + + /** + * fulfillment URL of the contract (valid as long as + * @e contract_terms is valid). + */ + const char *fulfillment_url; + + /** + * Contract terms of the payment we are checking. NULL when they + * are not (yet) known. + */ + json_t *contract_terms; + + /** + * Hash of @e contract_terms, set only once @e contract_terms + * is available. + */ + struct GNUNET_HashCode h_contract_terms; + + /** + * Total refunds granted for this payment. Only initialized + * if @e refunded is set to #GNUNET_YES. + */ + struct TALER_Amount refund_amount; + + /** + * Set to #GNUNET_YES if this payment has been refunded and + * @e refund_amount is initialized. + */ + int refunded; + + /** + * Initially #GNUNET_SYSERR. If we queued a response, set to the + * result code (i.e. #MHD_YES or #MHD_NO). + */ + int ret; + +}; + + +/** + * Clean up the session state for a check payment request. + * + * @param hc must be a `struct CheckPaymentRequestContext *` + */ +static void +cprc_cleanup (struct TM_HandlerContext *hc) +{ + struct CheckPaymentRequestContext *cprc = (struct + CheckPaymentRequestContext *) hc; + + if (NULL != cprc->contract_terms) + json_decref (cprc->contract_terms); + GNUNET_free_non_null (cprc->final_contract_url); + GNUNET_free (cprc); +} + + +/** + * Function called with information about a refund. + * It is responsible for summing up the refund amount. + * + * @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 CheckPaymentRequestContext *cprc = cls; + + if (cprc->refunded) + { + GNUNET_assert (0 <= + TALER_amount_add (&cprc->refund_amount, + &cprc->refund_amount, + refund_amount)); + return; + } + cprc->refund_amount = *refund_amount; + cprc->refunded = GNUNET_YES; +} + + +/** + * The client did not yet pay, send it the payment request. + * + * @param cprc check pay request context + * @return #MHD_YES on success + */ +static int +send_pay_request (struct CheckPaymentRequestContext *cprc) +{ + int ret; + char *already_paid_order_id = NULL; + char *taler_pay_uri; + struct GNUNET_TIME_Relative remaining; + + remaining = GNUNET_TIME_absolute_get_remaining (cprc->sc.long_poll_timeout); + if (0 != remaining.rel_value_us) + { + /* long polling: do not queue a response, suspend connection instead */ + TMH_compute_pay_key (cprc->order_id, + &cprc->mi->pubkey, + &cprc->sc.key); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending /check-payment on key %s\n", + GNUNET_h2s (&cprc->sc.key)); + TMH_long_poll_suspend (&cprc->sc, + NULL); + return MHD_YES; + } + + /* Check if resource_id has been paid for in the same session + * with another order_id. + */ + if ( (NULL != cprc->session_id) && + (NULL != cprc->fulfillment_url) ) + { + enum GNUNET_DB_QueryStatus qs; + + qs = db->find_session_info (db->cls, + &already_paid_order_id, + cprc->session_id, + cprc->fulfillment_url, + &cprc->mi->pubkey); + if (qs < 0) + { + /* 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 (cprc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_CHECK_PAYMENT_DB_FETCH_ORDER_ERROR, + "db error fetching pay session info"); + } + } + taler_pay_uri = TMH_make_taler_pay_uri (cprc->sc.con, + cprc->order_id, + cprc->session_id, + cprc->mi->id); + ret = TALER_MHD_reply_json_pack (cprc->sc.con, + MHD_HTTP_OK, + "{s:s, s:s, s:b, s:s?}", + "taler_pay_uri", taler_pay_uri, + "contract_url", cprc->final_contract_url, + "paid", 0, + "already_paid_order_id", + already_paid_order_id); + GNUNET_free (taler_pay_uri); + GNUNET_free_non_null (already_paid_order_id); + return ret; +} + + +/** + * Parse the "contract_terms" in @a cprc and set the + * "fulfillment_url" and the "h_contract_terms" in @a cprc + * accordingly. + * + * On errors, the response is being queued and the status + * code set in @cprc "ret". + * + * @param cprc[in,out] context to process + * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure + */ +static int +parse_contract_terms (struct CheckPaymentRequestContext *cprc) +{ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("fulfillment_url", + &cprc->fulfillment_url), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (cprc->contract_terms, + spec, + NULL, NULL)) + { + GNUNET_break (0); + cprc->ret + = TALER_MHD_reply_with_error (cprc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_CHECK_PAYMENT_DB_FETCH_CONTRACT_TERMS_ERROR, + "Merchant database error (contract terms corrupted)"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_JSON_hash (cprc->contract_terms, + &cprc->h_contract_terms)) + { + GNUNET_break (0); + cprc->ret + = TALER_MHD_reply_with_error (cprc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_CHECK_PAYMENT_FAILED_COMPUTE_PROPOSAL_HASH, + "Failed to hash proposal"); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Check that we are aware of @a order_id and if so request the payment, + * otherwise generate an error response. + * + * @param cprc session state + * @return status code to return to MHD for @a connection + */ +static int +check_order_and_request_payment (struct CheckPaymentRequestContext *cprc) +{ + enum GNUNET_DB_QueryStatus qs; + + if (NULL != cprc->contract_terms) + { + /* This should never happen. */ + GNUNET_break (0); + json_decref (cprc->contract_terms); + cprc->contract_terms = NULL; + } + qs = db->find_order (db->cls, + &cprc->contract_terms, + cprc->order_id, + &cprc->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 (cprc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_CHECK_PAYMENT_DB_FETCH_ORDER_ERROR, + "db error fetching order"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error (cprc->sc.con, + MHD_HTTP_NOT_FOUND, + TALER_EC_CHECK_PAYMENT_ORDER_ID_UNKNOWN, + "unknown order_id"); + } + + if (GNUNET_OK != + parse_contract_terms (cprc)) + return cprc->ret; + /* Offer was not picked up yet, but we ensured that it exists */ + return send_pay_request (cprc); +} + + +/** + * Manages a /check-payment call, checking the status + * of a payment and, if necessary, constructing the URL + * for a payment redirect URL. + * + * @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_check_payment (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi) +{ + struct CheckPaymentRequestContext *cprc = *connection_cls; + enum GNUNET_DB_QueryStatus qs; + MHD_RESULT ret; + + if (NULL == cprc) + { + const char *long_poll_timeout_s; + + /* First time here, parse request and check order is known */ + cprc = GNUNET_new (struct CheckPaymentRequestContext); + cprc->hc.cc = &cprc_cleanup; + cprc->ret = GNUNET_SYSERR; + cprc->sc.con = connection; + cprc->mi = mi; + *connection_cls = cprc; + + cprc->order_id = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "order_id"); + if (NULL == cprc->order_id) + { + /* order_id is required but missing */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "order_id required"); + } + cprc->contract_url = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "contract_url"); + if (NULL == cprc->contract_url) + { + cprc->final_contract_url = TALER_url_absolute_mhd (connection, + "/public/proposal", + "instance", mi->id, + "order_id", + cprc->order_id, + NULL); + GNUNET_assert (NULL != cprc->final_contract_url); + } + else + { + cprc->final_contract_url = GNUNET_strdup (cprc->contract_url); + } + cprc->session_id = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "session_id"); + long_poll_timeout_s = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "timeout"); + if (NULL != long_poll_timeout_s) + { + unsigned int timeout; + + if (1 != sscanf (long_poll_timeout_s, + "%u", + &timeout)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MALFORMED, + "timeout must be non-negative number"); + } + cprc->sc.long_poll_timeout + = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply ( + GNUNET_TIME_UNIT_SECONDS, + timeout)); + } + else + { + cprc->sc.long_poll_timeout = GNUNET_TIME_UNIT_ZERO_ABS; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting /check-payment processing with timeout %s\n", + GNUNET_STRINGS_absolute_time_to_string ( + cprc->sc.long_poll_timeout)); + } + if (NULL != cprc->contract_terms) + { + json_decref (cprc->contract_terms); + cprc->contract_terms = NULL; + } + db->preflight (db->cls); + qs = db->find_contract_terms (db->cls, + &cprc->contract_terms, + cprc->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_CHECK_PAYMENT_DB_FETCH_CONTRACT_TERMS_ERROR, + "db error fetching contract terms"); + } + + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* Check that we're at least aware of the order */ + return check_order_and_request_payment (cprc); + } + GNUNET_assert (NULL != cprc->contract_terms); + + if (GNUNET_OK != + parse_contract_terms (cprc)) + return cprc->ret; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Checkig payment status for order `%s' with contract %s\n", + cprc->order_id, + GNUNET_h2s (&cprc->h_contract_terms)); + GNUNET_assert (NULL != cprc->contract_terms); + + /* Check if the order has been paid for. */ + if (NULL != cprc->session_id) + { + /* Check if paid within a session. */ + char *already_paid_order_id = NULL; + + qs = db->find_session_info (db->cls, + &already_paid_order_id, + cprc->session_id, + cprc->fulfillment_url, + &mi->pubkey); + if (qs < 0) + { + /* 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_CHECK_PAYMENT_DB_FETCH_ORDER_ERROR, + "db error fetching pay session info"); + } + else if (0 == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order `%s' was not yet paid for session `%s'\n", + cprc->order_id, + cprc->session_id); + ret = send_pay_request (cprc); + GNUNET_free_non_null (already_paid_order_id); + return ret; + } + GNUNET_break (1 == qs); + GNUNET_break (0 == strcmp (cprc->order_id, + already_paid_order_id)); + GNUNET_free_non_null (already_paid_order_id); + } + else + { + /* Check if paid regardless of session. */ + json_t *xcontract_terms = NULL; + + qs = db->find_paid_contract_terms_from_hash (db->cls, + &xcontract_terms, + &cprc->h_contract_terms, + &mi->pubkey); + if (0 > 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_PAY_DB_FETCH_TRANSACTION_ERROR, + "Merchant database error"); + } + if (0 == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order `%s' (contract `%s') was not yet paid\n", + cprc->order_id, + GNUNET_h2s (&cprc->h_contract_terms)); + return send_pay_request (cprc); + } + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); + GNUNET_assert (NULL != xcontract_terms); + json_decref (xcontract_terms); + } + + /* Accumulate refunds, if any. */ + for (unsigned int i = 0; iget_refunds_from_contract_terms_hash (db->cls, + &mi->pubkey, + &cprc->h_contract_terms, + &process_refunds_cb, + cprc); + 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 (&cprc->h_contract_terms)); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR, + "Merchant database error"); + } + if (cprc->refunded) + return TALER_MHD_reply_json_pack (connection, + MHD_HTTP_OK, + "{s:O, s:b, s:b, s:o}", + "contract_terms", cprc->contract_terms, + "paid", 1, + "refunded", cprc->refunded, + "refund_amount", + TALER_JSON_from_amount ( + &cprc->refund_amount)); + return TALER_MHD_reply_json_pack (connection, + MHD_HTTP_OK, + "{s:O, s:b, s:b }", + "contract_terms", cprc->contract_terms, + "paid", 1, + "refunded", 0); +} -- cgit v1.2.3