/*
  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.c
 * @brief HTTP serving layer intended to perform crypto-work and
 * communication with the exchange
 * @author Marcello Stanisci
 * @author Christian Grothoff
 * @author Florian Dold
 * @author Priscilla HUANG
 */
#include "platform.h"
#include <taler/taler_dbevents.h>
#include <taler/taler_bank_service.h>
#include <taler/taler_mhd_lib.h>
#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_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_spa.h"
#include "taler-merchant-httpd_statics.h"
#include "taler-merchant-httpd_terms.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

/**
 * Backlog for listen operation on unix-domain sockets.
 */
#define UNIX_BACKLOG 500

/**
 * Default maximum upload size permitted.  Can be overridden
 * per handler.
 */
#define DEFAULT_MAX_UPLOAD_SIZE (16 * 1024)

char *TMH_currency;

char *TMH_base_url;

char *TMH_spa_dir;

char *TMH_helper_email;

char *TMH_helper_sms;

char *TMH_allowed_payment_targets;

char *TMH_default_persona;

char *TMH_payment_target_regex;

regex_t TMH_payment_target_re;

int TMH_force_audit;

struct TALER_MERCHANTDB_Plugin *TMH_db;

struct GNUNET_CONTAINER_MultiHashMap *TMH_by_id_map;

struct GNUNET_TIME_Relative TMH_default_pay_delay;

struct GNUNET_TIME_Relative TMH_default_refund_delay;

struct GNUNET_TIME_Relative TMH_default_wire_transfer_delay;

enum GNUNET_TIME_RounderInterval TMH_default_wire_transfer_rounding_interval;

int TMH_strict_v19;

int TMH_auth_disabled;

int TMH_have_self_provisioning;

enum TEH_TanChannelSet TEH_mandatory_tan_channels;

struct GNUNET_TIME_Relative TMH_legal_expiration;

unsigned int TMH_num_cspecs;

struct TALER_CurrencySpecification *TMH_cspecs;

struct GNUNET_CURL_Context *TMH_curl_ctx;

/**
 * Event handler for instance settings changes.
 */
static struct GNUNET_DB_EventHandler *instance_eh;

/**
 * True if we started any HTTP daemon.
 */
static bool have_daemons;

/**
 * Should a "Connection: close" header be added to each HTTP response?
 */
static int merchant_connection_close;

/**
 * Context for integrating #TMH_curl_ctx with the
 * GNUnet event loop.
 */
static struct GNUNET_CURL_RescheduleContext *merchant_curl_rc;

/**
 * Global return code
 */
static int global_ret;

/**
 * Our configuration.
 */
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)
{
  GNUNET_free (wm->payto_uri.full_payto);
  GNUNET_free (wm->wire_method);
  GNUNET_free (wm->credit_facade_url);
  json_decref (wm->credit_facade_credentials);
  GNUNET_free (wm);
}


void
TMH_instance_decref (struct TMH_MerchantInstance *mi)
{
  struct TMH_WireMethod *wm;

  mi->rc--;
  if (0 != mi->rc)
    return;
  TMH_force_get_orders_resume (mi);
  while (NULL != (wm = (mi->wm_head)))
  {
    GNUNET_CONTAINER_DLL_remove (mi->wm_head,
                                 mi->wm_tail,
                                 wm);
    TMH_wire_method_free (wm);
  }

  GNUNET_free (mi->settings.id);
  GNUNET_free (mi->settings.name);
  GNUNET_free (mi->settings.email);
  GNUNET_free (mi->settings.phone);
  GNUNET_free (mi->settings.website);
  GNUNET_free (mi->settings.logo);
  json_decref (mi->settings.address);
  json_decref (mi->settings.jurisdiction);
  GNUNET_free (mi);
}


enum GNUNET_GenericReturnValue
TMH_instance_free_cb (void *cls,
                      const struct GNUNET_HashCode *key,
                      void *value)
{
  struct TMH_MerchantInstance *mi = value;

  (void) cls;
  (void) key;
  GNUNET_assert (GNUNET_OK ==
                 GNUNET_CONTAINER_multihashmap_remove (TMH_by_id_map,
                                                       &mi->h_instance,
                                                       mi));
  TMH_instance_decref (mi);
  return GNUNET_YES;
}


/**
 * Shutdown task (invoked when the application is being
 * terminated for any reason)
 *
 * @param cls NULL
 */
static void
do_shutdown (void *cls)
{
  (void) cls;
  TALER_MHD_daemons_halt ();
  TMH_force_orders_resume ();
  TMH_force_ac_resume ();
  TMH_force_pc_resume ();
  TMH_force_kyc_resume ();
  TMH_force_gorc_resume ();
  TMH_force_wallet_get_order_resume ();
  TMH_force_wallet_refund_order_resume ();
  TMH_challenge_done ();
  TALER_MHD_daemons_destroy ();
  if (NULL != instance_eh)
  {
    TMH_db->event_listen_cancel (instance_eh);
    instance_eh = NULL;
  }
  TMH_EXCHANGES_done ();
  if (NULL != TMH_db)
  {
    TALER_MERCHANTDB_plugin_unload (TMH_db);
    TMH_db = NULL;
  }
  if (NULL != TMH_by_id_map)
  {
    GNUNET_CONTAINER_multihashmap_iterate (TMH_by_id_map,
                                           &TMH_instance_free_cb,
                                           NULL);
    GNUNET_CONTAINER_multihashmap_destroy (TMH_by_id_map);
    TMH_by_id_map = NULL;
  }
  TALER_TEMPLATING_done ();
  if (NULL != TMH_curl_ctx)
  {
    GNUNET_CURL_fini (TMH_curl_ctx);
    TMH_curl_ctx = NULL;
  }
  if (NULL != merchant_curl_rc)
  {
    GNUNET_CURL_gnunet_rc_destroy (merchant_curl_rc);
    merchant_curl_rc = NULL;
  }
  if (NULL != TMH_payment_target_regex)
  {
    regfree (&TMH_payment_target_re);
    GNUNET_free (TMH_payment_target_regex);
  }
}


/**
 * Function called whenever MHD is done with a request.  If the
 * request was a POST, we may have stored a `struct Buffer *` in the
 * @a con_cls that might still need to be cleaned up.  Call the
 * respective function to free the memory.
 *
 * @param cls client-defined closure
 * @param connection connection handle
 * @param con_cls value as set by the last call to
 *        the #MHD_AccessHandlerCallback
 * @param toe reason for request termination
 * @see #MHD_OPTION_NOTIFY_COMPLETED
 * @ingroup request
 */
static void
handle_mhd_completion_callback (void *cls,
                                struct MHD_Connection *connection,
                                void **con_cls,
                                enum MHD_RequestTerminationCode toe)
{
  struct TMH_HandlerContext *hc = *con_cls;

  (void) cls;
  if (NULL == hc)
    return;
  GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id);
  {
#if MHD_VERSION >= 0x00097304
    const union MHD_ConnectionInfo *ci;
    unsigned int http_status = 0;

    ci = MHD_get_connection_info (connection,
                                  MHD_CONNECTION_INFO_HTTP_STATUS);
    if (NULL != ci)
      http_status = ci->http_status;
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Request for `%s' completed with HTTP status %u (%d)\n",
                hc->url,
                http_status,
                toe);
#else
    (void) connection;
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Finished handling request for `%s' with MHD termination code %d\n",
                hc->url,
                (int) toe);
#endif
  }
  if (NULL != hc->cc)
    hc->cc (hc->ctx);
  TALER_MHD_parse_post_cleanup_callback (hc->json_parse_context);
  GNUNET_free (hc->infix);
  if (NULL != hc->request_body)
    json_decref (hc->request_body);
  if (NULL != hc->instance)
    TMH_instance_decref (hc->instance);
  TMH_db->preflight (TMH_db->cls);
  GNUNET_free (hc->full_url);
  GNUNET_free (hc);
  *con_cls = NULL;
}


struct TMH_MerchantInstance *
TMH_lookup_instance (const char *instance_id)
{
  struct GNUNET_HashCode h_instance;

  if (NULL == instance_id)
    instance_id = "admin";
  GNUNET_CRYPTO_hash (instance_id,
                      strlen (instance_id),
                      &h_instance);
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Looking for by-id key %s of '%s' in hashmap\n",
              GNUNET_h2s (&h_instance),
              instance_id);
  /* We're fine if that returns NULL, the calling routine knows how
     to handle that */
  return GNUNET_CONTAINER_multihashmap_get (TMH_by_id_map,
                                            &h_instance);
}


/**
 * Add instance definition to our active set of instances.
 *
 * @param[in,out] mi merchant instance details to define
 * @return #GNUNET_OK on success, #GNUNET_NO if the same ID is in use already
 */
enum GNUNET_GenericReturnValue
TMH_add_instance (struct TMH_MerchantInstance *mi)
{
  const char *id;
  int ret;

  id = mi->settings.id;
  if (NULL == id)
    id = "admin";
  GNUNET_CRYPTO_hash (id,
                      strlen (id),
                      &mi->h_instance);
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Looking for by-id key %s of `%s' in hashmap\n",
              GNUNET_h2s (&mi->h_instance),
              id);
  ret = GNUNET_CONTAINER_multihashmap_put (TMH_by_id_map,
                                           &mi->h_instance,
                                           mi,
                                           GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY);
  if (GNUNET_OK == ret)
  {
    GNUNET_assert (mi->rc < UINT_MAX);
    mi->rc++;
  }
  return ret;
}


/**
 * 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
 * @param full_url the full URL
 * @param con MHD connection object
 * @return our handler context
 */
static void *
full_url_track_callback (void *cls,
                         const char *full_url,
                         struct MHD_Connection *con)
{
  struct TMH_HandlerContext *hc;

  hc = GNUNET_new (struct TMH_HandlerContext);
  hc->connection = con;
  GNUNET_async_scope_fresh (&hc->async_scope_id);
  GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id);
  hc->full_url = GNUNET_strdup (full_url);
  return hc;
}


/**
 * 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;
}


/**
 * A client has requested the given url using the given method
 * (#MHD_HTTP_METHOD_GET, #MHD_HTTP_METHOD_PUT,
 * #MHD_HTTP_METHOD_DELETE, #MHD_HTTP_METHOD_POST, etc).  The callback
 * must call MHD callbacks to provide content to give back to the
 * client and return an HTTP status code (i.e. #MHD_HTTP_OK,
 * #MHD_HTTP_NOT_FOUND, etc.).
 *
 * @param cls argument given together with the function
 *        pointer when the handler was registered with MHD
 * @param connection the MHD connection to handle
 * @param url the requested url
 * @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
 *        #MHD_HTTP_METHOD_PUT, etc.)
 * @param version the HTTP version string (i.e.
 *        #MHD_HTTP_VERSION_1_1)
 * @param upload_data the data being uploaded (excluding HEADERS,
 *        for a POST that fits into memory and that is encoded
 *        with a supported encoding, the POST data will NOT be
 *        given in upload_data and is instead available as
 *        part of #MHD_get_connection_values; very large POST
 *        data *will* be made available incrementally in
 *        @a upload_data)
 * @param upload_data_size set initially to the size of the
 *        @a upload_data provided; the method must update this
 *        value to the number of bytes NOT processed;
 * @param con_cls pointer that the callback can set to some
 *        address and that will be preserved by MHD for future
 *        calls for this request; since the access handler may
 *        be called many times (i.e., for a PUT/POST operation
 *        with plenty of upload data) this allows the application
 *        to easily associate some request-specific state.
 *        If necessary, this state can be cleaned up in the
 *        global #MHD_RequestCompletedCallback (which
 *        can be set with the #MHD_OPTION_NOTIFY_COMPLETED).
 *        Initially, `*con_cls` will be set up by the
 *        full_url_track_callback().
 * @return #MHD_YES if the connection was handled successfully,
 *         #MHD_NO if the socket must be closed due to a serious
 *         error while handling the request
 */
static MHD_RESULT
url_handler (void *cls,
             struct MHD_Connection *connection,
             const char *url,
             const char *method,
             const char *version,
             const char *upload_data,
             size_t *upload_data_size,
             void **con_cls)
{
  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
    },
    /* 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
    },
    {
      .url_prefix = "*",
      .method = MHD_HTTP_METHOD_OPTIONS,
      .handler = &handle_server_options
    },
    {
      .url_prefix = NULL
    }
  };
  struct TMH_HandlerContext *hc = *con_cls;
  struct TMH_RequestHandler *handlers;
  bool use_default = false;

  (void) cls;
  (void) version;
  if (NULL != hc->url)
  {
    /* MHD calls us again for a request, for first call
       see 'else' case below */
    GNUNET_assert (NULL != hc->rh);
    GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id);
    if ( (hc->has_body) &&
         (NULL == hc->request_body) )
    {
      size_t mul = hc->rh->max_upload;
      enum GNUNET_GenericReturnValue res;

      if (0 == mul)
        mul = DEFAULT_MAX_UPLOAD_SIZE;
      if ( (hc->total_upload + *upload_data_size < hc->total_upload) ||
           (hc->total_upload + *upload_data_size > mul) )
      {
        /* Client exceeds upload limit. Should _usually_ be checked earlier
           when we look at the MHD_HTTP_HEADER_CONTENT_LENGTH, alas with
           chunked encoding an uploader MAY have omitted this, and thus
           not permitted us to check on time. In this case, we just close
           the connection once it exceeds our limit (instead of waiting
           for the upload to complete and then fail). This could theoretically
           cause some clients to retry, alas broken or malicious clients
           are likely to retry anyway, so little we can do about it, and
           failing earlier seems the best option here.  */
        GNUNET_break_op (0);
        return MHD_NO;
      }
      hc->total_upload += *upload_data_size;
      res = TALER_MHD_parse_post_json (connection,
                                       &hc->json_parse_context,
                                       upload_data,
                                       upload_data_size,
                                       &hc->request_body);
      if (GNUNET_SYSERR == res)
        return MHD_NO;
      /* A error response was already generated */
      if ( (GNUNET_NO == res) ||
           /* or, need more data to accomplish parsing */
           (NULL == hc->request_body) )
        return MHD_YES; /* let MHD call us *again* */
    }
    /* Upload complete (if any), call handler to generate reply */
    return hc->rh->handler (hc->rh,
                            connection,
                            hc);
  }
  hc->url = url;
  {
    const char *correlation_id;

    correlation_id = MHD_lookup_connection_value (connection,
                                                  MHD_HEADER_KIND,
                                                  "Taler-Correlation-Id");
    if ( (NULL != correlation_id) &&
         (GNUNET_YES !=
          GNUNET_CURL_is_valid_scope_id (correlation_id)) )
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Illegal incoming correlation ID\n");
      correlation_id = NULL;
    }
    if (NULL != correlation_id)
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Handling request for (%s) URL '%s', correlation_id=%s\n",
                  method,
                  url,
                  correlation_id);
    else
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Handling request (%s) for URL '%s'\n",
                  method,
                  url);
  }

  if (0 == strcasecmp (method,
                       MHD_HTTP_METHOD_HEAD))
    method = MHD_HTTP_METHOD_GET; /* MHD will deal with the rest */


  /* Find out the merchant backend instance for the request.
   * If there is an instance, remove the instance specification
   * from the beginning of the request URL. */
  {
    const char *instance_prefix = "/instances/";

    if (0 == strncmp (url,
                      instance_prefix,
                      strlen (instance_prefix)))
    {
      /* url starts with "/instances/" */
      const char *istart = url + strlen (instance_prefix);
      const char *slash = strchr (istart, '/');
      char *instance_id;

      if (NULL == slash)
        instance_id = GNUNET_strdup (istart);
      else
        instance_id = GNUNET_strndup (istart,
                                      slash - istart);
      if (0 == strcmp (instance_id,
                       "admin"))
      {
        MHD_RESULT ret;
        struct MHD_Response *response;
        const char *rstart = hc->full_url + strlen (instance_prefix);
        const char *rslash = strchr (rstart, '/');

        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Client used deprecated '/instances/default/' path. Redirecting to modern path\n");

        response
          = MHD_create_response_from_buffer (0,
                                             NULL,
                                             MHD_RESPMEM_PERSISTENT);
        TALER_MHD_add_global_headers (response,
                                      true);
        if (MHD_NO ==
            MHD_add_response_header (response,
                                     MHD_HTTP_HEADER_LOCATION,
                                     NULL == rslash
                                     ? "/"
                                     : rslash))
        {
          GNUNET_break (0);
          MHD_destroy_response (response);
          GNUNET_free (instance_id);
          return MHD_NO;
        }
        ret = MHD_queue_response (connection,
                                  MHD_HTTP_PERMANENT_REDIRECT,
                                  response);
        MHD_destroy_response (response);
        GNUNET_free (instance_id);
        return ret;
      }
      hc->instance = TMH_lookup_instance (instance_id);
      if ( (NULL == hc->instance) &&
           (0 == strcmp ("admin",
                         instance_id)) )
        hc->instance = TMH_lookup_instance (NULL);
      GNUNET_free (instance_id);
      if (NULL == slash)
        url = "";
      else
        url = slash;
    }
    else
    {
      /* use 'default' */
      use_default = true;
      hc->instance = TMH_lookup_instance (NULL);
    }
    if (NULL != hc->instance)
    {
      GNUNET_assert (hc->instance->rc < UINT_MAX);
      hc->instance->rc++;
    }
  }

  {
    const char *management_prefix = "/management/";
    const char *private_prefix = "/private/";

    if ( (0 == strncmp (url,
                        management_prefix,
                        strlen (management_prefix))) )
    {
      handlers = management_handlers;
      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"))
        url = "/";
      else
        url += strlen (private_prefix) - 1;
    }
    else
    {
      handlers = public_handlers;
    }
  }

  if (0 == strcmp (url,
                   ""))
    url = "/"; /* code below does not like empty string */

  {
    /* Matching URL found, but maybe method doesn't match */
    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 */

    /* 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++)
      {
        struct TMH_RequestHandler *rh = &handlers[i];

        if (rh->default_only && (! use_default))
          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 TALER_MHD_reply_cors_preflight (connection);
        }
        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++)
        {
          struct TMH_RequestHandler *rh = &handlers[i];

          if (rh->default_only && (! use_default))
            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 (connection,
                                  MHD_HTTP_METHOD_NOT_ALLOWED,
                                  reply);
        MHD_destroy_response (reply);
        return ret;
      }
      if (NULL == hc->rh)
      {
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Endpoint `%s' not known\n",
                    hc->url);
        return TALER_MHD_reply_with_error (connection,
                                           MHD_HTTP_NOT_FOUND,
                                           TALER_EC_GENERIC_ENDPOINT_UNKNOWN,
                                           hc->url);
      }
    }
  }
  /* At this point, we must have found a handler */
  GNUNET_assert (NULL != hc->rh);

  /* If an instance should be there, check one exists */
  if ( (NULL == hc->instance) &&
       (! hc->rh->skip_instance) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Instance for `%s' not known\n",
                hc->url);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_NOT_FOUND,
                                       TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
                                       hc->infix);
  }

  /* Access control for non-public handlers */
  if (public_handlers != handlers)
  {
    const char *auth;
    bool is_basic_auth = false;
    bool auth_malformed = false;

    auth = MHD_lookup_connection_value (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 TALER_MHD_reply_with_ec (connection,
                                        ec,
                                        NULL);
      }
    }
    /* 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 TALER_MHD_reply_with_error (
          connection,
          MHD_HTTP_UNAUTHORIZED,
          TALER_EC_GENERIC_PARAMETER_MALFORMED,
          "'" RFC_8959_PREFIX
          "' prefix or 'Bearer' missing in 'Authorization' header");
      }
      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 TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_UNAUTHORIZED,
                                         TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED,
                                         "Check credentials in 'Authorization' header");
    }
  } /* if (use_private) */


  if ( (NULL == hc->instance) &&
       (! hc->rh->skip_instance) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Instance for URL `%s' not known\n",
                url);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_NOT_FOUND,
                                       TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
                                       url);
  }
  if ( (NULL != hc->instance) && /* make static analysis happy */
       (! hc->rh->skip_instance) &&
       (hc->instance->deleted) &&
       (! hc->rh->allow_deleted_instance) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Instance `%s' was deleted\n",
                hc->instance->settings.id);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_NOT_FOUND,
                                       TALER_EC_MERCHANT_GENERIC_INSTANCE_DELETED,
                                       hc->instance->settings.id);
  }
  /* parse request body */
  hc->has_body = ( (0 == strcasecmp (method,
                                     MHD_HTTP_METHOD_POST)) ||
                   /* PUT is not yet used */
                   (0 == strcasecmp (method,
                                     MHD_HTTP_METHOD_PATCH)) );
  if (hc->has_body)
  {
    TALER_MHD_check_content_length (connection,
                                    0 == hc->rh->max_upload
                                    ? DEFAULT_MAX_UPLOAD_SIZE
                                    : hc->rh->max_upload);
    GNUNET_break (NULL == hc->request_body); /* can't have it already */
  }
  return MHD_YES; /* wait for MHD to call us again */
}


/**
 * Callback invoked with information about a bank account.
 *
 * @param cls closure with a `struct TMH_MerchantInstance *`
 * @param merchant_priv private key of the merchant instance
 * @param acc details about the account
 */
static void
add_account_cb (void *cls,
                const struct TALER_MerchantPrivateKeyP *merchant_priv,
                const struct TALER_MERCHANTDB_AccountDetails *acc)
{
  struct TMH_MerchantInstance *mi = cls;
  struct TMH_WireMethod *wm;

  (void) merchant_priv;
  wm = GNUNET_new (struct TMH_WireMethod);
  wm->h_wire = acc->h_wire;
  wm->payto_uri.full_payto
    = GNUNET_strdup (acc->payto_uri.full_payto);
  wm->wire_salt = acc->salt;
  wm->wire_method
    = TALER_payto_get_method (acc->payto_uri.full_payto);
  wm->active = acc->active;
  GNUNET_CONTAINER_DLL_insert (mi->wm_head,
                               mi->wm_tail,
                               wm);
}


/**
 * Function called during startup to add all known instances to our
 * hash map in memory for faster lookups when we receive requests.
 *
 * @param cls closure, NULL, unused
 * @param merchant_pub public key of the instance
 * @param merchant_priv private key of the instance, NULL if not available
 * @param is detailed configuration settings for the instance
 * @param ias authentication settings for the instance
 */
static void
add_instance_cb (void *cls,
                 const struct TALER_MerchantPublicKeyP *merchant_pub,
                 const struct TALER_MerchantPrivateKeyP *merchant_priv,
                 const struct TALER_MERCHANTDB_InstanceSettings *is,
                 const struct TALER_MERCHANTDB_InstanceAuthSettings *ias)
{
  struct TMH_MerchantInstance *mi;
  enum GNUNET_DB_QueryStatus qs;

  (void) cls;
  mi = TMH_lookup_instance (is->id);
  if (NULL != mi)
  {
    /* (outdated) entry exists, remove old entry */
    (void) TMH_instance_free_cb (NULL,
                                 &mi->h_instance,
                                 mi);
  }
  mi = GNUNET_new (struct TMH_MerchantInstance);
  mi->settings = *is;
  mi->auth = *ias;
  mi->settings.id = GNUNET_strdup (mi->settings.id);
  mi->settings.name = GNUNET_strdup (mi->settings.name);
  if (NULL != mi->settings.email)
    mi->settings.email = GNUNET_strdup (mi->settings.email);
  if (NULL != mi->settings.phone)
    mi->settings.phone = GNUNET_strdup (mi->settings.phone);
  if (NULL != mi->settings.website)
    mi->settings.website = GNUNET_strdup (mi->settings.website);
  if (NULL != mi->settings.logo)
    mi->settings.logo = GNUNET_strdup (mi->settings.logo);
  mi->settings.address = json_incref (mi->settings.address);
  mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction);
  if (NULL != merchant_priv)
    mi->merchant_priv = *merchant_priv;
  else
    mi->deleted = true;
  mi->merchant_pub = *merchant_pub;
  qs = TMH_db->select_accounts (TMH_db->cls,
                                mi->settings.id,
                                &add_account_cb,
                                mi);
  if (0 > qs)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Error loading accounts of `%s' from database\n",
                mi->settings.id);
  }
  GNUNET_assert (GNUNET_OK ==
                 TMH_add_instance (mi));
}


/**
 * Trigger (re)loading of instance settings from DB.
 *
 * @param cls NULL
 * @param extra ID of the instance that changed, NULL
 *              to load all instances (will not handle purges!)
 * @param extra_len number of bytes in @a extra
 */
static void
load_instances (void *cls,
                const void *extra,
                size_t extra_len)
{
  enum GNUNET_DB_QueryStatus qs;
  const char *id = extra;

  (void) cls;
  if ( (NULL != extra) &&
       ( (0 == extra_len) ||
         ('\0' != id[extra_len - 1]) ) )
  {
    GNUNET_break (0 == extra_len);
    extra = NULL;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Received instance settings notification: reload `%s'\n",
              id);
  if (NULL == extra)
  {
    qs = TMH_db->lookup_instances (TMH_db->cls,
                                   false,
                                   &add_instance_cb,
                                   NULL);
  }
  else
  {
    struct TMH_MerchantInstance *mi;

    /* This must be done here to handle instance
       purging, as for purged instances, the DB
       lookup below will otherwise do nothing */
    mi = TMH_lookup_instance (id);
    if (NULL != mi)
    {
      (void) TMH_instance_free_cb (NULL,
                                   &mi->h_instance,
                                   mi);
    }
    qs = TMH_db->lookup_instance (TMH_db->cls,
                                  id,
                                  false,
                                  &add_instance_cb,
                                  NULL);
  }
  if (0 > qs)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed initialization. Check database setup.\n");
    global_ret = EXIT_NOPERMISSION;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
}


/**
 * A transaction modified an instance setting (or created/deleted/purged
 * one). Notify all backends about the change.
 *
 * @param id ID of the instance that changed
 */
void
TMH_reload_instances (const char *id)
{
  struct GNUNET_DB_EventHeaderP es = {
    .size = ntohs (sizeof (es)),
    .type = ntohs (TALER_DBEVENT_MERCHANT_INSTANCE_SETTINGS)
  };

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Generating instance settings notification: reload `%s'\n",
              id);
  TMH_db->event_notify (TMH_db->cls,
                        &es,
                        id,
                        (NULL == id)
                        ? 0
                        : strlen (id) + 1);
}


/**
 * Callback invoked on every listen socket to start the
 * respective MHD HTTP daemon.
 *
 * @param cls unused
 * @param lsock the listen socket
 */
static void
start_daemon (void *cls,
              int lsock)
{
  struct MHD_Daemon *mhd;

  (void) cls;
  GNUNET_assert (-1 != lsock);
  mhd = MHD_start_daemon (MHD_USE_SUSPEND_RESUME | MHD_USE_DUAL_STACK
                          | MHD_USE_AUTO,
                          0 /* port */,
                          NULL, NULL,
                          &url_handler, NULL,
                          MHD_OPTION_LISTEN_SOCKET, lsock,
                          MHD_OPTION_URI_LOG_CALLBACK,
                          &full_url_track_callback, NULL,
                          MHD_OPTION_NOTIFY_COMPLETED,
                          &handle_mhd_completion_callback, NULL,
                          MHD_OPTION_CONNECTION_TIMEOUT,
                          (unsigned int) 10 /* 10s */,
                          MHD_OPTION_END);
  if (NULL == mhd)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to launch HTTP service.\n");
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  have_daemons = true;
  TALER_MHD_daemon_start (mhd);
}


/**
 * Main function that will be run by the scheduler.
 *
 * @param cls closure
 * @param args remaining command-line arguments
 * @param cfgfile name of the configuration file used (for saving, can be
 *        NULL!)
 * @param config configuration
 */
static void
run (void *cls,
     char *const *args,
     const char *cfgfile,
     const struct GNUNET_CONFIGURATION_Handle *config)
{
  enum TALER_MHD_GlobalOptions go;
  int elen;

  (void) cls;
  (void) args;
  (void) cfgfile;
  cfg = config;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Starting taler-merchant-httpd\n");
  go = TALER_MHD_GO_NONE;
  if (merchant_connection_close)
    go |= TALER_MHD_GO_FORCE_CONNECTION_CLOSE;
  TALER_MHD_setup (go);

  global_ret = EXIT_SUCCESS;
  GNUNET_SCHEDULER_add_shutdown (&do_shutdown,
                                 NULL);

  TMH_curl_ctx
    = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
                        &merchant_curl_rc);
  if (NULL == TMH_curl_ctx)
  {
    GNUNET_break (0);
    global_ret = EXIT_NO_RESTART;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  merchant_curl_rc = GNUNET_CURL_gnunet_rc_create (TMH_curl_ctx);
  /* Disable 100 continue processing */
  GNUNET_break (GNUNET_OK ==
                GNUNET_CURL_append_header (TMH_curl_ctx,
                                           MHD_HTTP_HEADER_EXPECT ":"));
  GNUNET_CURL_enable_async_scope_header (TMH_curl_ctx,
                                         "Taler-Correlation-Id");

  if (GNUNET_SYSERR ==
      TALER_config_get_currency (cfg,
                                 "merchant",
                                 &TMH_currency))
  {
    global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (GNUNET_OK !=
      TALER_CONFIG_parse_currencies (cfg,
                                     TMH_currency,
                                     &TMH_num_cspecs,
                                     &TMH_cspecs))
  {
    global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }

  if (GNUNET_SYSERR ==
      (TMH_strict_v19
         = GNUNET_CONFIGURATION_get_value_yesno (cfg,
                                                 "merchant",
                                                 "STRICT_PROTOCOL_V19")))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_INFO,
                               "merchant",
                               "STRICT_PROTOCOL_V19");
    TMH_strict_v19 = GNUNET_NO;
  }
  if (GNUNET_SYSERR ==
      (TMH_auth_disabled = GNUNET_CONFIGURATION_get_value_yesno (cfg,
                                                                 "merchant",
                                                                 "DISABLE_AUTHENTICATION")))
  {
    TMH_auth_disabled = GNUNET_NO;
  }
  if (GNUNET_YES == TMH_auth_disabled)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "DANGEROUS: Endpoint Authentication disabled!");
  }

  if (GNUNET_SYSERR ==
      (TMH_have_self_provisioning
         = GNUNET_CONFIGURATION_get_value_yesno (cfg,
                                                 "merchant",
                                                 "ENABLE_SELF_PROVISIONING")))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_INFO,
                               "merchant",
                               "ENABLE_SELF_PROVISIONING");
    TMH_have_self_provisioning = GNUNET_NO;
  }

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_time (cfg,
                                           "merchant",
                                           "LEGAL_PRESERVATION",
                                           &TMH_legal_expiration))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               "merchant",
                               "LEGAL_PRESERVATION");
    global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_time (cfg,
                                           "merchant",
                                           "DEFAULT_PAY_DELAY",
                                           &TMH_default_pay_delay))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_INFO,
                               "merchant",
                               "DEFAULT_PAY_DELAY");
    TMH_default_pay_delay = GNUNET_TIME_UNIT_DAYS;
  }
  if (GNUNET_TIME_relative_is_forever (TMH_default_pay_delay))
  {
    GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_INFO,
                               "merchant",
                               "DEFAULT_PAY_DELAY",
                               "forever is not allowed");
    global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_time (cfg,
                                           "merchant",
                                           "DEFAULT_REFUND_DELAY",
                                           &TMH_default_refund_delay))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_INFO,
                               "merchant",
                               "DEFAULT_REFUND_DELAY");
    TMH_default_refund_delay = GNUNET_TIME_relative_multiply (
      GNUNET_TIME_UNIT_DAYS,
      15);
  }
  if (GNUNET_TIME_relative_is_forever (TMH_default_refund_delay))
  {
    GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_INFO,
                               "merchant",
                               "DEFAULT_REFUND_DELAY",
                               "forever is not allowed");
    global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_time (cfg,
                                           "merchant",
                                           "DEFAULT_WIRE_TRANSFER_DELAY",
                                           &TMH_default_wire_transfer_delay))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_INFO,
                               "merchant",
                               "DEFAULT_WIRE_TRANSFER_DELAY");
    TMH_default_wire_transfer_delay = GNUNET_TIME_UNIT_MONTHS;
  }
  if (GNUNET_TIME_relative_is_forever (TMH_default_wire_transfer_delay))
  {
    GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_INFO,
                               "merchant",
                               "DEFAULT_WIRE_TRANSFER_DELAY",
                               "forever is not allowed");
    global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }

  {
    char *dwtri;

    if (GNUNET_OK !=
        GNUNET_CONFIGURATION_get_value_string (
          cfg,
          "merchant",
          "DEFAULT_WIRE_TRANSFER_ROUNDING_INTERVAL",
          &dwtri))
    {
      GNUNET_log_config_missing (GNUNET_ERROR_TYPE_INFO,
                                 "merchant",
                                 "DEFAULT_WIRE_TRANSFER_ROUNDING_INTERVAL");
      TMH_default_wire_transfer_rounding_interval = GNUNET_TIME_RI_NONE;
    }
    else
    {
      if (GNUNET_OK !=
          GNUNET_TIME_string_to_round_interval (
            dwtri,
            &TMH_default_wire_transfer_rounding_interval))
      {
        GNUNET_log_config_invalid (
          GNUNET_ERROR_TYPE_ERROR,
          "merchant",
          "DEFAULT_WIRE_TRANSFER_ROUNDING_INTERVAL",
          "invalid time rounding interval");
        global_ret = EXIT_NOTCONFIGURED;
        GNUNET_free (dwtri);
        GNUNET_SCHEDULER_shutdown ();
        return;
      }
      GNUNET_free (dwtri);
    }
  }

  TMH_load_terms (cfg);

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (cfg,
                                             "merchant",
                                             "PAYMENT_TARGET_TYPES",
                                             &TMH_allowed_payment_targets))
  {
    TMH_allowed_payment_targets = GNUNET_strdup ("*");
  }

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (cfg,
                                             "merchant",
                                             "DEFAULT_PERSONA",
                                             &TMH_default_persona))
  {
    TMH_default_persona = GNUNET_strdup ("expert");
  }

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (cfg,
                                             "merchant",
                                             "PAYMENT_TARGET_REGEX",
                                             &TMH_payment_target_regex))
  {
    TMH_payment_target_regex = NULL;
  }
  else
  {
    if (0 == strlen (TMH_payment_target_regex))
    {
      GNUNET_free (TMH_payment_target_regex);
    }
    else
    {
      if (0 != regcomp (&TMH_payment_target_re,
                        TMH_payment_target_regex,
                        REG_NOSUB | REG_EXTENDED))
      {
        GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                   "merchant",
                                   "PAYMENT_TARGET_REGEX",
                                   "malformed regular expression");
        global_ret = EXIT_NOTCONFIGURED;
        GNUNET_free (TMH_payment_target_regex);
        GNUNET_SCHEDULER_shutdown ();
        return;
      }
    }
  }

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (cfg,
                                             "merchant",
                                             "HELPER_SMS",
                                             &TMH_helper_sms))
  {
    GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING,
                               "merchant",
                               "HELPER_SMS",
                               "no helper specified");
  }

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (cfg,
                                             "merchant",
                                             "HELPER_EMAIL",
                                             &TMH_helper_email))
  {
    GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING,
                               "merchant",
                               "HELPER_EMAIL",
                               "no helper specified");
  }

  {
    char *tan_channels;

    if (GNUNET_OK ==
        GNUNET_CONFIGURATION_get_value_string (cfg,
                                               "merchant",
                                               "MANDATORY_TAN_CHANNELS",
                                               &tan_channels))
    {
      for (char *tok = strtok (tan_channels,
                               " ");
           NULL != tok;
           tok = strtok (NULL,
                         " "))
      {
        if (0 == strcasecmp (tok,
                             "sms"))
        {
          if (NULL == TMH_helper_sms)
          {
            GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                       "merchant",
                                       "MANDATORY_TAN_CHANNELS",
                                       "SMS mandatory, but no HELPER_SMS configured");
            global_ret = EXIT_NOTCONFIGURED;
            GNUNET_SCHEDULER_shutdown ();
            GNUNET_free (tan_channels);
            return;
          }
          TEH_mandatory_tan_channels |= TEH_TCS_SMS;
        }
        else if (0 == strcasecmp (tok,
                                  "email"))
        {
          if (NULL == TMH_helper_email)
          {
            GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                       "merchant",
                                       "MANDATORY_TAN_CHANNELS",
                                       "EMAIL mandatory, but no HELPER_EMAIL configured");
            global_ret = EXIT_NOTCONFIGURED;
            GNUNET_SCHEDULER_shutdown ();
            GNUNET_free (tan_channels);
            return;
          }
          TEH_mandatory_tan_channels |= TEH_TCS_EMAIL;
        }
        else
        {
          GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                     "merchant",
                                     "MANDATORY_TAN_CHANNELS",
                                     tok);
          global_ret = EXIT_NOTCONFIGURED;
          GNUNET_SCHEDULER_shutdown ();
          GNUNET_free (tan_channels);
          return;
        }
      }
      GNUNET_free (tan_channels);
    }
  }

  if (GNUNET_OK ==
      GNUNET_CONFIGURATION_get_value_string (cfg,
                                             "merchant",
                                             "BASE_URL",
                                             &TMH_base_url))
  {
    if (! TALER_is_web_url (TMH_base_url))
    {
      GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                 "merchant",
                                 "BASE_URL",
                                 "Needs to start with 'http://' or 'https://'");
      global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
  }
  if (GNUNET_OK ==
      GNUNET_CONFIGURATION_get_value_string (cfg,
                                             "merchant",
                                             "BACKOFFICE_SPA_DIR",
                                             &TMH_spa_dir))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Loading merchant SPA from %s\n",
                TMH_spa_dir);
  }
  else
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Loading merchant SPA from default location\n");
  }

  if (GNUNET_YES ==
      GNUNET_CONFIGURATION_get_value_yesno (cfg,
                                            "merchant",
                                            "FORCE_AUDIT"))
    TMH_force_audit = GNUNET_YES;
  if (GNUNET_OK !=
      TALER_TEMPLATING_init (TALER_MERCHANT_project_data ()))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to setup templates\n");
    global_ret = EXIT_NOTINSTALLED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (GNUNET_OK !=
      TMH_spa_init (TMH_spa_dir))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to load single page app\n");
    global_ret = EXIT_NOTINSTALLED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  /* /static/ is currently not used */
  /* (void) TMH_statics_init (); */
  if (NULL ==
      (TMH_by_id_map = GNUNET_CONTAINER_multihashmap_create (4,
                                                             GNUNET_YES)))
  {
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (NULL ==
      (TMH_db = TALER_MERCHANTDB_plugin_load (cfg)))
  {
    global_ret = EXIT_NOTINSTALLED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (GNUNET_OK !=
      TMH_db->connect (TMH_db->cls))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to connect to database. Consider running taler-merchant-dbinit!\n");
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  elen = TMH_EXCHANGES_init (config);
  if (GNUNET_SYSERR == elen)
  {
    global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (0 == elen)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Fatal: no trusted exchanges configured. Exiting.\n");
    global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }

  {
    struct GNUNET_DB_EventHeaderP es = {
      .size = ntohs (sizeof (es)),
      .type = ntohs (TALER_DBEVENT_MERCHANT_INSTANCE_SETTINGS)
    };

    instance_eh = TMH_db->event_listen (TMH_db->cls,
                                        &es,
                                        GNUNET_TIME_UNIT_FOREVER_REL,
                                        &load_instances,
                                        NULL);
  }
  load_instances (NULL,
                  NULL,
                  0);
  {
    enum GNUNET_GenericReturnValue ret;

    ret = TALER_MHD_listen_bind (cfg,
                                 "merchant",
                                 &start_daemon,
                                 NULL);
    switch (ret)
    {
    case GNUNET_SYSERR:
      global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      return;
    case GNUNET_NO:
      if (! have_daemons)
      {
        global_ret = EXIT_NOTCONFIGURED;
        GNUNET_SCHEDULER_shutdown ();
        return;
      }
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Could not open all configured listen sockets\n");
      break;
    case GNUNET_OK:
      break;
    }
  }
  global_ret = EXIT_SUCCESS;
}


/**
 * The main function of the serve tool
 *
 * @param argc number of arguments from the command line
 * @param argv command line arguments
 * @return 0 ok, non-zero on error
 */
int
main (int argc,
      char *const *argv)
{
  enum GNUNET_GenericReturnValue res;
  struct GNUNET_GETOPT_CommandLineOption options[] = {
    GNUNET_GETOPT_option_flag ('C',
                               "connection-close",
                               "force HTTP connections to be closed after each request",
                               &merchant_connection_close),
    GNUNET_GETOPT_option_timetravel ('T',
                                     "timetravel"),
    GNUNET_GETOPT_option_version (PACKAGE_VERSION "-" VCS_VERSION),
    GNUNET_GETOPT_OPTION_END
  };

  res = GNUNET_PROGRAM_run (
    TALER_MERCHANT_project_data (),
    argc, argv,
    "taler-merchant-httpd",
    "Taler merchant's HTTP backend interface",
    options,
    &run, NULL);
  if (GNUNET_SYSERR == res)
    return EXIT_INVALIDARGUMENT;
  if (GNUNET_NO == res)
    return EXIT_SUCCESS;
  return global_ret;
}
