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

  GNU Taler is free software; you can redistribute it and/or
  modify it under the terms of the GNU Affero General Public License
  as published by the Free Software Foundation; either version
  3, or (at your option) any later version.

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

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

/**
 * @author Martin Schanzenbach
 * @author Christian Grothoff
 * @author Marcello Stanisci
 * @file src/backend/paivana-httpd_reverse.c
 * @brief Reverse proxy logic that just forwards the request
 */
#include "platform.h"
#include <curl/curl.h>
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_curl_lib.h>
#include <taler/taler_mhd_lib.h>
#include "paivana-httpd.h"
#include "paivana-httpd_cookie.h"
#include "paivana-httpd_helper.h"
#include "paivana-httpd_reverse.h"


/**
 * Log curl error.
 *
 * @param level log level
 * @param fun name of curl_easy-function that gave the error
 * @param rc return code from curl
 */
#define LOG_CURL_EASY(level,fun,rc) \
        GNUNET_log (level, "%s failed at %s:%d: `%s'\n", fun, __FILE__, \
                    __LINE__, \
                    curl_easy_strerror (rc))

/**
 * Set curl option @a opt on handle @a h to @a val, saying so when
 * libcurl declines.
 *
 * Every option here used to be set and the result thrown away, so a
 * CURLE_OUT_OF_MEMORY on CURLOPT_URL or a CURLOPT_UNIX_SOCKET_PATH
 * libcurl was not built for left no trace at all: the transfer went on
 * with the option simply not in effect, and the operator saw only the
 * eventual 502.  Has to stay a macro because `curl_easy_setopt()' is
 * variadic -- @a val must reach libcurl with the type it was written
 * with.
 */
#define PH_SETOPT(h,opt,val)                                       \
        do {                                                       \
          CURLcode setopt_rc = curl_easy_setopt (h, opt, val);     \
                                                                   \
          if (CURLE_OK != setopt_rc)                               \
            LOG_CURL_EASY (GNUNET_ERROR_TYPE_WARNING,              \
                           "curl_easy_setopt(" # opt ")",          \
                           setopt_rc);                             \
        } while (0)

/**
 * Value of `upstream_content_length` when the upstream declared no
 * usable `Content-Length` for its response.  Not a length any message
 * can have: RFC 9110 §8.6 makes the field a count of octets, and we
 * never buffer more than #GNUNET_MAX_MALLOC_CHECKED of them.
 */
#define PH_NO_CONTENT_LENGTH UINT64_MAX

/**
 * The methods `configure_curl_method()' is willing to forward, in the
 * form RFC 9110 §10.2.1 wants for an `Allow' field: sent on the 405
 * for anything else, and on the OPTIONS we answer ourselves.
 */
#define PH_ALLOWED_METHODS \
        "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS"

/**
 * Value for CURLOPT_CONNECTTIMEOUT, in seconds.
 *
 * Must stay below #PH_upstream_timeout: a connect that hangs is a 502,
 * and it has to be able to say so before the gateway-timeout deadline
 * claims it and calls the same failure a 504.
 */
#define PH_CURL_CONNECT_TIMEOUT_S 30L


/**
 * Size of the buffer MHD fills from our content reader, and the unit
 * in which a chunked response is chunked.  Allocated per queued
 * response, so it is a per-in-flight-request cost on top of
 * #PH_response_buffer_max and wants to stay modest; 16 KiB also
 * matches CURL_MAX_WRITE_SIZE, the most libcurl hands us at once, so
 * one MHD read tends to correspond to one libcurl write.
 */
#define PH_STREAM_BLOCK_SIZE (16 * 1024)

/**
 * Smallest ring we will allocate, and the step the ring grows in.
 * Also the floor under the configured caps: a ring that cannot hold
 * one libcurl delivery would pause with an empty buffer and never be
 * woken, so #ring_put() has to be able to take CURL_MAX_WRITE_SIZE.
 */
#define PH_RING_MIN_SIZE (16 * 1024)


/**
 * How far the client's request body has got.
 *
 * Independent of #DownState: since the origin request now starts
 * before the body has arrived, the two directions run at the same
 * time and neither is a stage of the other.
 */
enum UploadState
{
  /**
   * Initial state.  MHD's first access-handler call (immediately
   * after parsing the request headers, `upload_data_size == 0`)
   * has not yet been observed.  In this state we can still queue a
   * final response before MHD auto-generates a 100 Continue and the
   * client invariably starts with the upload (if any).
   */
  UP_HEADERS_PENDING,

  /**
   * Relaying the client's body: MHD hands us chunks, they go into
   * @e up_ring, libcurl's read callback takes them out.
   */
  UP_STREAMING,

  /**
   * The client's body is fully received.  Whatever is still in
   * @e up_ring has to reach the origin, after which the read
   * callback reports end-of-body.
   */
  UP_CLIENT_DONE,

  /**
   * The origin has consumed the whole body.
   */
  UP_DONE,

  /**
   * The origin transfer is over — it answered early, or failed —
   * while the client is still sending.  Its remaining bytes are read
   * and discarded, because MHD will not let us queue a response
   * while the request body is still arriving and closing the
   * connection instead would throw away the status the origin took
   * the trouble to send.
   */
  UP_DRAINING
};


/**
 * How far the origin's response has got.
 */
enum DownState
{
  /**
   * The final response's header section has not ended yet.  Nothing
   * can be queued to the client: the status is not known.
   */
  DOWN_WAITING_HEADERS,

  /**
   * Headers are in and the response is (or is about to be) queued;
   * body bytes are moving from libcurl through @e down_ring into
   * MHD's content reader.
   */
  DOWN_STREAMING,

  /**
   * The origin delivered its whole body.  Once @e down_ring drains,
   * the content reader reports end of stream.
   */
  DOWN_COMPLETE,

  /**
   * The transfer failed.  If this happened before #DOWN_STREAMING we
   * still owe the client a status (502 or 504); if it happened after,
   * the status is long gone and all we can do is break the framing.
   */
  DOWN_FAILED
};


/**
 * One direction's worth of body bytes in flight, as a ring buffer.
 *
 * A ring rather than a list of chunks: one allocation instead of one
 * per delivery, a hard bound with no fragmentation, and no per-chunk
 * bookkeeping.  Grown lazily from #PH_RING_MIN_SIZE so that a 3 KiB
 * HTML page does not cost a quarter megabyte — which matters now that
 * every response takes this path, not only the large ones.
 */
struct PH_Ring
{
  /**
   * The bytes, @e size of them allocated, or NULL before the first
   * one arrives.
   */
  char *buf;

  /**
   * Bytes allocated in @e buf.
   */
  size_t size;

  /**
   * Bytes currently held, starting at @e off and wrapping.
   */
  size_t len;

  /**
   * Offset of the oldest byte held.
   */
  size_t off;

  /**
   * Largest @e size we will grow to on demand.
   */
  size_t cap;
};


/**
 * A header list
 */
struct HttpResponseHeader
{
  /**
   * DLL
   */
  struct HttpResponseHeader *next;

  /**
   * DLL
   */
  struct HttpResponseHeader *prev;

  /**
   * Header type
   */
  char *type;

  /**
   * Header value
   */
  char *value;
};


/**
 * A structure for socks requests
 */
struct HttpRequest
{

  /**
   * Kept in DLL.
   */
  struct HttpRequest *prev;

  /**
   * Kept in DLL.
   */
  struct HttpRequest *next;

  /**
   * MHD request that triggered us.
   */
  struct MHD_Connection *con;

  /**
   * MHD response object for this request.
   */
  struct MHD_Response *response;

  /**
   * The URL to fetch
   */
  char *url;

  /**
   * Handle to cURL
   */
  CURL *curl;

  /**
   * Job handle for the CURL request in our event loop.
   */
  struct GNUNET_CURL_Job *job;

  /**
   * HTTP request headers for the curl request.
   */
  struct curl_slist *headers;

  /**
   * Headers from response
   */
  struct HttpResponseHeader *header_head;

  /**
   * Headers from response
   */
  struct HttpResponseHeader *header_tail;

  /**
   * Client's request body on its way to the origin.
   */
  struct PH_Ring up_ring;

  /**
   * Origin's response body on its way to the client.
   */
  struct PH_Ring down_ring;

  /**
   * Bytes of request body received from the client so far, checked
   * against #PH_max_request_size.  A client that declares no
   * `Content-Length` can only be caught here.
   */
  uint64_t up_received;

  /**
   * Bytes of response body handed to MHD so far.  Only used for
   * logging a truncated response.
   */
  uint64_t down_sent;

  /**
   * HTTP response code to give to MHD for the response.  Taken from
   * the origin's status line in `curl_check_hdr()` rather than from
   * CURLINFO_RESPONSE_CODE, since it is needed while the transfer is
   * still running.
   */
  unsigned int response_code;

  /**
   * `Content-Length` the upstream declared for its response, or
   * #PH_NO_CONTENT_LENGTH if it declared none we could parse.
   * `curl_check_hdr()` drops the header itself and this value becomes
   * the size of the MHD response instead, which is what makes MHD
   * emit it again — and what makes a HEAD or a 304 report the length
   * of a body it does not send.
   */
  uint64_t upstream_content_length;

  /**
   * How far the client's request body has got.
   */
  enum UploadState up_state;

  /**
   * How far the origin's response has got.
   */
  enum DownState down_state;

  /**
   * Deferred unpause of libcurl, or NULL if none is pending.
   *
   * Never done directly from an MHD callback: GNUNET_CURL_job_unpause()
   * hands over whatever libcurl buffered while paused by calling
   * straight back into our stream callback, which would then be
   * running inside MHD's content reader and could resume a connection
   * that is not suspended.  One scheduler round trip of latency
   * removes the whole class of re-entrancy bug.
   */
  struct GNUNET_SCHEDULER_Task *unpause_task;

  /**
   * Watchdog for #PH_upstream_stall_timeout; see `stall_check()`.
   */
  struct GNUNET_SCHEDULER_Task *stall_task;

  /**
   * When a byte last moved in either direction.  The stall watchdog
   * measures from here rather than being rearmed on every delivery,
   * which would mean a scheduler cancel-and-add per 16 KiB.
   */
  struct GNUNET_TIME_Absolute last_progress;

  /**
   * Did we suspend MHD processing?
   */
  enum GNUNET_GenericReturnValue suspended;

  /**
   * Is @e response ours to destroy?
   *
   * False for the four shared error responses, which outlive any one
   * request.  This used to be inferred by comparing @e response
   * against those four statics, which only worked while they were
   * non-NULL: #PAIVANA_HTTPD_reverse_shutdown() clears them while MHD
   * may still hold queued references, and from then on every
   * comparison matched nothing and cleanup destroyed a response it
   * never owned.
   */
  bool own_response;

  /**
   * Was the client's method HEAD?  A response to HEAD never carries a
   * body, however large a `Content-Length` it declares (RFC 9110
   * §9.3.2), so the truncation check in `curl_download_cb()` must not
   * be applied to it.
   */
  bool head_request;

  /**
   * Has the blank line ending the final response's header section
   * arrived?  Everything `curl_check_hdr()' is handed after that is a
   * TRAILER field: libcurl delivers those through the same callback
   * (it flags them CURLH_TRAILER, which the callback cannot see) and
   * there is no CURLOPT_* to suppress them.
   *
   * RFC 9110 §6.5.1: "A recipient MUST NOT merge a trailer field into
   * a header section unless the recipient understands the
   * corresponding header field definition and that definition
   * explicitly permits and defines how trailer field values can be
   * safely merged."  Practically no field definition does; Set-Cookie
   * and Cache-Control certainly do not.  We also strip `Trailer' as
   * hop-by-hop, so the client would not even be told which fields had
   * been trailers.  Hence: drop them.
   */
  bool headers_complete;

  /**
   * Are we inside the header section of a 1xx interim response?
   *
   * libcurl reports the headers of *every* response it receives,
   * including interim ones, through the same callback.  Merging those
   * into the final response is how a 103 Early Hints `Link' -- which
   * origins commonly build out of the request path -- reappears on the
   * final response, where it is subject to entirely different
   * processing (preload fetches, CSP, cookie jars).
   *
   * RFC 9110 §15.2 says a proxy MUST forward 1xx responses; our fully
   * buffered model cannot, since MHD is handed one finished response.
   * Dropping them is therefore a known deviation -- but dropping is
   * what the status code degrades to gracefully, and merging is not.
   */
  bool interim_response;

  /**
   * Did #PH_upstream_timeout or #PH_upstream_stall_timeout expire on
   * this request?  Decides whether a #DOWN_FAILED that still owes the
   * client a status becomes a 504 or a 502.
   */
  bool upstream_timed_out;

  /**
   * Are we holding libcurl's receive side paused because @e down_ring
   * is full?  While this is set the stall watchdog does not run: the
   * origin is not sending because we told it not to.
   */
  bool curl_recv_paused;

  /**
   * Are we holding libcurl's send side paused because @e up_ring is
   * empty and the client has not sent the rest of its body?
   */
  bool curl_send_paused;

  /**
   * Directions @e unpause_task is to resume.
   */
  bool want_recv_cont;

  /**
   * Directions @e unpause_task is to resume.
   */
  bool want_send_cont;

  /**
   * Has the client's body been refused (413)?  Set on the declared
   * `Content-Length` where there is one, and otherwise once the body
   * actually exceeds #PH_max_request_size.
   */
  bool reject_upload;

  /**
   * Has `PAIVANA_HTTPD_reverse_cleanup()` run?  Half of the ownership
   * handshake with @e reader_gone: MHD calls its completion notifier
   * *before* it destroys the response, so at that moment MHD still
   * holds a content reader whose closure is this very structure.
   * Whoever finishes second frees it.
   */
  bool mhd_gone;

  /**
   * Has the content reader's free callback run, or was one never
   * armed?  Starts true, and is cleared when the streaming response
   * is created.  See @e mhd_gone.
   */
  bool reader_gone;

  /**
   * Deadline for the upstream request, armed while @e job is
   * outstanding.  See #PH_upstream_timeout.
   */
  struct GNUNET_SCHEDULER_Task *timeout_task;

  /**
   * Ready-made `Max-Forwards: N' line to send upstream in place of the
   * client's, with N one less than the value we received; NULL when
   * the client sent none or the method is not OPTIONS.  `con_val_iter'
   * drops the raw header whenever this is set, so the origin sees
   * exactly one, decremented (RFC 9110 §7.6.2).
   */
  char *max_forwards;

  /**
   * Concatenated value of the client's Via header(s), if any.  Per
   * RFC 9110 §7.6.3 a proxy must *append* its own entry to this
   * list, not replace it; we capture the inbound value here before
   * iterating over the other headers and emit our appended Via when
   * building the curl request.
   */
  char *client_via;

  /**
   * Concatenated value of the client's `X-Forwarded-For` header(s),
   * if any.  Only meaningful when #PH_respect_forwarded_headers is
   * set, in which case our own entry is *appended* to this chain
   * rather than replacing it: the origin needs the whole path a
   * request took, and the client address it names has to agree with
   * the one the access cookie was keyed on.  Captured here (like
   * @e client_via) because `con_val_iter` drops the raw header and
   * `append_forwarded_headers` re-emits a single combined one.
   */
  char *client_xff;

  /**
   * Value of the client's `X-Forwarded-Proto` header, if any.  Used
   * in place of the scheme of our own listener when
   * #PH_respect_forwarded_headers is set.  Not a list header: the
   * first value wins.
   */
  char *client_xfp;

  /**
   * Value of the client's `X-Forwarded-Port` header, if any.  Like
   * @e client_xfp this names a single value, so the first one wins.
   * `con_val_iter' drops every `X-Forwarded-*' on the way in, and
   * `append_forwarded_headers' used to re-emit only -For, -Proto and
   * -Host -- so a port the front-end had told us about was consumed
   * for our own base URL and then withheld from the origin, which
   * then generated absolute URLs on the wrong port.
   */
  char *client_xfport;

  /**
   * Value of the client's `X-Forwarded-Host` header, if any.  Used
   * in place of the client's `Host:` when
   * #PH_respect_forwarded_headers is set.  Not a list header: the
   * first value wins.
   */
  char *client_xfh;

  /**
   * Concatenated value of the client's `Forwarded` header(s), if any
   * (RFC 7239).  The standardized form of @e client_xff; like it, our
   * own element is appended when #PH_respect_forwarded_headers is
   * set, and it is replaced outright when it is not.
   */
  char *client_forwarded;

  /**
   * Concatenated value of the client's Connection header(s), if
   * any.  Per RFC 9110 §7.6.1 this lists additional header names
   * that are connection-specific and must not be forwarded; we
   * consult this list in `con_val_iter` to filter such headers out
   * of the upstream request.
   */
  char *client_connection;

  /**
   * Concatenated value of the upstream's Connection header(s), if
   * any.  The symmetric counterpart of @e client_connection for the
   * response direction: the names listed here must not be relayed to
   * the client.  Collected in `curl_check_hdr()` and applied to
   * @e header_head in `curl_download_cb()` — it cannot be applied as
   * the headers stream in, since Connection may arrive *after* the
   * headers it names.
   */
  char *upstream_connection;
};


/**
 * DLL of active HTTP requests.
 */
static struct HttpRequest *hr_head;

/**
 * DLL of active HTTP requests.
 */
static struct HttpRequest *hr_tail;

/**
 * Response we return on cURL failures.
 */
static struct MHD_Response *curl_failure_response;

/**
 * Response we return when the upstream did not answer in time.
 * Distinct from #curl_failure_response so the two can carry the status
 * codes RFC 9110 §15.6.3 and §15.6.5 actually assign them.
 */
static struct MHD_Response *timeout_failure_response;

/**
 * Response we return if the HTTP method is not allowed.
 */
static struct MHD_Response *method_failure_response;

/**
 * Response we return if the upload is too big.
 */
static struct MHD_Response *upload_failure_response;

/**
 * Response we return if we encountered an internal failure.
 */
static struct MHD_Response *internal_failure_response;

/**
 * Response we return to an OPTIONS request we may not forward, that
 * is one whose `Max-Forwards' had reached zero.
 */
static struct MHD_Response *options_response;


/**
 * Create HTML response using @a body
 *
 * @param body UTF-8 encoded 0-terminated body to use
 * @return NULL on error
 */
static struct MHD_Response *
make_html_response (const char *body)
{
  struct MHD_Response *ret;

  ret = MHD_create_response_from_buffer_static (strlen (body),
                                                body);
  if (NULL == ret)
  {
    GNUNET_break (0);
    return NULL;
  }
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (ret,
                                         MHD_HTTP_HEADER_CONTENT_TYPE,
                                         "text/html; charset=utf-8"));
  return ret;
}


bool
PAIVANA_HTTPD_reverse_init (void)
{
  static const char *curl_failure_body =
    "<!DOCTYPE html>\n"
    "<html><head><title>Bad Gateway</title></head>"
    "<body><h1>502 Bad Gateway</h1>"
    "<p>The upstream server could not be reached.</p>"
    "</body></html>\n";
  static const char *timeout_failure_body =
    "<!DOCTYPE html>\n"
    "<html><head><title>Gateway Timeout</title></head>"
    "<body><h1>504 Gateway Timeout</h1>"
    "<p>The upstream server did not respond in time.</p>"
    "</body></html>\n";
  static const char *internal_failure_body =
    "<!DOCTYPE html>\n"
    "<html><head><title>Internal server failure</title></head>"
    "<body><h1>500 Internal Server Failure</h1>"
    "<p>The server experienced an internal failure.</p>"
    "</body></html>\n";
  static const char *upload_failure_body =
    "<!DOCTYPE html>\n"
    "<html><head><title>Content too large</title></head>"
    "<body><h1>413 Content too large</h1>"
    "<p>The size of the body exceeds the limit.</p>"
    "</body></html>\n";
  static const char *method_failure_body =
    "<!DOCTYPE html>\n"
    "<html><head><title>Method not allowed</title></head>"
    "<body><h1>405 Method not allowed</h1>"
    "<p>The HTTP method specified is not allowed.</p>"
    "</body></html>\n";

  curl_failure_response
    = make_html_response (curl_failure_body);
  if (NULL == curl_failure_response)
  {
    GNUNET_break (0);
    return false;
  }
  timeout_failure_response
    = make_html_response (timeout_failure_body);
  if (NULL == timeout_failure_response)
  {
    GNUNET_break (0);
    return false;
  }
  upload_failure_response
    = make_html_response (upload_failure_body);
  if (NULL == upload_failure_response)
  {
    GNUNET_break (0);
    return false;
  }
  method_failure_response
    = make_html_response (method_failure_body);
  if (NULL == method_failure_response)
  {
    GNUNET_break (0);
    return false;
  }
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (method_failure_response,
                                         MHD_HTTP_HEADER_ALLOW,
                                         PH_ALLOWED_METHODS));
  /* Answer for an OPTIONS that arrives with `Max-Forwards: 0': we are
     then the final recipient and must describe ourselves, not ask the
     origin (RFC 9110 §7.6.2). */
  options_response
    = MHD_create_response_from_buffer_static (0,
                                              "");
  if (NULL == options_response)
  {
    GNUNET_break (0);
    return false;
  }
  /* MHD derives `Content-Length: 0' from the zero-size body itself,
     and rejects an explicit one here. */
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (options_response,
                                         MHD_HTTP_HEADER_ALLOW,
                                         PH_ALLOWED_METHODS));
  internal_failure_response
    = make_html_response (internal_failure_body);
  if (NULL == internal_failure_response)
  {
    GNUNET_break (0);
    return false;
  }
  return true;
}


void
PAIVANA_HTTPD_reverse_shutdown (void)
{
  for (struct HttpRequest *hr = hr_head;
       NULL != hr;
       hr = hr->next)
  {
    if (GNUNET_YES == hr->suspended)
    {
      hr->suspended = GNUNET_NO;
      MHD_resume_connection (hr->con);
    }
  }
  /* Dropping *our* reference to the shared responses here is safe even
     though MHD is stopped only afterwards: a queued response is
     reference-counted (MHD_queue_response() takes its own reference),
     so what follows merely relinquishes ownership.  What must not
     happen is a second release from
     #PAIVANA_HTTPD_reverse_cleanup() — hence the explicit
     `own_response` rather than a pointer comparison against the
     statics we are about to NULL out. */
  if (NULL != curl_failure_response)
  {
    MHD_destroy_response (curl_failure_response);
    curl_failure_response = NULL;
  }
  if (NULL != upload_failure_response)
  {
    MHD_destroy_response (upload_failure_response);
    upload_failure_response = NULL;
  }
  if (NULL != method_failure_response)
  {
    MHD_destroy_response (method_failure_response);
    method_failure_response = NULL;
  }
  if (NULL != internal_failure_response)
  {
    MHD_destroy_response (internal_failure_response);
    internal_failure_response = NULL;
  }
  if (NULL != options_response)
  {
    MHD_destroy_response (options_response);
    options_response = NULL;
  }
  if (NULL != timeout_failure_response)
  {
    MHD_destroy_response (timeout_failure_response);
    timeout_failure_response = NULL;
  }
}


/**
 * Is @a name a well-formed HTTP field name, i.e. a non-empty `token'
 * (RFC 9110 section 5.6.2)?
 *
 * What `curl_check_hdr()' calls a field name is whatever stood before
 * the first colon of a line the upstream sent, which is only a field
 * name if the line was a field line.  An obs-fold continuation line
 * that happens to contain a colon yields a "name" of leading
 * whitespace and text; MHD refuses to attach it, but only after we
 * have kept it around for the length of the request and asked.
 *
 * @param name candidate field name, 0-terminated
 * @return true if @a name is a token
 */
static bool
is_field_name (const char *name)
{
  if ('\0' == *name)
    return false;
  for (const char *p = name; '\0' != *p; p++)
  {
    if ( ( ('a' <= *p) && ('z' >= *p) ) ||
         ( ('A' <= *p) && ('Z' >= *p) ) ||
         ( ('0' <= *p) && ('9' >= *p) ) )
      continue;
    if (NULL == strchr ("!#$%&'*+-.^_`|~",
                        *p))
      return false;
  }
  return true;
}


/**
 * Is @a name a hop-by-hop HTTP header name that a proxy must not
 * forward (RFC 9110 section 7.6.1 and RFC 7230, section 6.1).
 * Either peer may name *additional* hop-by-hop headers in its
 * Connection header; those are handled separately in
 * `connection_lists_header()`.
 *
 * @param name header field name
 * @return true if @a name is a hop-by-hop header
 */
static bool
is_hop_by_hop_header (const char *name)
{
  static const char *const hop_headers[] = {
    MHD_HTTP_HEADER_CONNECTION,
    MHD_HTTP_HEADER_KEEP_ALIVE,
    /* RFC 9110 section 7.6.1 names it explicitly among the fields an
       intermediary should remove; libmicrohttpd has no
       MHD_HTTP_HEADER_* for it. */
    "Proxy-Connection",
    MHD_HTTP_HEADER_PROXY_AUTHENTICATE,
    MHD_HTTP_HEADER_PROXY_AUTHORIZATION,
    MHD_HTTP_HEADER_TE,
    MHD_HTTP_HEADER_TRAILER,
    MHD_HTTP_HEADER_TRANSFER_ENCODING,
    MHD_HTTP_HEADER_UPGRADE,
    NULL
  };

  for (unsigned int i = 0; NULL != hop_headers[i]; i++)
    if (0 == strcasecmp (name,
                         hop_headers[i]))
      return true;
  return false;
}


/**
 * Return true if @a name appears (case-insensitively) as a
 * comma-separated element of @a list.  Used to honor RFC 9110
 * §7.6.1: a Connection header lists additional hop-by-hop header
 * names that must not be forwarded.  Applied to the client's
 * Connection list on the request side and to the upstream's on the
 * response side.
 *
 * @param list comma-separated list of header names, may be NULL
 * @param name header name to look for
 * @return true if @a list names @a name
 */
static bool
connection_lists_header (const char *list,
                         const char *name)
{
  size_t namelen;
  const char *p;

  if (NULL == list)
    return false;
  namelen = strlen (name);
  p = list;
  while ('\0' != *p)
  {
    const char *comma;
    size_t len;

    while ( (' ' == *p) ||
            ('\t' == *p) ||
            (',' == *p) )
      p++;
    if ('\0' == *p)
      break;
    comma = strchr (p, ',');
    len = (NULL == comma) ? strlen (p) : (size_t) (comma - p);
    while ( (len > 0) &&
            ( (' ' == p[len - 1]) ||
              ('\t' == p[len - 1]) ) )
      len--;
    if ( (len == namelen) &&
         (0 == strncasecmp (p, name, namelen)) )
      return true;
    if (NULL == comma)
      break;
    p = comma + 1;
  }
  return false;
}


/**
 * Append @a value to the list header accumulated at @a target,
 * joining with ", " per the list-header combining rule of RFC 9110
 * §5.3.  Takes ownership of nothing; @a target is (re)allocated.
 *
 * @param[in,out] target where the combined value is kept, `*target`
 *        may be NULL (in which case @a value is simply duplicated)
 * @param value value to append
 */
static void
append_list_value (char **target,
                   const char *value)
{
  char *combined;

  if (NULL == *target)
  {
    *target = GNUNET_strdup (value);
    return;
  }
  GNUNET_asprintf (&combined,
                   "%s, %s",
                   *target,
                   value);
  GNUNET_free (*target);
  *target = combined;
}


/**
 * Pre-iteration collector: records the client's Via, Connection and
 * X-Forwarded-* headers on @a hr so the subsequent forwarding pass
 * can append to Via (RFC 9110 §7.6.3), honor the hop-by-hop names
 * listed in Connection (RFC 9110 §7.6.1), and extend rather than
 * discard an inbound forwarding chain.  `con_val_iter` drops all of
 * these from the upstream request; `append_forwarded_headers`
 * re-emits them, which is why they have to be captured before that
 * pass runs.
 *
 * Via, Connection and X-Forwarded-For are list headers: multiple
 * values are joined with ", " per RFC 9110 §5.3.  X-Forwarded-Proto
 * and X-Forwarded-Host name a single value each, so a repeat is
 * ignored rather than concatenated into something no longer parseable
 * as a scheme or an authority.
 *
 * @param cls our `struct HttpRequest *`
 * @param kind value kind (unused)
 * @param key header field name
 * @param value header field value
 * @return #MHD_YES to continue iteration
 */
static enum MHD_Result
collect_proxy_state (void *cls,
                     enum MHD_ValueKind kind,
                     const char *key,
                     const char *value)
{
  struct HttpRequest *hr = cls;
  char **target;

  (void) kind;
  if (NULL == value)
    return MHD_YES;
  if (0 == strcasecmp (PH_HEADER_X_FORWARDED_PROTO,
                       key))
  {
    if (NULL == hr->client_xfp)
      hr->client_xfp = GNUNET_strdup (value);
    return MHD_YES;
  }
  if (0 == strcasecmp (PH_HEADER_X_FORWARDED_HOST,
                       key))
  {
    if (NULL == hr->client_xfh)
      hr->client_xfh = GNUNET_strdup (value);
    return MHD_YES;
  }
  if (0 == strcasecmp (PH_HEADER_X_FORWARDED_PORT,
                       key))
  {
    if (NULL == hr->client_xfport)
      hr->client_xfport = GNUNET_strdup (value);
    return MHD_YES;
  }
  if (0 == strcasecmp (MHD_HTTP_HEADER_VIA,
                       key))
    target = &hr->client_via;
  else if (0 == strcasecmp (MHD_HTTP_HEADER_CONNECTION,
                            key))
    target = &hr->client_connection;
  else if (0 == strcasecmp (PH_HEADER_X_FORWARDED_FOR,
                            key))
    target = &hr->client_xff;
  else if (0 == strcasecmp (MHD_HTTP_HEADER_FORWARDED,
                            key))
    target = &hr->client_forwarded;
  else
    return MHD_YES;
  append_list_value (target,
                     value);
  return MHD_YES;
}


/* *************** HTTP handling with cURL ***************** */


/**
 * Is @a txt a port number we are willing to pass on?
 *
 * @param txt candidate text
 * @return true if @a txt is 1..5 digits naming a port in 1..65535
 */
static bool
valid_port_text (const char *txt)
{
  size_t len = strlen (txt);
  unsigned long port;

  if ( (0 == len) ||
       (len > 5) )
    return false;
  for (size_t i = 0; i < len; i++)
    if (! isdigit ((unsigned char) txt[i]))
      return false;
  port = strtoul (txt,
                  NULL,
                  10);
  return (port > 0) && (port <= 65535);
}


/**
 * Return the port of the authority @a host, if it carries one.
 *
 * Understands the bracketed form an IPv6 literal needs (RFC 3986
 * §3.2.2), where the colons of the address itself must not be mistaken
 * for the port separator.
 *
 * @param host authority, such as "example.com:8443" or "[::1]:8443"
 * @return pointer into @a host just past the ':', or NULL if there is
 *         no usable port
 */
static const char *
host_port_suffix (const char *host)
{
  const char *colon;

  if ('[' == host[0])
  {
    const char *close = strchr (host,
                                ']');

    if (NULL == close)
      return NULL;
    colon = (':' == close[1]) ? close + 1 : NULL;
  }
  else
  {
    colon = strchr (host,
                    ':');
    /* More than one colon and no brackets is a bare IPv6 literal,
       which has no port to find. */
    if ( (NULL != colon) &&
         (NULL != strchr (colon + 1,
                          ':')) )
      return NULL;
  }
  if (NULL == colon)
    return NULL;
  return valid_port_text (colon + 1) ? colon + 1 : NULL;
}


/**
 * Discard every response header accumulated for @a hr so far.
 *
 * @param[in,out] hr request whose collected headers to release
 */
static void
free_response_headers (struct HttpRequest *hr)
{
  struct HttpResponseHeader *header;

  while (NULL != (header = hr->header_head))
  {
    GNUNET_CONTAINER_DLL_remove (hr->header_head,
                                 hr->header_tail,
                                 header);
    GNUNET_free (header->type);
    GNUNET_free (header->value);
    GNUNET_free (header);
  }
}


/* ******************* ring buffers ******************** */


/**
 * Release the memory @a r holds.
 *
 * @param[in,out] r ring to empty
 */
static void
ring_free (struct PH_Ring *r)
{
  GNUNET_free (r->buf);
  r->size = 0;
  r->len = 0;
  r->off = 0;
}


/**
 * Make room for at least @a need bytes in @a r, if we may.
 *
 * Doubles from #PH_RING_MIN_SIZE and stops at @e cap, so a small
 * response never costs more than the one delivery it arrived in.
 * Linearises on the way, which is what keeps the wrap arithmetic in
 * `ring_put()` and `ring_get()` to a single branch each.
 *
 * @param[in,out] r ring to grow
 * @param need bytes the caller wants to be able to hold in total
 * @param beyond_cap grow past @e cap if that is what @a need takes;
 *        for the one caller that would otherwise deadlock
 */
static void
ring_grow (struct PH_Ring *r,
           size_t need,
           bool beyond_cap)
{
  size_t ns;
  char *nb;

  if ( (! beyond_cap) &&
       (need > r->cap) )
    need = r->cap;
  if (need <= r->size)
    return;
  ns = (0 == r->size) ? PH_RING_MIN_SIZE : r->size;
  while (ns < need)
    ns *= 2;
  if ( (! beyond_cap) &&
       (ns > r->cap) )
    ns = r->cap;
  /* Deliberately not GNUNET_malloc(): that aborts the whole daemon
     when malloc() returns NULL, dropping every other client because
     one body could not be relayed.  Keeping the smaller ring instead
     merely means more suspend/resume round trips. */
  nb = GNUNET_malloc_large (ns);
  if (NULL == nb)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Could not grow a relay buffer to %llu bytes\n",
                (unsigned long long) ns);
    return;
  }
  if (0 != r->len)
  {
    size_t first = GNUNET_MIN (r->len,
                               r->size - r->off);

    GNUNET_memcpy (nb,
                   &r->buf[r->off],
                   first);
    if (first < r->len)
      GNUNET_memcpy (&nb[first],
                     r->buf,
                     r->len - first);
  }
  GNUNET_free (r->buf);
  r->buf = nb;
  r->size = ns;
  r->off = 0;
}


/**
 * Copy as much of @a data into @a r as fits, growing it if it may.
 *
 * @param[in,out] r ring to append to
 * @param data bytes to append
 * @param n number of bytes in @a data
 * @param beyond_cap see `ring_grow()`
 * @return number of bytes taken, 0 if there was no room at all
 */
static size_t
ring_put (struct PH_Ring *r,
          const char *data,
          size_t n,
          bool beyond_cap)
{
  size_t wpos;
  size_t first;

  if (r->size - r->len < n)
    ring_grow (r,
               r->len + n,
               beyond_cap);
  if (n > r->size - r->len)
    n = r->size - r->len;
  if (0 == n)
    return 0;
  wpos = (r->off + r->len) % r->size;
  first = GNUNET_MIN (n,
                      r->size - wpos);
  GNUNET_memcpy (&r->buf[wpos],
                 data,
                 first);
  if (first < n)
    GNUNET_memcpy (r->buf,
                   &data[first],
                   n - first);
  r->len += n;
  return n;
}


/**
 * Copy all of @a data into @a r, or nothing at all.
 *
 * The all-or-nothing part is load-bearing on the download side.
 * libcurl's write callback cannot take a delivery partially — any
 * short return fails the transfer — so the only other answer is to
 * pause, and after the resume libcurl offers *the same bytes again
 * from the start*.  A partial copy followed by a pause therefore
 * writes its prefix into the ring twice, which shows up as a
 * response that is longer than the origin sent and corrupt from the
 * first pause onwards.
 *
 * @param[in,out] r ring to append to
 * @param data bytes to append
 * @param n number of bytes in @a data
 * @param beyond_cap see `ring_grow()'
 * @return true if all @a n bytes were taken
 */
static bool
ring_put_all (struct PH_Ring *r,
              const char *data,
              size_t n,
              bool beyond_cap)
{
  if (r->size - r->len < n)
    ring_grow (r,
               r->len + n,
               beyond_cap);
  if (r->size - r->len < n)
    return false; /* nothing copied */
  GNUNET_assert (n == ring_put (r,
                                data,
                                n,
                                beyond_cap));
  return true;
}


/**
 * Copy up to @a n bytes out of @a r into @a out.
 *
 * @param[in,out] r ring to consume from
 * @param[out] out where to put them
 * @param n capacity of @a out
 * @return number of bytes copied, 0 if @a r was empty
 */
static size_t
ring_get (struct PH_Ring *r,
          char *out,
          size_t n)
{
  size_t first;

  if (n > r->len)
    n = r->len;
  if (0 == n)
    return 0;
  first = GNUNET_MIN (n,
                      r->size - r->off);
  GNUNET_memcpy (out,
                 &r->buf[r->off],
                 first);
  if (first < n)
    GNUNET_memcpy (&out[first],
                   r->buf,
                   n - first);
  r->off = (r->off + n) % r->size;
  r->len -= n;
  if (0 == r->len)
    r->off = 0; /* keep the next fill linear */
  return n;
}


/* ************ waking the two halves up ************* */


/**
 * Let MHD get on with @a hr: resume the connection if we suspended
 * it, and make sure the daemon is polled again.
 *
 * Safe to call from a libcurl callback — MHD_resume_connection() is
 * documented as callable from any thread and TALER_MHD_daemon_trigger()
 * only re-arms a scheduler task.  The reverse direction, unpausing
 * libcurl, is *not* safe that way; see @e unpause_task.
 *
 * @param[in,out] hr request whose client connection to wake
 */
static void
wake_client (struct HttpRequest *hr)
{
  if (GNUNET_YES != hr->suspended)
    return;
  hr->suspended = GNUNET_NO;
  MHD_resume_connection (hr->con);
  TALER_MHD_daemon_trigger ();
}


/**
 * Stop letting MHD call us about @a hr until something wakes it.
 *
 * @param[in,out] hr request whose client connection to park
 */
static void
suspend_client (struct HttpRequest *hr)
{
  if (GNUNET_YES == hr->suspended)
    return;
  MHD_suspend_connection (hr->con);
  hr->suspended = GNUNET_YES;
}


/**
 * Resume the libcurl directions that were asked for.
 *
 * @param cls the `struct HttpRequest *`
 */
static void
do_unpause (void *cls)
{
  struct HttpRequest *hr = cls;
  bool woke = false;

  hr->unpause_task = NULL;
  if (hr->want_recv_cont)
  {
    hr->want_recv_cont = false;
    hr->curl_recv_paused = false;
    woke = true;
  }
  if (hr->want_send_cont)
  {
    hr->want_send_cont = false;
    hr->curl_send_paused = false;
    woke = true;
  }
  if ( (! woke) ||
       (NULL == hr->job) )
    return;
  /* The origin was never at fault for the interval we held it paused,
     so the stall clock restarts from the resume rather than from the
     last byte that moved. */
  hr->last_progress = GNUNET_TIME_absolute_get ();
  /* May call straight back into our stream or read callback with what
     libcurl buffered while paused, which is precisely why this is a
     task and not a call from inside MHD -- and why the two flags are
     cleared before the call rather than after. */
  GNUNET_CURL_job_set_paused (hr->job,
                              hr->curl_recv_paused,
                              hr->curl_send_paused);
}


/**
 * Arrange for libcurl's receive side to be resumed.
 *
 * @param[in,out] hr request to resume
 */
static void
schedule_recv_cont (struct HttpRequest *hr)
{
  if (! hr->curl_recv_paused)
    return;
  hr->want_recv_cont = true;
  if (NULL == hr->unpause_task)
    hr->unpause_task = GNUNET_SCHEDULER_add_now (&do_unpause,
                                                 hr);
}


/**
 * Arrange for libcurl's send side to be resumed.
 *
 * @param[in,out] hr request to resume
 */
static void
schedule_send_cont (struct HttpRequest *hr)
{
  if (! hr->curl_send_paused)
    return;
  hr->want_send_cont = true;
  if (NULL == hr->unpause_task)
    hr->unpause_task = GNUNET_SCHEDULER_add_now (&do_unpause,
                                                 hr);
}


/* **************** the stall watchdog **************** */


/**
 * Give up on @a hr because the origin transfer failed or stalled.
 *
 * @param[in,out] hr request to fail
 */
static void
fail_transfer (struct HttpRequest *hr)
{
  if (NULL != hr->job)
  {
    GNUNET_CURL_job_cancel (hr->job);
    hr->job = NULL;
  }
  hr->down_state = DOWN_FAILED;
  if (UP_DONE != hr->up_state)
    hr->up_state = UP_DRAINING;
  wake_client (hr);
}


static void
stall_check (void *cls);


/**
 * Arm the stall watchdog for @a hr, unless we are the reason nothing
 * is moving.
 *
 * @param[in,out] hr request to watch
 */
static void
stall_arm (struct HttpRequest *hr)
{
  if (NULL != hr->stall_task)
    return;
  if (NULL == hr->job)
    return;
  hr->stall_task = GNUNET_SCHEDULER_add_delayed (PH_upstream_stall_timeout,
                                                 &stall_check,
                                                 hr);
}


/**
 * Note that a byte moved on @a hr, in either direction.
 *
 * Records the time instead of rearming the watchdog, so that a 200 MiB
 * transfer does not cost a scheduler cancel-and-add per 16 KiB.
 *
 * @param[in,out] hr request that made progress
 */
static void
note_progress (struct HttpRequest *hr)
{
  hr->last_progress = GNUNET_TIME_absolute_get ();
  stall_arm (hr);
}


/**
 * Task checking whether the origin has made any progress within
 * #PH_upstream_stall_timeout.
 *
 * The clock does not run while we hold libcurl's receive side paused:
 * the origin has stopped sending because we told it to, and blaming it
 * for that would kill exactly the transfers this whole change exists
 * to support — a large download to a client on a slow link.  The send
 * side is different: there, nothing moving means the *client* has gone
 * quiet mid-body, and since a suspended connection is off MHD's
 * timeout lists (see `CLIENT_CONNECTION_TIMEOUT`) this watchdog is the
 * only thing that will ever notice.
 *
 * libcurl's own CURLOPT_LOW_SPEED_LIMIT / _TIME would be this, already
 * written and already tested — and it is deliberately not used.
 * `Curl_speedcheck()` exempts a transfer whose *receive* side is
 * paused ("A paused transfer is not qualified for speed checks"), but
 * there is no matching exemption for the send side, while the speed it
 * checks is the sum over both directions of one easy handle.  Measured
 * against libcurl 8.14.1: a receive-paused transfer survives a 15 s
 * pause and a 200 B/s client for 180 s, but a send-paused upload dies
 * at exactly LOW_SPEED_TIME.  Since one handle carries both directions
 * and the option is one setting on it, enabling it at all would make a
 * client on a slow uplink kill its own upload.
 *
 * @param cls the `struct HttpRequest *`
 */
static void
stall_check (void *cls)
{
  struct HttpRequest *hr = cls;
  struct GNUNET_TIME_Relative rem;

  hr->stall_task = NULL;
  if (NULL == hr->job)
    return;
  if (hr->curl_recv_paused)
  {
    /* Our doing, not the origin's.  Look again later. */
    hr->last_progress = GNUNET_TIME_absolute_get ();
    hr->stall_task = GNUNET_SCHEDULER_add_delayed (PH_upstream_stall_timeout,
                                                   &stall_check,
                                                   hr);
    return;
  }
  rem = GNUNET_TIME_absolute_get_remaining (
    GNUNET_TIME_absolute_add (hr->last_progress,
                              PH_upstream_stall_timeout));
  if (0 != rem.rel_value_us)
  {
    hr->stall_task = GNUNET_SCHEDULER_add_delayed (rem,
                                                   &stall_check,
                                                   hr);
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
              "Upstream `%s' moved no data for %s; giving up\n",
              hr->url,
              GNUNET_STRINGS_relative_time_to_string (
                PH_upstream_stall_timeout,
                true));
  hr->upstream_timed_out = true;
  fail_transfer (hr);
}


/**
 * Transform _one_ CURL header (gotten from the request) into
 * MHD format and put it into the response headers list; mostly
 * copies the headers, but makes special adjustments based on
 * control requests.
 *
 * @param cls our `struct HttpRequest *`
 * @param buffer curl buffer with a single
 *        line of header data; not 0-terminated!
 * @param bytes number of bytes in @a buffer
 * @return #GNUNET_OK to carry on with the transfer
 */
static enum GNUNET_GenericReturnValue
curl_check_hdr (void *cls,
                const char *buffer,
                size_t bytes)
{
  struct HttpRequest *hr = cls;
  struct HttpResponseHeader *header;
  size_t len;
  char *ndup;
  const char *hdr_type;
  char *hdr_val;
  char *tok;

  /* Past the end of the final header section: a trailer field.  See
     @e headers_complete. */
  if (hr->headers_complete)
    return GNUNET_OK;
  /* Raw line is not guaranteed to be null-terminated.  */
  ndup = GNUNET_malloc (bytes + 1);
  memcpy (ndup,
          buffer,
          bytes);
  ndup[bytes] = '\0';
  /* Trim the line terminator here rather than leaving it to the value
     parsing below: the blank line and the status line both have to be
     recognised before anything is treated as a field line. */
  len = strlen (ndup);
  while ( (len > 0) &&
          ( ('\r' == ndup[len - 1]) ||
            ('\n' == ndup[len - 1]) ) )
    ndup[--len] = '\0';
  if (0 == len)
  {
    /* Blank line: end of a header section.  Which one it ended is what
       decides whether trailers follow (final) or another response is
       still to come (interim). */
    if (hr->interim_response)
    {
      hr->interim_response = false;
      GNUNET_free (ndup);
      return GNUNET_OK;
    }
    hr->headers_complete = true;
    GNUNET_free (ndup);
    /* The status is known, the header list is complete, and any
       `Content-Length' the origin declared has been recorded: this is
       the earliest moment at which the client can be given a status
       line.  Everything after it is body, relayed as it arrives.

       The response is not queued here.  We are inside a libcurl
       callback, and queuing is MHD's business — and in any case MHD
       will not accept a response while the client's own request body
       is still coming in.  Waking the access handler lets it decide;
       see `queue_stream_response()'. */
    hr->down_state = DOWN_STREAMING;
    if (NULL != hr->timeout_task)
    {
      GNUNET_SCHEDULER_cancel (hr->timeout_task);
      hr->timeout_task = NULL;
    }
    note_progress (hr);
    wake_client (hr);
    return GNUNET_OK;
  }
  if (0 == strncasecmp (ndup,
                        "HTTP/",
                        strlen ("HTTP/")))
  {
    /* A status line, not a field line.  Recognising it is what keeps
       `HTTP/1.1 500 Error: foo' from being parsed as a header whose
       name is "HTTP/1.1 500 Error" -- MHD rejects that name, so the
       only visible effect used to be a remotely triggerable
       assertion in the log. */
    unsigned int code = 0;
    const char *sp = strchr (ndup, ' ');

    if (NULL != sp)
      code = (unsigned int) strtoul (sp + 1,
                                     NULL,
                                     10);
    hr->interim_response = ( (code >= 100) &&
                             (code < MHD_HTTP_OK) );
    if (! hr->interim_response)
    {
      /* Start of a final response's header section.  Discard anything
         accumulated so far: if a 1xx preceded us its fields belong to
         it alone, and libcurl can also replay a full exchange (for
         instance an authentication round) through this callback. */
      free_response_headers (hr);
      hr->upstream_content_length = PH_NO_CONTENT_LENGTH;
      GNUNET_free (hr->upstream_connection);
      hr->upstream_connection = NULL;
      /* Taken from the status line rather than from
         CURLINFO_RESPONSE_CODE, which libgnunetcurl only reads out at
         completion: the response is queued long before that. */
      hr->response_code = code;
    }
    GNUNET_free (ndup);
    return GNUNET_OK;
  }
  if (hr->interim_response)
  {
    PAIVANA_LOG_DEBUG ("Not merging interim-response header line `%s'\n",
                       ndup);
    GNUNET_free (ndup);
    return GNUNET_OK;
  }
  hdr_type = strtok (ndup, ":");
  if (NULL == hdr_type)
  {
    GNUNET_free (ndup);
    return GNUNET_OK;
  }
  if (! is_field_name (hdr_type))
  {
    /* Not a field line at all.  Dropping it here is what keeps the
       name out of `header_head' and out of MHD_add_response_header(),
       which would reject it once per response. */
    GNUNET_break_op (0);
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Upstream `%s' sent `%s' where a field name was due;"
                " dropping the line\n",
                hr->url,
                hdr_type);
    GNUNET_free (ndup);
    return GNUNET_OK;
  }
  hdr_val = strtok (NULL, "\n");
  if (NULL == hdr_val)
  {
    GNUNET_free (ndup);
    return GNUNET_OK;
  }
  while (' ' == *hdr_val)
    hdr_val++;

  /* MHD rejects CR and LF in header values, and forwarding either
     would be response splitting.  HTAB, on the other hand, is legal
     field-content (RFC 9110 section 5.5) and truncating at it corrupts
     values such as a Content-Disposition filename. */
  if (NULL != (tok = strchr (hdr_val, '\n')))
    *tok = '\0';
  if (NULL != (tok = strchr (hdr_val, '\r')))
    *tok = '\0';
  PAIVANA_LOG_DEBUG ("Parsed line: '%s: %s'\n",
                     hdr_type,
                     hdr_val);
  /* Do not relay the "Content-Length:" line itself: MHD emits the
     field from the size the response was created with, and adding a
     second one is a framing error.  The value becomes exactly that
     size in `queue_stream_response()', so the origin's number does
     reach the client — including on a HEAD or a 304, where it states
     the length of a body that is not sent (RFC 9110 §8.6). */
  if (0 == strcasecmp (hdr_type,
                       MHD_HTTP_HEADER_CONTENT_LENGTH))
  {
    char *endptr;
    unsigned long long cl;

    errno = 0;
    cl = strtoull (hdr_val,
                   &endptr,
                   10);
    if ( (0 == errno) &&
         (endptr != hdr_val) &&
         ('\0' == *endptr) )
      hr->upstream_content_length = (uint64_t) cl;
    GNUNET_free (ndup);
    return GNUNET_OK;
  }
  /* Remember the upstream's Connection list; the headers it names
     are connection-specific and get stripped from `header_head' in
     `curl_download_cb()' once the full header block is in (RFC 9110
     §7.6.1).  The header itself is hop-by-hop and dropped below. */
  if (0 == strcasecmp (hdr_type,
                       MHD_HTTP_HEADER_CONNECTION))
    append_list_value (&hr->upstream_connection,
                       hdr_val);
  /* Skip hop-by-hop headers. In particular Transfer-Encoding
     must not leak through: libcurl has already dechunked the
     body for us and MHD will decide whether to re-chunk. */
  if (is_hop_by_hop_header (hdr_type))
  {
    GNUNET_free (ndup);
    return GNUNET_OK;
  }
  if (0 != strlen (hdr_val)) /* Rely in MHD to set those */
  {
    header = GNUNET_new (struct HttpResponseHeader);
    header->type = GNUNET_strdup (hdr_type);
    header->value = GNUNET_strdup (hdr_val);
    /* RFC 9110 section 5.3: "a proxy MUST NOT change the order of
       these field line values when a message is forwarded".  A head
       insert reverses them, which matters for Set-Cookie, Link,
       WWW-Authenticate and Content-Security-Policy. */
    GNUNET_CONTAINER_DLL_insert_tail (hr->header_head,
                                      hr->header_tail,
                                      header);
  }
  GNUNET_free (ndup);
  return GNUNET_OK;
}


/**
 * Can a response with status @a code carry a body at all?
 *
 * RFC 9110 §6.4.1: 1xx, 204 and 304 responses never do.  §8.6 even
 * allows a 304 to state the `Content-Length` the full representation
 * would have, which is precisely a declared length that no body
 * follows — so these have to be excluded before comparing the two.
 *
 * @param code HTTP status code the upstream returned
 * @return true if a response body is to be expected
 */
static bool
status_allows_body (unsigned int code)
{
  return (code >= MHD_HTTP_OK) &&
         (MHD_HTTP_NO_CONTENT != code) &&
         (MHD_HTTP_NOT_MODIFIED != code);
}


/**
 * Task run when the origin did not produce response *headers* within
 * #PH_upstream_timeout.
 *
 * Exists so that a slow origin can be told apart from an unreachable
 * one: both reach `curl_done_cb()' as a response code of 0, and RFC
 * 9110 gives them different status codes (§15.6.5 504 "did not receive
 * a timely response from an upstream server" versus §15.6.3 502).
 * Monitoring and CDN retry policies act on the difference -- a 504 is
 * retried, a 502 usually is not.
 *
 * Cancelled once the header section ends, because from there on the
 * status is already on its way to the client and a deadline can no
 * longer produce one.  `stall_check()' takes over.
 *
 * @param cls the `struct HttpRequest *` that ran out of time
 */
static void
upstream_timeout (void *cls)
{
  struct HttpRequest *hr = cls;

  hr->timeout_task = NULL;
  hr->upstream_timed_out = true;
  GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
              "Upstream did not answer for `%s' within %s\n",
              hr->url,
              GNUNET_STRINGS_relative_time_to_string (PH_upstream_timeout,
                                                      true));
  fail_transfer (hr);
}


/**
 * Handle a chunk of the response body as it arrives from the origin.
 *
 * libcurl's write callback semantics leave no room for a partial take:
 * anything short of @a data_size fails the transfer.  So the ring
 * either has room for the whole delivery or we pause and libcurl
 * offers the same bytes again after the resume.
 *
 * @param cls our `struct HttpRequest *`
 * @param data body bytes
 * @param data_size number of bytes in @a data
 * @return @a data_size, or #GNUNET_CURL_STREAM_PAUSE
 */
static size_t
stream_body_cb (void *cls,
                const void *data,
                size_t data_size)
{
  struct HttpRequest *hr = cls;

  if (0 == data_size)
    return 0;
  if (DOWN_FAILED == hr->down_state)
    return 0; /* fail the transfer; we are already giving up */
  if (! ring_put_all (&hr->down_ring,
                      data,
                      data_size,
                      false))
  {
    if (0 != hr->down_ring.len)
    {
      /* The client has not drained what we already hold.  Stop reading
         from the origin until it does -- this is the backpressure the
         whole design turns on, and the reason the stall watchdog does
         not run while it is in effect.  Nothing has been taken from
         @a data; libcurl offers it again after the resume. */
      hr->curl_recv_paused = true;
      return GNUNET_CURL_STREAM_PAUSE;
    }
    /* Empty and still too small: the configured cap is below one
       libcurl delivery, or the allocation failed.  Pausing here would
       deadlock, since nothing will ever free more room. */
    if (! ring_put_all (&hr->down_ring,
                        data,
                        data_size,
                        true))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Could not buffer %llu bytes of the response to `%s'\n",
                  (unsigned long long) data_size,
                  hr->url);
      return 0; /* fail the transfer */
    }
  }
  note_progress (hr);
  wake_client (hr);
  return data_size;
}


/**
 * The origin transfer is over, one way or the other.
 *
 * @param cls our `struct HttpRequest *`
 * @param response_code HTTP status the origin returned, 0 if the
 *        transfer never got that far
 * @param body unused (NULL for a streaming job)
 * @param body_size unused (0 for a streaming job)
 */
static void
curl_done_cb (void *cls,
              long response_code,
              const void *body,
              size_t body_size)
{
  struct HttpRequest *hr = cls;

  (void) body;
  (void) body_size;
  /* libgnunetcurl frees the easy handle immediately after we return,
     so anything that might unpause it has to stop referring to it
     now. */
  hr->job = NULL;
  if (NULL != hr->timeout_task)
  {
    GNUNET_SCHEDULER_cancel (hr->timeout_task);
    hr->timeout_task = NULL;
  }
  if (NULL != hr->stall_task)
  {
    GNUNET_SCHEDULER_cancel (hr->stall_task);
    hr->stall_task = NULL;
  }
  if (NULL != hr->unpause_task)
  {
    GNUNET_SCHEDULER_cancel (hr->unpause_task);
    hr->unpause_task = NULL;
  }
  if (UP_DONE != hr->up_state)
    /* Whatever the client still owes us is no longer of interest, but
       it has to be read out of the way before MHD will let us queue.
       See #UP_DRAINING. */
    hr->up_state = UP_DRAINING;
  if (0 == response_code)
  {
    /* The transfer failed.  Which of the two failures it was depends
       on how far we had already got, and only we can tell: a chunked
       response cut off before its terminating chunk fails here just
       like an unreachable origin, and libgnunetcurl reports both as
       zero precisely so that the difference is ours to draw. */
    if (hr->headers_complete)
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Upstream `%s' failed after %llu bytes of a response"
                  " already in flight; it can only be truncated\n",
                  hr->url,
                  (unsigned long long) (hr->down_sent
                                        + hr->down_ring.len));
    else
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Failed to receive response from HTTP server\n");
    hr->down_state = DOWN_FAILED;
    wake_client (hr);
    return;
  }
  if (! hr->headers_complete)
  {
    /* libcurl called the transfer a success without ever ending a
       header section.  Should not happen; nothing has been promised
       to the client yet, so an ordinary gateway failure is the honest
       answer. */
    GNUNET_break (0);
    hr->down_state = DOWN_FAILED;
    wake_client (hr);
    return;
  }
  if ( (PH_NO_CONTENT_LENGTH != hr->upstream_content_length) &&
       (! hr->head_request) &&
       (status_allows_body (hr->response_code)) &&
       (hr->upstream_content_length !=
        hr->down_sent + (uint64_t) hr->down_ring.len) )
  {
    /* The origin promised a length and did not deliver it.  The
       promise is already with the client, so the only signal left is
       to break the framing: see `body_reader()'. */
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Upstream declared %llu bytes for `%s' but delivered %llu;"
                " the response is already in flight, so it can only be"
                " truncated\n",
                (unsigned long long) hr->upstream_content_length,
                hr->url,
                (unsigned long long) (hr->down_sent + hr->down_ring.len));
    hr->down_state = DOWN_FAILED;
    wake_client (hr);
    return;
  }
  hr->down_state = DOWN_COMPLETE;
  wake_client (hr);
}


/**
 * cURL callback asking for the next piece of the request body.
 *
 * @param buf where to write the data
 * @param size number of bytes per member
 * @param nmemb number of members available in @a buf
 * @param cls our `struct HttpRequest` that generated the data
 * @return number of bytes copied to @a buf, 0 for end of body,
 *         #CURL_READFUNC_PAUSE or #CURL_READFUNC_ABORT
 */
static size_t
curl_upload_cb (void *buf,
                size_t size,
                size_t nmemb,
                void *cls)
{
  struct HttpRequest *hr = cls;
  size_t len = size * nmemb;
  size_t n;

  n = ring_get (&hr->up_ring,
                buf,
                len);
  if (0 != n)
  {
    note_progress (hr);
    /* Room freed: MHD may hand us more of the client's body. */
    wake_client (hr);
    return n;
  }
  switch (hr->up_state)
  {
  case UP_HEADERS_PENDING:
  case UP_STREAMING:
    /* The client has not caught up.  Stop asking until it does. */
    hr->curl_send_paused = true;
    return CURL_READFUNC_PAUSE;
  case UP_CLIENT_DONE:
    hr->up_state = UP_DONE;
    return 0;
  case UP_DONE:
    return 0;
  case UP_DRAINING:
    /* The client went away, or we gave up on it, in the middle of a
       body whose length we already declared upstream.  We cannot
       deliver what we promised, so the origin has to be told the
       request is broken rather than left waiting for bytes that will
       never come. */
    return CURL_READFUNC_ABORT;
  }
  GNUNET_break (0);
  return CURL_READFUNC_ABORT;
}


/**
 * MHD content reader: hand the client the next piece of the response.
 *
 * The contract, stated in our own terms rather than MHD 1.x's so that
 * the MHD2 port is a rewrite against something written down: return
 * however many bytes fit in @a buf, or suspend the connection and
 * return nothing, or declare the stream finished -- with an error if
 * the body is short of what was promised, cleanly if it is not.
 *
 * @param cls our `struct HttpRequest *`
 * @param pos byte offset in the response (unused; the ring is the
 *        cursor)
 * @param[out] buf where to write
 * @param max capacity of @a buf
 * @return number of bytes written, 0 (suspended),
 *         #MHD_CONTENT_READER_END_OF_STREAM or
 *         #MHD_CONTENT_READER_END_WITH_ERROR
 */
static ssize_t
body_reader (void *cls,
             uint64_t pos,
             char *buf,
             size_t max)
{
  struct HttpRequest *hr = cls;
  size_t n;

  if (pos != hr->down_sent)
  {
    /* The ring *is* the cursor, so it cannot go back to an offset it
       has already handed out.  MHD 1.x never asks -- it caches a
       ready block itself and only ever advances, which is why this
       has held across every case in the suite -- but silently serving
       the wrong bytes would be far worse than failing the response.
       Anything that made this fire would also be the first thing to
       check when porting to MHD2. */
    GNUNET_break (0);
    return MHD_CONTENT_READER_END_WITH_ERROR;
  }
  if (hr->mhd_gone)
  {
    /* Cannot happen: MHD does not read from a request it has already
       reported as completed.  Answering rather than asserting keeps a
       surprise here from taking the daemon down. */
    GNUNET_break (0);
    return MHD_CONTENT_READER_END_WITH_ERROR;
  }
  n = ring_get (&hr->down_ring,
                buf,
                max);
  if (0 != n)
  {
    hr->down_sent += n;
    /* Room freed: let the origin send again. */
    schedule_recv_cont (hr);
    return (ssize_t) n;
  }
  switch (hr->down_state)
  {
  case DOWN_COMPLETE:
    return MHD_CONTENT_READER_END_OF_STREAM;
  case DOWN_FAILED:
    /* Never END_OF_STREAM here: under chunked framing that writes the
       terminating zero-length chunk, which tells the client a
       truncated body was complete.  END_WITH_ERROR closes without it,
       and closes short of a declared Content-Length, both of which RFC
       9112 §8.1.2 requires the client to treat as a failure. */
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Truncating the response to `%s' after %llu bytes\n",
                hr->url,
                (unsigned long long) hr->down_sent);
    return MHD_CONTENT_READER_END_WITH_ERROR;
  case DOWN_WAITING_HEADERS:
  case DOWN_STREAMING:
    /* Nothing to give yet.  Returning 0 without suspending would leave
       MHD in NORMAL_BODY_UNREADY with MHD_EVENT_LOOP_INFO_PROCESS,
       i.e. spinning. */
    suspend_client (hr);
    schedule_recv_cont (hr);
    return 0;
  }
  GNUNET_break (0);
  return MHD_CONTENT_READER_END_WITH_ERROR;
}


/**
 * Release @a hr once neither MHD nor the content reader can reach it.
 *
 * @param[in] hr request to free
 */
static void
reverse_destroy (struct HttpRequest *hr)
{
  ring_free (&hr->up_ring);
  ring_free (&hr->down_ring);
  free_response_headers (hr);
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Proxying of '%s' completely done\n",
              hr->url);
  GNUNET_free (hr->url);
  GNUNET_free (hr->client_via);
  GNUNET_free (hr->client_xff);
  GNUNET_free (hr->client_xfp);
  GNUNET_free (hr->client_xfh);
  GNUNET_free (hr->client_xfport);
  GNUNET_free (hr->max_forwards);
  GNUNET_free (hr->client_forwarded);
  GNUNET_free (hr->client_connection);
  GNUNET_free (hr->upstream_connection);
  GNUNET_CONTAINER_DLL_remove (hr_head,
                               hr_tail,
                               hr);
  GNUNET_free (hr);
}


/**
 * MHD is done with the streaming response and is dropping its
 * reference to our closure.
 *
 * The second half of the ownership handshake: MHD calls its completion
 * notifier -- and therefore `PAIVANA_HTTPD_reverse_cleanup()' -- while
 * it still holds this response, and destroys the response only
 * afterwards (see `connection.c' 1344-1354 and 7523-7532).  Whichever
 * of the two runs last does the freeing.
 *
 * @param cls our `struct HttpRequest *`
 */
static void
body_reader_free (void *cls)
{
  struct HttpRequest *hr = cls;

  hr->reader_gone = true;
  if (hr->mhd_gone)
    reverse_destroy (hr);
}


/* ************** helper functions ************* */

/**
 * Append the field line @a line to the header list we will send
 * upstream for @a hr.
 *
 * Replaces the `hr->headers = curl_slist_append (hr->headers, ...)'
 * idiom, which is wrong in the one case it exists to handle:
 * curl_slist_append() returns NULL on allocation failure *without*
 * touching the list it was given, so the assignment both leaks every
 * header accumulated so far and silently drops them -- the request
 * would go upstream stripped of its Authorization, its Cookie and its
 * Host, and the origin would answer something plausible.  Losing the
 * one line we could not allocate is the smaller error.
 *
 * @param[in,out] hr request whose upstream header list to extend
 * @param line complete "Name: value" line to append
 */
static void
append_curl_header (struct HttpRequest *hr,
                    const char *line)
{
  struct curl_slist *ext;

  ext = curl_slist_append (hr->headers,
                           line);
  if (NULL == ext)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Could not append `%s' to the upstream request headers\n",
                line);
    return;
  }
  hr->headers = ext;
}


/**
 * Build the `Host:` header line to send upstream, naming the
 * authority of @a url.
 *
 * The value is the host and — only if @a url states one explicitly —
 * the port.  Everything else in the authority must be left out: RFC
 * 9110 §7.2 forbids userinfo in `Host:`, so a base URL of
 * "http://user:pw@origin/" still has to yield "Host: origin".  An
 * IPv6 literal conversely has to *keep* its brackets (RFC 3986
 * §3.2.2), as the colons would otherwise be read as a port
 * delimiter.  Both are why the authority is taken from libcurl's URL
 * parser instead of being cut out of the string by hand.
 *
 * @param url full fledged URL
 * @return the header line, or NULL if @a url has no usable
 *         authority; to be freed by the caller
 */
static char *
build_host_header (const char *url)
{
  CURLU *h;
  char *host;
  char *port = NULL;
  char *header;

  h = curl_url ();
  if (NULL == h)
  {
    GNUNET_break (0);
    return NULL;
  }
  if ( (CURLUE_OK !=
        curl_url_set (h,
                      CURLUPART_URL,
                      url,
                      0)) ||
       (CURLUE_OK !=
        curl_url_get (h,
                      CURLUPART_HOST,
                      &host,
                      0)) )
  {
    /* PH_target_server_base_url passed TALER_is_web_url() at
       startup, so failing to parse it here would be a bug. */
    GNUNET_break (0);
    curl_url_cleanup (h);
    return NULL;
  }
  /* Absent unless the URL spells the port out; libcurl elides the
     scheme's default, which is what an origin expects to see. */
  if (CURLUE_OK !=
      curl_url_get (h,
                    CURLUPART_PORT,
                    &port,
                    0))
    port = NULL;
  GNUNET_asprintf (&header,
                   "Host: %s%s%s",
                   host,
                   (NULL != port) ? ":" : "",
                   (NULL != port) ? port : "");
  curl_free (host);
  curl_free (port);
  curl_url_cleanup (h);
  return header;
}


/**
 * Remove our own access cookie from a client-supplied `Cookie:`
 * header value.
 *
 * #PAIVANA_COOKIE_NAME carries the token that proves payment to
 * *this* proxy; relaying it would hand the origin a credential it
 * has no business seeing and that it could replay against us.  Every
 * other cookie-pair is the origin's own and is passed through
 * untouched, in the order and spelling the client used.
 *
 * The name comparison is case-insensitive because that is how MHD
 * matches it when we look the cookie up: anything MHD would accept
 * as our cookie must also be stripped here.
 *
 * @param value raw value of the client's `Cookie:` header
 * @return the remaining cookie string, or NULL if nothing is left to
 *         forward; to be freed by the caller
 */
static char *
strip_paivana_cookie (const char *value)
{
  struct GNUNET_Buffer buf = { 0 };
  const char *pos = value;
  bool empty = true;

  while ('\0' != *pos)
  {
    const char *start = pos;
    const char *end = strchrnul (pos,
                                 ';');
    const char *nend;

    pos = ('\0' == *end) ? end : end + 1;
    /* trim the optional whitespace around the cookie-pair */
    while ( (start < end) &&
            ( (' ' == *start) || ('\t' == *start) ) )
      start++;
    while ( (end > start) &&
            ( (' ' == end[-1]) || ('\t' == end[-1]) ) )
      end--;
    if (start == end)
      continue; /* empty element, e.g. from a stray ';' */
    /* the cookie-name runs up to the first '=' (RFC 6265 §4.2.1) */
    nend = memchr (start,
                   '=',
                   end - start);
    if (NULL == nend)
      nend = end;
    if ( (strlen (PAIVANA_COOKIE_NAME) == (size_t) (nend - start)) &&
         (0 == strncasecmp (start,
                            PAIVANA_COOKIE_NAME,
                            nend - start)) )
    {
      GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                  "Not forwarding our own access cookie upstream\n");
      continue;
    }
    if (! empty)
      GNUNET_buffer_write_str (&buf,
                               "; ");
    empty = false;
    GNUNET_buffer_write (&buf,
                         start,
                         end - start);
  }
  if (empty)
  {
    GNUNET_buffer_clear (&buf);
    return NULL;
  }
  return GNUNET_buffer_reap_str (&buf);
}


/* ************** main loop of cURL interaction ************* */


/**
 * "Filter" function that translates MHD request headers to
 * cURL's.
 *
 * @param cls our `struct HttpRequest`
 * @param kind value kind
 * @param key field key
 * @param value field value
 * @return #MHD_YES to continue to iterate
 */
static enum MHD_Result
con_val_iter (void *cls,
              enum MHD_ValueKind kind,
              const char *key,
              const char *value)
{
  struct HttpRequest *hr = cls;
  char *cookies = NULL;
  char *hdr;

  (void) kind;
  if (NULL == value)
    return MHD_YES;
  if (0 == strcasecmp (MHD_HTTP_HEADER_HOST,
                       key))
  {
    /* We don't take the host header as given in the request.
     * We'll instead put the proxied service's hostname in it*/
    return MHD_YES;
  }
  if (0 == strcasecmp (MHD_HTTP_HEADER_CONTENT_LENGTH,
                       key))
  {
    /* libcurl sets Content-Length itself from CURLOPT_POSTFIELDSIZE
       / CURLOPT_INFILESIZE. */
    return MHD_YES;
  }
  if (0 == strcasecmp (MHD_HTTP_HEADER_EXPECT,
                       key))
  {
    /* libcurl manages Expect: 100-continue on its own. */
    return MHD_YES;
  }
  if ( (NULL != hr->max_forwards) &&
       (0 == strcasecmp (MHD_HTTP_HEADER_MAX_FORWARDS,
                         key)) )
  {
    /* Replaced by the decremented copy `configure_curl_method' built. */
    return MHD_YES;
  }
  if (is_hop_by_hop_header (key))
    return MHD_YES;
  /* RFC 9110 §7.6.1: suppress any header named by the client's
     Connection list (additional hop-by-hop headers). */
  if (connection_lists_header (hr->client_connection,
                               key))
    return MHD_YES;
  if ( (0 == strncasecmp ("X-Forwarded-",
                          key,
                          strlen ("X-Forwarded-"))) ||
       (0 == strcasecmp (MHD_HTTP_HEADER_FORWARDED,
                         key)) )
  {
    /* We will replace these with our own below. */
    return MHD_YES;
  }
  if (0 == strcasecmp (MHD_HTTP_HEADER_VIA,
                       key))
  {
    /* The client's Via was captured in `collect_proxy_state` and
       will be emitted below with our own entry appended (RFC 9110
       §7.6.3).  Drop the raw header here so we don't forward it
       twice. */
    return MHD_YES;
  }
  if (0 == strcasecmp (MHD_HTTP_HEADER_COOKIE,
                       key))
  {
    /* Our access cookie is for us, not for the origin. */
    cookies = strip_paivana_cookie (value);
    if (NULL == cookies)
      return MHD_YES; /* it was the only cookie: no header to forward */
    value = cookies;
  }
  GNUNET_asprintf (&hdr,
                   "%s: %s",
                   key,
                   value);
  GNUNET_free (cookies);
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Adding header `%s' to HTTP request\n",
              hdr);
  append_curl_header (hr,
                      hdr);
  GNUNET_free (hdr);
  return MHD_YES;
}


struct HttpRequest *
PAIVANA_HTTPD_reverse_create (struct MHD_Connection *connection,
                              const char *url)
{
  struct HttpRequest *hr;

  hr = GNUNET_new (struct HttpRequest);
  hr->up_state = UP_HEADERS_PENDING;
  hr->down_state = DOWN_WAITING_HEADERS;
  hr->upstream_content_length = PH_NO_CONTENT_LENGTH;
  /* No content reader is armed until the streaming response is
     created, so until then this half of the handshake is already
     satisfied. */
  hr->reader_gone = true;
  /* A ring that cannot take one libcurl delivery would pause with an
     empty buffer and never be woken; the floor is what rules that
     out, whatever the operator configured. */
  hr->up_ring.cap = GNUNET_MAX (PH_RING_MIN_SIZE,
                                PH_request_buffer_max);
  hr->down_ring.cap = GNUNET_MAX (PH_RING_MIN_SIZE,
                                  PH_response_buffer_max);
  hr->con = connection;
  hr->url = GNUNET_strdup (url);
  GNUNET_CONTAINER_DLL_insert (hr_head,
                               hr_tail,
                               hr);
  return hr;
}


void
PAIVANA_HTTPD_reverse_cleanup (struct HttpRequest *hr)
{
  struct MHD_Response *resp;

  if (NULL != hr->curl)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Resetting cURL handle\n");
    curl_easy_cleanup (hr->curl);
    hr->curl = NULL;
  }
  if (NULL != hr->timeout_task)
  {
    GNUNET_SCHEDULER_cancel (hr->timeout_task);
    hr->timeout_task = NULL;
  }
  if (NULL != hr->stall_task)
  {
    GNUNET_SCHEDULER_cancel (hr->stall_task);
    hr->stall_task = NULL;
  }
  if (NULL != hr->unpause_task)
  {
    GNUNET_SCHEDULER_cancel (hr->unpause_task);
    hr->unpause_task = NULL;
  }
  if (NULL != hr->job)
  {
    /* Also the point of cancelling here rather than letting the
       transfer run out: a download whose client has gone stops
       consuming the origin's bandwidth instead of being read to the
       end into a buffer nobody will ever look at. */
    GNUNET_CURL_job_cancel (hr->job);
    hr->job = NULL;
  }
  if (NULL != hr->headers)
  {
    curl_slist_free_all (hr->headers);
    hr->headers = NULL;
  }
  /* The connection is MHD's again the moment we return from its
     completion notifier, so nothing may reach for it after this. */
  hr->con = NULL;
  hr->suspended = GNUNET_NO;
  resp = hr->response;
  hr->response = NULL;
  if ( (NULL != resp) &&
       (hr->own_response) )
    /* The shared error responses belong to this module, not to the
       request that queued one; releasing them here would take a
       reference we never held.

       Dropping our reference may run the content reader's free
       callback right here, when MHD held no reference of its own --
       which is why @e mhd_gone is still false at this point.  The
       callback then only records that it has fired, and the check
       below does the freeing. */
    MHD_destroy_response (resp);
  hr->mhd_gone = true;
  if (hr->reader_gone)
    reverse_destroy (hr);
}


/**
 * How the client framed its request body, if it sent one at all.
 */
enum ClientBodyKind
{
  /**
   * No body: neither `Content-Length` nor `Transfer-Encoding`, or a
   * `Content-Length` of zero.
   */
  CLIENT_BODY_NONE,

  /**
   * A body of a length the client stated.
   */
  CLIENT_BODY_LENGTH,

  /**
   * A body whose length is not known in advance.
   */
  CLIENT_BODY_CHUNKED,

  /**
   * A `Content-Length` we could not make sense of.  Not the same as
   * #CLIENT_BODY_NONE: MHD has already accepted the request, so
   * something may still arrive.
   */
  CLIENT_BODY_BAD
};


/**
 * Work out whether the client is sending a body, and how it is framed.
 *
 * Replaces looking at how much body had been buffered, which is no
 * longer knowable at the time the origin request is built: it now
 * starts before the body arrives.  Reading the client's own framing is
 * in any case the more correct test — a `Content-Length: 0` is a
 * declared empty body, not the absence of one.
 *
 * @param con MHD connection to look the headers up on
 * @param[out] len set to the declared length for #CLIENT_BODY_LENGTH
 * @return how the body is framed
 */
static enum ClientBodyKind
client_body_kind (struct MHD_Connection *con,
                  uint64_t *len)
{
  const char *cl_str;
  char *endptr;
  unsigned long long cl;

  *len = 0;
  cl_str = MHD_lookup_connection_value (con,
                                        MHD_HEADER_KIND,
                                        MHD_HTTP_HEADER_CONTENT_LENGTH);
  if (NULL == cl_str)
  {
    if (NULL != MHD_lookup_connection_value (con,
                                             MHD_HEADER_KIND,
                                             MHD_HTTP_HEADER_TRANSFER_ENCODING))
      return CLIENT_BODY_CHUNKED;
    return CLIENT_BODY_NONE;
  }
  /* RFC 9110 §8.6: Content-Length = 1*DIGIT.  strtoull() is more
     generous than that -- it skips leading whitespace and accepts a
     sign, so " +5" and even "-1" parse -- and the sign is the one that
     matters here, since strtoull() maps a negative value onto a huge
     unsigned one.  Check the syntax ourselves before believing the
     number. */
  if ('\0' == *cl_str)
    return CLIENT_BODY_BAD;
  for (const char *p = cl_str; '\0' != *p; p++)
    if ( ('0' > *p) ||
         ('9' < *p) )
      return CLIENT_BODY_BAD;
  errno = 0;
  cl = strtoull (cl_str,
                 &endptr,
                 10);
  if (endptr == cl_str)
    return CLIENT_BODY_BAD;
  if (ERANGE == errno)
  {
    /* All digits, but beyond ULLONG_MAX: larger than any limit we
       could be configured with, so it is a rejection rather than a
       parse failure. */
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Rejecting upload: Content-Length `%s' is out of range\n",
                cl_str);
    *len = UINT64_MAX;
    return CLIENT_BODY_LENGTH;
  }
  if (0 == cl)
    return CLIENT_BODY_NONE;
  *len = (uint64_t) cl;
  return CLIENT_BODY_LENGTH;
}


/**
 * Take as much of an upload chunk from the client as @e up_ring will
 * hold, and enforce #PH_max_request_size on the way.
 *
 * Unlike libcurl's write callback, MHD's access handler may take part
 * of what it is given: it panics only if the handler claims *more*
 * than it was handed, and warns only when the handler takes nothing
 * and does not suspend (`connection.c' 4774-4792).  So the ring-full
 * case has to suspend, and does.
 *
 * @param[in,out] hr request we are handling
 * @param[in,out] upload_data_size in: bytes offered, out: bytes left
 * @param upload_data the bytes
 * @return true if the upload may continue, false if it is now over
 *         #PH_max_request_size and the 413 path has to take over
 */
static bool
relay_upload_chunk (struct HttpRequest *hr,
                    size_t *upload_data_size,
                    const char *upload_data)
{
  size_t n;

  if (hr->up_received + (uint64_t) *upload_data_size > PH_max_request_size)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Rejecting upload: body exceeds the %llu byte limit\n",
                (unsigned long long) PH_max_request_size);
    return false;
  }
  n = ring_put (&hr->up_ring,
                upload_data,
                *upload_data_size,
                false);
  hr->up_received += n;
  *upload_data_size -= n;
  if (0 != n)
  {
    note_progress (hr);
    /* The origin may have run out of body to send; tell it there is
       more. */
    schedule_send_cont (hr);
  }
  return true;
}


/**
 * Arrange for a request body to be forwarded under @a meth, and set
 * @a hr->up_state to match.
 *
 * For the methods whose curl option is otherwise bodyless
 * (CURLOPT_HTTPGET, CURLOPT_NOBODY, or a bare CURLOPT_CUSTOMREQUEST),
 * turning on CURLOPT_POST is what enables the read callback;
 * CURLOPT_CUSTOMREQUEST then restores the verb on the wire.  This is
 * the same shape DELETE has always used.  With no body announced
 * nothing is changed, so the common case still goes out as a plain
 * GET.
 *
 * @param[in,out] hr the request
 * @param meth HTTP method to keep on the wire
 * @param kind how the client framed its body
 */
static void
forward_body_verbatim (struct HttpRequest *hr,
                       const char *meth,
                       enum ClientBodyKind kind)
{
  if (CLIENT_BODY_NONE == kind)
  {
    hr->up_state = UP_DONE;
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Forwarding a request body with %s\n",
              meth);
  PH_SETOPT (hr->curl,
             CURLOPT_POST,
             1L);
  PH_SETOPT (hr->curl,
             CURLOPT_CUSTOMREQUEST,
             meth);
  hr->up_state = UP_STREAMING;
}


/**
 * Choose the curl options for the HTTP method we're proxying and set
 * @a hr->up_state accordingly.  Queues an error response and returns
 * the corresponding MHD_Result for unsupported methods; otherwise
 * returns #MHD_YES with @a hr->up_state at #UP_STREAMING (there is a
 * body to relay) or #UP_DONE (there is not).
 *
 * On error, the "curl" handle is set to NULL (!).
 *
 * @param[in,out] hr the request
 * @param con client connection handle from MHD
 * @param meth HTTP method specified by the client
 * @param kind how the client framed its request body
 * @return MHD status to return
 */
static enum MHD_Result
configure_curl_method (struct HttpRequest *hr,
                       struct MHD_Connection *con,
                       const char *meth,
                       enum ClientBodyKind kind)
{
  if (0 == strcasecmp (meth,
                       MHD_HTTP_METHOD_GET))
  {
    PH_SETOPT (hr->curl,
               CURLOPT_HTTPGET,
               1L);
    /* A GET body is unusual and its semantics are undefined (RFC 9110
       §9.3.1), but it is not forbidden and real APIs use it --
       GraphQL-over-GET, several search endpoints.  Dropping it while
       `con_val_iter()' still forwards the client's Content-Type left
       the origin with a request advertising content that never
       arrived, and the client with a parse error it could not
       explain. */
    forward_body_verbatim (hr,
                           MHD_HTTP_METHOD_GET,
                           kind);
    return MHD_YES;
  }
  if (0 == strcasecmp (meth,
                       MHD_HTTP_METHOD_POST))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Crafting a CURL POST request\n");
    PH_SETOPT (hr->curl,
               CURLOPT_POST,
               1L);
    hr->up_state = UP_STREAMING;
    return MHD_YES;
  }
  if (0 == strcasecmp (meth,
                       MHD_HTTP_METHOD_HEAD))
  {
    /* The upstream still states the length the equivalent GET would
       have had; nothing follows it (RFC 9110 §9.3.2). */
    hr->head_request = true;
    PH_SETOPT (hr->curl,
               CURLOPT_NOBODY,
               1L);
    /* HEAD must be forwarded with whatever body the equivalent GET
       carried, or the origin cannot answer the question HEAD asks. */
    forward_body_verbatim (hr,
                           MHD_HTTP_METHOD_HEAD,
                           kind);
    return MHD_YES;
  }
  if (0 == strcasecmp (meth,
                       MHD_HTTP_METHOD_PUT))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Crafting a CURL PUT request\n");
    PH_SETOPT (hr->curl,
               CURLOPT_UPLOAD,
               1L);
    hr->up_state = UP_STREAMING;
    return MHD_YES;
  }
  if (0 == strcasecmp (meth,
                       MHD_HTTP_METHOD_DELETE))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Crafting a CURL DELETE request\n");
    PH_SETOPT (hr->curl,
               CURLOPT_CUSTOMREQUEST,
               "DELETE");
    if (CLIENT_BODY_NONE != kind)
    {
      /* DELETE with a request body is unusual but legal. */
      PH_SETOPT (hr->curl,
                 CURLOPT_POST,
                 1L);
      hr->up_state = UP_STREAMING;
    }
    else
    {
      hr->up_state = UP_DONE;
    }
    return MHD_YES;
  }
  if (0 == strcasecmp (meth,
                       MHD_HTTP_METHOD_PATCH))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Crafting a CURL PATCH request\n");
    /* CURLOPT_POST=1 turns on body upload via the read callback;
       CURLOPT_CUSTOMREQUEST then overrides the verb on the wire. */
    PH_SETOPT (hr->curl,
               CURLOPT_POST,
               1L);
    PH_SETOPT (hr->curl,
               CURLOPT_CUSTOMREQUEST,
               "PATCH");
    hr->up_state = UP_STREAMING;
    return MHD_YES;
  }
  if (0 == strcasecmp (meth,
                       MHD_HTTP_METHOD_OPTIONS))
  {
    const char *mf;

    PH_SETOPT (hr->curl,
               CURLOPT_CUSTOMREQUEST,
               "OPTIONS");
    /* RFC 9110 §7.6.2: "Each intermediary that receives a TRACE or
       OPTIONS request containing a Max-Forwards header field MUST
       check and update its value prior to forwarding ... If the
       received value is zero (0), the intermediary MUST NOT forward
       the request; instead, the intermediary MUST respond as the final
       recipient."  TRACE we reject outright, so OPTIONS is the only
       method this can reach. */
    mf = MHD_lookup_connection_value (con,
                                      MHD_HEADER_KIND,
                                      MHD_HTTP_HEADER_MAX_FORWARDS);
    if (NULL != mf)
    {
      char *endptr;
      unsigned long long hops;

      errno = 0;
      hops = strtoull (mf,
                       &endptr,
                       10);
      if ( (0 != errno) ||
           (endptr == mf) ||
           ('\0' != *endptr) )
      {
        /* Not a valid Max-Forwards.  Neither forwarding it nor acting
           as the final recipient is defined for that, so treat it as
           the client's error rather than guessing. */
        GNUNET_break_op (0);
        curl_easy_cleanup (hr->curl);
        hr->curl = NULL;
        return MHD_queue_response (con,
                                   MHD_HTTP_BAD_REQUEST,
                                   method_failure_response);
      }
      if (0 == hops)
      {
        GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                    "Answering OPTIONS as the final recipient:"
                    " Max-Forwards is exhausted\n");
        curl_easy_cleanup (hr->curl);
        hr->curl = NULL;
        return MHD_queue_response (con,
                                   MHD_HTTP_OK,
                                   options_response);
      }
      GNUNET_asprintf (&hr->max_forwards,
                       "%s: %llu",
                       MHD_HTTP_HEADER_MAX_FORWARDS,
                       hops - 1);
    }
    forward_body_verbatim (hr,
                           MHD_HTTP_METHOD_OPTIONS,
                           kind);
    return MHD_YES;
  }
  /* TRACE leaks headers back to the client; CONNECT is for TLS
     tunnelling and doesn't fit the reverse-proxy model.  Reject
     anything else with a proper 405. */
  GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
              "Unsupported HTTP method `%s'\n",
              meth);
  curl_easy_cleanup (hr->curl);
  hr->curl = NULL;
  return MHD_queue_response (con,
                             MHD_HTTP_METHOD_NOT_ALLOWED,
                             method_failure_response);
}


/**
 * Format the address of the peer we accepted @a con from, or return
 * NULL if it has none to speak of.
 *
 * A Unix-domain peer deliberately yields NULL: there is no address
 * to name, and inventing one ("127.0.0.1") would be both unverifiable
 * and indistinguishable from a genuine loopback client.  The hop is
 * still recorded — in `Via`, which does not require an address.
 *
 * @param con MHD connection we are processing
 * @param[out] buf scratch space to format into
 * @param buf_size number of bytes in @a buf
 * @return @a buf, or NULL if the peer has no IP address
 */
static const char *
peer_address (struct MHD_Connection *con,
              char *buf,
              size_t buf_size)
{
  const union MHD_ConnectionInfo *ci;

  ci = MHD_get_connection_info (con,
                                MHD_CONNECTION_INFO_CLIENT_ADDRESS);
  if ( (NULL == ci) ||
       (NULL == ci->client_addr) )
    return NULL;
  switch (ci->client_addr->sa_family)
  {
  case AF_INET:
    return inet_ntop (
      AF_INET,
      &((const struct sockaddr_in *) ci->client_addr)->sin_addr,
      buf,
      buf_size);
  case AF_INET6:
    return inet_ntop (
      AF_INET6,
      &((const struct sockaddr_in6 *) ci->client_addr)->sin6_addr,
      buf,
      buf_size);
  default:
    /* AF_UNIX: see above. */
    return NULL;
  }
}


/**
 * Determine the scheme the *client* used to reach us, for
 * `X-Forwarded-Proto`.
 *
 * Without #PH_respect_forwarded_headers this is a property of our own
 * listener and nothing else: #TALER_mhd_is_https() is documented as
 * reporting https "either directly or via proxy", i.e. it also
 * believes an inbound `X-Forwarded-Proto`, which an untrusted client
 * can simply assert.  So when we are not behind a trusted proxy we
 * ask MHD about the transport instead — a TLS session exists or it
 * does not.
 *
 * @param hr request we are handling, for the captured inbound headers
 * @param con MHD connection we are processing
 * @param[out] owned set to an allocated string the caller must free,
 *        or NULL if the returned value is a literal or borrowed
 * @return "https" or "http"
 */
static const char *
forwarded_proto (const struct HttpRequest *hr,
                 struct MHD_Connection *con,
                 char **owned)
{
  *owned = NULL;
  if (PH_respect_forwarded_headers)
  {
    if (NULL != hr->client_xfp)
      return hr->client_xfp;
    /* A proxy that speaks only RFC 7239 states the scheme there and
       nowhere else; ignoring it would tell the origin "http" about a
       request the client made over TLS. */
    if (NULL != hr->client_forwarded)
    {
      *owned = PAIVANA_HTTPD_forwarded_param (hr->client_forwarded,
                                              "proto");
      if (NULL != *owned)
        return *owned;
    }
    return (GNUNET_YES == TALER_mhd_is_https (con))
           ? "https" : "http";
  }
  return (NULL != MHD_get_connection_info (con,
                                           MHD_CONNECTION_INFO_PROTOCOL))
         ? "https" : "http";
}


/**
 * Attach the reverse-proxy forwarding headers (X-Forwarded-For /
 * -Proto / -Host and Via) to `hr->headers`.
 *
 * Which of the two roles paivana plays here is decided by
 * #PH_respect_forwarded_headers, the same flag that decides where the
 * access cookie's client address comes from — the two must agree, or
 * the origin is told one thing about a request while we authenticated
 * another:
 *
 * - Without the flag we are the outermost proxy.  Any inbound
 *   `X-Forwarded-*` is a client assertion and is replaced with what we
 *   can see for ourselves, so that a client cannot dictate what the
 *   origin believes about it.
 * - With the flag we are behind a trusted proxy, and the values it
 *   sent are the truthful ones.  We *extend* the chain with the peer
 *   we accepted from rather than discarding it, which is the whole
 *   point of the header (and what RFC 9110 §7.6.3 requires of `Via`
 *   in any case).
 *
 * `Forwarded` (RFC 7239) is treated exactly like `X-Forwarded-For`,
 * and both are emitted: the standardized header for origins that
 * speak it, the de-facto one for the many that only speak that.
 *
 * @param[in,out] hr the request
 * @param con MHD connection we are processing
 * @param ver HTTP version of the client, as given by MHD
 */
static void
append_forwarded_headers (struct HttpRequest *hr,
                          struct MHD_Connection *con,
                          const char *ver)
{
  char ipbuf[INET6_ADDRSTRLEN];
  char *hdr;
  char *owned_proto;
  char *owned_host = NULL;
  char *owned_xff = NULL;
  const char *xff;
  const char *peer;
  const char *proto;
  const char *fhost = NULL;
  const char *via_ver = "1.1";

  peer = peer_address (con,
                       ipbuf,
                       sizeof (ipbuf));
  xff = hr->client_xff;
  if ( (PH_respect_forwarded_headers) &&
       (NULL == xff) &&
       (NULL != hr->client_forwarded) )
  {
    /* A proxy that speaks only RFC 7239 still has to be understood by
       an origin that speaks only X-Forwarded-For. */
    owned_xff = PAIVANA_HTTPD_forwarded_for_chain (hr->client_forwarded);
    xff = owned_xff;
  }
  if ( (PH_respect_forwarded_headers) &&
       (NULL != xff) )
  {
    /* Extend the trusted chain.  If our own peer has no address
       (Unix socket) the chain is passed on as it stands. */
    if (NULL != peer)
      GNUNET_asprintf (&hdr,
                       "%s: %s, %s",
                       PH_HEADER_X_FORWARDED_FOR,
                       xff,
                       peer);
    else
      GNUNET_asprintf (&hdr,
                       "%s: %s",
                       PH_HEADER_X_FORWARDED_FOR,
                       xff);
    append_curl_header (hr,
                        hdr);
    GNUNET_free (hdr);
  }
  else if (NULL != peer)
  {
    GNUNET_asprintf (&hdr,
                     "%s: %s",
                     PH_HEADER_X_FORWARDED_FOR,
                     peer);
    append_curl_header (hr,
                        hdr);
    GNUNET_free (hdr);
  }
  if (NULL != hr->max_forwards)
    append_curl_header (hr,
                        hr->max_forwards);
  proto = forwarded_proto (hr,
                           con,
                           &owned_proto);
  GNUNET_asprintf (&hdr,
                   "%s: %s",
                   PH_HEADER_X_FORWARDED_PROTO,
                   proto);
  append_curl_header (hr,
                      hdr);
  GNUNET_free (hdr);
  if (PH_respect_forwarded_headers)
  {
    fhost = hr->client_xfh;
    if ( (NULL == fhost) &&
         (NULL != hr->client_forwarded) )
    {
      owned_host = PAIVANA_HTTPD_forwarded_param (hr->client_forwarded,
                                                  "host");
      fhost = owned_host;
    }
  }
  if (NULL == fhost)
    fhost = MHD_lookup_connection_value (con,
                                         MHD_HEADER_KIND,
                                         MHD_HTTP_HEADER_HOST);
  if (NULL != fhost)
  {
    GNUNET_asprintf (&hdr,
                     "%s: %s",
                     PH_HEADER_X_FORWARDED_HOST,
                     fhost);
    append_curl_header (hr,
                        hdr);
    GNUNET_free (hdr);
  }
  {
    /* `con_val_iter' drops every X-Forwarded-* on the way in, so
       whatever the front end told us about the port has to be put back
       here or the origin never learns it.  The documented nginx recipe
       sends `X-Forwarded-Port', and `$host' in `X-Forwarded-Host'
       omits the port -- so without this an origin behind
       https://example.com:8443 reconstructs https://example.com/ and
       every absolute URL it generates points at port 443. */
    const char *fport = NULL;

    if (PH_respect_forwarded_headers)
      fport = hr->client_xfport;
    if ( (NULL != fport) &&
         (! valid_port_text (fport)) )
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Ignoring unusable %s value `%s'\n",
                  PH_HEADER_X_FORWARDED_PORT,
                  fport);
      fport = NULL;
    }
    if ( (NULL == fport) &&
         (NULL != fhost) )
      fport = host_port_suffix (fhost);
    if (NULL != fport)
    {
      GNUNET_asprintf (&hdr,
                       "%s: %s",
                       PH_HEADER_X_FORWARDED_PORT,
                       fport);
      append_curl_header (hr,
                          hdr);
      GNUNET_free (hdr);
    }
  }
  /* RFC 7239.  The element we add describes the hop we are completing:
     `for` is the peer we accepted from, `by` the interface we accepted
     on, and `proto`/`host` what the client used.  A Unix-domain peer
     has no address, which §6.3 spells "unknown" -- unlike
     X-Forwarded-For, this header has somewhere to put that, so the hop
     need not go unrecorded. */
  {
    char *elem;
    char *node;
    char *qproto;
    char *qhost = NULL;
    void *ca = NULL;
    size_t ca_len = 0;

    if (NULL != peer)
    {
      struct in_addr a4;
      struct in6_addr a6;

      if (1 == inet_pton (AF_INET,
                          peer,
                          &a4))
      {
        ca = &a4;
        ca_len = sizeof (a4);
      }
      else if (1 == inet_pton (AF_INET6,
                               peer,
                               &a6))
      {
        ca = &a6;
        ca_len = sizeof (a6);
      }
      node = PAIVANA_HTTPD_forwarded_node (ca,
                                           ca_len);
    }
    else
    {
      node = PAIVANA_HTTPD_forwarded_node (NULL,
                                           0);
    }
    /* `proto` and `host` may both be raw client input: without -f they
       come from the Host header, with it from X-Forwarded-Proto /
       X-Forwarded-Host, neither of which we validated.  Splicing them
       in as-is would let a ';' in either open a parameter of ours, or
       a ',' an element of ours, in a header the origin reads as
       paivana's own statement about the hop -- so render them as an
       RFC 7239 section 4 `value` (token, or escaped quoted-string).
       PAIVANA_HTTPD_forwarded_value() returns NULL only for a control
       character, which no `value` production can carry at all; drop
       the parameter rather than emit something unparseable. */
    qproto = PAIVANA_HTTPD_forwarded_value (proto);
    if (NULL != fhost)
      qhost = PAIVANA_HTTPD_forwarded_value (fhost);
    if (NULL == qproto)
      GNUNET_asprintf (&elem,
                       "for=%s;by=_paivana",
                       node);
    else if (NULL != qhost)
      GNUNET_asprintf (&elem,
                       "for=%s;by=_paivana;proto=%s;host=%s",
                       node,
                       qproto,
                       qhost);
    else
      GNUNET_asprintf (&elem,
                       "for=%s;by=_paivana;proto=%s",
                       node,
                       qproto);
    GNUNET_free (qproto);
    GNUNET_free (qhost);
    GNUNET_free (node);
    if ( (PH_respect_forwarded_headers) &&
         (NULL != hr->client_forwarded) )
      GNUNET_asprintf (&hdr,
                       "%s: %s, %s",
                       MHD_HTTP_HEADER_FORWARDED,
                       hr->client_forwarded,
                       elem);
    else
      GNUNET_asprintf (&hdr,
                       "%s: %s",
                       MHD_HTTP_HEADER_FORWARDED,
                       elem);
    GNUNET_free (elem);
    append_curl_header (hr,
                        hdr);
    GNUNET_free (hdr);
  }
  /* MHD hands us e.g. "HTTP/1.1" but Via wants just "1.1". */
  if ( (NULL != ver) &&
       (0 == strncasecmp (ver,
                          "HTTP/",
                          strlen ("HTTP/"))) )
    via_ver = ver + 5;
  if (NULL != hr->client_via)
    GNUNET_asprintf (&hdr,
                     "%s: %s, %s paivana",
                     MHD_HTTP_HEADER_VIA,
                     hr->client_via,
                     via_ver);
  else
    GNUNET_asprintf (&hdr,
                     "%s: %s paivana",
                     MHD_HTTP_HEADER_VIA,
                     via_ver);
  append_curl_header (hr,
                      hdr);
  GNUNET_free (hdr);
  GNUNET_free (owned_proto);
  GNUNET_free (owned_host);
  GNUNET_free (owned_xff);
}


/**
 * Initialize the curl handle, attach the forwarding headers, and hand
 * the request off to the curl multi loop.  On success sets
 * @a hr->up_state to #UP_STREAMING or #UP_DONE and returns #MHD_YES.
 * On any failure queues an appropriate error response and returns its
 * MHD_Result.
 *
 * Called before the client's body has arrived -- that is the point of
 * streaming it -- so what the body will be is read out of the client's
 * own framing rather than out of a buffer.
 *
 * On error, the "curl" handle is set to NULL (!).
 *
 * @param[in,out] hr request we are handling
 * @param con MHD connection handle
 * @param meth HTTP method of the request
 * @param ver HTTP version to use
 * @param kind how the client framed its request body
 * @param body_len declared body length, for #CLIENT_BODY_LENGTH
 * @return MHD status code to return
 */
static enum MHD_Result
start_curl_request (struct HttpRequest *hr,
                    struct MHD_Connection *con,
                    const char *meth,
                    const char *ver,
                    enum ClientBodyKind kind,
                    uint64_t body_len)
{
  enum MHD_Result r;

  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Generating curl request\n");
  hr->curl = curl_easy_init ();
  if (NULL == hr->curl)
  {
    PAIVANA_LOG_ERROR ("Could not init the curl handle\n");
    return MHD_queue_response (con,
                               MHD_HTTP_INTERNAL_SERVER_ERROR,
                               internal_failure_response);
  }

  /* The client's own framing is reproduced upstream: a declared length
     stays a declared length, and a chunked body stays chunked (libcurl
     chunks for us when the size is left unknown).  Both options are
     set because which of the two libcurl reads depends on whether
     `configure_curl_method()' chose CURLOPT_POST or CURLOPT_UPLOAD.

     The casts are load-bearing, not decoration.  Both options are
     CURLOPTTYPE_OFF_T (<curl/curl.h>), so libcurl reads them with
     va_arg(curl_off_t), and curl_off_t is 64 bits on every platform
     (<curl/system.h>: "For any given platform/compiler curl_off_t MUST
     be typedef'ed to a 64-bit wide signed integral data type").
     Passing a size_t is therefore only correct where size_t is 64 bits
     too: on i386 or armv7 libcurl would take four bytes of the length
     and four bytes of whatever the caller left on the stack next to
     them as the high half, and declare a request body of some absurd
     length that never arrives. */
  if (CLIENT_BODY_LENGTH == kind)
  {
    PH_SETOPT (hr->curl,
               CURLOPT_POSTFIELDSIZE_LARGE,
               (curl_off_t) body_len);
    PH_SETOPT (hr->curl,
               CURLOPT_INFILESIZE_LARGE,
               (curl_off_t) body_len);
  }
  else
  {
    PH_SETOPT (hr->curl,
               CURLOPT_POSTFIELDSIZE_LARGE,
               (curl_off_t) -1);
    PH_SETOPT (hr->curl,
               CURLOPT_INFILESIZE_LARGE,
               (curl_off_t) -1);
  }
  PH_SETOPT (hr->curl,
             CURLOPT_FOLLOWLOCATION,
             0L); /* CURLOPTTYPE_LONG: read with va_arg(long) */
  PH_SETOPT (hr->curl,
             CURLOPT_CONNECTTIMEOUT,
             PH_CURL_CONNECT_TIMEOUT_S);
  /* Deliberately no CURLOPT_TIMEOUT and no CURLOPT_LOW_SPEED_LIMIT /
     _TIME.  The first is a deadline on the whole transfer, and a
     500 MiB download legitimately outlives any value that would be
     useful for anything else.  The second looks like exactly the
     watchdog we want and is not; see `stall_check()' for the measured
     reason, which is that it would kill a slow client's own upload. */
  PH_SETOPT (hr->curl,
             CURLOPT_NOSIGNAL,
             1L);
  /* No CURLOPT_PRIVATE here: GNUNET_CURL_job_add_stream() sets it to
     its own `struct GNUNET_CURL_Job *' and reads it back with
     GNUNET_assert() on the result, so anything we stored would be
     overwritten before the transfer starts -- and would abort the
     daemon if it were not.  Nor CURLOPT_WRITEFUNCTION or
     CURLOPT_HEADERFUNCTION, which the streaming job installs its own
     trampolines into; CURLOPT_READFUNCTION it leaves to us. */
  PH_SETOPT (hr->curl,
             CURLOPT_VERBOSE,
             0L); /* CURLOPTTYPE_LONG: read with va_arg(long) */
  PH_SETOPT (hr->curl,
             CURLOPT_READFUNCTION,
             &curl_upload_cb);
  PH_SETOPT (hr->curl,
             CURLOPT_READDATA,
             hr);
  {
    char *curlurl;

    GNUNET_asprintf (&curlurl,
                     "%s%s",
                     PH_target_server_base_url,
                     hr->url);
    PH_SETOPT (hr->curl,
               CURLOPT_URL,
               curlurl);
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Forwarding request to: %s\n",
                curlurl);
    GNUNET_free (curlurl);
  }

  if (NULL != PH_target_server_unixpath)
  {
    PH_SETOPT (hr->curl,
               CURLOPT_UNIX_SOCKET_PATH,
               PH_target_server_unixpath);

    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Forwarding using unixpath: %s\n",
                PH_target_server_unixpath);
  }

  {
    char *host_hdr;

    host_hdr = build_host_header (PH_target_server_base_url);
    if (NULL != host_hdr)
    {
      PAIVANA_LOG_DEBUG ("Faking the host header, %s\n",
                         host_hdr);
      append_curl_header (hr,
                          host_hdr);
      GNUNET_free (host_hdr);
    }
  }

  r = configure_curl_method (hr,
                             con,
                             meth,
                             kind);
  if (NULL == hr->curl)
    return r; /* unsupported method: response already queued */
  /* First pass: collect Via / Connection so `con_val_iter` can
     honor them (append to Via; drop headers named by Connection). */
  MHD_get_connection_values (con,
                             MHD_HEADER_KIND,
                             &collect_proxy_state,
                             hr);
  MHD_get_connection_values (con,
                             MHD_HEADER_KIND,
                             &con_val_iter,
                             hr);
  append_forwarded_headers (hr,
                            con,
                            ver);
  {
    struct GNUNET_CURL_StreamHandlers sh = {
      .hcb = &curl_check_hdr,
      .hcb_cls = hr,
      .scb = &stream_body_cb,
      .scb_cls = hr,
      .jcc = &curl_done_cb,
      .jcc_cls = hr
    };

    hr->job = GNUNET_CURL_job_add_stream (PH_proxy_ctx,
                                          hr->curl,
                                          hr->headers,
                                          &sh);
  }
  hr->curl = NULL;
  if (NULL == hr->job)
  {
    GNUNET_break (0);
    return MHD_queue_response (con,
                               MHD_HTTP_BAD_GATEWAY,
                               curl_failure_response);
  }
  hr->last_progress = GNUNET_TIME_absolute_get ();
  hr->timeout_task = GNUNET_SCHEDULER_add_delayed (PH_upstream_timeout,
                                                   &upstream_timeout,
                                                   hr);
  return MHD_YES;
}


/**
 * Build and queue the streaming response for @a hr.
 *
 * Reached once the origin's header section has ended *and* the
 * client's request body is out of the way; MHD will not accept a
 * response before the latter.  The body itself has not arrived and
 * need not: `body_reader()' supplies it as it does.
 *
 * The size handed to MHD is what decides the framing the client sees,
 * and it is the origin's own: a declared `Content-Length` is relayed
 * as one, and an unknown length becomes chunked for an HTTP/1.1 client
 * and connection-delimited for an HTTP/1.0 one.  Both are what the
 * origin chose, which is a conformance gain over recomputing a length
 * from an assembled buffer.  It is also what makes a HEAD or a 304
 * report the length of the body it does not send: MHD skips the
 * content reader for those but still emits the size (RFC 9110 §9.3.2,
 * §8.6).
 *
 * @param[in,out] hr request whose response to queue
 * @param con MHD client connection to send the response on
 * @return the #MHD_Result of queuing it
 */
static enum MHD_Result
queue_stream_response (struct HttpRequest *hr,
                       struct MHD_Connection *con)
{
  uint64_t size = MHD_SIZE_UNKNOWN;

  if (PH_NO_CONTENT_LENGTH != hr->upstream_content_length)
    size = hr->upstream_content_length;
  hr->response = MHD_create_response_from_callback (size,
                                                    PH_STREAM_BLOCK_SIZE,
                                                    &body_reader,
                                                    hr,
                                                    &body_reader_free);
  if (NULL == hr->response)
  {
    GNUNET_break (0);
    hr->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
    hr->response = internal_failure_response;
    return MHD_queue_response (con,
                               hr->response_code,
                               hr->response);
  }
  hr->own_response = true;
  /* From here MHD holds a closure that outlives its completion
     notifier; see @e mhd_gone. */
  hr->reader_gone = false;
  /* RFC 9110 §7.6.1: drop the headers named by the upstream's
     Connection list.  Deferred to here (rather than done in
     `curl_check_hdr()') because Connection may arrive after the
     headers it names. */
  {
    struct HttpResponseHeader *nxt;

    for (struct HttpResponseHeader *header = hr->header_head;
         NULL != header;
         header = nxt)
    {
      nxt = header->next;
      if (! connection_lists_header (hr->upstream_connection,
                                     header->type))
        continue;
      GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                  "Dropping connection-specific response header %s\n",
                  header->type);
      GNUNET_CONTAINER_DLL_remove (hr->header_head,
                                   hr->header_tail,
                                   header);
      GNUNET_free (header->type);
      GNUNET_free (header->value);
      GNUNET_free (header);
    }
  }
  for (struct HttpResponseHeader *header = hr->header_head;
       NULL != header;
       header = header->next)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Adding MHD response header %s->%s\n",
                header->type,
                header->value);
    if (MHD_YES !=
        MHD_add_response_header (hr->response,
                                 header->type,
                                 header->value))
    {
      /* MHD validates the field name (RFC 9112 §5 token) and the
         value; a malformed one is the upstream's fault, not ours, so
         GNUNET_break_op and not GNUNET_break -- the latter let an
         eccentric origin fill the log with "Assertion failed". */
      GNUNET_break_op (0);
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Upstream `%s' sent a header MHD rejected: `%s'\n",
                  hr->url,
                  header->type);
    }
  }
  return MHD_queue_response (con,
                             hr->response_code,
                             hr->response);
}


/**
 * Main MHD callback for reverse proxy.
 *
 * Two independent state machines, not one: since the origin request
 * now starts before the client's body has arrived, the two directions
 * run at the same time and neither is a phase of the other.  This
 * function owns the upload half; `stream_body_cb()' and
 * `body_reader()' own the download half between them, and meet here
 * only to decide when the response may be queued.
 *
 * @param hr the HTTP request context
 * @param con MHD connection handle
 * @param url the url in the request (unused; kept for ABI symmetry
 *        with the MHD access handler signature)
 * @param meth the HTTP method used ("GET", "PUT", etc.)
 * @param ver the HTTP version string (i.e. "HTTP/1.1")
 * @param upload_data the data being uploaded (excluding HEADERS)
 * @param upload_data_size set initially to the size of the
 *        @a upload_data provided; the method must update this
 *        value to the number of bytes NOT processed;
 * @return #MHD_YES if the connection was handled successfully,
 *         #MHD_NO if the socket must be closed due to a serious
 *         error while handling the request
 */
enum MHD_Result
PAIVANA_HTTPD_reverse (struct HttpRequest *hr,
                       struct MHD_Connection *con,
                       const char *url,
                       const char *meth,
                       const char *ver,
                       const char *upload_data,
                       size_t *upload_data_size)
{
  (void) url;

  if (UP_HEADERS_PENDING == hr->up_state)
  {
    enum ClientBodyKind kind;
    uint64_t body_len;

    /* MHD's HEADERS_PROCESSED callback.  This is our only chance to
       refuse an upload before MHD auto-generates a 100 Continue and
       the client starts sending (RFC 7231 §5.1.1), so a declared
       length over the limit is answered here and now. */
    kind = client_body_kind (con,
                             &body_len);
    if ( (CLIENT_BODY_LENGTH == kind) &&
         (body_len > PH_max_request_size) )
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Rejecting upload: Content-Length %llu exceeds the"
                  " %llu byte limit\n",
                  (unsigned long long) body_len,
                  (unsigned long long) PH_max_request_size);
      hr->reject_upload = true;
      hr->up_state = UP_DRAINING;
      return MHD_queue_response (con,
                                 MHD_HTTP_CONTENT_TOO_LARGE,
                                 upload_failure_response);
    }
    return start_curl_request (hr,
                               con,
                               meth,
                               ver,
                               kind,
                               body_len);
  }

  if (0 != *upload_data_size)
  {
    /* The client is sending its body. */
    if (hr->reject_upload ||
        (UP_DRAINING == hr->up_state))
    {
      /* Nobody wants these bytes -- we refused the upload, or the
         origin finished without them -- but MHD will not let us queue
         a response while the body is still arriving, so they have to
         be read and dropped.  See #UP_DRAINING. */
      *upload_data_size = 0;
      return MHD_YES;
    }
    if (! relay_upload_chunk (hr,
                              upload_data_size,
                              upload_data))
    {
      /* Over #PH_max_request_size with no Content-Length to have
         caught it earlier.  The origin is told the request is broken;
         the client gets the 413 once it stops talking. */
      hr->reject_upload = true;
      hr->up_state = UP_DRAINING;
      if (NULL != hr->job)
      {
        GNUNET_CURL_job_cancel (hr->job);
        hr->job = NULL;
      }
      *upload_data_size = 0;
      return MHD_YES;
    }
    if (0 != *upload_data_size)
      /* @e up_ring is full: what is left will be offered again once
         the origin has taken some of it (`curl_upload_cb()' resumes
         us).  Suspending is not optional -- MHD warns about a handler
         that takes nothing and stays runnable, and spins. */
      suspend_client (hr);
    return MHD_YES;
  }

  /* `*upload_data_size == 0': the client's request body, if it had
     one, is complete.  MHD will accept a response from here on. */
  if (UP_STREAMING == hr->up_state)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Finished receiving %llu bytes of request body\n",
                (unsigned long long) hr->up_received);
    hr->up_state = UP_CLIENT_DONE;
    /* The read callback may be parked waiting for bytes that will now
       never come; let it find that out and report end-of-body. */
    schedule_send_cont (hr);
  }
  if (hr->reject_upload)
    return MHD_queue_response (con,
                               MHD_HTTP_CONTENT_TOO_LARGE,
                               upload_failure_response);
  switch (hr->down_state)
  {
  case DOWN_WAITING_HEADERS:
    /* No status to send yet.  `curl_check_hdr()' or `curl_done_cb()'
       will wake us. */
    suspend_client (hr);
    return MHD_YES;
  case DOWN_STREAMING:
  case DOWN_COMPLETE:
    return queue_stream_response (hr,
                                  con);
  case DOWN_FAILED:
    if (hr->headers_complete)
    {
      /* The status went out with the headers, so there is nothing
         left to say: the response can only be truncated.  Queue it
         anyway -- `body_reader()' ends it with an error immediately,
         which breaks the framing where a bare close would have looked
         like a complete message. */
      return queue_stream_response (hr,
                                    con);
    }
    return MHD_queue_response (con,
                               hr->upstream_timed_out
                               ? MHD_HTTP_GATEWAY_TIMEOUT
                               : MHD_HTTP_BAD_GATEWAY,
                               hr->upstream_timed_out
                               ? timeout_failure_response
                               : curl_failure_response);
  }
  GNUNET_break (0);
  return MHD_queue_response (con,
                             MHD_HTTP_INTERNAL_SERVER_ERROR,
                             internal_failure_response);
}


/* end of paivana-httpd_reverse.c */
