/*
 * (C) 2010-2012 by Pablo Neira Ayuso <pablo@netfilter.org>
 *
 * Based on: kernel-space FTP extension for connection tracking.
 *
 * This port has been sponsored by Vyatta Inc. <http://www.vyatta.com>
 *
 * Original copyright notice:
 *
 * (C) 1999-2001 Paul `Rusty' Russell
 * (C) 2002-2004 Netfilter Core Team <coreteam@netfilter.org>
 * (C) 2003,2004 USAGI/WIDE Project <http://www.linux-ipv6.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#include "conntrackd.h"
#include "network.h"	/* for before and after */
#include "helper.h"
#include "myct.h"
#include "log.h"

#include <ctype.h>	/* for isdigit */
#include <errno.h>

#define _GNU_SOURCE
#include <netinet/tcp.h>

#include <libmnl/libmnl.h>
#include <libnetfilter_conntrack/libnetfilter_conntrack.h>
#include <linux/netfilter/nfnetlink_queue.h>
#include <libnetfilter_queue/libnetfilter_queue.h>
#include <libnetfilter_queue/libnetfilter_queue_tcp.h>
#include <libnetfilter_queue/pktbuff.h>
#include <linux/netfilter.h>

#include "helpers/ftp.h"

static bool loose; /* XXX: export this as config option. */

enum nf_ct_ftp_type {
	/* PORT command from client */
	NF_CT_FTP_PORT,
	/* PASV response from server */
	NF_CT_FTP_PASV,
	/* EPRT command from client */
	NF_CT_FTP_EPRT,
	/* EPSV response from server */
	NF_CT_FTP_EPSV,
};

static int
get_ipv6_addr(const char *src, size_t dlen, struct in6_addr *dst, uint8_t term)
{
	const char *end;
	int ret = in6_pton(src, min_t(size_t, dlen, 0xffff),
				(uint8_t *)dst, term, &end);
	if (ret > 0)
		return (int)(end - src);
	return 0;
}

static int try_number(const char *data, size_t dlen, uint32_t array[],
		      int array_size, char sep, char term)
{
	uint32_t len;
	int i;

	memset(array, 0, sizeof(array[0])*array_size);

	/* Keep data pointing at next char. */
	for (i = 0, len = 0; len < dlen && i < array_size; len++, data++) {
		if (*data >= '0' && *data <= '9') {
			array[i] = array[i]*10 + *data - '0';
		}
		else if (*data == sep)
			i++;
		else {
			/* Unexpected character; true if it's the
			   terminator and we're finished. */
			if (*data == term && i == array_size - 1)
				return len;
			pr_debug("Char %u (got %u nums) `%u' unexpected\n",
				 len, i, *data);
			return 0;
		}
	}
	pr_debug("Failed to fill %u numbers separated by %c\n",
		 array_size, sep);
	return 0;
}

/* Grab port: number up to delimiter */
static int get_port(const char *data, int start, size_t dlen, char delim,
		    struct myct_man *cmd)
{
	uint16_t tmp_port = 0;
	uint32_t i;

	for (i = start; i < dlen; i++) {
		/* Finished? */
		if (data[i] == delim) {
			if (tmp_port == 0)
				break;
			cmd->u.port = htons(tmp_port);
			pr_debug("get_port: return %d\n", tmp_port);
			return i + 1;
		}
		else if (data[i] >= '0' && data[i] <= '9')
			tmp_port = tmp_port*10 + data[i] - '0';
		else { /* Some other crap */
			pr_debug("get_port: invalid char.\n");
			break;
		}
	}
	return 0;
}

/* Returns 0, or length of numbers: 192,168,1,1,5,6 */
static int try_rfc959(const char *data, size_t dlen, struct myct_man *cmd,
		      uint16_t l3protonum, char term)
{
	int length;
	uint32_t array[6];

	length = try_number(data, dlen, array, 6, ',', term);
	if (length == 0)
		return 0;

	cmd->u3.ip =  htonl((array[0] << 24) | (array[1] << 16) |
			   (array[2] << 8) | array[3]);
	cmd->u.port = htons((array[4] << 8) | array[5]);
	return length;
}

/* Returns 0, or length of numbers: |1|132.235.1.2|6275| or |2|3ffe::1|6275| */
static int try_eprt(const char *data, size_t dlen,
		    struct myct_man *cmd, uint16_t l3protonum, char term)
{
	char delim;
	int length;

	/* First character is delimiter, then "1" for IPv4 or "2" for IPv6,
	   then delimiter again. */
	if (dlen <= 3) {
		pr_debug("EPRT: too short\n");
		return 0;
	}
	delim = data[0];
	if (isdigit(delim) || delim < 33 || delim > 126 || data[2] != delim) {
		pr_debug("try_eprt: invalid delimitter.\n");
		return 0;
	}

	if ((l3protonum == PF_INET && data[1] != '1') ||
	    (l3protonum == PF_INET6 && data[1] != '2')) {
		pr_debug("EPRT: invalid protocol number.\n");
		return 0;
	}

	pr_debug("EPRT: Got %c%c%c\n", delim, data[1], delim);
	if (data[1] == '1') {
		uint32_t array[4];

		/* Now we have IP address. */
		length = try_number(data + 3, dlen - 3, array, 4, '.', delim);
		if (length != 0)
			cmd->u3.ip = htonl((array[0] << 24) | (array[1] << 16)
					   | (array[2] << 8) | array[3]);
	} else {
		/* Now we have IPv6 address. */
		length = get_ipv6_addr(data + 3, dlen - 3,
				       (struct in6_addr *)cmd->u3.ip6, delim);
	}

	if (length == 0)
		return 0;
	pr_debug("EPRT: Got IP address!\n");
	/* Start offset includes initial "|1|", and trailing delimiter */
	return get_port(data, 3 + length + 1, dlen, delim, cmd);
}

/* Returns 0, or length of numbers: |||6446| */
static int try_epsv_response(const char *data, size_t dlen,
			     struct myct_man *cmd,
			     uint16_t l3protonum, char term)
{
	char delim;

	/* Three delimiters. */
	if (dlen <= 3) return 0;
	delim = data[0];
	if (isdigit(delim) || delim < 33 || delim > 126 ||
	    data[1] != delim || data[2] != delim)
		return 0;

	return get_port(data, 3, dlen, delim, cmd);
}

static struct ftp_search {
	const char *pattern;
	size_t plen;
	char skip;
	char term;
	enum nf_ct_ftp_type ftptype;
	int (*getnum)(const char *, size_t, struct myct_man *, uint16_t, char);
} search[MYCT_DIR_MAX][2] = {
	[MYCT_DIR_ORIG] = {
		{
			.pattern	= "PORT",
			.plen		= sizeof("PORT") - 1,
			.skip		= ' ',
			.term		= '\r',
			.ftptype	= NF_CT_FTP_PORT,
			.getnum		= try_rfc959,
		},
		{
			.pattern	= "EPRT",
			.plen		= sizeof("EPRT") - 1,
			.skip		= ' ',
			.term		= '\r',
			.ftptype	= NF_CT_FTP_EPRT,
			.getnum		= try_eprt,
		},
	},
	[MYCT_DIR_REPL] = {
		{
			.pattern	= "227 ",
			.plen		= sizeof("227 ") - 1,
			.skip		= '(',
			.term		= ')',
			.ftptype	= NF_CT_FTP_PASV,
			.getnum		= try_rfc959,
		},
		{
			.pattern	= "229 ",
			.plen		= sizeof("229 ") - 1,
			.skip		= '(',
			.term		= ')',
			.ftptype	= NF_CT_FTP_EPSV,
			.getnum		= try_epsv_response,
		},
	},
};

static int ftp_find_pattern(struct pkt_buff *pkt,
			    unsigned int dataoff, unsigned int dlen,
			    const char *pattern, size_t plen,
			    char skip, char term,
			    unsigned int *matchoff, unsigned int *matchlen,
			    struct myct_man *cmd,
			    int (*getnum)(const char *, size_t,
					  struct myct_man *cmd,
					  uint16_t, char),
			    int dir)
{
	char *data = (char *)pktb_network_header(pkt) + dataoff;
	int numlen;
	uint32_t i;

	if (dlen == 0)
		return 0;

	/* short packet, skip partial matching. */
	if (dlen <= plen)
		return 0;

	if (strncmp(data, pattern, plen) != 0)
		return 0;

	pr_debug("Pattern matches!\n");

	/* Now we've found the constant string, try to skip
	   to the 'skip' character */
	for (i = plen; data[i] != skip; i++)
		if (i == dlen - 1) return 0;

	/* Skip over the last character */
	i++;

	pr_debug("Skipped up to `%c'!\n", skip);

	numlen = getnum(data + i, dlen - i, cmd, PF_INET, term);
	if (!numlen)
		return 0;

	*matchoff = i;
	*matchlen = numlen;

	pr_debug("Match succeded!\n");
	return 1;
}

/* Look up to see if we're just after a \n. */
static int find_nl_seq(uint32_t seq, struct ftp_info *info, int dir)
{
	int i;

	for (i = 0; i < info->seq_aft_nl_num[dir]; i++)
		if (info->seq_aft_nl[dir][i] == seq)
			return 1;
	return 0;
}

/* We don't update if it's older than what we have. */
static void update_nl_seq(uint32_t nl_seq, struct ftp_info *info, int dir)
{
	int i, oldest;

	/* Look for oldest: if we find exact match, we're done. */
	for (i = 0; i < info->seq_aft_nl_num[dir]; i++) {
		if (info->seq_aft_nl[dir][i] == nl_seq)
			return;
	}

	if (info->seq_aft_nl_num[dir] < NUM_SEQ_TO_REMEMBER) {
		info->seq_aft_nl[dir][info->seq_aft_nl_num[dir]++] = nl_seq;
	} else {
		if (before(info->seq_aft_nl[dir][0], info->seq_aft_nl[dir][1]))
			oldest = 0;
		else
			oldest = 1;

		if (after(nl_seq, info->seq_aft_nl[dir][oldest]))
			info->seq_aft_nl[dir][oldest] = nl_seq;
	}
}

static int nf_nat_ftp_fmt_cmd(enum nf_ct_ftp_type type,
			      char *buffer, size_t buflen,
			      uint32_t addr, uint16_t port)
{
	union {
		unsigned char c[4];
		uint32_t d;
	} tmp;

	tmp.d = addr;
	switch (type) {
	case NF_CT_FTP_PORT:
	case NF_CT_FTP_PASV:
		return snprintf(buffer, buflen, "%u,%u,%u,%u,%u,%u",
				tmp.c[0], tmp.c[1], tmp.c[2], tmp.c[3],
				port >> 8, port & 0xFF);
	case NF_CT_FTP_EPRT:
		return snprintf(buffer, buflen, "|1|%u.%u.%u.%u|%u|",
				tmp.c[0], tmp.c[1], tmp.c[2], tmp.c[3], port);
	case NF_CT_FTP_EPSV:
		return snprintf(buffer, buflen, "|||%u|", port);
	}

	return 0;
}

/* So, this packet has hit the connection tracking matching code.
   Mangle it, and change the expectation to match the new version. */
static unsigned int nf_nat_ftp(struct pkt_buff *pkt,
			       int dir,
			       int ctinfo,
			       enum nf_ct_ftp_type type,
			       unsigned int matchoff,
			       unsigned int matchlen,
			       struct nf_conntrack *ct,
			       struct nf_expect *exp)
{
	union nfct_attr_grp_addr newip;
	uint16_t port;
	char buffer[sizeof("|1|255.255.255.255|65535|")];
	unsigned int buflen;
	const struct nf_conntrack *expected;
	struct nf_conntrack *nat_tuple;
	uint16_t initial_port;

	pr_debug("FTP_NAT: type %i, off %u len %u\n", type, matchoff, matchlen);

	/* Connection will come from wherever this packet goes, hence !dir */
	cthelper_get_addr_dst(ct, !dir, &newip);

	expected = nfexp_get_attr(exp, ATTR_EXP_EXPECTED);

	nat_tuple = nfct_new();
	if (nat_tuple == NULL)
		return NF_ACCEPT;

	initial_port = nfct_get_attr_u16(expected, ATTR_PORT_DST);

	nfexp_set_attr_u32(exp, ATTR_EXP_NAT_DIR, !dir);

	/* libnetfilter_conntrack needs this */
	nfct_set_attr_u8(nat_tuple, ATTR_L3PROTO, AF_INET);
	nfct_set_attr_u32(nat_tuple, ATTR_IPV4_SRC, 0);
	nfct_set_attr_u32(nat_tuple, ATTR_IPV4_DST, 0);
	nfct_set_attr_u8(nat_tuple, ATTR_L4PROTO, IPPROTO_TCP);
	nfct_set_attr_u16(nat_tuple, ATTR_PORT_DST, 0);

	/* When you see the packet, we need to NAT it the same as the
	 * this one. */
	nfexp_set_attr(exp, ATTR_EXP_FN, "nat-follow-master");

	/* Try to get same port: if not, try to change it. */
	for (port = ntohs(initial_port); port != 0; port++) {
		int ret;

		nfct_set_attr_u16(nat_tuple, ATTR_PORT_SRC, htons(port));
		nfexp_set_attr(exp, ATTR_EXP_NAT_TUPLE, nat_tuple);

		ret = cthelper_add_expect(exp);
		if (ret == 0)
			break;
		else if (ret != -EBUSY) {
			port = 0;
			break;
		}
	}
	nfct_destroy(nat_tuple);

	if (port == 0)
		return NF_DROP;

	buflen = nf_nat_ftp_fmt_cmd(type, buffer, sizeof(buffer),
					newip.ip, port);
	if (!buflen)
		goto out;

	if (!nfq_tcp_mangle_ipv4(pkt, matchoff, matchlen, buffer, buflen))
		goto out;

	return NF_ACCEPT;

out:
	cthelper_del_expect(exp);
	return NF_DROP;
}

static int
ftp_helper_cb(struct pkt_buff *pkt, uint32_t protoff,
	      struct myct *myct, uint32_t ctinfo)
{
	struct tcphdr *th;
	unsigned int dataoff;
	unsigned int matchoff = 0, matchlen = 0; /* makes gcc happy. */
	unsigned int datalen;
	unsigned int i;
	int found = 0, ends_in_nl;
	uint32_t seq;
	int ret = NF_ACCEPT;
	struct myct_man cmd;
	union nfct_attr_grp_addr addr;
	union nfct_attr_grp_addr daddr;
	int dir = CTINFO2DIR(ctinfo);
	struct ftp_info *ftp_info = myct->priv_data;
	struct nf_expect *exp = NULL;

	memset(&cmd, 0, sizeof(struct myct_man));
	memset(&addr, 0, sizeof(union nfct_attr_grp_addr));

	/* Until there's been traffic both ways, don't look in packets. */
	if (ctinfo != IP_CT_ESTABLISHED &&
	    ctinfo != IP_CT_ESTABLISHED_REPLY) {
		pr_debug("ftp: Conntrackinfo = %u\n", ctinfo);
		goto out;
	}

	th = (struct tcphdr *) (pktb_network_header(pkt) + protoff);

	dataoff = protoff + th->doff * 4;
	datalen = pktb_len(pkt) - dataoff;

	ends_in_nl = (pktb_network_header(pkt)[pktb_len(pkt) - 1] == '\n');
	seq = ntohl(th->seq) + datalen;

	/* Look up to see if we're just after a \n. */
	if (!find_nl_seq(ntohl(th->seq), ftp_info, dir)) {
		/* Now if this ends in \n, update ftp info. */
		pr_debug("nf_conntrack_ftp: wrong seq pos %s(%u) or %s(%u)\n",
		ftp_info->seq_aft_nl_num[dir] > 0 ? "" : "(UNSET)",
		ftp_info->seq_aft_nl[dir][0],
		ftp_info->seq_aft_nl_num[dir] > 1 ? "" : "(UNSET)",
		ftp_info->seq_aft_nl[dir][1]);
		goto out_update_nl;
	}

	/* Initialize IP/IPv6 addr to expected address (it's not mentioned
	   in EPSV responses) */
	cmd.l3num = nfct_get_attr_u16(myct->ct, ATTR_L3PROTO);
	nfct_get_attr_grp(myct->ct, ATTR_GRP_ORIG_ADDR_SRC, &cmd.u3);

	for (i = 0; i < ARRAY_SIZE(search[dir]); i++) {
		found = ftp_find_pattern(pkt, dataoff, datalen,
					 search[dir][i].pattern,
					 search[dir][i].plen,
					 search[dir][i].skip,
					 search[dir][i].term,
					 &matchoff, &matchlen,
					 &cmd,
					 search[dir][i].getnum,
					 dir);
		if (found) break;
	}
	if (found == 0) /* No match */
		goto out_update_nl;

	pr_debug("conntrack_ftp: match `%.*s' (%u bytes at %u)\n",
		 matchlen, pktb_network_header(pkt) + dataoff + matchoff,
		 matchlen, ntohl(th->seq) + matchoff);

	/* We refer to the reverse direction ("!dir") tuples here,
	 * because we're expecting something in the other direction.
	 * Doesn't matter unless NAT is happening.  */
	cthelper_get_addr_dst(myct->ct, !dir, &daddr);

	cthelper_get_addr_src(myct->ct, dir, &addr);

	/* Update the ftp info */
	if ((cmd.l3num == nfct_get_attr_u16(myct->ct, ATTR_L3PROTO)) &&
	    memcmp(&cmd.u3, &addr, sizeof(addr)) != 0) {
		/* Enrico Scholz's passive FTP to partially RNAT'd ftp
		   server: it really wants us to connect to a
		   different IP address.  Simply don't record it for
		   NAT. */
		if (cmd.l3num == PF_INET) {
			pr_debug("conntrack_ftp: NOT RECORDING: %pI4 != %pI4\n",
				 &cmd.u3.ip, &addr);
		} else {
			pr_debug("conntrack_ftp: NOT RECORDING: %pI6 != %pI6\n",
				 cmd.u3.ip6, &addr);
		}
		/* Thanks to Cristiano Lincoln Mattos
		   <lincoln@cesar.org.br> for reporting this potential
		   problem (DMZ machines opening holes to internal
		   networks, or the packet filter itself). */
		if (!loose) {
			ret = NF_ACCEPT;
			goto out;
		}
		memcpy(&daddr, &cmd.u3, sizeof(cmd.u3));
	}

	exp = nfexp_new();
	if (exp == NULL)
		goto out_update_nl;

	cthelper_get_addr_src(myct->ct, !dir, &addr);

	if (cthelper_expect_init(exp, myct->ct, 0, &addr, &daddr, IPPROTO_TCP,
				 NULL, &cmd.u.port, 0)) {
		pr_debug("conntrack_ftp: failed to init expectation\n");
		goto out_update_nl;
	}

	/* Now, NAT might want to mangle the packet, and register the
	 * (possibly changed) expectation itself. */
	if (nfct_get_attr_u32(myct->ct, ATTR_STATUS) & IPS_NAT_MASK) {
		ret = nf_nat_ftp(pkt, dir, ctinfo, search[dir][i].ftptype,
				 matchoff, matchlen, myct->ct, exp);
		goto out_update_nl;
	}

	/* Can't expect this?  Best to drop packet now. */
	if (cthelper_add_expect(exp) < 0) {
		pr_debug("conntrack_ftp: cannot add expectation: %s\n",
			strerror(errno));
		ret = NF_DROP;
		goto out_update_nl;
	}

out_update_nl:
	if (exp != NULL)
		nfexp_destroy(exp);

	/* Now if this ends in \n, update ftp info.  Seq may have been
	 * adjusted by NAT code. */
	if (ends_in_nl)
		update_nl_seq(seq, ftp_info, dir);
out:
	return ret;
}

static struct ctd_helper ftp_helper = {
	.name		= "ftp",
	.l4proto	= IPPROTO_TCP,
	.cb		= ftp_helper_cb,
	.priv_data_len	= sizeof(struct ftp_info),
	.policy		= {
		[0] = {
			.name			= "ftp",
			.expect_max		= 1,
			.expect_timeout		= 300,
		},
	},
};

void __attribute__ ((constructor)) ftp_init(void);

void ftp_init(void)
{
	helper_register(&ftp_helper);
}
