/*
 * rnews - setuid-news fake rnews for nntp: appends article to a batch with
 *	a fixed name under in.coming.  if batch is too big or old, rename
 *	batch to be an input candidate and kick off a newsrun to process
 *	the batch.  the batch file is locked during appending.
 * Cooperates with C news input subsystem.
 *	newsboot must be told to run partial batches left at a crash.
 */
#include "../common/conf.h"
#include "common.h"
#include <signal.h>

#define TOOBIG 300000L		/* batch > TOOBIG bytes, kick rnews */
#define TOOOLD (5*60)		/* batch > TOOOLD seconds old, kick rnews */
#define COPYSIZE 8192		/* bytes to copy at one time */
#define MAXDIGITS 25		/* lg(maxlongint) + epsilon */
#define MAXSTR 1024

#define INDIR		artfile("in.coming")
#define BATCHFILE	artfile("in.coming/nntp.XXXXXX")
#define NEWSRUN		binfile("input/newsrun")

#define YES 1
#define NO 0

/* imports */
extern time_t time();
extern char *malloc(), *mktemp(), *index(), *rindex();

/* forwards */
static char *artfile(), *binfile(), *strsave();
static void error(), warning();
static int xfer_timeout();

/* private data */
static char tempfile[256];
static int xfer_lines, old_xfer_lines;

static char art[COPYSIZE];		/* entire article, if it fits */
static char *endart = art;		/* points just past end of article */
static int incore = YES;

static struct batch_file {
	char *name;
	FILE *file;
	char isopen;
	time_t start;			/* time of creation */
	off_t size;			/* current size */
} btch = { NULL, NULL, NO, 0, 0 };

char *progname = "nntpd";

#ifndef lint
static	char	*sccsid = "@(#)batch.c	1.5	(Toronto) 31/4/89";
#endif

/*
 * stash stdin (up to ".") on the end of the batch input file.
 * kick newsrun if the batch is non-empty and too big or too old.
 *
 * Parameters:
 *	"cont_code" is the response code to transmit on successful startup.
 *	"err_code" is the response code to transmit when something goes wrong.
 *
 * Returns: -1 on non-zero return from child, 0 on error before fork/exec, 1 el
e.
 * Side effects: Creates and removes temporary file; accepts input from client.
 *		Can time out if XFER_TIMEOUT is defined.
 */
int
batch_input_article(cont_code, err_code, errbuf)
int cont_code, err_code;
char *errbuf;
{
	int status = 1;			/* okay status */

	/* protect locking */
	signal(SIGINT, SIG_IGN);
	signal(SIGQUIT, SIG_IGN);
	signal(SIGHUP, SIG_IGN);

	if (btch.name == NULL) {
		/* BATCHFILE may trigger unprivileged() */
		btch.name = mktemp(strsave(BATCHFILE));
	}
	if (btch.name == NULL)
		return 0;
#ifdef notdef
	(void) setgid(getegid());
	(void) setuid(geteuid());
#endif
	tempfile[0] = '\0';
	if (!cpstdin(cont_code, err_code, errbuf))	/* may create tempfile */
		return 0;
#ifdef POSTER
	(void) chown(tempfile, uid_poster, gid_poster);
#endif
	status = appbatch();
	if (tempfile[0] != '\0')
		(void) unlink(tempfile);
	if (status == 1 && oktorunbatch())
		status = enqueue(cont_code, err_code, errbuf);
	return status;
}

int						/* boolean */
oktorunbatch()
{
	struct stat stbuf;

	if (!btch.isopen || fstat(fileno(btch.file), &stbuf) < 0)
		return NO;
	btch.size = stbuf.st_size;
	return btch.size > TOOBIG ||
		btch.size > 0 && time((time_t *)NULL) - btch.start > TOOOLD;
}

/*
 * Copy standard input (up to a "." line) to art, if it fits,
 * else to a temporary file.
 */
/* ARGSUSED errbuf */
static int					/* boolean: got article ok? */
cpstdin(cont_code, err_code, errbuf)
int cont_code, err_code;
char *errbuf;
{
	register FILE *tfp = NULL;
	register char *cp, *realline;
	char line[NNTP_STRLEN];
	int toobig = NO;
	int (*otimeout)();

	/* TODO: is this right?  used to open here, with errors here */
	printf("%d Ok\r\n", cont_code);
	(void) fflush(stdout);

	xfer_lines = old_xfer_lines = 0;
	incore = YES;
	art[0] = '\0';
	endart = art;
#ifdef XFER_TIMEOUT
	otimeout = signal(SIGALRM, xfer_timeout);
	(void) alarm(XFER_TIMEOUT);
#endif
	while (fgets(line, sizeof line, stdin) != NULL) {
		xfer_lines++;
		if ((cp = rindex(line, '\r')) != NULL ||
		    (cp = rindex(line, '\n')) != NULL)
			*cp = '\0';			/* nuke CRLF */
		if (line[0] == '.' && line[1] == '\0')
			break;				/* article end: exit */
		/* remove hidden dot if present */
		realline = (line[0] == '.'? line+1: line);
		if (toobig) {				/* straight to disk */
			(void) fputs(realline, tfp);
			(void) putc('\n', tfp);
		} else {
			int len = strlen(realline);

			/*
			 * Does art have room to append realline + \n\0?
			 * If not, open temp file and dump art & realline there.
			 */
			if (sizeof art - (endart - art) < len + 1 + 1) {
				(void) strcpy(tempfile, "/tmp/rpostXXXXXX");
				(void) mktemp(tempfile);
				tfp = fopen(tempfile, "w");
				if (tfp == NULL) {
					printf("%d Cannot create temporary file.\r\n",
						err_code);
					(void) fflush(stdout);
					return 0;
				}
#ifdef OK_IN_MIDDLE_OKAY
				else {
					printf("%d Ok\r\n", cont_code);
					(void) fflush(stdout);
				}
#endif
				(void) fwrite(art, 1, endart - art, tfp);
				toobig = YES;
				incore = NO;
				art[0] = '\0';
				endart = art;
				(void) fputs(realline, tfp);
				(void) putc('\n', tfp);
			} else {
				/* fits: append realline\n to art at endart */
				(void) strcpy(endart, realline);
				endart += len;
				*endart++ = '\n';
				*endart = '\0';
			}
		}
	}
	if (tfp != NULL)
		(void) fclose(tfp);
#ifdef XFER_TIMEOUT
	(void) alarm(0);
	(void) signal(SIGALRM, otimeout);
#endif

	/* See if the connection got closed somehow... */
	if (line[0] != '.' && line[1] != '\0') {
		if (tempfile[0] != '\0')
			(void) unlink(tempfile);
#ifdef LOG
		syslog(LOG_ERR, "%s spawn: EOF before period on line by itself",
			hostname);
#else
		syslog(LOG_ERR, "spawn: EOF before period on line by itself");
#endif
		return 0;
	}
	return 1;
}

static int
xfer_timeout()
{
#ifdef XFER_TIMEOUT
	if (old_xfer_lines < xfer_lines) {
		old_xfer_lines = xfer_lines;
		(void) alarm(XFER_TIMEOUT);
		return;
	}
	/* Timed out. */
	printf("%d timeout after %d seconds, closing connection.\r\n",
		ERR_FAULT, XFER_TIMEOUT);
	fflush(stdout);
#ifdef LOG
	syslog(LOG_ERR, "%s transfer_timeout", hostname);
#endif LOG
	(void) unlink(tempfile);
	exit(1);
#endif XFER_TIMEOUT
}

/*
 * Append "#! rnews count" and art (or tempfile) to batch file, locking assumed

 * If batch file is too big or too old (but not empty), feed it to newsrun.
 */
static int					/* same as batch_input_article */
appbatch()
{
	register FILE *tfp = NULL;
	register int bytes = 0;
	int status = 1;				/* okay status */
	long size = 0;
	char artbuf[COPYSIZE];
	struct stat stbuf;

	if (btch.file == NULL) {
		btch.file = fopen(btch.name, "a");
		if (btch.file == NULL)
			return 0;
		btch.isopen = YES;
		btch.size = 0;
		btch.start = time(&btch.start);
	}

	/* find article size and write the article */
	if (incore)
		size = endart - art;
	else {
		tfp = fopen(tempfile, "r");
		if (tfp == NULL)
			return 0;
		if (fstat(fileno(tfp), &stbuf) >= 0)
			size = stbuf.st_size;
	}
	(void) fprintf(btch.file, "#! rnews %ld\n", size);

	/* copy the article to the batch file */
	if (incore)
		(void) fwrite(art, 1, endart - art, btch.file);
	else {
		while ((bytes = fread(artbuf, 1, sizeof artbuf, tfp)) > 0)
			if (fwrite(artbuf, 1, bytes, btch.file) != bytes) {
				warning("can't write %s", btch.name);
				status = 0;	/* hmm, #! count is off */
				break;
			}
		(void) fclose(tfp);
	}
	if (fflush(btch.file) == EOF) {
		warning("can't write %s", btch.name);
		status = 0;
	}
	return status;
}

/*
 * Enqueue any partial batch.  Called before exit.
 */
enqpartbatch(cont_code, err_code, errbuf)
int cont_code, err_code;
char *errbuf;
{
	struct stat stbuf;

	if (btch.isopen && fstat(fileno(btch.file), &stbuf) >= 0) {
		if (btch.size > 0)
			enqueue(cont_code, err_code, errbuf);
		else {
			(void) fclose(btch.file);
			btch.file = NULL;
			btch.isopen = NO;
			(void) unlink(btch.name);	/* remove empty batch */
		}
	}
}

/* 
 * insert the batch file into the input subsystem queue by renaming
 * it to an all-numeric name, then kick newsrun to process it.
 * locks btch.name as appropriate.
 */
static int					/* same as batch_input_article */
enqueue(cont_code, err_code, errbuf)
int cont_code, err_code;
char *errbuf;
{
	time_t now;
	int pid, wpid, status, fd, exitstat;
	char permname[MAXDIGITS], *number = permname, *newsrun;
	struct stat stbuf;

	(void) fclose(btch.file);
	btch.file = NULL;
	btch.isopen = NO;
	btch.start = 0;
	btch.size = 0;

	(void) fflush(stdout);
	(void) fflush(stderr);
	pid = fork();
	if (pid == -1) {
		warning("can't fork", "");
		return 0;
	} else if (pid != 0) {			/* parent */
		while ((wpid = wait(&status)) != -1 && wpid != pid)
			;
		exitstat = (status>>8)&0377;
		if (exitstat != 0) {
			syslog(LOG_ERR, "%s: enqueue returned exit status 0%o",
				progname, exitstat);
			strcpy(errbuf, "enqueue failed to run newsrun\n");
		}
		return exitstat != 0? -1 :1;
	}

	/* child: must exit */
	for (fd = 3; fd < 20; fd++)
		(void) close(fd);
	if (chdir(INDIR) < 0) {
		syslog(LOG_ERR, "%s: chdir(%s) failed", progname, INDIR);
		nerror("can't change directory to %s", INDIR);
	}

	/* rename btch.name to a number so newsrun will see it */
	sprintf(number, "%ld", (long)time(&now));
	while (link(btch.name, permname) < 0) {
		if (stat(btch.name, &stbuf) < 0)
			break;
		sleep(2);
		sprintf(number, "%ld", (long)time(&now));
	}
	if (unlink(btch.name) < 0)
		vanished(btch.name);

	signal(SIGINT, SIG_IGN);
	signal(SIGQUIT, SIG_IGN);
	signal(SIGHUP, SIG_IGN);
	(void) fflush(stdout);
	(void) fflush(stderr);
	newsrun = strsave(NEWSRUN);
	if (newsrun == NULL)
		newsrun = "/usr/lib/newsbin/input/newsrun";
	execl(newsrun, newsrun, (char *)NULL);
	syslog(LOG_ERR, "%s: can't run %s", progname, newsrun);
	error("attempt to run %s failed!", newsrun);
	exit(1);
	/* NOTREACHED */
}

vanished(s)					/* grieve for s, nerror [exit] */
char *s;
{
	syslog(LOG_ERR, "%s: %s vanished underfoot!", progname, s);
	nerror("%s vanished underfoot!", s);	/* unlocks, exits */
}

static
nerror(fmt, s)				/* error, unused to be with unlock */
char *fmt, *s;
{
	error(fmt, s);
}

/* C news library starts here */

/*
 * error - print best error message possible and exit
 */
static void warning();

static void
error(s1, s2)
char *s1;
char *s2;
{
	warning(s1, s2);
	exit(1);
}

/*
 * warning - print best error message possible and clear errno
 */
extern int errno, sys_nerr;
extern char *sys_errlist[];
extern char *progname;
extern char *getenv();

static void
warning(s1, s2)
char *s1;
char *s2;
{
	char *cmdname;

	(void) fflush(stdout);				/* hack */
	cmdname = getenv("CMDNAME");
	if (cmdname != NULL && *cmdname != '\0')
		fprintf(stderr, "%s:", cmdname);	/* No space after :. */
	if (progname != NULL)
		fprintf(stderr, "%s: ", progname);
	fprintf(stderr, s1, s2);
	if (errno > 0 && errno < sys_nerr)
		fprintf(stderr, " (%s)", sys_errlist[errno]);
	fprintf(stderr, "\n");
	errno = 0;
}

void
unprivileged()
{
	(void) setgid(getgid());
	(void) setuid(getuid());
}

static char *
artfile(s)
char *s;
{
	static char name[MAXSTR];

	strcpy(name, "/usr/spool/news/");
	strcat(name, s);
	return name;
}

static char *
binfile(s)
char *s;
{
	static char name[MAXSTR];

	strcpy(name, "/usr/lib/newsbin/");
	strcat(name, s);
	return name;
}

#ifdef notdef
static char *
ctlfile(s)
char *s;
{
	static char name[MAXSTR];

	strcpy(name, "/usr/lib/news/");
	strcat(name, s);
	return name;
}
#endif

static char *
strsave(s)
register char *s;
{
	register char *news = malloc((unsigned)(strlen(s) + 1));

	if (news != NULL)
		strcpy(news, s);
	return news;
}

#ifndef SYSLOG
/* VARARGS 2 */
static
syslog(level, fmt)
int level;
char *fmt;
{
}
#endif

