exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

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:
Msrc/exchange/taler-exchange-httpd.c | 1+
Msrc/exchange/taler-exchange-httpd_aml-attributes-get.c | 354+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/exchange/taler-exchange-httpd_aml-attributes-get.h | 7+++++++
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