/*
 * trigger.c
 *
 * Copyright (c) 2000 Dug Song <dugsong@monkey.org>
 *
 * $Id: trigger.c,v 1.21 2001/03/15 08:33:05 dugsong Exp $
 */

#include "config.h"

#include <sys/types.h>
#include <sys/uio.h>
#include <netinet/in.h>

#include <stdlib.h>
#include <err.h>
#include <libnet.h>
#include <nids.h>

#include "decode.h"
#include "magic.h"
#include "options.h"
#include "pathnames.h"
#include "record.h"
#include "tcp_raw.h"
#include "trigger.h"
#include "crc32.h"
#include "dsniff_services.h"

struct trigger {
	int num;
	struct decode *decode;
};

static struct trigger	ip_triggers[32];
static struct trigger	udp_triggers[512];
static struct trigger	tcp_triggers[512];
static struct trigger	rpc_triggers[32];

static u_int		ip_cnt = 0;
static u_int		udp_cnt = 0;
static u_int		tcp_cnt = 0;
static u_int		rpc_cnt = 0;

static char		obuf[4096];

extern struct _dc_meta dc_meta;

static int
trigger_compare(const void *a, const void *b)
{
	struct trigger *p, *q;

	q = (struct trigger *)a;
	p = (struct trigger *)b;
	
	if (p->num < q->num) {
		return (-1);
	}
	else if (p->num > q->num) {
		return (1);
	}
	return (0);
}

int
trigger_set_ip(int num, char *name)
{
	struct trigger *t, tr;

	tr.num = num;
	
	if ((tr.decode = getdecodebyname(name)) == NULL) {
		warnx("trigger_set_ip: unknown decode: %s", name);
		return (0);
	}
	t = (struct trigger *) bsearch(&tr, &ip_triggers, ip_cnt,
				       sizeof(tr), trigger_compare);
	if (t != NULL) {
		if (Opt_debug)
			warnx("trigger_set_ip: proto %d already set", num);
		return (0);
	}
	if (ip_cnt == sizeof(ip_triggers) / sizeof(tr)) {
		warnx("trigger_set_ip: ip_triggers full");
		return (0);
	}
	ip_triggers[ip_cnt++] = tr;
	
	qsort(&ip_triggers, ip_cnt, sizeof(tr), trigger_compare);
	
	if (Opt_debug)
		warnx("trigger_set_ip: proto %d -> %s", num, name);
	
	return (1);
}

int
trigger_set_udp(int num, char *name)
{
	struct trigger *t, tr;
	
	tr.num = num;
	
	if ((tr.decode = getdecodebyname(name)) == NULL) {
		warnx("trigger_set_udp: unknown decode: %s", name);
		return (0);
	}
	t = (struct trigger *) bsearch(&tr, &udp_triggers, udp_cnt,
				       sizeof(tr), trigger_compare);
	if (t != NULL) {
		if (Opt_debug)
			warnx("trigger_set_udp: port %d already set", num);
		return (0);
	}
	if (udp_cnt == sizeof(udp_triggers) / sizeof(tr)) {
		warnx("trigger_set_udp: udp_triggers full");
		return (0);
	}
	udp_triggers[udp_cnt++] = tr;
	
	qsort(&udp_triggers, udp_cnt, sizeof(tr), trigger_compare);
	
	if (Opt_debug)
		warnx("trigger_set_udp: port %d -> %s", num, name);

	return (1);
}

int
trigger_set_tcp(int num, char *name)
{
	struct trigger *t, tr;
	
	tr.num = num;
	
	if ((tr.decode = getdecodebyname(name)) == NULL) {
		warnx("trigger_set_tcp: unknown decode: %s", name);
		return (0);
	}
	t = (struct trigger *) bsearch(&tr, &tcp_triggers, tcp_cnt,
				       sizeof(tr), trigger_compare);
	if (t != NULL) {
		if (Opt_debug)
			warnx("trigger_set_tcp: port %d already set", num);
		return (0);
	}
	if (tcp_cnt == sizeof(tcp_triggers) / sizeof(tr)) {
		warnx("trigger_set_tcp: tcp_triggers full");
		return (0);
	}
	tcp_triggers[tcp_cnt++] = tr;
	
	qsort(&tcp_triggers, tcp_cnt, sizeof(tr), trigger_compare);
	
	if (Opt_debug)
		warnx("trigger_set_tcp: port %d -> %s", num, name);
	
	return (1);
}

int
trigger_set_rpc(int num, char *name)
{
	struct trigger *t, tr;

	tr.num = num;
	
	if ((tr.decode = getdecodebyname(name)) == NULL) {
		warnx("trigger_set_tcp: unknown decode: %s", name);
		return (0);
	}
	t = (struct trigger *) bsearch(&tr, &rpc_triggers, rpc_cnt,
				       sizeof(tr), trigger_compare);
	if (t != NULL) {
		if (Opt_debug)
			warnx("trigger_set_rpc: RPC program %d already set",
			      num);
		return (0);
	}
	if (rpc_cnt == sizeof(rpc_triggers) / sizeof(tr)) {
		warnx("trigger_set_rpc: rpc_triggers full");
		return (0);
	}
	rpc_triggers[rpc_cnt++] = tr;
	
	qsort(&rpc_triggers, rpc_cnt, sizeof(tr), trigger_compare);
	
	if (Opt_debug)
		warnx("trigger_set_rpc: program %d -> %s", num, name);
	
	return (1);
}

static int
trigger_set(char *proto, int num, char *name)
{
	if (strcasecmp(proto, "ip") == 0) {
		return (trigger_set_ip(num, name));
	}
	else if (strcasecmp(proto, "udp") == 0) {
		return (trigger_set_udp(num, name));
	}
	else if (strcasecmp(proto, "tcp") == 0) {
		return (trigger_set_tcp(num, name));
	}
	else if (strcasecmp(proto, "rpc") == 0) {
		return (trigger_set_rpc(num, name));
	}
	else warnx("trigger_set: unknown protocol %s", proto);

	return (0);
}

static struct trigger *
trigger_set_magic(int proto, int num, u_char *buf, int len)
{
	struct trigger *t, tr;
	char *name;

	if ((name = magic_match(buf, len)) == NULL)
		return NULL;

	t = NULL;
	tr.num = num;

	if (proto == IPPROTO_UDP) {
		trigger_set_udp(num, name);
		if (strcmp(name, "portmap") == 0 ||	/* XXX - hack */
		    strcmp(name, "mountd") == 0 ||
		    strcmp(name, "yppasswd") == 0) {
			trigger_set_udp(0 - num, name);
		}
		t = (struct trigger *) bsearch(&tr, &udp_triggers, udp_cnt,
					       sizeof(tr), trigger_compare);
	}
	else if (proto == IPPROTO_TCP) {
		trigger_set_tcp(num, name);
		if (strcmp(name, "portmap") == 0 ||	/* XXX - hack */
		    strcmp(name, "mountd") == 0 ||
		    strcmp(name, "yppasswd") == 0) {
			trigger_set_tcp(0 - num, name);
		}
		t = (struct trigger *) bsearch(&tr, &tcp_triggers, tcp_cnt,
					       sizeof(tr), trigger_compare);
	}
	return (t);
}

void
trigger_dump(void)
{
	FILE *f;
	int i;

	if ((f = fopen(DSNIFF_SERVICES, "w")) == NULL) {
		warn("trigger_dump: couldn't open " DSNIFF_SERVICES);
		return;
	}
	fprintf(f, "# $Id: trigger.c,v 1.21 2001/03/15 08:33:05 dugsong Exp $\n"
		"#\n# Network services, dsniff style\n#\n");
	
	for (i = 0; i < ip_cnt; i++) {
		fprintf(f, "%s\t\t%d/ip\n", ip_triggers[i].decode->dc_name,
			ip_triggers[i].num);
	}
	for (i = 0; i < udp_cnt; i++) {
		fprintf(f, "%s\t\t%d/udp\n", udp_triggers[i].decode->dc_name,
			udp_triggers[i].num);
	}
	for (i = 0; i < tcp_cnt; i++) {
		fprintf(f, "%s\t\t%d/tcp\n", tcp_triggers[i].decode->dc_name,
			tcp_triggers[i].num);
	}
	for (i = 0; i < rpc_cnt; i++) {
		fprintf(f, "%s\t\t%d/rpc\n", rpc_triggers[i].decode->dc_name,
			rpc_triggers[i].num);
	}
	fclose(f);
}

static int
dc_call(struct decode *dc, u_char *buf, int len, u_char *obuf, int olen) {
	dc_meta.is_hot = 0;
	dc_meta.crc = crc32_update(NULL, 0, CRC32_INITIAL);
	return dc->dc_func(buf, len, obuf, olen);
}

void
trigger_ip(struct libnet_ipv4_hdr *ip)
{
	struct trigger *t, tr;
	u_char *buf;
	int len;

	tr.num = ip->ip_p;
	
	t = (struct trigger *) bsearch(&tr, &ip_triggers, ip_cnt,
				       sizeof(tr), trigger_compare);

	if (t == NULL)
		return;
	
	buf = (u_char *)ip + (ip->ip_hl * 4);
	len = ntohs(ip->ip_len) - (ip->ip_hl * 4);
	
	if (Opt_debug)
		warnx("trigger_ip: decoding proto %d as %s",
		      tr.num, t->decode->dc_name);
	
	if ((len = dc_call(t->decode, buf, len, obuf, sizeof(obuf))) > 0) {
		record(ip->ip_src.s_addr, ip->ip_dst.s_addr, ip->ip_p,
		       0, 0, t->decode->dc_name, obuf, len);
	}		
}

/* libnids needs a nids_register_udp()... */
void
trigger_udp(struct libnet_ipv4_hdr *ip)
{
	struct trigger *t, tr;
	struct libnet_udp_hdr *udp;
	u_char *buf;
	int len, ip_hl = ip->ip_hl * 4;
	
	len = ntohs(ip->ip_len) - ip_hl;
	
	if (ip->ip_p != IPPROTO_UDP || len < sizeof(*udp))
		return;

	buf = (u_char *)ip + ip_hl;
	udp = (struct libnet_udp_hdr *)buf;
	
	if (len != ntohs(udp->uh_ulen))
		return;
	
	buf += sizeof(*udp);
	len -= sizeof(*udp);
	
	tr.num = ntohs(udp->uh_dport);
	t = (struct trigger *) bsearch(&tr, &udp_triggers, udp_cnt,
				       sizeof(tr), trigger_compare);
	if (t == NULL) {
		tr.num = 0 - (int) ntohs(udp->uh_sport);
		t = (struct trigger *) bsearch(&tr, &udp_triggers, udp_cnt,
					       sizeof(tr), trigger_compare);
		if (t == NULL && Opt_magic)
			t = trigger_set_magic(IPPROTO_UDP,
					      ntohs(udp->uh_dport), buf, len);
		if (t == NULL)
			return;
	}
	if (Opt_debug)
		warnx("trigger_udp: decoding port %d as %s",
		      tr.num, t->decode->dc_name);
	
	if ((len = dc_call(t->decode, buf, len, obuf, sizeof(obuf))) > 0) {
		record(ip->ip_src.s_addr, ip->ip_dst.s_addr, IPPROTO_UDP,
		       ntohs(udp->uh_sport), ntohs(udp->uh_dport),
		       t->decode->dc_name, obuf, len);
	}
}


static void
trigger_tcp_half(struct tuple4 *addr, struct half_stream *hs,
		 struct trigger *t)
{
	u_char *buf;
	int len;

	buf = hs->data;
	len = hs->count - hs->offset;

	if (len == 0 || buf == NULL)
		return;
	
	if (hs->bufsize > len)
		buf[len] = '\0';

	if (t == NULL && Opt_magic)
		t = trigger_set_magic(IPPROTO_TCP, addr->dest, buf, len);

	if (t != NULL) {
		if (Opt_debug)
			warnx("trigger_tcp: decoding port %d as %s",
			      addr->dest, t->decode->dc_name);
		
		if ((len = dc_call(t->decode, buf, len, obuf, sizeof(obuf))) > 0) {
			record(addr->saddr, addr->daddr, IPPROTO_TCP,
			       addr->source, addr->dest, t->decode->dc_name,
			       obuf, len);
		}
	}
	hs->collect = 0;
}


static void
trigger_tcp_full(struct tuple4 *addr, struct half_stream *hs, struct half_stream *buddy /* S>C */, struct trigger *t) {
	dc_meta.rbuf = NULL;
	dc_meta.rlen = buddy->count - buddy->offset;
	if (dc_meta.rlen > 0)
		dc_meta.rbuf = buddy->data;

	trigger_tcp_half(addr, hs, t);
	dc_meta.rbuf = NULL;
	buddy->collect = hs->collect;
}

void
trigger_tcp(struct tcp_stream *ts, void **conn_save)
{
	struct trigger *ct, *st, tr;
	int is_keep_data = 1;
	
	tr.num = ts->addr.dest;
	ct = (struct trigger *) bsearch(&tr, &tcp_triggers, tcp_cnt,
					sizeof(tr), trigger_compare);
	
	tr.num = 0 - (int) ts->addr.dest;
	st = (struct trigger *) bsearch(&tr, &tcp_triggers, tcp_cnt,
					sizeof(tr), trigger_compare);
	
	switch (ts->nids_state) {
		
	case NIDS_JUST_EST:
		// Always collect from both sides because HTTP-response
		// may contain "Location" redirect to https, which is HOTHOTHOT.
		ts->server.collect = 1;
		ts->client.collect = 1;
		// if (Opt_verbose) {
		// 	// Collect SSH banner from both sides.
		// 	ts->server.collect = 1;
		// 	ts->client.collect = 1;
		// }
		// if (ct != NULL || Opt_magic) {
		// 	ts->server.collect = 1;
		// }
		// if (st != NULL) {
		// 	ts->client.collect = 1;
		// }
		break;
	
	case NIDS_DATA:
		if (Opt_debug) {
			if (ts->client.count_new > 0)
				fprintf(stderr, "BY SERVER: ");
			else
				fprintf(stderr, "by client: ");
			fprintf(stderr, "%d/%d ct=%p (C>S=%d) st=%p (ByServer=%d)\n", ts->server.collect, ts->client.collect, ct, ts->server.count_new, st, ts->client.count_new);
		}
		if ((ct != NULL || Opt_magic) && ts->server.count_new) {
			if (ts->server.count - ts->server.offset >= Opt_snaplen) {
				trigger_tcp_full(&ts->addr, &ts->server, &ts->client, ct);
				is_keep_data = 0;
			}
		} else if (st != NULL && ts->client.count_new) {
			if (ts->client.count - ts->client.offset >= Opt_snaplen) {
				trigger_tcp_full(&ts->addr, &ts->client, &ts->server, st);
				is_keep_data = 0;
			}
		}
		if (is_keep_data) {
			nids_discard(ts, 0 /* Discard 0 bytes */);

			if ((ts->server.count_new > 0) &&  (ts->server.count - ts->server.offset) >= Opt_snaplen) {
				// fprintf(stderr, "STOPPING C>S collection\n");
				ts->server.collect = 0;
			}
			if ((ts->client.count_new > 0) &&  (ts->client.count - ts->client.offset) >= Opt_snaplen) {
				// fprintf(stderr, "STOPPING ByServer collection\n");
				ts->client.collect = 0;
			}
		}

		break;
		
	default:
		if (Opt_debug)
			warnx("CLOSE");
		if ((ct != NULL || Opt_magic) && ts->server.count > 0) {
			trigger_tcp_full(&ts->addr, &ts->server, &ts->client, ct);
		} else if (st != NULL && ts->client.count > 0) {
			trigger_tcp_full(&ts->addr, &ts->client, &ts->server, st);
		}
		break;
	}
}

void
trigger_tcp_raw(struct libnet_ipv4_hdr *ip)
{
	struct trigger *t, tr;
	struct libnet_tcp_hdr *tcp;
	struct iovec *iov;
	int len, ip_hl = ip->ip_hl * 4;
	
	len = ntohs(ip->ip_len) - ip_hl;
	if (ip->ip_p != IPPROTO_TCP || len < sizeof(*tcp))
		return;

	tcp = (struct libnet_tcp_hdr *)((u_char *)ip + ip_hl);
	
	tr.num = ntohs(tcp->th_dport);
	// fprintf(stderr, "Packet len=%d port=%d\n", len, tr.num);
	
	t = (struct trigger *) bsearch(&tr, &tcp_triggers, tcp_cnt,
				       sizeof(tr), trigger_compare);
	if (t == NULL) {
		tr.num = 0 - (int) ntohs(tcp->th_sport);
		t = (struct trigger *) bsearch(&tr, &tcp_triggers, tcp_cnt,
					       sizeof(tr), trigger_compare);
		if (t == NULL && !Opt_magic)
			return;
	}
	if ((iov = tcp_raw_input(ip, tcp, len)) == NULL)
		return;

	if (t == NULL && Opt_magic)
		t = trigger_set_magic(IPPROTO_TCP, ntohs(tcp->th_dport),
				      iov->iov_base, iov->iov_len);

	if (t != NULL) {
		if (Opt_debug)
			warnx("trigger_tcp_raw: decoding port %d as %s",
			      tr.num, t->decode->dc_name);
		
		len = dc_call(t->decode, iov->iov_base, iov->iov_len, obuf, sizeof(obuf));
		
		if (len > 0) {
			record(ip->ip_src.s_addr, ip->ip_dst.s_addr,
			       IPPROTO_TCP, ntohs(tcp->th_sport),
			       ntohs(tcp->th_dport), t->decode->dc_name,
			       obuf, len);
		}
	}
	free(iov->iov_base);
	free(iov);
}

static void
trigger_tcp_raw_callback(in_addr_t src, in_addr_t dst,
			 u_short sport, u_short dport,
			 u_char *buf, int len)
{
	struct trigger *t, tr;

	tr.num = dport;

	t = (struct trigger *) bsearch(&tr, &tcp_triggers, tcp_cnt,
				       sizeof(tr), trigger_compare);
	if (t == NULL && Opt_magic)
		t = trigger_set_magic(IPPROTO_TCP, dport, buf, len);

	if (t != NULL) {
		if (Opt_debug)
			warnx("trigger_tcp_raw_timeout: "
			      "decoding port %d as %s",
			      tr.num, t->decode->dc_name);
		
		if ((len = dc_call(t->decode, buf, len, obuf, sizeof(obuf))) > 0) {
			record(src, dst, IPPROTO_TCP, sport, dport,
			       t->decode->dc_name, obuf, len);
		}
	}
}

void
trigger_tcp_raw_timeout(int signal)
{
	tcp_raw_timeout(TRIGGER_TCP_RAW_TIMEOUT, trigger_tcp_raw_callback);
	alarm(TRIGGER_TCP_RAW_TIMEOUT);
}

void
trigger_rpc(int program, int proto, int port)
{
	struct trigger *t, tr;

	tr.num = program;

	t = (struct trigger *) bsearch(&tr, &rpc_triggers, rpc_cnt,
				       sizeof(tr), trigger_compare);
	if (t == NULL)
		return;

	if (proto == IPPROTO_UDP) {
		trigger_set_udp(port, t->decode->dc_name);
	}
	else if (proto == IPPROTO_TCP) {
		trigger_set_tcp(port, t->decode->dc_name);
	}
}
	
void
trigger_init_magic(char *filename)
{
	magic_init(filename);
}

void
trigger_init_list(char *list)
{
	char *name, *port, *proto = NULL;

	while ((name = strsep(&list, ",")) != NULL) {
		if ((port = strsep(&name, "/")) == NULL ||
		    (proto = strsep(&name, "=")) == NULL) {
			errx(1, "trigger_init_list: parse error");
		}
		trigger_set(proto, atoi(port), name);
	}
}

static int
trigger_init_services_file(char *services)
{
	FILE *f;
	char *name, *port, *proto, line[1024];
	char *fn;

	fn = services;
	if (fn == NULL)
		fn = DSNIFF_LIBDIR DSNIFF_SERVICES;

	if ((f = fopen(fn, "r")) == NULL) {
		if (services == NULL)
			return 1; // Continue;
		errx(1, "couldn't open %s", services);
	}
	
	while (fgets(line, sizeof(line), f) != NULL) {
		if (line[0] == '#' || line[0] == '\n')
			continue;
		
		if ((name = strtok(line, " \t")) == NULL ||
		    (port = strtok(NULL, " \t/")) == NULL ||
		    (proto = strtok(NULL, " \t#\n")) == NULL) {
			continue;
		}
		trigger_set(proto, atoi(port), name);
	}
	fclose(f);
	return 0;
}

void
trigger_init_services(char *services)
{
	if (trigger_init_services_file(services) == 0)
		return;

	for (int i = 0; i < sizeof dsx / sizeof *dsx; i++) {
		trigger_set(dsx[i].proto, dsx[i].port, dsx[i].name);
	}
}

