/* 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 (assert_grecs_value_type (&entry->locus, value, GRECS_TYPE_STRING)) 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 (assert_grecs_value_type (&entry->locus, argv[0], GRECS_TYPE_STRING)) 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 (assert_grecs_value_type (&value->locus, value, GRECS_TYPE_STRING)) 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_locus_t *locus, void *varptr, grecs_value_t *value, void *cb_data) { 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_locus_t *locus, void *varptr, grecs_value_t *value, void *cb_data) { return _acl_common_section_parser (cmd, locus, value, cb_data, ACL_TAG_REQUIRED); } static int allow_cb (enum grecs_callback_command cmd, grecs_locus_t *locus, void *varptr, grecs_value_t *value, void *cb_data) { 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_locus_t *locus, void *varptr, grecs_value_t *value, void *cb_data) { 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); }