/* This file is part of TALER Copyright (C) 2014-2020 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with TALER; see the file COPYING. If not, see */ /** * @file taler-exchange-httpd.c * @brief Serve the HTTP interface of the exchange * @author Florian Dold * @author Benedikt Mueller * @author Christian Grothoff */ #include "platform.h" #include #include #include #include #include #include #include #include "taler_mhd_lib.h" #include "taler-exchange-httpd_auditors.h" #include "taler-exchange-httpd_deposit.h" #include "taler-exchange-httpd_deposits_get.h" #include "taler-exchange-httpd_keys.h" #include "taler-exchange-httpd_link.h" #include "taler-exchange-httpd_loop.h" #include "taler-exchange-httpd_management.h" #include "taler-exchange-httpd_melt.h" #include "taler-exchange-httpd_mhd.h" #include "taler-exchange-httpd_recoup.h" #include "taler-exchange-httpd_refreshes_reveal.h" #include "taler-exchange-httpd_refund.h" #include "taler-exchange-httpd_reserves_get.h" #include "taler-exchange-httpd_terms.h" #include "taler-exchange-httpd_transfers_get.h" #include "taler-exchange-httpd_wire.h" #include "taler-exchange-httpd_withdraw.h" #include "taler_exchangedb_lib.h" #include "taler_exchangedb_plugin.h" #include /** * Backlog for listen operation on unix domain sockets. */ #define UNIX_BACKLOG 500 /** * Type of the closure associated with each HTTP request to the exchange. */ struct ExchangeHttpRequestClosure { /** * Async Scope ID associated with this request. */ struct GNUNET_AsyncScopeId async_scope_id; /** * Opaque parsing context. */ void *opaque_post_parsing_context; /** * Cached request handler for this request (once we have found one). */ struct TEH_RequestHandler *rh; }; /** * Base directory of the exchange (global) */ char *TEH_exchange_directory; /** * Directory where revocations are stored (global) */ char *TEH_revocation_directory; /** * Are clients allowed to request /keys for times other than the * current time? Allowing this could be abused in a DoS-attack * as building new /keys responses is expensive. Should only be * enabled for testcases, development and test systems. */ int TEH_allow_keys_timetravel; /** * The exchange's configuration (global) */ struct GNUNET_CONFIGURATION_Handle *TEH_cfg; /** * How long is caching /keys allowed at most? (global) */ struct GNUNET_TIME_Relative TEH_max_keys_caching; /** * How long is the delay before we close reserves? */ struct GNUNET_TIME_Relative TEH_reserve_closing_delay; /** * Master public key (according to the * configuration in the exchange directory). (global) */ struct TALER_MasterPublicKeyP TEH_master_public_key; /** * Our DB plugin. (global) */ struct TALER_EXCHANGEDB_Plugin *TEH_plugin; /** * Our currency. */ char *TEH_currency; /** * Default timeout in seconds for HTTP requests. */ static unsigned int connection_timeout = 30; /** * How many threads to use. * The default value (0) sets the actual number of threads * based on the number of available cores. */ static unsigned int num_threads = 0; /** * The HTTP Daemon. */ static struct MHD_Daemon *mhd; /** * Port to run the daemon on. */ static uint16_t serve_port; /** * Path for the unix domain-socket * to run the daemon on. */ static char *serve_unixpath; /** * File mode for unix-domain socket. */ static mode_t unixpath_mode; /** * Counter for the number of requests this HTTP has processed so far. */ static unsigned long long req_count; /** * Limit for the number of requests this HTTP may process before restarting. * (This was added as one way of dealing with unavoidable memory fragmentation * happening slowly over time.) */ static unsigned long long req_max; /** * Signature of functions that handle operations on coins. * * @param connection the MHD connection to handle * @param coin_pub the public key of the coin * @param root uploaded JSON data * @return MHD result code */ typedef MHD_RESULT (*CoinOpHandler)(struct MHD_Connection *connection, const struct TALER_CoinSpendPublicKeyP *coin_pub, const json_t *root); /** * Generate a 404 "not found" reply on @a connection with * the hint @a details. * * @param connection where to send the reply on * @param details details for the error message, can be NULL */ static MHD_RESULT r404 (struct MHD_Connection *connection, const char *details) { return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_EXCHANGE_GENERIC_OPERATION_UNKNOWN, details); } /** * Handle a "/coins/$COIN_PUB/$OP" POST request. Parses the "coin_pub" * EdDSA key of the coin and demultiplexes based on $OP. * * @param rh context of the handler * @param connection the MHD connection to handle * @param root uploaded JSON data * @param args array of additional options (first must be the * reserve public key, the second one should be "withdraw") * @return MHD result code */ static MHD_RESULT handle_post_coins (const struct TEH_RequestHandler *rh, struct MHD_Connection *connection, const json_t *root, const char *const args[2]) { struct TALER_CoinSpendPublicKeyP coin_pub; static const struct { /** * Name of the operation (args[1]) */ const char *op; /** * Function to call to perform the operation. */ CoinOpHandler handler; } h[] = { { .op = "deposit", .handler = &TEH_handler_deposit }, { .op = "melt", .handler = &TEH_handler_melt }, { .op = "recoup", .handler = &TEH_handler_recoup }, { .op = "refund", .handler = &TEH_handler_refund }, { .op = NULL, .handler = NULL }, }; (void) rh; if (GNUNET_OK != GNUNET_STRINGS_string_to_data (args[0], strlen (args[0]), &coin_pub, sizeof (coin_pub))) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_EXCHANGE_GENERIC_COINS_INVALID_COIN_PUB, args[0]); } for (unsigned int i = 0; NULL != h[i].op; i++) if (0 == strcmp (h[i].op, args[1])) return h[i].handler (connection, &coin_pub, root); return r404 (connection, args[1]); } /** * 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 ExchangeHttpRequestClosure *ecls = *con_cls; (void) cls; (void) connection; (void) toe; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Request completed\n"); if (NULL == ecls) return; TALER_MHD_parse_post_cleanup_callback (ecls->opaque_post_parsing_context); GNUNET_free (ecls); *con_cls = NULL; /* Sanity-check that we didn't leave any transactions hanging */ /* NOTE: In high-performance production, we could consider removing this as it should not be needed and might be costly (to be benchmarked). */ TEH_plugin->preflight (TEH_plugin->cls, TEH_plugin->get_session (TEH_plugin->cls)); } /** * We found @a rh responsible for handling a request. Parse the * @a upload_data (if applicable) and the @a url and call the * handler. * * @param rh request handler to call * @param connection connection being handled * @param url rest of the URL to parse * @param inner_cls closure for the handler, if needed * @param upload_data upload data to parse (if available) * @param[in,out] upload_data_size number of bytes in @a upload_data * @return MHD result code */ static MHD_RESULT proceed_with_handler (const struct TEH_RequestHandler *rh, struct MHD_Connection *connection, const char *url, void **inner_cls, const char *upload_data, size_t *upload_data_size) { const char *args[rh->nargs + 1]; size_t ulen = strlen (url) + 1; json_t *root = NULL; MHD_RESULT ret; /* We do check for "ulen" here, because we'll later stack-allocate a buffer of that size and don't want to enable malicious clients to cause us huge stack allocations. */ if (ulen > 512) { /* 512 is simply "big enough", as it is bigger than "6 * 54", which is the longest URL format we ever get (for /deposits/). The value should be adjusted if we ever define protocol endpoints with plausibly longer inputs. */ GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_URI_TOO_LONG, TALER_EC_GENERIC_URI_TOO_LONG, url); } /* All POST endpoints come with a body in JSON format. So we parse the JSON here. */ if (0 == strcasecmp (rh->method, MHD_HTTP_METHOD_POST)) { enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_post_json (connection, inner_cls, upload_data, upload_data_size, &root); if (GNUNET_SYSERR == res) { GNUNET_assert (NULL == root); return MHD_NO; /* bad upload, could not even generate error */ } if ( (GNUNET_NO == res) || (NULL == root) ) { GNUNET_assert (NULL == root); return MHD_YES; /* so far incomplete upload or parser error */ } } { char d[ulen]; /* Parse command-line arguments, if applicable */ args[0] = NULL; if (rh->nargs > 0) { unsigned int i; const char *fin; char *sp; /* make a copy of 'url' because 'strtok_r()' will modify */ memcpy (d, url, ulen); i = 0; args[i++] = strtok_r (d, "/", &sp); while ( (NULL != args[i - 1]) && (i < rh->nargs) ) args[i++] = strtok_r (NULL, "/", &sp); /* make sure above loop ran nicely until completion, and also that there is no excess data in 'd' afterwards */ if ( (! rh->nargs_is_upper_bound) && ( (i != rh->nargs) || (NULL == args[i - 1]) || (NULL != (fin = strtok_r (NULL, "/", &sp))) ) ) { char emsg[128 + 512]; GNUNET_snprintf (emsg, sizeof (emsg), "Got %u/%u segments for %s request ('%s')", (NULL == args[i - 1]) ? i - 1 : i + ((NULL != fin) ? 1 : 0), rh->nargs, rh->url, url); GNUNET_break_op (0); json_decref (root); return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_EXCHANGE_GENERIC_WRONG_NUMBER_OF_SEGMENTS, emsg); } /* just to be safe(r), we always terminate the array with a NULL (even if handlers requested precise number of arguments) */ args[i] = NULL; } /* Above logic ensures that 'root' is exactly non-NULL for POST operations, so we test for 'root' to decide which handler to invoke. */ if (NULL != root) ret = rh->handler.post (rh, connection, root, args); else /* We also only have "POST" or "GET" in the API for at this point (OPTIONS/HEAD are taken care of earlier) */ ret = rh->handler.get (rh, connection, args); } json_decref (root); return ret; } /** * Handle a "/seed" request. * * @param rh context of the handler * @param connection the MHD connection to handle * @param args array of additional options (must be empty for this function) * @return MHD result code */ static MHD_RESULT handler_seed (const struct TEH_RequestHandler *rh, struct MHD_Connection *connection, const char *const args[]) { #define SEED_SIZE 32 char *body; MHD_RESULT ret; struct MHD_Response *resp; (void) rh; body = malloc (SEED_SIZE); /* must use malloc(), because MHD will use free() */ if (NULL == body) return MHD_NO; GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, body, SEED_SIZE); resp = MHD_create_response_from_buffer (SEED_SIZE, body, MHD_RESPMEM_MUST_FREE); TALER_MHD_add_global_headers (resp); ret = MHD_queue_response (connection, MHD_HTTP_OK, resp); GNUNET_break (MHD_YES == ret); MHD_destroy_response (resp); return ret; #undef SEED_SIZE } /** * Handle POST "/management/..." requests. * * @param rh context of the handler * @param connection the MHD connection to handle * @param root uploaded JSON data * @param args array of additional options * @return MHD result code */ static MHD_RESULT handle_post_management (const struct TEH_RequestHandler *rh, struct MHD_Connection *connection, const json_t *root, const char *const args[]) { if (NULL == args[0]) { GNUNET_break_op (0); return r404 (connection, "/management"); } if (0 == strcmp (args[0], "auditors")) { struct TALER_AuditorPublicKeyP auditor_pub; if (NULL == args[1]) return TEH_handler_management_auditors (connection, root); if ( (NULL == args[1]) || (NULL == args[2]) || (0 != strcmp (args[2], "disable")) || (NULL != args[3]) ) return r404 (connection, "/management/auditors/$AUDITOR_PUB/disable"); if (GNUNET_OK != GNUNET_STRINGS_string_to_data (args[1], strlen (args[1]), &auditor_pub, sizeof (auditor_pub))) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, args[1]); } return TEH_handler_management_auditors_AP_disable (connection, &auditor_pub, root); } if (0 == strcmp (args[0], "denominations")) { struct GNUNET_HashCode h_denom_pub; if ( (NULL == args[0]) || (NULL == args[1]) || (NULL == args[2]) || (0 != strcmp (args[2], "revoke")) || (NULL != args[3]) ) return r404 (connection, "/management/denominations/$HDP/revoke"); if (GNUNET_OK != GNUNET_STRINGS_string_to_data (args[1], strlen (args[1]), &h_denom_pub, sizeof (h_denom_pub))) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, args[1]); } return TEH_handler_management_denominations_HDP_revoke (connection, &h_denom_pub, root); } if (0 == strcmp (args[0], "signkeys")) { struct TALER_ExchangePublicKeyP exchange_pub; if ( (NULL == args[0]) || (NULL == args[1]) || (NULL == args[2]) || (0 != strcmp (args[2], "revoke")) || (NULL != args[3]) ) return r404 (connection, "/management/signkeys/$HDP/revoke"); if (GNUNET_OK != GNUNET_STRINGS_string_to_data (args[1], strlen (args[1]), &exchange_pub, sizeof (exchange_pub))) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, args[1]); } return TEH_handler_management_signkeys_EP_revoke (connection, &exchange_pub, root); } if (0 == strcmp (args[0], "keys")) { if (NULL != args[1]) { GNUNET_break_op (0); return r404 (connection, "/management/keys/*"); } return TEH_handler_management_post_keys (connection, root); } if (0 == strcmp (args[0], "wire")) { if (NULL == args[1]) return TEH_handler_management_denominations_wire (connection, root); if ( (0 != strcmp (args[1], "disable")) || (NULL != args[2]) ) { GNUNET_break_op (0); return r404 (connection, "/management/wire/disable"); } return TEH_handler_management_denominations_wire_disable (connection, root); } if (0 == strcmp (args[0], "wire-fee")) { if (NULL != args[1]) { GNUNET_break_op (0); return r404 (connection, "/management/wire-fee/*"); } return TEH_handler_management_post_wire_fees (connection, root); } GNUNET_break_op (0); return r404 (connection, "/management/*"); } /** * Handle a get "/management" request. * * @param rh context of the handler * @param connection the MHD connection to handle * @param args array of additional options (must be empty for this function) * @return MHD result code */ static MHD_RESULT handle_get_management (const struct TEH_RequestHandler *rh, struct MHD_Connection *connection, const char *const args[1]) { if ( (NULL == args[0]) || (0 != strcmp (args[0], "keys")) || (NULL != args[1]) ) { GNUNET_break_op (0); return r404 (connection, "/management/*"); } return TEH_keys_management_get_handler (rh, connection); } /** * Handle POST "/auditors/..." requests. * * @param rh context of the handler * @param connection the MHD connection to handle * @param root uploaded JSON data * @param args array of additional options * @return MHD result code */ static MHD_RESULT handle_post_auditors (const struct TEH_RequestHandler *rh, struct MHD_Connection *connection, const json_t *root, const char *const args[]) { struct TALER_AuditorPublicKeyP auditor_pub; struct GNUNET_HashCode h_denom_pub; if ( (NULL == args[0]) || (NULL == args[1]) || (NULL != args[2]) ) { GNUNET_break_op (0); return r404 (connection, "/auditors/$AUDITOR_PUB/$H_DENOM_PUB"); } if (GNUNET_OK != GNUNET_STRINGS_string_to_data (args[0], strlen (args[0]), &auditor_pub, sizeof (auditor_pub))) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, args[0]); } if (GNUNET_OK != GNUNET_STRINGS_string_to_data (args[1], strlen (args[1]), &h_denom_pub, sizeof (h_denom_pub))) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, args[1]); } return TEH_handler_auditors (connection, &auditor_pub, &h_denom_pub, root); } /** * Handle incoming HTTP request. * * @param cls closure for MHD daemon (unused) * @param connection the connection * @param url the requested url * @param method the method (POST, GET, ...) * @param version HTTP version (ignored) * @param upload_data request data * @param upload_data_size size of @a upload_data in bytes * @param con_cls closure for request (a `struct Buffer *`) * @return MHD result code */ static MHD_RESULT handle_mhd_request (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 TEH_RequestHandler handlers[] = { /* /robots.txt: disallow everything */ { .url = "robots.txt", .method = MHD_HTTP_METHOD_GET, .handler.get = &TEH_handler_static_response, .mime_type = "text/plain", .data = "User-agent: *\nDisallow: /\n", .response_code = MHD_HTTP_OK }, /* Landing page, tell humans to go away. */ { .url = "", .method = MHD_HTTP_METHOD_GET, .handler.get = TEH_handler_static_response, .mime_type = "text/plain", .data = "Hello, I'm the Taler exchange. This HTTP server is not for humans.\n", .response_code = MHD_HTTP_OK }, /* AGPL licensing page, redirect to source. As per the AGPL-license, every deployment is required to offer the user a download of the source of the actual deployment. We make this easy by including a redirect to the source here. */ { .url = "agpl", .method = MHD_HTTP_METHOD_GET, .handler.get = &TEH_handler_agpl_redirect }, { .url = "seed", .method = MHD_HTTP_METHOD_GET, .handler.get = &handler_seed }, /* Terms of service */ { .url = "terms", .method = MHD_HTTP_METHOD_GET, .handler.get = &TEH_handler_terms }, /* Privacy policy */ { .url = "privacy", .method = MHD_HTTP_METHOD_GET, .handler.get = &TEH_handler_privacy }, /* Return key material and fundamental properties for this exchange */ { .url = "keys", .method = MHD_HTTP_METHOD_GET, .handler.get = &TEH_keys_get_handler, }, /* Requests for wiring information */ { .url = "wire", .method = MHD_HTTP_METHOD_GET, .handler.get = &TEH_handler_wire }, /* Withdrawing coins / interaction with reserves */ { .url = "reserves", .method = MHD_HTTP_METHOD_GET, .handler.get = &TEH_handler_reserves_get, .nargs = 1 }, { .url = "reserves", .method = MHD_HTTP_METHOD_POST, .handler.post = &TEH_handler_withdraw, .nargs = 2 }, /* coins */ { .url = "coins", .method = MHD_HTTP_METHOD_POST, .handler.post = &handle_post_coins, .nargs = 2 }, { .url = "coins", .method = MHD_HTTP_METHOD_GET, .handler.get = TEH_handler_link, .nargs = 2, }, /* refreshes/$RCH/reveal */ { .url = "refreshes", .method = MHD_HTTP_METHOD_POST, .handler.post = &TEH_handler_reveal, .nargs = 2 }, /* tracking transfers */ { .url = "transfers", .method = MHD_HTTP_METHOD_GET, .handler.get = &TEH_handler_transfers_get, .nargs = 1 }, /* tracking deposits */ { .url = "deposits", .method = MHD_HTTP_METHOD_GET, .handler.get = &TEH_handler_deposits_get, .nargs = 4 }, /* POST management endpoints */ { .url = "management", .method = MHD_HTTP_METHOD_POST, .handler.post = &handle_post_management, .nargs = 4, .nargs_is_upper_bound = true }, /* GET management endpoints (we only really have "/management/keys") */ { .url = "management", .method = MHD_HTTP_METHOD_GET, .handler.get = &handle_get_management, .nargs = 1 }, /* auditor endpoints */ { .url = "auditors", .method = MHD_HTTP_METHOD_POST, .handler.post = &handle_post_auditors, .nargs = 4, .nargs_is_upper_bound = true }, /* mark end of list */ { .url = NULL } }; struct ExchangeHttpRequestClosure *ecls = *con_cls; void **inner_cls; struct GNUNET_AsyncScopeSave old_scope; const char *correlation_id = NULL; (void) cls; (void) version; if (NULL == ecls) { unsigned long long cnt; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Handling new request\n"); /* Atomic operation, no need for a lock ;-) */ cnt = __sync_add_and_fetch (&req_count, 1LLU); if (req_max == cnt) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Restarting exchange service after %llu requests\n", cnt); (void) kill (getpid (), SIGHUP); } /* We're in a new async scope! */ ecls = *con_cls = GNUNET_new (struct ExchangeHttpRequestClosure); GNUNET_async_scope_fresh (&ecls->async_scope_id); /* We only read the correlation ID on the first callback for every client */ 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; } } inner_cls = &ecls->opaque_post_parsing_context; GNUNET_async_scope_enter (&ecls->async_scope_id, &old_scope); if (NULL != correlation_id) GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Handling request (%s) for 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); /* on repeated requests, check our cache first */ if (NULL != ecls->rh) { MHD_RESULT ret; const char *start; if ('\0' == url[0]) /* strange, should start with '/', treat as just "/" */ url = "/"; start = strchr (url + 1, '/'); if (NULL == start) start = ""; ret = proceed_with_handler (ecls->rh, connection, start, inner_cls, upload_data, upload_data_size); GNUNET_async_scope_restore (&old_scope); return ret; } if ( (0 == strcasecmp (method, MHD_HTTP_METHOD_OPTIONS)) && (0 == strcmp ("*", url)) ) return TALER_MHD_reply_cors_preflight (connection); if (0 == strcasecmp (method, MHD_HTTP_METHOD_HEAD)) method = MHD_HTTP_METHOD_GET; /* treat HEAD as GET here, MHD will do the rest */ /* parse first part of URL */ { bool found = false; size_t tok_size; const char *tok; const char *rest; if ('\0' == url[0]) /* strange, should start with '/', treat as just "/" */ url = "/"; tok = url + 1; rest = strchr (tok, '/'); if (NULL == rest) { tok_size = strlen (tok); } else { tok_size = rest - tok; rest++; /* skip over '/' */ } for (unsigned int i = 0; NULL != handlers[i].url; i++) { struct TEH_RequestHandler *rh = &handlers[i]; if ( (0 != strncmp (tok, rh->url, tok_size)) || (tok_size != strlen (rh->url) ) ) continue; found = true; /* The URL is a match! What we now do depends on the method. */ if (0 == strcasecmp (method, MHD_HTTP_METHOD_OPTIONS)) { GNUNET_async_scope_restore (&old_scope); return TALER_MHD_reply_cors_preflight (connection); } GNUNET_assert (NULL != rh->method); if (0 == strcasecmp (method, rh->method)) { MHD_RESULT ret; /* cache to avoid the loop next time */ ecls->rh = rh; /* run handler */ ret = proceed_with_handler (rh, connection, url + tok_size + 1, inner_cls, upload_data, upload_data_size); GNUNET_async_scope_restore (&old_scope); return ret; } } if (found) { /* we found a matching address, but the method is wrong */ struct MHD_Response *reply; MHD_RESULT ret; char *allowed = NULL; GNUNET_break_op (0); for (unsigned int i = 0; NULL != handlers[i].url; i++) { struct TEH_RequestHandler *rh = &handlers[i]; if ( (0 != strncmp (tok, rh->url, tok_size)) || (tok_size != strlen (rh->url) ) ) continue; if (NULL == allowed) { allowed = GNUNET_strdup (rh->method); } else { char *tmp; GNUNET_asprintf (&tmp, "%s, %s", allowed, rh->method); GNUNET_free (allowed); allowed = tmp; } if (0 == strcasecmp (rh->method, MHD_HTTP_METHOD_GET)) { char *tmp; GNUNET_asprintf (&tmp, "%s, %s", allowed, MHD_HTTP_METHOD_HEAD); GNUNET_free (allowed); allowed = tmp; } } reply = TALER_MHD_make_error (TALER_EC_GENERIC_METHOD_INVALID, method); GNUNET_break (MHD_YES == MHD_add_response_header (reply, MHD_HTTP_HEADER_ALLOW, allowed)); GNUNET_free (allowed); ret = MHD_queue_response (connection, MHD_HTTP_METHOD_NOT_ALLOWED, reply); MHD_destroy_response (reply); return ret; } } /* No handler matches, generate not found */ { MHD_RESULT ret; ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_GENERIC_ENDPOINT_UNKNOWN, url); GNUNET_async_scope_restore (&old_scope); return ret; } } /** * Load configuration parameters for the exchange * server into the corresponding global variables. * * @return #GNUNET_OK on success */ static enum GNUNET_GenericReturnValue exchange_serve_process_config (void) { if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_number (TEH_cfg, "exchange", "MAX_REQUESTS", &req_max)) { req_max = ULLONG_MAX; } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_time (TEH_cfg, "exchangedb", "IDLE_RESERVE_EXPIRATION_TIME", &TEH_reserve_closing_delay)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "exchangedb", "IDLE_RESERVE_EXPIRATION_TIME"); /* use default */ TEH_reserve_closing_delay = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_WEEKS, 4); } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_time (TEH_cfg, "exchange", "MAX_KEYS_CACHING", &TEH_max_keys_caching)) { GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, "exchange", "MAX_KEYS_CACHING", "valid relative time expected"); return GNUNET_SYSERR; } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_filename (TEH_cfg, "exchange", "KEYDIR", &TEH_exchange_directory)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "exchange", "KEYDIR"); return GNUNET_SYSERR; } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_filename (TEH_cfg, "exchange", "REVOCATION_DIR", &TEH_revocation_directory)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "exchange", "REVOCATION_DIR"); return GNUNET_SYSERR; } if (GNUNET_OK != TALER_config_get_currency (TEH_cfg, &TEH_currency)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "taler", "CURRENCY"); return GNUNET_SYSERR; } { char *master_public_key_str; if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (TEH_cfg, "exchange", "MASTER_PUBLIC_KEY", &master_public_key_str)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "exchange", "master_public_key"); return GNUNET_SYSERR; } if (GNUNET_OK != GNUNET_CRYPTO_eddsa_public_key_from_string (master_public_key_str, strlen ( master_public_key_str), &TEH_master_public_key. eddsa_pub)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Invalid master public key given in exchange configuration."); GNUNET_free (master_public_key_str); return GNUNET_SYSERR; } GNUNET_free (master_public_key_str); } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Launching exchange with public key `%s'...\n", GNUNET_p2s (&TEH_master_public_key.eddsa_pub)); if (NULL == (TEH_plugin = TALER_EXCHANGEDB_plugin_load (TEH_cfg))) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to initialize DB subsystem\n"); return GNUNET_SYSERR; } if (GNUNET_OK != TALER_MHD_parse_config (TEH_cfg, "exchange", &serve_port, &serve_unixpath, &unixpath_mode)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to setup HTTPd subsystem\n"); return GNUNET_SYSERR; } return GNUNET_OK; } /** * Called when the main thread exits, writes out performance * stats if requested. */ static void write_stats (void) { struct GNUNET_DISK_FileHandle *fh; pid_t pid = getpid (); char *benchmark_dir; char *s; struct rusage usage; benchmark_dir = getenv ("GNUNET_BENCHMARK_DIR"); if (NULL == benchmark_dir) return; GNUNET_asprintf (&s, "%s/taler-exchange-%llu.txt", benchmark_dir, (unsigned long long) pid); fh = GNUNET_DISK_file_open (s, (GNUNET_DISK_OPEN_WRITE | GNUNET_DISK_OPEN_TRUNCATE | GNUNET_DISK_OPEN_CREATE), (GNUNET_DISK_PERM_USER_READ | GNUNET_DISK_PERM_USER_WRITE)); GNUNET_free (s); if (NULL == fh) return; /* permission denied? */ /* Collect stats, summed up for all threads */ GNUNET_assert (0 == getrusage (RUSAGE_SELF, &usage)); GNUNET_asprintf (&s, "time_exchange sys %llu user %llu\n", (unsigned long long) (usage.ru_stime.tv_sec * 1000 * 1000 + usage.ru_stime.tv_usec), (unsigned long long) (usage.ru_utime.tv_sec * 1000 * 1000 + usage.ru_utime.tv_usec)); GNUNET_assert (GNUNET_SYSERR != GNUNET_DISK_file_write_blocking (fh, s, strlen (s))); GNUNET_free (s); GNUNET_assert (GNUNET_OK == GNUNET_DISK_file_close (fh)); } /* Developer logic for supporting the `-f' option. */ #if HAVE_DEVELOPER /** * Option `-f' (specifies an input file to give to the HTTP server). */ static char *input_filename; /** * We finished handling the request and should now terminate. */ static int do_terminate; /** * Run 'nc' or 'ncat' as a fake HTTP client using #input_filename * as the input for the request. If launching the client worked, * run the #TEH_KS_loop() event loop as usual. * * @return child pid */ static pid_t run_fake_client (void) { pid_t cld; char ports[6]; int fd; if (0 == strcmp (input_filename, "-")) fd = STDIN_FILENO; else fd = open (input_filename, O_RDONLY); if (-1 == fd) { fprintf (stderr, "Failed to open `%s': %s\n", input_filename, strerror (errno)); return -1; } /* Fake HTTP client request with #input_filename as input. We do this using the nc tool. */ GNUNET_snprintf (ports, sizeof (ports), "%u", serve_port); if (0 == (cld = fork ())) { GNUNET_break (0 == close (0)); GNUNET_break (0 == dup2 (fd, 0)); GNUNET_break (0 == close (fd)); if ( (0 != execlp ("nc", "nc", "localhost", ports, "-w", "30", NULL)) && (0 != execlp ("ncat", "ncat", "localhost", ports, "-i", "30", NULL)) ) { fprintf (stderr, "Failed to run both `nc' and `ncat': %s\n", strerror (errno)); } _exit (1); } /* parent process */ if (0 != strcmp (input_filename, "-")) GNUNET_break (0 == close (fd)); return cld; } /** * Signature of the callback used by MHD to notify the application * about completed connections. If we are running in test-mode with * an #input_filename, this function is used to terminate the HTTPD * after the first request has been processed. * * @param cls client-defined closure, NULL * @param connection connection handle (ignored) * @param socket_context socket-specific pointer (ignored) * @param toe reason for connection notification */ static void connection_done (void *cls, struct MHD_Connection *connection, void **socket_context, enum MHD_ConnectionNotificationCode toe) { (void) cls; (void) connection; (void) socket_context; /* We only act if the connection is closed. */ if (MHD_CONNECTION_NOTIFY_CLOSED != toe) return; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Connection done!\n"); do_terminate = GNUNET_YES; } /** * Run the exchange to serve a single request only, without threads. * * @return #GNUNET_OK on success */ static int run_single_request (void) { pid_t cld; int status; /* run only the testfile input, then terminate */ mhd = MHD_start_daemon (MHD_USE_PIPE_FOR_SHUTDOWN | MHD_USE_DEBUG | MHD_USE_DUAL_STACK | MHD_USE_TCP_FASTOPEN, 0, /* pick free port */ NULL, NULL, &handle_mhd_request, NULL, MHD_OPTION_LISTEN_BACKLOG_SIZE, (unsigned int) 10, MHD_OPTION_EXTERNAL_LOGGER, &TALER_MHD_handle_logs, NULL, MHD_OPTION_NOTIFY_COMPLETED, &handle_mhd_completion_callback, NULL, MHD_OPTION_CONNECTION_TIMEOUT, connection_timeout, MHD_OPTION_NOTIFY_CONNECTION, &connection_done, NULL, MHD_OPTION_END); if (NULL == mhd) { fprintf (stderr, "Failed to start HTTP server.\n"); return GNUNET_SYSERR; } serve_port = MHD_get_daemon_info (mhd, MHD_DAEMON_INFO_BIND_PORT)->port; cld = run_fake_client (); if (-1 == cld) return GNUNET_SYSERR; /* run the event loop until #connection_done() was called */ while (GNUNET_NO == do_terminate) { fd_set rs; fd_set ws; fd_set es; struct timeval tv; MHD_UNSIGNED_LONG_LONG timeout; int maxsock = -1; int have_tv; FD_ZERO (&rs); FD_ZERO (&ws); FD_ZERO (&es); if (MHD_YES != MHD_get_fdset (mhd, &rs, &ws, &es, &maxsock)) { GNUNET_break (0); return GNUNET_SYSERR; } have_tv = MHD_get_timeout (mhd, &timeout); tv.tv_sec = timeout / 1000; tv.tv_usec = 1000 * (timeout % 1000); if (-1 == select (maxsock + 1, &rs, &ws, &es, have_tv ? &tv : NULL)) { GNUNET_break (0); return GNUNET_SYSERR; } MHD_run (mhd); } TEH_resume_keys_requests (true); MHD_stop_daemon (mhd); mhd = NULL; if (cld != waitpid (cld, &status, 0)) fprintf (stderr, "Waiting for `nc' child failed: %s\n", strerror (errno)); return GNUNET_OK; } /* end of HAVE_DEVELOPER */ #endif /** * Run the ordinary multi-threaded main loop and the logic to * wait for CTRL-C. * * @param fh listen socket * @param argv command line arguments * @return #GNUNET_OK on success */ static int run_main_loop (int fh, char *const *argv) { int ret; GNUNET_assert (0 < num_threads); mhd = MHD_start_daemon (MHD_USE_SELECT_INTERNALLY | MHD_USE_PIPE_FOR_SHUTDOWN | MHD_USE_DEBUG | MHD_USE_DUAL_STACK | MHD_USE_INTERNAL_POLLING_THREAD | MHD_ALLOW_SUSPEND_RESUME | MHD_USE_TCP_FASTOPEN, (-1 == fh) ? serve_port : 0, NULL, NULL, &handle_mhd_request, NULL, MHD_OPTION_THREAD_POOL_SIZE, (unsigned int) num_threads, MHD_OPTION_LISTEN_BACKLOG_SIZE, (unsigned int) 1024, MHD_OPTION_LISTEN_SOCKET, fh, MHD_OPTION_EXTERNAL_LOGGER, &TALER_MHD_handle_logs, NULL, MHD_OPTION_NOTIFY_COMPLETED, &handle_mhd_completion_callback, NULL, MHD_OPTION_CONNECTION_TIMEOUT, connection_timeout, MHD_OPTION_END); if (NULL == mhd) { fprintf (stderr, "Failed to start HTTP server.\n"); return GNUNET_SYSERR; } atexit (&write_stats); ret = TEH_loop_run (); switch (ret) { case GNUNET_OK: case GNUNET_SYSERR: TEH_resume_keys_requests (true); MHD_stop_daemon (mhd); break; case GNUNET_NO: { MHD_socket sock = MHD_quiesce_daemon (mhd); pid_t chld; int flags; /* Set flags to make 'sock' inherited by child */ flags = fcntl (sock, F_GETFD); GNUNET_assert (-1 != flags); flags &= ~FD_CLOEXEC; GNUNET_assert (-1 != fcntl (sock, F_SETFD, flags)); chld = fork (); if (-1 == chld) { /* fork() failed, continue clean up, unhappily */ GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, "fork"); } if (0 == chld) { char pids[12]; /* exec another taler-exchange-httpd, passing on the listen socket; as in systemd it is expected to be on FD #3 */ if (3 != dup2 (sock, 3)) { GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, "dup2"); _exit (1); } /* Tell the child that it is the desired recipient for FD #3 */ GNUNET_snprintf (pids, sizeof (pids), "%u", getpid ()); setenv ("LISTEN_PID", pids, 1); setenv ("LISTEN_FDS", "1", 1); /* Finally, exec the (presumably) more recent exchange binary */ execvp ("taler-exchange-httpd", argv); GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, "execvp"); _exit (1); } /* we're the original process, handle remaining contextions before exiting; as the listen socket is no longer used, close it here */ GNUNET_break (0 == close (sock)); while (0 != MHD_get_daemon_info (mhd, MHD_DAEMON_INFO_CURRENT_CONNECTIONS)-> num_connections) sleep (1); /* Now we're really done, practice clean shutdown */ TEH_resume_keys_requests (true); MHD_stop_daemon (mhd); } break; default: GNUNET_break (0); TEH_resume_keys_requests (true); MHD_stop_daemon (mhd); break; } return ret; } /** * The main function of the taler-exchange-httpd server ("the exchange"). * * @param argc number of arguments from the command line * @param argv command line arguments * @return 0 ok, 1 on error */ int main (int argc, char *const *argv) { char *cfgfile = NULL; char *loglev = NULL; char *logfile = NULL; int connection_close = GNUNET_NO; const struct GNUNET_GETOPT_CommandLineOption options[] = { GNUNET_GETOPT_option_flag ('a', "allow-timetravel", "allow clients to request /keys for arbitrary timestamps (for testing and development only)", &TEH_allow_keys_timetravel), GNUNET_GETOPT_option_flag ('C', "connection-close", "force HTTP connections to be closed after each request", &connection_close), GNUNET_GETOPT_option_cfgfile (&cfgfile), GNUNET_GETOPT_option_uint ('t', "timeout", "SECONDS", "after how long do connections timeout by default (in seconds)", &connection_timeout), GNUNET_GETOPT_option_timetravel ('T', "timetravel"), GNUNET_GETOPT_option_uint ('n', "num-threads", "NUM_THREADS", "size of the thread pool", &num_threads), #if HAVE_DEVELOPER GNUNET_GETOPT_option_filename ('f', "file-input", "FILENAME", "run in test-mode using FILENAME as the HTTP request to process, use '-' to read from stdin", &input_filename), #endif GNUNET_GETOPT_option_help ( "HTTP server providing a RESTful API to access a Taler exchange"), GNUNET_GETOPT_option_loglevel (&loglev), GNUNET_GETOPT_option_logfile (&logfile), GNUNET_GETOPT_option_version (VERSION "-" VCS_VERSION), GNUNET_GETOPT_OPTION_END }; int ret; const char *listen_pid; const char *listen_fds; int fh = -1; enum TALER_MHD_GlobalOptions go; ret = GNUNET_GETOPT_run ("taler-exchange-httpd", options, argc, argv); if (ret < 0) return 1; if (0 == ret) return 0; if (0 == num_threads) { cpu_set_t mask; GNUNET_assert (0 == sched_getaffinity (0, sizeof (cpu_set_t), &mask)); num_threads = CPU_COUNT (&mask); } go = TALER_MHD_GO_NONE; if (connection_close) go |= TALER_MHD_GO_FORCE_CONNECTION_CLOSE; TALER_MHD_setup (go); GNUNET_assert (GNUNET_OK == GNUNET_log_setup ("taler-exchange-httpd", (NULL == loglev) ? "INFO" : loglev, logfile)); GNUNET_free (loglev); GNUNET_free (logfile); if (NULL == cfgfile) cfgfile = GNUNET_strdup (GNUNET_OS_project_data_get ()->user_config_file); TEH_cfg = GNUNET_CONFIGURATION_create (); if (GNUNET_SYSERR == GNUNET_CONFIGURATION_load (TEH_cfg, cfgfile)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Malformed configuration file `%s', exit ...\n", cfgfile); GNUNET_free (cfgfile); return 1; } GNUNET_free (cfgfile); if (GNUNET_OK != exchange_serve_process_config ()) return 1; TEH_load_terms (TEH_cfg); /* check for systemd-style FD passing */ listen_pid = getenv ("LISTEN_PID"); listen_fds = getenv ("LISTEN_FDS"); if ( (NULL != listen_pid) && (NULL != listen_fds) && (getpid () == strtol (listen_pid, NULL, 10)) && (1 == strtoul (listen_fds, NULL, 10)) ) { int flags; fh = 3; flags = fcntl (fh, F_GETFD); if ( (-1 == flags) && (EBADF == errno) ) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Bad listen socket passed, ignored\n"); fh = -1; } flags |= FD_CLOEXEC; if ( (-1 != fh) && (0 != fcntl (fh, F_SETFD, flags)) ) GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, "fcntl"); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Successfully obtained listen socket from hypervisor\n"); } /* initialize #internal_key_state with an RC of 1 */ if (GNUNET_OK != TEH_WIRE_init ()) return 42; if (GNUNET_OK != TEH_keys_init ()) return 43; ret = TEH_loop_init (); if (GNUNET_OK == ret) { #if HAVE_DEVELOPER if (NULL != input_filename) { ret = run_single_request (); } else #endif { /* consider unix path */ if ( (-1 == fh) && (NULL != serve_unixpath) ) { fh = TALER_MHD_open_unix_path (serve_unixpath, unixpath_mode); if (-1 == fh) return 1; } ret = run_main_loop (fh, argv); } /* release signal handlers */ TEH_loop_done (); } TALER_EXCHANGEDB_plugin_unload (TEH_plugin); TEH_WIRE_done (); return (GNUNET_SYSERR == ret) ? 1 : 0; } /* end of taler-exchange-httpd.c */