/* This file is part of GNU Pies.
   Copyright (C) 2007-2026 Sergey Poznyakoff

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

   GNU Pies 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 General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with GNU Pies.  If not, see <http://www.gnu.org/licenses/>. */

#include "pies.h"
#include <assert.h>

static void
switch_eids (uid_t *puid, gid_t *pgid, mode_t *pumask)
{
  uid_t ouid = geteuid ();
  gid_t ogid = getegid ();
  mode_t omask = umask (*pumask);

  if ((*puid && *puid != ouid) || (*pgid && *pgid != ogid))
    {
      if (setegid (*pgid))
	logmsg (LOG_ERR, _("cannot switch to EGID %lu: %s"),
		(unsigned long) *pgid, strerror (errno));
      if (seteuid (*puid))
	logmsg (LOG_ERR, _("cannot switch to EUID %lu: %s"),
		(unsigned long) *puid, strerror (errno));
    }
  *puid = ouid;
  *pgid = ogid;
  *pumask = omask;
}

int
create_socket (struct pies_url *url, int socket_type,
	       const char *user, mode_t umaskval)
{
  int rc;
  int fd;
  union
  {
    struct sockaddr sa;
    struct sockaddr_in s_in;
    struct sockaddr_un s_un;
  } addr;
  socklen_t socklen;
  uid_t uid = 0;
  gid_t gid = 0;
  int switch_back;

  if (strcmp (url->scheme, "unix") == 0
      || strcmp (url->scheme, "file") == 0
      || strcmp (url->scheme, "socket") == 0)
    {
      struct stat st;
      const char *group = NULL;

      user = url->user;
      if (url->argc)
	{
	  size_t i;
	  for (i = 0; i < url->argc; i++)
	    {
	      const char *arg = url->argv[i];
	      size_t len = strcspn (arg, "=");
	      if (strncmp (arg, "user", len) == 0)
		user = arg + len + 1;
	      else if (strncmp (arg, "group", len) == 0)
		group = arg + len + 1;
	      else if (strncmp (arg, "umask", len) == 0)
		{
		  char *p;
		  unsigned long n = strtoul (arg + len + 1, &p, 8);
		  if (*p)
		    logmsg (LOG_ERR, _("%s: invalid octal number (%s)"),
			    url->string, arg + len + 1);
		  else if (n & ~0777)
		    logmsg (LOG_ERR, _("%s: invalid umask (%s)"),
			    url->string, arg + len + 1);
		  else
		    umaskval = n & 0777;
		}
	      else if (strncmp (arg, "mode", len) == 0)
		{
		  char *p;
		  unsigned long n = strtoul (arg + len + 1, &p, 8);
		  if (*p)
		    logmsg (LOG_ERR, _("%s: invalid octal number (%s)"),
			    url->string, arg + len + 1);
		  else if (n & ~0777)
		    logmsg (LOG_ERR, _("%s: invalid mode (%s)"),
			    url->string, arg + len + 1);
		  else
		    umaskval = 0777 & ~n;
		}
	    }
	}

      if (user)
	{
	  struct passwd *pw = getpwnam (user);
	  if (!pw)
	    {
	      logmsg (LOG_ERR, _("no such user: %s"), user);
	      return -1;
	    }
	  uid = pw->pw_uid;
	  gid = pw->pw_gid;
	}

      if (group)
	{
	  struct group *grp = getgrnam (group);
	  if (!grp)
	    {
	      logmsg (LOG_ERR, _("no such group: %s"), user);
	      return -1;
	    }
	  gid = grp->gr_gid;
	}

      if (strlen (url->path) > sizeof addr.s_un.sun_path)
	{
	  errno = EINVAL;
	  logmsg (LOG_ERR, _("%s: UNIX socket name too long"), url->path);
	  return -1;
	}
      addr.sa.sa_family = PF_UNIX;
      socklen = sizeof (addr.s_un);
      strcpy (addr.s_un.sun_path, url->path);
      if (stat (url->path, &st))
	{
	  if (errno != ENOENT)
	    {
	      logfuncall ("stat", url->string, errno);
	      return -1;
	    }
	}
      else
	{
	  /* FIXME: Check permissions? */
	  if (!S_ISSOCK (st.st_mode))
	    {
	      logmsg (LOG_ERR, _("%s: not a socket"), url->string);
	      return -1;
	    }
	  if (/*rmsocket && */ unlink (url->path))
	    {
	      logfuncall ("unlink", url->path, errno);
	      return -1;
	    }
	}
    }
  else if (strcmp (url->scheme, "inet") == 0)
    {
      const char *host = url->host;
      short port = url->port;

      uid = 0;
      gid = 0;
      umaskval = 0;

      addr.sa.sa_family = PF_INET;
      socklen = sizeof (addr.s_in);

      if (!host)
	addr.s_in.sin_addr.s_addr = INADDR_ANY;
      else
	{
	  struct hostent *hp = gethostbyname (host);
	  if (!hp)
	    {
	      logmsg (LOG_ERR, _("%s: unknown host name %s"),
		      url->string, host);
	      return -1;
	    }
	  addr.sa.sa_family = hp->h_addrtype;
	  switch (hp->h_addrtype)
	    {
	    case AF_INET:
	      memmove (&addr.s_in.sin_addr, hp->h_addr, 4);
	      addr.s_in.sin_port = htons (port);
	      break;

	    default:
	      logmsg (LOG_ERR, _("%s: unsupported address family"),
		      url->string);
	      return -1;
	    }
	}
    }
  else
    {
      logmsg (LOG_ERR, "%s: unknown scheme", url->string);
      return -1;
    }

  fd = socket (addr.sa.sa_family, socket_type, url->proto);
  if (fd == -1)
    {
      logfuncall ("socket", url->string, errno);
      return -1;
    }

  rc = 1;
  if (addr.sa.sa_family != PF_UNIX
      && setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void *) &rc,
		    sizeof (rc)) == -1)
    {
      logmsg (LOG_ERR, _("%s: set reuseaddr failed: %s"),
	      url->string, strerror (errno));
      close (fd);
      return -1;
    }

  if (uid || gid || umaskval)
    {
      switch_eids (&uid, &gid, &umaskval);
      switch_back = 1;
    }
  else
    switch_back = 0;
  rc = bind (fd, &addr.sa, socklen);
  if (rc < 0)
    logfuncall ("bind", NULL, errno);
  if (switch_back)
    switch_eids (&uid, &gid, &umaskval);
  if (rc < 0)
    {
      close (fd);
      return -1;
    }
  return fd;
}

static int
pass_fd0 (int fd, int payload)
{
  struct msghdr msg;
  struct iovec iov[1];

#if HAVE_STRUCT_MSGHDR_MSG_CONTROL
# ifndef CMSG_LEN
#  define CMSG_LEN(size)         (sizeof(struct cmsghdr) + (size))
# endif /* ! CMSG_LEN */
# ifndef CMSG_SPACE
#  define CMSG_SPACE(size)       (sizeof(struct cmsghdr) + (size))
# endif /* ! CMSG_SPACE */

  char control[CMSG_SPACE (sizeof (int))];
  struct cmsghdr *cmptr;

  msg.msg_control = (caddr_t) control;
  msg.msg_controllen = CMSG_LEN (sizeof (int));

  cmptr = CMSG_FIRSTHDR (&msg);
  cmptr->cmsg_len = CMSG_LEN (sizeof(int));
  cmptr->cmsg_level = SOL_SOCKET;
  cmptr->cmsg_type = SCM_RIGHTS;
  *((int *) CMSG_DATA (cmptr)) = payload;
#elif HAVE_STRUCT_MSGHDR_MSG_ACCRIGHTS
  msg.msg_accrights = (caddr_t) &payload;
  msg.msg_accrightslen = sizeof (int);
#else
  logmsg (LOG_ERR, _("no way to send fd"));
  return 1;
#endif /* HAVE_MSGHDR_MSG_CONTROL */

  msg.msg_name = NULL;
  msg.msg_namelen = 0;

  iov[0].iov_base = "";
  iov[0].iov_len = 1;
  msg.msg_iov = iov;
  msg.msg_iovlen = 1;

  return sendmsg (fd, &msg, 0) == -1;
}

int
pass_fd (const char *socket_name, int fd, unsigned maxtime)
{
  enum { fds_init, fds_open, fds_connected, fds_ready } state = fds_init;
  static char *fds_descr[] = { "init", "open", "connected", "ready" };
  time_t start = time (NULL);
  int sockfd = -1;
  int res = -1;
  struct sockaddr_un addr;

  if (strlen (socket_name) > sizeof addr.sun_path)
    {
      logmsg (LOG_ERR, _("%s: UNIX socket name too long"), socket_name);
      return -1;
    }
  addr.sun_family = AF_UNIX;
  strcpy (addr.sun_path, socket_name);

  for (;;)
    {
      time_t now = time (NULL);

      if (now - start > maxtime)
	{
	  logmsg (LOG_ERR, _("pass-fd timed out in state %s"),
		  fds_descr[state]);
	  break;
	}

      if (state == fds_init)
	{
	  struct stat st;

	  if (stat (socket_name, &st) == 0)
	    {
	      if (!S_ISSOCK (st.st_mode))
		{
		  logmsg (LOG_ERR, _("%s: not a socket"), socket_name);
		  break;
		}

	      sockfd = socket (PF_UNIX, SOCK_STREAM, 0);
	      if (sockfd == -1)
		{
		  if (errno == EINTR)
		    continue;
		  logmsg (LOG_ERR, "socket: %s", strerror (errno));
		  break;
		}
	      state = fds_open;
	    }
	  else if (errno != ENOENT)
	    {
	      logmsg (LOG_ERR, _("cannot stat %s: %s"),
		      socket_name, strerror (errno));
	      break;
	    }
	}

      if (state == fds_open)
	{
	  if (connect (sockfd, (struct sockaddr *) &addr, sizeof (addr)))
	    {
	      switch (errno)
		{
		case EINTR:
		case ECONNREFUSED:
		case EAGAIN:
		  continue;
		}
	      logfuncall ("connect", socket_name, errno);
	      break;
	    }
	  state = fds_connected;
	}

      if (state == fds_connected)
	{
	  int rc;
	  fd_set fds;
	  struct timeval tv;

	  FD_ZERO (&fds);
	  FD_SET (sockfd, &fds);
	  tv.tv_usec = 0;
	  tv.tv_sec = maxtime - (now - start);
	  rc = select (sockfd + 1, NULL, &fds, NULL, &tv);
	  if (rc == 0)
	    continue;
	  if (rc < 0)
	    {
	      if (errno == EINTR)
		continue;
	      logfuncall ("select", NULL, errno);
	      break;
	    }
	  state = fds_ready;
	}

      if (state == fds_ready)
	{
	  res = pass_fd0 (sockfd, fd);
	  break;
	}
    }

  if (sockfd >= 0)
    close (sockfd);
  return res;
}

fd_set fdset[3];
int fd_max;

struct sockinst
{
  struct sockinst *prev, *next;
  int fd;
  int dead;
  socket_handler_t handler[3];
  void (*free_data)(void *);
  void *data;
};

struct sockinst *si_head, *si_tail;
static int si_iterating;

static struct sockinst *
find_socket (int fd)
{
  struct sockinst *sp;

  for (sp = si_head; sp; sp = sp->next)
    if (sp->fd == fd)
      break;
  return sp;
}

static void
calc_fd_max (void)
{
  struct sockinst *sp;

  fd_max = -1;
  for (sp = si_head; sp; sp = sp->next)
    if (sp->fd > fd_max)
      fd_max = sp->fd;
}

void *
register_socket (int fd,
		 socket_handler_t rd,
		 socket_handler_t wr,
		 socket_handler_t ex,
		 void *data,
		 void (*free_data) (void *))
{
  struct sockinst *sip = grecs_malloc (sizeof *sip);
  sip->fd = fd;
  sip->dead = 0;
  sip->handler[PIES_EVT_RD] = rd;
  sip->handler[PIES_EVT_WR] = wr;
  sip->handler[PIES_EVT_EX] = ex;
  sip->free_data = free_data;
  sip->data = data;
  sip->next = NULL;
  sip->prev = si_tail;
  if (si_tail)
    si_tail->next = sip;
  else
    {
      FD_ZERO (&fdset[PIES_EVT_RD]);
      FD_ZERO (&fdset[PIES_EVT_WR]);
      FD_ZERO (&fdset[PIES_EVT_EX]);
      si_head = sip;
    }

  si_tail = sip;
  if (rd)
    FD_SET (fd, &fdset[PIES_EVT_RD]);
  if (wr)
    FD_SET (fd, &fdset[PIES_EVT_WR]);
  if (ex)
    FD_SET (fd, &fdset[PIES_EVT_EX]);
  if (fd_max == -1)
    calc_fd_max ();
  else if (fd > fd_max)
    fd_max = fd;
  return sip;
}

/* FIXME: Don't use with disable_socket/enable_socket */
void
update_socket (int fd, int evt, socket_handler_t f)
{
  struct sockinst *sp = find_socket (fd);
  assert (sp != NULL);
  sp->handler[evt] = f;
  if (f)
    FD_SET (fd, &fdset[evt]);
  else
    FD_CLR (fd, &fdset[evt]);
}

static void
delete_sockinst (struct sockinst *sp)
{
  close (sp->fd);
  if (sp->handler[PIES_EVT_RD])
    FD_CLR (sp->fd, &fdset[PIES_EVT_RD]);
  if (sp->handler[PIES_EVT_WR])
    FD_CLR (sp->fd, &fdset[PIES_EVT_WR]);
  if (sp->handler[PIES_EVT_EX])
    FD_CLR (sp->fd, &fdset[PIES_EVT_EX]);
  fd_max = -1;

  if (sp->prev)
    sp->prev->next = sp->next;
  else
    si_head = sp->next;
  if (sp->next)
    sp->next->prev = sp->prev;
  else
    si_tail = sp->prev;

  free (sp);
}

void
deregister_socket (int fd)
{
  struct sockinst *sp = find_socket (fd);
  if (!sp)
    return;
  if (sp->free_data)
    sp->free_data (sp->data);
  sp->data = NULL;
  sp->free_data = NULL;
  if (si_iterating)
    sp->dead = 1;
  else
    delete_sockinst (sp);
}


int
register_program_socket (int socktype, int fd, void *data,
			 void (*free_data) (void*))
{
  if (socktype == SOCK_STREAM && listen (fd, 8) == -1)
    {
      logmsg (LOG_ERR, "listen: %s", strerror (errno));
      return 1;
    }
  register_socket (fd, progman_accept, NULL, NULL, data, free_data);
  return 0;
}

void
disable_socket (int fd)
{
  if (fd < 0)
    return;
  debug (2, (_("disabling fd %d"), fd));
  FD_CLR (fd, &fdset[PIES_EVT_RD]);
  FD_CLR (fd, &fdset[PIES_EVT_WR]);
  FD_CLR (fd, &fdset[PIES_EVT_EX]);
}

void
enable_socket (int fd)
{
  struct sockinst *sp;
  if (fd < 0)
    return;
  debug (2, (_("enabling fd %d"), fd));
  sp = find_socket (fd);
  assert (sp != NULL);
  if (sp->handler[PIES_EVT_RD])
    FD_SET (fd, &fdset[PIES_EVT_RD]);
  if (sp->handler[PIES_EVT_WR])
    FD_SET (fd, &fdset[PIES_EVT_WR]);
  if (sp->handler[PIES_EVT_EX])
    FD_SET (fd, &fdset[PIES_EVT_EX]);
}

static int (*pies_pause_hook) (void);

void
pies_set_hook (int (*f) (void))
{
  pies_pause_hook = f;
}

void
pies_pause (void)
{
  if (pies_pause_hook && pies_pause_hook ())
    return;

  if (fd_max == -1)
    calc_fd_max ();

  while (1)
    {
      fd_set rdset = fdset[PIES_EVT_RD];
      fd_set wrset = fdset[PIES_EVT_WR];
      fd_set exset = fdset[PIES_EVT_EX];

      int rc = select (fd_max + 1, &rdset, &wrset, &exset, NULL);
      if (rc > 0)
	{
	  struct sockinst *sp;
	  int delete = 0;
	  ++si_iterating;
	  for (sp = si_head; sp; sp = sp->next)
	    {
	      if (sp->dead)
		{
		  delete = 1;
		  continue;
		}
	      if (sp->handler[PIES_EVT_RD] && FD_ISSET (sp->fd, &rdset))
		sp->handler[PIES_EVT_RD] (sp->fd, sp->data);

	      if (sp->dead)
		{
		  delete = 1;
		  continue;
		}
	      if (sp->handler[PIES_EVT_WR] && FD_ISSET (sp->fd, &wrset))
		sp->handler[PIES_EVT_WR] (sp->fd, sp->data);

	      if (sp->dead)
		{
		  delete = 1;
		  continue;
		}
	      if (sp->handler[PIES_EVT_EX] && FD_ISSET (sp->fd, &exset))
		sp->handler[PIES_EVT_EX] (sp->fd, sp->data);
	    }
	  --si_iterating;

	  if (delete)
	    {
	      for (sp = si_head; sp; )
		{
		  struct sockinst *next = sp->next;
		  if (sp->dead)
		    delete_sockinst (sp);
		  sp = next;
		}
	      calc_fd_max ();
	    }
	  break;
	}
      else if (rc < 0)
	{
	  if (errno != EINTR)
	    logmsg (LOG_ERR, "select: %s", strerror (errno));
	  break;
	}
    }
}
