commit 0cd9c8b4f9f1d80e024b6478becf062a43473db4
parent c1e92e30d7cedb748db63bd917a8ff11beddf88b
Author: Christian Grothoff <christian@grothoff.org>
Date: Sun, 28 Dec 2025 12:49:38 +0100
more taler-merchant-httpd refactoring
Diffstat:
13 files changed, 2656 insertions(+), 2315 deletions(-)
diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am
@@ -75,8 +75,12 @@ taler_merchant_exchangekeyupdate_CFLAGS = \
taler_merchant_httpd_SOURCES = \
taler-merchant-httpd.c taler-merchant-httpd.h \
+ taler-merchant-httpd_auth.c \
+ taler-merchant-httpd_auth.h \
taler-merchant-httpd_config.c taler-merchant-httpd_config.h \
taler-merchant-httpd_contract.c taler-merchant-httpd_contract.h \
+ taler-merchant-httpd_dispatcher.c \
+ taler-merchant-httpd_dispatcher.h \
taler-merchant-httpd_exchanges.c taler-merchant-httpd_exchanges.h \
taler-merchant-httpd_get-orders-ID.c \
taler-merchant-httpd_get-orders-ID.h \
diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c
@@ -29,110 +29,25 @@
#include <taler/taler_templating_lib.h>
#include <taler/taler_exchange_service.h>
#include "taler_merchant_util.h"
-#include "taler-merchant-httpd_config.h"
-#include "taler-merchant-httpd_exchanges.h"
-#include "taler-merchant-httpd_get-orders-ID.h"
-#include "taler-merchant-httpd_get-products-image.h"
-#include "taler-merchant-httpd_get-templates-ID.h"
+#include "taler-merchant-httpd_auth.h"
+#include "taler-merchant-httpd_dispatcher.h"
#include "taler-merchant-httpd_helper.h"
#include "taler-merchant-httpd_mhd.h"
#include "taler-merchant-httpd_mfa.h"
-#include "taler-merchant-httpd_private-delete-account-ID.h"
-#include "taler-merchant-httpd_private-delete-categories-ID.h"
-#include "taler-merchant-httpd_private-delete-units-ID.h"
-#include "taler-merchant-httpd_private-delete-instances-ID.h"
-#include "taler-merchant-httpd_private-delete-instances-ID-token.h"
-#include "taler-merchant-httpd_private-delete-products-ID.h"
-#include "taler-merchant-httpd_private-delete-orders-ID.h"
-#include "taler-merchant-httpd_private-delete-otp-devices-ID.h"
-#include "taler-merchant-httpd_private-delete-templates-ID.h"
-#include "taler-merchant-httpd_private-delete-token-families-SLUG.h"
-#include "taler-merchant-httpd_private-delete-transfers-ID.h"
-#include "taler-merchant-httpd_private-delete-webhooks-ID.h"
-#include "taler-merchant-httpd_private-get-accounts.h"
-#include "taler-merchant-httpd_private-get-accounts-ID.h"
-#include "taler-merchant-httpd_private-get-categories.h"
-#include "taler-merchant-httpd_private-get-categories-ID.h"
-#include "taler-merchant-httpd_private-get-units.h"
-#include "taler-merchant-httpd_private-get-units-ID.h"
-#include "taler-merchant-httpd_private-get-incoming.h"
-#include "taler-merchant-httpd_private-get-instances.h"
-#include "taler-merchant-httpd_private-get-instances-ID.h"
-#include "taler-merchant-httpd_private-get-instances-ID-kyc.h"
-#include "taler-merchant-httpd_private-get-instances-ID-tokens.h"
-#include "taler-merchant-httpd_private-get-pos.h"
-#include "taler-merchant-httpd_private-get-products.h"
-#include "taler-merchant-httpd_private-get-products-ID.h"
-#include "taler-merchant-httpd_private-get-orders.h"
-#include "taler-merchant-httpd_private-get-orders-ID.h"
-#include "taler-merchant-httpd_private-get-otp-devices.h"
-#include "taler-merchant-httpd_private-get-otp-devices-ID.h"
-#include "taler-merchant-httpd_private-get-statistics-amount-SLUG.h"
-#include "taler-merchant-httpd_private-get-statistics-counter-SLUG.h"
-#include "taler-merchant-httpd_private-get-templates.h"
-#include "taler-merchant-httpd_private-get-templates-ID.h"
-#include "taler-merchant-httpd_private-get-token-families.h"
-#include "taler-merchant-httpd_private-get-token-families-SLUG.h"
-#include "taler-merchant-httpd_private-get-transfers.h"
-#include "taler-merchant-httpd_private-get-webhooks.h"
-#include "taler-merchant-httpd_private-get-webhooks-ID.h"
-#include "taler-merchant-httpd_private-patch-accounts-ID.h"
-#include "taler-merchant-httpd_private-patch-categories-ID.h"
-#include "taler-merchant-httpd_private-patch-units-ID.h"
-#include "taler-merchant-httpd_private-patch-instances-ID.h"
-#include "taler-merchant-httpd_private-patch-orders-ID-forget.h"
-#include "taler-merchant-httpd_private-patch-otp-devices-ID.h"
-#include "taler-merchant-httpd_private-patch-products-ID.h"
-#include "taler-merchant-httpd_private-patch-templates-ID.h"
-#include "taler-merchant-httpd_private-patch-token-families-SLUG.h"
-#include "taler-merchant-httpd_private-patch-webhooks-ID.h"
-#include "taler-merchant-httpd_private-post-account.h"
-#include "taler-merchant-httpd_private-post-categories.h"
-#include "taler-merchant-httpd_private-post-units.h"
-#include "taler-merchant-httpd_private-post-instances.h"
-#include "taler-merchant-httpd_private-post-instances-ID-auth.h"
-#include "taler-merchant-httpd_private-post-instances-ID-token.h"
-#include "taler-merchant-httpd_private-post-otp-devices.h"
#include "taler-merchant-httpd_private-post-orders.h"
-#include "taler-merchant-httpd_private-post-orders-ID-refund.h"
-#include "taler-merchant-httpd_private-post-products.h"
-#include "taler-merchant-httpd_private-post-products-ID-lock.h"
-#include "taler-merchant-httpd_private-post-templates.h"
-#include "taler-merchant-httpd_private-post-token-families.h"
-#include "taler-merchant-httpd_private-post-transfers.h"
-#include "taler-merchant-httpd_private-post-webhooks.h"
-#include "taler-merchant-httpd_post-challenge-ID.h"
-#include "taler-merchant-httpd_post-challenge-ID-confirm.h"
#include "taler-merchant-httpd_post-orders-ID-abort.h"
-#include "taler-merchant-httpd_post-orders-ID-claim.h"
-#include "taler-merchant-httpd_post-orders-ID-paid.h"
-#include "taler-merchant-httpd_post-orders-ID-pay.h"
-#include "taler-merchant-httpd_post-using-templates.h"
-#include "taler-merchant-httpd_post-orders-ID-refund.h"
+#include "taler-merchant-httpd_post-challenge-ID.h"
+#include "taler-merchant-httpd_get-orders-ID.h"
+#include "taler-merchant-httpd_exchanges.h"
#include "taler-merchant-httpd_spa.h"
-#include "taler-merchant-httpd_statics.h"
#include "taler-merchant-httpd_terms.h"
-#include "taler-merchant-httpd_post-reports-ID.h"
-#include "taler-merchant-httpd_private-delete-report-ID.h"
-#include "taler-merchant-httpd_private-get-report-ID.h"
-#include "taler-merchant-httpd_private-get-reports.h"
-#include "taler-merchant-httpd_private-patch-report-ID.h"
-#include "taler-merchant-httpd_private-post-reports.h"
-#include "taler-merchant-httpd_private-delete-pot-ID.h"
-#include "taler-merchant-httpd_private-get-pot-ID.h"
-#include "taler-merchant-httpd_private-get-pots.h"
-#include "taler-merchant-httpd_private-patch-pot-ID.h"
-#include "taler-merchant-httpd_private-post-pots.h"
-#include "taler-merchant-httpd_private-get-groups.h"
-#include "taler-merchant-httpd_private-post-groups.h"
-#include "taler-merchant-httpd_private-patch-group-ID.h"
-#include "taler-merchant-httpd_private-delete-group-ID.h"
-
-#ifdef HAVE_DONAU_DONAU_SERVICE_H
-#include "taler-merchant-httpd_private-get-donau-instances.h"
+#include "taler-merchant-httpd_private-get-instances-ID-kyc.h"
#include "taler-merchant-httpd_private-post-donau-instance.h"
-#include "taler-merchant-httpd_private-delete-donau-instance-ID.h"
-#endif
+#include "taler-merchant-httpd_private-get-orders-ID.h"
+#include "taler-merchant-httpd_private-get-orders.h"
+#include "taler-merchant-httpd_post-orders-ID-pay.h"
+#include "taler-merchant-httpd_post-orders-ID-refund.h"
+
/**
* Backlog for listen operation on unix-domain sockets.
@@ -224,435 +139,6 @@ static int global_ret;
*/
static const struct GNUNET_CONFIGURATION_Handle *cfg;
-/**
- * Maximum length of a permissions string of a scope
- */
-#define TMH_MAX_SCOPE_PERMISSIONS_LEN 4096
-
-/**
- * Maximum length of a name of a scope
- */
-#define TMH_MAX_NAME_LEN 255
-
-/**
- * Represents a hard-coded set of default scopes with their
- * permissions and names
- */
-struct ScopePermissionMap
-{
- /**
- * The scope enum value
- */
- enum TMH_AuthScope as;
-
- /**
- * The scope name
- */
- char name[TMH_MAX_NAME_LEN];
-
- /**
- * The scope permissions string.
- * Comma-separated.
- */
- char permissions[TMH_MAX_SCOPE_PERMISSIONS_LEN];
-};
-
-/**
- * The default scopes array for merchant
- */
-struct ScopePermissionMap scope_permissions[] = {
- /* Deprecated since v19 */
- {
- .as = TMH_AS_ALL,
- .name = "write",
- .permissions = "*"
- },
- /* Full access for SPA */
- {
- .as = TMH_AS_ALL,
- .name = "all",
- .permissions = "*"
- },
- /* Full access for SPA */
- {
- .as = TMH_AS_SPA,
- .name = "spa",
- .permissions = "*"
- },
- /* Read-only access */
- {
- .as = TMH_AS_READ_ONLY,
- .name = "readonly",
- .permissions = "*-read"
- },
- /* Simple order management */
- {
- .as = TMH_AS_ORDER_SIMPLE,
- .name = "order-simple",
- .permissions = "orders-read,orders-write"
- },
- /* Simple order management for PoS, also allows inventory locking */
- {
- .as = TMH_AS_ORDER_POS,
- .name = "order-pos",
- .permissions = "orders-read,orders-write,inventory-lock"
- },
- /* Simple order management, also allows refunding */
- {
- .as = TMH_AS_ORDER_MGMT,
- .name = "order-mgmt",
- .permissions = "orders-read,orders-write,orders-refund"
- },
- /* Full order management, allows inventory locking and refunds */
- {
- .as = TMH_AS_ORDER_FULL,
- .name = "order-full",
- .permissions = "orders-read,orders-write,inventory-lock,orders-refund"
- },
- /* No permissions, dummy scope */
- {
- .as = TMH_AS_NONE,
- }
-};
-
-
-/**
- * Get permissions string for scope.
- * Also extracts the leftmost bit into the @a refreshable
- * output parameter.
- *
- * @param as the scope to get the permissions string from
- * @param[out] refreshable true if the token associated with this scope is refreshable.
- * @return the permissions string, or NULL if no such scope found
- */
-static const char*
-get_scope_permissions (enum TMH_AuthScope as,
- bool *refreshable)
-{
- *refreshable = as & TMH_AS_REFRESHABLE;
- for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++)
- {
- /* We ignore the TMH_AS_REFRESHABLE bit */
- if ( (as & ~TMH_AS_REFRESHABLE) ==
- (scope_permissions[i].as & ~TMH_AS_REFRESHABLE) )
- return scope_permissions[i].permissions;
- }
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Failed to find required permissions for scope %d\n",
- as);
- return NULL;
-}
-
-
-/**
- * Checks if @a permission_required is in permissions of
- * @a scope.
- *
- * @param permission_required the permission to check.
- * @param scope the scope to check.
- * @return true if @a permission_required is in the permissions set of @a scope.
- */
-static bool
-permission_in_scope (const char *permission_required,
- enum TMH_AuthScope scope)
-{
- char *permissions;
- const char *perms_tmp;
- bool is_read_perm = false;
- bool is_write_perm = false;
- bool refreshable;
- const char *last_dash;
-
- perms_tmp = get_scope_permissions (scope,
- &refreshable);
- if (NULL == perms_tmp)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Permission check failed: scope %d not understood\n",
- (int) scope);
- return false;
- }
- last_dash = strrchr (permission_required,
- '-');
- if (NULL != last_dash)
- {
- is_write_perm = (0 == strcmp (last_dash,
- "-write"));
- is_read_perm = (0 == strcmp (last_dash,
- "-read"));
- }
-
- if (0 == strcmp ("token-refresh",
- permission_required))
- {
- if (! refreshable)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Permission check failed: token not refreshable\n");
- }
- return refreshable;
- }
- permissions = GNUNET_strdup (perms_tmp);
- {
- const char *perm = strtok (permissions,
- ",");
-
- if (NULL == perm)
- {
- GNUNET_free (permissions);
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Permission check failed: empty permission set\n");
- return false;
- }
- while (NULL != perm)
- {
- if (0 == strcmp ("*",
- perm))
- {
- GNUNET_free (permissions);
- return true;
- }
- if ( (0 == strcmp ("*-write",
- perm)) &&
- (is_write_perm) )
- {
- GNUNET_free (permissions);
- return true;
- }
- if ( (0 == strcmp ("*-read",
- perm)) &&
- (is_read_perm) )
- {
- GNUNET_free (permissions);
- return true;
- }
- if (0 == strcmp (permission_required,
- perm))
- {
- GNUNET_free (permissions);
- return true;
- }
- perm = strtok (NULL,
- ",");
- }
- }
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Permission check failed: %s not found in %s\n",
- permission_required,
- permissions);
- GNUNET_free (permissions);
- return false;
-}
-
-
-bool
-TMH_scope_is_subset (enum TMH_AuthScope as,
- enum TMH_AuthScope candidate)
-{
- const char *as_perms;
- const char *candidate_perms;
- char *permissions;
- bool as_refreshable;
- bool cand_refreshable;
-
- as_perms = get_scope_permissions (as,
- &as_refreshable);
- candidate_perms = get_scope_permissions (candidate,
- &cand_refreshable);
- if (! as_refreshable && cand_refreshable)
- return false;
- if ( (NULL == as_perms) &&
- (NULL != candidate_perms) )
- return false;
- if ( (NULL == candidate_perms) ||
- (0 == strcmp ("*",
- as_perms)))
- return true;
- permissions = GNUNET_strdup (candidate_perms);
- {
- const char *perm;
-
- perm = strtok (permissions,
- ",");
- if (NULL == perm)
- {
- GNUNET_free (permissions);
- return true;
- }
- while (NULL != perm)
- {
- if (! permission_in_scope (perm,
- as))
- {
- GNUNET_free (permissions);
- return false;
- }
- perm = strtok (NULL,
- ",");
- }
- }
- GNUNET_free (permissions);
- return true;
-}
-
-
-enum TMH_AuthScope
-TMH_get_scope_by_name (const char *name)
-{
- for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++)
- {
- if (0 == strcasecmp (scope_permissions[i].name,
- name))
- return scope_permissions[i].as;
- }
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Name `%s' does not match any scope we understand\n",
- name);
- return TMH_AS_NONE;
-}
-
-
-const char*
-TMH_get_name_by_scope (enum TMH_AuthScope scope,
- bool *refreshable)
-{
- *refreshable = scope & TMH_AS_REFRESHABLE;
- for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++)
- {
- /* We ignore the TMH_AS_REFRESHABLE bit */
- if ( (scope & ~TMH_AS_REFRESHABLE) ==
- (scope_permissions[i].as & ~TMH_AS_REFRESHABLE) )
- return scope_permissions[i].name;
- }
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Scope #%d does not match any scope we understand\n",
- (int) scope);
- return NULL;
-}
-
-
-enum GNUNET_GenericReturnValue
-TMH_check_auth (const char *password,
- struct TALER_MerchantAuthenticationSaltP *salt,
- struct TALER_MerchantAuthenticationHashP *hash)
-{
- struct TALER_MerchantAuthenticationHashP val;
-
- if (GNUNET_is_zero (hash))
- return GNUNET_OK;
- if (NULL == password)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Denying access: empty password provided\n");
- return GNUNET_SYSERR;
- }
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Checking against token with salt %s\n",
- TALER_B2S (salt));
- TALER_merchant_instance_auth_hash_with_salt (&val,
- salt,
- password);
- if (0 !=
- GNUNET_memcmp (&val,
- hash))
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Access denied: password does not match\n");
- return GNUNET_SYSERR;
- }
- return GNUNET_OK;
-}
-
-
-/**
- * Check if @a userpass grants access to @a instance.
- *
- * @param userpass base64 encoded "$USERNAME:$PASSWORD" value
- * from HTTP Basic "Authentication" header
- * @param instance the access controlled instance
- */
-static enum GNUNET_GenericReturnValue
-check_auth_instance (const char *userpass,
- struct TMH_MerchantInstance *instance)
-{
- char *tmp;
- char *colon;
- const char *instance_name;
- const char *password;
- const char *target_instance = "admin";
- enum GNUNET_GenericReturnValue ret;
-
- /* implicitly a zeroed out hash means no authentication */
- if (GNUNET_is_zero (&instance->auth.auth_hash))
- return GNUNET_OK;
- if (NULL == userpass)
- {
- GNUNET_break_op (0);
- return GNUNET_SYSERR;
- }
- if (0 ==
- GNUNET_STRINGS_base64_decode (userpass,
- strlen (userpass),
- (void**) &tmp))
- {
- GNUNET_break_op (0);
- return GNUNET_SYSERR;
- }
- colon = strchr (tmp,
- ':');
- if (NULL == colon)
- {
- GNUNET_break_op (0);
- GNUNET_free (tmp);
- return GNUNET_SYSERR;
- }
- *colon = '\0';
- instance_name = tmp;
- password = colon + 1;
- /* instance->settings.id can be NULL if there is no instance yet */
- if (NULL != instance->settings.id)
- target_instance = instance->settings.id;
- if (0 != strcmp (instance_name,
- target_instance))
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Somebody tried to login to instance %s with username %s (login failed).\n",
- target_instance,
- instance_name);
- GNUNET_free (tmp);
- return GNUNET_SYSERR;
- }
- ret = TMH_check_auth (password,
- &instance->auth.auth_salt,
- &instance->auth.auth_hash);
- GNUNET_free (tmp);
- if (GNUNET_OK != ret)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Password provided does not match credentials for %s\n",
- target_instance);
- }
- return ret;
-}
-
-
-void
-TMH_compute_auth (const char *token,
- struct TALER_MerchantAuthenticationSaltP *salt,
- struct TALER_MerchantAuthenticationHashP *hash)
-{
- GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE,
- salt,
- sizeof (*salt));
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Computing initial auth using token with salt %s\n",
- TALER_B2S (salt));
- TALER_merchant_instance_auth_hash_with_salt (hash,
- salt,
- token);
-}
-
-
void
TMH_wire_method_free (struct TMH_WireMethod *wm)
{
@@ -889,212 +375,6 @@ TMH_add_instance (struct TMH_MerchantInstance *mi)
/**
- * Handle a OPTIONS "*" request.
- *
- * @param rh context of the handler
- * @param connection the MHD connection to handle
- * @param[in,out] hc context with further information about the request
- * @return MHD result code
- */
-static MHD_RESULT
-handle_server_options (const struct TMH_RequestHandler *rh,
- struct MHD_Connection *connection,
- struct TMH_HandlerContext *hc)
-{
- (void) rh;
- (void) hc;
- return TALER_MHD_reply_cors_preflight (connection);
-}
-
-
-/**
- * Generates the response for "/", redirecting the
- * client to the "/webui/" from where we serve the SPA.
- *
- * @param rh request handler
- * @param connection MHD connection
- * @param hc handler context
- * @return MHD result code
- */
-static MHD_RESULT
-spa_redirect (const struct TMH_RequestHandler *rh,
- struct MHD_Connection *connection,
- struct TMH_HandlerContext *hc)
-{
- const char *text = "Redirecting to /webui/";
- struct MHD_Response *response;
- char *dst;
-
- response = MHD_create_response_from_buffer (strlen (text),
- (void *) text,
- MHD_RESPMEM_PERSISTENT);
- if (NULL == response)
- {
- GNUNET_break (0);
- return MHD_NO;
- }
- TALER_MHD_add_global_headers (response,
- true);
- GNUNET_break (MHD_YES ==
- MHD_add_response_header (response,
- MHD_HTTP_HEADER_CONTENT_TYPE,
- "text/plain"));
- if ( (NULL == hc->instance) ||
- (0 == strcmp ("admin",
- hc->instance->settings.id)) )
- dst = GNUNET_strdup ("/webui/");
- else
- GNUNET_asprintf (&dst,
- "/instances/%s/webui/",
- hc->instance->settings.id);
- if (MHD_NO ==
- MHD_add_response_header (response,
- MHD_HTTP_HEADER_LOCATION,
- dst))
- {
- GNUNET_break (0);
- MHD_destroy_response (response);
- GNUNET_free (dst);
- return MHD_NO;
- }
- GNUNET_free (dst);
-
- {
- MHD_RESULT ret;
-
- ret = MHD_queue_response (connection,
- MHD_HTTP_FOUND,
- response);
- MHD_destroy_response (response);
- return ret;
- }
-}
-
-
-/**
- * Extract the token from authorization header value @a auth.
- * The @a auth value can be a bearer token or a Basic
- * authentication header. In both cases, this function
- * updates @a auth to point to the actual credential,
- * skipping spaces.
- *
- * NOTE: We probably want to replace this function with MHD2
- * API calls in the future that are more robust.
- *
- * @param[in,out] auth pointer to authorization header value,
- * will be updated to point to the start of the token
- * or set to NULL if header value is invalid
- * @param[out] is_basic_auth will be set to true if the
- * authorization header uses basic authentication,
- * otherwise to false
- */
-static void
-extract_auth (const char **auth,
- bool *is_basic_auth)
-{
- const char *bearer = "Bearer ";
- const char *basic = "Basic ";
- const char *tok = *auth;
- size_t offset = 0;
- bool is_bearer = false;
-
- *is_basic_auth = false;
- if (0 == strncmp (tok,
- bearer,
- strlen (bearer)))
- {
- offset = strlen (bearer);
- is_bearer = true;
- }
- else if (0 == strncmp (tok,
- basic,
- strlen (basic)))
- {
- offset = strlen (basic);
- *is_basic_auth = true;
- }
- else
- {
- *auth = NULL;
- return;
- }
- tok += offset;
- while (' ' == *tok)
- tok++;
- if ( (is_bearer) &&
- (0 != strncasecmp (tok,
- RFC_8959_PREFIX,
- strlen (RFC_8959_PREFIX))) )
- {
- *auth = NULL;
- return;
- }
- *auth = tok;
-}
-
-
-/**
- * Checks if the @a rh matches the given (parsed) URL.
- *
- * @param rh handler to compare against
- * @param url the main URL (without "/private/" prefix, if any)
- * @param prefix_strlen length of the prefix, i.e. 8 for '/orders/' or 7 for '/config'
- * @param infix_url infix text, i.e. "$ORDER_ID".
- * @param infix_strlen length of the string in @a infix_url
- * @param suffix_url suffix, i.e. "/refund", including the "/"
- * @param suffix_strlen number of characters in @a suffix_url
- * @return true if @a rh matches this request
- */
-static bool
-prefix_match (const struct TMH_RequestHandler *rh,
- const char *url,
- size_t prefix_strlen,
- const char *infix_url,
- size_t infix_strlen,
- const char *suffix_url,
- size_t suffix_strlen)
-{
- if ( (prefix_strlen != strlen (rh->url_prefix)) ||
- (0 != memcmp (url,
- rh->url_prefix,
- prefix_strlen)) )
- return false;
- if (! rh->have_id_segment)
- {
- /* Require /$PREFIX/$SUFFIX or /$PREFIX */
- if (NULL != suffix_url)
- return false; /* too many segments to match */
- if ( (NULL == infix_url) /* either or */
- ^ (NULL == rh->url_suffix) )
- return false; /* suffix existence mismatch */
- /* If /$PREFIX/$SUFFIX, check $SUFFIX matches */
- if ( (NULL != infix_url) &&
- ( (infix_strlen != strlen (rh->url_suffix)) ||
- (0 != memcmp (infix_url,
- rh->url_suffix,
- infix_strlen)) ) )
- return false; /* cannot use infix as suffix: content mismatch */
- }
- else
- {
- /* Require /$PREFIX/$ID or /$PREFIX/$ID/$SUFFIX */
- if (NULL == infix_url)
- return false; /* infix existence mismatch */
- if ( ( (NULL == suffix_url)
- ^ (NULL == rh->url_suffix) ) )
- return false; /* suffix existence mismatch */
- if ( (NULL != suffix_url) &&
- ( (suffix_strlen != strlen (rh->url_suffix)) ||
- (0 != memcmp (suffix_url,
- rh->url_suffix,
- suffix_strlen)) ) )
- return false; /* suffix content mismatch */
- }
- return true;
-}
-
-
-/**
* Function called first by MHD with the full URL.
*
* @param cls NULL
@@ -1119,127 +399,6 @@ full_url_track_callback (void *cls,
/**
- * Function used to process Basic authorization header value.
- * Sets correct scope in the auth_scope parameter of the
- * #TMH_HandlerContext.
- *
- * @param hc the handler context
- * @param authn_s the value of the authorization header
- */
-static void
-process_basic_auth (struct TMH_HandlerContext *hc,
- const char *authn_s)
-{
- /* Handle token endpoint slightly differently: Only allow
- * instance password (Basic auth) to retrieve access token.
- * We need to handle authorization with Basic auth here first
- * The only time we need to handle authentication like this is
- * for the token endpoint!
- */
- if ( (0 != strncmp (hc->rh->url_prefix,
- "/token",
- strlen ("/token"))) ||
- (0 != strncmp (MHD_HTTP_METHOD_POST,
- hc->rh->method,
- strlen (MHD_HTTP_METHOD_POST))) ||
- (NULL == hc->instance))
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Called endpoint `%s' with Basic authentication. Rejecting...\n",
- hc->rh->url_prefix);
- hc->auth_scope = TMH_AS_NONE;
- return;
- }
- if (GNUNET_OK ==
- check_auth_instance (authn_s,
- hc->instance))
- {
- hc->auth_scope = TMH_AS_ALL;
- }
- else
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Basic authentication failed!\n");
- hc->auth_scope = TMH_AS_NONE;
- }
-}
-
-
-/**
- * Function used to process Bearer authorization header value.
- * Sets correct scope in the auth_scope parameter of the
- * #TMH_HandlerContext..
- *
- * @param hc the handler context
- * @param authn_s the value of the authorization header
- * @return TALER_EC_NONE on success.
- */
-static enum TALER_ErrorCode
-process_bearer_auth (struct TMH_HandlerContext *hc,
- const char *authn_s)
-{
- if (NULL == hc->instance)
- {
- hc->auth_scope = TMH_AS_NONE;
- return TALER_EC_NONE;
- }
- if (GNUNET_is_zero (&hc->instance->auth.auth_hash))
- {
- /* hash zero means no authentication for instance */
- hc->auth_scope = TMH_AS_ALL;
- return TALER_EC_NONE;
- }
- {
- enum TALER_ErrorCode ec;
-
- ec = TMH_check_token (authn_s,
- hc->instance->settings.id,
- &hc->auth_scope);
- if (TALER_EC_NONE != ec)
- {
- char *dec;
- size_t dec_len;
- const char *token;
-
- /* NOTE: Deprecated, remove sometime after v1.1 */
- if (0 != strncasecmp (authn_s,
- RFC_8959_PREFIX,
- strlen (RFC_8959_PREFIX)))
- {
- GNUNET_break_op (0);
- hc->auth_scope = TMH_AS_NONE;
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Authentication token invalid: %d\n",
- (int) ec);
- return ec;
- }
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Trying deprecated secret-token:password API authN\n");
- token = authn_s + strlen (RFC_8959_PREFIX);
- dec_len = GNUNET_STRINGS_urldecode (token,
- strlen (token),
- &dec);
- if ( (0 == dec_len) ||
- (GNUNET_OK !=
- TMH_check_auth (dec,
- &hc->instance->auth.auth_salt,
- &hc->instance->auth.auth_hash)) )
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Login failed\n");
- hc->auth_scope = TMH_AS_NONE;
- GNUNET_free (dec);
- return TALER_EC_NONE;
- }
- hc->auth_scope = TMH_AS_ALL;
- GNUNET_free (dec);
- }
- }
- return TALER_EC_NONE;
-}
-
-
-/**
* The callback was called again by MHD, continue processing
* the request with the already identified handler.
*
@@ -1441,1334 +600,6 @@ identify_instance (struct TMH_HandlerContext *hc,
/**
- * Determine the group of request handlers to call for the
- * given URL. Removes a possible prefix from @a purl by advancing
- * the pointer.
- *
- * @param[in,out] urlp pointer to the URL to analyze and update
- * @param[out] is_public set to true if these are public handlers
- * @return handler group to consider for the given URL
- */
-static const struct TMH_RequestHandler *
-determine_handler_group (const char **urlp,
- bool *is_public)
-{
- static struct TMH_RequestHandler management_handlers[] = {
- /* GET /instances */
- {
- .url_prefix = "/instances",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "instances-write",
- .skip_instance = true,
- .default_only = true,
- .handler = &TMH_private_get_instances
- },
- /* POST /instances */
- {
- .url_prefix = "/instances",
- .method = MHD_HTTP_METHOD_POST,
- .permission = "instances-write",
- .skip_instance = true,
- .default_only = true,
- .handler = &TMH_private_post_instances,
- /* allow instance data of up to 8 MB, that should be plenty;
- note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
- would require further changes to the allocation logic
- in the code... */
- .max_upload = 1024 * 1024 * 8
- },
- /* GET /instances/$ID/ */
- {
- .url_prefix = "/instances/",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "instances-write",
- .skip_instance = true,
- .default_only = true,
- .have_id_segment = true,
- .handler = &TMH_private_get_instances_default_ID
- },
- /* DELETE /instances/$ID */
- {
- .url_prefix = "/instances/",
- .method = MHD_HTTP_METHOD_DELETE,
- .permission = "instances-write",
- .skip_instance = true,
- .default_only = true,
- .have_id_segment = true,
- .handler = &TMH_private_delete_instances_default_ID
- },
- /* PATCH /instances/$ID */
- {
- .url_prefix = "/instances/",
- .method = MHD_HTTP_METHOD_PATCH,
- .permission = "instances-write",
- .skip_instance = true,
- .default_only = true,
- .have_id_segment = true,
- .handler = &TMH_private_patch_instances_default_ID,
- /* allow instance data of up to 8 MB, that should be plenty;
- note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
- would require further changes to the allocation logic
- in the code... */
- .max_upload = 1024 * 1024 * 8
- },
- /* POST /auth: */
- {
- .url_prefix = "/instances/",
- .url_suffix = "auth",
- .method = MHD_HTTP_METHOD_POST,
- .permission = "instances-auth-write",
- .skip_instance = true,
- .default_only = true,
- .have_id_segment = true,
- .handler = &TMH_private_post_instances_default_ID_auth,
- /* Body should be pretty small. */
- .max_upload = 1024 * 1024
- },
- /* GET /kyc: */
- {
- .url_prefix = "/instances/",
- .url_suffix = "kyc",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "instances-kyc-read",
- .skip_instance = true,
- .default_only = true,
- .have_id_segment = true,
- .handler = &TMH_private_get_instances_default_ID_kyc,
- },
- {
- .url_prefix = NULL
- }
- };
-
- static struct TMH_RequestHandler private_handlers[] = {
- /* GET /instances/$ID/: */
- {
- .url_prefix = "/",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "instances-read",
- .handler = &TMH_private_get_instances_ID
- },
- /* DELETE /instances/$ID/: */
- {
- .url_prefix = "/",
- .method = MHD_HTTP_METHOD_DELETE,
- .permission = "instances-write",
- .allow_deleted_instance = true,
- .handler = &TMH_private_delete_instances_ID
- },
- /* PATCH /instances/$ID/: */
- {
- .url_prefix = "/",
- .method = MHD_HTTP_METHOD_PATCH,
- .handler = &TMH_private_patch_instances_ID,
- .permission = "instances-write",
- .allow_deleted_instance = true,
- /* allow instance data of up to 8 MB, that should be plenty;
- note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
- would require further changes to the allocation logic
- in the code... */
- .max_upload = 1024 * 1024 * 8
- },
- /* POST /auth: */
- {
- .url_prefix = "/auth",
- .method = MHD_HTTP_METHOD_POST,
- .handler = &TMH_private_post_instances_ID_auth,
- .permission = "auth-write",
- /* Body should be pretty small. */
- .max_upload = 1024 * 1024,
- },
- /* GET /kyc: */
- {
- .url_prefix = "/kyc",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "kyc-read",
- .handler = &TMH_private_get_instances_ID_kyc,
- },
- /* GET /pos: */
- {
- .url_prefix = "/pos",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "pos-read",
- .handler = &TMH_private_get_pos
- },
- /* GET /categories: */
- {
- .url_prefix = "/categories",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "categories-read",
- .handler = &TMH_private_get_categories
- },
- /* POST /categories: */
- {
- .url_prefix = "/categories",
- .method = MHD_HTTP_METHOD_POST,
- .permission = "categories-write",
- .handler = &TMH_private_post_categories,
- /* allow category data of up to 8 kb, that should be plenty */
- .max_upload = 1024 * 8
- },
- /* GET /categories/$ID: */
- {
- .url_prefix = "/categories/",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "categories-read",
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .handler = &TMH_private_get_categories_ID
- },
- /* DELETE /categories/$ID: */
- {
- .url_prefix = "/categories/",
- .method = MHD_HTTP_METHOD_DELETE,
- .permission = "categories-write",
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .handler = &TMH_private_delete_categories_ID
- },
- /* PATCH /categories/$ID/: */
- {
- .url_prefix = "/categories/",
- .method = MHD_HTTP_METHOD_PATCH,
- .permission = "categories-write",
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .handler = &TMH_private_patch_categories_ID,
- /* allow category data of up to 8 kb, that should be plenty */
- .max_upload = 1024 * 8
- },
- /* GET /units: */
- {
- .url_prefix = "/units",
- .method = MHD_HTTP_METHOD_GET,
- .handler = &TMH_private_get_units
- },
- /* POST /units: */
- {
- .url_prefix = "/units",
- .method = MHD_HTTP_METHOD_POST,
- .permission = "units-write",
- .handler = &TMH_private_post_units,
- .max_upload = 1024 * 8
- },
- /* GET /units/$UNIT: */
- {
- .url_prefix = "/units/",
- .method = MHD_HTTP_METHOD_GET,
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .handler = &TMH_private_get_units_ID
- },
- /* DELETE /units/$UNIT: */
- {
- .url_prefix = "/units/",
- .method = MHD_HTTP_METHOD_DELETE,
- .permission = "units-write",
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .handler = &TMH_private_delete_units_ID
- },
- /* PATCH /units/$UNIT: */
- {
- .url_prefix = "/units/",
- .method = MHD_HTTP_METHOD_PATCH,
- .permission = "units-write",
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .handler = &TMH_private_patch_units_ID,
- .max_upload = 1024 * 8
- },
- /* GET /products: */
- {
- .url_prefix = "/products",
- .permission = "products-read",
- .method = MHD_HTTP_METHOD_GET,
- .handler = &TMH_private_get_products
- },
- /* POST /products: */
- {
- .url_prefix = "/products",
- .method = MHD_HTTP_METHOD_POST,
- .permission = "products-write",
- .handler = &TMH_private_post_products,
- /* allow product data of up to 8 MB, that should be plenty;
- note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
- would require further changes to the allocation logic
- in the code... */
- .max_upload = 1024 * 1024 * 8
- },
- /* GET /products/$ID: */
- {
- .url_prefix = "/products/",
- .method = MHD_HTTP_METHOD_GET,
- .have_id_segment = true,
- .permission = "products-read",
- .allow_deleted_instance = true,
- .handler = &TMH_private_get_products_ID
- },
- /* DELETE /products/$ID/: */
- {
- .url_prefix = "/products/",
- .method = MHD_HTTP_METHOD_DELETE,
- .have_id_segment = true,
- .permission = "products-write",
- .allow_deleted_instance = true,
- .handler = &TMH_private_delete_products_ID
- },
- /* PATCH /products/$ID/: */
- {
- .url_prefix = "/products/",
- .method = MHD_HTTP_METHOD_PATCH,
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .permission = "products-write",
- .handler = &TMH_private_patch_products_ID,
- /* allow product data of up to 8 MB, that should be plenty;
- note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
- would require further changes to the allocation logic
- in the code... */
- .max_upload = 1024 * 1024 * 8
- },
- /* POST /products/$ID/lock: */
- {
- .url_prefix = "/products/",
- .url_suffix = "lock",
- .method = MHD_HTTP_METHOD_POST,
- .have_id_segment = true,
- .permission = "products-lock",
- .handler = &TMH_private_post_products_ID_lock,
- /* the body should be pretty small, allow 1 MB of upload
- to set a conservative bound for sane wallets */
- .max_upload = 1024 * 1024
- },
- /* POST /orders: */
- {
- .url_prefix = "/orders",
- .method = MHD_HTTP_METHOD_POST,
- .permission = "orders-write",
- .handler = &TMH_private_post_orders,
- /* allow contracts of up to 8 MB, that should be plenty;
- note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
- would require further changes to the allocation logic
- in the code... */
- .max_upload = 1024 * 1024 * 8
- },
- /* GET /orders/$ID: */
- {
- .url_prefix = "/orders/",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "orders-read",
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .handler = &TMH_private_get_orders_ID
- },
- /* GET /orders: */
- {
- .url_prefix = "/orders",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "orders-read",
- .allow_deleted_instance = true,
- .handler = &TMH_private_get_orders
- },
- /* POST /orders/$ID/refund: */
- {
- .url_prefix = "/orders/",
- .url_suffix = "refund",
- .method = MHD_HTTP_METHOD_POST,
- .have_id_segment = true,
- .permission = "orders-refund",
- .handler = &TMH_private_post_orders_ID_refund,
- /* the body should be pretty small, allow 1 MB of upload
- to set a conservative bound for sane wallets */
- .max_upload = 1024 * 1024
- },
- /* PATCH /orders/$ID/forget: */
- {
- .url_prefix = "/orders/",
- .url_suffix = "forget",
- .method = MHD_HTTP_METHOD_PATCH,
- .permission = "orders-write",
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .handler = &TMH_private_patch_orders_ID_forget,
- /* the body should be pretty small, allow 1 MB of upload
- to set a conservative bound for sane wallets */
- .max_upload = 1024 * 1024
- },
- /* DELETE /orders/$ID: */
- {
- .url_prefix = "/orders/",
- .method = MHD_HTTP_METHOD_DELETE,
- .permission = "orders-write",
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .handler = &TMH_private_delete_orders_ID
- },
- /* POST /transfers: */
- {
- .url_prefix = "/transfers",
- .method = MHD_HTTP_METHOD_POST,
- .allow_deleted_instance = true,
- .handler = &TMH_private_post_transfers,
- .permission = "transfers-write",
- /* the body should be pretty small, allow 1 MB of upload
- to set a conservative bound for sane wallets */
- .max_upload = 1024 * 1024
- },
- /* DELETE /transfers/$ID: */
- {
- .url_prefix = "/transfers/",
- .method = MHD_HTTP_METHOD_DELETE,
- .permission = "transfers-write",
- .allow_deleted_instance = true,
- .handler = &TMH_private_delete_transfers_ID,
- .have_id_segment = true,
- /* the body should be pretty small, allow 1 MB of upload
- to set a conservative bound for sane wallets */
- .max_upload = 1024 * 1024
- },
- /* GET /transfers: */
- {
- .url_prefix = "/transfers",
- .permission = "transfers-read",
- .method = MHD_HTTP_METHOD_GET,
- .allow_deleted_instance = true,
- .handler = &TMH_private_get_transfers
- },
- /* GET /incoming: */
- {
- .url_prefix = "/incoming",
- .permission = "transfers-read",
- .method = MHD_HTTP_METHOD_GET,
- .allow_deleted_instance = true,
- .handler = &TMH_private_get_incoming
- },
- /* POST /otp-devices: */
- {
- .url_prefix = "/otp-devices",
- .permission = "otp-devices-write",
- .method = MHD_HTTP_METHOD_POST,
- .handler = &TMH_private_post_otp_devices
- },
- /* GET /otp-devices: */
- {
- .url_prefix = "/otp-devices",
- .permission = "opt-devices-read",
- .method = MHD_HTTP_METHOD_GET,
- .handler = &TMH_private_get_otp_devices
- },
- /* GET /otp-devices/$ID/: */
- {
- .url_prefix = "/otp-devices/",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "otp-devices-read",
- .have_id_segment = true,
- .handler = &TMH_private_get_otp_devices_ID
- },
- /* DELETE /otp-devices/$ID/: */
- {
- .url_prefix = "/otp-devices/",
- .method = MHD_HTTP_METHOD_DELETE,
- .permission = "otp-devices-write",
- .have_id_segment = true,
- .handler = &TMH_private_delete_otp_devices_ID
- },
- /* PATCH /otp-devices/$ID/: */
- {
- .url_prefix = "/otp-devices/",
- .method = MHD_HTTP_METHOD_PATCH,
- .permission = "otp-devices-write",
- .have_id_segment = true,
- .handler = &TMH_private_patch_otp_devices_ID
- },
- /* POST /templates: */
- {
- .url_prefix = "/templates",
- .method = MHD_HTTP_METHOD_POST,
- .permission = "templates-write",
- .handler = &TMH_private_post_templates,
- /* allow template data of up to 8 MB, that should be plenty;
- note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
- would require further changes to the allocation logic
- in the code... */
- .max_upload = 1024 * 1024 * 8
- },
- /* GET /templates: */
- {
- .url_prefix = "/templates",
- .permission = "templates-read",
- .method = MHD_HTTP_METHOD_GET,
- .handler = &TMH_private_get_templates
- },
- /* GET /templates/$ID/: */
- {
- .url_prefix = "/templates/",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "templates-read",
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .handler = &TMH_private_get_templates_ID
- },
- /* DELETE /templates/$ID/: */
- {
- .url_prefix = "/templates/",
- .method = MHD_HTTP_METHOD_DELETE,
- .permission = "templates-write",
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .handler = &TMH_private_delete_templates_ID
- },
- /* PATCH /templates/$ID/: */
- {
- .url_prefix = "/templates/",
- .method = MHD_HTTP_METHOD_PATCH,
- .permission = "templates-write",
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .handler = &TMH_private_patch_templates_ID,
- /* allow template data of up to 8 MB, that should be plenty;
- note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
- would require further changes to the allocation logic
- in the code... */
- .max_upload = 1024 * 1024 * 8
- },
- /* GET /webhooks: */
- {
- .url_prefix = "/webhooks",
- .permission = "webhooks-read",
- .method = MHD_HTTP_METHOD_GET,
- .handler = &TMH_private_get_webhooks
- },
- /* POST /webhooks: */
- {
- .url_prefix = "/webhooks",
- .method = MHD_HTTP_METHOD_POST,
- .permission = "webhooks-write",
- .handler = &TMH_private_post_webhooks,
- /* allow webhook data of up to 8 MB, that should be plenty;
- note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
- would require further changes to the allocation logic
- in the code... */
- .max_upload = 1024 * 1024 * 8
- },
- /* GET /webhooks/$ID/: */
- {
- .url_prefix = "/webhooks/",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "webhooks-read",
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .handler = &TMH_private_get_webhooks_ID
- },
- /* DELETE /webhooks/$ID/: */
- {
- .url_prefix = "/webhooks/",
- .permission = "webhooks-write",
- .method = MHD_HTTP_METHOD_DELETE,
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .handler = &TMH_private_delete_webhooks_ID
- },
- /* PATCH /webhooks/$ID/: */
- {
- .url_prefix = "/webhooks/",
- .method = MHD_HTTP_METHOD_PATCH,
- .permission = "webhooks-write",
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .handler = &TMH_private_patch_webhooks_ID,
- /* allow webhook data of up to 8 MB, that should be plenty;
- note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
- would require further changes to the allocation logic
- in the code... */
- .max_upload = 1024 * 1024 * 8
- },
- /* POST /accounts: */
- {
- .url_prefix = "/accounts",
- .method = MHD_HTTP_METHOD_POST,
- .permission = "accounts-write",
- .handler = &TMH_private_post_account,
- /* allow account details of up to 8 kb, that should be plenty */
- .max_upload = 1024 * 8
- },
- /* PATCH /accounts/$H_WIRE: */
- {
- .url_prefix = "/accounts/",
- .method = MHD_HTTP_METHOD_PATCH,
- .permission = "accounts-write",
- .handler = &TMH_private_patch_accounts_ID,
- .have_id_segment = true,
- /* allow account details of up to 8 kb, that should be plenty */
- .max_upload = 1024 * 8
- },
- /* GET /accounts: */
- {
- .url_prefix = "/accounts",
- .permission = "accounts-read",
- .method = MHD_HTTP_METHOD_GET,
- .handler = &TMH_private_get_accounts
- },
- /* GET /accounts/$H_WIRE: */
- {
- .url_prefix = "/accounts/",
- .permission = "accounts-read",
- .method = MHD_HTTP_METHOD_GET,
- .have_id_segment = true,
- .handler = &TMH_private_get_accounts_ID
- },
- /* DELETE /accounts/$H_WIRE: */
- {
- .url_prefix = "/accounts/",
- .permission = "accounts-write",
- .method = MHD_HTTP_METHOD_DELETE,
- .handler = &TMH_private_delete_account_ID,
- .have_id_segment = true
- },
- /* GET /tokens: */
- {
- .url_prefix = "/tokens",
- .permission = "tokens-read",
- .method = MHD_HTTP_METHOD_GET,
- .handler = &TMH_private_get_instances_ID_tokens,
- },
- /* POST /token: */
- {
- .url_prefix = "/token",
- .permission = "token-refresh",
- .method = MHD_HTTP_METHOD_POST,
- .handler = &TMH_private_post_instances_ID_token,
- /* Body should be tiny. */
- .max_upload = 1024
- },
- /* DELETE /tokens/$SERIAL: */
- {
- .url_prefix = "/tokens/",
- .permission = "tokens-write",
- .method = MHD_HTTP_METHOD_DELETE,
- .handler = &TMH_private_delete_instances_ID_token_SERIAL,
- .have_id_segment = true
- },
- /* DELETE /token: */
- {
- .url_prefix = "/token",
- .method = MHD_HTTP_METHOD_DELETE,
- .handler = &TMH_private_delete_instances_ID_token,
- },
- /* GET /tokenfamilies: */
- {
- .url_prefix = "/tokenfamilies",
- .permission = "tokenfamilies-read",
- .method = MHD_HTTP_METHOD_GET,
- .handler = &TMH_private_get_tokenfamilies
- },
- /* POST /tokenfamilies: */
- {
- .url_prefix = "/tokenfamilies",
- .permission = "tokenfamilies-write",
- .method = MHD_HTTP_METHOD_POST,
- .handler = &TMH_private_post_token_families
- },
- /* GET /tokenfamilies/$SLUG/: */
- {
- .url_prefix = "/tokenfamilies/",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "tokenfamilies-read",
- .have_id_segment = true,
- .handler = &TMH_private_get_tokenfamilies_SLUG
- },
- /* DELETE /tokenfamilies/$SLUG/: */
- {
- .url_prefix = "/tokenfamilies/",
- .method = MHD_HTTP_METHOD_DELETE,
- .permission = "tokenfamilies-write",
- .have_id_segment = true,
- .handler = &TMH_private_delete_token_families_SLUG
- },
- /* PATCH /tokenfamilies/$SLUG/: */
- {
- .url_prefix = "/tokenfamilies/",
- .method = MHD_HTTP_METHOD_PATCH,
- .permission = "tokenfamilies-write",
- .have_id_segment = true,
- .handler = &TMH_private_patch_token_family_SLUG,
- },
- #ifdef HAVE_DONAU_DONAU_SERVICE_H
- /* GET /donau */
- {
- .url_prefix = "/donau",
- .method = MHD_HTTP_METHOD_GET,
- .handler = &TMH_private_get_donau_instances
- },
- /* POST /donau */
- {
- .url_prefix = "/donau",
- .method = MHD_HTTP_METHOD_POST,
- .handler = &TMH_private_post_donau_instance
- },
- /* DELETE /donau/$charity-id */
- {
- .url_prefix = "/donau/",
- .method = MHD_HTTP_METHOD_DELETE,
- .have_id_segment = true,
- .handler = &TMH_private_delete_donau_instance_ID
- },
- #endif
- /* GET /statistics-counter/$SLUG: */
- {
- .url_prefix = "/statistics-counter/",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "statistics-read",
- .have_id_segment = true,
- .handler = &TMH_private_get_statistics_counter_SLUG,
- },
- /* GET /statistics-amount/$SLUG: */
- {
- .url_prefix = "/statistics-amount/",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "statistics-read",
- .have_id_segment = true,
- .handler = &TMH_private_get_statistics_amount_SLUG,
- },
- {
- .url_prefix = NULL
- }
- };
- static struct TMH_RequestHandler public_handlers[] = {
- {
- /* for "admin" instance, it does not even
- have to exist before we give the WebUI */
- .url_prefix = "/",
- .method = MHD_HTTP_METHOD_GET,
- .mime_type = "text/html",
- .skip_instance = true,
- .default_only = true,
- .handler = &spa_redirect,
- .response_code = MHD_HTTP_FOUND
- },
- {
- .url_prefix = "/config",
- .method = MHD_HTTP_METHOD_GET,
- .skip_instance = true,
- .default_only = true,
- .handler = &MH_handler_config
- },
- {
- /* for "normal" instance,s they must exist
- before we give the WebUI */
- .url_prefix = "/",
- .method = MHD_HTTP_METHOD_GET,
- .mime_type = "text/html",
- .handler = &spa_redirect,
- .response_code = MHD_HTTP_FOUND
- },
- {
- .url_prefix = "/webui/",
- .method = MHD_HTTP_METHOD_GET,
- .mime_type = "text/html",
- .skip_instance = true,
- .have_id_segment = true,
- .handler = &TMH_return_spa,
- .response_code = MHD_HTTP_OK
- },
- {
- .url_prefix = "/agpl",
- .method = MHD_HTTP_METHOD_GET,
- .skip_instance = true,
- .handler = &TMH_MHD_handler_agpl_redirect
- },
- {
- .url_prefix = "/agpl",
- .method = MHD_HTTP_METHOD_GET,
- .skip_instance = true,
- .handler = &TMH_MHD_handler_agpl_redirect
- },
- {
- .url_prefix = "/terms",
- .method = MHD_HTTP_METHOD_GET,
- .skip_instance = true,
- .handler = &TMH_handler_terms
- },
- {
- .url_prefix = "/privacy",
- .method = MHD_HTTP_METHOD_GET,
- .skip_instance = true,
- .handler = &TMH_handler_privacy
- },
- /* Also serve the same /config per instance */
- {
- .url_prefix = "/config",
- .method = MHD_HTTP_METHOD_GET,
- .handler = &MH_handler_config
- },
- /* POST /orders/$ID/abort: */
- {
- .url_prefix = "/orders/",
- .have_id_segment = true,
- .url_suffix = "abort",
- .method = MHD_HTTP_METHOD_POST,
- .handler = &TMH_post_orders_ID_abort,
- /* wallet may give us many coins to sign, allow 1 MB of upload
- to set a conservative bound for sane wallets */
- .max_upload = 1024 * 1024
- },
- /* POST /orders/$ID/claim: */
- {
- .url_prefix = "/orders/",
- .have_id_segment = true,
- .url_suffix = "claim",
- .method = MHD_HTTP_METHOD_POST,
- .handler = &TMH_post_orders_ID_claim,
- /* the body should be pretty small, allow 1 MB of upload
- to set a conservative bound for sane wallets */
- .max_upload = 1024 * 1024
- },
- /* POST /orders/$ID/pay: */
- {
- .url_prefix = "/orders/",
- .have_id_segment = true,
- .url_suffix = "pay",
- .method = MHD_HTTP_METHOD_POST,
- .handler = &TMH_post_orders_ID_pay,
- /* wallet may give us many coins to sign, allow 1 MB of upload
- to set a conservative bound for sane wallets */
- .max_upload = 1024 * 1024
- },
- /* POST /orders/$ID/paid: */
- {
- .url_prefix = "/orders/",
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .url_suffix = "paid",
- .method = MHD_HTTP_METHOD_POST,
- .handler = &TMH_post_orders_ID_paid,
- /* the body should be pretty small, allow 1 MB of upload
- to set a conservative bound for sane wallets */
- .max_upload = 1024 * 1024
- },
- /* POST /orders/$ID/refund: */
- {
- .url_prefix = "/orders/",
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .url_suffix = "refund",
- .method = MHD_HTTP_METHOD_POST,
- .handler = &TMH_post_orders_ID_refund,
- /* the body should be pretty small, allow 1 MB of upload
- to set a conservative bound for sane wallets */
- .max_upload = 1024 * 1024
- },
- /* GET /orders/$ID: */
- {
- .url_prefix = "/orders/",
- .method = MHD_HTTP_METHOD_GET,
- .allow_deleted_instance = true,
- .have_id_segment = true,
- .handler = &TMH_get_orders_ID
- },
- /* GET /static/ *: */
- {
- .url_prefix = "/static/",
- .method = MHD_HTTP_METHOD_GET,
- .have_id_segment = true,
- .handler = &TMH_return_static
- },
- /* POST /reports/$ID/ */
- {
- .url_prefix = "/reports",
- .method = MHD_HTTP_METHOD_POST,
- .have_id_segment = true,
- .handler = &TMH_post_reports_ID,
- },
- /* GET /templates/$ID/: */
- {
- .url_prefix = "/templates/",
- .method = MHD_HTTP_METHOD_GET,
- .have_id_segment = true,
- .handler = &TMH_get_templates_ID
- },
- /* GET /products/$HASH/image: */
- {
- .url_prefix = "/products/",
- .method = MHD_HTTP_METHOD_GET,
- .have_id_segment = true,
- .allow_deleted_instance = true,
- .url_suffix = "image",
- .handler = &TMH_get_products_image
- },
- /* POST /templates/$ID: */
- {
- .url_prefix = "/templates/",
- .method = MHD_HTTP_METHOD_POST,
- .have_id_segment = true,
- .handler = &TMH_post_using_templates_ID,
- .max_upload = 1024 * 1024
- },
- /* POST /challenge/$ID: */
- {
- .url_prefix = "/challenge/",
- .method = MHD_HTTP_METHOD_POST,
- .have_id_segment = true,
- .handler = &TMH_post_challenge_ID,
- .max_upload = 1024
- },
- /* POST /challenge/$ID/confirm: */
- {
- .url_prefix = "/challenge/",
- .method = MHD_HTTP_METHOD_POST,
- .have_id_segment = true,
- .url_suffix = "confirm",
- .handler = &TMH_post_challenge_ID_confirm,
- .max_upload = 1024
- },
- /* POST /instances */
- {
- .url_prefix = "/instances",
- .method = MHD_HTTP_METHOD_POST,
- .skip_instance = true,
- .default_only = true,
- .handler = &TMH_public_post_instances,
- /* allow instance data of up to 8 MB, that should be plenty;
- note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
- would require further changes to the allocation logic
- in the code... */
- .max_upload = 1024 * 1024 * 8
- },
- /* POST /forgot-password: */
- {
- .url_prefix = "/forgot-password",
- .method = MHD_HTTP_METHOD_POST,
- .handler = &TMH_public_post_instances_ID_auth,
- /* Body should be pretty small. */
- .max_upload = 1024 * 1024
- },
-
- /* Reports endpoints */
- {
- .url_prefix = "reports",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "reports-read",
- .handler = &TMH_private_get_reports,
- },
- {
- .url_prefix = "reports",
- .method = MHD_HTTP_METHOD_POST,
- .permission = "reports-write",
- .handler = &TMH_private_post_reports,
- },
- {
- .url_prefix = "reports",
- .method = MHD_HTTP_METHOD_GET,
- .handler = &TMH_private_get_report,
- .permission = "reports-read",
- .have_id_segment = true,
- },
- {
- .url_prefix = "reports",
- .method = MHD_HTTP_METHOD_PATCH,
- .handler = &TMH_private_patch_report,
- .permission = "reports-write",
- .have_id_segment = true,
- },
- {
- .url_prefix = "reports",
- .method = MHD_HTTP_METHOD_DELETE,
- .handler = &TMH_private_delete_report,
- .permission = "reports-write",
- .have_id_segment = true,
- },
-
- /* Groups endpoints */
- {
- .url_prefix = "groups",
- .method = MHD_HTTP_METHOD_GET,
- .permission = "groups-read",
- .handler = &TMH_private_get_groups,
- },
- {
- .url_prefix = "groups",
- .method = MHD_HTTP_METHOD_POST,
- .permission = "groups-write",
- .handler = &TMH_private_post_groups,
- },
- {
- .url_prefix = "groups",
- .method = MHD_HTTP_METHOD_PATCH,
- .handler = &TMH_private_patch_group,
- .permission = "groups-write",
- .have_id_segment = true,
- },
- {
- .url_prefix = "groups",
- .method = MHD_HTTP_METHOD_DELETE,
- .handler = &TMH_private_delete_group,
- .permission = "groups-write",
- .have_id_segment = true,
- },
-
- /* Money pots endpoints */
- {
- .url_prefix = "pots",
- .method = MHD_HTTP_METHOD_GET,
- .handler = &TMH_private_get_pots,
- .permission = "pots-read",
- },
- {
- .url_prefix = "pots",
- .method = MHD_HTTP_METHOD_POST,
- .handler = &TMH_private_post_pots,
- .permission = "pots-write"
- },
- {
- .url_prefix = "pots",
- .method = MHD_HTTP_METHOD_GET,
- .handler = &TMH_private_get_pot,
- .have_id_segment = true,
- .permission = "pots-read",
- },
- {
- .url_prefix = "pots",
- .method = MHD_HTTP_METHOD_PATCH,
- .handler = &TMH_private_patch_pot,
- .have_id_segment = true,
- .permission = "pots-write"
- },
- {
- .url_prefix = "pots",
- .method = MHD_HTTP_METHOD_DELETE,
- .handler = &TMH_private_delete_pot,
- .have_id_segment = true,
- .permission = "pots-write"
- },
- {
- .url_prefix = "*",
- .method = MHD_HTTP_METHOD_OPTIONS,
- .handler = &handle_server_options
- },
- {
- .url_prefix = NULL
- }
- };
- const char *management_prefix = "/management/";
- const char *private_prefix = "/private/";
- const char *url = *urlp;
- struct TMH_RequestHandler *handlers;
-
- *is_public = false; /* ensure safe default */
- if ( (0 == strncmp (url,
- management_prefix,
- strlen (management_prefix))) )
- {
- handlers = management_handlers;
- *urlp = url + strlen (management_prefix) - 1;
- }
- else if ( (0 == strncmp (url,
- private_prefix,
- strlen (private_prefix))) ||
- (0 == strcmp (url,
- "/private")) )
- {
- handlers = private_handlers;
- if (0 == strcmp (url,
- "/private"))
- *urlp = "/";
- else
- *urlp = url + strlen (private_prefix) - 1;
- }
- else
- {
- handlers = public_handlers;
- *is_public = true;
- }
- return handlers;
-}
-
-
-/**
- * Identify the handler of the request from the @a url and @a method
- *
- * @param[in,out] hc handler context to update with applicable handler
- * @param handlers array of handlers to consider
- * @param url URL to match against the handlers
- * @param method HTTP access method to consider
- * @param use_admin set to true if we are using the admin instance
- * @return #GNUNET_OK on success,
- * #GNUNET_NO if an error was queued (return #MHD_YES)
- * #GNUNET_SYSERR to close the connection (return #MHD_NO)
- */
-static enum GNUNET_GenericReturnValue
-identify_handler (struct TMH_HandlerContext *hc,
- const struct TMH_RequestHandler *handlers,
- const char *url,
- const char *method,
- bool use_admin)
-{
- size_t prefix_strlen; /* i.e. 8 for "/orders/", or 7 for "/config" */
- const char *infix_url = NULL; /* i.e. "$ORDER_ID", no '/'-es */
- size_t infix_strlen = 0; /* number of characters in infix_url */
- const char *suffix_url = NULL; /* i.e. "refund", excludes '/' at the beginning */
- size_t suffix_strlen = 0; /* number of characters in suffix_url */
-
- if (0 == strcasecmp (method,
- MHD_HTTP_METHOD_HEAD))
- method = MHD_HTTP_METHOD_GET; /* MHD will deal with the rest */
- if (0 == strcmp (url,
- ""))
- url = "/"; /* code below does not like empty string */
-
- /* parse the URL into the three different components */
- {
- const char *slash;
-
- slash = strchr (&url[1], '/');
- if (NULL == slash)
- {
- /* the prefix was everything */
- prefix_strlen = strlen (url);
- }
- else
- {
- prefix_strlen = slash - url + 1; /* includes both '/'-es if present! */
- infix_url = slash + 1;
- slash = strchr (infix_url, '/');
- if (NULL == slash)
- {
- /* the infix was the rest */
- infix_strlen = strlen (infix_url);
- }
- else
- {
- infix_strlen = slash - infix_url; /* excludes both '/'-es */
- suffix_url = slash + 1; /* skip the '/' */
- suffix_strlen = strlen (suffix_url);
- }
- hc->infix = GNUNET_strndup (infix_url,
- infix_strlen);
- }
- }
-
- /* find matching handler */
- {
- bool url_found = false;
-
- for (unsigned int i = 0; NULL != handlers[i].url_prefix; i++)
- {
- const struct TMH_RequestHandler *rh = &handlers[i];
-
- if (rh->default_only && (! use_admin))
- continue;
- if (! prefix_match (rh,
- url,
- prefix_strlen,
- infix_url,
- infix_strlen,
- suffix_url,
- suffix_strlen))
- continue;
- url_found = true;
- if (0 == strcasecmp (method,
- MHD_HTTP_METHOD_OPTIONS))
- {
- return (MHD_YES ==
- TALER_MHD_reply_cors_preflight (hc->connection))
- ? GNUNET_NO
- : GNUNET_SYSERR;
- }
- if ( (rh->method != NULL) &&
- (0 != strcasecmp (method,
- rh->method)) )
- continue;
- hc->rh = rh;
- break;
- }
- /* Handle HTTP 405: METHOD NOT ALLOWED case */
- if ( (NULL == hc->rh) &&
- (url_found) )
- {
- struct MHD_Response *reply;
- MHD_RESULT ret;
- char *allowed = NULL;
-
- GNUNET_break_op (0);
- /* compute 'Allowed:' header (required by HTTP spec for 405 replies) */
- for (unsigned int i = 0; NULL != handlers[i].url_prefix; i++)
- {
- const struct TMH_RequestHandler *rh = &handlers[i];
-
- if (rh->default_only && (! use_admin))
- continue;
- if (! prefix_match (rh,
- url,
- prefix_strlen,
- infix_url,
- infix_strlen,
- suffix_url,
- suffix_strlen))
- continue;
- if (NULL == allowed)
- {
- allowed = GNUNET_strdup (rh->method);
- }
- else
- {
- char *tmp;
-
- GNUNET_asprintf (&tmp,
- "%s, %s",
- allowed,
- rh->method);
- GNUNET_free (allowed);
- allowed = tmp;
- }
- if (0 == strcasecmp (rh->method,
- MHD_HTTP_METHOD_GET))
- {
- char *tmp;
-
- GNUNET_asprintf (&tmp,
- "%s, %s",
- allowed,
- MHD_HTTP_METHOD_HEAD);
- GNUNET_free (allowed);
- allowed = tmp;
- }
- }
- reply = TALER_MHD_make_error (TALER_EC_GENERIC_METHOD_INVALID,
- method);
- GNUNET_break (MHD_YES ==
- MHD_add_response_header (reply,
- MHD_HTTP_HEADER_ALLOW,
- allowed));
- GNUNET_free (allowed);
- ret = MHD_queue_response (hc->connection,
- MHD_HTTP_METHOD_NOT_ALLOWED,
- reply);
- MHD_destroy_response (reply);
- return (MHD_YES == ret)
- ? GNUNET_NO
- : GNUNET_SYSERR;
- }
- if (NULL == hc->rh)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Endpoint `%s' not known\n",
- hc->url);
- return (MHD_YES ==
- TALER_MHD_reply_with_error (hc->connection,
- MHD_HTTP_NOT_FOUND,
- TALER_EC_GENERIC_ENDPOINT_UNKNOWN,
- hc->url))
- ? GNUNET_NO
- : GNUNET_SYSERR;
- }
- }
- return GNUNET_OK;
-}
-
-
-/**
- * Check if the client has provided the necessary credentials
- * to access the selected endpoint of the selected instance.
- *
- * @param[in,out] hc handler context
- * @return #GNUNET_OK on success,
- * #GNUNET_NO if an error was queued (return #MHD_YES)
- * #GNUNET_SYSERR to close the connection (return #MHD_NO)
- */
-static enum GNUNET_GenericReturnValue
-perform_access_control (struct TMH_HandlerContext *hc)
-{
- const char *auth;
- bool is_basic_auth = false;
- bool auth_malformed = false;
-
- auth = MHD_lookup_connection_value (hc->connection,
- MHD_HEADER_KIND,
- MHD_HTTP_HEADER_AUTHORIZATION);
-
- if (NULL != auth)
- {
- extract_auth (&auth,
- &is_basic_auth);
- if (NULL == auth)
- auth_malformed = true;
- hc->auth_token = auth;
- }
-
- /* If we have zero configured instances (not even ones that have been
- purged) or explicitly disabled authentication, THEN we accept anything
- (no access control), as we then also have no data to protect. */
- if ((0 == GNUNET_CONTAINER_multihashmap_size (TMH_by_id_map)) ||
- (GNUNET_YES == TMH_auth_disabled))
- {
- hc->auth_scope = TMH_AS_ALL;
- }
- else if (is_basic_auth)
- {
- process_basic_auth (hc,
- auth);
- }
- else /* Check bearer token */
- {
- enum TALER_ErrorCode ec;
-
- ec = process_bearer_auth (hc,
- auth);
- if (TALER_EC_NONE != ec)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Bearer authentication failed: %d\n",
- (int) ec);
- return (MHD_YES ==
- TALER_MHD_reply_with_ec (hc->connection,
- ec,
- NULL))
- ? GNUNET_NO
- : GNUNET_SYSERR;
- }
- }
- /* We grant access if:
- - Endpoint does not require permissions
- - Authorization scope of bearer token contains permissions
- required by endpoint.
- */
- if ( (NULL != hc->rh->permission) &&
- (! permission_in_scope (hc->rh->permission,
- hc->auth_scope)))
- {
- if (auth_malformed &&
- (TMH_AS_NONE == hc->auth_scope) )
- {
- GNUNET_break_op (0);
- return (MHD_YES ==
- TALER_MHD_reply_with_error (
- hc->connection,
- MHD_HTTP_UNAUTHORIZED,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "'" RFC_8959_PREFIX
- "' prefix or 'Bearer' missing in 'Authorization' header"))
- ? GNUNET_NO
- : GNUNET_SYSERR;
- }
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Credentials provided are %d which are insufficient for access to `%s'\n",
- (int) hc->auth_scope,
- hc->rh->permission);
- return (MHD_YES ==
- TALER_MHD_reply_with_error (
- hc->connection,
- MHD_HTTP_UNAUTHORIZED,
- TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED,
- "Check credentials in 'Authorization' header"))
- ? GNUNET_NO
- : GNUNET_SYSERR;
- }
- return GNUNET_OK;
-}
-
-
-/**
* A client has requested the given url using the given method
* (#MHD_HTTP_METHOD_GET, #MHD_HTTP_METHOD_PUT,
* #MHD_HTTP_METHOD_DELETE, #MHD_HTTP_METHOD_POST, etc). The callback
@@ -2851,16 +682,13 @@ url_handler (void *cls,
}
{
- const struct TMH_RequestHandler *handlers;
enum GNUNET_GenericReturnValue ret;
- handlers = determine_handler_group (&url,
- &is_public);
- ret = identify_handler (hc,
- handlers,
- url,
- method,
- use_admin);
+ ret = TMH_dispatch_request (hc,
+ url,
+ method,
+ use_admin,
+ &is_public);
if (GNUNET_OK != ret)
return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
}
@@ -2886,7 +714,7 @@ url_handler (void *cls,
{
enum GNUNET_GenericReturnValue ret;
- ret = perform_access_control (hc);
+ ret = TMH_perform_access_control (hc);
if (GNUNET_OK != ret)
return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
}
diff --git a/src/backend/taler-merchant-httpd.h b/src/backend/taler-merchant-httpd.h
@@ -39,6 +39,70 @@
/**
+ * Possible authorization scopes. This is a bit mask.
+ */
+enum TMH_AuthScope
+{
+ /**
+ * Nothing is authorized.
+ */
+ TMH_AS_NONE = 0,
+
+ /**
+ * Read-only access is OK. Any GET request is
+ * automatically OK.
+ */
+ TMH_AS_READ_ONLY = 1,
+
+ /**
+ * 2 is Reserved. Was refreshable pre v42
+ */
+
+ /**
+ * Order creation and payment status check only
+ */
+ TMH_AS_ORDER_SIMPLE = 3,
+
+ /**
+ * Order creation and inventory locking,
+ * includes #TMH_AS_ORDER_SIMPLE
+ */
+ TMH_AS_ORDER_POS = 4,
+
+ /**
+ * Order creation and refund
+ */
+ TMH_AS_ORDER_MGMT = 5,
+
+ /**
+ * Order full
+ * Includes #TMH_AS_ORDER_POS and #TMH_AS_ORDER_MGMT
+ */
+ TMH_AS_ORDER_FULL = 6,
+
+ /**
+ * Full access is granted to everything.
+ * We want to deprecate and remove this!
+ * Old scope "write"
+ */
+ TMH_AS_ALL = 7 | 1 << 30,
+
+ /**
+ * Full access is granted to everything.
+ */
+ TMH_AS_SPA = 8,
+
+ /**
+ * /login access to renew the token is OK.
+ * This is actually combined with other scopes
+ * and not (usually) used as a scope itself.
+ */
+ TMH_AS_REFRESHABLE = 1 << 30,
+
+};
+
+
+/**
* Supported wire method. Kept in a DLL.
*/
struct TMH_WireMethod
@@ -409,70 +473,6 @@ struct TMH_HandlerContext;
/**
- * Possible authorization scopes. This is a bit mask.
- */
-enum TMH_AuthScope
-{
- /**
- * Nothing is authorized.
- */
- TMH_AS_NONE = 0,
-
- /**
- * Read-only access is OK. Any GET request is
- * automatically OK.
- */
- TMH_AS_READ_ONLY = 1,
-
- /**
- * 2 is Reserved. Was refreshable pre v42
- */
-
- /**
- * Order creation and payment status check only
- */
- TMH_AS_ORDER_SIMPLE = 3,
-
- /**
- * Order creation and inventory locking,
- * includes #TMH_AS_ORDER_SIMPLE
- */
- TMH_AS_ORDER_POS = 4,
-
- /**
- * Order creation and refund
- */
- TMH_AS_ORDER_MGMT = 5,
-
- /**
- * Order full
- * Includes #TMH_AS_ORDER_POS and #TMH_AS_ORDER_MGMT
- */
- TMH_AS_ORDER_FULL = 6,
-
- /**
- * Full access is granted to everything.
- * We want to deprecate and remove this!
- * Old scope "write"
- */
- TMH_AS_ALL = 7 | 1 << 30,
-
- /**
- * Full access is granted to everything.
- */
- TMH_AS_SPA = 8,
-
- /**
- * /login access to renew the token is OK.
- * This is actually combined with other scopes
- * and not (usually) used as a scope itself.
- */
- TMH_AS_REFRESHABLE = 1 << 30,
-
-};
-
-
-/**
* @brief Struct describing an URL and the handler for it.
*
* The overall URL is always @e url_prefix, optionally followed by the
@@ -916,66 +916,4 @@ void
TMH_reload_instances (const char *id);
-/**
- * Check that @a token hashes to @a hash under @a salt for
- * merchant instance authentication.
- *
- * @param token the token to check
- * @param salt the salt to use when hashing
- * @param hash the hash to check against
- * @return #GNUNET_OK if the @a token matches
- */
-enum GNUNET_GenericReturnValue
-TMH_check_auth (const char *token,
- struct TALER_MerchantAuthenticationSaltP *salt,
- struct TALER_MerchantAuthenticationHashP *hash);
-
-
-/**
- * Compute a @a hash from @a token hashes for
- * merchant instance authentication.
- *
- * @param password the password to check
- * @param[out] salt set to a fresh random salt
- * @param[out] hash set to the hash of @a token under @a salt
- */
-void
-TMH_compute_auth (const char *password,
- struct TALER_MerchantAuthenticationSaltP *salt,
- struct TALER_MerchantAuthenticationHashP *hash);
-
-
-/**
- * Check if @a candidate permissions are a subset of @a as permissions
- *
- * @param as scope to check against
- * @param candidate scope to check if its permissions are a subset of @a as permissions.
- * @return true if it was a subset, false otherwise.
- */
-bool
-TMH_scope_is_subset (enum TMH_AuthScope as,
- enum TMH_AuthScope candidate);
-
-
-/**
- * Return the TMH_AuthScope corresponding to @a name.
- *
- * @param name the name to look for
- * @return the scope corresponding to the name, or TMH_AS_NONE.
- */
-enum TMH_AuthScope
-TMH_get_scope_by_name (const char *name);
-
-
-/**
- * Return the name corresponding to @a scop.
- *
- * @param scope the scope to look for
- * @param[out] refreshable outputs if scope value was refreshable
- * @return the name corresponding to the scope, or NULL.
- */
-const char *
-TMH_get_name_by_scope (enum TMH_AuthScope scope,
- bool *refreshable);
-
#endif
diff --git a/src/backend/taler-merchant-httpd_auth.c b/src/backend/taler-merchant-httpd_auth.c
@@ -0,0 +1,739 @@
+/*
+ This file is part of TALER
+ (C) 2014--2025 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ 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 taler-merchant-httpd_auth.c
+ * @brief client authentication logic
+ * @author Martin Schanzenbach
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_db_lib.h>
+#include <taler/taler_json_lib.h>
+#include "taler-merchant-httpd_auth.h"
+#include "taler-merchant-httpd_helper.h"
+
+/**
+ * Maximum length of a permissions string of a scope
+ */
+#define TMH_MAX_SCOPE_PERMISSIONS_LEN 4096
+
+/**
+ * Maximum length of a name of a scope
+ */
+#define TMH_MAX_NAME_LEN 255
+
+/**
+ * Represents a hard-coded set of default scopes with their
+ * permissions and names
+ */
+struct ScopePermissionMap
+{
+ /**
+ * The scope enum value
+ */
+ enum TMH_AuthScope as;
+
+ /**
+ * The scope name
+ */
+ char name[TMH_MAX_NAME_LEN];
+
+ /**
+ * The scope permissions string.
+ * Comma-separated.
+ */
+ char permissions[TMH_MAX_SCOPE_PERMISSIONS_LEN];
+};
+
+/**
+ * The default scopes array for merchant
+ */
+static struct ScopePermissionMap scope_permissions[] = {
+ /* Deprecated since v19 */
+ {
+ .as = TMH_AS_ALL,
+ .name = "write",
+ .permissions = "*"
+ },
+ /* Full access for SPA */
+ {
+ .as = TMH_AS_ALL,
+ .name = "all",
+ .permissions = "*"
+ },
+ /* Full access for SPA */
+ {
+ .as = TMH_AS_SPA,
+ .name = "spa",
+ .permissions = "*"
+ },
+ /* Read-only access */
+ {
+ .as = TMH_AS_READ_ONLY,
+ .name = "readonly",
+ .permissions = "*-read"
+ },
+ /* Simple order management */
+ {
+ .as = TMH_AS_ORDER_SIMPLE,
+ .name = "order-simple",
+ .permissions = "orders-read,orders-write"
+ },
+ /* Simple order management for PoS, also allows inventory locking */
+ {
+ .as = TMH_AS_ORDER_POS,
+ .name = "order-pos",
+ .permissions = "orders-read,orders-write,inventory-lock"
+ },
+ /* Simple order management, also allows refunding */
+ {
+ .as = TMH_AS_ORDER_MGMT,
+ .name = "order-mgmt",
+ .permissions = "orders-read,orders-write,orders-refund"
+ },
+ /* Full order management, allows inventory locking and refunds */
+ {
+ .as = TMH_AS_ORDER_FULL,
+ .name = "order-full",
+ .permissions = "orders-read,orders-write,inventory-lock,orders-refund"
+ },
+ /* No permissions, dummy scope */
+ {
+ .as = TMH_AS_NONE,
+ }
+};
+
+
+/**
+ * Get permissions string for scope.
+ * Also extracts the leftmost bit into the @a refreshable
+ * output parameter.
+ *
+ * @param as the scope to get the permissions string from
+ * @param[out] refreshable true if the token associated with this scope is refreshable.
+ * @return the permissions string, or NULL if no such scope found
+ */
+static const char*
+get_scope_permissions (enum TMH_AuthScope as,
+ bool *refreshable)
+{
+ *refreshable = as & TMH_AS_REFRESHABLE;
+ for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++)
+ {
+ /* We ignore the TMH_AS_REFRESHABLE bit */
+ if ( (as & ~TMH_AS_REFRESHABLE) ==
+ (scope_permissions[i].as & ~TMH_AS_REFRESHABLE) )
+ return scope_permissions[i].permissions;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Failed to find required permissions for scope %d\n",
+ as);
+ return NULL;
+}
+
+
+/**
+ * Extract the token from authorization header value @a auth.
+ * The @a auth value can be a bearer token or a Basic
+ * authentication header. In both cases, this function
+ * updates @a auth to point to the actual credential,
+ * skipping spaces.
+ *
+ * NOTE: We probably want to replace this function with MHD2
+ * API calls in the future that are more robust.
+ *
+ * @param[in,out] auth pointer to authorization header value,
+ * will be updated to point to the start of the token
+ * or set to NULL if header value is invalid
+ * @param[out] is_basic_auth will be set to true if the
+ * authorization header uses basic authentication,
+ * otherwise to false
+ */
+static void
+extract_auth (const char **auth,
+ bool *is_basic_auth)
+{
+ const char *bearer = "Bearer ";
+ const char *basic = "Basic ";
+ const char *tok = *auth;
+ size_t offset = 0;
+ bool is_bearer = false;
+
+ *is_basic_auth = false;
+ if (0 == strncmp (tok,
+ bearer,
+ strlen (bearer)))
+ {
+ offset = strlen (bearer);
+ is_bearer = true;
+ }
+ else if (0 == strncmp (tok,
+ basic,
+ strlen (basic)))
+ {
+ offset = strlen (basic);
+ *is_basic_auth = true;
+ }
+ else
+ {
+ *auth = NULL;
+ return;
+ }
+ tok += offset;
+ while (' ' == *tok)
+ tok++;
+ if ( (is_bearer) &&
+ (0 != strncasecmp (tok,
+ RFC_8959_PREFIX,
+ strlen (RFC_8959_PREFIX))) )
+ {
+ *auth = NULL;
+ return;
+ }
+ *auth = tok;
+}
+
+
+/**
+ * Check if @a userpass grants access to @a instance.
+ *
+ * @param userpass base64 encoded "$USERNAME:$PASSWORD" value
+ * from HTTP Basic "Authentication" header
+ * @param instance the access controlled instance
+ */
+static enum GNUNET_GenericReturnValue
+check_auth_instance (const char *userpass,
+ struct TMH_MerchantInstance *instance)
+{
+ char *tmp;
+ char *colon;
+ const char *instance_name;
+ const char *password;
+ const char *target_instance = "admin";
+ enum GNUNET_GenericReturnValue ret;
+
+ /* implicitly a zeroed out hash means no authentication */
+ if (GNUNET_is_zero (&instance->auth.auth_hash))
+ return GNUNET_OK;
+ if (NULL == userpass)
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ if (0 ==
+ GNUNET_STRINGS_base64_decode (userpass,
+ strlen (userpass),
+ (void**) &tmp))
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ colon = strchr (tmp,
+ ':');
+ if (NULL == colon)
+ {
+ GNUNET_break_op (0);
+ GNUNET_free (tmp);
+ return GNUNET_SYSERR;
+ }
+ *colon = '\0';
+ instance_name = tmp;
+ password = colon + 1;
+ /* instance->settings.id can be NULL if there is no instance yet */
+ if (NULL != instance->settings.id)
+ target_instance = instance->settings.id;
+ if (0 != strcmp (instance_name,
+ target_instance))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Somebody tried to login to instance %s with username %s (login failed).\n",
+ target_instance,
+ instance_name);
+ GNUNET_free (tmp);
+ return GNUNET_SYSERR;
+ }
+ ret = TMH_check_auth (password,
+ &instance->auth.auth_salt,
+ &instance->auth.auth_hash);
+ GNUNET_free (tmp);
+ if (GNUNET_OK != ret)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Password provided does not match credentials for %s\n",
+ target_instance);
+ }
+ return ret;
+}
+
+
+void
+TMH_compute_auth (const char *token,
+ struct TALER_MerchantAuthenticationSaltP *salt,
+ struct TALER_MerchantAuthenticationHashP *hash)
+{
+ GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE,
+ salt,
+ sizeof (*salt));
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Computing initial auth using token with salt %s\n",
+ TALER_B2S (salt));
+ TALER_merchant_instance_auth_hash_with_salt (hash,
+ salt,
+ token);
+}
+
+
+/**
+ * Function used to process Basic authorization header value.
+ * Sets correct scope in the auth_scope parameter of the
+ * #TMH_HandlerContext.
+ *
+ * @param hc the handler context
+ * @param authn_s the value of the authorization header
+ */
+static void
+process_basic_auth (struct TMH_HandlerContext *hc,
+ const char *authn_s)
+{
+ /* Handle token endpoint slightly differently: Only allow
+ * instance password (Basic auth) to retrieve access token.
+ * We need to handle authorization with Basic auth here first
+ * The only time we need to handle authentication like this is
+ * for the token endpoint!
+ */
+ if ( (0 != strncmp (hc->rh->url_prefix,
+ "/token",
+ strlen ("/token"))) ||
+ (0 != strncmp (MHD_HTTP_METHOD_POST,
+ hc->rh->method,
+ strlen (MHD_HTTP_METHOD_POST))) ||
+ (NULL == hc->instance))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Called endpoint `%s' with Basic authentication. Rejecting...\n",
+ hc->rh->url_prefix);
+ hc->auth_scope = TMH_AS_NONE;
+ return;
+ }
+ if (GNUNET_OK ==
+ check_auth_instance (authn_s,
+ hc->instance))
+ {
+ hc->auth_scope = TMH_AS_ALL;
+ }
+ else
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Basic authentication failed!\n");
+ hc->auth_scope = TMH_AS_NONE;
+ }
+}
+
+
+/**
+ * Function used to process Bearer authorization header value.
+ * Sets correct scope in the auth_scope parameter of the
+ * #TMH_HandlerContext..
+ *
+ * @param hc the handler context
+ * @param authn_s the value of the authorization header
+ * @return TALER_EC_NONE on success.
+ */
+static enum TALER_ErrorCode
+process_bearer_auth (struct TMH_HandlerContext *hc,
+ const char *authn_s)
+{
+ if (NULL == hc->instance)
+ {
+ hc->auth_scope = TMH_AS_NONE;
+ return TALER_EC_NONE;
+ }
+ if (GNUNET_is_zero (&hc->instance->auth.auth_hash))
+ {
+ /* hash zero means no authentication for instance */
+ hc->auth_scope = TMH_AS_ALL;
+ return TALER_EC_NONE;
+ }
+ {
+ enum TALER_ErrorCode ec;
+
+ ec = TMH_check_token (authn_s,
+ hc->instance->settings.id,
+ &hc->auth_scope);
+ if (TALER_EC_NONE != ec)
+ {
+ char *dec;
+ size_t dec_len;
+ const char *token;
+
+ /* NOTE: Deprecated, remove sometime after v1.1 */
+ if (0 != strncasecmp (authn_s,
+ RFC_8959_PREFIX,
+ strlen (RFC_8959_PREFIX)))
+ {
+ GNUNET_break_op (0);
+ hc->auth_scope = TMH_AS_NONE;
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Authentication token invalid: %d\n",
+ (int) ec);
+ return ec;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Trying deprecated secret-token:password API authN\n");
+ token = authn_s + strlen (RFC_8959_PREFIX);
+ dec_len = GNUNET_STRINGS_urldecode (token,
+ strlen (token),
+ &dec);
+ if ( (0 == dec_len) ||
+ (GNUNET_OK !=
+ TMH_check_auth (dec,
+ &hc->instance->auth.auth_salt,
+ &hc->instance->auth.auth_hash)) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Login failed\n");
+ hc->auth_scope = TMH_AS_NONE;
+ GNUNET_free (dec);
+ return TALER_EC_NONE;
+ }
+ hc->auth_scope = TMH_AS_ALL;
+ GNUNET_free (dec);
+ }
+ }
+ return TALER_EC_NONE;
+}
+
+
+/**
+ * Checks if @a permission_required is in permissions of
+ * @a scope.
+ *
+ * @param permission_required the permission to check.
+ * @param scope the scope to check.
+ * @return true if @a permission_required is in the permissions set of @a scope.
+ */
+static bool
+permission_in_scope (const char *permission_required,
+ enum TMH_AuthScope scope)
+{
+ char *permissions;
+ const char *perms_tmp;
+ bool is_read_perm = false;
+ bool is_write_perm = false;
+ bool refreshable;
+ const char *last_dash;
+
+ perms_tmp = get_scope_permissions (scope,
+ &refreshable);
+ if (NULL == perms_tmp)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Permission check failed: scope %d not understood\n",
+ (int) scope);
+ return false;
+ }
+ last_dash = strrchr (permission_required,
+ '-');
+ if (NULL != last_dash)
+ {
+ is_write_perm = (0 == strcmp (last_dash,
+ "-write"));
+ is_read_perm = (0 == strcmp (last_dash,
+ "-read"));
+ }
+
+ if (0 == strcmp ("token-refresh",
+ permission_required))
+ {
+ if (! refreshable)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Permission check failed: token not refreshable\n");
+ }
+ return refreshable;
+ }
+ permissions = GNUNET_strdup (perms_tmp);
+ {
+ const char *perm = strtok (permissions,
+ ",");
+
+ if (NULL == perm)
+ {
+ GNUNET_free (permissions);
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Permission check failed: empty permission set\n");
+ return false;
+ }
+ while (NULL != perm)
+ {
+ if (0 == strcmp ("*",
+ perm))
+ {
+ GNUNET_free (permissions);
+ return true;
+ }
+ if ( (0 == strcmp ("*-write",
+ perm)) &&
+ (is_write_perm) )
+ {
+ GNUNET_free (permissions);
+ return true;
+ }
+ if ( (0 == strcmp ("*-read",
+ perm)) &&
+ (is_read_perm) )
+ {
+ GNUNET_free (permissions);
+ return true;
+ }
+ if (0 == strcmp (permission_required,
+ perm))
+ {
+ GNUNET_free (permissions);
+ return true;
+ }
+ perm = strtok (NULL,
+ ",");
+ }
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Permission check failed: %s not found in %s\n",
+ permission_required,
+ permissions);
+ GNUNET_free (permissions);
+ return false;
+}
+
+
+bool
+TMH_scope_is_subset (enum TMH_AuthScope as,
+ enum TMH_AuthScope candidate)
+{
+ const char *as_perms;
+ const char *candidate_perms;
+ char *permissions;
+ bool as_refreshable;
+ bool cand_refreshable;
+
+ as_perms = get_scope_permissions (as,
+ &as_refreshable);
+ candidate_perms = get_scope_permissions (candidate,
+ &cand_refreshable);
+ if (! as_refreshable && cand_refreshable)
+ return false;
+ if ( (NULL == as_perms) &&
+ (NULL != candidate_perms) )
+ return false;
+ if ( (NULL == candidate_perms) ||
+ (0 == strcmp ("*",
+ as_perms)))
+ return true;
+ permissions = GNUNET_strdup (candidate_perms);
+ {
+ const char *perm;
+
+ perm = strtok (permissions,
+ ",");
+ if (NULL == perm)
+ {
+ GNUNET_free (permissions);
+ return true;
+ }
+ while (NULL != perm)
+ {
+ if (! permission_in_scope (perm,
+ as))
+ {
+ GNUNET_free (permissions);
+ return false;
+ }
+ perm = strtok (NULL,
+ ",");
+ }
+ }
+ GNUNET_free (permissions);
+ return true;
+}
+
+
+enum TMH_AuthScope
+TMH_get_scope_by_name (const char *name)
+{
+ for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++)
+ {
+ if (0 == strcasecmp (scope_permissions[i].name,
+ name))
+ return scope_permissions[i].as;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Name `%s' does not match any scope we understand\n",
+ name);
+ return TMH_AS_NONE;
+}
+
+
+const char*
+TMH_get_name_by_scope (enum TMH_AuthScope scope,
+ bool *refreshable)
+{
+ *refreshable = scope & TMH_AS_REFRESHABLE;
+ for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++)
+ {
+ /* We ignore the TMH_AS_REFRESHABLE bit */
+ if ( (scope & ~TMH_AS_REFRESHABLE) ==
+ (scope_permissions[i].as & ~TMH_AS_REFRESHABLE) )
+ return scope_permissions[i].name;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Scope #%d does not match any scope we understand\n",
+ (int) scope);
+ return NULL;
+}
+
+
+enum GNUNET_GenericReturnValue
+TMH_check_auth (const char *password,
+ struct TALER_MerchantAuthenticationSaltP *salt,
+ struct TALER_MerchantAuthenticationHashP *hash)
+{
+ struct TALER_MerchantAuthenticationHashP val;
+
+ if (GNUNET_is_zero (hash))
+ return GNUNET_OK;
+ if (NULL == password)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Denying access: empty password provided\n");
+ return GNUNET_SYSERR;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Checking against token with salt %s\n",
+ TALER_B2S (salt));
+ TALER_merchant_instance_auth_hash_with_salt (&val,
+ salt,
+ password);
+ if (0 !=
+ GNUNET_memcmp (&val,
+ hash))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Access denied: password does not match\n");
+ return GNUNET_SYSERR;
+ }
+ return GNUNET_OK;
+}
+
+
+/**
+ * Check if the client has provided the necessary credentials
+ * to access the selected endpoint of the selected instance.
+ *
+ * @param[in,out] hc handler context
+ * @return #GNUNET_OK on success,
+ * #GNUNET_NO if an error was queued (return #MHD_YES)
+ * #GNUNET_SYSERR to close the connection (return #MHD_NO)
+ */
+enum GNUNET_GenericReturnValue
+TMH_perform_access_control (struct TMH_HandlerContext *hc)
+{
+ const char *auth;
+ bool is_basic_auth = false;
+ bool auth_malformed = false;
+
+ auth = MHD_lookup_connection_value (hc->connection,
+ MHD_HEADER_KIND,
+ MHD_HTTP_HEADER_AUTHORIZATION);
+
+ if (NULL != auth)
+ {
+ extract_auth (&auth,
+ &is_basic_auth);
+ if (NULL == auth)
+ auth_malformed = true;
+ hc->auth_token = auth;
+ }
+
+ /* If we have zero configured instances (not even ones that have been
+ purged) or explicitly disabled authentication, THEN we accept anything
+ (no access control), as we then also have no data to protect. */
+ if ((0 == GNUNET_CONTAINER_multihashmap_size (TMH_by_id_map)) ||
+ (GNUNET_YES == TMH_auth_disabled))
+ {
+ hc->auth_scope = TMH_AS_ALL;
+ }
+ else if (is_basic_auth)
+ {
+ process_basic_auth (hc,
+ auth);
+ }
+ else /* Check bearer token */
+ {
+ enum TALER_ErrorCode ec;
+
+ ec = process_bearer_auth (hc,
+ auth);
+ if (TALER_EC_NONE != ec)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Bearer authentication failed: %d\n",
+ (int) ec);
+ return (MHD_YES ==
+ TALER_MHD_reply_with_ec (hc->connection,
+ ec,
+ NULL))
+ ? GNUNET_NO
+ : GNUNET_SYSERR;
+ }
+ }
+ /* We grant access if:
+ - Endpoint does not require permissions
+ - Authorization scope of bearer token contains permissions
+ required by endpoint.
+ */
+ if ( (NULL != hc->rh->permission) &&
+ (! permission_in_scope (hc->rh->permission,
+ hc->auth_scope)))
+ {
+ if (auth_malformed &&
+ (TMH_AS_NONE == hc->auth_scope) )
+ {
+ GNUNET_break_op (0);
+ return (MHD_YES ==
+ TALER_MHD_reply_with_error (
+ hc->connection,
+ MHD_HTTP_UNAUTHORIZED,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "'" RFC_8959_PREFIX
+ "' prefix or 'Bearer' missing in 'Authorization' header"))
+ ? GNUNET_NO
+ : GNUNET_SYSERR;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Credentials provided are %d which are insufficient for access to `%s'\n",
+ (int) hc->auth_scope,
+ hc->rh->permission);
+ return (MHD_YES ==
+ TALER_MHD_reply_with_error (
+ hc->connection,
+ MHD_HTTP_UNAUTHORIZED,
+ TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED,
+ "Check credentials in 'Authorization' header"))
+ ? GNUNET_NO
+ : GNUNET_SYSERR;
+ }
+ return GNUNET_OK;
+}
diff --git a/src/backend/taler-merchant-httpd_auth.h b/src/backend/taler-merchant-httpd_auth.h
@@ -0,0 +1,103 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2021-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-merchant-httpd_auth.h
+ * @brief request authentication logic
+ * @author Florian Dold
+ * @author Martin Schanzenbach
+ * @author Christian Grothoff
+ */
+#ifndef TALER_MERCHANT_HTTPD_AUTH_H
+#define TALER_MERCHANT_HTTPD_AUTH_H
+
+#include "taler-merchant-httpd.h"
+
+/**
+ * Check that @a token hashes to @a hash under @a salt for
+ * merchant instance authentication.
+ *
+ * @param token the token to check
+ * @param salt the salt to use when hashing
+ * @param hash the hash to check against
+ * @return #GNUNET_OK if the @a token matches
+ */
+enum GNUNET_GenericReturnValue
+TMH_check_auth (const char *token,
+ struct TALER_MerchantAuthenticationSaltP *salt,
+ struct TALER_MerchantAuthenticationHashP *hash);
+
+
+/**
+ * Compute a @a hash from @a token hashes for
+ * merchant instance authentication.
+ *
+ * @param password the password to check
+ * @param[out] salt set to a fresh random salt
+ * @param[out] hash set to the hash of @a token under @a salt
+ */
+void
+TMH_compute_auth (const char *password,
+ struct TALER_MerchantAuthenticationSaltP *salt,
+ struct TALER_MerchantAuthenticationHashP *hash);
+
+
+/**
+ * Check if @a candidate permissions are a subset of @a as permissions
+ *
+ * @param as scope to check against
+ * @param candidate scope to check if its permissions are a subset of @a as permissions.
+ * @return true if it was a subset, false otherwise.
+ */
+bool
+TMH_scope_is_subset (enum TMH_AuthScope as,
+ enum TMH_AuthScope candidate);
+
+
+/**
+ * Return the TMH_AuthScope corresponding to @a name.
+ *
+ * @param name the name to look for
+ * @return the scope corresponding to the name, or TMH_AS_NONE.
+ */
+enum TMH_AuthScope
+TMH_get_scope_by_name (const char *name);
+
+
+/**
+ * Return the name corresponding to @a scop.
+ *
+ * @param scope the scope to look for
+ * @param[out] refreshable outputs if scope value was refreshable
+ * @return the name corresponding to the scope, or NULL.
+ */
+const char *
+TMH_get_name_by_scope (enum TMH_AuthScope scope,
+ bool *refreshable);
+
+
+/**
+ * Check if the client has provided the necessary credentials
+ * to access the selected endpoint of the selected instance.
+ *
+ * @param[in,out] hc handler context
+ * @return #GNUNET_OK on success,
+ * #GNUNET_NO if an error was queued (return #MHD_YES)
+ * #GNUNET_SYSERR to close the connection (return #MHD_NO)
+ */
+enum GNUNET_GenericReturnValue
+TMH_perform_access_control (struct TMH_HandlerContext *hc);
+
+#endif
diff --git a/src/backend/taler-merchant-httpd_dispatcher.c b/src/backend/taler-merchant-httpd_dispatcher.c
@@ -0,0 +1,1515 @@
+/*
+ This file is part of TALER
+ (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 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 taler-merchant-httpd_dispatcher.c
+ * @brief map requested URL and method to the respective request handler
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler-merchant-httpd_config.h"
+#include "taler-merchant-httpd_dispatcher.h"
+#include "taler-merchant-httpd_exchanges.h"
+#include "taler-merchant-httpd_get-orders-ID.h"
+#include "taler-merchant-httpd_get-products-image.h"
+#include "taler-merchant-httpd_get-templates-ID.h"
+#include "taler-merchant-httpd_mhd.h"
+#include "taler-merchant-httpd_private-delete-account-ID.h"
+#include "taler-merchant-httpd_private-delete-categories-ID.h"
+#include "taler-merchant-httpd_private-delete-units-ID.h"
+#include "taler-merchant-httpd_private-delete-instances-ID.h"
+#include "taler-merchant-httpd_private-delete-instances-ID-token.h"
+#include "taler-merchant-httpd_private-delete-products-ID.h"
+#include "taler-merchant-httpd_private-delete-orders-ID.h"
+#include "taler-merchant-httpd_private-delete-otp-devices-ID.h"
+#include "taler-merchant-httpd_private-delete-templates-ID.h"
+#include "taler-merchant-httpd_private-delete-token-families-SLUG.h"
+#include "taler-merchant-httpd_private-delete-transfers-ID.h"
+#include "taler-merchant-httpd_private-delete-webhooks-ID.h"
+#include "taler-merchant-httpd_private-get-accounts.h"
+#include "taler-merchant-httpd_private-get-accounts-ID.h"
+#include "taler-merchant-httpd_private-get-categories.h"
+#include "taler-merchant-httpd_private-get-categories-ID.h"
+#include "taler-merchant-httpd_private-get-units.h"
+#include "taler-merchant-httpd_private-get-units-ID.h"
+#include "taler-merchant-httpd_private-get-incoming.h"
+#include "taler-merchant-httpd_private-get-instances.h"
+#include "taler-merchant-httpd_private-get-instances-ID.h"
+#include "taler-merchant-httpd_private-get-instances-ID-kyc.h"
+#include "taler-merchant-httpd_private-get-instances-ID-tokens.h"
+#include "taler-merchant-httpd_private-get-pos.h"
+#include "taler-merchant-httpd_private-get-products.h"
+#include "taler-merchant-httpd_private-get-products-ID.h"
+#include "taler-merchant-httpd_private-get-orders.h"
+#include "taler-merchant-httpd_private-get-orders-ID.h"
+#include "taler-merchant-httpd_private-get-otp-devices.h"
+#include "taler-merchant-httpd_private-get-otp-devices-ID.h"
+#include "taler-merchant-httpd_private-get-statistics-amount-SLUG.h"
+#include "taler-merchant-httpd_private-get-statistics-counter-SLUG.h"
+#include "taler-merchant-httpd_private-get-templates.h"
+#include "taler-merchant-httpd_private-get-templates-ID.h"
+#include "taler-merchant-httpd_private-get-token-families.h"
+#include "taler-merchant-httpd_private-get-token-families-SLUG.h"
+#include "taler-merchant-httpd_private-get-transfers.h"
+#include "taler-merchant-httpd_private-get-webhooks.h"
+#include "taler-merchant-httpd_private-get-webhooks-ID.h"
+#include "taler-merchant-httpd_private-patch-accounts-ID.h"
+#include "taler-merchant-httpd_private-patch-categories-ID.h"
+#include "taler-merchant-httpd_private-patch-units-ID.h"
+#include "taler-merchant-httpd_private-patch-instances-ID.h"
+#include "taler-merchant-httpd_private-patch-orders-ID-forget.h"
+#include "taler-merchant-httpd_private-patch-otp-devices-ID.h"
+#include "taler-merchant-httpd_private-patch-products-ID.h"
+#include "taler-merchant-httpd_private-patch-templates-ID.h"
+#include "taler-merchant-httpd_private-patch-token-families-SLUG.h"
+#include "taler-merchant-httpd_private-patch-webhooks-ID.h"
+#include "taler-merchant-httpd_private-post-account.h"
+#include "taler-merchant-httpd_private-post-categories.h"
+#include "taler-merchant-httpd_private-post-units.h"
+#include "taler-merchant-httpd_private-post-instances.h"
+#include "taler-merchant-httpd_private-post-instances-ID-auth.h"
+#include "taler-merchant-httpd_private-post-instances-ID-token.h"
+#include "taler-merchant-httpd_private-post-otp-devices.h"
+#include "taler-merchant-httpd_private-post-orders.h"
+#include "taler-merchant-httpd_private-post-orders-ID-refund.h"
+#include "taler-merchant-httpd_private-post-products.h"
+#include "taler-merchant-httpd_private-post-products-ID-lock.h"
+#include "taler-merchant-httpd_private-post-templates.h"
+#include "taler-merchant-httpd_private-post-token-families.h"
+#include "taler-merchant-httpd_private-post-transfers.h"
+#include "taler-merchant-httpd_private-post-webhooks.h"
+#include "taler-merchant-httpd_post-challenge-ID.h"
+#include "taler-merchant-httpd_post-challenge-ID-confirm.h"
+#include "taler-merchant-httpd_post-orders-ID-abort.h"
+#include "taler-merchant-httpd_post-orders-ID-claim.h"
+#include "taler-merchant-httpd_post-orders-ID-paid.h"
+#include "taler-merchant-httpd_post-orders-ID-pay.h"
+#include "taler-merchant-httpd_post-using-templates.h"
+#include "taler-merchant-httpd_post-orders-ID-refund.h"
+#include "taler-merchant-httpd_spa.h"
+#include "taler-merchant-httpd_statics.h"
+#include "taler-merchant-httpd_terms.h"
+#include "taler-merchant-httpd_post-reports-ID.h"
+#include "taler-merchant-httpd_private-delete-report-ID.h"
+#include "taler-merchant-httpd_private-get-report-ID.h"
+#include "taler-merchant-httpd_private-get-reports.h"
+#include "taler-merchant-httpd_private-patch-report-ID.h"
+#include "taler-merchant-httpd_private-post-reports.h"
+#include "taler-merchant-httpd_private-delete-pot-ID.h"
+#include "taler-merchant-httpd_private-get-pot-ID.h"
+#include "taler-merchant-httpd_private-get-pots.h"
+#include "taler-merchant-httpd_private-patch-pot-ID.h"
+#include "taler-merchant-httpd_private-post-pots.h"
+#include "taler-merchant-httpd_private-get-groups.h"
+#include "taler-merchant-httpd_private-post-groups.h"
+#include "taler-merchant-httpd_private-patch-group-ID.h"
+#include "taler-merchant-httpd_private-delete-group-ID.h"
+
+#ifdef HAVE_DONAU_DONAU_SERVICE_H
+#include "taler-merchant-httpd_private-get-donau-instances.h"
+#include "taler-merchant-httpd_private-post-donau-instance.h"
+#include "taler-merchant-httpd_private-delete-donau-instance-ID.h"
+#endif
+
+
+/**
+ * Handle a OPTIONS "*" request.
+ *
+ * @param rh context of the handler
+ * @param connection the MHD connection to handle
+ * @param[in,out] hc context with further information about the request
+ * @return MHD result code
+ */
+static MHD_RESULT
+handle_server_options (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc)
+{
+ (void) rh;
+ (void) hc;
+ return TALER_MHD_reply_cors_preflight (connection);
+}
+
+
+/**
+ * Generates the response for "/", redirecting the
+ * client to the "/webui/" from where we serve the SPA.
+ *
+ * @param rh request handler
+ * @param connection MHD connection
+ * @param hc handler context
+ * @return MHD result code
+ */
+static MHD_RESULT
+spa_redirect (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc)
+{
+ const char *text = "Redirecting to /webui/";
+ struct MHD_Response *response;
+ char *dst;
+
+ response = MHD_create_response_from_buffer (strlen (text),
+ (void *) text,
+ MHD_RESPMEM_PERSISTENT);
+ if (NULL == response)
+ {
+ GNUNET_break (0);
+ return MHD_NO;
+ }
+ TALER_MHD_add_global_headers (response,
+ true);
+ GNUNET_break (MHD_YES ==
+ MHD_add_response_header (response,
+ MHD_HTTP_HEADER_CONTENT_TYPE,
+ "text/plain"));
+ if ( (NULL == hc->instance) ||
+ (0 == strcmp ("admin",
+ hc->instance->settings.id)) )
+ dst = GNUNET_strdup ("/webui/");
+ else
+ GNUNET_asprintf (&dst,
+ "/instances/%s/webui/",
+ hc->instance->settings.id);
+ if (MHD_NO ==
+ MHD_add_response_header (response,
+ MHD_HTTP_HEADER_LOCATION,
+ dst))
+ {
+ GNUNET_break (0);
+ MHD_destroy_response (response);
+ GNUNET_free (dst);
+ return MHD_NO;
+ }
+ GNUNET_free (dst);
+
+ {
+ MHD_RESULT ret;
+
+ ret = MHD_queue_response (connection,
+ MHD_HTTP_FOUND,
+ response);
+ MHD_destroy_response (response);
+ return ret;
+ }
+}
+
+
+/**
+ * Determine the group of request handlers to call for the
+ * given URL. Removes a possible prefix from @a purl by advancing
+ * the pointer.
+ *
+ * @param[in,out] urlp pointer to the URL to analyze and update
+ * @param[out] is_public set to true if these are public handlers
+ * @return handler group to consider for the given URL
+ */
+static const struct TMH_RequestHandler *
+determine_handler_group (const char **urlp,
+ bool *is_public)
+{
+ static struct TMH_RequestHandler management_handlers[] = {
+ /* GET /instances */
+ {
+ .url_prefix = "/instances",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "instances-write",
+ .skip_instance = true,
+ .default_only = true,
+ .handler = &TMH_private_get_instances
+ },
+ /* POST /instances */
+ {
+ .url_prefix = "/instances",
+ .method = MHD_HTTP_METHOD_POST,
+ .permission = "instances-write",
+ .skip_instance = true,
+ .default_only = true,
+ .handler = &TMH_private_post_instances,
+ /* allow instance data of up to 8 MB, that should be plenty;
+ note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
+ would require further changes to the allocation logic
+ in the code... */
+ .max_upload = 1024 * 1024 * 8
+ },
+ /* GET /instances/$ID/ */
+ {
+ .url_prefix = "/instances/",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "instances-write",
+ .skip_instance = true,
+ .default_only = true,
+ .have_id_segment = true,
+ .handler = &TMH_private_get_instances_default_ID
+ },
+ /* DELETE /instances/$ID */
+ {
+ .url_prefix = "/instances/",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .permission = "instances-write",
+ .skip_instance = true,
+ .default_only = true,
+ .have_id_segment = true,
+ .handler = &TMH_private_delete_instances_default_ID
+ },
+ /* PATCH /instances/$ID */
+ {
+ .url_prefix = "/instances/",
+ .method = MHD_HTTP_METHOD_PATCH,
+ .permission = "instances-write",
+ .skip_instance = true,
+ .default_only = true,
+ .have_id_segment = true,
+ .handler = &TMH_private_patch_instances_default_ID,
+ /* allow instance data of up to 8 MB, that should be plenty;
+ note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
+ would require further changes to the allocation logic
+ in the code... */
+ .max_upload = 1024 * 1024 * 8
+ },
+ /* POST /auth: */
+ {
+ .url_prefix = "/instances/",
+ .url_suffix = "auth",
+ .method = MHD_HTTP_METHOD_POST,
+ .permission = "instances-auth-write",
+ .skip_instance = true,
+ .default_only = true,
+ .have_id_segment = true,
+ .handler = &TMH_private_post_instances_default_ID_auth,
+ /* Body should be pretty small. */
+ .max_upload = 1024 * 1024
+ },
+ /* GET /kyc: */
+ {
+ .url_prefix = "/instances/",
+ .url_suffix = "kyc",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "instances-kyc-read",
+ .skip_instance = true,
+ .default_only = true,
+ .have_id_segment = true,
+ .handler = &TMH_private_get_instances_default_ID_kyc,
+ },
+ {
+ .url_prefix = NULL
+ }
+ };
+
+ static struct TMH_RequestHandler private_handlers[] = {
+ /* GET /instances/$ID/: */
+ {
+ .url_prefix = "/",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "instances-read",
+ .handler = &TMH_private_get_instances_ID
+ },
+ /* DELETE /instances/$ID/: */
+ {
+ .url_prefix = "/",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .permission = "instances-write",
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_delete_instances_ID
+ },
+ /* PATCH /instances/$ID/: */
+ {
+ .url_prefix = "/",
+ .method = MHD_HTTP_METHOD_PATCH,
+ .handler = &TMH_private_patch_instances_ID,
+ .permission = "instances-write",
+ .allow_deleted_instance = true,
+ /* allow instance data of up to 8 MB, that should be plenty;
+ note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
+ would require further changes to the allocation logic
+ in the code... */
+ .max_upload = 1024 * 1024 * 8
+ },
+ /* POST /auth: */
+ {
+ .url_prefix = "/auth",
+ .method = MHD_HTTP_METHOD_POST,
+ .handler = &TMH_private_post_instances_ID_auth,
+ .permission = "auth-write",
+ /* Body should be pretty small. */
+ .max_upload = 1024 * 1024,
+ },
+ /* GET /kyc: */
+ {
+ .url_prefix = "/kyc",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "kyc-read",
+ .handler = &TMH_private_get_instances_ID_kyc,
+ },
+ /* GET /pos: */
+ {
+ .url_prefix = "/pos",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "pos-read",
+ .handler = &TMH_private_get_pos
+ },
+ /* GET /categories: */
+ {
+ .url_prefix = "/categories",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "categories-read",
+ .handler = &TMH_private_get_categories
+ },
+ /* POST /categories: */
+ {
+ .url_prefix = "/categories",
+ .method = MHD_HTTP_METHOD_POST,
+ .permission = "categories-write",
+ .handler = &TMH_private_post_categories,
+ /* allow category data of up to 8 kb, that should be plenty */
+ .max_upload = 1024 * 8
+ },
+ /* GET /categories/$ID: */
+ {
+ .url_prefix = "/categories/",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "categories-read",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_get_categories_ID
+ },
+ /* DELETE /categories/$ID: */
+ {
+ .url_prefix = "/categories/",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .permission = "categories-write",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_delete_categories_ID
+ },
+ /* PATCH /categories/$ID/: */
+ {
+ .url_prefix = "/categories/",
+ .method = MHD_HTTP_METHOD_PATCH,
+ .permission = "categories-write",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_patch_categories_ID,
+ /* allow category data of up to 8 kb, that should be plenty */
+ .max_upload = 1024 * 8
+ },
+ /* GET /units: */
+ {
+ .url_prefix = "/units",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &TMH_private_get_units
+ },
+ /* POST /units: */
+ {
+ .url_prefix = "/units",
+ .method = MHD_HTTP_METHOD_POST,
+ .permission = "units-write",
+ .handler = &TMH_private_post_units,
+ .max_upload = 1024 * 8
+ },
+ /* GET /units/$UNIT: */
+ {
+ .url_prefix = "/units/",
+ .method = MHD_HTTP_METHOD_GET,
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_get_units_ID
+ },
+ /* DELETE /units/$UNIT: */
+ {
+ .url_prefix = "/units/",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .permission = "units-write",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_delete_units_ID
+ },
+ /* PATCH /units/$UNIT: */
+ {
+ .url_prefix = "/units/",
+ .method = MHD_HTTP_METHOD_PATCH,
+ .permission = "units-write",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_patch_units_ID,
+ .max_upload = 1024 * 8
+ },
+ /* GET /products: */
+ {
+ .url_prefix = "/products",
+ .permission = "products-read",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &TMH_private_get_products
+ },
+ /* POST /products: */
+ {
+ .url_prefix = "/products",
+ .method = MHD_HTTP_METHOD_POST,
+ .permission = "products-write",
+ .handler = &TMH_private_post_products,
+ /* allow product data of up to 8 MB, that should be plenty;
+ note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
+ would require further changes to the allocation logic
+ in the code... */
+ .max_upload = 1024 * 1024 * 8
+ },
+ /* GET /products/$ID: */
+ {
+ .url_prefix = "/products/",
+ .method = MHD_HTTP_METHOD_GET,
+ .have_id_segment = true,
+ .permission = "products-read",
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_get_products_ID
+ },
+ /* DELETE /products/$ID/: */
+ {
+ .url_prefix = "/products/",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .have_id_segment = true,
+ .permission = "products-write",
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_delete_products_ID
+ },
+ /* PATCH /products/$ID/: */
+ {
+ .url_prefix = "/products/",
+ .method = MHD_HTTP_METHOD_PATCH,
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .permission = "products-write",
+ .handler = &TMH_private_patch_products_ID,
+ /* allow product data of up to 8 MB, that should be plenty;
+ note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
+ would require further changes to the allocation logic
+ in the code... */
+ .max_upload = 1024 * 1024 * 8
+ },
+ /* POST /products/$ID/lock: */
+ {
+ .url_prefix = "/products/",
+ .url_suffix = "lock",
+ .method = MHD_HTTP_METHOD_POST,
+ .have_id_segment = true,
+ .permission = "products-lock",
+ .handler = &TMH_private_post_products_ID_lock,
+ /* the body should be pretty small, allow 1 MB of upload
+ to set a conservative bound for sane wallets */
+ .max_upload = 1024 * 1024
+ },
+ /* POST /orders: */
+ {
+ .url_prefix = "/orders",
+ .method = MHD_HTTP_METHOD_POST,
+ .permission = "orders-write",
+ .handler = &TMH_private_post_orders,
+ /* allow contracts of up to 8 MB, that should be plenty;
+ note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
+ would require further changes to the allocation logic
+ in the code... */
+ .max_upload = 1024 * 1024 * 8
+ },
+ /* GET /orders/$ID: */
+ {
+ .url_prefix = "/orders/",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "orders-read",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_get_orders_ID
+ },
+ /* GET /orders: */
+ {
+ .url_prefix = "/orders",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "orders-read",
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_get_orders
+ },
+ /* POST /orders/$ID/refund: */
+ {
+ .url_prefix = "/orders/",
+ .url_suffix = "refund",
+ .method = MHD_HTTP_METHOD_POST,
+ .have_id_segment = true,
+ .permission = "orders-refund",
+ .handler = &TMH_private_post_orders_ID_refund,
+ /* the body should be pretty small, allow 1 MB of upload
+ to set a conservative bound for sane wallets */
+ .max_upload = 1024 * 1024
+ },
+ /* PATCH /orders/$ID/forget: */
+ {
+ .url_prefix = "/orders/",
+ .url_suffix = "forget",
+ .method = MHD_HTTP_METHOD_PATCH,
+ .permission = "orders-write",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_patch_orders_ID_forget,
+ /* the body should be pretty small, allow 1 MB of upload
+ to set a conservative bound for sane wallets */
+ .max_upload = 1024 * 1024
+ },
+ /* DELETE /orders/$ID: */
+ {
+ .url_prefix = "/orders/",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .permission = "orders-write",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_delete_orders_ID
+ },
+ /* POST /transfers: */
+ {
+ .url_prefix = "/transfers",
+ .method = MHD_HTTP_METHOD_POST,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_post_transfers,
+ .permission = "transfers-write",
+ /* the body should be pretty small, allow 1 MB of upload
+ to set a conservative bound for sane wallets */
+ .max_upload = 1024 * 1024
+ },
+ /* DELETE /transfers/$ID: */
+ {
+ .url_prefix = "/transfers/",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .permission = "transfers-write",
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_delete_transfers_ID,
+ .have_id_segment = true,
+ /* the body should be pretty small, allow 1 MB of upload
+ to set a conservative bound for sane wallets */
+ .max_upload = 1024 * 1024
+ },
+ /* GET /transfers: */
+ {
+ .url_prefix = "/transfers",
+ .permission = "transfers-read",
+ .method = MHD_HTTP_METHOD_GET,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_get_transfers
+ },
+ /* GET /incoming: */
+ {
+ .url_prefix = "/incoming",
+ .permission = "transfers-read",
+ .method = MHD_HTTP_METHOD_GET,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_get_incoming
+ },
+ /* POST /otp-devices: */
+ {
+ .url_prefix = "/otp-devices",
+ .permission = "otp-devices-write",
+ .method = MHD_HTTP_METHOD_POST,
+ .handler = &TMH_private_post_otp_devices
+ },
+ /* GET /otp-devices: */
+ {
+ .url_prefix = "/otp-devices",
+ .permission = "opt-devices-read",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &TMH_private_get_otp_devices
+ },
+ /* GET /otp-devices/$ID/: */
+ {
+ .url_prefix = "/otp-devices/",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "otp-devices-read",
+ .have_id_segment = true,
+ .handler = &TMH_private_get_otp_devices_ID
+ },
+ /* DELETE /otp-devices/$ID/: */
+ {
+ .url_prefix = "/otp-devices/",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .permission = "otp-devices-write",
+ .have_id_segment = true,
+ .handler = &TMH_private_delete_otp_devices_ID
+ },
+ /* PATCH /otp-devices/$ID/: */
+ {
+ .url_prefix = "/otp-devices/",
+ .method = MHD_HTTP_METHOD_PATCH,
+ .permission = "otp-devices-write",
+ .have_id_segment = true,
+ .handler = &TMH_private_patch_otp_devices_ID
+ },
+ /* POST /templates: */
+ {
+ .url_prefix = "/templates",
+ .method = MHD_HTTP_METHOD_POST,
+ .permission = "templates-write",
+ .handler = &TMH_private_post_templates,
+ /* allow template data of up to 8 MB, that should be plenty;
+ note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
+ would require further changes to the allocation logic
+ in the code... */
+ .max_upload = 1024 * 1024 * 8
+ },
+ /* GET /templates: */
+ {
+ .url_prefix = "/templates",
+ .permission = "templates-read",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &TMH_private_get_templates
+ },
+ /* GET /templates/$ID/: */
+ {
+ .url_prefix = "/templates/",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "templates-read",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_get_templates_ID
+ },
+ /* DELETE /templates/$ID/: */
+ {
+ .url_prefix = "/templates/",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .permission = "templates-write",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_delete_templates_ID
+ },
+ /* PATCH /templates/$ID/: */
+ {
+ .url_prefix = "/templates/",
+ .method = MHD_HTTP_METHOD_PATCH,
+ .permission = "templates-write",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_patch_templates_ID,
+ /* allow template data of up to 8 MB, that should be plenty;
+ note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
+ would require further changes to the allocation logic
+ in the code... */
+ .max_upload = 1024 * 1024 * 8
+ },
+ /* GET /webhooks: */
+ {
+ .url_prefix = "/webhooks",
+ .permission = "webhooks-read",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &TMH_private_get_webhooks
+ },
+ /* POST /webhooks: */
+ {
+ .url_prefix = "/webhooks",
+ .method = MHD_HTTP_METHOD_POST,
+ .permission = "webhooks-write",
+ .handler = &TMH_private_post_webhooks,
+ /* allow webhook data of up to 8 MB, that should be plenty;
+ note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
+ would require further changes to the allocation logic
+ in the code... */
+ .max_upload = 1024 * 1024 * 8
+ },
+ /* GET /webhooks/$ID/: */
+ {
+ .url_prefix = "/webhooks/",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "webhooks-read",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_get_webhooks_ID
+ },
+ /* DELETE /webhooks/$ID/: */
+ {
+ .url_prefix = "/webhooks/",
+ .permission = "webhooks-write",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_delete_webhooks_ID
+ },
+ /* PATCH /webhooks/$ID/: */
+ {
+ .url_prefix = "/webhooks/",
+ .method = MHD_HTTP_METHOD_PATCH,
+ .permission = "webhooks-write",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_patch_webhooks_ID,
+ /* allow webhook data of up to 8 MB, that should be plenty;
+ note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
+ would require further changes to the allocation logic
+ in the code... */
+ .max_upload = 1024 * 1024 * 8
+ },
+ /* POST /accounts: */
+ {
+ .url_prefix = "/accounts",
+ .method = MHD_HTTP_METHOD_POST,
+ .permission = "accounts-write",
+ .handler = &TMH_private_post_account,
+ /* allow account details of up to 8 kb, that should be plenty */
+ .max_upload = 1024 * 8
+ },
+ /* PATCH /accounts/$H_WIRE: */
+ {
+ .url_prefix = "/accounts/",
+ .method = MHD_HTTP_METHOD_PATCH,
+ .permission = "accounts-write",
+ .handler = &TMH_private_patch_accounts_ID,
+ .have_id_segment = true,
+ /* allow account details of up to 8 kb, that should be plenty */
+ .max_upload = 1024 * 8
+ },
+ /* GET /accounts: */
+ {
+ .url_prefix = "/accounts",
+ .permission = "accounts-read",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &TMH_private_get_accounts
+ },
+ /* GET /accounts/$H_WIRE: */
+ {
+ .url_prefix = "/accounts/",
+ .permission = "accounts-read",
+ .method = MHD_HTTP_METHOD_GET,
+ .have_id_segment = true,
+ .handler = &TMH_private_get_accounts_ID
+ },
+ /* DELETE /accounts/$H_WIRE: */
+ {
+ .url_prefix = "/accounts/",
+ .permission = "accounts-write",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .handler = &TMH_private_delete_account_ID,
+ .have_id_segment = true
+ },
+ /* GET /tokens: */
+ {
+ .url_prefix = "/tokens",
+ .permission = "tokens-read",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &TMH_private_get_instances_ID_tokens,
+ },
+ /* POST /token: */
+ {
+ .url_prefix = "/token",
+ .permission = "token-refresh",
+ .method = MHD_HTTP_METHOD_POST,
+ .handler = &TMH_private_post_instances_ID_token,
+ /* Body should be tiny. */
+ .max_upload = 1024
+ },
+ /* DELETE /tokens/$SERIAL: */
+ {
+ .url_prefix = "/tokens/",
+ .permission = "tokens-write",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .handler = &TMH_private_delete_instances_ID_token_SERIAL,
+ .have_id_segment = true
+ },
+ /* DELETE /token: */
+ {
+ .url_prefix = "/token",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .handler = &TMH_private_delete_instances_ID_token,
+ },
+ /* GET /tokenfamilies: */
+ {
+ .url_prefix = "/tokenfamilies",
+ .permission = "tokenfamilies-read",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &TMH_private_get_tokenfamilies
+ },
+ /* POST /tokenfamilies: */
+ {
+ .url_prefix = "/tokenfamilies",
+ .permission = "tokenfamilies-write",
+ .method = MHD_HTTP_METHOD_POST,
+ .handler = &TMH_private_post_token_families
+ },
+ /* GET /tokenfamilies/$SLUG/: */
+ {
+ .url_prefix = "/tokenfamilies/",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "tokenfamilies-read",
+ .have_id_segment = true,
+ .handler = &TMH_private_get_tokenfamilies_SLUG
+ },
+ /* DELETE /tokenfamilies/$SLUG/: */
+ {
+ .url_prefix = "/tokenfamilies/",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .permission = "tokenfamilies-write",
+ .have_id_segment = true,
+ .handler = &TMH_private_delete_token_families_SLUG
+ },
+ /* PATCH /tokenfamilies/$SLUG/: */
+ {
+ .url_prefix = "/tokenfamilies/",
+ .method = MHD_HTTP_METHOD_PATCH,
+ .permission = "tokenfamilies-write",
+ .have_id_segment = true,
+ .handler = &TMH_private_patch_token_family_SLUG,
+ },
+ #ifdef HAVE_DONAU_DONAU_SERVICE_H
+ /* GET /donau */
+ {
+ .url_prefix = "/donau",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &TMH_private_get_donau_instances
+ },
+ /* POST /donau */
+ {
+ .url_prefix = "/donau",
+ .method = MHD_HTTP_METHOD_POST,
+ .handler = &TMH_private_post_donau_instance
+ },
+ /* DELETE /donau/$charity-id */
+ {
+ .url_prefix = "/donau/",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .have_id_segment = true,
+ .handler = &TMH_private_delete_donau_instance_ID
+ },
+ #endif
+ /* GET /statistics-counter/$SLUG: */
+ {
+ .url_prefix = "/statistics-counter/",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "statistics-read",
+ .have_id_segment = true,
+ .handler = &TMH_private_get_statistics_counter_SLUG,
+ },
+ /* GET /statistics-amount/$SLUG: */
+ {
+ .url_prefix = "/statistics-amount/",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "statistics-read",
+ .have_id_segment = true,
+ .handler = &TMH_private_get_statistics_amount_SLUG,
+ },
+ {
+ .url_prefix = NULL
+ }
+ };
+ static struct TMH_RequestHandler public_handlers[] = {
+ {
+ /* for "admin" instance, it does not even
+ have to exist before we give the WebUI */
+ .url_prefix = "/",
+ .method = MHD_HTTP_METHOD_GET,
+ .mime_type = "text/html",
+ .skip_instance = true,
+ .default_only = true,
+ .handler = &spa_redirect,
+ .response_code = MHD_HTTP_FOUND
+ },
+ {
+ .url_prefix = "/config",
+ .method = MHD_HTTP_METHOD_GET,
+ .skip_instance = true,
+ .default_only = true,
+ .handler = &MH_handler_config
+ },
+ {
+ /* for "normal" instance,s they must exist
+ before we give the WebUI */
+ .url_prefix = "/",
+ .method = MHD_HTTP_METHOD_GET,
+ .mime_type = "text/html",
+ .handler = &spa_redirect,
+ .response_code = MHD_HTTP_FOUND
+ },
+ {
+ .url_prefix = "/webui/",
+ .method = MHD_HTTP_METHOD_GET,
+ .mime_type = "text/html",
+ .skip_instance = true,
+ .have_id_segment = true,
+ .handler = &TMH_return_spa,
+ .response_code = MHD_HTTP_OK
+ },
+ {
+ .url_prefix = "/agpl",
+ .method = MHD_HTTP_METHOD_GET,
+ .skip_instance = true,
+ .handler = &TMH_MHD_handler_agpl_redirect
+ },
+ {
+ .url_prefix = "/agpl",
+ .method = MHD_HTTP_METHOD_GET,
+ .skip_instance = true,
+ .handler = &TMH_MHD_handler_agpl_redirect
+ },
+ {
+ .url_prefix = "/terms",
+ .method = MHD_HTTP_METHOD_GET,
+ .skip_instance = true,
+ .handler = &TMH_handler_terms
+ },
+ {
+ .url_prefix = "/privacy",
+ .method = MHD_HTTP_METHOD_GET,
+ .skip_instance = true,
+ .handler = &TMH_handler_privacy
+ },
+ /* Also serve the same /config per instance */
+ {
+ .url_prefix = "/config",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &MH_handler_config
+ },
+ /* POST /orders/$ID/abort: */
+ {
+ .url_prefix = "/orders/",
+ .have_id_segment = true,
+ .url_suffix = "abort",
+ .method = MHD_HTTP_METHOD_POST,
+ .handler = &TMH_post_orders_ID_abort,
+ /* wallet may give us many coins to sign, allow 1 MB of upload
+ to set a conservative bound for sane wallets */
+ .max_upload = 1024 * 1024
+ },
+ /* POST /orders/$ID/claim: */
+ {
+ .url_prefix = "/orders/",
+ .have_id_segment = true,
+ .url_suffix = "claim",
+ .method = MHD_HTTP_METHOD_POST,
+ .handler = &TMH_post_orders_ID_claim,
+ /* the body should be pretty small, allow 1 MB of upload
+ to set a conservative bound for sane wallets */
+ .max_upload = 1024 * 1024
+ },
+ /* POST /orders/$ID/pay: */
+ {
+ .url_prefix = "/orders/",
+ .have_id_segment = true,
+ .url_suffix = "pay",
+ .method = MHD_HTTP_METHOD_POST,
+ .handler = &TMH_post_orders_ID_pay,
+ /* wallet may give us many coins to sign, allow 1 MB of upload
+ to set a conservative bound for sane wallets */
+ .max_upload = 1024 * 1024
+ },
+ /* POST /orders/$ID/paid: */
+ {
+ .url_prefix = "/orders/",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .url_suffix = "paid",
+ .method = MHD_HTTP_METHOD_POST,
+ .handler = &TMH_post_orders_ID_paid,
+ /* the body should be pretty small, allow 1 MB of upload
+ to set a conservative bound for sane wallets */
+ .max_upload = 1024 * 1024
+ },
+ /* POST /orders/$ID/refund: */
+ {
+ .url_prefix = "/orders/",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .url_suffix = "refund",
+ .method = MHD_HTTP_METHOD_POST,
+ .handler = &TMH_post_orders_ID_refund,
+ /* the body should be pretty small, allow 1 MB of upload
+ to set a conservative bound for sane wallets */
+ .max_upload = 1024 * 1024
+ },
+ /* GET /orders/$ID: */
+ {
+ .url_prefix = "/orders/",
+ .method = MHD_HTTP_METHOD_GET,
+ .allow_deleted_instance = true,
+ .have_id_segment = true,
+ .handler = &TMH_get_orders_ID
+ },
+ /* GET /static/ *: */
+ {
+ .url_prefix = "/static/",
+ .method = MHD_HTTP_METHOD_GET,
+ .have_id_segment = true,
+ .handler = &TMH_return_static
+ },
+ /* POST /reports/$ID/ */
+ {
+ .url_prefix = "/reports",
+ .method = MHD_HTTP_METHOD_POST,
+ .have_id_segment = true,
+ .handler = &TMH_post_reports_ID,
+ },
+ /* GET /templates/$ID/: */
+ {
+ .url_prefix = "/templates/",
+ .method = MHD_HTTP_METHOD_GET,
+ .have_id_segment = true,
+ .handler = &TMH_get_templates_ID
+ },
+ /* GET /products/$HASH/image: */
+ {
+ .url_prefix = "/products/",
+ .method = MHD_HTTP_METHOD_GET,
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .url_suffix = "image",
+ .handler = &TMH_get_products_image
+ },
+ /* POST /templates/$ID: */
+ {
+ .url_prefix = "/templates/",
+ .method = MHD_HTTP_METHOD_POST,
+ .have_id_segment = true,
+ .handler = &TMH_post_using_templates_ID,
+ .max_upload = 1024 * 1024
+ },
+ /* POST /challenge/$ID: */
+ {
+ .url_prefix = "/challenge/",
+ .method = MHD_HTTP_METHOD_POST,
+ .have_id_segment = true,
+ .handler = &TMH_post_challenge_ID,
+ .max_upload = 1024
+ },
+ /* POST /challenge/$ID/confirm: */
+ {
+ .url_prefix = "/challenge/",
+ .method = MHD_HTTP_METHOD_POST,
+ .have_id_segment = true,
+ .url_suffix = "confirm",
+ .handler = &TMH_post_challenge_ID_confirm,
+ .max_upload = 1024
+ },
+ /* POST /instances */
+ {
+ .url_prefix = "/instances",
+ .method = MHD_HTTP_METHOD_POST,
+ .skip_instance = true,
+ .default_only = true,
+ .handler = &TMH_public_post_instances,
+ /* allow instance data of up to 8 MB, that should be plenty;
+ note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
+ would require further changes to the allocation logic
+ in the code... */
+ .max_upload = 1024 * 1024 * 8
+ },
+ /* POST /forgot-password: */
+ {
+ .url_prefix = "/forgot-password",
+ .method = MHD_HTTP_METHOD_POST,
+ .handler = &TMH_public_post_instances_ID_auth,
+ /* Body should be pretty small. */
+ .max_upload = 1024 * 1024
+ },
+
+ /* Reports endpoints */
+ {
+ .url_prefix = "reports",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "reports-read",
+ .handler = &TMH_private_get_reports,
+ },
+ {
+ .url_prefix = "reports",
+ .method = MHD_HTTP_METHOD_POST,
+ .permission = "reports-write",
+ .handler = &TMH_private_post_reports,
+ },
+ {
+ .url_prefix = "reports",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &TMH_private_get_report,
+ .permission = "reports-read",
+ .have_id_segment = true,
+ },
+ {
+ .url_prefix = "reports",
+ .method = MHD_HTTP_METHOD_PATCH,
+ .handler = &TMH_private_patch_report,
+ .permission = "reports-write",
+ .have_id_segment = true,
+ },
+ {
+ .url_prefix = "reports",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .handler = &TMH_private_delete_report,
+ .permission = "reports-write",
+ .have_id_segment = true,
+ },
+
+ /* Groups endpoints */
+ {
+ .url_prefix = "groups",
+ .method = MHD_HTTP_METHOD_GET,
+ .permission = "groups-read",
+ .handler = &TMH_private_get_groups,
+ },
+ {
+ .url_prefix = "groups",
+ .method = MHD_HTTP_METHOD_POST,
+ .permission = "groups-write",
+ .handler = &TMH_private_post_groups,
+ },
+ {
+ .url_prefix = "groups",
+ .method = MHD_HTTP_METHOD_PATCH,
+ .handler = &TMH_private_patch_group,
+ .permission = "groups-write",
+ .have_id_segment = true,
+ },
+ {
+ .url_prefix = "groups",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .handler = &TMH_private_delete_group,
+ .permission = "groups-write",
+ .have_id_segment = true,
+ },
+
+ /* Money pots endpoints */
+ {
+ .url_prefix = "pots",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &TMH_private_get_pots,
+ .permission = "pots-read",
+ },
+ {
+ .url_prefix = "pots",
+ .method = MHD_HTTP_METHOD_POST,
+ .handler = &TMH_private_post_pots,
+ .permission = "pots-write"
+ },
+ {
+ .url_prefix = "pots",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &TMH_private_get_pot,
+ .have_id_segment = true,
+ .permission = "pots-read",
+ },
+ {
+ .url_prefix = "pots",
+ .method = MHD_HTTP_METHOD_PATCH,
+ .handler = &TMH_private_patch_pot,
+ .have_id_segment = true,
+ .permission = "pots-write"
+ },
+ {
+ .url_prefix = "pots",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .handler = &TMH_private_delete_pot,
+ .have_id_segment = true,
+ .permission = "pots-write"
+ },
+ {
+ .url_prefix = "*",
+ .method = MHD_HTTP_METHOD_OPTIONS,
+ .handler = &handle_server_options
+ },
+ {
+ .url_prefix = NULL
+ }
+ };
+ const char *management_prefix = "/management/";
+ const char *private_prefix = "/private/";
+ const char *url = *urlp;
+ struct TMH_RequestHandler *handlers;
+
+ *is_public = false; /* ensure safe default */
+ if ( (0 == strncmp (url,
+ management_prefix,
+ strlen (management_prefix))) )
+ {
+ handlers = management_handlers;
+ *urlp = url + strlen (management_prefix) - 1;
+ }
+ else if ( (0 == strncmp (url,
+ private_prefix,
+ strlen (private_prefix))) ||
+ (0 == strcmp (url,
+ "/private")) )
+ {
+ handlers = private_handlers;
+ if (0 == strcmp (url,
+ "/private"))
+ *urlp = "/";
+ else
+ *urlp = url + strlen (private_prefix) - 1;
+ }
+ else
+ {
+ handlers = public_handlers;
+ *is_public = true;
+ }
+ return handlers;
+}
+
+
+/**
+ * Checks if the @a rh matches the given (parsed) URL.
+ *
+ * @param rh handler to compare against
+ * @param url the main URL (without "/private/" prefix, if any)
+ * @param prefix_strlen length of the prefix, i.e. 8 for '/orders/' or 7 for '/config'
+ * @param infix_url infix text, i.e. "$ORDER_ID".
+ * @param infix_strlen length of the string in @a infix_url
+ * @param suffix_url suffix, i.e. "/refund", including the "/"
+ * @param suffix_strlen number of characters in @a suffix_url
+ * @return true if @a rh matches this request
+ */
+static bool
+prefix_match (const struct TMH_RequestHandler *rh,
+ const char *url,
+ size_t prefix_strlen,
+ const char *infix_url,
+ size_t infix_strlen,
+ const char *suffix_url,
+ size_t suffix_strlen)
+{
+ if ( (prefix_strlen != strlen (rh->url_prefix)) ||
+ (0 != memcmp (url,
+ rh->url_prefix,
+ prefix_strlen)) )
+ return false;
+ if (! rh->have_id_segment)
+ {
+ /* Require /$PREFIX/$SUFFIX or /$PREFIX */
+ if (NULL != suffix_url)
+ return false; /* too many segments to match */
+ if ( (NULL == infix_url) /* either or */
+ ^ (NULL == rh->url_suffix) )
+ return false; /* suffix existence mismatch */
+ /* If /$PREFIX/$SUFFIX, check $SUFFIX matches */
+ if ( (NULL != infix_url) &&
+ ( (infix_strlen != strlen (rh->url_suffix)) ||
+ (0 != memcmp (infix_url,
+ rh->url_suffix,
+ infix_strlen)) ) )
+ return false; /* cannot use infix as suffix: content mismatch */
+ }
+ else
+ {
+ /* Require /$PREFIX/$ID or /$PREFIX/$ID/$SUFFIX */
+ if (NULL == infix_url)
+ return false; /* infix existence mismatch */
+ if ( ( (NULL == suffix_url)
+ ^ (NULL == rh->url_suffix) ) )
+ return false; /* suffix existence mismatch */
+ if ( (NULL != suffix_url) &&
+ ( (suffix_strlen != strlen (rh->url_suffix)) ||
+ (0 != memcmp (suffix_url,
+ rh->url_suffix,
+ suffix_strlen)) ) )
+ return false; /* suffix content mismatch */
+ }
+ return true;
+}
+
+
+/**
+ * Identify the handler of the request from the @a url and @a method
+ *
+ * @param[in,out] hc handler context to update with applicable handler
+ * @param handlers array of handlers to consider
+ * @param url URL to match against the handlers
+ * @param method HTTP access method to consider
+ * @param use_admin set to true if we are using the admin instance
+ * @return #GNUNET_OK on success,
+ * #GNUNET_NO if an error was queued (return #MHD_YES)
+ * #GNUNET_SYSERR to close the connection (return #MHD_NO)
+ */
+static enum GNUNET_GenericReturnValue
+identify_handler (struct TMH_HandlerContext *hc,
+ const struct TMH_RequestHandler *handlers,
+ const char *url,
+ const char *method,
+ bool use_admin)
+{
+ size_t prefix_strlen; /* i.e. 8 for "/orders/", or 7 for "/config" */
+ const char *infix_url = NULL; /* i.e. "$ORDER_ID", no '/'-es */
+ size_t infix_strlen = 0; /* number of characters in infix_url */
+ const char *suffix_url = NULL; /* i.e. "refund", excludes '/' at the beginning */
+ size_t suffix_strlen = 0; /* number of characters in suffix_url */
+
+ if (0 == strcasecmp (method,
+ MHD_HTTP_METHOD_HEAD))
+ method = MHD_HTTP_METHOD_GET; /* MHD will deal with the rest */
+ if (0 == strcmp (url,
+ ""))
+ url = "/"; /* code below does not like empty string */
+
+ /* parse the URL into the three different components */
+ {
+ const char *slash;
+
+ slash = strchr (&url[1], '/');
+ if (NULL == slash)
+ {
+ /* the prefix was everything */
+ prefix_strlen = strlen (url);
+ }
+ else
+ {
+ prefix_strlen = slash - url + 1; /* includes both '/'-es if present! */
+ infix_url = slash + 1;
+ slash = strchr (infix_url, '/');
+ if (NULL == slash)
+ {
+ /* the infix was the rest */
+ infix_strlen = strlen (infix_url);
+ }
+ else
+ {
+ infix_strlen = slash - infix_url; /* excludes both '/'-es */
+ suffix_url = slash + 1; /* skip the '/' */
+ suffix_strlen = strlen (suffix_url);
+ }
+ hc->infix = GNUNET_strndup (infix_url,
+ infix_strlen);
+ }
+ }
+
+ /* find matching handler */
+ {
+ bool url_found = false;
+
+ for (unsigned int i = 0; NULL != handlers[i].url_prefix; i++)
+ {
+ const struct TMH_RequestHandler *rh = &handlers[i];
+
+ if (rh->default_only && (! use_admin))
+ continue;
+ if (! prefix_match (rh,
+ url,
+ prefix_strlen,
+ infix_url,
+ infix_strlen,
+ suffix_url,
+ suffix_strlen))
+ continue;
+ url_found = true;
+ if (0 == strcasecmp (method,
+ MHD_HTTP_METHOD_OPTIONS))
+ {
+ return (MHD_YES ==
+ TALER_MHD_reply_cors_preflight (hc->connection))
+ ? GNUNET_NO
+ : GNUNET_SYSERR;
+ }
+ if ( (rh->method != NULL) &&
+ (0 != strcasecmp (method,
+ rh->method)) )
+ continue;
+ hc->rh = rh;
+ break;
+ }
+ /* Handle HTTP 405: METHOD NOT ALLOWED case */
+ if ( (NULL == hc->rh) &&
+ (url_found) )
+ {
+ struct MHD_Response *reply;
+ MHD_RESULT ret;
+ char *allowed = NULL;
+
+ GNUNET_break_op (0);
+ /* compute 'Allowed:' header (required by HTTP spec for 405 replies) */
+ for (unsigned int i = 0; NULL != handlers[i].url_prefix; i++)
+ {
+ const struct TMH_RequestHandler *rh = &handlers[i];
+
+ if (rh->default_only && (! use_admin))
+ continue;
+ if (! prefix_match (rh,
+ url,
+ prefix_strlen,
+ infix_url,
+ infix_strlen,
+ suffix_url,
+ suffix_strlen))
+ continue;
+ if (NULL == allowed)
+ {
+ allowed = GNUNET_strdup (rh->method);
+ }
+ else
+ {
+ char *tmp;
+
+ GNUNET_asprintf (&tmp,
+ "%s, %s",
+ allowed,
+ rh->method);
+ GNUNET_free (allowed);
+ allowed = tmp;
+ }
+ if (0 == strcasecmp (rh->method,
+ MHD_HTTP_METHOD_GET))
+ {
+ char *tmp;
+
+ GNUNET_asprintf (&tmp,
+ "%s, %s",
+ allowed,
+ MHD_HTTP_METHOD_HEAD);
+ GNUNET_free (allowed);
+ allowed = tmp;
+ }
+ }
+ reply = TALER_MHD_make_error (TALER_EC_GENERIC_METHOD_INVALID,
+ method);
+ GNUNET_break (MHD_YES ==
+ MHD_add_response_header (reply,
+ MHD_HTTP_HEADER_ALLOW,
+ allowed));
+ GNUNET_free (allowed);
+ ret = MHD_queue_response (hc->connection,
+ MHD_HTTP_METHOD_NOT_ALLOWED,
+ reply);
+ MHD_destroy_response (reply);
+ return (MHD_YES == ret)
+ ? GNUNET_NO
+ : GNUNET_SYSERR;
+ }
+ if (NULL == hc->rh)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Endpoint `%s' not known\n",
+ hc->url);
+ return (MHD_YES ==
+ TALER_MHD_reply_with_error (hc->connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_GENERIC_ENDPOINT_UNKNOWN,
+ hc->url))
+ ? GNUNET_NO
+ : GNUNET_SYSERR;
+ }
+ }
+ return GNUNET_OK;
+}
+
+
+enum GNUNET_GenericReturnValue
+TMH_dispatch_request (struct TMH_HandlerContext *hc,
+ const char *url,
+ const char *method,
+ bool use_admin,
+ bool *is_public)
+{
+ const struct TMH_RequestHandler *handlers;
+
+ *is_public = false;
+ handlers = determine_handler_group (&url,
+ is_public);
+ return identify_handler (hc,
+ handlers,
+ url,
+ method,
+ use_admin);
+}
diff --git a/src/backend/taler-merchant-httpd_dispatcher.h b/src/backend/taler-merchant-httpd_dispatcher.h
@@ -0,0 +1,49 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2021-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-merchant-httpd_dispatcher.h
+ * @brief request dispatch logic
+ * @author Christian Grothoff
+ */
+#ifndef TALER_MERCHANT_HTTPD_DISPATCHER_H
+#define TALER_MERCHANT_HTTPD_DISPATCHER_H
+
+#include "taler-merchant-httpd.h"
+
+
+/**
+ * Find the request handler for the given request based on
+ * the @a url and @a method. Only considers applicable
+ * request handlers, thus we need @a use_admin to see if admin
+ * handlers are in scope.
+ *
+ * @param[in,out] hc handler context to update with request handler
+ * @param url URL to match against the handlers
+ * @param method HTTP access method to consider
+ * @param use_admin true if we are using the admin instance
+ * @param[out] is_public set to true if the handler is a public endpoint
+ * @return #GNUNET_OK on success,
+ * #GNUNET_NO if an error was queued (return #MHD_YES)
+ * #GNUNET_SYSERR to close the connection (return #MHD_NO)
+ */
+enum GNUNET_GenericReturnValue
+TMH_dispatch_request (struct TMH_HandlerContext *hc,
+ const char *url,
+ const char *method,
+ bool use_admin,
+ bool *is_public);
+
+#endif
diff --git a/src/backend/taler-merchant-httpd_post-reports-ID.c b/src/backend/taler-merchant-httpd_post-reports-ID.c
@@ -0,0 +1,120 @@
+/*
+ This file is part of TALER
+ (C) 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 merchant/backend/taler-merchant-httpd_post-reports-ID.c
+ * @brief implementation of POST /reports/$REPORT_ID
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler-merchant-httpd_post-reports-ID.h"
+#include <taler/taler_json_lib.h>
+
+
+MHD_RESULT
+TMH_post_reports_ID (
+ const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc)
+{
+ const char *report_id_str = hc->infix;
+ unsigned long long report_id;
+ const char *mime_type;
+ struct TALER_MERCHANT_ReportToken report_token;
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_fixed_auto ("report_token",
+ &report_token),
+ GNUNET_JSON_spec_end ()
+ };
+ enum GNUNET_DB_QueryStatus qs;
+ char *instance_id;
+ char *data_source;
+
+ {
+ char dummy;
+
+ if (1 != sscanf (report_id_str,
+ "%llu%c",
+ &report_id,
+ &dummy))
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "report_id");
+ }
+ }
+
+ {
+ enum GNUNET_GenericReturnValue res;
+
+ res = TALER_MHD_parse_json_data (connection,
+ hc->request_body,
+ spec);
+ if (GNUNET_OK != res)
+ {
+ GNUNET_break_op (0);
+ return (GNUNET_NO == res)
+ ? MHD_YES
+ : MHD_NO;
+ }
+ }
+
+ mime_type = MHD_lookup_connection_value (connection,
+ MHD_HEADER_KIND,
+ MHD_HTTP_HEADER_ACCEPT);
+ if (NULL == mime_type)
+ mime_type = "application/json";
+ qs = TMH_db->check_report (TMH_db->cls,
+ report_id,
+ &report_token,
+ mime_type,
+ &instance_id,
+ &data_source);
+ if (qs < 0)
+ {
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "check_report");
+ }
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ {
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_GENERIC_REPORT_UNKNOWN,
+ report_id_str);
+ }
+
+ {
+ char *url;
+
+ GNUNET_asprintf (&url,
+ "/instances/%s%s",
+ instance_id,
+ data_source);
+ GNUNET_free (instance_id);
+ GNUNET_free (data_source);
+
+ /* FIXME: Generate and return report from URL */
+ GNUNET_free (url);
+ }
+
+ return MHD_NO;
+}
diff --git a/src/backend/taler-merchant-httpd_post-reports-ID.h b/src/backend/taler-merchant-httpd_post-reports-ID.h
@@ -0,0 +1,41 @@
+/*
+ This file is part of TALER
+ (C) 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 taler-merchant-httpd_post-reports-ID.h
+ * @brief headers for POST /reports handler
+ * @author Christian Grothoff
+ */
+#ifndef TALER_EXCHANGE_HTTPD_POST_REPORTS_ID_H
+#define TALER_EXCHANGE_HTTPD_POST_REPORTS_ID_H
+#include <microhttpd.h>
+#include "taler-merchant-httpd.h"
+
+
+/**
+ * Handles a POST /reports/$REPORT_ID request.
+ *
+ * @param rc request context
+ * @param root uploaded JSON data
+ * @param args array of additional options (first must be the report_id)
+ * @return MHD result code
+ */
+MHD_RESULT
+TMH_post_reports_ID (
+ const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc);
+
+#endif
diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID-tokens.c b/src/backend/taler-merchant-httpd_private-get-instances-ID-tokens.c
@@ -19,6 +19,7 @@
* @author Martin Schanzenbach
*/
#include "platform.h"
+#include "taler-merchant-httpd_auth.h"
#include "taler-merchant-httpd_private-get-instances-ID-tokens.h"
diff --git a/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.c b/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.c
@@ -25,6 +25,7 @@
*/
#include "platform.h"
#include "taler-merchant-httpd_private-post-instances-ID-auth.h"
+#include "taler-merchant-httpd_auth.h"
#include "taler-merchant-httpd_helper.h"
#include "taler-merchant-httpd_mfa.h"
#include <taler/taler_json_lib.h>
diff --git a/src/backend/taler-merchant-httpd_private-post-instances-ID-token.c b/src/backend/taler-merchant-httpd_private-post-instances-ID-token.c
@@ -24,6 +24,7 @@
*/
#include "platform.h"
#include "taler-merchant-httpd_private-post-instances-ID-token.h"
+#include "taler-merchant-httpd_auth.h"
#include "taler-merchant-httpd_helper.h"
#include "taler-merchant-httpd_mfa.h"
#include <taler/taler_json_lib.h>
diff --git a/src/backend/taler-merchant-httpd_private-post-instances.c b/src/backend/taler-merchant-httpd_private-post-instances.c
@@ -26,6 +26,7 @@
#include "taler-merchant-httpd_private-post-instances.h"
#include "taler-merchant-httpd_helper.h"
#include "taler-merchant-httpd.h"
+#include "taler-merchant-httpd_auth.h"
#include "taler-merchant-httpd_mfa.h"
#include "taler_merchant_bank_lib.h"
#include <taler/taler_dbevents.h>