merchant

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

commit ad46f95ca5709750ad6959fd0a2ecd72e3f4ed37
parent 7d22e626f9e67a4f6ca0102f45d0b0c01a907c60
Author: Christian Grothoff <christian@grothoff.org>
Date:   Mon, 23 Feb 2026 12:05:48 +0100

fix #11036

Diffstat:
Msrc/backend/taler-merchant-httpd_private-post-account.c | 412+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/backenddb/test_merchantdb.c | 12++++++++++--
2 files changed, 279 insertions(+), 145 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_private-post-account.c b/src/backend/taler-merchant-httpd_private-post-account.c @@ -31,6 +31,116 @@ #include "taler-merchant-httpd_mfa.h" #include <regex.h> +/** + * Maximum number of retries we do on serialization failures. + */ +#define MAX_RETRIES 5 + +/** + * Closure for account_cb(). + */ +struct PostAccountContext +{ + /** + * Payto URI of the account to add (from the request). + */ + struct TALER_FullPayto uri; + + /** + * Hash of the wire details (@e uri and @e salt). + * Set if @e have_same_account is true. + */ + struct TALER_MerchantWireHashP h_wire; + + /** + * Salt value used for hashing @e uri. + * Set if @e have_same_account is true. + */ + struct TALER_WireSaltP salt; + + /** + * Credit facade URL from the request. + */ + const char *credit_facade_url; + + /** + * Facade credentials from the request. + */ + const json_t *credit_facade_credentials; + + /** + * Wire subject metadata from the request. + */ + const char *extra_wire_subject_metadata; + + /** + * True if we have ANY account already and thus require MFA. + */ + bool have_any_account; + + /** + * True if we have exact match already and thus require MFA. + */ + bool have_same_account; + + /** + * True if we have an account with the same normalized payto + * already and thus the client can only do PATCH but not POST. + */ + bool have_conflicting_account; +}; + + +/** + * Callback invoked with information about a bank account. + * + * @param cls closure with a `struct PostAccountContext` + * @param merchant_priv private key of the merchant instance + * @param ad details about the account + */ +static void +account_cb ( + void *cls, + const struct TALER_MerchantPrivateKeyP *merchant_priv, + const struct TALER_MERCHANTDB_AccountDetails *ad) +{ + struct PostAccountContext *pac = cls; + + pac->have_any_account = true; + if ( (0 == TALER_full_payto_cmp (pac->uri, + ad->payto_uri) ) && + ( (pac->credit_facade_credentials == + ad->credit_facade_credentials) || + ( (NULL != pac->credit_facade_credentials) && + (NULL != ad->credit_facade_credentials) && + (1 == json_equal (pac->credit_facade_credentials, + ad->credit_facade_credentials)) ) ) && + ( (pac->extra_wire_subject_metadata == + ad->extra_wire_subject_metadata) || + ( (NULL != pac->extra_wire_subject_metadata) && + (NULL != ad->extra_wire_subject_metadata) && + (0 == strcmp (pac->extra_wire_subject_metadata, + ad->extra_wire_subject_metadata)) ) ) && + ( (pac->credit_facade_url == ad->credit_facade_url) || + ( (NULL != pac->credit_facade_url) && + (NULL != ad->credit_facade_url) && + (0 == strcmp (pac->credit_facade_url, + ad->credit_facade_url)) ) ) ) + { + pac->have_same_account = true; + pac->salt = ad->salt; + pac->h_wire = ad->h_wire; + return; + } + + if (0 == TALER_full_payto_normalize_and_cmp (pac->uri, + ad->payto_uri) ) + { + pac->have_conflicting_account = true; + return; + } +} + MHD_RESULT TMH_private_post_account (const struct TMH_RequestHandler *rh, @@ -38,28 +148,24 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, struct TMH_HandlerContext *hc) { struct TMH_MerchantInstance *mi = hc->instance; - const char *credit_facade_url = NULL; - const char *extra_wire_subject_metadata = NULL; - const json_t *credit_facade_credentials = NULL; - struct TALER_FullPayto uri; + struct PostAccountContext pac = { 0 }; struct GNUNET_JSON_Specification ispec[] = { TALER_JSON_spec_full_payto_uri ("payto_uri", - &uri), + &pac.uri), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_web_url ("credit_facade_url", - &credit_facade_url), + &pac.credit_facade_url), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("extra_wire_subject_metadata", - &extra_wire_subject_metadata), + &pac.extra_wire_subject_metadata), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_object_const ("credit_facade_credentials", - &credit_facade_credentials), + &pac.credit_facade_credentials), NULL), GNUNET_JSON_spec_end () }; - struct TMH_WireMethod *wm; { enum GNUNET_GenericReturnValue res; @@ -77,7 +183,7 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, char *err; if (NULL != - (err = TALER_payto_validate (uri))) + (err = TALER_payto_validate (pac.uri))) { MHD_RESULT mret; @@ -91,7 +197,7 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, } } if (! TALER_is_valid_subject_metadata_string ( - extra_wire_subject_metadata)) + pac.extra_wire_subject_metadata)) { GNUNET_break_op (0); return TALER_MHD_reply_with_error ( @@ -103,7 +209,7 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, { char *apt = GNUNET_strdup (TMH_allowed_payment_targets); - char *method = TALER_payto_get_method (uri.full_payto); + char *method = TALER_payto_get_method (pac.uri.full_payto); bool ok; ok = false; @@ -137,7 +243,7 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, if ( (NULL != TMH_payment_target_regex) && (0 != regexec (&TMH_payment_target_re, - uri.full_payto, + pac.uri.full_payto, 0, NULL, 0)) ) @@ -149,25 +255,25 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, "The specific account is forbidden by policy"); } - if ( (NULL == credit_facade_url) != - (NULL == credit_facade_credentials) ) + if ( (NULL == pac.credit_facade_url) != + (NULL == pac.credit_facade_credentials) ) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MISSING, - (NULL == credit_facade_url) + (NULL == pac.credit_facade_url) ? "credit_facade_url" : "credit_facade_credentials"); } - if ( (NULL != credit_facade_url) || - (NULL != credit_facade_credentials) ) + if ( (NULL != pac.credit_facade_url) || + (NULL != pac.credit_facade_credentials) ) { struct TALER_MERCHANT_BANK_AuthenticationData auth; if (GNUNET_OK != - TALER_MERCHANT_BANK_auth_parse_json (credit_facade_credentials, - credit_facade_url, + TALER_MERCHANT_BANK_auth_parse_json (pac.credit_facade_credentials, + pac.credit_facade_url, &auth)) { GNUNET_break_op (0); @@ -179,163 +285,183 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, TALER_MERCHANT_BANK_auth_free (&auth); } - /* FIXME: might want to do all of the DB interactions below in one transaction... */ + TMH_db->preflight (TMH_db->cls); + for (unsigned int retries = 0; + retries < MAX_RETRIES; + retries++) { enum GNUNET_DB_QueryStatus qs; - enum GNUNET_GenericReturnValue ret; + struct TMH_WireMethod *wm; + TMH_db->rollback (TMH_db->cls); + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "post-account")) + { + GNUNET_break (0); + break; + } qs = TMH_db->select_accounts (TMH_db->cls, mi->settings.id, - NULL, - NULL); + &account_cb, + &pac); switch (qs) { - case GNUNET_DB_STATUS_SOFT_ERROR: case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); + TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "select_accounts"); + case GNUNET_DB_STATUS_SOFT_ERROR: + continue; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* skip MFA */ break; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + + if (pac.have_same_account) + { + /* Idempotent request */ + TMH_db->rollback (TMH_db->cls); + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ( + "salt", + &pac.salt), + GNUNET_JSON_pack_data_auto ( + "h_wire", + &pac.h_wire)); + + } + + if (pac.have_conflicting_account) + { + /* Conflict, refuse request */ + TMH_db->rollback (TMH_db->cls); + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS, + pac.uri.full_payto); + } + + if (pac.have_any_account) + { + /* MFA needed */ + enum GNUNET_GenericReturnValue ret; + ret = TMH_mfa_check_simple (hc, TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, mi); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Account creation MFA check returned %d\n", (int) ret); if (GNUNET_OK != ret) { + TMH_db->rollback (TMH_db->cls); return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; } } - } - - /* convert provided payto URI into internal data structure with salts */ - wm = TMH_setup_wire_account (uri, - credit_facade_url, - credit_facade_credentials); - GNUNET_assert (NULL != wm); - { - struct TALER_MERCHANTDB_AccountDetails ad = { - .payto_uri = wm->payto_uri, - .salt = wm->wire_salt, - .instance_id = mi->settings.id, - .h_wire = wm->h_wire, - .credit_facade_url = wm->credit_facade_url, - .credit_facade_credentials = wm->credit_facade_credentials, - .extra_wire_subject_metadata = (char *) extra_wire_subject_metadata, - .active = wm->active - }; - enum GNUNET_DB_QueryStatus qs; - qs = TMH_db->insert_account (TMH_db->cls, - &ad); - switch (qs) + /* convert provided payto URI into internal data structure with salts */ + wm = TMH_setup_wire_account (pac.uri, + pac.credit_facade_url, + pac.credit_facade_credentials); + GNUNET_assert (NULL != wm); { - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* conflict: account exists */ + struct TALER_MERCHANTDB_AccountDetails ad = { + .payto_uri = wm->payto_uri, + .salt = wm->wire_salt, + .instance_id = mi->settings.id, + .h_wire = wm->h_wire, + .credit_facade_url = wm->credit_facade_url, + .credit_facade_credentials = wm->credit_facade_credentials, + .extra_wire_subject_metadata = (char *) pac.extra_wire_subject_metadata, + .active = wm->active + }; + struct GNUNET_DB_EventHeaderP es = { + .size = htons (sizeof (es)), + .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) + }; + + qs = TMH_db->insert_account (TMH_db->cls, + &ad); + switch (qs) { - struct TALER_MERCHANTDB_AccountDetails adx; - - qs = TMH_db->select_account_by_uri (TMH_db->cls, - mi->settings.id, - ad.payto_uri, - &adx); - switch (qs) - { - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - if ( (0 == TALER_full_payto_cmp (adx.payto_uri, - ad.payto_uri) ) && - ( (adx.credit_facade_credentials == - ad.credit_facade_credentials) || - ( (NULL != adx.credit_facade_credentials) && - (NULL != ad.credit_facade_credentials) && - (1 == json_equal (adx.credit_facade_credentials, - ad.credit_facade_credentials)) ) ) && - ( (adx.credit_facade_url == ad.credit_facade_url) || - ( (NULL != adx.credit_facade_url) && - (NULL != ad.credit_facade_url) && - (0 == strcmp (adx.credit_facade_url, - ad.credit_facade_url)) ) ) ) - { - TMH_wire_method_free (wm); - GNUNET_free (adx.payto_uri.full_payto); - GNUNET_free (adx.credit_facade_url); - json_decref (adx.credit_facade_credentials); - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ( - "salt", - &adx.salt), - GNUNET_JSON_pack_data_auto ( - "h_wire", - &adx.h_wire)); - } - GNUNET_free (adx.payto_uri.full_payto); - GNUNET_free (adx.credit_facade_url); - json_decref (adx.credit_facade_credentials); - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - case GNUNET_DB_STATUS_SOFT_ERROR: - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - TMH_wire_method_free (wm); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_account"); - } + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + TMH_wire_method_free (wm); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "insert_account"); + case GNUNET_DB_STATUS_SOFT_ERROR: + continue; + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + TMH_wire_method_free (wm); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_account"); } - TMH_wire_method_free (wm); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS, - uri.full_payto); - case GNUNET_DB_STATUS_SOFT_ERROR: - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - TMH_wire_method_free (wm); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_account"); - } - } - { - struct GNUNET_DB_EventHeaderP es = { - .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) - }; - - TMH_db->event_notify (TMH_db->cls, - &es, - NULL, - 0); - } - /* Finally, also update our running process */ - GNUNET_CONTAINER_DLL_insert (mi->wm_head, - mi->wm_tail, - wm); - /* Note: we may not need to do this, as we notified - about the account change above. But also hardly hurts. */ - TMH_reload_instances (mi->settings.id); - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ("salt", - &wm->wire_salt), - GNUNET_JSON_pack_data_auto ("h_wire", - &wm->h_wire)); + TMH_db->event_notify (TMH_db->cls, + &es, + NULL, + 0); + qs = TMH_db->commit (TMH_db->cls); + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + TMH_wire_method_free (wm); + continue; + case GNUNET_DB_STATUS_HARD_ERROR: + TMH_wire_method_free (wm); + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + "post-account"); + } + /* Finally, also update our running process */ + GNUNET_CONTAINER_DLL_insert (mi->wm_head, + mi->wm_tail, + wm); + /* Note: we may not need to do this, as we notified + about the account change above. But also hardly hurts. */ + TMH_reload_instances (mi->settings.id); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ("salt", + &wm-> + wire_salt + ), + GNUNET_JSON_pack_data_auto ("h_wire", + &wm->h_wire)); + } /* end retries */ + TMH_db->rollback (TMH_db->cls); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "post-accounts"); } diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c @@ -4183,6 +4183,10 @@ test_insert_transfer (const struct InstanceData *instance, const struct TransferData *transfer, enum GNUNET_DB_QueryStatus expected_result) { + bool no_instance; + bool no_account; + bool conflict; + TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_transfer (plugin->cls, instance->instance.id, @@ -4190,7 +4194,10 @@ test_insert_transfer (const struct InstanceData *instance, &transfer->wtid, &transfer->data.total_amount, account->payto_uri, - transfer->confirmed), + transfer->confirmed, + &no_instance, + &no_account, + &conflict), "Insert transfer failed\n"); return 0; } @@ -4447,7 +4454,8 @@ run_test_transfers (struct TestTransfers_Closure *cls) TEST_RET_ON_FAIL (test_insert_transfer (&cls->instance, &cls->account, &cls->transfers[0], - GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)) + ; TEST_RET_ON_FAIL (test_insert_deposit_to_transfer (&cls->instance, &cls->signkey, &cls->order,