
/*
 * GFILTER.C	log / filter incoming articles based on overview data
 *
 * By Matthew Dillon.
 * Public domain, do with this what you will.
 * Distribution from:  http://www.backplane.com/news
 *
 * This program was originally written to permanently filter out certain 
 * infamous net characters from our newsfeed, whos names aren't worth the
 * disk space it takes to say them.
 *
 * TODO:
 *	Make lookups more efficient (go with hash table) to allow a large
 *	number of filters in the control file.
 *
 * INSTRUCTIONS:
 *	Modify the locations as appropriate and create a newsfeeds entry
 *	as follows (for INN).  Compile the program, and create a
 *	'gfilter.ctl' file in your news home.
 *
 *	Get the filter running, and set up a simple logging filter to
 *	test it (see gfilter.ctl).
 *
 * gfilter:\
 *	 *:B4096/1024,Tc,WO:/home/news/bin/gfilter
 *
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifndef BSIZE
#define BSIZE		4096	/* line size for article contents	*/
#endif
#ifndef LSIZE
#define LSIZE		8192	/* line buffer size for overview data	     */
#endif
#ifndef RELOAD
#define RELOAD		10000	/* reload control file every 10,000 articles */
#endif
#ifndef MAXFIELDS
#define MAXFIELDS	16
#endif
#ifndef MAXFILES
#define MAXFILES	4096
#endif

#ifndef CONTROL
#define CONTROL		"/home/news/gfilter.ctl"
#endif
#ifndef LOGFILE
#define LOGFILE		"/home/news/log/gfilter.file.log"
#endif
#ifndef LOGART
#define LOGART		"/home/news/log/gfilter.log"
#endif
#ifndef SPOOLBASE
#define SPOOLBASE	"/home/news/spool/news"
#endif

typedef struct Node {
     struct Node *no_Next;
     char	*no_Name;
     char	*no_Flags;
     int	no_Field;
     int	no_MaxCross;
     int	no_MaxSize;
} Node;

Node *Base;
FILE *ArtFo;
FILE *FileFo;

void reloadControl(void);
void takeAction(Node *node, char **fields, char **files,int maxfields,int maxfiles);
char *dupstr(const char *str);
int checkSize(Node *node, char **files, int maxfiles);

int
main(int ac, char **av)
{
    char buf[LSIZE];
    int reload = 1;
    int nart = 0;
    int ntake = 0;

    if (ArtFo == NULL)
	ArtFo = fopen(LOGART, "a");
    if (chdir(SPOOLBASE) != 0) {
	perror("chdir(" SPOOLBASE ")");
	if (ArtFo)
	    fprintf(ArtFo, "chdir(" SPOOLBASE ") failed\n");
	exit(1);
    }
    if (ArtFo) {
	fprintf(ArtFo, "GFILTER\tbegin %d\n", getpid());
	fflush(ArtFo);
    }

    while (fgets(buf, sizeof(buf), stdin) != NULL) {
	char *fields[MAXFIELDS];

	++nart;
	if (--reload == 0) {
	    reload = RELOAD;
	    reloadControl();
	}

	/*
	 * Break the line up into fields (up to 16) and 
	 * scan for matches
	 */

	if ((fields[0] = strtok(buf, "\t\n")) != NULL) {
	    int i;
	    int j;
	    Node *node;
	    char *files[MAXFILES];

	    for (
		i = 1; 
		i < MAXFIELDS && (fields[i] = strtok(NULL, "\t\n")) != NULL;
		++i
	    ) {
		;
	    }
	    for (
		j = 0;
		j < MAXFILES && (files[j] = strtok(fields[0], " ")) != NULL;
		++j
	    ) {
		fields[0] = NULL;
	    }
	    for (node = Base; node; node = node->no_Next) {
		/*
		 * Field #1 (index 0) is a special case match
		 * against the article path(s) in the spool
		 */
		if (node->no_Field == 0 &&
		    node->no_Field < i &&
		    j >= node->no_MaxCross &&
		    checkSize(node, files, j)
		) {
		    int k;

		    for (k = 0; k < j; ++k) {
			if (strstr(files[k], node->no_Name) != NULL) {
			    takeAction(node, fields, files, i, j);
			    ++ntake;
			    break;
			}
		    }
		    continue;
		}

		/*
		 * Field #N general test
		 */

		if (node->no_Field > 0 && 
		    node->no_Field < i &&
		    j >= node->no_MaxCross &&
		    strstr(fields[node->no_Field], node->no_Name) != NULL &&
		    checkSize(node, files, j)
		) {
		    takeAction(node, fields, files, i, j);
		    ++ntake;
		}
	    }
	}
    }
    if (ArtFo) {
	fprintf(ArtFo, "GFILTER\tend %d\ttake %d/%d\n", getpid(), ntake, nart);
	fflush(ArtFo);
    }
    return(0);
}

void
reloadControl(void)
{
    FILE *fi;
    char buf[1024];

    /*
     * Clear out existing control file
     */

    {
	Node *node;
	while ((node = Base) != NULL) {
	    Base = node->no_Next;
	    free(node->no_Name);
	    free(node->no_Flags);
	    free(node);
	}
    }

    /*
     * Scan new control file
     */

    if ((fi = fopen(CONTROL, "r")) != NULL) {
	Node *node = NULL;


	while (fgets(buf, sizeof(buf), fi) != NULL) {
	    char *tmp;
	    char *ptr;

	    if (buf[0] == '#' || buf[0] == '\n')
		continue;

	    /*
	     *  Allocate new node 
	     */
	    if (node == NULL) {
		if ((node = malloc(sizeof(Node))) == NULL)
		    exit(5);
		bzero(node, sizeof(*node));
	    }

	    /*
	     *  Free data fields in previously allocated node,
	     *  clear (possibly previously used) node
	     */

	    if (node->no_Name)
		free(node->no_Name);
	    if (node->no_Flags)
		free(node->no_Flags);
	    bzero(node, sizeof(*node));

	    /*
	     * Parse data fields:  field# (1-N), flags, match,
	     * and maximum # of cross posts
	     */

	    if ((tmp = strtok(buf, " \t\n")) == NULL)
		continue;
	    node->no_Field = strtol(tmp, NULL, 0);
	    if ((tmp = strtok(NULL, " \t\n")) == NULL)
		continue;

	    node->no_Flags = dupstr(tmp);
	    if ((ptr = strchr(tmp, 'n')) != NULL)
		node->no_MaxCross = strtol(ptr + 1, NULL, 0);
	    if ((ptr = strchr(tmp, 's')) != NULL)
		node->no_MaxSize = strtol(ptr + 1, NULL, 0);

	    if ((tmp = strtok(NULL, "\n")) == NULL)
		continue;
	    node->no_Name = dupstr(tmp);

	    /*
	     * Enter into database (a simple linear list for now)
	     */

	    if (node->no_Field > 0 && node->no_Flags && node->no_Name) {
		node->no_Next = Base;
		--node->no_Field;
		Base = node;
		node = NULL;
	    }
	}
	fclose(fi);
    } else {
	if (ArtFo)
	    fprintf(ArtFo, "Unable to open %s\n", CONTROL);
    }
}

void
takeAction(Node *node, char **fields, char **files, int maxfields, int maxfiles)
{
    struct stat st;

    if (ArtFo && fstat(fileno(ArtFo), &st) == 0 && st.st_nlink == 0) {
	fclose(ArtFo);
	ArtFo = NULL;
    }
    if (FileFo && fstat(fileno(FileFo), &st) == 0 && st.st_nlink == 0) {
	fclose(FileFo);
	FileFo = NULL;
    }
    if (ArtFo == NULL)
	ArtFo = fopen(LOGART, "a");

    if (FileFo == NULL)
	FileFo = fopen(LOGFILE, "a");

    /*
     * l - log overview record to LOGART
     */

    if (ArtFo && maxfields > 0 && maxfiles > 0 && strchr(node->no_Flags, 'l') != NULL) {
	int i;

	fprintf(ArtFo, "OVERVIEW\t");
	fprintf(ArtFo, "%s", files[0]);
	for (i = 1; i < maxfiles; ++i)
	    fprintf(ArtFo, " %s", files[i]);

	for (i = 1; i < maxfields; ++i)
	    fprintf(ArtFo, "\t%s", fields[i]);
	fprintf(ArtFo, "\n");
	fflush(ArtFo);
    }

    /*
     * f - append article to LOGFILE
     */

    if (FileFo && maxfields > 0 && strchr(node->no_Flags, 'f') != NULL) {
	FILE *fi;

	if ((fi = fopen(files[0], "r")) != NULL) {
	    char buf[BSIZE];

	    fprintf(FileFo, "\n");
	    while (fgets(buf, sizeof(buf), fi) != NULL)
		fputs(buf, FileFo);
	    fprintf(FileFo, "\n");
	    fclose(fi);
	}
	fflush(FileFo);
    }
	
    /*
     * d - delete article
     */

    if (strchr(node->no_Flags, 'd') != NULL) {
	int i;

	for (i = 0; i < maxfiles; ++i) {
	    char *path = files[i];

	    if (*path == '/')			/* must be relative 	*/
		continue;
	    if (strstr(path, "..") != NULL)	/* must not contain ..	*/
		continue;
	    fprintf(ArtFo, "DELETE\t%s\n", files[i]);
	    remove(files[i]);
	}
	fflush(ArtFo);
    }
}

char *
dupstr(const char *str)
{
    char *ptr;

    if ((ptr = malloc(strlen(str) + 1)) == NULL)
       exit(10);
    strcpy(ptr, str);
    return(ptr);
}

/*
 * If the 's' flag is specified with a maximum article size,
 * and the article does not exceed this size, the match logic
 * is disabled.
 */

int 
checkSize(Node *node, char **files, int maxfiles)
{
    struct stat st;
    int r = 1;

    if (node->no_MaxSize > 0 && maxfiles > 0 && stat(files[0], &st) == 0) {
	if (st.st_size <= node->no_MaxSize)
	    r = 0;
    }
    return(r);
}

