/*
     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.
*/

/**
 * @file test_client_address.c
 * @brief tests the forwarding walk: who the client is, and what it
 *        thinks it connected to
 *
 * Two things come out of #PAIVANA_HTTPD_resolve_forwarding(), and both
 * are load-bearing:
 *
 * - The client address.  The access cookie is an HMAC over
 *   (expiration, website, client address).  If a host yields one byte
 *   string when it reaches paivana through a proxy and a different one
 *   when it reaches paivana directly -- or when two proxies spell its
 *   address differently -- then the cookie it was issued stops
 *   verifying, with no error anywhere: it just looks unpaid again.  So
 *   the representation has to be canonical, which is what this test
 *   pins down.  And if a client can *choose* that address, one paid
 *   cookie admits everybody, which is what the trust walk prevents.
 *
 * - The scheme and authority the base URL is rebuilt from, which the
 *   very same walk has to decide, or the cookie ends up keyed on a
 *   website string one hop reported and an address another one did.
 *
 * The integration suite cannot cover any of this: it runs paivana with
 * -n, where the cookie path is never reached at all, and it has no way
 * to present a forwarding chain from a chosen peer.
 */
#include "platform.h"
#include <gnunet/gnunet_util_lib.h>
#include "paivana-httpd_helper.h"
#include "paivana-httpd_cookie.h"

/**
 * Globals that paivana-httpd.c normally defines; the helper and
 * cookie compilation units reference them.
 */
int PH_respect_forwarded_headers;
char *PH_base_url;
int PH_global_cookie;
struct GNUNET_STRINGS_IPv4NetworkPolicy *PH_trusted_proxies4;
struct GNUNET_STRINGS_IPv6NetworkPolicy *PH_trusted_proxies6;
bool PH_have_trusted_proxies;

/**
 * Cookie key, normally set up by paivana-httpd.c from the SECRET
 * configuration value.
 */
extern struct GNUNET_HashCode paivana_secret;

/**
 * Website the test cookies are minted for.
 */
#define WEBSITE "http://example.com/page"

/**
 * Number of checks that did not hold.
 */
static unsigned int failures;


/**
 * One row of the table-driven forwarding tests.
 *
 * The header members are the *field lines* of that header, in the
 * order they arrived; leaving one empty means the header was absent.
 */
struct Case
{

  /**
   * What this row is about, for the log.
   */
  const char *name;

  /**
   * Socket peer, in presentation form, or NULL for a peer without an
   * address (AF_UNIX).
   */
  const char *peer;

  /**
   * `TRUSTED_PROXIES`, or NULL.
   */
  const char *v4;

  /**
   * `TRUSTED_PROXIES6`, or NULL.
   */
  const char *v6;

  /**
   * `Forwarded` field lines.
   */
  const char *fwd[4];

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

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

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

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

  /**
   * Expected client address in presentation form, or NULL if none
   * should be found.
   */
  const char *want_ca;

  /**
   * Expected scheme, or NULL for "nothing we trust said".
   */
  const char *want_proto;

  /**
   * Expected authority, or NULL for "nothing we trust said".
   */
  const char *want_host;

  /**
   * Was `-f` given?
   */
  bool respect;
};


/**
 * Print @a len bytes of @a p as hex into @a out.
 *
 * @param p bytes to render
 * @param len number of bytes in @a p
 * @param[out] out buffer of at least 2 * @a len + 1 bytes
 */
static void
tohex (const void *p,
       size_t len,
       char *out)
{
  const unsigned char *b = p;

  for (size_t i = 0; i < len; i++)
    sprintf (&out[2 * i],
             "%02x",
             b[i]);
  out[2 * len] = '\0';
}


/**
 * The binary address the socket branch of the resolver produces for a
 * peer whose address is @a literal.  Mirrors that branch rather than
 * calling it, as calling it would need a live MHD connection.
 *
 * @param literal address of the peer, in presentation form
 * @param[out] ca where to write the allocated address
 * @param[out] ca_len set to the number of bytes in @a ca
 */
static void
socket_address (const char *literal,
                void **ca,
                size_t *ca_len)
{
  struct in_addr a4;
  struct in6_addr a6;

  if (1 == inet_pton (AF_INET,
                      literal,
                      &a4))
  {
    *ca = GNUNET_memdup (&a4,
                         sizeof (a4));
    *ca_len = sizeof (a4);
    return;
  }
  GNUNET_assert (1 == inet_pton (AF_INET6,
                                 literal,
                                 &a6));
  if (IN6_IS_ADDR_V4MAPPED (&a6))
  {
    *ca = GNUNET_memdup (&a6.s6_addr[12],
                         sizeof (struct in_addr));
    *ca_len = sizeof (struct in_addr);
    return;
  }
  *ca = GNUNET_memdup (&a6,
                       sizeof (a6));
  *ca_len = sizeof (a6);
}


/**
 * Mint an access cookie for @a mint_ca and check it against
 * @a check_ca.
 *
 * @param mint_ca client address the cookie is issued for
 * @param mint_len number of bytes in @a mint_ca
 * @param check_ca client address presented on the later request
 * @param check_len number of bytes in @a check_ca
 * @return true if the cookie verified
 */
static bool
cookie_survives (const void *mint_ca,
                 size_t mint_len,
                 const void *check_ca,
                 size_t check_len)
{
  struct GNUNET_TIME_Timestamp ts;
  char *setcookie;
  char *val;
  char *semi;
  bool ok;

  ts = GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS);
  setcookie = PAIVANA_HTTPD_compute_cookie (ts,
                                            WEBSITE,
                                            mint_len,
                                            mint_ca);
  /* Reduce the Set-Cookie line to the bare cookie value, which is
     what check_cookie() is given on the next request. */
  val = strchr (setcookie,
                '=');
  GNUNET_assert (NULL != val);
  val++;
  semi = strchr (val,
                 ';');
  if (NULL != semi)
    *semi = '\0';
  ok = PAIVANA_HTTPD_check_cookie (val,
                                   WEBSITE,
                                   check_len,
                                   check_ca);
  GNUNET_free (setcookie);
  return ok;
}


/**
 * Install a trusted-proxy policy for the checks that follow, or clear
 * it when both arguments are NULL.
 *
 * The GNUnet policy parsers are lenient enough that "returned
 * non-NULL" is not the same as "understood something" -- see
 * load_trusted_proxies() in paivana-httpd.c -- so this asserts that
 * usable entries actually came back.
 *
 * @param v4 IPv4 policy string, or NULL
 * @param v6 IPv6 policy string, or NULL
 */
static void
set_policy (const char *v4,
            const char *v6)
{
  GNUNET_free (PH_trusted_proxies4);
  GNUNET_free (PH_trusted_proxies6);
  PH_trusted_proxies4 = NULL;
  PH_trusted_proxies6 = NULL;
  PH_have_trusted_proxies = false;
  if (NULL != v4)
  {
    PH_trusted_proxies4 = GNUNET_STRINGS_parse_ipv4_policy (v4);
    GNUNET_assert (NULL != PH_trusted_proxies4);
    GNUNET_assert (0 != PH_trusted_proxies4[0].network.s_addr);
    PH_have_trusted_proxies = true;
  }
  if (NULL != v6)
  {
    PH_trusted_proxies6 = GNUNET_STRINGS_parse_ipv6_policy (v6);
    GNUNET_assert (NULL != PH_trusted_proxies6);
    GNUNET_assert (! GNUNET_is_zero (&PH_trusted_proxies6[0].network));
    PH_have_trusted_proxies = true;
  }
}


/**
 * Resolve @a c and check the outcome against what it expects.
 *
 * @param c case to run
 */
static void
run_case (const struct Case *c)
{
  struct PAIVANA_HTTPD_Forwarding fi = { 0 };
  struct PAIVANA_HTTPD_Client cl;
  void *peer = NULL;
  void *want = NULL;
  size_t peer_len = 0;
  size_t want_len = 0;
  bool ok = true;

  set_policy (c->v4,
              c->v6);
  PH_respect_forwarded_headers = c->respect ? 1 : 0;
  if (NULL != c->peer)
    socket_address (c->peer,
                    &peer,
                    &peer_len);
  fi.peer = peer;
  fi.peer_len = peer_len;
  fi.respect_forwarded = c->respect;
  fi.forwarded = c->fwd;
  fi.xff = c->xff;
  fi.xfp = c->xfp;
  fi.xfh = c->xfh;
  fi.xfport = c->xfport;
  if (PAIVANA_HTTPD_resolve_forwarding (&fi,
                                        &cl) != (NULL != c->want_ca))
    ok = false;
  if (NULL != c->want_ca)
  {
    socket_address (c->want_ca,
                    &want,
                    &want_len);
    if ( (want_len != cl.ca_len) ||
         (NULL == cl.ca) ||
         (0 != memcmp (want,
                       cl.ca,
                       want_len)) )
      ok = false;
    /* The identity has to be the same bytes however it was learned,
       or a cookie minted on one path stops verifying on the next. */
    else if (! cookie_survives (cl.ca,
                                cl.ca_len,
                                want,
                                want_len))
      ok = false;
  }
  else if (NULL != cl.ca)
  {
    ok = false;
  }
  if ( ( (NULL == cl.proto) != (NULL == c->want_proto) ) ||
       ( (NULL != cl.proto) &&
         (0 != strcmp (cl.proto,
                       c->want_proto)) ) )
    ok = false;
  if ( ( (NULL == cl.host) != (NULL == c->want_host) ) ||
       ( (NULL != cl.host) &&
         (0 != strcmp (cl.host,
                       c->want_host)) ) )
    ok = false;
  if (! ok)
  {
    char ghex[2 * sizeof (struct in6_addr) + 1];
    char ehex[2 * sizeof (struct in6_addr) + 1];

    tohex (cl.ca,
           cl.ca_len,
           ghex);
    tohex (want,
           want_len,
           ehex);
    fprintf (stderr,
             "FAIL: %s\n"
             "      address %s, want %s (%s)\n"
             "      proto   %s, want %s\n"
             "      host    %s, want %s\n",
             c->name,
             ghex,
             ehex,
             (NULL != c->want_ca) ? c->want_ca : "(none)",
             (NULL != cl.proto) ? cl.proto : "(none)",
             (NULL != c->want_proto) ? c->want_proto : "(none)",
             (NULL != cl.host) ? cl.host : "(none)",
             (NULL != c->want_host) ? c->want_host : "(none)");
    failures++;
  }
  else
  {
    fprintf (stderr,
             "  ok: %s\n",
             c->name);
  }
  PAIVANA_HTTPD_client_clear (&cl);
  GNUNET_free (peer);
  GNUNET_free (want);
  set_policy (NULL,
              NULL);
}


/**
 * Run every case in the NULL-name-terminated table @a tab.
 *
 * @param tab cases to run
 */
static void
run_cases (const struct Case *tab)
{
  for (unsigned int i = 0; NULL != tab[i].name; i++)
    run_case (&tab[i]);
}


/**
 * Resolve @a xff as the only `X-Forwarded-For` field line of a request
 * from a peer with no address of its own, so that whatever comes out
 * came out of the header.
 *
 * @param xff header value
 * @param[out] ca where to write the allocated address
 * @param[out] ca_len set to the number of bytes in @a ca
 * @return true if an address was found
 */
static bool
resolve_xff (const char *xff,
             void **ca,
             size_t *ca_len)
{
  struct PAIVANA_HTTPD_Forwarding fi = { 0 };
  struct PAIVANA_HTTPD_Client cl;
  const char *lines[] = {
    xff,
    NULL
  };
  bool ret;

  PH_respect_forwarded_headers = 1;
  fi.respect_forwarded = true;
  fi.xff = lines;
  ret = PAIVANA_HTTPD_resolve_forwarding (&fi,
                                          &cl);
  *ca = cl.ca;
  *ca_len = cl.ca_len;
  cl.ca = NULL;
  cl.ca_len = 0;
  PAIVANA_HTTPD_client_clear (&cl);
  return ret;
}


/**
 * Check that @a xff yields exactly the address a direct connection
 * from @a peer would, and that a cookie issued on one path is
 * accepted on the other.
 *
 * @param xff `X-Forwarded-For` value the proxied request carries
 * @param peer socket address of the same host connecting directly
 */
static void
same_host (const char *xff,
           const char *peer)
{
  void *fa;
  void *sa;
  size_t fa_len;
  size_t sa_len;
  char fhex[2 * sizeof (struct in6_addr) + 1];
  char shex[2 * sizeof (struct in6_addr) + 1];

  socket_address (peer,
                  &sa,
                  &sa_len);
  if (! resolve_xff (xff,
                     &fa,
                     &fa_len))
  {
    fprintf (stderr,
             "FAIL: X-Forwarded-For `%s' rejected, want the address of %s\n",
             xff,
             peer);
    failures++;
    GNUNET_free (sa);
    return;
  }
  tohex (fa,
         fa_len,
         fhex);
  tohex (sa,
         sa_len,
         shex);
  if ( (fa_len != sa_len) ||
       (0 != memcmp (fa,
                     sa,
                     fa_len)) )
  {
    fprintf (stderr,
             "FAIL: X-Forwarded-For `%s' gives %s, socket %s gives %s\n",
             xff,
             fhex,
             peer,
             shex);
    failures++;
  }
  else if (! cookie_survives (fa,
                              fa_len,
                              sa,
                              sa_len))
  {
    fprintf (stderr,
             "FAIL: cookie issued via X-Forwarded-For `%s' rejected for %s\n",
             xff,
             peer);
    failures++;
  }
  else
  {
    fprintf (stderr,
             "  ok: `%s' == socket %s == %s\n",
             xff,
             peer,
             fhex);
  }
  GNUNET_free (fa);
  GNUNET_free (sa);
}


/**
 * Check that @a xff names no address: it is not a bare IP address, so
 * there is no canonical form for it and it must not become an
 * identity.  With no socket address to fall back on either, nothing
 * comes out at all.
 *
 * @param xff `X-Forwarded-For` value to reject
 */
static void
refused (const char *xff)
{
  void *ca;
  size_t ca_len;

  if (resolve_xff (xff,
                   &ca,
                   &ca_len))
  {
    char hex[2 * sizeof (struct in6_addr) + 1];

    tohex (ca,
           ca_len,
           hex);
    fprintf (stderr,
             "FAIL: X-Forwarded-For `%s' accepted as %s, want refusal\n",
             xff,
             hex);
    failures++;
    GNUNET_free (ca);
    return;
  }
  if ( (NULL != ca) ||
       (0 != ca_len) )
  {
    fprintf (stderr,
             "FAIL: X-Forwarded-For `%s' refused but left ca=%p len=%u\n",
             xff,
             ca,
             (unsigned int) ca_len);
    failures++;
    return;
  }
  fprintf (stderr,
           "  ok: `%s' refused\n",
           xff);
}


/**
 * Check that @a a and @a b, two spellings of one host, are one
 * identity.
 *
 * @param a first spelling
 * @param b second spelling
 */
static void
same_identity (const char *a,
               const char *b)
{
  void *ca;
  void *cb;
  size_t ca_len;
  size_t cb_len;

  /* Not GNUNET_assert: these are the code under test, not this
     test's own inputs.  A regression that makes a spelling
     unresolvable would abort here and print one backtrace, where the
     whole point of this file is to run every check and show the
     shape of the damage. */
  if (! resolve_xff (a,
                     &ca,
                     &ca_len))
  {
    fprintf (stderr,
             "FAIL: `%s' does not resolve at all\n",
             a);
    failures++;
    return;
  }
  if (! resolve_xff (b,
                     &cb,
                     &cb_len))
  {
    fprintf (stderr,
             "FAIL: `%s' does not resolve at all\n",
             b);
    failures++;
    GNUNET_free (ca);
    return;
  }
  if ( (ca_len != cb_len) ||
       (0 != memcmp (ca,
                     cb,
                     ca_len)) ||
       (! cookie_survives (ca,
                           ca_len,
                           cb,
                           cb_len)) )
  {
    fprintf (stderr,
             "FAIL: `%s' and `%s' are one host but not one identity\n",
             a,
             b);
    failures++;
  }
  else
  {
    fprintf (stderr,
             "  ok: `%s' == `%s'\n",
             a,
             b);
  }
  GNUNET_free (ca);
  GNUNET_free (cb);
}


/**
 * Check that @a addr is (or is not) inside the installed policy.
 *
 * @param addr address in presentation form
 * @param want expected verdict
 */
static void
trusted_is (const char *addr,
            bool want)
{
  void *ca;
  size_t ca_len;
  bool got;

  socket_address (addr,
                  &ca,
                  &ca_len);
  got = PAIVANA_HTTPD_is_trusted_proxy (ca,
                                        ca_len);
  if (got != want)
  {
    fprintf (stderr,
             "FAIL: %s trusted=%s, want %s\n",
             addr,
             got ? "true" : "false",
             want ? "true" : "false");
    failures++;
  }
  else
  {
    fprintf (stderr,
             "  ok: %s trusted=%s\n",
             addr,
             got ? "true" : "false");
  }
  GNUNET_free (ca);
}


/**
 * Check that parameter @a name of @a fwd reads as @a want (NULL for
 * "not present, or not usable").
 *
 * @param fwd `Forwarded` value
 * @param name parameter to read
 * @param want expected value, or NULL
 */
static void
fwd_param_is (const char *fwd,
              const char *name,
              const char *want)
{
  char *got;

  got = PAIVANA_HTTPD_forwarded_param (fwd,
                                       name);
  if ( ( (NULL == got) != (NULL == want) ) ||
       ( (NULL != got) &&
         (0 != strcmp (got,
                       want)) ) )
  {
    fprintf (stderr,
             "FAIL: `%s' %s=%s, want %s\n",
             fwd,
             name,
             (NULL != got) ? got : "(none)",
             (NULL != want) ? want : "(none)");
    failures++;
  }
  else
  {
    fprintf (stderr,
             "  ok: `%s' %s=%s\n",
             fwd,
             name,
             (NULL != got) ? got : "(none)");
  }
  GNUNET_free (got);
}


/**
 * Check that @a v renders as the RFC 7239 §4 value @a want (NULL if
 * it cannot be rendered at all).
 *
 * @param v value to render
 * @param want expected rendering, or NULL
 */
static void
value_is (const char *v,
          const char *want)
{
  char *got;

  got = PAIVANA_HTTPD_forwarded_value (v);
  if ( ( (NULL == got) != (NULL == want) ) ||
       ( (NULL != got) &&
         (0 != strcmp (got,
                       want)) ) )
  {
    fprintf (stderr,
             "FAIL: value of `%s' is `%s', want `%s'\n",
             v,
             (NULL != got) ? got : "(none)",
             (NULL != want) ? want : "(none)");
    failures++;
  }
  else
  {
    fprintf (stderr,
             "  ok: value of `%s' is `%s'\n",
             v,
             (NULL != got) ? got : "(none)");
  }
  GNUNET_free (got);
}


/**
 * Check that @a literal renders as the RFC 7239 node identifier
 * @a want.
 *
 * @param literal address in presentation form, or NULL for a peer
 *        that has none
 * @param want expected identifier
 */
static void
node_is (const char *literal,
         const char *want)
{
  void *ca = NULL;
  size_t ca_len = 0;
  char *got;

  if (NULL != literal)
    socket_address (literal,
                    &ca,
                    &ca_len);
  got = PAIVANA_HTTPD_forwarded_node (ca,
                                      ca_len);
  if (0 != strcmp (got,
                   want))
  {
    fprintf (stderr,
             "FAIL: node for %s is `%s', want `%s'\n",
             (NULL != literal) ? literal : "(no address)",
             got,
             want);
    failures++;
  }
  else
  {
    fprintf (stderr,
             "  ok: node for %s is `%s'\n",
             (NULL != literal) ? literal : "(no address)",
             got);
  }
  GNUNET_free (got);
  GNUNET_free (ca);
}


/**
 * Check that @a fwd renders as the X-Forwarded-For chain @a want
 * (NULL if it cannot be represented).
 *
 * @param fwd `Forwarded` value
 * @param want expected chain, or NULL
 */
static void
chain_is (const char *fwd,
          const char *want)
{
  char *got;

  got = PAIVANA_HTTPD_forwarded_for_chain (fwd);
  if ( ( (NULL == got) != (NULL == want) ) ||
       ( (NULL != got) &&
         (0 != strcmp (got,
                       want)) ) )
  {
    fprintf (stderr,
             "FAIL: chain of `%s' is `%s', want `%s'\n",
             fwd,
             (NULL != got) ? got : "(none)",
             (NULL != want) ? want : "(none)");
    failures++;
  }
  else
  {
    fprintf (stderr,
             "  ok: chain of `%s' is `%s'\n",
             fwd,
             (NULL != got) ? got : "(none)");
  }
  GNUNET_free (got);
}


/**
 * Which hop the walk stops at, in every shape of chain we can be
 * handed.  The socket peer is trusted implicitly whenever -f is set --
 * that is what the flag means -- and TRUSTED_PROXIES names the
 * additional hops further out that may speak for their predecessor.
 */
static void
test_walk (void)
{
  static const struct Case tab[] = {
    {
      .name = "without -f the socket peer wins, headers or not",
      .peer = "203.0.113.7",
      .respect = false,
      .fwd = { "for=1.2.3.4;proto=https;host=evil.example.com" },
      .xff = { "5.6.7.8" },
      .xfp = { "https" },
      .xfh = { "evil.example.com" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "without -f not even a truthful header is read",
      .peer = "198.51.100.9",
      .respect = false,
      .xff = { "203.0.113.7" },
      .want_ca = "198.51.100.9"
    },
    {
      .name = "-f, one proxy (the peer), one element: the client",
      .peer = "10.0.0.1",
      .respect = true,
      .xff = { "203.0.113.7" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "-f, one element, Forwarded spelling",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=203.0.113.7" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "-f, no TRUSTED_PROXIES: only the peer's own report"
              " counts, so a client cannot prepend itself an identity",
      .peer = "10.0.0.1",
      .respect = true,
      .xff = { "1.2.3.4, 203.0.113.7" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "-f, two proxies, the outer one trusted: the walk steps"
              " over it and stops at the client",
      .peer = "10.0.0.1",
      .v4 = "10.0.0.0/8;",
      .respect = true,
      .xff = { "203.0.113.7, 10.0.0.2" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "-f, three proxies, all trusted",
      .peer = "10.0.0.1",
      .v4 = "10.0.0.0/8;",
      .respect = true,
      .xff = { "203.0.113.7, 10.0.0.2, 10.0.0.3" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "-f, untrusted node in the middle: the walk stops there",
      .peer = "10.0.0.1",
      .v4 = "10.0.0.0/8;",
      .respect = true,
      .xff = { "1.2.3.4, 203.0.113.7, 10.0.0.2" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "-f, the peer is trusted implicitly even where"
              " TRUSTED_PROXIES does not cover it",
      .peer = "192.0.2.5",
      .v4 = "10.0.0.0/8;",
      .respect = true,
      .xff = { "203.0.113.7" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "-f, every element is one of ours: the leftmost is all"
              " we have",
      .peer = "10.0.0.1",
      .v4 = "10.0.0.0/8;",
      .respect = true,
      .xff = { "10.0.0.9, 10.0.0.2" },
      .want_ca = "10.0.0.9"
    },
    {
      .name = "-f, IPv6 client, bracketed node",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=\"[2001:db8::1]\"" },
      .want_ca = "2001:db8::1"
    },
    {
      .name = "-f, IPv6 client with a port: the port is not part of"
              " the identity",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=\"[2001:0db8:0000:0000:0000:0000:0000:0001]:47011\"" },
      .want_ca = "2001:db8::1"
    },
    {
      .name = "-f, IPv4 node with a port",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=\"203.0.113.7:8080\"" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "-f, IPv6 trusted proxy, IPv6 policy",
      .peer = "2001:db8::2",
      .v6 = "2001:db8::/32;",
      .respect = true,
      .fwd = { "for=\"[2001:db9::1]\", for=\"[2001:db8::3]\"" },
      .want_ca = "2001:db9::1"
    },
    {
      .name = "-f, for=unknown (RFC 7239 §6.3): back to the peer",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=unknown" },
      .want_ca = "10.0.0.1"
    },
    {
      .name = "-f, obfuscated node: back to the peer",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=_hidden" },
      .want_ca = "10.0.0.1"
    },
    {
      .name = "-f, unknown to the left of a trusted proxy stops the"
              " walk without losing the peer",
      .peer = "10.0.0.1",
      .v4 = "10.0.0.0/8;",
      .respect = true,
      .fwd = { "for=203.0.113.7, for=unknown, for=10.0.0.2" },
      .want_ca = "10.0.0.1"
    },
    {
      .name = "-f, element without a for= at all",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "proto=https;host=e.com" },
      .want_ca = "10.0.0.1",
      .want_proto = "https",
      .want_host = "e.com"
    },
    /* RFC 9110 §5.3: repeated field lines of a list-based field are
       one list.  A proxy adding a line of its own instead of extending
       the last one is explicitly permitted by RFC 7239 §4, and reading
       only the first would let the client's element outrank it. */
    {
      .name = "-f, two Forwarded field lines: both are the header",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=1.2.3.4", "for=203.0.113.7" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "-f, three X-Forwarded-For field lines",
      .peer = "10.0.0.1",
      .v4 = "10.0.0.0/8;",
      .respect = true,
      .xff = { "1.2.3.4", "203.0.113.7", "10.0.0.2" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "-f, a field line that is itself a list",
      .peer = "10.0.0.1",
      .v4 = "10.0.0.0/8;",
      .respect = true,
      .fwd = { "for=1.2.3.4, for=203.0.113.7", "for=10.0.0.2" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "-f, Forwarded wins where both headers are present",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=203.0.113.7" },
      .xff = { "1.2.3.4" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "-f, X-Forwarded-For is read when Forwarded is absent",
      .peer = "10.0.0.1",
      .respect = true,
      .xff = { "203.0.113.7" },
      .want_ca = "203.0.113.7"
    },
    /* A quoted-string is data: a ',' or ';' inside one does not end an
       element or a parameter (RFC 7239 §4 via RFC 9110 §5.6.4). */
    {
      .name = "-f, comma inside a quoted string is not a delimiter",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "host=\"a,b\";for=203.0.113.7" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "-f, quoted-pair escapes are unescaped, not counted",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "by=\"x\\\"y;for=1.2.3.4\";for=203.0.113.7" },
      .want_ca = "203.0.113.7"
    },
    /* An unterminated quoted-string used to be read past the end of
       the header.  It is malformed, and malformed means the peer. */
    {
      .name = "-f, unterminated quoted string: malformed, back to the"
              " peer",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=\"unterminated" },
      .want_ca = "10.0.0.1"
    },
    {
      .name = "-f, unterminated quoted string in a later element",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=203.0.113.7, for=\"1.2.3.4" },
      .want_ca = "10.0.0.1"
    },
    {
      .name = "-f, backslash at the very end of the header",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=\"1.2.3.4\\" },
      .want_ca = "10.0.0.1"
    },
    {
      .name = "-f, empty Forwarded field line: back to the peer",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "" },
      .want_ca = "10.0.0.1"
    },
    {
      .name = "-f, whitespace-only field line",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "   \t " },
      .want_ca = "10.0.0.1"
    },
    {
      .name = "-f, whitespace-only X-Forwarded-For",
      .peer = "10.0.0.1",
      .respect = true,
      .xff = { "   " },
      .want_ca = "10.0.0.1"
    },
    {
      .name = "-f, a parameter without a value",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=" },
      .want_ca = "10.0.0.1"
    },
    {
      .name = "-f, a duplicate parameter (RFC 7239 §4 forbids it)",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=203.0.113.7;for=1.2.3.4" },
      .want_ca = "10.0.0.1"
    },
    {
      .name = "-f, garbage where a forwarded-pair should be",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=1.2.3.4 nonsense" },
      .want_ca = "10.0.0.1"
    },
    {
      .name = "-f, X-Forwarded-For element that is not an address",
      .peer = "10.0.0.1",
      .respect = true,
      .xff = { "garbage" },
      .want_ca = "10.0.0.1"
    },
    {
      .name = "-f over a Unix socket: the header is all there is",
      .peer = NULL,
      .respect = true,
      .fwd = { "for=203.0.113.7" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "-f over a Unix socket, unusable header: nothing at all",
      .peer = NULL,
      .respect = true,
      .fwd = { "for=unknown" },
      .want_ca = NULL
    },
    {
      .name = "a dual-stack listener folds ::ffff:a.b.c.d to IPv4",
      .peer = "::ffff:203.0.113.7",
      .respect = false,
      .want_ca = "203.0.113.7"
    },
    { .name = NULL }
  };

  run_cases (tab);
}


/**
 * Where the scheme and authority of the base URL come from.  They are
 * decided by the same walk as the address, so that the string the
 * cookie is keyed on and the address it is keyed on were reported by
 * the same hop.
 */
static void
test_base (void)
{
  static const struct Case tab[] = {
    {
      .name = "Forwarded proto=/host= are read, not only the"
              " X-Forwarded-* spellings",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=203.0.113.7;proto=https;host=example.com" },
      .want_ca = "203.0.113.7",
      .want_proto = "https",
      .want_host = "example.com"
    },
    /* The element the Apache snippet in debian/ builds: an unquoted
       authority (`:` is not a tchar, but that is what mod_headers
       emits) and an unbracketed IPv6 node. */
    {
      .name = "the shipped Apache element, with a port in the host",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=203.0.113.7;proto=https;host=example.com:8443" },
      .want_ca = "203.0.113.7",
      .want_proto = "https",
      .want_host = "example.com:8443"
    },
    {
      .name = "the shipped Apache element, IPv6 client, unbracketed",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=2001:db8::1;proto=https;host=example.com" },
      .want_ca = "2001:db8::1",
      .want_proto = "https",
      .want_host = "example.com"
    },
    {
      .name = "proto= and host= come from the element the address"
              " came from",
      .peer = "10.0.0.1",
      .v4 = "10.0.0.0/8;",
      .respect = true,
      .fwd = { "for=203.0.113.7;proto=https;host=example.com,"
               " for=10.0.0.2;proto=http;host=inner.example.net" },
      .want_ca = "203.0.113.7",
      .want_proto = "https",
      .want_host = "example.com"
    },
    {
      .name = "X-Forwarded-Proto/-Host fill in where Forwarded is"
              " silent",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=203.0.113.7" },
      .xfp = { "https" },
      .xfh = { "example.com" },
      .want_ca = "203.0.113.7",
      .want_proto = "https",
      .want_host = "example.com"
    },
    {
      .name = "without -f neither is believed",
      .peer = "10.0.0.1",
      .respect = false,
      .xfp = { "https" },
      .xfh = { "evil.example.com" },
      .want_ca = "10.0.0.1"
    },
    {
      .name = "X-Forwarded-Port is appended when the host has none",
      .peer = "10.0.0.1",
      .respect = true,
      .xff = { "203.0.113.7" },
      .xfp = { "https" },
      .xfh = { "example.com" },
      .xfport = { "8443" },
      .want_ca = "203.0.113.7",
      .want_proto = "https",
      .want_host = "example.com:8443"
    },
    {
      .name = "X-Forwarded-Host with a port plus X-Forwarded-Port does"
              " not yield example.com:8443:8443",
      .peer = "10.0.0.1",
      .respect = true,
      .xff = { "203.0.113.7" },
      .xfp = { "https" },
      .xfh = { "example.com:8443" },
      .xfport = { "8443" },
      .want_ca = "203.0.113.7",
      .want_proto = "https",
      .want_host = "example.com:8443"
    },
    {
      .name = "the default port for the scheme is left off",
      .peer = "10.0.0.1",
      .respect = true,
      .xff = { "203.0.113.7" },
      .xfp = { "https" },
      .xfh = { "example.com" },
      .xfport = { "443" },
      .want_ca = "203.0.113.7",
      .want_proto = "https",
      .want_host = "example.com"
    },
    {
      .name = "a port is re-rendered, not echoed: 0443 is 443",
      .peer = "10.0.0.1",
      .respect = true,
      .xff = { "203.0.113.7" },
      .xfp = { "https" },
      .xfh = { "example.com" },
      .xfport = { "0443" },
      .want_ca = "203.0.113.7",
      .want_proto = "https",
      .want_host = "example.com"
    },
    {
      .name = "a port that is not a number is ignored",
      .peer = "10.0.0.1",
      .respect = true,
      .xff = { "203.0.113.7" },
      .xfh = { "example.com" },
      .xfport = { "+80" },
      .want_ca = "203.0.113.7",
      .want_host = "example.com"
    },
    {
      .name = "an out-of-range port is ignored",
      .peer = "10.0.0.1",
      .respect = true,
      .xff = { "203.0.113.7" },
      .xfh = { "example.com" },
      .xfport = { "70000" },
      .want_ca = "203.0.113.7",
      .want_host = "example.com"
    },
    {
      .name = "an IPv6 X-Forwarded-Host keeps its brackets and takes"
              " the port outside them",
      .peer = "10.0.0.1",
      .respect = true,
      .xff = { "203.0.113.7" },
      .xfh = { "[2001:db8::1]" },
      .xfport = { "8080" },
      .want_ca = "203.0.113.7",
      .want_host = "[2001:db8::1]:8080"
    },
    {
      .name = "a host that is not an authority is refused",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=203.0.113.7;host=\"a, for=1.2.3.4\"" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "a host with a path is refused",
      .peer = "10.0.0.1",
      .respect = true,
      .xff = { "203.0.113.7" },
      .xfh = { "example.com/evil" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "a scheme we cannot serve is refused",
      .peer = "10.0.0.1",
      .respect = true,
      .fwd = { "for=203.0.113.7;proto=javascript" },
      .want_ca = "203.0.113.7"
    },
    {
      .name = "the leftmost element of a listed X-Forwarded-Proto is"
              " the client's",
      .peer = "10.0.0.1",
      .respect = true,
      .xff = { "203.0.113.7" },
      .xfp = { "https, http" },
      .want_ca = "203.0.113.7",
      .want_proto = "https"
    },
    { .name = NULL }
  };

  run_cases (tab);
}


/**
 * A chain long enough to hurt.  Every element used to be found by
 * rescanning the header from byte 0, so a client could buy Θ(n·len) of
 * the daemon's only thread for the price of one request.  The walk is
 * a single pass now, and declines outright to look at more elements
 * than any real deployment has.
 */
static void
test_long_chain (void)
{
  struct PAIVANA_HTTPD_Forwarding fi = { 0 };
  struct PAIVANA_HTTPD_Client cl;
  struct GNUNET_Buffer buf = { 0 };
  struct GNUNET_TIME_Absolute start;
  struct GNUNET_TIME_Relative dur;
  void *peer;
  size_t peer_len;
  char *big;
  const char *lines[2] = {
    NULL,
    NULL
  };
  unsigned int rounds = 200;

  socket_address ("10.0.0.1",
                  &peer,
                  &peer_len);
  set_policy ("10.0.0.0/8;",
              NULL);
  PH_respect_forwarded_headers = 1;
  fi.peer = peer;
  fi.peer_len = peer_len;
  fi.respect_forwarded = true;

  /* Exactly the cap: still a chain, still walked. */
  for (unsigned int i = 0;
       i < PAIVANA_HTTPD_MAX_FORWARDED_ELEMENTS;
       i++)
  {
    char elem[64];

    if (0 != i)
      GNUNET_buffer_write_str (&buf,
                               ", ");
    GNUNET_snprintf (elem,
                     sizeof (elem),
                     "for=10.0.0.%u",
                     1 + i);
    GNUNET_buffer_write_str (&buf,
                             elem);
  }
  big = GNUNET_buffer_reap_str (&buf);
  lines[0] = big;
  fi.forwarded = lines;
  (void) PAIVANA_HTTPD_resolve_forwarding (&fi,
                                           &cl);
  if ( (sizeof (struct in_addr) != cl.ca_len) ||
       (0 != memcmp (cl.ca,
                     "\x0a\x00\x00\x01",
                     4)) )
  {
    fprintf (stderr,
             "FAIL: a chain of exactly %u trusted hops should resolve to"
             " its leftmost element\n",
             (unsigned int) PAIVANA_HTTPD_MAX_FORWARDED_ELEMENTS);
    failures++;
  }
  else
  {
    fprintf (stderr,
             "  ok: a chain of exactly %u elements is walked\n",
             (unsigned int) PAIVANA_HTTPD_MAX_FORWARDED_ELEMENTS);
  }
  PAIVANA_HTTPD_client_clear (&cl);
  GNUNET_free (big);

  /* Everything an MHD connection pool can hold.  The answer is the
     peer -- we decline to look -- and saying so has to stay linear in
     the length of the header. */
  for (unsigned int i = 0; i < 2500; i++)
  {
    if (0 != i)
      GNUNET_buffer_write_str (&buf,
                               ", ");
    GNUNET_buffer_write_str (&buf,
                             "for=10.0.0.1");
  }
  big = GNUNET_buffer_reap_str (&buf);
  lines[0] = big;
  fi.forwarded = lines;
  start = GNUNET_TIME_absolute_get ();
  for (unsigned int r = 0; r < rounds; r++)
  {
    (void) PAIVANA_HTTPD_resolve_forwarding (&fi,
                                             &cl);
    if ( (peer_len != cl.ca_len) ||
         (NULL == cl.ca) ||
         (0 != memcmp (cl.ca,
                       peer,
                       peer_len)) )
    {
      fprintf (stderr,
               "FAIL: an over-long chain must fall back to the socket"
               " peer\n");
      failures++;
      PAIVANA_HTTPD_client_clear (&cl);
      break;
    }
    PAIVANA_HTTPD_client_clear (&cl);
  }
  dur = GNUNET_TIME_absolute_get_duration (start);
  fprintf (stderr,
           "  ok: 2500 elements over %u bytes, %u times, refused in %s\n",
           (unsigned int) strlen (big),
           rounds,
           GNUNET_STRINGS_relative_time_to_string (dur,
                                                   GNUNET_YES));
  GNUNET_free (big);
  GNUNET_free (peer);
  set_policy (NULL,
              NULL);
}


/**
 * Pin the behaviour of the GNUnet policy parsers that
 * load_trusted_proxies() has to compensate for.  These are not our
 * functions, and their edge cases are what the configuration loader
 * rejects on the operator's behalf; if upstream ever changes them,
 * this is where it shows up.
 */
static void
test_policy_parser (void)
{
  struct GNUNET_STRINGS_IPv4NetworkPolicy *p4;
  struct GNUNET_STRINGS_IPv6NetworkPolicy *p6;
  unsigned int n;

  /* The trailing ';' is a terminator, not a separator: without it
     nothing parses at all. */
  p4 = GNUNET_STRINGS_parse_ipv4_policy ("10.0.0.0/8");
  if (NULL != p4)
  {
    fprintf (stderr,
             "FAIL: unterminated IPv4 policy accepted\n");
    failures++;
    GNUNET_free (p4);
  }
  else
  {
    fprintf (stderr,
             "  ok: unterminated IPv4 policy refused\n");
  }
  p6 = GNUNET_STRINGS_parse_ipv6_policy ("2001:db8::/32");
  if (NULL != p6)
  {
    fprintf (stderr,
             "FAIL: unterminated IPv6 policy accepted\n");
    failures++;
    GNUNET_free (p6);
  }
  else
  {
    fprintf (stderr,
             "  ok: unterminated IPv6 policy refused\n");
  }
  /* A missing terminator on the LAST entry drops only that entry, and
     the parser reports success for the prefix it did understand --
     which is why load_trusted_proxies() counts the ';' itself. */
  p4 = GNUNET_STRINGS_parse_ipv4_policy ("10.0.0.0/8;192.168.0.0/16");
  n = 0;
  if (NULL != p4)
    while (0 != p4[n].network.s_addr)
      n++;
  if ( (NULL == p4) ||
       (1 != n) )
  {
    fprintf (stderr,
             "FAIL: a partially terminated IPv4 policy no longer parses"
             " to its first entry -- load_trusted_proxies() can stop"
             " counting separators\n");
    failures++;
  }
  else
  {
    fprintf (stderr,
             "  ok: `10.0.0.0/8;192.168.0.0/16' silently yields 1 of 2"
             " entries (rejected at startup)\n");
  }
  GNUNET_free (p4);
  /* Likewise trailing garbage after the last ';'. */
  p4 = GNUNET_STRINGS_parse_ipv4_policy ("10.0.0.0/8;garbage");
  n = 0;
  if (NULL != p4)
    while (0 != p4[n].network.s_addr)
      n++;
  if ( (NULL == p4) ||
       (1 != n) )
  {
    fprintf (stderr,
             "FAIL: trailing garbage in an IPv4 policy no longer slips"
             " through the parser\n");
    failures++;
  }
  else
  {
    fprintf (stderr,
             "  ok: trailing garbage is silently dropped (rejected at"
             " startup)\n");
  }
  GNUNET_free (p4);
  /* The list is terminated by an all-zero entry, so a /0 network is
     indistinguishable from the end of it: "trust everyone" parses to
     "trust no one", taking any later entries with it. */
  p4 = GNUNET_STRINGS_parse_ipv4_policy ("0.0.0.0/0;10.0.0.0/8;");
  if ( (NULL == p4) ||
       (0 != p4[0].network.s_addr) )
  {
    fprintf (stderr,
             "FAIL: 0.0.0.0/0 no longer swallows the IPv4 list --"
             " load_trusted_proxies() can stop apologising for it\n");
    failures++;
  }
  else
  {
    fprintf (stderr,
             "  ok: 0.0.0.0/0 yields an empty list (rejected at startup)\n");
  }
  GNUNET_free (p4);
  p6 = GNUNET_STRINGS_parse_ipv6_policy ("::/0;2001:db8::/32;");
  if ( (NULL == p6) ||
       (! GNUNET_is_zero (&p6[0].network)) )
  {
    fprintf (stderr,
             "FAIL: ::/0 no longer swallows the IPv6 list\n");
    failures++;
  }
  else
  {
    fprintf (stderr,
             "  ok: ::/0 yields an empty list (rejected at startup)\n");
  }
  GNUNET_free (p6);
  /* An IPv6 address in the IPv4 key returns a valid pointer to an
     empty list rather than an error, which is why the loader counts
     entries instead of just checking for NULL. */
  p4 = GNUNET_STRINGS_parse_ipv4_policy ("::1;");
  if ( (NULL == p4) ||
       (0 != p4[0].network.s_addr) )
  {
    fprintf (stderr,
             "FAIL: an IPv6 address in the IPv4 key no longer parses to"
             " an empty list\n");
    failures++;
  }
  else
  {
    fprintf (stderr,
             "  ok: IPv6 in the IPv4 key yields an empty list\n");
  }
  GNUNET_free (p4);
  /* Whitespace between entries: tolerated by the v4 parser, fatal to
     the v6 one.  Documented in the loader's error message. */
  p4 = GNUNET_STRINGS_parse_ipv4_policy ("10.0.0.0/8; 192.168.0.0/16;");
  if (NULL == p4)
  {
    fprintf (stderr,
             "FAIL: IPv4 policy with spaces refused\n");
    failures++;
  }
  else
  {
    fprintf (stderr,
             "  ok: IPv4 policy tolerates spaces between entries\n");
  }
  GNUNET_free (p4);
  p6 = GNUNET_STRINGS_parse_ipv6_policy ("2001:db8::/32; fe80::/10;");
  if (NULL != p6)
  {
    fprintf (stderr,
             "FAIL: IPv6 policy with spaces accepted -- the loader's"
             " error message says otherwise\n");
    failures++;
    GNUNET_free (p6);
  }
  else
  {
    fprintf (stderr,
             "  ok: IPv6 policy refuses spaces between entries\n");
  }
  /* Out-of-range prefix lengths are caught. */
  p4 = GNUNET_STRINGS_parse_ipv4_policy ("10.0.0.0/33;");
  if (NULL != p4)
  {
    fprintf (stderr,
             "FAIL: IPv4 /33 accepted\n");
    failures++;
    GNUNET_free (p4);
  }
  else
  {
    fprintf (stderr,
             "  ok: IPv4 /33 refused\n");
  }
  p6 = GNUNET_STRINGS_parse_ipv6_policy ("2001:db8::/129;");
  if (NULL != p6)
  {
    fprintf (stderr,
             "FAIL: IPv6 /129 accepted\n");
    failures++;
    GNUNET_free (p6);
  }
  else
  {
    fprintf (stderr,
             "  ok: IPv6 /129 refused\n");
  }
}


int
main (int argc,
      char *const *argv)
{
  (void) argc;
  (void) argv;
  /* Quiet: the refusal cases trip GNUNET_break_op() by design. */
  GNUNET_assert (GNUNET_OK ==
                 GNUNET_log_setup ("test-client-address",
                                   "ERROR",
                                   NULL));
  GNUNET_CRYPTO_hash ("test-client-address",
                      strlen ("test-client-address"),
                      &paivana_secret);

  fprintf (stderr,
           "-- a host reaching paivana through a proxy and directly --\n");
  same_host ("203.0.113.7",
             "203.0.113.7");
  same_host ("2001:db8::1",
             "2001:db8::1");
  same_host ("::1",
             "::1");
  same_host ("127.0.0.1",
             "127.0.0.1");
  /* A dual-stack listener reports an IPv4 peer as ::ffff:a.b.c.d;
     a proxy in front of it reports the plain IPv4 address. */
  same_host ("203.0.113.7",
             "::ffff:203.0.113.7");
  same_host ("  203.0.113.7  ",
             "203.0.113.7");
  same_host ("\t2001:db8::1\t",
             "2001:db8::1");

  fprintf (stderr,
           "-- one host, several spellings --\n");
  same_identity ("::1",
                 "0:0:0:0:0:0:0:1");
  same_identity ("2001:db8::1",
                 "2001:0db8:0000:0000:0000:0000:0000:0001");
  same_identity ("2001:DB8::1",
                 "2001:db8::1");
  same_identity ("203.0.113.7",
                 "::ffff:203.0.113.7");

  fprintf (stderr,
           "-- values that are not bare addresses --\n");
  refused ("");
  refused ("   ");
  refused (",");
  refused ("203.0.113.7:4711");           /* port suffix */
  refused ("[2001:db8::1]");              /* bracketed */
  refused ("[2001:db8::1]:443");
  refused ("unknown");                    /* RFC 7239 */
  refused ("_hidden");
  refused ("client.example.com");         /* hostname */
  refused ("203.0.113.7 198.51.100.9");   /* no comma */
  refused ("fe80::1%eth0");               /* zone id: RFC 6874 */
  refused ("999.1.1.1");
  refused ("203.0.113");
  refused ("::gggg");
  /* Longer than any address; must not overrun the parse buffer. */
  refused (
    "2001:0db8:0000:0000:0000:0000:0000:0001:0002:0003:0004:0005:0006");

  fprintf (stderr,
           "-- the walk --\n");
  test_walk ();

  fprintf (stderr,
           "-- the scheme and authority the base URL is built from --\n");
  test_base ();

  fprintf (stderr,
           "-- a chain long enough to hurt --\n");
  test_long_chain ();

  fprintf (stderr,
           "-- Forwarded parameters and node rendering --\n");
  fwd_param_is ("for=203.0.113.7;proto=https;host=e.com",
                "proto",
                "https");
  fwd_param_is ("for=203.0.113.7;proto=https;host=e.com",
                "host",
                "e.com");
  fwd_param_is ("for=203.0.113.7;host=\"e.com:8443\"",
                "host",
                "e.com:8443");
  /* Unquoted, as mod_headers writes it. */
  fwd_param_is ("for=203.0.113.7;host=e.com:8443",
                "host",
                "e.com:8443");
  fwd_param_is ("for=203.0.113.7",
                "proto",
                NULL);
  /* The leftmost element describes the client's own connection. */
  fwd_param_is ("proto=https, proto=http",
                "proto",
                "https");
  /* What comes back is validated, so that a caller splicing it into a
     header it builds cannot be made to emit a second
     forwarded-element on the client's behalf. */
  fwd_param_is ("for=10.0.0.1;host=\"a, for=1.2.3.4\"",
                "host",
                NULL);
  fwd_param_is ("for=10.0.0.1;proto=\"https;by=x\"",
                "proto",
                NULL);
  fwd_param_is ("for=10.0.0.1;host=\"e.com/../evil\"",
                "host",
                NULL);
  value_is ("203.0.113.7",
            "203.0.113.7");
  value_is ("[2001:db8::1]",
            "\"[2001:db8::1]\"");
  value_is ("e.com:8443",
            "\"e.com:8443\"");
  value_is ("a\"b",
            "\"a\\\"b\"");
  value_is ("a\\b",
            "\"a\\\\b\"");
  value_is ("a, for=1.2.3.4",
            "\"a, for=1.2.3.4\"");
  value_is ("a\nb",
            NULL);
  node_is ("203.0.113.7",
           "203.0.113.7");
  node_is ("2001:db8::1",
           "\"[2001:db8::1]\"");
  node_is (NULL,
           "unknown");
  chain_is ("for=203.0.113.7, for=198.51.100.9",
            "203.0.113.7, 198.51.100.9");
  chain_is ("for=\"[2001:db8::1]:443\", for=10.0.0.1",
            "2001:db8::1, 10.0.0.1");
  /* Not every element is an address, so no chain can be rendered. */
  chain_is ("for=unknown, for=10.0.0.1",
            NULL);
  chain_is ("for=\"unterminated",
            NULL);

  fprintf (stderr,
           "-- the GNUnet policy parsers behave as the loader assumes --\n");
  test_policy_parser ();

  fprintf (stderr,
           "-- matching against a trusted-proxy policy --\n");
  set_policy ("10.0.0.0/8;192.168.0.0/16;",
              "2001:db8::/32;");
  trusted_is ("10.0.0.1", true);
  trusted_is ("10.255.255.255", true);
  trusted_is ("11.0.0.1", false);
  trusted_is ("192.168.0.1", true);
  trusted_is ("192.169.0.1", false);
  trusted_is ("2001:db8::1", true);
  trusted_is ("2001:db9::1", false);
  trusted_is ("203.0.113.7", false);
  /* An IPv4-mapped address is folded to 4 bytes before it gets here,
     so it is matched against the IPv4 list -- which is why IPv4
     proxies belong in TRUSTED_PROXIES and not in TRUSTED_PROXIES6 as
     ::ffff:10.0.0.1. */
  trusted_is ("::ffff:10.0.0.1", true);
  set_policy (NULL, NULL);
  trusted_is ("10.0.0.1", false);   /* no policy: nothing is trusted */

  fprintf (stderr,
           "-- distinct hosts stay distinct --\n");
  {
    void *a = NULL;
    void *b = NULL;
    size_t al = 0;
    size_t bl = 0;

    /* Again: a resolver that stopped understanding dotted quads is a
       regression to report, not a reason to abort the run. */
    if ( (! resolve_xff ("203.0.113.7",
                         &a,
                         &al)) ||
         (! resolve_xff ("203.0.113.8",
                         &b,
                         &bl)) )
    {
      fprintf (stderr,
               "FAIL: 203.0.113.7 / 203.0.113.8 do not resolve at all\n");
      failures++;
    }
    else if (cookie_survives (a,
                         al,
                         b,
                         bl))
    {
      fprintf (stderr,
               "FAIL: cookie for 203.0.113.7 accepted for 203.0.113.8\n");
      failures++;
    }
    else
    {
      fprintf (stderr,
               "  ok: cookie for 203.0.113.7 rejected for 203.0.113.8\n");
    }
    GNUNET_free (a);
    GNUNET_free (b);
  }

  set_policy (NULL,
              NULL);
  if (0 != failures)
  {
    fprintf (stderr,
             "%u check(s) failed\n",
             failures);
    return 1;
  }
  fprintf (stderr,
           "all checks passed\n");
  return 0;
}
