/* This file is part of GNU Pies
Copyright (C) 2009-2019 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 . */
#ifdef HAVE_CONFIG_H
# include
#endif
#include "pies.h"
#include
#include
#include
#include
#include
#include
#include
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 |user ]
[acl ] [from ] */
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 |group ] [acl ] [from ]"),
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 |group ] [acl ] [from ]"),
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);
}