merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit 3390f10fe27cd11766c2225b73c8ad12fd706289
parent 9f091a0583f2c493696605438b165bd11f40dcae
Author: Christian Grothoff <grothoff@gnunet.org>
Date:   Mon, 26 Jan 2026 03:34:58 +0100

improve long-polling support for GET /private/orders/, for #9955

Diffstat:
Msrc/backend/taler-merchant-httpd_post-orders-ID-claim.c | 27++++++++++++++++++++++++---
Msrc/backend/taler-merchant-httpd_post-orders-ID-pay.c | 18++++++++++++++++++
Msrc/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c | 2++
Msrc/backend/taler-merchant-httpd_private-get-orders-ID.c | 283++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/backend/taler-merchant-httpd_private-post-orders-ID-refund.c | 58+++++++++++++++++++++++++++++++++++++++-------------------
5 files changed, 278 insertions(+), 110 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-claim.c b/src/backend/taler-merchant-httpd_post-orders-ID-claim.c @@ -26,6 +26,7 @@ #include "platform.h" #include <jansson.h> #include <taler/taler_signatures.h> +#include <taler/taler_dbevents.h> #include <taler/taler_json_lib.h> #include "taler-merchant-httpd_private-get-orders.h" #include "taler-merchant-httpd_post-orders-ID-claim.h" @@ -40,7 +41,7 @@ /** * Run transaction to claim @a order_id for @a nonce. * - * @param instance_id instance to claim order at + * @param hc handler context with information about instance to claim order at * @param order_id order to claim * @param nonce nonce to use for the claim * @param claim_token the token that should be used to verify the claim @@ -53,12 +54,13 @@ * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT if the order was successfully claimed */ static enum GNUNET_DB_QueryStatus -claim_order (const char *instance_id, +claim_order (struct TMH_HandlerContext *hc, const char *order_id, const char *nonce, const struct TALER_ClaimTokenP *claim_token, json_t **contract_terms) { + const char *instance_id = hc->instance->settings.id; struct TALER_ClaimTokenP order_ct; enum GNUNET_DB_QueryStatus qs; uint64_t order_serial; @@ -179,10 +181,29 @@ claim_order (const char *instance_id, *contract_terms = NULL; return qs; } + // FIXME: unify notifications? or do we need both? TMH_notify_order_change (TMH_lookup_instance (instance_id), TMH_OSF_CLAIMED, timestamp, order_serial); + { + struct TMH_OrderPayEventP pay_eh = { + .header.size = htons (sizeof (pay_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED), + .merchant_pub = hc->instance->merchant_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying clients about status change of order %s\n", + order_id); + GNUNET_CRYPTO_hash (order_id, + strlen (order_id), + &pay_eh.h_order_id); + TMH_db->event_notify (TMH_db->cls, + &pay_eh.header, + NULL, + 0); + } qs = TMH_db->commit (TMH_db->cls); if (0 > qs) return qs; @@ -232,7 +253,7 @@ TMH_post_orders_ID_claim (const struct TMH_RequestHandler *rh, for (unsigned int i = 0; i<MAX_RETRIES; i++) { TMH_db->preflight (TMH_db->cls); - qs = claim_order (hc->instance->settings.id, + qs = claim_order (hc, order_id, nonce, &claim_token, diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -1906,6 +1906,24 @@ phase_payment_notification (struct PayContext *pc) NULL, 0); } + { + struct TMH_OrderPayEventP pay_eh = { + .header.size = htons (sizeof (pay_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED), + .merchant_pub = pc->hc->instance->merchant_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying clients about status change of order %s\n", + pc->order_id); + GNUNET_CRYPTO_hash (pc->order_id, + strlen (pc->order_id), + &pay_eh.h_order_id); + TMH_db->event_notify (TMH_db->cls, + &pay_eh.header, + NULL, + 0); + } if ( (NULL != pc->parse_pay.session_id) && (NULL != pc->check_contract.contract_terms->fulfillment_url) ) { diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c b/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c @@ -443,6 +443,8 @@ resume_kyc_with_response (struct KycContext *kc) } return; } + // FIXME: should check that client set if-not-modified header to + // our ETAG before going 304 here! kc->response_code = not_modified ? MHD_HTTP_NOT_MODIFIED : MHD_HTTP_OK; diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.c b/src/backend/taler-merchant-httpd_private-get-orders-ID.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2017-2024 Taler Systems SA + (C) 2017-2024, 2026 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -285,6 +285,16 @@ struct GetOrderRequestContext struct TALER_PrivateContractHashP h_contract_terms; /** + * Set to the Etag of a response already known to the + * client. We should only return from long-polling + * on timeout (with "Not Modified") or when the Etag + * of the response differs from what is given here. + * Only set if @a have_lp_not_etag is true. + * Set from "lp_etag" query parameter. + */ + struct GNUNET_ShortHashCode lp_not_etag; + + /** * Total amount the exchange deposited into our bank account * (confirmed or unconfirmed), excluding fees. */ @@ -363,6 +373,11 @@ struct GetOrderRequestContext bool refunded; /** + * True if @e lp_not_etag was given. + */ + bool have_lp_not_etag; + + /** * True if the order was paid. */ bool paid; @@ -543,7 +558,7 @@ phase_init (struct GetOrderRequestContext *gorc) struct TMH_HandlerContext *hc = gorc->hc; struct TMH_OrderPayEventP pay_eh = { .header.size = htons (sizeof (pay_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID), + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED), .merchant_pub = hc->instance->merchant_pub }; @@ -843,6 +858,88 @@ phase_check_paid (struct GetOrderRequestContext *gorc) /** + * Check if the @a reply satisfies the long-poll not_etag + * constraint. If so, return it as a reponse for @a gorc, + * otherwise suspend and wait for a change. + * + * @param[in,out] gorc request to handle + * @param reply body for JSON response (#MHD_HTTP_OK) + */ +static void +check_reply (struct GetOrderRequestContext *gorc, + const json_t *reply) +{ + struct GNUNET_ShortHashCode sh; + unsigned int http_response_code; + bool not_modified; + struct MHD_Response *response; + + { + char *can; + + can = TALER_JSON_canonicalize (reply); + GNUNET_assert (GNUNET_YES == + GNUNET_CRYPTO_kdf (&sh, + sizeof (sh), + "GOR-SALT", + strlen ("GOR-SALT"), + can, + strlen (can), + NULL, + 0)); + GNUNET_free (can); + } + not_modified = gorc->have_lp_not_etag && + (0 == GNUNET_memcmp (&sh, + &gorc->lp_not_etag)); + + if (not_modified && + (! GNUNET_TIME_absolute_is_past (gorc->sc.long_poll_timeout)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Status unchanged, not returning response yet\n"); + GNUNET_assert (GNUNET_NO == gorc->suspended); + /* note: not necessarily actually unpaid ... */ + GNUNET_CONTAINER_DLL_insert (gorc_head, + gorc_tail, + gorc); + gorc->phase = GOP_SUSPENDED_ON_UNPAID; + gorc->suspended = GNUNET_YES; + MHD_suspend_connection (gorc->sc.con); + return; + } + // FIXME: should check that client set if-not-modified header to + // our ETAG before going 304 here! + http_response_code = not_modified + ? MHD_HTTP_NOT_MODIFIED + : MHD_HTTP_OK; + response = TALER_MHD_make_json (reply); + { + char *etag; + + etag = GNUNET_STRINGS_data_to_string_alloc (&sh, + sizeof (sh)); + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_ETAG, + etag)); + GNUNET_free (etag); + } + + { + MHD_RESULT ret; + + ret = MHD_queue_response (gorc->sc.con, + http_response_code, + response); + MHD_destroy_response (response); + phase_end (gorc, + ret); + } +} + + +/** * Check if re-purchase detection applies to the order. * * @param[in,out] gorc order context to update @@ -855,7 +952,7 @@ phase_check_repurchase (struct GetOrderRequestContext *gorc) enum GNUNET_DB_QueryStatus qs; char *taler_pay_uri; char *order_status_url; - MHD_RESULT ret; + json_t *reply; if ( (gorc->paid) || (NULL == gorc->contract_terms->fulfillment_url) || @@ -938,9 +1035,7 @@ phase_check_repurchase (struct GetOrderRequestContext *gorc) "host")); return; } - ret = TALER_MHD_REPLY_JSON_PACK ( - gorc->sc.con, - MHD_HTTP_OK, + reply = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("taler_pay_uri", taler_pay_uri), GNUNET_JSON_pack_string ("order_status_url", @@ -963,11 +1058,13 @@ phase_check_repurchase (struct GetOrderRequestContext *gorc) gorc->contract_terms->pay_deadline), GNUNET_JSON_pack_timestamp ("creation_time", gorc->contract_terms->timestamp)); + GNUNET_free (order_status_url); GNUNET_free (taler_pay_uri); GNUNET_free (already_paid_order_id); - phase_end (gorc, - ret); + check_reply (gorc, + reply); + json_decref (reply); } @@ -980,9 +1077,7 @@ static void phase_unpaid_finish (struct GetOrderRequestContext *gorc) { struct TMH_HandlerContext *hc = gorc->hc; - char *taler_pay_uri; char *order_status_url; - MHD_RESULT ret; if (gorc->paid) { @@ -1012,49 +1107,54 @@ phase_unpaid_finish (struct GetOrderRequestContext *gorc) NULL); if (! gorc->order_only) { + json_t *reply; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Order %s claimed but not paid yet\n", hc->infix); - phase_end (gorc, - TALER_MHD_REPLY_JSON_PACK ( - gorc->sc.con, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("order_status_url", - order_status_url), - GNUNET_JSON_pack_object_incref ("contract_terms", - gorc->contract_terms_json), - GNUNET_JSON_pack_string ("order_status", - "claimed"))); + reply = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("order_status_url", + order_status_url), + GNUNET_JSON_pack_object_incref ("contract_terms", + gorc->contract_terms_json), + GNUNET_JSON_pack_string ("order_status", + "claimed")); GNUNET_free (order_status_url); + check_reply (gorc, + reply); + json_decref (reply); return; } - taler_pay_uri = TMH_make_taler_pay_uri (gorc->sc.con, - hc->infix, - gorc->session_id, - hc->instance->settings.id, - &gorc->claim_token); - ret = TALER_MHD_REPLY_JSON_PACK ( - gorc->sc.con, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("taler_pay_uri", - taler_pay_uri), - GNUNET_JSON_pack_string ("order_status_url", - order_status_url), - GNUNET_JSON_pack_string ("order_status", - "unpaid"), - /* undefined for unpaid v1 contracts */ - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("total_amount", - &gorc->contract_amount)), - GNUNET_JSON_pack_string ("summary", - gorc->contract_terms->summary), - GNUNET_JSON_pack_timestamp ("creation_time", - gorc->contract_terms->timestamp)); - GNUNET_free (taler_pay_uri); + { + char *taler_pay_uri; + + taler_pay_uri = TMH_make_taler_pay_uri (gorc->sc.con, + hc->infix, + gorc->session_id, + hc->instance->settings.id, + &gorc->claim_token); + json_t *reply; + + reply = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("taler_pay_uri", + taler_pay_uri), + GNUNET_JSON_pack_string ("order_status_url", + order_status_url), + GNUNET_JSON_pack_string ("order_status", + "unpaid"), + /* undefined for unpaid v1 contracts */ + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("total_amount", + &gorc->contract_amount)), + GNUNET_JSON_pack_string ("summary", + gorc->contract_terms->summary), + GNUNET_JSON_pack_timestamp ("creation_time", + gorc->contract_terms->timestamp)); + check_reply (gorc, + reply); + GNUNET_free (taler_pay_uri); + } GNUNET_free (order_status_url); - phase_end (gorc, - ret); - } @@ -1493,54 +1593,57 @@ phase_reply_result (struct GetOrderRequestContext *gorc) TALER_amount_is_zero (&gorc->contract_amount)); gorc->last_payment = gorc->contract_terms->timestamp; } - ret = TALER_MHD_REPLY_JSON_PACK ( - gorc->sc.con, - MHD_HTTP_OK, - // Deprecated in protocol v6. - GNUNET_JSON_pack_array_steal ("wire_reports", - json_array ()), - GNUNET_JSON_pack_uint64 ("exchange_code", - gorc->exchange_ec), - GNUNET_JSON_pack_uint64 ("exchange_http_status", - gorc->exchange_hc), - /* legacy: */ - GNUNET_JSON_pack_uint64 ("exchange_ec", - gorc->exchange_ec), - /* legacy: */ - GNUNET_JSON_pack_uint64 ("exchange_hc", - gorc->exchange_hc), - TALER_JSON_pack_amount ("deposit_total", - &gorc->deposits_total), - GNUNET_JSON_pack_object_incref ("contract_terms", - gorc->contract_terms_json), - GNUNET_JSON_pack_string ("order_status", - "paid"), - GNUNET_JSON_pack_timestamp ("last_payment", - gorc->last_payment), - GNUNET_JSON_pack_bool ("refunded", - gorc->refunded), - GNUNET_JSON_pack_bool ("wired", - gorc->wired), - GNUNET_JSON_pack_bool ("refund_pending", - gorc->refund_pending), - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("refund_amount", - &gorc->refund_amount)), - GNUNET_JSON_pack_array_steal ("wire_details", - gorc->wire_details), - GNUNET_JSON_pack_array_steal ("refund_details", - gorc->refund_details), - GNUNET_JSON_pack_string ("order_status_url", - order_status_url), - (gorc->choice_index >= 0) + { + json_t *reply; + + reply = GNUNET_JSON_PACK ( + // Deprecated in protocol v6! + GNUNET_JSON_pack_array_steal ("wire_reports", + json_array ()), + GNUNET_JSON_pack_uint64 ("exchange_code", + gorc->exchange_ec), + GNUNET_JSON_pack_uint64 ("exchange_http_status", + gorc->exchange_hc), + /* legacy: */ + GNUNET_JSON_pack_uint64 ("exchange_ec", + gorc->exchange_ec), + /* legacy: */ + GNUNET_JSON_pack_uint64 ("exchange_hc", + gorc->exchange_hc), + TALER_JSON_pack_amount ("deposit_total", + &gorc->deposits_total), + GNUNET_JSON_pack_object_incref ("contract_terms", + gorc->contract_terms_json), + GNUNET_JSON_pack_string ("order_status", + "paid"), + GNUNET_JSON_pack_timestamp ("last_payment", + gorc->last_payment), + GNUNET_JSON_pack_bool ("refunded", + gorc->refunded), + GNUNET_JSON_pack_bool ("wired", + gorc->wired), + GNUNET_JSON_pack_bool ("refund_pending", + gorc->refund_pending), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("refund_amount", + &gorc->refund_amount)), + GNUNET_JSON_pack_array_steal ("wire_details", + gorc->wire_details), + GNUNET_JSON_pack_array_steal ("refund_details", + gorc->refund_details), + GNUNET_JSON_pack_string ("order_status_url", + order_status_url), + (gorc->choice_index >= 0) ? GNUNET_JSON_pack_int64 ("choice_index", gorc->choice_index) : GNUNET_JSON_pack_end_ ()); + check_reply (gorc, + reply); + json_decref (reply); + } GNUNET_free (order_status_url); gorc->wire_details = NULL; gorc->refund_details = NULL; - phase_end (gorc, - ret); } @@ -1595,6 +1698,10 @@ TMH_private_get_orders_ID ( "allow_refunded_for_repurchase"); TALER_MHD_parse_request_timeout (connection, &gorc->sc.long_poll_timeout); + TALER_MHD_parse_request_arg_auto (connection, + "lp_not_etag", + &gorc->lp_not_etag, + gorc->have_lp_not_etag); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Starting GET /private/orders/%s processing with timeout %s\n", hc->infix, diff --git a/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.c b/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.c @@ -49,26 +49,46 @@ trigger_refund_notification ( struct TMH_HandlerContext *hc, const struct TALER_Amount *amount) { - const char *as; - struct TMH_OrderRefundEventP refund_eh = { - .header.size = htons (sizeof (refund_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_REFUND), - .merchant_pub = hc->instance->merchant_pub - }; + { + const char *as; + struct TMH_OrderRefundEventP refund_eh = { + .header.size = htons (sizeof (refund_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_REFUND), + .merchant_pub = hc->instance->merchant_pub + }; + + /* Resume clients that may wait for this refund */ + as = TALER_amount2s (amount); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Awakening clients on %s waiting for refund of no more than %s\n", + hc->infix, + as); + GNUNET_CRYPTO_hash (hc->infix, + strlen (hc->infix), + &refund_eh.h_order_id); + TMH_db->event_notify (TMH_db->cls, + &refund_eh.header, + as, + strlen (as)); + } + { + struct TMH_OrderPayEventP pay_eh = { + .header.size = htons (sizeof (pay_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED), + .merchant_pub = hc->instance->merchant_pub + }; - /* Resume clients that may wait for this refund */ - as = TALER_amount2s (amount); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Awakening clients on %s waiting for refund of no more than %s\n", - hc->infix, - as); - GNUNET_CRYPTO_hash (hc->infix, - strlen (hc->infix), - &refund_eh.h_order_id); - TMH_db->event_notify (TMH_db->cls, - &refund_eh.header, - as, - strlen (as)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying clients about status change of order %s\n", + hc->infix); + GNUNET_CRYPTO_hash (hc->infix, + strlen (hc->infix), + &pay_eh.h_order_id); + TMH_db->event_notify (TMH_db->cls, + &pay_eh.header, + NULL, + 0); + } }