Files
inetutils/talkd/acl.c
2025-01-01 18:21:25 +01:00

445 lines
9.0 KiB
C

/*
Copyright (C) 2001-2025 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;
in_addr_t ipaddr;
in_addr_t netmask;
};
typedef struct acl acl_t;
struct acl
{
acl_t *next;
regex_t re;
netdef_t *netlist;
int system;
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)
{
in_addr_t 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, int system)
{
FILE *fp;
int line;
char buf[128];
char *ptr;
if (!config_file || !config_file[0])
return;
fp = fopen (config_file, "r");
if (!fp)
{
if (system > 0)
{
/* A missing, yet specified, site-wide ACL is a serious error.
* Abort execution whenever this happens.
*/
syslog (LOG_ERR, "Cannot open config file %s: %m", config_file);
exit (EXIT_FAILURE);
}
return; /* User setting may fail to exist. Just ignore. */
}
if (system < 0)
{
/* Alarmed state, violating file access policy.
* Insert a single, general denial equivalent to
* a rule reading `deny .* any'.
*/
const char any_user[] = "^.*$";
char any_host[] = "any";
acl_t *acl;
regex_t re;
netdef_t *cur;
fclose (fp);
acl = malloc (sizeof *acl);
if (!acl)
{
syslog (LOG_ERR, "Out of memory");
exit (EXIT_FAILURE);
}
if (regcomp (&re, any_user, 0) != 0)
{
syslog (LOG_ERR, "Bad regexp '%s'.", any_user);
exit (EXIT_FAILURE);
}
cur = netdef_parse (any_host);
if (!cur)
{
syslog (LOG_ERR, "Cannot parse netdef '%s'.", any_host);
exit (EXIT_FAILURE);
}
acl->next = NULL;
acl->action = ACL_DENY;
acl->system = 0;
acl->re = re;
acl->netlist = cur;
if (!acl_tail)
acl_head = acl;
else
acl_tail->next = acl;
acl_tail = acl;
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 = ACL_ALLOW;
else if (strcmp (argv[0], "deny") == 0)
action = ACL_DENY;
else
{
syslog (LOG_WARNING, "%s:%d: unknown keyword", config_file, line);
argcv_free (argc, argv);
continue;
}
if (regcomp (&re, argv[1], 0) != 0)
{
syslog (LOG_WARNING, "%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_ERR, "Out of memory");
exit (EXIT_FAILURE);
}
acl->next = NULL;
acl->action = action;
acl->system = system;
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)
{
int level = 0; /* Private file, not mandatory. */
int rc;
char *filename;
struct passwd *pw;
struct stat st;
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);
/* The location must be a file, and must be owned by the
* indicated user and his corresponding group. No write
* access by group or world. Record a syslog warning,
* should either of these not be true.
*/
rc = stat (filename, &st);
if (rc < 0)
return NULL;
if (!S_ISREG (st.st_mode)
|| st.st_uid != pw->pw_uid
|| st.st_gid != pw->pw_gid
|| st.st_mode & S_IWGRP || st.st_mode & S_IWOTH)
{
if (logging || debug)
syslog (LOG_WARNING, "Discarding '%s': insecure access.", filename);
level = -1; /* Enforce a deny rule. */
}
mark = acl_tail;
read_acl (filename, level);
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;
in_addr_t ip;
int system_action = ACL_ALLOW, user_action = ACL_ALLOW;
int found_user_acl = 0;
if (strict_policy)
system_action = ACL_DENY;
mark = open_users_acl (msg->r_name);
ip = sa_in->sin_addr.s_addr;
if (mark && (mark != acl_tail))
found_user_acl = 1;
for (acl = acl_head; acl; acl = acl->next)
{
netdef_t *net;
for (net = acl->netlist; net; net = net->next)
{
/* Help the administrator and his users
* to simplify net list syntax:
*
* mask the address `net->ipaddr' with
* `net->netmask' for less computations
* within the ACL specification.
*/
if ((net->ipaddr & net->netmask) == (ip & net->netmask))
{
/*
* Site-wide ACLs concern user's name on this machine,
* whereas user's rules concern the incoming client name.
*/
if (acl->system &&
regexec (&acl->re, msg->r_name, 0, NULL, 0) == 0)
system_action = acl->action;
else if (regexec (&acl->re, msg->l_name, 0, NULL, 0) == 0)
user_action = acl->action;
}
}
}
discard_acl (mark);
if (system_action == ACL_ALLOW)
return user_action;
if (strict_policy)
return ACL_DENY; /* Equal to `system_action'. */
/* At this point it is known that last activated site-wide
* ACL rule has set SYSTEM_ACTION to ACL_DENY. Do we
* always want it to be overridable?
*/
/* Override ACL_DENY only if there was a user specific file
* ~/.talkrc containing some active rules at all. In other
* words, a site-policy claiming `deny' will need an act of
* will by the user in order that it be overridden.
*/
return found_user_acl ? user_action : ACL_DENY;
}