/* This file is part of GNU Pies.
   Copyright (C) 2009-2022 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 <sys/types.h>
#include <dirent.h>

#define IFLD_SERVICE      0	/* service name */
#define IFLD_SOCKET       1	/* socket type */
#define IFLD_PROTOCOL     2	/* protocol */
#define IFLD_WAIT         3	/* wait/nowait */
#define IFLD_USER         4	/* user */
#define IFLD_SERVER_PATH  5	/* server program path */
#define IFLD_SERVER_ARGS  6	/* server program arguments */

#define IFLD_MIN_COUNT   6	/* Minimum number of fields in entry */

/* FIXME: Copied from grecs/src/tree.c */
static void
listel_dispose(void *el)
{
  free(el);
}

#define TCPMUX_PREFIX_STR "tcpmux/"
#define TCPMUX_PREFIX_LEN (sizeof(TCPMUX_PREFIX_STR)-1)

static int
tcpmux_service (const char *service, char **psrv, int *pflag)
{
  if (strncmp (service, TCPMUX_PREFIX_STR, TCPMUX_PREFIX_LEN) == 0)
    {
      service += TCPMUX_PREFIX_LEN;
      if (*service == '+')
	{
	  *pflag |= CF_TCPMUXPLUS;
	  service++;
	}
      else
	*pflag |= CF_TCPMUX;
      *psrv = (char *) service;
      return 1;
    }
  return 0;
}

static char *
mktag (const char *address, const char *service)
{
  char *str;
  
  if (address)
    {
      str = grecs_malloc (strlen (address) + 1 + strlen (service) + 1);
      strcpy (str, address);
      strcat (str, ":");
      strcat (str, service);
    }
  else
    str = grecs_strdup (service);
  return str;
}


static int
inetd_conf_file (const char *file)
{
  FILE *fp;
  size_t size = 0;
  char *buf = NULL;
  unsigned long line_no = 0;
  struct wordsplit ws;
  char *dfl_address = NULL;
  int wsflags;
  
  fp = fopen (file, "r");
  if (!fp)
    {
      logmsg (LOG_ERR,
	      _("cannot open configuration file %s: %s"),
	      file, strerror (errno));
      return 1;
    }

  wsflags = WRDSF_NOVAR | WRDSF_NOCMD | WRDSF_WS | WRDSF_SQUEEZE_DELIMS;
  while (grecs_getline (&buf, &size, fp) >= 0)
    {
      char *p;      
      struct component *comp;
      int socket_type;
      struct pies_url *url;
      size_t max_rate = 0;
      int flags = 0;
      char *str;
      char *user = NULL;
      char *group = NULL;
      char *address;
      char *service;
      size_t len;
      struct inetd_builtin *builtin;
      char *tag;
      
      line_no++;
      for (p = buf; *p && c_isblank (*p); p++)
	;

      if (!p || *p == '\n' || *p == '#')
	continue;

      if (wordsplit (p, &ws, wsflags))
	{
	  logmsg (LOG_ERR, "wordsplit: %s", strerror (errno));
	  continue;
	}
      wsflags |= WRDSF_REUSE;
      if (ws.ws_wordc == 1)
	{
	  size_t len = strlen (ws.ws_wordv[IFLD_SERVICE]);
	  if (len > 0 && ws.ws_wordv[IFLD_SERVICE][len-1] == ':')
	    {
	      free (dfl_address);

	      if (len == 2 && ws.ws_wordv[IFLD_SERVICE][0] == '*')
		dfl_address = NULL;
	      else
		{
		  dfl_address = grecs_malloc (len);
		  memcpy (dfl_address, ws.ws_wordv[IFLD_SERVICE], len-1);
		  dfl_address[len-1] = 0;
		}
	      continue;
	    }
	}
      
      if (ws.ws_wordc < IFLD_MIN_COUNT)
	{
	  logmsg (LOG_ERR, "%s:%lu: too few fields", file, line_no);
	  continue;
	}

      /* Parse service */
      str = strchr (ws.ws_wordv[IFLD_SERVICE], ':');
      if (str)
	{
	  *str++ = 0;
	  address = ws.ws_wordv[IFLD_SERVICE];
	  service = str;
	}
      else
	{
	  address = dfl_address;
	  service = ws.ws_wordv[IFLD_SERVICE];
	}
      
      /* Parse socket type */
      if (str_to_socket_type (ws.ws_wordv[IFLD_SOCKET], &socket_type))
	{
	  logmsg (LOG_ERR, "%s:%lu: %s",
		  file, line_no, _("bad socket type"));
	  continue;
	}

      tag = service;
      /* Handle eventual "tcpmux/" */
      if (tcpmux_service (service, &service, &flags))
	{
	  if (strncmp (ws.ws_wordv[IFLD_PROTOCOL], "tcp", 3))
	    {
	      logmsg (LOG_ERR, "%s:%lu: %s",
		      file, line_no, _("bad protocol for tcpmux service"));
	      continue;
	    }
	  if (socket_type != SOCK_STREAM)
	    {
	      logmsg (LOG_ERR, "%s:%lu: %s",
		      file, line_no, _("bad socket type for tcpmux service"));
	      continue;
	    }
	  url = NULL;
	}
      else
	{
	  char *s = NULL;
	  size_t l = 0;
	  int rc;
	  
	  /* Create URL from protocol and service fields. */
	  if (grecs_asprintf (&s, &l, "inet+%s://%s:%s",
			      ws.ws_wordv[IFLD_PROTOCOL],
			      address ? address : "0.0.0.0",
			      service))
	    grecs_alloc_die ();
	  rc = pies_url_create (&url, s);
	  free (s);
	  if (rc)
	    {
	      /* FIXME: Better error message */
	      logmsg (LOG_ERR, "%s:%lu: %s",
		      file, line_no, _("invalid socket address"));
	      continue;
	    }
	}
      
      /* Parse wait/nowait field */
      str = strchr (ws.ws_wordv[IFLD_WAIT], '.');
      if (str)
	{
	  size_t n;
	  
	  *str++ = 0;
	  n = strtoul(str, &p, 10);
	  if (*p)
	    logmsg (LOG_WARNING, "%s:%lu: invalid number (near %s)",
		    file, line_no, p);
	  else
	    max_rate = n;
	}

      if (strcmp (ws.ws_wordv[IFLD_WAIT], "wait") == 0)
	flags |= CF_WAIT;
      else if (strcmp (ws.ws_wordv[IFLD_WAIT], "nowait"))
	{
	  logmsg (LOG_ERR, "%s:%lu: %s",
		  file, line_no, _("invalid wait field"));
	  pies_url_destroy(&url);
	  continue;
	}

      /* Parse user/group */
      len = strcspn (ws.ws_wordv[IFLD_USER], ":.");
      if (ws.ws_wordv[IFLD_USER][len])
	{
	  ws.ws_wordv[IFLD_USER][len] = 0;
	  group = ws.ws_wordv[IFLD_USER] + len + 1;
	}
      user = ws.ws_wordv[IFLD_USER];

      /* Is it a built-in? */
      if (strcmp (ws.ws_wordv[IFLD_SERVER_PATH], "internal") == 0)
	{
	  builtin = inetd_builtin_lookup (service, socket_type);
	  if (!builtin)
	    {
	      logmsg (LOG_ERR, "%s:%lu: %s",
		      file, line_no, _("unknown internal service"));
	      continue;
	    }
	}
      else
	builtin = NULL;
      
      /* Create the component */
      str = mktag (address, tag);
      comp = component_create (str);
      free (str);

      comp->mode = pies_comp_inetd;
      comp->socket_type = socket_type;
      comp->socket_url = url;
      comp->max_rate = max_rate;
      if (builtin)
	{
	  comp->builtin = builtin;
	  comp->flags = builtin->flags;
	}
      else
	comp->flags = flags;
      if (ISCF_TCPMUX (comp->flags))
	comp->tcpmux = mktag (address, "tcpmux");
      comp->service = grecs_strdup (service);
      comp->privs.user = grecs_strdup (user);
      if (group)
	{
	  comp->privs.groups = grecs_list_create ();
	  comp->privs.groups->free_entry = listel_dispose;
	  grecs_list_append (comp->privs.groups, grecs_strdup (group));
	}

      comp->program = grecs_strdup (ws.ws_wordv[IFLD_SERVER_PATH]);

      if (ws.ws_wordc > IFLD_MIN_COUNT)
	{
	  size_t i, j;
	  
	  comp->argc = ws.ws_wordc - IFLD_MIN_COUNT;
	  comp->argv = grecs_calloc (comp->argc + 1, sizeof (comp->argv[0]));
	  for (i = IFLD_SERVER_ARGS, j = 0; i < ws.ws_wordc; i++, j++)
	    comp->argv[j] = grecs_strdup (ws.ws_wordv[i]);
	}
      else
	{
	  comp->argc = 1;
	  comp->argv = grecs_calloc (comp->argc + 1, sizeof (comp->argv[0]));
	  comp->argv[0] = grecs_strdup (comp->program);
	}
    }
  
  if (wsflags & WRDSF_REUSE)
    wordsplit_free (&ws);
  free (dfl_address);
  free (buf);
  fclose (fp);
  return 0;
}

#ifndef S_ISLNK
# define S_ISLNK(m) 0
#endif
#define NAME_INIT_ALLOC 16

static int
inetd_conf_dir (const char *name)
{
  DIR *dir;
  struct dirent *ent;
  int errs = 0;
  char *namebuf;
  size_t namebufsize;
  size_t namelen;
  
  dir = opendir (name);
  if (!dir)
    {
      logmsg (LOG_ERR, _("cannot open directory %s: %s"),
	      name, strerror (errno));
      return 1;
    }

  namelen = strlen (name);
  namebufsize = namelen;
  if (name[namelen-1] != '/')
    namebufsize++;
  namebufsize += NAME_INIT_ALLOC;
  namebuf = grecs_malloc (namebufsize);
  memcpy (namebuf, name, namelen);
  if (name[namelen-1] != '/')
    namebuf[namelen++] = 0;

  while ((ent = readdir (dir)))
    {
      struct stat st;
      
      if (stat (ent->d_name, &st))
	{
	  int ec = errno;
	  char *name = mkfilename (name, ent->d_name, NULL);
	  logfuncall ("stat", name, ec);
	  free (name);
	  errs |= 1;
	}
      else if (S_ISREG (st.st_mode) || S_ISLNK (st.st_mode))
	{
	  size_t len = strlen (ent->d_name);
	  if (namelen + len >= namebufsize)
	    {
	      namebufsize = namelen + len + 1;
	      namebuf = grecs_realloc (namebuf, namebufsize);
	    }
	  strcpy (namebuf + namelen, ent->d_name);
	  errs |= inetd_conf_file (namebuf);
	}
    }
  free (namebuf);
  closedir (dir);
  return errs;
}

int
inetd_config_parse (const char *file)
{
  struct stat st;

  if (stat (file, &st))
    {
      logfuncall ("stat", file, errno);
      return 1;
    }
  if (S_ISDIR (st.st_mode))
    return inetd_conf_dir (file);

  return inetd_conf_file (file);
}
