/*
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 on payment.
*/
struct GNUNET_DB_EventHandler *pay_eh;
/**
* Database event we are waiting on to be resuming for refunds.
*/
struct GNUNET_DB_EventHandler *refund_eh;
/**
* Database event we are waiting on to be resuming for repurchase
* detection updating some equivalent order (same fulfillment URL)
* to our session.
*/
struct GNUNET_DB_EventHandler *session_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->session_eh)
{
TMH_db->event_listen_cancel (god->session_eh);
god->session_eh = 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 (%s/%s) by event with argument `%.*s`\n",
GNUNET_TIME_absolute_is_future (god->sc.long_poll_timeout)
? "future"
: "past",
god->sc.awaiting_refund
? "awaiting refund"
: "not waiting for 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;
/* Convert order_id to h_contract_terms */
TMH_db->preflight (TMH_db->cls);
GNUNET_assert (NULL == god->contract_terms);
{
enum GNUNET_DB_QueryStatus qs;
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_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;
if ( (NULL != god->session_id) &&
(NULL != god->fulfillment_url) &&
(NULL == god->session_eh) )
{
struct TMH_SessionEventP session_eh = {
.header.size = htons (sizeof (session_eh)),
.header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED),
.merchant_pub = god->hc->instance->merchant_pub
};
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Subscribing to session triggers for %p\n",
god);
GNUNET_CRYPTO_hash (god->session_id,
strlen (god->session_id),
&session_eh.h_session_id);
GNUNET_CRYPTO_hash (god->fulfillment_url,
strlen (god->fulfillment_url),
&session_eh.h_fulfillment_url);
god->session_eh
= TMH_db->event_listen (
TMH_db->cls,
&session_eh.header,
GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout),
&resume_by_event,
god);
}
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));
{
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 */