sync

Backup service to store encrypted wallet databases (experimental)
Log | Files | Refs | Submodules | README | LICENSE

commit f0752580e0a1fb8ae2afa25ee12fad2bf594294b
parent 1086d6223ca6b400ca2f9313a71bea6e2d1d6b9e
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun,  2 Mar 2025 03:06:54 +0100

work on MHD2 port

Diffstat:
Msrc/sync/Makefile.am | 25+++++++++++++++++++++++++
Asrc/sync/sync-httpd2.c | 587+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2.h | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_backup-post.c | 1036+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_backup.c | 247+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_backup.h | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_config.c | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_config.h | 39+++++++++++++++++++++++++++++++++++++++
8 files changed, 2257 insertions(+), 0 deletions(-)

diff --git a/src/sync/Makefile.am b/src/sync/Makefile.am @@ -14,6 +14,12 @@ pkgcfg_DATA = \ bin_PROGRAMS = \ sync-httpd +if HAVE_MHD2 +# Experimental port to libmicrohttpd2 +bin_PROGRAMS += \ + sync-httpd2 +endif + sync_httpd_SOURCES = \ sync-httpd.c sync-httpd.h \ sync-httpd_backup.c sync-httpd_backup.h \ @@ -34,5 +40,24 @@ sync_httpd_LDADD = \ -lgnunetutil \ $(XLIB) +sync_httpd2_SOURCES = \ + sync-httpd2.c sync-httpd2.h \ + sync-httpd2_backup.c sync-httpd2_backup.h \ + sync-httpd2_backup-post.c \ + sync-httpd2_config.c sync-httpd2_config.h +sync_httpd2_LDADD = \ + $(top_builddir)/src/util/libsyncutil.la \ + $(top_builddir)/src/syncdb/libsyncdb.la \ + -lmicrohttpd2 \ + -ljansson \ + -ltalermerchant \ + -ltalermhd2 \ + -ltalerjson \ + -ltalerutil \ + -lgnunetcurl \ + -lgnunetjson \ + -lgnunetutil \ + $(XLIB) + EXTRA_DIST = \ $(pkgcfg_DATA) diff --git a/src/sync/sync-httpd2.c b/src/sync/sync-httpd2.c @@ -0,0 +1,587 @@ +/* + This file is part of TALER + (C) 2019--2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file sync/sync-httpd.c + * @brief HTTP serving layer intended to provide basic backup operations + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include "sync_util.h" +#include "sync-httpd2.h" +#include "sync_database_lib.h" +#include "sync-httpd2_backup.h" +#include "sync-httpd2_config.h" + +/** + * Backlog for listen operation on unix-domain sockets. + */ +#define UNIX_BACKLOG 500 + + +/** + * Should a "Connection: close" header be added to each HTTP response? + */ +static int SH_sync_connection_close; + +/** + * Upload limit to the service, in megabytes. + */ +unsigned long long int SH_upload_limit_mb; + +/** + * Annual fee for the backup account. + */ +struct TALER_Amount SH_annual_fee; + +/** + * Our Taler backend to process payments. + */ +char *SH_backend_url; + +/** + * Our fulfillment URL. + */ +char *SH_fulfillment_url; + +/** + * Our context for making HTTP requests. + */ +struct GNUNET_CURL_Context *SH_ctx; + +/** + * Reschedule context for #SH_ctx. + */ +static struct GNUNET_CURL_RescheduleContext *rc; + +/** + * Global return code + */ +static int global_ret; + +/** + * Connection handle to the our database + */ +struct SYNC_DatabasePlugin *db; + +/** + * 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; + +/** + * Amount of insurance. + */ +struct TALER_Amount SH_insurance; + + +/** + * Function to respond to GET requests on '/'. + * + * @param request the MHD request to handle + * @param upload_size number of bytes uploaded + * @return MHD action + */ +static const struct MHD_Action * +respond_root (struct MHD_Request *request, + uint_fast64_t upload_size) +{ + const char *msg = "Hello, I'm sync. This HTTP server is not for humans.\n"; + struct MHD_Response *resp; + + GNUNET_break_op (0 == upload_size); + resp = MHD_response_from_buffer_static ( + MHD_HTTP_STATUS_OK, + strlen (msg), + msg); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/plain")); + return MHD_action_from_response (request, + resp); +} + + +/** + * Function to respond to GET requests on unknown URLs. + * + * @param request the MHD request to handle + * @param upload_size number of bytes uploaded + * @return MHD action + */ +static const struct MHD_Action * +respond_404 (struct MHD_Request *request, + uint_fast64_t upload_size) +{ + const char *msg = "<html><title>404: not found</title></html>"; + struct MHD_Response *resp; + + GNUNET_break_op (0 == upload_size); + resp = MHD_response_from_buffer_static ( + MHD_HTTP_STATUS_NOT_FOUND, + strlen (msg), + msg); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/html")); + return MHD_action_from_response (request, + resp); +} + + +/** + * Function to respond to GET requests on '/agpl'. + * + * @param request the MHD request to handle + * @param upload_size number of bytes uploaded + * @return MHD action + */ +static const struct MHD_Action * +respond_agpl (struct MHD_Request *request, + uint_fast64_t upload_size) +{ + GNUNET_break_op (0 == upload_size); + return TALER_MHD2_reply_agpl (request, + "https://git.taler.net/sync.git/"); +} + + +/** + * 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. + * + * @param cls argument given together with the function + * pointer when the handler was registered with MHD + * @param request request the request to handle + * @param path the requested uri (without arguments after "?") + * @param method the HTTP method used (#MHD_HTTP_METHOD_GET, + * #MHD_HTTP_METHOD_PUT, etc.) + * @param upload_size the size of the message upload content payload, + * #MHD_SIZE_UNKNOWN for chunked uploads (if the + * final chunk has not been processed yet) + * @return next action + */ +static const struct MHD_Action * +url_handler (void *cls, + struct MHD_Request *request, + const struct MHD_String *path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size) +{ + static struct SH_RequestHandler handlers[] = { + /* Landing page, tell humans to go away. */ + { + .url = "/", + .method = MHD_HTTP_METHOD_GET, + .handler = &respond_root + }, + { + .url = "/agpl", + .method = MHD_HTTP_METHOD_GET, + .handler = &respond_agpl, + }, + { + .url = "/config", + .method = MHD_HTTP_METHOD_GET, + .handler = &SH_handler_config, + }, + { + .url = NULL + } + }; + struct SYNC_AccountPublicKeyP account_pub; + + (void) cls; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling %s request for URL '%s'\n", + MHD_http_method_to_string (method)->cstr, + path->cstr); + if (0 == strncmp (path->cstr, + "/backups/", + strlen ("/backups/"))) + { + const char *ac = &path->cstr[strlen ("/backups/")]; + + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_public_key_from_string (ac, + strlen (ac), + &account_pub.eddsa_pub)) + { + GNUNET_break_op (0); + return TALER_MHD2_reply_with_error (request, + MHD_HTTP_STATUS_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + ac); + } + if (MHD_HTTP_METHOD_OPTIONS == method) + { + return TALER_MHD2_reply_cors_preflight (request); + } + if (MHD_HTTP_METHOD_GET == method) + { + return SH_backup_get (request, + &account_pub); + } + if (MHD_HTTP_METHOD_POST == method) + { + return SH_backup_post (request, + &account_pub, + upload_size); + } + // FIXME: return bad method! + } + for (unsigned int i = 0; NULL != handlers[i].url; i++) + { + struct SH_RequestHandler *rh = &handlers[i]; + + if (0 == strcmp (path->cstr, + rh->url)) + { + if (MHD_HTTP_METHOD_OPTIONS == method) + { + return TALER_MHD2_reply_cors_preflight (request); + } + if (rh->method == method) + { + return rh->handler (request, + upload_size); + } + } + } + return respond_404 (request, + upload_size); +} + + +/** + * Shutdown task. Invoked when the application is being terminated. + * + * @param cls NULL + */ +static void +do_shutdown (void *cls) +{ + struct MHD_Daemon *mhd; + + (void) cls; + SH_resume_all_bc (); + if (NULL != SH_ctx) + { + GNUNET_CURL_fini (SH_ctx); + SH_ctx = NULL; + } + if (NULL != rc) + { + GNUNET_CURL_gnunet_rc_destroy (rc); + rc = NULL; + } + mhd = TALER_MHD2_daemon_stop (); + if (NULL != mhd) + { + MHD_daemon_destroy (mhd); + mhd = NULL; + } + if (NULL != db) + { + SYNC_DB_plugin_unload (db); + db = NULL; + } +} + + +/** + * Kick MHD to run now, to be called after MHD_request_resume(). + * 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. + */ +// FIXME: replace with direct call... +void +SH_trigger_daemon () +{ + TALER_MHD2_daemon_trigger (); +} + + +/** + * Kick GNUnet Curl scheduler to begin curl interactions. + */ +void +SH_trigger_curl () +{ + GNUNET_CURL_gnunet_scheduler_reschedule (&rc); +} + + +/** + * Main function that will be run by the scheduler. + * + * @param cls closure + * @param args remaining command-line arguments + * @param cfgfile name of the configuration file used (for saving, can be + * NULL!) + * @param config configuration + */ +static void +run (void *cls, + char *const *args, + const char *cfgfile, + const struct GNUNET_CONFIGURATION_Handle *config) +{ + int fh; + enum TALER_MHD2_GlobalOptions go; + uint16_t port; + struct MHD_Daemon *mhd; + + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Starting sync-httpd\n"); + go = TALER_MHD2_GO_NONE; + if (SH_sync_connection_close) + go |= TALER_MHD2_GO_FORCE_CONNECTION_CLOSE; + TALER_MHD2_setup (go); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_add_shutdown (&do_shutdown, + NULL); + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_number (config, + "sync", + "UPLOAD_LIMIT_MB", + &SH_upload_limit_mb)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "sync", + "UPLOAD_LIMIT_MB"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_config_get_amount (config, + "sync", + "INSURANCE", + &SH_insurance)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "sync", + "INSURANCE"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_config_get_amount (config, + "sync", + "ANNUAL_FEE", + &SH_annual_fee)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "sync", + "ANNUAL_FEE"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (config, + "sync", + "PAYMENT_BACKEND_URL", + &SH_backend_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "sync", + "PAYMENT_BACKEND_URL"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (config, + "sync", + "FULFILLMENT_URL", + &SH_fulfillment_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "sync", + "BASE_URL"); + GNUNET_SCHEDULER_shutdown (); + return; + } + + /* setup HTTP client event loop */ + SH_ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, + &rc); + rc = GNUNET_CURL_gnunet_rc_create (SH_ctx); + if (NULL != userpass) + GNUNET_CURL_set_userpass (SH_ctx, + userpass); + if (NULL != keyfile) + GNUNET_CURL_set_tlscert (SH_ctx, + certtype, + certfile, + keyfile, + keypass); + if (GNUNET_OK == + GNUNET_CONFIGURATION_get_value_string (config, + "sync", + "API_KEY", + &apikey)) + { + char *auth_header; + + GNUNET_asprintf (&auth_header, + "%s: %s", + MHD_HTTP_HEADER_AUTHORIZATION, + apikey); + if (GNUNET_OK != + GNUNET_CURL_append_header (SH_ctx, + auth_header)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to set %s header, trying without\n", + MHD_HTTP_HEADER_AUTHORIZATION); + } + GNUNET_free (auth_header); + } + + if (NULL == + (db = SYNC_DB_plugin_load (config, + false))) + { + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + db->preflight (db->cls)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database not setup. Did you run sync-dbinit?\n"); + GNUNET_SCHEDULER_shutdown (); + return; + } + fh = TALER_MHD2_bind (config, + "sync", + &port); + if ( (0 == port) && + (-1 == fh) ) + { + global_ret = EXIT_NO_RESTART; + GNUNET_SCHEDULER_shutdown (); + return; + } + mhd = MHD_daemon_create (&url_handler, + NULL); + if (NULL == mhd) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to launch HTTP service, exiting.\n"); + global_ret = EXIT_NO_RESTART; + GNUNET_SCHEDULER_shutdown (); + return; + } + GNUNET_assert (MHD_SC_OK == + MHD_DAEMON_SET_OPTIONS ( + mhd, + MHD_D_OPTION_DEFAULT_TIMEOUT (10), + (-1 == fh) + ? MHD_D_OPTION_BIND_PORT (MHD_AF_AUTO, + port) + : MHD_D_OPTION_LISTEN_SOCKET (fh))); + global_ret = EXIT_SUCCESS; + TALER_MHD2_daemon_start (mhd); +} + + +/** + * 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) +{ + 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", + &SH_sync_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 ('t', + "type", + "CERTTYPE", + "type of the TLS client certificate, defaults to PEM if not specified", + &certtype), + GNUNET_GETOPT_OPTION_END + }; + enum GNUNET_GenericReturnValue ret; + + ret = GNUNET_PROGRAM_run (SYNC_project_data (), + argc, argv, + "sync-httpd", + "sync HTTP interface", + options, + &run, NULL); + if (GNUNET_NO == ret) + return EXIT_SUCCESS; + if (GNUNET_SYSERR == ret) + return EXIT_INVALIDARGUMENT; + return global_ret; +} diff --git a/src/sync/sync-httpd2.h b/src/sync/sync-httpd2.h @@ -0,0 +1,151 @@ +/* + 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 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file sync/sync-httpd2.h + * @brief HTTP serving layer + * @author Christian Grothoff + */ +#ifndef SYNC_HTTPD2_H +#define SYNC_HTTPD2_H + +#include "platform.h" +#include <microhttpd2.h> +#include <taler/taler_mhd2_lib.h> +#include "sync_database_lib.h" + +/** + * @brief Struct describing an URL and the handler for it. + */ +struct SH_RequestHandler +{ + + /** + * URL the handler is for. Must not start with "/backups/". + */ + const char *url; + + /** + * HTTP Method the handler is for. + */ + enum MHD_HTTP_Method method; + + /** + * Function to call to handle the request. + * + * @param request the MHD request to handle + * @param upload_size number of bytes to be uploaded + * @return MHD action + */ + const struct MHD_Action * + (*handler)(struct MHD_Request *request, + size_t upload_size); +}; + + +/** + * 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; + + /** + * Asynchronous request context id. + */ + struct GNUNET_AsyncScopeId async_scope_id; +}; + + +/** + * Handle to the database backend. + */ +extern struct SYNC_DatabasePlugin *db; + +/** + * Upload limit to the service, in megabytes. + */ +extern unsigned long long SH_upload_limit_mb; + +/** + * Annual fee for the backup account. + */ +extern struct TALER_Amount SH_annual_fee; + +/** + * Our Taler backend to process payments. + */ +extern char *SH_backend_url; + +/** + * Our fulfillment URL + */ +extern char *SH_fulfillment_url; + +/** + * Our context for making HTTP requests. + */ +extern struct GNUNET_CURL_Context *SH_ctx; + +/** + * Amount of insurance. + */ +extern struct TALER_Amount SH_insurance; + +/** + * Kick MHD to run now, to be called after MHD_resume_connection(). + * Basically, we need to explicitly resume MHD's event loop whenever + * we made progress serving a request. This function re-schedules + * the task processing MHD's activities to run immediately. + */ +void +SH_trigger_daemon (void); + + +/** + * Kick GNUnet Curl scheduler to begin curl interactions. + */ +void +SH_trigger_curl (void); + + +#endif diff --git a/src/sync/sync-httpd2_backup-post.c b/src/sync/sync-httpd2_backup-post.c @@ -0,0 +1,1036 @@ +/* + 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 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd_backup_post.c + * @brief functions to handle incoming requests for backups + * @author Christian Grothoff + */ +#include "platform.h" +#include "sync-httpd.h" +#include <gnunet/gnunet_util_lib.h> +#include "sync-httpd_backup.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_merchant_service.h> +#include <taler/taler_signatures.h> + + +/** + * 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 BackupContext +{ + + /** + * Context for cleanup logic. + */ + struct TM_HandlerContext hc; + + /** + * Signature of the account holder. + */ + struct SYNC_AccountSignatureP account_sig; + + /** + * Public key of the account holder. + */ + struct SYNC_AccountPublicKeyP account; + + /** + * Hash of the previous upload, or zeros if first upload. + */ + struct GNUNET_HashCode old_backup_hash; + + /** + * Hash of the upload we are receiving right now (as promised + * by the client, to be verified!). + */ + struct GNUNET_HashCode new_backup_hash; + + /** + * Claim token, all zeros if not known. Only set if @e existing_order_id is non-NULL. + */ + struct TALER_ClaimTokenP token; + + /** + * Hash context for the upload. + */ + struct GNUNET_HashContext *hash_ctx; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct BackupContext *next; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct BackupContext *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 *omgh; + + /** + * 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; + + /** + * Order ID for the client that we found in our database. + */ + char *existing_order_id; + + /** + * Timestamp of the order in @e existing_order_id. Used to + * select the most recent unpaid offer. + */ + struct GNUNET_TIME_Timestamp existing_order_timestamp; + + /** + * 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; + + /** + * Do not look for an existing order, force a fresh order to be created. + */ + bool force_fresh_order; +}; + + +/** + * Kept in DLL for shutdown handling while suspended. + */ +static struct BackupContext *bc_head; + +/** + * Kept in DLL for shutdown handling while suspended. + */ +static struct BackupContext *bc_tail; + + +/** + * Service is shutting down, resume all MHD connections NOW. + */ +void +SH_resume_all_bc () +{ + struct BackupContext *bc; + + while (NULL != (bc = bc_head)) + { + GNUNET_CONTAINER_DLL_remove (bc_head, + bc_tail, + bc); + MHD_resume_connection (bc->con); + if (NULL != bc->po) + { + TALER_MERCHANT_orders_post_cancel (bc->po); + bc->po = NULL; + } + if (NULL != bc->omgh) + { + TALER_MERCHANT_merchant_order_get_cancel (bc->omgh); + bc->omgh = NULL; + } + } +} + + +/** + * Function called to clean up a backup context. + * + * @param hc a `struct BackupContext` + */ +static void +cleanup_ctx (struct TM_HandlerContext *hc) +{ + struct BackupContext *bc = (struct BackupContext *) hc; + + if (NULL != bc->po) + TALER_MERCHANT_orders_post_cancel (bc->po); + if (NULL != bc->hash_ctx) + GNUNET_CRYPTO_hash_context_abort (bc->hash_ctx); + if (NULL != bc->resp) + MHD_destroy_response (bc->resp); + GNUNET_free (bc->existing_order_id); + GNUNET_free (bc->upload); + GNUNET_free (bc); +} + + +/** + * Transmit a payment request for @a order_id on @a connection + * + * @param order_id our backend's order ID + * @param token the claim token generated by the merchant (NULL if + * it wasn't generated). + * @return MHD response to use + */ +static struct MHD_Response * +make_payment_request (const char *order_id, + const struct TALER_ClaimTokenP *token) +{ + struct MHD_Response *resp; + + /* request payment via Taler */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Creating payment request for order `%s'\n", + order_id); + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + TALER_MHD_add_global_headers (resp); + { + char *hdr; + const char *pfx; + char *hn; + struct GNUNET_Buffer hdr_buf = { 0 }; + + if (0 == strncasecmp ("https://", + SH_backend_url, + strlen ("https://"))) + { + pfx = "taler://"; + hn = &SH_backend_url[strlen ("https://")]; + } + else if (0 == strncasecmp ("http://", + SH_backend_url, + strlen ("http://"))) + { + pfx = "taler+http://"; + hn = &SH_backend_url[strlen ("http://")]; + } + else + { + GNUNET_break (0); + MHD_destroy_response (resp); + return NULL; + } + if (0 == strlen (hn)) + { + GNUNET_break (0); + MHD_destroy_response (resp); + return NULL; + } + + GNUNET_buffer_write_str (&hdr_buf, pfx); + GNUNET_buffer_write_str (&hdr_buf, "pay/"); + GNUNET_buffer_write_str (&hdr_buf, hn); + GNUNET_buffer_write_path (&hdr_buf, order_id); + /* No session ID */ + GNUNET_buffer_write_path (&hdr_buf, ""); + if (NULL != token) + { + GNUNET_buffer_write_str (&hdr_buf, "?c="); + GNUNET_buffer_write_data_encoded (&hdr_buf, token, sizeof (*token)); + } + hdr = GNUNET_buffer_reap_str (&hdr_buf); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + "Taler", + hdr)); + GNUNET_free (hdr); + } + return resp; +} + + +/** + * Callbacks of this type are used to serve the result of submitting a + * /contract request to a merchant. + * + * @param cls our `struct BackupContext` + * @param por response details + */ +static void +proposal_cb (void *cls, + const struct TALER_MERCHANT_PostOrdersReply *por) +{ + struct BackupContext *bc = cls; + enum SYNC_DB_QueryStatus qs; + + bc->po = NULL; + GNUNET_CONTAINER_DLL_remove (bc_head, + bc_tail, + bc); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming connection with order `%s'\n", + bc->order_id); + MHD_resume_connection (bc->con); + SH_trigger_daemon (); + if (MHD_HTTP_OK != por->hr.http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Backend returned status %u/%u\n", + por->hr.http_status, + (unsigned int) por->hr.ec); + GNUNET_break_op (0); + bc->resp = TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec (TALER_EC_SYNC_PAYMENT_CREATE_BACKEND_ERROR), + GNUNET_JSON_pack_uint64 ("backend-ec", + por->hr.ec), + GNUNET_JSON_pack_uint64 ("backend-http-status", + por->hr.http_status), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("backend-reply", + (json_t *) por->hr.reply))); + bc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Storing payment request for order `%s'\n", + por->details.ok.order_id); + qs = db->store_payment_TR (db->cls, + &bc->account, + por->details.ok.order_id, + por->details.ok.token, + &SH_annual_fee); + if (0 >= qs) + { + GNUNET_break (0); + bc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, + "Failed to persist payment request in sync database"); + GNUNET_assert (NULL != bc->resp); + bc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Obtained fresh order `%s'\n", + por->details.ok.order_id); + bc->resp = make_payment_request (por->details.ok.order_id, + por->details.ok.token); + GNUNET_assert (NULL != bc->resp); + bc->response_code = MHD_HTTP_PAYMENT_REQUIRED; +} + + +/** + * Function called on all pending payments for the right + * account. + * + * @param cls closure, our `struct BackupContext` + * @param timestamp for how long have we been waiting + * @param order_id order id in the backend + * @param token claim token to use (or NULL for none) + * @param amount how much is the order for + */ +static void +ongoing_payment_cb (void *cls, + struct GNUNET_TIME_Timestamp timestamp, + const char *order_id, + const struct TALER_ClaimTokenP *token, + const struct TALER_Amount *amount) +{ + struct BackupContext *bc = cls; + + (void) amount; + if (0 != TALER_amount_cmp (amount, + &SH_annual_fee)) + return; /* can't reuse, fees changed */ + if ( (NULL == bc->existing_order_id) || + (GNUNET_TIME_timestamp_cmp (bc->existing_order_timestamp, + <, + timestamp)) ) + { + GNUNET_free (bc->existing_order_id); + bc->existing_order_id = GNUNET_strdup (order_id); + bc->existing_order_timestamp = timestamp; + if (NULL != token) + bc->token = *token; + } +} + + +/** + * Callback to process a GET /check-payment request + * + * @param cls our `struct BackupContext` + * @param osr order status + */ +static void +check_payment_cb (void *cls, + const struct TALER_MERCHANT_OrderStatusResponse *osr) +{ + struct BackupContext *bc = cls; + const struct TALER_MERCHANT_HttpResponse *hr = &osr->hr; + + /* refunds are not supported, verify */ + bc->omgh = NULL; + GNUNET_CONTAINER_DLL_remove (bc_head, + bc_tail, + bc); + MHD_resume_connection (bc->con); + SH_trigger_daemon (); + switch (hr->http_status) + { + case 0: + /* Likely timeout, complain! */ + bc->response_code = MHD_HTTP_GATEWAY_TIMEOUT; + bc->resp = TALER_MHD_make_error ( + TALER_EC_SYNC_GENERIC_BACKEND_TIMEOUT, + NULL); + return; + case MHD_HTTP_OK: + break; /* handled below */ + default: + /* Unexpected backend response */ + bc->response_code = MHD_HTTP_BAD_GATEWAY; + bc->resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("code", + TALER_EC_SYNC_GENERIC_BACKEND_ERROR), + GNUNET_JSON_pack_string ("hint", + TALER_ErrorCode_get_hint ( + TALER_EC_SYNC_GENERIC_BACKEND_ERROR)), + GNUNET_JSON_pack_uint64 ("backend-ec", + (json_int_t) hr->ec), + GNUNET_JSON_pack_uint64 ("backend-http-status", + (json_int_t) hr->http_status), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("backend-reply", + (json_t *) hr->reply))); + return; + } + + GNUNET_assert (MHD_HTTP_OK == hr->http_status); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment status checked: %d\n", + osr->details.ok.status); + switch (osr->details.ok.status) + { + case TALER_MERCHANT_OSC_PAID: + { + enum SYNC_DB_QueryStatus qs; + + qs = db->increment_lifetime_TR (db->cls, + &bc->account, + bc->order_id, + GNUNET_TIME_UNIT_YEARS); /* always annual */ + if (0 <= qs) + return; /* continue as planned */ + GNUNET_break (0); + bc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, + "increment lifetime"); + GNUNET_assert (NULL != bc->resp); + bc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; /* continue as planned */ + } + case TALER_MERCHANT_OSC_UNPAID: + case TALER_MERCHANT_OSC_CLAIMED: + break; + } + if (NULL != bc->existing_order_id) + { + /* repeat payment request */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Repeating payment request\n"); + bc->resp = make_payment_request (bc->existing_order_id, + (GNUNET_YES == GNUNET_is_zero (&bc->token)) + ? NULL + : &bc->token); + GNUNET_assert (NULL != bc->resp); + bc->response_code = MHD_HTTP_PAYMENT_REQUIRED; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Timeout waiting for payment\n"); + bc->resp = TALER_MHD_make_error (TALER_EC_SYNC_PAYMENT_GENERIC_TIMEOUT, + "Timeout awaiting promised payment"); + GNUNET_assert (NULL != bc->resp); + bc->response_code = MHD_HTTP_REQUEST_TIMEOUT; +} + + +/** + * Helper function used to ask our backend to await + * a payment for the user's account. + * + * @param bc context to begin payment for. + * @param timeout when to give up trying + * @param order_id which order to check for the payment + */ +static void +await_payment (struct BackupContext *bc, + struct GNUNET_TIME_Relative timeout, + const char *order_id) +{ + GNUNET_CONTAINER_DLL_insert (bc_head, + bc_tail, + bc); + MHD_suspend_connection (bc->con); + bc->order_id = order_id; + bc->omgh = TALER_MERCHANT_merchant_order_get (SH_ctx, + SH_backend_url, + order_id, + NULL /* our payments are NOT session-bound */ + , + timeout, + &check_payment_cb, + bc); + SH_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 bc context to begin payment for. + * @param pay_req #GNUNET_YES if payment was explicitly requested, + * #GNUNET_NO if payment is needed + * @return MHD status code + */ +static MHD_RESULT +begin_payment (struct BackupContext *bc, + int pay_req) +{ + static const char *no_uuids[1] = { NULL }; + json_t *order; + + if (! bc->force_fresh_order) + { + enum GNUNET_DB_QueryStatus qs; + + qs = db->lookup_pending_payments_by_account_TR (db->cls, + &bc->account, + &ongoing_payment_cb, + bc); + if (qs < 0) + { + struct MHD_Response *resp; + MHD_RESULT ret; + + resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED, + "pending payments"); + ret = MHD_queue_response (bc->con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } + if (NULL != bc->existing_order_id) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Have existing order, waiting for `%s' to complete\n", + bc->existing_order_id); + await_payment (bc, + GNUNET_TIME_UNIT_ZERO /* no long polling */, + bc->existing_order_id); + return MHD_YES; + } + } + GNUNET_CONTAINER_DLL_insert (bc_head, + bc_tail, + bc); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending connection while creating order at `%s'\n", + SH_backend_url); + MHD_suspend_connection (bc->con); + order = GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + &SH_annual_fee), + GNUNET_JSON_pack_string ("summary", + "annual fee for sync service"), + GNUNET_JSON_pack_string ("fulfillment_url", + SH_fulfillment_url)); + bc->po = TALER_MERCHANT_orders_post2 (SH_ctx, + SH_backend_url, + order, + GNUNET_TIME_UNIT_ZERO, + NULL, /* no payment target */ + 0, + NULL, /* no inventory products */ + 0, + no_uuids, /* no uuids */ + false, /* do NOT require claim token */ + &proposal_cb, + bc); + SH_trigger_curl (); + json_decref (order); + return MHD_YES; +} + + +/** + * We got some query status from the DB. Handle the error cases. + * May perform asynchronous operations by suspending the connection + * if required. + * + * @param bc connection to handle status for + * @param qs query status to handle + * @return #MHD_YES or #MHD_NO + */ +static MHD_RESULT +handle_database_error (struct BackupContext *bc, + enum SYNC_DB_QueryStatus qs) +{ + switch (qs) + { + case SYNC_DB_OLD_BACKUP_MISSING: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Update failed: no existing backup\n"); + return TALER_MHD_reply_with_error (bc->con, + MHD_HTTP_NOT_FOUND, + TALER_EC_SYNC_PREVIOUS_BACKUP_UNKNOWN, + NULL); + case SYNC_DB_OLD_BACKUP_MISMATCH: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Conflict detected, returning existing backup\n"); + return SH_return_backup (bc->con, + &bc->account, + MHD_HTTP_CONFLICT); + case SYNC_DB_PAYMENT_REQUIRED: + { + const char *order_id; + + order_id = MHD_lookup_connection_value (bc->con, + MHD_GET_ARGUMENT_KIND, + "paying"); + if (NULL == order_id) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment required, starting payment process\n"); + return begin_payment (bc, + GNUNET_NO); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment required, awaiting completion of `%s'\n", + order_id); + await_payment (bc, + CHECK_PAYMENT_GENERIC_TIMEOUT, + order_id); + } + return MHD_YES; + case SYNC_DB_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (bc->con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + case SYNC_DB_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (bc->con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + case SYNC_DB_NO_RESULTS: + GNUNET_assert (0); + return MHD_NO; + /* intentional fall-through! */ + case SYNC_DB_ONE_RESULT: + GNUNET_assert (0); + return MHD_NO; + } + GNUNET_break (0); + return MHD_NO; +} + + +MHD_RESULT +SH_backup_post (struct MHD_Connection *connection, + void **con_cls, + const struct SYNC_AccountPublicKeyP *account, + const char *upload_data, + size_t *upload_data_size) +{ + struct BackupContext *bc; + + bc = *con_cls; + if (NULL == bc) + { + /* first call, setup internals */ + bc = GNUNET_new (struct BackupContext); + bc->hc.cc = &cleanup_ctx; + bc->con = connection; + bc->account = *account; + { + const char *fresh; + + fresh = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "fresh"); + if (NULL != fresh) + bc->force_fresh_order = true; + } + *con_cls = bc; + + /* now setup 'bc' */ + { + 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_SYNC_MALFORMED_CONTENT_LENGTH + : TALER_EC_SYNC_MISSING_CONTENT_LENGTH, + lens); + } + if (len / 1024 / 1024 >= SH_upload_limit_mb) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_PAYLOAD_TOO_LARGE, + TALER_EC_SYNC_EXCESSIVE_CONTENT_LENGTH, + NULL); + } + bc->upload = GNUNET_malloc_large (len); + if (NULL == bc->upload) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "malloc"); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_PAYLOAD_TOO_LARGE, + TALER_EC_SYNC_OUT_OF_MEMORY_ON_CONTENT_LENGTH, + NULL); + } + bc->upload_size = (size_t) len; + } + { + const char *im; + + im = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_MATCH); + if (NULL != im) + { + if ( (2 >= strlen (im)) || + ('"' != im[0]) || + ('"' != im[strlen (im) - 1]) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (im + 1, + strlen (im) - 2, + &bc->old_backup_hash, + sizeof (bc->old_backup_hash))) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_SYNC_BAD_IF_MATCH, + NULL); + } + } + } + { + const char *sig_s; + + sig_s = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + "Sync-Signature"); + if ( (NULL == sig_s) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (sig_s, + strlen (sig_s), + &bc->account_sig, + sizeof (bc->account_sig))) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_SYNC_BAD_SYNC_SIGNATURE, + NULL); + } + } + { + const char *etag; + + etag = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if ( (NULL == etag) || + (2 >= strlen (etag)) || + ('"' != etag[0]) || + ('"' != etag[strlen (etag) - 1]) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (etag + 1, + strlen (etag) - 2, + &bc->new_backup_hash, + sizeof (bc->new_backup_hash))) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_SYNC_BAD_IF_NONE_MATCH, + NULL); + } + } + /* validate signature */ + { + struct SYNC_UploadSignaturePS usp = { + .purpose.size = htonl (sizeof (usp)), + .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD), + .old_backup_hash = bc->old_backup_hash, + .new_backup_hash = bc->new_backup_hash + }; + + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD, + &usp, + &bc->account_sig.eddsa_sig, + &account->eddsa_pub)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_SYNC_INVALID_SIGNATURE, + NULL); + } + } + /* get ready to hash (done here as we may go async for payments next) */ + bc->hash_ctx = GNUNET_CRYPTO_hash_context_start (); + + /* Check database to see if the transaction is permissible */ + { + struct GNUNET_HashCode hc; + enum SYNC_DB_QueryStatus qs; + + qs = db->lookup_account_TR (db->cls, + account, + &hc); + if (qs < 0) + return handle_database_error (bc, + qs); + if (SYNC_DB_NO_RESULTS == qs) + memset (&hc, 0, sizeof (hc)); + if (0 == GNUNET_memcmp (&hc, + &bc->new_backup_hash)) + { + /* Refuse upload: we already have that backup! */ + struct MHD_Response *resp; + MHD_RESULT ret; + + 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); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } + if (0 != GNUNET_memcmp (&hc, + &bc->old_backup_hash)) + { + /* Refuse upload: if-none-match failed! */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Conflict detected, returning existing backup\n"); + return SH_return_backup (connection, + account, + MHD_HTTP_CONFLICT); + } + } + /* check if the client insists on paying */ + { + const char *order_req; + + order_req = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "pay"); + if (NULL != order_req) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment requested, starting payment process\n"); + return begin_payment (bc, + GNUNET_YES); + } + } + /* ready to begin! */ + return MHD_YES; + } + /* handle upload */ + if (0 != *upload_data_size) + { + /* check MHD invariant */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Processing %u bytes of upload data\n", + (unsigned int) *upload_data_size); + GNUNET_assert (bc->upload_off + *upload_data_size <= bc->upload_size); + memcpy (&bc->upload[bc->upload_off], + upload_data, + *upload_data_size); + bc->upload_off += *upload_data_size; + GNUNET_CRYPTO_hash_context_read (bc->hash_ctx, + upload_data, + *upload_data_size); + *upload_data_size = 0; + return MHD_YES; + } + else if ( (0 == bc->upload_off) && + (0 != bc->upload_size) && + (NULL == bc->resp) ) + { + /* wait for upload */ + return MHD_YES; + } + if (NULL != bc->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", + bc->response_code); + ret = MHD_queue_response (connection, + bc->response_code, + bc->resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (bc->resp); + bc->resp = NULL; + return ret; + } + + /* finished with upload, check hash */ + { + struct GNUNET_HashCode our_hash; + + GNUNET_CRYPTO_hash_context_finish (bc->hash_ctx, + &our_hash); + bc->hash_ctx = NULL; + if (0 != GNUNET_memcmp (&our_hash, + &bc->new_backup_hash)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_SYNC_INVALID_UPLOAD, + NULL); + } + } + + /* store backup to database */ + { + enum SYNC_DB_QueryStatus qs; + + if (GNUNET_YES == GNUNET_is_zero (&bc->old_backup_hash)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Uploading first backup to account\n"); + qs = db->store_backup_TR (db->cls, + account, + &bc->account_sig, + &bc->new_backup_hash, + bc->upload_size, + bc->upload); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Uploading existing backup of account\n"); + qs = db->update_backup_TR (db->cls, + account, + &bc->old_backup_hash, + &bc->account_sig, + &bc->new_backup_hash, + bc->upload_size, + bc->upload); + } + if (qs < 0) + return handle_database_error (bc, + qs); + if (0 == qs) + { + /* 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); + ret = MHD_queue_response (connection, + MHD_HTTP_NOT_MODIFIED, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } + } + + /* 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); + ret = MHD_queue_response (connection, + MHD_HTTP_NO_CONTENT, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; + } +} diff --git a/src/sync/sync-httpd2_backup.c b/src/sync/sync-httpd2_backup.c @@ -0,0 +1,247 @@ +/* + This file is part of TALER + Copyright (C) 2019--2025 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd2_backup.c + * @brief functions to handle incoming requests for backups + * @author Christian Grothoff + */ +#include "platform.h" +#include "sync-httpd2.h" +#include <gnunet/gnunet_util_lib.h> +#include "sync-httpd2_backup.h" + + +/** + * Handle request on @a request for retrieval of the latest + * backup of @a account. + * + * @param request the MHD request to handle + * @param account public key of the account the request is for + * @return MHD action + */ +const struct MHD_Action * +SH_backup_get (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account) +{ + struct GNUNET_HashCode backup_hash; + enum SYNC_DB_QueryStatus qs; + + qs = db->lookup_account_TR (db->cls, + account, + &backup_hash); + switch (qs) + { + case SYNC_DB_OLD_BACKUP_MISSING: + GNUNET_break (0); + return TALER_MHD2_reply_with_error (request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + case SYNC_DB_OLD_BACKUP_MISMATCH: + GNUNET_break (0); + return TALER_MHD2_reply_with_error (request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + case SYNC_DB_PAYMENT_REQUIRED: + return TALER_MHD2_reply_with_error (request, + MHD_HTTP_STATUS_NOT_FOUND, + TALER_EC_SYNC_ACCOUNT_UNKNOWN, + NULL); + case SYNC_DB_HARD_ERROR: + return TALER_MHD2_reply_with_error (request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case SYNC_DB_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD2_reply_with_error (request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + case SYNC_DB_NO_RESULTS: + { + struct MHD_Response *resp; + + resp = MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT); + TALER_MHD2_add_global_headers (resp); + return MHD_action_from_response (request, + resp); + } + case SYNC_DB_ONE_RESULT: + { + const char *inm; + + inm = MHD_request_get_value (request, + MHD_VK_HEADER, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if ( (NULL != inm) && + (2 < strlen (inm)) && + ('"' == inm[0]) && + ('=' == inm[strlen (inm) - 1]) ) + { + struct GNUNET_HashCode inm_h; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (inm + 1, + strlen (inm) - 2, + &inm_h, + sizeof (inm_h))) + { + GNUNET_break_op (0); + return TALER_MHD2_reply_with_error (request, + MHD_HTTP_STATUS_BAD_REQUEST, + TALER_EC_SYNC_BAD_IF_NONE_MATCH, + "Etag does not include a base32-encoded SHA-512 hash"); + } + if (0 == GNUNET_memcmp (&inm_h, + &backup_hash)) + { + struct MHD_Response *resp; + + resp = MHD_response_from_empty (MHD_HTTP_STATUS_NOT_MODIFIED); + TALER_MHD2_add_global_headers (resp); + return MHD_action_from_response (request, + resp); + } + } + } + /* We have a result, should fetch and return it! */ + break; + } + return SH_return_backup (request, + account, + MHD_HTTP_STATUS_OK); +} + + +/** + * Return the current backup of @a account on @a request + * using @a default_http_status on success. + * + * @param request MHD request to use + * @param account account to query + * @param default_http_status HTTP status to queue response + * with on success (#MHD_HTTP_STATUS_OK or #MHD_HTTP_STATUS_CONFLICT) + * @return MHD action + */ +const struct MHD_Action * +SH_return_backup (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account, + enum MHD_HTTP_StatusCode default_http_status) +{ + enum SYNC_DB_QueryStatus qs; + struct MHD_Response *resp; + struct SYNC_AccountSignatureP account_sig; + struct GNUNET_HashCode backup_hash; + struct GNUNET_HashCode prev_hash; + size_t backup_size; + void *backup; + + qs = db->lookup_backup_TR (db->cls, + account, + &account_sig, + &prev_hash, + &backup_hash, + &backup_size, + &backup); + switch (qs) + { + case SYNC_DB_OLD_BACKUP_MISSING: + GNUNET_break (0); + return TALER_MHD2_reply_with_error (request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected return status (backup missing)"); + case SYNC_DB_OLD_BACKUP_MISMATCH: + GNUNET_break (0); + return TALER_MHD2_reply_with_error (request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected return status (backup mismatch)"); + case SYNC_DB_PAYMENT_REQUIRED: + GNUNET_break (0); + return TALER_MHD2_reply_with_error (request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected return status (payment required)"); + case SYNC_DB_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD2_reply_with_error (request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case SYNC_DB_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD2_reply_with_error (request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + case SYNC_DB_NO_RESULTS: + GNUNET_break (0); + /* Note: can theoretically happen due to non-transactional nature if + the backup expired / was gc'ed JUST between the two SQL calls. + But too rare to handle properly, as doing a transaction would be + expensive. Just admit to failure ;-) */ + return TALER_MHD2_reply_with_error (request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + NULL); + case SYNC_DB_ONE_RESULT: + /* interesting case below */ + break; + } + resp = MHD_response_from_buffer (default_http_status, + backup_size, + backup, + &free, + backup); + TALER_MHD2_add_global_headers (resp); + { + char *sig_s; + char *prev_s; + char *etag; + char *etagq; + + sig_s = GNUNET_STRINGS_data_to_string_alloc (&account_sig, + sizeof (account_sig)); + prev_s = GNUNET_STRINGS_data_to_string_alloc (&prev_hash, + sizeof (prev_hash)); + etag = GNUNET_STRINGS_data_to_string_alloc (&backup_hash, + sizeof (backup_hash)); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + "Sync-Signature", + sig_s)); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + "Sync-Previous", + prev_s)); + GNUNET_asprintf (&etagq, + "\"%s\"", + etag); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + MHD_HTTP_HEADER_ETAG, + etagq)); + GNUNET_free (etagq); + GNUNET_free (etag); + GNUNET_free (prev_s); + GNUNET_free (sig_s); + } + return MHD_action_from_response (request, + resp); +} diff --git a/src/sync/sync-httpd2_backup.h b/src/sync/sync-httpd2_backup.h @@ -0,0 +1,76 @@ +/* + 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd2_backup.h + * @brief functions to handle incoming requests on /backup/ + * @author Christian Grothoff + */ +#ifndef SYNC_HTTPD2_BACKUP_H +#define SYNC_HTTPD2_BACKUP_H + +#include <microhttpd2.h> + +/** + * Service is shutting down, resume all MHD requests NOW. + */ +void +SH_resume_all_bc (void); + + +/** + * Return the current backup of @a account on @a request + * using @a default_http_status on success. + * + * @param request MHD request to use + * @param account account to query + * @param default_http_status HTTP status to queue response + * with on success (#MHD_HTTP_OK or #MHD_HTTP_CONFLICT) + * @return MHD action + */ +const struct MHD_Action * +SH_return_backup (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account, + unsigned int default_http_status); + + +/** + * Handle request on @a request for retrieval of the latest + * backup of @a account. + * + * @param request the MHD request to handle + * @param account public key of the account the request is for + * @return MHD action + */ +const struct MHD_Action * +SH_backup_get (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account); + + +/** + * Handle POST /backup requests. + * + * @param request the MHD request to handle + * @param account public key of the account the request is for + * @param upload_size number of bytes being uploaded + * @return MHD result code + */ +const struct MHD_Action * +SH_backup_post (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account, + uint_fast64_t upload_size); + + +#endif diff --git a/src/sync/sync-httpd2_config.c b/src/sync/sync-httpd2_config.c @@ -0,0 +1,96 @@ +/* + This file is part of Sync + Copyright (C) 2020--2025 Taler Systems SA + + Sync 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. + + Sync 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 + Sync; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync/sync-httpd2_config.c + * @brief headers for /config handler + * @author Christian Grothoff + */ +#include "platform.h" +#include "sync-httpd2_config.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_mhd2_lib.h> + + +/* + * Protocol version history: + * + * 0: original design + * 1: adds ?fresh=y to POST backup operation to force fresh contract + * to be created + */ + +const struct MHD_Action * +SH_handler_config (struct MHD_Request *request, + uint_fast64_t upload_size) +{ + static struct MHD_Response *response; + static struct GNUNET_TIME_Absolute a; + + GNUNET_break (0 == upload_size); + if ( (GNUNET_TIME_absolute_is_past (a)) && + (NULL != response) ) + { + MHD_response_destroy (response); + response = NULL; + } + if (NULL == response) + { + struct GNUNET_TIME_Timestamp km; + char dat[128]; + + a = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_DAYS); + /* Round up to next full day to ensure the expiration + time does not become a fingerprint! */ + a = GNUNET_TIME_absolute_round_down (a, + GNUNET_TIME_UNIT_DAYS); + a = GNUNET_TIME_absolute_add (a, + GNUNET_TIME_UNIT_DAYS); + /* => /config response stays at most 48h in caches! */ + km = GNUNET_TIME_absolute_to_timestamp (a); + TALER_MHD2_get_date_string (km.abs_time, + dat); + response = TALER_MHD2_MAKE_JSON_PACK ( + MHD_HTTP_STATUS_OK, + GNUNET_JSON_pack_string ("name", + "sync"), + GNUNET_JSON_pack_string ("implementation", + "urn:net:taler:specs:sync:c-reference"), + GNUNET_JSON_pack_uint64 ("storage_limit_in_megabytes", + SH_upload_limit_mb), + TALER_JSON_pack_amount ("liability_limit", + &SH_insurance), + TALER_JSON_pack_amount ("annual_fee", + &SH_annual_fee), + GNUNET_JSON_pack_string ("version", + "2:2:0")); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (response, + MHD_HTTP_HEADER_EXPIRES, + dat)); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (response, + MHD_HTTP_HEADER_CACHE_CONTROL, + "public,max-age=21600")); /* 6h */ + GNUNET_assert (MHD_SC_OK == + MHD_RESPONSE_SET_OPTIONS (response, + MHD_R_OPTION_REUSABLE (true))); + } + return MHD_action_from_response (request, + response); +} + + +/* end of sync-httpd2_config.c */ diff --git a/src/sync/sync-httpd2_config.h b/src/sync/sync-httpd2_config.h @@ -0,0 +1,39 @@ +/* + This file is part of Sync + Copyright (C) 2020 Taler Systems SA + + Sync 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. + + Sync 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 + Sync; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync/sync-httpd_config.h + * @brief headers for /config handler + * @author Christian Grothoff + */ +#ifndef SYNC_HTTPD2_CONFIG_H +#define SYNC_HTTPD2_CONFIG_H +#include <microhttpd2.h> +#include "sync-httpd2.h" + +/** + * Manages a /config call. + * + * @param request the MHD request to handle + * @param upload_size number of bytes that were uploaded + * @return MHD action + */ +const struct MHD_Action * +SH_handler_config (struct MHD_Request *request, + uint_fast64_t upload_size); + +#endif + +/* end of sync-httpd_config.h */