diff options
Diffstat (limited to 'src/backend/taler-merchant-httpd_get-orders-ID.c')
-rw-r--r-- | src/backend/taler-merchant-httpd_get-orders-ID.c | 1967 |
1 files changed, 1055 insertions, 912 deletions
diff --git a/src/backend/taler-merchant-httpd_get-orders-ID.c b/src/backend/taler-merchant-httpd_get-orders-ID.c index 76dfefd5..53136628 100644 --- a/src/backend/taler-merchant-httpd_get-orders-ID.c +++ b/src/backend/taler-merchant-httpd_get-orders-ID.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2014-2022 Taler Systems SA + (C) 2014-2024 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 @@ -25,12 +25,13 @@ #include <taler/taler_signatures.h> #include <taler/taler_dbevents.h> #include <taler/taler_json_lib.h> +#include <taler/taler_templating_lib.h> #include <taler/taler_exchange_service.h> #include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_get-orders-ID.h" #include "taler-merchant-httpd_mhd.h" #include "taler-merchant-httpd_qr.h" -#include "taler-merchant-httpd_templating.h" /** * How often do we retry DB transactions on serialization failures? @@ -39,6 +40,25 @@ /** + * The different phases in which we handle the request. + */ +enum Phase +{ + GOP_INIT = 0, + GOP_LOOKUP_TERMS = 1, + GOP_PARSE_CONTRACT = 2, + GOP_CHECK_CLIENT_ACCESS = 3, + GOP_CHECK_PAID = 4, + GOP_REDIRECT_TO_PAID_ORDER = 5, + GOP_HANDLE_UNPAID = 6, + GOP_CHECK_REFUNDED = 7, + GOP_RETURN_STATUS = 8, + GOP_RETURN_MHD_YES = 9, + GOP_RETURN_MHD_NO = 10 +}; + + +/** * Context for the operation. */ struct GetOrderData @@ -119,6 +139,22 @@ struct GetOrderData json_t *contract_terms; /** + * Merchant base URL from @e contract_terms. + */ + const char *merchant_base_url; + + /** + * Public reorder URL from @e contract_terms. + * Could be NULL if contract does not have one. + */ + const char *public_reorder_url; + + /** + * Total amount in contract. + */ + struct TALER_Amount contract_total; + + /** * Total refunds granted for this payment. Only initialized * if @e refunded is set to true. */ @@ -131,6 +167,12 @@ struct GetOrderData struct TALER_Amount refund_taken; /** + * Phase in which we currently are handling this + * request. + */ + enum Phase phase; + + /** * Return code: #TALER_EC_NONE if successful. */ enum TALER_ErrorCode ec; @@ -145,6 +187,22 @@ struct GetOrderData enum GNUNET_GenericReturnValue suspended; /** + * Set to YES if refunded orders should be included when + * doing repurchase detection. + */ + enum TALER_EXCHANGE_YesNoAll allow_refunded_for_repurchase; + + /** + * Set to true if the client passed 'h_contract'. + */ + bool h_contract_provided; + + /** + * Set to true if the client passed a 'claim' token. + */ + bool claim_token_provided; + + /** * Set to true if we are dealing with a claimed order * (and thus @e h_contract_terms is set, otherwise certain * DB queries will not work). @@ -152,7 +210,12 @@ struct GetOrderData bool claimed; /** - * Set to true if this payment has been refunded and + * Set to true if this order was paid. + */ + bool paid; + + /** + * Set to true if this order has been refunded and * @e refund_amount is initialized. */ bool refunded; @@ -169,6 +232,35 @@ struct GetOrderData */ bool generate_html; + /** + * Did we parse the contract terms? + */ + bool contract_parsed; + + /** + * Set to true if the refunds found in the DB have + * a different currency then the main contract. + */ + bool bad_refund_currency_in_db; + + /** + * Did the hash of the contract match the contract + * hash supplied by the client? + */ + bool contract_match; + + /** + * True if we had a claim token and the claim token + * provided by the client matched our claim token. + */ + bool token_match; + + /** + * True if we found a (claimed) contract for the order, + * false if we had an unclaimed order. + */ + bool contract_available; + }; @@ -202,6 +294,112 @@ TMH_force_wallet_get_order_resume (void) /** + * Suspend this @a god until the trigger is satisfied. + * + * @param god request to suspend + */ +static void +suspend_god (struct GetOrderData *god) +{ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending GET /orders/%s\n", + god->order_id); + /* We reset the contract terms and start by looking them up + again, as while we are suspended fundamental things could + change (such as the contract being claimed) */ + if (NULL != god->contract_terms) + { + json_decref (god->contract_terms); + god->fulfillment_url = NULL; + god->contract_terms = NULL; + god->contract_parsed = false; + god->merchant_base_url = NULL; + god->public_reorder_url = NULL; + } + GNUNET_assert (! god->suspended); + god->contract_parsed = false; + god->contract_match = false; + god->token_match = false; + god->contract_available = false; + god->phase = GOP_LOOKUP_TERMS; + god->suspended = GNUNET_YES; + GNUNET_CONTAINER_DLL_insert (god_head, + god_tail, + god); + MHD_suspend_connection (god->sc.con); +} + + +/** + * Clean up the session state for a GET /orders/$ID request. + * + * @param cls must be a `struct GetOrderData *` + */ +static void +god_cleanup (void *cls) +{ + struct GetOrderData *god = cls; + + if (NULL != god->contract_terms) + { + json_decref (god->contract_terms); + god->contract_terms = NULL; + } + if (NULL != god->refund_eh) + { + TMH_db->event_listen_cancel (god->refund_eh); + god->refund_eh = NULL; + } + if (NULL != god->pay_eh) + { + TMH_db->event_listen_cancel (god->pay_eh); + god->pay_eh = NULL; + } + GNUNET_free (god); +} + + +/** + * Finish the request by returning @a mret as the + * final result. + * + * @param[in,out] god request we are processing + * @param mret MHD result to return + */ +static void +phase_end (struct GetOrderData *god, + MHD_RESULT mret) +{ + god->phase = (MHD_YES == mret) + ? GOP_RETURN_MHD_YES + : GOP_RETURN_MHD_NO; +} + + +/** + * Finish the request by returning an error @a ec + * with HTTP status @a http_status and @a message. + * + * @param[in,out] god request we are processing + * @param http_status HTTP status code to return + * @param ec error code to return + * @param message human readable hint to return, can be NULL + */ +static void +phase_fail (struct GetOrderData *god, + unsigned int http_status, + enum TALER_ErrorCode ec, + const char *message) +{ + phase_end (god, + TALER_MHD_reply_with_error (god->sc.con, + http_status, + ec, + message)); +} + + +/** * We have received a trigger from the database * that we should (possibly) resume the request. * @@ -252,8 +450,10 @@ resume_by_event (void *cls, { GNUNET_break (0); GNUNET_async_scope_restore (&old); + GNUNET_free (as); return; } + GNUNET_free (as); if (GNUNET_OK != TALER_amount_cmp_currency (&god->sc.refund_expected, &a)) @@ -288,261 +488,385 @@ resume_by_event (void *cls, /** - * Suspend this @a god until the trigger is satisfied. + * First phase (after request parsing). + * Set up long-polling. * - * @param god request to suspend + * @param[in,out] god request context */ static void -suspend_god (struct GetOrderData *god) +phase_init (struct GetOrderData *god) { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Suspending GET /orders/%s\n", - god->order_id); - if (NULL != god->contract_terms) + god->phase++; + if (god->generate_html) + return; /* If HTML is requested, we never actually long poll. */ + if (! GNUNET_TIME_absolute_is_future (god->sc.long_poll_timeout)) + return; /* long polling not requested */ + + if (god->sc.awaiting_refund || + god->sc.awaiting_refund_obtained) { - json_decref (god->contract_terms); - god->fulfillment_url = NULL; - god->contract_terms = NULL; + struct TMH_OrderPayEventP refund_eh = { + .header.size = htons (sizeof (refund_eh)), + .header.type = htons (god->sc.awaiting_refund_obtained + ? TALER_DBEVENT_MERCHANT_REFUND_OBTAINED + : TALER_DBEVENT_MERCHANT_ORDER_REFUND), + .merchant_pub = god->hc->instance->merchant_pub + }; + + GNUNET_CRYPTO_hash (god->order_id, + strlen (god->order_id), + &refund_eh.h_order_id); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Subscribing %p to refunds on %s\n", + god, + god->order_id); + god->refund_eh + = TMH_db->event_listen ( + TMH_db->cls, + &refund_eh.header, + GNUNET_TIME_absolute_get_remaining ( + god->sc.long_poll_timeout), + &resume_by_event, + god); + } + { + struct TMH_OrderPayEventP pay_eh = { + .header.size = htons (sizeof (pay_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID), + .merchant_pub = god->hc->instance->merchant_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Subscribing to payments on %s\n", + god->order_id); + GNUNET_CRYPTO_hash (god->order_id, + strlen (god->order_id), + &pay_eh.h_order_id); + god->pay_eh + = TMH_db->event_listen ( + TMH_db->cls, + &pay_eh.header, + GNUNET_TIME_absolute_get_remaining ( + god->sc.long_poll_timeout), + &resume_by_event, + god); } - GNUNET_assert (! god->suspended); - god->suspended = GNUNET_YES; - GNUNET_CONTAINER_DLL_insert (god_head, - god_tail, - god); - MHD_suspend_connection (god->sc.con); } /** - * Create a taler://refund/ URI for the given @a con and @a order_id - * and @a instance_id. + * Lookup contract terms and check client has the + * right to access this order (by claim token or + * contract hash). * - * @param merchant_base_url URL to take host and path from; - * we cannot take it from the MHD connection as a browser - * may have changed 'http' to 'https' and we MUST be consistent - * with what the merchant's frontend used initially - * @param order_id the order id - * @return corresponding taler://refund/ URI, or NULL on missing "host" + * @param[in,out] god request context */ -static char * -make_taler_refund_uri (const char *merchant_base_url, - const char *order_id) +static void +phase_lookup_terms (struct GetOrderData *god) { - struct GNUNET_Buffer buf = { 0 }; - char *url; - struct GNUNET_Uri uri; + uint64_t order_serial; + struct TALER_ClaimTokenP db_claim_token; + enum GNUNET_DB_QueryStatus qs; - url = GNUNET_strdup (merchant_base_url); - if (-1 == GNUNET_uri_parse (&uri, - url)) + /* Convert order_id to h_contract_terms */ + TMH_db->preflight (TMH_db->cls); + GNUNET_assert (NULL == god->contract_terms); + qs = TMH_db->lookup_contract_terms ( + TMH_db->cls, + god->hc->instance->settings.id, + god->order_id, + &god->contract_terms, + &order_serial, + &db_claim_token); + 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 (0); - GNUNET_free (url); - return NULL; + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_contract_terms"); + return; } - GNUNET_assert (NULL != order_id); - GNUNET_buffer_write_str (&buf, - "taler"); - if (0 == strcasecmp ("http", - uri.scheme)) - GNUNET_buffer_write_str (&buf, - "+http"); - GNUNET_buffer_write_str (&buf, - "://refund/"); - GNUNET_buffer_write_str (&buf, - uri.host); - if (0 != uri.port) - GNUNET_buffer_write_fstr (&buf, - ":%u", - (unsigned int) uri.port); - if (NULL != uri.path) - GNUNET_buffer_write_path (&buf, - uri.path); - GNUNET_buffer_write_path (&buf, - order_id); - GNUNET_buffer_write_path (&buf, - ""); // Trailing slash - GNUNET_free (url); - return GNUNET_buffer_reap_str (&buf); -} + /* Note: when "!ord.requireClaimToken" and the client does not provide + a claim token (all zeros!), then token_match==TRUE below: */ + god->token_match + = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + && (0 == GNUNET_memcmp (&db_claim_token, + &god->claim_token)); - -char * -TMH_make_order_status_url (struct MHD_Connection *con, - const char *order_id, - const char *session_id, - const char *instance_id, - struct TALER_ClaimTokenP *claim_token, - struct TALER_PrivateContractHashP *h_contract) -{ - const char *host; - const char *forwarded_host; - const char *uri_path; - struct GNUNET_Buffer buf = { 0 }; - /* Number of query parameters written so far */ - unsigned int num_qp = 0; - - host = MHD_lookup_connection_value (con, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_HOST); - forwarded_host = MHD_lookup_connection_value (con, - MHD_HEADER_KIND, - "X-Forwarded-Host"); - uri_path = MHD_lookup_connection_value (con, - MHD_HEADER_KIND, - "X-Forwarded-Prefix"); - if (NULL != forwarded_host) - host = forwarded_host; - if (NULL == host) - { - GNUNET_break (0); - return NULL; - } - if (NULL != strchr (host, '/')) + /* Check if client provided the right hash code of the contract terms */ + if (NULL != god->contract_terms) { - GNUNET_break_op (0); - return NULL; - } - GNUNET_assert (NULL != instance_id); - GNUNET_assert (NULL != order_id); + god->contract_available = true; + if (GNUNET_YES == + GNUNET_is_zero (&god->h_contract_terms)) + { + if (GNUNET_OK != + TALER_JSON_contract_hash (god->contract_terms, + &god->h_contract_terms)) + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + "contract terms"); + return; + } + } + else + { + struct TALER_PrivateContractHashP h; - if (GNUNET_NO == TALER_mhd_is_https (con)) - GNUNET_buffer_write_str (&buf, - "http://"); - else - GNUNET_buffer_write_str (&buf, - "https://"); - GNUNET_buffer_write_str (&buf, - host); - if (NULL != uri_path) - GNUNET_buffer_write_path (&buf, - uri_path); - if (0 != strcmp ("default", - instance_id)) - { - GNUNET_buffer_write_path (&buf, - "instances"); - GNUNET_buffer_write_path (&buf, - instance_id); - } - GNUNET_buffer_write_path (&buf, - "/orders"); - GNUNET_buffer_write_path (&buf, - order_id); - if ((NULL != claim_token) && - (GNUNET_NO == GNUNET_is_zero (claim_token))) - { - /* 'token=' for human readability */ - GNUNET_buffer_write_str (&buf, - "?token="); - GNUNET_buffer_write_data_encoded (&buf, - (char *) claim_token, - sizeof (*claim_token)); - num_qp++; + if (GNUNET_OK != + TALER_JSON_contract_hash (god->contract_terms, + &h)) + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + "contract terms"); + return; + } + god->contract_match = (0 == + GNUNET_memcmp (&h, + &god->h_contract_terms)); + if (! god->contract_match) + { + GNUNET_break_op (0); + phase_fail (god, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, + NULL); + return; + } + } } - if (NULL != session_id) + if (god->contract_available) { - if (num_qp > 0) - GNUNET_buffer_write_str (&buf, - "&session_id="); - else - GNUNET_buffer_write_str (&buf, - "?session_id="); - GNUNET_buffer_write_str (&buf, - session_id); - num_qp++; + god->claimed = true; } - - if (NULL != h_contract) + else { - if (num_qp > 0) - GNUNET_buffer_write_str (&buf, - "&h_contract="); - else - GNUNET_buffer_write_str (&buf, - "?h_contract="); - GNUNET_buffer_write_data_encoded (&buf, - (char *) h_contract, - sizeof (*h_contract)); - } - - return GNUNET_buffer_reap_str (&buf); + struct TALER_ClaimTokenP db_claim_token; + struct TALER_MerchantPostDataHashP unused; + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_order ( + TMH_db->cls, + god->hc->instance->settings.id, + god->order_id, + &db_claim_token, + &unused, + (NULL == god->contract_terms) + ? &god->contract_terms + : NULL); + 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 (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_order"); + return; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Unknown order id given: `%s'\n", + god->order_id); + phase_fail (god, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, + god->order_id); + return; + } + /* Note: when "!ord.requireClaimToken" and the client does not provide + a claim token (all zeros!), then token_match==TRUE below: */ + god->token_match + = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) && + (0 == GNUNET_memcmp (&db_claim_token, + &god->claim_token)); + } /* end unclaimed order logic */ + god->phase++; } -char * -TMH_make_taler_pay_uri (struct MHD_Connection *con, - const char *order_id, - const char *session_id, - const char *instance_id, - struct TALER_ClaimTokenP *claim_token) +/** + * Parse contract terms. + * + * @param[in,out] god request context + */ +static void +phase_parse_contract (struct GetOrderData *god) { - const char *host; - const char *forwarded_host; - const char *uri_path; - struct GNUNET_Buffer buf = { 0 }; + struct GNUNET_JSON_Specification espec[] = { + TALER_JSON_spec_amount_any ("amount", + &god->contract_total), + TALER_JSON_spec_web_url ("merchant_base_url", + &god->merchant_base_url), + GNUNET_JSON_spec_mark_optional ( + /* this one does NOT have to be a Web URL! */ + GNUNET_JSON_spec_string ("fulfillment_url", + &god->fulfillment_url), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url ("public_reorder_url", + &god->public_reorder_url), + NULL), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + const char *ename; + unsigned int eline; - host = MHD_lookup_connection_value (con, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_HOST); - forwarded_host = MHD_lookup_connection_value (con, - MHD_HEADER_KIND, - "X-Forwarded-Host"); - uri_path = MHD_lookup_connection_value (con, - MHD_HEADER_KIND, - "X-Forwarded-Prefix"); - if (NULL != forwarded_host) - host = forwarded_host; - if (NULL == host) + GNUNET_assert (NULL != god->contract_terms); + if (god->contract_parsed) + return; /* not sure this is possible... */ + + res = GNUNET_JSON_parse (god->contract_terms, + espec, + &ename, + &eline); + if (GNUNET_OK != res) { GNUNET_break (0); - return NULL; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse contract %s in DB at field %s\n", + god->order_id, + ename); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + god->order_id); + return; } - if (NULL != strchr (host, '/')) + god->contract_parsed = true; + god->phase++; +} + + +/** + * Check that this order is unclaimed or claimed by + * this client. + * + * @param[in,out] god request context + */ +static void +phase_check_client_access (struct GetOrderData *god) +{ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Token match: %d, contract_available: %d, contract match: %d, claimed: %d\n", + god->token_match, + god->contract_available, + god->contract_match, + god->claimed); + + if (god->claim_token_provided && ! god->token_match) { + /* Authentication provided but wrong. */ GNUNET_break_op (0); - return NULL; + phase_fail (god, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN, + "authentication with claim token provided but wrong"); + return; } - GNUNET_assert (NULL != instance_id); - GNUNET_assert (NULL != order_id); - GNUNET_buffer_write_str (&buf, - "taler"); - if (GNUNET_NO == TALER_mhd_is_https (con)) - GNUNET_buffer_write_str (&buf, - "+http"); - GNUNET_buffer_write_str (&buf, - "://pay/"); - GNUNET_buffer_write_str (&buf, - host); - if (NULL != uri_path) - GNUNET_buffer_write_path (&buf, - uri_path); - if (0 != strcmp ("default", - instance_id)) + + if (god->h_contract_provided && ! god->contract_match) { - GNUNET_buffer_write_path (&buf, - "instances"); - GNUNET_buffer_write_path (&buf, - instance_id); + /* Authentication provided but wrong. */ + GNUNET_break_op (0); + phase_fail (god, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH, + NULL); + return; } - GNUNET_buffer_write_path (&buf, - order_id); - GNUNET_buffer_write_path (&buf, - (session_id == NULL) ? "" : session_id); - if ((NULL != claim_token) && - (GNUNET_NO == GNUNET_is_zero (claim_token))) + + if (! (god->token_match || + god->contract_match) ) { - /* Just 'c=' because this goes into QR - codes, so this is more compact. */ - GNUNET_buffer_write_str (&buf, - "?c="); - GNUNET_buffer_write_data_encoded (&buf, - (char *) claim_token, - sizeof (struct TALER_ClaimTokenP)); - } - return GNUNET_buffer_reap_str (&buf); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Neither claim token nor contract matched\n"); + /* Client has no rights to this order */ + if (NULL == god->public_reorder_url) + { + /* We cannot give the client a new order, just fail */ + if (! GNUNET_is_zero (&god->h_contract_terms)) + { + GNUNET_break_op (0); + phase_fail (god, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, + NULL); + return; + } + GNUNET_break_op (0); + phase_fail (god, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN, + "no 'public_reorder_url'"); + return; + } + /* We have a fulfillment URL, redirect the client there, maybe + the frontend can generate a fresh order for this new customer */ + if (god->generate_html) + { + /* Contract was claimed (maybe by another device), so this client + cannot get the status information. Redirect to fulfillment page, + where the client may be able to pickup a fresh order -- or might + be able authenticate via session ID */ + struct MHD_Response *reply; + MHD_RESULT ret; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Contract claimed, redirecting to fulfillment page for order %s\n", + god->order_id); + reply = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + if (NULL == reply) + { + GNUNET_break (0); + phase_end (god, + MHD_NO); + return; + } + GNUNET_break (MHD_YES == + MHD_add_response_header (reply, + MHD_HTTP_HEADER_LOCATION, + god->public_reorder_url)); + ret = MHD_queue_response (god->sc.con, + MHD_HTTP_FOUND, + reply); + MHD_destroy_response (reply); + phase_end (god, + ret); + return; + } + /* Need to generate JSON reply */ + phase_end (god, + TALER_MHD_REPLY_JSON_PACK ( + god->sc.con, + MHD_HTTP_ACCEPTED, + GNUNET_JSON_pack_string ("public_reorder_url", + god->public_reorder_url))); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Claim token or contract matched\n"); + god->phase++; } @@ -586,9 +910,9 @@ get_order_summary (const struct GetOrderData *god) * @param already_paid_order_id if for the fulfillment URI there is * already a paid order, this is the order ID to redirect * the wallet to; NULL if not applicable - * @return #MHD_YES on success + * @return true to exit due to suspension */ -static MHD_RESULT +static bool send_pay_request (struct GetOrderData *god, const char *already_paid_order_id) { @@ -605,7 +929,7 @@ send_pay_request (struct GetOrderData *god, GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Suspending request: long polling for payment\n"); suspend_god (god); - return MHD_YES; + return true; } /* Check if resource_id has been paid for in the same session @@ -613,27 +937,30 @@ send_pay_request (struct GetOrderData *god, */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Sending payment request\n"); - taler_pay_uri = TMH_make_taler_pay_uri (god->sc.con, - god->order_id, - god->session_id, - god->hc->instance->settings.id, - &god->claim_token); - order_status_url = TMH_make_order_status_url (god->sc.con, - god->order_id, - god->session_id, - god->hc->instance->settings.id, - &god->claim_token, - NULL); + taler_pay_uri = TMH_make_taler_pay_uri ( + god->sc.con, + god->order_id, + god->session_id, + god->hc->instance->settings.id, + &god->claim_token); + order_status_url = TMH_make_order_status_url ( + god->sc.con, + god->order_id, + god->session_id, + god->hc->instance->settings.id, + &god->claim_token, + NULL); if ( (NULL == taler_pay_uri) || (NULL == order_status_url) ) { GNUNET_break_op (0); GNUNET_free (taler_pay_uri); GNUNET_free (order_status_url); - return TALER_MHD_reply_with_error (god->sc.con, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, - "host"); + phase_fail (god, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, + "host"); + return false; } if (god->generate_html) { @@ -652,7 +979,9 @@ send_pay_request (struct GetOrderData *god, if (NULL == reply) { GNUNET_break (0); - return MHD_NO; + phase_end (god, + MHD_NO); + return false; } GNUNET_break (MHD_YES == MHD_add_response_header (reply, @@ -665,7 +994,9 @@ send_pay_request (struct GetOrderData *god, MHD_HTTP_FOUND, reply); MHD_destroy_response (reply); - return ret; + phase_end (god, + ret); + return false; } } @@ -676,7 +1007,9 @@ send_pay_request (struct GetOrderData *god, if (NULL == qr) { GNUNET_break (0); - return MHD_NO; + phase_end (god, + MHD_NO); + return false; } { enum GNUNET_GenericReturnValue res; @@ -691,12 +1024,13 @@ send_pay_request (struct GetOrderData *god, qr), GNUNET_JSON_pack_string ("order_summary", get_order_summary (god))); - res = TMH_return_from_template (god->sc.con, - MHD_HTTP_PAYMENT_REQUIRED, - "request_payment", - god->hc->instance->settings.id, - taler_pay_uri, - context); + res = TALER_TEMPLATING_reply ( + god->sc.con, + MHD_HTTP_PAYMENT_REQUIRED, + "request_payment", + god->hc->instance->settings.id, + taler_pay_uri, + context); if (GNUNET_SYSERR == res) { GNUNET_break (0); @@ -727,550 +1061,55 @@ send_pay_request (struct GetOrderData *god, } GNUNET_free (taler_pay_uri); GNUNET_free (order_status_url); - return ret; + phase_end (god, + ret); + return false; } /** - * Function called with detailed information about a refund. - * It is responsible for packing up the data to return. + * Check if the order has been paid. * - * @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 + * @param[in,out] god request context */ static void -process_refunds_cb (void *cls, - uint64_t refund_serial, - struct GNUNET_TIME_Timestamp 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) +phase_check_paid (struct GetOrderData *god) { - struct GetOrderData *god = cls; - - 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); - god->refund_pending |= pending; - if (!pending) - { - GNUNET_assert (0 <= - TALER_amount_add (&god->refund_taken, - &god->refund_taken, - refund_amount)); + enum GNUNET_DB_QueryStatus qs; + struct TALER_PrivateContractHashP h_contract; + + god->paid = false; + qs = TMH_db->lookup_order_status ( + TMH_db->cls, + god->hc->instance->settings.id, + god->order_id, + &h_contract, + &god->paid); + if (0 > qs) + { + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_order_status"); + return; } - GNUNET_assert (0 <= - TALER_amount_add (&god->refund_amount, - &god->refund_amount, - refund_amount)); - god->refunded = true; + god->phase++; } /** - * Clean up the session state for a GET /orders/$ID request. + * Check if the client already paid for an equivalent + * order under this session, and if so redirect to + * that order. * - * @param cls must be a `struct GetOrderData *` + * @param[in,out] god request context + * @return true to exit due to suspension */ -static void -god_cleanup (void *cls) -{ - struct GetOrderData *god = cls; - - if (NULL != god->contract_terms) - { - json_decref (god->contract_terms); - god->contract_terms = NULL; - } - if (NULL != god->refund_eh) - { - TMH_db->event_listen_cancel (god->refund_eh); - god->refund_eh = NULL; - } - if (NULL != god->pay_eh) - { - TMH_db->event_listen_cancel (god->pay_eh); - god->pay_eh = NULL; - } - GNUNET_free (god); -} - - -MHD_RESULT -TMH_get_orders_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) +static bool +phase_redirect_to_paid_order (struct GetOrderData *god) { - struct GetOrderData *god = hc->ctx; - const char *order_id = hc->infix; - enum GNUNET_DB_QueryStatus qs; - bool contract_match = false; - bool token_match = false; - bool h_contract_provided = false; - bool claim_token_provided = false; - bool contract_available = false; - const char *merchant_base_url; - - if (NULL == god) - { - god = GNUNET_new (struct GetOrderData); - hc->ctx = god; - hc->cc = &god_cleanup; - god->sc.con = connection; - god->hc = hc; - god->order_id = order_id; - god->generate_html = TMH_MHD_test_html_desired (connection); - - - /* first-time initialization / sanity checks */ - { - const char *cts; - - cts = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "h_contract"); - if ( (NULL != cts) && - (GNUNET_OK != - GNUNET_CRYPTO_hash_from_string (cts, - &god->h_contract_terms.hash)) ) - { - /* cts has wrong encoding */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "h_contract"); - } - if (NULL != cts) - h_contract_provided = true; - } - - { - const char *ct; - - ct = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "token"); - if ( (NULL != ct) && - (GNUNET_OK != - GNUNET_STRINGS_string_to_data (ct, - strlen (ct), - &god->claim_token, - sizeof (god->claim_token))) ) - { - /* ct has wrong encoding */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "token"); - } - if (NULL != ct) - claim_token_provided = true; - } - god->session_id = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "session_id"); - - /* process await_refund_obtained argument */ - { - const char *await_refund_obtained_s; - - await_refund_obtained_s = - MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "await_refund_obtained"); - god->sc.awaiting_refund_obtained = - (NULL != await_refund_obtained_s) - ? 0 == strcasecmp (await_refund_obtained_s, - "yes") - : false; - if (god->sc.awaiting_refund_obtained) - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Awaiting refund obtained\n"); - } - - { - const char *min_refund; - - min_refund = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "refund"); - if (NULL != min_refund) - { - if ( (GNUNET_OK != - TALER_string_to_amount (min_refund, - &god->sc.refund_expected)) || - (0 != strcasecmp (god->sc.refund_expected.currency, - TMH_currency) ) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "refund"); - } - god->sc.awaiting_refund = true; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Awaiting minimum refund of %s\n", - min_refund); - } - } - - - /* process timeout_ms argument */ - { - const char *long_poll_timeout_ms; - - long_poll_timeout_ms = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "timeout_ms"); - if (NULL != long_poll_timeout_ms) - { - unsigned int timeout_ms; - char dummy; - - if (1 != sscanf (long_poll_timeout_ms, - "%u%c", - &timeout_ms, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "timeout_ms (must be non-negative number)"); - } - /* If HTML is requested, we never long poll. Makes no sense */ - if (! god->generate_html) - { - struct GNUNET_TIME_Relative timeout; - - timeout = GNUNET_TIME_relative_multiply ( - GNUNET_TIME_UNIT_MILLISECONDS, - timeout_ms); - god->sc.long_poll_timeout - = GNUNET_TIME_relative_to_absolute (timeout); - if (! GNUNET_TIME_relative_is_zero (timeout)) - { - if (god->sc.awaiting_refund || - god->sc.awaiting_refund_obtained) - { - struct TMH_OrderPayEventP refund_eh = { - .header.size = htons (sizeof (refund_eh)), - .header.type = htons (god->sc.awaiting_refund_obtained - ? TALER_DBEVENT_MERCHANT_REFUND_OBTAINED - : TALER_DBEVENT_MERCHANT_ORDER_REFUND), - .merchant_pub = hc->instance->merchant_pub - }; - - GNUNET_CRYPTO_hash (god->order_id, - strlen (god->order_id), - &refund_eh.h_order_id); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Subscribing %p to refunds on %s\n", - god, - god->order_id); - god->refund_eh = TMH_db->event_listen (TMH_db->cls, - &refund_eh.header, - timeout, - &resume_by_event, - god); - } - { - struct TMH_OrderPayEventP pay_eh = { - .header.size = htons (sizeof (pay_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID), - .merchant_pub = hc->instance->merchant_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Subscribing to payments on %s\n", - god->order_id); - GNUNET_CRYPTO_hash (god->order_id, - strlen (god->order_id), - &pay_eh.h_order_id); - god->pay_eh = TMH_db->event_listen (TMH_db->cls, - &pay_eh.header, - timeout, - &resume_by_event, - god); - } - } /* end of timeout non-zero */ - } /* end of HTML generation NOT requested */ - } /* end of timeout_ms argument provided */ - } /* end of timeout_ms argument handling */ - - } /* end of first-time initialization / sanity checks */ - - if (GNUNET_SYSERR == god->suspended) - return MHD_NO; /* we are in shutdown */ - if (GNUNET_YES == god->suspended) - { - god->suspended = GNUNET_NO; - GNUNET_CONTAINER_DLL_remove (god_head, - god_tail, - god); - } - - /* Convert order_id to h_contract_terms */ - TMH_db->preflight (TMH_db->cls); - if (NULL == god->contract_terms) - { - uint64_t order_serial; - struct TALER_ClaimTokenP db_claim_token; - - qs = TMH_db->lookup_contract_terms (TMH_db->cls, - hc->instance->settings.id, - order_id, - &god->contract_terms, - &order_serial, - &db_claim_token); - 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 (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_contract_terms"); - } - - /* Note: when "!ord.requireClaimToken" and the client does not provide - a claim token (all zeros!), then token_match==TRUE below: */ - token_match = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - && (0 == GNUNET_memcmp (&db_claim_token, - &god->claim_token)); - } - - /* Check if client provided the right hash code of the contract terms */ - if (NULL != god->contract_terms) - { - contract_available = true; - - if (GNUNET_YES == GNUNET_is_zero (&god->h_contract_terms)) - { - - if (GNUNET_OK != - TALER_JSON_contract_hash (god->contract_terms, - &god->h_contract_terms)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - "contract terms"); - } - - } - else - { - - struct TALER_PrivateContractHashP h; - - if (GNUNET_OK != - TALER_JSON_contract_hash (god->contract_terms, - &h)) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - "contract terms"); - } - contract_match = (0 == - GNUNET_memcmp (&h, - &god->h_contract_terms)); - if ( !contract_match ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, - NULL); - } - - } - - } - - if (contract_available) - { - god->claimed = true; - } - else - { - struct TALER_ClaimTokenP db_claim_token; - struct TALER_MerchantPostDataHashP unused; - - qs = TMH_db->lookup_order (TMH_db->cls, - hc->instance->settings.id, - order_id, - &db_claim_token, - &unused, - (NULL == god->contract_terms) - ? &god->contract_terms - : NULL); - 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 (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_order"); - } - 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_MERCHANT_GENERIC_ORDER_UNKNOWN, - order_id); - } - /* Note: when "!ord.requireClaimToken" and the client does not provide - a claim token (all zeros!), then token_match==TRUE below: */ - token_match = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) && - (0 == GNUNET_memcmp (&db_claim_token, - &god->claim_token)); - } /* end unclaimed order logic */ - - GNUNET_assert (NULL != god->contract_terms); - merchant_base_url = json_string_value (json_object_get (god->contract_terms, - "merchant_base_url")); - if (NULL == merchant_base_url) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, - order_id); - } - - if (NULL == god->fulfillment_url) - god->fulfillment_url = json_string_value (json_object_get ( - god->contract_terms, - "fulfillment_url")); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Token match: %d, contract_available: %d, contract match: %d, claimed: %d\n", - token_match, - contract_available, - contract_match, - god->claimed); - - if (claim_token_provided && ! token_match) - { - /* Authentication provided but wrong. */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN, - "authentication with claim token provided but wrong"); - } - - if (h_contract_provided && ! contract_match) - { - /* Authentication provided but wrong. */ - GNUNET_break_op (0); - /* FIXME: use better error code */ - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN, - "authentication with h_contract provided but wrong"); - } - - if (! (token_match || - contract_match) ) - { - const char *public_reorder_url; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Neither claim token nor contract matched\n"); - public_reorder_url = json_string_value (json_object_get ( - god->contract_terms, - "public_reorder_url")); - /* Client has no rights to this order */ - if (NULL == public_reorder_url) - { - /* We cannot give the client a new order, just fail */ - if (! GNUNET_is_zero (&god->h_contract_terms)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, - NULL); - } - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN, - "no 'public_reorder_url'"); - } - /* We have a fulfillment URL, redirect the client there, maybe - the frontend can generate a fresh order for this new customer */ - if (god->generate_html) - { - /* Contract was claimed (maybe by another device), so this client - cannot get the status information. Redirect to fulfillment page, - where the client may be able to pickup a fresh order -- or might - be able authenticate via session ID */ - struct MHD_Response *reply; - MHD_RESULT ret; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Contract claimed, redirecting to fulfillment page for order %s\n", - order_id); - reply = MHD_create_response_from_buffer (0, - NULL, - MHD_RESPMEM_PERSISTENT); - if (NULL == reply) - { - GNUNET_break (0); - return MHD_NO; - } - GNUNET_break (MHD_YES == - MHD_add_response_header (reply, - MHD_HTTP_HEADER_LOCATION, - public_reorder_url)); - ret = MHD_queue_response (connection, - MHD_HTTP_FOUND, - reply); - MHD_destroy_response (reply); - return ret; - } - /* Need to generate JSON reply */ - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_ACCEPTED, - GNUNET_JSON_pack_string ("public_reorder_url", - public_reorder_url)); - } - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Claim token or contract matched\n"); - if ( (NULL != god->session_id) && (NULL != god->fulfillment_url) ) { @@ -1283,16 +1122,19 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, ID (because it purchased the article earlier) than the one that the browser is waiting for. */ char *already_paid_order_id = NULL; + enum GNUNET_DB_QueryStatus qs; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Running re-purchase detection for %s/%s\n", god->session_id, god->fulfillment_url); - qs = TMH_db->lookup_order_by_fulfillment (TMH_db->cls, - hc->instance->settings.id, - god->fulfillment_url, - god->session_id, - &already_paid_order_id); + qs = TMH_db->lookup_order_by_fulfillment ( + TMH_db->cls, + god->hc->instance->settings.id, + god->fulfillment_url, + god->session_id, + TALER_EXCHANGE_YNA_NO != god->allow_refunded_for_repurchase, + &already_paid_order_id); if (qs < 0) { /* single, read-only SQL statements should never cause @@ -1300,20 +1142,22 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, 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, - "order by fulfillment"); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "order by fulfillment"); + return false; } - if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || - (0 != strcmp (order_id, - already_paid_order_id)) ) + if ( (! god->paid) && + ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || + (0 != strcmp (god->order_id, + already_paid_order_id)) ) ) { - MHD_RESULT ret; + bool ret; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Sending pay request for order %s (already paid: %s)\n", - order_id, + god->order_id, already_paid_order_id); ret = send_pay_request (god, already_paid_order_id); @@ -1323,70 +1167,161 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); GNUNET_free (already_paid_order_id); } + god->phase++; + return false; +} + - if (! god->claimed) +/** + * Check if the order has been paid, and if not + * request payment. + * + * @param[in,out] god request context + * @return true to exit due to suspension + */ +static bool +phase_handle_unpaid (struct GetOrderData *god) +{ + if (god->paid) + { + god->phase++; + return false; + } + if (god->claimed) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order claimed but unpaid, sending pay request for order %s\n", + god->order_id); + } + else { - /* Order is unclaimed, no need to check for payments or even - refunds, simply always generate payment request */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Order unclaimed, sending pay request for order %s\n", - order_id); - return send_pay_request (god, - NULL); + god->order_id); } + return send_pay_request (god, + NULL); +} + +/** + * Function called with detailed information about a refund. + * It is responsible for packing up the data to return. + * + * @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_Timestamp 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 GetOrderData *god = cls; + + (void) refund_serial; + (void) timestamp; + (void) exchange_url; + (void) rtransaction_id; + 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); + god->refund_pending |= pending; + if ( (GNUNET_OK != + TALER_amount_cmp_currency (&god->refund_taken, + refund_amount)) || + (GNUNET_OK != + TALER_amount_cmp_currency (&god->refund_amount, + refund_amount)) ) { - /* Check if paid. */ - struct TALER_PrivateContractHashP h_contract; - bool paid; - - qs = TMH_db->lookup_order_status (TMH_db->cls, - hc->instance->settings.id, - order_id, - &h_contract, - &paid); - 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_GENERIC_DB_FETCH_FAILED, - "lookup_order_status"); - } - GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); - if (! paid) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order claimed but unpaid, sending pay request for order %s\n", - order_id); - return send_pay_request (god, - NULL); - } + god->bad_refund_currency_in_db = true; + return; + } + if (! pending) + { + GNUNET_assert (0 <= + TALER_amount_add (&god->refund_taken, + &god->refund_taken, + refund_amount)); + } + GNUNET_assert (0 <= + TALER_amount_add (&god->refund_amount, + &god->refund_amount, + refund_amount)); + god->refunded = true; +} + + +/** + * Check if the order has been refunded. + * + * @param[in,out] god request context + * @return true to exit due to suspension + */ +static bool +phase_check_refunded (struct GetOrderData *god) +{ + enum GNUNET_DB_QueryStatus qs; + + if ( (god->sc.awaiting_refund) && + (GNUNET_OK != + TALER_amount_cmp_currency (&god->contract_total, + &god->sc.refund_expected)) ) + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + god->contract_total.currency); + return false; } /* At this point, we know the contract was paid. Let's check for refunds. First, clear away refunds found from previous invocations. */ GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TMH_currency, + TALER_amount_set_zero (god->contract_total.currency, &god->refund_amount)); GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TMH_currency, + TALER_amount_set_zero (god->contract_total.currency, &god->refund_taken)); - qs = TMH_db->lookup_refunds_detailed (TMH_db->cls, - hc->instance->settings.id, - &god->h_contract_terms, - &process_refunds_cb, - god); + qs = TMH_db->lookup_refunds_detailed ( + TMH_db->cls, + god->hc->instance->settings.id, + &god->h_contract_terms, + &process_refunds_cb, + god); if (0 > qs) { GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_refunds_detailed"); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_refunds_detailed"); + return false; + } + if (god->bad_refund_currency_in_db) + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "currency mix-up between contract price and refunds in database"); + return false; } - if ( ((god->sc.awaiting_refund) && ( (! god->refunded) || (1 != TALER_amount_cmp (&god->refund_amount, @@ -1399,7 +1334,8 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, struct GNUNET_TIME_Relative remaining; remaining = GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout); - if (! GNUNET_TIME_relative_is_zero (remaining)) + if ( (! GNUNET_TIME_relative_is_zero (remaining)) && + (! god->generate_html) ) { /* yes, indeed suspend */ if (god->sc.awaiting_refund) @@ -1410,108 +1346,315 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Awaiting pending refunds\n"); suspend_god (god); - return MHD_YES; + return true; } } + god->phase++; + return false; +} - /* All operations done, build final response */ - if (god->generate_html) + +/** + * Create a taler://refund/ URI for the given @a con and @a order_id + * and @a instance_id. + * + * @param merchant_base_url URL to take host and path from; + * we cannot take it from the MHD connection as a browser + * may have changed 'http' to 'https' and we MUST be consistent + * with what the merchant's frontend used initially + * @param order_id the order id + * @return corresponding taler://refund/ URI, or NULL on missing "host" + */ +static char * +make_taler_refund_uri (const char *merchant_base_url, + const char *order_id) +{ + struct GNUNET_Buffer buf = { 0 }; + char *url; + struct GNUNET_Uri uri; + + url = GNUNET_strdup (merchant_base_url); + if (-1 == GNUNET_uri_parse (&uri, + url)) { - enum GNUNET_GenericReturnValue res; + GNUNET_break (0); + GNUNET_free (url); + return NULL; + } + GNUNET_assert (NULL != order_id); + GNUNET_buffer_write_str (&buf, + "taler"); + if (0 == strcasecmp ("http", + uri.scheme)) + GNUNET_buffer_write_str (&buf, + "+http"); + GNUNET_buffer_write_str (&buf, + "://refund/"); + GNUNET_buffer_write_str (&buf, + uri.host); + if (0 != uri.port) + GNUNET_buffer_write_fstr (&buf, + ":%u", + (unsigned int) uri.port); + if (NULL != uri.path) + GNUNET_buffer_write_path (&buf, + uri.path); + GNUNET_buffer_write_path (&buf, + order_id); + GNUNET_buffer_write_path (&buf, + ""); // Trailing slash + GNUNET_free (url); + return GNUNET_buffer_reap_str (&buf); +} - if (god->refund_pending) - { - char *qr; - char *uri; - GNUNET_assert (NULL != god->contract_terms); - uri = make_taler_refund_uri (merchant_base_url, - order_id); - if (NULL == uri) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (god->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_ALLOCATION_FAILURE, - "refund URI"); - } - qr = TMH_create_qrcode (uri); - if (NULL == qr) - { - GNUNET_break (0); - GNUNET_free (uri); - return TALER_MHD_reply_with_error (god->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_ALLOCATION_FAILURE, - "qr code"); - } - { - json_t *context; +/** + * Generate the order status response. + * + * @param[in,out] god request context + */ +static void +phase_return_status (struct GetOrderData *god) +{ + /* All operations done, build final response */ + if (! god->generate_html) + { + phase_end (god, + TALER_MHD_REPLY_JSON_PACK ( + god->sc.con, + MHD_HTTP_OK, + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("fulfillment_url", + god->fulfillment_url)), + GNUNET_JSON_pack_bool ("refunded", + god->refunded), + GNUNET_JSON_pack_bool ("refund_pending", + god->refund_pending), + TALER_JSON_pack_amount ("refund_taken", + &god->refund_taken), + TALER_JSON_pack_amount ("refund_amount", + &god->refund_amount))); + return; + } - context = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("order_summary", - get_order_summary (god)), - TALER_JSON_pack_amount ("refund_amount", - &god->refund_amount), - TALER_JSON_pack_amount ("refund_taken", - &god->refund_taken), - GNUNET_JSON_pack_string ("taler_refund_uri", - uri), - GNUNET_JSON_pack_string ("taler_refund_qrcode_svg", - qr)); - res = TMH_return_from_template (god->sc.con, - MHD_HTTP_OK, - "offer_refund", - hc->instance->settings.id, - uri, - context); - json_decref (context); - } + if (god->refund_pending) + { + char *qr; + char *uri; + + GNUNET_assert (NULL != god->contract_terms); + uri = make_taler_refund_uri (god->merchant_base_url, + god->order_id); + if (NULL == uri) + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_ALLOCATION_FAILURE, + "refund URI"); + return; + } + qr = TMH_create_qrcode (uri); + if (NULL == qr) + { + GNUNET_break (0); GNUNET_free (uri); - GNUNET_free (qr); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_ALLOCATION_FAILURE, + "qr code"); + return; } - else + { + enum GNUNET_GenericReturnValue res; json_t *context; context = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_object_incref ("contract_terms", - god->contract_terms), GNUNET_JSON_pack_string ("order_summary", get_order_summary (god)), TALER_JSON_pack_amount ("refund_amount", &god->refund_amount), TALER_JSON_pack_amount ("refund_taken", - &god->refund_taken)); - res = TMH_return_from_template (god->sc.con, - MHD_HTTP_OK, - "show_order_details", - hc->instance->settings.id, - NULL, - context); + &god->refund_taken), + GNUNET_JSON_pack_string ("taler_refund_uri", + uri), + GNUNET_JSON_pack_string ("taler_refund_qrcode_svg", + qr)); + res = TALER_TEMPLATING_reply ( + god->sc.con, + MHD_HTTP_OK, + "offer_refund", + god->hc->instance->settings.id, + uri, + context); + GNUNET_break (GNUNET_OK == res); json_decref (context); + phase_end (god, + (GNUNET_SYSERR == res) + ? MHD_NO + : MHD_YES); } - if (GNUNET_SYSERR == res) + GNUNET_free (uri); + GNUNET_free (qr); + return; + } + + { + enum GNUNET_GenericReturnValue res; + json_t *context; + + context = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_object_incref ("contract_terms", + god->contract_terms), + GNUNET_JSON_pack_string ("order_summary", + get_order_summary (god)), + TALER_JSON_pack_amount ("refund_amount", + &god->refund_amount), + TALER_JSON_pack_amount ("refund_taken", + &god->refund_taken)); + res = TALER_TEMPLATING_reply ( + god->sc.con, + MHD_HTTP_OK, + "show_order_details", + god->hc->instance->settings.id, + NULL, + context); + GNUNET_break (GNUNET_OK == res); + json_decref (context); + phase_end (god, + (GNUNET_SYSERR == res) + ? MHD_NO + : MHD_YES); + } +} + + +MHD_RESULT +TMH_get_orders_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct GetOrderData *god = hc->ctx; + + (void) rh; + if (NULL == god) + { + god = GNUNET_new (struct GetOrderData); + hc->ctx = god; + hc->cc = &god_cleanup; + god->sc.con = connection; + god->hc = hc; + god->order_id = hc->infix; + god->generate_html + = TMH_MHD_test_html_desired (connection); + + /* first-time initialization / sanity checks */ + TALER_MHD_parse_request_arg_auto (connection, + "h_contract", + &god->h_contract_terms, + god->h_contract_provided); + TALER_MHD_parse_request_arg_auto (connection, + "token", + &god->claim_token, + god->claim_token_provided); + if (! (TALER_arg_to_yna (connection, + "allow_refunded_for_repurchase", + TALER_EXCHANGE_YNA_NO, + &god->allow_refunded_for_repurchase)) ) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "allow_refunded_for_repurchase"); + god->session_id = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "session_id"); + + /* process await_refund_obtained argument */ { - GNUNET_break (0); + const char *await_refund_obtained_s; + + await_refund_obtained_s = + MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "await_refund_obtained"); + god->sc.awaiting_refund_obtained = + (NULL != await_refund_obtained_s) + ? 0 == strcasecmp (await_refund_obtained_s, + "yes") + : false; + if (god->sc.awaiting_refund_obtained) + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Awaiting refund obtained\n"); + } + + TALER_MHD_parse_request_amount (connection, + "refund", + &god->sc.refund_expected); + if (TALER_amount_is_valid (&god->sc.refund_expected)) + { + god->sc.awaiting_refund = true; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Awaiting minimum refund of %s\n", + TALER_amount2s (&god->sc.refund_expected)); + } + TALER_MHD_parse_request_timeout (connection, + &god->sc.long_poll_timeout); + } + + if (GNUNET_SYSERR == god->suspended) + return MHD_NO; /* we are in shutdown */ + if (GNUNET_YES == god->suspended) + { + god->suspended = GNUNET_NO; + GNUNET_CONTAINER_DLL_remove (god_head, + god_tail, + god); + } + + while (1) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling request in phase %d\n", + (int) god->phase); + switch (god->phase) + { + case GOP_INIT: + phase_init (god); + break; + case GOP_LOOKUP_TERMS: + phase_lookup_terms (god); + break; + case GOP_PARSE_CONTRACT: + phase_parse_contract (god); + break; + case GOP_CHECK_CLIENT_ACCESS: + phase_check_client_access (god); + break; + case GOP_CHECK_PAID: + phase_check_paid (god); + break; + case GOP_REDIRECT_TO_PAID_ORDER: + if (phase_redirect_to_paid_order (god)) + return MHD_YES; + break; + case GOP_HANDLE_UNPAID: + if (phase_handle_unpaid (god)) + return MHD_YES; + break; + case GOP_CHECK_REFUNDED: + if (phase_check_refunded (god)) + return MHD_YES; + break; + case GOP_RETURN_STATUS: + phase_return_status (god); + break; + case GOP_RETURN_MHD_YES: + return MHD_YES; + case GOP_RETURN_MHD_NO: return MHD_NO; } - return MHD_YES; } - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("fulfillment_url", - god->fulfillment_url)), - GNUNET_JSON_pack_bool ("refunded", - god->refunded), - GNUNET_JSON_pack_bool ("refund_pending", - god->refund_pending), - TALER_JSON_pack_amount ("refund_taken", - &god->refund_taken), - TALER_JSON_pack_amount ("refund_amount", - &god->refund_amount)); } |