/*
This file is part of TALER
(C) 2014-2021 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 "taler-merchant-httpd_exchanges.h"
#include "taler-merchant-httpd_get-orders-ID.h"
#include "taler-merchant-httpd_mhd.h"
#include "taler-merchant-httpd_qr.h"
#include "taler-merchant-httpd_templating.h"
/**
* How often do we retry DB transactions on serialization failures?
*/
#define MAX_RETRIES 5
/**
* Context for the operation.
*/
struct GetOrderData
{
/**
* Hashed version of contract terms. All zeros if not provided.
*/
struct TALER_PrivateContractHash 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;
/**
* Total refunds granted for this payment. Only initialized
* if @e refunded is set to true.
*/
struct TALER_Amount refund_amount;
/**
* Return code: #TALER_EC_NONE if successful.
*/
enum TALER_ErrorCode ec;
/**
* Did we suspend @a connection and are thus in
* the #god_head DLL?
*/
bool suspended;
/**
* 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 payment 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.
*/
bool refund_available;
/**
* Set to true if the client requested HTML, otherwise we generate JSON.
*/
bool generate_html;
};
/**
* 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 = false;
MHD_resume_connection (god->sc.con);
TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */
}
}
/**
* 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);
return;
}
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 = false;
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);
}
/**
* 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);
if (NULL != god->contract_terms)
{
json_decref (god->contract_terms);
god->fulfillment_url = NULL;
god->contract_terms = NULL;
}
GNUNET_assert (! god->suspended);
god->suspended = true;
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.
*
* @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);
}
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_PrivateContractHash *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, '/'))
{
GNUNET_break_op (0);
return NULL;
}
GNUNET_assert (NULL != instance_id);
GNUNET_assert (NULL != order_id);
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 (NULL != session_id)
{
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++;
}
if (NULL != h_contract)
{
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);
}
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)
{
const char *host;
const char *forwarded_host;
const char *uri_path;
struct GNUNET_Buffer buf = { 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, '/'))
{
GNUNET_break_op (0);
return NULL;
}
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))
{
GNUNET_buffer_write_path (&buf,
"instances");
GNUNET_buffer_write_path (&buf,
instance_id);
}
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)))
{
/* 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);
}
/**
* 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 #MHD_YES on success
*/
static MHD_RESULT
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 MHD_YES;
}
/* 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);
return TALER_MHD_reply_with_error (god->sc.con,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED,
"host");
}
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);
return MHD_NO;
}
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);
return ret;
}
}
{
char *qr;
qr = TMH_create_qrcode (taler_pay_uri);
if (NULL == qr)
{
GNUNET_break (0);
return MHD_NO;
}
{
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 = TMH_return_from_template (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);
return ret;
}
/**
* 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_Absolute 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;
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_available |= pending;
if (god->refunded)
{
GNUNET_assert (0 <=
TALER_amount_add (&god->refund_amount,
&god->refund_amount,
refund_amount));
return;
}
god->refund_amount = *refund_amount;
god->refunded = true;
}
/**
* 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);
}
MHD_RESULT
TMH_get_orders_ID (const struct TMH_RequestHandler *rh,
struct MHD_Connection *connection,
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;
if (NULL == god)
{
god = GNUNET_new (struct GetOrderData);
hc->ctx = god;
hc->cc = &god_cleanup;
god->sc.con = connection;
god->hc = hc;
god->order_id = order_id;
god->generate_html = TMH_MHD_test_html_desired (connection);
/* first-time initialization / sanity checks */
{
const char *cts;
cts = MHD_lookup_connection_value (connection,
MHD_GET_ARGUMENT_KIND,
"h_contract");
if ( (NULL != cts) &&
(GNUNET_OK !=
GNUNET_CRYPTO_hash_from_string (cts,
&god->h_contract_terms.hash)) )
{
/* cts has wrong encoding */
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"h_contract");
}
if (NULL != cts)
h_contract_provided = true;
}
{
const char *ct;
ct = MHD_lookup_connection_value (connection,
MHD_GET_ARGUMENT_KIND,
"token");
if ( (NULL != ct) &&
(GNUNET_OK !=
GNUNET_STRINGS_string_to_data (ct,
strlen (ct),
&god->claim_token,
sizeof (god->claim_token))) )
{
/* ct has wrong encoding */
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"token");
}
if (NULL != ct)
claim_token_provided = true;
}
god->session_id = MHD_lookup_connection_value (connection,
MHD_GET_ARGUMENT_KIND,
"session_id");
/* process await_refund_obtained argument */
{
const char *await_refund_obtained_s;
await_refund_obtained_s =
MHD_lookup_connection_value (connection,
MHD_GET_ARGUMENT_KIND,
"await_refund_obtained");
god->sc.awaiting_refund_obtained =
(NULL != await_refund_obtained_s)
? 0 == strcasecmp (await_refund_obtained_s,
"yes")
: false;
if (god->sc.awaiting_refund_obtained)
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Awaiting refund obtained\n");
}
{
const char *min_refund;
min_refund = MHD_lookup_connection_value (connection,
MHD_GET_ARGUMENT_KIND,
"refund");
if (NULL != min_refund)
{
if ( (GNUNET_OK !=
TALER_string_to_amount (min_refund,
&god->sc.refund_expected)) ||
(0 != strcasecmp (god->sc.refund_expected.currency,
TMH_currency) ) )
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"refund");
}
god->sc.awaiting_refund = true;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Awaiting minimum refund of %s\n",
min_refund);
}
}
/* process timeout_ms argument */
{
const char *long_poll_timeout_ms;
long_poll_timeout_ms = MHD_lookup_connection_value (connection,
MHD_GET_ARGUMENT_KIND,
"timeout_ms");
if (NULL != long_poll_timeout_ms)
{
unsigned int timeout_ms;
char dummy;
if (1 != sscanf (long_poll_timeout_ms,
"%u%c",
&timeout_ms,
&dummy))
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"timeout_ms (must be non-negative number)");
}
/* If HTML is requested, we never long poll. Makes no sense */
if (! god->generate_html)
{
struct GNUNET_TIME_Relative timeout;
timeout = GNUNET_TIME_relative_multiply (
GNUNET_TIME_UNIT_MILLISECONDS,
timeout_ms);
god->sc.long_poll_timeout
= GNUNET_TIME_relative_to_absolute (timeout);
if (! GNUNET_TIME_relative_is_zero (timeout))
{
if (god->sc.awaiting_refund ||
god->sc.awaiting_refund_obtained)
{
struct TMH_OrderPayEventP refund_eh = {
.header.size = htons (sizeof (refund_eh)),
.header.type = htons (god->sc.awaiting_refund_obtained
? TALER_DBEVENT_MERCHANT_REFUND_OBTAINED
: TALER_DBEVENT_MERCHANT_ORDER_REFUND),
.merchant_pub = hc->instance->merchant_pub
};
GNUNET_CRYPTO_hash (god->order_id,
strlen (god->order_id),
&refund_eh.h_order_id);
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Subscribing %p to refunds on %s\n",
god,
god->order_id);
god->refund_eh = TMH_db->event_listen (TMH_db->cls,
&refund_eh.header,
timeout,
&resume_by_event,
god);
}
{
struct TMH_OrderPayEventP pay_eh = {
.header.size = htons (sizeof (pay_eh)),
.header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID),
.merchant_pub = hc->instance->merchant_pub
};
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Subscribing to payments on %s\n",
god->order_id);
GNUNET_CRYPTO_hash (god->order_id,
strlen (god->order_id),
&pay_eh.h_order_id);
god->pay_eh = TMH_db->event_listen (TMH_db->cls,
&pay_eh.header,
timeout,
&resume_by_event,
god);
}
} /* end of timeout non-zero */
} /* end of HTML generation NOT requested */
} /* end of timeout_ms argument provided */
} /* end of timeout_ms argument handling */
} /* end of first-time initialization / sanity checks */
if (god->suspended)
{
god->suspended = false;
GNUNET_CONTAINER_DLL_remove (god_head,
god_tail,
god);
}
/* Convert order_id to h_contract_terms */
TMH_db->preflight (TMH_db->cls);
if (NULL == god->contract_terms)
{
uint64_t order_serial;
struct TALER_ClaimTokenP db_claim_token;
qs = TMH_db->lookup_contract_terms (TMH_db->cls,
hc->instance->settings.id,
order_id,
&god->contract_terms,
&order_serial,
&db_claim_token);
if (0 > qs)
{
/* single, read-only SQL statements should never cause
serialization problems */
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
/* Always report on hard error as well to enable diagnostics */
GNUNET_break (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"lookup_contract_terms");
}
/* Note: when "!ord.requireClaimToken" and the client does not provide
a claim token (all zeros!), then token_match==TRUE below: */
token_match = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
&& (0 == GNUNET_memcmp (&db_claim_token,
&god->claim_token));
}
/* Check if client provided the right hash code of the contract terms */
if (NULL != god->contract_terms)
{
struct TALER_PrivateContractHash h;
contract_available = true;
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 ( (GNUNET_NO ==
GNUNET_is_zero (&god->h_contract_terms)) &&
(! 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 GNUNET_HashCode unused; /* h_post_data */
qs = TMH_db->lookup_order (TMH_db->cls,
hc->instance->settings.id,
order_id,
&db_claim_token,
&unused,
(NULL == god->contract_terms)
? &god->contract_terms
: NULL);
if (0 > qs)
{
/* single, read-only SQL statements should never cause
serialization problems */
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
/* Always report on hard error as well to enable diagnostics */
GNUNET_break (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"lookup_order");
}
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Unknown order id given: `%s'\n",
order_id);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_NOT_FOUND,
TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
order_id);
}
/* Note: when "!ord.requireClaimToken" and the client does not provide
a claim token (all zeros!), then token_match==TRUE below: */
token_match = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) &&
(0 == GNUNET_memcmp (&db_claim_token,
&god->claim_token));
} /* end unclaimed order logic */
GNUNET_assert (NULL != god->contract_terms);
merchant_base_url = json_string_value (json_object_get (god->contract_terms,
"merchant_base_url"));
if (NULL == merchant_base_url)
{
GNUNET_break (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
order_id);
}
if (NULL == god->fulfillment_url)
god->fulfillment_url = json_string_value (json_object_get (
god->contract_terms,
"fulfillment_url"));
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Token match: %d, contract_available: %d, contract match: %d, claimed: %d\n",
token_match,
contract_available,
contract_match,
god->claimed);
if (claim_token_provided && ! token_match)
{
/* Authentication provided but wrong. */
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_FORBIDDEN,
TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN,
"authentication with claim token provided but wrong");
}
if (h_contract_provided && ! contract_match)
{
/* Authentication provided but wrong. */
GNUNET_break_op (0);
/* FIXME: use better error code */
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_FORBIDDEN,
TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN,
"authentication with h_contract provided but wrong");
}
if (! (token_match ||
contract_match) )
{
const char *public_reorder_url;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Neither claim token nor contract matched\n");
public_reorder_url = json_string_value (json_object_get (
god->contract_terms,
"public_reorder_url"));
/* Client has no rights to this order */
if (NULL == public_reorder_url)
{
/* We cannot give the client a new order, just fail */
if (! GNUNET_is_zero (&god->h_contract_terms))
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (
connection,
MHD_HTTP_FORBIDDEN,
TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER,
NULL);
}
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_FORBIDDEN,
TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN,
"no 'public_reorder_url'");
}
/* We have a fulfillment URL, redirect the client there, maybe
the frontend can generate a fresh order for this new customer */
if (god->generate_html)
{
/* Contract was claimed (maybe by another device), so this client
cannot get the status information. Redirect to fulfillment page,
where the client may be able to pickup a fresh order -- or might
be able authenticate via session ID */
struct MHD_Response *reply;
MHD_RESULT ret;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Contract claimed, redirecting to fulfillment page for order %s\n",
order_id);
reply = MHD_create_response_from_buffer (0,
NULL,
MHD_RESPMEM_PERSISTENT);
if (NULL == reply)
{
GNUNET_break (0);
return MHD_NO;
}
GNUNET_break (MHD_YES ==
MHD_add_response_header (reply,
MHD_HTTP_HEADER_LOCATION,
public_reorder_url));
ret = MHD_queue_response (connection,
MHD_HTTP_FOUND,
reply);
MHD_destroy_response (reply);
return ret;
}
/* Need to generate JSON reply */
return TALER_MHD_REPLY_JSON_PACK (
connection,
MHD_HTTP_ACCEPTED,
GNUNET_JSON_pack_string ("public_reorder_url",
public_reorder_url));
}
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Claim token or contract matched\n");
if ( (NULL != god->session_id) &&
(NULL != god->fulfillment_url) )
{
/* 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)
{
/* 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_PrivateContractHash 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));
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_available) ) )
{
/* 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))
{
/* 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 MHD_YES;
}
}
/* All operations done, build final response */
if (god->generate_html)
{
enum GNUNET_GenericReturnValue res;
if (god->refund_available)
{
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),
GNUNET_JSON_pack_string ("taler_refund_uri",
uri),
GNUNET_JSON_pack_string ("taler_refund_qrcode_svg",
qr));
res = TMH_return_from_template (god->sc.con,
MHD_HTTP_OK,
"offer_refund",
hc->instance->settings.id,
uri,
context);
json_decref (context);
}
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));
res = TMH_return_from_template (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);
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_available),
TALER_JSON_pack_amount ("refund_amount",
&god->refund_amount));
}
/* end of taler-merchant-httpd_get-orders-ID.c */