merchant

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

commit 43ba0c9d221a82157bbad4e4da9af313fa4f4eda
parent 2d23acc74baa3b77bd70989df7101d99e4a53b05
Author: Christian Grothoff <grothoff@gnunet.org>
Date:   Tue, 20 Jan 2026 07:50:43 +0100

add basic Paivana support (#10806)

Diffstat:
Msrc/backend/taler-merchant-httpd_post-using-templates.c | 250++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/include/taler_merchant_util.h | 55+++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/util/contract_parse.c | 21+++++++++------------
Msrc/util/template_parse.c | 47++++++++++++++++++++++++++++++++++++-----------
4 files changed, 292 insertions(+), 81 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_post-using-templates.c b/src/backend/taler-merchant-httpd_post-using-templates.c @@ -30,6 +30,8 @@ #include "taler-merchant-httpd_exchanges.h" #include "taler_merchant_util.h" #include <taler/taler_json_lib.h> +#include <regex.h> + /** * Item selected from inventory_selection. @@ -176,16 +178,6 @@ struct UseContext const char *summary; /** - * Fulfillment URL override from request, if any. - */ - const char *fulfillment_url; - - /** - * Fulfillment message override from request, if any. - */ - const char *fulfillment_message; - - /** * Amount provided by the client. */ struct TALER_Amount amount; @@ -222,6 +214,26 @@ struct UseContext } inventory; + /** + * Request details if this is a paivana instantiation. + */ + struct + { + + /** + * Target website for the request. + */ + const char *website; + + /** + * Unique client identifier, consisting of + * current time, "-", and the hash of a nonce, + * the website and the current time. + */ + const char *paivana_id; + + } paivana; + } parse_request; /** @@ -258,6 +270,11 @@ struct UseContext */ unsigned int totals_len; + /** + * Array of payment choices, used with Paviana. + */ + json_t *choices; + } compute_price; }; @@ -302,6 +319,7 @@ cleanup_use_context (void *cls) uc->parse_request.inventory.items); GNUNET_free (uc->compute_price.totals); uc->compute_price.totals_len = 0; + json_decref (uc->compute_price.choices); if (NULL != uc->ihc.cc) uc->ihc.cc (uc->ihc.ctx); GNUNET_free (uc->ihc.infix); @@ -459,7 +477,57 @@ static enum GNUNET_GenericReturnValue parse_using_templates_paivana_request ( struct UseContext *uc) { - /* FIXME: not implemented */ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("website", + &uc->parse_request.paivana.website), + GNUNET_JSON_spec_string ("paivana_id", + &uc->parse_request.paivana.paivana_id), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + struct GNUNET_HashCode sh; + unsigned long long tv; + const char *dash; + + GNUNET_assert (NULL == uc->ihc.request_body); + res = TALER_MHD_parse_json_data (uc->hc->connection, + uc->hc->request_body, + spec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + use_finalize_parse (uc, + res); + return GNUNET_SYSERR; + } + if (1 != + sscanf (uc->parse_request.paivana.paivana_id, + "%llu-", + &tv)) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "paivana_id"); + return GNUNET_SYSERR; + } + dash = strchr (uc->parse_request.paivana.paivana_id, + '-'); + GNUNET_assert (NULL != dash); + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (dash + 1, + strlen (dash + 1), + &sh, + sizeof (sh))) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "paivana_id"); + return GNUNET_SYSERR; + } return GNUNET_OK; } @@ -754,7 +822,9 @@ category_allowed (const json_t *allowed_categories, if (NULL == allowed_categories) return false; - json_array_foreach ((json_t *) allowed_categories, idx, entry) + json_array_foreach ((json_t *) allowed_categories, + idx, + entry) { uint64_t selected_id; @@ -790,7 +860,7 @@ category_allowed (const json_t *allowed_categories, static enum GNUNET_GenericReturnValue verify_using_templates_inventory (struct UseContext *uc) { - if (uc->template_contract.inventory.choose_one && + if (uc->template_contract.details.inventory.choose_one && (1 != uc->parse_request.inventory.items_len)) { GNUNET_break_op (0); @@ -800,7 +870,7 @@ verify_using_templates_inventory (struct UseContext *uc) "inventory_selection"); return GNUNET_SYSERR; } - if (uc->template_contract.inventory.selected_all) + if (uc->template_contract.details.inventory.selected_all) return GNUNET_OK; for (unsigned int i = 0; i < uc->parse_request.inventory.items_len; @@ -848,11 +918,12 @@ verify_using_templates_inventory (struct UseContext *uc) struct InventoryTemplateItemContext *item = &uc->parse_request.inventory.items[i]; - if (product_id_allowed (uc->template_contract.inventory.selected_products, + if (product_id_allowed (uc->template_contract.details.inventory. + selected_products, item->product_id)) continue; if (category_allowed ( - uc->template_contract.inventory.selected_categories, + uc->template_contract.details.inventory.selected_categories, item->num_categories, item->categories)) continue; @@ -914,8 +985,34 @@ static enum GNUNET_GenericReturnValue verify_using_templates_paivana ( struct UseContext *uc) { - /* TODO: PAIVANA include paivana-specific verification. */ - return verify_using_templates_fixed (uc); + if (NULL != uc->template_contract.details.paivana.website_regex) + { + regex_t ex; + bool allowed = false; + + if (0 != regcomp (&ex, + uc->template_contract.details.paivana.website_regex, + REG_NOSUB | REG_EXTENDED)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (0 == + regexec (&ex, + uc->parse_request.paivana.website, + 0, NULL, + 0)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Website `%s' allowed by template\n", + uc->parse_request.paivana.website); + allowed = true; + } + regfree (&ex); + if (! allowed) + return GNUNET_SYSERR; + } + return GNUNET_OK; } @@ -969,7 +1066,6 @@ handle_phase_verify ( res = verify_using_templates_fixed (uc); break; case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: - /* TODO: PAIVANA handle paivana-specific instantiation. */ res = verify_using_templates_paivana (uc); break; case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: @@ -1236,7 +1332,6 @@ handle_phase_compute_price (struct UseContext *uc) switch (uc->template_type) { case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: - case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: uc->compute_price.totals = GNUNET_new (struct TALER_Amount); uc->compute_price.totals_len @@ -1258,6 +1353,59 @@ handle_phase_compute_price (struct UseContext *uc) case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: /* handled below */ break; + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + { + const struct TALER_MERCHANT_TemplateContractPaivana *tcp + = &uc->template_contract.details.paivana; + json_t *choices; + + choices = json_array (); + GNUNET_assert (NULL != choices); + for (size_t i = 0; i < tcp->choices_len; i++) + { + /* Make deep copy, we're going to MODIFY it! */ + struct TALER_MERCHANT_ContractChoice choice + = tcp->choices[i]; + + if ( (! uc->parse_request.no_tip) && + (GNUNET_YES != + TALER_amount_cmp_currency (&choice.amount, + &uc->parse_request.tip)) ) + continue; /* tip does not match choice currency */ + if (0 > + TALER_amount_add (&choice.amount, + &choice.amount, + &uc->parse_request.tip)) + { + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, + "tip"); + return; + } + GNUNET_assert (0 == + json_array_append_new ( + choices, + TALER_MERCHANT_json_from_contract_choice (&choice, + true))); + } + if (0 == json_array_size (choices)) + { + GNUNET_break_op (0); + json_decref (choices); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_CURRENCY, + "tip"); + return; + } + uc->compute_price.choices = choices; + } + /* Note: we already did the tip and pricing + fully here, so we skip these phases. */ + uc->phase = USE_PHASE_CREATE_ORDER; + return; case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: GNUNET_assert (0); } @@ -1276,17 +1424,19 @@ handle_phase_compute_price (struct UseContext *uc) = GNUNET_new (struct TALER_Amount); uc->compute_price.totals_len = 1; - ret = compute_inventory_total (uc->parse_request.inventory.items_len, - uc->parse_request.inventory.items, - primary_currency, - uc->compute_price.totals); + ret = compute_inventory_total ( + uc->parse_request.inventory.items_len, + uc->parse_request.inventory.items, + primary_currency, + uc->compute_price.totals); } if (GNUNET_SYSERR == ret) { - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, - "calculation of currency totals failed"); + use_reply_with_error ( + uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, + "calculation of currency totals failed"); return; } if (GNUNET_NO == ret) @@ -1500,12 +1650,6 @@ create_using_templates_inventory (struct UseContext *uc) ? uc->template_contract.summary : uc->parse_request.summary), GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("fulfillment_url", - uc->parse_request.fulfillment_url)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("fulfillment_message", - uc->parse_request.fulfillment_message)), - GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_array_steal ("products", tip_products))))); } @@ -1521,7 +1665,6 @@ create_using_templates_fixed (struct UseContext *uc) { json_t *tip_products = get_tip_product (uc); - GNUNET_assert (1 == uc->compute_price.totals_len); uc->ihc.request_body = GNUNET_JSON_PACK ( GNUNET_JSON_pack_allow_null ( @@ -1539,12 +1682,6 @@ create_using_templates_fixed (struct UseContext *uc) ? uc->template_contract.summary : uc->parse_request.summary), GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("fulfillment_url", - uc->parse_request.fulfillment_url)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("fulfillment_message", - uc->parse_request.fulfillment_message)), - GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_array_steal ("products", tip_products))))); } @@ -1553,13 +1690,30 @@ create_using_templates_fixed (struct UseContext *uc) /** * Create order request for paivana templates. * - * @param uc use context + * @param[in,out] uc use context */ static void create_using_templates_paivana (struct UseContext *uc) { - /* TODO: PAIVANA include paivana-specific order creation. */ - create_using_templates_fixed (uc); + uc->ihc.request_body + = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ( + "session_id", + uc->parse_request.paivana.paivana_id), + GNUNET_JSON_pack_object_steal ( + "order", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("version", + 1), + GNUNET_JSON_pack_array_incref ("choices", + uc->compute_price.choices), + GNUNET_JSON_pack_string ( + "summary", + NULL == uc->parse_request.summary + ? uc->template_contract.summary + : uc->parse_request.summary), + GNUNET_JSON_pack_string ("fulfillment_url", + uc->parse_request.paivana.website)))); } @@ -1573,7 +1727,6 @@ handle_phase_create_order (struct UseContext *uc) create_using_templates_fixed (uc); break; case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: - /* TODO: PAIVANA handle paivana-specific instantiation. */ create_using_templates_paivana (uc); break; case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: @@ -1589,9 +1742,10 @@ handle_phase_create_order (struct UseContext *uc) /* ***************** Main handler **************** */ MHD_RESULT -TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) +TMH_post_using_templates_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) { struct UseContext *uc = hc->ctx; diff --git a/src/include/taler_merchant_util.h b/src/include/taler_merchant_util.h @@ -283,9 +283,23 @@ struct TALER_MERCHANT_TemplateContractInventory struct TALER_MERCHANT_TemplateContractPaivana { /** - * Paivana reference ID. + * Paivana website regular expression. + * NULL to allow any site. */ - const char *paivana_id; + const char *website_regex; + + /** + * Array of possible specific contracts the wallet/customer may choose + * from by selecting the respective index when signing the deposit + * confirmation. + */ + struct TALER_MERCHANT_ContractChoice *choices; + + /** + * Length of the @e choices array. + */ + unsigned int choices_len; + }; /** @@ -329,15 +343,21 @@ struct TALER_MERCHANT_TemplateContract */ struct GNUNET_TIME_Relative pay_duration; - /** - * Parsed fields for inventory templates. - */ - struct TALER_MERCHANT_TemplateContractInventory inventory; + union + { + + /** + * Parsed fields for inventory templates. + */ + struct TALER_MERCHANT_TemplateContractInventory inventory; + + /** + * Parsed fields for paivana templates. + */ + struct TALER_MERCHANT_TemplateContractPaivana paivana; + + } details; - /** - * Parsed fields for paivana templates. - */ - struct TALER_MERCHANT_TemplateContractPaivana paivana; }; /** @@ -1281,6 +1301,21 @@ TALER_MERCHANT_json_spec_cit (const char *name, /** + * Provide specification to parse given JSON array to contract terms + * choices. All fields from @a choices elements are copied. + * + * @param name name of the choices field in the JSON + * @param[out] choices where the contract choices array has to be written + * @param[out] choices_len length of the @a choices array + */ +struct GNUNET_JSON_Specification +TALER_MERCHANT_spec_choices ( + const char *name, + struct TALER_MERCHANT_ContractChoice **choices, + unsigned int *choices_len); + + +/** * Parse JSON contract terms choice input. * * @param[in] root JSON object containing choice input diff --git a/src/util/contract_parse.c b/src/util/contract_parse.c @@ -362,7 +362,12 @@ parse_choices ( GNUNET_break_op (0); return GNUNET_SYSERR; } - + if (0 == json_array_size (root)) + { + /* empty list of choices is not allowed */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } GNUNET_array_grow (*choices, *choices_len, json_array_size (root)); @@ -487,16 +492,8 @@ parse_choices ( } -/** - * Provide specification to parse given JSON array to contract terms - * choices. All fields from @a choices elements are copied. - * - * @param name name of the choices field in the JSON - * @param[out] choices where the contract choices array has to be written - * @param[out] choices_len length of the @a choices array - */ -static struct GNUNET_JSON_Specification -spec_choices ( +struct GNUNET_JSON_Specification +TALER_MERCHANT_spec_choices ( const char *name, struct TALER_MERCHANT_ContractChoice **choices, unsigned int *choices_len) @@ -1075,7 +1072,7 @@ parse_contract_v1 ( struct TALER_MERCHANT_Contract *contract) { struct GNUNET_JSON_Specification espec[] = { - spec_choices ( + TALER_MERCHANT_spec_choices ( "choices", &contract->details.v1.choices, &contract->details.v1.choices_len), diff --git a/src/util/template_parse.c b/src/util/template_parse.c @@ -26,6 +26,7 @@ #include <taler/taler_json_lib.h> #include <taler/taler_util.h> #include "taler_merchant_util.h" +#include <regex.h> enum TALER_MERCHANT_TemplateType @@ -100,23 +101,24 @@ parse_template_inventory (const json_t *template_contract, struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_bool ("request_tip", - &out->inventory.request_tip), + &out->details.inventory.request_tip), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_bool ("selected_all", - &out->inventory.selected_all), + &out->details.inventory.selected_all), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_array_const ("selected_categories", - &out->inventory.selected_categories), + &out->details.inventory.selected_categories) + , NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_array_const ("selected_products", - &out->inventory.selected_products), + &out->details.inventory.selected_products), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_bool ("choose_one", - &out->inventory.choose_one), + &out->details.inventory.choose_one), NULL), GNUNET_JSON_spec_end () }; @@ -133,12 +135,13 @@ parse_template_inventory (const json_t *template_contract, return GNUNET_SYSERR; } - if (NULL != out->inventory.selected_categories) + if (NULL != out->details.inventory.selected_categories) { const json_t *entry; size_t idx; - json_array_foreach ((json_t *) out->inventory.selected_categories, idx, + json_array_foreach ((json_t *) out->details.inventory.selected_categories, + idx, entry) { if ( (! json_is_integer (entry)) || @@ -151,12 +154,14 @@ parse_template_inventory (const json_t *template_contract, } } - if (NULL != out->inventory.selected_products) + if (NULL != out->details.inventory.selected_products) { const json_t *entry; size_t idx; - json_array_foreach ((json_t *) out->inventory.selected_products, idx, entry) + json_array_foreach ((json_t *) out->details.inventory.selected_products, + idx, + entry) { if (! json_is_string (entry)) { @@ -184,8 +189,13 @@ parse_template_paivana (const json_t *template_contract, const char **error_name) { struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("paivana_id", - &out->paivana.paivana_id), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("website_regex", + &out->details.paivana.website_regex), + NULL), + TALER_MERCHANT_spec_choices ("choices", + &out->details.paivana.choices, + &out->details.paivana.choices_len), GNUNET_JSON_spec_end () }; @@ -200,6 +210,21 @@ parse_template_paivana (const json_t *template_contract, *error_name); return GNUNET_SYSERR; } + if (NULL != out->details.paivana.website_regex) + { + regex_t ex; + + if (0 != regcomp (&ex, + out->details.paivana.website_regex, + REG_NOSUB | REG_EXTENDED)) + { + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid paivana website_regex given\n"); + return GNUNET_SYSERR; + } + regfree (&ex); + } return GNUNET_OK; }