merchant

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

commit 5c83bb2ac1de5419e735c4ba4d09bb459cd4bc0d
parent 6a997dd759fb6d408d314d580750357458cebf22
Author: Christian Grothoff <grothoff@gnunet.org>
Date:   Sun, 25 Jan 2026 15:28:57 +0900

add ETag-based long polling to /kyc endpoint

Diffstat:
Msrc/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c | 168++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 145 insertions(+), 23 deletions(-)

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 @@ -118,8 +118,7 @@ struct ExchangeKycRequest enum TALER_ErrorCode last_ec; /** - * True if this account - * cannot work at this exchange because KYC auth is + * True if this account cannot work at this exchange because KYC auth is * impossible. */ bool kyc_auth_conflict; @@ -146,7 +145,6 @@ struct ExchangeKycRequest */ bool in_aml_review; - }; @@ -204,34 +202,60 @@ struct KycContext struct ExchangeKycRequest *exchange_pending_tail; /** + * Notification handler from database on changes + * to the KYC status. + */ + struct GNUNET_DB_EventHandler *eh; + + /** * Set to the exchange URL, or NULL to not filter by - * exchange. + * exchange. "exchange_url" query parameter. */ const char *exchange_url; /** - * Notification handler from database on changes - * to the KYC status. + * How long are we willing to wait for the exchange(s)? + * Based on "timeout_ms" query parameter. */ - struct GNUNET_DB_EventHandler *eh; + struct GNUNET_TIME_Absolute timeout; /** * Set to the h_wire of the merchant account if * @a have_h_wire is true, used to filter by account. + * Set from "h_wire" query parameter. */ struct TALER_MerchantWireHashP h_wire; /** - * How long are we willing to wait for the exchange(s)? + * 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_TIME_Absolute timeout; + struct GNUNET_ShortHashCode lp_not_etag; /** - * HTTP status code to use for the reply, i.e 200 for "OK". - * Special value UINT_MAX is used to indicate hard errors - * (no reply, return #MHD_NO). + * Specifies what status change we are long-polling for. If specified, the + * endpoint will only return once the status *matches* the given value. If + * multiple accounts or exchanges match the query, any account reaching the + * STATUS will cause the response to be returned. + * + * FIXME: not yet used! */ - unsigned int response_code; + const char *lp_status; + + /** + * Specifies what status change we are long-polling for. If specified, the + * endpoint will only return once the status no longer matches the given + * value. If multiple accounts or exchanges *no longer matches* the given + * STATUS will cause the response to be returned. + * + + * FIXME: not yet used! + */ + const char *lp_not_status; /** * #GNUNET_NO if the @e connection was not suspended, @@ -242,16 +266,28 @@ struct KycContext enum GNUNET_GenericReturnValue suspended; /** - * What state are we long-polling for? + * What state are we long-polling for? "lpt" argument. */ enum TALER_EXCHANGE_KycLongPollTarget lpt; /** + * HTTP status code to use for the reply, i.e 200 for "OK". + * Special value UINT_MAX is used to indicate hard errors + * (no reply, return #MHD_NO). + */ + unsigned int response_code; + + /** * True if @e h_wire was given. */ bool have_h_wire; /** + * True if @e lp_not_etag was given. + */ + bool have_lp_not_etag; + + /** * We're still waiting on the exchange to determine * the KYC status of our deposit(s). */ @@ -344,10 +380,56 @@ kyc_context_cleanup (void *cls) static void resume_kyc_with_response (struct KycContext *kc) { - kc->response_code = MHD_HTTP_OK; + struct GNUNET_ShortHashCode sh; + bool not_modified; + + { + char *can; + + can = TALER_JSON_canonicalize (kc->kycs_data); + GNUNET_assert (GNUNET_YES == + GNUNET_CRYPTO_kdf (&sh, + sizeof (sh), + "KYC-SALT", + strlen ("KYC-SALT"), + can, + strlen (can), + NULL, + 0)); + GNUNET_free (can); + } + not_modified = kc->have_lp_not_etag && + (0 == GNUNET_memcmp (&sh, + &kc->lp_not_etag)); + if (not_modified && + (! GNUNET_TIME_absolute_is_past (kc->timeout)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Status unchanged, not returning response yet\n"); + if (GNUNET_NO == kc->suspended) + { + MHD_suspend_connection (kc->connection); + kc->suspended = GNUNET_YES; + } + return; + } + kc->response_code = not_modified + ? MHD_HTTP_NOT_MODIFIED + : MHD_HTTP_OK; kc->response = TALER_MHD_MAKE_JSON_PACK ( GNUNET_JSON_pack_array_incref ("kyc_data", kc->kycs_data)); + { + char *etag; + + etag = GNUNET_STRINGS_data_to_string_alloc (&sh, + sizeof (sh)); + GNUNET_break (MHD_YES == + MHD_add_response_header (kc->response, + MHD_HTTP_HEADER_ETAG, + etag)); + GNUNET_free (etag); + } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Resuming /kyc handling as exchange interaction is done (%u)\n", MHD_HTTP_OK); @@ -1150,14 +1232,24 @@ get_instances_ID_kyc ( TALER_EC_GENERIC_PARAMETER_MALFORMED, "exchange_url must be a valid HTTP(s) URL"); } - + kc->lp_status = MHD_lookup_connection_value ( + connection, + MHD_GET_ARGUMENT_KIND, + "lp_status"); + kc->lp_not_status = MHD_lookup_connection_value ( + connection, + MHD_GET_ARGUMENT_KIND, + "lp_not_status"); TALER_MHD_parse_request_arg_auto (connection, "h_wire", &kc->h_wire, kc->have_h_wire); + TALER_MHD_parse_request_arg_auto (connection, + "lp_not_etag", + &kc->lp_not_etag, + kc->have_lp_not_etag); - if ( (TALER_EXCHANGE_KLPT_NONE != kc->lpt) && - (! GNUNET_TIME_absolute_is_past (kc->timeout)) ) + if (! GNUNET_TIME_absolute_is_past (kc->timeout)) { if (kc->have_h_wire) { @@ -1242,13 +1334,43 @@ get_instances_ID_kyc ( } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { + /* We use an Etag of all zeros for the 204 status code */ + static struct GNUNET_ShortHashCode zero_etag; + /* no matching accounts, could not have suspended */ GNUNET_assert (GNUNET_NO == kc->suspended); - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); + if (kc->have_lp_not_etag && + (0 == GNUNET_memcmp (&zero_etag, + &kc->lp_not_etag)) && + (! GNUNET_TIME_absolute_is_past (kc->timeout)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No matching accounts, suspending to wait for this to change\n"); + MHD_suspend_connection (kc->connection); + kc->suspended = GNUNET_YES; + return MHD_YES; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No matching accounts, returning empty response\n"); + kc->response_code = MHD_HTTP_NO_CONTENT; + kc->response = MHD_create_response_from_buffer_static (0, + NULL); + TALER_MHD_add_global_headers (kc->response, + false); + { + char *etag; + + etag = GNUNET_STRINGS_data_to_string_alloc (&zero_etag, + sizeof (zero_etag)); + GNUNET_break (MHD_YES == + MHD_add_response_header (kc->response, + MHD_HTTP_HEADER_ETAG, + etag)); + GNUNET_free (etag); + } + return MHD_queue_response (connection, + kc->response_code, + kc->response); } } if (GNUNET_YES == kc->suspended)