#define __USE_GNU
#include "../module_iface.h"
#include <string.h>
#include <stdlib.h>
#include <netdb.h>
#include <stdio.h>
#include <netinet/in.h>
#include <limits.h>

#define MODULE_TYPE MODULE_MATCH
#define MODULE_DATATYPE struct ipt_tcp
#define MODULE_NAME "tcp"

#if MODULE_TYPE == MODULE_TARGET
#  define MODULE_ENTRYTYPE struct ipt_entry_match
#else 
#  if MODULE_TYPE == MODULE_MATCH
#    define MODULE_ENTRYTYPE struct ipt_entry_target
#  else
#    error MODULE_TYPE is unknown!
#  endif
#endif

typedef struct {
	char *name;
	unsigned int value;
} FlagList;

FlagList tcp_flags[] = {
	{"FIN", 1 << 0},
	{"SYN", 1 << 1},
	{"RST", 1 << 2},
	{"PSH", 1 << 3},
	{"ACK", 1 << 4},
	{"URG", 1 << 5},
	{"ALL", 0x3f},
	{"NONE", 0},
};

static void setup(void *myinfo, unsigned int *nfcache) {
	MODULE_DATATYPE *info = (void *)((MODULE_ENTRYTYPE *)myinfo)->data;

	info->spts[0] = info->dpts[0] = 0;
	info->spts[1] = info->dpts[1] = USHRT_MAX;
}

static bool parse_ports_sv(SV *portval, u_int16_t *ports, int *inv,
				const char *protocol_name) {
	struct servent *service;
	int val;
	
	*inv = FALSE;
	if(SvIOK(portval)) {
		val = SvIV(portval);
		if(val < 0 || val > USHRT_MAX) {
			SET_ERRSTR("Port number out of range");
			return(FALSE);
		}
		ports[0] = ports[1] = val;
	}
	else if(SvPOK(portval)) {
		STRLEN len;
		char *temp = SvPV(portval, len);
		char *text = NULL, *sep = NULL, *base = NULL;

		base = text = malloc(len + 1);
		strncpy(text, temp, len);
		text[len] = '\0';

		if(*text == INVCHAR) {
			*inv = TRUE;
			text++;
		}
		sep = strchr(text, ':');
		if(sep == text)
			ports[0] = 0;
		else {
			if(sep)
				*sep = '\0';
			if((service = getservbyname(text, protocol_name)))
				ports[0] = ntohs(service->s_port);
			else {
				val = strtoul(text, &temp, 10);
				if((text + strlen(text) > temp) || val < 0 || val > USHRT_MAX) {
					SET_ERRSTR("Unable to parse '%s'", text);
					free(base);
					return(FALSE);
				}
				ports[0] = val;
			}
			if(temp)
				*temp = ':';
		}

		if(sep) {
			text = ++sep;
			if(*text == '\0')
				ports[1] = USHRT_MAX;
			else {
				if((service = getservbyname(text, protocol_name)))
					ports[1] = ntohs(service->s_port);
				else {
					val = strtoul(text, &temp, 10);
					if((text + strlen(text) > temp) || val < 0 ||
							val > USHRT_MAX) {
						SET_ERRSTR("Unable to parse '%s'", text);
						free(base);
						return(FALSE);
					}
					ports[1] = val;
				}
			}
		}
		else
			ports[1] = ports[0];

		free(base);
	}
	else {
		SET_ERRSTR("Must be passed as integer or string");
		return(FALSE);
	}
	return(TRUE);
}

static bool assemble_mask_from_arrayref(SV *arrayref, FlagList *flags,
		int nflags, u_int8_t *mask) {
	SV **svp, *sv;
	char *temp, *str;
	int i, j;
	STRLEN len;
	
	if(!SvROK(arrayref) || !(sv = SvRV(arrayref)) || SvTYPE(sv) != SVt_PVAV)
		return(FALSE);
	for(i = 0; i <= av_len((AV *)sv); i++) {
		svp = av_fetch((AV *)sv, i, FALSE);
		if(!svp || !SvPOK(*svp))
			return(FALSE);
		temp = SvPV(*svp, len);
		str = malloc(len + 1);
		strncpy(str, temp, len);
		str[len] = '\0';
		for(j = 0; j < nflags; j++) {
			if(!strcmp(str, flags[j].name)) {
				*mask |= flags[j].value;
				break;
			}
		}
		free(str);
		if(j == nflags)
			return(FALSE);
	}
	return(TRUE);
}

static int parse_field(char *field, SV *value, void *myinfo,
		unsigned int *nfcache, struct ipt_entry *entry, int *flags) {
	MODULE_DATATYPE *info = (void *)(*(MODULE_ENTRYTYPE **)myinfo)->data;
	char *temp, *text, *base;
	SV **svp;
	int val, inv;
	STRLEN len;

	if(!strcmp(field, "source-port")) {
		if(!parse_ports_sv(value, info->spts, &inv, "tcp")) {
			char *temp = strdup(SvPV_nolen(ERROR_SV));
			SET_ERRSTR("%s: %s", field, temp);
			free(temp);
			return(FALSE);
		}

		if(inv)
			info->invflags |= IPT_TCP_INV_SRCPT;

		*nfcache |= NFC_IP_SRC_PT;
		return(TRUE);
	}
	else if(!strcmp(field, "destination-port")) {
		if(!parse_ports_sv(value, info->dpts, &inv, "tcp")) {
			char *temp = strdup(SvPV_nolen(ERROR_SV));
			SET_ERRSTR("%s: %s", field, temp);
			free(temp);
			return(FALSE);
		}

		if(inv)
			info->invflags |= IPT_TCP_INV_DSTPT;

		*nfcache |= NFC_IP_DST_PT;
		return(TRUE);
	}
	else if(!strcmp(field, "tcp-flags")) {
		if(!SvROK(value) || !(value = SvRV(value)) ||
				SvTYPE(value) != SVt_PVHV) {
			SET_ERRSTR("%s: Must be passed as a hash ref", field);
			return(FALSE);
		}
		if(hv_fetch((HV *)value, "inv", 3, FALSE))
			info->invflags |= IPT_TCP_INV_FLAGS;
		if(!(svp = hv_fetch((HV *)value, "mask", 4, FALSE)) ||
				!assemble_mask_from_arrayref(*svp, tcp_flags,
					sizeof(tcp_flags) / sizeof(FlagList),
					&info->flg_mask)) {
			SET_ERRSTR("%s: Unable to parse mask flags", field);
			return(FALSE);
		}
		if(!(svp = hv_fetch((HV *)value, "comp", 4, FALSE)) ||
				!assemble_mask_from_arrayref(*svp, tcp_flags,
					sizeof(tcp_flags) / sizeof(FlagList),
					&info->flg_cmp)) {
			SET_ERRSTR("%s: Unable to parse compare flags", field);
			return(FALSE);
		}
		*nfcache |= NFC_IP_TCPFLAGS;
		return(TRUE);
	}
	else if(!strcmp(field, "tcp-option")) {
		if(SvIOK(value))
			val = SvIV(value);
		else if(SvPOK(value)) {
			temp = SvPV(value, len);
			base = text = malloc(len + 1);
			strncpy(text, temp, len);
			text[len] = '\0';
			if(*text == INVCHAR) {
				info->invflags |= IPT_TCP_INV_OPTION;
				text++;
			}
			temp = NULL;
			val = strtoul(text, &temp, 10);
			if((temp - text) < strlen(text)) {
				SET_ERRSTR("%s: Unable to parse option number", field);
				free(base);
				return(FALSE);
			}
			free(base);
		}
		else {
			SET_ERRSTR("%s: Must be passed as integer or string", field);
			return(FALSE);
		}

		if(val < 0 || val > 0xFF) {
			SET_ERRSTR("%s: Value out of range", field);
			return(FALSE);
		}
		info->option = val;
		*nfcache |= NFC_IP_PROTO_UNKNOWN;
		return(TRUE);
	}
	return(FALSE);
}

static SV *build_flag_list_from_mask(u_int8_t mask, FlagList *flags,
		int nflags) {
	int i;
	AV *av = newAV();
	
	for(i = 0; i < nflags; i++) {
		if(flags[i].value & mask)
			av_push(av, newSVpv(flags[i].name, 0));
	}
	return((SV *)newRV((SV *)av));
}

static SV *build_sv_from_portrange(u_int16_t *ports, bool inv) {
	char *temp, *temp2;
	struct servent *service;
	SV *sv;
	
	if((service = getservbyport(htons(ports[0]), "tcp")))
		temp = strdup(service->s_name);
	else {
		if(ports[0] == ports[1] && !inv)
			return(newSViv(ports[0]));	
		asprintf(&temp, "%u", ports[0]);
	}
	if(ports[0] != ports[1]) {
		if((service = getservbyport(htons(ports[1]), "tcp")))
			asprintf(&temp2, "%s:%s", temp, service->s_name);
		else
			asprintf(&temp2, "%s:%u", temp, ports[1]);
		free(temp);
		temp = temp2;
	}
	if(inv) {
		asprintf(&temp2, "%c%s", INVCHAR, temp);
		free(temp);
		temp = temp2;
	}
	sv = newSVpv(temp, 0);
	free(temp);
	return(sv);
}

static void get_fields(HV *ent_hash, void *myinfo, struct ipt_entry *entry) {
	MODULE_DATATYPE *info = (void *)((MODULE_ENTRYTYPE *)myinfo)->data;
	char *temp;
	SV *sv;

	if(info->spts[0] != 0 || info->spts[1] != USHRT_MAX) {
		sv = build_sv_from_portrange(info->spts,
				info->invflags & IPT_TCP_INV_SRCPT);
		hv_store(ent_hash, "source-port", 11, sv, 0);
	}

	if(info->dpts[0] != 0 || info->dpts[1] != USHRT_MAX) {
		sv = build_sv_from_portrange(info->dpts,
				info->invflags & IPT_TCP_INV_DSTPT);
		hv_store(ent_hash, "destination-port", 16, sv, 0);
	}
	if(info->flg_mask || info->flg_cmp) {
		HV *hv = newHV();
		if(info->flg_mask)
			hv_store(hv, "mask", 4, build_flag_list_from_mask(info->flg_mask,
									tcp_flags, (sizeof(tcp_flags) /
											sizeof(FlagList)) - 2), 0);
		if(info->flg_cmp)
			hv_store(hv, "comp", 4, build_flag_list_from_mask(info->flg_cmp,
									tcp_flags, (sizeof(tcp_flags) /
											sizeof(FlagList)) - 2), 0);
		if(info->invflags & IPT_TCP_INV_FLAGS)
			hv_store(hv, "inv", 3, newSViv(1), 0);
		hv_store(ent_hash, "tcp-flags", 9, newRV((SV *)hv), 0);
	}
	if(info->option) {
		if(info->invflags & IPT_TCP_INV_OPTION) {
			asprintf(&temp, "%c%u", INVCHAR, info->option);
			sv = newSVpv(temp, 0);
			free(temp);
		}
		else
			sv = newSViv(info->option);
		hv_store(ent_hash, "tcp-option", 10, sv, 0);
	}
}

static ModuleDef tcp_module = {
	NULL, /* always NULL */
	MODULE_TYPE,
	MODULE_NAME,
	IPT_ALIGN(sizeof(MODULE_DATATYPE)),
	IPT_ALIGN(sizeof(MODULE_DATATYPE)),
	setup,
	parse_field,
	get_fields,
	NULL /* final_check */
};

ModuleDef *init(void) {
	return(&tcp_module);
}
/* vim: ts=4
 */
