/*
This file is part of TALER
(C) 2014-2020 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 backend/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 "taler-merchant-httpd_exchanges.h"
#include "taler-merchant-httpd_get-orders-ID.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
/**
* Information we keep for each coin to be refunded.
*/
struct CoinRefund
{
/**
* Kept in a DLL.
*/
struct CoinRefund *next;
/**
* Kept in a DLL.
*/
struct CoinRefund *prev;
/**
* Request to connect to the target exchange.
*/
struct TMH_EXCHANGES_FindOperation *fo;
/**
* Handle for the refund operation with the exchange.
*/
struct TALER_EXCHANGE_RefundHandle *rh;
/**
* Request this operation is part of.
*/
struct GetOrderData *god;
/**
* URL of the exchange for this @e coin_pub.
*/
char *exchange_url;
/**
* Fully reply from the exchange, only possibly set if
* we got a JSON reply and a non-#MHD_HTTP_OK error code
*/
json_t *exchange_reply;
/**
* When did the merchant grant the refund. To be used to group events
* in the wallet.
*/
struct GNUNET_TIME_Absolute execution_time;
/**
* Coin to refund.
*/
struct TALER_CoinSpendPublicKeyP coin_pub;
/**
* Refund transaction ID to use.
*/
uint64_t rtransaction_id;
/**
* Unique serial number identifying the refund.
*/
uint64_t refund_serial;
/**
* Amount to refund.
*/
struct TALER_Amount refund_amount;
/**
* Public key of the exchange affirming the refund.
*/
struct TALER_ExchangePublicKeyP exchange_pub;
/**
* Signature of the exchange affirming the refund.
*/
struct TALER_ExchangeSignatureP exchange_sig;
/**
* HTTP status from the exchange, #MHD_HTTP_OK if
* @a exchange_pub and @a exchange_sig are valid.
*/
unsigned int exchange_status;
/**
* HTTP error code from the exchange.
*/
enum TALER_ErrorCode exchange_code;
};
/**
* Context for the operation.
*/
struct GetOrderData
{
/**
* Hashed version of contract terms. All zeros if
* not provided.
*/
struct GNUNET_HashCode 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;
/**
* Refunds for this order. Head of DLL.
*/
struct CoinRefund *cr_head;
/**
* Refunds for this order. Tail of DLL.
*/
struct CoinRefund *cr_tail;
/**
* 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;
/**
* 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).
*/
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;
/**
* Did we suspend @a connection?
*/
bool suspended;
/**
* Return code: #TALER_EC_NONE if successful.
*/
enum TALER_ErrorCode ec;
/**
* Set to true if we are dealing with an unclaimed order
* (and thus @e h_contract_terms is not set, and certain
* DB queries will not work).
*/
bool unclaimed;
/**
* 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;
/**
* Force resuming all suspended order lookups, needed during shutdown.
*/
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);
}
}
/**
* 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);
TMH_long_poll_suspend (god->order_id,
god->hc->instance,
&god->sc,
(god->sc.awaiting_refund)
? &god->sc.refund_expected
: NULL);
}
/**
* Create a taler://refund/ URI for the given @a con and @a order_id
* and @a instance_id.
*
* @param con HTTP connection
* @param order_id the order id
* @param instance_id instance, may be "default"
* @return corresponding taler://refund/ URI, or NULL on missing "host"
*/
static char *
make_taler_refund_uri (struct MHD_Connection *con,
const char *order_id,
const char *instance_id)
{
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,
"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;
}
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,
"://refund/");
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);
return GNUNET_buffer_reap_str (&buf);
}
/**
* Create a taler://pay/ URI for the given @a con and @a order_id
* and @a session_id and @a instance_id.
*
* @param con HTTP connection
* @param order_id the order id
* @param session_id session, may be NULL
* @param instance_id instance, may be "default"
* @param claim_token claim token for the order, may be NULL
* @return corresponding taler://pay/ URI, or NULL on missing "host"
*/
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,
"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;
}
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) &&
(0 != GNUNET_is_zero (claim_token)))
{
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);
}
/**
* 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;
struct GNUNET_TIME_Relative remaining;
remaining = GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout);
if (0 != remaining.rel_value_us)
{
/* long polling: do not queue a response, suspend connection instead */
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 in /poll-payment\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);
if (god->generate_html)
{
char *qr;
qr = TMH_create_qrcode (taler_pay_uri);
if (NULL == qr)
{
GNUNET_break (0);
return MHD_NO;
}
{
struct KVC kvc[] = {
{ "taler_pay_uri",
taler_pay_uri },
{ "taler_pay_qrcode_svg",
qr },
{ "order_summary",
// FIXME #6458: implement logic to extract summary based on
// language preferences from summary_i18n if present.
json_string_value (json_object_get (god->contract_terms,
"summary")) },
{ NULL, NULL }
};
enum GNUNET_GenericReturnValue res;
res = TMH_return_from_template (god->sc.con,
MHD_HTTP_PAYMENT_REQUIRED,
"request_payment",
taler_pay_uri,
kvc);
if (GNUNET_SYSERR == res)
{
GNUNET_break (0);
ret = MHD_NO;
}
ret = MHD_YES;
}
GNUNET_free (qr);
}
else
{
ret = TALER_MHD_reply_json_pack (god->sc.con,
MHD_HTTP_PAYMENT_REQUIRED,
"{s:s, s:s?}",
"taler_pay_uri",
taler_pay_uri,
"already_paid_order_id",
already_paid_order_id);
}
GNUNET_free (taler_pay_uri);
return ret;
}
/**
* Check if @a god has sub-activities still pending.
*
* @param god request to check
* @return true if activities are still pending
*/
static bool
god_pending (struct GetOrderData *god)
{
for (struct CoinRefund *cr = god->cr_head;
NULL != cr;
cr = cr->next)
{
if ( (NULL != cr->fo) ||
(NULL != cr->rh) )
return true;
}
return false;
}
/**
* Check if @a god is ready to be resumed, and if so, do it.
*
* @param god refund request to be possibly ready
*/
static void
check_resume_god (struct GetOrderData *god)
{
if (god_pending (god))
return;
GNUNET_CONTAINER_DLL_remove (god_head,
god_tail,
god);
GNUNET_assert (god->suspended);
god->suspended = false;
MHD_resume_connection (god->sc.con);
TMH_trigger_daemon ();
}
/**
* Callbacks of this type are used to serve the result of submitting a
* refund request to an exchange.
*
* @param cls a `struct CoinRefund`
* @param hr HTTP response data
* @param exchange_pub exchange key used to sign refund confirmation
* @param exchange_sig exchange's signature over refund
*/
static void
refund_cb (void *cls,
const struct TALER_EXCHANGE_HttpResponse *hr,
const struct TALER_ExchangePublicKeyP *exchange_pub,
const struct TALER_ExchangeSignatureP *exchange_sig)
{
struct CoinRefund *cr = cls;
cr->rh = NULL;
cr->exchange_status = hr->http_status;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Exchange refund status for coin %s is %u\n",
TALER_B2S (&cr->coin_pub),
hr->http_status);
if (MHD_HTTP_OK != hr->http_status)
{
cr->exchange_code = hr->ec;
cr->exchange_reply = json_incref ((json_t*) hr->reply);
}
else
{
enum GNUNET_DB_QueryStatus qs;
cr->exchange_pub = *exchange_pub;
cr->exchange_sig = *exchange_sig;
qs = TMH_db->insert_refund_proof (TMH_db->cls,
cr->refund_serial,
exchange_sig,
exchange_pub);
if (0 >= qs)
{
/* generally, this is relatively harmless for the merchant, but let's at
least log this. */
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Failed to persist exchange response to /refund in database: %d\n",
qs);
}
}
check_resume_god (cr->god);
}
/**
* Function called with the result of a #TMH_EXCHANGES_find_exchange()
* operation.
*
* @param cls a `struct CoinRefund *`
* @param hr HTTP response details
* @param eh handle to the exchange context
* @param payto_uri payto://-URI of the exchange
* @param wire_fee current applicable wire fee for dealing with @a eh, NULL if not available
* @param exchange_trusted true if this exchange is trusted by config
*/
static void
exchange_found_cb (void *cls,
const struct TALER_EXCHANGE_HttpResponse *hr,
struct TALER_EXCHANGE_Handle *eh,
const char *payto_uri,
const struct TALER_Amount *wire_fee,
bool exchange_trusted)
{
struct CoinRefund *cr = cls;
(void) payto_uri;
cr->fo = NULL;
if (TALER_EC_NONE == hr->ec)
{
cr->rh = TALER_EXCHANGE_refund (eh,
&cr->refund_amount,
&cr->god->h_contract_terms,
&cr->coin_pub,
cr->rtransaction_id,
&cr->god->hc->instance->merchant_priv,
&refund_cb,
cr);
return;
}
cr->exchange_status = hr->http_status;
cr->exchange_code = hr->ec;
cr->exchange_reply = json_incref ((json_t*) hr->reply);
check_resume_god (cr->god);
}
/**
* 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;
struct CoinRefund *cr;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Found refund of %s for coin %s with reason `%s' in database\n",
TALER_B2S (coin_pub),
TALER_amount2s (refund_amount),
reason);
cr = GNUNET_new (struct CoinRefund);
cr->refund_serial = refund_serial;
cr->exchange_url = GNUNET_strdup (exchange_url);
cr->god = god;
cr->coin_pub = *coin_pub;
cr->rtransaction_id = rtransaction_id;
cr->refund_amount = *refund_amount;
cr->execution_time = timestamp;
GNUNET_CONTAINER_DLL_insert (god->cr_head,
god->cr_tail,
cr);
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;
god->refund_available |= pending;
}
/**
* Clean up refund processing.
*
* @param god handle to clean up refund processing for
*/
static void
rf_cleanup (struct GetOrderData *god)
{
struct CoinRefund *cr;
while (NULL != (cr = god->cr_head))
{
GNUNET_CONTAINER_DLL_remove (god->cr_head,
god->cr_tail,
cr);
if (NULL != cr->fo)
{
TMH_EXCHANGES_find_exchange_cancel (cr->fo);
cr->fo = NULL;
}
if (NULL != cr->rh)
{
TALER_EXCHANGE_refund_cancel (cr->rh);
cr->rh = NULL;
}
if (NULL != cr->exchange_reply)
{
json_decref (cr->exchange_reply);
cr->exchange_reply = NULL;
}
GNUNET_free (cr->exchange_url);
GNUNET_free (cr);
}
}
/**
* 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;
rf_cleanup (god);
if (NULL != god->contract_terms)
json_decref (god->contract_terms);
GNUNET_free (god);
}
/**
* Handle a GET "/orders/$ID" request.
*
* @param rh context of the handler
* @param connection the MHD connection to handle
* @param[in,out] hc context with further information about the request
* @return MHD result code
*/
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;
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;
{
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_PARAMETER_MALFORMED,
"token malformed");
}
}
{
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)) )
{
/* cts has wrong encoding */
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_PARAMETER_MALFORMED,
"h_contract malformed");
}
}
{ /* check for 'Accept' header */
const char *accept;
accept = MHD_lookup_connection_value (connection,
MHD_HEADER_KIND,
MHD_HTTP_HEADER_ACCEPT);
if (NULL != accept)
{
char *a = GNUNET_strdup (accept);
char *saveptr;
for (char *t = strtok_r (a, ",", &saveptr);
NULL != t;
t = strtok_r (NULL, ",", &saveptr))
{
char *end;
/* skip leading whitespace */
while (isspace ((unsigned char) t[0]))
t++;
/* trim of ';q=' parameter and everything after space */
end = strchr (t, ';');
if (NULL != end)
*end = '\0';
end = strchr (t, ' ');
if (NULL != end)
*end = '\0';
if (0 == strcasecmp ("text/html",
t))
{
god->generate_html = true;
break;
}
}
GNUNET_free (a);
}
} /* end check for 'Accept' header */
{
const char *long_poll_timeout_s;
long_poll_timeout_s = MHD_lookup_connection_value (connection,
MHD_GET_ARGUMENT_KIND,
"timeout");
if (NULL != long_poll_timeout_s)
{
unsigned int timeout;
if (1 != sscanf (long_poll_timeout_s,
"%u",
&timeout))
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_PARAMETER_MALFORMED,
"timeout must be non-negative number");
}
god->sc.long_poll_timeout
= GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply (
GNUNET_TIME_UNIT_SECONDS,
timeout));
}
else
{
god->sc.long_poll_timeout = GNUNET_TIME_UNIT_ZERO_ABS;
}
}
{
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_PARAMETER_MALFORMED,
"invalid amount given for refund argument");
}
god->sc.awaiting_refund = true;
}
}
god->session_id = MHD_lookup_connection_value (connection,
MHD_GET_ARGUMENT_KIND,
"session_id");
/* Convert order_id to h_contract_terms */
TMH_db->preflight (TMH_db->cls);
{
uint64_t order_serial;
qs = TMH_db->lookup_contract_terms (TMH_db->cls,
hc->instance->settings.id,
order_id,
&god->contract_terms,
&order_serial);
}
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 (GNUNET_DB_STATUS_HARD_ERROR == qs);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GET_ORDERS_DB_LOOKUP_ERROR,
"database error looking up contract");
}
/* Check client provided the right hash code of the contract terms */
if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
{
struct GNUNET_HashCode h;
if (GNUNET_OK !=
TALER_JSON_contract_hash (god->contract_terms,
&h))
{
GNUNET_break (0);
GNUNET_free (god);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_INTERNAL_LOGIC_ERROR,
"Could not hash contract terms");
}
if (0 !=
GNUNET_memcmp (&h,
&god->h_contract_terms))
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_FORBIDDEN,
TALER_EC_GET_ORDER_WRONG_CONTRACT,
"Contract hash does not match order");
}
}
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
{
struct TALER_ClaimTokenP db_claim_token;
qs = TMH_db->lookup_order (TMH_db->cls,
hc->instance->settings.id,
order_id,
&db_claim_token,
&god->contract_terms);
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 (GNUNET_DB_STATUS_HARD_ERROR == qs);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GET_ORDERS_DB_LOOKUP_ERROR,
"database error looking up 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_GET_ORDERS_ID_UNKNOWN,
"order_id not found in database");
}
if (0 != GNUNET_memcmp (&db_claim_token,
&god->claim_token))
{
/* Token wrong */
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_FORBIDDEN,
TALER_EC_MERCHANT_GET_ORDER_INVALID_TOKEN,
"Claim token invalid");
}
god->unclaimed = true;
} /* end unclaimed order logic */
{
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_string ("fulfillment_url",
&god->fulfillment_url),
GNUNET_JSON_spec_end ()
};
if (GNUNET_OK !=
GNUNET_JSON_parse (god->contract_terms,
spec,
NULL, NULL))
{
GNUNET_break (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GET_ORDERS_DB_LOOKUP_ERROR,
"Merchant database error (contract terms corrupted)");
}
}
} /* end of first-time initialization / sanity checks */
if (god->unclaimed)
{
/* Order is unclaimed, no need to check for payments or even
refunds, simply always generate payment request */
return send_pay_request (god,
NULL);
}
if ( (NULL != god->session_id) &&
(NULL != god->fulfillment_url) )
{
/* Check if paid within a session. */
char *already_paid_order_id = NULL;
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_GET_ORDERS_DB_LOOKUP_ERROR,
"db error fetching pay session info");
}
if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) ||
(0 != strcmp (order_id,
already_paid_order_id)) )
{
MHD_RESULT ret;
ret = send_pay_request (god,
already_paid_order_id);
GNUNET_free (already_paid_order_id);
return ret;
}
GNUNET_break (1 == qs);
GNUNET_free (already_paid_order_id);
}
else
{
/* Check if paid regardless of session. */
struct GNUNET_HashCode 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_GET_ORDERS_DB_LOOKUP_ERROR,
"Merchant database error");
}
GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs);
GNUNET_break (0 ==
GNUNET_memcmp (&h_contract,
&god->h_contract_terms));
if (! paid)
{
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. */
rf_cleanup (god);
GNUNET_assert (GNUNET_OK == TALER_amount_get_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_GET_ORDERS_DB_LOOKUP_ERROR,
"Failed to lookup refunds for contract");
}
/* Now launch exchange interactions, unless we already have the
response in the database! */
for (struct CoinRefund *cr = god->cr_head;
NULL != cr;
cr = cr->next)
{
enum GNUNET_DB_QueryStatus qs;
qs = TMH_db->lookup_refund_proof (TMH_db->cls,
cr->refund_serial,
&cr->exchange_sig,
&cr->exchange_pub);
switch (qs)
{
case GNUNET_DB_STATUS_HARD_ERROR:
case GNUNET_DB_STATUS_SOFT_ERROR:
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GET_ORDERS_DB_LOOKUP_ERROR,
"Merchant database error");
case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
/* We need to talk to the exchange */
cr->fo = TMH_EXCHANGES_find_exchange (cr->exchange_url,
NULL,
GNUNET_NO,
&exchange_found_cb,
cr);
break;
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
/* We got a reply earlier, set status accordingly */
cr->exchange_status = MHD_HTTP_OK;
break;
}
}
if ( (god->sc.awaiting_refund) &&
( (! god->refunded) ||
(1 != TALER_amount_cmp (&god->refund_amount,
&god->sc.refund_expected)) ) )
{
/* 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 (0 != remaining.rel_value_us)
{
/* yes, indeed suspend */
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Awaiting refund exceeding %s\n",
TALER_amount2s (&god->sc.refund_expected));
suspend_god (god);
return MHD_YES;
}
}
/* Check if there are still exchange operations pending */
if (god_pending (god))
{
if (! god->suspended)
{
god->suspended = true;
MHD_suspend_connection (connection);
GNUNET_CONTAINER_DLL_insert (god_head,
god_tail,
god);
}
return MHD_YES; /* we're still talking to the exchange */
}
/* All operations done, build final response */
{
json_t *ra;
ra = json_array ();
GNUNET_assert (NULL != ra);
for (struct CoinRefund *cr = god->cr_head;
NULL != cr;
cr = cr->next)
{
json_t *refund;
if (MHD_HTTP_OK != cr->exchange_status)
{
if (NULL == cr->exchange_reply)
{
refund = json_pack ("{s:s, s:I,s:I,s:o,s:o,s:o}"
"type",
"failure",
"exchange_status",
(json_int_t) cr->exchange_status,
"rtransaction_id",
(json_int_t) cr->rtransaction_id,
"coin_pub",
GNUNET_JSON_from_data_auto (&cr->coin_pub),
"refund_amount",
TALER_JSON_from_amount (&cr->refund_amount),
"execution_time",
GNUNET_JSON_from_time_abs (cr->execution_time));
}
else
{
refund = json_pack ("{s:s,s:I,s:I,s:o,s:I,s:o,s:o,s:o}"
"type",
"failure",
"exchange_status",
(json_int_t) cr->exchange_status,
"exchange_code",
(json_int_t) cr->exchange_code,
"exchange_reply",
cr->exchange_reply,
"rtransaction_id",
(json_int_t) cr->rtransaction_id,
"coin_pub",
GNUNET_JSON_from_data_auto (&cr->coin_pub),
"refund_amount",
TALER_JSON_from_amount (&cr->refund_amount),
"execution_time",
GNUNET_JSON_from_time_abs (cr->execution_time));
}
}
else
{
refund = json_pack ("{s:s,s:I,s:o,s:o,s:I,s:o,s:o,s:o}",
"type",
"success",
"exchange_status",
(json_int_t) cr->exchange_status,
"exchange_sig",
GNUNET_JSON_from_data_auto (&cr->exchange_sig),
"exchange_pub",
GNUNET_JSON_from_data_auto (&cr->exchange_pub),
"rtransaction_id",
(json_int_t) cr->rtransaction_id,
"coin_pub",
GNUNET_JSON_from_data_auto (&cr->coin_pub),
"refund_amount",
TALER_JSON_from_amount (&cr->refund_amount),
"execution_time",
GNUNET_JSON_from_time_abs (cr->execution_time));
}
GNUNET_assert (
0 ==
json_array_append_new (ra,
refund));
}
if (god->generate_html)
{
enum GNUNET_GenericReturnValue res;
if (god->refund_available)
{
char *qr;
char *uri;
uri = make_taler_refund_uri (god->sc.con,
order_id,
hc->instance->settings.id);
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_ALLOCATION_FAILURE,
"during QR code generation");
}
{
struct KVC kvc[] = {
{ "refund_amount",
TALER_amount2s (&god->refund_amount) },
{ "refund_uri",
qr },
{ NULL, NULL }
};
res = TMH_return_from_template (god->sc.con,
MHD_HTTP_OK,
"offer_refund",
uri,
kvc);
}
GNUNET_free (uri);
GNUNET_free (qr);
}
else
{
struct KVC kvc[] = {
{ NULL, NULL }
};
res = TMH_return_from_template (god->sc.con,
MHD_HTTP_OK,
"show_order_details",
NULL,
kvc);
}
if (GNUNET_SYSERR == res)
{
GNUNET_break (0);
return MHD_NO;
}
return MHD_YES;
}
else
{
return TALER_MHD_reply_json_pack (
connection,
MHD_HTTP_OK,
"{s:b, s:o, s:o, s:o}",
"refunded", god->refunded,
"refund_amount",
TALER_JSON_from_amount (&god->refund_amount),
"refunds",
ra,
"merchant_pub",
GNUNET_JSON_from_data_auto (&hc->instance->merchant_pub));
}
}
}
/* end of taler-merchant-httpd_get-orders-ID.c */