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

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

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

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

/**
 * @file upstream_mhd.c
 * @brief libmicrohttpd-based upstream test server for the
 *        paivana reverse-proxy test suite.  Implements a small
 *        set of canned endpoints (see README).
 */
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <microhttpd.h>

#define SERVER_NAME "mhd"

struct Ctx
{
  char *body;
  size_t body_len;
  size_t body_cap;
};


static void
ctx_free (struct Ctx *c)
{
  free (c->body);
  free (c);
}


struct HdrBuf
{
  char *data;
  size_t len;
  size_t cap;
};


static enum MHD_Result
append_hdr (void *cls,
            enum MHD_ValueKind kind,
            const char *key,
            const char *value)
{
  struct HdrBuf *hb = cls;
  size_t need = strlen (key) + strlen (value) + 4;

  (void) kind;
  if (hb->len + need > hb->cap)
  {
    size_t nc = hb->cap ? hb->cap * 2 : 1024;
    char *nb;

    while (nc < hb->len + need)
      nc *= 2;
    nb = realloc (hb->data, nc);
    if (NULL == nb)
      return MHD_NO;
    hb->data = nb;
    hb->cap = nc;
  }
  hb->len += (size_t) snprintf (hb->data + hb->len,
                                hb->cap - hb->len,
                                "%s: %s\n",
                                key,
                                value);
  return MHD_YES;
}


static struct MHD_Response *
make_text_response (const char *s)
{
  struct MHD_Response *r;

  r = MHD_create_response_from_buffer (strlen (s),
                                       (void *) s,
                                       MHD_RESPMEM_MUST_COPY);
  MHD_add_response_header (r,
                           MHD_HTTP_HEADER_CONTENT_TYPE,
                           "text/plain");
  MHD_add_response_header (r,
                           "X-Upstream",
                           SERVER_NAME);
  return r;
}


static enum MHD_Result
reply_text (struct MHD_Connection *con,
            unsigned int status,
            const char *s)
{
  struct MHD_Response *r = make_text_response (s);
  enum MHD_Result ret;

  ret = MHD_queue_response (con,
                            status,
                            r);
  MHD_destroy_response (r);
  return ret;
}


static enum MHD_Result
reply_bytes (struct MHD_Connection *con,
             unsigned int status,
             const char *buf,
             size_t len)
{
  struct MHD_Response *r;
  enum MHD_Result ret;

  r = MHD_create_response_from_buffer (len,
                                       (void *) buf,
                                       MHD_RESPMEM_MUST_COPY);
  MHD_add_response_header (r,
                           "X-Upstream",
                           SERVER_NAME);
  MHD_add_response_header (r,
                           MHD_HTTP_HEADER_CONTENT_TYPE,
                           "application/octet-stream");
  ret = MHD_queue_response (con,
                            status,
                            r);
  MHD_destroy_response (r);
  return ret;
}


static enum MHD_Result
handler (void *cls,
         struct MHD_Connection *con,
         const char *url,
         const char *method,
         const char *version,
         const char *upload_data,
         size_t *upload_data_size,
         void **con_cls)
{
  struct Ctx *ctx = *con_cls;

  (void) cls;
  (void) version;
  if (NULL == ctx)
  {
    ctx = calloc (1,
                  sizeof (*ctx));
    if (NULL == ctx)
      return MHD_NO;
    *con_cls = ctx;
    return MHD_YES;
  }
  if (0 != *upload_data_size)
  {
    if (ctx->body_len + *upload_data_size > ctx->body_cap)
    {
      size_t n = ctx->body_cap ? ctx->body_cap * 2 : 4096;
      char *nb;

      while (n < ctx->body_len + *upload_data_size)
        n *= 2;
      nb = realloc (ctx->body,
                    n);
      if (NULL == nb)
        return MHD_NO;
      ctx->body = nb;
      ctx->body_cap = n;
    }
    memcpy (ctx->body + ctx->body_len,
            upload_data,
            *upload_data_size);
    ctx->body_len += *upload_data_size;
    *upload_data_size = 0;
    return MHD_YES;
  }

  /* OPTIONS on anything */
  if (0 == strcmp (method,
                   MHD_HTTP_METHOD_OPTIONS))
  {
    struct MHD_Response *r = make_text_response ("");
    enum MHD_Result ret;

    MHD_add_response_header (r,
                             MHD_HTTP_HEADER_ALLOW,
                             "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS");
    ret = MHD_queue_response (con,
                              MHD_HTTP_NO_CONTENT,
                              r);
    MHD_destroy_response (r);
    return ret;
  }

  /* GET /hello */
  if ( (0 == strcmp (method,
                     MHD_HTTP_METHOD_GET) ||
        0 == strcmp (method,
                     MHD_HTTP_METHOD_HEAD)) &&
       (0 == strcmp (url,
                     "/hello")) )
    return reply_text (con,
                       MHD_HTTP_OK,
                       "Hello from " SERVER_NAME "\n");

  /* GET /echo-headers — return all request headers in the body */
  if ( (0 == strcmp (method,
                     MHD_HTTP_METHOD_GET)) &&
       (0 == strcmp (url,
                     "/echo-headers")) )
  {
    struct HdrBuf hb = { 0 };
    enum MHD_Result ret;

    MHD_get_connection_values (con,
                               MHD_HEADER_KIND,
                               &append_hdr,
                               &hb);
    ret = reply_bytes (con,
                       MHD_HTTP_OK,
                       hb.data ? hb.data : "",
                       hb.len);
    free (hb.data);
    return ret;
  }

  /* GET /conn-response — reply naming two of our own response
     headers in Connection; a conforming proxy must strip both
     (RFC 9110 section 7.6.1) but keep X-Keep-Resp. */
  if ( (0 == strcmp (method,
                     MHD_HTTP_METHOD_GET)) &&
       (0 == strcmp (url,
                     "/conn-response")) )
  {
    struct MHD_Response *r = make_text_response ("conn\n");
    enum MHD_Result ret;

    MHD_add_response_header (r,
                             "X-Hop-Before",
                             "must-not-leak");
    MHD_add_response_header (r,
                             MHD_HTTP_HEADER_CONNECTION,
                             "X-Hop-Before, X-Hop-After");
    MHD_add_response_header (r,
                             "X-Hop-After",
                             "must-not-leak");
    MHD_add_response_header (r,
                             "X-Keep-Resp",
                             "survivor");
    ret = MHD_queue_response (con,
                              MHD_HTTP_OK,
                              r);
    MHD_destroy_response (r);
    return ret;
  }

  /* GET /status/NNN */
  if ( (0 == strcmp (method,
                     MHD_HTTP_METHOD_GET)) &&
       (0 == strncmp (url,
                      "/status/",
                      strlen ("/status/"))) )
  {
    char msg[64];
    int s = atoi (url + 8);

    if (s < 100 || s > 599)
      s = 500;
    snprintf (msg,
              sizeof (msg),
              "status %d\n",
              s);
    return reply_text (con,
                       (unsigned int) s,
                       msg);
  }

  /* GET /large/NNN */
  if ( (0 == strcmp (method,
                     MHD_HTTP_METHOD_GET) ||
        0 == strcmp (method,
                     MHD_HTTP_METHOD_HEAD)) &&
       (0 == strncmp (url,
                      "/large/",
                      strlen ("/large/"))) )
  {
    long n = atol (url + 7);
    char *b;
    enum MHD_Result ret;

    if (n < 0)
      n = 0;
    if (n > 10 * 1024 * 1024)
      n = 10 * 1024 * 1024;
    b = malloc ((size_t) n);
    if (NULL == b && n > 0)
      return reply_text (con, 500, "oom\n");
    for (long i = 0; i < n; i++)
      b[i] = 'A' + (char) (i % 26);
    ret = reply_bytes (con,
                       MHD_HTTP_OK,
                       b,
                       (size_t) n);
    free (b);
    return ret;
  }

  /* GET /slow/NNN — sleep NNN ms then respond */
  if ( (0 == strcmp (method,
                     MHD_HTTP_METHOD_GET)) &&
       (0 == strncmp (url,
                      "/slow/",
                      strlen ("/slow/"))) )
  {
    int ms = atoi (url + 6);
    struct timespec ts;

    if (ms < 0)
      ms = 0;
    if (ms > 30000)
      ms = 30000;
    ts.tv_sec = ms / 1000;
    ts.tv_nsec = (ms % 1000) * 1000000L;
    nanosleep (&ts, NULL);
    return reply_text (con,
                       MHD_HTTP_OK,
                       "slept\n");
  }

  /* POST /echo — echoes body */
  if ( (0 == strcmp (method,
                     MHD_HTTP_METHOD_POST)) &&
       (0 == strcmp (url, "/echo")) )
    return reply_bytes (con,
                        MHD_HTTP_OK,
                        ctx->body ? ctx->body : "",
                        ctx->body_len);

  /* POST /upload — returns count */
  if ( (0 == strcmp (method,
                     MHD_HTTP_METHOD_POST)) &&
       (0 == strcmp (url,
                     "/upload")) )
  {
    char msg[64];

    snprintf (msg,
              sizeof (msg),
              "Received %zu bytes\n",
              ctx->body_len);
    return reply_text (con,
                       MHD_HTTP_OK,
                       msg);
  }

  /* PUT /put */
  if ( (0 == strcmp (method,
                     MHD_HTTP_METHOD_PUT)) &&
       (0 == strcmp (url,
                     "/put")) )
  {
    char msg[64];

    snprintf (msg,
              sizeof (msg),
              "PUT received %zu\n",
              ctx->body_len);
    return reply_text (con,
                       MHD_HTTP_OK,
                       msg);
  }

  /* PATCH /patch */
  if ( (0 == strcmp (method,
                     MHD_HTTP_METHOD_PATCH)) &&
       (0 == strcmp (url,
                     "/patch")) )
  {
    char msg[64];

    snprintf (msg,
              sizeof (msg),
              "PATCH received %zu\n",
              ctx->body_len);
    return reply_text (con,
                       MHD_HTTP_OK,
                       msg);
  }

  /* DELETE /item */
  if ( (0 == strcmp (method,
                     "DELETE")) &&
       (0 == strncmp (url,
                      "/item",
                      strlen ("/item"))) )
  {
    struct MHD_Response *r = make_text_response ("");
    enum MHD_Result ret = MHD_queue_response (con,
                                              MHD_HTTP_NO_CONTENT,
                                              r);
    MHD_destroy_response (r);
    return ret;
  }

  return reply_text (con,
                     MHD_HTTP_NOT_FOUND,
                     "not found\n");
}


static void
completed_cb (void *cls,
              struct MHD_Connection *con,
              void **con_cls,
              enum MHD_RequestTerminationCode toe)
{
  struct Ctx *ctx = *con_cls;

  (void) cls;
  (void) con;
  (void) toe;
  if (NULL != ctx)
    ctx_free (ctx);
  *con_cls = NULL;
}


/**
 * Parse @a arg as a TCP port number.
 *
 * atoi(3) would map "garbage" to 0, MHD would then bind an ephemeral
 * port, and the driver's readiness probe would time out five seconds
 * later blaming the port it asked for -- a failure pointing at
 * something other than the mistake.
 *
 * @param arg argument to parse
 * @param[out] port set to the parsed port on success
 * @return 0 on success, -1 if @a arg is not a port number
 */
static int
parse_port (const char *arg,
            unsigned int *port)
{
  char *end;
  long long v;

  errno = 0;
  v = strtoll (arg,
               &end,
               10);
  if ( (0 != errno) ||
       (end == arg) ||
       ('\0' != *end) ||
       (v < 1) ||
       (v > 65535) )
    return -1;
  *port = (unsigned int) v;
  return 0;
}


int
main (int argc, char **argv)
{
  unsigned int port = 8401;
  struct MHD_Daemon *d;
  struct sockaddr_in addr;
  sigset_t quit;
  int sig;

  if ( (argc > 1) &&
       (0 != parse_port (argv[1],
                         &port)) )
  {
    fprintf (stderr,
             "invalid port `%s'\n",
             argv[1]);
    return 1;
  }
  signal (SIGPIPE,
          SIG_IGN);
  /* Block the two shutdown signals here, while we are still
     single-threaded, so that MHD's internal polling thread inherits
     the block and the signal can only be taken by the sigwait below.
     A handler plus `while (run_flag) sleep (1);' cost up to a second
     of shutdown latency per upstream, and a handler plus pause(2)
     would deadlock outright whenever the process-directed signal
     happened to be delivered to the MHD thread. */
  sigemptyset (&quit);
  sigaddset (&quit,
             SIGINT);
  sigaddset (&quit,
             SIGTERM);
  if (0 != sigprocmask (SIG_BLOCK,
                        &quit,
                        NULL))
  {
    perror ("sigprocmask");
    return 1;
  }
  /* Bind the loopback interface explicitly.  This is a test server
     that echoes an arbitrary POST body back and hands out 10 MiB on
     request; it has no business being reachable from the network for
     the duration of `make check'. */
  memset (&addr,
          0,
          sizeof (addr));
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr = htonl (INADDR_LOOPBACK);
  addr.sin_port = htons ((uint16_t) port);
  d = MHD_start_daemon (MHD_USE_INTERNAL_POLLING_THREAD,
                        port,
                        NULL, NULL,
                        &handler, NULL,
                        MHD_OPTION_SOCK_ADDR, &addr,
                        MHD_OPTION_NOTIFY_COMPLETED, &completed_cb, NULL,
                        MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int) 30,
                        MHD_OPTION_END);
  if (NULL == d)
  {
    fprintf (stderr,
             "MHD_start_daemon failed on port %u\n",
             port);
    return 1;
  }
  fprintf (stderr,
           "upstream_mhd listening on port %u\n",
           port);
  fflush (stderr);
  while (0 != sigwait (&quit,
                       &sig))
    /* EINTR is the only failure sigwait can report here; retry. */;
  MHD_stop_daemon (d);
  return 0;
}
