/* This file is part of GNU Pies
   Copyright (C) 2009-2025 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/>. */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include "pies.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

struct pies_sockaddr
{
  unsigned netmask;
  int salen;
  struct sockaddr sa;
};

enum name_match
  {
    match_none,
    match_user_name,
    match_group_name
  };

struct acl_entry
{
  grecs_locus_t locus;
  int allow;
  int authenticated;
  pies_acl_t acl;
  enum name_match name_match;
  char **names;
  struct grecs_list *sockaddrs;
};

struct pies_acl
{
  char *name;
  size_t refcnt;
  grecs_locus_t locus;
  struct grecs_list *list;
};
   


/* ACL creation */
void
grecs_locus_point_copy (struct grecs_locus_point *dst,
			struct grecs_locus_point *src)
{
  dst->file = grecs_strdup (src->file);
  dst->line = src->line;
  dst->col = src->col;
}

void
grecs_locus_copy (struct grecs_locus *dst, struct grecs_locus *src)
{
  grecs_locus_point_copy (&dst->beg, &src->beg);
  grecs_locus_point_copy (&dst->end, &src->end);
}

void
grecs_locus_point_free (struct grecs_locus_point *p)
{
  grecs_free (p->file);
}

void
grecs_locus_free (struct grecs_locus *loc)
{
  grecs_locus_point_free (&loc->beg);
  grecs_locus_point_free (&loc->end);
}

static void
acl_free_entry (void *p)
{
  struct acl_entry *ent = p;
  pies_acl_free (ent->acl);
  grecs_locus_free (&ent->locus);
  grecs_list_free (ent->sockaddrs);
  if (ent->names)
    {
      size_t i;

      for (i = 0; ent->names[i]; i++)
	free (ent->names[i]);
      free (ent->names);
    }
  free (ent);
}

void
pies_acl_use (pies_acl_t acl)
{
  ++acl->refcnt;
}

pies_acl_t
pies_acl_create (const char *name, grecs_locus_t *locus)
{
  pies_acl_t acl = grecs_malloc (sizeof (acl[0]));
  acl->name = name ? grecs_strdup (name) : NULL;
  acl->refcnt = 0;
  grecs_locus_copy (&acl->locus, locus);
  acl->list = grecs_list_create ();
  acl->list->free_entry = acl_free_entry;
  pies_acl_use (acl);
  return acl;
}

void
pies_acl_destroy (pies_acl_t *pacl)
{
  if (pacl && *pacl && (*pacl)->refcnt)
    {
      pies_acl_t acl = *pacl;
      if (--acl->refcnt == 0)
	{
	  free (acl->name);
	  grecs_locus_free (&acl->locus);
	  grecs_list_free (acl->list);
	  free (acl);
	  *pacl = NULL;
	}
    }
}

void
pies_acl_free (pies_acl_t acl)
{
  pies_acl_destroy (&acl);
}

static struct pies_sockaddr *
create_acl_sockaddr (int family, int len)
{
  struct pies_sockaddr *p = grecs_zalloc (sizeof (*p));
  p->salen = len;
  p->sa.sa_family = family;
  return p;
}

/* allow|deny [all|authenticated|group <grp: list>|user <usr: list>]
              [acl <name: string>] [from <addr: list>] */

static int
_parse_token (struct acl_entry *entry, const grecs_value_t *value)
{
  if (strcmp (value->v.string, "all") == 0
      || strcmp (value->v.string, "any") == 0)
    /* nothing */ ;
  else if (strcmp (value->v.string, "auth") == 0
	   || strcmp (value->v.string, "authenticated") == 0)
    entry->authenticated = 1;
  else
    return 1;
  return 0;
}

static int
_parse_sockaddr (struct acl_entry *entry, const grecs_value_t *value)
{
  struct pies_sockaddr *sptr;
  const char *string;

  if (grecs_assert_value_type (value, GRECS_TYPE_STRING, &entry->locus))
    return 1;

  string = value->v.string;

  if (string[0] == '/')
    {
      size_t len;
      struct sockaddr_un *s_un;

      len = strlen (string);
      if (len >= sizeof (s_un->sun_path))
	{
	  grecs_error (&entry->locus, 0,
		       _("socket name too long: %s"), string);
	  return 1;
	}
      sptr = create_acl_sockaddr (AF_UNIX, sizeof (s_un));
      s_un = (struct sockaddr_un *) &sptr->sa;
      memcpy (s_un->sun_path, string, len);
      s_un->sun_path[len] = 0;
    }
  else
    {
      struct in_addr addr;
      struct sockaddr_in *s_in;
      char *p = strchr (string, '/');

      if (p)
	*p = 0;

      if (inet_aton (string, &addr) == 0)
	{
	  struct hostent *hp = gethostbyname (string);
	  if (!hp)
	    {
	      grecs_error (&entry->locus, 0,
			   _("cannot resolve host name: %s"), string);
	      if (p)
		*p = '/';
	      return 1;
	    }
	  memcpy (&addr.s_addr, hp->h_addr, sizeof (addr.s_addr));
	}
      addr.s_addr = ntohl (addr.s_addr);

      sptr = create_acl_sockaddr (AF_INET, sizeof (s_in));
      s_in = (struct sockaddr_in *) &sptr->sa;
      s_in->sin_addr = addr;
      
      if (p)
	{
	  *p++ = '/';
	  char *q;
	  unsigned netlen;

	  netlen = strtoul (p, &q, 10);
	  if (*q == 0)
	    {
	      if (netlen == 0)
		sptr->netmask = 0;
	      else
		{
		  sptr->netmask = 0xfffffffful >> (32 - netlen);
		  sptr->netmask <<= (32 - netlen);
		}
	    }
	  else if (*q == '.')
	    {
	      struct in_addr addr;

	      if (inet_aton (p, &addr) == 0)
		{
		  grecs_error (&entry->locus, 0,
			       _("invalid netmask: %s"), p);
		  return 1;
		}
	      sptr->netmask = addr.s_addr;
	    }
	  else
	    {
	      grecs_error (&entry->locus, 0, _("invalid netmask: %s"), p);
	      return 1;
	    }
	}
      else
	sptr->netmask = 0xfffffffful;
    }
  grecs_list_append (entry->sockaddrs, sptr);
  return 0;
}

static int
sockaddr_cmp (void const *a, void const *b)
{
  struct pies_sockaddr const *ap = a;
  struct pies_sockaddr const *bp = b;

  if (ap->netmask != bp->netmask)
    return 1;
  if (ap->salen != bp->salen)
    return 1;
  return memcmp (&ap->sa, &bp->sa, ap->salen);
}
  
static void
sockaddr_free (void *p)
{
  free (p);
}

static int
_parse_from (struct acl_entry *entry, size_t argc, grecs_value_t **argv)
{
  if (argc == 0)
    return 0;
  else if (argv[0]->type == GRECS_TYPE_LIST)
    {
      grecs_error (&entry->locus, 0, _("expected \"from\", but found list"));
      return 1;
    }
  else if (strcmp (argv[0]->v.string, "from"))
    {
      grecs_error (&entry->locus, 0, _("expected \"from\", but found \"%s\""),
		    argv[0]->v.string);
      return 1;
    }
  argc--;
  argv++;

  if (argc == 0)
    {
      grecs_error (&entry->locus, 0,
		   _("unexpected end of statement after \"from\""));
      return 1;
    }

  entry->sockaddrs = grecs_list_create ();
  entry->sockaddrs->cmp = sockaddr_cmp;
  entry->sockaddrs->free_entry = sockaddr_free;
  if (argv[0]->type == GRECS_TYPE_STRING)
    {
      if (_parse_sockaddr (entry, argv[0]))
	return 1;
    }
  else
    {
      int rc = 0;
      struct grecs_list_entry *ep;

      for (ep = argv[0]->v.list->head; ep; ep = ep->next)
	rc += _parse_sockaddr (entry, (const grecs_value_t*) ep->data);
      if (rc)
	return rc;
    }

  if (argc - 1)
    {
      grecs_warning (&entry->locus, 0, _("junk after from-list"));
      return 1;
    }
  return 0;
}

static int
_parse_sub_acl (struct acl_entry *entry, size_t argc, grecs_value_t **argv)
{
  if (argc == 0)
    return 0;
  if (strcmp (argv[0]->v.string, "acl") == 0)
    {
      argc--;
      argv++;
      if (argc == 0)
	{
	  grecs_error (&entry->locus, 0,
		       _("expected ACL name, but found end of statement"));
	  return 1;
	}

      if (argv[0]->type != GRECS_TYPE_STRING)
	{
	  grecs_error (&entry->locus, 0,
		       _("expected string, but found list"));
	  return 1;
	}

      entry->acl = pies_acl_lookup (argv[0]->v.string);

      if (!entry->acl)
	{
	  grecs_error (&entry->locus, 0, _("ACL not defined: %s"),
		       argv[0]->v.string);
	  return 1;
	}
      pies_acl_use (entry->acl);
      
      argc--;
      argv++;
    }
  return _parse_from (entry, argc, argv);
}

static int
_parse_group (struct acl_entry *entry, size_t argc, grecs_value_t **argv)
{
  if (strcmp (argv[0]->v.string, "group") == 0)
    entry->name_match = match_group_name;
  else if (strcmp (argv[0]->v.string, "user") == 0)
    entry->name_match = match_user_name;
  else
    entry->name_match = match_none;

  if (entry->name_match != match_none)
    {
      argc--;
      argv++;
      if (argc == 0)
	{
	  grecs_error (&entry->locus, 0,
		       _("expected identity list, but found end of statement"));
	  return 1;
	}
      if (argv[0]->type == GRECS_TYPE_STRING)
	{
	  entry->names = grecs_calloc (2, sizeof (entry->names[0]));
	  entry->names[0] = grecs_strdup (argv[0]->v.string);
	  entry->names[1] = NULL;
	}
      else
	{
	  size_t i;
	  struct grecs_list_entry *ep;
	  entry->names = grecs_calloc (argv[0]->v.list->count + 1,
				       sizeof (entry->names[0]));
	  for (i = 0, ep = argv[0]->v.list->head; ep; ep = ep->next, ++i)
	    entry->names[i] = grecs_strdup (ep->data);
	  entry->names[i] = NULL;
	}
      argc--;
      argv++;
    }
  return _parse_sub_acl (entry, argc, argv);
}

static int
_parse_acl (struct acl_entry *entry, size_t argc, grecs_value_t **argv)
{
  if (grecs_assert_value_type (argv[0], GRECS_TYPE_STRING, &entry->locus))
    return 1;
  else if (_parse_token (entry, argv[0]) == 0)
    return _parse_sub_acl (entry, argc - 1, argv + 1);
  else
    return _parse_group (entry, argc, argv);
}

int
parse_acl_line (grecs_locus_t *locus, int allow, pies_acl_t acl,
		grecs_value_t *value)
{
  struct acl_entry *entry = grecs_zalloc (sizeof (*entry));

  grecs_locus_copy (&entry->locus, locus);
  entry->allow = allow;

  if (value)
    switch (value->type)
      {
      case GRECS_TYPE_STRING:
	if (_parse_token (entry, value))
	  {
	    grecs_error (&entry->locus, 0, _("unknown word: %s"),
			 value->v.string);
	    return 1;
	  }
	break;
	
      case GRECS_TYPE_ARRAY:
	if (_parse_acl (entry, value->v.arg.c, value->v.arg.v))
	  return 1;
	break;
	
      case GRECS_TYPE_LIST:
	grecs_error (locus, 0, _("unexpected list"));
	return 1;
      }
  grecs_list_append (acl->list, entry);
  return 0;
}

static int
acl_entry_cmp (void const *a, void const *b)
{
  struct acl_entry const *ap = a;
  struct acl_entry const *bp = b;
  size_t i;
    
  if (ap->allow != bp->allow)
    return 1;
  if (ap->authenticated != bp->authenticated)
    return 1;
  if (pies_acl_cmp (ap->acl, bp->acl))
    return 1;
  if (ap->name_match != bp->name_match)
    return 1;

  if (ap->names && bp->names)
    {
      for (i = 0; ap->names[i]; i++)
	if (!bp->names[i] || strcmp (ap->names[i], bp->names[i]))
	  return 1;
      if (bp->names[i])
	return 1;
    }
  else if (ap->names || bp->names)
    return 1;
  
  return grecs_list_compare (ap->sockaddrs, bp->sockaddrs);
}

#define ACL_TAG_NONE     0
#define ACL_TAG_IGNORE   1
#define ACL_TAG_OPTIONAL 2
#define ACL_TAG_REQUIRED 3

int
_acl_common_section_parser (enum grecs_callback_command cmd,
			    grecs_locus_t *locus,
			    grecs_value_t *value,
			    pies_acl_t *pacl,
			    int flag)
{
  pies_acl_t acl;
  const char *tag = NULL;
  int has_value = 0;
  
  switch (cmd)
    {
    case grecs_callback_section_begin:
      if (value)
	{
	  if (value->type != GRECS_TYPE_STRING)
	    {
	      grecs_error (locus, 0, _("ACL name must be a string"));
	      return 1;
	    }
	  has_value = value->v.string != NULL;
	}
      if (has_value)
	{
	  switch (flag)
	    {
	    case ACL_TAG_NONE:
	      grecs_error (locus, 0, _("ACL name is not expected"));
	      return 1;

	    case ACL_TAG_IGNORE:
	      grecs_warning (locus, 0, _("ACL name is ignored"));
	      break;

	    case ACL_TAG_OPTIONAL:
	    case ACL_TAG_REQUIRED:
	      tag = value->v.string;
	    }
	}
      else if (flag == ACL_TAG_REQUIRED)
	{
	  grecs_error (locus, 0, _("missing ACL name"));
	  return 1;
	}
      acl = pies_acl_create (tag, locus);
      if (tag && (acl = pies_acl_install (acl)) == NULL)
	return 1;
      if (pacl)
	{
	  pies_acl_free (*pacl);
	  *pacl = acl;
	}
      break;

    case grecs_callback_section_end:
      acl = *pacl;
      if (acl->list)
	acl->list->cmp = acl_entry_cmp;
      break;
      
    case grecs_callback_set_value:
      if (grecs_assert_value_type (value, GRECS_TYPE_STRING, &value->locus))
	return 0;
      acl = pies_acl_lookup (value->v.string);
      if (!acl)
	{
	  grecs_error (&value->locus, 0, _("ACL not defined: %s"),
		       value->v.string);
	  return 0;
	}
      pies_acl_use (acl);
      if (pacl)
	{
	  pies_acl_free (*pacl);
	  *pacl = acl;
	}
      break;
    }
  return 0;
}

int
acl_section_parser (enum grecs_callback_command cmd,
		    grecs_node_t *node,
		    void *varptr, void *cb_data)
{
  grecs_locus_t *locus = &node->locus;
  grecs_value_t *value = node->v.value;
  int rc = _acl_common_section_parser (cmd, locus, value, varptr,
				       ACL_TAG_NONE);
  if (rc == 0)
    *(void**)cb_data = *(pies_acl_t*)varptr;
  return rc;
}

int
defacl_section_parser (enum grecs_callback_command cmd,
		       grecs_node_t *node,
		       void *varptr, void *cb_data)
{
  grecs_locus_t *locus = &node->locus;
  grecs_value_t *value = node->v.value;
  return _acl_common_section_parser (cmd, locus, value, cb_data,
				     ACL_TAG_REQUIRED);
}

static int
allow_cb (enum grecs_callback_command cmd,
	  grecs_node_t *node,
	  void *varptr, void *cb_data)
{
  grecs_locus_t *locus = &node->locus;
  grecs_value_t *value = node->v.value;
  pies_acl_t acl = varptr;

  if (cmd != grecs_callback_set_value)
    {
      grecs_error (locus, 0, _("unexpected block statement"));
      return 1;
    }
  parse_acl_line (locus, 1, acl, value);
  return 0;
}

static int
deny_cb (enum grecs_callback_command cmd,
	 grecs_node_t *node,
	 void *varptr, void *cb_data)
{
  grecs_locus_t *locus = &node->locus;
  grecs_value_t *value = node->v.value;
  pies_acl_t acl = varptr;
  if (cmd != grecs_callback_set_value)
    {
      grecs_error (locus, 0, _("unexpected block statement"));
      return 1;
    }
  parse_acl_line (locus, 0, acl, value);
  return 0;
}

struct grecs_keyword acl_keywords[] = {
  /* TRANSLATORS: only words within angle brackets are translatable */
  { "allow", N_("[all|authenticated|user <usr: list>|group <grp: list>] [acl <name: string>] [from <addr: list>]"),
    N_("Allow access"),
    grecs_type_string, GRECS_MULT, NULL, 0,
    allow_cb },
  /* TRANSLATORS: only words within angle brackets are translatable */
  { "deny", N_("[all|authenticated|user <usr: list>|group <grp: list>] [acl <name: string>] [from <addr: list>]"),
    N_("Deny access"),
    grecs_type_string, GRECS_MULT, NULL, 0,
    deny_cb },
  { NULL }
};


/* ACL verification */

#define S_UN_NAME(sa, salen) \
  ((salen < offsetof (struct sockaddr_un,sun_path)) ? "" : (sa)->sun_path)

static int
_check_sockaddr (struct pies_sockaddr *sptr, struct acl_input *input)
{
  if (sptr->sa.sa_family != input->addr->sa_family)
    return 0;

  switch (sptr->sa.sa_family)
    {
    case AF_INET:
      {
	struct sockaddr_in *sin_clt = (struct sockaddr_in *) input->addr;
	struct sockaddr_in *sin_item = (struct sockaddr_in *) &sptr->sa;

	if (sin_item->sin_addr.s_addr ==
	    (ntohl (sin_clt->sin_addr.s_addr) & sptr->netmask))
	  return 1;
	break;
      }

    case AF_UNIX:
      {
	struct sockaddr_un *sun_clt = (struct sockaddr_un *) input->addr;
	struct sockaddr_un *sun_item = (struct sockaddr_un *) &sptr->sa;

	if (S_UN_NAME (sun_clt, input->addrlen)[0]
	    && S_UN_NAME (sun_item, sptr->salen)[0]
	    && strcmp (sun_clt->sun_path, sun_item->sun_path) == 0)
	  return 1;
      }
    }
  return 0;
}

static int
_acl_check (struct acl_entry *ent, struct acl_input *input)
{
  int result = 1;
  
  if (ent->authenticated)
    {
      result = input->identity != NULL;
      if (!result)
	return result;
    }

  switch (ent->name_match)
    {
    case match_none:
      break;

    case match_user_name:
      result = pies_identity_is_user (input->identity, ent->names);
      if (!result)
	return result;
      break;
      
    case match_group_name:
      result = pies_identity_is_group_member (input->identity, ent->names);
      if (!result)
	return result;
    }

  result = pies_acl_check (ent->acl, input, 1);
  if (!result)
    return result;

  if (ent->sockaddrs)
    {
      struct grecs_list_entry *ep;

      result = 0;
      for (ep = ent->sockaddrs->head; ep; ep = ep->next)
	{
	  result = _check_sockaddr ((struct pies_sockaddr *)ep->data, input);
	  if (result)
	    break;
	}
    }

  return result;
}

static int
_acl_check_cb (struct acl_entry *ent, struct acl_input *input, int *pres)
{
  int result = _acl_check (ent, input);
  debug (1, ("%s:%d: %s", ent->locus.beg.file, ent->locus.beg.line,
	      /* TRANSLATORS: `MATCHES' is the verb `match' in 2nd person.
		 E.g., in French: CONCORD AVEC */
	      result ? _("MATCHES") : _("does not match")));

  if (result)
    {
      *pres = ent->allow;
      return 1;
    }
  return 0;
}

int
pies_acl_check (pies_acl_t acl, struct acl_input *input, int result)
{
  if (acl)
    {
      struct grecs_list_entry *ep;

      for (ep = acl->list->head; ep; ep = ep->next)
	if (_acl_check_cb ((struct acl_entry *)ep->data, input, &result))
	  break;
    }
  return result;
}


/* Hash table */

static struct grecs_symtab *acl_table;

/* Calculate the hash of a string.  */
static unsigned
acl_hasher (void *data, unsigned long n_buckets)
{
  const struct pies_acl *p = data;
  return grecs_hash_string (p->name, n_buckets);
}

/* Compare two strings for equality.  */
static int
acl_compare (void const *data1, void const *data2)
{
  const struct pies_acl *p1 = data1;
  const struct pies_acl *p2 = data2;
  return strcasecmp (p1->name, p2->name);
}

static int
acl_copy (void *a, void *b)
{
  memcpy (a, b, sizeof (struct pies_acl));
  memset (b, 0, sizeof (struct pies_acl));
  return 0;
}

static void
acl_free (void *p)
{
  pies_acl_free (p);
}

pies_acl_t
pies_acl_install (pies_acl_t acl)
{
  pies_acl_t ret;
  int install = 1;
  
  if (!acl_table)
    {
      acl_table = grecs_symtab_create(sizeof (struct pies_acl),
				      acl_hasher,
				      acl_compare,
				      acl_copy,
				      NULL,
				      acl_free);
      if (!acl_table)
	grecs_alloc_die ();
    }

  ret = grecs_symtab_lookup_or_install (acl_table, acl, &install);

  if (!ret)
    {
      logmsg (LOG_ERR, _("cannot install ACL: %s"), strerror (errno));
      exit (1);
    }
  
  if (!install)
    {
      grecs_error (&acl->locus, 0,
		   _("redefinition of ACL %s"),
		   ret->name);
      grecs_error (&ret->locus, 0,
		   _("location of the previous definition"));
      ret = NULL;
    }      
  pies_acl_free (acl);
  return ret;
}

pies_acl_t
pies_acl_lookup (const char *name)
{
  struct pies_acl samp;
  if (!acl_table)
    return NULL;
  samp.name = (char *) name;
  return grecs_symtab_lookup_or_install (acl_table, &samp, NULL);
}

int
pies_acl_cmp (struct pies_acl *a, struct pies_acl *b)
{
  if (!a)
    return !!b;
  else
    return 1;
  return grecs_list_compare (a->list, b->list);
}
