/* 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 #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_get-orders-ID.h" #include "../mustach/mustach.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 the client requested HTML, otherwise * we generate JSON. */ bool generate_html; }; /** * Entry in a key-value array we use as the mustach closure. */ struct KVC { /** * A name, used as the key. NULL for the last entry. */ const char *name; /** * 0-terminated string value to return for @e name. */ char *value; }; /** * Entry in a key-value array we use to cache templates. */ struct TVE { /** * A name, used as the key. NULL for the last entry. */ const char *name; /** * 0-terminated (!) file data to return for @e name. */ char *value; }; /** * Head of DLL of (suspended) requests. */ static struct GetOrderData *god_head; /** * Tail of DLL of (suspended) requests. */ static struct GetOrderData *god_tail; /** * Templated loaded into RAM. */ static struct TVE *loaded; /** * Length of the #loaded array. */ static unsigned int loaded_length; /** * Function called by Mustach to enter the section @a name. * As we do not support sections, we always return 0. * * @param cls a `struct KVC[]` array * @param name section to enter * @return 0 (do not enter) */ static int m_enter (void *cls, const char *name) { (void) cls; (void) name; return 0; } /** * Function called by mustach to activate the next item in the * section. Does nothing, as we do not support sections. * * @param cls a `struct KVC[]` array * @return 0 (no next item to activate) */ static int m_next (void *cls) { (void) cls; return 0; } /** * Function called by Mustach to leave the current section. * As we do not support sections, we should never be called. * * @param cls a `struct KVC[]` array * @return 0 (not documented by mustach) */ static int m_leave (void *cls) { GNUNET_assert (0); return 0; } /** * Return the value of @a name in @a sbuf. * * @param cls a `struct KVC[]` array * @param name the value to lookup * @param[out] sbuf where to return the data * @return mustach-defined status code */ static int m_get (void *cls, const char *name, struct mustach_sbuf *sbuf) { struct KVC *kvc = cls; for (unsigned int i = 0; NULL != kvc[i].name; i++) { if (0 == strcmp (name, kvc[i].name)) { sbuf->value = kvc[i].value; sbuf->releasecb = NULL; sbuf->closure = &kvc[i]; return MUSTACH_OK; } } GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Template requires value for unexpected name `%s'\n", name); return MUSTACH_ERROR_ITEM_NOT_FOUND; } /** * Mustach callback at the end. Cleans up the @a cls. * * @param cls a `struct KVC[]` array * @param status status of mustach (ignored) */ static void m_stop (void *cls, int status) { struct KVC *kvc = cls; (void) status; for (unsigned int i = 0; NULL != kvc[i].name; i++) GNUNET_free (kvc[i].value); } /** * Our 'universal' callbacks for mustach. */ static struct mustach_itf itf = { .enter = &m_enter, .next = &m_next, .leave = &m_leave, .get = &m_get, .stop = &m_stop }; /** * Load Mustach template into memory. Note that we intentionally cache * failures, that is if we ever failed to load a template, we will never try * again. * * FIXME: support i18n: evaluate Language: header from the browser! * * @param name name of the template file to load * (MUST be a 'static' string in memory!) * @return NULL on error, otherwise the template */ static const char * load_template (const char *name) { char *fn; char *map; int fd; struct stat sb; for (unsigned int i = 0; i with inline SVG instead of
 here! */
  GNUNET_buffer_write_str (&buf,
                           "


\n
\n
\n
\n"); for (unsigned int y = 0; ywidth; y++) { GNUNET_buffer_write_str (&buf, "    "); for (unsigned int x = 0; xwidth; x++) { unsigned int off = x + y * qrc->width; GNUNET_buffer_write_fstr (&buf, "%s", (0 != (qrc->data[off] & 1)) ? "██" : "  "); } GNUNET_buffer_write_str (&buf, "    
"); } GNUNET_buffer_write_str (&buf, "
\n
\n
\n
\n

"); QRcode_free (qrc); return GNUNET_buffer_reap_str (&buf); } /** * 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://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) && (GNUNET_NO == 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) { struct MHD_Response *reply; char *qr; char *body; size_t body_size; qr = create_qrcode (taler_pay_uri); if (NULL == qr) { GNUNET_break (0); return MHD_NO; } { struct KVC kvc[] = { { "taler_pay_uri", GNUNET_strdup (taler_pay_uri) }, { "taler_pay_qrcode_svg", qr }, { "order_summary", GNUNET_strdup ("FIXME") }, { NULL, NULL } }; const char *tmpl; int eno; tmpl = load_template ("request_payment"); if (NULL == tmpl) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to load template `%s'\n", "request_payment"); return MHD_NO; // FIXME: add nicer error reply... } if (0 != (eno = mustach (tmpl, &itf, &kvc, &body, &body_size))) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "mustach failed with error %d\n", eno); return MHD_NO; // FIXME: add nicer error reply... } } reply = MHD_create_response_from_buffer (body_size, body, MHD_RESPMEM_MUST_FREE); if (NULL == reply) { GNUNET_break (0); return MHD_NO; } GNUNET_break (MHD_NO != MHD_add_response_header (reply, "Taler", taler_pay_uri)); GNUNET_break (MHD_NO != MHD_add_response_header (reply, MHD_HTTP_HEADER_CONTENT_TYPE, "text/html")); ret = MHD_queue_response (god->sc.con, MHD_HTTP_PAYMENT_REQUIRED, reply); MHD_destroy_response (reply); } 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 */ 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) { 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; } /** * 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) { int ret; struct MHD_Response *reply; char *body; size_t body_size; char *qr; qr = create_qrcode ("taler://refund/FIXME"); if (NULL == qr) { GNUNET_break (0); return MHD_NO; // FIXME: add nicer error reply... } if (god->refunded) { struct KVC kvc[] = { { "refund_amount", GNUNET_strdup (TALER_amount2s (&god->refund_amount)) }, { "refund_uri", qr }, { NULL, NULL } }; const char *tmpl; tmpl = load_template ("offer_refund"); if (NULL == tmpl) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to load template `%s'\n", "offer_refund"); return MHD_NO; // FIXME: add nicer error reply... } if (0 != mustach (tmpl, &itf, &kvc, &body, &body_size)) { GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, "mustach"); return MHD_NO; // FIXME: add nicer error reply... } } else { struct KVC kvc[] = { { NULL, NULL } }; const char *tmpl; tmpl = load_template ("show_order_details"); if (NULL == tmpl) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to load template `%s'\n", "show_order_details"); return MHD_NO; // FIXME: add nicer error reply... } if (0 != mustach (tmpl, &itf, &kvc, &body, &body_size)) { GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, "mustach"); return MHD_NO; // FIXME: add nicer error reply... } } reply = MHD_create_response_from_buffer (body_size, body, MHD_RESPMEM_MUST_FREE); if (NULL == reply) { GNUNET_break (0); return MHD_NO; } GNUNET_break (MHD_NO != MHD_add_response_header (reply, MHD_HTTP_HEADER_CONTENT_TYPE, "text/html")); ret = MHD_queue_response (god->sc.con, MHD_HTTP_OK, reply); MHD_destroy_response (reply); return ret; } 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)); } } } /** * Nicely shut down. */ void __attribute__ ((destructor)) get_orders_fini () { for (unsigned int i = 0; i