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:
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,