/*
 * readcf: read the firewall configuration table into memory
 */
#include <stdio.h>
#include <string.h>
#include <malloc.h>
#include <stdarg.h>
#include <netdb.h>
#include <syslog.h>
#include <signal.h>

#include <assert.h>

#include "firewall.h"
#include "tokens.h"

static int firsttimein=1;
PERMS perms;

typedef struct {
    connection from;
    connection to;
    int action;
    int dir;
} PARSED;


extern int line_number;
extern char yytext[];
extern unsigned long netmask[];


char *config_file = FIREWALL_CF;

static int getaddr(char*,connection*);

/*
 * warn() is a local that shrieks bitterly if something goes wrong
 */
warn(char *fmt, ...)
{
    va_list ptr;
    char msg[300];

    va_start(ptr, fmt);
#ifdef TEST
    fprintf(stderr, "conf: at line %d, ", line_number);
    vfprintf(stderr, fmt, ptr);
    fputc('\n', stderr);
#else
    vsprintf(msg, fmt, ptr);
    syslog(LOG_ERR, "conf: at line %d, %s", line_number, msg);
#endif
    va_end(ptr);
} /* warn */


/*
 * gettok() does yylex() with a 1-token pushback
 */
static int pushback = EOF;
static int
gettok()
{
    int tmp;
    if (pushback != EOF) {
	tmp = pushback;
	pushback = EOF;
	return tmp;
    }
    tmp = yylex();
    return tmp;
} /* gettok */


/*
 * ungettok() pushes back a token
 */
static void
ungettok(int token)
{
    pushback = token;
} /* ungettok */

/*
 * freeplist() punts the contents of a plist with extreme prejudice
 */
void
freeplist(PLIST victim)
{
    if (victim) {
	free(victim->list);
	free(victim);
    }
} /* freeplist */


/*
 * dropcfg() is a local function that throws away the existing firewall
 *           configuration before loading in the new one.
 */
static void
dropcfg()
{
    freeplist(perms.p_from);
    freeplist(perms.p_to);
    perms.p_from = perms.p_to = 0;
} /* dropcfg */


/*
 * addtoplist() is a local that adds a connection pair to a given
 *              plist, initializing it if need be.
 */
void
addtoplist(PARSED *entry, PLIST* plp)
{
    PLIST tmp;
    if (*plp == 0) {
	tmp = *plp = malloc(sizeof *tmp);
	memset(tmp, 0, sizeof *tmp);
	tmp->list = malloc(0);
    }
    else
	tmp = *plp;

    tmp->list = realloc(tmp->list, (tmp->nrlist + 1) * sizeof tmp->list[0]);
    tmp->list[tmp->nrlist].from = entry->from;
    tmp->list[tmp->nrlist].to   = entry->to;
    tmp->list[tmp->nrlist].todo = entry->action;
    tmp->nrlist++;
} /* addtoplist */



/*
 * getsource() attempts to parse a source address
 */
static int
getsource(PARSED *data)
{
    assert(data);
    return getaddr("source", &data->from);
}

/*
 * getdest() attempts to parse a destination address
 */
static int
getdest(PARSED *data)
{
    assert(data);
    return getaddr("destination", &data->to);
}


/*
 * getport() gets a port number either by name or by number
 */
static int
getport(char *desc, connection* inetaddr)
{
    int tok;
    struct servent *service;

    if ((tok = gettok()) == t_NUMBER) {
	inetaddr->port = htons(atoi(yytext));
	return 1;
    }
    else if (tok == t_WORD) {
	if ((service = getservbyname(yytext, "tcp")) != 0) {
	    inetaddr->port = service->s_port;
	    return 1;
	}
	warn("unknown %s port <%s>", desc, yytext);
    }
    else if (tok != t_FINI)
	warn("looking for a %s port, but got <%s>", desc, yytext);
    return (tok == t_FINI) ? EOF : 0;
} /* getport */


/*
 * getaddr() gets a ip address + port
 */
static int
getaddr(char *desc, connection* inetaddr)
{
    int tok;
    int ct, mask;
    struct hostent *host;
    int c1, c2, c3, c4;
    ADDRESS addr;

    if ( (tok=gettok()) == t_ALL) {
	/* wildcard */
	inetaddr->ip = 0;
	inetaddr->netmask = 0;
	inetaddr->port = 0;
	return 1;
    }
    else if (tok == t_PORT) {
	/* wildcard port */
	inetaddr->ip = 0;
	inetaddr->netmask = 0;
	return getport(desc, inetaddr);
    }
    else if (tok == t_WORD || tok == t_QUAD) {
	/* a host or possibly a netmask.
	 */
	inetaddr->netmask = 0xffffffff;	/* default to matching the whole addr */
	if ((host = gethostbyname(yytext)) != 0) {
	    /* that was simple!
	     */
	    memcpy(&addr, host->h_addr, 4);
	    inetaddr->ip = addr.ip;	/* the documentation sez this is
					 * in network byte order, but
					 * ntohl() returns garbage
					 */
	}
	else if (tok != t_QUAD) {
	    warn("cannot resolv %s host name <%s>", desc, yytext);
	    return 0;
	}
	else { /* try to parse it by hand */
	    c1 = c2 = c3 = c4 = 0;
	    ct = sscanf(yytext, "%d.%d.%d.%d", &c1, &c2, &c3, &c4);
	    addr.octet[0] = c1; addr.octet[1] = c2;
	    addr.octet[2] = c3; addr.octet[3] = c4;

	    /* it's either (a) bogus, (b) a dotted-quad IP address,
	     * or (c) a fractional ip address
	     */

	    if (ct == 0) {
		warn("malformed %s ip address <%s>", desc, yytext);
		return 0;
	    }
	    else if (ct == 4)
		inetaddr->ip = addr.ip;
	    else {
		/* fractional ip addresses (x.x.x.) are treated as
		 * address + classed netmask
		 */
		mask = ct * 8;	/* 8,16,24 bits of address */
		inetaddr->ip = addr.ip;
		inetaddr->netmask = htonl(netmask[mask-1]);

		ungettok(tok = gettok());
		if (tok == t_NUMBER || tok == t_WORD)
		    return getport(desc, inetaddr);
		else
		    return 1;
	    }
	}

	/* after getting the ip address, see if a netmask is given
	 * in CIDR or dotted-quad format.
	 */
	tok = gettok();

	if (tok == t_SLASH) {
	    int mask;

	    if ((tok = gettok()) != t_NUMBER) {
		warn("expected a cidr width for the %s address, but got <%s>",
			desc, yytext);
		return 0;
	    }
	    mask = atoi(yytext);
	    if (mask < 1 || mask > 31) {
		warn("cidr address widths need to be >= 1 && <= 31, not <%s>",
			yytext);
		return 0;
	    }
	    inetaddr->netmask = htonl(netmask[mask-1]);
	}
	else if (tok == t_QUAD) {
	    /* read in the dotted-quad.  If we're given a fractional
	     * dotted-quad, that's okay, because we can interpolate
	     * from the fractional out to the complete netmask
	     */
	    c1 = c2 = c3 = c4 = 0;
	    ct = sscanf(yytext, "%d.%d.%d.%d", &c1, &c2, &c3, &c4);
	    addr.octet[0] = c1; addr.octet[1] = c2;
	    addr.octet[2] = c3; addr.octet[3] = c4;

	    if (ct == 0) {
		warn("malformed %s netmask <%s>", desc, yytext);
		return 0;
	    }
	    inetaddr->netmask = addr.ip;
	}
	else {
	    inetaddr->port = 0;
	    ungettok(tok);
	}

	/* after getting the address plus the optional netmask, we'll
	 * peek ahead to see if we've got a port number
	 */
	ungettok(tok = gettok());

	if (tok == t_NUMBER || tok == t_WORD)
	    return getport(desc, inetaddr);

	return 1;
    }
    else if (tok == t_FINI) {
	warn("unexpected EOF looking for a %s addr", desc);
	return EOF;
    }
    warn("looking for a %s address, but got <%s>", desc, yytext);
    return 0;
} /* getaddr */


/*
 * parse() attempts to parse a firewall action
 */
static int
parse(PARSED *data)
{
    int tok;
    connection temp;

    tok = gettok();
    if (tok == t_FINI)
	return EOF;
    while (tok == t_NEWLINE)
	tok = gettok();

    if (tok != t_ALLOW && tok != t_DENY) {
	warn("expected ALLOW or DENY, not <%s>", yytext);
fail:
	while (tok != t_NEWLINE && tok != t_FINI)
	    tok = gettok();
	return (tok == t_FINI) ? EOF : 0;
    }
    data->action = (tok == t_ALLOW) ? ALLOW : DENY;

    if ((tok=getdest(data)) <= 0) {
	if (tok == t_FINI)
	    warn("unexpected EOF while parsing source address");
	goto fail;
    }

    if ((tok = gettok()) == t_FROM || tok == t_TO)
	data->dir = (tok == t_FROM) ? OUT : IN;
    else {
	warn("expected FROM or TO, not <%s>", yytext);
	goto fail;
    }

    if ((tok = getsource(data)) == 0)
	goto fail;

    if ((tok = gettok()) != t_NEWLINE && tok != t_FINI) {
	warn("garbage at end of line");
	while (tok != t_NEWLINE && tok != t_FINI)
	    tok = gettok();
    }
    if (data->dir == OUT) {
	temp = data->from;
	data->from = data->to;
	data->to = temp;
    }
    return 1;
} /* parse */



/*
 * readcfg() reads the firewall configuration file
 */
int
readcfg()
{
    char line[2000];
    extern FILE *yyin;
    PARSED res;
    int rc;

#ifndef TEST
    syslog(LOG_INFO, "reading %s", config_file);
    signal(SIGHUP, SIG_IGN);
#endif
    if (firsttimein) {
	memset(&perms, 0, sizeof perms);
	firsttimein = 0;
    }
    else
	dropcfg();

    line_number = 0;
    if ((yyin = fopen(config_file, "r")) != 0) {
	while ((rc = parse(&res)) != EOF) {
	    if (rc > 0)
		addtoplist(&res, &perms.traf[res.dir]);

	}
	fclose(yyin);
	rc = 1;
    }
    else
	rc = 0;
#ifndef TEST
    signal(SIGHUP, readcfg);
#endif
    return rc;
} /* readcfg */


#ifdef TEST
int debug = 0;

void
printconnection(connection *p)
{
    ADDRESS addr;
    if (p->netmask == 0)
	if (p->port == 0)
	    printf("any ");
	else
	    printf("port %d ", ntohs(p->port));
    else {
	addr.ip = p->ip;
	printf("%d.%d.%d.%d ", addr.octet[0], addr.octet[1],
			       addr.octet[2], addr.octet[3]);
	if (p->netmask != 0) {
	    addr.ip = p->netmask;
	    printf("netmask %d.%d.%d.%d ", addr.octet[0], addr.octet[1],
					   addr.octet[2], addr.octet[3]);
	}
	if (p->port != 0)
	    printf("port %d ", ntohs(p->port));
    }
}

main()
{
    int i, w;
    PENTRY *ent;

    readcfg();

    puts("incoming connections:");
    if (perms.p_from) {
	for (i=0; i < perms.p_from->nrlist; i++) {
	    ent = &(perms.p_from->list[i]);
#if 0
	    printf("%08x/%08x (%03x) <- %08x/%08x (%03x)\n",
		    ent->to.ip, ent->to.netmask, ent->to.port,
		    ent->from.ip, ent->from.netmask, ent->from.port);
#endif
	    printf("%s ", (ent->todo == ALLOW) ? "allow" : "deny");

	    printconnection( &(ent->from) );
	    printf("from ");
	    printconnection( &(ent->to) );
	    putchar('\n');
	}
    }
    puts("deny all from all\n");

    puts("outgoing connections:");
    if (perms.p_to) {
	for (i=0; i < perms.p_to->nrlist; i++) {
	    ent = &(perms.p_to->list[i]);
#if 0
	    printf("%08x/%08x (%03x) <- %08x/%08x (%03x)\n",
		    ent->to.ip, ent->to.netmask, ent->to.port,
		    ent->from.ip, ent->from.netmask, ent->from.port);
#endif
	    printf("%s ", (ent->todo == ALLOW) ? "allow" : "deny");

	    printconnection( &(ent->from) );
	    printf("to ");
	    printconnection( &(ent->to) );
	    putchar('\n');
	}
    }
    puts("accept all to all\n");
}
#endif
