/*
  Copyright (C) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
  2010, 2011, 2012 Free Software Foundation, Inc.

  This file is part of GNU Inetutils.

  GNU Inetutils 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 of the License, or (at
  your option) any later version.

  GNU Inetutils 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 this program.  If not, see `http://www.gnu.org/licenses/'. */

#include <config.h>

#include <intalkd.h>
#include <pwd.h>
#include <regex.h>
#include <ctype.h>
#include "argcv.h"

typedef struct netdef netdef_t;

struct netdef
{
  netdef_t *next;
  unsigned int ipaddr;
  unsigned int netmask;
};

#define ACT_ALLOW  0
#define ACT_DENY   1

typedef struct acl acl_t;

struct acl
{
  acl_t *next;
  regex_t re;
  netdef_t *netlist;
  int action;
};


acl_t *acl_head, *acl_tail;

#define DOTTED_QUAD_LEN 16

static int
read_address (char **line_ptr, char *ptr)
{
  char *startp = *line_ptr;
  char *endp;
  int dotcount = 0;

  for (endp = startp; *endp; endp++, ptr++)
    if (!(isdigit (*endp) || *endp == '.'))
      break;
    else if (endp < startp + DOTTED_QUAD_LEN)
      {
	if (*endp == '.')
	  dotcount++;
	*ptr = *endp;
      }
    else
      break;
  *line_ptr = endp;
  *ptr = 0;
  return dotcount;
}

static netdef_t *
netdef_parse (char *str)
{
  unsigned int ipaddr, netmask;
  netdef_t *netdef;
  char ipbuf[DOTTED_QUAD_LEN + 1];

  if (strcmp (str, "any") == 0)
    {
      ipaddr = 0;
      netmask = 0;
    }
  else
    {
      read_address (&str, ipbuf);
      ipaddr = inet_addr (ipbuf);
      if (ipaddr == INADDR_NONE)
	return NULL;
      if (*str == 0)
	netmask = 0xfffffffful;
      else if (*str != '/')
	return NULL;
      else
	{
	  str++;
	  if (read_address (&str, ipbuf) == 0)
	    {
	      /* netmask length */
	      unsigned int len = strtoul (ipbuf, NULL, 0);
	      if (len > 32)
		return NULL;
	      netmask = 0xfffffffful >> (32 - len);
	      netmask <<= (32 - len);
	      /*FIXME: hostorder? */
	    }
	  else
	    netmask = inet_network (ipbuf);
	  netmask = htonl (netmask);
	}
    }

  netdef = malloc (sizeof *netdef);
  if (!netdef)
    {
      syslog (LOG_ERR, "out of memory");
      exit (EXIT_FAILURE);
    }

  netdef->next = NULL;
  netdef->ipaddr = ipaddr;
  netdef->netmask = netmask;

  return netdef;
}

void
read_acl (char *config_file)
{
  FILE *fp;
  int line;
  char buf[128];
  char *ptr;

  if (!config_file)
    return;

  fp = fopen (config_file, "r");
  if (!fp)
    {
      syslog (LOG_ERR, "can't open config file %s: %m", config_file);
      return;
    }

  line = 0;
  while ((ptr = fgets (buf, sizeof buf, fp)))
    {
      int len, i;
      int argc;
      char **argv;
      int action;
      regex_t re;
      netdef_t *head, *tail;
      acl_t *acl;

      line++;
      len = strlen (ptr);
      if (len > 0 && ptr[len - 1] == '\n')
	ptr[--len] = 0;

      while (*ptr && isspace (*ptr))
	ptr++;
      if (!*ptr || *ptr == '#')
	continue;

      argcv_get (ptr, "", &argc, &argv);
      if (argc < 2)
	{
	  syslog (LOG_ERR, "%s:%d: too few fields", config_file, line);
	  argcv_free (argc, argv);
	  continue;
	}

      if (strcmp (argv[0], "allow") == 0)
	action = ACT_ALLOW;
      else if (strcmp (argv[0], "deny") == 0)
	action = ACT_DENY;
      else
	{
	  syslog (LOG_ERR, "%s:%d: unknown keyword", config_file, line);
	  argcv_free (argc, argv);
	  continue;
	}

      if (regcomp (&re, argv[1], 0) != 0)
	{
	  syslog (LOG_ERR, "%s:%d: bad regexp", config_file, line);
	  argcv_free (argc, argv);
	  continue;
	}

      head = tail = NULL;
      for (i = 2; i < argc; i++)
	{
	  netdef_t *cur = netdef_parse (argv[i]);
	  if (!cur)
	    {
	      syslog (LOG_ERR, "%s:%d: can't parse netdef: %s",
		      config_file, line, argv[i]);
	      continue;
	    }
	  if (!tail)
	    head = cur;
	  else
	    tail->next = cur;
	  tail = cur;
	}

      argcv_free (argc, argv);

      acl = malloc (sizeof *acl);
      if (!acl)
	{
	  syslog (LOG_CRIT, "out of memory");
	  exit (EXIT_FAILURE);
	}
      acl->next = NULL;
      acl->action = action;
      acl->netlist = head;
      acl->re = re;

      if (!acl_tail)
	acl_head = acl;
      else
	acl_tail->next = acl;
      acl_tail = acl;
    }

  fclose (fp);
}

static acl_t *
open_users_acl (char *name)
{
  char *filename;
  struct passwd *pw;
  acl_t *mark;

  pw = getpwnam (name);
  if (!pw)
    return NULL;

  filename =
    malloc (strlen (pw->pw_dir) + sizeof (USER_ACL_NAME) +
	    2 /* Null and separator.  */ );
  if (!filename)
    {
      syslog (LOG_ERR, "out of memory");
      return NULL;
    }

  sprintf (filename, "%s/%s", pw->pw_dir, USER_ACL_NAME);

  /* existence check */
  if (access (filename, F_OK) != 0)
    {
      switch (errno)
        {
          case ENOENT:
            /* no error message if user doesn't have a .talkrc file */
            break;
          default:
            syslog (LOG_ERR, "con't open config file %s: %m", filename);
            break;
        }
      return NULL;
    }

  /* read check */
  if (access (filename, R_OK) != 0)
    {
      syslog (LOG_ERR, "con't open config file %s: %m", filename);
      return NULL;
    }

  mark = acl_tail;
  read_acl (filename);
  free (filename);
  return mark;
}

static void
netdef_free (netdef_t * netdef)
{
  netdef_t *next;

  while (netdef)
    {
      next = netdef->next;
      free (netdef);
      netdef = next;
    }
}

static void
acl_free (acl_t * acl)
{
  acl_t *next;

  while (acl)
    {
      next = acl->next;
      regfree (&acl->re);
      netdef_free (acl->netlist);
      free (acl);
      acl = next;
    }
}

static void
discard_acl (acl_t * mark)
{
  if (mark)
    {
      acl_free (mark->next);
      acl_tail = mark;
      acl_tail->next = NULL;
    }
  else
    acl_head = acl_tail = NULL;
}

int
acl_match (CTL_MSG * msg, struct sockaddr_in *sa_in)
{
  acl_t *acl, *mark;
  unsigned int ip;

  mark = open_users_acl (msg->r_name);
  ip = sa_in->sin_addr.s_addr;
  for (acl = acl_head; acl; acl = acl->next)
    {
      netdef_t *net;

      for (net = acl->netlist; net; net = net->next)
	{
	  if (net->ipaddr == (ip & net->netmask))
	    {
	      if (regexec (&acl->re, msg->l_name, 0, NULL, 0) == 0)
		{
		  discard_acl (mark);
		  return acl->action;
		}
	    }
	}
    }
  discard_acl (mark);
  return ACT_ALLOW;
}
