 /*
  * This module implements a simple access control language that is based on
  * host (or domain) names, netgroup, internet addresses (or network numbers)
  * and daemon process names. When a match is found an optional shell command
  * is executed and the search is terminated.
  * 
  * Diagnostics are reported through syslog(3).
  * 
  * Compile with -DNETGROUP if your library provides support for netgroups.
  * 
  * Compile with -DUSER_AT_HOST for rule-driven username lookups.
  * 
  * Author: Wietse Venema, Eindhoven University of Technology, The Netherlands.
  */

#ifndef lint
static char sccsid[] = "@(#) hosts_access.c 1.10 93/03/07 22:47:36";
#endif

 /* System libraries. */

#include <sys/types.h>
#include <sys/param.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <syslog.h>
#include <ctype.h>
#include <errno.h>

extern char *fgets();
extern char *strchr();
extern char *strtok();

#ifndef	INADDR_NONE
#define	INADDR_NONE	(-1)		/* XXX should be 0xffffffff */
#endif

/* Local stuff. */

#include "log_tcp.h"

#ifdef INET_ADDR_BUG
#include "inet_addr_fix"
#endif

/* Delimiters for lists of daemons or clients. */

static char sep[] = ", \t";

/* Constants to be used in assignments only, not in comparisons... */

#define	YES		1
#define	NO		0
#define	FAIL		(-1)

/* Forward declarations. */

static int table_match();
static int list_match();
static int client_match();
static int string_match();
static int masked_match();
static char *xgets();

/* The user@host access control. Trivial to add but complicates use. */

#ifdef USER_AT_HOST
static int userhost_match();
#define CLIENT_MATCH userhost_match
#else
#define CLIENT_MATCH client_match
#endif

/* Size of logical line buffer. */

#define	BUFLEN 2048

/* hosts_access - host access control facility */

int     hosts_access(daemon, client)
char   *daemon;
struct from_host *client;		/* host or user name may be empty */
{

    /*
     * If the (daemon, client) pair is matched by an entry in the file
     * /etc/hosts.allow, access is granted. Otherwise, if the (daemon,
     * client) pair is matched by an entry in the file /etc/hosts.deny,
     * access is denied. Otherwise, access is granted. A non-existent
     * access-control file is treated as an empty file.
     */

    if (table_match(HOSTS_ALLOW, daemon, client))
	return (YES);
    if (table_match(HOSTS_DENY, daemon, client))
	return (NO);
    return (YES);
}

/* table_match - match table entries with (daemon, client) pair */

static int table_match(table, daemon, client)
char   *table;
char   *daemon;
struct from_host *client;		/* host or user name may be empty */
{
    FILE   *fp;
    char    sv_list[BUFLEN];		/* becomes list of daemons */
    char   *cl_list;			/* becomes list of clients */
    char   *sh_cmd;			/* becomes optional shell command */
    int     match;
    int     end;

    /* The following variables should always be tested together. */

    int     sv_match = NO;		/* daemon matched */
    int     cl_match = NO;		/* client matced */

    /*
     * Process the table one logical line at a time. Lines that begin with a
     * '#' character are ignored. Non-comment lines are broken at the ':'
     * character (we complain if there is none). The first field is matched
     * against the daemon process name (argv[0]), the second field against
     * the host name or address. A non-existing table is treated as if it
     * were an empty table. The search terminates at the first matching rule.
     * When a match is found an optional shell command is executed.
     */

    if (fp = fopen(table, "r")) {
	while (!(sv_match && cl_match) && xgets(sv_list, sizeof(sv_list), fp)) {
	    if (sv_list[end = strlen(sv_list) - 1] != '\n') {
		syslog(LOG_ERR, "%s: missing newline or line too long", table);
		continue;
	    }
	    if (sv_list[0] == '#')		/* skip comments */
		continue;
	    while (end > 0 && isspace(sv_list[end - 1]))
		 end--;
	    sv_list[end] = '\0';		/* strip trailing whitespace */
	    if (sv_list[0] == 0)		/* skip blank lines */
		continue;
	    if ((cl_list = strchr(sv_list, ':')) == 0) {
		syslog(LOG_ERR, "%s: malformed entry: \"%s\"", table, sv_list);
		continue;
	    }
	    *cl_list++ = '\0';			/* split 1st and 2nd fields */
	    if ((sh_cmd = strchr(cl_list, ':')) != 0)
		*sh_cmd++ = '\0';		/* split 2nd and 3rd fields */
	    if ((sv_match = list_match(sv_list, daemon, string_match)))
		cl_match = list_match(cl_list, (char *) client, CLIENT_MATCH);
	}
	(void) fclose(fp);
    } else if (errno != ENOENT) {
	syslog(LOG_ERR, "cannot open %s: %m", table);
    }
    match = (sv_match == YES && cl_match == YES);
    if (match && sh_cmd)
	OPTIONS_STYLE(sh_cmd, daemon, client);
    return (match);
}

/* list_match - match an item against a list of tokens with exceptions */

static int list_match(list, item, match_fn)
char   *list;
char   *item;
int   (*match_fn) ();
{
    char   *tok;
    int     match = NO;

    /*
     * Process tokens one at a time. We have exhausted all possible matches
     * when we reach an "EXCEPT" token or the end of the list. If we do find
     * a match, look for an "EXCEPT" list and recurse to determine whether
     * the match is affected by any exceptions.
     */

    for (tok = strtok(list, sep); tok != 0; tok = strtok((char *) 0, sep)) {
	if (strcasecmp(tok, "EXCEPT") == 0)	/* EXCEPT: give up */
	    break;
	if (match = (*match_fn) (tok, item))	/* YES or FAIL */
	    break;
    }
    /* Process exceptions to YES or FAIL matches. */

    if (match != NO) {
	while ((tok = strtok((char *) 0, sep)) && strcasecmp(tok, "EXCEPT"))
	     /* VOID */ ;
	if (tok == 0 || list_match((char *) 0, item, match_fn) == NO)
	    return (match);
    }
    return (NO);
}

/* client_match - match host name and address against token */

static int client_match(tok, item)
char   *tok;
char   *item;
{
    struct from_host *client = (struct from_host *) item;
    int     match;

    /*
     * Try to match the address first. If that fails, try to match the host
     * name if available.
     */

    if ((match = string_match(tok, client->addr)) == 0)
	if (client->name[0] != 0)
	    match = string_match(tok, client->name);
    return (match);
}

#ifdef USER_AT_HOST

/* userhost_match - do user@host access control */

static int userhost_match(tok, item)
char   *tok;
char   *item;
{
    struct from_host *client = (struct from_host *) item;
    int     match = NO;
    char   *at;
    int     host_match;
    int     user_match;

    /*
     * Warning: experimental code, enabled only when USER_AT_HOST is defined.
     * 
     * Basically, you specify user_pattern@host_pattern where remote username
     * lookups are desired, and plain host_pattern for all other cases. The
     * syntax of user name patterns is the same as for hosts or daemons, but
     * ALL is probably the only user_pattern that makes sense.
     * 
     * In case of UDP connections, the result of username lookup will always be
     * "unknown".
     * 
     * Return FAIL if we match a pattern of the form user@FAIL or FAIL@host:
     * FAIL, like NO, is transitive. According to some people, such patterns
     * should be taken out and shot. Good news: FAIL is on its way out.
     */

    if (at = strchr(tok + 1, '@')) {		/* user@host */
	*at = 0;
	if (host_match = client_match(at + 1, item)) {
	    if (client->user[0] == 0) {
		if (client->sock_type != FROM_CONNECTED) {
		    client->user = FROM_UNKNOWN;
		} else if (client->sin == 0) {
		    syslog(LOG_ERR, "no socket info for user name lookup");
		    client->user = FROM_UNKNOWN;
		} else {
		    client->user = rfc931_name(client->sin);
		}
	    }
	    user_match = string_match(tok, client->user);
	    if (user_match == NO || user_match == FAIL) {
		match = user_match;
	    } else {
		match = host_match;
	    }
	}
	*at = '@';
    } else {					/* host */
	match = client_match(tok, item);
    }
    return (match);
}

#endif /* USER_AT_HOST */

/* string_match - match string against token */

static int string_match(tok, string)
char   *tok;
char   *string;
{
    int     tok_len;
    int     str_len;
    char   *cut;
#ifdef	NETGROUP
    static char *mydomain = 0;
#endif

    /*
     * Return YES if a token has the magic value "ALL". Return FAIL if the
     * token is "FAIL". If the token starts with a "." (domain name), return
     * YES if it matches the last fields of the string. If the token has the
     * magic value "LOCAL", return YES if the string does not contain a "."
     * character. If the token ends on a "." (network number), return YES if
     * it matches the first fields of the string. If the token begins with a
     * "@" (netgroup name), return YES if the string is a (host) member of
     * the netgroup. Return YES if the token fully matches the string. If the
     * token is a netnumber/netmask pair, return YES if the address is a
     * member of the specified subnet.
     */

    if (tok[0] == '.') {			/* domain: match last fields */
	if ((str_len = strlen(string)) > (tok_len = strlen(tok))
	    && strcasecmp(tok, string + str_len - tok_len) == 0)
	    return (YES);
    } else if (tok[0] == '@') {			/* netgroup: look it up */
#ifdef	NETGROUP
	if (mydomain == 0)
	    yp_get_default_domain(&mydomain);
	if (!isdigit(string[0])
	    && innetgr(tok + 1, string, (char *) 0, mydomain))
	    return (YES);
#else
	syslog(LOG_ERR, "wrapper: netgroup support is not configured");
	return (NO);
#endif
    } else if (strcasecmp(tok, "ALL") == 0) {	/* all: match any */
	return (YES);
    } else if (strcasecmp(tok, "FAIL") == 0) {	/* fail: match any */
	return (FAIL);
    } else if (strcasecmp(tok, "LOCAL") == 0) {	/* local: no dots */
	if (strchr(string, '.') == 0 && strcasecmp(string, "unknown") != 0)
	    return (YES);
    } else if (!strcasecmp(tok, string)) {	/* match host name or address */
	return (YES);
    } else if (tok[(tok_len = strlen(tok)) - 1] == '.') {	/* network */
	if (strncmp(tok, string, tok_len) == 0)
	    return (YES);
    } else if ((cut = strchr(tok, '/')) != 0) {	/* netnumber/netmask */
	if (isdigit(string[0]) && masked_match(tok, cut, string))
	    return (YES);
    }
    return (NO);
}

/* masked_match - match address against netnumber/netmask */

static int masked_match(tok, slash, string)
char   *tok;
char   *slash;
char   *string;
{
    unsigned long net;
    unsigned long mask;
    unsigned long addr;

    if ((addr = inet_addr(string)) == INADDR_NONE)
	return (NO);
    *slash = 0;
    net = inet_addr(tok);
    *slash = '/';
    if (net == INADDR_NONE || (mask = inet_addr(slash + 1)) == INADDR_NONE) {
	syslog(LOG_ERR, "bad net/mask access control: %s", tok);
	return (NO);
    }
    return ((addr & mask) == net);
}

/* xgets - fgets() with backslash-newline stripping */

static char *xgets(buf, len, fp)
char   *buf;
int     len;
FILE   *fp;
{
    int     got;
    char   *start = buf;

    for (;;) {
	if (fgets(buf, len, fp) == 0)
	    return (buf > start ? start : 0);
	got = strlen(buf);
	if (got >= 2 && buf[got - 2] == '\\' && buf[got - 1] == '\n') {
	    got -= 2;
	    buf += got;
	    len -= got;
	    buf[0] = 0;
	} else {
	    return (start);
	}
    }
}
