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

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

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

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

/**
 * @author Christian Grothoff
 * @file paivana-httpd_daemon.c
 * @brief daemon functions
 */

#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_cookie.h"
#include "paivana-httpd_daemon.h"
#include "paivana-httpd_helper.h"
#include "paivana-httpd_pay.h"
#include "paivana-httpd_reverse.h"
#include "paivana-httpd_templates.h"


struct RequestContext
{

  /**
   * HTTP connection to the client.
   */
  struct MHD_Connection *connection;

  /**
   * Handle for request forwarding as reverse proxy.
   */
  struct HttpRequest *hr;

  /**
   * Handle for processing actual payment.
   */
  struct PayRequest *hp;

  /**
   * Full request URL.
   */
  char *url;

  /**
   * True if this is a POST to the .well-known/paivana endpoint.
   */
  bool is_paivana;

  /**
   * We are past the paywall, forward to client.
   */
  bool do_forward;
};


/**
 * How many seconds MHD lets a client connection sit idle before it
 * closes it.
 *
 * This is *not* a per-request ceiling: MHD takes a suspended
 * connection off its timeout lists, and every request that waits for
 * the upstream or the merchant backend is suspended for exactly that
 * time.  What it bounds is the phases MHD itself owns -- reading the
 * request line and headers, reading an upload, writing the response --
 * which is what keeps a client that opens a connection and says
 * nothing from holding one of the #PH_connection_limit slots.  The
 * time a request may spend waiting is bounded by the libcurl timeouts
 * in paivana-httpd_reverse.c instead.
 */
#define CLIENT_CONNECTION_TIMEOUT 16


/**
 * Set to true if we started a daemon.
 */
static bool have_daemons;


/**
 * Is the request target something we may decide about and then forward
 * unchanged?
 *
 * Two strings describe one request here, and they are not the same:
 * @a raw is the target exactly as it arrived on the request line --
 * that is what we hand to libcurl -- while @a url is MHD's
 * percent-decoded, query-stripped path, and that is what the WHITELIST
 * expression and the templates' expressions are matched against.
 * Neither is normalized, but libcurl normalizes the URL we give it
 * (CURLOPT_PATH_AS_IS is not set), so "/assets/../premium/x" matches a
 * WHITELIST of "/assets/.*" and is then fetched as "/premium/x".
 * Rather than decide about one resource and fetch another, refuse
 * anything not already in normal form; browsers normalize before
 * sending, so only hand-built requests are affected.
 *
 * Non-origin-form targets are refused for a second reason: such a
 * target -- an absolute-form one, or merely one starting with "@" --
 * is concatenated onto DESTINATION_BASE_URL, where it can re-parse as
 * userinfo (RFC 3986 section 3.2.1) and let the client choose the host
 * we connect to.
 *
 * @param raw request target as it arrived on the request line
 * @param url percent-decoded path MHD gives the handler
 * @return true if the request may proceed
 */
static bool
canonical_request_target (const char *raw,
                          const char *url)
{
  const char *p;

  if ( ('/' != raw[0]) ||
       ('/' != url[0]) )
    return false; /* not origin-form; RFC 9112 section 3.2.1 */
  /* Reject ".", ".." and empty segments: RFC 3986 section 5.2.4's
     remove_dot_segments is exactly what libcurl would apply behind our
     back, after we have already decided. */
  for (p = url; NULL != p; p = strchr (p + 1, '/'))
  {
    const char *seg = p + 1;
    const char *end = strchr (seg, '/');
    size_t len = (NULL == end) ? strlen (seg) : (size_t) (end - seg);

    if (0 == len)
      return ('\0' == *seg); /* a trailing '/' is fine, "//" is not */
    if ( ( (1 == len) &&
           ('.' == seg[0]) ) ||
         ( (2 == len) &&
           ('.' == seg[0]) &&
           ('.' == seg[1]) ) )
      return false;
  }
  return true;
}


/**
 * Main MHD callback for handling requests.
 *
 * @param cls unused
 * @param con MHD connection handle
 * @param url the url in the request
 * @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,
 *        for a POST that fits into memory and that is encoded
 *        with a supported encoding, the POST data will NOT be
 *        given in upload_data and is instead available as
 *        part of MHD_get_connection_values; very large POST
 *        data *will* be made available incrementally in
 *        upload_data)
 * @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;
 * @param con_cls pointer to the `struct RequestContext` that
 *        mhd_log_callback() made for this request
 * @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
 */
static enum MHD_Result
create_response (void *cls,
                 struct MHD_Connection *con,
                 const char *url,
                 const char *meth,
                 const char *ver,
                 const char *upload_data,
                 size_t *upload_data_size,
                 void **con_cls)
{
  struct RequestContext *rc = *con_cls;
  const char *cookie;
  bool ok = false;
  struct GNUNET_Buffer buf;
  char *website;

  (void) cls;
  memset (&buf,
          0,
          sizeof (buf));
  if (! canonical_request_target (rc->url,
                                  url))
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (
      rc->connection,
      MHD_HTTP_BAD_REQUEST,
      TALER_EC_GENERIC_PARAMETER_MALFORMED,
      "request-target must be in origin-form and free of '.', '..'"
      " and empty path segments");
  }
  if ( (! rc->is_paivana) &&
       (0 == strcmp (url,
                     "/.well-known/paivana")) &&
       (0 == strcasecmp (meth,
                         MHD_HTTP_METHOD_POST)) )
  {
    rc->is_paivana = true;
  }
  if (rc->is_paivana)
  {
    if (PH_no_check)
    {
      /* paywall disabled, 501 */
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Paywall disabled, refusing to respond to %s\n",
                  url);
      return TALER_MHD_reply_with_error (rc->connection,
                                         MHD_HTTP_NOT_IMPLEMENTED,
                                         TALER_EC_PAIVANA_PAYWALL_DISABLED,
                                         NULL);
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Client POSTed payment, checking validity\n");
    if (NULL == rc->hp)
      rc->hp = PAIVANA_HTTPD_payment_create (rc->connection);
    return PAIVANA_HTTPD_payment_handle (rc->hp,
                                         upload_data,
                                         upload_data_size);
  }

  if ( (! rc->do_forward) &&
       (PH_MAX_URL_LENGTH < strlen (url)) )
  {
    /* Refuse before either regexec() -- the whitelist just below and
       the templates' expressions further down both run on the
       pre-payment path with a client-controlled subject.  Refusing
       outright (rather than skipping the whitelist) also keeps this
       from being read as "not whitelisted, so charge for it": there
       is nothing to charge for at this length.  Requests already
       destined to be forwarded reach no expression at all, and are
       left alone -- notably every request under -n, where paivana is
       a plain reverse proxy. */
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Refusing %llu byte URL\n",
                (unsigned long long) strlen (url));
    return TALER_MHD_reply_with_error (rc->connection,
                                       MHD_HTTP_URI_TOO_LONG,
                                       TALER_EC_GENERIC_URI_TOO_LONG,
                                       NULL);
  }

  if (PH_have_whitelist_ex && (! rc->do_forward))
  {
    rc->do_forward = (0 ==
                      regexec (&PH_whitelist_ex,
                               url,
                               0,
                               NULL,
                               0));
  }

  if (rc->do_forward)
    goto do_forward;

  if ( (0 == strncmp (url,
                      "/.well-known/paivana/templates/",
                      strlen ("/.well-known/paivana/templates/"))) &&
       ( (0 == strcasecmp (meth,
                           MHD_HTTP_METHOD_GET)) ||
         /* HEAD must be answered like GET (MHD suppresses the body);
            otherwise it falls through to the paywall check below,
            which redirects to this very URL again -- forever. */
         (0 == strcasecmp (meth,
                           MHD_HTTP_METHOD_HEAD)) ) )
  {
    const char *id = &url[strlen ("/.well-known/paivana/templates/")];

    return PAIVANA_HTTPD_return_template (rc->connection,
                                          id);
  }

  if (! PAIVANA_HTTPD_get_base_url (con,
                                    &buf))
  {
    GNUNET_break (0);
    GNUNET_buffer_clear (&buf);
    return TALER_MHD_reply_with_error (
      con,
      MHD_HTTP_BAD_REQUEST,
      TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED,
      "Host or X-Forwarded-Host required");
  }
  GNUNET_buffer_write_str (&buf,
                           url);
  website = GNUNET_buffer_reap_str (&buf);
  cookie = MHD_lookup_connection_value (con,
                                        MHD_COOKIE_KIND,
                                        PAIVANA_COOKIE_NAME);
  if (NULL != cookie)
  {
    void *ca = NULL;
    size_t ca_len = 0;

    /* If we cannot get the client address, we just
       use 0/NULL and log an error. */
    GNUNET_break (PAIVANA_HTTPD_get_client_address (con,
                                                    &ca,
                                                    &ca_len));
    ok = PAIVANA_HTTPD_check_cookie (cookie,
                                     website,
                                     ca_len,
                                     ca);
    /* The cookie value itself is deliberately not logged: it is the
       bearer credential the client paid for, it outlives its own
       Max-Age in the log file, and anyone sharing the client's address
       -- everyone behind one NAT -- could replay it. */
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Client sent an access cookie for %s: %s\n",
                website,
                ok ? "good" : "invalid");
    GNUNET_free (ca);
  }
  if (! ok)
  {
    enum GNUNET_GenericReturnValue ret;

    ret = PAIVANA_HTTPD_search_templates (con,
                                          website);
    if (GNUNET_SYSERR != ret)
    {
      GNUNET_free (website);
      /* #GNUNET_OK only says a response was queued; besides the
         redirect to the paywall that may be a 414 or a 400. */
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "A paywall applies, response queued: %s\n",
                  (GNUNET_OK == ret) ? "ok" : "failed");
      return (GNUNET_OK == ret) ? MHD_YES : MHD_NO;
    }
  }
  GNUNET_free (website);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Request OK, no paywall applies!\n");
  rc->do_forward = true;
do_forward:
  if (NULL == rc->hr)
    rc->hr = PAIVANA_HTTPD_reverse_create (rc->connection,
                                           rc->url);
  return PAIVANA_HTTPD_reverse (rc->hr,
                                con,
                                url,
                                meth,
                                ver,
                                upload_data,
                                upload_data_size);
}


/**
 * Function called when MHD decides that we
 * are done with a request.
 *
 * @param cls NULL
 * @param connection connection handle
 * @param con_cls value as set by the last call to
 *        the MHD_AccessHandlerCallback, should be
 *        our `struct RequestContext *` (created in `mhd_log_callback()`)
 * @param toe reason for request termination (ignored)
 */
static void
mhd_completed_cb (void *cls,
                  struct MHD_Connection *connection,
                  void **con_cls,
                  enum MHD_RequestTerminationCode toe)
{
  struct RequestContext *rc = *con_cls;

  (void) cls;
  (void) connection;
  if (NULL == rc)
    return;
  if (MHD_REQUEST_TERMINATED_COMPLETED_OK != toe)
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "MHD encountered error handling request to %s: %d\n",
                rc->url,
                toe);
  if (NULL != rc->hr)
    PAIVANA_HTTPD_reverse_cleanup (rc->hr);
  if (NULL != rc->hp)
    PAIVANA_HTTPD_payment_destroy (rc->hp);
  GNUNET_free (rc->url);
  GNUNET_free (rc);
  *con_cls = NULL;
}


/**
 * Function called when MHD first processes an incoming connection.
 * Gives us the respective URI information.
 *
 * We use this to associate the `struct MHD_Connection` with our
 * internal `struct HttpRequest` data structure (by checking
 * for matching sockets).
 *
 * @param cls unused
 * @param url the request target exactly as it arrived on the request
 *        line: RAW, i.e. neither percent-decoded nor stripped of its
 *        query, unlike the @a url create_response() is given.  That is
 *        the string we forward, and the difference between the two is
 *        why canonical_request_target() has to see both.
 * @param connection MHD connection object for the request
 * @return the `struct RequestContext` that this @a connection is for
 */
static void *
mhd_log_callback (void *cls,
                  const char *url,
                  struct MHD_Connection *connection)
{
  struct RequestContext *rc;

  (void) cls;
  rc = GNUNET_new (struct RequestContext);
  rc->connection = connection;
  rc->url = GNUNET_strdup (url);
  rc->do_forward = (1 == PH_no_check);
  return rc;
}


/**
 * Listen sockets handed to us by TALER_MHD_listen_bind(), collected
 * before any daemon is started; see #collect_socket().
 */
static int *lsocks;

/**
 * Length of #lsocks.
 */
static unsigned int lsocks_length;


/**
 * Callback invoked on every listen socket: remember it.
 *
 * The daemons are not started here because
 * MHD_OPTION_CONNECTION_LIMIT is per daemon and cannot be changed
 * afterwards, while the budget it has to divide is process-wide --
 * TALER_MHD_listen_bind() starts one daemon per getaddrinfo() result,
 * so an empty BIND_TO yields two, and giving each the whole budget
 * would double it.  We cannot know the divisor until the last socket
 * has been handed over.
 *
 * @param cls unused
 * @param lsock the listen socket
 */
static void
collect_socket (void *cls,
                int lsock)
{
  (void) cls;
  GNUNET_assert (-1 != lsock);
  GNUNET_array_append (lsocks,
                       lsocks_length,
                       lsock);
}


/**
 * Start one MHD daemon on @a lsock, entitled to @a climit concurrent
 * connections.
 *
 * @param lsock the listen socket, ownership passes to MHD on success
 * @param climit connection limit for this daemon
 */
static void
start_daemon (int lsock,
              unsigned int climit)
{
  struct MHD_Daemon *mhd;

  mhd = MHD_start_daemon (
    MHD_USE_DEBUG
    | MHD_ALLOW_SUSPEND_RESUME,
    0,
    NULL, NULL,
    &create_response, NULL,
    /* First, and MHD says so itself: it logs about the options it is
       still parsing, and anything before this one would be printed by
       its built-in logger.  Without the option at all -- MHD_USE_DEBUG
       above is the same bit as MHD_USE_ERROR_LOG -- MHD fprintf()s to
       stderr for the lifetime of the daemon, outside GNUnet's level
       and logfile handling entirely.  Malformed request lines are
       remotely triggerable, so that is client-influenced output
       arriving on stderr regardless of -L ERROR. */
    MHD_OPTION_EXTERNAL_LOGGER, &TALER_MHD_handle_logs, NULL,
    MHD_OPTION_LISTEN_SOCKET,
    lsock,
    MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int) CLIENT_CONNECTION_TIMEOUT,
    MHD_OPTION_CONNECTION_LIMIT, climit,
    MHD_OPTION_PER_IP_CONNECTION_LIMIT, PH_per_ip_connection_limit,
    MHD_OPTION_NOTIFY_COMPLETED, &mhd_completed_cb, NULL,
    MHD_OPTION_URI_LOG_CALLBACK, &mhd_log_callback, NULL,
    MHD_OPTION_END);

  if (NULL == mhd)
  {
    /* Not fatal on its own: TALER_MHD_listen_bind() starts one daemon
       per getaddrinfo() result, so an empty BIND_TO yields a v4 and a
       v6 socket, and losing the whole process because MHD would not
       take the v6 one is the same over-reaction the GNUNET_NO arm in
       PAIVANA_HTTPD_serve_requests() already refuses to make for a
       socket that would not bind.  Serving on nothing at all is still
       fatal; that is decided once, by the caller. */
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Failed to start HTTP daemon on one of the listen sockets\n");
    /* MHD did not take ownership of the socket; close it ourselves. */
    GNUNET_break (0 == close (lsock));
    return;
  }
  have_daemons = true;
  TALER_MHD_daemon_start (mhd);
}


void
PAIVANA_HTTPD_serve_requests ()
{
  enum GNUNET_GenericReturnValue ret;

  ret = TALER_MHD_listen_bind (PH_cfg,
                               "paivana",
                               &collect_socket,
                               NULL);
  if (0 != lsocks_length)
  {
    unsigned int climit
      = GNUNET_MAX (1,
                    PH_connection_limit / lsocks_length);

    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Serving on %u listen socket(s), %u connection(s) each"
                " (%u per client address, 0 = unlimited)\n",
                lsocks_length,
                climit,
                PH_per_ip_connection_limit);
    for (unsigned int i = 0; i < lsocks_length; i++)
      start_daemon (lsocks[i],
                    climit);
    GNUNET_array_grow (lsocks,
                       lsocks_length,
                       0);
  }
  switch (ret)
  {
  case GNUNET_SYSERR:
    /* Configuration error; TALER_MHD_listen_bind() has diagnosed it. */
    PH_global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  case GNUNET_NO:
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Could not open all configured listen sockets\n");
    break;
  case GNUNET_OK:
    break;
  }
  if (! have_daemons)
  {
    /* One policy for both ways a listener can fail to come up: a
       socket that would not bind, and a socket MHD would not serve
       on.  Some is enough; none is not. */
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Not a single listen socket came up, refusing to run\n");
    PH_global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
}
