merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

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:
Msrc/backend/Makefile.am | 4++++
Msrc/backend/taler-merchant-httpd.c | 2206+------------------------------------------------------------------------------
Msrc/backend/taler-merchant-httpd.h | 190+++++++++++++++++++++++++++----------------------------------------------------
Asrc/backend/taler-merchant-httpd_auth.c | 739+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_auth.h | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_dispatcher.c | 1515+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_dispatcher.h | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-reports-ID.c | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-reports-ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Msrc/backend/taler-merchant-httpd_private-get-instances-ID-tokens.c | 1+
Msrc/backend/taler-merchant-httpd_private-post-instances-ID-auth.c | 1+
Msrc/backend/taler-merchant-httpd_private-post-instances-ID-token.c | 1+
Msrc/backend/taler-merchant-httpd_private-post-instances.c | 1+
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>