/* This file is part of the Project Athena Zephyr Notification System.
 * It contains functions for maintaining Access Control Lists.
 *
 *	Created by:	John T. Kohl
 *
 *	$Id: 9f67fd79b986cb27704d51d3fbc73b2743b0d546 $
 *
 *	Copyright (c) 1987,1988 by the Massachusetts Institute of Technology.
 *	For copying and distribution information, see the file
 *	"mit-copyright.h". 
 */

/* Define this if you really want the ACL-writing code included.  */

/*
 * Stolen from lib/acl_files.c because acl_load needs to be externally
 * declared and not statically declared.
 */

#include <zephyr/mit-copyright.h>
#include "zserver.h"
#include <fnmatch.h>

#ifndef SABER
#ifndef lint
static const char rcsid_acl_files_c[] = "$Id: 9f67fd79b986cb27704d51d3fbc73b2743b0d546 $";
#endif /* lint */
#endif /* SABER */

/*** Routines for manipulating access control list files ***/

#define REALM_SEP '@'
#define ESCAPE '\\'

#define CACHED_ACLS 256		/* How many acls to cache */
#define ACL_LEN 256		/* Twice a reasonable acl length */

/* Eliminate all whitespace character in buf */
/* Modifies its argument */
static void
nuke_whitespace(char *buf)
{
    char *pin, *pout;

    for (pin = pout = buf; *pin != '\0'; pin++)
	if (!isspace(*pin)) *pout++ = *pin;
    *pout = '\0';		/* Terminate the string */
}

struct host_ace {
    struct host_ace *next;
    unsigned long addr, mask;
};

static int
add_host(struct host_ace **list,
	 char *buf)
{
    struct host_ace *e;
    struct in_addr addr;
    unsigned long mask = 0;
    long i;
    char *m, *x;

    m = strchr(buf, '/');
    if (m) {
	*(m++) = 0;
	if (!*m)
            return EINVAL;
	i = strtol(m, &x, 10);
	if (*x || i < 0 || i > 32)
            return EINVAL;
	while (i--)
	    mask = (mask >> 1) | 0x80000000;
    } else {
	mask = 0xffffffff;
    }

    if (!inet_aton(buf, &addr))
	return EINVAL;

    e = (struct host_ace *)malloc(sizeof(struct host_ace));
    if (e == NULL)
        return errno;
    memset(e, 0, sizeof(struct host_ace));
    e->addr = addr.s_addr;
    e->mask = htonl(mask);
    e->next = *list;
    *list = e;

    return 0;
}

static void
destroy_hosts(struct host_ace **list)
{
    struct host_ace *e;

    while ((e = *list)) {
	*list = e->next;
	free(e);
    }
}

static char *
split_name(char *princ) {
    int i;
    int len = strlen(princ);

    for (i = 0; i < len && princ[i] != REALM_SEP; i++)
        if (princ[i] == ESCAPE && (i + 1) < len)
            i++;
    if (i != len) { /* yay found an @ */
        princ[i] = 0;
        return &princ[i + 1];
    }
    return &princ[i]; /* failure, just return a pointer empty string */
}

struct user_ace {
    struct user_ace *next;
    char *princ;
    char *realm;
};

static int
add_user(struct user_ace **list, char *princ) {
    struct user_ace *e;
    char *realm = split_name(princ);

    e = (struct user_ace *)malloc(sizeof(struct user_ace));
    if (e == NULL)
        return errno;
    memset(e, 0, sizeof(struct user_ace));

    if (!strcmp(princ, "*.*"))     /* #ifdef KRB4_COMPAT */
        e->princ = strdup("*");
    else                           /* #endif */
        e->princ = strdup(princ);
    if (e->princ == NULL) {
        free(e);
        return errno;
    }

    e->realm = strdup(realm);
    if (e->realm == NULL) {
        free(e->princ);
        free(e);
        return errno;
    }
    e->next = *list;
    *list = e;

    return 0;
}

static void
destroy_user(struct user_ace **list) {
    struct user_ace *e;
    while ((e = *list)) {
        *list = e->next;
        free(e->princ);
        free(e->realm);
        free(e);
    }
}

static int
check_user(struct user_ace *list, char *princ, char *realm) {
    struct user_ace *e;

    e = list;
    while (e) {
        if (fnmatch(e->princ, princ, 0) == 0
            && fnmatch(e->realm, realm, 0) == 0)
            return 1;
        e = e->next;
    }

    return 0;
}

struct acl {
    String *filename;	        /* Name of acl file */
    int loaded;
    struct user_ace *acl;       /* Positive acl entries */
    struct user_ace *negacl;    /* Negative acl entries */
    struct host_ace *hosts;     /* Positive host entries */
    struct host_ace *neghosts;  /* Negative host entries */
};

static struct acl acl_cache[CACHED_ACLS];

static int acl_cache_count = 0;
static int acl_cache_next = 0;

/* wipe an entry in the acl cache */
static void
destroy_acl(int i) {
    if (acl_cache[i].filename)
        free_string(acl_cache[i].filename);
    if (acl_cache[i].acl)
        destroy_user(&acl_cache[i].acl);
    if (acl_cache[i].negacl)
        destroy_user(&acl_cache[i].negacl);
    if (acl_cache[i].hosts)
        destroy_hosts(&acl_cache[i].hosts);
    if (acl_cache[i].neghosts)
        destroy_hosts(&acl_cache[i].neghosts);
    acl_cache[i].filename = NULL;
    acl_cache[i].acl = (struct user_ace *) 0;
    acl_cache[i].negacl = (struct user_ace *) 0;
    acl_cache[i].hosts = (struct host_ace *) 0;
    acl_cache[i].neghosts = (struct host_ace *) 0;
    acl_cache[i].loaded = 0;
}

/* Returns < 0 if unsuccessful in loading acl */
/* Returns index into acl_cache otherwise */
/* Note that if acl is already loaded, this is just a lookup */
int acl_load(char *name)
{
    int i;
    FILE *f;
    char buf[BUFSIZ];
    int ret = 0;
    String *interned_name;

    syslog(LOG_DEBUG, "acl_load(%s)", name);
    /* See if it's there already */
    interned_name = make_string(name, 0);
    for (i = 0; i < acl_cache_count; i++) {
	if (acl_cache[i].filename == interned_name)
	    goto got_it;
    }

    /* It isn't, load it in */
    /* maybe there's still room */
    if (acl_cache_count < CACHED_ACLS) {
	i = acl_cache_count++;
    } else {
	/* No room, clean one out */
	i = acl_cache_next;
	acl_cache_next = (acl_cache_next + 1) % CACHED_ACLS;
        destroy_acl(i);
    }

    /* Set up the acl */
    acl_cache[i].filename = interned_name;
    /* Force reload */
    acl_cache[i].loaded = 0;

  got_it:
    /*
     * See if we need to reload the ACL
     */
    if (!acl_cache[i].loaded) {
        syslog(LOG_DEBUG, "acl_load(%s) actually loading", name);
	/* Gotta reload */
	if ((f = fopen(name, "r")) == NULL) {
	    syslog(LOG_ERR, "Error loading acl file %s: %m", name);
	    return -errno;
	}
	if (acl_cache[i].acl)
            destroy_user(&acl_cache[i].acl);
	if (acl_cache[i].negacl)
            destroy_user(&acl_cache[i].negacl);
	acl_cache[i].acl = (struct user_ace *)0;
	acl_cache[i].negacl = (struct user_ace *)0;
	while(fgets(buf, sizeof(buf), f) != NULL && ret == 0) {
	    nuke_whitespace(buf);
            if (!buf[0])
                continue;
	    if (buf[0] == '!' && buf[1] == '@')
		ret = add_host(&acl_cache[i].neghosts, buf + 2);
	    else if (buf[0] == '@')
		ret = add_host(&acl_cache[i].hosts, buf + 1);
	    else if (buf[0] == '!')
		ret = add_user(&acl_cache[i].negacl, buf + 1);
	    else
		ret = add_user(&acl_cache[i].acl, buf);
	}
	fclose(f);
        if (ret) {
            destroy_acl(i);
            return -ret;
        }
        acl_cache[i].loaded = 1;
    }
    return i;
}

/*
 * This destroys all cached ACL's so that new ones will be loaded in
 * the next time they are requested.
 */
void
acl_cache_reset(void)
{
    int	i;

    syslog(LOG_DEBUG, "acl_cache_reset()");
    for (i = 0; i < acl_cache_count; i++)
        destroy_acl(i);
    acl_cache_count = 0;
    acl_cache_next = 0;
}

/* Returns nonzero if it can be determined that acl contains principal */
/* Principal is not canonicalized, and no wildcarding is done */
/* If neg is nonzero, we look for negative entries */
static int
acl_match(char *acl, char *princ, char *realm, int neg)
{
    int idx;

    if ((idx = acl_load(acl)) < 0)
        return 0;
    if (neg)
        return check_user(acl_cache[idx].negacl, princ, realm);
    else
        return check_user(acl_cache[idx].acl, princ, realm);
}

/* Returns nonzero if it can be determined that acl contains who */
/* If neg is nonzero, we look for negative entries */
static int
acl_host_match(char *acl,
               unsigned long who,
               int neg)
{
    int idx;
    struct host_ace *e;

    if ((idx = acl_load(acl)) < 0)
        return 0;
    e = neg ? acl_cache[idx].neghosts : acl_cache[idx].hosts;
    while (e) {
	if ((e->addr & e->mask) == (who & e->mask))
            return 1;
	e = e->next;
    }
    return 0;
}

/* Returns nonzero if it can be determined that acl contains principal */
/* Recognizes wildcards in acl. */
/* Also checks for IP address entries and applies negative ACL's */
int
acl_check(char *acl, char *princ, struct sockaddr_in *who)
{
    char *realm;
    char *name;
    int result = 0;

    if (princ) {
        name = strdup(princ);
        realm = split_name(name);

        if (acl_match(acl, name, realm, 1))
            return 0;
        if (acl_match(acl, name, realm, 0))
            result = 1;
        free(name);
    }

    if (who) {
	if (acl_host_match(acl, who->sin_addr.s_addr, 1))
            return 0;
	if (acl_host_match(acl, who->sin_addr.s_addr, 0))
            result = 1;
    }

    return result;
}
