merchant

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

commit 00a03bf279e21ce4e87fbc0dc54fef37de84c297
parent 0b8e550d09635f762033626c8c12b7b4a0d0faf7
Author: Christian Grothoff <christian@grothoff.org>
Date:   Fri, 17 Apr 2020 15:11:16 +0200

sketch instance loading

Diffstat:
Msrc/backend/taler-merchant-httpd.c | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/backend/taler-merchant-httpd.h | 75+++++++++++++++++++++++++--------------------------------------------------
Msrc/backend/taler-merchant-httpd_config.h | 2+-
Msrc/backend/taler-merchant-httpd_mhd.c | 2+-
Msrc/backenddb/merchant-0001.sql | 12+++++++++++-
Msrc/include/taler_merchant_service.h | 10----------
Msrc/include/taler_merchantdb_plugin.h | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/merchant_api_config.c | 84++-----------------------------------------------------------------------------
Msrc/lib/test_merchant_api.c | 2++
Msrc/lib/test_merchant_api.conf | 88++++++++-----------------------------------------------------------------------
10 files changed, 309 insertions(+), 241 deletions(-)

diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -113,22 +113,18 @@ static const struct GNUNET_CONFIGURATION_Handle *cfg; /** - * Callback that frees all the instances in the hashmap + * Decrement reference counter of @a mi, and free if it hits zero. * - * @param cls closure, NULL - * @param key current key - * @param value a `struct TMH_MerchantInstance` + * @param[in,out] mi merchant instance to update and possibly free */ -static int -instance_free (void *cls, - const struct GNUNET_HashCode *key, - void *value) +void +TMH_instance_decref (struct TMH_MerchantInstance *mi) { - struct TMH_MerchantInstance *mi = value; struct TMH_WireMethod *wm; - (void) cls; - (void) key; + mi->rc--; + if (0 != mi->rc) + return; while (NULL != (wm = (mi->wm_head))) { GNUNET_CONTAINER_DLL_remove (mi->wm_head, @@ -139,11 +135,31 @@ instance_free (void *cls, GNUNET_free (wm); } - GNUNET_free (mi->id); - GNUNET_free (mi->keyfile); - GNUNET_free (mi->name); - GNUNET_free_non_null (mi->tip_exchange); + GNUNET_free (mi->settings.id); + GNUNET_free (mi->settings.name); + json_decref (mi->settings.location); + json_decref (mi->settings.jurisdiction); GNUNET_free (mi); +} + + +/** + * Callback that frees all the instances in the hashmap + * + * @param cls closure, NULL + * @param key current key + * @param value a `struct TMH_MerchantInstance` + */ +static int +instance_free_cb (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + struct TMH_MerchantInstance *mi = value; + + (void) cls; + (void) key; + TMH_instance_decref (mi); return GNUNET_YES; } @@ -430,7 +446,7 @@ do_shutdown (void *cls) if (NULL != by_id_map) { GNUNET_CONTAINER_multihashmap_iterate (by_id_map, - &instance_free, + &instance_free_cb, NULL); GNUNET_CONTAINER_multihashmap_destroy (by_id_map); by_id_map = NULL; @@ -612,6 +628,39 @@ lookup_instance (const char *instance_id) /** + * Add instance definition to our active set of instances. + * + * @param[in,out] mi merchant instance details to define + * @return #GNUNET_OK on success, #GNUNET_NO if the same ID is in use already + */ +int +TMH_add_instance (struct TMH_MerchantInstance *mi) +{ + struct GNUNET_HashCode h_instance; + const char *id; + int ret; + + id = mi->id; + if (NULL == id) + id = "default"; + GNUNET_CRYPTO_hash (id, + strlen (id), + &h_instance); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Looking for by-id key %s of `%s' in hashmap\n", + GNUNET_h2s (&h_instance), + id); + ret = GNUNET_CONTAINER_multihashmap_put (by_id_map, + &h_instance, + mi, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY); + if (GNUNET_OK == ret) + mi->rc++; + return ret; +} + + +/** * A client has requested the given url using the given method * (#MHD_HTTP_METHOD_GET, #MHD_HTTP_METHOD_PUT, * #MHD_HTTP_METHOD_DELETE, #MHD_HTTP_METHOD_POST, etc). The callback @@ -939,7 +988,8 @@ url_handler (void *cls, if (hc->is_post) { /* FIXME: Maybe check for maximum upload size here - and refuse if it is too big? */ + and refuse if it is too big? (Note: maximum upload + size may need to vary based on the handler.) */ GNUNET_break (NULL == hc->json); /* can't have it already */ return MHD_YES; /* proceed with upload */ @@ -951,6 +1001,58 @@ url_handler (void *cls, /** + * Function called during startup to add all known instances to our + * hash map in memory for faster lookups when we receive requests. + * + * @param cls closure, NULL, unused + * @param merchant_pub public key of the instance + * @param merchant_priv private key of the instance, NULL if not available + * @param is detailed configuration settings for the instance + * @param accounts_length length of the @a accounts array + * @param accounts list of accounts of the merchant + */ +static void +add_instance_cb (void *cls, + const struct TALER_MerchantPublicKeyP *merchant_pub, + const struct TALER_MerchantPrivateKeyP *merchant_priv, + const struct TALER_MERCHANTDB_InstanceSettings *is, + unsigned int accounts_length, + struct TALER_MERCHANTDB_AccountDetails accounts[]) +{ + struct TMH_MerchantInstance *mi; + + (void) cls; + GNUNET_assert (NULL != merchant_priv); + mi = GNUNET_new (struct TMH_MerchantInstance); + mi->settings = *is; + mi->settings.id = GNUNET_strdup (mi->settings.id); + mi->settings.name = GNUNET_strdup (mi->settings.name); + mi->settings.location = json_incref (mi->settings.location); + mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction); + mi->privkey = *merchant_priv; + mi->pubkey = *merchant_pub; + for (unsigned int i = 0; i<accounts_length; i++) + { + const struct TALER_MERCHANTDB_AccountDetails *acc = &accounts[i]; + struct TMH_WireMethod *wm; + + wm = GNUNET_new (struct TMH_WireMethod); + wm->h_wire = acc->h_wire; + wm->j_wire = json_pack ("{s:s, s:s}", + "payto_uri", acc->payto_uri, + "salt", GNUNET_JSON_from_data_auto (&acc->salt)); + wm->wire_method = TALER_payto_get_method (acc->payto_uri); + wm->active = acc->active; + GNUNET_CONTAINER_DLL_insert (mi->wm_head, + mi->wm_tail, + wm); + } + GNUNET_assert (GNUNET_OK == + TMH_add_instance (mi)); +} + + +/** * Main function that will be run by the scheduler. * * @param cls closure @@ -1019,6 +1121,21 @@ run (void *cls, GNUNET_SCHEDULER_shutdown (); return; } + /* load instances */ + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_instances (TMH_db->cls, + true, + &add_instance_cb, + NULL); + if (0 > qs) + { + GNUNET_break (0); + GNUNET_SCHEDULER_shutdown (); + return; + } + } fh = TALER_MHD_bind (cfg, "merchant", diff --git a/src/backend/taler-merchant-httpd.h b/src/backend/taler-merchant-httpd.h @@ -95,11 +95,6 @@ struct TMH_MerchantInstance char *name; /** - * File holding the merchant's private key - */ - char *keyfile; - - /** * Next entry in DLL. */ struct TMH_WireMethod *wm_head; @@ -110,7 +105,7 @@ struct TMH_MerchantInstance struct TMH_WireMethod *wm_tail; /** - * Merchant's private key + * Merchant's private key. */ struct TALER_MerchantPrivateKeyP privkey; @@ -120,54 +115,15 @@ struct TMH_MerchantInstance struct TALER_MerchantPublicKeyP pubkey; /** - * Default maximum wire fee to assume, unless stated differently in the proposal - * already. - */ - struct TALER_Amount default_max_wire_fee; - - /** - * Default max deposit fee that the merchant is willing to - * pay; if deposit costs more, then the customer will cover - * the difference. - */ - struct TALER_Amount default_max_deposit_fee; - - /** - * Default factor for wire fee amortization. - */ - unsigned long long default_wire_fee_amortization; - - /** - * If the frontend does NOT specify an execution date, how long should - * we tell the exchange to wait to aggregate transactions before - * executing the wire transfer? This delay is added to the current - * time when we generate the advisory execution time for the exchange. - */ - struct GNUNET_TIME_Relative default_wire_transfer_delay; - - /** - * If the frontend does NOT specify a payment deadline, how long should - * offers we make be valid by default? - */ - struct GNUNET_TIME_Relative default_pay_deadline; - - /** - * Exchange this instance uses for tipping, NULL if tipping - * is not supported. - */ - char *tip_exchange; - - /** - * Locations from the configuration. Mapping from - * label to location data. + * General settings for an instance. */ - json_t *default_locations; + struct TALER_MERCHANTDB_InstanceSettings settings; /** - * What is the private key of the reserve used for signing tips by this exchange? - * Only valid if @e tip_exchange is non-null. + * Reference counter on this structure. Only destroyed if the + * counter hits zero. */ - struct TALER_ReservePrivateKeyP tip_reserve; + unsigned int rc; }; @@ -444,4 +400,23 @@ TMH_long_poll_resume (const char *order_id, const struct TALER_Amount *refund_amount); +/** + * Decrement reference counter of @a mi, and free if it hits zero. + * + * @param[in,out] mi merchant instance to update and possibly free + */ +void +TMH_instance_decref (struct TMH_MerchantInstance *mi); + + +/** + * Add instance definition to our active set of instances. + * + * @param[in,out] mi merchant instance details to define + * @return #GNUNET_OK on success, #GNUNET_NO if the same ID is in use already + */ +int +TMH_add_instance (struct TMH_MerchantInstance *mi); + + #endif diff --git a/src/backend/taler-merchant-httpd_config.h b/src/backend/taler-merchant-httpd_config.h @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2019 Taler Systems SA + (C) 2019, 2020 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software diff --git a/src/backend/taler-merchant-httpd_mhd.c b/src/backend/taler-merchant-httpd_mhd.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2019 Taler Systems SA + Copyright (C) 2014-2020 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 diff --git a/src/backenddb/merchant-0001.sql b/src/backenddb/merchant-0001.sql @@ -61,12 +61,22 @@ COMMENT ON COLUMN merchant_exchange_signing_keys.master_pub CREATE TABLE IF NOT EXISTS merchant_instances (merchant_serial BIGSERIAL PRIMARY KEY ,merchant_pub BYTEA NOT NULL UNIQUE CHECK (LENGTH(merchant_pub)=32) + ,merchant_id VARCHAR NOT NULL ,merchant_name VARCHAR NOT NULL ,location BYTEA NOT NULL ,jurisdiction BYTEA NOT NULL + ,default_max_deposit_fee_val INT8 NOT NULL + ,default_max_deposit_fee_frac INT4 NOT NULL + ,default_max_wire_fee_val INT8 NOT NULL + ,default_max_wire_fee_frac INT4 NOT NULL + ,default_wire_fee_amortization INT4 NOT NULL + ,default_wire_transfer_delay INT8 NOT NULL + ,default_pay_deadline INT8 NOT NULL ); COMMENT ON TABLE merchant_instances IS 'all the instances supported by this backend'; +COMMENT ON COLUMN merchant_instances.merchant_id + IS 'identifier of the merchant as used in the base URL (required)'; COMMENT ON COLUMN merchant_instances.merchant_name IS 'legal name of the merchant as a simple string (required)'; COMMENT ON COLUMN merchant_instances.location @@ -88,7 +98,7 @@ CREATE TABLE IF NOT EXISTS merchant_instance_accounts REFERENCES merchant_instances (merchant_serial) ON DELETE CASCADE ,h_wire BYTEA NOT NULL CHECK (LENGTH(h_wire)=64) ,active boolean NOT NULL - ,salt VARCHAR NOT NULL + ,salt BYTEA NOT NULL CHECK (LENGTH(salt)==64) ,payto_uri VARCHAR NOT NULL ,UNIQUE (merchant_serial,payto_uri) ); diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h @@ -226,16 +226,6 @@ struct TALER_MERCHANT_ConfigInformation */ const char *version; - /** - * Array with information about the merchant's instances. - */ - struct TALER_MERCHANT_InstanceInformation *iis; - - /** - * Length of the @e iis array. - */ - unsigned int iis_len; - }; diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -33,6 +33,114 @@ struct TALER_MERCHANTDB_Plugin; /** + * Details about a wire account of the merchant. + */ +struct TALER_MERCHANTDB_AccountDetails +{ + /** + * Hash of the wire details (@e payto_uri and @e salt). + */ + struct GNUNET_HashCode h_wire; + + /** + * Salt value used for hashing @e payto_uri. + */ + struct GNUNET_HashCode salt; + + /** + * Actual account address as a payto://-URI. + */ + const char *payto_uri; + + /** + * Is the account set for active use in new contracts? + */ + bool active; + +}; + + +/** + * General settings for an instance. + */ +struct TALER_MERCHANTDB_InstanceSettings +{ + /** + * prefix for the instance under "/instances/" + */ + char *id; + + /** + * legal name of the instance + */ + char *name; + + /** + * location of the business + */ + json_t *location; + + /** + * jurisdiction of the business + */ + json_t *jurisdiction; + + /** + * Default maximum wire fee to assume, unless stated differently in the + * proposal already. + */ + struct TALER_Amount default_max_wire_fee; + + /** + * Default max deposit fee that the merchant is willing to + * pay; if deposit costs more, then the customer will cover + * the difference. + */ + struct TALER_Amount default_max_deposit_fee; + + /** + * Default factor for wire fee amortization. + */ + unsigned long long default_wire_fee_amortization; + + /** + * If the frontend does NOT specify an execution date, how long should + * we tell the exchange to wait to aggregate transactions before + * executing the wire transfer? This delay is added to the current + * time when we generate the advisory execution time for the exchange. + */ + struct GNUNET_TIME_Relative default_wire_transfer_delay; + + /** + * If the frontend does NOT specify a payment deadline, how long should + * offers we make be valid by default? + */ + struct GNUNET_TIME_Relative default_pay_deadline; + +}; + + +/** + * Typically called by `lookup_instances`. + * + * @param cls closure + * @param merchant_pub public key of the instance + * @param merchant_priv private key of the instance, NULL if not available + * @param is general instance settings + * @param accounts_length length of the @a accounts array + * @param accounts list of accounts of the merchant + */ +typedef void +(*TALER_MERCHANTDB_InstanceCallback)( + void *cls, + const struct TALER_MerchantPublicKeyP *merchant_pub, + const struct TALER_MerchantPrivateKeyP *merchant_priv, + const struct TALER_MERCHANTDB_InstanceSettings *is, + unsigned int accounts_length, + struct TALER_MERCHANTDB_AccountDetails accounts[]); + + +/** * Typically called by `find_contract_terms_by_date`. * * @param cls closure @@ -187,6 +295,22 @@ struct TALER_MERCHANTDB_Plugin int (*drop_tables) (void *cls); + + /** + * Lookup all of the instances this backend has configured. + * + * @param cls closure + * @param active_only only find 'active' instances + * @param cb function to call on all instances found + * @param cb_cls closure for @a cb + */ + enum GNUNET_DB_QueryStatus + (*lookup_instances)(void *cls, + bool active_only, + TALER_MERCHANTDB_InstanceCallback cb, + void *cb_cls); + + /** * Insert order into db. * diff --git a/src/lib/merchant_api_config.c b/src/lib/merchant_api_config.c @@ -75,70 +75,8 @@ struct TALER_MERCHANT_ConfigGetHandle /** - * Parse instance information from @a ia. - * - * @param ia JSON array (or NULL!) with instance data - * @param[in,out] ci config information to update - * @return #GNUNET_OK on success - */ -static int -parse_instances (const json_t *ia, - struct TALER_MERCHANT_ConfigInformation *ci) -{ - size_t index; - json_t *value; - int ret; - - if (NULL == ia) - return GNUNET_OK; /* permit not disclosing instances for now */ - if (! json_is_array (ia)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - GNUNET_array_grow (ci->iis, - ci->iis_len, - json_array_size (ia)); - ret = GNUNET_OK; - json_array_foreach (ia, index, value) { - struct TALER_MERCHANT_InstanceInformation *ii = &ci->iis[index]; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("merchant_pub", - &ii->merchant_pub), - GNUNET_JSON_spec_string ("instance_baseurl", - &ii->instance_baseurl), - GNUNET_JSON_spec_string ("name", - &ii->name), - GNUNET_JSON_spec_end () - }; - json_t *teb; - - if (GNUNET_OK != - GNUNET_JSON_parse (value, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - ret = GNUNET_SYSERR; - continue; - } - teb = json_object_get (value, - "tipping_exchange_baseurl"); - if (! json_is_string (teb)) - { - GNUNET_break_op (0); - ret = GNUNET_SYSERR; - continue; - } - ii->tipping_exchange_baseurl = json_string_value (teb); - } - return ret; -} - - -/** * Function called when we're done processing the - * HTTP /track/transaction request. + * HTTP /config request. * * @param cls the `struct TALER_MERCHANT_ConfigGetHandle` * @param response_code HTTP response code, 0 on error @@ -165,10 +103,7 @@ handle_config_finished (void *cls, { case MHD_HTTP_OK: { - struct TALER_MERCHANT_ConfigInformation vi = { - .iis = NULL, - .iis_len = 0 - }; + struct TALER_MERCHANT_ConfigInformation vi; enum TALER_MERCHANT_VersionCompatibility vc = TALER_MERCHANT_VC_PROTOCOL_ERROR; struct GNUNET_JSON_Specification spec[] = { @@ -219,25 +154,10 @@ handle_config_finished (void *cls, } } } - if (0 == (vc & (TALER_MERCHANT_VC_INCOMPATIBLE - | TALER_MERCHANT_VC_PROTOCOL_ERROR))) - { - if (GNUNET_OK != - parse_instances (json_object_get (json, - "instances"), - &vi)) - { - /* Let's keep the 200 OK, as we got at least the version data */ - hr.ec = TALER_EC_INVALID_RESPONSE; - } - } vgh->cb (vgh->cb_cls, &hr, &vi, vc); - GNUNET_array_grow (vi.iis, - vi.iis_len, - 0); TALER_MERCHANT_config_get_cancel (vgh); return; } diff --git a/src/lib/test_merchant_api.c b/src/lib/test_merchant_api.c @@ -899,6 +899,7 @@ run (void *cls, TALER_TESTING_cmd_config ("config", merchant_url, MHD_HTTP_OK), +#if 0 TALER_TESTING_cmd_batch ("pay", pay), TALER_TESTING_cmd_batch ("double-spending", @@ -930,6 +931,7 @@ run (void *cls, GNUNET_TIME_UNIT_ZERO_ABS, 5, /* Expected number of records */ -100), /* Delta */ +#endif /** * End the suite. Fixme: better to have a label for this * too, as it shows a "(null)" token on logs. diff --git a/src/lib/test_merchant_api.conf b/src/lib/test_merchant_api.conf @@ -30,92 +30,14 @@ HTTP_PORT = 8082 # Which port do we run the backend on? (HTTP server) PORT = 8080 -# How quickly do we want the exchange to send us our money? -# Used only if the frontend does not specify a value. -WIRE_TRANSFER_DELAY = 0 s - # Which plugin (backend) do we use for the DB. DB = postgres -# Default choice for maximum wire fee. -DEFAULT_MAX_WIRE_FEE = EUR:0.10 - -# Default choice for maximum deposit fee. -DEFAULT_MAX_DEPOSIT_FEE = EUR:0.10 - # This specifies which database the postgres backend uses. [merchantdb-postgres] CONFIG = postgres:///talercheck -# Different instances operated by this merchant: -[instance-default] -KEYFILE = ${TALER_CONFIG_HOME}/merchant/default.priv -NAME = Kudos Inc. - -[instance-tor] -KEYFILE = ${TALER_CONFIG_HOME}/merchant/tor.priv -NAME = The Tor Project - - -[instance-tip] -KEYFILE = ${TALER_CONFIG_HOME}/merchant/tip.priv -TIP_EXCHANGE = http://localhost:8081/ -TIP_RESERVE_PRIV_FILENAME = ${TALER_CONFIG_HOME}/merchant/reserve/tip.priv -NAME = Test Tipping Merchant - - -[instance-dtip] -KEYFILE = ${TALER_CONFIG_HOME}/merchant/dtip.priv -TIP_EXCHANGE = http://localhost:8081/ -TIP_RESERVE_PRIV_FILENAME = ${TALER_CONFIG_HOME}/merchant/reserve/dtip.priv -NAME = Test Tipping Merchant 2 - -[instance-nulltip] -KEYFILE = ${TALER_CONFIG_HOME}/merchant/nulltip.priv -TIP_EXCHANGE = http://localhost:8081/ -# This key will NEVER be used to create a reserve, so -# as to check tip authorization against a non-reserve -# key. -TIP_RESERVE_PRIV_FILENAME = ${TALER_CONFIG_HOME}/merchant/reserve/nulltip.priv -NAME = Test Null-Tipping Merchant - -# Account of the MERCHANT -[merchant-account-merchant] -# What is the merchant's bank account? -PAYTO_URI = "payto://x-taler-bank/localhost/3" - -# This is the *salted* response we give out for /contract requests. -# File is generated on first use, no need for merchants to generate -# the salt! -WIRE_RESPONSE = ${TALER_CONFIG_HOME}/merchant/account-3.json - -# Accept payments to this account in instance-default -HONOR_default = YES - -# Accept payments to this account in instance-tor -HONOR_tor = YES - -# Accept payments to this account in instance-tip -HONOR_tip = YES - -# Accept payments to this account in instance-dtip -HONOR_dtip = YES - -HONOR_nulltip = YES - -# Advertise in new contracts of instance-default -ACTIVE_default = YES - -# Advertise in new contracts of instance-default -ACTIVE_tor = YES - -# Advertise in new contracts of instance-default -ACTIVE_tip = YES - -# Advertise in new contracts of instance-default -ACTIVE_nulltip = YES - # Sections starting with "merchant-exchange-" specify trusted exchanges # (by the merchant) [merchant-exchange-test] @@ -123,10 +45,18 @@ MASTER_KEY = T1VVFQZZARQ1CMF4BN58EE7SKTW5AV2BS18S87ZEGYS4S29J6DNG EXCHANGE_BASE_URL = http://localhost:8081/ CURRENCY = EUR -# only fixes skips. + +####################################################### +# Configuration for the auditor for the testcase +####################################################### [auditor] BASE_URL = http://the.auditor/ + +####################################################### +# Configuration for ??? Is this used? +####################################################### + # Auditors must be in sections "auditor-", the rest of the section # name could be anything. [auditor-ezb]