#include <net/if.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>

#include <netlink/genl/genl.h>
#include <netlink/genl/family.h>
#include <netlink/genl/ctrl.h>
#include <netlink/msg.h>
#include <netlink/attr.h>

#include <arpa/inet.h>

#include "nl80211.h"
#include "iw.h"

SECTION(wowlan);

static int wowlan_parse_tcp_file(struct nl_msg *msg, const char *fn)
{
	char buf[16768];
	int err = 1;
	FILE *f = fopen(fn, "r");
	struct nlattr *tcp;

	if (!f)
		return 1;
	tcp = nla_nest_start(msg, NL80211_WOWLAN_TRIG_TCP_CONNECTION);
	if (!tcp)
		goto nla_put_failure;

	while (!feof(f)) {
		char *eol;

		if (!fgets(buf, sizeof(buf), f))
			break;

		eol = strchr(buf + 5, '\r');
		if (eol)
			*eol = 0;
		eol = strchr(buf + 5, '\n');
		if (eol)
			*eol = 0;

		if (strncmp(buf, "source=", 7) == 0) {
			struct in_addr in_addr;
			char *addr = buf + 7;
			char *port = strchr(buf + 7, ':');

			if (port) {
				*port = 0;
				port++;
			}
			if (inet_aton(addr, &in_addr) == 0)
				goto close;
			NLA_PUT_U32(msg, NL80211_WOWLAN_TCP_SRC_IPV4,
				    in_addr.s_addr);
			if (port)
				NLA_PUT_U16(msg, NL80211_WOWLAN_TCP_SRC_PORT,
					    atoi(port));
		} else if (strncmp(buf, "dest=", 5) == 0) {
			struct in_addr in_addr;
			char *addr = buf + 5;
			char *port = strchr(buf + 5, ':');
			char *mac;
			unsigned char macbuf[6];

			if (!port)
				goto close;
			*port = 0;
			port++;
			mac = strchr(port, '@');
			if (!mac)
				goto close;
			*mac = 0;
			mac++;
			if (inet_aton(addr, &in_addr) == 0)
				goto close;
			NLA_PUT_U32(msg, NL80211_WOWLAN_TCP_DST_IPV4,
				    in_addr.s_addr);
			NLA_PUT_U16(msg, NL80211_WOWLAN_TCP_DST_PORT,
				    atoi(port));
			if (mac_addr_a2n(macbuf, mac))
				goto close;
			NLA_PUT(msg, NL80211_WOWLAN_TCP_DST_MAC,
				6, macbuf);
		} else if (strncmp(buf, "data=", 5) == 0) {
			size_t len;
			unsigned char *pkt = parse_hex(buf + 5, &len);

			if (!pkt)
				goto close;
			NLA_PUT(msg, NL80211_WOWLAN_TCP_DATA_PAYLOAD, len, pkt);
			free(pkt);
		} else if (strncmp(buf, "data.interval=", 14) == 0) {
			NLA_PUT_U32(msg, NL80211_WOWLAN_TCP_DATA_INTERVAL,
				    atoi(buf + 14));
		} else if (strncmp(buf, "wake=", 5) == 0) {
			unsigned char *pat, *mask;
			size_t patlen;

			if (parse_hex_mask(buf + 5, &pat, &patlen, &mask))
				goto close;
			NLA_PUT(msg, NL80211_WOWLAN_TCP_WAKE_MASK,
				DIV_ROUND_UP(patlen, 8), mask);
			NLA_PUT(msg, NL80211_WOWLAN_TCP_WAKE_PAYLOAD,
				patlen, pat);
			free(mask);
			free(pat);
		} else if (strncmp(buf, "data.seq=", 9) == 0) {
			struct nl80211_wowlan_tcp_data_seq seq = {};
			char *len, *offs, *start;

			len = buf + 9;
			offs = strchr(len, ',');
			if (!offs)
				goto close;
			*offs = 0;
			offs++;
			start = strchr(offs, ',');
			if (start) {
				*start = 0;
				start++;
				seq.start = atoi(start);
			}
			seq.len = atoi(len);
			seq.offset = atoi(offs);

			NLA_PUT(msg, NL80211_WOWLAN_TCP_DATA_PAYLOAD_SEQ,
				sizeof(seq), &seq);
		} else if (strncmp(buf, "data.tok=", 9) == 0) {
			struct nl80211_wowlan_tcp_data_token *tok;
			size_t stream_len;
			char *len, *offs, *toks;
			unsigned char *stream;

			len = buf + 9;
			offs = strchr(len, ',');
			if (!offs)
				goto close;
			*offs = 0;
			offs++;
			toks = strchr(offs, ',');
			if (!toks)
				goto close;
			*toks = 0;
			toks++;

			stream = parse_hex(toks, &stream_len);
			if (!stream)
				goto close;
			tok = malloc(sizeof(*tok) + stream_len);
			if (!tok) {
				free(stream);
				err = -ENOMEM;
				goto close;
			}

			tok->len = atoi(len);
			tok->offset = atoi(offs);
			memcpy(tok->token_stream, stream, stream_len);

			NLA_PUT(msg, NL80211_WOWLAN_TCP_DATA_PAYLOAD_TOKEN,
				sizeof(*tok) + stream_len, tok);
			free(stream);
			free(tok);
		} else {
			if (buf[0] == '#')
				continue;
			goto close;
		}
	}

	err = 0;
	goto close;
 nla_put_failure:
	err = -ENOBUFS;
 close:
	fclose(f);
	nla_nest_end(msg, tcp);
	return err;
}

static int handle_wowlan_enable(struct nl80211_state *state, struct nl_cb *cb,
				struct nl_msg *msg, int argc, char **argv,
				enum id_input id)
{
	struct nlattr *wowlan, *pattern;
	struct nl_msg *patterns = NULL;
	enum {
		PS_REG,
		PS_PAT,
	} parse_state = PS_REG;
	int err = -ENOBUFS;
	unsigned char *pat, *mask;
	size_t patlen;
	int patnum = 0, pkt_offset;
	char *eptr, *value1, *value2, *sptr = NULL;

	wowlan = nla_nest_start(msg, NL80211_ATTR_WOWLAN_TRIGGERS);
	if (!wowlan)
		return -ENOBUFS;

	while (argc) {
		switch (parse_state) {
		case PS_REG:
			if (strcmp(argv[0], "any") == 0)
				NLA_PUT_FLAG(msg, NL80211_WOWLAN_TRIG_ANY);
			else if (strcmp(argv[0], "disconnect") == 0)
				NLA_PUT_FLAG(msg, NL80211_WOWLAN_TRIG_DISCONNECT);
			else if (strcmp(argv[0], "magic-packet") == 0)
				NLA_PUT_FLAG(msg, NL80211_WOWLAN_TRIG_MAGIC_PKT);
			else if (strcmp(argv[0], "gtk-rekey-failure") == 0)
				NLA_PUT_FLAG(msg, NL80211_WOWLAN_TRIG_GTK_REKEY_FAILURE);
			else if (strcmp(argv[0], "eap-identity-request") == 0)
				NLA_PUT_FLAG(msg, NL80211_WOWLAN_TRIG_EAP_IDENT_REQUEST);
			else if (strcmp(argv[0], "4way-handshake") == 0)
				NLA_PUT_FLAG(msg, NL80211_WOWLAN_TRIG_4WAY_HANDSHAKE);
			else if (strcmp(argv[0], "rfkill-release") == 0)
				NLA_PUT_FLAG(msg, NL80211_WOWLAN_TRIG_RFKILL_RELEASE);
			else if (strcmp(argv[0], "tcp") == 0) {
				argv++;
				argc--;
				if (!argc) {
					err = 1;
					goto nla_put_failure;
				}
				err = wowlan_parse_tcp_file(msg, argv[0]);
				if (err)
					goto nla_put_failure;
			} else if (strcmp(argv[0], "patterns") == 0) {
				parse_state = PS_PAT;
				patterns = nlmsg_alloc();
				if (!patterns) {
					err = -ENOMEM;
					goto nla_put_failure;
				}
			} else {
				err = 1;
				goto nla_put_failure;
			}
			break;
		case PS_PAT:
			value1 = strtok_r(argv[0], "+", &sptr);
			value2 = strtok_r(NULL, "+", &sptr);

			if (!value2) {
				pkt_offset = 0;
				value2 = value1;
			} else {
				pkt_offset = strtoul(value1, &eptr, 10);
				if (eptr != value1 + strlen(value1)) {
					err = 1;
					goto nla_put_failure;
				}
			}

			if (parse_hex_mask(value2, &pat, &patlen, &mask)) {
				err = 1;
				goto nla_put_failure;
			}

			pattern = nla_nest_start(patterns, ++patnum);
			NLA_PUT(patterns, NL80211_WOWLAN_PKTPAT_MASK,
				DIV_ROUND_UP(patlen, 8), mask);
			NLA_PUT(patterns, NL80211_WOWLAN_PKTPAT_PATTERN,
				patlen, pat);
			NLA_PUT_U32(patterns, NL80211_WOWLAN_PKTPAT_OFFSET, pkt_offset);
			nla_nest_end(patterns, pattern);
			free(mask);
			free(pat);
			break;
		}
		argv++;
		argc--;
	}

	if (patterns)
		nla_put_nested(msg, NL80211_WOWLAN_TRIG_PKT_PATTERN,
				patterns);

	nla_nest_end(msg, wowlan);
	err = 0;
 nla_put_failure:
	nlmsg_free(patterns);
	return err;
}
COMMAND(wowlan, enable, "[any] [disconnect] [magic-packet] [gtk-rekey-failure] [eap-identity-request]"
	" [4way-handshake] [rfkill-release] [tcp <config-file>] [patterns [offset1+]<pattern1> ...]",
	NL80211_CMD_SET_WOWLAN, 0, CIB_PHY, handle_wowlan_enable,
	"Enable WoWLAN with the given triggers.\n"
	"Each pattern is given as a bytestring with '-' in places where any byte\n"
	"may be present, e.g. 00:11:22:-:44 will match 00:11:22:33:44 and\n"
	"00:11:22:33:ff:44 etc.\n"
	"Offset and pattern should be separated by '+', e.g. 18+43:34:00:12 will match "
	"'43:34:00:12' after 18 bytes of offset in Rx packet.\n\n"
	"The TCP configuration file contains:\n"
	"  source=ip[:port]\n"
	"  dest=ip:port@mac\n"
	"  data=<hex data packet>\n"
	"  data.interval=seconds\n"
	"  [wake=<hex packet with masked out bytes indicated by '-'>]\n"
	"  [data.seq=len,offset[,start]]\n"
	"  [data.tok=len,offset,<token stream>]");


static int handle_wowlan_disable(struct nl80211_state *state, struct nl_cb *cb,
				 struct nl_msg *msg, int argc, char **argv,
				 enum id_input id)
{
	/* just a set w/o wowlan attribute */
	return 0;
}
COMMAND(wowlan, disable, "", NL80211_CMD_SET_WOWLAN, 0, CIB_PHY, handle_wowlan_disable,
	"Disable WoWLAN.");


static int print_wowlan_handler(struct nl_msg *msg, void *arg)
{
	struct nlattr *attrs[NL80211_ATTR_MAX + 1];
	struct nlattr *trig[NUM_NL80211_WOWLAN_TRIG];
	struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
	struct nlattr *pattern;
	int rem_pattern;

	nla_parse(attrs, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
		  genlmsg_attrlen(gnlh, 0), NULL);

	if (!attrs[NL80211_ATTR_WOWLAN_TRIGGERS]) {
		printf("WoWLAN is disabled.\n");
		return NL_SKIP;
	}

	/* XXX: use policy */
	nla_parse(trig, MAX_NL80211_WOWLAN_TRIG,
		  nla_data(attrs[NL80211_ATTR_WOWLAN_TRIGGERS]),
		  nla_len(attrs[NL80211_ATTR_WOWLAN_TRIGGERS]),
		  NULL);

	printf("WoWLAN is enabled:\n");
	if (trig[NL80211_WOWLAN_TRIG_ANY])
		printf(" * wake up on special any trigger\n");
	if (trig[NL80211_WOWLAN_TRIG_DISCONNECT])
		printf(" * wake up on disconnect\n");
	if (trig[NL80211_WOWLAN_TRIG_MAGIC_PKT])
		printf(" * wake up on magic packet\n");
	if (trig[NL80211_WOWLAN_TRIG_GTK_REKEY_FAILURE])
		printf(" * wake up on GTK rekeying failure\n");
	if (trig[NL80211_WOWLAN_TRIG_EAP_IDENT_REQUEST])
		printf(" * wake up on EAP identity request\n");
	if (trig[NL80211_WOWLAN_TRIG_4WAY_HANDSHAKE])
		printf(" * wake up on 4-way handshake\n");
	if (trig[NL80211_WOWLAN_TRIG_RFKILL_RELEASE])
		printf(" * wake up on RF-kill release\n");
	if (trig[NL80211_WOWLAN_TRIG_PKT_PATTERN]) {
		nla_for_each_nested(pattern,
				    trig[NL80211_WOWLAN_TRIG_PKT_PATTERN],
				    rem_pattern) {
			struct nlattr *patattr[NUM_NL80211_WOWLAN_PKTPAT];
			int i, patlen, masklen, pkt_offset;
			uint8_t *mask, *pat;
			nla_parse(patattr, MAX_NL80211_WOWLAN_PKTPAT,
				  nla_data(pattern), nla_len(pattern),
				  NULL);
			if (!patattr[NL80211_WOWLAN_PKTPAT_MASK] ||
			    !patattr[NL80211_WOWLAN_PKTPAT_PATTERN] ||
			    !patattr[NL80211_WOWLAN_PKTPAT_OFFSET]) {
				printf(" * (invalid pattern specification)\n");
				continue;
			}
			masklen = nla_len(patattr[NL80211_WOWLAN_PKTPAT_MASK]);
			patlen = nla_len(patattr[NL80211_WOWLAN_PKTPAT_PATTERN]);
			pkt_offset = nla_get_u32(patattr[NL80211_WOWLAN_PKTPAT_OFFSET]);
			if (DIV_ROUND_UP(patlen, 8) != masklen) {
				printf(" * (invalid pattern specification)\n");
				continue;
			}
			printf(" * wake up on packet offset: %d", pkt_offset);
			printf(" pattern: ");
			pat = nla_data(patattr[NL80211_WOWLAN_PKTPAT_PATTERN]);
			mask = nla_data(patattr[NL80211_WOWLAN_PKTPAT_MASK]);
			for (i = 0; i < patlen; i++) {
				if (mask[i / 8] & (1 << (i % 8)))
					printf("%.2x", pat[i]);
				else
					printf("--");
				if (i != patlen - 1)
					printf(":");
			}
			printf("\n");
		}
	}
	if (trig[NL80211_WOWLAN_TRIG_TCP_CONNECTION])
		printf(" * wake up on TCP connection\n");

	return NL_SKIP;
}

static int handle_wowlan_show(struct nl80211_state *state, struct nl_cb *cb,
			      struct nl_msg *msg, int argc, char **argv,
			      enum id_input id)
{
	nl_cb_set(cb, NL_CB_VALID, NL_CB_CUSTOM,
		  print_wowlan_handler, NULL);

	return 0;
}
COMMAND(wowlan, show, "", NL80211_CMD_GET_WOWLAN, 0, CIB_PHY, handle_wowlan_show,
	"Show WoWLAN status.");
