diff options
Diffstat (limited to 'src/backend/taler-merchant-httpd.c')
-rw-r--r-- | src/backend/taler-merchant-httpd.c | 1335 |
1 files changed, 295 insertions, 1040 deletions
diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c index 86b36306..60979d2e 100644 --- a/src/backend/taler-merchant-httpd.c +++ b/src/backend/taler-merchant-httpd.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2014-2018 Taler Systems SA + (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 @@ -32,23 +32,8 @@ #include "taler_merchantdb_lib.h" #include "taler-merchant-httpd.h" #include "taler-merchant-httpd_auditors.h" -#include "taler-merchant-httpd_check-payment.h" #include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_history.h" #include "taler-merchant-httpd_mhd.h" -#include "taler-merchant-httpd_order.h" -#include "taler-merchant-httpd_pay.h" -#include "taler-merchant-httpd_poll-payment.h" -#include "taler-merchant-httpd_proposal.h" -#include "taler-merchant-httpd_refund.h" -#include "taler-merchant-httpd_refund_increase.h" -#include "taler-merchant-httpd_refund_lookup.h" -#include "taler-merchant-httpd_track-transaction.h" -#include "taler-merchant-httpd_track-transfer.h" -#include "taler-merchant-httpd_tip-authorize.h" -#include "taler-merchant-httpd_tip-pickup.h" -#include "taler-merchant-httpd_tip-query.h" -#include "taler-merchant-httpd_tip-reserve-helper.h" #include "taler-merchant-httpd_config.h" /** @@ -58,45 +43,26 @@ /** - * Used by the iterator of the various merchant's instances given - * in configuration + * Which currency do we use? */ -struct IterateInstancesCls -{ - - /** - * Current index in the global array of #MerchantInstance - * types. Used by the callback in order to know which index - * is associated to the element being processed. - */ - unsigned int current_index; - - /** - * Flag indicating whether config contains a default instance - */ - unsigned int default_instance; +char *TMH_currency; - /** - * Tells if the parsing encountered any error. We need this - * field since the iterator must return void - */ - unsigned int ret; -}; +/** + * Inform the auditor for all deposit confirmations (global option) + */ +int TMH_force_audit; +/** + * Connection handle to the our database + */ +struct TALER_MERCHANTDB_Plugin *TMH_db; /** * Hashmap pointing at merchant instances by 'id'. An 'id' is * just a string that identifies a merchant instance. When a frontend * needs to specify an instance to the backend, it does so by 'id' */ -struct GNUNET_CONTAINER_MultiHashMap *by_id_map; - -/** - * Hashmap pointing at merchant instances by public key. This map - * is mainly used to check whether there is more than one instance - * using the same key - */ -struct GNUNET_CONTAINER_MultiHashMap *by_kpub_map; +static struct GNUNET_CONTAINER_MultiHashMap *by_id_map; /** * The port we are running on @@ -104,55 +70,9 @@ struct GNUNET_CONTAINER_MultiHashMap *by_kpub_map; static uint16_t port; /** - * This value tells the exchange by which date this merchant would like - * to receive the funds for a deposited payment - */ -struct GNUNET_TIME_Relative default_wire_transfer_delay; - -/** - * Locations from the configuration. Mapping from - * label to location data. - */ -json_t *default_locations; - -/** - * 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; - -/** - * 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; - -/** * Should a "Connection: close" header be added to each HTTP response? */ -static int TMH_merchant_connection_close; - -/** - * Which currency do we use? - */ -char *TMH_currency; - -/** - * Inform the auditor for all deposit confirmations (global option) - */ -int TMH_force_audit; +static int merchant_connection_close; /** * Task running the HTTP server. @@ -165,11 +85,6 @@ static struct GNUNET_SCHEDULER_Task *mhd_task; static int result; /** - * Connection handle to the our database - */ -struct TALER_MERCHANTDB_Plugin *db; - -/** * The MHD Daemon */ static struct MHD_Daemon *mhd; @@ -178,39 +93,39 @@ static struct MHD_Daemon *mhd; * MIN-Heap of suspended connections to resume when the timeout expires, * ordered by timeout. Values are of type `struct MHD_Connection` */ -struct GNUNET_CONTAINER_Heap *resume_timeout_heap; +static struct GNUNET_CONTAINER_Heap *resume_timeout_heap; /** * Hash map from H(order_id,merchant_pub) to `struct MHD_Connection` * entries to resume when a payment is made for the given order. */ -struct GNUNET_CONTAINER_MultiHashMap *payment_trigger_map; +static struct GNUNET_CONTAINER_MultiHashMap *payment_trigger_map; /** * Task responsible for timeouts in the #resume_timeout_heap. */ -struct GNUNET_SCHEDULER_Task *resume_timeout_task; +static struct GNUNET_SCHEDULER_Task *resume_timeout_task; /** * Our configuration. */ -static struct GNUNET_CONFIGURATION_Handle *cfg; +static const struct GNUNET_CONFIGURATION_Handle *cfg; /** - * Callback that frees all the elements in the hashmap + * Callback that frees all the instances in the hashmap * * @param cls closure, NULL * @param key current key - * @param value a `struct MerchantInstance` + * @param value a `struct TMH_MerchantInstance` */ static int -hashmap_free (void *cls, - const struct GNUNET_HashCode *key, - void *value) +instance_free (void *cls, + const struct GNUNET_HashCode *key, + void *value) { - struct MerchantInstance *mi = value; - struct WireMethod *wm; + struct TMH_MerchantInstance *mi = value; + struct TMH_WireMethod *wm; (void) cls; (void) key; @@ -266,10 +181,10 @@ payment_trigger_free (void *cls, * @param mpub an instance public key * @param key[out] set to the hash map key to use */ -void -TMH_compute_pay_key (const char *order_id, - const struct TALER_MerchantPublicKeyP *mpub, - struct GNUNET_HashCode *key) +static void +compute_pay_key (const char *order_id, + const struct TALER_MerchantPublicKeyP *mpub, + struct GNUNET_HashCode *key) { size_t olen = strlen (order_id); char buf[sizeof (*mpub) + olen]; @@ -287,7 +202,6 @@ TMH_compute_pay_key (const char *order_id, "Pay key for %s is %s\n", order_id, GNUNET_h2s (key)); - } @@ -332,14 +246,21 @@ do_resume (void *cls) /** * Suspend connection from @a sc until payment has been received. * + * @param order_id the order that we are waiting on + * @param mi the merchant instance we are waiting on * @param sc connection to suspend * @param min_refund refund amount we are waiting on to be exceeded before resuming, * NULL if we are not waiting for refunds */ void -TMH_long_poll_suspend (struct TMH_SuspendedConnection *sc, +TMH_long_poll_suspend (const char *order_id, + const struct TMH_MerchantInstance *mi, + struct TMH_SuspendedConnection *sc, const struct TALER_Amount *min_refund) { + compute_pay_key (order_id, + &mi->pubkey, + &sc->key); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Suspending operation on key %s\n", GNUNET_h2s (&sc->key)); @@ -350,7 +271,7 @@ TMH_long_poll_suspend (struct TMH_SuspendedConnection *sc, GNUNET_CONTAINER_MULTIHASHMAPOPTION_MULTIPLE)); if (NULL != min_refund) { - sc->awaiting_refund = GNUNET_YES; + sc->awaiting_refund = true; sc->refund_expected = *min_refund; } sc->hn = GNUNET_CONTAINER_heap_insert (resume_timeout_heap, @@ -385,7 +306,7 @@ resume_operation (void *cls, const struct TALER_Amount *have_refund = cls; struct TMH_SuspendedConnection *sc = value; - if ( (GNUNET_YES == sc->awaiting_refund) && + if ( (sc->awaiting_refund) && ( (NULL == have_refund) || (1 != TALER_amount_cmp (have_refund, &sc->refund_expected)) ) ) @@ -415,20 +336,20 @@ resume_operation (void *cls, * Find out if we have any clients long-polling for @a order_id to be * confirmed at merchant @a mpub, and if so, tell them to resume. * - * @param order_id the order that was paid - * @param mpub the merchant's public key of the instance where the payment happened + * @param order_id the order that was paid or refunded + * @param mi the merchant instance where the payment or refund happened * @param have_refund refunded amount, NULL if there was no refund */ void TMH_long_poll_resume (const char *order_id, - const struct TALER_MerchantPublicKeyP *mpub, + const struct TMH_MerchantInstance *mi, const struct TALER_Amount *have_refund) { struct GNUNET_HashCode key; - TMH_compute_pay_key (order_id, - mpub, - &key); + compute_pay_key (order_id, + &mi->pubkey, + &key); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming operations suspended pending payment on key %s\n", GNUNET_h2s (&key)); @@ -443,73 +364,6 @@ TMH_long_poll_resume (const char *order_id, /** - * Create a taler://pay/ URI for the given @a con and @a order_id - * and @a session_id and @a instance_id. - * - * @param con HTTP connection - * @param order_id the order id - * @param session_id session, may be NULL - * @param instance_id instance, may be "default" - * @return corresponding taler://pay/ URI, or NULL on missing "host" - */ -char * -TMH_make_taler_pay_uri (struct MHD_Connection *con, - const char *order_id, - const char *session_id, - const char *instance_id) -{ - const char *host; - const char *forwarded_host; - const char *uri_path; - const char *uri_instance_id; - const char *query; - char *result; - - host = MHD_lookup_connection_value (con, - MHD_HEADER_KIND, - "Host"); - forwarded_host = MHD_lookup_connection_value (con, - MHD_HEADER_KIND, - "X-Forwarded-Host"); - - uri_path = MHD_lookup_connection_value (con, - MHD_HEADER_KIND, - "X-Forwarded-Prefix"); - if (NULL == uri_path) - uri_path = "-"; - if (NULL != forwarded_host) - host = forwarded_host; - if (0 == strcmp (instance_id, - "default")) - uri_instance_id = "-"; - else - uri_instance_id = instance_id; - if (NULL == host) - { - /* Should never happen, at least the host header should be defined */ - GNUNET_break (0); - return NULL; - } - - if (GNUNET_YES == TALER_mhd_is_https (con)) - query = ""; - else - query = "?insecure=1"; - GNUNET_assert (NULL != order_id); - GNUNET_assert (0 < GNUNET_asprintf (&result, - "taler://pay/%s/%s/%s/%s%s%s%s", - host, - uri_path, - uri_instance_id, - order_id, - (NULL == session_id) ? "" : "/", - (NULL == session_id) ? "" : session_id, - query)); - return result; -} - - -/** * Shutdown task (magically invoked when the application is being * quit) * @@ -521,10 +375,12 @@ do_shutdown (void *cls) struct TMH_SuspendedConnection *sc; (void) cls; - MH_force_pc_resume (); - MH_force_trh_resume (); - MH_force_refund_resume (); - MH_force_tip_pickup_resume (); +#if 0 + TMH_force_pc_resume (); + TMH_force_trh_resume (); + TMH_force_refund_resume (); + TMH_force_tip_pickup_resume (); +#endif if (NULL != mhd_task) { GNUNET_SCHEDULER_cancel (mhd_task); @@ -556,10 +412,10 @@ do_shutdown (void *cls) MHD_stop_daemon (mhd); mhd = NULL; } - if (NULL != db) + if (NULL != TMH_db) { - TALER_MERCHANTDB_plugin_unload (db); - db = NULL; + TALER_MERCHANTDB_plugin_unload (TMH_db); + TMH_db = NULL; } TMH_EXCHANGES_done (); TMH_AUDITORS_done (); @@ -574,16 +430,11 @@ do_shutdown (void *cls) if (NULL != by_id_map) { GNUNET_CONTAINER_multihashmap_iterate (by_id_map, - &hashmap_free, + &instance_free, NULL); GNUNET_CONTAINER_multihashmap_destroy (by_id_map); by_id_map = NULL; } - if (NULL != by_kpub_map) - { - GNUNET_CONTAINER_multihashmap_destroy (by_kpub_map); - by_kpub_map = NULL; - } } @@ -607,15 +458,21 @@ handle_mhd_completion_callback (void *cls, void **con_cls, enum MHD_RequestTerminationCode toe) { - struct TM_HandlerContext *hc = *con_cls; + struct TMH_HandlerContext *hc = *con_cls; if (NULL == hc) return; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Finished handling request for `%s' with status %d\n", - hc->rh->url, + hc->url, (int) toe); - hc->cc (hc); + if (NULL != hc->cc) + hc->cc (hc->ctx); + TALER_MHD_parse_post_cleanup_callback (hc->json_parse_context); + GNUNET_free_non_null (hc->infix); + if (NULL != hc->json) + json_decref (hc->json); + GNUNET_free (hc); *con_cls = NULL; } @@ -625,7 +482,6 @@ handle_mhd_completion_callback (void *cls, * starts the task waiting for them. */ static struct GNUNET_SCHEDULER_Task * - prepare_daemon (void); @@ -729,504 +585,18 @@ prepare_daemon (void) /** - * Callback that looks for 'merchant-location-*' sections, - * and populates @a default_locations. - * - * @param cls closure - * @section section name this callback gets - */ -static void -locations_iterator_cb (void *cls, - const char *section) -{ - static const char *keys[] = { - "country", - "city", - "state", - "region", - "province", - "zip_code", - "street", - "street_number", - NULL, - }; - const char *prefix = "merchant-location-"; - const char *substr = strstr (section, prefix); - const char *locname; - json_t *loc; - - (void) cls; - if ( (NULL == substr) || (substr != section) ) - return; - locname = section + strlen (prefix); - if (0 == strlen (locname)) - return; - GNUNET_assert (json_is_object (default_locations)); - - loc = json_object (); - json_object_set_new (default_locations, - locname, - loc); - for (unsigned int pos = 0; NULL != keys[pos]; pos++) - { - char *val; - - if (GNUNET_OK == - GNUNET_CONFIGURATION_get_value_string (cfg, - section, - keys[pos], - &val)) - { - json_object_set_new (loc, - keys[pos], - json_string (val)); - GNUNET_free (val); - } - } -} - - -/** - * Closure for the #wireformat_iterator_cb(). - */ -struct WireFormatIteratorContext -{ - /** - * The global iteration context. - */ - struct IterateInstancesCls *iic; - - /** - * The merchant instance we are currently building. - */ - struct MerchantInstance *mi; - - /** - * Set to #GNUNET_YES if the default instance was found. - */ - int default_instance; -}; - - -/** - * Callback that looks for 'merchant-account-*' sections, - * and populates our wire method according to the data - * - * @param cls closure with a `struct WireFormatIteratorContext *` - * @section section name this callback gets - */ -static void -wireformat_iterator_cb (void *cls, - const char *section) -{ - struct WireFormatIteratorContext *wfic = cls; - struct MerchantInstance *mi = wfic->mi; - struct IterateInstancesCls *iic = wfic->iic; - char *instance_option; - struct WireMethod *wm; - char *payto; - char *fn; - json_t *j; - struct GNUNET_HashCode jh_wire; - char *wire_file_mode; - - if (0 != strncasecmp (section, - "merchant-account-", - strlen ("merchant-account-"))) - return; - GNUNET_asprintf (&instance_option, - "HONOR_%s", - mi->id); - if (GNUNET_YES != - GNUNET_CONFIGURATION_get_value_yesno (cfg, - section, - instance_option)) - { - GNUNET_free (instance_option); - return; - } - GNUNET_free (instance_option); - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (cfg, - section, - "PAYTO_URI", - &payto)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - section, - "PAYTO_URI"); - iic->ret = GNUNET_SYSERR; - return; - } - - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_filename (cfg, - section, - "WIRE_RESPONSE", - &fn)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - section, - "WIRE_RESPONSE"); - GNUNET_free (payto); - iic->ret = GNUNET_SYSERR; - return; - } - - /* Try loading existing JSON from file */ - if (GNUNET_YES == - GNUNET_DISK_file_test (fn)) - { - json_error_t err; - char *url; - - if (NULL == - (j = json_load_file (fn, - JSON_REJECT_DUPLICATES, - &err))) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to load JSON from `%s': %s at %d:%d\n", - fn, - err.text, - err.line, - err.column); - GNUNET_free (fn); - GNUNET_free (payto); - iic->ret = GNUNET_SYSERR; - return; - } - url = TALER_JSON_wire_to_payto (j); - if (NULL == url) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "URL missing in `%s', disabling account `%s'\n", - fn, - section); - GNUNET_free (fn); - GNUNET_free (payto); - iic->ret = GNUNET_SYSERR; - return; - } - if (0 != strcasecmp (url, - payto)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "URL `%s' does not match configuration `%s', disabling account `%s'\n", - url, - payto, - section); - GNUNET_free (fn); - GNUNET_free (payto); - GNUNET_free (url); - iic->ret = GNUNET_SYSERR; - return; - } - GNUNET_free (url); - } - else /* need to generate JSON */ - { - struct GNUNET_HashCode salt; - char *salt_str; - - GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, - &salt, - sizeof (salt)); - salt_str = GNUNET_STRINGS_data_to_string_alloc (&salt, - sizeof (salt)); - j = json_pack ("{s:s, s:s}", - "payto_uri", payto, - "salt", salt_str); - GNUNET_free (salt_str); - - /* Make sure every path component exists. */ - if (GNUNET_OK != GNUNET_DISK_directory_create_for_file (fn)) - { - GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, - "mkdir", - fn); - GNUNET_free (fn); - GNUNET_free (payto); - json_decref (j); - iic->ret = GNUNET_SYSERR; - return; - } - - if (0 != json_dump_file (j, - fn, - JSON_COMPACT | JSON_SORT_KEYS)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to write hashed wire details to `%s'\n", - fn); - GNUNET_free (fn); - GNUNET_free (payto); - json_decref (j); - iic->ret = GNUNET_SYSERR; - return; - } - - if (GNUNET_OK == - GNUNET_CONFIGURATION_get_value_string (cfg, - section, - "WIRE_FILE_MODE", - &wire_file_mode)) - { - errno = 0; - mode_t mode = (mode_t) strtoul (wire_file_mode, - NULL, - 8); - if (0 != errno) - { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - section, - "WIRE_FILE_MODE", - "Must be octal number\n"); - iic->ret = GNUNET_SYSERR; - GNUNET_free (fn); - return; - } - if (0 != chmod (fn, mode)) - { - TALER_LOG_ERROR ("chmod failed on %s\n", fn); - iic->ret = GNUNET_SYSERR; - GNUNET_free (fn); - return; - } - } - } - - GNUNET_free (fn); - - if (GNUNET_OK != - TALER_JSON_merchant_wire_signature_hash (j, - &jh_wire)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to hash wire input\n"); - GNUNET_free (fn); - GNUNET_free (payto); - json_decref (j); - iic->ret = GNUNET_SYSERR; - return; - } - - wm = GNUNET_new (struct WireMethod); - wm->wire_method = TALER_payto_get_method (payto); - GNUNET_free (payto); - GNUNET_asprintf (&instance_option, - "ACTIVE_%s", - mi->id); - wm->active = GNUNET_CONFIGURATION_get_value_yesno (cfg, - section, - instance_option); - GNUNET_free (instance_option); - if (GNUNET_YES == wm->active) - GNUNET_CONTAINER_DLL_insert (mi->wm_head, - mi->wm_tail, - wm); - else - GNUNET_CONTAINER_DLL_insert_tail (mi->wm_head, - mi->wm_tail, - wm); - wm->j_wire = j; - wm->h_wire = jh_wire; -} - - -/** - * Callback that looks for 'instance-*' sections, - * and populates accordingly each instance's data - * - * @param cls closure of type `struct IterateInstancesCls` - * @section section name this callback gets - */ -static void -instances_iterator_cb (void *cls, - const char *section) -{ - struct IterateInstancesCls *iic = cls; - char *token; - struct MerchantInstance *mi; - /* used as hashmap keys */ - struct GNUNET_HashCode h_pk; - struct GNUNET_HashCode h_id; - - if (0 != strncasecmp (section, - "instance-", - strlen ("instance-"))) - return; - /** Get id **/ - token = strrchr (section, '-'); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Extracted token: %s\n", - token + 1); - mi = GNUNET_new (struct MerchantInstance); - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (cfg, - section, - "NAME", - &mi->name)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - section, - "NAME"); - GNUNET_free (mi); - iic->ret = GNUNET_SYSERR; - return; - } - - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_filename (cfg, - section, - "KEYFILE", - &mi->keyfile)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - section, - "KEYFILE"); - GNUNET_free (mi->name); - GNUNET_free (mi); - iic->ret = GNUNET_SYSERR; - return; - } - if (GNUNET_OK == - GNUNET_CONFIGURATION_get_value_string (cfg, - section, - "TIP_EXCHANGE", - &mi->tip_exchange)) - { - char *tip_reserves; - - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_filename (cfg, - section, - "TIP_RESERVE_PRIV_FILENAME", - &tip_reserves)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - section, - "TIP_RESERVE_PRIV_FILENAME"); - GNUNET_free (mi->keyfile); - GNUNET_free (mi->name); - GNUNET_free (mi); - iic->ret = GNUNET_SYSERR; - return; - } - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_key_from_file (tip_reserves, - GNUNET_NO, - &mi->tip_reserve.eddsa_priv)) - { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - section, - "TIP_RESERVE_PRIV_FILENAME", - "Failed to read private key"); - GNUNET_free (tip_reserves); - GNUNET_free (mi->keyfile); - GNUNET_free (mi->name); - GNUNET_free (mi); - iic->ret = GNUNET_SYSERR; - return; - } - GNUNET_free (tip_reserves); - } - - if (GNUNET_SYSERR == - GNUNET_CRYPTO_eddsa_key_from_file (mi->keyfile, - GNUNET_YES, - &mi->privkey.eddsa_priv)) - { - GNUNET_break (0); - GNUNET_free (mi->keyfile); - GNUNET_free (mi->name); - GNUNET_free (mi); - iic->ret = GNUNET_SYSERR; - return; - } - GNUNET_CRYPTO_eddsa_key_get_public (&mi->privkey.eddsa_priv, - &mi->pubkey.eddsa_pub); - - mi->id = GNUNET_strdup (token + 1); - if (0 == strcasecmp ("default", - mi->id)) - iic->default_instance = GNUNET_YES; - - GNUNET_CRYPTO_hash (mi->id, - strlen (mi->id), - &h_id); - if (GNUNET_OK != - GNUNET_CONTAINER_multihashmap_put (by_id_map, - &h_id, - mi, - GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to put an entry into the 'by_id' hashmap\n"); - iic->ret = GNUNET_SYSERR; - GNUNET_free (mi->keyfile); - GNUNET_free (mi->name); - GNUNET_free (mi); - return; - } - GNUNET_CRYPTO_hash (&mi->pubkey.eddsa_pub, - sizeof (struct GNUNET_CRYPTO_EddsaPublicKey), - &h_pk); - if (GNUNET_OK != - GNUNET_CONTAINER_multihashmap_put (by_kpub_map, - &h_pk, - mi, - GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to put an entry into the 'by_kpub_map' hashmap\n"); - GNUNET_assert (GNUNET_OK == - GNUNET_CONTAINER_multihashmap_remove (by_id_map, - &h_id, - mi)); - iic->ret = GNUNET_SYSERR; - GNUNET_free (mi->keyfile); - GNUNET_free (mi->name); - GNUNET_free (mi); - return; - } - - /* Initialize wireformats */ - { - struct WireFormatIteratorContext wfic = { - .iic = iic, - .mi = mi - }; - - GNUNET_CONFIGURATION_iterate_sections (cfg, - &wireformat_iterator_cb, - &wfic); - } - if (NULL == mi->wm_head) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to load wire formats for instance `%s'\n", - mi->id); - iic->ret = GNUNET_SYSERR; - } - -} - - -/** * Lookup a merchant instance by its instance ID. * * @param instance_id identifier of the instance to resolve * @return NULL if that instance is unknown to us */ -static struct MerchantInstance * +static struct TMH_MerchantInstance * lookup_instance (const char *instance_id) { struct GNUNET_HashCode h_instance; if (NULL == instance_id) instance_id = "default"; - GNUNET_CRYPTO_hash (instance_id, strlen (instance_id), &h_instance); @@ -1242,57 +612,6 @@ lookup_instance (const char *instance_id) /** - * Iterate over locations in config in order to populate - * the location data. - * - * @return #GNUNET_OK if successful, #GNUNET_SYSERR upon errors - */ -static void -iterate_locations (void) -{ - GNUNET_assert (NULL == default_locations); - default_locations = json_object (); - GNUNET_CONFIGURATION_iterate_sections (cfg, - &locations_iterator_cb, - NULL); -} - - -/** - * Iterate over each merchant instance, in order to populate - * each instance's own data - * - * @return #GNUNET_OK if successful, #GNUNET_SYSERR upon errors - * (for example, if no "default" instance is defined) - */ -static int -iterate_instances (void) -{ - struct IterateInstancesCls iic; - - iic.default_instance = GNUNET_NO; - iic.ret = GNUNET_OK; - GNUNET_CONFIGURATION_iterate_sections (cfg, - &instances_iterator_cb, - &iic); - - if (GNUNET_NO == iic.default_instance) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "No default merchant instance found\n"); - return GNUNET_SYSERR; - } - if (GNUNET_OK != iic.ret) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "At least one instance was not successfully parsed\n"); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** * 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 @@ -1341,158 +660,143 @@ url_handler (void *cls, size_t *upload_data_size, void **con_cls) { - static struct TMH_RequestHandler handlers[] = { - /* Landing page, tell humans to go away. */ - { "/", MHD_HTTP_METHOD_GET, "text/plain", - "Hello, I'm a merchant's Taler backend. This HTTP server is not for humans.\n", - 0, - &TMH_MHD_handler_static_response, MHD_HTTP_OK }, - { "/agpl", MHD_HTTP_METHOD_GET, "text/plain", - NULL, 0, - &TMH_MHD_handler_agpl_redirect, MHD_HTTP_FOUND }, - { "/track/transfer", MHD_HTTP_METHOD_GET, "application/json", - NULL, 0, - &MH_handler_track_transfer, MHD_HTTP_OK}, - { "/track/transfer", NULL, "text/plain", - "Only GET is allowed", 0, - &TMH_MHD_handler_static_response, MHD_HTTP_OK}, - { "/track/transaction", MHD_HTTP_METHOD_GET, "application/json", - NULL, 0, - &MH_handler_track_transaction, MHD_HTTP_OK}, - { "/track/transaction", NULL, "text/plain", - "Only GET is allowed", 0, - &TMH_MHD_handler_static_response, MHD_HTTP_OK}, - { "/history", MHD_HTTP_METHOD_GET, "text/plain", - "Only GET is allowed", 0, - &MH_handler_history, MHD_HTTP_OK}, - { "/order", MHD_HTTP_METHOD_POST, "application/json", - NULL, 0, - &MH_handler_order_post, MHD_HTTP_OK }, - { "/refund", MHD_HTTP_METHOD_POST, "application/json", - NULL, 0, - &MH_handler_refund_increase, MHD_HTTP_OK}, - { "/tip-authorize", MHD_HTTP_METHOD_POST, "text/plain", - NULL, 0, - &MH_handler_tip_authorize, MHD_HTTP_OK}, - { "/tip-query", MHD_HTTP_METHOD_GET, "text/plain", - NULL, 0, - &MH_handler_tip_query, MHD_HTTP_OK}, - { "/check-payment", MHD_HTTP_METHOD_GET, "text/plain", - NULL, 0, - &MH_handler_check_payment, MHD_HTTP_OK}, - { "/config", MHD_HTTP_METHOD_GET, "text/plain", - NULL, 0, - &MH_handler_config, MHD_HTTP_OK}, - {NULL, NULL, NULL, NULL, 0, 0 } + static struct TMH_RequestHandler private_handlers[] = { + { + .url_prefix = "/", + .method = MHD_HTTP_METHOD_GET, + .mime_type = "text/plain", + .skip_instance = true, + .data = "This is a GNU Taler merchant backend. See https://taler.net/.\n", + .data_size = strlen ( + "This is a GNU Taler merchant backend. See https://taler.net/.\n"), + .handler = &TMH_MHD_handler_static_response, + .response_code = MHD_HTTP_OK + }, + { + .url_prefix = "/agpl", + .method = MHD_HTTP_METHOD_GET, + .skip_instance = true, + .handler = &TMH_MHD_handler_agpl_redirect + }, + { + NULL + } }; static struct TMH_RequestHandler public_handlers[] = { - { "/pay", MHD_HTTP_METHOD_POST, "application/json", - NULL, 0, - &MH_handler_pay, MHD_HTTP_OK }, - { "/proposal", MHD_HTTP_METHOD_GET, "text/plain", - NULL, 0, - &MH_handler_proposal_lookup, MHD_HTTP_OK }, - { "/tip-pickup", MHD_HTTP_METHOD_POST, "text/plain", - NULL, 0, - &MH_handler_tip_pickup, MHD_HTTP_OK }, - { "/refund", MHD_HTTP_METHOD_GET, "text/plain", - NULL, 0, - &MH_handler_refund_lookup, MHD_HTTP_OK }, - { "/tip-pickup", MHD_HTTP_METHOD_GET, "text/plain", - NULL, 0, - &MH_handler_tip_pickup_get, MHD_HTTP_OK }, - { "/poll-payment", MHD_HTTP_METHOD_GET, "text/plain", - NULL, 0, - &MH_handler_poll_payment, MHD_HTTP_OK}, - { "/config", MHD_HTTP_METHOD_GET, "text/plain", - NULL, 0, - &MH_handler_config, MHD_HTTP_OK}, - {NULL, NULL, NULL, NULL, 0, 0 } + { + .url_prefix = "/", + .method = MHD_HTTP_METHOD_GET, + .mime_type = "text/plain", + .skip_instance = true, + .data = "This is a GNU Taler merchant backend. See https://taler.net/.\n", + .data_size = strlen ( + "This is a GNU Taler merchant backend. See https://taler.net/.\n"), + .handler = &TMH_MHD_handler_static_response, + .response_code = MHD_HTTP_OK + }, + { + .url_prefix = "/agpl", + .method = MHD_HTTP_METHOD_GET, + .skip_instance = true, + .handler = &TMH_MHD_handler_agpl_redirect + }, + { + .url_prefix = "/config", + .method = MHD_HTTP_METHOD_GET, + .skip_instance = true, + .handler = &MH_handler_config + }, + { + NULL + } }; static struct TMH_RequestHandler h404 = { - "", NULL, "text/html", - "<html><title>404: not found</title><body>404: not found</body></html>", 0, - &TMH_MHD_handler_static_response, MHD_HTTP_NOT_FOUND + .mime_type = "text/html", + .data = "<html><title>404: not found</title>" + "<body>404: not found</body></html>", + .data_size = strlen ("<html><title>404: not found</title>" + "<body>404: not found</body></html>"), + .handler = &TMH_MHD_handler_static_response, + .response_code = MHD_HTTP_NOT_FOUND }; - - struct TM_HandlerContext *hc = *con_cls; - struct GNUNET_AsyncScopeId aid; - const char *correlation_id = NULL; - struct MerchantInstance *instance; - const char *effective_url; - /* Is a publicly facing endpoint being requested? */ - int is_public; - /* Matching URL found, but maybe method doesn't match */ - int url_found = GNUNET_NO; - MHD_RESULT ret; - struct TMH_RequestHandler *selected_handler = NULL; + struct TMH_HandlerContext *hc = *con_cls; + struct TMH_RequestHandler *handlers; (void) cls; (void) version; - if (NULL == hc) + if (NULL != hc) { - GNUNET_async_scope_fresh (&aid); - /* We only read the correlation ID on the first callback for every client */ + GNUNET_assert (NULL != hc->rh); + GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id); + if ( (hc->is_post) && + (NULL == hc->json) ) + { + int res; + + res = TALER_MHD_parse_post_json (connection, + &hc->json_parse_context, + upload_data, + upload_data_size, + &hc->json); + if (GNUNET_SYSERR == res) + return MHD_NO; + /* A error response was already generated */ + if ( (GNUNET_NO == res) || + /* or, need more data to accomplish parsing */ + (NULL == hc->json) ) + return MHD_YES; + } + return hc->rh->handler (hc->rh, + connection, + hc); + } + hc = GNUNET_new (struct TMH_HandlerContext); + *con_cls = hc; + GNUNET_async_scope_fresh (&hc->async_scope_id); + GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id); + hc->url = url; + { + const char *correlation_id; + correlation_id = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, "Taler-Correlation-Id"); - if ((NULL != correlation_id) && - (GNUNET_YES != GNUNET_CURL_is_valid_scope_id (correlation_id))) + if ( (NULL != correlation_id) && + (GNUNET_YES != GNUNET_CURL_is_valid_scope_id (correlation_id)) ) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "illegal incoming correlation ID\n"); correlation_id = NULL; } - } - else - { - aid = hc->async_scope_id; - } - - GNUNET_SCHEDULER_begin_async_scope (&aid); - - if (NULL != correlation_id) - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Handling request for (%s) URL '%s', correlation_id=%s\n", - method, - url, - correlation_id); - else - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Handling request (%s) for URL '%s'\n", - method, - url); - - effective_url = url; - - { - const char *public_prefix = "/public/"; - - if (0 == strncmp (effective_url, - public_prefix, - strlen (public_prefix))) - { - is_public = GNUNET_YES; - effective_url = effective_url + strlen (public_prefix) - 1; - } + if (NULL != correlation_id) + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling request for (%s) URL '%s', correlation_id=%s\n", + method, + url, + correlation_id); else - { - is_public = GNUNET_NO; - } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling request (%s) for URL '%s'\n", + method, + url); } + if (0 == strcasecmp (method, + MHD_HTTP_METHOD_HEAD)) + method = MHD_HTTP_METHOD_GET; /* MHD will deal with the rest */ + /* Find out the merchant backend instance for the request. * If there is an instance, remove the instance specification * from the beginning of the request URL. */ { const char *instance_prefix = "/instances/"; - if (0 == strncmp (effective_url, + if (0 == strncmp (url, instance_prefix, strlen (instance_prefix))) { /* url starts with "/instances/" */ - const char *istart = effective_url + strlen (instance_prefix); + const char *istart = url + strlen (instance_prefix); const char *slash = strchr (istart, '/'); char *instance_id; @@ -1500,114 +804,149 @@ url_handler (void *cls, { return TMH_MHD_handler_static_response (&h404, connection, - con_cls, - upload_data, - upload_data_size, - NULL); + hc); } instance_id = GNUNET_strndup (istart, slash - istart); - instance = lookup_instance (instance_id); + hc->instance = lookup_instance (instance_id); GNUNET_free (instance_id); - effective_url = slash; + url = slash; } else { - instance = lookup_instance (NULL); + /* use 'default' */ + hc->instance = lookup_instance (NULL); } } - if (NULL == instance) - return TALER_MHD_reply_json_pack (connection, - MHD_HTTP_NOT_FOUND, - "{s:I, s:s}", - "code", - (json_int_t) TALER_EC_INSTANCE_UNKNOWN, - "error", - "merchant instance unknown"); - if (GNUNET_NO == is_public) { - for (unsigned int i = 0; NULL != handlers[i].url; i++) - { - struct TMH_RequestHandler *rh = &handlers[i]; + const char *private_prefix = "/private/"; - if ( (0 != strcasecmp (effective_url, rh->url)) ) - continue; - url_found = GNUNET_YES; - if (0 == strcasecmp (method, - MHD_HTTP_METHOD_OPTIONS)) - { - return TALER_MHD_reply_cors_preflight (connection); - } - if ( (rh->method != NULL) && - (0 != strcasecmp (method, rh->method)) ) - continue; - selected_handler = rh; - break; + if (0 == strncmp (url, + private_prefix, + strlen (private_prefix))) + { + handlers = private_handlers; + url += strlen (private_prefix) - 1; + } + else + { + handlers = public_handlers; } } + if (strcmp (url, + "")) + url = "/"; /* code below does not like empty string */ - if (NULL == selected_handler) { - for (unsigned int i = 0; NULL != public_handlers[i].url; i++) + /* Matching URL found, but maybe method doesn't match */ + size_t prefix_strlen; /* i.e. 8 for "/orders/", or 7 for "/config" */ + const char *infix_url = NULL; /* i.e. "$ORDER_ID", no '/'-es */ + size_t infix_strlen = 0; /* number of characters in infix_url */ + const char *suffix_url = NULL; /* i.e. "/refund", includes '/' at the beginning */ + size_t suffix_strlen = 0; /* number of characters in suffix_url */ + { - struct TMH_RequestHandler *rh = &public_handlers[i]; + const char *slash; - if ( (0 != strcasecmp (effective_url, rh->url)) ) - continue; - url_found = GNUNET_YES; - if (0 == strcasecmp (method, - MHD_HTTP_METHOD_OPTIONS)) + slash = strchr (&url[1], '/'); + if (NULL == slash) { - return TALER_MHD_reply_cors_preflight (connection); + prefix_strlen = strlen (url); + } + else + { + prefix_strlen = slash - url + 1; /* includes both '/'-es if present! */ + infix_url = slash + 1; + slash = strchr (&infix_url[1], '/'); + if (NULL == slash) + { + infix_strlen = strlen (infix_url); + } + else + { + infix_strlen = slash - infix_url; + suffix_url = slash; + suffix_strlen = strlen (suffix_url); + } + hc->infix = GNUNET_strndup (infix_url, + infix_strlen); } - if ( (rh->method != NULL) && (0 != strcasecmp (method, rh->method)) ) - continue; - selected_handler = rh; - break; } - } - if (NULL == selected_handler) - { - if (GNUNET_YES == url_found) { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "invalid request: method '%s' for '%s' not allowed\n", - method, - url); - return TALER_MHD_reply_json_pack (connection, - MHD_HTTP_METHOD_NOT_ALLOWED, - "{s:s}", - "error", - "method not allowed"); + bool url_found = false; + + for (unsigned int i = 0; NULL != handlers[i].url_prefix; i++) + { + struct TMH_RequestHandler *rh = &handlers[i]; + + if ( (prefix_strlen != strlen (rh->url_prefix)) || + (0 != memcmp (url, + rh->url_prefix, + prefix_strlen)) ) + continue; + if ( (NULL == infix_url) + ^ (GNUNET_NO == rh->have_id_segment) ) + continue; /* infix existence missmatch */ + if ( (NULL == suffix_url) + ^ (NULL != rh->url_suffix) ) + continue; /* suffix existence missmatch */ + if ( (NULL != suffix_url) && + ( (suffix_strlen != strlen (rh->url_suffix)) || + (0 != memcmp (suffix_url, + rh->url_suffix, + suffix_strlen)) ) ) + continue; /* suffix content missmatch */ + url_found = true; + if (0 == strcasecmp (method, + MHD_HTTP_METHOD_OPTIONS)) + { + return TALER_MHD_reply_cors_preflight (connection); + } + if ( (rh->method != NULL) && + (0 != strcasecmp (method, rh->method)) ) + continue; + hc->rh = rh; + break; + } + if ( (NULL == hc->rh) && + (url_found) ) + return TALER_MHD_reply_json_pack (connection, + MHD_HTTP_METHOD_NOT_ALLOWED, + "{s:s}", + "error", + "method not allowed"); + if (NULL == hc->rh) + return TMH_MHD_handler_static_response (&h404, + connection, + hc); } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "invalid request: URL '%s' not handled\n", - url); - return TMH_MHD_handler_static_response (&h404, - connection, - con_cls, - upload_data, - upload_data_size, - instance); } - - ret = selected_handler->handler (selected_handler, - connection, - con_cls, - upload_data, - upload_data_size, - instance); - hc = *con_cls; - if (NULL != hc) + /* At this point, we must have found a handler */ + GNUNET_assert (NULL != hc->rh); + if ( (NULL == hc->instance) && + (GNUNET_YES != hc->rh->skip_instance) ) + return TALER_MHD_reply_json_pack (connection, + MHD_HTTP_NOT_FOUND, + "{s:I, s:s}", + "code", + (json_int_t) TALER_EC_INSTANCE_UNKNOWN, + "error", + "merchant instance unknown"); + hc->is_post = (0 == strcasecmp (method, + MHD_HTTP_METHOD_POST)); + if (hc->is_post) { - hc->rh = selected_handler; - /* Store the async context ID, so we can restore it if - * we get another callback for this request. */ - hc->async_scope_id = aid; + /* FIXME: Maybe check for maximum upload size here + and refuse if it is too big? */ + + GNUNET_break (NULL == hc->json); /* can't have it already */ + return MHD_YES; /* proceed with upload */ } - return ret; + return hc->rh->handler (hc->rh, + connection, + hc); } @@ -1632,10 +971,11 @@ run (void *cls, (void) cls; (void) args; (void) cfgfile; + cfg = config; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Starting taler-merchant-httpd\n"); go = TALER_MHD_GO_NONE; - if (TMH_merchant_connection_close) + if (merchant_connection_close) go |= TALER_MHD_GO_FORCE_CONNECTION_CLOSE; TALER_MHD_setup (go); @@ -1643,14 +983,14 @@ run (void *cls, GNUNET_SCHEDULER_add_shutdown (&do_shutdown, NULL); if (GNUNET_OK != - TALER_config_get_currency (config, + TALER_config_get_currency (cfg, &TMH_currency)) { GNUNET_SCHEDULER_shutdown (); return; } if (GNUNET_YES == - GNUNET_CONFIGURATION_get_value_yesno (config, + GNUNET_CONFIGURATION_get_value_yesno (cfg, "merchant", "FORCE_AUDIT")) TMH_force_audit = GNUNET_YES; @@ -1666,7 +1006,6 @@ run (void *cls, GNUNET_SCHEDULER_shutdown (); return; } - if (NULL == (by_id_map = GNUNET_CONTAINER_multihashmap_create (1, GNUNET_NO))) @@ -1674,97 +1013,14 @@ run (void *cls, GNUNET_SCHEDULER_shutdown (); return; } - if (NULL == - (by_kpub_map = GNUNET_CONTAINER_multihashmap_create (1, - GNUNET_NO))) - { - GNUNET_SCHEDULER_shutdown (); - return; - } - - if (GNUNET_SYSERR == - GNUNET_CONFIGURATION_get_value_time (config, - "merchant", - "WIRE_TRANSFER_DELAY", - &default_wire_transfer_delay)) + (TMH_db = TALER_MERCHANTDB_plugin_load (cfg))) { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "merchant", - "WIRE_TRANSFER_DELAY"); GNUNET_SCHEDULER_shutdown (); return; } - if (GNUNET_SYSERR == - GNUNET_CONFIGURATION_get_value_time (config, - "merchant", - "DEFAULT_PAY_DEADLINE", - &default_pay_deadline)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "merchant", - "DEFAULT_PAY_DEADLINE"); - GNUNET_SCHEDULER_shutdown (); - return; - } - - if (GNUNET_OK != - TALER_config_get_amount (config, - "merchant", - "DEFAULT_MAX_WIRE_FEE", - &default_max_wire_fee)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "merchant", - "DEFAULT_MAX_WIRE_FEE"); - GNUNET_SCHEDULER_shutdown (); - return; - } - - if (GNUNET_OK != - TALER_config_get_amount (config, - "merchant", - "DEFAULT_MAX_DEPOSIT_FEE", - &default_max_deposit_fee)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "merchant", - "DEFAULT_MAX_DEPOSIT_FEE"); - GNUNET_SCHEDULER_shutdown (); - return; - } - - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_number (config, - "merchant", - "DEFAULT_WIRE_FEE_AMORTIZATION", - &default_wire_fee_amortization)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "merchant", - "DEFAULT_WIRE_FEE_AMORTIZATION"); - GNUNET_SCHEDULER_shutdown (); - return; - } - - cfg = GNUNET_CONFIGURATION_dup (config); - if (GNUNET_OK != - iterate_instances ()) - { - GNUNET_SCHEDULER_shutdown (); - return; - } - iterate_locations (); - - if (NULL == - (db = TALER_MERCHANTDB_plugin_load (cfg))) - { - GNUNET_SCHEDULER_shutdown (); - return; - } - - fh = TALER_MHD_bind (config, + fh = TALER_MHD_bind (cfg, "merchant", &port); if ( (0 == port) && @@ -1805,7 +1061,7 @@ run (void *cls, * * @param argc number of arguments from the command line * @param argv command line arguments - * @return 0 ok, 1 on error + * @return 0 ok, non-zero on error */ int main (int argc, @@ -1815,12 +1071,11 @@ main (int argc, GNUNET_GETOPT_option_flag ('C', "connection-close", "force HTTP connections to be closed after each request", - &TMH_merchant_connection_close), + &merchant_connection_close), GNUNET_GETOPT_option_timetravel ('T', "timetravel"), GNUNET_GETOPT_OPTION_END }; - if (GNUNET_OK != GNUNET_PROGRAM_run (argc, argv, "taler-merchant-httpd", |