sync

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

commit c12ae1f5fd5a32883c210c196bb4e9a731d2bfd5
parent a59f2302373b3ee83cf4d43773d04b5646e0c358
Author: Iván Ávalos <avalos@disroot.org>
Date:   Thu,  1 Oct 2026 14:37:51 +0200

move in talermhd2 headers, gone from exchange

Diffstat:
Mmeson.build | 17+++++------------
Asrc/include/taler/taler_mhd2_lib.h | 1072+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/meson.build | 1+
Asrc/mhd2/meson.build | 25+++++++++++++++++++++++++
Asrc/mhd2/mhd2.c | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/mhd2/mhd2_legal.c | 702+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/mhd2/mhd2_responses.c | 462+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/mhd2/mhd2_run.c | 414+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/mhd2/mhd2_spa.c | 390+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/sync/meson.build | 2+-
10 files changed, 3181 insertions(+), 13 deletions(-)

diff --git a/meson.build b/meson.build @@ -79,6 +79,11 @@ if not get_option('only-doc') mhd2_dep = cc.find_library('microhttpd2', required: true) endif + zlib_dep = dependency('zlib', required: false) + if not zlib_dep.found() + zlib_dep = cc.find_library('zlib', required: true) + endif + json_dep = dependency('jansson', required: false) if not json_dep.found() json_dep = cc.find_library('jansson', required: true) @@ -228,18 +233,6 @@ if not get_option('only-doc') ) private_config.set10('HAVE_TALERMHD', talermhd_dep.found()) - talermhd2_dep = dependency('talermhd2', required: false) - if not talermhd2_dep.found() - talermhd2_dep = cc.find_library('talermhd2', required: true) - endif - cc.has_header_symbol( - 'taler/taler_mhd2_lib.h', - 'TALER_MHD2_get_date_string', - required: true, - dependencies: [talermhd2_dep], - ) - private_config.set10('HAVE_TALERMHD2', talermhd2_dep.found()) - talerjson_dep = dependency('talerjson', required: false) if not talerjson_dep.found() talerjson_dep = cc.find_library('talerjson', required: true) diff --git a/src/include/taler/taler_mhd2_lib.h b/src/include/taler/taler_mhd2_lib.h @@ -0,0 +1,1072 @@ +/* + This file is part of TALER + Copyright (C) 2014-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 taler_mhd2_lib.h + * @brief API for generating MHD replies + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#ifndef TALER_MHD2_LIB_H +#define TALER_MHD2_LIB_H +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd2.h> +#include <taler/taler_error_codes.h> +#include <taler/taler_util.h> + + +/** + * Maximum POST request size. + */ +#define TALER_MHD2_REQUEST_BUFFER_MAX (1024 * 1024 * 16) + + +/** + * Global options for response generation. + */ +enum TALER_MHD2_GlobalOptions +{ + + /** + * Use defaults. + */ + TALER_MHD2_GO_NONE = 0, + + /** + * Add "Connection: Close" header. + */ + TALER_MHD2_GO_FORCE_CONNECTION_CLOSE = 1, + + /** + * Disable use of compression, even if the client + * supports it. + */ + TALER_MHD2_GO_DISABLE_COMPRESSION = 2 + +}; + + +/** + * Find out if an MHD connection is using HTTPS (either + * directly or via proxy). + * + * @param request MHD request + * @returns #GNUNET_YES if the MHD connection is using https, + * #GNUNET_NO if the MHD connection is using http, + * #GNUNET_SYSERR if the connection type couldn't be determined + */ +enum GNUNET_GenericReturnValue +TALER_MHD2_is_https (struct MHD_Request *request); + + +/** + * Convert query argument to @a yna value. + * + * @param request request to take query argument from + * @param arg argument to try for + * @param default_val value to assign if the argument is not present + * @param[out] yna value to set + * @return true on success, false if the parameter was malformed + */ +bool +TALER_MHD2_arg_to_yna (struct MHD_Request *request, + const char *arg, + enum TALER_EXCHANGE_YesNoAll default_val, + enum TALER_EXCHANGE_YesNoAll *yna); + + +/** + * Set global options for response generation within libtalermhd. + * + * @param go global options to use + */ +void +TALER_MHD2_setup (enum TALER_MHD2_GlobalOptions go); + + +/** + * Add headers we want to return in every response. Useful for testing, like + * if we want to always close connections. + * + * @param response response to modify + * @param allow_store set to true to NOT add a "Cache-Control" + * directive that prevents caches and browsers from storing the data; + * if false, we set "Cache-Control: no-store" (privacy by default); + * set to true if the response contains no personal data + */ +void +TALER_MHD2_add_global_headers (struct MHD_Response *response, + bool allow_store); + + +/** + * Try to compress a response body. Updates @a buf and @a buf_size. + * + * @param[in,out] buf pointer to body to compress + * @param[in,out] buf_size pointer to initial size of @a buf + * @return true if @a buf was compressed + */ +bool +TALER_MHD2_body_compress (void **buf, + size_t *buf_size); + + +/** + * Is HTTP body deflate compression supported by the client? + * + * @param request the request to check + * @return true if 'deflate' compression is allowed + */ +bool +TALER_MHD2_can_compress (struct MHD_Request *request); + + +/** + * Send JSON object as response. + * + * @param request the MHD request to handle, used to determine + * whether compression is possible + * @param json the json object + * @param sc the http response code + * @return MHD action + */ +const struct MHD_Action * +TALER_MHD2_reply_json (struct MHD_Request *request, + const json_t *json, + enum MHD_HTTP_StatusCode sc); + + +/** + * Send JSON object as response, and free the @a json + * object. + * + * @param request the MHD request to handle + * @param json the json object (freed!) + * @param sc the http response code + * @return MHD action + */ +const struct MHD_Action * +TALER_MHD2_reply_json_steal (struct MHD_Request *request, + json_t *json, + enum MHD_HTTP_StatusCode sc); + + +/** + * Make JSON response object. + * + * @param sc HTTP response code to use + * @param json the json object + * @return MHD response object + */ +struct MHD_Response * +TALER_MHD2_make_json (enum MHD_HTTP_StatusCode sc, + const json_t *json); + + +/** + * Make JSON response object and free @a json. + * + * @param sc HTTP response code to use + * @param json the json object, freed. + * @return MHD response object + */ +struct MHD_Response * +TALER_MHD2_make_json_steal (enum MHD_HTTP_StatusCode sc, + json_t *json); + + +/** + * Make response object + * + * @param sc HTTP status code to use + * @param ... varargs + * @return MHD action + */ +#define TALER_MHD2_MAKE_JSON_PACK(sc,...) \ + TALER_MHD2_make_json_steal (sc, GNUNET_JSON_PACK (__VA_ARGS__)) + + +/** + * Pack Taler error code @a ec and associated hint into a + * JSON object. + * + * @param ec error code to pack + * @return packer array entries (two!) + */ +#define TALER_MHD2_PACK_EC(ec) \ + GNUNET_JSON_pack_uint64 ("code", ec), \ + GNUNET_JSON_pack_string ("hint", TALER_ErrorCode_get_hint (ec)) + +/** + * Create a response indicating an internal error. + * + * @param ec error code to return + * @param detail additional optional detail about the error, can be NULL + * @return a MHD response object + */ +struct MHD_Response * +TALER_MHD2_make_error (enum TALER_ErrorCode ec, + const char *detail); + + +/** + * Function to call to handle the request by building a JSON + * reply from varargs. + * + * @param request the MHD request to handle + * @param sc HTTP response code to use + * @param ... varargs of JSON pack specification + * @return MHD action + */ +#define TALER_MHD2_REPLY_JSON_PACK(request,sc,...) \ + TALER_MHD2_reply_json_steal (request, GNUNET_JSON_PACK (__VA_ARGS__), \ + sc) + + +/** + * Send a response indicating an error. + * + * @param request the MHD request to use + * @param sc HTTP status code to use + * @param ec error code uniquely identifying the error + * @param detail additional optional detail about the error + * @return MHD action + */ +const struct MHD_Action * +TALER_MHD2_reply_with_error (struct MHD_Request *request, + enum MHD_HTTP_StatusCode sc, + enum TALER_ErrorCode ec, + const char *detail); + + +/** + * Send a response indicating an error. The HTTP status code is + * to be derived from the @a ec. + * + * @param request the MHD request to use + * @param ec error code uniquely identifying the error + * @param detail additional optional detail about the error + * @return MHD action + */ +const struct MHD_Action * +TALER_MHD2_reply_with_ec (struct MHD_Request *request, + enum TALER_ErrorCode ec, + const char *detail); + + +/** + * Produce HTTP "Date:" header. + * + * @param at time to write to @a date + * @param[out] date where to write the header, with + * at least 128 bytes available space. + */ +void +TALER_MHD2_get_date_string (struct GNUNET_TIME_Absolute at, + char date[128]); + + +/** + * Send a response indicating that the request was too big. + * + * @param request the MHD request to use + * @return MHD action + */ +const struct MHD_Action * +TALER_MHD2_reply_request_too_large (struct MHD_Request *request); + + +/** + * Function to call to handle the request by sending + * back a redirect to the AGPL source code. + * + * @param request the MHD request to handle + * @param url where to redirect for the sources + * @return MHD action + */ +const struct MHD_Action * +TALER_MHD2_reply_agpl (struct MHD_Request *request, + const char *url); + + +/** + * Function to call to handle the request by sending + * back static data. + * + * @param request the MHD request to handle + * @param sc status code to return + * @param mime_type content-type to use + * @param body response payload + * @param body_size number of bytes in @a body + * @return MHD action + */ +const struct MHD_Action * +TALER_MHD2_reply_static (struct MHD_Request *request, + enum MHD_HTTP_StatusCode sc, + const char *mime_type, + const char *body, + size_t body_size); + + +/* The request-parsing surface below has not been ported to MHD2 yet: + none of the functions it declares is implemented anywhere, so the + block must stay disabled until src/mhd2/mhd2_parsing.c exists. Note + that "#if 0" is deliberate: a guard on an undefined macro name would + silently flip meaning as soon as anything defines that name, handing + the user a header that compiles and then fails to link. */ +#if 0 + +/** + * Process a POST request containing a JSON object. This + * function realizes an MHD POST processor that will + * (incrementally) process JSON data uploaded to the HTTP + * server. It will store the required state in the + * "request_cls", which must be cleaned up using + * #TALER_MHD2_parse_post_cleanup_callback(). + * + * @param request the MHD request + * @param con_cls the closure (points to a `struct Buffer *`) + * @param upload_data the POST data + * @param upload_data_size number of bytes in @a upload_data + * @param json the JSON object for a completed request + * @return + * #GNUNET_YES if json object was parsed or at least + * may be parsed in the future (call again); + * `*json` will be NULL if we need to be called again, + * and non-NULL if we are done. + * #GNUNET_NO is request incomplete or invalid + * (error message was generated) + * #GNUNET_SYSERR on internal error + * (we could not even queue an error message, + * close HTTP session with MHD_NO) + */ +enum GNUNET_GenericReturnValue +TALER_MHD2_parse_post_json (struct MHD_Request *request, + void **con_cls, + const char *upload_data, + size_t *upload_data_size, + json_t **json); + + +/** + * Function called whenever we are done with a request + * to clean up our state. + * + * @param con_cls value as it was left by + * #TALER_MHD2_parse_post_json(), to be cleaned up + */ +void +TALER_MHD2_parse_post_cleanup_callback (void *con_cls); + + +/** + * Parse JSON object into components based on the given field + * specification. If parsing fails, we return an HTTP + * status code of 400 (#MHD_HTTP_BAD_REQUEST). + * + * @param request the request to send an error response to + * @param root the JSON node to start the navigation at. + * @param spec field specification for the parser + * @return + * #GNUNET_YES if navigation was successful (caller is responsible + * for freeing allocated variable-size data using + * GNUNET_JSON_parse_free() when done) + * #GNUNET_NO if json is malformed, error response was generated + * #GNUNET_SYSERR on internal error + */ +enum GNUNET_GenericReturnValue +TALER_MHD2_parse_json_data (struct MHD_Request *request, + const json_t *root, + struct GNUNET_JSON_Specification *spec); + + +/** + * Parse JSON object that we (the server!) generated into components based on + * the given field specification. The difference to + * #TALER_MHD2_parse_json_data() is that this function will fail + * with an HTTP failure of 500 (internal server error) in case + * parsing fails, instead of blaming it on the client with a + * 400 (#MHD_HTTP_BAD_REQUEST). + * + * @param request the request to send an error response to + * @param root the JSON node to start the navigation at. + * @param spec field specification for the parser + * @return + * #GNUNET_YES if navigation was successful (caller is responsible + * for freeing allocated variable-size data using + * GNUNET_JSON_parse_free() when done) + * #GNUNET_NO if json is malformed, error response was generated + * #GNUNET_SYSERR on internal error + */ +enum GNUNET_GenericReturnValue +TALER_MHD2_parse_internal_json_data (struct MHD_Request *request, + const json_t *root, + struct GNUNET_JSON_Specification *spec); + + +/** + * Parse JSON array into components based on the given field + * specification. Generates error response on parse errors. + * + * @param request the request to send an error response to + * @param root the JSON node to start the navigation at. + * @param[in,out] spec field specification for the parser + * @param ... -1-terminated list of array offsets of type 'int' + * @return + * #GNUNET_YES if navigation was successful (caller is responsible + * for freeing allocated variable-size data using + * GNUNET_JSON_parse_free() when done) + * #GNUNET_NO if json is malformed, error response was generated + * #GNUNET_SYSERR on internal error + */ +enum GNUNET_GenericReturnValue +TALER_MHD2_parse_json_array (struct MHD_Request *request, + const json_t *root, + struct GNUNET_JSON_Specification *spec, + ...); + + +/** + * Extract optional "timeout_ms" argument from request. + * + * @param request the MHD request + * @param[out] expiration set to #GNUNET_TIME_UNIT_ZERO_ABS if there was no timeout, + * the current time plus the value given under "timeout_ms" otherwise + * @return #GNUNET_OK on success, #GNUNET_NO if an + * error was returned on @a request (caller should return #MHD_YES) and + * #GNUNET_SYSERR if we failed to return an error (caller should return #MHD_NO) + */ +enum GNUNET_GenericReturnValue +TALER_MHD2_parse_request_arg_timeout (struct MHD_Request *request, + struct GNUNET_TIME_Absolute *expiration); + + +/** + * Extract optional "timeout_ms" argument from request. + * Macro that *returns* #MHD_YES/#MHD_NO if the "timeout_ms" + * argument existed but failed to parse. + * + * @param request the MHD request + * @param[out] expiration set to #GNUNET_TIME_UNIT_ZERO_ABS if there was no timeout, + * the current time plus the value given under "timeout_ms" otherwise + */ +#define TALER_MHD2_parse_request_timeout(request,expiration) \ + do { \ + switch (TALER_MHD2_parse_request_arg_timeout (request, \ + expiration)) \ + { \ + case GNUNET_SYSERR: \ + GNUNET_break (0); \ + return MHD_NO; \ + case GNUNET_NO: \ + GNUNET_break_op (0); \ + case GNUNET_OK: \ + break; \ + } \ + } while (0) + + +/** + * Extract optional timestamp argument from request. + * + * @param request the MHD request + * @param fname name of the argument to parse + * @param[out] ts set to #GNUNET_TIME_UNIT_ZERO_TS if there was no timestamp + * @return #GNUNET_OK on success, #GNUNET_NO if an + * error was returned on @a request (caller should return #MHD_YES) and + * #GNUNET_SYSERR if we failed to return an error (caller should return #MHD_NO) + */ +enum GNUNET_GenericReturnValue +TALER_MHD2_parse_request_arg_timestamp (struct MHD_Request *request, + const char *fname, + struct GNUNET_TIME_Timestamp *ts); + + +/** + * Extract optional timestamp argument from request. + * Macro that *returns* #MHD_YES/#MHD_NO if the timestamp + * argument existed but failed to parse. + * + * @param request the MHD request + * @param fname name of the argument + * @param[out] ts set to #GNUNET_TIME_UNIT_ZERO_TS if there was no timestamp + */ +#define TALER_MHD2_parse_request_timestamp(request,fname,ts) \ + do { \ + switch (TALER_MHD2_parse_request_arg_timestamp (request, \ + fname, \ + ts)) \ + { \ + case GNUNET_SYSERR: \ + GNUNET_break (0); \ + return MHD_NO; \ + case GNUNET_NO: \ + GNUNET_break_op (0); \ + case GNUNET_OK: \ + break; \ + } \ + } while (0) + + +/** + * Extract optional "yes/no/all" argument from request. + * Macro that *returns* #MHD_YES/#MHD_NO if the + * argument existed but failed to parse. + * + * @param request the MHD request + * @param name name of the query parameter to parse + * @param def default value to set if absent + * @param[out] ret set to the yes/no/all value + */ +#define TALER_MHD2_parse_request_yna(request,name,def,ret) \ + do { \ + if (! (TALER_arg_to_yna (request, \ + name, \ + def, \ + ret)) ) \ + { \ + GNUNET_break_op (0); \ + return TALER_MHD2_reply_with_error ( \ + request, \ + MHD_HTTP_BAD_REQUEST, \ + TALER_EC_GENERIC_PARAMETER_MALFORMED, \ + name); \ + } \ + } while (0) + +/** + * Extract optional numeric limit argument from request. + * + * @param request the MHD request + * @param name name of the query parameter + * @param[out] off set to the offset, unchanged if the + * option was not given + * @return #GNUNET_OK on success, + * #GNUNET_NO if an error was returned on @a request (caller should return #MHD_YES) and + * #GNUNET_SYSERR if we failed to return an error (caller should return #MHD_NO) + */ +enum GNUNET_GenericReturnValue +TALER_MHD2_parse_request_arg_number (struct MHD_Request *request, + const char *name, + uint64_t *off); + + +/** + * Extract optional numeric argument from request. + * Macro that *returns* #MHD_YES/#MHD_NO if the + * requested argument existed but failed to parse. + * + * @param request the MHD request + * @param name name of the argument to parse + * @param[out] off set to the given numeric value, + * unchanged if value was not specified + */ +#define TALER_MHD2_parse_request_number(request,name,off) \ + do { \ + switch (TALER_MHD2_parse_request_arg_number (request, \ + name, \ + off)) \ + { \ + case GNUNET_SYSERR: \ + GNUNET_break (0); \ + return MHD_NO; \ + case GNUNET_NO: \ + GNUNET_break_op (0); \ + case GNUNET_OK: \ + break; \ + } \ + } while (0) + + +/** + * Extract optional signed numeric limit argument from request. + * + * @param request the MHD request + * @param name name of the query parameter + * @param[out] val set to the signed value, unchanged if the + * option was not given + * @return #GNUNET_OK on success, + * #GNUNET_NO if an error was returned on @a request (caller should return #MHD_YES) and + * #GNUNET_SYSERR if we failed to return an error (caller should return #MHD_NO) + */ +enum GNUNET_GenericReturnValue +TALER_MHD2_parse_request_arg_snumber (struct MHD_Request *request, + const char *name, + int64_t *val); + + +/** + * Extract optional numeric argument from request. + * Macro that *returns* #MHD_YES/#MHD_NO if the + * requested argument existed but failed to parse. + * + * @param request the MHD request + * @param name name of the argument to parse + * @param[out] val set to the given numeric value, + * unchanged if value was not specified + */ +#define TALER_MHD2_parse_request_snumber(request,name,val) \ + do { \ + switch (TALER_MHD2_parse_request_arg_snumber (request, \ + name, \ + val)) \ + { \ + case GNUNET_SYSERR: \ + GNUNET_break (0); \ + return MHD_NO; \ + case GNUNET_NO: \ + GNUNET_break_op (0); \ + case GNUNET_OK: \ + break; \ + } \ + } while (0) + + +/** + * Extract optional amount argument from request. + * + * @param request the MHD request + * @param name name of the query parameter + * @param[out] val set to the amount, unchanged if the + * option was not given + * @return #GNUNET_OK on success, + * #GNUNET_NO if an error was returned on @a request (caller should return #MHD_YES) and + * #GNUNET_SYSERR if we failed to return an error (caller should return #MHD_NO) + */ +enum GNUNET_GenericReturnValue +TALER_MHD2_parse_request_arg_amount (struct MHD_Request *request, + const char *name, + struct TALER_Amount *val); + + +/** + * Extract optional amount argument from request. Macro that *returns* + * #MHD_YES/#MHD_NO if the requested argument existed but failed to parse. + * + * @param request the MHD request + * @param name name of the argument to parse + * @param[out] val set to the given amount, + * unchanged if value was not specified + */ +#define TALER_MHD2_parse_request_amount(request,name,val) \ + do { \ + switch (TALER_MHD2_parse_request_arg_amount (request, \ + name, \ + val)) \ + { \ + case GNUNET_SYSERR: \ + GNUNET_break (0); \ + return MHD_NO; \ + case GNUNET_NO: \ + GNUNET_break_op (0); \ + case GNUNET_OK: \ + break; \ + } \ + } while (0) + + +/** + * Extract fixed-size base32crockford encoded data from request argument. + * + * Queues an error response to the request if the parameter is missing or + * invalid. + * + * @param request the MHD request + * @param param_name the name of the parameter with the key + * @param[out] out_data pointer to store the result + * @param out_size expected size of @a out_data + * @param[out] present set to true if argument was found + * @return + * #GNUNET_YES if the the argument is present + * #GNUNET_NO if the argument is malformed + * #GNUNET_SYSERR on internal error (error response could not be sent) + */ +enum GNUNET_GenericReturnValue +TALER_MHD2_parse_request_arg_data (struct MHD_Request *request, + const char *param_name, + void *out_data, + size_t out_size, + bool *present); + + +/** + * Extract fixed-size base32crockford encoded data from request header. + * + * Queues an error response to the request if the parameter is missing or + * invalid. + * + * @param request the MHD request + * @param header_name the name of the HTTP header with the value + * @param[out] out_data pointer to store the result + * @param out_size expected size of @a out_data + * @param[out] present set to true if argument was found + * @return + * #GNUNET_YES if the the argument is present + * #GNUNET_NO if the argument is malformed + * #GNUNET_SYSERR on internal error (error response could not be sent) + */ +enum GNUNET_GenericReturnValue +TALER_MHD2_parse_request_header_data (struct MHD_Request *request, + const char *header_name, + void *out_data, + size_t out_size, + bool *present); + +/** + * Extract fixed-size base32crockford encoded data from request. + * + * @param request the MHD request + * @param name the name of the parameter with the key + * @param[out] val pointer to store the result, type must determine size + * @param[in,out] required pass true to require presence of this argument; if 'false' + * set to true if the argument was found + * @return + * #GNUNET_YES if the the argument is present + * #GNUNET_NO if the argument is absent or malformed + * #GNUNET_SYSERR on internal error (error response could not be sent) + */ +#define TALER_MHD2_parse_request_arg_auto(request,name,val,required) \ + do { \ + bool p; \ + switch (TALER_MHD2_parse_request_arg_data (request, name, \ + val, sizeof (*val), &p)) \ + { \ + case GNUNET_SYSERR: \ + GNUNET_break (0); \ + return MHD_NO; \ + case GNUNET_NO: \ + GNUNET_break_op (0); \ + return MHD_YES; \ + case GNUNET_OK: \ + if (required & (! p)) \ + { \ + GNUNET_break_op (0); \ + return TALER_MHD2_reply_with_error ( \ + request, \ + MHD_HTTP_BAD_REQUEST, \ + TALER_EC_GENERIC_PARAMETER_MISSING, \ + name); \ + } \ + required = p; \ + break; \ + } \ + } while (0) + + +/** + * Extract required fixed-size base32crockford encoded data from request. + * + * @param request the MHD request + * @param name the name of the parameter with the key + * @param[out] val pointer to store the result, type must determine size + * @return + * #GNUNET_YES if the the argument is present + * #GNUNET_NO if the argument is absent or malformed + * #GNUNET_SYSERR on internal error (error response could not be sent) + */ +#define TALER_MHD2_parse_request_arg_auto_t(request,name,val) \ + do { \ + bool b = true; \ + TALER_MHD2_parse_request_arg_auto (request,name,val,b); \ + } while (0) + +/** + * Extract fixed-size base32crockford encoded data from request. + * + * @param request the MHD request + * @param name the name of the header with the key + * @param[out] val pointer to store the result, type must determine size + * @param[in,out] required pass true to require presence of this argument; if 'false' + * set to true if the argument was found + * @return + * #GNUNET_YES if the the argument is present + * #GNUNET_NO if the argument is absent or malformed + * #GNUNET_SYSERR on internal error (error response could not be sent) + */ +#define TALER_MHD2_parse_request_header_auto(request,name,val,required) \ + do { \ + bool p; \ + switch (TALER_MHD2_parse_request_header_data (request, name, \ + val, sizeof (*val), &p)) \ + { \ + case GNUNET_SYSERR: \ + GNUNET_break (0); \ + return MHD_NO; \ + case GNUNET_NO: \ + GNUNET_break_op (0); \ + return MHD_YES; \ + case GNUNET_OK: \ + if (required & (! p)) \ + return TALER_MHD2_reply_with_error ( \ + request, \ + MHD_HTTP_BAD_REQUEST, \ + TALER_EC_GENERIC_PARAMETER_MISSING, \ + name); \ + required = p; \ + break; \ + } \ + } while (0) + + +/** + * Extract required fixed-size base32crockford encoded data from request. + * + * @param request the MHD request + * @param name the name of the header with the key + * @param[out] val pointer to store the result, type must determine size + * @return + * #GNUNET_YES if the the argument is present + * #GNUNET_NO if the argument is absent or malformed + * #GNUNET_SYSERR on internal error (error response could not be sent) + */ +#define TALER_MHD2_parse_request_header_auto_t(request,name,val) \ + do { \ + bool b = true; \ + TALER_MHD2_parse_request_header_auto (request,name,val,b); \ + } while (0) + + +/** + * Check that the 'Content-Length' header is giving + * a length below @a max_len. If not, return an + * appropriate error response and return the + * correct #MHD_YES/#MHD_NO value from this function. + * + * @param request the MHD request + * @param max_len maximum allowed content length + * @return + * #GNUNET_YES if the the argument is present + * #GNUNET_NO if the argument is absent or malformed + * #GNUNET_SYSERR on internal error (error response could not be sent) + */ +enum GNUNET_GenericReturnValue +TALER_MHD2_check_content_length_ (struct MHD_Request *request, + unsigned long long max_len); + + +/** + * Check that the 'Content-Length' header is giving + * a length below @a max_len. If not, return an + * appropriate error response and return the + * correct #MHD_YES/#MHD_NO value from this function. + * + * @param request the MHD request + * @param max_len maximum allowed content length + */ +#define TALER_MHD2_check_content_length(request,max_len) \ + do { \ + switch (TALER_MHD2_check_content_length_ (request, max_len)) \ + { \ + case GNUNET_SYSERR: \ + GNUNET_break (0); \ + return MHD_NO; \ + case GNUNET_NO: \ + GNUNET_break_op (0); \ + return MHD_YES; \ + case GNUNET_OK: \ + break; \ + } \ + } while (0) + +#endif + + +/** + * Function called for logging by MHD. + * + * @param cls closure, NULL + * @param fm format string (`printf()`-style) + * @param ap arguments to @a fm + */ +void +TALER_MHD_handle_logs (void *cls, + const char *fm, + va_list ap); + + +/** + * Function called on each successfully bound listen + * socket by #TALER_MHD_listen_bind(). + * + * @param cls closure + * @param fd bound listen socket (must be used and eventually + * closed by the callee). Never -1. + */ +typedef void +(*TALER_MHD_ListenSocketCallback)(void *cls, + int fd); + + +/** + * Bind a listen socket to the UNIX domain path, + * or the TCP port(s) and IP address(es) configured, + * or return to @a cb the inherited sockets from systemd, + * all depending on what was specified in @a cfg in + * the section named @a section. + * + * @param cfg configuration to parse + * @param section configuration section to use + * @param cb function to call with each bound socket + * @param cb_cls closure for @a cb + * @return #GNUNET_OK on success (all configured sockets were bound) + * #GNUNET_NO if some configured binding failed but the config is OK, + * note that some listen sockets may have been created + * #GNUNET_SYSERR if the configuration is invalid + */ +enum GNUNET_GenericReturnValue +TALER_MHD_listen_bind (const struct GNUNET_CONFIGURATION_Handle *cfg, + const char *section, + TALER_MHD_ListenSocketCallback cb, + void *cb_cls); + + +/** + * Start to run an event loop for @a daemon. + * Only one daemon can be running per process + * using this API. + * + * @param daemon the MHD service to run + */ +void +TALER_MHD2_daemon_start (struct MHD_Daemon *daemon); + + +/** + * Stop running the event loops of all MHD daemons. + */ +void +TALER_MHD2_daemons_halt (void); + + +/** + * Stop accepting new connections on all MHD daemons + * (and close the listen sockets). + */ +void +TALER_MHD2_daemons_quiesce (void); + +/** + * Wake up all daemons. + */ +void +TALER_MHD2_daemons_trigger (void); + +/** + * Destroy all state associated with all MHD daemons. + */ +void +TALER_MHD2_daemons_destroy (void); + + +/** + * Prepared responses for legal documents + * (terms of service, privacy policy). + */ +struct TALER_MHD2_Legal; + + +/** + * Load set of legal documents as specified in @a cfg in section @a section + * where the Etag is given under the @a tagoption and the directory under + * the @a diroption. + * + * @param cfg configuration to use + * @param section section to load values from + * @param diroption name of the option with the + * path to the legal documents + * @param tagoption name of the files to use + * for the legal documents and the Etag + * @return NULL on error + */ +struct TALER_MHD2_Legal * +TALER_MHD2_legal_load (const struct GNUNET_CONFIGURATION_Handle *cfg, + const char *section, + const char *diroption, + const char *tagoption); + + +/** + * Free set of legal documents + * + * @param legal legal documents to free + */ +void +TALER_MHD2_legal_free (struct TALER_MHD2_Legal *legal); + + +/** + * Generate a response with a legal document in + * the format and language of the user's choosing. + * + * @param conn HTTP request to handle + * @param legal legal document to serve + * @return MHD action + */ +const struct MHD_Action * +TALER_MHD2_reply_legal (struct MHD_Request *conn, + struct TALER_MHD2_Legal *legal); + + +/** + * Send back a "204 No Content" response with headers + * for the CORS pre-flight request. + * + * @param request the MHD request + * @return MHD action + */ +const struct MHD_Action * +TALER_MHD2_reply_cors_preflight (struct MHD_Request *request); + + +/** + * Load SPA files from @a dir + * + * @param pd project data to use to determine the parent directory + * @param dir directory suffix to append to our data directory with the location of the files of the SPA + * @return handle to serve static files from @a dir + */ +struct TALER_MHD2_Spa * +TALER_MHD2_spa_load (const struct GNUNET_OS_ProjectData *pd, + const char *dir); + + +/** + * Release resources used by SPA handler. + * + * @param[in] spa data structure to release + */ +void +TALER_MHD2_spa_free (struct TALER_MHD2_Spa *spa); + + +/** + * Handle HTTP request for files in a @a spa. Generates + * a 404 if no file at @a path does exists. + * + * @param spa the SPA to serve files from + * @param request HTTP request to return data on + * @param path request path to match against the @a spa + * @return MHD action + */ +const struct MHD_Action * +TALER_MHD2_spa_handler (const struct TALER_MHD2_Spa *spa, + struct MHD_Request *request, + const char *path); + + +#endif diff --git a/src/meson.build b/src/meson.build @@ -1,6 +1,7 @@ subdir('include') subdir('util') subdir('syncdb') +subdir('mhd2') subdir('sync') subdir('lib') subdir('testing') diff --git a/src/mhd2/meson.build b/src/mhd2/meson.build @@ -0,0 +1,25 @@ +# This build file is in the public domain + +libtalermhd2 = static_library( + 'talermhd2', + [ + 'mhd2.c', + 'mhd2_legal.c', + 'mhd2_responses.c', + 'mhd2_run.c', + 'mhd2_spa.c', + ], + dependencies: [ + talerutil_dep, + talerjson_dep, + gnunetutil_dep, + gnunetjson_dep, + json_dep, + mhd2_dep, + zlib_dep, + ], + include_directories: [incdir, configuration_inc], + install: false, +) + +libtalermhd2_dep = declare_dependency(link_with: libtalermhd2) diff --git a/src/mhd2/mhd2.c b/src/mhd2/mhd2.c @@ -0,0 +1,109 @@ +/* + This file is part of TALER + Copyright (C) 2014-2025 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 mhd2.c + * @brief MHD utility functions (used by the merchant backend) + * @author Christian Grothoff + */ +#include "taler/taler_util.h" +#include "taler/taler_mhd2_lib.h" + + +enum GNUNET_GenericReturnValue +TALER_MHD2_is_https (struct MHD_Request *request) +{ + struct MHD_StringNullable forwarded_proto; + union MHD_RequestInfoFixedData ci; + union MHD_DaemonInfoFixedData di; + + if ( (MHD_NO != + MHD_request_get_value (request, + MHD_VK_HEADER, + "X-Forwarded-Proto", + &forwarded_proto)) && + (NULL != forwarded_proto.cstr) ) + { + if (0 == strcasecmp (forwarded_proto.cstr, + "https")) + return GNUNET_YES; + if (0 == strcasecmp (forwarded_proto.cstr, + "http")) + return GNUNET_NO; + GNUNET_break (0); + return GNUNET_SYSERR; + } + /* likely not reverse proxy, figure out if we are + http by asking MHD */ + if (MHD_SC_OK != + MHD_request_get_info_fixed (request, + MHD_REQUEST_INFO_FIXED_DAEMON, + &ci)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (MHD_SC_OK != + MHD_daemon_get_info_fixed (ci.v_daemon, + MHD_DAEMON_INFO_FIXED_TLS_BACKEND, + &di)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (MHD_TLS_BACKEND_NONE != di.v_tls_backend) + return GNUNET_YES; + return GNUNET_NO; +} + + +bool +TALER_MHD2_arg_to_yna (struct MHD_Request *request, + const char *arg, + enum TALER_EXCHANGE_YesNoAll default_val, + enum TALER_EXCHANGE_YesNoAll *yna) +{ + struct MHD_StringNullable nstr; + + if ( (MHD_NO == + MHD_request_get_value (request, + MHD_VK_URI_QUERY_PARAM, + arg, + &nstr)) || + (NULL == nstr.cstr) ) + { + *yna = default_val; + return true; + } + if (0 == strcasecmp (nstr.cstr, + "yes")) + { + *yna = TALER_EXCHANGE_YNA_YES; + return true; + } + if (0 == strcasecmp (nstr.cstr, + "no")) + { + *yna = TALER_EXCHANGE_YNA_NO; + return true; + } + if (0 == strcasecmp (nstr.cstr, + "all")) + { + *yna = TALER_EXCHANGE_YNA_ALL; + return true; + } + return false; +} diff --git a/src/mhd2/mhd2_legal.c b/src/mhd2/mhd2_legal.c @@ -0,0 +1,702 @@ +/* + 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 mhd2_legal.c + * @brief API for returning legal documents based on client language + * and content type preferences + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd2.h> +#include "taler/taler_util.h" +#include "taler/taler_mhd2_lib.h" + +/** + * How long should browsers/proxies cache the "legal" replies? + */ +#define MAX_TERMS_CACHING GNUNET_TIME_UNIT_DAYS + +/** + * HTTP header with the version of the terms of service. + */ +#define TALER_TERMS_VERSION "Taler-Terms-Version" + +/** + * Entry in the terms-of-service array. + */ +struct Terms +{ + /** + * Kept in a DLL. + */ + struct Terms *prev; + + /** + * Kept in a DLL. + */ + struct Terms *next; + + /** + * Mime type of the terms. + */ + const char *mime_type; + + /** + * The terms (NOT 0-terminated!), mmap()'ed. Do not free, + * use munmap() instead. + */ + void *terms; + + /** + * The desired language. + */ + char *language; + + /** + * deflated @e terms, to return if client supports deflate compression. + * malloc()'ed. NULL if @e terms does not compress. + */ + void *compressed_terms; + + /** + * Etag we use for this response. + */ + char *terms_etag; + + /** + * Number of bytes in @e terms. + */ + size_t terms_size; + + /** + * Number of bytes in @e compressed_terms. + */ + size_t compressed_terms_size; + + /** + * Sorting key by format preference in case + * everything else is equal. Higher is preferred. + */ + unsigned int priority; + +}; + + +/** + * Prepared responses for legal documents + * (terms of service, privacy policy). + */ +struct TALER_MHD2_Legal +{ + /** + * DLL of terms of service. + */ + struct Terms *terms_head; + + /** + * DLL of terms of service. + */ + struct Terms *terms_tail; + + /** + * Etag to use for the terms of service (= version). + */ + char *terms_version; +}; + + +const struct MHD_Action * +TALER_MHD2_reply_legal (struct MHD_Request *request, + struct TALER_MHD2_Legal *legal) +{ + /* Default terms of service if none are configured */ + static struct Terms none = { + .mime_type = "text/plain", + .terms = (void *) "not configured", + .language = (void *) "en", + .terms_size = strlen ("not configured") + }; + struct MHD_Response *resp; + struct Terms *t; + struct GNUNET_TIME_Absolute a; + struct GNUNET_TIME_Timestamp m; + char dat[128]; + char *langs; + + t = NULL; + langs = NULL; + a = GNUNET_TIME_relative_to_absolute (MAX_TERMS_CACHING); + m = GNUNET_TIME_absolute_to_timestamp (a); + /* Round up to next full day to ensure the expiration + time does not become a fingerprint! */ + a = GNUNET_TIME_absolute_round_down (a, + MAX_TERMS_CACHING); + a = GNUNET_TIME_absolute_add (a, + MAX_TERMS_CACHING); + TALER_MHD2_get_date_string (m.abs_time, + dat); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Setting '%s' header to '%s'\n", + MHD_HTTP_HEADER_EXPIRES, + dat); + if (NULL == legal) + { + t = &none; + goto return_t; + } + + if (NULL != legal) + { + struct MHD_StringNullable mimen; + struct MHD_StringNullable langn; + const char *mime; + const char *lang; + double best_mime_q = 0.0; + double best_lang_q = 0.0; + + if ( (MHD_NO == + MHD_request_get_value (request, + MHD_VK_HEADER, + MHD_HTTP_HEADER_ACCEPT, + &mimen)) || + (NULL == mimen.cstr) ) + mime = "text/plain"; + else + mime = mimen.cstr; + if ( (MHD_NO == + MHD_request_get_value (request, + MHD_VK_HEADER, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE, + &langn)) || + (NULL == langn.cstr) ) + lang = "en"; + else + lang = langn.cstr; + /* Find best match: must match mime type (if possible), and if + mime type matches, ideally also language */ + for (struct Terms *p = legal->terms_head; + NULL != p; + p = p->next) + { + double q; + + q = TALER_pattern_matches (mime, + p->mime_type); + if (q > best_mime_q) + best_mime_q = q; + } + for (struct Terms *p = legal->terms_head; + NULL != p; + p = p->next) + { + double q; + + q = TALER_pattern_matches (mime, + p->mime_type); + if (q < best_mime_q) + continue; + if (NULL == langs) + { + langs = GNUNET_strdup (p->language); + } + else if (NULL == strstr (langs, + p->language)) + { + char *tmp = langs; + + GNUNET_asprintf (&langs, + "%s,%s", + tmp, + p->language); + GNUNET_free (tmp); + } + q = TALER_pattern_matches (lang, + p->language); + if (NULL != t) + { + if (q < best_lang_q) + continue; + /* Client expressed no preference between these two formats, + so fall back to our own ranking instead of letting the + last entry in the list win. */ + if ( (q <= best_lang_q) && + (p->priority <= t->priority) ) + continue; + } + best_lang_q = q; + t = p; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Best match for %s/%s: %s / %s\n", + lang, + mime, + (NULL != t) ? t->mime_type : "<none>", + (NULL != t) ? t->language : "<none>"); + } + + if (NULL != t) + { + struct MHD_StringNullable etag; + + if ( (MHD_NO != + MHD_request_get_value (request, + MHD_VK_HEADER, + MHD_HTTP_HEADER_IF_NONE_MATCH, + &etag)) && + (NULL != etag.cstr) && + (NULL != t->terms_etag) && + (0 == strcasecmp (etag.cstr, + t->terms_etag)) ) + { + resp = MHD_response_from_empty (MHD_HTTP_STATUS_NOT_MODIFIED); + GNUNET_break (MHD_SC_OK == + MHD_RESPONSE_SET_OPTIONS ( + resp, + MHD_R_OPTION_HEAD_ONLY_RESPONSE (true))); + TALER_MHD2_add_global_headers (resp, + true); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + MHD_HTTP_HEADER_EXPIRES, + dat)); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + MHD_HTTP_HEADER_ETAG, + t->terms_etag)); + if (NULL != legal) + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + TALER_TERMS_VERSION, + legal->terms_version)); + return MHD_action_from_response (request, + resp); + } + } + + if (NULL == t) + t = &none; /* 501 if not configured */ + +return_t: + /* try to compress the response */ + resp = NULL; + if ( (MHD_YES == + TALER_MHD2_can_compress (request)) && + (NULL != t->compressed_terms) ) + { + resp = MHD_response_from_buffer_static (t == &none + ? MHD_HTTP_STATUS_NOT_IMPLEMENTED + : MHD_HTTP_STATUS_OK, + t->compressed_terms_size, + t->compressed_terms); + if (MHD_SC_OK != + MHD_response_add_header (resp, + MHD_HTTP_HEADER_CONTENT_ENCODING, + "deflate")) + { + GNUNET_break (0); + MHD_response_destroy (resp); + resp = NULL; + } + } + if (NULL == resp) + { + /* could not generate compressed response, return uncompressed */ + resp = MHD_response_from_buffer_static (t == &none + ? MHD_HTTP_STATUS_NOT_IMPLEMENTED + : MHD_HTTP_STATUS_OK, + t->terms_size, + (void *) t->terms); + } + TALER_MHD2_add_global_headers (resp, + true); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + MHD_HTTP_HEADER_EXPIRES, + dat)); + if (NULL != langs) + { + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + "Avail-Languages", + langs)); + GNUNET_free (langs); + } + /* Set cache control headers: our response varies depending on these headers */ + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + MHD_HTTP_HEADER_VARY, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE "," + MHD_HTTP_HEADER_ACCEPT "," + MHD_HTTP_HEADER_ACCEPT_ENCODING)); + /* Information is always public, revalidate after 10 days */ + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + MHD_HTTP_HEADER_CACHE_CONTROL, + "public,max-age=864000")); + if (NULL != t->terms_etag) + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + MHD_HTTP_HEADER_ETAG, + t->terms_etag)); + if (NULL != legal) + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + TALER_TERMS_VERSION, + legal->terms_version)); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + t->mime_type)); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + MHD_HTTP_HEADER_CONTENT_LANGUAGE, + t->language)); + return MHD_action_from_response (request, + resp); +} + + +/** + * Load all the terms of service from @a path under language @a lang + * from file @a name + * + * @param[in,out] legal where to write the result + * @param path where the terms are found + * @param lang which language directory to crawl + * @param name specific file to access + */ +static void +load_terms (struct TALER_MHD2_Legal *legal, + const char *path, + const char *lang, + const char *name) +{ + static struct MimeMap + { + const char *ext; + const char *mime; + unsigned int priority; + } mm[] = { + { .ext = ".txt", .mime = "text/plain", .priority = 150 }, + { .ext = ".html", .mime = "text/html", .priority = 100 }, + { .ext = ".htm", .mime = "text/html", .priority = 99 }, + { .ext = ".md", .mime = "text/markdown", .priority = 50 }, + { .ext = ".pdf", .mime = "application/pdf", .priority = 25 }, + { .ext = ".jpg", .mime = "image/jpeg" }, + { .ext = ".jpeg", .mime = "image/jpeg" }, + { .ext = ".png", .mime = "image/png" }, + { .ext = ".gif", .mime = "image/gif" }, + { .ext = ".epub", .mime = "application/epub+zip", .priority = 10 }, + { .ext = ".xml", .mime = "text/xml", .priority = 10 }, + { .ext = NULL, .mime = NULL } + }; + const char *ext = strrchr (name, '.'); + const char *mime; + unsigned int priority; + + if (NULL == ext) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unsupported file `%s' in directory `%s/%s': lacks extension\n", + name, + path, + lang); + return; + } + if ( (NULL == legal->terms_version) || + (0 != strncmp (legal->terms_version, + name, + ext - name)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Filename `%s' does not match Etag `%s' in directory `%s/%s'. Ignoring it.\n", + name, + legal->terms_version, + path, + lang); + return; + } + mime = NULL; + for (unsigned int i = 0; NULL != mm[i].ext; i++) + if (0 == strcasecmp (mm[i].ext, + ext)) + { + mime = mm[i].mime; + priority = mm[i].priority; + break; + } + if (NULL == mime) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unsupported file extension `%s' of file `%s' in directory `%s/%s'\n", + ext, + name, + path, + lang); + return; + } + /* try to read the file with the terms of service */ + { + struct stat st; + char *fn; + int fd; + + GNUNET_asprintf (&fn, + "%s/%s/%s", + path, + lang, + name); + fd = open (fn, O_RDONLY); + if (-1 == fd) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, + "open", + fn); + GNUNET_free (fn); + return; + } + if (0 != fstat (fd, &st)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, + "fstat", + fn); + GNUNET_break (0 == close (fd)); + GNUNET_free (fn); + return; + } + if (SIZE_MAX < ((unsigned long long) st.st_size)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, + "fstat-size", + fn); + GNUNET_break (0 == close (fd)); + GNUNET_free (fn); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Loading legal information from file `%s'\n", + fn); + { + void *buf; + size_t bsize; + + bsize = (size_t) st.st_size; + buf = mmap (NULL, + bsize, + PROT_READ, + MAP_SHARED, + fd, + 0); + if (MAP_FAILED == buf) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, + "mmap", + fn); + GNUNET_break (0 == close (fd)); + GNUNET_free (fn); + return; + } + GNUNET_break (0 == close (fd)); + GNUNET_free (fn); + + /* insert into global list of terms of service */ + { + struct Terms *t; + struct GNUNET_HashCode hc; + + GNUNET_CRYPTO_hash (buf, + bsize, + &hc); + t = GNUNET_new (struct Terms); + t->mime_type = mime; + t->terms = buf; + t->language = GNUNET_strdup (lang); + t->terms_size = bsize; + t->priority = priority; + t->terms_etag + = GNUNET_STRINGS_data_to_string_alloc (&hc, + sizeof (hc) / 2); + buf = GNUNET_memdup (t->terms, + t->terms_size); + if (TALER_MHD2_body_compress (&buf, + &bsize)) + { + t->compressed_terms = buf; + t->compressed_terms_size = bsize; + } + else + { + GNUNET_free (buf); + } + { + struct Terms *prev = NULL; + + for (struct Terms *pos = legal->terms_head; + NULL != pos; + pos = pos->next) + { + if (pos->priority < priority) + break; + prev = pos; + } + GNUNET_CONTAINER_DLL_insert_after (legal->terms_head, + legal->terms_tail, + prev, + t); + } + } + } + } +} + + +/** + * Load all the terms of service from @a path under language @a lang. + * + * @param[in,out] legal where to write the result + * @param path where the terms are found + * @param lang which language directory to crawl + */ +static void +load_language (struct TALER_MHD2_Legal *legal, + const char *path, + const char *lang) +{ + char *dname; + DIR *d; + + GNUNET_asprintf (&dname, + "%s/%s", + path, + lang); + d = opendir (dname); + if (NULL == d) + { + GNUNET_free (dname); + return; + } + for (struct dirent *de = readdir (d); + NULL != de; + de = readdir (d)) + { + const char *fn = de->d_name; + + if (fn[0] == '.') + continue; + load_terms (legal, + path, + lang, + fn); + } + GNUNET_break (0 == closedir (d)); + GNUNET_free (dname); +} + + +struct TALER_MHD2_Legal * +TALER_MHD2_legal_load (const struct GNUNET_CONFIGURATION_Handle *cfg, + const char *section, + const char *diroption, + const char *tagoption) +{ + struct TALER_MHD2_Legal *legal; + char *path; + DIR *d; + + legal = GNUNET_new (struct TALER_MHD2_Legal); + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + tagoption, + &legal->terms_version)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, + section, + tagoption); + GNUNET_free (legal); + return NULL; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_filename (cfg, + section, + diroption, + &path)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, + section, + diroption); + GNUNET_free (legal->terms_version); + GNUNET_free (legal); + return NULL; + } + d = opendir (path); + if (NULL == d) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING, + section, + diroption, + "Could not open directory"); + GNUNET_free (legal->terms_version); + GNUNET_free (legal); + GNUNET_free (path); + return NULL; + } + for (struct dirent *de = readdir (d); + NULL != de; + de = readdir (d)) + { + const char *lang = de->d_name; + + if (lang[0] == '.') + continue; + if (0 == strcmp (lang, + "locale")) + continue; + load_language (legal, + path, + lang); + } + GNUNET_break (0 == closedir (d)); + GNUNET_free (path); + return legal; +} + + +void +TALER_MHD2_legal_free (struct TALER_MHD2_Legal *legal) +{ + struct Terms *t; + if (NULL == legal) + return; + while (NULL != (t = legal->terms_head)) + { + GNUNET_free (t->language); + GNUNET_free (t->compressed_terms); + if (0 != munmap (t->terms, t->terms_size)) + GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, + "munmap"); + GNUNET_CONTAINER_DLL_remove (legal->terms_head, + legal->terms_tail, + t); + GNUNET_free (t->terms_etag); + GNUNET_free (t); + } + GNUNET_free (legal->terms_version); + GNUNET_free (legal); +} diff --git a/src/mhd2/mhd2_responses.c b/src/mhd2/mhd2_responses.c @@ -0,0 +1,462 @@ +/* + This file is part of TALER + Copyright (C) 2014-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 mhd2_responses.c + * @brief API for generating HTTP replies + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "platform.h" +#include <zlib.h> +#include "taler/taler_util.h" +#include "taler/taler_mhd2_lib.h" + + +/** + * Global options for response generation. + */ +static enum TALER_MHD2_GlobalOptions TM_go; + + +void +TALER_MHD2_setup (enum TALER_MHD2_GlobalOptions go) +{ + TM_go = go; +} + + +void +TALER_MHD2_add_global_headers (struct MHD_Response *response, + bool allow_store) +{ + if (0 != (TM_go & TALER_MHD2_GO_FORCE_CONNECTION_CLOSE)) + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (response, + MHD_HTTP_HEADER_CONNECTION, + "close")); + /* The wallet, operating from a background page, needs CORS to + be disabled otherwise browsers block access. */ + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (response, + MHD_HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + "*")); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (response, + /* Not available as MHD constant yet */ + "Access-Control-Expose-Headers", + "*")); + if (! allow_store) + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (response, + MHD_HTTP_HEADER_CACHE_CONTROL, + "no-store")); +} + + +bool +TALER_MHD2_can_compress (struct MHD_Request *request) +{ + struct MHD_StringNullable aeb; + const char *ae; + const char *de; + + if (0 != (TM_go & TALER_MHD2_GO_DISABLE_COMPRESSION)) + return MHD_NO; + if ( (MHD_NO == + MHD_request_get_value (request, + MHD_VK_HEADER, + MHD_HTTP_HEADER_ACCEPT_ENCODING, + &aeb)) || + (NULL == aeb.cstr) ) + return false; + ae = aeb.cstr; + if (0 == strcmp (ae, + "*")) + return true; + de = strstr (ae, + "deflate"); + if (NULL == de) + return false; + if ( ( (de == ae) || + (de[-1] == ',') || + (de[-1] == ' ') ) && + ( (de[strlen ("deflate")] == '\0') || + (de[strlen ("deflate")] == ',') || + (de[strlen ("deflate")] == ';') ) ) + return true; + return false; +} + + +bool +TALER_MHD2_body_compress (void **buf, + size_t *buf_size) +{ + Bytef *cbuf; + uLongf cbuf_size; + int ret; + + cbuf_size = compressBound (*buf_size); + cbuf = malloc (cbuf_size); + if (NULL == cbuf) + return false; + ret = compress (cbuf, + &cbuf_size, + (const Bytef *) *buf, + *buf_size); + if ( (Z_OK != ret) || + (cbuf_size >= *buf_size) ) + { + /* compression failed */ + free (cbuf); + return false; + } + free (*buf); + *buf = (void *) cbuf; + *buf_size = (size_t) cbuf_size; + return true; +} + + +struct MHD_Response * +TALER_MHD2_make_json (enum MHD_HTTP_StatusCode sc, + const json_t *json) +{ + struct MHD_Response *response; + char *json_str; + + json_str = json_dumps (json, + JSON_INDENT (2)); + if (NULL == json_str) + { + GNUNET_break (0); + return NULL; + } + response = MHD_response_from_buffer (sc, + strlen (json_str), + json_str, + &free, + json_str); + if (NULL == response) + { + free (json_str); + GNUNET_break (0); + return NULL; + } + TALER_MHD2_add_global_headers (response, + false); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (response, + MHD_HTTP_HEADER_CONTENT_TYPE, + "application/json")); + return response; +} + + +struct MHD_Response * +TALER_MHD2_make_json_steal (enum MHD_HTTP_StatusCode sc, + json_t *json) +{ + struct MHD_Response *res; + + res = TALER_MHD2_make_json (sc, + json); + json_decref (json); + return res; +} + + +const struct MHD_Action * +TALER_MHD2_reply_json (struct MHD_Request *request, + const json_t *json, + enum MHD_HTTP_StatusCode sc) +{ + struct MHD_Response *response; + void *json_str; + size_t json_len; + bool is_compressed; + + json_str = json_dumps (json, + JSON_INDENT (2)); + if (NULL == json_str) + { + /** + * This log helps to figure out which + * function called this one and assert-failed. + */ + TALER_LOG_ERROR ("Aborting json-packing for HTTP code: %u\n", + sc); + + GNUNET_assert (0); + return MHD_action_abort_request (request); + } + json_len = strlen (json_str); + /* try to compress the body */ + is_compressed = MHD_NO; + if (TALER_MHD2_can_compress (request)) + is_compressed = TALER_MHD2_body_compress (&json_str, + &json_len); + response = MHD_response_from_buffer (sc, + json_len, + json_str, + &free, + json_str); + if (NULL == response) + { + free (json_str); + GNUNET_break (0); + return MHD_action_abort_request (request); + } + TALER_MHD2_add_global_headers (response, + false); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (response, + MHD_HTTP_HEADER_CONTENT_TYPE, + "application/json")); + if (is_compressed) + { + /* Need to indicate to client that body is compressed */ + if (MHD_SC_OK != + MHD_response_add_header (response, + MHD_HTTP_HEADER_CONTENT_ENCODING, + "deflate")) + { + GNUNET_break (0); + MHD_response_destroy (response); + return MHD_action_abort_request (request); + } + } + + return MHD_action_from_response (request, + response); +} + + +const struct MHD_Action * +TALER_MHD2_reply_json_steal (struct MHD_Request *request, + json_t *json, + enum MHD_HTTP_StatusCode sc) +{ + const struct MHD_Action *ret; + + ret = TALER_MHD2_reply_json (request, + json, + sc); + json_decref (json); + return ret; +} + + +const struct MHD_Action * +TALER_MHD2_reply_cors_preflight (struct MHD_Request *request) +{ + struct MHD_Response *response; + + response = MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT); + if (NULL == response) + return MHD_action_abort_request (request); + /* This adds the Access-Control-Allow-Origin header. + * All endpoints of the exchange allow CORS. */ + TALER_MHD2_add_global_headers (response, + true); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (response, + /* Not available as MHD constant yet */ + "Access-Control-Allow-Headers", + "*")); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (response, + /* Not available as MHD constant yet */ + "Access-Control-Allow-Methods", + "*")); + return MHD_action_from_response (request, + response); +} + + +struct MHD_Response * +TALER_MHD2_make_error (enum TALER_ErrorCode ec, + const char *detail) +{ + return TALER_MHD2_MAKE_JSON_PACK ( + TALER_ErrorCode_get_http_status (ec), + TALER_MHD2_PACK_EC (ec), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("detail", detail))); +} + + +const struct MHD_Action * +TALER_MHD2_reply_with_error (struct MHD_Request *request, + enum MHD_HTTP_StatusCode sc, + enum TALER_ErrorCode ec, + const char *detail) +{ + return TALER_MHD2_REPLY_JSON_PACK ( + request, + sc, + TALER_MHD2_PACK_EC (ec), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("detail", detail))); +} + + +const struct MHD_Action * +TALER_MHD2_reply_with_ec (struct MHD_Request *request, + enum TALER_ErrorCode ec, + const char *detail) +{ + unsigned int hc + = TALER_ErrorCode_get_http_status (ec); + + if ( (0 == hc) || + (UINT_MAX == hc) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Invalid Taler error code %u provided for response!\n", + (unsigned int) ec); + hc = MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + return TALER_MHD2_reply_with_error (request, + (enum MHD_HTTP_StatusCode) hc, + ec, + detail); +} + + +const struct MHD_Action * +TALER_MHD2_reply_request_too_large (struct MHD_Request *request) +{ + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_CONTENT_TOO_LARGE, + TALER_EC_GENERIC_UPLOAD_EXCEEDS_LIMIT, + NULL); +} + + +const struct MHD_Action * +TALER_MHD2_reply_agpl (struct MHD_Request *request, + const char *url) +{ + const char *agpl = + "This server is licensed under the Affero GPL. You will now be redirected to the source code."; + struct MHD_Response *response; + + response = MHD_response_from_buffer_static (MHD_HTTP_STATUS_FOUND, + strlen (agpl), + (void *) agpl); + if (NULL == response) + { + GNUNET_break (0); + return MHD_action_abort_request (request); + } + TALER_MHD2_add_global_headers (response, + true); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (response, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/plain")); + if (MHD_SC_OK != + MHD_response_add_header (response, + MHD_HTTP_HEADER_LOCATION, + url)) + { + GNUNET_break (0); + MHD_response_destroy (response); + return MHD_action_abort_request (request); + } + return MHD_action_from_response (request, + response); +} + + +const struct MHD_Action * +TALER_MHD2_reply_static (struct MHD_Request *request, + enum MHD_HTTP_StatusCode sc, + const char *mime_type, + const char *body, + size_t body_size) +{ + struct MHD_Response *response; + + response = MHD_response_from_buffer_static (sc, + body_size, + body); + if (NULL == response) + { + GNUNET_break (0); + return MHD_action_abort_request (request); + } + TALER_MHD2_add_global_headers (response, + true); + if (NULL != mime_type) + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (response, + MHD_HTTP_HEADER_CONTENT_TYPE, + mime_type)); + return MHD_action_from_response (request, + response); +} + + +void +TALER_MHD2_get_date_string (struct GNUNET_TIME_Absolute at, + char date[128]) +{ + static const char *const days[] = + { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; + static const char *const mons[] = + { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", + "Nov", "Dec"}; + struct tm now; + time_t t; +#if ! defined(HAVE_C11_GMTIME_S) && ! defined(HAVE_W32_GMTIME_S) && \ + ! defined(HAVE_GMTIME_R) + struct tm*pNow; +#endif + + date[0] = 0; + t = (time_t) (at.abs_value_us / 1000LL / 1000LL); +#if defined(HAVE_C11_GMTIME_S) + if (NULL == gmtime_s (&t, &now)) + return; +#elif defined(HAVE_W32_GMTIME_S) + if (0 != gmtime_s (&now, &t)) + return; +#elif defined(HAVE_GMTIME_R) + if (NULL == gmtime_r (&t, &now)) + return; +#else + pNow = gmtime (&t); + if (NULL == pNow) + return; + now = *pNow; +#endif + sprintf (date, + "%3s, %02u %3s %04u %02u:%02u:%02u GMT", + days[now.tm_wday % 7], + (unsigned int) now.tm_mday, + mons[now.tm_mon % 12], + (unsigned int) (1900 + now.tm_year), + (unsigned int) now.tm_hour, + (unsigned int) now.tm_min, + (unsigned int) now.tm_sec); +} + + +/* end of mhd_responses.c */ diff --git a/src/mhd2/mhd2_run.c b/src/mhd2/mhd2_run.c @@ -0,0 +1,414 @@ +/* + 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 mhd2_run.c + * @brief API for running an MHD daemon with the + * GNUnet scheduler + * @author Christian Grothoff + */ +#include <gnunet/gnunet_util_lib.h> +#include <jansson.h> +#define MHD_APP_SOCKET_CNTX_TYPE struct SocketContext +#include <microhttpd2.h> +#include "taler/taler_mhd2_lib.h" + + +/** + * Context to track whatever MHD wants us to wait for. + */ +struct SocketContext +{ + /** + * Task for this socket. + */ + struct GNUNET_SCHEDULER_Task *mhd_rtask; + + /** + * Task for this socket. + */ + struct GNUNET_SCHEDULER_Task *mhd_wtask; + + /** + * Internal handle to pass to MHD when ready. + */ + struct MHD_EventUpdateContext *ecb_cntx; + + /** + * Socket to watch for. + */ + struct GNUNET_NETWORK_Handle *fd; + + /** + * Daemon this socket is about. + */ + struct DaemonEntry *de; +}; + + +/** + * Entry in list of HTTP servers we are running. + */ +struct DaemonEntry +{ + /** + * Kept in a DLL. + */ + struct DaemonEntry *next; + + /** + * Kept in a DLL. + */ + struct DaemonEntry *prev; + + /** + * The actual daemon. + */ + struct MHD_Daemon *mhd; + + /** + * Task running the HTTP server. + */ + struct GNUNET_SCHEDULER_Task *mhd_task; + + /** + * Task waiting for timeout on the HTTP server. + */ + struct GNUNET_SCHEDULER_Task *timeout_task; + + /** + * Set to true if we should immediately MHD_run() again. + */ + bool triggered; + +}; + + +/** + * Head of list of HTTP servers. + */ +static struct DaemonEntry *mhd_head; + +/** + * Tail of list of HTTP servers. + */ +static struct DaemonEntry *mhd_tail; + + +/** + * Function that queries MHD's select sets and + * starts the task waiting for them. + * + * @param[in,out] de daemon to start tasks for + */ +static void +prepare_daemon (struct DaemonEntry *de); + + +/** + * Trigger MHD on timeout. + * + * @param cls a `struct DaemonEntry` + */ +static void +handle_timeout (void *cls) +{ + struct DaemonEntry *de = cls; + + de->timeout_task = NULL; + prepare_daemon (de); +} + + +static void +prepare_daemon (struct DaemonEntry *de) +{ + uint_fast64_t next_max_wait; + enum MHD_StatusCode sc; + + sc = MHD_daemon_process_reg_events (de->mhd, + &next_max_wait); + if (MHD_SC_OK != sc) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "MHD_daemon_process_reg_events failed: %d\n", + (int) sc); + return; + } + if (MHD_WAIT_INDEFINITELY == next_max_wait) + return; + de->timeout_task + = GNUNET_SCHEDULER_add_delayed ( + GNUNET_TIME_relative_multiply ( + GNUNET_TIME_UNIT_MICROSECONDS, + next_max_wait), + &handle_timeout, + de); +} + + +/** + * Call MHD to process pending requests and then go back + * and schedule the next run. + * + * @param cls our `struct DaemonEntry *` + */ +static void +run_daemon (void *cls) +{ + struct DaemonEntry *de = cls; + + de->mhd_task = NULL; + prepare_daemon (de); +} + + +/** + * Called whenever MHD should process read-events on the socket. + * + * @param cls a `struct SocketContext` + */ +static void +mhd_rready (void *cls) +{ + struct SocketContext *sc = cls; + struct DaemonEntry *de = sc->de; + + sc->mhd_rtask = NULL; + MHD_daemon_event_update (de->mhd, + sc->ecb_cntx, + MHD_FD_STATE_RECV); + if (NULL != de->mhd_task) + GNUNET_SCHEDULER_cancel (de->mhd_task); + de->mhd_task = GNUNET_SCHEDULER_add_now (&run_daemon, + de); +} + + +/** + * Called whenever MHD should process write-events on the socket. + * + * @param cls a `struct SocketContext` + */ +static void +mhd_wready (void *cls) +{ + struct SocketContext *sc = cls; + struct DaemonEntry *de = sc->de; + + sc->mhd_wtask = NULL; + MHD_daemon_event_update (de->mhd, + sc->ecb_cntx, + MHD_FD_STATE_SEND); + if (NULL != de->mhd_task) + GNUNET_SCHEDULER_cancel (de->mhd_task); + de->mhd_task = GNUNET_SCHEDULER_add_now (&run_daemon, + de); +} + + +/** + * Callback for registration/de-registration of the sockets to watch. + * + * @param cls our `struct DaemonEntry` + * @param fd the socket to watch + * @param watch_for the states of the @a fd to watch, if set to + * #MHD_FD_STATE_NONE the socket must be de-registred + * @param app_cntx_old the old application defined context for the socket, + * NULL if @a fd socket was not registered before + * @param ecb_cntx the context handle to be used + * with #MHD_daemon_event_update() + * @return NULL if error (to connection will be aborted), + * or the new socket context + */ +static MHD_APP_SOCKET_CNTX_TYPE * +socket_registration_update ( + void *cls, + MHD_Socket fd, + enum MHD_FdState watch_for, + MHD_APP_SOCKET_CNTX_TYPE *app_cntx_old, + struct MHD_EventUpdateContext *ecb_cntx) +{ + struct DaemonEntry *de = cls; + + if (NULL == app_cntx_old) + { + app_cntx_old = GNUNET_new (struct SocketContext); + app_cntx_old->ecb_cntx = ecb_cntx; + app_cntx_old->fd = GNUNET_NETWORK_socket_box_native (fd); + app_cntx_old->de = de; + } + if (MHD_FD_STATE_NONE == watch_for) + { + if (NULL != app_cntx_old->mhd_rtask) + GNUNET_SCHEDULER_cancel (app_cntx_old->mhd_rtask); + if (NULL != app_cntx_old->mhd_wtask) + GNUNET_SCHEDULER_cancel (app_cntx_old->mhd_wtask); + GNUNET_NETWORK_socket_free_memory_only_ (app_cntx_old->fd); + GNUNET_free (app_cntx_old); + return NULL; + } + if ( (MHD_FD_STATE_RECV & watch_for) && + (NULL == app_cntx_old->mhd_rtask) ) + { + app_cntx_old->mhd_rtask + = GNUNET_SCHEDULER_add_read_net (GNUNET_TIME_UNIT_FOREVER_REL, + app_cntx_old->fd, + &mhd_rready, + app_cntx_old); + } + if ( (MHD_FD_STATE_SEND & watch_for) && + (NULL == app_cntx_old->mhd_wtask) ) + { + app_cntx_old->mhd_wtask + = GNUNET_SCHEDULER_add_write_net (GNUNET_TIME_UNIT_FOREVER_REL, + app_cntx_old->fd, + &mhd_wready, + app_cntx_old); + } + if ( (0 == (MHD_FD_STATE_RECV & watch_for)) && + (NULL != app_cntx_old->mhd_rtask) ) + { + GNUNET_SCHEDULER_cancel (app_cntx_old->mhd_rtask); + app_cntx_old->mhd_rtask = NULL; + } + if ( (0 == (MHD_FD_STATE_SEND & watch_for)) && + (NULL != app_cntx_old->mhd_wtask) ) + { + GNUNET_SCHEDULER_cancel (app_cntx_old->mhd_wtask); + app_cntx_old->mhd_wtask = NULL; + } + return app_cntx_old; +} + + +void +TALER_MHD2_daemon_start (struct MHD_Daemon *daemon) +{ + struct DaemonEntry *de; + enum MHD_StatusCode sc; + + de = GNUNET_new (struct DaemonEntry); + GNUNET_assert (MHD_SC_OK == + MHD_DAEMON_SET_OPTIONS ( + daemon, + MHD_D_OPTION_REREGISTER_ALL (true), + MHD_D_OPTION_WM_EXTERNAL_EVENT_LOOP_CB_LEVEL ( + &socket_registration_update, + de))); + de->mhd = daemon; + sc = MHD_daemon_start (de->mhd); + if (MHD_SC_OK != sc) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "MHD_daemon_start failed: %d\n", + (int) sc); + GNUNET_free (de); + return; + } + GNUNET_CONTAINER_DLL_insert (mhd_head, + mhd_tail, + de); + prepare_daemon (de); +} + + +void +TALER_MHD2_daemons_halt (void) +{ + for (struct DaemonEntry *de = mhd_head; + NULL != de; + de = de->next) + { + if (NULL != de->mhd_task) + { + GNUNET_SCHEDULER_cancel (de->mhd_task); + de->mhd_task = NULL; + } + de->triggered = false; + } +} + + +void +TALER_MHD2_daemons_quiesce (void) +{ + for (struct DaemonEntry *de = mhd_head; + NULL != de; + de = de->next) + { +#if FIXME_MHD2 + int fd; +#endif + + if (NULL != de->mhd_task) + { + GNUNET_SCHEDULER_cancel (de->mhd_task); + de->mhd_task = NULL; + } + de->triggered = false; +#if FIXME_MHD2 + fd = MHD_daemon_quiesce (de->mhd); + GNUNET_break (0 == close (fd)); +#endif + } +} + + +void +TALER_MHD2_daemons_destroy (void) +{ + struct DaemonEntry *de; + + while (NULL != (de = mhd_head)) + { + struct MHD_Daemon *mhd = de->mhd; + + if (NULL != de->mhd_task) + { + GNUNET_SCHEDULER_cancel (de->mhd_task); + de->mhd_task = NULL; + } + MHD_daemon_destroy (mhd); + GNUNET_CONTAINER_DLL_remove (mhd_head, + mhd_tail, + de); + GNUNET_free (de); + } +} + + +void +TALER_MHD2_daemons_trigger (void) +{ + for (struct DaemonEntry *de = mhd_head; + NULL != de; + de = de->next) + { + if (NULL != de->mhd_task) + { + GNUNET_SCHEDULER_cancel (de->mhd_task); + de->mhd_task = GNUNET_SCHEDULER_add_now (&run_daemon, + de); + } + else + { + de->triggered = true; + } + } +} + + +/* end of mhd2_run.c */ diff --git a/src/mhd2/mhd2_spa.c b/src/mhd2/mhd2_spa.c @@ -0,0 +1,390 @@ +/* + This file is part of TALER + Copyright (C) 2020--2025 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 EXCHANGEABILITY 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 mhd2_spa.c + * @brief logic to load single page apps + * @author Christian Grothoff + */ +#include "platform.h" /* UNNECESSARY? */ +#include <gnunet/gnunet_util_lib.h> +#include "taler/taler_util.h" +#include "taler/taler_mhd2_lib.h" + + +/** + * Resource from the WebUi. + */ +struct WebuiFile +{ + /** + * Kept in a DLL. + */ + struct WebuiFile *next; + + /** + * Kept in a DLL. + */ + struct WebuiFile *prev; + + /** + * Path this resource matches. + */ + char *path; + + /** + * SPA resource, compressed. + */ + struct MHD_Response *zresponse; + + /** + * SPA resource, vanilla. + */ + struct MHD_Response *response; + +}; + + +/** + * Resource from the WebUi. + */ +struct TALER_MHD2_Spa +{ + /** + * Resources of the WebUI, kept in a DLL. + */ + struct WebuiFile *webui_head; + + /** + * Resources of the WebUI, kept in a DLL. + */ + struct WebuiFile *webui_tail; +}; + + +const struct MHD_Action * +TALER_MHD2_spa_handler (const struct TALER_MHD2_Spa *spa, + struct MHD_Request *request, + const char *path) +{ + struct WebuiFile *w = NULL; + + if ( (NULL == path) || + (0 == strcmp (path, + "")) ) + path = "index.html"; + for (struct WebuiFile *pos = spa->webui_head; + NULL != pos; + pos = pos->next) + if (0 == strcmp (path, + pos->path)) + { + w = pos; + break; + } + if (NULL == w) + return TALER_MHD2_reply_with_error (request, + MHD_HTTP_STATUS_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + path); + if ( (MHD_YES == + TALER_MHD2_can_compress (request)) && + (NULL != w->zresponse) ) + return MHD_action_from_response (request, + w->zresponse); + return MHD_action_from_response (request, + w->response); +} + + +/** + * Function called on each file to load for the WebUI. + * + * @param cls the `struct TALER_MHD2_Spa *` to build + * @param dn name of the file to load + */ +static enum GNUNET_GenericReturnValue +build_webui (void *cls, + const char *dn) +{ + static const struct + { + const char *ext; + const char *mime; + } mime_map[] = { + { + .ext = "css", + .mime = "text/css" + }, + { + .ext = "html", + .mime = "text/html" + }, + { + .ext = "js", + .mime = "text/javascript" + }, + { + .ext = "jpg", + .mime = "image/jpeg" + }, + { + .ext = "jpeg", + .mime = "image/jpeg" + }, + { + .ext = "png", + .mime = "image/png" + }, + { + .ext = "svg", + .mime = "image/svg+xml" + }, + { + .ext = NULL, + .mime = NULL + }, + }; + struct TALER_MHD2_Spa *spa = cls; + int fd; + struct stat sb; + struct MHD_Response *zresponse = NULL; + struct MHD_Response *response; + const char *ext; + const char *mime; + + fd = open (dn, + O_RDONLY); + if (-1 == fd) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "open", + dn); + return GNUNET_SYSERR; + } + if (0 != + fstat (fd, + &sb)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "open", + dn); + GNUNET_break (0 == close (fd)); + return GNUNET_SYSERR; + } + + mime = NULL; + ext = strrchr (dn, '.'); + if (NULL == ext) + { + GNUNET_break (0 == close (fd)); + return GNUNET_OK; + } + ext++; + for (unsigned int i = 0; NULL != mime_map[i].ext; i++) + if (0 == strcasecmp (ext, + mime_map[i].ext)) + { + mime = mime_map[i].mime; + break; + } + + { + void *in; + ssize_t r; + size_t csize; + + in = GNUNET_malloc_large (sb.st_size); + if (NULL == in) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "malloc"); + GNUNET_break (0 == close (fd)); + return GNUNET_SYSERR; + } + r = read (fd, + in, + sb.st_size); + if ( (-1 == r) || + (sb.st_size != (size_t) r) ) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "read", + dn); + GNUNET_free (in); + GNUNET_break (0 == close (fd)); + return GNUNET_SYSERR; + } + csize = (size_t) r; + if (TALER_MHD2_body_compress (&in, + &csize)) + { + zresponse = MHD_response_from_buffer (MHD_HTTP_STATUS_OK, + csize, + in, + &free, + in); + if (NULL != zresponse) + { + if (MHD_SC_OK != + MHD_response_add_header (zresponse, + MHD_HTTP_HEADER_CONTENT_ENCODING, + "deflate")) + { + GNUNET_break (0); + MHD_response_destroy (zresponse); + zresponse = NULL; + } + else if (NULL != mime) + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (zresponse, + MHD_HTTP_HEADER_CONTENT_TYPE, + mime)); + /* Which representation we return depends on the + Accept-Encoding header of the client, so shared caches + must not serve one variant to a client that asked for + another. */ + if (NULL != zresponse) + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (zresponse, + MHD_HTTP_HEADER_VARY, + MHD_HTTP_HEADER_ACCEPT_ENCODING)); + } + } + else + { + GNUNET_free (in); + } + } + + response = MHD_response_from_fd (MHD_HTTP_STATUS_OK, + fd, + 0LLU, + sb.st_size); + if (NULL == response) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "open", + dn); + GNUNET_break (0 == close (fd)); + if (NULL != zresponse) + { + MHD_response_destroy (zresponse); + zresponse = NULL; + } + return GNUNET_SYSERR; + } + if (NULL != mime) + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (response, + MHD_HTTP_HEADER_CONTENT_TYPE, + mime)); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (response, + MHD_HTTP_HEADER_VARY, + MHD_HTTP_HEADER_ACCEPT_ENCODING)); + + { + struct WebuiFile *w; + const char *fn; + + fn = strrchr (dn, '/'); + GNUNET_assert (NULL != fn); + w = GNUNET_new (struct WebuiFile); + w->path = GNUNET_strdup (fn + 1); + w->response = response; + w->zresponse = zresponse; + GNUNET_assert (MHD_SC_OK == + MHD_RESPONSE_SET_OPTIONS (response, + MHD_R_OPTION_REUSABLE (MHD_YES))); + if (NULL != zresponse) + GNUNET_assert (MHD_SC_OK == + MHD_RESPONSE_SET_OPTIONS (zresponse, + MHD_R_OPTION_REUSABLE (MHD_YES))) + ; + GNUNET_CONTAINER_DLL_insert (spa->webui_head, + spa->webui_tail, + w); + } + return GNUNET_OK; +} + + +struct TALER_MHD2_Spa * +TALER_MHD2_spa_load (const struct GNUNET_OS_ProjectData *pd, + const char *dir) +{ + struct TALER_MHD2_Spa *spa; + char *dn; + + { + char *path; + + path = GNUNET_OS_installation_get_path (pd, + GNUNET_OS_IPK_DATADIR); + if (NULL == path) + { + GNUNET_break (0); + return NULL; + } + GNUNET_asprintf (&dn, + "%s%s", + path, + dir); + GNUNET_free (path); + } + spa = GNUNET_new (struct TALER_MHD2_Spa); + if (-1 == + GNUNET_DISK_directory_scan (dn, + &build_webui, + spa)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to load WebUI from `%s'\n", + dn); + GNUNET_free (dn); + TALER_MHD2_spa_free (spa); + return NULL; + } + GNUNET_free (dn); + return spa; +} + + +void +TALER_MHD2_spa_free (struct TALER_MHD2_Spa *spa) +{ + struct WebuiFile *w; + + while (NULL != (w = spa->webui_head)) + { + GNUNET_CONTAINER_DLL_remove (spa->webui_head, + spa->webui_tail, + w); + if (NULL != w->response) + { + MHD_response_destroy (w->response); + w->response = NULL; + } + if (NULL != w->zresponse) + { + MHD_response_destroy (w->zresponse); + w->zresponse = NULL; + } + GNUNET_free (w->path); + GNUNET_free (w); + } + GNUNET_free (spa); +} diff --git a/src/sync/meson.build b/src/sync/meson.build @@ -22,7 +22,7 @@ executable( talerjson_dep, talermerchant_dep, talermhd_dep, - talermhd2_dep, + libtalermhd2_dep, gnunetutil_dep, gnunetjson_dep, gnunetcurl_dep,