/* This file is part of TALER (C) 2014-2021 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 Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * @file taler-merchant-httpd.c * @brief HTTP serving layer intended to perform crypto-work and * communication with the exchange * @author Marcello Stanisci * @author Christian Grothoff * @author Florian Dold */ #include "platform.h" #include #include #include "taler-merchant-httpd_auditors.h" #include "taler-merchant-httpd_config.h" #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_get-orders-ID.h" #include "taler-merchant-httpd_get-tips-ID.h" #include "taler-merchant-httpd_mhd.h" #include "taler-merchant-httpd_private-delete-instances-ID.h" #include "taler-merchant-httpd_private-delete-products-ID.h" #include "taler-merchant-httpd_private-delete-orders-ID.h" #include "taler-merchant-httpd_private-delete-reserves-ID.h" #include "taler-merchant-httpd_private-get-instances.h" #include "taler-merchant-httpd_private-get-instances-ID.h" #include "taler-merchant-httpd_private-get-products.h" #include "taler-merchant-httpd_private-get-products-ID.h" #include "taler-merchant-httpd_private-get-orders.h" #include "taler-merchant-httpd_private-get-orders-ID.h" #include "taler-merchant-httpd_private-get-reserves.h" #include "taler-merchant-httpd_private-get-reserves-ID.h" #include "taler-merchant-httpd_private-get-tips-ID.h" #include "taler-merchant-httpd_private-get-tips.h" #include "taler-merchant-httpd_private-get-transfers.h" #include "taler-merchant-httpd_private-patch-instances-ID.h" #include "taler-merchant-httpd_private-patch-orders-ID-forget.h" #include "taler-merchant-httpd_private-patch-products-ID.h" #include "taler-merchant-httpd_private-post-instances.h" #include "taler-merchant-httpd_private-post-instances-ID-auth.h" #include "taler-merchant-httpd_private-post-orders.h" #include "taler-merchant-httpd_private-post-orders-ID-refund.h" #include "taler-merchant-httpd_private-post-products.h" #include "taler-merchant-httpd_private-post-products-ID-lock.h" #include "taler-merchant-httpd_private-post-reserves.h" #include "taler-merchant-httpd_private-post-reserves-ID-authorize-tip.h" #include "taler-merchant-httpd_private-post-transfers.h" #include "taler-merchant-httpd_post-orders-ID-abort.h" #include "taler-merchant-httpd_post-orders-ID-claim.h" #include "taler-merchant-httpd_post-orders-ID-paid.h" #include "taler-merchant-httpd_post-orders-ID-pay.h" #include "taler-merchant-httpd_post-orders-ID-refund.h" #include "taler-merchant-httpd_post-tips-ID-pickup.h" #include "taler-merchant-httpd_reserves.h" #include "taler-merchant-httpd_statics.h" #include "taler-merchant-httpd_templating.h" /** * Backlog for listen operation on unix-domain sockets. */ #define UNIX_BACKLOG 500 /** * Default maximum upload size permitted. Can be overridden * per handler. */ #define DEFAULT_MAX_UPLOAD_SIZE (16 * 1024) /** * Which currency do we use? */ char *TMH_currency; /** * 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 *TMH_by_id_map; /** * How long do we need to keep information on paid contracts on file for tax * or other legal reasons? Used to block deletions for younger transaction * data. */ struct GNUNET_TIME_Relative TMH_legal_expiration; /** * The port we are running on */ static uint16_t port; /** * Should a "Connection: close" header be added to each HTTP response? */ static int merchant_connection_close; /** * Task running the HTTP server. */ static struct GNUNET_SCHEDULER_Task *mhd_task; /** * Global return code */ static int result; /** * The MHD Daemon */ 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` */ 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. */ static struct GNUNET_CONTAINER_MultiHashMap *payment_trigger_map; /** * Task responsible for timeouts in the #resume_timeout_heap. */ static struct GNUNET_SCHEDULER_Task *resume_timeout_task; /** * Our configuration. */ static const struct GNUNET_CONFIGURATION_Handle *cfg; /** * Initial authorization token. */ static char *default_auth; /** * Holds data needed to determine when to resume a connection for * GET /orders/$ORDER_ID */ struct ResumeData { /** * How much of the order has been refunded. */ const struct TALER_Amount *refund_amount; /** * Whether the refunds for the order were obtained. */ bool obtained; }; int TMH_check_auth (const char *token, const struct GNUNET_ShortHashCode *salt, const struct GNUNET_HashCode *hash) { struct GNUNET_HashCode val; if (GNUNET_is_zero (hash)) return GNUNET_OK; if (NULL == token) return GNUNET_SYSERR; GNUNET_assert (GNUNET_YES == GNUNET_CRYPTO_kdf (&val, sizeof (val), salt, sizeof (*salt), token, strlen (token), "merchant-instance-auth", strlen ("merchant-instance-auth"), NULL, 0)); return (0 == GNUNET_memcmp (&val, hash)) ? GNUNET_OK : GNUNET_SYSERR; } void TMH_compute_auth (const char *token, struct GNUNET_ShortHashCode *salt, struct GNUNET_HashCode *hash) { GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, salt, sizeof (*salt)); GNUNET_assert (GNUNET_YES == GNUNET_CRYPTO_kdf (hash, sizeof (*hash), salt, sizeof (*salt), token, strlen (token), "merchant-instance-auth", strlen ("merchant-instance-auth"), NULL, 0)); } /** * 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) { struct TMH_WireMethod *wm; struct GNUNET_HashCode h_instance; mi->rc--; if (0 != mi->rc) return; GNUNET_CRYPTO_hash (mi->settings.id, strlen (mi->settings.id), &h_instance); GNUNET_assert (GNUNET_OK == GNUNET_CONTAINER_multihashmap_remove (TMH_by_id_map, &h_instance, mi)); TMH_force_get_orders_resume (mi); while (NULL != (wm = (mi->wm_head))) { GNUNET_CONTAINER_DLL_remove (mi->wm_head, mi->wm_tail, wm); json_decref (wm->j_wire); GNUNET_free (wm->wire_method); GNUNET_free (wm); } GNUNET_free (mi->settings.id); GNUNET_free (mi->settings.name); json_decref (mi->settings.address); 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; } /** * Callback that frees all the elements in the #payment_trigger_map. * This function should actually never be called, as by the time we * get to it, all payment triggers should have been cleaned up! * * @param cls closure, NULL * @param key current key * @param value a `struct TMH_SuspendedConnection` * @return #GNUNET_OK */ static int payment_trigger_free (void *cls, const struct GNUNET_HashCode *key, void *value) { struct TMH_SuspendedConnection *sc = value; (void) cls; (void) key; (void) sc; /* cannot really 'clean up' */ GNUNET_break (0); return GNUNET_OK; } /** * Compute @a key to use for @a order_id and @a mpub in our * #payment_trigger_map. * * @param order_id an order ID * @param mpub an instance public key * @param[out] key set to the hash map key to use */ 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]; memcpy (buf, mpub, sizeof (*mpub)); memcpy (&buf[sizeof (*mpub)], order_id, olen); GNUNET_CRYPTO_hash (buf, sizeof (buf), key); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Pay key for %s is %s\n", order_id, GNUNET_h2s (key)); } /** * Compute @a key to use for @a session_id and @a fulfillment_url in our * #payment_trigger_map. * * @param session_id the session for which @a fulfillment_url matters * @param fulfillment_url fullfillment URL of an order * @param[out] key set to the hash map key to use */ static void compute_pay_key2 (const char *session_id, const char *fulfillment_url, struct GNUNET_HashCode *key) { size_t slen = strlen (session_id) + 1; size_t ulen = strlen (fulfillment_url) + 1; char buf[slen + ulen]; memcpy (buf, session_id, slen); memcpy (&buf[slen], fulfillment_url, ulen); GNUNET_CRYPTO_hash (buf, sizeof (buf), key); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Pay key2 for %s/%s is %s\n", session_id, fulfillment_url, GNUNET_h2s (key)); } /** * Resume processing all suspended connections past timeout. * * @param cls unused */ static void do_resume (void *cls) { struct TMH_SuspendedConnection *sc; (void) cls; resume_timeout_task = NULL; while (1) { sc = GNUNET_CONTAINER_heap_peek (resume_timeout_heap); if (NULL == sc) return; if (0 != GNUNET_TIME_absolute_get_remaining ( sc->long_poll_timeout).rel_value_us) break; GNUNET_assert (sc == GNUNET_CONTAINER_heap_remove_root (resume_timeout_heap)); sc->hn = NULL; GNUNET_assert (GNUNET_YES == GNUNET_CONTAINER_multihashmap_remove (payment_trigger_map, &sc->key, sc)); if (sc->has_key2) GNUNET_assert (GNUNET_YES == GNUNET_CONTAINER_multihashmap_remove (payment_trigger_map, &sc->key2, sc)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming long polled job due to timeout\n"); MHD_resume_connection (sc->con); TMH_trigger_daemon (); /* we resumed, kick MHD */ } resume_timeout_task = GNUNET_SCHEDULER_add_at (sc->long_poll_timeout, &do_resume, NULL); } /** * Suspend connection from @a sc until payment has been received. * * @param order_id the order that we are waiting on * @param session_id session ID of the requester * @param fulfillment_url fulfillment URL of the contract * @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 (const char *order_id, const char *session_id, const char *fulfillment_url, const struct TMH_MerchantInstance *mi, struct TMH_SuspendedConnection *sc, const struct TALER_Amount *min_refund) { compute_pay_key (order_id, &mi->merchant_pub, &sc->key); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Suspending operation on key %s\n", GNUNET_h2s (&sc->key)); GNUNET_assert (GNUNET_OK == GNUNET_CONTAINER_multihashmap_put (payment_trigger_map, &sc->key, sc, GNUNET_CONTAINER_MULTIHASHMAPOPTION_MULTIPLE)); if ( (NULL != session_id) && (NULL != fulfillment_url) ) { sc->has_key2 = true; compute_pay_key2 (session_id, fulfillment_url, &sc->key2); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Suspending operation on %s/%s key2 %s\n", session_id, fulfillment_url, GNUNET_h2s (&sc->key2)); GNUNET_assert (GNUNET_OK == GNUNET_CONTAINER_multihashmap_put (payment_trigger_map, &sc->key2, sc, GNUNET_CONTAINER_MULTIHASHMAPOPTION_MULTIPLE)); } if (NULL != min_refund) { sc->awaiting_refund = true; sc->refund_expected = *min_refund; } sc->hn = GNUNET_CONTAINER_heap_insert (resume_timeout_heap, sc, sc->long_poll_timeout.abs_value_us); MHD_suspend_connection (sc->con); if (NULL != resume_timeout_task) { GNUNET_SCHEDULER_cancel (resume_timeout_task); resume_timeout_task = NULL; } sc = GNUNET_CONTAINER_heap_peek (resume_timeout_heap); resume_timeout_task = GNUNET_SCHEDULER_add_at (sc->long_poll_timeout, &do_resume, NULL); } /** * Function called to resume suspended connections. * * @param cls pointer to a `struct TALER_Amount` indicating the refund amount, or NULL * @param key key in the #payment_trigger_map * @param value a `struct TMH_SuspendedConnection` to resume * @return #GNUNET_OK (continue to iterate) */ static int resume_operation (void *cls, const struct GNUNET_HashCode *key, void *value) { const struct ResumeData *rd = cls; struct TMH_SuspendedConnection *sc = value; GNUNET_assert (0 == GNUNET_memcmp (key, &sc->key)); /* If the conditions are satisfied partially, turn them off for future calls. */ if ( (sc->awaiting_refund_obtained) && (rd->obtained)) sc->awaiting_refund_obtained = false; if ( (sc->awaiting_refund) && ( (NULL != rd->refund_amount) && (1 == TALER_amount_cmp (rd->refund_amount, &sc->refund_expected)) ) ) sc->awaiting_refund = false; if ( (sc->awaiting_refund_obtained) && (! rd->obtained)) { GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Not awaking client, refunds not yet obtained\n"); return GNUNET_OK; } if ( (sc->awaiting_refund) && ( (NULL == rd->refund_amount) || (1 != TALER_amount_cmp (rd->refund_amount, &sc->refund_expected)) ) ) { GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Not awaking client, refund amount of %s not yet satisfied\n", TALER_amount2s (&sc->refund_expected)); return GNUNET_OK; /* skip */ } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming operation suspended pending payment on key %s\n", GNUNET_h2s (key)); GNUNET_assert (GNUNET_YES == GNUNET_CONTAINER_multihashmap_remove (payment_trigger_map, key, sc)); if (sc->has_key2) GNUNET_assert (GNUNET_YES == GNUNET_CONTAINER_multihashmap_remove (payment_trigger_map, &sc->key2, sc)); GNUNET_assert (sc == GNUNET_CONTAINER_heap_remove_node (sc->hn)); sc->hn = NULL; MHD_resume_connection (sc->con); TMH_trigger_daemon (); return GNUNET_OK; } /** * 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 or refunded * @param mi the merchant instance where the payment or refund happened * @param refund_amount refunded amount, if the trigger was a refund, otherwise NULL * @param obtained if true, the wallet has obtained the refunds for the order */ void TMH_long_poll_resume (const char *order_id, const struct TMH_MerchantInstance *mi, const struct TALER_Amount *refund_amount, bool obtained) { struct GNUNET_HashCode key; struct ResumeData rd = { .refund_amount = refund_amount, .obtained = obtained }; int ret; compute_pay_key (order_id, &mi->merchant_pub, &key); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming operations suspended pending payment on key %s up to refund %s\n", GNUNET_h2s (&key), (NULL != refund_amount) ? TALER_amount2s (refund_amount) : ""); ret = GNUNET_CONTAINER_multihashmap_get_multiple (payment_trigger_map, &key, &resume_operation, (void *) &rd); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "%u operations remain suspended pending payment (%d)\n", GNUNET_CONTAINER_multihashmap_size (payment_trigger_map), ret); } /** * Function called to resume suspended connections. * * @param cls NULL * @param key key in the #payment_trigger_map * @param value a `struct TMH_SuspendedConnection` to resume * @return #GNUNET_OK (continue to iterate) */ static int resume_operation2 (void *cls, const struct GNUNET_HashCode *key, void *value) { struct TMH_SuspendedConnection *sc = value; GNUNET_assert (sc->has_key2); GNUNET_assert (0 == GNUNET_memcmp (key, &sc->key2)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming operation suspended pending payment on key %s\n", GNUNET_h2s (key)); GNUNET_assert (GNUNET_YES == GNUNET_CONTAINER_multihashmap_remove (payment_trigger_map, &sc->key, sc)); GNUNET_assert (GNUNET_YES == GNUNET_CONTAINER_multihashmap_remove (payment_trigger_map, &sc->key2, sc)); GNUNET_assert (sc == GNUNET_CONTAINER_heap_remove_node (sc->hn)); sc->hn = NULL; MHD_resume_connection (sc->con); TMH_trigger_daemon (); return GNUNET_OK; } /** * 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 session_id the session for which @a fulfillment_url became paid * @param fulfillment_url fullfillment URL of which an order was paid */ void TMH_long_poll_resume2 (const char *session_id, const char *fulfillment_url) { struct GNUNET_HashCode key; int ret; compute_pay_key2 (session_id, fulfillment_url, &key); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming operations suspended pending payment on %s/%s with key2 %s\n", session_id, fulfillment_url, GNUNET_h2s (&key)); ret = GNUNET_CONTAINER_multihashmap_get_multiple (payment_trigger_map, &key, &resume_operation2, NULL); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "%u operations remain suspended pending payment (%d)\n", GNUNET_CONTAINER_multihashmap_size (payment_trigger_map), ret); } /** * Shutdown task (magically invoked when the application is being * quit) * * @param cls NULL */ static void do_shutdown (void *cls) { struct TMH_SuspendedConnection *sc; (void) cls; TMH_force_ac_resume (); TMH_force_pc_resume (); TMH_force_rc_resume (); TMH_force_post_transfers_resume (); TMH_force_tip_pickup_resume (); TMH_force_wallet_refund_order_resume (); if (NULL != mhd_task) { GNUNET_SCHEDULER_cancel (mhd_task); mhd_task = NULL; } /* resume all suspended connections, must be done before stopping #mhd */ if (NULL != resume_timeout_heap) { while (NULL != (sc = GNUNET_CONTAINER_heap_remove_root ( resume_timeout_heap))) { sc->hn = NULL; GNUNET_assert (GNUNET_YES == GNUNET_CONTAINER_multihashmap_remove (payment_trigger_map, &sc->key, sc)); MHD_resume_connection (sc->con); } GNUNET_CONTAINER_heap_destroy (resume_timeout_heap); resume_timeout_heap = NULL; } if (NULL != resume_timeout_task) { GNUNET_SCHEDULER_cancel (resume_timeout_task); resume_timeout_task = NULL; } if (NULL != mhd) { MHD_stop_daemon (mhd); mhd = NULL; } TMH_RESERVES_done (); if (NULL != TMH_db) { TALER_MERCHANTDB_plugin_unload (TMH_db); TMH_db = NULL; } TMH_EXCHANGES_done (); TMH_AUDITORS_done (); if (NULL != payment_trigger_map) { GNUNET_CONTAINER_multihashmap_iterate (payment_trigger_map, &payment_trigger_free, NULL); GNUNET_CONTAINER_multihashmap_destroy (payment_trigger_map); payment_trigger_map = NULL; } if (NULL != TMH_by_id_map) { GNUNET_CONTAINER_multihashmap_iterate (TMH_by_id_map, &instance_free_cb, NULL); GNUNET_CONTAINER_multihashmap_destroy (TMH_by_id_map); TMH_by_id_map = NULL; } } /** * Function called whenever MHD is done with a request. If the * request was a POST, we may have stored a `struct Buffer *` in the * @a con_cls that might still need to be cleaned up. Call the * respective function to free the memory. * * @param cls client-defined closure * @param connection connection handle * @param con_cls value as set by the last call to * the #MHD_AccessHandlerCallback * @param toe reason for request termination * @see #MHD_OPTION_NOTIFY_COMPLETED * @ingroup request */ static void handle_mhd_completion_callback (void *cls, struct MHD_Connection *connection, void **con_cls, enum MHD_RequestTerminationCode toe) { struct TMH_HandlerContext *hc = *con_cls; if (NULL == hc) return; GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Finished handling request for `%s' with MHD termination code %d\n", hc->url, (int) toe); if (NULL != hc->cc) hc->cc (hc->ctx); TALER_MHD_parse_post_cleanup_callback (hc->json_parse_context); GNUNET_free (hc->infix); if (NULL != hc->request_body) json_decref (hc->request_body); if (NULL != hc->instance) TMH_instance_decref (hc->instance); memset (&hc->async_scope_id, 0, sizeof (struct GNUNET_AsyncScopeId)); GNUNET_free (hc); *con_cls = NULL; } /** * Function that queries MHD's select sets and * starts the task waiting for them. */ static struct GNUNET_SCHEDULER_Task * prepare_daemon (void); /** * Set if we should immediately #MHD_run again. */ static int triggered; /** * Call MHD to process pending requests and then go back * and schedule the next run. * * @param cls NULL */ static void run_daemon (void *cls) { mhd_task = NULL; do { triggered = 0; GNUNET_assert (MHD_YES == MHD_run (mhd)); } while (0 != triggered); mhd_task = prepare_daemon (); } /** * Kick MHD to run now, to be called after MHD_resume_connection(). * Basically, we need to explicitly resume MHD's event loop whenever * we made progress serving a request. This function re-schedules * the task processing MHD's activities to run immediately. */ void TMH_trigger_daemon () { if (NULL != mhd_task) { GNUNET_SCHEDULER_cancel (mhd_task); mhd_task = NULL; run_daemon (NULL); } else { triggered = 1; } } /** * Function that queries MHD's select sets and * starts the task waiting for them. */ static struct GNUNET_SCHEDULER_Task * prepare_daemon (void) { struct GNUNET_SCHEDULER_Task *ret; fd_set rs; fd_set ws; fd_set es; struct GNUNET_NETWORK_FDSet *wrs; struct GNUNET_NETWORK_FDSet *wws; int max; MHD_UNSIGNED_LONG_LONG timeout; int haveto; struct GNUNET_TIME_Relative tv; FD_ZERO (&rs); FD_ZERO (&ws); FD_ZERO (&es); wrs = GNUNET_NETWORK_fdset_create (); wws = GNUNET_NETWORK_fdset_create (); max = -1; GNUNET_assert (MHD_YES == MHD_get_fdset (mhd, &rs, &ws, &es, &max)); haveto = MHD_get_timeout (mhd, &timeout); if (haveto == MHD_YES) tv.rel_value_us = (uint64_t) timeout * 1000LL; else tv = GNUNET_TIME_UNIT_FOREVER_REL; GNUNET_NETWORK_fdset_copy_native (wrs, &rs, max + 1); GNUNET_NETWORK_fdset_copy_native (wws, &ws, max + 1); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Adding run_daemon select task\n"); ret = GNUNET_SCHEDULER_add_select (GNUNET_SCHEDULER_PRIORITY_HIGH, tv, wrs, wws, &run_daemon, NULL); GNUNET_NETWORK_fdset_destroy (wrs); GNUNET_NETWORK_fdset_destroy (wws); return ret; } /** * 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 */ struct TMH_MerchantInstance * TMH_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); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Looking for by-id key %s of '%s' in hashmap\n", GNUNET_h2s (&h_instance), instance_id); /* We're fine if that returns NULL, the calling routine knows how to handle that */ return GNUNET_CONTAINER_multihashmap_get (TMH_by_id_map, &h_instance); } /** * 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->settings.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 (TMH_by_id_map, &h_instance, mi, GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY); if (GNUNET_OK == ret) mi->rc++; return ret; } /** * Extract the token from authorization header value @a auth. * * @param auth pointer to authorization header value, * will be updated to point to the start of the token * or set to NULL if header value is invalid */ static void extract_token (const char **auth) { const char *bearer = "Bearer "; const char *tok = *auth; if (0 != strncmp (tok, bearer, strlen (bearer))) { *auth = NULL; return; } tok = tok + strlen (bearer); while (' ' == *tok) tok++; if (0 != strncasecmp (tok, RFC_8959_PREFIX, strlen (RFC_8959_PREFIX))) { *auth = NULL; return; } *auth = tok; } /** * 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 * must call MHD callbacks to provide content to give back to the * client and return an HTTP status code (i.e. #MHD_HTTP_OK, * #MHD_HTTP_NOT_FOUND, etc.). * * @param cls argument given together with the function * pointer when the handler was registered with MHD * @param connection the MHD connection to handle * @param url the requested url * @param method the HTTP method used (#MHD_HTTP_METHOD_GET, * #MHD_HTTP_METHOD_PUT, etc.) * @param version the HTTP version string (i.e. * #MHD_HTTP_VERSION_1_1) * @param upload_data the data being uploaded (excluding HEADERS, * for a POST that fits into memory and that is encoded * with a supported encoding, the POST data will NOT be * given in upload_data and is instead available as * part of #MHD_get_connection_values; very large POST * data *will* be made available incrementally in * @a upload_data) * @param upload_data_size set initially to the size of the * @a upload_data provided; the method must update this * value to the number of bytes NOT processed; * @param con_cls pointer that the callback can set to some * address and that will be preserved by MHD for future * calls for this request; since the access handler may * be called many times (i.e., for a PUT/POST operation * with plenty of upload data) this allows the application * to easily associate some request-specific state. * If necessary, this state can be cleaned up in the * global #MHD_RequestCompletedCallback (which * can be set with the #MHD_OPTION_NOTIFY_COMPLETED). * Initially, `*con_cls` will be NULL. * @return #MHD_YES if the connection was handled successfully, * #MHD_NO if the socket must be closed due to a serious * error while handling the request */ static MHD_RESULT url_handler (void *cls, struct MHD_Connection *connection, const char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **con_cls) { static struct TMH_RequestHandler private_handlers[] = { /* GET /instances; MUST be at the beginning of the array, as this endpoint ONLY applies to the default instance! See use_default logic below. */ { .url_prefix = "/instances", .method = MHD_HTTP_METHOD_GET, .skip_instance = true, .handler = &TMH_private_get_instances }, /* POST /instances; MUST be at the beginning of the array, as this endpoint ONLY applies to the default instance! See use_default logic below. */ { .url_prefix = "/instances", .method = MHD_HTTP_METHOD_POST, .skip_instance = true, .handler = &TMH_private_post_instances, /* allow instance data of up to 8 MB, that should be plenty; note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB) would require further changes to the allocation logic in the code... */ .max_upload = 1024 * 1024 * 8 }, /* **** End of array entries specific to default instance **** */ /* GET /instances/$ID/: */ { .url_prefix = "/", .method = MHD_HTTP_METHOD_GET, .handler = &TMH_private_get_instances_ID }, /* DELETE /instances/$ID/: */ { .url_prefix = "/", .method = MHD_HTTP_METHOD_DELETE, .handler = &TMH_private_delete_instances_ID }, /* PATCH /instances/$ID/: */ { .url_prefix = "/", .method = MHD_HTTP_METHOD_PATCH, .handler = &TMH_private_patch_instances_ID, /* allow instance data of up to 8 MB, that should be plenty; note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB) would require further changes to the allocation logic in the code... */ .max_upload = 1024 * 1024 * 8 }, /* POST /auth: */ { .url_prefix = "/auth", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_private_post_instances_ID_auth, /* Body should be pretty small. */ .max_upload = 1024 * 1024, }, /* GET /products: */ { .url_prefix = "/products", .method = MHD_HTTP_METHOD_GET, .handler = &TMH_private_get_products }, /* POST /products: */ { .url_prefix = "/products", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_private_post_products, /* allow product data of up to 8 MB, that should be plenty; note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB) would require further changes to the allocation logic in the code... */ .max_upload = 1024 * 1024 * 8 }, /* GET /products/$ID/: */ { .url_prefix = "/products/", .method = MHD_HTTP_METHOD_GET, .have_id_segment = true, .handler = &TMH_private_get_products_ID }, /* DELETE /products/$ID/: */ { .url_prefix = "/products/", .method = MHD_HTTP_METHOD_DELETE, .have_id_segment = true, .handler = &TMH_private_delete_products_ID }, /* PATCH /products/$ID/: */ { .url_prefix = "/products/", .method = MHD_HTTP_METHOD_PATCH, .have_id_segment = true, .handler = &TMH_private_patch_products_ID, /* allow product data of up to 8 MB, that should be plenty; note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB) would require further changes to the allocation logic in the code... */ .max_upload = 1024 * 1024 * 8 }, /* POST /products/$ID/lock: */ { .url_prefix = "/products/", .url_suffix = "lock", .method = MHD_HTTP_METHOD_POST, .have_id_segment = true, .handler = &TMH_private_post_products_ID_lock, /* the body should be pretty small, allow 1 MB of upload to set a conservative bound for sane wallets */ .max_upload = 1024 * 1024 }, /* POST /orders: */ { .url_prefix = "/orders", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_private_post_orders, /* allow contracts of up to 8 MB, that should be plenty; note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB) would require further changes to the allocation logic in the code... */ .max_upload = 1024 * 1024 * 8 }, /* GET /orders/$ID: */ { .url_prefix = "/orders/", .method = MHD_HTTP_METHOD_GET, .have_id_segment = true, .handler = &TMH_private_get_orders_ID }, /* GET /orders: */ { .url_prefix = "/orders", .method = MHD_HTTP_METHOD_GET, .handler = &TMH_private_get_orders }, /* POST /orders/$ID/refund: */ { .url_prefix = "/orders/", .url_suffix = "refund", .method = MHD_HTTP_METHOD_POST, .have_id_segment = true, .handler = &TMH_private_post_orders_ID_refund, /* the body should be pretty small, allow 1 MB of upload to set a conservative bound for sane wallets */ .max_upload = 1024 * 1024 }, /* PATCH /orders/$ID/forget: */ { .url_prefix = "/orders/", .url_suffix = "forget", .method = MHD_HTTP_METHOD_PATCH, .have_id_segment = true, .handler = &TMH_private_patch_orders_ID_forget, /* the body should be pretty small, allow 1 MB of upload to set a conservative bound for sane wallets */ .max_upload = 1024 * 1024 }, /* DELETE /orders/$ID: */ { .url_prefix = "/orders/", .method = MHD_HTTP_METHOD_DELETE, .have_id_segment = true, .handler = &TMH_private_delete_orders_ID }, /* POST /reserves: */ { .url_prefix = "/reserves", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_private_post_reserves, /* the body should be pretty small, allow 1 MB of upload to set a conservative bound for sane wallets */ .max_upload = 1024 * 1024 }, /* DELETE /reserves/$ID: */ { .url_prefix = "/reserves/", .have_id_segment = true, .method = MHD_HTTP_METHOD_DELETE, .handler = &TMH_private_delete_reserves_ID }, /* POST /reserves/$ID/authorize-tip: */ { .url_prefix = "/reserves/", .url_suffix = "authorize-tip", .have_id_segment = true, .method = MHD_HTTP_METHOD_POST, .handler = &TMH_private_post_reserves_ID_authorize_tip, /* the body should be pretty small, allow 1 MB of upload to set a conservative bound for sane wallets */ .max_upload = 1024 * 1024 }, /* POST /tips: */ { .url_prefix = "/tips", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_private_post_tips, /* the body should be pretty small, allow 1 MB of upload to set a conservative bound for sane wallets */ .max_upload = 1024 * 1024 }, /* GET /tips: */ { .url_prefix = "/tips", .method = MHD_HTTP_METHOD_GET, .handler = &TMH_private_get_tips }, /* GET /tips/$ID: */ { .url_prefix = "/tips/", .method = MHD_HTTP_METHOD_GET, .have_id_segment = true, .handler = &TMH_private_get_tips_ID }, /* GET /reserves: */ { .url_prefix = "/reserves", .method = MHD_HTTP_METHOD_GET, .handler = &TMH_private_get_reserves }, /* GET /reserves: */ { .url_prefix = "/reserves/", .have_id_segment = true, .method = MHD_HTTP_METHOD_GET, .handler = &TMH_private_get_reserves_ID }, /* POST /transfers: */ { .url_prefix = "/transfers", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_private_post_transfers, /* the body should be pretty small, allow 1 MB of upload to set a conservative bound for sane wallets */ .max_upload = 1024 * 1024 }, /* GET /transfers: */ { .url_prefix = "/transfers", .method = MHD_HTTP_METHOD_GET, .handler = &TMH_private_get_transfers }, { NULL } }; static struct TMH_RequestHandler public_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 }, { .url_prefix = "/config", .method = MHD_HTTP_METHOD_GET, .skip_instance = true, .handler = &MH_handler_config }, /* Also serve the same /config per instance */ { .url_prefix = "/config", .method = MHD_HTTP_METHOD_GET, .skip_instance = false, .handler = &MH_handler_config }, /* POST /orders/$ID/abort: */ { .url_prefix = "/orders/", .have_id_segment = true, .url_suffix = "abort", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_post_orders_ID_abort, /* wallet may give us many coins to sign, allow 1 MB of upload to set a conservative bound for sane wallets */ .max_upload = 1024 * 1024 }, /* POST /orders/$ID/claim: */ { .url_prefix = "/orders/", .have_id_segment = true, .url_suffix = "claim", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_post_orders_ID_claim, /* the body should be pretty small, allow 1 MB of upload to set a conservative bound for sane wallets */ .max_upload = 1024 * 1024 }, /* POST /orders/$ID/pay: */ { .url_prefix = "/orders/", .have_id_segment = true, .url_suffix = "pay", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_post_orders_ID_pay, /* wallet may give us many coins to sign, allow 1 MB of upload to set a conservative bound for sane wallets */ .max_upload = 1024 * 1024 }, /* POST /orders/$ID/paid: */ { .url_prefix = "/orders/", .have_id_segment = true, .url_suffix = "paid", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_post_orders_ID_paid, /* the body should be pretty small, allow 1 MB of upload to set a conservative bound for sane wallets */ .max_upload = 1024 * 1024 }, /* POST /orders/$ID/refund: */ { .url_prefix = "/orders/", .have_id_segment = true, .url_suffix = "refund", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_post_orders_ID_refund, /* the body should be pretty small, allow 1 MB of upload to set a conservative bound for sane wallets */ .max_upload = 1024 * 1024 }, /* GET /orders/$ID: */ { .url_prefix = "/orders/", .method = MHD_HTTP_METHOD_GET, .have_id_segment = true, .handler = &TMH_get_orders_ID }, /* GET /tips/$ID: */ { .url_prefix = "/tips/", .method = MHD_HTTP_METHOD_GET, .have_id_segment = true, .handler = &TMH_get_tips_ID }, /* POST /tips/$ID/pickup: */ { .url_prefix = "/tips/", .method = MHD_HTTP_METHOD_POST, .have_id_segment = true, .url_suffix = "pickup", .handler = &TMH_post_tips_ID_pickup, /* wallet may give us many coins to sign, allow 1 MB of upload to set a conservative bound for sane wallets */ .max_upload = 1024 * 1024 }, /* GET /static/ *: */ { .url_prefix = "/static/", .method = MHD_HTTP_METHOD_GET, .have_id_segment = true, .handler = &TMH_return_static }, { NULL } }; struct TMH_HandlerContext *hc = *con_cls; struct TMH_RequestHandler *handlers; bool use_private = false; bool use_default = false; (void) cls; (void) version; if (NULL != hc) { GNUNET_assert (NULL != hc->rh); GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id); if ( (hc->has_body) && (NULL == hc->request_body) ) { int res; if ( (hc->total_upload + *upload_data_size < hc->total_upload) || (hc->total_upload + *upload_data_size > hc->rh->max_upload) ) { /* Client exceeds upload limit. Should _usually_ be checked earlier when we look at the MHD_HTTP_HEADER_CONTENT_LENGTH, alas with chunked encoding an uploader MAY have omitted this, and thus not permitted us to check on time. In this case, we just close the connection once it exceeds our limit (instead of waiting for the upload to complete and then fail). This could theoretically cause some clients to retry, alas broken or malicious clients are likely to retry anyway, so little we can do about it, and failing earlier seems the best option here. */// GNUNET_break_op (0); return MHD_NO; } hc->total_upload += *upload_data_size; res = TALER_MHD_parse_post_json (connection, &hc->json_parse_context, upload_data, upload_data_size, &hc->request_body); 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->request_body) ) 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)) ) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Illegal incoming correlation ID\n"); correlation_id = NULL; } 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); } 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 (url, instance_prefix, strlen (instance_prefix))) { /* url starts with "/instances/" */ const char *istart = url + strlen (instance_prefix); const char *slash = strchr (istart, '/'); char *instance_id; if (NULL == slash) instance_id = GNUNET_strdup (istart); else instance_id = GNUNET_strndup (istart, slash - istart); hc->instance = TMH_lookup_instance (instance_id); GNUNET_free (instance_id); if (NULL == hc->instance) return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_GENERIC_ENDPOINT_UNKNOWN, url); if (NULL == slash) url = ""; else url = slash; } else { /* use 'default' */ use_default = true; hc->instance = TMH_lookup_instance (NULL); if ( (NULL != default_auth) && (NULL != hc->instance) ) { /* Override default instance access control */ TMH_compute_auth (default_auth, &hc->instance->auth.auth_salt, &hc->instance->auth.auth_hash); GNUNET_free (default_auth); } } if (NULL != hc->instance) hc->instance->rc++; } { const char *private_prefix = "/private/"; if ( (0 == strncmp (url, private_prefix, strlen (private_prefix))) || (0 == strcmp (url, "/private")) ) { if (use_default) handlers = private_handlers; else handlers = &private_handlers[2]; /* skip first two methods: default instance-only! */ url += strlen (private_prefix) - 1; use_private = true; } else { handlers = public_handlers; } } if (0 == strcmp (url, "")) url = "/"; /* code below does not like empty string */ { /* 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 */ { const char *slash; slash = strchr (&url[1], '/'); if (NULL == slash) { 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 + 1; /* skip the '/' */ suffix_strlen = strlen (suffix_url); } hc->infix = GNUNET_strndup (infix_url, infix_strlen); } } { 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 (GNUNET_NO == rh->have_id_segment) { if (NULL != suffix_url) continue; /* too many segments to match */ if ( (NULL == infix_url) ^ (NULL == rh->url_suffix) ) continue; /* suffix existence mismatch */ if ( (NULL != infix_url) && ( (infix_strlen != strlen (rh->url_suffix)) || (0 != memcmp (infix_url, rh->url_suffix, infix_strlen)) ) ) continue; /* cannot use infix as suffix: content mismatch */ } else { if ( (NULL == infix_url) ^ (GNUNET_NO == rh->have_id_segment) ) continue; /* infix existence mismatch */ if ( ( (NULL == suffix_url) ^ (NULL == rh->url_suffix) ) ) continue; /* suffix existence mismatch */ if ( (NULL != suffix_url) && ( (suffix_strlen != strlen (rh->url_suffix)) || (0 != memcmp (suffix_url, rh->url_suffix, suffix_strlen)) ) ) continue; /* suffix content mismatch */ } 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_with_error (connection, MHD_HTTP_METHOD_NOT_ALLOWED, TALER_EC_GENERIC_METHOD_INVALID, method); if (NULL == hc->rh) return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_GENERIC_ENDPOINT_UNKNOWN, hc->url); } } /* At this point, we must have found a handler */ GNUNET_assert (NULL != hc->rh); /* Access control for private handlers */ if (use_private) { const char *auth; struct TMH_MerchantInstance *def_instance; bool auth_ok; bool auth_malformed = false; /* PATCHing an instance can alternatively be checked against the default instance */ def_instance = TMH_lookup_instance (NULL); auth = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_AUTHORIZATION); if (NULL != auth) { /* We _only_ complain about malformed auth headers if authorization was truly required (#6737). This helps in case authorization was disabled in the backend because some reverse proxy is already doing it, and then that reverse proxy may forward malformed auth headers to the backend. */ extract_token (&auth); if (NULL == auth) auth_malformed = true; } /* Are the credentials provided OK for the default instance? Check against CLI override and default instance. */ auth_ok = ( (NULL == default_auth) || ( (NULL != auth) && (0 == strcmp (auth, default_auth)) ) ); /* If we have no default instance, authentication is satisfied EVEN if the 'default_auth' is NULL; otherwise, only if the default_auth matched OR the auth_hash matched */ if ( (NULL != def_instance) && (NULL == default_auth) ) auth_ok = (GNUNET_OK == TMH_check_auth (auth, &def_instance->auth.auth_salt, &def_instance->auth.auth_hash)); /* Only permit 'default' auth if we are either working with the default instance OR patching/deleting an instance OR have no instance */ if ( (hc->rh->handler != &TMH_private_patch_instances_ID) && (hc->rh->handler != &TMH_private_delete_instances_ID) && ( (NULL != hc->instance) || (def_instance != hc->instance) ) ) auth_ok = false; /* Check against selected instance if we have one */ if (NULL != hc->instance) auth_ok |= (GNUNET_OK == TMH_check_auth (auth, &hc->instance->auth.auth_salt, &hc->instance->auth.auth_hash)); if (! auth_ok) { if (auth_malformed) return TALER_MHD_reply_with_error (connection, MHD_HTTP_UNAUTHORIZED, TALER_EC_GENERIC_PARAMETER_MALFORMED, "'" RFC_8959_PREFIX "' prefix missing in 'Authorization' header"); return TALER_MHD_reply_with_error (connection, MHD_HTTP_UNAUTHORIZED, TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED, "Check 'Authorization' header"); } } /* if (use_private) */ if ( (NULL == hc->instance) && (GNUNET_YES != hc->rh->skip_instance) ) return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, url); hc->has_body = ( (0 == strcasecmp (method, MHD_HTTP_METHOD_POST)) || (0 == strcasecmp (method, MHD_HTTP_METHOD_PATCH)) ); if (hc->has_body) { const char *cl; /* Maybe check for maximum upload size and refuse requests if they are just too big. */ cl = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_LENGTH); if (NULL != cl) { unsigned long long cv; size_t mul = hc->rh->max_upload; if (0 == mul) mul = DEFAULT_MAX_UPLOAD_SIZE; if (1 != sscanf (cl, "%llu", &cv)) { /* Not valid HTTP request, just close connection. */ GNUNET_break_op (0); return MHD_NO; } if (cv > mul) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_PAYLOAD_TOO_LARGE, TALER_EC_GENERIC_UPLOAD_EXCEEDS_LIMIT, cl); } } GNUNET_break (NULL == hc->request_body); /* can't have it already */ return MHD_YES; /* proceed with upload */ } return hc->rh->handler (hc->rh, connection, hc); } /** * 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, const struct TALER_MERCHANTDB_InstanceAuthSettings *ias, unsigned int accounts_length, const 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->auth = *ias; mi->settings.id = GNUNET_strdup (mi->settings.id); mi->settings.name = GNUNET_strdup (mi->settings.name); mi->settings.address = json_incref (mi->settings.address); mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction); mi->merchant_priv = *merchant_priv; mi->merchant_pub = *merchant_pub; for (unsigned int i = 0; ih_wire = acc->h_wire; wm->j_wire = json_pack ("{s:s, s:o}", "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 * @param args remaining command-line arguments * @param cfgfile name of the configuration file used (for saving, can be * NULL!) * @param config configuration */ static void run (void *cls, char *const *args, const char *cfgfile, const struct GNUNET_CONFIGURATION_Handle *config) { int fh; enum TALER_MHD_GlobalOptions go; int elen; int alen; const char *tok; (void) cls; (void) args; (void) cfgfile; tok = getenv ("TALER_MERCHANT_TOKEN"); if ( (NULL != tok) && (NULL == default_auth) ) default_auth = GNUNET_strdup (tok); cfg = config; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Starting taler-merchant-httpd\n"); go = TALER_MHD_GO_NONE; if (merchant_connection_close) go |= TALER_MHD_GO_FORCE_CONNECTION_CLOSE; TALER_MHD_setup (go); result = GNUNET_SYSERR; GNUNET_SCHEDULER_add_shutdown (&do_shutdown, NULL); resume_timeout_heap = GNUNET_CONTAINER_heap_create (GNUNET_CONTAINER_HEAP_ORDER_MIN); payment_trigger_map = GNUNET_CONTAINER_multihashmap_create (16, GNUNET_YES); if (GNUNET_OK != TALER_config_get_currency (cfg, &TMH_currency)) { GNUNET_SCHEDULER_shutdown (); return; } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_time (cfg, "merchant", "LEGAL_PRESERVATION", &TMH_legal_expiration)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "merchant", "LEGAL_PRESERVATION"); GNUNET_SCHEDULER_shutdown (); return; } if (GNUNET_YES == GNUNET_CONFIGURATION_get_value_yesno (cfg, "merchant", "FORCE_AUDIT")) TMH_force_audit = GNUNET_YES; TMH_templating_init (); TMH_statics_init (); elen = TMH_EXCHANGES_init (config); if (GNUNET_SYSERR == elen) { GNUNET_SCHEDULER_shutdown (); return; } alen = TMH_AUDITORS_init (config); if (GNUNET_SYSERR == alen) { GNUNET_SCHEDULER_shutdown (); return; } if (0 == elen + alen) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Fatal: no trusted exchanges and no trusted auditors configured. Exiting.\n"); GNUNET_SCHEDULER_shutdown (); return; } if (NULL == (TMH_by_id_map = GNUNET_CONTAINER_multihashmap_create (1, GNUNET_NO))) { GNUNET_SCHEDULER_shutdown (); return; } if (NULL == (TMH_db = TALER_MERCHANTDB_plugin_load (cfg))) { GNUNET_SCHEDULER_shutdown (); return; } if (GNUNET_OK != TMH_db->connect (TMH_db->cls)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to initialze database connection\n"); 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; } } /* start watching reserves */ TMH_RESERVES_init (); fh = TALER_MHD_bind (cfg, "merchant", &port); if ( (0 == port) && (-1 == fh) ) { GNUNET_SCHEDULER_shutdown (); return; } mhd = MHD_start_daemon (MHD_USE_SUSPEND_RESUME | MHD_USE_DUAL_STACK | MHD_USE_AUTO, port, NULL, NULL, &url_handler, NULL, MHD_OPTION_LISTEN_SOCKET, fh, MHD_OPTION_NOTIFY_COMPLETED, &handle_mhd_completion_callback, NULL, MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int) 10 /* 10s */, MHD_OPTION_END); if (NULL == mhd) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to launch HTTP service. Is the port in use?\n"); GNUNET_SCHEDULER_shutdown (); return; } result = GNUNET_OK; mhd_task = prepare_daemon (); } /** * The main function of the serve tool * * @param argc number of arguments from the command line * @param argv command line arguments * @return 0 ok, non-zero on error */ int main (int argc, char *const *argv) { enum GNUNET_GenericReturnValue res; struct GNUNET_GETOPT_CommandLineOption options[] = { GNUNET_GETOPT_option_flag ('C', "connection-close", "force HTTP connections to be closed after each request", &merchant_connection_close), GNUNET_GETOPT_option_timetravel ('T', "timetravel"), GNUNET_GETOPT_option_string ('a', "auth", "TOKEN", "use TOKEN to initially authenticate access to the default instance (you can also set the TALER_MERCHANT_TOKEN environment variable instead)", &default_auth), GNUNET_GETOPT_OPTION_END }; res = GNUNET_PROGRAM_run (argc, argv, "taler-merchant-httpd", "Taler merchant's HTTP backend interface", options, &run, NULL); if (GNUNET_SYSERR == res) return 3; if (GNUNET_NO == res) return 0; return (GNUNET_OK == result) ? 0 : 1; }