From 7e669bcf6b6336ec429da949bcb4aa456971dba2 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Fri, 30 Jul 2021 10:38:27 +0200 Subject: folding history in preparation of GNU Anastasis v0.0.0 release --- src/backend/Makefile.am | 45 + src/backend/anastasis-httpd.c | 943 ++++++++++++++++++ src/backend/anastasis-httpd.h | 225 +++++ src/backend/anastasis-httpd_config.c | 132 +++ src/backend/anastasis-httpd_config.h | 41 + src/backend/anastasis-httpd_mhd.c | 70 ++ src/backend/anastasis-httpd_mhd.h | 61 ++ src/backend/anastasis-httpd_policy.c | 252 +++++ src/backend/anastasis-httpd_policy.h | 66 ++ src/backend/anastasis-httpd_policy_upload.c | 1211 +++++++++++++++++++++++ src/backend/anastasis-httpd_terms.c | 98 ++ src/backend/anastasis-httpd_terms.h | 62 ++ src/backend/anastasis-httpd_truth.c | 1428 +++++++++++++++++++++++++++ src/backend/anastasis-httpd_truth.h | 75 ++ src/backend/anastasis-httpd_truth_upload.c | 855 ++++++++++++++++ src/backend/anastasis.conf | 77 ++ 16 files changed, 5641 insertions(+) create mode 100644 src/backend/Makefile.am create mode 100644 src/backend/anastasis-httpd.c create mode 100644 src/backend/anastasis-httpd.h create mode 100644 src/backend/anastasis-httpd_config.c create mode 100644 src/backend/anastasis-httpd_config.h create mode 100644 src/backend/anastasis-httpd_mhd.c create mode 100644 src/backend/anastasis-httpd_mhd.h create mode 100644 src/backend/anastasis-httpd_policy.c create mode 100644 src/backend/anastasis-httpd_policy.h create mode 100644 src/backend/anastasis-httpd_policy_upload.c create mode 100644 src/backend/anastasis-httpd_terms.c create mode 100644 src/backend/anastasis-httpd_terms.h create mode 100644 src/backend/anastasis-httpd_truth.c create mode 100644 src/backend/anastasis-httpd_truth.h create mode 100644 src/backend/anastasis-httpd_truth_upload.c create mode 100644 src/backend/anastasis.conf (limited to 'src/backend') diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am new file mode 100644 index 0000000..1046810 --- /dev/null +++ b/src/backend/Makefile.am @@ -0,0 +1,45 @@ +# This Makefile.am is in the public domain +AM_CPPFLAGS = -I$(top_srcdir)/src/include + +pkgcfgdir = $(prefix)/share/anastasis/config.d/ + +if USE_COVERAGE + AM_CFLAGS = --coverage -O0 + XLIB = -lgcov +endif + +pkgcfg_DATA = \ + anastasis.conf + +bin_PROGRAMS = \ + anastasis-httpd + +anastasis_httpd_SOURCES = \ + anastasis-httpd.c anastasis-httpd.h \ + anastasis-httpd_mhd.c anastasis-httpd_mhd.h \ + anastasis-httpd_policy.c anastasis-httpd_policy.h \ + anastasis-httpd_policy_upload.c \ + anastasis-httpd_truth.c anastasis-httpd_truth.h \ + anastasis-httpd_terms.c anastasis-httpd_terms.h \ + anastasis-httpd_config.c anastasis-httpd_config.h \ + anastasis-httpd_truth_upload.c + +anastasis_httpd_LDADD = \ + $(top_builddir)/src/util/libanastasisutil.la \ + $(top_builddir)/src/stasis/libanastasisdb.la \ + $(top_builddir)/src/authorization/libanastasisauthorization.la \ + -ljansson \ + -ltalermerchant \ + -ltalermhd \ + -ltalerjson \ + -ltalerutil \ + -lgnunetcurl \ + -lgnunetrest \ + -lgnunetjson \ + -lgnunetutil \ + -lmicrohttpd \ + -luuid \ + $(XLIB) + +EXTRA_DIST = \ + $(pkgcfg_DATA) diff --git a/src/backend/anastasis-httpd.c b/src/backend/anastasis-httpd.c new file mode 100644 index 0000000..56bd7c9 --- /dev/null +++ b/src/backend/anastasis-httpd.c @@ -0,0 +1,943 @@ +/* + This file is part of TALER + (C) 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 backup/anastasis-httpd.c + * @brief HTTP serving layer intended to provide basic backup operations + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis-httpd.h" +#include "anastasis_util_lib.h" +#include "anastasis-httpd_mhd.h" +#include "anastasis_database_lib.h" +#include "anastasis-httpd_policy.h" +#include "anastasis-httpd_truth.h" +#include "anastasis-httpd_terms.h" +#include "anastasis-httpd_config.h" + + +/** + * Backlog for listen operation on unix-domain sockets. + */ +#define UNIX_BACKLOG 500 + +/** + * Upload limit to the service, in megabytes. + */ +unsigned long long int AH_upload_limit_mb; + +/** + * Annual fee for the backup account. + */ +struct TALER_Amount AH_annual_fee; + +/** + * Fee for a truth upload. + */ +struct TALER_Amount AH_truth_upload_fee; + +/** + * Amount of insurance. + */ +struct TALER_Amount AH_insurance; + +/** + * Cost for secure question truth download. + */ +struct TALER_Amount AH_question_cost; + +/** + * Our configuration. + */ +const struct GNUNET_CONFIGURATION_Handle *AH_cfg; + +/** + * Our Taler backend to process payments. + */ +char *AH_backend_url; + +/** + * Taler currency. + */ +char *AH_currency; + +/** + * Our fulfillment URL. + */ +char *AH_fulfillment_url; + +/** + * Our business name. + */ +char *AH_business_name; + +/** + * Our server salt. + */ +struct ANASTASIS_CRYPTO_ProviderSaltP AH_server_salt; + +/** + * Number of policy uploads permitted per annual fee payment. + */ +unsigned long long AH_post_counter = 64LLU; + +/** + * Our context for making HTTP requests. + */ +struct GNUNET_CURL_Context *AH_ctx; + +/** + * Should a "Connection: close" header be added to each HTTP response? + */ +static int AH_connection_close; + +/** + * Task running the HTTP server. + */ +static struct GNUNET_SCHEDULER_Task *mhd_task; + +/** + * Global return code + */ +static int global_result; + +/** + * The MHD Daemon + */ +static struct MHD_Daemon *mhd; + +/** + * Connection handle to the our database + */ +struct ANASTASIS_DatabasePlugin *db; + +/** + * Reschedule context for #SH_ctx. + */ +static struct GNUNET_CURL_RescheduleContext *rc; + +/** + * Set if we should immediately #MHD_run again. + */ +static int triggered; + +/** + * Username and password to use for client authentication + * (optional). + */ +static char *userpass; + +/** + * Type of the client's TLS certificate (optional). + */ +static char *certtype; + +/** + * File with the client's TLS certificate (optional). + */ +static char *certfile; + +/** + * File with the client's TLS private key (optional). + */ +static char *keyfile; + +/** + * This value goes in the Authorization:-header. + */ +static char *apikey; + +/** + * Passphrase to decrypt client's TLS private key file (optional). + */ +static char *keypass; + + +/** + * Function that queries MHD's select sets and + * starts the task waiting for them. + */ +static struct GNUNET_SCHEDULER_Task * +prepare_daemon (void); + + +/** + * Call MHD to process pending requests and then go back + * and schedule the next run. + * + * @param cls the `struct MHD_Daemon` of the HTTP server to run + */ +static void +run_daemon (void *cls) +{ + (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. + * + * @param cls NULL + */ +void +AH_trigger_daemon (void *cls) +{ + (void) cls; + if (NULL != mhd_task) + { + GNUNET_SCHEDULER_cancel (mhd_task); + mhd_task = GNUNET_SCHEDULER_add_now (&run_daemon, + NULL); + } + else + { + triggered = 1; + } +} + + +/** + * Kick GNUnet Curl scheduler to begin curl interactions. + */ +void +AH_trigger_curl (void) +{ + GNUNET_CURL_gnunet_scheduler_reschedule (&rc); +} + + +/** + * 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 AH_RequestHandler handlers[] = { + /* Landing page, tell humans to go away. */ + { "/", MHD_HTTP_METHOD_GET, "text/plain", + "Hello, I'm Anastasis. This HTTP server is not for humans.\n", 0, + &TMH_MHD_handler_static_response, MHD_HTTP_OK }, + { "/agpl", MHD_HTTP_METHOD_GET, "text/plain", + NULL, 0, + &TMH_MHD_handler_agpl_redirect, MHD_HTTP_FOUND }, + { "/terms", MHD_HTTP_METHOD_GET, NULL, + NULL, 0, + &AH_handler_terms, MHD_HTTP_OK }, + { "/privacy", MHD_HTTP_METHOD_GET, NULL, + NULL, 0, + &AH_handler_terms, MHD_HTTP_OK }, + { "/config", MHD_HTTP_METHOD_GET, "text/json", + NULL, 0, + &AH_handler_config, MHD_HTTP_OK }, + {NULL, NULL, NULL, NULL, 0, 0 } + }; + static struct AH_RequestHandler h404 = { + "", NULL, "text/html", + "404: not found", 0, + &TMH_MHD_handler_static_response, MHD_HTTP_NOT_FOUND + }; + static struct AH_RequestHandler h405 = { + "", NULL, "text/html", + "405: method not allowed", 0, + &TMH_MHD_handler_static_response, MHD_HTTP_METHOD_NOT_ALLOWED + }; + struct TM_HandlerContext *hc = *con_cls; + const char *correlation_id = NULL; + bool path_matched; + + if (NULL == hc) + { + struct GNUNET_AsyncScopeId aid; + + GNUNET_async_scope_fresh (&aid); + /* We only read the correlation ID on the first callback for every client */ + correlation_id = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + "Anastasis-Correlation-Id"); + if ((NULL != correlation_id) && + (GNUNET_YES != GNUNET_CURL_is_valid_scope_id (correlation_id))) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid incoming correlation ID\n"); + correlation_id = NULL; + } + hc = GNUNET_new (struct TM_HandlerContext); + *con_cls = hc; + hc->async_scope_id = aid; + } + if (0 == strcasecmp (method, + MHD_HTTP_METHOD_HEAD)) + method = MHD_HTTP_METHOD_GET; /* MHD will throw away the body */ + + GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id); + 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 == strncmp (url, + "/policy/", + strlen ("/policy/"))) + { + const char *account = url + strlen ("/policy/"); + struct ANASTASIS_CRYPTO_AccountPublicKeyP account_pub; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + account, + strlen (account), + &account_pub, + sizeof (struct ANASTASIS_CRYPTO_AccountPublicKeyP))) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "account public key"); + } + if (0 == strcmp (method, + MHD_HTTP_METHOD_GET)) + { + return AH_policy_get (connection, + &account_pub); + } + if (0 == strcmp (method, + MHD_HTTP_METHOD_POST)) + { + return AH_handler_policy_post (connection, + hc, + &account_pub, + upload_data, + upload_data_size); + } + return TMH_MHD_handler_static_response (&h405, + connection); + } + if (0 == strncmp (url, + "/truth/", + strlen ("/truth/"))) + { + struct ANASTASIS_CRYPTO_TruthUUIDP tu; + const char *pub_key_str; + + pub_key_str = &url[strlen ("/truth/")]; + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + pub_key_str, + strlen (pub_key_str), + &tu, + sizeof(tu))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "truth UUID"); + } + if (0 == strcmp (method, + MHD_HTTP_METHOD_GET)) + { + return AH_handler_truth_get (connection, + &tu, + hc); + } + if (0 == strcmp (method, + MHD_HTTP_METHOD_POST)) + { + return AH_handler_truth_post (connection, + hc, + &tu, + upload_data, + upload_data_size); + } + return TMH_MHD_handler_static_response (&h405, + connection); + } + path_matched = false; + for (unsigned int i = 0; NULL != handlers[i].url; i++) + { + struct AH_RequestHandler *rh = &handlers[i]; + + if (0 == strcmp (url, + rh->url)) + { + path_matched = true; + if (0 == strcasecmp (method, + MHD_HTTP_METHOD_OPTIONS)) + { + return TALER_MHD_reply_cors_preflight (connection); + } + if ( (NULL == rh->method) || + (0 == strcasecmp (method, + rh->method)) ) + { + return rh->handler (rh, + connection); + } + } + } + if (path_matched) + return TMH_MHD_handler_static_response (&h405, + connection); + return TMH_MHD_handler_static_response (&h404, + connection); +} + + +/** + * Shutdown task (magically invoked when the application is being + * quit) + * + * @param cls NULL + */ +static void +do_shutdown (void *cls) +{ + (void) cls; + AH_resume_all_bc (); + AH_truth_shutdown (); + AH_truth_upload_shutdown (); + if (NULL != mhd_task) + { + GNUNET_SCHEDULER_cancel (mhd_task); + mhd_task = NULL; + } + if (NULL != AH_ctx) + { + GNUNET_CURL_fini (AH_ctx); + AH_ctx = NULL; + } + if (NULL != rc) + { + GNUNET_CURL_gnunet_rc_destroy (rc); + rc = NULL; + } + if (NULL != mhd) + { + MHD_stop_daemon (mhd); + mhd = NULL; + } + if (NULL != db) + { + ANASTASIS_DB_plugin_unload (db); + db = 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 TM_HandlerContext *hc = *con_cls; + + (void) cls; + (void) connection; + if (NULL == hc) + return; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Finished handling request with status %d\n", + (int) toe); + if (NULL != hc->cc) + hc->cc (hc); + GNUNET_free (hc); + *con_cls = NULL; +} + + +/** + * 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 = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, + timeout); + 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; +} + + +/** + * 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; + uint16_t port; + enum TALER_MHD_GlobalOptions go; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting anastasis-httpd\n"); + go = TALER_MHD_GO_NONE; + if (AH_connection_close) + go |= TALER_MHD_GO_FORCE_CONNECTION_CLOSE; + AH_load_terms (config); + TALER_MHD_setup (go); + AH_cfg = config; + global_result = GNUNET_SYSERR; + GNUNET_SCHEDULER_add_shutdown (&do_shutdown, + NULL); + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_number (config, + "anastasis", + "UPLOAD_LIMIT_MB", + &AH_upload_limit_mb)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "UPLOAD_LIMIT_MB"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_config_get_amount (config, + "anastasis", + "INSURANCE", + &AH_insurance)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "INSURANCE"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_config_get_amount (config, + "authorization-question", + "COST", + &AH_question_cost)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "authorization-question", + "COST"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_config_get_amount (config, + "anastasis", + "ANNUAL_FEE", + &AH_annual_fee)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "ANNUAL_FEE"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_config_get_amount (config, + "anastasis", + "TRUTH_UPLOAD_FEE", + &AH_truth_upload_fee)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "TRUTH_UPLOAD_FEE"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_config_get_currency (config, + &AH_currency)) + { + GNUNET_SCHEDULER_shutdown (); + return; + } + if (0 != strcasecmp (AH_currency, + AH_annual_fee.currency)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "ANNUAL_FEE", + "currency mismatch"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_amount_cmp_currency (&AH_insurance, + &AH_annual_fee)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "INSURANCE", + "currency mismatch"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (config, + "anastasis", + "PAYMENT_BACKEND_URL", + &AH_backend_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "PAYMENT_BACKEND_URL"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if ( (0 != strncasecmp ("https://", + AH_backend_url, + strlen ("https://"))) && + (0 != strncasecmp ("http://", + AH_backend_url, + strlen ("http://"))) ) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "PAYMENT_BACKEND_URL", + "Must be HTTP(S) URL"); + GNUNET_SCHEDULER_shutdown (); + return; + } + + if ( (0 == strcasecmp ("https://", + AH_backend_url)) || + (0 == strcasecmp ("http://", + AH_backend_url)) ) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "PAYMENT_BACKEND_URL", + "Must have domain name"); + GNUNET_SCHEDULER_shutdown (); + return; + } + + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (config, + "anastasis", + "FULFILLMENT_URL", + &AH_fulfillment_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "FULFILLMENT_URL"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_number (config, + "anastasis", + "ANNUAL_POLICY_UPLOAD_LIMIT", + &AH_post_counter)) + { + /* only warn, we will use the default */ + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, + "anastasis", + "ANNUAL_POLICY_UPLOAD_LIMIT"); + } + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (config, + "anastasis", + "BUSINESS_NAME", + &AH_business_name)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "BUSINESS_NAME"); + GNUNET_SCHEDULER_shutdown (); + return; + } + { + char *server_salt; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (config, + "anastasis", + "SERVER_SALT", + &server_salt)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "anastasis", + "SERVER_SALT"); + GNUNET_SCHEDULER_shutdown (); + return; + } + GNUNET_assert (GNUNET_YES == + GNUNET_CRYPTO_kdf (&AH_server_salt, + sizeof (AH_server_salt), + "anastasis-server-salt", + strlen ("anastasis-server-salt"), + server_salt, + strlen (server_salt), + NULL, + 0)); + GNUNET_free (server_salt); + } + + /* setup HTTP client event loop */ + AH_ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, + &rc); + rc = GNUNET_CURL_gnunet_rc_create (AH_ctx); + if (NULL != userpass) + GNUNET_CURL_set_userpass (AH_ctx, + userpass); + if (NULL != keyfile) + GNUNET_CURL_set_tlscert (AH_ctx, + certtype, + certfile, + keyfile, + keypass); + if (NULL != apikey) + { + char *auth_header; + + GNUNET_asprintf (&auth_header, + "%s: %s", + MHD_HTTP_HEADER_AUTHORIZATION, + apikey); + if (GNUNET_OK != + GNUNET_CURL_append_header (AH_ctx, + auth_header)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed so set %s header, trying without\n", + MHD_HTTP_HEADER_AUTHORIZATION); + } + GNUNET_free (auth_header); + } + + if (NULL == + (db = ANASTASIS_DB_plugin_load (config))) + { + GNUNET_SCHEDULER_shutdown (); + return; + } + + fh = TALER_MHD_bind (config, + "anastasis", + &port); + if ( (0 == port) && + (-1 == fh) ) + { + GNUNET_SCHEDULER_shutdown (); + return; + } + 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 (port %u in use?), exiting.\n", + port); + GNUNET_SCHEDULER_shutdown (); + return; + } + global_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, 1 on error + */ +int +main (int argc, + char *const *argv) +{ + enum GNUNET_GenericReturnValue res; + struct GNUNET_GETOPT_CommandLineOption options[] = { + GNUNET_GETOPT_option_string ('A', + "auth", + "USERNAME:PASSWORD", + "use the given USERNAME and PASSWORD for client authentication", + &userpass), + GNUNET_GETOPT_option_flag ('C', + "connection-close", + "force HTTP connections to be closed after each request", + &AH_connection_close), + GNUNET_GETOPT_option_string ('k', + "key", + "KEYFILE", + "file with the private TLS key for TLS client authentication", + &keyfile), + GNUNET_GETOPT_option_string ('p', + "pass", + "KEYFILEPASSPHRASE", + "passphrase needed to decrypt the TLS client private key file", + &keypass), + GNUNET_GETOPT_option_string ('K', + "apikey", + "APIKEY", + "API key to use in the HTTP request to the merchant backend", + &apikey), + GNUNET_GETOPT_option_string ('t', + "type", + "CERTTYPE", + "type of the TLS client certificate, defaults to PEM if not specified", + &certtype), + + GNUNET_GETOPT_OPTION_END + }; + + /* FIRST get the libtalerutil initialization out + of the way. Then throw that one away, and force + the ANASTASIS defaults to be used! */ + (void) TALER_project_data_default (); + GNUNET_OS_init (ANASTASIS_project_data_default ()); + res = GNUNET_PROGRAM_run (argc, argv, + "anastasis-httpd", + "Anastasis HTTP interface", + options, &run, NULL); + if (GNUNET_SYSERR == res) + return 3; + if (GNUNET_NO == res) + return 0; + return (GNUNET_OK == global_result) ? 0 : 1; +} diff --git a/src/backend/anastasis-httpd.h b/src/backend/anastasis-httpd.h new file mode 100644 index 0000000..6fe0023 --- /dev/null +++ b/src/backend/anastasis-httpd.h @@ -0,0 +1,225 @@ +/* + This file is part of TALER + Copyright (C) 2019 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + 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 anastasis/anastasis-httpd.h + * @brief HTTP serving layer + * @author Christian Grothoff + */ +#ifndef ANASTASIS_HTTPD_H +#define ANASTASIS_HTTPD_H + +#include "platform.h" +#include "anastasis_database_lib.h" +#include +#include +#include + + +/** + * For how many years do we allow users to store truth at most? Also + * how long we store things if the cost is zero. + */ +#define ANASTASIS_MAX_YEARS_STORAGE 5 + + +/** + * @brief Struct describing an URL and the handler for it. + */ +struct AH_RequestHandler +{ + + /** + * URL the handler is for. + */ + const char *url; + + /** + * Method the handler is for, NULL for "all". + */ + const char *method; + + /** + * Mime type to use in reply (hint, can be NULL). + */ + const char *mime_type; + + /** + * Raw data for the @e handler + */ + const void *data; + + /** + * Number of bytes in @e data, 0 for 0-terminated. + */ + size_t data_size; + + + /** + * Function to call to handle the request. + * + * @param rh this struct + * @param connection the MHD connection to handle + * @return MHD result code + */ + MHD_RESULT (*handler)(struct AH_RequestHandler *rh, + struct MHD_Connection *connection); + + /** + * Default response code. + */ + unsigned int response_code; +}; + + +/** + * Each MHD response handler that sets the "connection_cls" to a + * non-NULL value must use a struct that has this struct as its first + * member. This struct contains a single callback, which will be + * invoked to clean up the memory when the contection is completed. + */ +struct TM_HandlerContext; + +/** + * Signature of a function used to clean up the context + * we keep in the "connection_cls" of MHD when handling + * a request. + * + * @param hc header of the context to clean up. + */ +typedef void +(*TM_ContextCleanup)(struct TM_HandlerContext *hc); + + +/** + * Each MHD response handler that sets the "connection_cls" to a + * non-NULL value must use a struct that has this struct as its first + * member. This struct contains a single callback, which will be + * invoked to clean up the memory when the connection is completed. + */ +struct TM_HandlerContext +{ + + /** + * Function to execute the handler-specific cleanup of the + * (typically larger) context. + */ + TM_ContextCleanup cc; + + /** + * Handler-specific context. + */ + void *ctx; + + /** + * Which request handler is handling this request? + */ + const struct AH_RequestHandler *rh; + + /** + * Asynchronous request context id. + */ + struct GNUNET_AsyncScopeId async_scope_id; +}; + +/** + * Handle to the database backend. + */ +extern struct ANASTASIS_DatabasePlugin *db; + +/** + * Upload limit to the service, in megabytes. + */ +extern unsigned long long AH_upload_limit_mb; + +/** + * Annual fee for the backup account. + */ +extern struct TALER_Amount AH_annual_fee; + +/** + * Fee for a truth upload. + */ +extern struct TALER_Amount AH_truth_upload_fee; + +/** + * Amount of insurance. + */ +extern struct TALER_Amount AH_insurance; + +/** + * Cost for secure question truth download. + */ +extern struct TALER_Amount AH_question_cost; + +/** + * Our Taler backend to process payments. + */ +extern char *AH_backend_url; + +/** + * Taler currency. + */ +extern char *AH_currency; + +/** + * Our configuration. + */ +extern const struct GNUNET_CONFIGURATION_Handle *AH_cfg; + +/** + * Number of policy uploads permitted per annual fee payment. + */ +extern unsigned long long AH_post_counter; + +/** + * Our fulfillment URL + */ +extern char *AH_fulfillment_url; + +/** + * Our business name. + */ +extern char *AH_business_name; + +/** + * Our server salt. + */ +extern struct ANASTASIS_CRYPTO_ProviderSaltP AH_server_salt; + +/** + * Our context for making HTTP requests. + */ +extern struct GNUNET_CURL_Context *AH_ctx; + + +/** + * 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. + * + * @param cls NULL + */ +void +AH_trigger_daemon (void *cls); + +/** + * Kick GNUnet Curl scheduler to begin curl interactions. + */ +void +AH_trigger_curl (void); + +#endif diff --git a/src/backend/anastasis-httpd_config.c b/src/backend/anastasis-httpd_config.c new file mode 100644 index 0000000..fff6bcb --- /dev/null +++ b/src/backend/anastasis-httpd_config.c @@ -0,0 +1,132 @@ +/* + This file is part of Anastasis + Copyright (C) 2020 Taler Systems SA + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis 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 + Anastasis; see the file COPYING.GPL. If not, see +*/ +/** + * @file backend/anastasis-httpd_config.c + * @brief headers for /terms handler + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include +#include "anastasis-httpd_config.h" +#include "anastasis-httpd.h" +#include +#include "anastasis_authorization_lib.h" + + +/** + * Add enabled methods and their fees to the ``/config`` response. + * + * @param[in,out] cls a `json_t` array to build + * @param section configuration section to inspect + */ +static void +add_methods (void *cls, + const char *section) +{ + json_t *method_arr = cls; + struct ANASTASIS_AuthorizationPlugin *p; + struct TALER_Amount cost; + json_t *method; + + if (0 != strncasecmp (section, + "authorization-", + strlen ("authorization-"))) + return; + if (GNUNET_YES != + GNUNET_CONFIGURATION_get_value_yesno (AH_cfg, + section, + "ENABLED")) + return; + section += strlen ("authorization-"); + p = ANASTASIS_authorization_plugin_load (section, + AH_cfg, + &cost); + if (NULL == p) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to load authorization plugin `%s'\n", + section); + return; + } + method = json_pack ("{s:s, s:o}", + "type", + section, + "cost", + TALER_JSON_from_amount (&cost)); + GNUNET_assert (NULL != method); + GNUNET_assert ( + 0 == + json_array_append_new (method_arr, + method)); +} + + +MHD_RESULT +AH_handler_config (struct AH_RequestHandler *rh, + struct MHD_Connection *connection) +{ + json_t *method_arr = json_array (); + + GNUNET_assert (NULL != method_arr); + { + json_t *method; + + method = json_pack ("{s:s, s:o}", + "type", + "question", + "cost", + TALER_JSON_from_amount (&AH_question_cost)); + GNUNET_assert ( + 0 == + json_array_append_new (method_arr, + method)); + } + GNUNET_CONFIGURATION_iterate_sections (AH_cfg, + &add_methods, + method_arr); + return TALER_MHD_reply_json_pack (connection, + MHD_HTTP_OK, + "{s:s, s:s, s:s, s:s, s:o, s:I," + " s:o, s:o, s:o, s:o }", + "name", + "anastasis", + "version", + "0:0:0", + "business_name", + AH_business_name, + "currency", + (char *) AH_currency, + "methods", + method_arr, + "storage_limit_in_megabytes", + (json_int_t) AH_upload_limit_mb, + /* 6 */ + "annual_fee", + TALER_JSON_from_amount (&AH_annual_fee), + "truth_upload_fee", + TALER_JSON_from_amount ( + &AH_truth_upload_fee), + "liability_limit", + TALER_JSON_from_amount (&AH_insurance), + "server_salt", + GNUNET_JSON_from_data_auto ( + &AH_server_salt)); +} + + +/* end of anastasis-httpd_config.c */ diff --git a/src/backend/anastasis-httpd_config.h b/src/backend/anastasis-httpd_config.h new file mode 100644 index 0000000..7d58792 --- /dev/null +++ b/src/backend/anastasis-httpd_config.h @@ -0,0 +1,41 @@ +/* + This file is part of Anastasis + Copyright (C) 2020 Taler Systems SA + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis 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 + Anastasis; see the file COPYING.GPL. If not, see +*/ +/** + * @file backend/anastasis-httpd_config.h + * @brief headers for /terms handler + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#ifndef ANASTASIS_HTTPD_CONFIG_H +#define ANASTASIS_HTTPD_CONFIG_H +#include +#include "anastasis-httpd.h" + +/** + * Manages a /config call. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +AH_handler_config (struct AH_RequestHandler *rh, + struct MHD_Connection *connection); + +#endif + +/* end of anastasis-httpd_config.h */ diff --git a/src/backend/anastasis-httpd_mhd.c b/src/backend/anastasis-httpd_mhd.c new file mode 100644 index 0000000..c39a54c --- /dev/null +++ b/src/backend/anastasis-httpd_mhd.c @@ -0,0 +1,70 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015, 2016 GNUnet e.V. and INRIA + + 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 anastasis-httpd_mhd.c + * @brief helpers for MHD interaction; these are TALER_EXCHANGE_handler_ functions + * that generate simple MHD replies that do not require any real operations + * to be performed (error handling, static pages, etc.) + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include "anastasis-httpd_mhd.h" + + +/** + * Function to call to handle the request by sending + * back static data from the @a rh. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +TMH_MHD_handler_static_response (struct AH_RequestHandler *rh, + struct MHD_Connection *connection) +{ + if (0 == rh->data_size) + rh->data_size = strlen ((const char *) rh->data); + return TALER_MHD_reply_static (connection, + rh->response_code, + rh->mime_type, + (void *) rh->data, + rh->data_size); +} + + +/** + * Function to call to handle the request by sending + * back a redirect to the AGPL source code. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +TMH_MHD_handler_agpl_redirect (struct AH_RequestHandler *rh, + struct MHD_Connection *connection) +{ + (void) rh; + return TALER_MHD_reply_agpl (connection, + "http://www.git.taler.net/anastasis.git"); +} + + +/* end of anastasis-httpd_mhd.c */ diff --git a/src/backend/anastasis-httpd_mhd.h b/src/backend/anastasis-httpd_mhd.h new file mode 100644 index 0000000..628abfa --- /dev/null +++ b/src/backend/anastasis-httpd_mhd.h @@ -0,0 +1,61 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015 GNUnet e.V. and INRIA + + 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 anastasis-httpd_mhd.h + * @brief helpers for MHD interaction, used to generate simple responses + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#ifndef ANASTASIS_HTTPD_MHD_H +#define ANASTASIS_HTTPD_MHD_H +#include +#include +#include "anastasis-httpd.h" + + +/** + * Function to call to handle the request by sending + * back static data from the @a rh. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] connection_cls the connection's closure (can be updated) + * @param upload_data upload data + * @param[in,out] upload_data_size number of bytes (left) in @a upload_data + * @param mi merchant backend instance, NULL is allowed in this case! + * @return MHD result code + */ +MHD_RESULT +TMH_MHD_handler_static_response (struct AH_RequestHandler *rh, + struct MHD_Connection *connection); + + +/** + * Function to call to handle the request by sending + * back a redirect to the AGPL source code. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +TMH_MHD_handler_agpl_redirect (struct AH_RequestHandler *rh, + struct MHD_Connection *connection); + + +#endif diff --git a/src/backend/anastasis-httpd_policy.c b/src/backend/anastasis-httpd_policy.c new file mode 100644 index 0000000..2417e15 --- /dev/null +++ b/src/backend/anastasis-httpd_policy.c @@ -0,0 +1,252 @@ +/* + This file is part of TALER + Copyright (C) 2019, 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 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 anastasis-httpd_policy.c + * @brief functions to handle incoming requests on /policy/ + * @author Dennis Neufeld + * @author Dominik Meister + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis-httpd.h" +#include "anastasis-httpd_policy.h" +#include "anastasis_service.h" +#include +#include +#include +#include +#include + +/** + * How long do we hold an HTTP client connection if + * we are awaiting payment before giving up? + */ +#define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MINUTES, 30) + + +/** + * Return the current recoverydocument of @a account on @a connection + * using @a default_http_status on success. + * + * @param connection MHD connection to use + * @param account account to query + * @return MHD result code + */ +static MHD_RESULT +return_policy (struct MHD_Connection *connection, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub) +{ + enum GNUNET_DB_QueryStatus qs; + struct MHD_Response *resp; + struct ANASTASIS_AccountSignatureP account_sig; + struct GNUNET_HashCode recovery_data_hash; + const char *version_s; + char version_b[14]; + uint32_t version; + void *res_recovery_data; + size_t res_recovery_data_size; + + version_s = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "version"); + if (NULL != version_s) + { + char dummy; + + if (1 != sscanf (version_s, + "%u%c", + &version, + &dummy)) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "version"); + } + qs = db->get_recovery_document (db->cls, + account_pub, + version, + &account_sig, + &recovery_data_hash, + &res_recovery_data_size, + &res_recovery_data); + } + else + { + qs = db->get_latest_recovery_document (db->cls, + account_pub, + &account_sig, + &recovery_data_hash, + &res_recovery_data_size, + &res_recovery_data, + &version); + GNUNET_snprintf (version_b, + sizeof (version_b), + "%u", + (unsigned int) version); + version_s = version_b; + } + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_recovery_document"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "get_recovery_document"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_ANASTASIS_POLICY_NOT_FOUND, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* interesting case below */ + break; + } + resp = MHD_create_response_from_buffer (res_recovery_data_size, + res_recovery_data, + MHD_RESPMEM_MUST_FREE); + TALER_MHD_add_global_headers (resp); + { + char *sig_s; + char *etag; + + sig_s = GNUNET_STRINGS_data_to_string_alloc (&account_sig, + sizeof (account_sig)); + etag = GNUNET_STRINGS_data_to_string_alloc (&recovery_data_hash, + sizeof (recovery_data_hash)); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE, + sig_s)); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_POLICY_VERSION, + version_s)); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_ETAG, + etag)); + GNUNET_free (etag); + GNUNET_free (sig_s); + } + { + MHD_RESULT ret; + + ret = MHD_queue_response (connection, + MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + return ret; + } +} + + +MHD_RESULT +AH_policy_get (struct MHD_Connection *connection, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub) +{ + struct GNUNET_HashCode recovery_data_hash; + enum ANASTASIS_DB_AccountStatus as; + MHD_RESULT ret; + uint32_t version; + struct GNUNET_TIME_Absolute expiration; + + as = db->lookup_account (db->cls, + account_pub, + &expiration, + &recovery_data_hash, + &version); + switch (as) + { + case ANASTASIS_DB_ACCOUNT_STATUS_PAYMENT_REQUIRED: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_SYNC_ACCOUNT_UNKNOWN, + NULL); + case ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup account"); + case ANASTASIS_DB_ACCOUNT_STATUS_NO_RESULTS: + { + struct MHD_Response *resp; + + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + TALER_MHD_add_global_headers (resp); + ret = MHD_queue_response (connection, + MHD_HTTP_NO_CONTENT, + resp); + MHD_destroy_response (resp); + } + return ret; + case ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED: + { + const char *inm; + + inm = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if (NULL != inm) + { + struct GNUNET_HashCode inm_h; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (inm, + strlen (inm), + &inm_h, + sizeof (inm_h))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ANASTASIS_POLICY_BAD_IF_NONE_MATCH, + "Etag must be a base32-encoded SHA-512 hash"); + } + if (0 == GNUNET_memcmp (&inm_h, + &recovery_data_hash)) + { + struct MHD_Response *resp; + + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + TALER_MHD_add_global_headers (resp); + ret = MHD_queue_response (connection, + MHD_HTTP_NOT_MODIFIED, + resp); + MHD_destroy_response (resp); + return ret; + } + } + } + /* We have a result, should fetch and return it! */ + break; + } + return return_policy (connection, + account_pub); +} diff --git a/src/backend/anastasis-httpd_policy.h b/src/backend/anastasis-httpd_policy.h new file mode 100644 index 0000000..9fb630d --- /dev/null +++ b/src/backend/anastasis-httpd_policy.h @@ -0,0 +1,66 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015, 2016 GNUnet e.V. + + 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 anastasis-httpd_policy.h + * @brief functions to handle incoming requests on /policy/ + * @author Dennis Neufeld + * @author Dominik Meister + * @author Christian Grothoff + */ +#ifndef ANASTASIS_HTTPD_POLICY_H +#define ANASTASIS_HTTPD_POLICY_H +#include + + +/** + * Service is shutting down, resume all MHD connections NOW. + */ +void +AH_resume_all_bc (void); + + +/** + * Handle GET /policy/$ACCOUNT_PUB request. + * + * @param connection the MHD connection to handle + * @param account_pub public key of the account + * @return MHD result code + */ +MHD_RESULT +AH_policy_get (struct MHD_Connection *connection, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub); + + +/** + * Handle POST /policy/$ACCOUNT_PUB request. + * + * @param connection the MHD connection to handle + * @param con_cls the connection's closure + * @param account_pub public key of the account + * @param upload_data upload data + * @param upload_data_size number of bytes (left) in @a upload_data + * @return MHD result code + */ +MHD_RESULT +AH_handler_policy_post ( + struct MHD_Connection *connection, + struct TM_HandlerContext *hc, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub, + const char *upload_data, + size_t *upload_data_size); + + +#endif diff --git a/src/backend/anastasis-httpd_policy_upload.c b/src/backend/anastasis-httpd_policy_upload.c new file mode 100644 index 0000000..b8bd5ed --- /dev/null +++ b/src/backend/anastasis-httpd_policy_upload.c @@ -0,0 +1,1211 @@ +/* + This file is part of TALER + Copyright (C) 2021 Anastasis SARL + + 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 anastasis-httpd_policy.c + * @brief functions to handle incoming requests on /policy/ + * @author Dennis Neufeld + * @author Dominik Meister + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis-httpd.h" +#include "anastasis-httpd_policy.h" +#include "anastasis_service.h" +#include +#include +#include +#include +#include + +/** + * How long do we hold an HTTP client connection if + * we are awaiting payment before giving up? + */ +#define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MINUTES, 30) + + +/** + * Context for an upload operation. + */ +struct PolicyUploadContext +{ + + /** + * Signature of the account holder. + */ + struct ANASTASIS_AccountSignatureP account_sig; + + /** + * Public key of the account holder. + */ + struct ANASTASIS_CRYPTO_AccountPublicKeyP account; + + /** + * Hash of the upload we are receiving right now (as promised + * by the client, to be verified!). + */ + struct GNUNET_HashCode new_policy_upload_hash; + + /** + * Hash context for the upload. + */ + struct GNUNET_HashContext *hash_ctx; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct PolicyUploadContext *next; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct PolicyUploadContext *prev; + + /** + * Used while suspended for resumption. + */ + struct MHD_Connection *con; + + /** + * Upload, with as many bytes as we have received so far. + */ + char *upload; + + /** + * Used while we are awaiting proposal creation. + */ + struct TALER_MERCHANT_PostOrdersHandle *po; + + /** + * Used while we are waiting payment. + */ + struct TALER_MERCHANT_OrderMerchantGetHandle *cpo; + + /** + * HTTP response code to use on resume, if non-NULL. + */ + struct MHD_Response *resp; + + /** + * Order under which the client promised payment, or NULL. + */ + const char *order_id; + + /** + * Payment Identifier + */ + struct ANASTASIS_PaymentSecretP payment_identifier; + + /** + * Timestamp of the order in @e payment_identifier. Used to + * select the most recent unpaid offer. + */ + struct GNUNET_TIME_Absolute existing_pi_timestamp; + + /** + * When does the operation timeout? + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * How long must the account be valid? Determines whether we should + * trigger payment, and if so how much. + */ + struct GNUNET_TIME_Absolute end_date; + + /** + * How long is the account already valid? + * Determines how much the user needs to pay. + */ + struct GNUNET_TIME_Absolute paid_until; + + /** + * Expected total upload size. + */ + size_t upload_size; + + /** + * Current offset for the upload. + */ + size_t upload_off; + + /** + * HTTP response code to use on resume, if resp is set. + */ + unsigned int response_code; + + /** + * For how many years does the client still have + * to pay? + */ + unsigned int years_to_pay; + + /** + * true if client provided a payment secret / order ID? + */ + bool payment_identifier_provided; + +}; + + +/** + * Kept in DLL for shutdown handling while suspended. + */ +static struct PolicyUploadContext *puc_head; + +/** + * Kept in DLL for shutdown handling while suspended. + */ +static struct PolicyUploadContext *puc_tail; + + +/** + * Service is shutting down, resume all MHD connections NOW. + */ +void +AH_resume_all_bc () +{ + struct PolicyUploadContext *puc; + + while (NULL != (puc = puc_head)) + { + GNUNET_CONTAINER_DLL_remove (puc_head, + puc_tail, + puc); + if (NULL != puc->po) + { + TALER_MERCHANT_orders_post_cancel (puc->po); + puc->po = NULL; + } + if (NULL != puc->cpo) + { + TALER_MERCHANT_merchant_order_get_cancel (puc->cpo); + puc->cpo = NULL; + } + MHD_resume_connection (puc->con); + } +} + + +/** + * Function called to clean up a backup context. + * + * @param hc a `struct PolicyUploadContext` + */ +static void +cleanup_ctx (struct TM_HandlerContext *hc) +{ + struct PolicyUploadContext *puc = hc->ctx; + + if (NULL != puc->po) + TALER_MERCHANT_orders_post_cancel (puc->po); + if (NULL != puc->cpo) + TALER_MERCHANT_merchant_order_get_cancel (puc->cpo); + if (NULL != puc->hash_ctx) + GNUNET_CRYPTO_hash_context_abort (puc->hash_ctx); + if (NULL != puc->resp) + MHD_destroy_response (puc->resp); + GNUNET_free (puc->upload); + GNUNET_free (puc); +} + + +/** + * Transmit a payment request for @a order_id on @a connection + * + * @param connection MHD connection + * @param order_id our backend's order ID + * @return #GNUNET_OK on success + */ +static int +make_payment_request (struct PolicyUploadContext *puc) +{ + struct MHD_Response *resp; + + /* request payment via Taler */ + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + if (NULL == resp) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + TALER_MHD_add_global_headers (resp); + { + char *hdr; + char *pfx; + char *hn; + + if (0 == strncasecmp ("https://", + AH_backend_url, + strlen ("https://"))) + { + pfx = "taler://"; + hn = &AH_backend_url[strlen ("https://")]; + } + else if (0 == strncasecmp ("http://", + AH_backend_url, + strlen ("http://"))) + { + pfx = "taler+http://"; + hn = &AH_backend_url[strlen ("http://")]; + } + else + { + GNUNET_break (0); + MHD_destroy_response (resp); + return GNUNET_SYSERR; + } + if (0 == strlen (hn)) + { + GNUNET_break (0); + MHD_destroy_response (resp); + return GNUNET_SYSERR; + } + { + char *order_id; + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &puc->payment_identifier, + sizeof (puc->payment_identifier)); + GNUNET_asprintf (&hdr, + "%spay/%s%s/", + pfx, + hn, + order_id); + GNUNET_free (order_id); + } + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_TALER, + hdr)); + GNUNET_free (hdr); + } + puc->resp = resp; + puc->response_code = MHD_HTTP_PAYMENT_REQUIRED; + return GNUNET_OK; +} + + +/** + * Callbacks of this type are used to serve the result of submitting a + * POST /private/orders request to a merchant. + * + * @param cls our `struct PolicyUploadContext` + * @param por response details + */ +static void +proposal_cb (void *cls, + const struct TALER_MERCHANT_PostOrdersReply *por) +{ + struct PolicyUploadContext *puc = cls; + enum GNUNET_DB_QueryStatus qs; + + puc->po = NULL; + GNUNET_CONTAINER_DLL_remove (puc_head, + puc_tail, + puc); + MHD_resume_connection (puc->con); + AH_trigger_daemon (NULL); + if (MHD_HTTP_OK != por->hr.http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Backend returned status %u/%d\n", + por->hr.http_status, + (int) por->hr.ec); + GNUNET_break (0); + puc->resp = TALER_MHD_make_json_pack ( + "{s:I, s:s, s:I, s:I, s:O?}", + "code", + (json_int_t) TALER_EC_SYNC_PAYMENT_CREATE_BACKEND_ERROR, + "hint", + "Failed to setup order with merchant backend", + "backend-ec", + (json_int_t) por->hr.ec, + "backend-http-status", + (json_int_t) por->hr.http_status, + "backend-reply", + por->hr.reply); + GNUNET_assert (NULL != puc->resp); + puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Storing payment request for order `%s'\n", + por->details.ok.order_id); + + qs = db->record_recdoc_payment (db->cls, + &puc->account, + (uint32_t) AH_post_counter, + &puc->payment_identifier, + &AH_annual_fee); + if (0 >= qs) + { + GNUNET_break (0); + puc->resp = TALER_MHD_make_error ( + TALER_EC_GENERIC_DB_STORE_FAILED, + "record recdoc payment"); + puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; + } + if (GNUNET_OK != + make_payment_request (puc)) + { + GNUNET_break (0); + puc->resp = TALER_MHD_make_error ( + TALER_EC_GENERIC_DB_STORE_FAILED, + "failed to initiate payment"); + puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + } +} + + +/** + * Callback to process a GET /check-payment request + * + * @param cls our `struct PolicyUploadContext` + * @param hr HTTP response details + * @param osr order status + */ +static void +check_payment_cb (void *cls, + const struct TALER_MERCHANT_HttpResponse *hr, + const struct TALER_MERCHANT_OrderStatusResponse *osr) +{ + struct PolicyUploadContext *puc = cls; + + /* refunds are not supported, verify */ + puc->cpo = NULL; + GNUNET_CONTAINER_DLL_remove (puc_head, + puc_tail, + puc); + MHD_resume_connection (puc->con); + AH_trigger_daemon (NULL); + switch (hr->http_status) + { + case MHD_HTTP_OK: + GNUNET_assert (NULL != osr); + break; /* processed below */ + case MHD_HTTP_UNAUTHORIZED: + puc->resp = TALER_MHD_make_error ( + TALER_EC_ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED, + NULL); + puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; + default: + puc->resp = TALER_MHD_make_error ( + TALER_EC_ANASTASIS_GENERIC_BACKEND_ERROR, + "failed to initiate payment"); + puc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment status checked: %s\n", + osr->status ? "paid" : "unpaid"); + switch (osr->status) + { + case TALER_MERCHANT_OSC_PAID: + { + enum GNUNET_DB_QueryStatus qs; + unsigned int years; + struct GNUNET_TIME_Relative paid_until; + const json_t *contract; + struct TALER_Amount amount; + struct GNUNET_JSON_Specification cspec[] = { + TALER_JSON_spec_amount ("amount", + AH_currency, + &amount), + GNUNET_JSON_spec_end () + }; + + contract = osr->details.paid.contract_terms; + if (GNUNET_OK != + GNUNET_JSON_parse (contract, + cspec, + NULL, NULL)) + { + GNUNET_break (0); + puc->resp = TALER_MHD_make_error ( + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + "no amount given"); + puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; /* continue as planned */ + } + years = TALER_amount_divide2 (&amount, + &AH_annual_fee); + paid_until = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, + years); + /* add 1 week grace period, otherwise if a user + wants to pay for 1 year, the first seconds + would have passed between making the payment + and our subsequent check if +1 year was + paid... So we actually say 1 year = 52 weeks + on the server, while the client calculates + with 365 days. */ + paid_until = GNUNET_TIME_relative_add (paid_until, + GNUNET_TIME_UNIT_WEEKS); + + qs = db->increment_lifetime (db->cls, + &puc->account, + &puc->payment_identifier, + paid_until, + &puc->paid_until); + if (0 <= qs) + return; /* continue as planned */ + GNUNET_break (0); + puc->resp = TALER_MHD_make_error ( + TALER_EC_GENERIC_DB_FETCH_FAILED, + "increment lifetime"); + puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; /* continue as planned */ + } + case TALER_MERCHANT_OSC_UNPAID: + case TALER_MERCHANT_OSC_CLAIMED: + break; + } + if (0 != puc->existing_pi_timestamp.abs_value_us) + { + /* repeat payment request */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Repeating payment request\n"); + if (GNUNET_OK != + make_payment_request (puc)) + { + GNUNET_break (0); + puc->resp = TALER_MHD_make_error ( + TALER_EC_GENERIC_DB_STORE_FAILED, + "failed to initiate payment"); + puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + } + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Timeout waiting for payment\n"); + puc->resp = TALER_MHD_make_error (TALER_EC_SYNC_PAYMENT_GENERIC_TIMEOUT, + "Timeout awaiting promised payment"); + GNUNET_assert (NULL != puc->resp); + puc->response_code = MHD_HTTP_REQUEST_TIMEOUT; +} + + +/** + * Helper function used to ask our backend to await + * a payment for the user's account. + * + * @param puc context to begin payment for. + * @param timeout when to give up trying + */ +static void +await_payment (struct PolicyUploadContext *puc) +{ + struct GNUNET_TIME_Relative timeout + = GNUNET_TIME_absolute_get_remaining (puc->timeout); + + GNUNET_CONTAINER_DLL_insert (puc_head, + puc_tail, + puc); + MHD_suspend_connection (puc->con); + { + char *order_id; + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &puc->payment_identifier, + sizeof(struct ANASTASIS_PaymentSecretP)); + puc->cpo = TALER_MERCHANT_merchant_order_get (AH_ctx, + AH_backend_url, + order_id, + NULL /* our payments are NOT session-bound */, + false, + timeout, + &check_payment_cb, + puc); + GNUNET_free (order_id); + } + AH_trigger_curl (); +} + + +/** + * Helper function used to ask our backend to begin processing a + * payment for the user's account. May perform asynchronous + * operations by suspending the connection if required. + * + * @param puc context to begin payment for. + * @return MHD status code + */ +static MHD_RESULT +begin_payment (struct PolicyUploadContext *puc) +{ + json_t *order; + + GNUNET_CONTAINER_DLL_insert (puc_head, + puc_tail, + puc); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending connection while creating order at `%s'\n", + AH_backend_url); + { + char *order_id; + struct TALER_Amount upload_fee; + + if (0 > + TALER_amount_multiply (&upload_fee, + &AH_annual_fee, + puc->years_to_pay)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (puc->con, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "storage_duration_years"); + } + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &puc->payment_identifier, + sizeof(struct ANASTASIS_PaymentSecretP)); + order = json_pack ("{s:o, s:s, s:[{s:s,s:I,s:s}], s:s }", + "amount", TALER_JSON_from_amount (&upload_fee), + "summary", "Anastasis policy storage fee", + "products", + "description", "policy storage fee", + "quantity", (json_int_t) puc->years_to_pay, + "unit", "years", + "order_id", order_id); + GNUNET_free (order_id); + } + MHD_suspend_connection (puc->con); + puc->po = TALER_MERCHANT_orders_post2 (AH_ctx, + AH_backend_url, + order, + GNUNET_TIME_UNIT_ZERO, + NULL, /* no payment target */ + 0, + NULL, /* no inventory products */ + 0, + NULL, /* no uuids */ + false, /* do NOT require claim token */ + &proposal_cb, + puc); + AH_trigger_curl (); + json_decref (order); + return MHD_YES; +} + + +/** + * Prepare to receive a payment, possibly requesting it, or just waiting + * for it to be completed by the client. + * + * @param puc context to prepare payment for + * @return MHD status + */ +static MHD_RESULT +prepare_payment (struct PolicyUploadContext *puc) +{ + if (! puc->payment_identifier_provided) + { + GNUNET_CRYPTO_random_block ( + GNUNET_CRYPTO_QUALITY_NONCE, + &puc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP)); + puc->payment_identifier_provided = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No payment identifier, initiating payment\n"); + return begin_payment (puc); + } + await_payment (puc); + return MHD_YES; +} + + +MHD_RESULT +AH_handler_policy_post ( + struct MHD_Connection *connection, + struct TM_HandlerContext *hc, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub, + const char *recovery_data, + size_t *recovery_data_size) +{ + struct PolicyUploadContext *puc = hc->ctx; + + if (NULL == puc) + { + /* first call, setup internals */ + puc = GNUNET_new (struct PolicyUploadContext); + hc->ctx = puc; + hc->cc = &cleanup_ctx; + puc->con = connection; + + { + const char *pay_id; + + pay_id = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER); + if (NULL != pay_id) + { + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + pay_id, + strlen (pay_id), + &puc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER + " header must be a base32-encoded Payment-Secret"); + } + puc->payment_identifier_provided = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Policy upload started with payment identifier `%s'\n", + pay_id); + } + } + puc->account = *account_pub; + /* now setup 'puc' */ + { + const char *lens; + unsigned long len; + + lens = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_CONTENT_LENGTH); + if ( (NULL == lens) || + (1 != sscanf (lens, + "%lu", + &len)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + (NULL == lens) + ? TALER_EC_ANASTASIS_GENERIC_MISSING_CONTENT_LENGTH + : TALER_EC_ANASTASIS_GENERIC_MALFORMED_CONTENT_LENGTH, + NULL); + } + if (len / 1024 / 1024 >= AH_upload_limit_mb) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_PAYLOAD_TOO_LARGE, + TALER_EC_SYNC_MALFORMED_CONTENT_LENGTH, + "Content-length value not acceptable"); + } + puc->upload = GNUNET_malloc_large (len); + if (NULL == puc->upload) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "malloc"); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_PAYLOAD_TOO_LARGE, + TALER_EC_ANASTASIS_POLICY_OUT_OF_MEMORY_ON_CONTENT_LENGTH, + NULL); + } + puc->upload_size = (size_t) len; + } + { + /* Check if header contains Anastasis-Policy-Signature */ + const char *sig_s; + + sig_s = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE); + if ( (NULL == sig_s) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (sig_s, + strlen (sig_s), + &puc->account_sig, + sizeof (puc->account_sig))) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ANASTASIS_POLICY_BAD_SIGNATURE, + ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE + " header must include a base32-encoded EdDSA signature"); + } + } + { + /* Check if header contains an ETAG */ + const char *etag; + + etag = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if ( (NULL == etag) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (etag, + strlen (etag), + &puc->new_policy_upload_hash, + sizeof (puc->new_policy_upload_hash))) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ANASTASIS_POLICY_BAD_IF_MATCH, + MHD_HTTP_HEADER_IF_NONE_MATCH + " header must include a base32-encoded SHA-512 hash"); + } + } + /* validate signature */ + { + struct ANASTASIS_UploadSignaturePS usp = { + .purpose.size = htonl (sizeof (usp)), + .purpose.purpose = htonl (TALER_SIGNATURE_ANASTASIS_POLICY_UPLOAD), + .new_recovery_data_hash = puc->new_policy_upload_hash + }; + + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_ANASTASIS_POLICY_UPLOAD, + &usp, + &puc->account_sig.eddsa_sig, + &account_pub->pub)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_POLICY_BAD_SIGNATURE, + ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE); + } + } + + { + const char *long_poll_timeout_ms; + + long_poll_timeout_ms = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "timeout_ms"); + if (NULL != long_poll_timeout_ms) + { + unsigned int timeout; + + if (1 != sscanf (long_poll_timeout_ms, + "%u", + &timeout)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "timeout_ms (must be non-negative number)"); + } + puc->timeout + = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply ( + GNUNET_TIME_UNIT_MILLISECONDS, + timeout)); + } + else + { + puc->timeout = GNUNET_TIME_relative_to_absolute + (CHECK_PAYMENT_GENERIC_TIMEOUT); + } + } + + /* check if the client insists on paying */ + { + const char *req; + unsigned int years; + + req = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "storage_duration"); + if (NULL != req) + { + char dummy; + + if (1 != sscanf (req, + "%u%c", + &years, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "storage_duration (must be non-negative number)"); + } + } + else + { + years = 0; + } + puc->end_date = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, + years)); + } + + /* get ready to hash (done here as we may go async for payments next) */ + puc->hash_ctx = GNUNET_CRYPTO_hash_context_start (); + + /* Check database to see if the transaction is permissible */ + { + struct GNUNET_TIME_Relative rem; + + rem = GNUNET_TIME_absolute_get_remaining (puc->end_date); + puc->years_to_pay = rem.rel_value_us + / GNUNET_TIME_UNIT_YEARS.rel_value_us; + if (0 != (rem.rel_value_us % GNUNET_TIME_UNIT_YEARS.rel_value_us)) + puc->years_to_pay++; + + if (puc->payment_identifier_provided) + { + /* check if payment identifier is valid (existing and paid) */ + bool paid; + bool valid_counter; + enum GNUNET_DB_QueryStatus qs; + + qs = db->check_payment_identifier (db->cls, + &puc->payment_identifier, + &paid, + &valid_counter); + if (qs < 0) + return TALER_MHD_reply_with_error (puc->con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + + if ( (! paid) || + (! valid_counter) ) + { + if (! valid_counter) + { + puc->payment_identifier_provided = false; + if (0 == puc->years_to_pay) + puc->years_to_pay = 1; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Too many uploads with this payment identifier, initiating fresh payment\n"); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Given payment identifier not known to be paid, initiating payment\n"); + } + return prepare_payment (puc); + } + } + + if (! puc->payment_identifier_provided) + { + struct TALER_Amount zero_amount; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Relative rel; + + TALER_amount_set_zero (AH_currency, + &zero_amount); + /* generate fresh payment identifier */ + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG, + &puc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP)); + if (0 != TALER_amount_cmp (&AH_annual_fee, + &zero_amount)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No payment identifier, requesting payment\n"); + return begin_payment (puc); + } + /* Cost is zero, fake "zero" payment having happened */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Policy upload is free, allowing upload without payment\n"); + qs = db->record_recdoc_payment (db->cls, + account_pub, + AH_post_counter, + &puc->payment_identifier, + &AH_annual_fee); + if (qs <= 0) + return TALER_MHD_reply_with_error (puc->con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + rel = GNUNET_TIME_relative_multiply ( + GNUNET_TIME_UNIT_YEARS, + ANASTASIS_MAX_YEARS_STORAGE); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Policy lifetime is %s (%u years)\n", + GNUNET_STRINGS_relative_time_to_string (rel, + GNUNET_YES), + ANASTASIS_MAX_YEARS_STORAGE); + puc->paid_until = GNUNET_TIME_relative_to_absolute (rel); + qs = db->update_lifetime (db->cls, + account_pub, + &puc->payment_identifier, + puc->paid_until); + if (qs <= 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (puc->con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + } + } + + /* Check if existing policy matches upload (and if, skip it) */ + { + struct GNUNET_HashCode hc; + enum ANASTASIS_DB_AccountStatus as; + uint32_t version; + struct GNUNET_TIME_Absolute now; + struct GNUNET_TIME_Relative rem; + + as = db->lookup_account (db->cls, + account_pub, + &puc->paid_until, + &hc, + &version); + now = GNUNET_TIME_absolute_get (); + if (puc->paid_until.abs_value_us < now.abs_value_us) + puc->paid_until = now; + rem = GNUNET_TIME_absolute_get_difference (puc->paid_until, + puc->end_date); + puc->years_to_pay = rem.rel_value_us + / GNUNET_TIME_UNIT_YEARS.rel_value_us; + if (0 != (rem.rel_value_us % GNUNET_TIME_UNIT_YEARS.rel_value_us)) + puc->years_to_pay++; + + if ( (ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED == as) && + (0 != puc->years_to_pay) ) + { + /* user requested extension, force payment */ + as = ANASTASIS_DB_ACCOUNT_STATUS_PAYMENT_REQUIRED; + } + switch (as) + { + case ANASTASIS_DB_ACCOUNT_STATUS_PAYMENT_REQUIRED: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Expiration too low, initiating payment\n"); + return prepare_payment (puc); + case ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error (puc->con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case ANASTASIS_DB_ACCOUNT_STATUS_NO_RESULTS: + /* continue below */ + break; + case ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED: + if (0 == GNUNET_memcmp (&hc, + &puc->new_policy_upload_hash)) + { + /* Refuse upload: we already have that backup! */ + struct MHD_Response *resp; + MHD_RESULT ret; + char version_s[14]; + + GNUNET_snprintf (version_s, + sizeof (version_s), + "%u", + (unsigned int) version); + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + TALER_MHD_add_global_headers (resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_POLICY_VERSION, + version_s)); + ret = MHD_queue_response (connection, + MHD_HTTP_NOT_MODIFIED, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } + break; + } + } + /* ready to begin! */ + return MHD_YES; + } + + if (NULL != puc->resp) + { + MHD_RESULT ret; + + /* We generated a response asynchronously, queue that */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning asynchronously generated response with HTTP status %u\n", + puc->response_code); + ret = MHD_queue_response (connection, + puc->response_code, + puc->resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (puc->resp); + puc->resp = NULL; + return ret; + } + + /* handle upload */ + if (0 != *recovery_data_size) + { + /* check MHD invariant */ + GNUNET_assert (puc->upload_off + *recovery_data_size <= puc->upload_size); + memcpy (&puc->upload[puc->upload_off], + recovery_data, + *recovery_data_size); + puc->upload_off += *recovery_data_size; + GNUNET_CRYPTO_hash_context_read (puc->hash_ctx, + recovery_data, + *recovery_data_size); + *recovery_data_size = 0; + return MHD_YES; + } + + if ( (0 == puc->upload_off) && + (0 != puc->upload_size) && + (NULL == puc->resp) ) + { + /* wait for upload */ + return MHD_YES; + } + + /* finished with upload, check hash */ + if (NULL != puc->hash_ctx) + { + struct GNUNET_HashCode our_hash; + + GNUNET_CRYPTO_hash_context_finish (puc->hash_ctx, + &our_hash); + puc->hash_ctx = NULL; + if (0 != GNUNET_memcmp (&our_hash, + &puc->new_policy_upload_hash)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ANASTASIS_POLICY_INVALID_UPLOAD, + "Data uploaded does not match Etag promise"); + } + } + + /* store backup to database */ + { + enum ANASTASIS_DB_StoreStatus ss; + uint32_t version = UINT32_MAX; + char version_s[14]; + char expir_s[32]; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Uploading recovery document\n"); + ss = db->store_recovery_document (db->cls, + &puc->account, + &puc->account_sig, + &puc->new_policy_upload_hash, + puc->upload, + puc->upload_size, + &puc->payment_identifier, + &version); + GNUNET_snprintf (version_s, + sizeof (version_s), + "%u", + (unsigned int) version); + GNUNET_snprintf (expir_s, + sizeof (expir_s), + "%llu", + (unsigned long long) + (puc->paid_until.abs_value_us + / GNUNET_TIME_UNIT_SECONDS.rel_value_us)); + switch (ss) + { + case ANASTASIS_DB_STORE_STATUS_STORE_LIMIT_EXCEEDED: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Storage request limit exceeded, requesting payment\n"); + if (! puc->payment_identifier_provided) + { + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG, + &puc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP)); + puc->payment_identifier_provided = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Also no payment identifier, requesting payment\n"); + } + return begin_payment (puc); + case ANASTASIS_DB_STORE_STATUS_PAYMENT_REQUIRED: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Policy store operation requires payment\n"); + if (! puc->payment_identifier_provided) + { + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG, + &puc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP)); + puc->payment_identifier_provided = true; + } + return begin_payment (puc); + case ANASTASIS_DB_STORE_STATUS_HARD_ERROR: + case ANASTASIS_DB_STORE_STATUS_SOFT_ERROR: + return TALER_MHD_reply_with_error (puc->con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case ANASTASIS_DB_STORE_STATUS_NO_RESULTS: + { + /* database says nothing actually changed, 304 (could + theoretically happen if another equivalent upload succeeded + since we last checked!) */ + struct MHD_Response *resp; + MHD_RESULT ret; + + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + TALER_MHD_add_global_headers (resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + "Anastasis-Version", + version_s)); + ret = MHD_queue_response (connection, + MHD_HTTP_NOT_MODIFIED, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } + case ANASTASIS_DB_STORE_STATUS_SUCCESS: + /* generate main (204) standard success reply */ + { + struct MHD_Response *resp; + MHD_RESULT ret; + + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + TALER_MHD_add_global_headers (resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_POLICY_VERSION, + version_s)); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_POLICY_EXPIRATION, + expir_s)); + ret = MHD_queue_response (connection, + MHD_HTTP_NO_CONTENT, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } + } + } + GNUNET_break (0); + return MHD_NO; +} diff --git a/src/backend/anastasis-httpd_terms.c b/src/backend/anastasis-httpd_terms.c new file mode 100644 index 0000000..6be5690 --- /dev/null +++ b/src/backend/anastasis-httpd_terms.c @@ -0,0 +1,98 @@ +/* + This file is part of Anastasis + Copyright (C) 2020 Taler Systems SA + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis 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 + Anastasis; see the file COPYING.GPL. If not, see +*/ +/** + * @file backend/anastasis-httpd_terms.c + * @brief headers for /terms handler + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#include "platform.h" +#include "anastasis-httpd_terms.h" +#include + +/** + * Our terms of service. + */ +static struct TALER_MHD_Legal *tos; + + +/** + * Our privacy policy. + */ +static struct TALER_MHD_Legal *pp; + + +/** + * Manages a /terms call. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +AH_handler_terms (struct AH_RequestHandler *rh, + struct MHD_Connection *connection) +{ + (void) rh; + return TALER_MHD_reply_legal (connection, + tos); +} + + +/** + * Handle a "/privacy" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +AH_handler_privacy (const struct AH_RequestHandler *rh, + struct MHD_Connection *connection) +{ + (void) rh; + return TALER_MHD_reply_legal (connection, + pp); +} + + +/** + * Load our terms of service as per configuration. + * + * @param cfg configuration to process + */ +void +AH_load_terms (const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + tos = TALER_MHD_legal_load (cfg, + "anastasis", + "TERMS_DIR", + "TERMS_ETAG"); + if (NULL == tos) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Terms of service not configured\n"); + pp = TALER_MHD_legal_load (cfg, + "anastasis", + "PRIVACY_DIR", + "PRIVACY_ETAG"); + if (NULL == pp) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Privacy policy not configured\n"); +} + + +/* end of anastasis-httpd_terms.c */ diff --git a/src/backend/anastasis-httpd_terms.h b/src/backend/anastasis-httpd_terms.h new file mode 100644 index 0000000..dc59d41 --- /dev/null +++ b/src/backend/anastasis-httpd_terms.h @@ -0,0 +1,62 @@ +/* + This file is part of Anastasis + Copyright (C) 2020, 2021 Taler Systems SA + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis 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 + Anastasis; see the file COPYING.GPL. If not, see +*/ +/** + * @file backend/anastasis-httpd_terms.h + * @brief headers for /terms handler + * @author Christian Grothoff + * @author Dennis Neufeld + * @author Dominik Meister + */ +#ifndef ANASTASIS_HTTPD_TERMS_H +#define ANASTASIS_HTTPD_TERMS_H +#include +#include "anastasis-httpd.h" + +/** + * Manages a /terms call. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +AH_handler_terms (struct AH_RequestHandler *rh, + struct MHD_Connection *connection); + + +/** + * Handle a "/privacy" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @return MHD result code + */ +MHD_RESULT +AH_handler_privacy (const struct AH_RequestHandler *rh, + struct MHD_Connection *connection); + +/** + * Load our terms of service as per configuration. + * + * @param cfg configuration to process + */ +void +AH_load_terms (const struct GNUNET_CONFIGURATION_Handle *cfg); + + +#endif + +/* end of anastasis-httpd_terms.h */ diff --git a/src/backend/anastasis-httpd_truth.c b/src/backend/anastasis-httpd_truth.c new file mode 100644 index 0000000..164c33a --- /dev/null +++ b/src/backend/anastasis-httpd_truth.c @@ -0,0 +1,1428 @@ +/* + This file is part of TALER + Copyright (C) 2019, 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 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 anastasis-httpd_truth.c + * @brief functions to handle incoming requests on /truth + * @author Dennis Neufeld + * @author Dominik Meister + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis-httpd.h" +#include "anastasis_service.h" +#include "anastasis-httpd_truth.h" +#include +#include +#include "anastasis_authorization_lib.h" +#include +#include + +/** + * What is the maximum frequency at which we allow + * clients to attempt to answer security questions? + */ +#define MAX_QUESTION_FREQ GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_SECONDS, 30) + +/** + * How long do we hold an HTTP client connection if + * we are awaiting payment before giving up? + */ +#define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MINUTES, 30) + +/** + * How long should the wallet check for auto-refunds before giving up? + */ +#define AUTO_REFUND_TIMEOUT GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MINUTES, 2) + + +/** + * How many retries do we allow per code? + */ +#define INITIAL_RETRY_COUNTER 3 + +struct GetContext +{ + + /** + * Payment Identifier + */ + struct ANASTASIS_PaymentSecretP payment_identifier; + + /** + * Public key of the challenge which is solved. + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Key to decrypt the truth. + */ + struct ANASTASIS_CRYPTO_TruthKeyP truth_key; + + /** + * true if client provided a payment secret / order ID? + */ + struct TALER_Amount challenge_cost; + + /** + * Our handler context. + */ + struct TM_HandlerContext *hc; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct GetContext *next; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct GetContext *prev; + + /** + * Connection handle for closing or resuming + */ + struct MHD_Connection *connection; + + /** + * Reference to the authorization plugin which was loaded + */ + struct ANASTASIS_AuthorizationPlugin *authorization; + + /** + * Status of the authorization + */ + struct ANASTASIS_AUTHORIZATION_State *as; + + /** + * Used while we are awaiting proposal creation. + */ + struct TALER_MERCHANT_PostOrdersHandle *po; + + /** + * Used while we are waiting payment. + */ + struct TALER_MERCHANT_OrderMerchantGetHandle *cpo; + + /** + * HTTP response code to use on resume, if non-NULL. + */ + struct MHD_Response *resp; + + /** + * How long do we wait at most for payment? + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * Random authorization code we are using. + */ + uint64_t code; + + /** + * HTTP response code to use on resume, if resp is set. + */ + unsigned int response_code; + + /** + * true if client provided a payment secret / order ID? + */ + bool payment_identifier_provided; + + /** + * True if this entry is in the #gc_head DLL. + */ + bool in_list; + + /** + * True if this entry is currently suspended. + */ + bool suspended; + + /** + * Did the request include a response? + */ + bool have_response; + +}; + +/** + * Information we track for refunds. + */ +struct RefundEntry +{ + /** + * Kept in a DLL. + */ + struct RefundEntry *next; + + /** + * Kept in a DLL. + */ + struct RefundEntry *prev; + + /** + * Operation handle. + */ + struct TALER_MERCHANT_OrderRefundHandle *ro; + + /** + * Which order is being refunded. + */ + char *order_id; + + /** + * Payment Identifier + */ + struct ANASTASIS_PaymentSecretP payment_identifier; + + /** + * Public key of the challenge which is solved. + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; +}; + + +/** + * Head of linked list of active refund operations. + */ +static struct RefundEntry *re_head; + +/** + * Tail of linked list of active refund operations. + */ +static struct RefundEntry *re_tail; + +/** + * Head of linked list over all authorization processes + */ +static struct GetContext *gc_head; + +/** + * Tail of linked list over all authorization processes + */ +static struct GetContext *gc_tail; + + +void +AH_truth_shutdown (void) +{ + struct GetContext *gc; + struct RefundEntry *re; + + while (NULL != (re = re_head)) + { + GNUNET_CONTAINER_DLL_remove (re_head, + re_tail, + re); + if (NULL != re->ro) + { + TALER_MERCHANT_post_order_refund_cancel (re->ro); + re->ro = NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Refund `%s' failed due to shutdown\n", + re->order_id); + GNUNET_free (re->order_id); + GNUNET_free (re); + } + + while (NULL != (gc = gc_head)) + { + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + if (NULL != gc->cpo) + { + TALER_MERCHANT_merchant_order_get_cancel (gc->cpo); + gc->cpo = NULL; + } + if (NULL != gc->po) + { + TALER_MERCHANT_orders_post_cancel (gc->po); + gc->po = NULL; + } + if (gc->suspended) + { + MHD_resume_connection (gc->connection); + gc->suspended = false; + } + if (NULL != gc->as) + { + gc->authorization->cleanup (gc->as); + gc->as = NULL; + gc->authorization = NULL; + } + } + ANASTASIS_authorization_plugin_shutdown (); +} + + +/** + * Callback to process a POST /orders/ID/refund request + * + * @param cls closure + * @param http_status HTTP status code for this request + * @param ec taler-specific error code + * @param taler_refund_uri the refund uri offered to the wallet + * @param h_contract hash of the contract a Browser may need to authorize + * obtaining the HTTP response. + */ +static void +refund_cb ( + void *cls, + const struct TALER_MERCHANT_HttpResponse *hr, + const char *taler_refund_uri, + const struct GNUNET_HashCode *h_contract) +{ + struct RefundEntry *re = cls; + + re->ro = NULL; + switch (hr->http_status) + { + case MHD_HTTP_OK: + { + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Refund `%s' succeeded\n", + re->order_id); + qs = db->record_challenge_refund (db->cls, + &re->truth_uuid, + &re->payment_identifier); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Refund `%s' failed with HTTP status %u: %s (#%u)\n", + re->order_id, + hr->http_status, + hr->hint, + (unsigned int) hr->ec); + break; + } + GNUNET_CONTAINER_DLL_remove (re_head, + re_tail, + re); + GNUNET_free (re->order_id); + GNUNET_free (re); +} + + +/** + * Start to give a refund for the challenge created by @a gc. + * + * @param gc request where we failed and should now grant a refund for + */ +static void +begin_refund (const struct GetContext *gc) +{ + struct RefundEntry *re; + + re = GNUNET_new (struct RefundEntry); + re->order_id = GNUNET_STRINGS_data_to_string_alloc ( + &gc->payment_identifier, + sizeof (gc->payment_identifier)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Challenge execution failed, triggering refund for order `%s'\n", + re->order_id); + re->payment_identifier = gc->payment_identifier; + re->truth_uuid = gc->truth_uuid; + re->ro = TALER_MERCHANT_post_order_refund (AH_ctx, + AH_backend_url, + re->order_id, + &gc->challenge_cost, + "failed to issue challenge", + &refund_cb, + re); + if (NULL == re->ro) + { + GNUNET_break (0); + GNUNET_free (re->order_id); + GNUNET_free (re); + return; + } + GNUNET_CONTAINER_DLL_insert (re_head, + re_tail, + re); +} + + +/** + * Callback used to notify the application about completed requests. + * Cleans up the requests data structures. + * + * @param hc + */ +static void +request_done (struct TM_HandlerContext *hc) +{ + struct GetContext *gc = hc->ctx; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Request completed\n"); + if (NULL == gc) + return; + hc->cc = NULL; + GNUNET_assert (! gc->suspended); + if (gc->in_list) + { + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + } + if (NULL != gc->as) + { + gc->authorization->cleanup (gc->as); + gc->authorization = NULL; + gc->as = NULL; + } + if (NULL != gc->cpo) + { + TALER_MERCHANT_merchant_order_get_cancel (gc->cpo); + gc->cpo = NULL; + } + if (NULL != gc->po) + { + TALER_MERCHANT_orders_post_cancel (gc->po); + gc->po = NULL; + } + GNUNET_free (gc); + hc->ctx = NULL; +} + + +/** + * Transmit a payment request for @a order_id on @a connection + * + * @param gc context to make payment request for + */ +static void +make_payment_request (struct GetContext *gc) +{ + struct MHD_Response *resp; + + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + GNUNET_assert (NULL != resp); + TALER_MHD_add_global_headers (resp); + { + char *hdr; + char *order_id; + const char *pfx; + const char *hn; + + if (0 == strncasecmp ("https://", + AH_backend_url, + strlen ("https://"))) + { + pfx = "taler://"; + hn = &AH_backend_url[strlen ("https://")]; + } + else if (0 == strncasecmp ("http://", + AH_backend_url, + strlen ("http://"))) + { + pfx = "taler+http://"; + hn = &AH_backend_url[strlen ("http://")]; + } + else + { + /* This invariant holds as per check in anastasis-httpd.c */ + GNUNET_assert (0); + } + /* This invariant holds as per check in anastasis-httpd.c */ + GNUNET_assert (0 != strlen (hn)); + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &gc->payment_identifier, + sizeof (gc->payment_identifier)); + GNUNET_asprintf (&hdr, + "%spay/%s%s/", + pfx, + hn, + order_id); + GNUNET_free (order_id); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Sending payment request `%s'\n", + hdr); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_TALER, + hdr)); + GNUNET_free (hdr); + } + gc->resp = resp; + gc->response_code = MHD_HTTP_PAYMENT_REQUIRED; +} + + +/** + * Callbacks of this type are used to serve the result of submitting a + * /contract request to a merchant. + * + * @param cls our `struct GetContext` + * @param por response details + */ +static void +proposal_cb (void *cls, + const struct TALER_MERCHANT_PostOrdersReply *por) +{ + struct GetContext *gc = cls; + enum GNUNET_DB_QueryStatus qs; + + gc->po = NULL; + GNUNET_assert (gc->in_list); + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + GNUNET_assert (gc->suspended); + MHD_resume_connection (gc->connection); + gc->suspended = false; + AH_trigger_daemon (NULL); + if (MHD_HTTP_OK != por->hr.http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Backend returned status %u/%d\n", + por->hr.http_status, + (int) por->hr.ec); + GNUNET_break (0); + gc->resp = TALER_MHD_make_json_pack ( + "{s:I, s:s, s:I, s:I, s:O?}", + "code", + (json_int_t) TALER_EC_ANASTASIS_TRUTH_PAYMENT_CREATE_BACKEND_ERROR, + "hint", + "Failed to setup order with merchant backend", + "backend-ec", + (json_int_t) por->hr.ec, + "backend-http-status", + (json_int_t) por->hr.http_status, + "backend-reply", + por->hr.reply); + GNUNET_assert (NULL != gc->resp); + gc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + } + qs = db->record_challenge_payment (db->cls, + &gc->truth_uuid, + &gc->payment_identifier, + &gc->challenge_cost); + if (0 >= qs) + { + GNUNET_break (0); + gc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, + "record challenge payment"); + gc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Setup fresh order, creating payment request\n"); + make_payment_request (gc); +} + + +/** + * Callback to process a GET /check-payment request + * + * @param cls our `struct GetContext` + * @param hr HTTP response details + * @param osr order status + */ +static void +check_payment_cb (void *cls, + const struct TALER_MERCHANT_HttpResponse *hr, + const struct TALER_MERCHANT_OrderStatusResponse *osr) + +{ + struct GetContext *gc = cls; + + gc->cpo = NULL; + GNUNET_assert (gc->in_list); + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + GNUNET_assert (gc->suspended); + MHD_resume_connection (gc->connection); + gc->suspended = false; + AH_trigger_daemon (NULL); + + switch (hr->http_status) + { + case MHD_HTTP_OK: + GNUNET_assert (NULL != osr); + break; + case MHD_HTTP_NOT_FOUND: + /* We created this order before, how can it be not found now? */ + GNUNET_break (0); + gc->resp = TALER_MHD_make_error (TALER_EC_ANASTASIS_TRUTH_ORDER_DISAPPEARED, + NULL); + gc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + case MHD_HTTP_BAD_GATEWAY: + gc->resp = TALER_MHD_make_error ( + TALER_EC_ANASTASIS_TRUTH_BACKEND_EXCHANGE_BAD, + NULL); + gc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + case MHD_HTTP_GATEWAY_TIMEOUT: + gc->resp = TALER_MHD_make_error (TALER_EC_ANASTASIS_GENERIC_BACKEND_TIMEOUT, + "Timeout check payment status"); + GNUNET_assert (NULL != gc->resp); + gc->response_code = MHD_HTTP_GATEWAY_TIMEOUT; + return; + default: + { + char status[14]; + + GNUNET_snprintf (status, + sizeof (status), + "%u", + hr->http_status); + gc->resp = TALER_MHD_make_error ( + TALER_EC_ANASTASIS_TRUTH_UNEXPECTED_PAYMENT_STATUS, + status); + GNUNET_assert (NULL != gc->resp); + gc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; + } + } + + switch (osr->status) + { + case TALER_MERCHANT_OSC_PAID: + { + enum GNUNET_DB_QueryStatus qs; + + qs = db->update_challenge_payment (db->cls, + &gc->truth_uuid, + &gc->payment_identifier); + if (0 <= qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order has been paid, continuing with request processing\n"); + return; /* continue as planned */ + } + GNUNET_break (0); + gc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, + "update challenge payment"); + gc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; /* continue as planned */ + } + case TALER_MERCHANT_OSC_CLAIMED: + case TALER_MERCHANT_OSC_UNPAID: + /* repeat payment request */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order remains unpaid, sending payment request again\n"); + make_payment_request (gc); + return; + } + /* should never get here */ + GNUNET_break (0); +} + + +/** + * Helper function used to ask our backend to begin processing a + * payment for the user's account. May perform asynchronous + * operations by suspending the connection if required. + * + * @param gc context to begin payment for. + * @return MHD status code + */ +static MHD_RESULT +begin_payment (struct GetContext *gc) +{ + enum GNUNET_DB_QueryStatus qs; + char *order_id; + + qs = db->lookup_challenge_payment (db->cls, + &gc->truth_uuid, + &gc->payment_identifier); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup challenge payment"); + } + GNUNET_assert (! gc->in_list); + gc->in_list = true; + GNUNET_CONTAINER_DLL_insert (gc_tail, + gc_head, + gc); + GNUNET_assert (! gc->suspended); + gc->suspended = true; + MHD_suspend_connection (gc->connection); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + /* We already created the order, check if it was paid */ + struct GNUNET_TIME_Relative timeout; + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &gc->payment_identifier, + sizeof (gc->payment_identifier)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order exists, checking payment status for order `%s'\n", + order_id); + timeout = GNUNET_TIME_absolute_get_remaining (gc->timeout); + gc->cpo = TALER_MERCHANT_merchant_order_get (AH_ctx, + AH_backend_url, + order_id, + NULL /* NOT session-bound */, + false, + timeout, + &check_payment_cb, + gc); + } + else + { + /* Create a fresh order */ + json_t *order; + struct GNUNET_TIME_Absolute pay_deadline; + + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, + &gc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP)); + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &gc->payment_identifier, + sizeof (gc->payment_identifier)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Creating fresh order `%s'\n", + order_id); + pay_deadline = GNUNET_TIME_relative_to_absolute ( + ANASTASIS_CHALLENGE_OFFER_LIFETIME); + GNUNET_TIME_round_abs (&pay_deadline); + order = json_pack ("{s:o, s:s, s:s, s:o, s:o}", + "amount", TALER_JSON_from_amount (&gc->challenge_cost), + "summary", "challenge fee for anastasis service", + "order_id", order_id, + "auto_refund", GNUNET_JSON_from_time_rel ( + AUTO_REFUND_TIMEOUT), + "pay_deadline", GNUNET_JSON_from_time_abs ( + pay_deadline)); + gc->po = TALER_MERCHANT_orders_post2 (AH_ctx, + AH_backend_url, + order, + AUTO_REFUND_TIMEOUT, + NULL, /* no payment target */ + 0, + NULL, /* no inventory products */ + 0, + NULL, /* no uuids */ + false, /* do NOT require claim token */ + &proposal_cb, + gc); + json_decref (order); + } + GNUNET_free (order_id); + AH_trigger_curl (); + return MHD_YES; +} + + +/** + * Load encrypted keyshare from db and return it to the client. + * + * @param truth_uuid UUID to the truth for the looup + * @param connection the connection to respond upon + * @return MHD status code + */ +static MHD_RESULT +return_key_share ( + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + struct MHD_Connection *connection) +{ + struct ANASTASIS_CRYPTO_EncryptedKeyShareP encrypted_keyshare; + + { + enum GNUNET_DB_QueryStatus qs; + + qs = db->get_key_share (db->cls, + truth_uuid, + &encrypted_keyshare); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get key share"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_ANASTASIS_TRUTH_KEY_SHARE_GONE, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning key share\n"); + { + struct MHD_Response *resp; + MHD_RESULT ret; + + resp = MHD_create_response_from_buffer (sizeof (encrypted_keyshare), + &encrypted_keyshare, + MHD_RESPMEM_MUST_COPY); + TALER_MHD_add_global_headers (resp); + ret = MHD_queue_response (connection, + MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + return ret; + } +} + + +/** + * Run the authorization method-specific 'process' function and continue + * based on its result with generating an HTTP response. + * + * @param connection the connection we are handling + * @param gc our overall handler context + */ +static MHD_RESULT +run_authorization_process (struct MHD_Connection *connection, + struct GetContext *gc) +{ + enum ANASTASIS_AUTHORIZATION_Result ret; + enum GNUNET_DB_QueryStatus qs; + + ret = gc->authorization->process (gc->as, + connection); + switch (ret) + { + case ANASTASIS_AUTHORIZATION_RES_SUCCESS: + /* Challenge sent successfully */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Authorization request sent successfully\n"); + qs = db->mark_challenge_sent (db->cls, + &gc->payment_identifier, + &gc->truth_uuid, + gc->code); + GNUNET_break (0 < qs); + gc->authorization->cleanup (gc->as); + gc->as = NULL; + return MHD_YES; + case ANASTASIS_AUTHORIZATION_RES_FAILED: + if (gc->payment_identifier_provided) + { + begin_refund (gc); + } + gc->authorization->cleanup (gc->as); + gc->as = NULL; + return MHD_YES; + case ANASTASIS_AUTHORIZATION_RES_SUSPENDED: + /* connection was suspended again, odd that this happens */ + return MHD_YES; + case ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED: + /* Challenge sent successfully */ + qs = db->mark_challenge_sent (db->cls, + &gc->payment_identifier, + &gc->truth_uuid, + gc->code); + GNUNET_break (0 < qs); + gc->authorization->cleanup (gc->as); + gc->as = NULL; + return MHD_NO; + case ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED: + gc->authorization->cleanup (gc->as); + gc->as = NULL; + return MHD_NO; + } + GNUNET_break (0); + return MHD_NO; +} + + +/** + * @param connection the MHD connection to handle + * @param url handles a URL of the format "/truth/$UUID[&response=$RESPONSE]" + * @param hc + * @return MHD result code + */ +MHD_RESULT +AH_handler_truth_get ( + struct MHD_Connection *connection, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + struct TM_HandlerContext *hc) +{ + struct GetContext *gc = hc->ctx; + struct GNUNET_HashCode challenge_response; + void *encrypted_truth; + size_t encrypted_truth_size; + void *decrypted_truth; + size_t decrypted_truth_size; + char *truth_mime = NULL; + bool is_question; + + if (NULL == gc) + { + /* Fresh request, do initial setup */ + gc = GNUNET_new (struct GetContext); + gc->hc = hc; + hc->ctx = gc; + gc->connection = connection; + gc->truth_uuid = *truth_uuid; + gc->hc->cc = &request_done; + { + const char *pay_id; + + pay_id = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER); + if (NULL != pay_id) + { + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + pay_id, + strlen (pay_id), + &gc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER); + } + gc->payment_identifier_provided = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client provided payment identifier `%s'\n", + pay_id); + } + } + + { + /* check if header contains Truth-Decryption-Key */ + const char *tdk; + + tdk = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + ANASTASIS_HTTP_HEADER_TRUTH_DECRYPTION_KEY); + if (NULL == tdk) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + ANASTASIS_HTTP_HEADER_TRUTH_DECRYPTION_KEY); + } + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data ( + tdk, + strlen (tdk), + &gc->truth_key, + sizeof (struct ANASTASIS_CRYPTO_TruthKeyP))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + ANASTASIS_HTTP_HEADER_TRUTH_DECRYPTION_KEY); + } + } + + { + const char *challenge_response_s; + + challenge_response_s = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "response"); + if ( (NULL != challenge_response_s) && + (GNUNET_OK != + GNUNET_CRYPTO_hash_from_string (challenge_response_s, + &challenge_response)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "response"); + } + gc->have_response = (NULL != challenge_response_s); + } + + { + const char *long_poll_timeout_ms; + + long_poll_timeout_ms = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "timeout_ms"); + if (NULL != long_poll_timeout_ms) + { + unsigned int timeout; + + if (1 != sscanf (long_poll_timeout_ms, + "%u", + &timeout)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "timeout_ms (must be non-negative number)"); + } + gc->timeout + = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply ( + GNUNET_TIME_UNIT_MILLISECONDS, + timeout)); + } + else + { + gc->timeout = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_UNIT_SECONDS); + } + } + + } /* end of first-time initialization (if NULL == gc) */ + else + { + if (NULL != gc->resp) + { + MHD_RESULT ret; + + /* We generated a response asynchronously, queue that */ + ret = MHD_queue_response (connection, + gc->response_code, + gc->resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (gc->resp); + gc->resp = NULL; + return ret; + } + if (NULL != gc->as) + { + /* Authorization process is "running", check what is going on */ + GNUNET_assert (NULL != gc->authorization); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Continuing with running the authorization process\n"); + return run_authorization_process (connection, + gc); + + } + /* We get here if the async check for payment said this request + was indeed paid! */ + } + + { + /* load encrypted truth from DB */ + enum GNUNET_DB_QueryStatus qs; + char *method; + + qs = db->get_escrow_challenge (db->cls, + &gc->truth_uuid, + &encrypted_truth, + &encrypted_truth_size, + &truth_mime, + &method); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get escrow challenge"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_ANASTASIS_TRUTH_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + is_question = (0 == strcmp ("question", + method)); + if (! is_question) + { + gc->authorization + = ANASTASIS_authorization_plugin_load (method, + AH_cfg, + &gc->challenge_cost); + if (NULL == gc->authorization) + { + MHD_RESULT ret; + + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_METHOD_NO_LONGER_SUPPORTED, + method); + GNUNET_free (encrypted_truth); + GNUNET_free (truth_mime); + GNUNET_free (method); + return ret; + } + } + else + { + gc->challenge_cost = AH_question_cost; + } + GNUNET_free (method); + } + + { + struct TALER_Amount zero_amount; + + TALER_amount_set_zero (AH_currency, + &zero_amount); + if (0 != TALER_amount_cmp (&gc->challenge_cost, + &zero_amount)) + { + /* Check database to see if the transaction is paid for */ + enum GNUNET_DB_QueryStatus qs; + bool paid; + + if (! gc->payment_identifier_provided) + { + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Beginning payment, client did not provide payment identifier\n"); + return begin_payment (gc); + } + qs = db->check_challenge_payment (db->cls, + &gc->payment_identifier, + &gc->truth_uuid, + &paid); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "check challenge payment"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* Create fresh payment identifier (cannot trust client) */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client-provided payment identifier is unknown.\n"); + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + return begin_payment (gc); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (! paid) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment identifier known. Checking payment with client's payment identifier\n"); + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + return begin_payment (gc); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment confirmed\n"); + break; + } + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Request is free of charge\n"); + } + } + + /* We've been paid, now validate response */ + { + /* decrypt encrypted_truth */ + ANASTASIS_CRYPTO_truth_decrypt (&gc->truth_key, + encrypted_truth, + encrypted_truth_size, + &decrypted_truth, + &decrypted_truth_size); + GNUNET_free (encrypted_truth); + } + if (NULL == decrypted_truth) + { + GNUNET_free (truth_mime); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_EXPECTATION_FAILED, + TALER_EC_ANASTASIS_TRUTH_DECRYPTION_FAILED, + NULL); + } + + /* Special case for secure question: we do not generate a numeric challenge, + but check that the hash matches */ + if (is_question) + { + if (! gc->have_response) + { + GNUNET_free (decrypted_truth); + GNUNET_free (truth_mime); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED, + NULL); + } + + { + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Absolute rt; + uint64_t code; + enum ANASTASIS_DB_CodeStatus cs; + struct GNUNET_HashCode hc; + + rt = GNUNET_TIME_UNIT_FOREVER_ABS; + qs = db->create_challenge_code (db->cls, + &gc->truth_uuid, + MAX_QUESTION_FREQ, + GNUNET_TIME_UNIT_HOURS, + INITIAL_RETRY_COUNTER, + &rt, + &code); + if (0 > qs) + { + GNUNET_break (0 < qs); + GNUNET_free (decrypted_truth); + GNUNET_free (truth_mime); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "create_challenge_code (for rate limiting)"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_free (decrypted_truth); + GNUNET_free (truth_mime); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_TOO_MANY_REQUESTS, + TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, + NULL); + } + /* decrement trial counter */ + ANASTASIS_hash_answer (code + 1, /* always use wrong answer */ + &hc); + cs = db->verify_challenge_code (db->cls, + &gc->truth_uuid, + &hc); + switch (cs) + { + case ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH: + /* good, what we wanted */ + break; + case ANASTASIS_DB_CODE_STATUS_HARD_ERROR: + case ANASTASIS_DB_CODE_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "verify_challenge_code"); + case ANASTASIS_DB_CODE_STATUS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_TOO_MANY_REQUESTS, + TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, + NULL); + case ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED: + /* this should be impossible, we used code+1 */ + GNUNET_assert (0); + } + } + if ( (decrypted_truth_size != sizeof (challenge_response)) || + (0 != memcmp (&challenge_response, + decrypted_truth, + decrypted_truth_size)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Wrong answer provided to secure question had %u bytes, wanted %u\n", + (unsigned int) decrypted_truth_size, + (unsigned int) sizeof (challenge_response)); + GNUNET_free (decrypted_truth); + GNUNET_free (truth_mime); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED, + NULL); + } + GNUNET_free (decrypted_truth); + GNUNET_free (truth_mime); + return return_key_share (&gc->truth_uuid, + connection); + } + + /* Not security question, check for answer in DB */ + if (gc->have_response) + { + enum ANASTASIS_DB_CodeStatus cs; + + GNUNET_free (decrypted_truth); + GNUNET_free (truth_mime); + cs = db->verify_challenge_code (db->cls, + &gc->truth_uuid, + &challenge_response); + switch (cs) + { + case ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Provided response does not match our stored challenge\n"); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED, + NULL); + case ANASTASIS_DB_CODE_STATUS_HARD_ERROR: + case ANASTASIS_DB_CODE_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "verify_challenge_code"); + case ANASTASIS_DB_CODE_STATUS_NO_RESULTS: + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "No challenge known (challenge is invalidated after %u requests)\n", + INITIAL_RETRY_COUNTER); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_TOO_MANY_REQUESTS, + TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, + NULL); + case ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED: + return return_key_share (&gc->truth_uuid, + connection); + } + GNUNET_break (0); + return MHD_NO; + } + + /* Not security question and no answer: use plugin to check if + decrypted truth is a valid challenge! */ + { + enum GNUNET_GenericReturnValue ret; + + ret = gc->authorization->validate (gc->authorization->cls, + connection, + truth_mime, + decrypted_truth, + decrypted_truth_size); + GNUNET_free (truth_mime); + switch (ret) + { + case GNUNET_OK: + /* data valid, continued below */ + break; + case GNUNET_NO: + /* data invalid, reply was queued */ + GNUNET_free (decrypted_truth); + return MHD_YES; + case GNUNET_SYSERR: + /* data invalid, reply was NOT queued */ + GNUNET_free (decrypted_truth); + return MHD_NO; + } + } + + /* Setup challenge and begin authorization process */ + { + struct GNUNET_TIME_Absolute transmission_date; + enum GNUNET_DB_QueryStatus qs; + + qs = db->create_challenge_code (db->cls, + &gc->truth_uuid, + gc->authorization->code_rotation_period, + gc->authorization->code_validity_period, + INITIAL_RETRY_COUNTER, + &transmission_date, + &gc->code); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + GNUNET_free (decrypted_truth); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "store_challenge_code"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* 0 == retry_counter of existing challenge => rate limit exceeded */ + GNUNET_free (decrypted_truth); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_TOO_MANY_REQUESTS, + TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* challenge code was stored successfully*/ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Created fresh challenge\n"); + break; + } + + if (GNUNET_TIME_absolute_get_duration (transmission_date).rel_value_us < + gc->authorization->code_retransmission_frequency.rel_value_us) + { + /* Too early for a retransmission! */ + GNUNET_free (decrypted_truth); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_ALREADY_REPORTED, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_ACTIVE, + NULL); + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Beginning authorization process\n"); + gc->as = gc->authorization->start (gc->authorization->cls, + &AH_trigger_daemon, + NULL, + &gc->truth_uuid, + gc->code, + decrypted_truth, + decrypted_truth_size); + GNUNET_free (decrypted_truth); + if (NULL == gc->as) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED, + NULL); + } + GNUNET_assert (! gc->in_list); + gc->in_list = true; + GNUNET_CONTAINER_DLL_insert (gc_head, + gc_tail, + gc); + return run_authorization_process (connection, + gc); +} diff --git a/src/backend/anastasis-httpd_truth.h b/src/backend/anastasis-httpd_truth.h new file mode 100644 index 0000000..7a1b95f --- /dev/null +++ b/src/backend/anastasis-httpd_truth.h @@ -0,0 +1,75 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015, 2016, 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 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 anastasis-httpd_truth.h + * @brief functions to handle incoming requests on /truth + * @author Dennis Neufeld + * @author Dominik Meister + * @author Christian Grothoff + */ +#ifndef ANASTASIS_HTTPD_TRUTH_H +#define ANASTASIS_HTTPD_TRUTH_H +#include + + +/** + * Prepare all active GET truth requests for system shutdown. + */ +void +AH_truth_shutdown (void); + + +/** + * Prepare all active POST truth requests for system shutdown. + */ +void +AH_truth_upload_shutdown (void); + + +/** + * Handle a GET to /truth/$UUID + * + * @param connection the MHD connection to handle + * @param truth_uuid the truth UUID + * @param con_cls + * @return MHD result code + */ +MHD_RESULT +AH_handler_truth_get ( + struct MHD_Connection *connection, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + struct TM_HandlerContext *hc); + + +/** + * Handle a POST to /truth/$UUID. + * + * @param connection the MHD connection to handle + * @param con_cls the connection's closure + * @param truth_uuid the truth UUID + * @param truth_data truth data + * @param truth_data_size number of bytes (left) in @a truth_data + * @return MHD result code + */ +int +AH_handler_truth_post ( + struct MHD_Connection *connection, + struct TM_HandlerContext *hc, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const char *truth_data, + size_t *truth_data_size); + +#endif diff --git a/src/backend/anastasis-httpd_truth_upload.c b/src/backend/anastasis-httpd_truth_upload.c new file mode 100644 index 0000000..9767087 --- /dev/null +++ b/src/backend/anastasis-httpd_truth_upload.c @@ -0,0 +1,855 @@ +/* + This file is part of TALER + Copyright (C) 2019, 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 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 anastasis-httpd_truth_upload.c + * @brief functions to handle incoming POST request on /truth + * @author Dennis Neufeld + * @author Dominik Meister + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis-httpd.h" +#include "anastasis_service.h" +#include "anastasis-httpd_truth.h" +#include +#include +#include +#include +#include +#include "anastasis_authorization_lib.h" + + +/** + * Information we track per truth upload. + */ +struct TruthUploadContext +{ + + /** + * UUID of the truth object we are processing. + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct TruthUploadContext *next; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct TruthUploadContext *prev; + + /** + * Used while we are awaiting proposal creation. + */ + struct TALER_MERCHANT_PostOrdersHandle *po; + + /** + * Used while we are waiting payment. + */ + struct TALER_MERCHANT_OrderMerchantGetHandle *cpo; + + /** + * Post parser context. + */ + void *post_ctx; + + /** + * Handle to the client request. + */ + struct MHD_Connection *connection; + + /** + * Incoming JSON, NULL if not yet available. + */ + json_t *json; + + /** + * HTTP response code to use on resume, if non-NULL. + */ + struct MHD_Response *resp; + + /** + * When should this request time out? + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * Fee that is to be paid for this upload. + */ + struct TALER_Amount upload_fee; + + /** + * HTTP response code to use on resume, if resp is set. + */ + unsigned int response_code; + + /** + * For how many years must the customer still pay? + */ + unsigned int years_to_pay; + +}; + + +/** + * Head of linked list over all truth upload processes + */ +static struct TruthUploadContext *tuc_head; + +/** + * Tail of linked list over all truth upload processes + */ +static struct TruthUploadContext *tuc_tail; + + +void +AH_truth_upload_shutdown (void) +{ + struct TruthUploadContext *tuc; + + while (NULL != (tuc = tuc_head)) + { + GNUNET_CONTAINER_DLL_remove (tuc_head, + tuc_tail, + tuc); + if (NULL != tuc->cpo) + { + TALER_MERCHANT_merchant_order_get_cancel (tuc->cpo); + tuc->cpo = NULL; + } + if (NULL != tuc->po) + { + TALER_MERCHANT_orders_post_cancel (tuc->po); + tuc->po = NULL; + } + MHD_resume_connection (tuc->connection); + } +} + + +/** + * Function called to clean up a `struct TruthUploadContext`. + * + * @param hc general handler context + */ +static void +cleanup_truth_post (struct TM_HandlerContext *hc) +{ + struct TruthUploadContext *tuc = hc->ctx; + + TALER_MHD_parse_post_cleanup_callback (tuc->post_ctx); + if (NULL != tuc->po) + TALER_MERCHANT_orders_post_cancel (tuc->po); + if (NULL != tuc->cpo) + TALER_MERCHANT_merchant_order_get_cancel (tuc->cpo); + if (NULL != tuc->resp) + MHD_destroy_response (tuc->resp); + if (NULL != tuc->json) + json_decref (tuc->json); + GNUNET_free (tuc); +} + + +/** + * Transmit a payment request for @a tuc. + * + * @param tuc upload context to generate payment request for + */ +static void +make_payment_request (struct TruthUploadContext *tuc) +{ + struct MHD_Response *resp; + + /* request payment via Taler */ + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + GNUNET_assert (NULL != resp); + TALER_MHD_add_global_headers (resp); + { + char *hdr; + const char *pfx; + const char *hn; + + if (0 == strncasecmp ("https://", + AH_backend_url, + strlen ("https://"))) + { + pfx = "taler://"; + hn = &AH_backend_url[strlen ("https://")]; + } + else if (0 == strncasecmp ("http://", + AH_backend_url, + strlen ("http://"))) + { + pfx = "taler+http://"; + hn = &AH_backend_url[strlen ("http://")]; + } + else + { + /* This invariant holds as per check in anastasis-httpd.c */ + GNUNET_assert (0); + } + /* This invariant holds as per check in anastasis-httpd.c */ + GNUNET_assert (0 != strlen (hn)); + { + char *order_id; + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &tuc->truth_uuid, + sizeof (tuc->truth_uuid)); + GNUNET_asprintf (&hdr, + "%spay/%s%s/", + pfx, + hn, + order_id); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Returning %u %s\n", + MHD_HTTP_PAYMENT_REQUIRED, + order_id); + GNUNET_free (order_id); + } + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_TALER, + hdr)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "TRUTH payment request made: %s\n", + hdr); + GNUNET_free (hdr); + } + tuc->resp = resp; + tuc->response_code = MHD_HTTP_PAYMENT_REQUIRED; +} + + +/** + * Callbacks of this type are used to serve the result of submitting a + * POST /private/orders request to a merchant. + * + * @param cls our `struct TruthUploadContext` + * @param por response details + */ +static void +proposal_cb (void *cls, + const struct TALER_MERCHANT_PostOrdersReply *por) +{ + struct TruthUploadContext *tuc = cls; + + tuc->po = NULL; + GNUNET_CONTAINER_DLL_remove (tuc_head, + tuc_tail, + tuc); + MHD_resume_connection (tuc->connection); + AH_trigger_daemon (NULL); + if (MHD_HTTP_OK != por->hr.http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Backend returned status %u/%d\n", + por->hr.http_status, + (int) por->hr.ec); + GNUNET_break (0); + tuc->resp = TALER_MHD_make_json_pack ( + "{s:I, s:s, s:I, s:I, s:O?}", + "code", + (json_int_t) TALER_EC_ANASTASIS_GENERIC_ORDER_CREATE_BACKEND_ERROR, + "hint", + "Failed to setup order with merchant backend", + "backend-ec", + (json_int_t) por->hr.ec, + "backend-http-status", + (json_int_t) por->hr.http_status, + "backend-reply", + por->hr.reply); + GNUNET_assert (NULL != tuc->resp); + tuc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + } + make_payment_request (tuc); +} + + +/** + * Callback to process a GET /check-payment request + * + * @param cls our `struct PolicyUploadContext` + * @param hr HTTP response details + * @param osr order status + */ +static void +check_payment_cb (void *cls, + const struct TALER_MERCHANT_HttpResponse *hr, + const struct TALER_MERCHANT_OrderStatusResponse *osr) +{ + struct TruthUploadContext *tuc = cls; + + tuc->cpo = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Checking backend order status returned %u\n", + hr->http_status); + switch (hr->http_status) + { + case 0: + /* Likely timeout, complain! */ + tuc->response_code = MHD_HTTP_GATEWAY_TIMEOUT; + tuc->resp = TALER_MHD_make_error ( + TALER_EC_ANASTASIS_GENERIC_BACKEND_TIMEOUT, + NULL); + break; + case MHD_HTTP_OK: + switch (osr->status) + { + case TALER_MERCHANT_OSC_PAID: + { + enum GNUNET_DB_QueryStatus qs; + unsigned int years; + struct GNUNET_TIME_Relative paid_until; + const json_t *contract; + struct TALER_Amount amount; + struct GNUNET_JSON_Specification cspec[] = { + TALER_JSON_spec_amount ("amount", + AH_currency, + &amount), + GNUNET_JSON_spec_end () + }; + + contract = osr->details.paid.contract_terms; + if (GNUNET_OK != + GNUNET_JSON_parse (contract, + cspec, + NULL, NULL)) + { + GNUNET_break (0); + tuc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + tuc->resp = TALER_MHD_make_error ( + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + "contract terms in database are malformed"); + break; + } + years = TALER_amount_divide2 (&amount, + &AH_truth_upload_fee); + paid_until = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, + years); + /* add 1 week grace period, otherwise if a user + wants to pay for 1 year, the first seconds + would have passed between making the payment + and our subsequent check if +1 year was + paid... So we actually say 1 year = 52 weeks + on the server, while the client calculates + with 365 days. */ + paid_until = GNUNET_TIME_relative_add (paid_until, + GNUNET_TIME_UNIT_WEEKS); + qs = db->record_truth_upload_payment ( + db->cls, + &tuc->truth_uuid, + &osr->details.paid.deposit_total, + paid_until); + if (qs <= 0) + { + GNUNET_break (0); + tuc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + tuc->resp = TALER_MHD_make_error ( + TALER_EC_GENERIC_DB_STORE_FAILED, + "record_truth_upload_payment"); + break; + } + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Payment confirmed, resuming upload\n"); + break; + case TALER_MERCHANT_OSC_UNPAID: + case TALER_MERCHANT_OSC_CLAIMED: + make_payment_request (tuc); + break; + } + break; + case MHD_HTTP_UNAUTHORIZED: + /* Configuration issue, complain! */ + tuc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + tuc->resp = TALER_MHD_make_json_pack ( + "{s:I, s:s, s:I, s:I, s:O?}", + "code", + (json_int_t) TALER_EC_ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED, + "hint", + TALER_ErrorCode_get_hint ( + TALER_EC_ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED), + "backend-ec", + (json_int_t) hr->ec, + "backend-http-status", + (json_int_t) hr->http_status, + "backend-reply", + hr->reply); + GNUNET_assert (NULL != tuc->resp); + break; + case MHD_HTTP_NOT_FOUND: + /* Setup fresh order */ + { + char *order_id; + json_t *order; + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &tuc->truth_uuid, + sizeof(tuc->truth_uuid)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "%u, setting up fresh order %s\n", + MHD_HTTP_NOT_FOUND, + order_id); + order = json_pack ("{s:o, s:s, s:[{s:s,s:I,s:s}], s:s}", + "amount", + TALER_JSON_from_amount (&tuc->upload_fee), + "summary", + "Anastasis challenge storage fee", + "products", + "description", "challenge storage fee", + "quantity", (json_int_t) tuc->years_to_pay, + "unit", "years", + + "order_id", + order_id); + GNUNET_free (order_id); + tuc->po = TALER_MERCHANT_orders_post2 (AH_ctx, + AH_backend_url, + order, + GNUNET_TIME_UNIT_ZERO, + NULL, /* no payment target */ + 0, + NULL, /* no inventory products */ + 0, + NULL, /* no uuids */ + false, /* do NOT require claim token */ + &proposal_cb, + tuc); + AH_trigger_curl (); + json_decref (order); + return; + } + default: + /* Unexpected backend response */ + tuc->response_code = MHD_HTTP_BAD_GATEWAY; + tuc->resp = TALER_MHD_make_json_pack ( + "{s:I, s:s, s:I, s:I, s:O?}", + "code", + (json_int_t) TALER_EC_ANASTASIS_GENERIC_BACKEND_ERROR, + "hint", + TALER_ErrorCode_get_hint (TALER_EC_ANASTASIS_GENERIC_BACKEND_ERROR), + "backend-ec", + (json_int_t) hr->ec, + "backend-http-status", + (json_int_t) hr->http_status, + "backend-reply", + hr->reply); + break; + } + GNUNET_CONTAINER_DLL_remove (tuc_head, + tuc_tail, + tuc); + MHD_resume_connection (tuc->connection); + AH_trigger_daemon (NULL); +} + + +/** + * Helper function used to ask our backend to begin processing a + * payment for the truth upload. May perform asynchronous operations + * by suspending the connection if required. + * + * @param tuc context to begin payment for. + * @return MHD status code + */ +static MHD_RESULT +begin_payment (struct TruthUploadContext *tuc) +{ + char *order_id; + struct GNUNET_TIME_Relative timeout; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Checking backend order status...\n"); + timeout = GNUNET_TIME_absolute_get_remaining (tuc->timeout); + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &tuc->truth_uuid, + sizeof (tuc->truth_uuid)); + tuc->cpo = TALER_MERCHANT_merchant_order_get (AH_ctx, + AH_backend_url, + order_id, + NULL /* our payments are NOT session-bound */, + false, + timeout, + &check_payment_cb, + tuc); + GNUNET_free (order_id); + if (NULL == tuc->cpo) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (tuc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_GENERIC_PAYMENT_CHECK_START_FAILED, + "Could not check order status"); + } + GNUNET_CONTAINER_DLL_insert (tuc_head, + tuc_tail, + tuc); + MHD_suspend_connection (tuc->connection); + return MHD_YES; +} + + +int +AH_handler_truth_post ( + struct MHD_Connection *connection, + struct TM_HandlerContext *hc, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const char *truth_data, + size_t *truth_data_size) +{ + struct TruthUploadContext *tuc = hc->ctx; + MHD_RESULT ret; + int res; + struct ANASTASIS_CRYPTO_EncryptedKeyShareP keyshare_data; + void *encrypted_truth; + size_t encrypted_truth_size; + const char *truth_mime; + const char *type; + enum GNUNET_DB_QueryStatus qs; + uint32_t storage_years; + struct GNUNET_TIME_Absolute paid_until; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("keyshare_data", + &keyshare_data), + GNUNET_JSON_spec_string ("type", + &type), + GNUNET_JSON_spec_varsize ("encrypted_truth", + &encrypted_truth, + &encrypted_truth_size), + GNUNET_JSON_spec_string ("truth_mime", + &truth_mime), + GNUNET_JSON_spec_uint32 ("storage_duration_years", + &storage_years), + GNUNET_JSON_spec_end () + }; + + if (NULL == tuc) + { + tuc = GNUNET_new (struct TruthUploadContext); + tuc->connection = connection; + tuc->truth_uuid = *truth_uuid; + hc->ctx = tuc; + hc->cc = &cleanup_truth_post; + + /* check for excessive upload */ + { + const char *lens; + unsigned long len; + char dummy; + + lens = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_CONTENT_LENGTH); + if ( (NULL == lens) || + (1 != sscanf (lens, + "%lu%c", + &len, + &dummy)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + (NULL == lens) + ? TALER_EC_ANASTASIS_GENERIC_MISSING_CONTENT_LENGTH + : TALER_EC_ANASTASIS_GENERIC_MALFORMED_CONTENT_LENGTH, + NULL); + } + if (len / 1024 / 1024 >= AH_upload_limit_mb) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_PAYLOAD_TOO_LARGE, + TALER_EC_SYNC_MALFORMED_CONTENT_LENGTH, + "Content-length value not acceptable"); + } + } + + { + const char *long_poll_timeout_ms; + + long_poll_timeout_ms = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "timeout_ms"); + if (NULL != long_poll_timeout_ms) + { + unsigned int timeout; + + if (1 != sscanf (long_poll_timeout_ms, + "%u", + &timeout)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "timeout_ms (must be non-negative number)"); + } + tuc->timeout + = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply ( + GNUNET_TIME_UNIT_MILLISECONDS, + timeout)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Long polling for %u ms enabled\n", + timeout); + } + else + { + tuc->timeout = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_UNIT_SECONDS); + } + } + + } /* end 'if (NULL == tuc)' */ + + if (NULL != tuc->resp) + { + /* We generated a response asynchronously, queue that */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Returning asynchronously generated response with HTTP status %u\n", + tuc->response_code); + ret = MHD_queue_response (connection, + tuc->response_code, + tuc->resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (tuc->resp); + tuc->resp = NULL; + return ret; + } + + if (NULL == tuc->json) + { + res = TALER_MHD_parse_post_json (connection, + &tuc->post_ctx, + truth_data, + truth_data_size, + &tuc->json); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; + } + if ( (GNUNET_NO == res) || + (NULL == tuc->json) ) + return MHD_YES; + } + res = TALER_MHD_parse_json_data (connection, + tuc->json, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + + /* check method is supported */ + { + struct TALER_Amount dummy; + + if ( (0 != strcmp ("question", + type)) && + (NULL == + ANASTASIS_authorization_plugin_load (type, + AH_cfg, + &dummy)) ) + { + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ANASTASIS_TRUTH_UPLOAD_METHOD_NOT_SUPPORTED, + type); + } + } + + if (storage_years > ANASTASIS_MAX_YEARS_STORAGE) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "storage_duration_years"); + } + if (0 == storage_years) + storage_years = 1; + + { + struct TALER_Amount zero_amount; + + TALER_amount_set_zero (AH_currency, + &zero_amount); + if (0 != TALER_amount_cmp (&AH_truth_upload_fee, + &zero_amount)) + { + struct GNUNET_TIME_Absolute desired_until; + enum GNUNET_DB_QueryStatus qs; + + desired_until + = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, + storage_years)); + qs = db->check_truth_upload_paid (db->cls, + truth_uuid, + &paid_until); + if (qs < 0) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + if ( (0 == qs) || + (paid_until.abs_value_us < desired_until.abs_value_us) ) + { + struct GNUNET_TIME_Absolute now; + struct GNUNET_TIME_Relative rem; + + now = GNUNET_TIME_absolute_get (); + if (paid_until.abs_value_us < now.abs_value_us) + paid_until = now; + rem = GNUNET_TIME_absolute_get_difference (paid_until, + desired_until); + tuc->years_to_pay = rem.rel_value_us + / GNUNET_TIME_UNIT_YEARS.rel_value_us; + if (0 != (rem.rel_value_us % GNUNET_TIME_UNIT_YEARS.rel_value_us)) + tuc->years_to_pay++; + if (0 > + TALER_amount_multiply (&tuc->upload_fee, + &AH_truth_upload_fee, + tuc->years_to_pay)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "storage_duration_years"); + } + if ( (0 != tuc->upload_fee.fraction) || + (0 != tuc->upload_fee.value) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Truth upload payment required (%d)!\n", + qs); + return begin_payment (tuc); + } + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "TRUTH paid until %s (%d)!\n", + GNUNET_STRINGS_relative_time_to_string ( + GNUNET_TIME_absolute_get_remaining ( + paid_until), + GNUNET_YES), + qs); + } + else + { + paid_until + = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, + ANASTASIS_MAX_YEARS_STORAGE)); + } + } + + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Storing truth until %s!\n", + GNUNET_STRINGS_absolute_time_to_string (paid_until)); + qs = db->store_truth (db->cls, + truth_uuid, + &keyshare_data, + truth_mime, + encrypted_truth, + encrypted_truth_size, + type, + GNUNET_TIME_absolute_get_remaining (paid_until)); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "store_truth"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + { + void *xtruth; + size_t xtruth_size; + char *xtruth_mime; + char *xmethod; + bool ok = false; + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + db->get_escrow_challenge (db->cls, + truth_uuid, + &xtruth, + &xtruth_size, + &xtruth_mime, + &xmethod)) + { + ok = ( (xtruth_size == encrypted_truth_size) && + (0 == strcmp (xmethod, + type)) && + (0 == strcmp (truth_mime, + xtruth_mime)) && + (0 == memcmp (xtruth, + encrypted_truth, + xtruth_size)) ); + GNUNET_free (encrypted_truth); + GNUNET_free (xtruth_mime); + GNUNET_free (xmethod); + } + if (! ok) + { + GNUNET_JSON_parse_free (spec); + + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_ANASTASIS_TRUTH_UPLOAD_UUID_EXISTS, + NULL); + } + /* idempotency detected, intentional fall through! */ + } + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + { + struct MHD_Response *resp; + + GNUNET_JSON_parse_free (spec); + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + TALER_MHD_add_global_headers (resp); + ret = MHD_queue_response (connection, + MHD_HTTP_NO_CONTENT, + resp); + MHD_destroy_response (resp); + GNUNET_break (MHD_YES == ret); + return ret; + } + } + GNUNET_JSON_parse_free (spec); + GNUNET_break (0); + return MHD_NO; +} diff --git a/src/backend/anastasis.conf b/src/backend/anastasis.conf new file mode 100644 index 0000000..ddc1a65 --- /dev/null +++ b/src/backend/anastasis.conf @@ -0,0 +1,77 @@ +# This file is in the public domain. + +# These are default/sample settings for a merchant backend. + + +# General settings for the backend. +[anastasis] + +# Use TCP or UNIX domain sockets? +SERVE = tcp + +# Which HTTP port does the backend listen on? Only used if "SERVE" is 'tcp'. +PORT = 9977 + +# Which IP address should we bind to? i.e. 127.0.0.1 or ::1 for loopback. +# Can also be given as a hostname. We will bind to the wildcard (dual-stack) +# if left empty. Only used if "SERVE" is 'tcp'. +# BIND_TO = + + +# Which unix domain path should we bind to? Only used if "SERVE" is 'unix'. +UNIXPATH = ${ANASTASIS_RUNTIME_DIR}/backend.http +# What should be the file access permissions (see chmod) for "UNIXPATH"? +UNIXPATH_MODE = 660 + +# Which database backend do we use? +DB = postgres + +# Annual fee for an account +# ANNUAL_FEE = TESTKUDOS:0.1 + +# Number of policy uploads included in one annual fee payment +ANNUAL_POLICY_UPLĂ„OAD_LIMIT = 64 + +# Insurance +# INSURANCE = TESTKUDOS:1.0 + +# Upload limit per backup, in megabytes +UPLOAD_LIMIT_MB = 16 + +# Authentication costs + +# Cost of authentication by question +#QUESTION_COST = EUR:0 + +# Cost of authentication by file (only for testing purposes) +#FILE_COST = EUR:1 + +# Cost of authentication by E-Mail +#EMAIL_COST = EUR:0 + +# Cost of authentication by SMS +#SMS_COST = EUR:0 + +# Cost of authentication by postal +#POSTAL_COST = EUR:0 + +# Cost of authentication by video +#VIDEO_COST = EUR:0 + +#SMS authentication command which is executed +#SMSAUTH_COMMAND = some_sms_script.sh + +#E-Mail authentication command which is executed +#EMAILAUTH_COMMAND = some_email_script.sh + +# Fulfillment URL of the ANASTASIS service itself. +FULFILLMENT_URL = taler://fulfillment-success + +# Base URL of our payment backend +# PAYMENT_BACKEND_URL = http://localhost:9976/ + +# Server salt 16 Byte +# SERVER_SALT = gUfO1KGOKYIFlFQg + +# Supported methods +SUPPORTED_METHODS = question -- cgit v1.2.3