summaryrefslogtreecommitdiff
path: root/src/backend/taler-merchant-httpd_get-orders-ID.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/backend/taler-merchant-httpd_get-orders-ID.c')
-rw-r--r--src/backend/taler-merchant-httpd_get-orders-ID.c1951
1 files changed, 1044 insertions, 907 deletions
diff --git a/src/backend/taler-merchant-httpd_get-orders-ID.c b/src/backend/taler-merchant-httpd_get-orders-ID.c
index c1db5ea9..53136628 100644
--- a/src/backend/taler-merchant-httpd_get-orders-ID.c
+++ b/src/backend/taler-merchant-httpd_get-orders-ID.c
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- (C) 2014-2022 Taler Systems SA
+ (C) 2014-2024 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free Software
@@ -28,6 +28,7 @@
#include <taler/taler_templating_lib.h>
#include <taler/taler_exchange_service.h>
#include "taler-merchant-httpd_exchanges.h"
+#include "taler-merchant-httpd_helper.h"
#include "taler-merchant-httpd_get-orders-ID.h"
#include "taler-merchant-httpd_mhd.h"
#include "taler-merchant-httpd_qr.h"
@@ -39,6 +40,25 @@
/**
+ * The different phases in which we handle the request.
+ */
+enum Phase
+{
+ GOP_INIT = 0,
+ GOP_LOOKUP_TERMS = 1,
+ GOP_PARSE_CONTRACT = 2,
+ GOP_CHECK_CLIENT_ACCESS = 3,
+ GOP_CHECK_PAID = 4,
+ GOP_REDIRECT_TO_PAID_ORDER = 5,
+ GOP_HANDLE_UNPAID = 6,
+ GOP_CHECK_REFUNDED = 7,
+ GOP_RETURN_STATUS = 8,
+ GOP_RETURN_MHD_YES = 9,
+ GOP_RETURN_MHD_NO = 10
+};
+
+
+/**
* Context for the operation.
*/
struct GetOrderData
@@ -119,6 +139,22 @@ struct GetOrderData
json_t *contract_terms;
/**
+ * Merchant base URL from @e contract_terms.
+ */
+ const char *merchant_base_url;
+
+ /**
+ * Public reorder URL from @e contract_terms.
+ * Could be NULL if contract does not have one.
+ */
+ const char *public_reorder_url;
+
+ /**
+ * Total amount in contract.
+ */
+ struct TALER_Amount contract_total;
+
+ /**
* Total refunds granted for this payment. Only initialized
* if @e refunded is set to true.
*/
@@ -131,6 +167,12 @@ struct GetOrderData
struct TALER_Amount refund_taken;
/**
+ * Phase in which we currently are handling this
+ * request.
+ */
+ enum Phase phase;
+
+ /**
* Return code: #TALER_EC_NONE if successful.
*/
enum TALER_ErrorCode ec;
@@ -145,6 +187,22 @@ struct GetOrderData
enum GNUNET_GenericReturnValue suspended;
/**
+ * Set to YES if refunded orders should be included when
+ * doing repurchase detection.
+ */
+ enum TALER_EXCHANGE_YesNoAll allow_refunded_for_repurchase;
+
+ /**
+ * Set to true if the client passed 'h_contract'.
+ */
+ bool h_contract_provided;
+
+ /**
+ * Set to true if the client passed a 'claim' token.
+ */
+ bool claim_token_provided;
+
+ /**
* Set to true if we are dealing with a claimed order
* (and thus @e h_contract_terms is set, otherwise certain
* DB queries will not work).
@@ -152,7 +210,12 @@ struct GetOrderData
bool claimed;
/**
- * Set to true if this payment has been refunded and
+ * Set to true if this order was paid.
+ */
+ bool paid;
+
+ /**
+ * Set to true if this order has been refunded and
* @e refund_amount is initialized.
*/
bool refunded;
@@ -169,6 +232,35 @@ struct GetOrderData
*/
bool generate_html;
+ /**
+ * Did we parse the contract terms?
+ */
+ bool contract_parsed;
+
+ /**
+ * Set to true if the refunds found in the DB have
+ * a different currency then the main contract.
+ */
+ bool bad_refund_currency_in_db;
+
+ /**
+ * Did the hash of the contract match the contract
+ * hash supplied by the client?
+ */
+ bool contract_match;
+
+ /**
+ * True if we had a claim token and the claim token
+ * provided by the client matched our claim token.
+ */
+ bool token_match;
+
+ /**
+ * True if we found a (claimed) contract for the order,
+ * false if we had an unclaimed order.
+ */
+ bool contract_available;
+
};
@@ -202,6 +294,112 @@ TMH_force_wallet_get_order_resume (void)
/**
+ * Suspend this @a god until the trigger is satisfied.
+ *
+ * @param god request to suspend
+ */
+static void
+suspend_god (struct GetOrderData *god)
+{
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Suspending GET /orders/%s\n",
+ god->order_id);
+ /* We reset the contract terms and start by looking them up
+ again, as while we are suspended fundamental things could
+ change (such as the contract being claimed) */
+ if (NULL != god->contract_terms)
+ {
+ json_decref (god->contract_terms);
+ god->fulfillment_url = NULL;
+ god->contract_terms = NULL;
+ god->contract_parsed = false;
+ god->merchant_base_url = NULL;
+ god->public_reorder_url = NULL;
+ }
+ GNUNET_assert (! god->suspended);
+ god->contract_parsed = false;
+ god->contract_match = false;
+ god->token_match = false;
+ god->contract_available = false;
+ god->phase = GOP_LOOKUP_TERMS;
+ god->suspended = GNUNET_YES;
+ GNUNET_CONTAINER_DLL_insert (god_head,
+ god_tail,
+ god);
+ MHD_suspend_connection (god->sc.con);
+}
+
+
+/**
+ * Clean up the session state for a GET /orders/$ID request.
+ *
+ * @param cls must be a `struct GetOrderData *`
+ */
+static void
+god_cleanup (void *cls)
+{
+ struct GetOrderData *god = cls;
+
+ if (NULL != god->contract_terms)
+ {
+ json_decref (god->contract_terms);
+ god->contract_terms = NULL;
+ }
+ if (NULL != god->refund_eh)
+ {
+ TMH_db->event_listen_cancel (god->refund_eh);
+ god->refund_eh = NULL;
+ }
+ if (NULL != god->pay_eh)
+ {
+ TMH_db->event_listen_cancel (god->pay_eh);
+ god->pay_eh = NULL;
+ }
+ GNUNET_free (god);
+}
+
+
+/**
+ * Finish the request by returning @a mret as the
+ * final result.
+ *
+ * @param[in,out] god request we are processing
+ * @param mret MHD result to return
+ */
+static void
+phase_end (struct GetOrderData *god,
+ MHD_RESULT mret)
+{
+ god->phase = (MHD_YES == mret)
+ ? GOP_RETURN_MHD_YES
+ : GOP_RETURN_MHD_NO;
+}
+
+
+/**
+ * Finish the request by returning an error @a ec
+ * with HTTP status @a http_status and @a message.
+ *
+ * @param[in,out] god request we are processing
+ * @param http_status HTTP status code to return
+ * @param ec error code to return
+ * @param message human readable hint to return, can be NULL
+ */
+static void
+phase_fail (struct GetOrderData *god,
+ unsigned int http_status,
+ enum TALER_ErrorCode ec,
+ const char *message)
+{
+ phase_end (god,
+ TALER_MHD_reply_with_error (god->sc.con,
+ http_status,
+ ec,
+ message));
+}
+
+
+/**
* We have received a trigger from the database
* that we should (possibly) resume the request.
*
@@ -252,8 +450,10 @@ resume_by_event (void *cls,
{
GNUNET_break (0);
GNUNET_async_scope_restore (&old);
+ GNUNET_free (as);
return;
}
+ GNUNET_free (as);
if (GNUNET_OK !=
TALER_amount_cmp_currency (&god->sc.refund_expected,
&a))
@@ -288,261 +488,385 @@ resume_by_event (void *cls,
/**
- * Suspend this @a god until the trigger is satisfied.
+ * First phase (after request parsing).
+ * Set up long-polling.
*
- * @param god request to suspend
+ * @param[in,out] god request context
*/
static void
-suspend_god (struct GetOrderData *god)
+phase_init (struct GetOrderData *god)
{
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Suspending GET /orders/%s\n",
- god->order_id);
- if (NULL != god->contract_terms)
+ god->phase++;
+ if (god->generate_html)
+ return; /* If HTML is requested, we never actually long poll. */
+ if (! GNUNET_TIME_absolute_is_future (god->sc.long_poll_timeout))
+ return; /* long polling not requested */
+
+ if (god->sc.awaiting_refund ||
+ god->sc.awaiting_refund_obtained)
{
- json_decref (god->contract_terms);
- god->fulfillment_url = NULL;
- god->contract_terms = NULL;
+ struct TMH_OrderPayEventP refund_eh = {
+ .header.size = htons (sizeof (refund_eh)),
+ .header.type = htons (god->sc.awaiting_refund_obtained
+ ? TALER_DBEVENT_MERCHANT_REFUND_OBTAINED
+ : TALER_DBEVENT_MERCHANT_ORDER_REFUND),
+ .merchant_pub = god->hc->instance->merchant_pub
+ };
+
+ GNUNET_CRYPTO_hash (god->order_id,
+ strlen (god->order_id),
+ &refund_eh.h_order_id);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Subscribing %p to refunds on %s\n",
+ god,
+ god->order_id);
+ god->refund_eh
+ = TMH_db->event_listen (
+ TMH_db->cls,
+ &refund_eh.header,
+ GNUNET_TIME_absolute_get_remaining (
+ god->sc.long_poll_timeout),
+ &resume_by_event,
+ god);
+ }
+ {
+ struct TMH_OrderPayEventP pay_eh = {
+ .header.size = htons (sizeof (pay_eh)),
+ .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID),
+ .merchant_pub = god->hc->instance->merchant_pub
+ };
+
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Subscribing to payments on %s\n",
+ god->order_id);
+ GNUNET_CRYPTO_hash (god->order_id,
+ strlen (god->order_id),
+ &pay_eh.h_order_id);
+ god->pay_eh
+ = TMH_db->event_listen (
+ TMH_db->cls,
+ &pay_eh.header,
+ GNUNET_TIME_absolute_get_remaining (
+ god->sc.long_poll_timeout),
+ &resume_by_event,
+ god);
}
- GNUNET_assert (! god->suspended);
- god->suspended = GNUNET_YES;
- GNUNET_CONTAINER_DLL_insert (god_head,
- god_tail,
- god);
- MHD_suspend_connection (god->sc.con);
}
/**
- * Create a taler://refund/ URI for the given @a con and @a order_id
- * and @a instance_id.
+ * Lookup contract terms and check client has the
+ * right to access this order (by claim token or
+ * contract hash).
*
- * @param merchant_base_url URL to take host and path from;
- * we cannot take it from the MHD connection as a browser
- * may have changed 'http' to 'https' and we MUST be consistent
- * with what the merchant's frontend used initially
- * @param order_id the order id
- * @return corresponding taler://refund/ URI, or NULL on missing "host"
+ * @param[in,out] god request context
*/
-static char *
-make_taler_refund_uri (const char *merchant_base_url,
- const char *order_id)
+static void
+phase_lookup_terms (struct GetOrderData *god)
{
- struct GNUNET_Buffer buf = { 0 };
- char *url;
- struct GNUNET_Uri uri;
+ uint64_t order_serial;
+ struct TALER_ClaimTokenP db_claim_token;
+ enum GNUNET_DB_QueryStatus qs;
- url = GNUNET_strdup (merchant_base_url);
- if (-1 == GNUNET_uri_parse (&uri,
- url))
+ /* Convert order_id to h_contract_terms */
+ TMH_db->preflight (TMH_db->cls);
+ GNUNET_assert (NULL == god->contract_terms);
+ qs = TMH_db->lookup_contract_terms (
+ TMH_db->cls,
+ god->hc->instance->settings.id,
+ god->order_id,
+ &god->contract_terms,
+ &order_serial,
+ &db_claim_token);
+ if (0 > qs)
{
+ /* single, read-only SQL statements should never cause
+ serialization problems */
+ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
+ /* Always report on hard error as well to enable diagnostics */
GNUNET_break (0);
- GNUNET_free (url);
- return NULL;
+ phase_fail (god,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "lookup_contract_terms");
+ return;
}
- GNUNET_assert (NULL != order_id);
- GNUNET_buffer_write_str (&buf,
- "taler");
- if (0 == strcasecmp ("http",
- uri.scheme))
- GNUNET_buffer_write_str (&buf,
- "+http");
- GNUNET_buffer_write_str (&buf,
- "://refund/");
- GNUNET_buffer_write_str (&buf,
- uri.host);
- if (0 != uri.port)
- GNUNET_buffer_write_fstr (&buf,
- ":%u",
- (unsigned int) uri.port);
- if (NULL != uri.path)
- GNUNET_buffer_write_path (&buf,
- uri.path);
- GNUNET_buffer_write_path (&buf,
- order_id);
- GNUNET_buffer_write_path (&buf,
- ""); // Trailing slash
- GNUNET_free (url);
- return GNUNET_buffer_reap_str (&buf);
-}
-
+ /* Note: when "!ord.requireClaimToken" and the client does not provide
+ a claim token (all zeros!), then token_match==TRUE below: */
+ god->token_match
+ = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
+ && (0 == GNUNET_memcmp (&db_claim_token,
+ &god->claim_token));
-char *
-TMH_make_order_status_url (struct MHD_Connection *con,
- const char *order_id,
- const char *session_id,
- const char *instance_id,
- struct TALER_ClaimTokenP *claim_token,
- struct TALER_PrivateContractHashP *h_contract)
-{
- const char *host;
- const char *forwarded_host;
- const char *uri_path;
- struct GNUNET_Buffer buf = { 0 };
- /* Number of query parameters written so far */
- unsigned int num_qp = 0;
-
- host = MHD_lookup_connection_value (con,
- MHD_HEADER_KIND,
- MHD_HTTP_HEADER_HOST);
- forwarded_host = MHD_lookup_connection_value (con,
- MHD_HEADER_KIND,
- "X-Forwarded-Host");
- uri_path = MHD_lookup_connection_value (con,
- MHD_HEADER_KIND,
- "X-Forwarded-Prefix");
- if (NULL != forwarded_host)
- host = forwarded_host;
- if (NULL == host)
- {
- GNUNET_break (0);
- return NULL;
- }
- if (NULL != strchr (host, '/'))
+ /* Check if client provided the right hash code of the contract terms */
+ if (NULL != god->contract_terms)
{
- GNUNET_break_op (0);
- return NULL;
- }
- GNUNET_assert (NULL != instance_id);
- GNUNET_assert (NULL != order_id);
+ god->contract_available = true;
+ if (GNUNET_YES ==
+ GNUNET_is_zero (&god->h_contract_terms))
+ {
+ if (GNUNET_OK !=
+ TALER_JSON_contract_hash (god->contract_terms,
+ &god->h_contract_terms))
+ {
+ GNUNET_break (0);
+ phase_fail (god,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
+ "contract terms");
+ return;
+ }
+ }
+ else
+ {
+ struct TALER_PrivateContractHashP h;
- if (GNUNET_NO == TALER_mhd_is_https (con))
- GNUNET_buffer_write_str (&buf,
- "http://");
- else
- GNUNET_buffer_write_str (&buf,
- "https://");
- GNUNET_buffer_write_str (&buf,
- host);
- if (NULL != uri_path)
- GNUNET_buffer_write_path (&buf,
- uri_path);
- if (0 != strcmp ("default",
- instance_id))
- {
- GNUNET_buffer_write_path (&buf,
- "instances");
- GNUNET_buffer_write_path (&buf,
- instance_id);
- }
- GNUNET_buffer_write_path (&buf,
- "/orders");
- GNUNET_buffer_write_path (&buf,
- order_id);
- if ((NULL != claim_token) &&
- (GNUNET_NO == GNUNET_is_zero (claim_token)))
- {
- /* 'token=' for human readability */
- GNUNET_buffer_write_str (&buf,
- "?token=");
- GNUNET_buffer_write_data_encoded (&buf,
- (char *) claim_token,
- sizeof (*claim_token));
- num_qp++;
+ if (GNUNET_OK !=
+ TALER_JSON_contract_hash (god->contract_terms,
+ &h))
+ {
+ GNUNET_break (0);
+ phase_fail (god,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
+ "contract terms");
+ return;
+ }
+ god->contract_match = (0 ==
+ GNUNET_memcmp (&h,
+ &god->h_contract_terms));
+ if (! god->contract_match)
+ {
+ GNUNET_break_op (0);
+ phase_fail (god,
+ MHD_HTTP_FORBIDDEN,
+ TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER,
+ NULL);
+ return;
+ }
+ }
}
- if (NULL != session_id)
+ if (god->contract_available)
{
- if (num_qp > 0)
- GNUNET_buffer_write_str (&buf,
- "&session_id=");
- else
- GNUNET_buffer_write_str (&buf,
- "?session_id=");
- GNUNET_buffer_write_str (&buf,
- session_id);
- num_qp++;
+ god->claimed = true;
}
-
- if (NULL != h_contract)
+ else
{
- if (num_qp > 0)
- GNUNET_buffer_write_str (&buf,
- "&h_contract=");
- else
- GNUNET_buffer_write_str (&buf,
- "?h_contract=");
- GNUNET_buffer_write_data_encoded (&buf,
- (char *) h_contract,
- sizeof (*h_contract));
- }
-
- return GNUNET_buffer_reap_str (&buf);
+ struct TALER_ClaimTokenP db_claim_token;
+ struct TALER_MerchantPostDataHashP unused;
+ enum GNUNET_DB_QueryStatus qs;
+
+ qs = TMH_db->lookup_order (
+ TMH_db->cls,
+ god->hc->instance->settings.id,
+ god->order_id,
+ &db_claim_token,
+ &unused,
+ (NULL == god->contract_terms)
+ ? &god->contract_terms
+ : NULL);
+ if (0 > qs)
+ {
+ /* single, read-only SQL statements should never cause
+ serialization problems */
+ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
+ /* Always report on hard error as well to enable diagnostics */
+ GNUNET_break (0);
+ phase_fail (god,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "lookup_order");
+ return;
+ }
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Unknown order id given: `%s'\n",
+ god->order_id);
+ phase_fail (god,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
+ god->order_id);
+ return;
+ }
+ /* Note: when "!ord.requireClaimToken" and the client does not provide
+ a claim token (all zeros!), then token_match==TRUE below: */
+ god->token_match
+ = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) &&
+ (0 == GNUNET_memcmp (&db_claim_token,
+ &god->claim_token));
+ } /* end unclaimed order logic */
+ god->phase++;
}
-char *
-TMH_make_taler_pay_uri (struct MHD_Connection *con,
- const char *order_id,
- const char *session_id,
- const char *instance_id,
- struct TALER_ClaimTokenP *claim_token)
+/**
+ * Parse contract terms.
+ *
+ * @param[in,out] god request context
+ */
+static void
+phase_parse_contract (struct GetOrderData *god)
{
- const char *host;
- const char *forwarded_host;
- const char *uri_path;
- struct GNUNET_Buffer buf = { 0 };
+ struct GNUNET_JSON_Specification espec[] = {
+ TALER_JSON_spec_amount_any ("amount",
+ &god->contract_total),
+ TALER_JSON_spec_web_url ("merchant_base_url",
+ &god->merchant_base_url),
+ GNUNET_JSON_spec_mark_optional (
+ /* this one does NOT have to be a Web URL! */
+ GNUNET_JSON_spec_string ("fulfillment_url",
+ &god->fulfillment_url),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_web_url ("public_reorder_url",
+ &god->public_reorder_url),
+ NULL),
+ GNUNET_JSON_spec_end ()
+ };
+ enum GNUNET_GenericReturnValue res;
+ const char *ename;
+ unsigned int eline;
- host = MHD_lookup_connection_value (con,
- MHD_HEADER_KIND,
- MHD_HTTP_HEADER_HOST);
- forwarded_host = MHD_lookup_connection_value (con,
- MHD_HEADER_KIND,
- "X-Forwarded-Host");
- uri_path = MHD_lookup_connection_value (con,
- MHD_HEADER_KIND,
- "X-Forwarded-Prefix");
- if (NULL != forwarded_host)
- host = forwarded_host;
- if (NULL == host)
+ GNUNET_assert (NULL != god->contract_terms);
+ if (god->contract_parsed)
+ return; /* not sure this is possible... */
+
+ res = GNUNET_JSON_parse (god->contract_terms,
+ espec,
+ &ename,
+ &eline);
+ if (GNUNET_OK != res)
{
GNUNET_break (0);
- return NULL;
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to parse contract %s in DB at field %s\n",
+ god->order_id,
+ ename);
+ phase_fail (god,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
+ god->order_id);
+ return;
}
- if (NULL != strchr (host, '/'))
+ god->contract_parsed = true;
+ god->phase++;
+}
+
+
+/**
+ * Check that this order is unclaimed or claimed by
+ * this client.
+ *
+ * @param[in,out] god request context
+ */
+static void
+phase_check_client_access (struct GetOrderData *god)
+{
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Token match: %d, contract_available: %d, contract match: %d, claimed: %d\n",
+ god->token_match,
+ god->contract_available,
+ god->contract_match,
+ god->claimed);
+
+ if (god->claim_token_provided && ! god->token_match)
{
+ /* Authentication provided but wrong. */
GNUNET_break_op (0);
- return NULL;
+ phase_fail (god,
+ MHD_HTTP_FORBIDDEN,
+ TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN,
+ "authentication with claim token provided but wrong");
+ return;
}
- GNUNET_assert (NULL != instance_id);
- GNUNET_assert (NULL != order_id);
- GNUNET_buffer_write_str (&buf,
- "taler");
- if (GNUNET_NO == TALER_mhd_is_https (con))
- GNUNET_buffer_write_str (&buf,
- "+http");
- GNUNET_buffer_write_str (&buf,
- "://pay/");
- GNUNET_buffer_write_str (&buf,
- host);
- if (NULL != uri_path)
- GNUNET_buffer_write_path (&buf,
- uri_path);
- if (0 != strcmp ("default",
- instance_id))
+
+ if (god->h_contract_provided && ! god->contract_match)
{
- GNUNET_buffer_write_path (&buf,
- "instances");
- GNUNET_buffer_write_path (&buf,
- instance_id);
+ /* Authentication provided but wrong. */
+ GNUNET_break_op (0);
+ phase_fail (god,
+ MHD_HTTP_FORBIDDEN,
+ TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH,
+ NULL);
+ return;
}
- GNUNET_buffer_write_path (&buf,
- order_id);
- GNUNET_buffer_write_path (&buf,
- (session_id == NULL) ? "" : session_id);
- if ((NULL != claim_token) &&
- (GNUNET_NO == GNUNET_is_zero (claim_token)))
+
+ if (! (god->token_match ||
+ god->contract_match) )
{
- /* Just 'c=' because this goes into QR
- codes, so this is more compact. */
- GNUNET_buffer_write_str (&buf,
- "?c=");
- GNUNET_buffer_write_data_encoded (&buf,
- (char *) claim_token,
- sizeof (struct TALER_ClaimTokenP));
- }
- return GNUNET_buffer_reap_str (&buf);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Neither claim token nor contract matched\n");
+ /* Client has no rights to this order */
+ if (NULL == god->public_reorder_url)
+ {
+ /* We cannot give the client a new order, just fail */
+ if (! GNUNET_is_zero (&god->h_contract_terms))
+ {
+ GNUNET_break_op (0);
+ phase_fail (god,
+ MHD_HTTP_FORBIDDEN,
+ TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER,
+ NULL);
+ return;
+ }
+ GNUNET_break_op (0);
+ phase_fail (god,
+ MHD_HTTP_FORBIDDEN,
+ TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN,
+ "no 'public_reorder_url'");
+ return;
+ }
+ /* We have a fulfillment URL, redirect the client there, maybe
+ the frontend can generate a fresh order for this new customer */
+ if (god->generate_html)
+ {
+ /* Contract was claimed (maybe by another device), so this client
+ cannot get the status information. Redirect to fulfillment page,
+ where the client may be able to pickup a fresh order -- or might
+ be able authenticate via session ID */
+ struct MHD_Response *reply;
+ MHD_RESULT ret;
+
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Contract claimed, redirecting to fulfillment page for order %s\n",
+ god->order_id);
+ reply = MHD_create_response_from_buffer (0,
+ NULL,
+ MHD_RESPMEM_PERSISTENT);
+ if (NULL == reply)
+ {
+ GNUNET_break (0);
+ phase_end (god,
+ MHD_NO);
+ return;
+ }
+ GNUNET_break (MHD_YES ==
+ MHD_add_response_header (reply,
+ MHD_HTTP_HEADER_LOCATION,
+ god->public_reorder_url));
+ ret = MHD_queue_response (god->sc.con,
+ MHD_HTTP_FOUND,
+ reply);
+ MHD_destroy_response (reply);
+ phase_end (god,
+ ret);
+ return;
+ }
+ /* Need to generate JSON reply */
+ phase_end (god,
+ TALER_MHD_REPLY_JSON_PACK (
+ god->sc.con,
+ MHD_HTTP_ACCEPTED,
+ GNUNET_JSON_pack_string ("public_reorder_url",
+ god->public_reorder_url)));
+ return;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Claim token or contract matched\n");
+ god->phase++;
}
@@ -586,9 +910,9 @@ get_order_summary (const struct GetOrderData *god)
* @param already_paid_order_id if for the fulfillment URI there is
* already a paid order, this is the order ID to redirect
* the wallet to; NULL if not applicable
- * @return #MHD_YES on success
+ * @return true to exit due to suspension
*/
-static MHD_RESULT
+static bool
send_pay_request (struct GetOrderData *god,
const char *already_paid_order_id)
{
@@ -605,7 +929,7 @@ send_pay_request (struct GetOrderData *god,
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Suspending request: long polling for payment\n");
suspend_god (god);
- return MHD_YES;
+ return true;
}
/* Check if resource_id has been paid for in the same session
@@ -613,27 +937,30 @@ send_pay_request (struct GetOrderData *god,
*/
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Sending payment request\n");
- taler_pay_uri = TMH_make_taler_pay_uri (god->sc.con,
- god->order_id,
- god->session_id,
- god->hc->instance->settings.id,
- &god->claim_token);
- order_status_url = TMH_make_order_status_url (god->sc.con,
- god->order_id,
- god->session_id,
- god->hc->instance->settings.id,
- &god->claim_token,
- NULL);
+ taler_pay_uri = TMH_make_taler_pay_uri (
+ god->sc.con,
+ god->order_id,
+ god->session_id,
+ god->hc->instance->settings.id,
+ &god->claim_token);
+ order_status_url = TMH_make_order_status_url (
+ god->sc.con,
+ god->order_id,
+ god->session_id,
+ god->hc->instance->settings.id,
+ &god->claim_token,
+ NULL);
if ( (NULL == taler_pay_uri) ||
(NULL == order_status_url) )
{
GNUNET_break_op (0);
GNUNET_free (taler_pay_uri);
GNUNET_free (order_status_url);
- return TALER_MHD_reply_with_error (god->sc.con,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED,
- "host");
+ phase_fail (god,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED,
+ "host");
+ return false;
}
if (god->generate_html)
{
@@ -652,7 +979,9 @@ send_pay_request (struct GetOrderData *god,
if (NULL == reply)
{
GNUNET_break (0);
- return MHD_NO;
+ phase_end (god,
+ MHD_NO);
+ return false;
}
GNUNET_break (MHD_YES ==
MHD_add_response_header (reply,
@@ -665,7 +994,9 @@ send_pay_request (struct GetOrderData *god,
MHD_HTTP_FOUND,
reply);
MHD_destroy_response (reply);
- return ret;
+ phase_end (god,
+ ret);
+ return false;
}
}
@@ -676,7 +1007,9 @@ send_pay_request (struct GetOrderData *god,
if (NULL == qr)
{
GNUNET_break (0);
- return MHD_NO;
+ phase_end (god,
+ MHD_NO);
+ return false;
}
{
enum GNUNET_GenericReturnValue res;
@@ -691,12 +1024,13 @@ send_pay_request (struct GetOrderData *god,
qr),
GNUNET_JSON_pack_string ("order_summary",
get_order_summary (god)));
- res = TALER_TEMPLATING_reply (god->sc.con,
- MHD_HTTP_PAYMENT_REQUIRED,
- "request_payment",
- god->hc->instance->settings.id,
- taler_pay_uri,
- context);
+ res = TALER_TEMPLATING_reply (
+ god->sc.con,
+ MHD_HTTP_PAYMENT_REQUIRED,
+ "request_payment",
+ god->hc->instance->settings.id,
+ taler_pay_uri,
+ context);
if (GNUNET_SYSERR == res)
{
GNUNET_break (0);
@@ -727,7 +1061,146 @@ send_pay_request (struct GetOrderData *god,
}
GNUNET_free (taler_pay_uri);
GNUNET_free (order_status_url);
- return ret;
+ phase_end (god,
+ ret);
+ return false;
+}
+
+
+/**
+ * Check if the order has been paid.
+ *
+ * @param[in,out] god request context
+ */
+static void
+phase_check_paid (struct GetOrderData *god)
+{
+ enum GNUNET_DB_QueryStatus qs;
+ struct TALER_PrivateContractHashP h_contract;
+
+ god->paid = false;
+ qs = TMH_db->lookup_order_status (
+ TMH_db->cls,
+ god->hc->instance->settings.id,
+ god->order_id,
+ &h_contract,
+ &god->paid);
+ if (0 > qs)
+ {
+ /* Always report on hard error as well to enable diagnostics */
+ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
+ phase_fail (god,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "lookup_order_status");
+ return;
+ }
+ god->phase++;
+}
+
+
+/**
+ * Check if the client already paid for an equivalent
+ * order under this session, and if so redirect to
+ * that order.
+ *
+ * @param[in,out] god request context
+ * @return true to exit due to suspension
+ */
+static bool
+phase_redirect_to_paid_order (struct GetOrderData *god)
+{
+ if ( (NULL != god->session_id) &&
+ (NULL != god->fulfillment_url) )
+ {
+ /* Check if client paid for this fulfillment article
+ already within this session, but using a different
+ order ID. If so, redirect the client to the order
+ it already paid. Allows, for example, the case
+ where a mobile phone pays for a browser's session,
+ where the mobile phone has a different order
+ ID (because it purchased the article earlier)
+ than the one that the browser is waiting for. */
+ char *already_paid_order_id = NULL;
+ enum GNUNET_DB_QueryStatus qs;
+
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Running re-purchase detection for %s/%s\n",
+ god->session_id,
+ god->fulfillment_url);
+ qs = TMH_db->lookup_order_by_fulfillment (
+ TMH_db->cls,
+ god->hc->instance->settings.id,
+ god->fulfillment_url,
+ god->session_id,
+ TALER_EXCHANGE_YNA_NO != god->allow_refunded_for_repurchase,
+ &already_paid_order_id);
+ if (qs < 0)
+ {
+ /* single, read-only SQL statements should never cause
+ serialization problems */
+ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
+ /* Always report on hard error as well to enable diagnostics */
+ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
+ phase_fail (god,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "order by fulfillment");
+ return false;
+ }
+ if ( (! god->paid) &&
+ ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) ||
+ (0 != strcmp (god->order_id,
+ already_paid_order_id)) ) )
+ {
+ bool ret;
+
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Sending pay request for order %s (already paid: %s)\n",
+ god->order_id,
+ already_paid_order_id);
+ ret = send_pay_request (god,
+ already_paid_order_id);
+ GNUNET_free (already_paid_order_id);
+ return ret;
+ }
+ GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs);
+ GNUNET_free (already_paid_order_id);
+ }
+ god->phase++;
+ return false;
+}
+
+
+/**
+ * Check if the order has been paid, and if not
+ * request payment.
+ *
+ * @param[in,out] god request context
+ * @return true to exit due to suspension
+ */
+static bool
+phase_handle_unpaid (struct GetOrderData *god)
+{
+ if (god->paid)
+ {
+ god->phase++;
+ return false;
+ }
+ if (god->claimed)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Order claimed but unpaid, sending pay request for order %s\n",
+ god->order_id);
+ }
+ else
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Order unclaimed, sending pay request for order %s\n",
+ god->order_id);
+ }
+ return send_pay_request (god,
+ NULL);
}
@@ -768,6 +1241,16 @@ process_refunds_cb (void *cls,
TALER_B2S (coin_pub),
reason);
god->refund_pending |= pending;
+ if ( (GNUNET_OK !=
+ TALER_amount_cmp_currency (&god->refund_taken,
+ refund_amount)) ||
+ (GNUNET_OK !=
+ TALER_amount_cmp_currency (&god->refund_amount,
+ refund_amount)) )
+ {
+ god->bad_refund_currency_in_db = true;
+ return;
+ }
if (! pending)
{
GNUNET_assert (0 <=
@@ -784,31 +1267,266 @@ process_refunds_cb (void *cls,
/**
- * Clean up the session state for a GET /orders/$ID request.
+ * Check if the order has been refunded.
*
- * @param cls must be a `struct GetOrderData *`
+ * @param[in,out] god request context
+ * @return true to exit due to suspension
*/
-static void
-god_cleanup (void *cls)
+static bool
+phase_check_refunded (struct GetOrderData *god)
{
- struct GetOrderData *god = cls;
+ enum GNUNET_DB_QueryStatus qs;
- if (NULL != god->contract_terms)
+ if ( (god->sc.awaiting_refund) &&
+ (GNUNET_OK !=
+ TALER_amount_cmp_currency (&god->contract_total,
+ &god->sc.refund_expected)) )
{
- json_decref (god->contract_terms);
- god->contract_terms = NULL;
+ GNUNET_break (0);
+ phase_fail (god,
+ MHD_HTTP_CONFLICT,
+ TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH,
+ god->contract_total.currency);
+ return false;
}
- if (NULL != god->refund_eh)
+
+ /* At this point, we know the contract was paid. Let's check for
+ refunds. First, clear away refunds found from previous invocations. */
+ GNUNET_assert (GNUNET_OK ==
+ TALER_amount_set_zero (god->contract_total.currency,
+ &god->refund_amount));
+ GNUNET_assert (GNUNET_OK ==
+ TALER_amount_set_zero (god->contract_total.currency,
+ &god->refund_taken));
+ qs = TMH_db->lookup_refunds_detailed (
+ TMH_db->cls,
+ god->hc->instance->settings.id,
+ &god->h_contract_terms,
+ &process_refunds_cb,
+ god);
+ if (0 > qs)
{
- TMH_db->event_listen_cancel (god->refund_eh);
- god->refund_eh = NULL;
+ GNUNET_break (0);
+ phase_fail (god,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "lookup_refunds_detailed");
+ return false;
}
- if (NULL != god->pay_eh)
+ if (god->bad_refund_currency_in_db)
{
- TMH_db->event_listen_cancel (god->pay_eh);
- god->pay_eh = NULL;
+ GNUNET_break (0);
+ phase_fail (god,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "currency mix-up between contract price and refunds in database");
+ return false;
+ }
+ if ( ((god->sc.awaiting_refund) &&
+ ( (! god->refunded) ||
+ (1 != TALER_amount_cmp (&god->refund_amount,
+ &god->sc.refund_expected)) )) ||
+ ( (god->sc.awaiting_refund_obtained) &&
+ (god->refund_pending) ) )
+ {
+ /* Client is waiting for a refund larger than what we have, suspend
+ until timeout */
+ struct GNUNET_TIME_Relative remaining;
+
+ remaining = GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout);
+ if ( (! GNUNET_TIME_relative_is_zero (remaining)) &&
+ (! god->generate_html) )
+ {
+ /* yes, indeed suspend */
+ if (god->sc.awaiting_refund)
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Awaiting refund exceeding %s\n",
+ TALER_amount2s (&god->sc.refund_expected));
+ if (god->sc.awaiting_refund_obtained)
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Awaiting pending refunds\n");
+ suspend_god (god);
+ return true;
+ }
+ }
+ god->phase++;
+ return false;
+}
+
+
+/**
+ * Create a taler://refund/ URI for the given @a con and @a order_id
+ * and @a instance_id.
+ *
+ * @param merchant_base_url URL to take host and path from;
+ * we cannot take it from the MHD connection as a browser
+ * may have changed 'http' to 'https' and we MUST be consistent
+ * with what the merchant's frontend used initially
+ * @param order_id the order id
+ * @return corresponding taler://refund/ URI, or NULL on missing "host"
+ */
+static char *
+make_taler_refund_uri (const char *merchant_base_url,
+ const char *order_id)
+{
+ struct GNUNET_Buffer buf = { 0 };
+ char *url;
+ struct GNUNET_Uri uri;
+
+ url = GNUNET_strdup (merchant_base_url);
+ if (-1 == GNUNET_uri_parse (&uri,
+ url))
+ {
+ GNUNET_break (0);
+ GNUNET_free (url);
+ return NULL;
+ }
+ GNUNET_assert (NULL != order_id);
+ GNUNET_buffer_write_str (&buf,
+ "taler");
+ if (0 == strcasecmp ("http",
+ uri.scheme))
+ GNUNET_buffer_write_str (&buf,
+ "+http");
+ GNUNET_buffer_write_str (&buf,
+ "://refund/");
+ GNUNET_buffer_write_str (&buf,
+ uri.host);
+ if (0 != uri.port)
+ GNUNET_buffer_write_fstr (&buf,
+ ":%u",
+ (unsigned int) uri.port);
+ if (NULL != uri.path)
+ GNUNET_buffer_write_path (&buf,
+ uri.path);
+ GNUNET_buffer_write_path (&buf,
+ order_id);
+ GNUNET_buffer_write_path (&buf,
+ ""); // Trailing slash
+ GNUNET_free (url);
+ return GNUNET_buffer_reap_str (&buf);
+}
+
+
+/**
+ * Generate the order status response.
+ *
+ * @param[in,out] god request context
+ */
+static void
+phase_return_status (struct GetOrderData *god)
+{
+ /* All operations done, build final response */
+ if (! god->generate_html)
+ {
+ phase_end (god,
+ TALER_MHD_REPLY_JSON_PACK (
+ god->sc.con,
+ MHD_HTTP_OK,
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_string ("fulfillment_url",
+ god->fulfillment_url)),
+ GNUNET_JSON_pack_bool ("refunded",
+ god->refunded),
+ GNUNET_JSON_pack_bool ("refund_pending",
+ god->refund_pending),
+ TALER_JSON_pack_amount ("refund_taken",
+ &god->refund_taken),
+ TALER_JSON_pack_amount ("refund_amount",
+ &god->refund_amount)));
+ return;
+ }
+
+ if (god->refund_pending)
+ {
+ char *qr;
+ char *uri;
+
+ GNUNET_assert (NULL != god->contract_terms);
+ uri = make_taler_refund_uri (god->merchant_base_url,
+ god->order_id);
+ if (NULL == uri)
+ {
+ GNUNET_break (0);
+ phase_fail (god,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_ALLOCATION_FAILURE,
+ "refund URI");
+ return;
+ }
+ qr = TMH_create_qrcode (uri);
+ if (NULL == qr)
+ {
+ GNUNET_break (0);
+ GNUNET_free (uri);
+ phase_fail (god,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_ALLOCATION_FAILURE,
+ "qr code");
+ return;
+ }
+
+ {
+ enum GNUNET_GenericReturnValue res;
+ json_t *context;
+
+ context = GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_string ("order_summary",
+ get_order_summary (god)),
+ TALER_JSON_pack_amount ("refund_amount",
+ &god->refund_amount),
+ TALER_JSON_pack_amount ("refund_taken",
+ &god->refund_taken),
+ GNUNET_JSON_pack_string ("taler_refund_uri",
+ uri),
+ GNUNET_JSON_pack_string ("taler_refund_qrcode_svg",
+ qr));
+ res = TALER_TEMPLATING_reply (
+ god->sc.con,
+ MHD_HTTP_OK,
+ "offer_refund",
+ god->hc->instance->settings.id,
+ uri,
+ context);
+ GNUNET_break (GNUNET_OK == res);
+ json_decref (context);
+ phase_end (god,
+ (GNUNET_SYSERR == res)
+ ? MHD_NO
+ : MHD_YES);
+ }
+ GNUNET_free (uri);
+ GNUNET_free (qr);
+ return;
+ }
+
+ {
+ enum GNUNET_GenericReturnValue res;
+ json_t *context;
+
+ context = GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_object_incref ("contract_terms",
+ god->contract_terms),
+ GNUNET_JSON_pack_string ("order_summary",
+ get_order_summary (god)),
+ TALER_JSON_pack_amount ("refund_amount",
+ &god->refund_amount),
+ TALER_JSON_pack_amount ("refund_taken",
+ &god->refund_taken));
+ res = TALER_TEMPLATING_reply (
+ god->sc.con,
+ MHD_HTTP_OK,
+ "show_order_details",
+ god->hc->instance->settings.id,
+ NULL,
+ context);
+ GNUNET_break (GNUNET_OK == res);
+ json_decref (context);
+ phase_end (god,
+ (GNUNET_SYSERR == res)
+ ? MHD_NO
+ : MHD_YES);
}
- GNUNET_free (god);
}
@@ -818,14 +1536,6 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh,
struct TMH_HandlerContext *hc)
{
struct GetOrderData *god = hc->ctx;
- const char *order_id = hc->infix;
- enum GNUNET_DB_QueryStatus qs;
- bool contract_match = false;
- bool token_match = false;
- bool h_contract_provided = false;
- bool claim_token_provided = false;
- bool contract_available = false;
- const char *merchant_base_url;
(void) rh;
if (NULL == god)
@@ -835,56 +1545,27 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh,
hc->cc = &god_cleanup;
god->sc.con = connection;
god->hc = hc;
- god->order_id = order_id;
- god->generate_html = TMH_MHD_test_html_desired (connection);
-
+ god->order_id = hc->infix;
+ god->generate_html
+ = TMH_MHD_test_html_desired (connection);
/* first-time initialization / sanity checks */
- {
- const char *cts;
-
- cts = MHD_lookup_connection_value (connection,
- MHD_GET_ARGUMENT_KIND,
- "h_contract");
- if ( (NULL != cts) &&
- (GNUNET_OK !=
- GNUNET_CRYPTO_hash_from_string (cts,
- &god->h_contract_terms.hash)) )
- {
- /* cts has wrong encoding */
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "h_contract");
- }
- if (NULL != cts)
- h_contract_provided = true;
- }
-
- {
- const char *ct;
-
- ct = MHD_lookup_connection_value (connection,
- MHD_GET_ARGUMENT_KIND,
- "token");
- if ( (NULL != ct) &&
- (GNUNET_OK !=
- GNUNET_STRINGS_string_to_data (ct,
- strlen (ct),
- &god->claim_token,
- sizeof (god->claim_token))) )
- {
- /* ct has wrong encoding */
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "token");
- }
- if (NULL != ct)
- claim_token_provided = true;
- }
+ TALER_MHD_parse_request_arg_auto (connection,
+ "h_contract",
+ &god->h_contract_terms,
+ god->h_contract_provided);
+ TALER_MHD_parse_request_arg_auto (connection,
+ "token",
+ &god->claim_token,
+ god->claim_token_provided);
+ if (! (TALER_arg_to_yna (connection,
+ "allow_refunded_for_repurchase",
+ TALER_EXCHANGE_YNA_NO,
+ &god->allow_refunded_for_repurchase)) )
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "allow_refunded_for_repurchase");
god->session_id = MHD_lookup_connection_value (connection,
MHD_GET_ARGUMENT_KIND,
"session_id");
@@ -907,118 +1588,19 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh,
"Awaiting refund obtained\n");
}
+ TALER_MHD_parse_request_amount (connection,
+ "refund",
+ &god->sc.refund_expected);
+ if (TALER_amount_is_valid (&god->sc.refund_expected))
{
- const char *min_refund;
-
- min_refund = MHD_lookup_connection_value (connection,
- MHD_GET_ARGUMENT_KIND,
- "refund");
- if (NULL != min_refund)
- {
- if ( (GNUNET_OK !=
- TALER_string_to_amount (min_refund,
- &god->sc.refund_expected)) ||
- (0 != strcasecmp (god->sc.refund_expected.currency,
- TMH_currency) ) )
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "refund");
- }
- god->sc.awaiting_refund = true;
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Awaiting minimum refund of %s\n",
- min_refund);
- }
+ god->sc.awaiting_refund = true;
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Awaiting minimum refund of %s\n",
+ TALER_amount2s (&god->sc.refund_expected));
}
-
-
- /* process timeout_ms argument */
- {
- const char *long_poll_timeout_ms;
-
- long_poll_timeout_ms = MHD_lookup_connection_value (connection,
- MHD_GET_ARGUMENT_KIND,
- "timeout_ms");
- if (NULL != long_poll_timeout_ms)
- {
- unsigned int timeout_ms;
- char dummy;
-
- if (1 != sscanf (long_poll_timeout_ms,
- "%u%c",
- &timeout_ms,
- &dummy))
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "timeout_ms (must be non-negative number)");
- }
- /* If HTML is requested, we never long poll. Makes no sense */
- if (! god->generate_html)
- {
- struct GNUNET_TIME_Relative timeout;
-
- timeout = GNUNET_TIME_relative_multiply (
- GNUNET_TIME_UNIT_MILLISECONDS,
- timeout_ms);
- god->sc.long_poll_timeout
- = GNUNET_TIME_relative_to_absolute (timeout);
- if (! GNUNET_TIME_relative_is_zero (timeout))
- {
- if (god->sc.awaiting_refund ||
- god->sc.awaiting_refund_obtained)
- {
- struct TMH_OrderPayEventP refund_eh = {
- .header.size = htons (sizeof (refund_eh)),
- .header.type = htons (god->sc.awaiting_refund_obtained
- ? TALER_DBEVENT_MERCHANT_REFUND_OBTAINED
- : TALER_DBEVENT_MERCHANT_ORDER_REFUND),
- .merchant_pub = hc->instance->merchant_pub
- };
-
- GNUNET_CRYPTO_hash (god->order_id,
- strlen (god->order_id),
- &refund_eh.h_order_id);
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Subscribing %p to refunds on %s\n",
- god,
- god->order_id);
- god->refund_eh = TMH_db->event_listen (TMH_db->cls,
- &refund_eh.header,
- timeout,
- &resume_by_event,
- god);
- }
- {
- struct TMH_OrderPayEventP pay_eh = {
- .header.size = htons (sizeof (pay_eh)),
- .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID),
- .merchant_pub = hc->instance->merchant_pub
- };
-
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Subscribing to payments on %s\n",
- god->order_id);
- GNUNET_CRYPTO_hash (god->order_id,
- strlen (god->order_id),
- &pay_eh.h_order_id);
- god->pay_eh = TMH_db->event_listen (TMH_db->cls,
- &pay_eh.header,
- timeout,
- &resume_by_event,
- god);
- }
- } /* end of timeout non-zero */
- } /* end of HTML generation NOT requested */
- } /* end of timeout_ms argument provided */
- } /* end of timeout_ms argument handling */
-
- } /* end of first-time initialization / sanity checks */
+ TALER_MHD_parse_request_timeout (connection,
+ &god->sc.long_poll_timeout);
+ }
if (GNUNET_SYSERR == god->suspended)
return MHD_NO; /* we are in shutdown */
@@ -1030,494 +1612,49 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh,
god);
}
- /* Convert order_id to h_contract_terms */
- TMH_db->preflight (TMH_db->cls);
- if (NULL == god->contract_terms)
- {
- uint64_t order_serial;
- bool paid = false;
- struct TALER_ClaimTokenP db_claim_token;
-
- qs = TMH_db->lookup_contract_terms (TMH_db->cls,
- hc->instance->settings.id,
- order_id,
- &god->contract_terms,
- &order_serial,
- &paid,
- &db_claim_token);
- if (0 > qs)
- {
- /* single, read-only SQL statements should never cause
- serialization problems */
- GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
- /* Always report on hard error as well to enable diagnostics */
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- "lookup_contract_terms");
- }
-
- /* Note: when "!ord.requireClaimToken" and the client does not provide
- a claim token (all zeros!), then token_match==TRUE below: */
- token_match = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
- && (0 == GNUNET_memcmp (&db_claim_token,
- &god->claim_token));
- }
-
- /* Check if client provided the right hash code of the contract terms */
- if (NULL != god->contract_terms)
- {
- contract_available = true;
-
- if (GNUNET_YES == GNUNET_is_zero (&god->h_contract_terms))
- {
-
- if (GNUNET_OK !=
- TALER_JSON_contract_hash (god->contract_terms,
- &god->h_contract_terms))
- {
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
- "contract terms");
- }
-
- }
- else
- {
-
- struct TALER_PrivateContractHashP h;
-
- if (GNUNET_OK !=
- TALER_JSON_contract_hash (god->contract_terms,
- &h))
- {
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
- "contract terms");
- }
- contract_match = (0 ==
- GNUNET_memcmp (&h,
- &god->h_contract_terms));
- if (! contract_match)
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (
- connection,
- MHD_HTTP_FORBIDDEN,
- TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER,
- NULL);
- }
-
- }
-
- }
-
- if (contract_available)
- {
- god->claimed = true;
- }
- else
- {
- struct TALER_ClaimTokenP db_claim_token;
- struct TALER_MerchantPostDataHashP unused;
-
- qs = TMH_db->lookup_order (TMH_db->cls,
- hc->instance->settings.id,
- order_id,
- &db_claim_token,
- &unused,
- (NULL == god->contract_terms)
- ? &god->contract_terms
- : NULL);
- if (0 > qs)
- {
- /* single, read-only SQL statements should never cause
- serialization problems */
- GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
- /* Always report on hard error as well to enable diagnostics */
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- "lookup_order");
- }
- if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Unknown order id given: `%s'\n",
- order_id);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_NOT_FOUND,
- TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
- order_id);
- }
- /* Note: when "!ord.requireClaimToken" and the client does not provide
- a claim token (all zeros!), then token_match==TRUE below: */
- token_match = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) &&
- (0 == GNUNET_memcmp (&db_claim_token,
- &god->claim_token));
- } /* end unclaimed order logic */
-
- GNUNET_assert (NULL != god->contract_terms);
- merchant_base_url = json_string_value (json_object_get (god->contract_terms,
- "merchant_base_url"));
- if (NULL == merchant_base_url)
- {
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
- order_id);
- }
-
- if (NULL == god->fulfillment_url)
- god->fulfillment_url = json_string_value (json_object_get (
- god->contract_terms,
- "fulfillment_url"));
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Token match: %d, contract_available: %d, contract match: %d, claimed: %d\n",
- token_match,
- contract_available,
- contract_match,
- god->claimed);
-
- if (claim_token_provided && ! token_match)
- {
- /* Authentication provided but wrong. */
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_FORBIDDEN,
- TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN,
- "authentication with claim token provided but wrong");
- }
-
- if (h_contract_provided && ! contract_match)
- {
- /* Authentication provided but wrong. */
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_FORBIDDEN,
- TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH,
- NULL);
- }
-
- if (! (token_match ||
- contract_match) )
- {
- const char *public_reorder_url;
-
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Neither claim token nor contract matched\n");
- public_reorder_url = json_string_value (json_object_get (
- god->contract_terms,
- "public_reorder_url"));
- /* Client has no rights to this order */
- if (NULL == public_reorder_url)
- {
- /* We cannot give the client a new order, just fail */
- if (! GNUNET_is_zero (&god->h_contract_terms))
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (
- connection,
- MHD_HTTP_FORBIDDEN,
- TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER,
- NULL);
- }
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_FORBIDDEN,
- TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN,
- "no 'public_reorder_url'");
- }
- /* We have a fulfillment URL, redirect the client there, maybe
- the frontend can generate a fresh order for this new customer */
- if (god->generate_html)
- {
- /* Contract was claimed (maybe by another device), so this client
- cannot get the status information. Redirect to fulfillment page,
- where the client may be able to pickup a fresh order -- or might
- be able authenticate via session ID */
- struct MHD_Response *reply;
- MHD_RESULT ret;
-
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Contract claimed, redirecting to fulfillment page for order %s\n",
- order_id);
- reply = MHD_create_response_from_buffer (0,
- NULL,
- MHD_RESPMEM_PERSISTENT);
- if (NULL == reply)
- {
- GNUNET_break (0);
- return MHD_NO;
- }
- GNUNET_break (MHD_YES ==
- MHD_add_response_header (reply,
- MHD_HTTP_HEADER_LOCATION,
- public_reorder_url));
- ret = MHD_queue_response (connection,
- MHD_HTTP_FOUND,
- reply);
- MHD_destroy_response (reply);
- return ret;
- }
- /* Need to generate JSON reply */
- return TALER_MHD_REPLY_JSON_PACK (
- connection,
- MHD_HTTP_ACCEPTED,
- GNUNET_JSON_pack_string ("public_reorder_url",
- public_reorder_url));
- }
-
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Claim token or contract matched\n");
-
- if ( (NULL != god->session_id) &&
- (NULL != god->fulfillment_url) )
- {
- /* Check if client paid for this fulfillment article
- already within this session, but using a different
- order ID. If so, redirect the client to the order
- it already paid. Allows, for example, the case
- where a mobile phone pays for a browser's session,
- where the mobile phone has a different order
- ID (because it purchased the article earlier)
- than the one that the browser is waiting for. */
- char *already_paid_order_id = NULL;
-
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Running re-purchase detection for %s/%s\n",
- god->session_id,
- god->fulfillment_url);
- qs = TMH_db->lookup_order_by_fulfillment (TMH_db->cls,
- hc->instance->settings.id,
- god->fulfillment_url,
- god->session_id,
- &already_paid_order_id);
- if (qs < 0)
- {
- /* single, read-only SQL statements should never cause
- serialization problems */
- GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
- /* Always report on hard error as well to enable diagnostics */
- GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- "order by fulfillment");
- }
- if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) ||
- (0 != strcmp (order_id,
- already_paid_order_id)) )
- {
- MHD_RESULT ret;
-
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Sending pay request for order %s (already paid: %s)\n",
- order_id,
- already_paid_order_id);
- ret = send_pay_request (god,
- already_paid_order_id);
- GNUNET_free (already_paid_order_id);
- return ret;
- }
- GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs);
- GNUNET_free (already_paid_order_id);
- }
-
- if (! god->claimed)
+ while (1)
{
- /* Order is unclaimed, no need to check for payments or even
- refunds, simply always generate payment request */
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Order unclaimed, sending pay request for order %s\n",
- order_id);
- return send_pay_request (god,
- NULL);
- }
-
- {
- /* Check if paid. */
- struct TALER_PrivateContractHashP h_contract;
- bool paid;
-
- qs = TMH_db->lookup_order_status (TMH_db->cls,
- hc->instance->settings.id,
- order_id,
- &h_contract,
- &paid);
- if (0 >= qs)
- {
- /* Always report on hard error as well to enable diagnostics */
- GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- "lookup_order_status");
- }
- GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs);
- if (! paid)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Order claimed but unpaid, sending pay request for order %s\n",
- order_id);
- return send_pay_request (god,
- NULL);
- }
- }
-
- /* At this point, we know the contract was paid. Let's check for
- refunds. First, clear away refunds found from previous invocations. */
- GNUNET_assert (GNUNET_OK ==
- TALER_amount_set_zero (TMH_currency,
- &god->refund_amount));
- GNUNET_assert (GNUNET_OK ==
- TALER_amount_set_zero (TMH_currency,
- &god->refund_taken));
- qs = TMH_db->lookup_refunds_detailed (TMH_db->cls,
- hc->instance->settings.id,
- &god->h_contract_terms,
- &process_refunds_cb,
- god);
- if (0 > qs)
- {
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- "lookup_refunds_detailed");
- }
-
- if ( ((god->sc.awaiting_refund) &&
- ( (! god->refunded) ||
- (1 != TALER_amount_cmp (&god->refund_amount,
- &god->sc.refund_expected)) )) ||
- ( (god->sc.awaiting_refund_obtained) &&
- (god->refund_pending) ) )
- {
- /* Client is waiting for a refund larger than what we have, suspend
- until timeout */
- struct GNUNET_TIME_Relative remaining;
-
- remaining = GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout);
- if (! GNUNET_TIME_relative_is_zero (remaining))
+ "Handling request in phase %d\n",
+ (int) god->phase);
+ switch (god->phase)
{
- /* yes, indeed suspend */
- if (god->sc.awaiting_refund)
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Awaiting refund exceeding %s\n",
- TALER_amount2s (&god->sc.refund_expected));
- if (god->sc.awaiting_refund_obtained)
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Awaiting pending refunds\n");
- suspend_god (god);
+ case GOP_INIT:
+ phase_init (god);
+ break;
+ case GOP_LOOKUP_TERMS:
+ phase_lookup_terms (god);
+ break;
+ case GOP_PARSE_CONTRACT:
+ phase_parse_contract (god);
+ break;
+ case GOP_CHECK_CLIENT_ACCESS:
+ phase_check_client_access (god);
+ break;
+ case GOP_CHECK_PAID:
+ phase_check_paid (god);
+ break;
+ case GOP_REDIRECT_TO_PAID_ORDER:
+ if (phase_redirect_to_paid_order (god))
+ return MHD_YES;
+ break;
+ case GOP_HANDLE_UNPAID:
+ if (phase_handle_unpaid (god))
+ return MHD_YES;
+ break;
+ case GOP_CHECK_REFUNDED:
+ if (phase_check_refunded (god))
+ return MHD_YES;
+ break;
+ case GOP_RETURN_STATUS:
+ phase_return_status (god);
+ break;
+ case GOP_RETURN_MHD_YES:
return MHD_YES;
- }
- }
-
- /* All operations done, build final response */
- if (god->generate_html)
- {
- enum GNUNET_GenericReturnValue res;
-
- if (god->refund_pending)
- {
- char *qr;
- char *uri;
-
- GNUNET_assert (NULL != god->contract_terms);
- uri = make_taler_refund_uri (merchant_base_url,
- order_id);
- if (NULL == uri)
- {
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (god->sc.con,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_ALLOCATION_FAILURE,
- "refund URI");
- }
- qr = TMH_create_qrcode (uri);
- if (NULL == qr)
- {
- GNUNET_break (0);
- GNUNET_free (uri);
- return TALER_MHD_reply_with_error (god->sc.con,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_ALLOCATION_FAILURE,
- "qr code");
- }
- {
- json_t *context;
-
- context = GNUNET_JSON_PACK (
- GNUNET_JSON_pack_string ("order_summary",
- get_order_summary (god)),
- TALER_JSON_pack_amount ("refund_amount",
- &god->refund_amount),
- TALER_JSON_pack_amount ("refund_taken",
- &god->refund_taken),
- GNUNET_JSON_pack_string ("taler_refund_uri",
- uri),
- GNUNET_JSON_pack_string ("taler_refund_qrcode_svg",
- qr));
- res = TALER_TEMPLATING_reply (god->sc.con,
- MHD_HTTP_OK,
- "offer_refund",
- hc->instance->settings.id,
- uri,
- context);
- json_decref (context);
- }
- GNUNET_free (uri);
- GNUNET_free (qr);
- }
- else
- {
- json_t *context;
-
- context = GNUNET_JSON_PACK (
- GNUNET_JSON_pack_object_incref ("contract_terms",
- god->contract_terms),
- GNUNET_JSON_pack_string ("order_summary",
- get_order_summary (god)),
- TALER_JSON_pack_amount ("refund_amount",
- &god->refund_amount),
- TALER_JSON_pack_amount ("refund_taken",
- &god->refund_taken));
- res = TALER_TEMPLATING_reply (god->sc.con,
- MHD_HTTP_OK,
- "show_order_details",
- hc->instance->settings.id,
- NULL,
- context);
- json_decref (context);
- }
- if (GNUNET_SYSERR == res)
- {
- GNUNET_break (0);
+ case GOP_RETURN_MHD_NO:
return MHD_NO;
}
- return MHD_YES;
}
- return TALER_MHD_REPLY_JSON_PACK (
- connection,
- MHD_HTTP_OK,
- GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_string ("fulfillment_url",
- god->fulfillment_url)),
- GNUNET_JSON_pack_bool ("refunded",
- god->refunded),
- GNUNET_JSON_pack_bool ("refund_pending",
- god->refund_pending),
- TALER_JSON_pack_amount ("refund_taken",
- &god->refund_taken),
- TALER_JSON_pack_amount ("refund_amount",
- &god->refund_amount));
}