/*-
 * Copyright (c) 1998 Joe Greco and sol.net Network Services
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 */

/* Todo:	option for only filtering headers
 *		option for "early abort" like Cyclone does
 */

#include "defs.h"

Prototype int DiabFilter(char *fpath, char *loc, int wireformat);
Prototype void DiabFilter_freeMem(void);
Prototype void DiabFilterClose(void);

static ssize_t writeAll(int fd, char *buf, ssize_t len);
void filter_failed(char *reason, char *fpath);
void open_filter_program(char *fpath);
int sizeit(int len);

int filter_fd_stdin = -1;
int filter_fd_stdout = -1;
int filter_fail_count = 0;
time_t filter_try_again = 0;
time_t filter_last_fail = 0;
pid_t filter_pid = 0;

int filter_abufsiz = 0;
int filter_nbufsiz = 0;
char *filter_abuf = NULL;
char *filter_nbuf = NULL;
MemPool *filter_abufPool = NULL;
MemPool *filter_nbufPool = NULL;

static ssize_t
writeAll(int fd, char *buf, ssize_t len)
{
    size_t off = 0;
    ssize_t res;

    while (len) {
	if ((res = write(fd, buf+off, len)) >= 0) {
	    off += res;
	    len -= res;
	} else
	    return res;
    }
    return off;
}

void
DiabFilter_freeMem(void)
{
	if (filter_abufPool) {
		freePool(&filter_abufPool);
		filter_abufPool = NULL;
		filter_abuf = NULL;
	}

	if (filter_nbufPool) {
		freePool(&filter_nbufPool);
		filter_nbufPool = NULL;
		filter_nbuf = NULL;
	}
}

void 
filter_failed(char *reason, char *fpath)
{
	int delay;
	time_t now = time(NULL);

	/* Yeah, yeah, it's hokey. */
	if (now - filter_last_fail < 300) {
		filter_fail_count++;
	} else {
		filter_fail_count--;
	}
	filter_last_fail = now;
	delay = filter_fail_count * filter_fail_count;
	delay = (delay > 900) ? 900 : delay;
	filter_try_again = time(NULL) + delay;
	logit(LOG_ERR, "diab-filter(%s): filter failed, %s, sleeping for %d seconds\n", fpath, reason, delay);
}

void 
DiabFilterClose(void)
{
	int status, rval, loop;

	if (! (filter_fd_stdin < 0)) {
		close(filter_fd_stdin);
		filter_fd_stdin = -1;
	}
	if (! (filter_fd_stdout < 0)) {
		close(filter_fd_stdout);
		filter_fd_stdout = -1;
	}
	if (filter_pid) {
		for (loop = 0; loop < 10; loop++) {
			if ((rval = waitpid(filter_pid, &status, WNOHANG)) < 0) {
				logit(LOG_ERR, "External filter waitpid for %d failed: %m", filter_pid);
				filter_pid = 0;
				return;
			}
			if (WIFEXITED(status)) {
				if (WEXITSTATUS(status)) {
					logit(LOG_ERR, "External filter returned exit %d", WEXITSTATUS(status));
				} else {
					logit(LOG_NOTICE, "External filter exited normally");
				}
				filter_pid = 0;
				return;
			}
			if (WIFSIGNALED(status)) {
				logit(LOG_ERR, "External filter exited on signal %d", WTERMSIG(status));
				filter_pid = 0;
				return;
			}
			sleep(1);
		}
		logit(LOG_ERR, "filter failed to exit");
		filter_pid = 0;
		return;
	}
}

void
open_filter_program(char *fpath)
{
	int stdinfds[2];
	int stdoutfds[2];
	int nfd;
	pid_t newpid;

	if (filter_try_again) {
		if (time(NULL) < filter_try_again) {
			return;
		}
		filter_try_again = 0;
	}

	if (! (*fpath == '/')) {
		/* Not a path name!  Guess that it is a TCP connection */
		if ((nfd = connect_tcp_socket(fpath, 0, 0)) < 0) {
			logit(LOG_ERR, "couldnt connect to remote filter (%s): %m", fpath);
			filter_failed("couldnt connect to remote filter", fpath);
			return;
		}
		filter_fd_stdin = nfd;
		filter_fd_stdout = nfd;

		if (fcntl(filter_fd_stdin, F_SETFD, 1) < 0) {
			logit(LOG_ERR, "fcntl filter stdin: %m");
		}
		if (fcntl(filter_fd_stdout, F_SETFD, 1) < 0) {
			logit(LOG_ERR, "fcntl filter stdout: %m");
		}

		filter_pid = 0;

		/* "Woohoo!" */
		logit(LOG_NOTICE, "filter connected to remote filter");
		return;
	}

	if (pipe(stdinfds) < 0) {
		filter_failed("cant create pipe", fpath);
		return;
	}

	if (pipe(stdoutfds) < 0) {
		filter_failed("cant create pipe", fpath);
		close(stdinfds[0]);
		close(stdinfds[1]);
		return;
	}

	/* We foolishly assume SIGPIPE has been handled elsewhere as SIG_IGN */
	/* Assumption is the mother ... XXX */

	if ((newpid = fork()) < 0) {
		filter_failed("cant create child process", fpath);
		close(stdinfds[0]);
		close(stdinfds[1]);
		close(stdoutfds[0]);
		close(stdoutfds[1]);
		return;
	}

	if (! newpid) {
		/* Child processing. */

		if (dup2(stdinfds[0], fileno(stdin)) < 0) {
			filter_failed("cant dup2 stdin", fpath);
			close(stdinfds[0]);
			close(stdinfds[1]);
			close(stdoutfds[0]);
			close(stdoutfds[1]);
			exit(1);
		}
		close(stdinfds[0]);
		close(stdinfds[1]);

		if (dup2(stdoutfds[1], fileno(stdout)) < 0) {
			filter_failed("cant dup2 stdout", fpath);
			close(fileno(stdin));
			close(stdoutfds[0]);
			close(stdoutfds[1]);
			exit(1);
		}
		close(stdoutfds[0]);
		close(stdoutfds[1]);

		execl(fpath, fpath, NULL);
		filter_failed("cant exec external filter", fpath);
		close(fileno(stdin));
		close(fileno(stdout));
		exit(1);
	}
	/* Parent processing. */

	close(stdinfds[0]);
	close(stdoutfds[1]);

	filter_fd_stdin = stdinfds[1];
	filter_fd_stdout = stdoutfds[0];

	if (fcntl(filter_fd_stdin, F_SETFD, 1) < 0) {
		logit(LOG_ERR, "fcntl filter stdin: %m");
	}
	if (fcntl(filter_fd_stdout, F_SETFD, 1) < 0) {
		logit(LOG_ERR, "fcntl filter stdout: %m");
	}

	filter_pid = newpid;

	/* "Woohoo!" */
	logit(LOG_NOTICE, "External filter launched");
	return;
}

int 
sizeit(int len)
{
	int c = 0;

	while (len >>= 1) {
		c++;
	}
	len = 2;
	while (c--) {
		len <<= 1;
	}
	return(len);
}

int 
DiabFilter(char *fpath, char *loc, int wireformat)
{
	int rval, count, eoln, nbytes, llen;
	char *aptr, *nptr, *pptr;

	if (! loc || ! (aptr = strrchr(loc, ','))) {
		return(-1);
	}

	llen = atoi(aptr + 1);	/* get article length */

	if (llen >= filter_abufsiz) {
		count = sizeit(llen);
		if (filter_abuf) {
			freePool(&filter_abufPool);
			filter_abufPool = NULL;
			filter_abuf = NULL;
		}
		aptr = nzalloc(&filter_abufPool, count);
		if (aptr == NULL) {
			logit(LOG_ERR, "zalloc %d failed", count);
			return(-1);
		}
		filter_abufsiz = count;
		filter_abuf = aptr;

		count = filter_abufsiz * 2 + 6;
		if (filter_nbuf) {
			freePool(&filter_nbufPool);
			filter_nbufPool = NULL;
			filter_nbuf = NULL;
		}
		nptr = nzalloc(&filter_nbufPool, count);
		if (nptr == NULL) {
			logit(LOG_ERR, "zalloc %d failed", count);
			return(-1);
		}
		filter_nbufsiz = count;
		filter_nbuf = nptr;
	}

	if (filter_fd_stdin < 0) {
		open_filter_program(fpath);
	}
	if (filter_fd_stdin < 0) {
		return(-1);
	}

	if ((rval = diab_read(loc, filter_abuf, filter_abufsiz)) < 0) {
#ifdef DEBUG
		logit(LOG_ERR, "diab_read failed: %s", loc);
#endif
		return(-1);
	}

	count = rval;
	aptr = filter_abuf + sizeof(SpoolArtHdr);
	nptr = filter_nbuf;
	pptr = aptr;
	eoln = 0;
	while (count--) {
		if (wireformat) {
			if (*pptr == '\r' && *aptr == '\n')
			    eoln = 1;
			pptr = aptr;
			*nptr++ = *aptr++;
		} else if (*aptr == '\n') {
			*nptr++ = '\r';
			*nptr++ = *aptr++;
			eoln = 1;
		} else {
			if (eoln) {
				if (*aptr == '.') {
					*nptr++ = '.';
				}
				eoln = 0;
			}
			*nptr++ = *aptr++;
		}
	}
	if (! eoln) {
		logit(LOG_ERR, "article did not end with a return: %s", loc);
		*nptr++ = '\r';
		*nptr++ = '\n';
	}
	if (!wireformat || rval < 5 || strncmp(nptr - 5, "\r\n.\r\n", 5) != 0) {
		*nptr++ = '.';
		*nptr++ = '\r';
		*nptr++ = '\n';
	}

	nbytes = nptr - filter_nbuf;

	/* Send the article to the filter ... */
	if ((rval = writeAll(filter_fd_stdin, filter_nbuf, nbytes)) != nbytes) {
		logit(LOG_ERR, "filter write failure: wanted to write %d, wrote %d: %m", nbytes, rval);
		filter_failed("write", fpath);
		DiabFilterClose();
		return(-1);
	}

	/* Get the response of the filter ... */
	/* XXX this is Pure Evil(tm) because the response isn't going to 
	 * have to be atomic */
	filter_abuf[0] = '\0';
	if ((rval = read(filter_fd_stdout, filter_abuf, filter_abufsiz)) <= 0) {
		logit(LOG_ERR, "filter read failure: got %d: %m", rval);
		filter_failed("read", fpath);
		DiabFilterClose();
		return(-1);
	}
	filter_abuf[rval] = '\0';
	if (DebugOpt > 1)
	    printf("External filter response: %s\n", filter_abuf);

	filter_abuf[rval] = '\0';

	if (*filter_abuf == '3') {
		return(0);
	}
	if (*filter_abuf == '4') {
		return(1);
	}
	logit(LOG_ERR, "filter read failure: got unknown response: %s",
			filter_abuf);
	return(-1);
}



syntax highlighted by Code2HTML, v. 0.9.1