commit 8202d9e04c56ce537fc87557c77cf36a27c407d9
parent 26e7c86b255e44962246ab3addc4d25cb5b24ad9
Author: Christian Grothoff <christian@grothoff.org>
Date: Sun, 16 Nov 2025 23:35:30 +0100
towards having GET /attributes return PDFs
Diffstat:
3 files changed, 339 insertions(+), 23 deletions(-)
diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c
@@ -2735,6 +2735,7 @@ do_shutdown (void *cls)
TEH_kyc_proof_cleanup ();
TEH_kyc_start_cleanup ();
TEH_aml_decision_cleanup ();
+ TEH_handler_aml_attributes_get_cleanup ();
TALER_KYCLOGIC_kyc_done ();
TALER_MHD_daemons_destroy ();
TEH_extensions_done ();
diff --git a/src/exchange/taler-exchange-httpd_aml-attributes-get.c b/src/exchange/taler-exchange-httpd_aml-attributes-get.c
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- Copyright (C) 2024 Taler Systems SA
+ Copyright (C) 2024, 2025 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
@@ -36,6 +36,147 @@
*/
#define MAX_RECORDS 1024
+
+/**
+ * Closure for the detail_cb().
+ */
+struct ResponseContext
+{
+ /**
+ * Format of the response we are to generate.
+ */
+ enum
+ {
+ RCF_JSON,
+ RCF_PDF
+ } format;
+
+ /**
+ * Stored in a DLL while suspended.
+ */
+ struct ResponseContext *next;
+
+ /**
+ * Stored in a DLL while suspended.
+ */
+ struct ResponseContext *prev;
+
+ /**
+ * Context for this request.
+ */
+ struct TEH_RequestContext *rc;
+
+ /**
+ * Async context used to run Typst.
+ */
+ struct TALER_MHD_TypstContext *tc;
+
+ /**
+ * Response to return.
+ */
+ struct MHD_Response *response;
+
+ /**
+ * HTTP status to use with @e response.
+ */
+ unsigned int http_status;
+
+ /**
+ * Where we store the response data.
+ */
+ union
+ {
+ /**
+ * If @e format is #RCF_JSON.
+ */
+ json_t *json;
+
+ /**
+ * If @e format is #RCF_PDF.
+ */
+ struct
+ {
+
+ /**
+ * Typst forms to compile.
+ */
+ struct TALER_MHD_TypstDocument docs[MAX_RECORDS];
+
+ /**
+ * JSON data for each Typst form.
+ */
+ json_t *jdata[MAX_RECORDS];
+
+ /**
+ * Next write offset into @e docs and @e jdata.
+ */
+ size_t off;
+ } pdf;
+
+ } details;
+};
+
+
+/**
+ * DLL of requests awaiting Typst.
+ */
+static struct ResponseContext *rctx_head;
+
+/**
+ * DLL of requests awaiting Typst.
+ */
+static struct ResponseContext *rctx_tail;
+
+
+void
+TEH_handler_aml_attributes_get_cleanup ()
+{
+ struct ResponseContext *rctx;
+
+ while (NULL != (rctx = rctx_head))
+ {
+ GNUNET_CONTAINER_DLL_remove (rctx_head,
+ rctx_tail,
+ rctx);
+ MHD_resume_connection (rctx->rc->connection);
+ }
+}
+
+
+/**
+ * Free resources from @a rc
+ *
+ * @param[in] rc context to clean up
+ */
+static void
+free_rc (struct TEH_RequestContext *rc)
+{
+ struct ResponseContext *rctx = rc->rh_ctx;
+
+ if (NULL != rctx->tc)
+ {
+ TALER_MHD_typst_cancel (rctx->tc);
+ rctx->tc = NULL;
+ }
+ if (NULL != rctx->response)
+ {
+ MHD_destroy_response (rctx->response);
+ rctx->response = NULL;
+ }
+ switch (rctx->format)
+ {
+ case RCF_JSON:
+ json_decref (rctx->details.json);
+ break;
+ case RCF_PDF:
+ for (size_t i = 0; i<rctx->details.pdf.off; i++)
+ json_decref (rctx->details.pdf.jdata[i]);
+ break;
+ }
+ GNUNET_free (rctx);
+}
+
+
/**
* Return AML account attributes.
*
@@ -53,9 +194,17 @@ detail_cb (
size_t enc_attributes_size,
const void *enc_attributes)
{
- json_t *records = cls;
+ static char *datadir = NULL;
+ struct ResponseContext *rc = cls;
json_t *attrs;
+ const char *form_id;
+ if (NULL == datadir)
+ {
+ datadir = GNUNET_OS_installation_get_path (
+ TALER_EXCHANGE_project_data (),
+ GNUNET_OS_IPK_DATADIR);
+ }
attrs = TALER_CRYPTO_kyc_attributes_decrypt (&TEH_attribute_key,
enc_attributes,
enc_attributes_size);
@@ -64,19 +213,86 @@ detail_cb (
GNUNET_break (0);
return;
}
- GNUNET_assert (
- 0 ==
- json_array_append_new (
- records,
- GNUNET_JSON_PACK (
- GNUNET_JSON_pack_int64 ("rowid",
- row_id),
- GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_object_steal ("attributes",
- attrs)),
- GNUNET_JSON_pack_timestamp ("collection_time",
- collection_time)
- )));
+ form_id = json_string_value (json_object_get (attrs,
+ "FORM_ID"));
+ if (NULL == form_id)
+ {
+ GNUNET_break (0);
+ return;
+ }
+ switch (rc->format)
+ {
+ case RCF_JSON:
+ GNUNET_assert (
+ 0 ==
+ json_array_append_new (
+ rc->details.json,
+ GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_int64 ("rowid",
+ row_id),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_object_steal ("attributes",
+ attrs)),
+ GNUNET_JSON_pack_timestamp ("collection_time",
+ collection_time)
+ )));
+ break;
+ case RCF_PDF:
+ GNUNET_assert (rc->details.pdf.off < MAX_RECORDS);
+ GNUNET_assert (0 ==
+ json_object_set_new (attrs,
+ "DATADIR",
+ json_string (datadir)));
+ // FIXME: add FILE_NUMBER to attrs (=> need it in rc first!)
+ // FIXME: add VQF_MEMBER_NUMBER and possibly other meta data to attrs!
+ // (need new config option for VQF_MEMBER_NUMBER-style globals!)
+
+ // FIXME: probably should move this into a helper function,
+ // we'll need it in various places:
+ rc->details.pdf.jdata[rc->details.pdf.off]
+ = attrs;
+ rc->details.pdf.docs[rc->details.pdf.off].form_name
+ = form_id;
+ rc->details.pdf.docs[rc->details.pdf.off].data
+ = rc->details.pdf.jdata[rc->details.pdf.off];
+ rc->details.pdf.off++;
+ // FIXME: deal with attachments, use extra slots for those!
+ break;
+ }
+}
+
+
+/**
+ * Function called with the result of a #TALER_MHD_typst() operation.
+ *
+ * @param cls closure
+ * @param tr result of the operation
+ */
+static void
+pdf_cb (void *cls,
+ const struct TALER_MHD_TypstResponse *tr)
+{
+ struct ResponseContext *rctx = cls;
+
+ rctx->tc = NULL;
+ GNUNET_CONTAINER_DLL_remove (rctx_head,
+ rctx_tail,
+ rctx);
+ MHD_resume_connection (rctx->rc->connection);
+ TALER_MHD_daemon_trigger ();
+ if (TALER_EC_NONE != tr->ec)
+ {
+ rctx->http_status
+ = TALER_ErrorCode_get_http_status (tr->ec);
+ rctx->response
+ = TALER_MHD_make_error (tr->ec,
+ tr->details.hint);
+ return;
+ }
+ rctx->http_status
+ = MHD_HTTP_OK;
+ rctx->response
+ = TALER_MHD_response_from_pdf_file (tr->details.filename);
}
@@ -86,10 +302,29 @@ TEH_handler_aml_attributes_get (
const struct TALER_AmlOfficerPublicKeyP *officer_pub,
const char *const args[])
{
+ struct ResponseContext *rctx = rc->rh_ctx;
int64_t limit = -20;
uint64_t offset;
struct TALER_NormalizedPaytoHashP h_payto;
+ if (NULL == rctx)
+ {
+ rctx = GNUNET_new (struct ResponseContext);
+ rctx->rc = rc;
+ rc->rh_ctx = rctx;
+ rc->rh_cleaner = &free_rc;
+ }
+ else
+ {
+ if (NULL == rctx->response)
+ {
+ GNUNET_break (0);
+ return MHD_NO;
+ }
+ return MHD_queue_response (rc->connection,
+ rctx->http_status,
+ rctx->response);
+ }
if ( (NULL == args[0]) ||
(NULL != args[1]) )
{
@@ -124,12 +359,45 @@ TEH_handler_aml_attributes_get (
TALER_MHD_parse_request_number (rc->connection,
"offset",
&offset);
+
+ {
+ const char *mime;
+
+ mime = MHD_lookup_connection_value (rc->connection,
+ MHD_HEADER_KIND,
+ MHD_HTTP_HEADER_ACCEPT);
+ if (NULL == mime)
+ mime = "application/json";
+ if (0 == strcmp (mime,
+ "application/json"))
+ {
+ rctx->format = RCF_JSON;
+ rctx->details.json = json_array ();
+ GNUNET_assert (NULL != rctx->details.json);
+ }
+ else if (0 == strcmp (mime,
+ "application/pdf"))
+ {
+ rctx->format = RCF_PDF;
+ // FIXME: need to obtain globals about the
+ // account here first, like the FILE_NUMBER.
+ // Might just as well select current rules/etc
+ // at the same time and generate a cover page!
+ }
+ else
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD_REPLY_JSON_PACK (
+ rc->connection,
+ MHD_HTTP_NOT_ACCEPTABLE,
+ GNUNET_JSON_pack_string ("hint",
+ mime));
+ }
+ }
+
{
- json_t *details;
enum GNUNET_DB_QueryStatus qs;
- details = json_array ();
- GNUNET_assert (NULL != details);
if (limit > MAX_RECORDS)
limit = MAX_RECORDS;
if (limit < -MAX_RECORDS)
@@ -140,12 +408,11 @@ TEH_handler_aml_attributes_get (
offset,
limit,
&detail_cb,
- details);
+ rctx);
switch (qs)
{
case GNUNET_DB_STATUS_HARD_ERROR:
case GNUNET_DB_STATUS_SOFT_ERROR:
- json_decref (details);
GNUNET_break (0);
return TALER_MHD_reply_with_error (
rc->connection,
@@ -153,7 +420,6 @@ TEH_handler_aml_attributes_get (
TALER_EC_GENERIC_DB_FETCH_FAILED,
"select_aml_attributes");
case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
- json_decref (details);
return TALER_MHD_reply_static (
rc->connection,
MHD_HTTP_NO_CONTENT,
@@ -163,12 +429,54 @@ TEH_handler_aml_attributes_get (
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
break;
}
+ }
+
+ switch (rctx->format)
+ {
+ case RCF_JSON:
return TALER_MHD_REPLY_JSON_PACK (
rc->connection,
MHD_HTTP_OK,
- GNUNET_JSON_pack_array_steal ("details",
- details));
+ GNUNET_JSON_pack_array_incref ("details",
+ rctx->details.json));
+ case RCF_PDF:
+ if (0 == rctx->details.pdf.off)
+ {
+ /* Note: this case goes away if/when we generate a
+ title page about the account in the future */
+ return TALER_MHD_reply_static (
+ rc->connection,
+ MHD_HTTP_NO_CONTENT,
+ NULL,
+ NULL,
+ 0);
+ }
+ rctx->tc = TALER_MHD_typst (TEH_cfg,
+ true,
+ "exchange",
+ rctx->details.pdf.off,
+ rctx->details.pdf.docs,
+ &pdf_cb,
+ rctx);
+ if (NULL == rctx->tc)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Client requested PDF, but Typst is unavailable\n");
+ return TALER_MHD_reply_static (
+ rc->connection,
+ MHD_HTTP_NOT_IMPLEMENTED,
+ NULL,
+ NULL,
+ 0);
+ }
+ GNUNET_CONTAINER_DLL_insert (rctx_head,
+ rctx_tail,
+ rctx);
+ MHD_suspend_connection (rc->connection);
+ return MHD_YES;
}
+ GNUNET_assert (0);
+ return MHD_NO;
}
diff --git a/src/exchange/taler-exchange-httpd_aml-attributes-get.h b/src/exchange/taler-exchange-httpd_aml-attributes-get.h
@@ -42,4 +42,11 @@ TEH_handler_aml_attributes_get (
const char *const args[]);
+/**
+ * Stop async running attribute GET requests.
+ */
+void
+TEH_handler_aml_attributes_get_cleanup (void);
+
+
#endif