/*
 * server mainline. Must be linked with netplan_acl.o.
 */

typedef void *Widget;	/* dummy for prototype declarations in config.h */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdarg.h>
#include <time.h>
#include <limits.h>
#include <sys/types.h>
#include <dirent.h>
#include <errno.h>
#ifdef IBM
#include <sys/select.h>
#endif
#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"
#include "netplan_if.h"

#if defined(BSD)||defined(MIPS)	/* detach forked process from terminal */
#define ORPHANIZE setpgrp(0,0)	/* session, and attach to pid 1 (init). */
#else				/* This means we'll get no ^C and other */
#define ORPHANIZE setsid()	/* signals. If -DBSD doesn't help, the */
#endif				/* daemon works without, sort of. */

#define ACLCHKINTER 60          /* interval (secs) for check of the acl file */

static char		*progname;	/* name of this program, argv[0] */
static BOOL		debug;		/* foreground, logging is enabled */
static BOOL		foregnd;	/* foreground without logging */
BOOL			verbose;	/* debug mode, don't daemonize */
static BOOL		allow_all = FALSE; /* no access list, never deny */
static int		nclients;	/* size of array */
static unsigned int	row_id = 1;	/* next available row ID */
static int		max_files;	/* size of file_list array */
static struct pfile	*file_list;	/* array of open files */
struct client		*client_list;	/* array of clients (entry 0 unused) */
static fd_set		rd0, wr0, ex0;	/* preset fd masks for main select */
static time_t		update_time;	/* date of last unwritten change */
static BOOL             do_auth = FALSE;/* authentication required (identd) */
static int		netplan_port[2];/* ports we are listening on */
static int		fd_inet[2];	/* file descriptor for base socket */

static const char homepath[] = HOMEDIR "/" NETDIR "/.";
static const char aclpath[]  = HOMEDIR "/" NETDIR "/" ACL_FILE;

static void  init_inet_port(void);
static BOOL message(int);
static void eval_message(int, char *);
static void appendbuf(struct buffer *, char *, int);
static void notify_client(int, unsigned int, int);
static void close_client(int);
static BOOL check_file(FILE *, char *, int, struct client *);
static int  open_file(char *, int, BOOL *);
static void close_file(int, int, struct client *);
static void write_file(int);
static int  find_row(int, unsigned int, BOOL *);
static unsigned int add_row(int, unsigned int, int, char *, int);
static void delete_row(int, unsigned int, int);
static BOOL lock_row(int, unsigned int, int, BOOL);
static void send_row(int, unsigned int, int, char);
static int count_rows(int, int);
static void reply(int, struct client *, char *, ...);
static void error(int, char *, ...);
static void fatal(char *, ...);
char *ip_addr(register struct sockaddr_in *);
static void setup_gids(struct client *, int, char *);
static char *make_descr (struct client *, const char *username,
			 const char *arg);
void log(char *, ...);

void *allocate(int n)
	{void *p = malloc(n); if (!p) fatal("no memory"); return(p);}
void *reallocate(void *o, int n)
	{void *p = o ? realloc(o, n) : malloc(n);
	 if (!p) fatal("no memory"); return(p);}
void release(void *p)
	{if (p) free(p);}
char *mystrdup(const char *s)
	{char *p = (char *)allocate(strlen(s)+1); strcpy(p, s); return(p);}

static char usage[] = "recognized server commands:\\\n\
(S=success(t|f), V=version, F=file ID, R=row ID)\\\n\\\n\
-          -> !SV <msg>		version banner after connect\\\n\
=string    ->			save optional client self-description\\\n\
t<type>    ->			set client type: 0=plan, 1=grok\\\n\
m<dname>   -> mS		make directory (not used)\\\n\
o<fname>   -> oSwF		open file, is writable\\\n\
           -> oSrF		open file, is read-only\\\n\
nF         -> nF N		return number of rows in F\\\n\
cF         ->			close file\\\n\
rF R       -> rSF R <row>	read all (R=0) or one row, multiple replies\\\n\
-          -> RSF R <row>	unsolicited row msg: somebody changed it\\\n\
wF R <row> -> wSR		write one row, R=0: create\\\n\
dF R       ->			delete a row\\\n\
lF R       -> lS		lock row for editing\\\n\
uF R       ->			unlock row\\\n\
q          ->                   close connection explicitly\\\n\
L          -> LS<user> ... LS   list of users\\\n\
-          -> ?<msg>		server wants client to put up error popup\\\n\
.          -> .msg		debugging info";


/*------------------------------------- networking --------------------------*/

int main(
	int		argc,
	char		**argv)
{
	struct sockaddr_in addr;	/* used to accept fd_inet connection */
	struct timeval	timeout;	/* for auto-sync after ten seconds */
	fd_set		rd, wr, ex;	/* returned fd masks from select */
	int		fd;		/* file descriptor/counter */
	int		c, i, n, on;
	struct client	*cp;
	time_t          last_acl_check = 0;
	time_t          acl_mod_time = 0;
	time_t          t;
	struct stat     st;
	static const char nobody[] = "anonymous";
                                        /* descriptive name of unknown users */

	progname = argv[0];				/* cmd line options */
	while ((c = getopt(argc, argv, "advf")) != EOF)
		switch(c) {
		  case 'a': do_auth = TRUE;	break;
		  case 'v': verbose = TRUE;	break;
		  case 'd': debug   = TRUE;	break;
		  case 'f': foregnd = TRUE;	break;
		  default:  fatal("usage: %s [-f] [-d [-v]] [-a]\n"
				  "-f=foreground, -d=debug, -v=verbose "
				  "-a=authentication via identd required",
				  progname);
		}

	t = time(0);
	log("netplan version %s starting at %s", DVERSION, ctime(&t));
	log("database directory is %.200s/%.200s\n", HOMEDIR, NETDIR);

	if (NOBODY_UID == 0 || NOBODY_GID == 0)
		fatal("netplan compiled with nobody == root");
#ifndef __hpux
#ifdef SETREUID
	if (getuid() == 0 || geteuid() != 0)
		setreuid(-1, getuid());
	if (getgid() == 0 || getegid() != 0)
		setregid(-1, getgid());
#else
	if (getuid() == 0 || geteuid() != 0)
		seteuid(getuid());
	if (getgid() == 0 || getegid() != 0)
		setegid(getgid());
#endif
#endif
	if (getuid() == 0 || geteuid() == 0) {
		log("switching from user <root> to <uid %d gid %d>\n",
						NOBODY_UID, NOBODY_GID);
#ifndef __hpux
#ifdef SETREUID
		setregid(NOBODY_GID, NOBODY_GID);
#else
		setgid(NOBODY_GID);
		setegid(NOBODY_GID);
#endif
#else
		setgid(NOBODY_GID);
#endif

#ifndef __hpux
#ifdef SETREUID
		setreuid(NOBODY_UID, NOBODY_UID);
#else
		setuid(NOBODY_UID);
		seteuid(NOBODY_UID);
#endif
#else
		setuid(NOBODY_UID);
#endif
	}
	if (getgid() == 0 || getegid() == 0)
		fatal("refusing to run setgid root but not setuid root");
	if (getuid() == 0 || geteuid() == 0)
		fatal("refusing to run as root");

	log("running with uid=%d gid=%d euid=%d egid=%d\n",
				getuid(), getgid(), geteuid(), getegid());
	if (access(homepath, R_OK | W_OK | X_OK)) {
		fprintf(stderr, "%s: no read/write access to ", argv[0]);
		perror(homepath);
		_exit(1);
	}
	if (!debug && !foregnd) {
		(void)umask(0077);
		setuid(getuid());
		setgid(getgid());
		switch(fork()) {
		  case -1:				/* error */
			log("cannot fork\n");
			break;
		  case 0:				/* child */
			ORPHANIZE;
			chdir("/usr/tmp");
			break;
		  default:				/* parent */
			sleep(1);
			_exit(0);
		}
	}
	last_acl_check = time(NULL);
	if (!stat(aclpath, &st))
		acl_mod_time = st.st_mtime;
	log("reading access list file %.200s\n", aclpath);
	if (!acl_read(aclpath)) {
		if (do_auth)
			log("cannot read %.200s, allowing NOTHING\n", aclpath);
		else {
			log("cannot read %.200s, allowing everything\n",
								aclpath);
			allow_all = TRUE;
		}
	}
	nclients = sizeof(fd_set)*8;			/* max # of clients */
	client_list = allocate(nclients * sizeof(struct client));
	memset(client_list, 0, nclients * sizeof(struct client));

	init_inet_port();				/* network connection*/
	FD_ZERO(&rd0);
	FD_ZERO(&wr0);
	FD_ZERO(&ex0);
	if (fd_inet[0] >= 0)
		FD_SET(fd_inet[0], &rd0);
	if (fd_inet[1] >= 0)
		FD_SET(fd_inet[1], &rd0);

	for (;;) {					/* event loop */
		timeout.tv_sec = 11;
		timeout.tv_usec = 0;
		rd = rd0;
		wr = wr0;
		ex = ex0;
		n = select(nclients, &rd, &wr, &ex, update_time ? &timeout :0);

		if (update_time && update_time + 10 < time(0)) {
			log("writing all modified files to disk\n");
			for (fd=0; fd < max_files; fd++)
				write_file(fd);
			update_time = 0;
		}
		if (n == 0)
			continue;
		if ((t = time(NULL)) > last_acl_check + ACLCHKINTER &&
							!stat(aclpath, &st)) {
			last_acl_check = t;
			if (st.st_mtime > acl_mod_time) {
				acl_mod_time = st.st_mtime;
				log("rereading acl file\n");
				acl_exit();
				if (!acl_read(aclpath))
					if (allow_all)
						log("cannot read %.200s, "
						    "allowing everything\n",
						    aclpath);
					else
						log("cannot read %.200s, "
						    "allowing NOTHING\n",
						    aclpath);
				else
					allow_all = FALSE;
			}
		}
		for (c=0; c < 2; c++)				/* connect */
			if (fd_inet[c] >= 0 && FD_ISSET(fd_inet[c], &rd)) {
				n = sizeof(addr);
				if ((fd = accept(fd_inet[c], (struct sockaddr
							*)&addr, &n)) < 0)
					perror(progname);
				FD_SET(fd, &rd0);
				on = 1;
				setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE,&on,4);
				on = 1;
				setsockopt(fd, IPPROTO_TCP, TCP_NODELAY,&on,4);
				cp = &client_list[fd];

				memset(cp, 0, sizeof(struct client));
				memcpy(&cp->addr, &addr, n);
				cp->uid     = NOBODY_UID;
				cp->gids[0] = NOBODY_GID;
				cp->descr   = mystrdup(nobody);
				cp->user    = mystrdup(nobody);
				cp->port    = netplan_port[c];
				for (i=1; i < NGROUPS_MAX; i++)
					cp->gids[i] = -1;

				log("client %d (%.200s) connected from %s\n",
						fd, cp->descr, ip_addr(&addr));
				reply(fd, cp,"!t%s enter ? for help",DVERSION);
				cp->time = time(0);
			}
		for (fd=0; fd < nclients; fd++) {
			if (fd == fd_inet[0] || fd == fd_inet[1])
				continue;
			if (FD_ISSET(fd, &rd))			/* recv cmd */
				if (!message(fd)) {
					log("client %d disconnected\n", fd);
					close_client(fd);
					FD_CLR(fd, &rd0);
					close(fd);
				}
			if (FD_ISSET(fd, &wr)) {		/* send reply*/
				register struct buffer *b;
				b = &client_list[fd].out;
				n = write(fd, b->data+b->deq, b->enq-b->deq);
				if (n < 0)
					log("client %d write fail (%s)\n",
							fd, strerror(errno));
				b->deq += n;
				if (b->deq == b->enq || n < 0) {
					b->enq = b->deq = 0;
					FD_CLR(fd, &wr0);
				}
			}
		}
	}
}


/*
 * forced exit. Same trick as in plan, this is highly irregular but more
 * portable than atexit.
 */

void exit(
	int		ret)
{
	static BOOL	exiting;
	int		n;

	if (!exiting) {
		exiting = TRUE;
		log("exiting, writing all modified files to disk\n");
		for (n=0; n < max_files; n++)
			write_file(n);
		update_time = 0;
	}
	_exit(ret);
}


/*
 * fill in group access list for a connection
 */

static void setup_gids(
	struct client	*cp,
	int		gid,
	char		*user)
{
	struct group	*gr;
	int		ngroups = 0;
	char		**p;

	cp->gids[ngroups++] = gid;
	setgrent();
	while (gr = getgrent())
		for (p=gr->gr_mem; *p; p++)
			if (!strcmp(user, *p)) {
				if (ngroups < NGROUPS_MAX)
					cp->gids[ngroups] = gr->gr_gid;
				ngroups++;
				break;
			}
	if (ngroups > NGROUPS_MAX)
		log("too many groups (%d, used %d)\n", ngroups, NGROUPS_MAX);
}


/*
 * make a readable description for a client authenticated by identd
 */

static char *make_descr(
	struct client	*cp,
	const char	*username,
	const char	*arg)
{
	char		buf[512];
	struct hostent	*h = gethostbyaddr((const char *)&cp->addr.sin_addr,
					sizeof(cp->addr.sin_addr), AF_INET);
	char		*hostname = ip_addr(&cp->addr);
	const char	*start_id = strchr(arg, '<');
	const char	*end_id = strstr(arg, "pid=");
	char		*p;

	if (h)
		hostname = h->h_name;
	p = buf;
	if (start_id) {
		register int n = start_id - arg + 1;
		if (n > 40) n = 40;
		strncpy (p, arg, n);
		p += n;
	} else
		*p++ = '<';

	sprintf(p, "%.10s on %.20s", username, hostname);
	p += strlen (p);
	if (end_id > start_id) {
		*p++ = ',';
		strncpy(p, end_id, 40);
		p[40] = 0;
	} else {
		*p++ = '>';
		*p++ = 0;
	}
	return(mystrdup(buf));
}


/*
 * base socket connection, used for accepting. Listen to the port specified
 * in /etc/services. If there isn't one (none is installed by default), use
 * both PORT (IANA-registered, 2983) and OLDPORT (for backwards compatibility,
 * 5444).
 */

static void init_inet_port(void)
{
	int 			i, on=1;
	struct sockaddr_in	addr;
	struct servent		*serv;

	if (!(serv = getservbyname("netplan", "tcp")))
		log("netplan/tcp not found in /etc/services, using ports %d "
						"and %d\n", PORT, OLDPORT);
	netplan_port[0] = netplan_port[1] = 0;
	fd_inet[0] = fd_inet[1] = -1;
	for (i=0; i < 2; i++) {
		fd_inet[i] = socket(AF_INET, SOCK_STREAM, 0);
		if (fd_inet[i] < 0) {
			perror("socket");
			fatal("cannot open socket (error %d)", errno);
		}
		if (setsockopt(fd_inet[i], SOL_SOCKET, SO_REUSEADDR, &on,
								sizeof(on))) {
			perror("setsockopt");
			fatal("cannot setsockopt socket", 0);
		}
		netplan_port[i] = serv ? ntohs(serv->s_port) : i ?OLDPORT:PORT;
		addr.sin_family      = AF_INET;
		addr.sin_port	     = htons(netplan_port[i]);
		addr.sin_addr.s_addr = INADDR_ANY;
		if (bind(fd_inet[i], (struct sockaddr *)&addr, sizeof(addr))) {
			perror("bind");
			fatal("cannot bind to port %d, is another daemon "
						"running?", netplan_port[i]);
		}
		if (listen(fd_inet[i], 100)) {
			perror("listen");
			fatal("cannot listen to socket bound to port %d",
						netplan_port[i]);
		}
		if (serv)
			break;
	}
}


/*------------------------------------- clients -----------------------------*/
/*
 * terminate a client. Delete the client structure, close unclosed files,
 * and unlock locked rows.
 */

static void close_client(
	int		fd)
{
	struct client	*c = &client_list[fd];
	int		fid;

	for (fid=0; fid < max_files; fid++)
		close_file(fid, fd, c);
	release(c->descr);
	release(c->user);
	release(c->in.data);
	release(c->out.data);
	memset(c, 0, sizeof(*c));
}


/*
 * got message from port <fd>. Read it, and when it's complete do what was
 * requested. This may be called multiple times when the message got split
 * into chunks by the networking code. Return FALSE on disconnect. Every
 * message consists of a command character followed by data bytes. Every
 * message is terminated by a newline; newlines in the arguments must be
 * escaped with backslashes. \r on input are eliminated to allow testing
 * with telnet.
 * The problem here is that all bytes pending must be read, but it isn't
 * known how many are pending so multiple reads are needed. Unfortunately,
 * an extra select () must be used to make sure this loop doesn't block.
 * Whenever a non-escaped (not preceded by a backslash) newline is found,
 * the command up to that point is evaluated (multiple commands or command
 * fragments may arrive in the same network read).
 */

static BOOL message(
	int		fd)
{
	register struct client *c = &client_list[fd];
	register char	*p, *q;		/* for eliminating \r and \\n->\n */
	register int	n;		/* number of bytes read from socket */
	char		buf[1026];	/* temp incoming message buffer */
	char		*begin;		/* first un-evaluated byte in buf */
	fd_set		rd, wr, ex;	/* returned fd masks from select */
	struct timeval	timeout;	/* select must return immediately */

	for (;;) {
		if ((n = read(fd, buf, sizeof(buf)-2)) <= 0)
			return(FALSE);
		buf[n] = 0;
		for (p=q=begin=buf; *p; p++) {
			if (*p == '\\' && !c->esc) {
				c->esc = TRUE;
				continue;
			}
			if (*p == '\r')
				continue;
			*q++ = *p;
			if (*p == '\n' && !c->esc) {
				char save = *q;
				*q++ = 0;
				appendbuf(&c->in, begin, q-begin);
				eval_message(fd, c->in.data);
				*--q = save;
				begin = q;
				c->in.enq = 0;
			}
			c->esc = FALSE;
		}
		appendbuf(&c->in, begin, q-begin);
		if (MAXMSGSZ && c->in.enq > MAXMSGSZ) {
			error(fd, "dropped huge message (%d bytes)",c->in.enq);
			error(fd, "Communication error, ignoring\\\n excessive"
				  "ly large message, expect\\\nsynchronization"
				  " problems");
		}
		FD_ZERO(&rd);
		FD_ZERO(&wr);
		FD_ZERO(&ex);
		FD_SET(fd, &rd);
		timeout.tv_sec = timeout.tv_usec = 0;
		if (!select(nclients, &rd, &wr, &ex, &timeout))
			break;
	}
	return(TRUE);
}


#define SKIP(p)								   \
	while (p - c->in.data < c->in.enq && *p != ' ' && *p != '\n') p++; \
	if (p - c->in.data < c->in.enq && *p == ' ') p++;

static void eval_message(
	int		fd,		/* client that sent message */
	register char	*arg)		/* command and argument scan ptr */
{
	register struct client *c = &client_list[fd];
	register char	*p;		/* for eliminating \r and \\n->\n */
	register int	n, i;		/* number of bytes read from socket */
	int		fid;		/* file ID argument */
	BOOL            ronly;          /* read-only flag */
	unsigned int	rid;		/* row ID argument */
	char            *id = 0;
	struct passwd   *pw;
	char            buf[4096];
	DIR		*dir;
	struct dirent	*entry;

	static const char * const cmode[] = {
		"not connected",
		"readonly",
		"readwrite",
		"modified" };

	if (verbose)
		log("client %d sends %.400s", fd, arg);
	switch(*arg++) {
	  case '\n':
		break;

	  case '=':
		arg[strlen(arg)-1] = 0;
		if (do_auth) {
			if ((id = ident_id(fd)) && (pw = getpwnam(id))) {
				release(c->descr);
				release(c->user);
				c->descr     = make_descr(c, id, arg);
				c->user      = mystrdup(id);
				c->uid       = pw->pw_uid;
				c->auth_fail = FALSE;
				setup_gids(c, pw->pw_gid, id);
				endgrent();
				sprintf(buf, "client %d: identd authenticated "
						"user %.100s, uid %d, gids",
						fd, c->user, c->uid);
				p = buf + strlen(buf);
				for (i=0; i<NGROUPS_MAX && c->gids[i]>=0; i++){
					sprintf(p, " %d", c->gids[i]);
					p += strlen(p);
				}
				strcpy(p, "\n");
				log(buf);
			} else {
				c->auth_fail = TRUE;
				error(fd, "Unable to verify your identity "
						"using identd,\\\nonly public "
						"files will be accessible.");
				log("client %d: identd authentication failed\n"
						,fd);
			}
			release(id);
		} else {
			release(c->descr);
			c->descr = mystrdup(arg);
			c->auth_fail = FALSE;
			if (p = strstr(arg, "uid=")) {
				c->uid = atoi(p+4);
				if (pw = getpwuid(c->uid)) {
					release(c->user);
					c->user = mystrdup(pw->pw_name);
					setup_gids(c, pw->pw_gid, pw->pw_name);
				} else
					log("client %d: WARNING: unknown uid "
						"%d, owner and group ACLs will"
						" be ineffective\n",fd,c->uid);
			}
			sprintf(buf, "client %d is %.200s (uid %d, gids",
				 			fd, arg, c->uid);
			p = buf + strlen(buf);
			for (i=0; i < NGROUPS_MAX && c->gids[i] >= 0; i++) {
				sprintf(p, " %d", c->gids[i]);
				p += strlen(p);
			}
			strcpy(p, ")\n");
			log(buf);
		}
		endpwent();
		break;

	  case 't':
		c->type = atoi(arg);
		break;

	  case 'm':
		break;

	  case 'o':
		for (p=arg; *p && *p != '\n'; p++);
		*p = 0;
		if ((n = open_file(arg, fd, &ronly)) < 0)
			reply(fd, c, "oft0");
		else
			reply(fd, c, "ot%c%d", ronly ? 'r' : 'w', n);
		break;

	  case 'n':
		fid = atoi(arg);
		reply(fd, c, "n%d %d", fid, count_rows(fid, fd));
		break;

	  case 'c':
		close_file(atoi(arg), fd, c);
		break;

	  case 'r':
		fid = atoi(arg); SKIP(arg);
		rid = atoi(arg);
		send_row(fid, rid, fd, 'r');
		break;

	  case 'w':
		fid = atoi(arg); SKIP(arg);
		rid = atoi(arg); SKIP(arg);
		if (fid < 0 || fid >= max_files || file_list[fid].client[fd]<2)
			rid = 0; /* readonly or bad file */
		else
			rid = add_row(fid, rid, fd, arg,
				      c->in.enq - (arg-c->in.data));
		reply(fd, c, "w%c%d", "tf"[!rid], rid);
		break;

	  case 'd':
		fid = atoi(arg); SKIP(arg);
		rid = atoi(arg);
		delete_row(fid, rid, fd);
		break;

	  case 'l':
		fid = atoi(arg); SKIP(arg);
		rid = atoi(arg);
		reply(fd, c, "l%c", "ft"[(int)lock_row(fid, rid, fd, TRUE)]);
		break;

	  case 'u':
		fid = atoi(arg); SKIP(arg);
		rid = atoi(arg);
		lock_row(fid, rid, fd, FALSE);
		break;

	  case '.': {
		BOOL pending_nl = FALSE;
		struct client *cp;
		struct buffer *out = &client_list[fd].out;
		char msg[512];
		for (i=0; i < max_files; i++) {
			register struct pfile *f = &file_list[i];
			if (!f->name) continue;
			sprintf(msg, "\\\n.file %2d: ro=%d mod=%d "
				"err=%d type=%d nrows=%d "
				"name=\"%.20s\"",
					i, f->readonly, f->modified,
					f->error, f->type, f->nrows, f->name);
			n = pending_nl ? 0 : 2;
			appendbuf(out, msg+n, strlen(msg)-n);
			FD_SET(fd, &wr0);
			for (cp=client_list, n=0; n < nclients; n++, cp++) {
				if (!f->client[n]) continue;
				sprintf(msg, "\\\n.\tclient %2d: %.50s@"
					"%.20s, since %.15s %.20s",
					n, cp->descr, ip_addr(&cp->addr),
					ctime(&cp->time)+4,
					cmode[(int)f->client[n]]);
				appendbuf(out, msg, strlen(msg));
				FD_SET(fd, &wr0);
			}
			pending_nl = TRUE;
		}
		appendbuf(out, "\n", 1);
		FD_SET(fd, &wr0);
		break; }

	  case 'L':
		if (dir = opendir(HOMEDIR "/" NETDIR)) {
			while (entry = readdir(dir))
				if (entry->d_name[0] != '.')
					reply(fd, c, "Lt%s", entry->d_name);
			closedir(dir);
			reply(fd, c, "Lt");
		} else
			reply(fd, c, "Lf");
		break;

	  case 'q':
		close_client(fd);
		FD_CLR(fd, &rd0);
		close(fd);
		log("client %d disconnected\n", fd);
		break;

	  default:
		error(fd, "%.2048s", usage);
	}
}


/*
 * append a block of data to a buffer (incoming or outgoing message buffer).
 * After appending data, enable transmission with "FD_SET(fd, &wr0);".
 */

static void appendbuf(
	struct buffer	*buf,		/* buffer to append to */
	char		*data,		/* message to append */
	int		num)		/* number of bytes in data */
{
	if (buf->enq + num > buf->max) {
		buf->max += (num / MSGBUFSZ + 1) * MSGBUFSZ;
		buf->data = reallocate(buf->data, buf->max);
	}
	memcpy(buf->data + buf->enq, data, num);
	buf->enq += num;
}


/*
 * a row has changed. Tell all clients that have the row open, except the
 * one that caused the change (if any).
 */

static void notify_client(
	int		fid,		/* file to close */
	unsigned int	rid,		/* ID of new row */
	int		o_fd)		/* client that caused the change */
{
	struct pfile	*f = &file_list[fid];
	int		fd;

	for (fd=0; fd < nclients; fd++)
		if (fd != o_fd && f->client[fd])
			send_row(fid, rid, fd, 'R');
}


/*------------------------------------- files -------------------------------*/
/*
 * check whether file <fp> is stat-able, not a directory, and safe. Hard and
 * soft links and character or block devices are considered unsafe because
 * somebody could link a normally inaccessible file to the daemon directory
 * and use the daemon to inspect or modify it. Named pipes are considered
 * safe. This function is called after opening the file to prevent a mkdir-
 * like vulnerability. Return FALSE if a check failed.
 */

static BOOL check_file(
	FILE		*fp,		/* open file to check */
	char		*fname,		/* file name for error messages */
	int		fd,		/* client fd */
	struct client   *c)             /* client that requests check */
{
	struct stat	sbuf;		/* check for directories */
	char		*msg = 0;	/* file error message */

	if (strstr("..", fname))
		msg = "\"..\" in %.80s not allowed%s";
	else if (fstat(fileno(fp), &sbuf) < 0)
		msg = "cannot stat %.80s%s";
	else if (S_ISDIR(sbuf.st_mode))
		msg = "%.80s is a directory%s";
	else if ((sbuf.st_mode & S_IFMT) == S_IFLNK)
		msg = "%.80s is a symbolic link%s";
	else if (sbuf.st_mode & S_IFCHR)
		msg = "%.80s is a device node%s";
	else if (sbuf.st_nlink != 1)
		msg = "%.80s has more than one link%s";
	if (msg) {
		error(0,  msg, fname, " (this is a security risk)");
		error(fd, msg, fname, "\\\n(this is a security risk).");
		log("client %d fails to open %.80s\n", fd, fname);
		return(FALSE);
	}
	return(TRUE);
}


/*
 * client <fd> wants file <fname> opened. See if it is already open; if not,
 * open it now and read the contents. If the file does not exist, assume it's
 * empty. The entries are read byte-by-byte. Return the new file ID (fid) or
 * -1 on error. Refuse to open files beginning with . to prevent access to
 * .netplan-acl and others.
 */

#define FILEINC	32			/* file list chunk size */
#define BUFINC	4094			/* buffer chunk size */

static int open_file(
	char		*fname,		/* name of file to open */
	int		fd,		/* client that requests open */
	BOOL            *ronly)         /* readonly flag */
{
	struct client	*c = &client_list[fd];
	char		*path;		/* full path with directory part */
	struct pfile	*f;		/* file entry */
	int		fid, ffree;	/* file ID (file_list index), free */
	FILE		*fp;		/* file to write */
	BOOL		ro = FALSE;	/* client's read-only flag */
	BOOL            readonly = FALSE;/* file's read-only flag */
	char		*buf = 0;	/* row buffer */
	int		size = 0;	/* row buffer size */
	int		i = 0;		/* row buffer index */
	BOOL		skip = TRUE;	/* header skip mode */
	BOOL		newline = TRUE;	/* first char of line? */
	int		type;		/* file type requested by client */
	int		r, w, d;	/* permissions from access list */

	if (!*fname) {
		error(0, "client %d opens null file name", fd);
		error(fd, "cannot open null file name");
		return(-1);
	}
	path = strrchr(fname, '/');			/* no dot files */
	if (path)
		fname = path + 1;
	if (allow_all)
		r = w = d = TRUE;
	else
		acl_verify(&r, &w, &d, fname, c);
	ro = !w;
	if (!r || *fname == '.') {
		error(0, "access to %.20s refused to client %d", fname, fd);
		error(fd, "Access to file %.20s denied by server.", fname);
		return(-1);
	}
							/* already open? */
	type = c->type;
	for (f=file_list, fid=0, ffree=-1; fid < max_files; fid++, f++) {
		if (!f->name)
			ffree = fid;
		else if (!strcmp(f->name, fname) && f->type == type) {
			if (f->readonly)
				ro = TRUE;
			if (!f->client[fd])
				f->nclients++;
			f->client[fd] = ro ? 1 : 2;
			log("client %d opens cached file %.200s %.20s\n", fd,
					fname, ro ? "readonly" : "readwrite");
			*ronly = ro;
			return(fid);
		}
	}
	path = allocate(strlen(HOMEDIR "/" NETDIR "/") + strlen(fname) + 1);
	sprintf(path, HOMEDIR "/" NETDIR "/%s", fname);
	if (fp = fopen(path, "r")) {			/* open and check */
		if (!check_file(fp, path, fd, c)) {
			release(path);
			fclose(fp);
			return(-1);
		}
		if (access(path, W_OK))
			ro = readonly = TRUE;
	}
	release(path);
	if (ro && ! fp) {
		error(fd, "File %.20s does not exist on server.", fname);
		return -1;
	}
	if (ffree != -1)				/* new file slot */
		fid = ffree;
	else {
		int max = max_files + FILEINC;
		file_list = reallocate(file_list, max * sizeof(struct pfile));
		memset(file_list+max_files, 0, FILEINC * sizeof(struct pfile));
		fid = max_files;
		max_files = max;
	}
	f = &file_list[fid];
	memset(f, 0, sizeof(struct pfile));
	f->readonly   = readonly;
	f->type       = type;
	f->name       = allocate(strlen(fname)+1);
	f->client     = allocate(nclients);
	strcpy(f->name, fname);
	memset(f->client, 0, nclients);

	if (fp) {
		for (;;) {				/* read entries */
			char c = fgetc(fp);
			if (feof(fp)) {
				if (i)
					add_row(fid, 0, -1, buf, i);
				break;
			}
			switch(type) {
			  case 0:			/*... plan */
				if (newline && c >= '0' && c <= '9') {
					skip = FALSE;
					if (i) {
						add_row(fid, 0, -1, buf, i);
						i = 0;
					}
				}
				break;
			  default:
			  case 1:			/* ...grok */
				skip = FALSE;
				if (newline && i) {
					add_row(fid, 0, -1, buf, i);
					i = 0;
				}
				break;
			}
			newline = c == '\n';
			if (skip)
				continue;
			if (i >= size)
				buf = reallocate(buf, size += BUFINC);
			buf[i++] = c;
		}
		log("client %d opens new file %.20s (type %d), read %d entrie"
					"s\n", fd, fname, f->type, f->nrows);
		fclose(fp);
	} else
		log("client %d opens non-existing file %.20s\n", fd, fname);
	release(buf);
	f->client[fd] = ro ? 1 : 2;	/* do this here to avoid notify msgs */
	f->nclients   = 1;
	for (i=0; i < f->nrows; i++)
		f->row[i].modified = FALSE;
	*ronly = ro;
	return(fid);
}


/*
 * client <fd> wants to close file <f>. If the last client closes a file it
 * is written to disk and removed from the file array.
 */

static void close_file(
	int		fid,		/* file to close */
	int		fd,		/* client's fd */
	struct client  *c)              /* client that closes the file */
{
	int		r;		/* row counter for releasing */
	struct pfile	*f = &file_list[fid];

	if (fid < 0 || fid >= max_files || !f->name || !f->client[fd])
		return;
	log("client %d closes file %.20s\n", fd, f->name);
	write_file(fid);			/* write changes */
	f->client[fd] = 0;			/* detach client from file */
	f->nclients--;
	if (!f->nclients && !f->error) {	/* last client gone: uncache */
		log("uncaching file %.20s\n", f->name);
		for (r=0; r < f->nrows; r++)
			release(f->row[r].data);
		release(f->row);
		release(f->name);
		release(f->client);
		memset(f, 0, sizeof(struct pfile));
	} else					/* else release stale locks */
		for (r=0; r < f->nrows; r++)
			if (f->row[r].lock_fd == fd)
				f->row[r].lock_fd = 0;
}


/*
 * write file <fid> to disk if it has been modified. This happens when the
 * last client closes the file, when the daemon exits, and periodically
 * from a timer interrupt. This is split into two functions to avoid goto.
 * WARNING - still two security race conditions here!! Use creat/rename. <<<
 */

static void do_write(struct pfile *, char *, char *);

static void write_file(
	int		fid)		/* file to close */
{
	struct pfile	*f = &file_list[fid];
	char		*npath, *path;	/* full path with directory part */
	int		n;

	if (fid < 0 || fid >= max_files || !f->name || !f->modified)
		return;
	n = strlen(HOMEDIR "/" NETDIR "/") + strlen(f->name) + 1;
	path = allocate(n);
	sprintf(path, HOMEDIR "/" NETDIR "/%s", f->name);
	npath = allocate(n + 4);
	sprintf(npath, "%s.new", path);

	do_write(f, path, npath);

	release(path);
	release(npath);
}

static void do_write(			/* called by previous function only */
	struct pfile	*f,		/* file to write */
	char		*path,		/* full path with directory part */
	char		*npath)		/* temporary file */
{
	FILE		*fp;		/* file to write */
	int		fd;		/* for error reporting */
	int		r;		/* current row index */
	struct row	*row;		/* current row */
	BOOL		err = FALSE;	/* detect errors during writing */
	char            buf[256];

	unlink(npath);
	if (!(fp = fopen(npath, "w+"))) {
		sprintf(buf, "cannot create temporary data file\\\n%.20s",
									npath);
		return;
	}
	if (chmod(npath, 0666))
		error(0, "WARNING - cannot chmod 666 %.200s", npath);
	for (r=0, row=f->row; r < f->nrows; r++, row++)
		if (row->data)
			if (fputs(row->data, fp) == EOF) {
				err = TRUE;
				break;
			}
	if (fclose(fp))
		err = TRUE;
	if (err) {
		sprintf(buf, "cannot write temporary data file\\\n%.20s",
									npath);
		return;
	}
	unlink(path);
	if (link(npath, path))
		err = TRUE;
	unlink(npath);
	if (err) {
		sprintf(buf, "cannot rename data file %.20s", path);
		return;
	}
	for (r=0, row=f->row; r < f->nrows; r++, row++)
		row->modified = FALSE;
	for (fd=0; fd < nclients; fd++)
		if (f->client[fd] == 3)
			f->client[fd] = 2;
	f->modified = FALSE;
	f->error = FALSE;
	log("written data file %.400s\n", path);
}


/*------------------------------------- rows --------------------------------*/
/*
 * find a row ID in the row list. Return the row number where the entry is
 * to be stored. *found is set if the entry has the same ID and should be
 * replaced instead of inserting a new one.
 */

#define ROWINC	256			/* increase row table size by chunks */

static int find_row(
	int			fid,		/* file to close */
	unsigned int		rid,		/* ID of new row; 0=unknown */
	BOOL			*found)		/* set if found, clear if ins*/
{
	int			lo, mid, hi;	/* for binary search for ID */
	register struct row	*row;		/* current row */
	struct pfile		*f = &file_list[fid];

	*found = FALSE;
	if (!f->nrows)					/* empty list */
		return(0);
	if (rid > f->row[f->nrows-1].id)		/* after last */
		return(f->nrows);
	for (lo=mid=0, hi=f->nrows-1; lo < hi; ) {	/* insert */
		mid = lo + (hi - lo) / 2;
		row = &f->row[mid];
		if (rid == row->id)
			break;
		if (rid < row->id)
			hi = mid;
		else
			lo = ++mid;
	}
	*found = rid == f->row[mid].id;
	return(mid);
}


/*
 * add a row to a file, either one from reading a file or a new one (one
 * with a row ID of 0) sent by a client. Insert the row such that the table
 * is sorted by row ID, for easier lookup. If the row already exists, just
 * replace the data. Return the new ID code (a new one is made if rid==0).
 */

#define ROWINC	256			/* increase row table size by chunks */

static unsigned int add_row(
	int			fid,		/* file to close */
	unsigned int		rid,		/* ID of new row; 0=unknown */
	int			fd,		/* client that caused add */
	char			*data,		/* data to store in row */
	int			len)		/* number of data bytes */
{
	struct pfile		*f = &file_list[fid];
	int			index, i;	/* index of new row in table */
	BOOL			found = FALSE;	/* replace existing row? */
	register struct row	*row;		/* current row */

	if (fid < 0 || fid >= max_files || !f->name)
		return(0);
	
	if (!rid)					/* new: make ID */
		do {
			rid = row_id++;
			row_id += !row_id;
			index = find_row(fid, rid, &found); /* insert-sort */
		} while (found);
	else
		index = find_row(fid, rid, &found);	/* insert-sort */
	
	if (!found) {					/* make new slot */
		if (++f->nrows > f->maxrows) {		/* ...extend list? */
			f->maxrows += ROWINC;
			f->row = reallocate(f->row,
					    f->maxrows * sizeof(struct row));
		}
		for (i=f->nrows-1; i > index; i--)
			f->row[i] = f->row[i-1];
		memset(&f->row[index], 0, sizeof(struct row));
	}
	row = &f->row[index];				/* got it, fill in */
	row->data = reallocate(row->data, len+1);
	memcpy(row->data, data, len);
	row->data[len] = 0;
	row->id = rid;
	if (fd >= 0) {			/* real insert (not at open time) */
		row->modified = f->modified = TRUE;
		f->client[fd] = 3;
		notify_client(fid, rid, fd);
	}
	if (!update_time)
		update_time = time(0);
	return(rid);
}


/*
 * delete a row in a file, and shift following rows to fill the hole
 */

static void delete_row(
	int			fid,		/* file to delete row from */
	unsigned int		rid,		/* ID of new row; 0=unknown */
	int			fd)		/* client that caused delete */
{
	int			index;		/* index of new row in table */
	BOOL			found;		/* replace existing row? */
	struct pfile		*f = &file_list[fid];

	if (!rid || fid < 0 || fid >= max_files || !f->name ||
	    f->client[fd] < 2) {
		return;
	}
	index = find_row(fid, rid, &found);		/* find rid */
	if (!found)
		return;
	f->nrows--;
	while (index++ < f->nrows)			/* delete */
		f->row[index-1] = f->row[index];
	f->modified = TRUE;
	f->client[fd] = 3;
	notify_client(fid, rid, fd);
	if (!update_time)
		update_time = time(0);
}


/*
 * lock or unlock a row in a file. Return FALSE if the row is already locked.
 * Locking a nonexistent row has no effect and returns TRUE.
 */

static BOOL lock_row(
	int			fid,		/* file to close */
	unsigned int		rid,		/* ID of new row */
	int			fd,		/* client identifier */
	BOOL			lock)		/* lock or unlock? */
{
	int			index;		/* index of new row in table */
	BOOL			found;		/* replace existing row? */
	struct pfile		*f = &file_list[fid];

	if (fid < 0 || fid >= max_files || !f->name || f->client[fd] < 2)
		return(FALSE);
	if (!rid)
		return(TRUE);
	index = find_row(fid, rid, &found);		/* find rid */
	if (!found)
		return(TRUE);
	if (lock && f->row[index].lock_fd && f->row[index].lock_fd != fd)
		return(FALSE);
	f->row[index].lock_fd = lock ? fd : 0;
	return(TRUE);
}


/*
 * send row <rid> in file <fid> to client <fd>. If <rid> is 0, send all rows.
 * If the row contains internal newlines, escape them with backslashes. Skip
 * a trailing newline if any, but add a newline for terminating the message.
 * This is done when a client explicitly requests one or all rows, and when
 * another client changes the row and all others need to be informed. If the
 * arguments are wrong, send an "rf" error code.
 * If this row is sent as an update, without an explicit request, use opcode
 * 'R' instead of 'r'.
 */

static void send_row(
	int		fid,		/* file to close */
	unsigned int	rid,		/* ID of new row; 0=all */
	int		fd,		/* client identifier */
	char		opcode)		/* 'r'=reply, 'R'=async */
{
	struct client	*c = &client_list[fd];
	struct pfile	*f = &file_list[fid];
	struct row	*row;		/* current row being sent */
	BOOL		found;		/* does row exist? err check */
	int		n, beg, end;	/* index into row table */
	char		buf[4097];	/* message header and body */
	register char	*p, *q;		/* for escaping \n with \ */

	if (fid < 0 || fid >= max_files || !f->name || !f->client[fd]) {
		reply(fd, c, "%cf%d 0 ", opcode, fid);
		return;
	}
	if (rid) {					/* single row */
		beg = end = find_row(fid, rid, &found);
		if (!found) {
			reply(fd, c, "%cf%d %d ", opcode, fid, rid);
			return;
		}
	} else {					/* all rows */
		beg = 0;
		end = f->nrows-1;
	}
	for (n=beg; n <= end; n++) {
		row = &f->row[n];
		sprintf(buf, "%ct%d %d ", opcode, fid, row->id);
		appendbuf(&c->out, buf, strlen(buf));
		if (verbose)
			log("client %d reply %.50s<data>\n", fd, buf);
		for (p=row->data; *p; ) {
			for (q=buf; *p && q < buf+sizeof(buf)-1; ) {
				if (*p == '\n') {
					if (!p[1]) {p++; break;}
					*q++ = '\\';
				}
				*q++ = *p++;
			}
			if (q != buf)
				appendbuf(&c->out, buf, q-buf);
		}
		appendbuf(&c->out, "\n", 1);
	}
	FD_SET(fd, &wr0);
}


/*
 * return the number of rows in file <fid>. Return 0 if <fid> is invalid.
 */

static int count_rows(
	int		fid,		/* file to count */
	int		fd)		/* client identifier */
{
	struct pfile	*f = &file_list[fid];

	if (fid < 0 || fid >= max_files || !f->name || !f->client[fd])
		return(0);
	return(f->nrows);
}


/*------------------------------------- errors ------------------------------*/
/*
 * print fatal error and exit.
 */

static void fatal(char *fmt, ...)
{
	va_list		parm;

	va_start(parm, fmt);
	fprintf(stderr, "%s: ", progname);
	vfprintf(stderr, fmt, parm);
	va_end(parm);
	fprintf(stderr, ", exiting.\n");
	exit(1);
}


/*
 * send a message to client <fd>. The printf-like arguments make it more
 * convenient than write(). Also enables the write-select bit. Strictly for
 * short reply messages -- may not be used for row data because the buffer
 * is too small!
 */

static void reply(int fd, struct client *c, char *fmt, ...)
{
	va_list		parm;
	char		msg[4096];

	va_start(parm, fmt);
	vsprintf(msg, fmt, parm);
	strcat(msg, "\n");
	appendbuf(&client_list[fd].out, msg, strlen(msg));
	if (verbose)
		log("client %d reply %.400s", fd, msg);
	FD_SET(fd, &wr0);
}


/*
 * print nonfatal error. If <fd> is nonzero, send the error message to client
 * <fd> so it can put up an error popup. If <fd> is 0, print the error message
 * to stderr. \r is used for newlines because \n is a message terminator.
 */

void error(int fd, char *fmt, ...)
{
	va_list		parm;
	char		msg[4096];

	va_start(parm, fmt);
	if (fd) {
		strcpy(msg, "?Error on netplan server ");
		gethostname(msg+strlen(msg), 256);
		strcat(msg+strlen(msg), ":\\\n");
		vsprintf(msg+strlen(msg), fmt, parm);
		strcat(msg, "\n");
		appendbuf(&client_list[fd].out, msg, strlen(msg));
		if (fd)
			FD_SET(fd, &wr0);
	} else {
		fprintf(stderr, "%s: error: ", progname);
		vfprintf(stderr, fmt, parm);
		putc('\n', stderr);
	}
	va_end(parm);
}


/*
 * print verbose logging message
 */

void log(char *fmt, ...)
{
	va_list		parm;

	if (debug) {
		va_start(parm, fmt);
		fprintf(stderr, "%s: ", progname);
		vfprintf(stderr, fmt, parm);
		va_end(parm);
	}
}


/*
 * return static string describing an IP address with port number
 */

char *ip_addr(register struct sockaddr_in *addr)
{
	static char buf[64];
	unsigned long a = ntohl(addr->sin_addr.s_addr);
	sprintf(buf, "%d.%d.%d.%d:%d",
		(int)((a >> 24) & 255),
		(int)((a >> 16) & 255),
		(int)((a >>  8) & 255),
		(int)((a      ) & 255),
		ntohs(addr->sin_port));
	return(buf);
}
