/*
 * netplan access lists: read and write the access list file and verify
 * whether a file is accessible or not based on the access list. The
 * functions in this file are called from netplan.c. The functions are:
 *
 * acl_read	read an access list file into memory
 * acl_write	write memory to an access list file
 * acl_verify	verify the rights a client has on a file based on the list
 * acl_exit	shut down, release the list
 * ident_id	get user ID from remote identd server
 *
 * The syntax for the access list file is a sequence of rules like this:
 *
 *	name | owner | * :
 *		   [permit | deny] [read] [write] [delete] [netmask n.n.n.n]
 *	           [[user | group | host] data [data ...]]
 *
 * <name> is the file the rule applies to; "owner" applies if the file name
 * matches the user name (derived from uid), an asterisk (*) applies to all
 * files. Permit is the default. If none of read,write,delete are specified,
 * all three are the default. The netmask applies to the client's IP address,
 * and is ffffffff if missing.
 * <data> is one or more numerical UIDs or user names, numerical GIDs or group
 * names, or numerical n.n.n.n IP addresses or host names, for user, group,
 * and host rules, respectively. User is the default. Trailing n=0 IP address
 * components are not assumed to denote nets, use the netmask specifier for
 * subnet masking. All whitespace is ignored.
 * Pound signs (#) introduce comments that extend to the end of the line.
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>
#include <limits.h>
#ifdef IBM
#include <sys/select.h>
#endif
#include <sys/types.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netdb.h>
#ifdef NEWSOS4
#include <machine/endian.h>
#endif
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <pwd.h>
#include <grp.h>
#include "netplan.h"

typedef enum {aUSER=0, aGROUP, aHOST, aUSERHOST} Mode;

typedef struct Acl {
	struct Acl	*next;		/* next access list entry in chain */
	char		*name;		/* applies to this file, 0=all */
	BOOL		deny;		/* TRUE=deny, FALSE=permit */
	BOOL		read;		/* permit/deny reading */
	BOOL		write;		/* permit/deny writing */
	BOOL		delete;		/* permit/deny deleting */
	Mode		mode;		/* check user or group? */
	unsigned int	netmask;	/* if a(USER)HOST, netmask, 0=default*/
	int		ndata;		/* # of data elems, -1 is wildcard */
	int		maxdata;	/* allocated # of elems in data list */
	unsigned long	*ugid, *host;	/* uid/gid and host/net data */
} Acl;

static Acl		*acl_root;	/* root of current access list */
static char		*word;		/* current word from input stream */
static char		*nextword;	/* next word from input (readahead) */

static BOOL getword(FILE *);
static unsigned int ipaddr_atoi(const char *);
static char *ip_itoa (unsigned int);
static void acl_add_data(Acl *acl, unsigned int ugid, unsigned int host);

extern void *allocate(int n);
extern void *reallocate(void *o, int n);
extern void release(void *p);
extern char *mystrdup(const char *s);
extern void log(char *fmt, ...);
extern char *ip_addr(register struct sockaddr_in *);


/*
 * forget the previous access list and read a new one ffrom the given file.
 * This must be done before the first verify because an empty access list
 * means deny everything. Returns FALSE if reading fails; in this case the
 * access list is left empty even if the file was ok partially, on the
 * theory that it's better to deny everything than permitting too much.
 *
 * phase 0:	name :
 * phase 1:	permid deny   read write delete   user group host netmask
 * phase 2:	data data data ...
 */

int acl_read(
	const char	*fname)		/* access list file name to read */
{
	FILE		*fp;		/* open file */
	Acl		*tail = 0;	/* previous access list entry */
	Acl		*acl  = 0;	/* current new access list entry */
	int		phase = 0;	/* 0=name, 1=mode, 2=data */
	int		ret   = TRUE;	/* return code */
	struct hostent	*h;		/* for adding hosts to ACL */
	struct passwd	*u;		/* for adding users to ACL */
	struct group	*g;		/* for adding groups to ACL */
	int		**ipp;
	char		*c;
	unsigned int	usr;

	acl_exit();
	if (!(fp = fopen(fname, "r"))) {
		perror(fname);
		fprintf(stderr, "this is ok; access control disabled.\n");
		return(FALSE);
	}
	while (ret && getword(fp)) {
		if (*nextword == ':')				/* new rule */
			phase = 0;
		if (phase == 1) {				/* read, ... */
			if (!strcmp(word, "permit"))
				acl->deny = FALSE;
			else if (!strcmp(word, "deny"))
				acl->deny = TRUE;

			else if (!strcmp(word, "read"))
				acl->read = TRUE;
			else if (!strcmp(word, "write"))
				acl->write = TRUE;
			else if (!strcmp(word, "delete"))
				acl->delete = TRUE;

			else if (!strcmp(word, "user")) {
				acl->mode = aUSER;
				phase = 2;
				continue;
			} else if (!strcmp(word, "group")) {
				acl->mode = aGROUP;
				phase = 2;
				continue;
			} else if (!strcmp(word, "host")) {
				acl->mode = aHOST;
				phase = 2;
				continue;
			} else if (!strcmp(word, "netmask")) {
				getword(fp);
				acl->netmask = ipaddr_atoi(word);
			} else
				phase = 2;
		}
		if (phase == 2) {				/* data */
			if (acl->mode == aUSER) {
				for (c=word; *c && *c != '@'; c++);
				if (*c)
					acl->mode = aUSERHOST;
			}
			if (*word>='0' && *word<='9' && acl->mode != aUSERHOST)
				if (acl->mode == aHOST)
					acl_add_data(acl, 0,ipaddr_atoi(word));
				else
					acl_add_data(acl, atoi(word), 0);
			else switch(acl->mode) {
			  case aUSER:
				if (u = getpwnam(word))
					acl_add_data(acl, u->pw_uid, 0);
				else
					log("%s: unknown user %s, ign"
						"ored\n", fname, word);
				break;

			  case aGROUP:
				if (g = getgrnam(word))
					acl_add_data(acl, g->gr_gid, 0);
				else
					log("%s: unknown group %s, ignored\n",
						fname, word);
				break;

			  case aHOST:
				if (h = gethostbyname(word)) {
				    for (ipp=(int**)h->h_addr_list; *ipp;ipp++)
					acl_add_data(acl, 0, ntohl(**ipp));
				} else
					log("%s: unknown host %s, ignored\n",
						fname, word);
				break;

			  case aUSERHOST:
			        acl->mode = aUSER; /* GU: preparing for next word */ 
				*c++ = 0;
				if (*word >= '0' && *word <= '9')
					usr = atoi(word);
				else if (!(u = getpwnam(word))) {
					log("%s: unknown user %s, ignored\n",
						fname, word);
					break;
				} else
					usr = u->pw_uid;
				if (*c >= '0' && *c <= '9')
					acl_add_data(acl, usr, ipaddr_atoi(c));
				else if (h = gethostbyname(c))
				    for (ipp=(int**)h->h_addr_list; *ipp;ipp++)
					acl_add_data(acl, usr, ntohl(**ipp));
				else
					log("%s: unknown host %s, ignored\n",
						fname, word);
			}
		}
		if (phase == 0) {				/* name : */
			acl = (Acl *)allocate(sizeof(Acl));
			memset(acl, 0, sizeof(Acl));
			if (tail)
				tail = tail->next = acl;
			else
				tail = acl_root = acl;
			if (strcmp(word, "*") && !(acl->name=mystrdup(word))) {
				perror(fname);
				ret = FALSE;

			} else if (getword(fp) && *word != ':') {
				ret = FALSE;
				log("%s: no ':' after \"%s\"\n",
					fname, acl->name ? acl->name : "*");
			} else
				phase = 1;
		}
	}
	for (acl=acl_root; acl; acl=acl->next)
		if (!acl->read && !acl->write && !acl->delete)
			acl->read = acl->write = acl->delete = TRUE;
	fclose(fp);
	if (!fname) {
		perror(fname);
		ret = FALSE;
	}
	if (!ret)
		acl_exit();
	return(ret);
}


/*
 * add one word of data payload to the Acl structure. Return FALSE on error.
 */

static void acl_add_data(
	Acl		*acl,
	unsigned int	ugid,
	unsigned int	host)
{
	if (acl->ndata >= acl->maxdata)
	{	acl->ugid = acl->maxdata
		    ? reallocate(acl->ugid, sizeof(ugid) * (acl->maxdata *= 2))
		    : allocate(sizeof(ugid) * (acl->maxdata = 16));
		acl->host = acl->maxdata
		    ? reallocate(acl->host, sizeof(host) * (acl->maxdata *= 2))
		    : allocate(sizeof(host) * (acl->maxdata = 16));
	}
	acl->ugid[acl->ndata] = ugid;
	acl->host[acl->ndata++] = host;
}


/*
 * a subfunction of the previous, which reads the access list file word by
 * word. A word is either a sequence of one or more characters of the char
 * set [a-zA-Z0-9_.], or a single character that is not in the set and is
 * not a comma or a minus. Whitespace, comma, and minus are ignored. This
 * makes the access list file free-format, newlines are treated like spaces.
 * Pound signs (#) introduce comments that extend to the end of the line.
 * This function always reads one word ahead so the parser can check when
 * the next word is ':', which means that the current word begins a new
 * file rule and is not another data word.
 */

#define ISBLANK(c) strchr(", \t\n\r", c)
#define ISWORD(c) (c >= 'a' && c <= 'z' ||\
		   c >= 'A' && c <= 'Z' ||\
		   c >= '0' && c <= '9' ||\
		   c == '_' || c == '.' || c == '-' || c == '@')

static BOOL getword(
	FILE		*fp)		/* file to read from */
{
	static char	buf1[256];	/* current/next word buffer */
	static char	buf2[256];	/* current/next word buffer */
	char		*p;		/* next free char in nextword buffer */
	int		c;		/* current char read from file */
	int		max = sizeof(buf1);

	if (!nextword) {			/* startup init */
		word = buf1;
		nextword = buf2;
		getword(fp);
	}
	p = word;				/* swap buffers, word=next */
	word = nextword;
	nextword = p;

	for (;;) {				/* readahead next word */
		while (ISBLANK(c = fgetc(fp)));
		if (c != '#')
			break;
		while ((c = fgetc(fp)) != EOF && c != '\n');
	}
	if (c != EOF)
		*p++ = c;
	while (--max > 1 && (c = fgetc(fp)) != EOF && ISWORD(c))
		*p++ = c;
	*p = 0;
	ungetc(c, fp);
	return(*word);
}


/*
 * write an access list back to a given file, in the same format expected
 * by acl_read. If writing fails (probably because the file is not writable),
 * return FALSE. This function is currently not used because there is no way
 * to modify the list as it was read from the file originally, so why bother?
 */

int acl_write(
	const char	*fname)		/* access list file name to write */
{
	FILE		*fp;		/* open file */
	Acl		*acl;		/* current new access list entry */
	int		i;		/* for scanning data list */
	static const char fstderr[] = "stderr";

	if (!fname) {
		fname = fstderr;
		fp = stderr;
	} else if (!(fp = fopen(fname, "w"))) {
		perror(fname);
		return(FALSE);
	}
	fprintf(fp, "# netplan database access permissions file\n");
	fprintf(fp, "#\n");
	fprintf(fp, "# file|'*' ':' permit|deny\n");
	fprintf(fp, "#	[read] [write] [delete] [netmask n.n.n.n]\n");
	fprintf(fp, "#	[user|group|host <id>*|'*']\n");
	fprintf(fp, "#\n");

	for (acl=acl_root; acl; acl=acl->next) {
		fprintf(fp, "%s: %s", acl->name ? acl->name : "*",
				      acl->deny ? "deny" : "permit");
		if (acl->read)
			fprintf(fp, " read");
		if (acl->write)
			fprintf(fp, " write");
		if (acl->delete)
			fprintf(fp, " delete");
		if (acl->netmask)
			fprintf(fp, " netmask %s", ip_itoa(acl->netmask));
		if (acl->ndata)
			switch(acl->mode) {
			  case aUSER:	  fprintf(fp, "\n\tuser");	break;
			  case aGROUP:	  fprintf(fp, "\n\tgroup");	break;
			  case aHOST:	  fprintf(fp, "\n\thost");	break;
			}
		for (i=0; i < acl->ndata; i++)
			if (acl->mode == aHOST)
				fprintf(fp, " %s", ip_itoa(acl->host[i]));
			else if (acl->mode == aUSERHOST)
				fprintf(fp, " %d@%s", (int)acl->ugid[i],
					ip_itoa(acl->host[i]));
			else
				fprintf(fp, " %d", (int)acl->ugid[i]);
		fprintf(fp, "\n");
	}
	if (fname != fstderr)
		fclose(fp);
	return(TRUE);
}


/*
 * simple conversion functions for IP addresses: 32-bit unsigned ints to and
 * from four-part dotted-decimal notation. Symbolic IP addresses are not
 * supported.
 */

static unsigned int ipaddr_atoi(
	const char	*addr)		/* n.n.n.n string to convert */
{
	unsigned int	a=0, b=0, c=0, d=0;

	sscanf(addr, "%d.%d.%d.%d", &a, &b, &c, &d);
	return((a&255) << 24 | (b&255) << 16 | (c&255) << 8 | (d&255));
}


static char *ip_itoa(
	unsigned int	ip)		/* IP address to convert */
{
	static char	addr[16];	/* n.n.n.n address string buffer */

	sprintf(addr, "%d.%d.%d.%d", ip>>24, ip>>16&255, ip>>8&255, ip&255);
	return(addr);
}


/*
 * the reason why all of the above is done: return the rights that the user
 * described by the client structure <c> (containing uid, gid, IP address, and
 * name), has for <file>. The right to read, write, delete are distinguished.
 * The access list is scanned for rules matching the file name and for default
 * rules (marked by * in the access list file). This is called whenever a
 * client opens a file. A client who failed authentication (-a was specified
 * but identd didn't confirm 
 */

void acl_verify(
	int		*r,		/* set to TRUE if reading is ok */
	int		*w,		/* set to TRUE if writing is ok */
	int		*d,		/* set to TRUE if deleting is ok */
	char		*name,		/* file name to verify */
	struct client   *c)             /* client structure */
{
	Acl		*acl;		/* current acl entry */
	unsigned int	nm;		/* current netmask for <ip> */
	int		i, j;		/* for searching data[] and gids[]*/
	BOOL		found;

	*r = *w = *d = FALSE;
	for (acl=acl_root; acl; acl=acl->next) {
		if (acl->name && !c->auth_fail &&
		   (strcmp(acl->name, "owner") || strcmp(c->user, name)) &&
		    strcmp(acl->name, name))
			continue;
		switch(acl->mode) {
		  case aUSER:
			if (c->auth_fail)
				continue;
			for (i=0; i < acl->ndata; i++)
				if (acl->ugid[i] == c->uid)
					break;
			break;

		  case aGROUP:
			if (c->auth_fail)
				continue;
			for (found=FALSE, i=0; !found && i < acl->ndata;
								!found && i++)
				for (j=0; j<NGROUPS_MAX && c->gids[j]>=0; j++)
					if (found = acl->ugid[i] == c->gids[j])
						break;
			break;

		  case aHOST:
			nm = acl->netmask ? acl->netmask : 0xffffffff;
			for (i=0; i < acl->ndata; i++)
				if ((nm & acl->host[i]) ==
				    (nm & ntohl(c->addr.sin_addr.s_addr)))
					break;
			break;

		  case aUSERHOST:
			nm = acl->netmask ? acl->netmask : 0xffffffff;
			for (i=0; i < acl->ndata; i++)
				if ((acl->ugid[i] == c->uid) &&
					((nm & acl->host[i]) ==
					(nm & ntohl(c->addr.sin_addr.s_addr))))
					break;
		}
		if (!acl->ndata || i < acl->ndata) {
			if (acl->read)	  *r = !acl->deny;
			if (acl->write)	  *w = !acl->deny;
			if (acl->delete)  *d = !acl->deny;
		}
	}
}


/*
 * shut down access lists. Release all memory. This is also done when the
 * access list file is re-read to keep rules from accumulating.
 */

void acl_exit(void)
{
	Acl		*acl, *next;	/* current and next acl entry */

	for (acl=acl_root; acl; acl=next) {
		next = acl->next;
		release(acl->name);
		release(acl->ugid);
		release(acl->host);
		release(acl);
	}
	acl_root = 0;
	word = nextword = 0;		/* make sure to restart readahead */
}


/*
 * ask an identd server for the user name at the other end of a connection.
 * This is not actually part of ACL but is used to see whether we can trust
 * the claimed user ID before it is applied to ACL verification. netplan
 * will use the identd name if available and ignore who the client said
 * he is. Return 0 if authentication failed, or the remote user name if ok.
 */

extern BOOL		verbose;	/* debug mode, don't daemonize */
extern struct client	*client_list;	/* array of clients (entry 0 unused) */

char *ident_id(
	int		fd)		/* client ID and socket descriptor */
{
	register struct client *c = &client_list[fd];
	char		*hostname;
	int		port;

	struct sockaddr_in addr;	/* IP address */
	int		idfd;		/* temporary socket descriptor */
	int		i, on;		/* for setting socket options */
	char		buf[1024], *p;	/* banner reply from server */

	struct servent *serv = getservbyname("auth", "tcp");
	port = serv ? serv->s_port : htons(113);
	hostname = ip_addr(&c->addr);

	if (verbose)
		log("client %d: authenticating, connecting to identd on "
						"%.100s\n", fd, hostname);
	if ((idfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		log("client %d: cannot open socket to authentication server on"
						"%.100s\n", fd, hostname);
		return(0);
	}
	addr.sin_family = AF_INET;
	addr.sin_port   = port;
	memcpy(&addr.sin_addr, &c->addr.sin_addr, sizeof(addr.sin_addr));
	if (connect(idfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
		close(idfd);
		log("client %d: cannot connect to authentication server on "
						"%.100s\n", fd, hostname);
		return(0);
	}
	on = 1;
	setsockopt(idfd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on));
	on = 1;
	setsockopt(idfd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on));

	sprintf(buf, "%d,%d\n", ntohs(c->addr.sin_port), c->port);
	write(idfd, buf, strlen(buf));
	if ((i = read(idfd, buf, sizeof(buf)-1)) < 0) {
		close(idfd);
		log("client %d: authentication server on %.100s does not "
						"reply\n", fd, hostname);
		return(0);
	}
	close(idfd);
	buf[i > 2 ? i-2 : 0] = 0;
	if (verbose)
		log("client %d: identd replied \"%.500s\"\n", fd, buf);
	if (!strstr(buf, "USERID")) {
		log("client %d: authentication failure on %.100s, identd "
				"replied \"%.500s\"\n", fd, hostname, buf);
		return(0);
	}
	if (p = strrchr(buf, ':'))
		while (*++p == ' ');
	return(p ? mystrdup(p) : 0);
}
