#include <u.h>
#include <libc.h>
#include <ip.h>
#include <auth.h>
#include "ppp.h"

typedef struct Iphdr Iphdr;
struct Iphdr
{
	uchar	vihl;		/* Version and header length */
	uchar	tos;		/* Type of service */
	uchar	length[2];	/* packet length */
	uchar	id[2];		/* Identification */
	uchar	frag[2];	/* Fragment information */
	uchar	ttl;		/* Time to live */
	uchar	proto;		/* Protocol */
	uchar	cksum[2];	/* Header checksum */
	ulong	src;		/* Ip source (uchar ordering unimportant) */
	ulong	dst;		/* Ip destination (uchar ordering unimportant) */
};

typedef struct Tcphdr Tcphdr;
struct Tcphdr
{
	ulong	ports;		/* defined as a ulong to make comparisons easier */
	uchar	seq[4];
	uchar	ack[4];
	uchar	flag[2];
	uchar	win[2];
	uchar	cksum[2];
	uchar	urg[2];
};

typedef struct Ilhdr Ilhdr;
struct Ilhdr
{
	uchar	sum[2];	/* Checksum including header */
	uchar	len[2];	/* Packet length */
	uchar	type;		/* Packet type */
	uchar	spec;		/* Special */
	uchar	src[2];	/* Src port */
	uchar	dst[2];	/* Dst port */
	uchar	id[4];	/* Sequence id */
	uchar	ack[4];	/* Acked sequence */
};

enum
{
	URG		= 0x20,		/* Data marked urgent */
	ACK		= 0x10,		/* Aknowledge is valid */
	PSH		= 0x08,		/* Whole data pipe is pushed */
	RST		= 0x04,		/* Reset connection */
	SYN		= 0x02,		/* Pkt. is synchronise */
	FIN		= 0x01,		/* Start close down */

	IP_DF		= 0x4000,	/* Don't fragment */

	IP_TCPPROTO	= 6,
	IP_ILPROTO	= 40,
	IL_IPHDR	= 20,
};

typedef struct Hdr Hdr;
struct Hdr
{
	uchar	buf[128];
	Iphdr	*ip;
	Tcphdr	*tcp;
	int	len;
};

typedef struct Tcpc Tcpc;
struct Tcpc
{
	uchar	lastrecv;
	uchar	lastxmit;
	uchar	basexmit;
	uchar	err;
	uchar	compressid;
	Hdr	t[MAX_STATES];
	Hdr	r[MAX_STATES];
};

enum
{	/* flag bits for what changed in a packet */
	NEW_U=(1<<0),	/* tcp only */
	NEW_W=(1<<1),	/* tcp only */
	NEW_A=(1<<2),	/* il tcp */
	NEW_S=(1<<3),	/* tcp only */
	NEW_P=(1<<4),	/* tcp only */
	NEW_I=(1<<5),	/* il tcp */
	NEW_C=(1<<6),	/* il tcp */
	NEW_T=(1<<7),	/* il only */
	TCP_PUSH_BIT	= 0x10,
};

/* reserved, special-case values of above for tcp */
#define SPECIAL_I (NEW_S|NEW_W|NEW_U)		/* echoed interactive traffic */
#define SPECIAL_D (NEW_S|NEW_A|NEW_W|NEW_U)	/* unidirectional data */
#define SPECIALS_MASK (NEW_S|NEW_A|NEW_W|NEW_U)

int
encode(void *p, ulong n)
{
	uchar	*cp;

	cp = p;
	if(n >= 256 || n == 0) {
		*cp++ = 0;
		cp[0] = n >> 8;
		cp[1] = n;
		return 3;
	}
	*cp = n;
	return 1;
}

#define DECODEL(f) { \
	if (*cp == 0) {\
		hnputl(f, nhgetl(f) + ((cp[1] << 8) | cp[2])); \
		cp += 3; \
	} else { \
		hnputl(f, nhgetl(f) + (ulong)*cp++); \
	} \
}
#define DECODES(f) { \
	if (*cp == 0) {\
		hnputs(f, nhgets(f) + ((cp[1] << 8) | cp[2])); \
		cp += 3; \
	} else { \
		hnputs(f, nhgets(f) + (ulong)*cp++); \
	} \
}

Block*
tcpcompress(Tcpc *comp, Block *b, int *protop)
{
	Iphdr	*ip;		/* current packet */
	Tcphdr	*tcp;		/* current pkt */
	ulong 	iplen, tcplen, hlen;	/* header length in uchars */
	ulong 	deltaS, deltaA;	/* general purpose temporaries */
	ulong 	changes;	/* change mask */
	uchar 	new_seq[16];	/* changes from last to current */
	uchar 	*cp;
	Hdr	*h;		/* last packet */
	int 	i, j;

	/*
	 * Bail if this is not a compressible TCP/IP packet
	 */
	ip = (Iphdr*)b->rptr;
	iplen = (ip->vihl & 0xf) << 2;
	tcp = (Tcphdr*)(b->rptr + iplen);
	tcplen = (tcp->flag[0] & 0xf0) >> 2;
	hlen = iplen + tcplen;
	if((tcp->flag[1] & (SYN|FIN|RST|ACK)) != ACK){
		*protop = Pip;
		return b;		/* connection control */
	}

	/*
	 * Packet is compressible, look for a connection
	 */
	changes = 0;
	cp = new_seq;
	j = comp->lastxmit;
	h = &comp->t[j];
	if(ip->src != h->ip->src || ip->dst != h->ip->dst
	|| tcp->ports != h->tcp->ports) {
		for(i = 0; i < MAX_STATES; ++i) {
			j = (comp->basexmit + i) % MAX_STATES;
			h = &comp->t[j];
			if(ip->src == h->ip->src && ip->dst == h->ip->dst
			&& tcp->ports == h->tcp->ports)
				goto found;
		}

		/* no connection, reuse the oldest */
		if(i == MAX_STATES) {
			j = comp->basexmit;
			j = (j + MAX_STATES - 1) % MAX_STATES;
			comp->basexmit = j;
			h = &comp->t[j];
			goto rescue;
		}
	}
found:

	/*
	 * Make sure that only what we expect to change changed. 
	 */
	if(ip->vihl  != h->ip->vihl || ip->tos   != h->ip->tos ||
	   ip->ttl   != h->ip->ttl  || ip->proto != h->ip->proto)
		goto rescue;	/* headers changed */
	if(iplen != sizeof(Iphdr) && memcmp(ip+1, h->ip+1, iplen - sizeof(Iphdr)))
		goto rescue;	/* ip options changed */
	if(tcplen != sizeof(Tcphdr) && memcmp(tcp+1, h->tcp+1, tcplen - sizeof(Tcphdr)))
		goto rescue;	/* tcp options changed */

	if(tcp->flag[1] & URG) {
		cp += encode(cp, nhgets(tcp->urg));
		changes |= NEW_U;
	} else if(memcmp(tcp->urg, h->tcp->urg, sizeof(tcp->urg)) != 0)
		goto rescue;
	if(deltaS = nhgets(tcp->win) - nhgets(h->tcp->win)) {
		cp += encode(cp, deltaS);
		changes |= NEW_W;
	}
	if(deltaA = nhgetl(tcp->ack) - nhgetl(h->tcp->ack)) {
		if(deltaA > 0xffff)
			goto rescue;
		cp += encode(cp, deltaA);
		changes |= NEW_A;
	}
	if(deltaS = nhgetl(tcp->seq) - nhgetl(h->tcp->seq)) {
		if (deltaS > 0xffff)
			goto rescue;
		cp += encode(cp, deltaS);
		changes |= NEW_S;
	}

	/*
	 * Look for the special-case encodings.
	 */
	switch(changes) {
	case 0:
		/*
		 * Nothing changed. If this packet contains data and the last
		 * one didn't, this is probably a data packet following an
		 * ack (normal on an interactive connection) and we send it
		 * compressed. Otherwise it's probably a retransmit,
		 * retransmitted ack or window probe.  Send it uncompressed
		 * in case the other side missed the compressed version.
		 */
		if(nhgets(ip->length) == nhgets(h->ip->length) ||
		   nhgets(h->ip->length) != hlen)
			goto rescue;
		break;
	case SPECIAL_I:
	case SPECIAL_D:
		/*
		 * Actual changes match one of our special case encodings --
		 * send packet uncompressed.
		 */
		goto rescue;
	case NEW_S | NEW_A:
		if (deltaS == deltaA &&
			deltaS == nhgets(h->ip->length) - hlen) {
			/* special case for echoed terminal traffic */
			changes = SPECIAL_I;
			cp = new_seq;
		}
		break;
	case NEW_S:
		if (deltaS == nhgets(h->ip->length) - hlen) {
			/* special case for data xfer */
			changes = SPECIAL_D;
			cp = new_seq;
		}
		break;
	}
	deltaS = nhgets(ip->id) - nhgets(h->ip->id);
	if(deltaS != 1) {
		cp += encode(cp, deltaS);
		changes |= NEW_I;
	}
	if (tcp->flag[1] & PSH)
		changes |= TCP_PUSH_BIT;
	/*
	 * Grab the cksum before we overwrite it below. Then update our
	 * state with this packet's header.
	 */
	deltaA = nhgets(tcp->cksum);
	memmove(h->buf, b->rptr, hlen);
	h->len = hlen;
	h->tcp = (Tcphdr*)(h->buf + iplen);

	/*
	 * We want to use the original packet as our compressed packet. (cp -
	 * new_seq) is the number of uchars we need for compressed sequence
	 * numbers. In addition we need one uchar for the change mask, one
	 * for the connection id and two for the tcp checksum. So, (cp -
	 * new_seq) + 4 uchars of header are needed. hlen is how many uchars
	 * of the original packet to toss so subtract the two to get the new
	 * packet size. The temporaries are gross -egs.
	 */
	deltaS = cp - new_seq;
	cp = b->rptr;
	if(comp->lastxmit != j || comp->compressid == 0) {
		comp->lastxmit = j;
		hlen -= deltaS + 4;
		cp += hlen;
		*cp++ = (changes | NEW_C);
		*cp++ = j;
	} else {
		hlen -= deltaS + 3;
		cp += hlen;
		*cp++ = changes;
	}
	b->rptr += hlen;
	hnputs(cp, deltaA);
	cp += 2;
	memmove(cp, new_seq, deltaS);
	*protop = Pvjctcp;
	return b;

rescue:
	/*
	 * Update connection state & send uncompressed packet
	 */
	memmove(h->buf, b->rptr, hlen);
	h->tcp = (Tcphdr*)(h->buf + iplen);
	h->len = hlen;
	ip->proto = j;
	comp->lastxmit = j;
	*protop = Pvjutcp;
	return b;
}

Block*
tcpuncompress(Tcpc *comp, Block *b, int type)
{
	uchar	*cp, changes;
	int	i;
	int	iplen, len;
	Iphdr	*ip;
	Tcphdr	*tcp;
	Hdr	*h;

	if(type == Pvjutcp) {
		/*
		 *  Locate the saved state for this connection. If the state
		 *  index is legal, clear the 'discard' flag.
		 */
		ip = (Iphdr*)b->rptr;
		if(ip->proto >= MAX_STATES)
			goto rescue;
		iplen = (ip->vihl & 0xf) << 2;
		tcp = (Tcphdr*)(b->rptr + iplen);
		comp->lastrecv = ip->proto;
		len = iplen + ((tcp->flag[0] & 0xf0) >> 2);
		comp->err = 0;
		/*
		 * Restore the IP protocol field then save a copy of this
		 * packet header. The checksum is zeroed in the copy so we
		 * don't have to zero it each time we process a compressed
		 * packet.
		 */
		ip->proto = IP_TCPPROTO;
		h = &comp->r[comp->lastrecv];
		memmove(h->buf, b->rptr, len);
		h->tcp = (Tcphdr*)(h->buf + iplen);
		h->len = len;
		h->ip->cksum[0] = h->ip->cksum[1] = 0;
		return b;
	}

	cp = b->rptr;
	changes = *cp++;
	if(changes & NEW_C) {
		/*
		 * Make sure the state index is in range, then grab the
		 * state. If we have a good state index, clear the 'discard'
		 * flag.
		 */
		if(*cp >= MAX_STATES)
			goto rescue;
		comp->err = 0;
		comp->lastrecv = *cp++;
	} else {
		/*
		 * This packet has no state index. If we've had a
		 * line error since the last time we got an explicit state
		 * index, we have to toss the packet.
		 */
		if(comp->err != 0){
			freeb(b);
			return nil;
		}
	}

	/*
	 * Find the state then fill in the TCP checksum and PUSH bit.
	 */
	h = &comp->r[comp->lastrecv];
	ip = h->ip;
	tcp = h->tcp;
	len = h->len;
	memmove(tcp->cksum, cp, sizeof tcp->cksum);
	cp += 2;
	if(changes & TCP_PUSH_BIT)
		tcp->flag[1] |= PSH;
	else
		tcp->flag[1] &= ~PSH;
	/*
	 * Fix up the state's ack, seq, urg and win fields based on the
	 * changemask.
	 */
	switch (changes & SPECIALS_MASK) {
	case SPECIAL_I:
		i = nhgets(ip->length) - len;
		hnputl(tcp->ack, nhgetl(tcp->ack) + i);
		hnputl(tcp->seq, nhgetl(tcp->seq) + i);
		break;

	case SPECIAL_D:
		hnputl(tcp->seq, nhgetl(tcp->seq) + nhgets(ip->length) - len);
		break;

	default:
		if(changes & NEW_U) {
			tcp->flag[1] |= URG;
			if(*cp == 0){
				hnputs(tcp->urg, nhgets(cp+1));
				cp += 3;
			}else
				hnputs(tcp->urg, *cp++);
		} else
			tcp->flag[1] &= ~URG;
		if(changes & NEW_W)
			DECODES(tcp->win)
		if(changes & NEW_A)
			DECODEL(tcp->ack)
		if(changes & NEW_S)
			DECODEL(tcp->seq)
		break;
	}

	/* Update the IP ID */
	if(changes & NEW_I)
		DECODES(ip->id)
	else
		hnputs(ip->id, nhgets(ip->id) + 1);

	/*
	 *  At this point, cp points to the first uchar of data in the packet.
	 *  Back up cp by the TCP/IP header length to make room for the
	 *  reconstructed header.
	 *  We assume the packet we were handed has enough space to prepend
	 *  up to 128 uchars of header.
	 */
	b->rptr = cp - len;
	assert(b->rptr >= b->base);
	hnputs(ip->length, BLEN(b));
	memmove(b->rptr, ip, len);
	
	/* recompute the ip header checksum */
	ip = (Iphdr*)b->rptr;
	ip->cksum[0] = ip->cksum[1] = 0;
	hnputs(ip->cksum, ipcsum(b->rptr));

	return b;

rescue:
	netlog("ppp: vj: Bad Packet!\n");
	comp->err = 1;
	freeb(b);
	return nil;
}

Tcpc*
compress_init(Tcpc *c)
{
	int i;
	Hdr *h;

	if(c == nil)
		c = malloc(sizeof(Tcpc));

	memset(c, 0, sizeof(*c));
	for(i = 0; i < MAX_STATES; i++){
		h = &c->t[i];
		h->ip = (Iphdr*)h->buf;
		h->tcp = (Tcphdr*)(h->buf + 20);
		h->len = 40;
		h = &c->r[i];
		h->ip = (Iphdr*)h->buf;
		h->tcp = (Tcphdr*)(h->buf + 20);
		h->len = 40;
	}

	return c;
}

Block*
compress(Tcpc *tcp, Block *b, int *protop)
{
	Iphdr		*ip;

	/*
	 * Bail if this is not a compressible IP packet
	 */
	ip = (Iphdr*)b->rptr;
	if((nhgets(ip->frag) & 0x3fff) != 0){
		*protop = Pip;
		return b;
	}

	switch(ip->proto) {
	case IP_TCPPROTO:
		return tcpcompress(tcp, b, protop);
	default:
		*protop = Pip;
		return b;
	}
}

int
compress_negotiate(Tcpc *tcp, uchar *data)
{
	if(data[0] != MAX_STATES - 1)
		return -1;
	tcp->compressid = data[1];
	return 0;
}

/* called by ppp when there was a bad frame received */
void
compress_error(Tcpc *tcp)
{
	tcp->err = 1;
}
