/*
     This file is part of GNUnet.
     Copyright (C) 2026 Taler Systems SA

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

     Paivana 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 Paivana; see the file COPYING.  If not,
     write to the Free Software Foundation, Inc., 51 Franklin
     Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

/**
 * @author Christian Grothoff
 * @file paivana-httpd_templates.c
 * @brief template functions
 */
#include "platform.h"
#include <curl/curl.h>
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_uri_lib.h>
#include <gnunet/gnunet_curl_lib.h>
#include "paivana-httpd.h"
#include "paivana-httpd_daemon.h"
#include "paivana-httpd_helper.h"
#include "paivana-httpd_templates.h"
#include <taler/taler_mhd_lib.h>
#include <taler/taler_templating_lib.h>
#include "paivana_pd.h"
#include <regex.h>


struct Template;
#define TALER_MERCHANT_GET_PRIVATE_TEMPLATE_RESULT_CLOSURE struct Template
#include <taler/merchant/get-private-templates-TEMPLATE_ID.h>
#include <taler/merchant/get-private-templates.h>


/**
 * Maximum number of rendered paywall responses we cache per template.
 *
 * The key is derived from the client-supplied Accept-Language and
 * Accept-Encoding headers, so without a bound an attacker could send
 * unlimited distinct header values and grow the cache without limit
 * (memory-exhaustion DoS on the cheap pre-payment path).  The key is
 * normalised to what we can actually serve — see cache_key_language()
 * and TALER_MHD_can_compress() — which is what keeps every request
 * after the first few a *hit*; this cap only backstops that.  On
 * reaching it we evict the least recently used entry.
 */
#define MAX_RESPONSE_CACHE_ENTRIES 128

/**
 * How long we give the merchant backend to answer our template
 * queries before abandoning the startup sequence.
 *
 * Nothing is served until they are in: #PAIVANA_HTTPD_serve_requests()
 * — which binds the listen sockets — is only reached from the last of
 * these callbacks, and neither `TALER_MERCHANT_curl_easy_get_()' nor
 * anything above it arms CURLOPT_TIMEOUT.  A backend that accepts the
 * TCP connection and then never answers would otherwise leave paivana
 * neither serving nor exiting, with no log line after the startup
 * banner; under `SERVE = systemd' the listening socket already
 * exists, so clients connect successfully and then hang forever with
 * nothing accepting them.  Generous enough for the round-trips a load
 * takes (GET /private/templates, then one GET per template, issued in
 * parallel).
 */
#define TEMPLATE_LOAD_TIMEOUT \
        GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 2)


/**
 * Entry in the cache of responses for a given template.
 */
struct ResponseCacheEntry
{

  /**
   * Kept in a DLL.
   */
  struct ResponseCacheEntry *next;

  /**
   * Kept in a DLL.
   */
  struct ResponseCacheEntry *prev;

  /**
   * Language of the response.
   */
  char *lang;

  /**
   * True if @e paywall carries a deflate-compressed body.
   */
  bool deflate;

  /**
   * Paywall response for these request parameters.
   */
  struct MHD_Response *paywall;

  /**
   * HTTP status to return with @e paywall.
   */
  unsigned int http_status;

};


/**
 * Information about a template in the merchant backend.
 */
struct Template
{

  /**
   * Kept in a DLL.
   */
  struct Template *next;

  /**
   * Kept in a DLL.
   */
  struct Template *prev;

  /**
   * ID of the template.
   */
  char *template_id;

  /**
   * Summary of the template, NULL if not given.
   */
  char *summary;

  /**
   * Maximum pickup delay for the pages.
   */
  struct GNUNET_TIME_Relative max_pickup_delay;

  /**
   * Ways how to pay for the template.
   */
  json_t *choices;

  /**
   * Regular expression of websites the template is for.
   */
  char *regex;

  /**
   * Pre-compiled regular expression @e regex.
   */
  regex_t ex;

  /**
   * Handle used to request more information about the template.
   */
  struct TALER_MERCHANT_GetPrivateTemplateHandle *gt;

  /**
   * Number of entries in the template cache starting at @e rce_head.
   */
  unsigned int rce_length;

  /**
   * Kept in a DLL.
   */
  struct ResponseCacheEntry *rce_head;

  /**
   * Kept in a DLL.
   */
  struct ResponseCacheEntry *rce_tail;

};


/**
 * Kept in a DLL.
 */
static struct Template *t_head;

/**
 * Kept in a DLL.
 */
static struct Template *t_tail;

/**
 * Handle to get all the templates.
 */
static struct TALER_MERCHANT_GetPrivateTemplatesHandle *gpt;

/**
 * Watchdog for #TEMPLATE_LOAD_TIMEOUT, NULL once the templates are in
 * (or once we have given up on them).
 */
static struct GNUNET_SCHEDULER_Task *load_timeout_task;


/**
 * Task run when the merchant backend did not answer our template
 * queries within #TEMPLATE_LOAD_TIMEOUT.
 *
 * Treated exactly like any other failure to load the templates (an
 * unauthorized or unexpected status from the backend): the daemon
 * exits with a diagnosis instead of stalling.  Whether that policy is
 * the right one is a separate question — this task only makes sure the
 * stall is not a third, silent outcome.
 *
 * @param cls NULL
 */
static void
load_timeout (void *cls)
{
  (void) cls;
  load_timeout_task = NULL;
  GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
              "Merchant backend at `%s' did not answer our template queries"
              " within %s; giving up instead of never starting to serve\n",
              PH_merchant_base_url,
              GNUNET_STRINGS_relative_time_to_string (TEMPLATE_LOAD_TIMEOUT,
                                                      true));
  PH_global_ret = EXIT_FAILURE;
  GNUNET_SCHEDULER_shutdown ();
}


/**
 * The templates are in: stop the watchdog and open the listen sockets.
 *
 * Refuses to start if there is not a single one.  Only reachable with
 * the paywall enabled -- `-n' returns before
 * #PAIVANA_HTTPD_load_templates() is ever called -- and a paywall with
 * nothing to sell is not a paywall: PAIVANA_HTTPD_search_templates()
 * would answer "no template matched" for every URL, which create_response()
 * reads as "no paywall applies" and forwards.  The whole site would be
 * free, silently, which is exactly the outcome an operator running a
 * paywall did not ask for.  An operator who does want a plain reverse
 * proxy says so with `-n'.
 */
static void
templates_ready (void)
{
  if (NULL == t_head)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "The merchant backend at `%s' offers no paivana template;"
                " refusing to start, as every request would then be"
                " forwarded for free.  Pass -n if serving the site without"
                " a paywall is what you want.\n",
                PH_merchant_base_url);
    PH_global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (NULL != load_timeout_task)
  {
    GNUNET_SCHEDULER_cancel (load_timeout_task);
    load_timeout_task = NULL;
  }
  PAIVANA_HTTPD_serve_requests ();
}


/**
 * Check if two strings are equal, including both being NULL
 *
 * @param s1 a string, possibly NULL
 * @param s2 a string. possibly NULL
 * @return true if both are equal
 */
static bool
eq (const char *s1,
    const char *s2)
{
  if (s1 == s2)
    return true;
  if (NULL == s1)
    return false;
  if (NULL == s2)
    return false;
  return (0 == strcmp (s1,
                       s2));
}


/**
 * Create a taler://pay-template/ URI for the given @a con and @a template_id
 * and @a instance_id.
 *
 * @param merchant_base_url URL to take host and path from;
 *        we cannot take it from the MHD connection as a browser
 *        may have changed 'http' to 'https' and we MUST be consistent
 *        with what the merchant's frontend used initially
 * @param template_id the template id
 * @return corresponding taler://pay-template/ URI, or NULL on missing "host"
 */
static char *
make_taler_pay_template_uri (const char *merchant_base_url,
                             const char *template_id)
{
  struct GNUNET_Buffer buf = { 0 };
  char *url;
  struct GNUNET_Uri uri;

  url = GNUNET_strdup (merchant_base_url);
  if (-1 == GNUNET_uri_parse (&uri,
                              url))
  {
    GNUNET_break (0);
    GNUNET_free (url);
    return NULL;
  }
  if ( (NULL == uri.scheme) ||
       (NULL == uri.host) )
  {
    GNUNET_break (0);
    GNUNET_free (url);
    return NULL;
  }
  GNUNET_assert (NULL != template_id);
  GNUNET_buffer_write_str (&buf,
                           "taler");
  if (0 == strcasecmp ("http",
                       uri.scheme))
    GNUNET_buffer_write_str (&buf,
                             "+http");
  GNUNET_buffer_write_str (&buf,
                           "://pay-template/");
  GNUNET_buffer_write_str (&buf,
                           uri.host);
  if (0 != uri.port)
    GNUNET_buffer_write_fstr (&buf,
                              ":%u",
                              (unsigned int) uri.port);
  if (NULL != uri.path)
    GNUNET_buffer_write_path (&buf,
                              uri.path);
  GNUNET_buffer_write_path (&buf,
                            template_id);
  GNUNET_free (url);
  return GNUNET_buffer_reap_str (&buf);
}


/**
 * Render @a s as a complete, double-quoted JavaScript string literal.
 *
 * The paywall page carries its context in `const' declarations inside a
 * <script> element, and mustache's default escaping is the wrong
 * escaping there twice over: it escapes exactly '<', '>', '&' and '"'
 * (mustach-wrap.c), which leaves the apostrophe free to close a
 * single-quoted literal and let arbitrary JavaScript follow, and the
 * entity references it does produce are never decoded, because the HTML
 * parser does not decode them inside a raw-text element -- an '&' in the
 * merchant base URL reached the script as the literal text "&amp;".
 *
 * So the value is escaped for the context it actually lands in, and
 * interpolated with the unescaped {{{ }}} since it arrives complete with
 * its quotes.  '<', '>' and '&' are still escaped, as \\uXXXX rather
 * than as entities: without that a value containing "</script>" would
 * end the element regardless of how well the string literal itself is
 * quoted.
 *
 * @param s string to render, must be valid UTF-8
 * @return JavaScript literal including the surrounding quotes,
 *         to be freed by the caller
 */
static char *
js_string_literal (const char *s)
{
  struct GNUNET_Buffer buf = { 0 };

  GNUNET_buffer_write_str (&buf,
                           "\"");
  for (const unsigned char *p = (const unsigned char *) s;
       '\0' != *p;
       p++)
  {
    switch (*p)
    {
    case '"':
      GNUNET_buffer_write_str (&buf,
                               "\\\"");
      break;
    case '\\':
      GNUNET_buffer_write_str (&buf,
                               "\\\\");
      break;
    default:
      if ( (*p < 0x20) ||
           (0x7F == *p) ||
           ('<' == *p) ||
           ('>' == *p) ||
           ('&' == *p) )
        GNUNET_buffer_write_fstr (&buf,
                                  "\\u%04x",
                                  (unsigned int) *p);
      else
        GNUNET_buffer_write_fstr (&buf,
                                  "%c",
                                  (char) *p);
      break;
    }
  }
  GNUNET_buffer_write_str (&buf,
                           "\"");
  return GNUNET_buffer_reap_str (&buf);
}


/**
 * Number of installed `paywall.*.must' templates, counted once by
 * count_paywall_templates().  UINT_MAX until then.
 */
static unsigned int paywall_template_count = UINT_MAX;


/**
 * Count the installed paywall templates.
 *
 * TALER_TEMPLATING_build() picks among them by matching the client's
 * `Accept-Language' against the language tag in each file name, and
 * does not report which one it chose.  We do not need to know: if there
 * is only one to choose from — the shipped configuration, which ships
 * `paywall.en.must' and nothing else — then every `Accept-Language'
 * whatsoever produces the same body, and the header can be dropped from
 * the cache key entirely.
 *
 * @param cls unused
 * @param filename file found in the template directory
 * @return #GNUNET_OK to continue the scan
 */
static enum GNUNET_GenericReturnValue
count_paywall_template (void *cls,
                        const char *filename)
{
  const char *base;

  (void) cls;
  base = strrchr (filename,
                  '/');
  base = (NULL == base) ? filename : base + 1;
  if (0 == strncmp (base,
                    "paywall.",
                    strlen ("paywall.")))
    paywall_template_count++;
  return GNUNET_OK;
}


/**
 * The `Content-Security-Policy' for the paywall page, built once from
 * #PH_merchant_base_url.  NULL until first needed.
 */
static char *paywall_csp;


/**
 * Return the `Content-Security-Policy' for the paywall page.
 *
 * The page is entirely self-contained -- every stylesheet, script and
 * image is inline -- so everything but the merchant backend it polls
 * can be denied outright.  That is the part worth having: with
 * `default-src' at 'none' and `connect-src' naming exactly two origins,
 * script that does run cannot reach an attacker's host to report what
 * it found, and `frame-ancestors' keeps the taler:// link from being
 * framed and clicked by proxy.
 *
 * `script-src' still has to permit inline script: the page carries three
 * inline <script> elements and one `onclick' attribute, and neither
 * hashes nor a nonce survive our response cache, which serves one
 * rendered body to every client for five minutes.  Removing
 * 'unsafe-inline' means moving that handler into paywall.js and hashing
 * each block after rendering; worth doing, but it is not what makes the
 * injection this policy backs up impossible -- js_string_literal() is.
 *
 * @return the policy, owned by this module, or NULL if the merchant
 *         base URL cannot be parsed
 */
static const char *
get_paywall_csp (void)
{
  struct GNUNET_Buffer buf = { 0 };
  struct GNUNET_Uri uri;
  char *url;

  if (NULL != paywall_csp)
    return paywall_csp;
  url = GNUNET_strdup (PH_merchant_base_url);
  if ( (-1 == GNUNET_uri_parse (&uri,
                                url)) ||
       (NULL == uri.scheme) ||
       (NULL == uri.host) )
  {
    /* Cannot name the backend, and a policy that omits it would break
       the polling the page exists to do.  Serve without one rather than
       with a broken one; the URL is checked at startup, so this is the
       unreachable arm. */
    GNUNET_break (0);
    GNUNET_free (url);
    return NULL;
  }
  GNUNET_buffer_write_str (&buf,
                           "default-src 'none'; "
                           "script-src 'unsafe-inline'; "
                           "style-src 'unsafe-inline'; "
                           /* the QR code is drawn to a canvas and
                              handed to an <img> as a data: URL */
                           "img-src data:; "
                           "connect-src 'self' ");
  GNUNET_buffer_write_str (&buf,
                           uri.scheme);
  GNUNET_buffer_write_str (&buf,
                           "://");
  GNUNET_buffer_write_str (&buf,
                           uri.host);
  if (0 != uri.port)
    GNUNET_buffer_write_fstr (&buf,
                              ":%u",
                              (unsigned int) uri.port);
  GNUNET_buffer_write_str (&buf,
                           "; frame-ancestors 'none'"
                           "; base-uri 'none'"
                           "; form-action 'none'");
  GNUNET_free (url);
  paywall_csp = GNUNET_buffer_reap_str (&buf);
  return paywall_csp;
}


/**
 * Return the language component of the render cache key for @a conn.
 *
 * With a single installed paywall template there is nothing to
 * negotiate, so the key does not depend on `Accept-Language' at all and
 * the whole header — an unauthenticated client's free choice, on the
 * pre-payment path — stops being able to force a fresh Mustache render
 * per distinct value.  With several installed we cannot tell which one
 * the templating library picked (it has no accessor for that; the
 * proper fix belongs there), so we fall back to keying on the raw
 * header and accept the amplification for that configuration.
 *
 * @param conn connection to derive the key component for
 * @return the key component, or NULL if `Accept-Language' does not
 *         affect the rendered body
 */
static const char *
cache_key_language (struct MHD_Connection *conn)
{
  if (UINT_MAX == paywall_template_count)
  {
    char *dir;
    char *tdir;

    paywall_template_count = 0;
    dir = GNUNET_OS_installation_get_path (PAIVANA_project_data (),
                                           GNUNET_OS_IPK_DATADIR);
    GNUNET_asprintf (&tdir,
                     "%stemplates",
                     dir);
    GNUNET_free (dir);
    if (0 > GNUNET_DISK_directory_scan (tdir,
                                        &count_paywall_template,
                                        NULL))
    {
      /* Cannot tell; assume the worst and keep the old key. */
      GNUNET_break (0);
      paywall_template_count = 2;
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "%u paywall template(s) installed in `%s'\n",
                paywall_template_count,
                tdir);
    GNUNET_free (tdir);
  }
  if (2 > paywall_template_count)
    return NULL;
  return MHD_lookup_connection_value (conn,
                                      MHD_HEADER_KIND,
                                      MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
}


/**
 * Try to initialize the paywall response.
 *
 * @param conn connection to create the response for
 * @param t template to create the response for
 * @return MHD status code to return
 */
static enum MHD_Result
load_paywall (struct MHD_Connection *conn,
              struct Template *t)
{
  struct MHD_Response *reply;
  const char *lang;
  bool deflate;
  unsigned int http_status = MHD_HTTP_PAYMENT_REQUIRED;

  lang = cache_key_language (conn);
  /* `Accept-Encoding' reaches the body through exactly this predicate
     (templating_api.c), so keying on its result rather than on the
     header text is not an approximation: it is the decision itself,
     and it has two outcomes instead of unboundedly many. */
  deflate = (TALER_MHD_CT_DEFLATE ==
             TALER_MHD_can_compress (conn,
                                     TALER_MHD_CT_DEFLATE));
  for (struct ResponseCacheEntry *pos = t->rce_head;
       NULL != pos;
       pos = pos->next)
  {
    if ( (eq (lang,
              pos->lang)) &&
         (deflate == pos->deflate) )
    {
      if (t->rce_head != pos)
      {
        /* Hit, move pos to head of DLL for proper LRU eviction */
        GNUNET_CONTAINER_DLL_remove (t->rce_head,
                                     t->rce_tail,
                                     pos);
        GNUNET_CONTAINER_DLL_insert (t->rce_head,
                                     t->rce_tail,
                                     pos);
      }
      return MHD_queue_response (conn,
                                 pos->http_status,
                                 pos->paywall);
    }
  }

  {
    enum GNUNET_GenericReturnValue ret;
    json_t *data;
    char *tid_js = js_string_literal (t->template_id);
    char *mb_js = js_string_literal (PH_merchant_base_url);

    data = GNUNET_JSON_PACK (
      GNUNET_JSON_pack_string (
        "template_id",
        t->template_id),
      /* The `_js' variants are complete JavaScript string literals,
         quotes included, for the <script> block; the plain ones are for
         the HTML body, where mustache's own escaping is correct. */
      GNUNET_JSON_pack_string (
        "template_id_js",
        tid_js),
      GNUNET_JSON_pack_string (
        "merchant_backend_js",
        mb_js),
      GNUNET_JSON_pack_allow_null (
        GNUNET_JSON_pack_string (
          "summary",
          t->summary)),
      GNUNET_JSON_pack_allow_null (
        GNUNET_JSON_pack_array_incref (
          "choices",
          t->choices)),
      GNUNET_JSON_pack_bool (
        "has_choices",
        1 < json_array_size (t->choices)),
      GNUNET_JSON_pack_allow_null (
        GNUNET_JSON_pack_object_incref (
          "default_choice",
          json_array_get (t->choices, 0))),
      GNUNET_JSON_pack_uint64 (
        "max_pickup_delay",
        /* Note: 'forever' will result in a very large number
           here, that is intentional, it is equivalent and avoids
           a special case on the client-side. */
        t->max_pickup_delay.rel_value_us / 1000LLU / 1000LLU),
      GNUNET_JSON_pack_string (
        "merchant_backend",
        PH_merchant_base_url));
    GNUNET_free (tid_js);
    GNUNET_free (mb_js);
    ret = TALER_TEMPLATING_build (
      conn,
      &http_status,
      "paywall",
      NULL /* no instance */,
      NULL /* no Taler URI (needs dynamic paivana_id!) */,
      data,
      &reply);
    json_decref (data);
    if (GNUNET_NO == ret)
    {
      enum MHD_Result mret;

      /* taler_templating_lib.h: #GNUNET_NO means an *error reply* was
         built — typically because the paywall template is not
         installed where TALER_TEMPLATING_init() looked.  It is a live
         MHD_Response and it is ours: returning #MHD_YES without
         queuing it leaked it once per unauthenticated request and left
         the client waiting for a status line that never came. */
      GNUNET_break (0);
      mret = MHD_queue_response (conn,
                                 http_status,
                                 reply);
      MHD_destroy_response (reply);
      return mret;
    }
    if (GNUNET_OK != ret)
    {
      /* #GNUNET_SYSERR: no reply was built, so there is nothing to
         queue and nothing to free; MHD must close the connection. */
      GNUNET_break (0);
      return MHD_NO;
    }
  }


  GNUNET_break (MHD_YES ==
                MHD_add_response_header (reply,
                                         MHD_HTTP_HEADER_CONTENT_TYPE,
                                         "text/html"));
  /* The paywall body depends on the negotiated language and on
     whether we deflated it for the client; tell intermediaries to
     key their cache entries on both. */
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (reply,
                                         MHD_HTTP_HEADER_VARY,
                                         MHD_HTTP_HEADER_ACCEPT_LANGUAGE ", "
                                         MHD_HTTP_HEADER_ACCEPT_ENCODING ", "
                                         "Cookie"));
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (reply,
                                         MHD_HTTP_HEADER_CACHE_CONTROL,
                                         "public, max-age=300"));
  {
    const char *csp = get_paywall_csp ();

    if (NULL != csp)
      GNUNET_break (MHD_YES ==
                    MHD_add_response_header (reply,
                                             MHD_HTTP_HEADER_CONTENT_SECURITY_POLICY,
                                             csp));
  }
  /* frame-ancestors covers this for anything current; X-Frame-Options
     is for the user agents that do not implement it. */
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (reply,
                                         MHD_HTTP_HEADER_X_FRAME_OPTIONS,
                                         "DENY"));
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (reply,
                                         MHD_HTTP_HEADER_X_CONTENT_TYPE_OPTIONS,
                                         "nosniff"));
  {
    char *uri;

    uri = make_taler_pay_template_uri (PH_merchant_base_url,
                                       t->template_id);
    if (NULL != uri)
    {
      GNUNET_break (MHD_YES ==
                    MHD_add_response_header (reply,
                                             "Paivana",
                                             uri));
      GNUNET_free (uri);
    }
  }

  {
    struct ResponseCacheEntry *rce;

    /* '>=', not '>': the insert below is what takes us to the cap, so
       testing '>' left the steady state one entry above it. */
    while (t->rce_length >= MAX_RESPONSE_CACHE_ENTRIES)
    {
      /* Evict the least recently used entry; the hit path above
         promotes to the head, so the tail is the coldest. */
      struct ResponseCacheEntry *old = t->rce_tail;

      GNUNET_CONTAINER_DLL_remove (t->rce_head,
                                   t->rce_tail,
                                   old);
      GNUNET_assert (t->rce_length > 0);
      t->rce_length--;
      MHD_destroy_response (old->paywall);
      GNUNET_free (old->lang);
      GNUNET_free (old);
    }
    rce = GNUNET_new (struct ResponseCacheEntry);
    if (NULL != lang)
      rce->lang = GNUNET_strdup (lang);
    rce->deflate = deflate;
    rce->paywall = reply;
    rce->http_status = http_status;
    t->rce_length++;
    GNUNET_CONTAINER_DLL_insert (t->rce_head,
                                 t->rce_tail,
                                 rce);
    return MHD_queue_response (conn,
                               rce->http_status,
                               reply);
  }
}


/**
 * Parse template contract to (mostly) determine the
 * regex specifying which websites the template applies to.
 *
 * @param[in,out] t template to update
 * @param contract contract to parse
 * @return true on success, false on failure
 */
static bool
parse_template (struct Template *t,
                const json_t *contract)
{
  /* An absent website_regex means "every URL".  So does an empty one:
     it is what a merchant UI stores for a template that was never
     given a restriction, and anchoring it would yield "^()$", which
     matches only the empty string and therefore no URL at all -- a
     template that silently paywalls nothing. */
  const char *regex = NULL;
  const char *summary = NULL;
  const json_t *choices = NULL;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_string ("website_regex",
                               &regex),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_string ("summary",
                               &summary),
      NULL),
    GNUNET_JSON_spec_array_const ("choices",
                                  &choices),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_relative_time ("max_pickup_duration",
                                      &t->max_pickup_delay),
      NULL),
    GNUNET_JSON_spec_end ()
  };
  const char *en;

  if (GNUNET_OK !=
      GNUNET_JSON_parse ((json_t *) contract,
                         spec,
                         &en,
                         NULL))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Invalid template %s at field %s\n",
                t->template_id,
                en);
    return false;
  }
  /* GNUNET_JSON_spec_array_const() only establishes that "choices" is
     an array; what is *in* it is whatever the backend sent.
     load_paywall() hands element 0 to
     GNUNET_JSON_pack_object_incref(), which aborts on anything that is
     not an object — and it does so from an unauthenticated GET of the
     paywall page, so a backend that disagrees with us about the schema
     would turn every visitor into a crash loop.  We cross a network
     trust boundary here and must not assert on what comes back. */
  {
    size_t idx;
    json_t *choice;

    json_array_foreach ((json_t *) choices, idx, choice)
    {
      if (json_is_object (choice))
        continue;
      GNUNET_break_op (0);
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Template %s has a non-object choice at index %u\n",
                  t->template_id,
                  (unsigned int) idx);
      return false;
    }
  }
  if ( (NULL != regex) &&
       ('\0' != regex[0]) )
  {
    char *anchored;
    regex_t bare;
    int rc;

    /* Compile the merchant's expression as it stands *before* wrapping
       it, and refuse the template if that fails.  Splicing into a
       group is not a syntactic no-op: "a)|(b" becomes "^(a)|(b)$",
       which is "^a" OR "b$" -- each anchored on one side only, so the
       anchoring below stops being the guarantee the manual states.
       Every such breakout needs a parenthesis that only balances
       against the ones we add, which is exactly what a bare regcomp()
       rejects.  The merchant backend compiles the bare expression on
       POST /private/templates for the same reason; this closes the gap
       for a template that reached the database some other way.  Note
       that no expression that compiles on its own changes meaning
       here. */
    rc = regcomp (&bare,
                  regex,
                  REG_NOSUB | REG_EXTENDED);
    if (0 != rc)
    {
      GNUNET_break_op (0);
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Invalid regex in template %s: %s\n",
                  t->template_id,
                  regex);
      return false;
    }
    regfree (&bare);
    /* Anchor the merchant's expression: regexec(3) is unanchored, so
       an expression like "/premium/" would otherwise put a paywall on
       every URL merely *containing* it.  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)$",
                     regex);
    rc = regcomp (&t->ex,
                  anchored,
                  REG_NOSUB | REG_EXTENDED);
    GNUNET_free (anchored);
    if (0 != rc)
    {
      GNUNET_break_op (0);
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Invalid regex in template %s: %s\n",
                  t->template_id,
                  regex);
      return false;
    }
    t->regex = GNUNET_strdup (regex);
  }
  if (NULL != summary)
    t->summary = GNUNET_strdup (summary);
  t->choices = json_incref ((json_t *) choices);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Using payment template %s for `%s'\n",
              t->template_id,
              (NULL != t->regex) ? t->regex : "(all URLs)");
  return true;
}


/**
 * Is @a contract a template that is ours to serve?
 *
 * The merchant's `template_type' discriminator; the literals are
 * TALER_MERCHANT_template_type_from_string(), which we would call if
 * it did not live in a library paivana does not otherwise need.  Note
 * that an absent `template_type' is `fixed-order' there, so it is not
 * ours either.
 *
 * @param contract template contract from the backend
 * @return true if this is a paivana template
 */
static bool
is_paivana_template (const json_t *contract)
{
  const json_t *tt;

  tt = json_object_get (contract,
                        "template_type");
  return ( (NULL != tt) &&
           (json_is_string (tt)) &&
           (0 == strcmp ("paivana",
                         json_string_value (tt))) );
}


/**
 * Remove @a t from the list of templates and free it.
 *
 * Only for a template we decided not to use before anything was parsed
 * into it: @e gt must already be NULL and nothing but the ID
 * allocated.
 *
 * @param[in] t template to drop
 */
static void
drop_template (struct Template *t)
{
  GNUNET_assert (NULL == t->gt);
  GNUNET_assert (NULL == t->rce_head);
  GNUNET_CONTAINER_DLL_remove (t_head,
                               t_tail,
                               t);
  GNUNET_free (t->template_id);
  GNUNET_free (t);
}


/**
 * Callback for a GET /private/templates/$TEMPLATE_ID request.
 *
 * @param t template the request was about
 * @param tgr response details
 */
static void
setup_template (
  struct Template *t,
  const struct TALER_MERCHANT_GetPrivateTemplateResponse *tgr)
{
  t->gt = NULL;
  switch (tgr->hr.http_status)
  {
  case MHD_HTTP_OK:
    if (! is_paivana_template (tgr->details.ok.template_contract))
    {
      /* One merchant instance serves every kind of template, and
         `template_type' is what says which of them are ours -- as the
         manual promises.  Without this check a fixed-order template is
         handed to parse_template(), which fails it for want of
         `choices' and reports "Invalid template X at field choices":
         an error about a template that is simply none of our business,
         and (since a parse failure is fatal) one that keeps paivana
         from starting at all next to a perfectly good paivana
         template. */
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Ignoring template %s: not a paivana template\n",
                  t->template_id);
      drop_template (t);
      break;
    }
    if (! parse_template (t,
                          tgr->details.ok.template_contract))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Failed to parse template %s, refusing to start\n",
                  t->template_id);
      PH_global_ret = EXIT_FAILURE;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
    break;
  default:
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to load template %s from backend"
                " (HTTP status %u), refusing to start\n",
                t->template_id,
                tgr->hr.http_status);
    PH_global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  for (struct Template *p = t_head; NULL != p; p = p->next)
    if (NULL != p->gt)
      return;
  /* all templates done, continue with main logic */
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Templates loaded, starting to serve requests\n");
  templates_ready ();
}


/**
 * Callback for a GET /private/templates request.
 *
 * @param cls unused
 * @param tgr response details
 */
static void
check_templates (
  void *cls,
  const struct TALER_MERCHANT_GetPrivateTemplatesResponse *tgr)
{
  (void) cls;
  gpt = NULL;
  switch (tgr->hr.http_status)
  {
  case MHD_HTTP_OK:
    break;
  case MHD_HTTP_UNAUTHORIZED:
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Access to templates unauthorized: %s\n",
                TALER_ErrorCode_get_hint (tgr->hr.ec));
    PH_global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  default:
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Unexpected HTTP status code %u on GET /private/templates (%d)\n",
                tgr->hr.http_status,
                (int) tgr->hr.ec);
    PH_global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (0 == tgr->details.ok.templates_length)
  {
    templates_ready ();
    return;
  }

  for (unsigned int i = 0; i<tgr->details.ok.templates_length; i++)
  {
    const struct TALER_MERCHANT_GetPrivateTemplatesTemplateEntry *te
      = &tgr->details.ok.templates[i];
    struct Template *t;
    struct Template *before;

    t = GNUNET_new (struct Template);
    t->template_id = GNUNET_strdup (te->template_id);
    t->max_pickup_delay = GNUNET_TIME_UNIT_FOREVER_REL;
    t->gt = TALER_MERCHANT_get_private_template_create (PH_merchant_ctx,
                                                        PH_merchant_base_url,
                                                        t->template_id);
    /* Insert sorted by template ID.  Where two expressions both match a
       URL, PAIVANA_HTTPD_search_templates() quotes the price of
       whichever template it reaches first, so this list's order is a
       pricing decision.  The backend's array arrives in whatever order
       the database returned it -- the SELECT behind
       GET /private/templates has no ORDER BY -- so taking it as given
       (in either direction) makes that decision depend on nothing an
       operator can see or set, and it can differ between two restarts
       with no configuration change.  Sorting by ID is an arbitrary
       rule, but it is a rule, and it is one an operator can act on. */
    before = t_head;
    while ( (NULL != before) &&
            (0 > strcmp (before->template_id,
                         t->template_id)) )
      before = before->next;
    GNUNET_CONTAINER_DLL_insert_before (t_head,
                                        t_tail,
                                        before,
                                        t);
    GNUNET_assert (
      TALER_EC_NONE ==
      TALER_MERCHANT_get_private_template_start (t->gt,
                                                 &setup_template,
                                                 t));
  }
}


void
PAIVANA_HTTPD_load_templates ()
{
  GNUNET_assert (NULL == load_timeout_task);
  load_timeout_task
    = GNUNET_SCHEDULER_add_delayed (TEMPLATE_LOAD_TIMEOUT,
                                    &load_timeout,
                                    NULL);
  gpt = TALER_MERCHANT_get_private_templates_create (PH_merchant_ctx,
                                                     PH_merchant_base_url);
  GNUNET_assert (NULL != gpt);
  GNUNET_assert (
    TALER_EC_NONE ==
    TALER_MERCHANT_get_private_templates_start (gpt,
                                                &check_templates,
                                                NULL));
}


enum GNUNET_GenericReturnValue
PAIVANA_HTTPD_search_templates (struct MHD_Connection *connection,
                                const char *website)
{
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Searching templates for `%s'\n",
              website);
  if (PH_MAX_URL_LENGTH < strlen (website))
  {
    enum MHD_Result ret;

    /* Refuse rather than returning #GNUNET_SYSERR: that would mean
       "no paywall applies" and hand the request to the upstream for
       free. */
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Refusing to match templates against %llu byte URL\n",
                (unsigned long long) strlen (website));
    ret = TALER_MHD_reply_with_error (
      connection,
      MHD_HTTP_URI_TOO_LONG,
      TALER_EC_GENERIC_URI_TOO_LONG,
      NULL);
    return (MHD_YES == ret) ? GNUNET_OK : GNUNET_NO;
  }
  for (struct Template *t = t_head; NULL != t; t = t->next)
  {
    struct MHD_Response *redirect;
    enum MHD_Result ret;
    struct GNUNET_Buffer buf = { 0 };
    char *enc = NULL;
    char *url;

    if (NULL != t->regex)
    {
      int rc;

      rc = regexec (&t->ex,
                    website,
                    0, NULL,
                    0);
      if (REG_NOMATCH == rc)
      {
        GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                    "Request for %s did not match template %s\n",
                    website,
                    t->template_id);
        continue;
      }
      if (0 != rc)
      {
        char errbuf[128];

        /* Not "did not match": regexec(3) also reports REG_ESPACE,
           whose likelihood is a function of the merchant's pattern and
           the client's URL.  Taking the `continue' would drop this
           template from consideration, and if it were the last one the
           caller reads #GNUNET_SYSERR as "no paywall applies" and
           serves the page for free.  Fail closed instead. */
        GNUNET_break (0);
        (void) regerror (rc,
                         &t->ex,
                         errbuf,
                         sizeof (errbuf));
        GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                    "Failed to match template %s against %s: %s\n",
                    t->template_id,
                    website,
                    errbuf);
        ret = TALER_MHD_reply_with_error (
          connection,
          MHD_HTTP_INTERNAL_SERVER_ERROR,
          TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
          errbuf);
        return (MHD_YES == ret) ? GNUNET_OK : GNUNET_NO;
      }
    }

    if (! PAIVANA_HTTPD_get_base_url (connection,
                                      &buf))
    {
      GNUNET_break (0);
      GNUNET_buffer_clear (&buf);
      ret = TALER_MHD_reply_with_error (
        connection,
        MHD_HTTP_BAD_REQUEST,
        TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED,
        "Host or X-Forwarded-Host required");
      return (MHD_YES == ret) ? GNUNET_OK : GNUNET_NO;
    }
    (void) GNUNET_STRINGS_base64url_encode (website,
                                            strlen (website),
                                            &enc);
    GNUNET_buffer_write_str (&buf,
                             "/.well-known/paivana/templates/");
    GNUNET_buffer_write_str (&buf,
                             t->template_id);
    GNUNET_buffer_write_str (&buf,
                             "#");
    GNUNET_buffer_write_str (&buf,
                             enc);
    GNUNET_free (enc);
    url = GNUNET_buffer_reap_str (&buf);
    redirect = MHD_create_response_from_buffer_static (0,
                                                       NULL);
    GNUNET_assert (NULL != redirect);
    GNUNET_break (MHD_YES ==
                  MHD_add_response_header (redirect,
                                           MHD_HTTP_HEADER_LOCATION,
                                           url));
    /* Both the Location and the fragment are built from the base URL,
       which -- unless BASE_URL pins it -- comes out of the forwarding
       headers.  A cache sitting between the terminating proxy and us
       would otherwise serve one client's redirect to another virtual
       host (RFC 9110 section 12.5.5). */
    GNUNET_break (MHD_YES ==
                  MHD_add_response_header (redirect,
                                           MHD_HTTP_HEADER_VARY,
                                           (NULL != PH_base_url)
                                           ? "Cookie"
                                           : "Cookie, "
                                           MHD_HTTP_HEADER_FORWARDED ", "
                                           PH_HEADER_X_FORWARDED_PROTO ", "
                                           PH_HEADER_X_FORWARDED_HOST ", "
                                           PH_HEADER_X_FORWARDED_PORT));
    GNUNET_break (MHD_YES ==
                  MHD_add_response_header (redirect,
                                           MHD_HTTP_HEADER_CACHE_CONTROL,
                                           "public, max-age=60"));
    GNUNET_free (url);
    ret = MHD_queue_response (connection,
                              MHD_HTTP_FOUND,
                              redirect);
    MHD_destroy_response (redirect);
    return (MHD_YES == ret) ? GNUNET_OK : GNUNET_NO;
  }
  return GNUNET_SYSERR;
}


/**
 * Return the paywall page for the given @a template.
 *
 * @param connection request to search paywall response for
 * @param template template to return paywall page for
 * @return MHD status code
 */
enum MHD_Result
PAIVANA_HTTPD_return_template (struct MHD_Connection *connection,
                               const char *template)
{
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Searching template `%s'\n",
              template);
  for (struct Template *t = t_head; NULL != t; t = t->next)
  {
    if (0 == strcmp (template,
                     t->template_id))
      return load_paywall (connection,
                           t);
  }
  /* No GNUNET_break_op() here: the ID is whatever the client put in the
     path, this runs before any payment, and a stale bookmark or a
     crawler would otherwise write an ERROR-level "Assertion failed" per
     request.  A 404 is the whole answer. */
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "No template `%s', returning 404\n",
              template);
  return TALER_MHD_reply_with_error (connection,
                                     MHD_HTTP_NOT_FOUND,
                                     TALER_EC_PAIVANA_TEMPLATE_UNKNOWN,
                                     template);
}


/**
 * Unload all of the template state.
 */
void
PAIVANA_HTTPD_unload_templates ()
{
  if (NULL != load_timeout_task)
  {
    GNUNET_SCHEDULER_cancel (load_timeout_task);
    load_timeout_task = NULL;
  }
  while (NULL != t_head)
  {
    struct Template *t = t_head;

    while (NULL != t->rce_head)
    {
      struct ResponseCacheEntry *rce = t->rce_head;

      GNUNET_assert (t->rce_length > 0);
      t->rce_length--;
      GNUNET_CONTAINER_DLL_remove (t->rce_head,
                                   t->rce_tail,
                                   rce);
      MHD_destroy_response (rce->paywall);
      GNUNET_free (rce->lang);
      GNUNET_free (rce);
    }
    GNUNET_CONTAINER_DLL_remove (t_head,
                                 t_tail,
                                 t);
    if (NULL != t->gt)
      TALER_MERCHANT_get_private_template_cancel (t->gt);
    if (NULL != t->regex)
    {
      regfree (&t->ex);
      GNUNET_free (t->regex);
    }
    GNUNET_free (t->template_id);
    GNUNET_free (t->summary);
    json_decref (t->choices);
    GNUNET_free (t);
  }
  if (NULL != gpt)
  {
    TALER_MERCHANT_get_private_templates_cancel (gpt);
    gpt = NULL;
  }
  GNUNET_free (paywall_csp);
}
