/*
     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_helper.c
 * @brief helper functions
 */
#include "platform.h"
#include "paivana-httpd.h"
#include "paivana-httpd_helper.h"
#include <taler/taler_mhd_lib.h>

/**
 * Longest node identifier we keep.  RFC 7239 §6 has
 * `node = nodename [ ":" node-port ]` with
 * `nodename = ... / "[" IPv6address "]"`, so the longest one that can
 * name an address is `[` + 45 + `]` + `:` + 5 = 53 bytes -- more than
 * INET6_ADDRSTRLEN, which is why the port cannot be stripped after a
 * length check against that.
 */
#define PH_NODE_MAX 64

/**
 * Longest authority we accept in a `host` parameter or an
 * `X-Forwarded-Host`.  A DNS name is at most 253 bytes and an IPv6
 * literal with brackets and port 53; anything longer is not an
 * authority.
 */
#define PH_HOST_MAX 256

/**
 * Longest `Forwarded` parameter value we parse.  The values we care
 * about are a node, a scheme or an authority; the rest (`by`,
 * extensions) only has to fit for the element to be understood at all.
 */
#define PH_VALUE_MAX 512


/**
 * One hop of a forwarding chain, as reported by the hop to its right.
 */
struct Element
{

  /**
   * Node identifier as it appeared in the header, for logging; "" if
   * it did not fit.
   */
  char node[PH_NODE_MAX];

  /**
   * Authority from the element's `host` parameter, or "" if it had
   * none we could use.
   */
  char host[PH_HOST_MAX];

  /**
   * Binary form of @e node, in the representation the socket branch
   * produces for the same host.
   */
  unsigned char addr[sizeof (struct in6_addr)];

  /**
   * Number of bytes in @e addr; 0 if @e node names no address.
   */
  size_t addr_len;

  /**
   * Did the element carry a `proto` we understood?
   */
  bool have_proto;

  /**
   * Was that `proto` https?
   */
  bool https;
};


/**
 * Store IPv6 address @a a6 in @a out / @a out_len.
 *
 * An IPv4-mapped address (::ffff:a.b.c.d) is unwrapped to the four
 * bytes a plain IPv4 peer would have yielded: it names the same host,
 * and the two spellings must not produce two identities.
 *
 * @param a6 address to store
 * @param[out] out buffer of at least `sizeof (struct in6_addr)` bytes
 * @param[out] out_len set to the number of bytes written to @a out
 */
static void
pack_v6 (const struct in6_addr *a6,
         unsigned char *out,
         size_t *out_len)
{
  if (IN6_IS_ADDR_V4MAPPED (a6))
  {
    memcpy (out,
            &a6->s6_addr[12],
            sizeof (struct in_addr));
    *out_len = sizeof (struct in_addr);
    return;
  }
  memcpy (out,
          a6,
          sizeof (*a6));
  *out_len = sizeof (*a6);
}


/**
 * Is @a c a `tchar`, i.e. legal in a token (RFC 9110 §5.6.2)?
 *
 * @param c character to test
 * @return true if @a c may appear in a token
 */
static bool
is_tchar (char c)
{
  if ('\0' == c)
    return false;
  return ( (c >= 'a') && (c <= 'z') ) ||
         ( (c >= 'A') && (c <= 'Z') ) ||
         ( (c >= '0') && (c <= '9') ) ||
         (NULL != strchr ("!#$%&'*+-.^_`|~",
                          c));
}


/**
 * May @a c appear in an unquoted RFC 7239 §4 `value`?
 *
 * The grammar says `token`, but proxies in the field -- the Apache
 * snippet we ship among them -- emit an unquoted authority, and `:`
 * is not a `tchar`.  Refusing those outright would throw away the
 * whole chain a correctly configured front server sent, so anything
 * that is not a delimiter, whitespace or a control character is read
 * as part of the value.  That keeps the element boundaries exactly
 * where the grammar puts them, which is the part that matters; whether
 * the value is one we can *use* is then decided per parameter.
 *
 * @param c character to test
 * @return true if @a c continues an unquoted value
 */
static bool
is_value_char (char c)
{
  unsigned char u = (unsigned char) c;

  return (u > 0x20) &&
         (0x7F != u) &&
         (';' != u) &&
         (',' != u) &&
         ('"' != u);
}


/**
 * Is @a c a `qdtext`, i.e. legal unescaped inside a quoted-string
 * (RFC 9110 §5.6.4)?
 *
 * @param c character to test
 * @return true if @a c may appear unescaped
 */
static bool
is_qdtext (char c)
{
  unsigned char u = (unsigned char) c;

  return ('\t' == u) ||
         (' ' == u) ||
         (0x21 == u) ||
         ( (u >= 0x23) && (u <= 0x5B) ) ||
         ( (u >= 0x5D) && (u <= 0x7E) ) ||
         (u >= 0x80);
}


/**
 * May @a c follow a backslash in a quoted-pair (RFC 9110 §5.6.4)?
 *
 * @param c character to test
 * @return true if @a c may be escaped
 */
static bool
is_escapable (char c)
{
  unsigned char u = (unsigned char) c;

  return ('\t' == u) ||
         ( (u >= 0x20) && (u <= 0x7E) ) ||
         (u >= 0x80);
}


/**
 * Skip optional whitespace (RFC 9110 §5.6.3).
 *
 * @param p position to start at
 * @return first position that is neither SP nor HTAB
 */
static const char *
skip_ows (const char *p)
{
  while ( (' ' == *p) ||
          ('\t' == *p) )
    p++;
  return p;
}


/**
 * Read one RFC 7239 §4 `value` -- a token or a quoted-string -- from
 * @a pp into @a out, advancing @a pp past it.
 *
 * The quoted-string is unescaped here, which is the only place that
 * may happen: a caller handed the raw bytes could not tell a `,` that
 * was data from one that was a delimiter.
 *
 * @param[in,out] pp position to read from; advanced past the value
 * @param[out] out buffer for the unescaped value
 * @param out_size number of bytes in @a out
 * @return true if a well-formed value was read
 */
static bool
parse_value (const char **pp,
             char *out,
             size_t out_size)
{
  const char *p = *pp;
  size_t len = 0;

  if ('"' == *p)
  {
    p++;
    while ('"' != *p)
    {
      char c = *p;

      if ('\0' == c)
      {
        /* An unterminated quoted-string is not a value at all, and
           reading on would run off the end of the header. */
        GNUNET_break_op (0);
        return false;
      }
      if ('\\' == c)
      {
        c = p[1];
        if (! is_escapable (c))
        {
          GNUNET_break_op (0);
          return false;
        }
        p++;
      }
      else if (! is_qdtext (c))
      {
        GNUNET_break_op (0);
        return false;
      }
      if (len + 1 >= out_size)
      {
        GNUNET_break_op (0);
        return false;
      }
      out[len++] = c;
      p++;
    }
    p++; /* closing DQUOTE */
  }
  else
  {
    while (is_value_char (*p))
    {
      if (len + 1 >= out_size)
      {
        GNUNET_break_op (0);
        return false;
      }
      out[len++] = *p++;
    }
    if (0 == len)
    {
      /* `for=` with nothing after it is not a `value`. */
      GNUNET_break_op (0);
      return false;
    }
  }
  out[len] = '\0';
  *pp = p;
  return true;
}


/**
 * Parse the RFC 7239 §6 node identifier @a node into binary form.
 *
 * What we return has to be the *same bytes* the socket branch would
 * have produced for this client, or the cookie MAC -- which covers the
 * client address -- silently stops matching as soon as a request
 * arrives without the header, or through a proxy that spells the
 * address differently ("::1" and "0:0:0:0:0:0:0:1" are one host).
 * Hence the address is parsed into its binary form rather than carried
 * around as text.
 *
 * Brackets and any port are removed first: a port is not part of the
 * identity of a host and the cookie must not depend on it.  A node
 * that is not an address at all -- the `unknown` of §6.3, an
 * obfuscated identifier, a hostname -- has no such form and is
 * reported as naming none.
 *
 * @param node identifier to parse
 * @param[out] addr buffer of at least `sizeof (struct in6_addr)` bytes
 * @param[out] addr_len set to the number of bytes written, 0 if
 *   @a node names no address
 */
static void
node_to_addr (const char *node,
              unsigned char *addr,
              size_t *addr_len)
{
  char tmp[PH_NODE_MAX];
  const char *a;
  size_t len = strlen (node);
  struct in_addr a4;
  struct in6_addr a6;

  *addr_len = 0;
  if ( (0 == len) ||
       (len >= sizeof (tmp)) )
    return;
  memcpy (tmp,
          node,
          len + 1);
  if ('[' == tmp[0])
  {
    char *close = strchr (tmp,
                          ']');

    if (NULL == close)
    {
      /* RFC 7239 §6 requires the closing bracket. */
      GNUNET_break_op (0);
      return;
    }
    *close = '\0';
    if ( ('\0' != close[1]) &&
         (':' != close[1]) )
    {
      GNUNET_break_op (0);
      return;
    }
    a = &tmp[1];
  }
  else
  {
    char *colon = strchr (tmp,
                          ':');

    /* Only a *single* colon can be a port separator; more than one
       means this is a bare IPv6 address, which RFC 7239 requires to
       be bracketed but which we accept anyway. */
    if ( (NULL != colon) &&
         (NULL == strchr (colon + 1,
                          ':')) )
      *colon = '\0';
    a = tmp;
  }
  if (1 == inet_pton (AF_INET,
                      a,
                      &a4))
  {
    memcpy (addr,
            &a4,
            sizeof (a4));
    *addr_len = sizeof (a4);
    return;
  }
  if (1 == inet_pton (AF_INET6,
                      a,
                      &a6))
    pack_v6 (&a6,
             addr,
             addr_len);
}


/**
 * Is @a c legal in an RFC 3986 §3.2.2 `reg-name` as we accept them?
 *
 * Deliberately narrower than the grammar: percent-encoding and the
 * sub-delims have no business in a host we are about to concatenate
 * into the string the access cookie is keyed on and the templates'
 * regular expressions are matched against.
 *
 * @param c character to test
 * @return true if @a c may appear in a host name
 */
static bool
is_host_char (char c)
{
  return ( (c >= 'a') && (c <= 'z') ) ||
         ( (c >= 'A') && (c <= 'Z') ) ||
         ( (c >= '0') && (c <= '9') ) ||
         ('-' == c) ||
         ('.' == c) ||
         ('_' == c);
}


/**
 * Is @a p an RFC 3986 §3.2.3 port naming one that can be connected to?
 *
 * @param p text to check
 * @return true if @a p is a usable port number
 */
static bool
valid_port_text (const char *p)
{
  unsigned long v = 0;
  size_t len = strlen (p);

  if ( (0 == len) ||
       (len > 5) )
    return false;
  for (size_t i = 0; i < len; i++)
  {
    if ( (p[i] < '0') ||
         (p[i] > '9') )
      return false;
    v = v * 10 + (unsigned long) (p[i] - '0');
  }
  return ( (v > 0) &&
           (v < 65536) );
}


/**
 * Is @a h an authority we are willing to build a base URL from, i.e.
 * an RFC 3986 §3.2 `host [ ":" port ]`?
 *
 * @param h text to check
 * @return true if @a h is such an authority
 */
static bool
valid_host (const char *h)
{
  size_t len = strlen (h);
  const char *port;

  if ( (0 == len) ||
       (len >= PH_HOST_MAX) )
    return false;
  if ('[' == h[0])
  {
    /* RFC 3986 §3.2.2: an IPv6 literal is bracketed, and RFC 5952 §6
       asks for exactly that spelling in a URI. */
    char tmp[INET6_ADDRSTRLEN];
    const char *close = strchr (h,
                                ']');
    struct in6_addr a6;
    size_t hl;

    if (NULL == close)
      return false;
    hl = (size_t) (close - h) - 1;
    if ( (0 == hl) ||
         (hl >= sizeof (tmp)) )
      return false;
    memcpy (tmp,
            h + 1,
            hl);
    tmp[hl] = '\0';
    if (1 != inet_pton (AF_INET6,
                        tmp,
                        &a6))
      return false;
    if ('\0' == close[1])
      return true;
    if (':' != close[1])
      return false;
    port = close + 2;
  }
  else
  {
    const char *colon = strchr (h,
                                ':');
    size_t hl = (NULL != colon)
      ? (size_t) (colon - h)
      : len;

    if (0 == hl)
      return false;
    for (size_t i = 0; i < hl; i++)
      if (! is_host_char (h[i]))
        return false;
    if (NULL == colon)
      return true;
    port = colon + 1;
  }
  return valid_port_text (port);
}


/**
 * Does the authority @a h already carry a port?
 *
 * @param h authority that passed #valid_host()
 * @return true if a port is present
 */
static bool
host_has_port (const char *h)
{
  const char *p = ('[' == h[0])
    ? strchr (h,
              ']')
    : h;

  if (NULL == p)
    return false;
  return (NULL != strchr (p,
                          ':'));
}


/**
 * Parse one RFC 7239 §4 `forwarded-element` from @a pp into @a e,
 * advancing @a pp to the `,` or NUL that ended it.
 *
 * @param[in,out] pp position to read from
 * @param[out] e element to fill in
 * @return true if the element was well-formed
 */
static bool
parse_forwarded_element (const char **pp,
                         struct Element *e)
{
  const char *p = *pp;
  bool have_for = false;
  bool have_host = false;
  bool have_proto = false;

  memset (e,
          0,
          sizeof (*e));
  while (true)
  {
    char name[32];
    char value[PH_VALUE_MAX];
    size_t nlen = 0;

    p = skip_ows (p);
    if (';' == *p)
    {
      /* RFC 7239 §4 permits an empty forwarded-pair. */
      p++;
      continue;
    }
    if ( (',' == *p) ||
         ('\0' == *p) )
      break;
    while (is_tchar (*p))
    {
      if (nlen + 1 >= sizeof (name))
      {
        GNUNET_break_op (0);
        return false;
      }
      name[nlen++] = *p++;
    }
    if (0 == nlen)
    {
      GNUNET_break_op (0);
      return false;
    }
    name[nlen] = '\0';
    p = skip_ows (p);
    if ('=' != *p)
    {
      GNUNET_break_op (0);
      return false;
    }
    p++;
    p = skip_ows (p);
    if (! parse_value (&p,
                       value,
                       sizeof (value)))
      return false;
    /* RFC 7239 §4: "Each parameter MUST NOT occur more than once per
       field-value."  A second one would leave which of the two we
       believe up to the direction we happen to scan in. */
    if (0 == strcasecmp (name,
                         "for"))
    {
      if (have_for)
      {
        GNUNET_break_op (0);
        return false;
      }
      have_for = true;
      if (strlen (value) < sizeof (e->node))
        memcpy (e->node,
                value,
                strlen (value) + 1);
      node_to_addr (value,
                    e->addr,
                    &e->addr_len);
    }
    else if (0 == strcasecmp (name,
                              "host"))
    {
      if (have_host)
      {
        GNUNET_break_op (0);
        return false;
      }
      have_host = true;
      if (valid_host (value))
        memcpy (e->host,
                value,
                strlen (value) + 1);
      else
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Ignoring unusable Forwarded host `%s'\n",
                    value);
    }
    else if (0 == strcasecmp (name,
                              "proto"))
    {
      if (have_proto)
      {
        GNUNET_break_op (0);
        return false;
      }
      have_proto = true;
      /* Anything but the two schemes we can serve would end up in a
         Location header and in the string the cookie is keyed on. */
      if (0 == strcasecmp (value,
                           "https"))
      {
        e->have_proto = true;
        e->https = true;
      }
      else if (0 == strcasecmp (value,
                                "http"))
      {
        e->have_proto = true;
      }
      else
      {
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Ignoring unusable Forwarded proto `%s'\n",
                    value);
      }
    }
    /* `by` and extension parameters are none of our business, but had
       to be parsed to find the end of the element. */
    p = skip_ows (p);
    if (';' != *p)
      break;
    p++;
  }
  if ( (',' != *p) &&
       ('\0' != *p) )
  {
    GNUNET_break_op (0);
    return false;
  }
  *pp = p;
  return true;
}


/**
 * Was this header present at all?
 *
 * @param lines NULL-terminated array of field line values, or NULL
 * @return true if there is at least one field line
 */
static bool
have_lines (const char *const *lines)
{
  return ( (NULL != lines) &&
           (NULL != lines[0]) );
}


/**
 * Parse every `Forwarded` field line of @a lines into @a el, in order.
 *
 * RFC 9110 §5.3 makes repeated field lines of a list-based field one
 * list, and RFC 7239 §4 explicitly blesses a proxy adding a field line
 * of its own instead of extending the last one -- so reading only the
 * first line would let a client's element outrank the trusted proxy's.
 *
 * @param lines NULL-terminated array of field line values
 * @param[out] el array of #PAIVANA_HTTPD_MAX_FORWARDED_ELEMENTS
 *   elements to fill in
 * @return number of elements parsed, 0 if the header is unusable
 */
static unsigned int
parse_forwarded_lines (const char *const *lines,
                       struct Element *el)
{
  unsigned int n = 0;

  for (unsigned int i = 0; NULL != lines[i]; i++)
  {
    const char *p = lines[i];

    while (true)
    {
      p = skip_ows (p);
      if ('\0' == *p)
        break;
      if (',' == *p)
      {
        /* RFC 9110 §5.6.1.2: an empty list element is to be ignored. */
        p++;
        continue;
      }
      if (PAIVANA_HTTPD_MAX_FORWARDED_ELEMENTS == n)
      {
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Forwarded chain longer than %u elements; ignoring it\n",
                    (unsigned int) PAIVANA_HTTPD_MAX_FORWARDED_ELEMENTS);
        GNUNET_break_op (0);
        return 0;
      }
      if (! parse_forwarded_element (&p,
                                     &el[n]))
      {
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Malformed Forwarded header; ignoring it\n");
        return 0;
      }
      n++;
      if (',' == *p)
        p++;
    }
  }
  return n;
}


/**
 * Parse every `X-Forwarded-For` field line of @a lines into @a el, in
 * order.
 *
 * The de-facto header has no grammar beyond "comma-separated
 * addresses"; an element that is not a bare address has no binary form
 * and is recorded as naming none.
 *
 * @param lines NULL-terminated array of field line values
 * @param[out] el array of #PAIVANA_HTTPD_MAX_FORWARDED_ELEMENTS
 *   elements to fill in
 * @return number of elements parsed, 0 if the header is unusable
 */
static unsigned int
parse_xff_lines (const char *const *lines,
                 struct Element *el)
{
  unsigned int n = 0;

  for (unsigned int i = 0; NULL != lines[i]; i++)
  {
    const char *p = lines[i];

    while (true)
    {
      const char *comma;
      struct Element *e;
      size_t len;

      p = skip_ows (p);
      comma = strchr (p,
                      ',');
      len = (NULL != comma)
        ? (size_t) (comma - p)
        : strlen (p);
      while ( (len > 0) &&
              ( (' ' == p[len - 1]) ||
                ('\t' == p[len - 1]) ) )
        len--;
      if (0 == len)
      {
        if (NULL == comma)
          break;
        p = comma + 1;
        continue;
      }
      if (PAIVANA_HTTPD_MAX_FORWARDED_ELEMENTS == n)
      {
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "X-Forwarded-For chain longer than %u elements;"
                    " ignoring it\n",
                    (unsigned int) PAIVANA_HTTPD_MAX_FORWARDED_ELEMENTS);
        GNUNET_break_op (0);
        return 0;
      }
      e = &el[n++];
      memset (e,
              0,
              sizeof (*e));
      if (len < sizeof (e->node))
      {
        struct in_addr a4;
        struct in6_addr a6;

        memcpy (e->node,
                p,
                len);
        e->node[len] = '\0';
        if (1 == inet_pton (AF_INET,
                            e->node,
                            &a4))
        {
          memcpy (e->addr,
                  &a4,
                  sizeof (a4));
          e->addr_len = sizeof (a4);
        }
        else if (1 == inet_pton (AF_INET6,
                                 e->node,
                                 &a6))
        {
          pack_v6 (&a6,
                   e->addr,
                   &e->addr_len);
        }
      }
      if (0 == e->addr_len)
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "X-Forwarded-For element `%.*s' is not a bare address\n",
                    (int) len,
                    p);
      if (NULL == comma)
        break;
      p = comma + 1;
    }
  }
  return n;
}


bool
PAIVANA_HTTPD_is_trusted_proxy (const void *ca,
                                size_t ca_len)
{
  if (! PH_have_trusted_proxies)
    return false;
  if (sizeof (struct in_addr) == ca_len)
  {
    const struct in_addr *a4 = ca;

    if (NULL == PH_trusted_proxies4)
      return false;
    /* The list is terminated by an all-zero entry; GNUnet does not
       hand out a count.  See load_trusted_proxies(). */
    for (unsigned int i = 0;
         0 != PH_trusted_proxies4[i].network.s_addr;
         i++)
      if ( (a4->s_addr & PH_trusted_proxies4[i].netmask.s_addr) ==
           (PH_trusted_proxies4[i].network.s_addr &
            PH_trusted_proxies4[i].netmask.s_addr) )
        return true;
    return false;
  }
  if (sizeof (struct in6_addr) == ca_len)
  {
    const struct in6_addr *a6 = ca;

    if (NULL == PH_trusted_proxies6)
      return false;
    for (unsigned int i = 0;
         ! GNUNET_is_zero (&PH_trusted_proxies6[i].network);
         i++)
    {
      const struct in6_addr *net = &PH_trusted_proxies6[i].network;
      const struct in6_addr *mask = &PH_trusted_proxies6[i].netmask;
      bool match = true;

      for (unsigned int j = 0; j < sizeof (struct in6_addr); j++)
        if ( (a6->s6_addr[j] & mask->s6_addr[j]) !=
             (net->s6_addr[j] & mask->s6_addr[j]) )
        {
          match = false;
          break;
        }
      if (match)
        return true;
    }
    return false;
  }
  GNUNET_break (0);
  return false;
}


/**
 * Walk the chain @a el from the right and return the index of the
 * element that speaks for the client.
 *
 * We arrive here already standing on the peer we accepted from, which
 * is trusted because `-f` says something in front of us is.  Each step
 * leftwards is permitted only by the node we are stepping over: that
 * node wrote the element to its left, so unless *it* is one of ours,
 * that element is hearsay.  The first node we may not step over is
 * therefore as far back as the chain can be believed, and is the
 * client.
 *
 * @param el chain, leftmost element first
 * @param n number of elements in @a el, at least 1
 * @return index into @a el
 */
static unsigned int
select_element (const struct Element *el,
                unsigned int n)
{
  for (unsigned int i = n; i > 1; i--)
  {
    const struct Element *e = &el[i - 1];

    if (0 == e->addr_len)
      return i - 1; /* nothing further left is reachable */
    if (! PAIVANA_HTTPD_is_trusted_proxy (e->addr,
                                          e->addr_len))
      return i - 1; /* the client */
  }
  /* Every hop is a proxy we trust; the leftmost is all we have. */
  return 0;
}


/**
 * Copy the leftmost element of the first field line of @a lines into
 * @a out, with whitespace stripped.
 *
 * The de-facto `X-Forwarded-*` headers other than `-For` are
 * single-valued in practice, but a chain of proxies may still have
 * turned one into a list; its leftmost element is the one describing
 * the client's own request.
 *
 * @param lines NULL-terminated array of field line values, or NULL
 * @param[out] out buffer for the value
 * @param out_size number of bytes in @a out
 * @return true if a non-empty value was found
 */
static bool
first_value (const char *const *lines,
             char *out,
             size_t out_size)
{
  const char *p;
  const char *comma;
  size_t len;

  if (! have_lines (lines))
    return false;
  p = skip_ows (lines[0]);
  comma = strchr (p,
                  ',');
  len = (NULL != comma)
    ? (size_t) (comma - p)
    : strlen (p);
  while ( (len > 0) &&
          ( (' ' == p[len - 1]) ||
            ('\t' == p[len - 1]) ) )
    len--;
  if ( (0 == len) ||
       (len >= out_size) )
    return false;
  memcpy (out,
          p,
          len);
  out[len] = '\0';
  return true;
}


void
PAIVANA_HTTPD_client_clear (struct PAIVANA_HTTPD_Client *cl)
{
  GNUNET_free (cl->ca);
  GNUNET_free (cl->proto);
  GNUNET_free (cl->host);
  memset (cl,
          0,
          sizeof (*cl));
}


bool
PAIVANA_HTTPD_resolve_forwarding (const struct PAIVANA_HTTPD_Forwarding *fi,
                                  struct PAIVANA_HTTPD_Client *cl)
{
  struct Element el[PAIVANA_HTTPD_MAX_FORWARDED_ELEMENTS];
  unsigned int n = 0;
  bool from_forwarded = false;

  memset (cl,
          0,
          sizeof (*cl));
  if (fi->respect_forwarded)
  {
    /* RFC 7239 is the standardized form and says more than the
       de-facto headers do, so it wins where both are present -- and
       the reverse-proxy configurations we ship set it.  A proxy that
       emits both should agree with itself; if it does not, we would
       rather be predictable than clever. */
    if (have_lines (fi->forwarded))
    {
      if (have_lines (fi->xff))
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Both Forwarded and X-Forwarded-For present; using"
                    " Forwarded\n");
      from_forwarded = true;
      n = parse_forwarded_lines (fi->forwarded,
                                 el);
    }
    else if (have_lines (fi->xff))
    {
      n = parse_xff_lines (fi->xff,
                           el);
    }
  }
  if (0 != n)
  {
    unsigned int sel = select_element (el,
                                       n);

    if (0 != el[sel].addr_len)
    {
      cl->ca = GNUNET_memdup (el[sel].addr,
                              el[sel].addr_len);
      cl->ca_len = el[sel].addr_len;
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Client address is based on %s: `%s'\n",
                  from_forwarded
                  ? MHD_HTTP_HEADER_FORWARDED
                  : PH_HEADER_X_FORWARDED_FOR,
                  el[sel].node);
    }
    else
    {
      /* RFC 7239 §6.3 `unknown`, an obfuscated identifier, a name we
         cannot turn into bytes: legal, but nothing to bind a cookie
         to.  The peer is what we know for certain, and a header the
         client controls must never be able to take that away. */
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Forwarding chain names no client address at `%s';"
                  " falling back to the socket peer\n",
                  el[sel].node);
    }
    if (from_forwarded)
    {
      /* Same element, same author: the hop that reported this node
         also reported the connection its predecessor made to it. */
      if (el[sel].have_proto)
        cl->proto = GNUNET_strdup (el[sel].https
                                   ? "https"
                                   : "http");
      if ('\0' != el[sel].host[0])
        cl->host = GNUNET_strdup (el[sel].host);
    }
  }
  if ( (NULL == cl->ca) &&
       (NULL != fi->peer) &&
       (0 != fi->peer_len) )
  {
    cl->ca = GNUNET_memdup (fi->peer,
                            fi->peer_len);
    cl->ca_len = fi->peer_len;
  }
  if (fi->respect_forwarded)
  {
    char buf[PH_HOST_MAX];

    if ( (NULL == cl->proto) &&
         (first_value (fi->xfp,
                       buf,
                       sizeof (buf))) )
    {
      if (0 == strcasecmp (buf,
                           "https"))
        cl->proto = GNUNET_strdup ("https");
      else if (0 == strcasecmp (buf,
                                "http"))
        cl->proto = GNUNET_strdup ("http");
      else
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Ignoring unusable %s value `%s'\n",
                    PH_HEADER_X_FORWARDED_PROTO,
                    buf);
    }
    if ( (NULL == cl->host) &&
         (first_value (fi->xfh,
                       buf,
                       sizeof (buf))) )
    {
      if (valid_host (buf))
        cl->host = GNUNET_strdup (buf);
      else
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Ignoring unusable %s value `%s'\n",
                    PH_HEADER_X_FORWARDED_HOST,
                    buf);
    }
    /* An authority carries at most one port.  nginx's
       `X-Forwarded-Host $http_host` includes one and its
       `X-Forwarded-Port` repeats it; appending regardless would yield
       "example.com:8443:8443", which is no authority at all. */
    if ( (NULL != cl->host) &&
         (! host_has_port (cl->host)) &&
         (first_value (fi->xfport,
                       buf,
                       sizeof (buf))) )
    {
      if (! valid_port_text (buf))
      {
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Ignoring unusable %s value `%s'\n",
                    PH_HEADER_X_FORWARDED_PORT,
                    buf);
      }
      else
      {
        unsigned int port = (unsigned int) strtoul (buf,
                                                    NULL,
                                                    10);
        bool https = ( (NULL != cl->proto) &&
                       (0 == strcmp (cl->proto,
                                     "https")) );

        /* Re-rendered rather than echoed: "0443" is a valid spelling
           of 443 and must not reach the URL as itself.  The default
           port for the scheme is left off, as RFC 3986 §6.2.3 asks. */
        if (port != (https ? 443U : 80U))
        {
          char *hp;

          GNUNET_asprintf (&hp,
                           "%s:%u",
                           cl->host,
                           port);
          GNUNET_free (cl->host);
          cl->host = hp;
        }
      }
    }
  }
  return (NULL != cl->ca);
}


char *
PAIVANA_HTTPD_forwarded_param (const char *fwd,
                               const char *name)
{
  struct Element el[PAIVANA_HTTPD_MAX_FORWARDED_ELEMENTS];
  const char *lines[] = {
    fwd,
    NULL
  };

  if (0 == parse_forwarded_lines (lines,
                                  el))
    return NULL;
  /* The leftmost element describes the connection the client itself
     made, which is what `proto` and `host` are being asked about.
     With a single proxy in front -- the ordinary case -- there is only
     one element and the question does not arise. */
  if (0 == strcasecmp (name,
                       "proto"))
    return el[0].have_proto
           ? GNUNET_strdup (el[0].https
                            ? "https"
                            : "http")
           : NULL;
  if (0 == strcasecmp (name,
                       "host"))
    return ('\0' != el[0].host[0])
           ? GNUNET_strdup (el[0].host)
           : NULL;
  GNUNET_break (0); /* no other parameter is validated here */
  return NULL;
}


char *
PAIVANA_HTTPD_forwarded_value (const char *v)
{
  struct GNUNET_Buffer buf = { 0 };
  bool token = ('\0' != v[0]);

  for (const char *p = v; '\0' != *p; p++)
    if (! is_tchar (*p))
    {
      token = false;
      break;
    }
  if (token)
    return GNUNET_strdup (v);
  GNUNET_buffer_write (&buf,
                       "\"",
                       1);
  for (const char *p = v; '\0' != *p; p++)
  {
    if ( ('"' == *p) ||
         ('\\' == *p) )
    {
      GNUNET_buffer_write (&buf,
                           "\\",
                           1);
    }
    else if (! is_qdtext (*p))
    {
      /* A control character has no quoted-pair either, so there is no
         way to say this at all. */
      GNUNET_break_op (0);
      GNUNET_buffer_clear (&buf);
      return NULL;
    }
    GNUNET_buffer_write (&buf,
                         p,
                         1);
  }
  GNUNET_buffer_write (&buf,
                       "\"",
                       1);
  return GNUNET_buffer_reap_str (&buf);
}


char *
PAIVANA_HTTPD_forwarded_node (const void *ca,
                              size_t ca_len)
{
  char buf[INET6_ADDRSTRLEN];
  char bracketed[INET6_ADDRSTRLEN + 2];

  if ( (NULL == ca) ||
       (0 == ca_len) )
  {
    /* RFC 7239 §6.3 provides exactly this for a hop whose predecessor
       has no address we can name -- a Unix-domain peer, here. */
    return GNUNET_strdup ("unknown");
  }
  if (sizeof (struct in_addr) == ca_len)
  {
    GNUNET_assert (NULL != inet_ntop (AF_INET,
                                      ca,
                                      buf,
                                      sizeof (buf)));
    return PAIVANA_HTTPD_forwarded_value (buf);
  }
  GNUNET_assert (sizeof (struct in6_addr) == ca_len);
  GNUNET_assert (NULL != inet_ntop (AF_INET6,
                                    ca,
                                    buf,
                                    sizeof (buf)));
  /* RFC 7239 §6: an IPv6 identifier is bracketed, and the brackets
     force the whole thing to be a quoted-string. */
  GNUNET_snprintf (bracketed,
                   sizeof (bracketed),
                   "[%s]",
                   buf);
  return PAIVANA_HTTPD_forwarded_value (bracketed);
}


char *
PAIVANA_HTTPD_forwarded_for_chain (const char *fwd)
{
  struct Element el[PAIVANA_HTTPD_MAX_FORWARDED_ELEMENTS];
  struct GNUNET_Buffer buf = { 0 };
  const char *lines[] = {
    fwd,
    NULL
  };
  unsigned int n;

  n = parse_forwarded_lines (lines,
                             el);
  if (0 == n)
    return NULL;
  for (unsigned int i = 0; i < n; i++)
  {
    char node[INET6_ADDRSTRLEN];

    /* All or nothing: X-Forwarded-For has no way to say "this hop had
       no address", so an element whose `for` is RFC 7239's "unknown",
       an obfuscated identifier, or anything else that is not an
       address cannot be represented.  Dropping it would silently
       shift every position to its left, which is worse than declining
       to translate the header at all. */
    if (0 == el[i].addr_len)
    {
      GNUNET_buffer_clear (&buf);
      return NULL;
    }
    GNUNET_assert (NULL !=
                   inet_ntop ( (sizeof (struct in_addr) == el[i].addr_len)
                               ? AF_INET
                               : AF_INET6,
                               el[i].addr,
                               node,
                               sizeof (node)));
    if (0 != i)
      GNUNET_buffer_write_str (&buf,
                               ", ");
    GNUNET_buffer_write_str (&buf,
                             node);
  }
  return GNUNET_buffer_reap_str (&buf);
}


/**
 * Store the address of the peer we accepted @a connection from.
 *
 * @param connection HTTP client connection
 * @param[out] addr buffer of at least `sizeof (struct in6_addr)` bytes
 * @param[out] addr_len set to the number of bytes written, 0 for a
 *   peer that has no IP address
 */
static void
socket_address (struct MHD_Connection *connection,
                unsigned char *addr,
                size_t *addr_len)
{
  const union MHD_ConnectionInfo *ci;
  const struct sockaddr *sa;

  *addr_len = 0;
  ci = MHD_get_connection_info (connection,
                                MHD_CONNECTION_INFO_CLIENT_ADDRESS);
  if ( (NULL == ci) ||
       (NULL == ci->client_addr) )
  {
    /* MHD documents this as NULL-able -- it returns NULL when the
       connection carries no address at all -- and no reachable case
       was found.  Still: "a peer with no address" is a case every
       caller of ours already handles (it is the AF_UNIX case), so
       aborting the single-process daemon over it would turn a
       hypothetical into an outage. */
    GNUNET_break (0);
    return;
  }
  sa = ci->client_addr;
  switch (sa->sa_family)
  {
  case AF_INET:
    memcpy (addr,
            &((const struct sockaddr_in *) sa)->sin_addr,
            sizeof (struct in_addr));
    *addr_len = sizeof (struct in_addr);
    return;
  case AF_INET6:
    /* A dual-stack listener hands us ::ffff:a.b.c.d for an IPv4
       peer; pack_v6() folds that back to the IPv4 form. */
    pack_v6 (&((const struct sockaddr_in6 *) sa)->sin6_addr,
             addr,
             addr_len);
    return;
  default:
    /* AF_UNIX: no address exists. */
    return;
  }
}


/**
 * The field lines of the forwarding headers of one request, as
 * collected from MHD.
 */
struct HeaderLines
{

  /**
   * `Forwarded` field lines, NULL-terminated once collected.
   */
  const char **forwarded;

  /**
   * `X-Forwarded-For` field lines.
   */
  const char **xff;

  /**
   * `X-Forwarded-Proto` field lines.
   */
  const char **xfp;

  /**
   * `X-Forwarded-Host` field lines.
   */
  const char **xfh;

  /**
   * `X-Forwarded-Port` field lines.
   */
  const char **xfport;

  /**
   * Number of entries in @e forwarded.
   */
  unsigned int forwarded_len;

  /**
   * Number of entries in @e xff.
   */
  unsigned int xff_len;

  /**
   * Number of entries in @e xfp.
   */
  unsigned int xfp_len;

  /**
   * Number of entries in @e xfh.
   */
  unsigned int xfh_len;

  /**
   * Number of entries in @e xfport.
   */
  unsigned int xfport_len;
};


/**
 * Collect one header field line into the arrays of @a cls.
 *
 * @param cls a `struct HeaderLines *`
 * @param kind header kind, always #MHD_HEADER_KIND here
 * @param key field name
 * @param value field line value
 * @return #MHD_YES to keep iterating
 */
static enum MHD_Result
collect_header (void *cls,
                enum MHD_ValueKind kind,
                const char *key,
                const char *value)
{
  struct HeaderLines *hl = cls;

  (void) kind;
  if ( (NULL == key) ||
       (NULL == value) )
    return MHD_YES;
  if (0 == strcasecmp (key,
                       MHD_HTTP_HEADER_FORWARDED))
    GNUNET_array_append (hl->forwarded,
                         hl->forwarded_len,
                         value);
  else if (0 == strcasecmp (key,
                            PH_HEADER_X_FORWARDED_FOR))
    GNUNET_array_append (hl->xff,
                         hl->xff_len,
                         value);
  else if (0 == strcasecmp (key,
                            PH_HEADER_X_FORWARDED_PROTO))
    GNUNET_array_append (hl->xfp,
                         hl->xfp_len,
                         value);
  else if (0 == strcasecmp (key,
                            PH_HEADER_X_FORWARDED_HOST))
    GNUNET_array_append (hl->xfh,
                         hl->xfh_len,
                         value);
  else if (0 == strcasecmp (key,
                            PH_HEADER_X_FORWARDED_PORT))
    GNUNET_array_append (hl->xfport,
                         hl->xfport_len,
                         value);
  return MHD_YES;
}


/**
 * Collect the forwarding header field lines of @a connection.
 *
 * MHD stores repeated field lines separately and
 * MHD_lookup_connection_value() returns only one of them, so iterating
 * is the only way to see them all -- and RFC 9110 §5.3 says all of
 * them together are the field value.  The values are borrowed from MHD
 * and live as long as the connection; only the arrays are ours.
 *
 * @param connection connection to read from
 * @param[out] hl where to collect; release with #free_headers()
 */
static void
collect_headers (struct MHD_Connection *connection,
                 struct HeaderLines *hl)
{
  memset (hl,
          0,
          sizeof (*hl));
  MHD_get_connection_values (connection,
                             MHD_HEADER_KIND,
                             &collect_header,
                             hl);
  if (0 != hl->forwarded_len)
    GNUNET_array_append (hl->forwarded,
                         hl->forwarded_len,
                         NULL);
  if (0 != hl->xff_len)
    GNUNET_array_append (hl->xff,
                         hl->xff_len,
                         NULL);
  if (0 != hl->xfp_len)
    GNUNET_array_append (hl->xfp,
                         hl->xfp_len,
                         NULL);
  if (0 != hl->xfh_len)
    GNUNET_array_append (hl->xfh,
                         hl->xfh_len,
                         NULL);
  if (0 != hl->xfport_len)
    GNUNET_array_append (hl->xfport,
                         hl->xfport_len,
                         NULL);
}


/**
 * Release the arrays of @a hl.
 *
 * @param[in,out] hl what #collect_headers() filled in
 */
static void
free_headers (struct HeaderLines *hl)
{
  GNUNET_array_grow (hl->forwarded,
                     hl->forwarded_len,
                     0);
  GNUNET_array_grow (hl->xff,
                     hl->xff_len,
                     0);
  GNUNET_array_grow (hl->xfp,
                     hl->xfp_len,
                     0);
  GNUNET_array_grow (hl->xfh,
                     hl->xfh_len,
                     0);
  GNUNET_array_grow (hl->xfport,
                     hl->xfport_len,
                     0);
}


/**
 * Run the forwarding walk for @a connection.
 *
 * @param connection connection to resolve
 * @param[out] cl what we concluded; to be released with
 *   #PAIVANA_HTTPD_client_clear()
 * @return true if a client address was determined
 */
static bool
resolve_connection (struct MHD_Connection *connection,
                    struct PAIVANA_HTTPD_Client *cl)
{
  struct HeaderLines hl;
  struct PAIVANA_HTTPD_Forwarding fi = { 0 };
  unsigned char peer[sizeof (struct in6_addr)];
  size_t peer_len;
  bool ret;

  socket_address (connection,
                  peer,
                  &peer_len);
  collect_headers (connection,
                   &hl);
  fi.peer = (0 != peer_len)
    ? peer
    : NULL;
  fi.peer_len = peer_len;
  fi.respect_forwarded = (0 != PH_respect_forwarded_headers);
  fi.forwarded = hl.forwarded;
  fi.xff = hl.xff;
  fi.xfp = hl.xfp;
  fi.xfh = hl.xfh;
  fi.xfport = hl.xfport;
  ret = PAIVANA_HTTPD_resolve_forwarding (&fi,
                                          cl);
  free_headers (&hl);
  return ret;
}


bool
PAIVANA_HTTPD_get_client_address (struct MHD_Connection *connection,
                                  void **ca,
                                  size_t *ca_len)
{
  struct PAIVANA_HTTPD_Client cl;
  bool ret;

  ret = resolve_connection (connection,
                            &cl);
  *ca = cl.ca;
  *ca_len = cl.ca_len;
  cl.ca = NULL;
  cl.ca_len = 0;
  PAIVANA_HTTPD_client_clear (&cl);
  if (! ret)
  {
    /* Only a peer that has no address at all gets here, i.e. AF_UNIX
       with no forwarding header to stand in for it.  The shipped
       packaging serves over a Unix socket and passes -f for exactly
       this reason. */
    GNUNET_break (0);
  }
  return ret;
}


bool
PAIVANA_HTTPD_get_base_url (struct MHD_Connection *connection,
                            struct GNUNET_Buffer *buf)
{
  struct PAIVANA_HTTPD_Client cl;
  const char *host;

  GNUNET_buffer_clear (buf);
  if (NULL != PH_base_url)
  {
    GNUNET_buffer_write_str (buf,
                             PH_base_url);
    return true;
  }
  /* run() refuses to start without BASE_URL unless -f was given: with
     no proxy in front of us the Host header is unverified client
     input and reconstructing a URL from it is not something we may
     do.  So everything below is the behind-a-trusted-proxy case. */
  GNUNET_assert (0 != PH_respect_forwarded_headers);
  (void) resolve_connection (connection,
                             &cl);
  if (NULL != cl.proto)
  {
    GNUNET_buffer_write_str (buf,
                             cl.proto);
    GNUNET_buffer_write_str (buf,
                             "://");
  }
  else
  {
    /* The proxy said nothing about the scheme, so go by the transport:
       MHD reports a TLS session or it does not.  (TALER_mhd_is_https()
       would consult X-Forwarded-Proto again, without applying the
       walk's verdict on whether it may be believed.) */
    GNUNET_buffer_write_str (buf,
                             (NULL !=
                              MHD_get_connection_info (
                                connection,
                                MHD_CONNECTION_INFO_PROTOCOL))
                             ? "https://"
                             : "http://");
  }
  host = cl.host;
  if (NULL == host)
  {
    /* No `Forwarded` host and no X-Forwarded-Host: the proxy passed
       the client's Host through, and -f says it vouches for it. */
    host = MHD_lookup_connection_value (connection,
                                        MHD_HEADER_KIND,
                                        MHD_HTTP_HEADER_HOST);
    if (NULL == host)
    {
      /* RFC 9112 §3.2 requires a Host on every HTTP/1.1 request. */
      GNUNET_break_op (0);
      PAIVANA_HTTPD_client_clear (&cl);
      return false;
    }
    if (! valid_host (host))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Refusing to build a base URL from Host `%s'\n",
                  host);
      GNUNET_break_op (0);
      PAIVANA_HTTPD_client_clear (&cl);
      return false;
    }
  }
  GNUNET_buffer_write_str (buf,
                           host);
  PAIVANA_HTTPD_client_clear (&cl);
  return true;
}
