/* This file is part of TALER (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 Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * @file taler-merchant-httpd_get-orders-ID.c * @brief implementation of GET /orders/$ID * @author Marcello Stanisci * @author Christian Grothoff */ #include "platform.h" #include #include #include #include #include #include #include #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" /** * How often do we retry DB transactions on serialization failures? */ #define MAX_RETRIES 5 /** * 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 { /** * Hashed version of contract terms. All zeros if not provided. */ struct TALER_PrivateContractHashP h_contract_terms; /** * Claim token used for access control. All zeros if not provided. */ struct TALER_ClaimTokenP claim_token; /** * DLL of (suspended) requests. */ struct GetOrderData *next; /** * DLL of (suspended) requests. */ struct GetOrderData *prev; /** * Context of the request. */ struct TMH_HandlerContext *hc; /** * Entry in the #resume_timeout_heap for this check payment, if we are * suspended. */ struct TMH_SuspendedConnection sc; /** * Database event we are waiting on to be resuming. */ struct GNUNET_DB_EventHandler *pay_eh; /** * Database event we are waiting on to be resuming. */ struct GNUNET_DB_EventHandler *refund_eh; /** * Which merchant instance is this for? */ struct MerchantInstance *mi; /** * order ID for the payment */ const char *order_id; /** * Where to get the contract */ const char *contract_url; /** * fulfillment URL of the contract (valid as long as @e contract_terms is * valid; but can also be NULL if the contract_terms does not come with * a fulfillment URL). */ const char *fulfillment_url; /** * session of the client */ const char *session_id; /** * Contract terms of the payment we are checking. NULL when they * are not (yet) known. */ json_t *contract_terms; /** * 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. */ struct TALER_Amount refund_amount; /** * Total refunds already collected. * if @e refunded is set to true. */ 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; /** * Did we suspend @a connection and are thus in * the #god_head DLL (#GNUNET_YES). Set to * #GNUNET_NO if we are not suspended, and to * #GNUNET_SYSERR if we should close the connection * without a response due to shutdown. */ 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). */ bool claimed; /** * 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; /** * Set to true if a refund is still available for the * wallet for this payment. * @deprecated: true if refund_taken < refund_amount */ bool refund_pending; /** * Set to true if the client requested HTML, otherwise we generate JSON. */ 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; }; /** * Head of DLL of (suspended) requests. */ static struct GetOrderData *god_head; /** * Tail of DLL of (suspended) requests. */ static struct GetOrderData *god_tail; void TMH_force_wallet_get_order_resume (void) { struct GetOrderData *god; while (NULL != (god = god_head)) { GNUNET_CONTAINER_DLL_remove (god_head, god_tail, god); GNUNET_assert (god->suspended); god->suspended = GNUNET_SYSERR; MHD_resume_connection (god->sc.con); TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ } } /** * 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. * * @param cls a `struct GetOrderData` to resume * @param extra string encoding refund amount (or NULL) * @param extra_size number of bytes in @a extra */ static void resume_by_event (void *cls, const void *extra, size_t extra_size) { struct GetOrderData *god = cls; struct GNUNET_AsyncScopeSave old; GNUNET_async_scope_enter (&god->hc->async_scope_id, &old); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Received event for %s with argument `%.*s`\n", god->order_id, (int) extra_size, (const char *) extra); if (! god->suspended) { GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Not suspended, ignoring event\n"); GNUNET_async_scope_restore (&old); return; /* duplicate event is possible */ } if (GNUNET_TIME_absolute_is_future (god->sc.long_poll_timeout) && god->sc.awaiting_refund) { char *as; struct TALER_Amount a; if (0 == extra_size) { GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "No amount given, but need refund above threshold\n"); GNUNET_async_scope_restore (&old); return; /* not relevant */ } as = GNUNET_strndup (extra, extra_size); if (GNUNET_OK != TALER_string_to_amount (as, &a)) { 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)) { GNUNET_break (0); GNUNET_async_scope_restore (&old); return; /* bad currency!? */ } if (1 == TALER_amount_cmp (&god->sc.refund_expected, &a)) { GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Amount too small to trigger resuming\n"); GNUNET_async_scope_restore (&old); return; /* refund too small */ } } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Resuming (%d/%d) by event with argument `%.*s`\n", (int) GNUNET_TIME_absolute_is_future (god->sc.long_poll_timeout), god->sc.awaiting_refund, (int) extra_size, (const char *) extra); god->suspended = GNUNET_NO; GNUNET_CONTAINER_DLL_remove (god_head, god_tail, god); MHD_resume_connection (god->sc.con); TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ GNUNET_async_scope_restore (&old); } /** * First phase (after request parsing). * Set up long-polling. * * @param[in,out] god request context */ static void phase_init (struct GetOrderData *god) { 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) { 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); } } /** * Lookup contract terms and check client has the * right to access this order (by claim token or * contract hash). * * @param[in,out] god request context */ static void phase_lookup_terms (struct GetOrderData *god) { uint64_t order_serial; struct TALER_ClaimTokenP db_claim_token; enum GNUNET_DB_QueryStatus qs; /* 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); phase_fail (god, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "lookup_contract_terms"); 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)); /* Check if client provided the right hash code of the contract terms */ if (NULL != god->contract_terms) { 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_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 (god->contract_available) { god->claimed = true; } else { 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++; } /** * Parse contract terms. * * @param[in,out] god request context */ static void phase_parse_contract (struct GetOrderData *god) { 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; 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); 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; } 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); phase_fail (god, MHD_HTTP_FORBIDDEN, TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN, "authentication with claim token provided but wrong"); return; } if (god->h_contract_provided && ! god->contract_match) { /* 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; } if (! (god->token_match || god->contract_match) ) { 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++; } /** * Return the order summary of the contract of @a god in the * preferred language of the HTTP client. * * @param god order to extract summary from * @return dummy error message summary if no summary was provided in the contract */ static const char * get_order_summary (const struct GetOrderData *god) { const char *language_pattern; const char *ret; language_pattern = MHD_lookup_connection_value (god->sc.con, MHD_HEADER_KIND, MHD_HTTP_HEADER_ACCEPT_LANGUAGE); if (NULL == language_pattern) language_pattern = "en"; ret = json_string_value (TALER_JSON_extract_i18n (god->contract_terms, language_pattern, "summary")); if (NULL == ret) { /* Upon order creation (and insertion into the database), the presence of a summary should have been checked. So if we get here, someone did something fishy to our database... */ GNUNET_break (0); ret = ""; } return ret; } /** * The client did not yet pay, send it the payment request. * * @param god check pay request context * @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 true to exit due to suspension */ static bool send_pay_request (struct GetOrderData *god, const char *already_paid_order_id) { MHD_RESULT ret; char *taler_pay_uri; char *order_status_url; struct GNUNET_TIME_Relative remaining; remaining = GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout); if ( (! GNUNET_TIME_relative_is_zero (remaining)) && (NULL == already_paid_order_id) ) { /* long polling: do not queue a response, suspend connection instead */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Suspending request: long polling for payment\n"); suspend_god (god); return true; } /* Check if resource_id has been paid for in the same session * with another order_id. */ 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); if ( (NULL == taler_pay_uri) || (NULL == order_status_url) ) { GNUNET_break_op (0); GNUNET_free (taler_pay_uri); GNUNET_free (order_status_url); phase_fail (god, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, "host"); return false; } if (god->generate_html) { if (NULL != already_paid_order_id) { struct MHD_Response *reply; GNUNET_assert (NULL != god->fulfillment_url); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Redirecting to already paid order %s via fulfillment URL %s\n", already_paid_order_id, god->fulfillment_url); reply = MHD_create_response_from_buffer (0, NULL, MHD_RESPMEM_PERSISTENT); if (NULL == reply) { GNUNET_break (0); phase_end (god, MHD_NO); return false; } GNUNET_break (MHD_YES == MHD_add_response_header (reply, MHD_HTTP_HEADER_LOCATION, god->fulfillment_url)); { MHD_RESULT ret; ret = MHD_queue_response (god->sc.con, MHD_HTTP_FOUND, reply); MHD_destroy_response (reply); phase_end (god, ret); return false; } } { char *qr; qr = TMH_create_qrcode (taler_pay_uri); if (NULL == qr) { GNUNET_break (0); phase_end (god, MHD_NO); return false; } { enum GNUNET_GenericReturnValue res; json_t *context; context = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("taler_pay_uri", taler_pay_uri), GNUNET_JSON_pack_string ("order_status_url", order_status_url), GNUNET_JSON_pack_string ("taler_pay_qrcode_svg", 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); if (GNUNET_SYSERR == res) { GNUNET_break (0); ret = MHD_NO; } else { ret = MHD_YES; } json_decref (context); } GNUNET_free (qr); } } else /* end of 'generate HTML' */ { ret = TALER_MHD_REPLY_JSON_PACK ( god->sc.con, MHD_HTTP_PAYMENT_REQUIRED, GNUNET_JSON_pack_string ("taler_pay_uri", taler_pay_uri), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("fulfillment_url", god->fulfillment_url)), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("already_paid_order_id", already_paid_order_id))); } GNUNET_free (taler_pay_uri); GNUNET_free (order_status_url); 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); } /** * 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)) ) { 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 (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) { GNUNET_break (0); 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, &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); } } 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 */ { 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; } } } /* end of taler-merchant-httpd_get-orders-ID.c */