/* ebt_string
 *
 * Author:
 * Bernie Harris <bernie.harris@alliedtelesis.co.nz>
 *
 * February, 2018
 *
 * Based on:
 *  libxt_string.c, Copyright (C) 2000 Emmanuel Roger  <winfield@freegates.be>
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <getopt.h>
#include <netdb.h>
#include <ctype.h>
#include "../include/ebtables_u.h"
#include <linux/if_packet.h>
#include <linux/netfilter/xt_string.h>

#define STRING_FROM  '1'
#define STRING_TO    '2'
#define STRING_ALGO  '3'
#define STRING_ICASE '4'
#define STRING       '5'
#define STRING_HEX   '6'
#define OPT_STRING_FROM  (1 << 0)
#define OPT_STRING_TO    (1 << 1)
#define OPT_STRING_ALGO  (1 << 2)
#define OPT_STRING_ICASE (1 << 3)
#define OPT_STRING       (1 << 4)
#define OPT_STRING_HEX   (1 << 5)

static const struct option opts[] =
{
	{ "string-from"             , required_argument, 0, STRING_FROM },
	{ "string-to"               , required_argument, 0, STRING_TO },
	{ "string-algo"             , required_argument, 0, STRING_ALGO },
	{ "string-icase"            , no_argument,       0, STRING_ICASE },
	{ "string"                  , required_argument, 0, STRING },
	{ "string-hex"              , required_argument, 0, STRING_HEX },
	{ 0 }
};

static void print_help()
{
	printf(
"string options:\n"
"--string-from offset    : Offset to start searching from (default: 0)\n"
"--string-to   offset    : Offset to stop searching (default: packet size)\n"
"--string-algo algorithm : Algorithm (bm = Boyer-Moore, kmp = Knuth-Pratt-Morris)\n"
"--string-icase          : Ignore case when searching\n"
"--string     [!] string : Match a string in a packet\n"
"--string-hex [!] string : Match a hex string in a packet, e.g. |0D 0A|, |0D0A|, netfilter|03|org\n");
}

static void init(struct ebt_entry_match *match)
{
	struct xt_string_info *info = (struct xt_string_info *)match->data;

	info->to_offset = UINT16_MAX;
}

static void parse_string(const char *s, struct xt_string_info *info)
{
	/* xt_string does not need \0 at the end of the pattern */
	if (strlen(s) <= XT_STRING_MAX_PATTERN_SIZE) {
		strncpy(info->pattern, s, XT_STRING_MAX_PATTERN_SIZE);
		info->patlen = strnlen(s, XT_STRING_MAX_PATTERN_SIZE);
		return;
	}
	ebt_print_error3("STRING too long \"%s\"", s);
}

static void parse_hex_string(const char *s, struct xt_string_info *info)
{
	int i=0, slen, sindex=0, schar;
	short hex_f = 0, literal_f = 0;
	char hextmp[3];

	slen = strlen(s);

	if (slen == 0) {
		ebt_print_error3("STRING must contain at least one char");
	}

	while (i < slen) {
		if (s[i] == '\\' && !hex_f) {
			literal_f = 1;
		} else if (s[i] == '\\') {
			ebt_print_error3("Cannot include literals in hex data");
		} else if (s[i] == '|') {
			if (hex_f)
				hex_f = 0;
			else {
				hex_f = 1;
				/* get past any initial whitespace just after the '|' */
				while (s[i+1] == ' ')
					i++;
			}
			if (i+1 >= slen)
				break;
			else
				i++;  /* advance to the next character */
		}

		if (literal_f) {
			if (i+1 >= slen) {
				ebt_print_error3("Bad literal placement at end of string");
			}
			info->pattern[sindex] = s[i+1];
			i += 2;  /* skip over literal char */
			literal_f = 0;
		} else if (hex_f) {
			if (i+1 >= slen) {
				ebt_print_error3("Odd number of hex digits");
			}
			if (i+2 >= slen) {
				/* must end with a "|" */
				ebt_print_error3("Invalid hex block");
			}
			if (! isxdigit(s[i])) /* check for valid hex char */
				ebt_print_error3("Invalid hex char '%c'", s[i]);
			if (! isxdigit(s[i+1])) /* check for valid hex char */
				ebt_print_error3("Invalid hex char '%c'", s[i+1]);
			hextmp[0] = s[i];
			hextmp[1] = s[i+1];
			hextmp[2] = '\0';
			if (! sscanf(hextmp, "%x", &schar))
				ebt_print_error3("Invalid hex char `%c'", s[i]);
			info->pattern[sindex] = (char) schar;
			if (s[i+2] == ' ')
				i += 3;  /* spaces included in the hex block */
			else
				i += 2;
		} else {  /* the char is not part of hex data, so just copy */
			info->pattern[sindex] = s[i];
			i++;
		}
		if (sindex > XT_STRING_MAX_PATTERN_SIZE)
			ebt_print_error3("STRING too long \"%s\"", s);
		sindex++;
	}
	info->patlen = sindex;
}

static int parse(int c, char **argv, int argc, const struct ebt_u_entry *entry,
		 unsigned int *flags, struct ebt_entry_match **match)
{
	struct xt_string_info *info = (struct xt_string_info *)(*match)->data;

	switch (c) {
	case STRING_FROM:
		ebt_check_option2(flags, OPT_STRING_FROM);
		if (ebt_check_inverse2(optarg))
			ebt_print_error2("Unexpected `!' after --string-from");
		info->from_offset = (__u16)strtoul(optarg, NULL, 10);
		break;
	case STRING_TO:
		ebt_check_option2(flags, OPT_STRING_TO);
		if (ebt_check_inverse2(optarg))
			ebt_print_error2("Unexpected `!' after --string-to");
		info->to_offset = (__u16)strtoul(optarg, NULL, 10);
		break;
	case STRING_ALGO:
		ebt_check_option2(flags, OPT_STRING_ALGO);
		if (ebt_check_inverse2(optarg))
			ebt_print_error2("Unexpected `!' after --string-algo");
		if (snprintf(info->algo, sizeof(info->algo), "%s", optarg) >=
				sizeof(info->algo))
			ebt_print_error2("\"%s\" is truncated", info->algo);
		break;
	case STRING_ICASE:
		ebt_check_option2(flags, OPT_STRING_ICASE);
		if (ebt_check_inverse2(optarg))
			ebt_print_error2("Unexpected `!' after --string-icase");
		info->u.v1.flags |= XT_STRING_FLAG_IGNORECASE;
		break;
	case STRING:
		ebt_check_option2(flags, OPT_STRING);
		parse_string(optarg, info);
		if (ebt_check_inverse2(optarg)) {
			info->u.v1.flags |= XT_STRING_FLAG_INVERT;
		}
		break;
	case STRING_HEX:
		ebt_check_option2(flags, OPT_STRING_HEX);
		parse_hex_string(optarg, info);
		if (ebt_check_inverse2(optarg)) {
			info->u.v1.flags |= XT_STRING_FLAG_INVERT;
		}
		break;
	default:
		return 0;
	}
	return 1;
}

static void final_check(const struct ebt_u_entry *entry,
			const struct ebt_entry_match *match, const char *name,
			unsigned int hookmask, unsigned int time)
{
	struct xt_string_info *info = (struct xt_string_info *)match->data;

	if (info->to_offset < info->from_offset) {
		ebt_print_error3("'to' offset should not be less than 'from' "
				 "offset");
	}
}

/* Test to see if the string contains non-printable chars or quotes */
static unsigned short int is_hex_string(const char *str,
					const unsigned short int len)
{
	unsigned int i;
	for (i=0; i < len; i++) {
		if (! isprint(str[i])) {
			/* string contains at least one non-printable char */
			return 1;
		}
	}
	/* use hex output if the last char is a "\" */
	if (str[len-1] == '\\')
		return 1;
	return 0;
}

/* Print string with "|" chars included as one would pass to --string-hex */
static void print_hex_string(const char *str, const unsigned short int len)
{
	unsigned int i;
	/* start hex block */
	printf("\"|");
	for (i=0; i < len; i++)
		printf("%02x", (unsigned char)str[i]);
	/* close hex block */
	printf("|\" ");
}

static void print_string(const char *str, const unsigned short int len)
{
	unsigned int i;
	printf("\"");
	for (i=0; i < len; i++) {
		if (str[i] == '\"' || str[i] == '\\')
			putchar('\\');
		printf("%c", (unsigned char) str[i]);
	}
	printf("\" ");  /* closing quote */
}

static void print(const struct ebt_u_entry *entry,
		  const struct ebt_entry_match *match)
{
	const struct xt_string_info *info =
		(const struct xt_string_info *) match->data;
	int invert = info->u.v1.flags & XT_STRING_FLAG_INVERT;

	if (is_hex_string(info->pattern, info->patlen)) {
		printf("--string-hex %s", invert ? "! " : "");
		print_hex_string(info->pattern, info->patlen);
	} else {
		printf("--string %s", invert ? "! " : "");
		print_string(info->pattern, info->patlen);
	}
	printf("--string-algo %s ", info->algo);
	if (info->from_offset != 0)
		printf("--string-from %u ", info->from_offset);
	if (info->to_offset != 0)
		printf("--string-to %u ", info->to_offset);
	if (info->u.v1.flags & XT_STRING_FLAG_IGNORECASE)
		printf("--string-icase ");
}

static int compare(const struct ebt_entry_match *m1,
		   const struct ebt_entry_match *m2)
{
	const struct xt_string_info *info1 =
		(const struct xt_string_info *) m1->data;
	const struct xt_string_info *info2 =
		(const struct xt_string_info *) m2->data;

	if (info1->from_offset != info2->from_offset)
		return 0;
	if (info1->to_offset != info2->to_offset)
		return 0;
	if (info1->u.v1.flags != info2->u.v1.flags)
		return 0;
	if (info1->patlen != info2->patlen)
		return 0;
	if (strncmp (info1->algo, info2->algo, XT_STRING_MAX_ALGO_NAME_SIZE) != 0)
		return 0;
	if (strncmp (info1->pattern, info2->pattern, info1->patlen) != 0)
		return 0;

	return 1;
}

static struct ebt_u_match string_match =
{
	.name		= "string",
	.revision	= 1,
	.size		= sizeof(struct xt_string_info),
	.help		= print_help,
	.init		= init,
	.parse		= parse,
	.final_check	= final_check,
	.print		= print,
	.compare	= compare,
	.extra_ops	= opts,
};

void _init(void)
{
	ebt_register_match(&string_match);
}
