/*
 * relaynews - relay Usenet news (version C)
 * See the file COPYRIGHT for the copyright notice.
 *
 * relaynews should be setuid-news, setgid-news.  You'll need to install
 * setnewsids setuid-root if setuid(geteuid()) doesn't work on your
 * machine (e.g. on V7 and possibly SystemIII).
 *
 * Written by Geoff Collyer, 15-20 November 1985 and revised periodically
 * since.
 *
 * relaynews parses article headers, rejects articles by newsgroup &
 * message-id, files articles, updates the active & history files,
 * transmits articles, and honours (infrequent) control messages, which do
 * all sorts of varied and rococo things.  Control messages are implemented
 * by separate programs.  relaynews reads a "sys" file to control the
 * transmission of articles but can function as a promiscuous leaf node
 * without one.  See ARPA Internet RFC 1036 nee 850 for the whole story.
 *
 * A truly radical notion: people may over-ride via environment variables
 * the compiled-in default directories so IHCC kludges are not needed and
 * testing is possible (and encouraged) in alternate directories.  This
 * does cause a loss of privilege, to avoid spoofing.
 *
 * The disused old ihave/sendme protocol is dead; it's been broken in
 * B news for ages but no one has noticed because it's essentially
 * useless on the uucp network, especially when batching news articles.
 * It is also only semi-documented, very wasteful and kludgey in the extreme.
 * It is unreasonable to expect an ill-documented protocol to be implemented
 * in new software, particularly one as kludgey as this one.
 * The new ihave sys flag ("I") for NNTP is implemented (trivially).
 *
 * Portability vs SystemV.  relaynews uses dbm(3) and makes no apologies
 * for so doing.  Imitation UNIX (registered trademark of AT&T in the
 * United States) brand operating systems that lack dbm are going to
 * have to use my incredibly slow dbm simulation, or another.
 */

#ifdef DEBUG
#  define STAT(x)	{	fprintf(stderr,"%s: %d, ",__FUNC__,__LINE__);\
						fprintf x;\
						fflush(stderr); }
#else
#  define STAT(x)
#endif

#include <stdio.h>
#include <ctype.h>
#include <signal.h>		/* to make locking safe */
#ifndef AMIGA
#  include <sys/types.h>
#endif /* AMIGA */

#include "libc.h"
#include "news.h"
#include "config.h"
#include "fgetmfs.h"
#include "active.h"
#include "caches.h"
#include "cpu.h"
#include "fileart.h"
#include "headers.h"
#include "history.h"
#include "transmit.h"

/*
 * setuid-root program to set ids to news/news & rexec rnews with
 * NEWSPERMS in the environment to break loops.
 */
#ifndef SETNEWSIDS
#define SETNEWSIDS "setnewsids"
#endif

/* exports */
char *progname;
boolean okrefusal = YES;			/* okay to refuse articles? */
char *exclude = NULL;				/* site to exclude, for erik */
boolean histreject = NO;			/* keep history of rejects? */

/* internal */
static boolean userealids = NO;

/* imports */
extern int optind;			/* set by getopt */
extern char *optarg;
extern statust cpinsart();		/* from procart.c */

/* forwards */
extern void prelude(), setids(), procopts(), redirectlogs(), logfile();
extern void getwdandcd();
extern statust procargs(), relnmprocess(), process(), unbatch();
extern boolean batchln();
FORWARD boolean debugon();

/*
 * main - take setuid precautions, switch to "news" ids, ignore signals,
 * handle options, lock news system, process files & unlock news system.
 */
int
main(argc, argv)
int argc;
char *argv[];
{
	statust status = ST_OKAY;
	int redirlogs = 0;		/* redirect n std output streams to logs */
	char *origdir = NULL;		/* current directory at start */

	progname = argv[0];
#ifdef CSRIMALLOC
	mal_debug(0);		/* was 2; 3 is too slow */
	mal_leaktrace(0);	/* was 1 */
#endif
	prelude(argv);		/* various precautions; switch to "news" */

	/*
	 * ignore signals (for locking). relaynews runs quickly,
	 * so don't worry.
	 */
#ifdef SIGINT
	(void) signal(SIGINT, (sigarg_t)SIG_IGN);
#endif
#ifdef SIGQUIT
	(void) signal(SIGQUIT, (sigarg_t)SIG_IGN);
#endif
#ifdef SIGHUP
	(void) signal(SIGHUP, (sigarg_t)SIG_IGN);
#endif
#ifdef SIGTERM
	(void) signal(SIGTERM, (sigarg_t)SIG_IGN);
#endif

	procopts(argc, argv, &redirlogs, &okrefusal);

	newslock();			/* done here due to dbm internal cacheing */
	if (redirlogs > 0) {
		redirectlogs(redirlogs); /* redirect std output streams to logs */
#ifdef MANYERRORS
		(void) putc('\n', stderr);	/* leave a blank line */
		/* prints "Jun  5 12:34:56" */
		timestamp(stderr, (time_t *)NULL);
		(void) putc('\n', stderr);
#endif
	}

	getwdandcd(argc, argv, &origdir);
	status |= procargs(argc, argv, &origdir);

	status |= synccaches();		/* being cautious: write & close caches */
	(void) fflush(stdout);		/* log file */
	(void) fflush(stderr);		/* errlog file */

#ifdef notdef
#  ifdef CSRIMALLOC
	mal_dumpleaktrace(fileno(stderr));
#  endif
#endif
	newsunlock();
	exit(status);
	/* NOTREACHED */
}

/*
 * reset various environmental things for safety: umask, alarm,
 * environment variables (PATH, IFS), standard file descriptors,
 * user & group ids.
 */
void
prelude(argv)				/* setuid daemon prelude */
char **argv;
{
	register char *newpath;

	(void) umask(2);		/* undo silly umasks, ignore newsumask() */
	(void) alarm(0);		/* cancel any pending alarm */
	newpath = malloc(STRLEN("PATH=") + strlen(newspath()) + 1);
	if (newpath == NULL)
		exit(1);		/* no chatter until stdfdopen */
	(void) strcpy(newpath, "PATH=");
	(void) strcat(newpath, newspath());
#ifndef AMIGA
	if (putenv(newpath) || putenv("IFS= \t\n"))
		exit(1);		/* no chatter until stdfdopen */
#endif
	closeall(1);			/* closes all but std descriptors */
	stdfdopen();			/* ensure standard descriptors are open */
	setids(argv);			/* change of real and effective ids */
}

/*
 * change real and effective ids to real ids if unprivileged() is called,
 * else to effective ("news") ids.  ctlfile((char *)0) will trigger a call
 * to unprivileged() if any environment variables override the default
 * path names.  unprivileged() in turn sets userealids.
 *
 * If setuid(geteuid()) fails, try execing a small, setuid-root program
 * to just do "getpwnam(), getgrnam() (with NEWSPERMS set), setgid(),
 * setuid()," and exec this program again.  If NEWSPERMS is set,
 * the failure is a fatal error (recursive loop).
 * This program (relaynews) can be setuid-news.
 */

void setids(argv)
char **argv;
{
	int newsuid, newsgid;

	(void) ctlfile((char *)NULL);
	if (userealids)
		newsuid = getuid(), newsgid = getgid();
	else
		newsuid = geteuid(), newsgid = getegid();
	if (setgid(newsgid) < 0 || setuid(newsuid) < 0) {
		if (getenv("NEWSPERMS") != 0)
			error("recursive loop setting ids", "");
		execv(ctlfile(SETNEWSIDS), argv);
		error("can't exec `%s' to set ids", ctlfile(SETNEWSIDS));
		/* NOTREACHED */
	}
	/* we are now running as news, so you can all relax */
}

/*
 * parse options and set flags
 */

void procopts(argc, argv, redirlogsp, okrefusalp)
int argc;
char **argv;
int *redirlogsp;
boolean *okrefusalp;
{
	int c, errflg = 0;

	while ((c = getopt(argc, argv, "d:inrsx:")) != EOF)
		switch (c) {
		case 'd':		/* -d debug-options; thanks, henry */
			if (!debugon(optarg))
				errflg++;	/* debugon has complained */
			break;
		case 'i':		/* redirect stdout to log (inews) */
			*redirlogsp = 1; /* just stdout */
			break;
		case 'n':		/* nntp mode: keep history of rejects */
			histreject = YES;
			break;
		case 'r':		/* redirect std. ostreams to logs (rnews) */
			*redirlogsp = 2; /* stdout & stderr */
			break;
		case 's':		/* dropping input is serious (inews) */
			*okrefusalp = NO;
			break;
		case 'x':		/* -x site: don't send to site */
			/* you're welcome, erik */
			/* erik says he only needs one -x per inews */
			if (exclude != NULL) {
				(void) fprintf(stderr,
					"%s: more than one -x site (%s)\n", progname, optarg);
				errflg++;
			} else
				exclude = optarg;
			break;
		default:
			errflg++;
			break;
		}
	if (errflg) {
		(void) fprintf(stderr,
			"usage: %s [-inrs][-d fhlmt][-x site]\n", progname);
		(void) fprintf(stderr,
			"       -df ... file, -dh ... hdr, -dl ... lock,\n");
		(void) fprintf(stderr,
			"       -dm ... match, -dt ... trans\n");
		exit(2);
	}
}

void unprivileged()		/* called if NEWSARTS, NEWSCTL or NEWSBIN present */
{
	userealids = YES;
}

STATIC boolean
debugon(dbopt)
register char *dbopt;
{
	statust status = YES;

	for (; *dbopt != '\0'; dbopt++)
		switch (*dbopt) {
		case 'f':
			filedebug(YES);
			break;
		case 'h':
			hdrdebug(YES);
			break;
		case 'l':
			lockdebug(YES);
			break;
		case 'm':
			matchdebug(YES);
			break;
		case 't':
			transdebug(YES);
			break;
		default:
			status = NO;	/* unknown debugging option */
			(void) fprintf(stderr, "%s: bad -d %c\n", progname, *dbopt);
			break;
		}
	return status;
}

/*
 * Redirect stdout or stderr into log files at known locations.
 */

void redirectlogs(count)
int count;
{
	if (count > 0)
		logfile(stdout, ctlfile("log"));
	if (count > 1)
		logfile(stderr, ctlfile("errlog"));
}

void logfile(stream, name)			/* redirect stream into name */
FILE *stream;
char *name;
{
	if (freopen(name, "a", stream) == NULL)
		errunlock("can't redirect standard stream to `%s'", name);
}

/*
 * if argv contains relative file name arguments, save current directory name
 * in malloced memory, through origdirp.
 * then change directory to the spool directory ($NEWSARTS).
 */

void getwdandcd(argc, argv, origdirp)
int argc;
char **argv;
char **origdirp;
{
	register int argind;
	boolean needpwd = NO;
	char dirtmp[MAXPATH];			/* much bigger than needed */

	for (argind = optind; argind < argc; argind++)
		if (argv[argind][0] != FNDELIM)
			needpwd = YES;

	*origdirp = "/???";			/* pessimism */
	if (needpwd && getcwd(dirtmp, sizeof dirtmp) != 0)
		*origdirp = dirtmp;
	*origdirp = strsave(*origdirp);		/* save a smaller copy */
	cd(fullartfile((char *)NULL));		/* move to spool directory */
}

/*
 * process files named as arguments (or implied)
 */

statust procargs(argc, argv, origdirp)
int argc;
char **argv;
char **origdirp;
{
	register statust status = ST_OKAY;

	if (optind == argc)
		status |= process(stdin, "stdin");
	else
		for (; optind < argc; optind++)
			status |= relnmprocess(argv[optind], *origdirp);
	nnfree(origdirp);
	return status;
}

statust relnmprocess(name, origdir)		/* process a (relative) file name */
char *name, *origdir;
{
	register statust status = ST_OKAY;
	register FILE *in;
	register char *fullname;

	fullname = nemalloc((unsigned)strlen(origdir) + STRLEN(SFNDELIM) +
		strlen(name) + 1);
	fullname[0] = '\0';

	if (name[0] != FNDELIM) {	/* relative path */
		(void) strcat(fullname, origdir);
		(void) strcat(fullname, SFNDELIM);
	}
	(void) strcat(fullname, name);

	in = fopenwclex(fullname, "r");
	if (in != NULL) {
		status |= process(in, fullname);
		(void) nfclose(in);
	}
	free(fullname);
	return status;
}

/*
 * process - process input file
 * If it starts with '#', assume it's a batch and unravel it,
 * else it's a single article, so just inject it.
 */

statust process(in, inname)
FILE *in;
char *inname;
{
	register int c;

	if ((c = getc(in)) == EOF)
		return ST_OKAY; 		/* normal EOF */
	(void) ungetc(c, in);
	if (c == '#')
		return unbatch(in, inname);
	else
		return cpinsart(in, inname, MAXLONG, NO);
}

/*
 * Unwind "in" and insert each article.
 * For each article, call cpinsart to copy the article from "in" into
 * a (temporary) file in the news spool directory and rename the temp file
 * to the correct final name if it isn't right already.
 *
 * If the unbatcher gets out of sync with the input batch, the unbatcher
 * will print and discard each input line until it gets back in sync.
 */

statust unbatch(in, inname)
register FILE *in;
char *inname;
{
	register int c;
	/* register */ char *line;
	register statust status = ST_OKAY;
	long charcnt;

	while (!(status&ST_DISKFULL) && (c = getc(in)) != EOF) {
		(void) ungetc(c, in);
		STAT((stderr, "read char '%c'\n", c));
		while ((line = fgetms(in)) != NULL &&
				    !batchln(line, &charcnt)) {		/* returns charcnt */
			status |= ST_DROPPED;
			(void) fprintf(stderr,
			    "\n%s: unbatcher out of synch: ", progname);
	    	(void) fputs(line, stderr);
			free(line);
		}
		STAT((stderr, "count (%ld) line okay: %s", charcnt, line));
		nnfree(&line);			/* free "#! rnews n" */
		if (!feof(in))
			status |= cpinsart(in, inname, charcnt, YES);
	}
	if (ferror(in))
		errunlock("error reading `%s'", inname);
	return status;
}

/*
 * Is line a batcher-produced line (#! rnews count)?
 * If so, return the count through charcntp.
 * This is slightly less convenient than sscanf, but a lot smaller.
 */

boolean batchln(line, charcntp)
register char *line;
register long *charcntp;
{
	extern long atol();
	register char *countp;
	static char batchtext[] = "#! rnews ";

	countp = line + STRLEN(batchtext);
	STAT((stderr, "line: %s\ncount: %s\n", line, countp));
	if (STREQN(line, batchtext, STRLEN(batchtext)) &&
		    isascii(*countp) && isdigit(*countp)) {
		*charcntp = atol(countp);
		return YES;
	} else {
		*charcntp = 0;
		return NO;
	}
}
