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:
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)