/*
  This file is part of GNU Taler
  Copyright (C) 2012-2014 GNUnet e.V.
  Copyright (C) 2018, 2025, 2026 Taler Systems SA

  GNU 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.

  GNU Taler is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public
  License along with GNU Taler; see the file COPYING.  If not,
  write to the Free Software Foundation, Inc., 51 Franklin
  Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

/**
 * @author Martin Schanzenbach
 * @author Christian Grothoff
 * @author Marcello Stanisci
 * @file src/backend/paivana-httpd.c
 * @brief HTTP proxy that acts as a GNU Taler paywall
 */
#include "platform.h"
#include <curl/curl.h>
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_curl_lib.h>
#include <taler/taler_mhd_lib.h>
#include <taler/taler_templating_lib.h>
#include <taler/merchant/common.h>
#include "paivana-httpd.h"
#include "paivana-httpd_cookie.h"
#include "paivana-httpd_daemon.h"
#include "paivana-httpd_helper.h"
#include "paivana-httpd_pay.h"
#include "paivana-httpd_reverse.h"
#include "paivana-httpd_templates.h"
#include "paivana_pd.h"


char *PH_target_server_base_url;

char *PH_target_server_unixpath;

char *PH_merchant_base_url;

char *PH_base_url;

struct GNUNET_CURL_Context *PH_merchant_ctx;

struct GNUNET_CURL_Context *PH_proxy_ctx;

int PH_no_check;

int PH_respect_forwarded_headers;

struct GNUNET_STRINGS_IPv4NetworkPolicy *PH_trusted_proxies4;

struct GNUNET_STRINGS_IPv6NetworkPolicy *PH_trusted_proxies6;

bool PH_have_trusted_proxies;

/**
 * Compiled-in default for #PH_request_buffer_max.  Named because
 * `run()` has to be able to tell it apart from a value the operator
 * wrote, which is what decides whether #PH_max_request_size inherits
 * it.
 */
#define PH_DEFAULT_REQUEST_BUFFER_MAX (256 * 1024)

unsigned long long PH_request_buffer_max = PH_DEFAULT_REQUEST_BUFFER_MAX;

unsigned long long PH_response_buffer_max = 256 * 1024;

unsigned long long PH_max_request_size = 1024 * 1024;

struct GNUNET_TIME_Relative PH_upstream_timeout;

struct GNUNET_TIME_Relative PH_upstream_stall_timeout;

unsigned int PH_connection_limit = 512;

unsigned int PH_per_ip_connection_limit = 32;

int PH_global_ret;

int PH_global_cookie;

regex_t PH_whitelist_ex;

bool PH_have_whitelist_ex;

/**
 * Our configuration.
 */
const struct GNUNET_CONFIGURATION_Handle *PH_cfg;


/**
 * Closure for #GNUNET_CURL_gnunet_scheduler_reschedule() of
 * #PH_merchant_ctx.
 */
static struct GNUNET_CURL_RescheduleContext *merchant_ctx_rc;

/**
 * Closure for #GNUNET_CURL_gnunet_scheduler_reschedule() of
 * #PH_proxy_ctx.
 */
static struct GNUNET_CURL_RescheduleContext *proxy_ctx_rc;


/* *************** General / main code *************** */


/**
 * Load one of the `TRUSTED_PROXIES` options.
 *
 * The GNUnet policy parsers are lenient in ways that matter here, so
 * a non-NULL return is not on its own evidence that anything was
 * understood:
 *
 * - the list is terminated by an all-zero entry, so a network of
 *   0.0.0.0/0 or ::/0 *is* the terminator and silently truncates
 *   everything after it.  "Trust everyone" is therefore inexpressible
 *   — and would be a strange thing to write anyway;
 * - the v4 parser accepts a value whose last entry is not terminated
 *   by ';' -- the operator wrote the separators but not the
 *   terminator -- and drops that entry without a word, along with
 *   anything else after the final ';'.  (Its v6 counterpart refuses
 *   such a value outright, so this one is the v4 side's alone.)
 *
 * A genuinely malformed entry, including a v6 network handed to the
 * v4 parser, does come back as NULL rather than as a shorter list; it
 * is the only one of these that announces itself.
 *
 * The first comes out as "parsed, but nothing usable", which we reject
 * along with NULL: quietly trusting nobody would send every visitor to
 * the socket address, and the operator would have no hint why.  The
 * second is worse, because it succeeds: a single missing ';' would
 * leave the clients behind an unlisted proxy sharing that proxy's
 * address as their identity, i.e. sharing one paid cookie.  So the
 * entries that came back are counted against the ';' that went in, and
 * the value has to end in one.
 *
 * Not checked for here: an entry may also carry a port policy
 * ("10.0.0.0/8:80;"), which parses into a perfectly usable entry whose
 * port range nothing in paivana ever consults -- it reads as a
 * restriction on the trust and is not one.
 *
 * @param c configuration to read from
 * @param option name of the option
 * @param[out] count set to the number of usable entries
 * @return false if the option is present but unusable
 */
static bool
load_trusted_proxies (const struct GNUNET_CONFIGURATION_Handle *c,
                      const char *option,
                      unsigned int *count)
{
  char *opt;
  bool v6 = (0 != strcmp (option,
                          "TRUSTED_PROXIES"));
  unsigned int want = 0;
  size_t len;

  *count = 0;
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (c,
                                             "paivana",
                                             option,
                                             &opt))
    return true; /* not configured at all: fine */
  len = strlen (opt);
  while ( (len > 0) &&
          ( (' ' == opt[len - 1]) ||
            ('\t' == opt[len - 1]) ) )
    opt[--len] = '\0';
  for (const char *p = strchr (opt, ';'); NULL != p; p = strchr (p + 1, ';'))
    want++;
  if (v6)
  {
    PH_trusted_proxies6 = GNUNET_STRINGS_parse_ipv6_policy (opt);
    if (NULL != PH_trusted_proxies6)
      while (! GNUNET_is_zero (&PH_trusted_proxies6[*count].network))
        (*count)++;
  }
  else
  {
    PH_trusted_proxies4 = GNUNET_STRINGS_parse_ipv4_policy (opt);
    if (NULL != PH_trusted_proxies4)
      while (0 != PH_trusted_proxies4[*count].network.s_addr)
        (*count)++;
  }
  if ( (0 == *count) ||
       (*count != want) ||
       (0 == len) ||
       (';' != opt[len - 1]) )
  {
    if ( (0 != *count) &&
         (*count != want) )
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Only %u of the %u entries of `%s' were understood;"
                  " refusing to trust a prefix of the list\n",
                  *count,
                  want,
                  option);
    else if (0 != *count)
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "`%s' does not end in the ';' that terminates the last"
                  " entry; anything after the final one is dropped\n",
                  option);
    GNUNET_log_config_invalid (
      GNUNET_ERROR_TYPE_ERROR,
      "paivana",
      option,
      v6
      ? "not a usable IPv6 network list; entries are separated *and*"
      " terminated by ';' and must not contain spaces, e.g."
      " \"2001:db8::/32;fe80::/10;\" (note that ::/0 is indistinguishable"
      " from the end of the list and cannot be used)"
      : "not a usable IPv4 network list; entries are separated *and*"
      " terminated by ';', e.g. \"10.0.0.0/8;192.168.0.0/16;\""
      " (note that 0.0.0.0/0 is indistinguishable from the end of the"
      " list and cannot be used)");
    GNUNET_free (opt);
    return false;
  }
  GNUNET_free (opt);
  PH_have_trusted_proxies = true;
  return true;
}


/**
 * Task run on shutdown
 *
 * @param cls closure
 */
static void
do_shutdown (void *cls)
{
  (void) cls;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Shutting down...\n");
  TALER_MHD_daemons_halt ();
  PAIVANA_HTTPD_payment_shutdown ();
  PAIVANA_HTTPD_reverse_shutdown ();
  TALER_MHD_daemons_destroy ();
  PAIVANA_HTTPD_unload_templates ();
  TALER_TEMPLATING_done ();
  GNUNET_free (PH_target_server_base_url);
  GNUNET_free (PH_target_server_unixpath);
  GNUNET_free (PH_trusted_proxies4);
  GNUNET_free (PH_trusted_proxies6);
  GNUNET_free (PH_merchant_base_url);
  GNUNET_free (PH_base_url);
  if (PH_have_whitelist_ex)
  {
    regfree (&PH_whitelist_ex);
    PH_have_whitelist_ex = false;
  }
  if (NULL != PH_merchant_ctx)
  {
    GNUNET_CURL_fini (PH_merchant_ctx);
    PH_merchant_ctx = NULL;
  }
  if (NULL != merchant_ctx_rc)
  {
    GNUNET_CURL_gnunet_rc_destroy (merchant_ctx_rc);
    merchant_ctx_rc = NULL;
  }
  if (NULL != PH_proxy_ctx)
  {
    GNUNET_CURL_fini (PH_proxy_ctx);
    PH_proxy_ctx = NULL;
  }
  if (NULL != proxy_ctx_rc)
  {
    GNUNET_CURL_gnunet_rc_destroy (proxy_ctx_rc);
    proxy_ctx_rc = NULL;
  }
}


/**
 * Remove trailing slashes from the web URL @a url, in place.
 *
 * Our configuration syntax prefers base URLs to be written with a
 * trailing '/', while everything we append to one -- a request path,
 * a "/.well-known/..." endpoint -- brings a leading '/' of its own.
 * Dropping them here is what keeps the concatenation from yielding
 * "//", which would otherwise reach the upstream verbatim and, for
 * BASE_URL, end up in the string the access cookie is keyed on and in
 * the URL the templates' regular expressions are matched against.
 *
 * Never strips below "scheme://h", so that a URL consisting of
 * nothing but a scheme and a host keeps its host.
 *
 * Refuses a URL carrying a query or a fragment instead of mangling it:
 * the trailing '/' would then be part of the query ("http://h/?p=a/")
 * and stripping it changes what the query says.  Neither has a sensible
 * reading in a base URL we concatenate a request path onto anyway --
 * "http://h/?p=a" + "/x" is not a request for /x -- so the operator is
 * better told than quietly corrected.
 *
 * @param[in,out] url URL to canonicalize; must have passed
 *        TALER_is_web_url()
 * @return false if @a url carries a query or a fragment, in which case
 *        it is left untouched
 */
static bool
strip_trailing_slashes (char *url)
{
  size_t len = strlen (url);
  const char *sep;
  size_t min_len;

  if ( (NULL != strchr (url,
                        '?')) ||
       (NULL != strchr (url,
                        '#')) )
    return false;
  sep = strstr (url,
                "://");
  GNUNET_assert (NULL != sep); /* was a web URL after all! */
  min_len = (size_t) (sep - url) + strlen ("://") + 1;
  while ( (len > min_len) &&
          ('/' == url[len - 1]) )
    url[--len] = '\0';
  return true;
}


/**
 * Main function that will be run.  Main tasks are (1) init. the
 * curl infrastructure (curl_global_init() / curl_multi_init()),
 * then fetch the HTTP port where its Web service should listen at,
 * and finally start MHD on that port.
 *
 * @param cls closure
 * @param args remaining command-line arguments
 * @param cfgfile name of the configuration file used (for saving, can be NULL!)
 * @param c configuration
 */
static void
run (void *cls,
     char *const *args,
     const char *cfgfile,
     const struct GNUNET_CONFIGURATION_Handle *c)
{
  char *secret;
  bool buffer_max_explicit;

  (void) cls;
  (void) args;
  (void) cfgfile;
  PH_cfg = c;
  PH_upstream_timeout
    = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS,
                                     60);
  PH_upstream_stall_timeout
    = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS,
                                     60);
  GNUNET_SCHEDULER_add_shutdown (&do_shutdown,
                                 NULL);
  if ( (0 == PH_request_buffer_max) ||
       (PH_request_buffer_max > GNUNET_MAX_MALLOC_CHECKED) )
  {
    /* 0 would leave no room to move a body through at all; above the
       hard allocation cap the buffer simply cannot be made.  A negative
       argument ends up here too: GNUnet parses the option with
       sscanf("%llu"), which reads "-1" as ULLONG_MAX rather than
       complaining. */
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "-u/--max-upload must be between 1 and %llu:"
                " it sizes the buffer a request body is relayed through\n",
                (unsigned long long) GNUNET_MAX_MALLOC_CHECKED);
    PH_global_ret = EXIT_INVALIDARGUMENT;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  /* `-u' is applied by GNUNET_PROGRAM_run() before we are called, so a
     value differing from the compiled-in default is one the operator
     wrote.  See PH_max_request_size for why that has to be visible. */
  buffer_max_explicit = (PH_DEFAULT_REQUEST_BUFFER_MAX
                         != PH_request_buffer_max);
  if (! PH_no_check)
  {
    if (GNUNET_OK !=
        TALER_TEMPLATING_init (PAIVANA_project_data ()))
    {
      /* Almost always a missing or unreadable $PREFIX/share/paivana/
         templates/ -- an installation problem, not a bug, so say so
         instead of adding a second "Assertion failed" to the one
         TALER_TEMPLATING_init() already logged. */
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Failed to load the paywall templates; is paivana"
                  " installed, and does PAIVANA_PREFIX point at the"
                  " installation?\n");
      PH_global_ret = EXIT_NOTINSTALLED;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
  }
  if (! PAIVANA_HTTPD_reverse_init ())
  {
    GNUNET_break (0);
    PH_global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (
        c,
        "paivana",
        "DESTINATION_BASE_URL",
        &PH_target_server_base_url))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               "paivana",
                               "DESTINATION_BASE_URL");
    PH_global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (! TALER_is_web_url (PH_target_server_base_url))
  {
    GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                               "paivana",
                               "DESTINATION_BASE_URL",
                               "not a web url");
    PH_global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  {
    unsigned long long v;

    if (GNUNET_OK ==
        GNUNET_CONFIGURATION_get_value_number (c,
                                               "paivana",
                                               "CONNECTION_LIMIT",
                                               &v))
    {
      if ( (0 == v) ||
           (v > UINT_MAX) )
      {
        GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                   "paivana",
                                   "CONNECTION_LIMIT",
                                   "must be between 1 and UINT_MAX");
        PH_global_ret = EXIT_NOTCONFIGURED;
        GNUNET_SCHEDULER_shutdown ();
        return;
      }
      PH_connection_limit = (unsigned int) v;
    }
    if (GNUNET_OK ==
        GNUNET_CONFIGURATION_get_value_number (c,
                                               "paivana",
                                               "PER_IP_CONNECTION_LIMIT",
                                               &v))
    {
      if (v > UINT_MAX)
      {
        GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                   "paivana",
                                   "PER_IP_CONNECTION_LIMIT",
                                   "must not exceed UINT_MAX");
        PH_global_ret = EXIT_NOTCONFIGURED;
        GNUNET_SCHEDULER_shutdown ();
        return;
      }
      PH_per_ip_connection_limit = (unsigned int) v;
    }
    if (GNUNET_OK ==
        GNUNET_CONFIGURATION_get_value_number (c,
                                               "paivana",
                                               "REQUEST_BUFFER_MAX",
                                               &v))
    {
      if ( (0 == v) ||
           (v > GNUNET_MAX_MALLOC_CHECKED) )
      {
        GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                   "paivana",
                                   "REQUEST_BUFFER_MAX",
                                   "must be between 1 and 40 MiB");
        PH_global_ret = EXIT_NOTCONFIGURED;
        GNUNET_SCHEDULER_shutdown ();
        return;
      }
      PH_request_buffer_max = v;
      buffer_max_explicit = true;
    }
    if (GNUNET_OK ==
        GNUNET_CONFIGURATION_get_value_number (c,
                                               "paivana",
                                               "RESPONSE_BUFFER_MAX",
                                               &v))
    {
      if ( (0 == v) ||
           (v > GNUNET_MAX_MALLOC_CHECKED) )
      {
        GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                   "paivana",
                                   "RESPONSE_BUFFER_MAX",
                                   "must be between 1 and 40 MiB");
        PH_global_ret = EXIT_NOTCONFIGURED;
        GNUNET_SCHEDULER_shutdown ();
        return;
      }
      PH_response_buffer_max = v;
    }
    if (GNUNET_OK ==
        GNUNET_CONFIGURATION_get_value_number (c,
                                               "paivana",
                                               "MAX_REQUEST_SIZE",
                                               &v))
    {
      PH_max_request_size = v;
    }
    else if (buffer_max_explicit)
    {
      /* Before streaming these were one number: the buffer a body was
         assembled in *was* the largest body we would accept.  An
         operator who raised it to permit large uploads meant the limit,
         so honour that reading rather than silently tightening their
         configuration back down to the 1 MiB default. */
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Taking MAX_REQUEST_SIZE from the configured request"
                  " buffer size (%llu bytes); set it explicitly to say"
                  " otherwise\n",
                  PH_request_buffer_max);
      PH_max_request_size = PH_request_buffer_max;
    }
  }
  {
    struct GNUNET_TIME_Relative st;

    if (GNUNET_OK ==
        GNUNET_CONFIGURATION_get_value_time (c,
                                             "paivana",
                                             "UPSTREAM_TIMEOUT",
                                             &st))
    {
      if (0 == st.rel_value_us)
      {
        GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                   "paivana",
                                   "UPSTREAM_TIMEOUT",
                                   "must not be zero");
        PH_global_ret = EXIT_NOTCONFIGURED;
        GNUNET_SCHEDULER_shutdown ();
        return;
      }
      PH_upstream_timeout = st;
    }
    if (GNUNET_OK ==
        GNUNET_CONFIGURATION_get_value_time (c,
                                             "paivana",
                                             "UPSTREAM_STALL_TIMEOUT",
                                             &st))
    {
      if (0 == st.rel_value_us)
      {
        GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                   "paivana",
                                   "UPSTREAM_STALL_TIMEOUT",
                                   "must not be zero");
        PH_global_ret = EXIT_NOTCONFIGURED;
        GNUNET_SCHEDULER_shutdown ();
        return;
      }
      PH_upstream_stall_timeout = st;
    }
  }
  {
    unsigned int n4;
    unsigned int n6;

    if ( (! load_trusted_proxies (c,
                                  "TRUSTED_PROXIES",
                                  &n4)) ||
         (! load_trusted_proxies (c,
                                  "TRUSTED_PROXIES6",
                                  &n6)) )
    {
      PH_global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
    if (PH_have_trusted_proxies)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Trusting %u IPv4 and %u IPv6 network(s) as reverse proxies\n",
                  n4,
                  n6);
      if (! PH_respect_forwarded_headers)
      {
        /* The policy says which proxies may speak for a client; it is
           the -f flag that says we listen at all.  Configuring one
           without the other is a mistake in either direction, but only
           this one leaves the policy inert. */
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "TRUSTED_PROXIES configured but -f/--respect-forwarded-headers"
                    " is not set; forwarded headers are ignored entirely\n");
      }
    }
    else if (PH_respect_forwarded_headers)
    {
      /* Not the same mistake in reverse: a single proxy in front, one
         that writes the forwarding headers itself, needs no policy at
         all.  PAIVANA_HTTPD_resolve_forwarding() walks the chain from
         the right and stops at the first hop it does not trust, so
         with no policy it stops on its first step -- at the element
         our peer wrote, which nothing the client prepends can displace.
         What the policy adds is the ability to keep stepping leftwards
         through hops that are named there, which is what a chain of
         more than one proxy needs.  The hazard worth warning about is
         narrower, and it is upstream of us. */
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "-f/--respect-forwarded-headers is set without TRUSTED_PROXIES:"
                  " the client address is taken from the rightmost element of the"
                  " forwarding chain, which is the client's own address only if"
                  " the server in front sets or appends these headers itself."
                  "  One that passes the client's `%s' through unchanged -- as"
                  " nginx does, and `%s' is the header we prefer over `%s' --"
                  " leaves the client choosing the element we believe."
                  "  TRUSTED_PROXIES is needed only for a chain of more than one"
                  " proxy\n",
                  MHD_HTTP_HEADER_FORWARDED,
                  MHD_HTTP_HEADER_FORWARDED,
                  PH_HEADER_X_FORWARDED_FOR);
    }
  }
  /* No need to check the return value.  If given, we take it,
   * otherwise it stays NULL.  */
  GNUNET_CONFIGURATION_get_value_filename (
    c,
    "paivana",
    "DESTINATION_UNIXPATH",
    &PH_target_server_unixpath);
  if (! strip_trailing_slashes (PH_target_server_base_url))
  {
    GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                               "paivana",
                               "DESTINATION_BASE_URL",
                               "must not carry a query or a fragment");
    PH_global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (! PH_no_check)
  {
    if (GNUNET_OK !=
        GNUNET_CONFIGURATION_get_value_string (
          c,
          "paivana",
          "MERCHANT_BACKEND_URL",
          &PH_merchant_base_url))
    {
      GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                                 "paivana",
                                 "MERCHANT_BACKEND_URL");
      PH_global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
    if (! TALER_is_web_url (PH_merchant_base_url))
    {
      GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                 "paivana",
                                 "MERCHANT_BACKEND_URL",
                                 "not a web url");
      PH_global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
    /* Deliberately *not* run through strip_trailing_slashes(): a Taler
       merchant API base URL is joined with relative paths, so it wants
       the '/' the other base URLs above shed.  TALER_url_join() insists
       on it and returns NULL without it, which surfaces much later as
       an assertion failure on the handle built from the joined URL --
       so check for it here, where we can still name the option that is
       wrong. */
    if ('/' != PH_merchant_base_url[strlen (PH_merchant_base_url) - 1])
    {
      GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                 "paivana",
                                 "MERCHANT_BACKEND_URL",
                                 "must end with a '/'");
      PH_global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
  }
  {
    char *merchant_unix_path;

    if (GNUNET_OK ==
        GNUNET_CONFIGURATION_get_value_string (
          c,
          "paivana",
          "MERCHANT_BACKEND_UNIX_PATH",
          &merchant_unix_path))
    {
      if (! TALER_MERCHANT_global_set_unixpath (merchant_unix_path))
      {
        GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING,
                                   "paivana",
                                   "MERCHANT_BACKEND_UNIX_PATH",
                                   "invalid path; ignoring the setting");
      }
      GNUNET_free (merchant_unix_path);
    }
  }
  {
    char *whitelist;

    if (GNUNET_OK ==
        GNUNET_CONFIGURATION_get_value_string (
          c,
          "paivana",
          "WHITELIST",
          &whitelist))
    {
      regex_t bare;
      int rc;

      /* Compile the expression exactly as written first, and refuse
         the configuration if that fails.  The wrapping below is a
         textual splice, and a value like "a)|(b" splices into
         "^(a)|(b)$" -- which compiles, but means "^a" OR "b$", each
         anchored on one side only.  Getting out of the group that way
         needs parentheses that do not balance on their own, and that
         is precisely what a bare regcomp() rejects; an expression that
         is valid by itself is unaffected and keeps matching what it
         always did.  (The merchant backend validates the website_regex
         of a template the same way.) */
      rc = regcomp (&bare,
                    whitelist,
                    REG_NOSUB | REG_EXTENDED);
      if (0 == rc)
      {
        char *anchored;

        regfree (&bare);
        /* Anchor the expression: regexec(3) is unanchored, so a
           WHITELIST of "/free/" would otherwise waive payment for every
           URL merely *containing* it -- including one an attacker
           appends to a path they want for free.  Wrapping in a group
           keeps alternations ("a|b") from binding the anchors to only
           the first and last branch.  An expression that already
           anchors itself is unaffected, as ^ and $ inside still match
           at string start/end. */
        GNUNET_asprintf (&anchored,
                         "^(%s)$",
                         whitelist);
        rc = regcomp (&PH_whitelist_ex,
                      anchored,
                      REG_NOSUB | REG_EXTENDED);
        GNUNET_free (anchored);
      }
      if (0 != rc)
      {
        GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                   "paivana",
                                   "WHITELIST",
                                   "Invalid regular expression");
        GNUNET_free (whitelist);
        PH_global_ret = EXIT_NOTCONFIGURED;
        GNUNET_SCHEDULER_shutdown ();
        return;
      }
      PH_have_whitelist_ex = true;
      GNUNET_free (whitelist);
    }
  }

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (
        c,
        "paivana",
        "BASE_URL",
        &PH_base_url))
  {
    /* Without BASE_URL we reconstruct our own URL from the request, and
       the only thing that makes that safe is a reverse proxy in front
       of us that enforced a correct Host: -- which is exactly what -f
       asserts.  Talking to clients directly, Host: is whatever the
       client typed, and it decides both the string the access cookie is
       keyed on and the string the templates' website_regex is matched
       against: a client sending "Host: anything.invalid" would match no
       template and be served for free.  The scheme has the same
       problem in reverse: direct connections have no X-Forwarded-Proto
       to consult, so a site reached over https would generate http://
       URLs and again match no template. */
    if ( (! PH_respect_forwarded_headers) &&
         (! PH_no_check) )
    {
      /* -n has no paywall, hence no access decision that the site's
         own identity could be got wrong for; the base URL is then only
         cosmetic and the "pure reverse proxy" mode stays configurable
         with nothing but DESTINATION_BASE_URL. */
      GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                                 "paivana",
                                 "BASE_URL");
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "BASE_URL is required unless -f is given: without a"
                  " reverse proxy vouching for it, the client's Host"
                  " header cannot be used to identify this site\n");
      PH_global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_INFO,
                               "paivana",
                               "BASE_URL");
  }
  if (NULL != PH_base_url)
  {
    if (! TALER_is_web_url (PH_base_url))
    {
      GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                 "paivana",
                                 "BASE_URL",
                                 "not a web url");
      PH_global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
    if (! strip_trailing_slashes (PH_base_url))
    {
      GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                 "paivana",
                                 "BASE_URL",
                                 "must not carry a query or a fragment");
      PH_global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
  }

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (
        c,
        "paivana",
        "SECRET",
        &secret))
  {
    if (! PH_no_check)
    {
      /* The key is the only input to the access-cookie MAC that the
         client does not supply, so a fresh random one invalidates
         every access anyone has paid for.  That used to be a warning
         and a random key, which is survivable only if restarts are
         rare -- and the shipped unit restarts hourly (RuntimeMaxSec),
         so a customer paying at 10:59 was shown the paywall again at
         11:01.  Refuse to start instead: the packaging generates one
         at install time, and an operator configuring by hand needs to
         be told rather than silently sold a paywall that forgets.  `-n'
         mints no cookies at all and so needs no key. */
      GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                                 "paivana",
                                 "SECRET");
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Refusing to start without `SECRET': every restart"
                  " would invalidate all access already paid for."
                  "  Generate one with `gpg --gen-random 0 32 | base64'"
                  " (or pass -n to serve without a paywall).\n");
      PH_global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
    GNUNET_CRYPTO_random_block (&paivana_secret,
                                sizeof (paivana_secret));
  }
  else
  {
    GNUNET_CRYPTO_hash (secret,
                        strlen (secret),
                        &paivana_secret);
    GNUNET_free (secret);
  }
  PH_proxy_ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
                                   &proxy_ctx_rc);
  GNUNET_assert (NULL != PH_proxy_ctx);
  proxy_ctx_rc = GNUNET_CURL_gnunet_rc_create (PH_proxy_ctx);
  if (! PH_no_check)
  {
    char *merchant_access_token;
    char *auth_header;

    if (GNUNET_OK !=
        GNUNET_CONFIGURATION_get_value_string (
          c,
          "paivana",
          "MERCHANT_ACCESS_TOKEN",
          &merchant_access_token))
    {
      GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                                 "paivana",
                                 "MERCHANT_ACCESS_TOKEN");
      PH_global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
    if ('\0' == merchant_access_token[0])
    {
      /* An empty value builds "Authorization: Bearer " and the backend
         answers 401 to every request we make with it, i.e. no template
         ever loads and no payment is ever confirmed -- and nothing
         says why.  The option being present but blank is the shape a
         half-finished install has. */
      GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                 "paivana",
                                 "MERCHANT_ACCESS_TOKEN",
                                 "must not be empty");
      GNUNET_free (merchant_access_token);
      PH_global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
    /* A second context, because the credential below is appended to
       *every* request the context makes: on a shared context we would
       hand our merchant bearer token to the origin server (and to
       whoever it redirects to) on each forwarded request. */
    PH_merchant_ctx
      = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
                          &merchant_ctx_rc);
    GNUNET_assert (NULL != PH_merchant_ctx);
    merchant_ctx_rc = GNUNET_CURL_gnunet_rc_create (PH_merchant_ctx);
    GNUNET_asprintf (&auth_header,
                     "%s: Bearer %s",
                     MHD_HTTP_HEADER_AUTHORIZATION,
                     merchant_access_token);
    GNUNET_free (merchant_access_token);
    GNUNET_assert (GNUNET_OK ==
                   GNUNET_CURL_append_header (PH_merchant_ctx,
                                              auth_header));
    GNUNET_free (auth_header);
  }
  /* Once templates are done loading, this will
     start the daemon as well.  In -n (no-payment) mode we skip
     the merchant round-trip entirely. */
  if (PH_no_check)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Paywall disabled (-n), skipping template load\n");
    PAIVANA_HTTPD_serve_requests ();
    return;
  }
  PAIVANA_HTTPD_load_templates ();
}


/**
 * Main function.
 */
int
main (int argc,
      char *const *argv)
{
  struct GNUNET_GETOPT_CommandLineOption options[] = {
    GNUNET_GETOPT_option_flag (
      'f',
      "respect-forwarded-headers",
      gettext_noop (
        "trust X-Forwarded-For for the client address (only safe behind a trusted reverse proxy)"),
      &PH_respect_forwarded_headers),
    GNUNET_GETOPT_option_flag (
      'g',
      "global-payment",
      gettext_noop (
        "disables per-page payment, useful if a single payment should grant access to the entire site"),
      &PH_global_cookie),
    GNUNET_GETOPT_option_flag (
      'n',
      "no-payment",
      gettext_noop (
        "disables payment, useful for testing reverse-proxy only"),
      &PH_no_check),
    GNUNET_GETOPT_option_ulong (
      'u',
      "max-upload",
      "BYTES",
      gettext_noop (
        "bytes of a request body to hold in memory while relaying it upstream (default: 262144); the largest body accepted is MAX_REQUEST_SIZE"),
      &PH_request_buffer_max),
    GNUNET_GETOPT_OPTION_END
  };
  enum GNUNET_GenericReturnValue ret;

  ret = GNUNET_PROGRAM_run (
    PAIVANA_project_data (),
    argc,
    argv,
    "paivana-httpd",
    "reverse proxy requesting Taler payment",
    options,
    &run, NULL);
  if (GNUNET_SYSERR == ret)
    return EXIT_INVALIDARGUMENT;
  if (GNUNET_NO == ret)
    return EXIT_SUCCESS;
  return PH_global_ret;
}


/* end of paivana-httpd.c */
