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 | 1951 |
1 files changed, 1044 insertions, 907 deletions
diff --git a/src/backend/taler-merchant-httpd_get-orders-ID.c b/src/backend/taler-merchant-httpd_get-orders-ID.c index c1db5ea9..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 @@ -28,6 +28,7 @@ #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" @@ -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 = TALER_TEMPLATING_reply (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,7 +1061,146 @@ send_pay_request (struct GetOrderData *god, } GNUNET_free (taler_pay_uri); GNUNET_free (order_status_url); - return ret; + phase_end (god, + ret); + return false; +} + + +/** + * Check if the order has been paid. + * + * @param[in,out] god request context + */ +static void +phase_check_paid (struct GetOrderData *god) +{ + 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; + } + god->phase++; +} + + +/** + * Check if the client already paid for an equivalent + * order under this session, and if so redirect to + * that order. + * + * @param[in,out] god request context + * @return true to exit due to suspension + */ +static bool +phase_redirect_to_paid_order (struct GetOrderData *god) +{ + if ( (NULL != god->session_id) && + (NULL != god->fulfillment_url) ) + { + /* Check if client paid for this fulfillment article + already within this session, but using a different + order ID. If so, redirect the client to the order + it already paid. Allows, for example, the case + where a mobile phone pays for a browser's session, + where the mobile phone has a different order + 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_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, + 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 + 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); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "order by fulfillment"); + return false; + } + if ( (! god->paid) && + ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || + (0 != strcmp (god->order_id, + already_paid_order_id)) ) ) + { + bool ret; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Sending pay request for order %s (already paid: %s)\n", + god->order_id, + already_paid_order_id); + ret = send_pay_request (god, + already_paid_order_id); + GNUNET_free (already_paid_order_id); + return ret; + } + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); + GNUNET_free (already_paid_order_id); + } + god->phase++; + return false; +} + + +/** + * 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 + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order unclaimed, sending pay request for order %s\n", + god->order_id); + } + return send_pay_request (god, + NULL); } @@ -768,6 +1241,16 @@ process_refunds_cb (void *cls, 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)) ) + { + god->bad_refund_currency_in_db = true; + return; + } if (! pending) { GNUNET_assert (0 <= @@ -784,31 +1267,266 @@ process_refunds_cb (void *cls, /** - * Clean up the session state for a GET /orders/$ID request. + * Check if the order has been refunded. * - * @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) +static bool +phase_check_refunded (struct GetOrderData *god) { - struct GetOrderData *god = cls; + enum GNUNET_DB_QueryStatus qs; - if (NULL != god->contract_terms) + if ( (god->sc.awaiting_refund) && + (GNUNET_OK != + TALER_amount_cmp_currency (&god->contract_total, + &god->sc.refund_expected)) ) { - json_decref (god->contract_terms); - god->contract_terms = NULL; + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + god->contract_total.currency); + return false; } - if (NULL != god->refund_eh) + + /* 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 (god->contract_total.currency, + &god->refund_amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (god->contract_total.currency, + &god->refund_taken)); + 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) { - TMH_db->event_listen_cancel (god->refund_eh); - god->refund_eh = NULL; + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_refunds_detailed"); + return false; } - if (NULL != god->pay_eh) + if (god->bad_refund_currency_in_db) { - TMH_db->event_listen_cancel (god->pay_eh); - god->pay_eh = NULL; + 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, + &god->sc.refund_expected)) )) || + ( (god->sc.awaiting_refund_obtained) && + (god->refund_pending) ) ) + { + /* Client is waiting for a refund larger than what we have, suspend + until timeout */ + struct GNUNET_TIME_Relative remaining; + + remaining = GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout); + if ( (! GNUNET_TIME_relative_is_zero (remaining)) && + (! god->generate_html) ) + { + /* yes, indeed suspend */ + if (god->sc.awaiting_refund) + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Awaiting refund exceeding %s\n", + TALER_amount2s (&god->sc.refund_expected)); + if (god->sc.awaiting_refund_obtained) + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Awaiting pending refunds\n"); + suspend_god (god); + return true; + } + } + god->phase++; + return false; +} + + +/** + * 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)) + { + 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); +} + + +/** + * 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; + } + + 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); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_ALLOCATION_FAILURE, + "qr code"); + return; + } + + { + enum GNUNET_GenericReturnValue res; + json_t *context; + + 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 = 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); + } + 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); } - GNUNET_free (god); } @@ -818,14 +1536,6 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, struct TMH_HandlerContext *hc) { 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; (void) rh; if (NULL == god) @@ -835,56 +1545,27 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, 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); - + god->order_id = hc->infix; + 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; - } + 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"); @@ -907,118 +1588,19 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, "Awaiting refund obtained\n"); } + TALER_MHD_parse_request_amount (connection, + "refund", + &god->sc.refund_expected); + if (TALER_amount_is_valid (&god->sc.refund_expected)) { - 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); - } + god->sc.awaiting_refund = true; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Awaiting minimum refund of %s\n", + TALER_amount2s (&god->sc.refund_expected)); } - - - /* 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 */ + TALER_MHD_parse_request_timeout (connection, + &god->sc.long_poll_timeout); + } if (GNUNET_SYSERR == god->suspended) return MHD_NO; /* we are in shutdown */ @@ -1030,494 +1612,49 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, god); } - /* Convert order_id to h_contract_terms */ - TMH_db->preflight (TMH_db->cls); - if (NULL == god->contract_terms) - { - uint64_t order_serial; - bool paid = false; - 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, - &paid, - &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); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH, - NULL); - } - - 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) ) - { - /* Check if client paid for this fulfillment article - already within this session, but using a different - order ID. If so, redirect the client to the order - it already paid. Allows, for example, the case - where a mobile phone pays for a browser's session, - where the mobile phone has a different order - ID (because it purchased the article earlier) - than the one that the browser is waiting for. */ - char *already_paid_order_id = NULL; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "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); - 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_GENERIC_DB_FETCH_FAILED, - "order by fulfillment"); - } - if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || - (0 != strcmp (order_id, - already_paid_order_id)) ) - { - MHD_RESULT ret; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Sending pay request for order %s (already paid: %s)\n", - order_id, - already_paid_order_id); - ret = send_pay_request (god, - already_paid_order_id); - GNUNET_free (already_paid_order_id); - return ret; - } - GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); - GNUNET_free (already_paid_order_id); - } - - if (! god->claimed) + while (1) { - /* 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); - } - - { - /* 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); - } - } - - /* 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, - &god->refund_amount)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TMH_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); - 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"); - } - - if ( ((god->sc.awaiting_refund) && - ( (! god->refunded) || - (1 != TALER_amount_cmp (&god->refund_amount, - &god->sc.refund_expected)) )) || - ( (god->sc.awaiting_refund_obtained) && - (god->refund_pending) ) ) - { - /* Client is waiting for a refund larger than what we have, suspend - until timeout */ - struct GNUNET_TIME_Relative remaining; - - remaining = GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout); - if (! GNUNET_TIME_relative_is_zero (remaining)) + "Handling request in phase %d\n", + (int) god->phase); + switch (god->phase) { - /* yes, indeed suspend */ - if (god->sc.awaiting_refund) - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Awaiting refund exceeding %s\n", - TALER_amount2s (&god->sc.refund_expected)); - if (god->sc.awaiting_refund_obtained) - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Awaiting pending refunds\n"); - suspend_god (god); + 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; - } - } - - /* All operations done, build final response */ - if (god->generate_html) - { - enum GNUNET_GenericReturnValue res; - - 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; - - 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 = TALER_TEMPLATING_reply (god->sc.con, - MHD_HTTP_OK, - "offer_refund", - hc->instance->settings.id, - uri, - context); - json_decref (context); - } - GNUNET_free (uri); - GNUNET_free (qr); - } - else - { - 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", - hc->instance->settings.id, - NULL, - context); - json_decref (context); - } - if (GNUNET_SYSERR == res) - { - GNUNET_break (0); + 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)); } |