/*
  This file is part of TALER
  Copyright (C) 2014--2025 Taler Systems SA

  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.

  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
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file mhd_config.c
 * @brief functions to configure and setup MHD
 * @author Florian Dold
 * @author Benedikt Mueller
 * @author Christian Grothoff
 */
#include "taler/platform.h"
#include <gnunet/gnunet_util_lib.h>
#include "taler/taler_mhd_lib.h"


/**
 * Backlog for listen operation.
 */
#define LISTEN_BACKLOG 500


/**
 * Function called for logging by MHD.
 *
 * @param cls closure, NULL
 * @param fm format string (`printf()`-style)
 * @param ap arguments to @a fm
 */
void
TALER_MHD_handle_logs (void *cls,
                       const char *fm,
                       va_list ap)
{
  static int cache;
  char buf[2048];

  (void) cls;
  if (-1 == cache)
    return;
  if (0 == cache)
  {
    if (0 ==
        GNUNET_get_log_call_status (GNUNET_ERROR_TYPE_INFO,
                                    "libmicrohttpd",
                                    __FILE__,
                                    __FUNCTION__,
                                    __LINE__))
    {
      cache = -1;
      return;
    }
  }
  cache = 1;
  vsnprintf (buf,
             sizeof (buf),
             fm,
             ap);
  GNUNET_log_from_nocheck (GNUNET_ERROR_TYPE_INFO,
                           "libmicrohttpd",
                           "%s",
                           buf);
}


/**
 * Open UNIX domain socket for listining at @a unix_path with
 * permissions @a unix_mode.
 *
 * @param unix_path where to listen
 * @param unix_mode access permissions to set
 * @return -1 on error, otherwise the listen socket
 */
static int
open_unix_path (const char *unix_path,
                mode_t unix_mode)
{
  struct GNUNET_NETWORK_Handle *nh;
  struct sockaddr_un *un;

  if (sizeof (un->sun_path) <= strlen (unix_path))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "unixpath `%s' is too long\n",
                unix_path);
    return -1;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Creating listen socket '%s' with mode %o\n",
              unix_path,
              unix_mode);

  if (GNUNET_OK !=
      GNUNET_DISK_directory_create_for_file (unix_path))
  {
    GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
                              "mkdir",
                              unix_path);
  }

  un = GNUNET_new (struct sockaddr_un);
  un->sun_family = AF_UNIX;
  strncpy (un->sun_path,
           unix_path,
           sizeof (un->sun_path) - 1);
  GNUNET_NETWORK_unix_precheck (un);

  if (NULL == (nh = GNUNET_NETWORK_socket_create (AF_UNIX,
                                                  SOCK_STREAM,
                                                  0)))
  {
    GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
                         "socket");
    GNUNET_free (un);
    return -1;
  }

  if (GNUNET_OK !=
      GNUNET_NETWORK_socket_bind (nh,
                                  (void *) un,
                                  sizeof (struct sockaddr_un)))
  {
    GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
                              "bind",
                              unix_path);
    GNUNET_free (un);
    GNUNET_NETWORK_socket_close (nh);
    return -1;
  }
  GNUNET_free (un);
  if (GNUNET_OK !=
      GNUNET_NETWORK_socket_listen (nh,
                                    LISTEN_BACKLOG))
  {
    GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
                         "listen");
    GNUNET_NETWORK_socket_close (nh);
    return -1;
  }

  if (0 != chmod (unix_path,
                  unix_mode))
  {
    GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
                         "chmod");
    GNUNET_NETWORK_socket_close (nh);
    return -1;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "set socket '%s' to mode %o\n",
              unix_path,
              unix_mode);

  /* extract and return actual socket handle from 'nh' */
  {
    int fd;

    fd = GNUNET_NETWORK_get_fd (nh);
    GNUNET_NETWORK_socket_free_memory_only_ (nh);
    return fd;
  }
}


enum GNUNET_GenericReturnValue
TALER_MHD_listen_bind (const struct GNUNET_CONFIGURATION_Handle *cfg,
                       const char *section,
                       TALER_MHD_ListenSocketCallback cb,
                       void *cb_cls)
{
  const char *choices[] = {
    "tcp",
    "unix",
    "systemd",
    NULL
  };
  const char *serve_type;
  enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_choice (cfg,
                                             section,
                                             "SERVE",
                                             choices,
                                             &serve_type))
  {
    GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                               section,
                               "SERVE",
                               "serve type (tcp, unix or systemd) required");
    return GNUNET_SYSERR;
  }


  /* try systemd passing always first */
  {
    const char *listen_pid;
    const char *listen_fds;
    const char *listen_fdn;

    /* check for systemd-style FD passing */
    if ( (NULL != (listen_pid = getenv ("LISTEN_PID"))) &&
         (getpid () ==
          strtol (listen_pid,
                  NULL,
                  10)) &&
         (NULL != (listen_fds = getenv ("LISTEN_FDS"))) &&
         (NULL != (listen_fdn = getenv ("LISTEN_FDNAMES"))) )
    {
      int off = 3;
      unsigned int cnt;
      char dummy;

      if (0 != strcmp (serve_type,
                       "systemd"))
      {
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Using systemd activation due to environment variables. Set SERVE=systemd to disable this warning!\n");
      }
      else
      {
        ret = GNUNET_NO;
      }
      if (1 != sscanf (listen_fds,
                       "%u%c",
                       &cnt,
                       &dummy))
      {
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Invalid number `%s' of systemd sockets specified\n",
                    listen_fds);
      }
      else
      {
        char *fdns;

        fdns = GNUNET_strdup (listen_fdn);
        for (const char *tok = strtok (fdns,
                                       ":");
             cnt-- > 0;
             tok = strtok (NULL,
                           ":"))
        {
          int fh = off++;
          int flags;

          flags = fcntl (fh,
                         F_GETFD);
          if ( (-1 == flags) &&
               (EBADF == errno) )
          {
            GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                        "Bad listen socket %s (%d) passed, ignored\n",
                        tok,
                        fh);
            continue;
          }
          flags |= FD_CLOEXEC;
          if (0 != fcntl (fh,
                          F_SETFD,
                          flags))
            GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
                                 "fcntl");
          GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                      "Successfully obtained listen socket %s (#%d) from hypervisor\n",
                      tok,
                      fh);
          cb (cb_cls,
              fh);
          ret = GNUNET_OK;
        }
        GNUNET_free (fdns);
      }
    }
  }

  /* now try configuration file */
  if (0 == strcmp (serve_type,
                   "unix"))
  {
    char *serve_unixpath;
    mode_t unixpath_mode;
    struct sockaddr_un s_un;
    char *modestring;

    if (GNUNET_OK !=
        GNUNET_CONFIGURATION_get_value_filename (cfg,
                                                 section,
                                                 "UNIXPATH",
                                                 &serve_unixpath))
    {
      GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                 section,
                                 "UNIXPATH",
                                 "UNIXPATH value required");
      return GNUNET_SYSERR;
    }
    if (strlen (serve_unixpath) >= sizeof (s_un.sun_path))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "unixpath `%s' is too long\n",
                  serve_unixpath);
      GNUNET_free (serve_unixpath);
      return GNUNET_SYSERR;
    }

    if (GNUNET_OK !=
        GNUNET_CONFIGURATION_get_value_string (cfg,
                                               section,
                                               "UNIXPATH_MODE",
                                               &modestring))
    {
      GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                                 section,
                                 "UNIXPATH_MODE");
      GNUNET_free (serve_unixpath);
      return GNUNET_SYSERR;
    }
    errno = 0;
    unixpath_mode = (mode_t) strtoul (modestring,
                                      NULL,
                                      8);
    if (0 != errno)
    {
      GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                 section,
                                 "UNIXPATH_MODE",
                                 "must be octal number");
      GNUNET_free (modestring);
      GNUNET_free (serve_unixpath);
      return GNUNET_SYSERR;
    }
    GNUNET_free (modestring);

    {
      int fd;

      fd = open_unix_path (serve_unixpath,
                           unixpath_mode);
      GNUNET_free (serve_unixpath);
      if (-1 == fd)
        return GNUNET_NO;
      cb (cb_cls,
          fd);
      return GNUNET_OK;
    }
  }

  if (0 == strcasecmp (serve_type,
                       "tcp"))
  {
    unsigned long long lport;
    struct GNUNET_NETWORK_Handle *nh;
    char *bind_to;

    if (GNUNET_OK !=
        GNUNET_CONFIGURATION_get_value_number (cfg,
                                               section,
                                               "PORT",
                                               &lport))
    {
      GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                 section,
                                 "PORT",
                                 "port number required");
      return GNUNET_SYSERR;
    }

    if ( (0 == lport) ||
         (lport > UINT16_MAX) )
    {
      GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                 section,
                                 "PORT",
                                 "port number not in [1,65535]");
      return GNUNET_SYSERR;
    }

    if (GNUNET_OK !=
        GNUNET_CONFIGURATION_get_value_string (cfg,
                                               section,
                                               "BIND_TO",
                                               &bind_to))
      bind_to = NULL;

    /* let's have fun binding... */
    {
      char port_str[6];
      struct addrinfo hints;
      struct addrinfo *res;
      int ec;

      GNUNET_snprintf (port_str,
                       sizeof (port_str),
                       "%u",
                       (unsigned int) lport);
      memset (&hints,
              0,
              sizeof (hints));
      hints.ai_family = AF_UNSPEC;
      hints.ai_socktype = SOCK_STREAM;
      hints.ai_protocol = IPPROTO_TCP;
      hints.ai_flags = AI_PASSIVE
#ifdef AI_IDN
                       | AI_IDN
#endif
      ;

      if (0 !=
          (ec = getaddrinfo (bind_to,
                             port_str,
                             &hints,
                             &res)))
      {
        GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                    "Failed to resolve BIND_TO address `%s': %s\n",
                    bind_to,
                    gai_strerror (ec));
        GNUNET_free (bind_to);
        return GNUNET_SYSERR;
      }
      GNUNET_free (bind_to);
      for (struct addrinfo *ai = res;
           NULL != ai;
           ai = ai->ai_next)
      {
        if (NULL == (nh = GNUNET_NETWORK_socket_create (ai->ai_family,
                                                        ai->ai_socktype,
                                                        ai->ai_protocol)))
        {
          GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
                               "socket");
          freeaddrinfo (res);
          return GNUNET_NO;
        }
        {
          const int on = 1;

          if (GNUNET_OK !=
              GNUNET_NETWORK_socket_setsockopt (nh,
                                                SOL_SOCKET,
                                                SO_REUSEPORT,
                                                &on,
                                                sizeof(on)))
            GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING,
                                 "setsockopt");
        }
        if (GNUNET_OK !=
            GNUNET_NETWORK_socket_bind (nh,
                                        ai->ai_addr,
                                        ai->ai_addrlen))
        {
          GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
                               "bind");
          GNUNET_break (GNUNET_OK ==
                        GNUNET_NETWORK_socket_close (nh));
          freeaddrinfo (res);
          return GNUNET_NO;
        }

        if (GNUNET_OK !=
            GNUNET_NETWORK_socket_listen (nh,
                                          LISTEN_BACKLOG))
        {
          GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
                               "listen");
          GNUNET_SCHEDULER_shutdown ();
          GNUNET_break (GNUNET_OK ==
                        GNUNET_NETWORK_socket_close (nh));
          freeaddrinfo (res);
          return GNUNET_NO;
        }

        /* extract and return actual socket handle from 'nh' */
        {
          int fh;

          fh = GNUNET_NETWORK_get_fd (nh);
          GNUNET_NETWORK_socket_free_memory_only_ (nh);
          if (-1 == fh)
          {
            GNUNET_break (0);
            return GNUNET_NO;
          }
          cb (cb_cls,
              fh);
        }
      } /* for all addrinfos */
      freeaddrinfo (res);
      return GNUNET_OK;
    } /* bind data scope */
  } /* tcp */
  return ret;
}
