/* This file is part of TALER (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 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 merchant/backend/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-products-ID.h" #include "taler-merchant-httpd_private-post-instances.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-pay.h" /** * Backlog for listen operation on unix-domain sockets. */ #define UNIX_BACKLOG 500 /** * 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; /** * 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)); 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 key[out] 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)); } /** * 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)); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "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 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 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 != 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 TALER_Amount *have_refund = cls; struct TMH_SuspendedConnection *sc = value; if ( (sc->awaiting_refund) && ( (NULL == have_refund) || (1 != TALER_amount_cmp (have_refund, &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)); 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 have_refund refunded amount, NULL if there was no refund */ void TMH_long_poll_resume (const char *order_id, const struct TMH_MerchantInstance *mi, const struct TALER_Amount *have_refund) { struct GNUNET_HashCode key; compute_pay_key (order_id, &mi->merchant_pub, &key); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming operations suspended pending payment on key %s\n", GNUNET_h2s (&key)); GNUNET_CONTAINER_multihashmap_get_multiple (payment_trigger_map, &key, &resume_operation, (void *) have_refund); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "%u operations remain suspended pending payment\n", GNUNET_CONTAINER_multihashmap_size (payment_trigger_map)); } /** * 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_get_orders_resume (); TMH_force_ac_resume (); TMH_force_pc_resume (); TMH_force_post_transfers_resume (); #if 0 TMH_force_trh_resume (); TMH_force_refund_resume (); TMH_force_tip_pickup_resume (); #endif 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; } 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_log (GNUNET_ERROR_TYPE_INFO, "Finished handling request for `%s' with status %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_non_null (hc->infix); if (NULL != hc->request_body) json_decref (hc->request_body); if (NULL != hc->instance) TMH_instance_decref (hc->instance); 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. * * @param daemon_handle HTTP server to prepare to run */ 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; } /** * 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 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: */ { .url_prefix = "/instances", .method = MHD_HTTP_METHOD_GET, .skip_instance = true, .handler = &TMH_private_get_instances }, /* 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 }, /* POST /instances: */ { .url_prefix = "/instances", .method = MHD_HTTP_METHOD_POST, .skip_instance = true, .handler = &TMH_private_post_instances }, /* 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 }, /* 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 }, /* 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 }, /* POST /orders: */ { .url_prefix = "/orders", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_private_post_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 }, /* POST /reserves: */ { .url_prefix = "/reserves", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_private_post_reserves }, /* 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 }, /* POST /tips: */ { .url_prefix = "/tips", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_private_post_tips }, /* GET /tips: */ { .url_prefix = "/tips", .method = MHD_HTTP_METHOD_GET, .handler = &TMH_private_get_tips }, /* 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 }, /* 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 }, /* 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 }, /* 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 }, /* 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 }, /* 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 }, { NULL } }; static struct TMH_RequestHandler h404 = { .mime_type = "application/json", .data = "{\"code\":10}", .data_size = strlen ("{\"code\":10}"), .handler = &TMH_MHD_handler_static_response, .response_code = MHD_HTTP_NOT_FOUND }; struct TMH_HandlerContext *hc = *con_cls; struct TMH_RequestHandler *handlers; bool use_private = 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; 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 */ { const char *private_prefix = "/private/"; if (0 == strncmp (url, private_prefix, strlen (private_prefix))) { use_private = true; url += strlen (private_prefix) - 1; } } /* 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 == slash) url = ""; else url = slash; } else { /* use 'default' */ hc->instance = TMH_lookup_instance (NULL); } if (NULL != hc->instance) hc->instance->rc++; } { const char *private_prefix = "/private/"; if (0 == strncmp (url, private_prefix, strlen (private_prefix))) { handlers = private_handlers; url += strlen (private_prefix) - 1; } else { handlers = (use_private) ? private_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 missmatch */ 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 missmatch */ } else { 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); } } /* 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_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_INSTANCE_UNKNOWN, "merchant instance unknown"); hc->has_body = ( (0 == strcasecmp (method, MHD_HTTP_METHOD_POST)) || (0 == strcasecmp (method, MHD_HTTP_METHOD_PATCH)) ); if (hc->has_body) { /* FIXME: Maybe check for maximum upload size here and refuse if it is too big? (Note: maximum upload size may need to vary based on the handler.) */ 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, 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->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:s}", "payto_uri", acc->payto_uri, "salt", GNUNET_JSON_from_data_auto (&acc->salt)); wm->wire_method = TALER_payto_get_method (acc->payto_uri); wm->active = acc->active; GNUNET_CONTAINER_DLL_insert (mi->wm_head, mi->wm_tail, wm); } GNUNET_assert (GNUNET_OK == TMH_add_instance (mi)); } /** * Main function that will be run by the scheduler. * * @param cls closure * @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; (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 (merchant_connection_close) go |= TALER_MHD_GO_FORCE_CONNECTION_CLOSE; TALER_MHD_setup (go); result = GNUNET_SYSERR; GNUNET_SCHEDULER_add_shutdown (&do_shutdown, NULL); 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; if (GNUNET_SYSERR == TMH_EXCHANGES_init (config)) { GNUNET_SCHEDULER_shutdown (); return; } if (GNUNET_SYSERR == TMH_AUDITORS_init (config)) { 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; } /* load instances */ { enum GNUNET_DB_QueryStatus qs; qs = TMH_db->lookup_instances (TMH_db->cls, true, &add_instance_cb, NULL); if (0 > qs) { GNUNET_break (0); GNUNET_SCHEDULER_shutdown (); return; } } fh = TALER_MHD_bind (cfg, "merchant", &port); if ( (0 == port) && (-1 == fh) ) { GNUNET_SCHEDULER_shutdown (); return; } resume_timeout_heap = GNUNET_CONTAINER_heap_create (GNUNET_CONTAINER_HEAP_ORDER_MIN); payment_trigger_map = GNUNET_CONTAINER_multihashmap_create (16, GNUNET_YES); mhd = MHD_start_daemon (MHD_USE_SUSPEND_RESUME | MHD_USE_DUAL_STACK, 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, exiting.\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) { 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_END }; if (GNUNET_OK != GNUNET_PROGRAM_run (argc, argv, "taler-merchant-httpd", "Taler merchant's HTTP backend interface", options, &run, NULL)) return 3; return (GNUNET_OK == result) ? 0 : 1; }