#include "u.h"
#include "../port/lib.h"
#include "mem.h"
#include "dat.h"
#include "fns.h"
#include "../port/netif.h"
#include "../port/error.h"

#include	<libsec.h>
#include "../port/thwack.h"

/*
 * sdp - secure datagram protocol
 */

typedef struct Sdp Sdp;
typedef struct Conv Conv;
typedef struct OneWay OneWay;
typedef struct Stats Stats;
typedef struct AckPkt AckPkt;
typedef struct Algorithm Algorithm;
typedef struct CipherRc4 CipherRc4;

enum
{
	Qtopdir=	1,		/* top level directory */

	Qsdpdir,			/* sdp directory */
	Qclone,
	Qlog,

	Qconvdir,			/* directory per conversation */
	Qctl,
	Qdata,				/* unreliable packet channel */
	Qcontrol,			/* reliable control channel */
	Qstatus,
	Qstats,
	Qrstats,

	MaxQ,

	Maxconv= 256,		// power of 2
	Nfs= 4,			// number of file systems
	MaxRetries=	12,
	KeepAlive = 300,	// keep alive in seconds - should probably be about 60 but is higher to avoid linksys bug
	SecretLength= 32,	// a secret per direction
	SeqMax = (1<<24),
	SeqWindow = 32,
	NCompStats = 8,
};

#define TYPE(x) 	(((ulong)(x).path) & 0xff)
#define CONV(x) 	((((ulong)(x).path) >> 8)&(Maxconv-1))
#define QID(x, y) 	(((x)<<8) | (y))

struct Stats
{
	ulong	outPackets;
	ulong	outDataPackets;
	ulong	outDataBytes;
	ulong	outCompDataBytes;
	ulong	outCompBytes;
	ulong	outCompStats[NCompStats];
	ulong	inPackets;
	ulong	inDataPackets;
	ulong	inDataBytes;
	ulong	inCompDataBytes;
	ulong	inMissing;
	ulong	inDup;
	ulong	inReorder;
	ulong	inBadComp;
	ulong	inBadAuth;
	ulong	inBadSeq;
	ulong	inBadOther;
};

struct OneWay
{
	Rendez	statsready;

	ulong	seqwrap;	// number of wraps of the sequence number
	ulong	seq;
	ulong	window;

	uchar	secret[SecretLength];

	QLock	controllk;
	Rendez	controlready;
	Block	*controlpkt;		// control channel
	ulong	controlseq;

	void	*cipherstate;	// state cipher
	int		cipherivlen;	// initial vector length
	int		cipherblklen;	// block length
	int		(*cipher)(OneWay*, uchar *buf, int len);

	void	*authstate;		// auth state
	int		authlen;		// auth data length in bytes
	int		(*auth)(OneWay*, uchar *buf, int len);

	void	*compstate;
	int		(*comp)(Conv*, int subtype, ulong seq, Block **);
};

// conv states
enum {
	CFree,
	CInit,
	CDial,
	CAccept,
	COpen,
	CLocalClose,
	CRemoteClose,
	CClosed,
};

struct Conv {
	QLock;
	Sdp	*sdp;
	int	id;

	int ref;	// holds conv up

	int state;

	int dataopen;	// ref count of opens on Qdata
	int controlopen;	// ref count of opens on Qcontrol
	int reader;		// reader proc has been started

	Stats	lstats;
	Stats	rstats;
	
	ulong	lastrecv;	// time last packet was received 
	ulong	timeout;
	int		retries;

	// the following pair uniquely define conversation on this port
	ulong dialid;
	ulong acceptid;

	QLock readlk;		// protects readproc
	Proc *readproc;

	Chan *chan;		// packet channel
	char *channame;

	char owner[KNAMELEN];		/* protections */
	int	perm;

	Algorithm *auth;
	Algorithm *cipher;
	Algorithm *comp;

	int drop;

	OneWay	in;
	OneWay	out;
};

struct Sdp {
	QLock;
	Log;
	int	nconv;
	Conv *conv[Maxconv];
	int ackproc;
};

enum {
	TConnect,
	TControl,
	TData,
	TCompData,
};

enum {
	ControlMesg,
	ControlAck,
};

enum {
	ThwackU,
	ThwackC,
};

enum {
	ConOpenRequest,
	ConOpenAck,
	ConOpenAckAck,
	ConClose,
	ConCloseAck,
	ConReset,
};

struct AckPkt
{
	uchar	cseq[4];
	uchar	outPackets[4];
	uchar	outDataPackets[4];
	uchar	outDataBytes[4];
	uchar	outCompDataBytes[4];
	uchar	outCompStats[4*NCompStats];
	uchar	inPackets[4];
	uchar	inDataPackets[4];
	uchar	inDataBytes[4];
	uchar	inCompDataBytes[4];
	uchar	inMissing[4];
	uchar	inDup[4];
	uchar	inReorder[4];
	uchar	inBadComp[4];
	uchar	inBadAuth[4];
	uchar	inBadSeq[4];
	uchar	inBadOther[4];
};

struct Algorithm
{
	char 	*name;
	int		keylen;		// in bytes
	void	(*init)(Conv*);
};

enum {
	RC4forward	= 10*1024*1024,	// maximum skip forward
	RC4back = 100*1024,		// maximum look back
};

struct CipherRc4
{
	ulong cseq;	// current byte sequence number
	RC4state current;

	int ovalid;	// old is valid
	ulong lgseq; // last good sequence
	ulong oseq;	// old byte sequence number
	RC4state old;
};

static Dirtab sdpdirtab[]={
	"log",		{Qlog},		0,	0666,
	"clone",	{Qclone},		0,	0666,
};

static Dirtab convdirtab[]={
	"ctl",		{Qctl},	0,	0666,
	"data",		{Qdata},	0,	0666,
	"control",	{Qcontrol},	0,	0666,
	"status",	{Qstatus},	0,	0444,
	"stats",	{Qstats},	0,	0444,
	"rstats",	{Qrstats},	0,	0444,
};

static int m2p[] = {
	[OREAD]		4,
	[OWRITE]	2,
	[ORDWR]		6
};

enum {
	Logcompress=	(1<<0),
	Logauth=	(1<<1),
	Loghmac=	(1<<2),
};

static Logflag logflags[] =
{
	{ "compress",	Logcompress, },
	{ "auth",	Logauth, },
	{ "hmac",	Loghmac, },
	{ nil,		0, },
};

static Dirtab	*dirtab[MaxQ];
static Sdp sdptab[Nfs];
static char *convstatename[] = {
	[CFree]		"Free",
	[CInit]		"Init",
	[CDial]		"Dial",
	[CAccept]	"Accept",
	[COpen]		"Open",
	[CLocalClose] "LocalClose",
	[CRemoteClose] "RemoteClose",
	[CClosed]	"Closed",
};

static int sdpgen(Chan *c, char*, Dirtab*, int, int s, Dir *dp);
static Conv *sdpclone(Sdp *sdp);
static void sdpackproc(void *a);
static void onewaycleanup(OneWay *ow);
static int readready(void *a);
static int controlread();
static void convsetstate(Conv *c, int state);
static Block *readcontrol(Conv *c, int n);
static void writecontrol(Conv *c, void *p, int n, int wait);
static Block *readdata(Conv *c, int n);
static long writedata(Conv *c, Block *b);
static void convderef(Conv *c);
static Block *conviput(Conv *c, Block *b, int control);
static void conviconnect(Conv *c, int op, Block *b);
static void convicontrol(Conv *c, int op, Block *b);
static Block *convicomp(Conv *c, int op, ulong, Block *b);
static void convoput(Conv *c, int type, int subtype, Block *b);
static void convoconnect(Conv *c, int op, ulong dialid, ulong acceptid);
static void convopenchan(Conv *c, char *path);
static void convstats(Conv *c, int local, char *buf, int n);
static void convreader(void *a);

static void setalg(Conv *c, char *name, Algorithm *tab, Algorithm **);
static void setsecret(OneWay *cc, char *secret);

static void nullcipherinit(Conv*c);
static void descipherinit(Conv*c);
static void rc4cipherinit(Conv*c);
static void nullauthinit(Conv*c);
static void shaauthinit(Conv*c);
static void md5authinit(Conv*c);
static void nullcompinit(Conv*c);
static void thwackcompinit(Conv*c);

static Algorithm cipheralg[] =
{
	"null",			0,	nullcipherinit,
	"des_56_cbc",	7,	descipherinit,
	"rc4_128",		16,	rc4cipherinit,
	"rc4_256",		32,	rc4cipherinit,
	nil,			0,	nil,
};

static Algorithm authalg[] =
{
	"null",			0,	nullauthinit,
	"hmac_sha1_96",	16,	shaauthinit,
	"hmac_md5_96",	16,	md5authinit,
	nil,			0,	nil,
};

static Algorithm compalg[] =
{
	"null",			0,	nullcompinit,
	"thwack",		0,	thwackcompinit,
	nil,			0,	nil,
};


static void
sdpinit(void)
{
	int i;
	Dirtab *dt;
	
	// setup dirtab with non directory entries
	for(i=0; i<nelem(sdpdirtab); i++) {
		dt = sdpdirtab + i;
		dirtab[TYPE(dt->qid)] = dt;
	}

	for(i=0; i<nelem(convdirtab); i++) {
		dt = convdirtab + i;
		dirtab[TYPE(dt->qid)] = dt;
	}

}

static Chan*
sdpattach(char* spec)
{
	Chan *c;
	int dev;
	char buf[100];
	Sdp *sdp;
	int start;

	dev = atoi(spec);
	if(dev<0 || dev >= Nfs)
		error("bad specification");

	c = devattach('E', spec);
	c->qid = (Qid){QID(0, Qtopdir), 0, QTDIR};
	c->dev = dev;

	sdp = sdptab + dev;
	qlock(sdp);
	start = sdp->ackproc == 0;
	sdp->ackproc = 1;
	qunlock(sdp);

	if(start) {
		snprint(buf, sizeof(buf), "sdpackproc%d", dev);
		kproc(buf, sdpackproc, sdp);
	}
	
	return c;
}

static Walkqid*
sdpwalk(Chan *c, Chan *nc, char **name, int nname)
{
	return devwalk(c, nc, name, nname, 0, 0, sdpgen);
}

static int
sdpstat(Chan* c, uchar* db, int n)
{
	return devstat(c, db, n, nil, 0, sdpgen);
}

static Chan*
sdpopen(Chan* ch, int omode)
{
	int perm;
	Sdp *sdp;
	Conv *c;

	omode &= 3;
	perm = m2p[omode];
	USED(perm);

	sdp = sdptab + ch->dev;

	switch(TYPE(ch->qid)) {
	default:
		break;
	case Qtopdir:
	case Qsdpdir:
	case Qconvdir:
		if(omode != OREAD)
			error(Eperm);
		break;
	case Qlog:
		logopen(sdp);
		break;
	case Qclone:
		c = sdpclone(sdp);
		if(c == nil)
			error(Enodev);
		ch->qid.path = QID(c->id, Qctl);
		break;
	case Qdata:
	case Qctl:
	case Qstatus:
	case Qcontrol:
	case Qstats:
	case Qrstats:
		c = sdp->conv[CONV(ch->qid)];
		qlock(c);
		if(waserror()) {
			qunlock(c);
			nexterror();
		}
		if((perm & (c->perm>>6)) != perm)
		if(strcmp(up->user, c->owner) != 0 || (perm & c->perm) != perm)
				error(Eperm);

		c->ref++;
		if(TYPE(ch->qid) == Qdata) {
			c->dataopen++;
			// kill reader if Qdata is opened for the first time
			if(c->dataopen == 1)
			if(c->readproc != nil)
				postnote(c->readproc, 1, "interrupt", 0);
		} else if(TYPE(ch->qid) == Qcontrol) {	
			c->controlopen++;
		}
		qunlock(c);
		poperror();
		break;
	}
	ch->mode = openmode(omode);
	ch->flag |= COPEN;
	ch->offset = 0;
	return ch;
}

static void
sdpclose(Chan* ch)
{
	Sdp *sdp  = sdptab + ch->dev;
	Conv *c;

	if(!(ch->flag & COPEN))
		return;
	switch(TYPE(ch->qid)) {
	case Qlog:
		logclose(sdp);
		break;
	case Qctl:
	case Qstatus:
	case Qstats:
	case Qrstats:
		c = sdp->conv[CONV(ch->qid)];
		qlock(c);
		convderef(c);
		qunlock(c);
		break;

	case Qdata:
		c = sdp->conv[CONV(ch->qid)];
		qlock(c);
		c->dataopen--;
		convderef(c);
		if(c->dataopen == 0)
		if(c->reader == 0)
		if(c->chan != nil)
		if(!waserror()) {
			kproc("convreader", convreader, c);
			c->reader = 1;
			c->ref++;
			poperror();
		}
		qunlock(c);
		break;

	case Qcontrol:
		c = sdp->conv[CONV(ch->qid)];
		qlock(c);
		c->controlopen--;
		convderef(c);
		if(c->controlopen == 0 && c->ref != 0) {
			switch(c->state) {
			default:
				convsetstate(c, CClosed);
				break;
			case CAccept:
			case COpen:
				convsetstate(c, CLocalClose);
				break;
			}
		}
		qunlock(c);
		break;
	}
}

static long
sdpread(Chan *ch, void *a, long n, vlong off)
{
	char buf[256];
	char *s;
	Sdp *sdp = sdptab + ch->dev;
	Conv *c;
	Block *b;
	int rv;

	USED(off);
	switch(TYPE(ch->qid)) {
	default:
		error(Eperm);
	case Qtopdir:
	case Qsdpdir:
	case Qconvdir:
		return devdirread(ch, a, n, 0, 0, sdpgen);
	case Qlog:
		return logread(sdp, a, off, n);
	case Qstatus:
		c = sdp->conv[CONV(ch->qid)];
		qlock(c);
		n = readstr(off, a, n, convstatename[c->state]);
		qunlock(c);
		return n;
	case Qctl:
		sprint(buf, "%lud", CONV(ch->qid));
		return readstr(off, a, n, buf);
	case Qcontrol:
		b = readcontrol(sdp->conv[CONV(ch->qid)], n);
		if(b == nil)
			return 0;
		if(BLEN(b) < n)
			n = BLEN(b);
		memmove(a, b->rp, n);
		freeb(b);
		return n;
	case Qdata:
		b = readdata(sdp->conv[CONV(ch->qid)], n);
		if(b == nil)
			return 0;
		if(BLEN(b) < n)
			n = BLEN(b);
		memmove(a, b->rp, n);
		freeb(b);
		return n;
	case Qstats:
	case Qrstats:
		c = sdp->conv[CONV(ch->qid)];
		s = smalloc(1000);
		convstats(c, TYPE(ch->qid) == Qstats, s, 1000);
		rv = readstr(off, a, n, s);
		free(s);
		return rv;
	}
}

static Block*
sdpbread(Chan* ch, long n, ulong offset)
{
	Sdp *sdp = sdptab + ch->dev;

	if(TYPE(ch->qid) != Qdata)
		return devbread(ch, n, offset);
	return readdata(sdp->conv[CONV(ch->qid)], n);
}


static long
sdpwrite(Chan *ch, void *a, long n, vlong off)
{
	Sdp *sdp = sdptab + ch->dev;
	Cmdbuf *cb;
	char *arg0;
	char *p;
	Conv *c;
	Block *b;
	
	USED(off);
	switch(TYPE(ch->qid)) {
	default:
		error(Eperm);
	case Qctl:
		c = sdp->conv[CONV(ch->qid)];
		cb = parsecmd(a, n);
		qlock(c);
		if(waserror()) {
			qunlock(c);
			free(cb);
			nexterror();
		}
		if(cb->nf == 0)
			error("short write");
		arg0 = cb->f[0];
		if(strcmp(arg0, "accept") == 0) {
			if(cb->nf != 2)
				error("usage: accect file");
			convopenchan(c, cb->f[1]);
		} else if(strcmp(arg0, "dial") == 0) {
			if(cb->nf != 2)
				error("usage: accect file");
			convopenchan(c, cb->f[1]);
			convsetstate(c, CDial);
		} else if(strcmp(arg0, "drop") == 0) {
			if(cb->nf != 2)
				error("usage: drop permil");
			c->drop = atoi(cb->f[1]);
		} else if(strcmp(arg0, "cipher") == 0) {
			if(cb->nf != 2)
				error("usage: cipher alg");
			setalg(c, cb->f[1], cipheralg, &c->cipher);
		} else if(strcmp(arg0, "auth") == 0) {
			if(cb->nf != 2)
				error("usage: auth alg");
			setalg(c, cb->f[1], authalg, &c->auth);
		} else if(strcmp(arg0, "comp") == 0) {
			if(cb->nf != 2)
				error("usage: comp alg");
			setalg(c, cb->f[1], compalg, &c->comp);
		} else if(strcmp(arg0, "insecret") == 0) {
			if(cb->nf != 2)
				error("usage: insecret secret");
			setsecret(&c->in, cb->f[1]);
			if(c->cipher)
				c->cipher->init(c);
			if(c->auth)
				c->auth->init(c);
		} else if(strcmp(arg0, "outsecret") == 0) {
			if(cb->nf != 2)
				error("usage: outsecret secret");
			setsecret(&c->out, cb->f[1]);
			if(c->cipher)
				c->cipher->init(c);
			if(c->auth)
				c->auth->init(c);
		} else
			error("unknown control request");
		poperror();
		qunlock(c);
		free(cb);
		return n;
	case Qlog:
		cb = parsecmd(a, n);
		p = logctl(sdp, cb->nf, cb->f, logflags);
		free(cb);
		if(p != nil)
			error(p);
		return n;
	case Qcontrol:
		writecontrol(sdp->conv[CONV(ch->qid)], a, n, 0);
		return n;
	case Qdata:
		b = allocb(n);
		memmove(b->wp, a, n);
		b->wp += n;
		return writedata(sdp->conv[CONV(ch->qid)], b);
	}
}

long
sdpbwrite(Chan *ch, Block *bp, ulong offset)
{
	Sdp *sdp = sdptab + ch->dev;

	if(TYPE(ch->qid) != Qdata)
		return devbwrite(ch, bp, offset);
	return writedata(sdp->conv[CONV(ch->qid)], bp);
}

static int
sdpgen(Chan *c, char*, Dirtab*, int, int s, Dir *dp)
{
	Sdp *sdp = sdptab + c->dev;
	int type = TYPE(c->qid);
	Dirtab *dt;
	Qid qid;

	if(s == DEVDOTDOT){
		switch(TYPE(c->qid)){
		case Qtopdir:
		case Qsdpdir:
			snprint(up->genbuf, sizeof(up->genbuf), "#E%ld", c->dev);
			mkqid(&qid, Qtopdir, 0, QTDIR);
			devdir(c, qid, up->genbuf, 0, eve, 0555, dp);
			break;
		case Qconvdir:
			snprint(up->genbuf, sizeof(up->genbuf), "%d", s);
			mkqid(&qid, Qsdpdir, 0, QTDIR);
			devdir(c, qid, up->genbuf, 0, eve, 0555, dp);
			break;
		default:
			panic("sdpwalk %llux", c->qid.path);
		}
		return 1;
	}

	switch(type) {
	default:
		// non directory entries end up here
		if(c->qid.type & QTDIR)
			panic("sdpgen: unexpected directory");	
		if(s != 0)
			return -1;
		dt = dirtab[TYPE(c->qid)];
		if(dt == nil)
			panic("sdpgen: unknown type: %lud", TYPE(c->qid));
		devdir(c, c->qid, dt->name, dt->length, eve, dt->perm, dp);
		return 1;
	case Qtopdir:
		if(s != 0)
			return -1;
		mkqid(&qid, QID(0, Qsdpdir), 0, QTDIR);
		devdir(c, qid, "sdp", 0, eve, 0555, dp);
		return 1;
	case Qsdpdir:
		if(s<nelem(sdpdirtab)) {
			dt = sdpdirtab+s;
			devdir(c, dt->qid, dt->name, dt->length, eve, dt->perm, dp);
			return 1;
		}
		s -= nelem(sdpdirtab);
		if(s >= sdp->nconv)
			return -1;
		mkqid(&qid, QID(s, Qconvdir), 0, QTDIR);
		snprint(up->genbuf, sizeof(up->genbuf), "%d", s);
		devdir(c, qid, up->genbuf, 0, eve, 0555, dp);
		return 1;
	case Qconvdir:
		if(s>=nelem(convdirtab))
			return -1;
		dt = convdirtab+s;
		mkqid(&qid, QID(CONV(c->qid),TYPE(dt->qid)), 0, QTFILE);
		devdir(c, qid, dt->name, dt->length, eve, dt->perm, dp);
		return 1;
	}
}

static Conv*
sdpclone(Sdp *sdp)
{
	Conv *c, **pp, **ep;

	c = nil;
	ep = sdp->conv + nelem(sdp->conv);
	qlock(sdp);
	if(waserror()) {
		qunlock(sdp);
		nexterror();
	}
	for(pp = sdp->conv; pp < ep; pp++) {
		c = *pp;
		if(c == nil){
			c = malloc(sizeof(Conv));
			if(c == nil)
				error(Enomem);
			memset(c, 0, sizeof(Conv));
			qlock(c);
			c->sdp = sdp;
			c->id = pp - sdp->conv;
			*pp = c;
			sdp->nconv++;
			break;
		}
		if(c->ref == 0 && canqlock(c)){
			if(c->ref == 0)
				break;
			qunlock(c);
		}
	}
	poperror();
	qunlock(sdp);

	if(pp >= ep)
		return nil;

	assert(c->state == CFree);
	// set ref to 2 - 1 ref for open - 1 ref for channel state
	c->ref = 2;
	c->state = CInit;
	c->in.window = ~0;
	strncpy(c->owner, up->user, sizeof(c->owner));
	c->perm = 0660;
	qunlock(c);

	return c;
}

// assume c is locked
static void
convretryinit(Conv *c)
{
	c->retries = 0;
	// +2 to avoid rounding effects.
	c->timeout = TK2SEC(m->ticks) + 2;
}

// assume c is locked
static int
convretry(Conv *c, int reset)
{
	c->retries++;
	if(c->retries > MaxRetries) {
		if(reset)
			convoconnect(c, ConReset, c->dialid, c->acceptid);
		convsetstate(c, CClosed);
		return 0;
	}
	c->timeout = TK2SEC(m->ticks) + (c->retries+1);
	return 1;
}

// assumes c is locked
static void
convtimer(Conv *c, ulong sec)
{
	Block *b;

	if(c->timeout > sec)
		return;

	switch(c->state) {
	case CInit:
		break;
	case CDial:
		if(convretry(c, 1))
			convoconnect(c, ConOpenRequest, c->dialid, 0);
		break;
	case CAccept:
		if(convretry(c, 1))
			convoconnect(c, ConOpenAck, c->dialid, c->acceptid);
		break;
	case COpen:
		b = c->out.controlpkt;
		if(b != nil) {
			if(convretry(c, 1))
				convoput(c, TControl, ControlMesg, copyblock(b, blocklen(b)));
			break;
		}

		c->timeout = c->lastrecv + KeepAlive;
		if(c->timeout > sec)
			break;
		// keepalive - randomly spaced between KeepAlive and 2*KeepAlive
		if(c->timeout + KeepAlive > sec && nrand(c->lastrecv + 2*KeepAlive - sec) > 0)
			break;
		// can not use writecontrol
		b = allocb(4);
		c->out.controlseq++;
		hnputl(b->wp, c->out.controlseq);
		b->wp += 4;
		c->out.controlpkt = b;
		convretryinit(c);
		if(!waserror()) {
			convoput(c, TControl, ControlMesg, copyblock(b, blocklen(b)));
			poperror();
		}
		break;
	case CLocalClose:
		if(convretry(c, 0))
			convoconnect(c, ConClose, c->dialid, c->acceptid);
		break;
	case CRemoteClose:
	case CClosed:
		break;
	}
}


static void
sdpackproc(void *a)
{
	Sdp *sdp = a;
	ulong sec;
	int i;
	Conv *c;

	for(;;) {
		tsleep(&up->sleep, return0, 0, 1000);
		sec = TK2SEC(m->ticks);
		qlock(sdp);
		for(i=0; i<sdp->nconv; i++) {
			c = sdp->conv[i];
			if(c->ref == 0)
				continue;
			qunlock(sdp);
			qlock(c);
			if(c->ref > 0 && !waserror()) {
				convtimer(c, sec);
				poperror();
			}
			qunlock(c);
			qlock(sdp);
		}
		qunlock(sdp);
	}
}

Dev sdpdevtab = {
	'E',
	"sdp",

	devreset,
	sdpinit,
	devshutdown,
	sdpattach,
	sdpwalk,
	sdpstat,
	sdpopen,
	devcreate,
	sdpclose,
	sdpread,
	devbread,
	sdpwrite,
	devbwrite,
	devremove,
	devwstat,
};

// assume hold lock on c
static void
convsetstate(Conv *c, int state)
{

if(0)print("convsetstate %d: %s -> %s\n", c->id, convstatename[c->state], convstatename[state]);

	switch(state) {
	default:
		panic("setstate: bad state: %d", state);
	case CDial:
		assert(c->state == CInit);
		c->dialid = (rand()<<16) + rand();
		convretryinit(c);
		convoconnect(c, ConOpenRequest, c->dialid, 0);
		break;
	case CAccept:
		assert(c->state == CInit);
		c->acceptid = (rand()<<16) + rand();
		convretryinit(c);
		convoconnect(c, ConOpenAck, c->dialid, c->acceptid);
		break;
	case COpen:
		assert(c->state == CDial || c->state == CAccept);
		c->lastrecv = TK2SEC(m->ticks);
		if(c->state == CDial) {
			convretryinit(c);
			convoconnect(c, ConOpenAckAck, c->dialid, c->acceptid);
			hnputl(c->in.secret, c->acceptid);
			hnputl(c->in.secret+4, c->dialid);
			hnputl(c->out.secret, c->dialid);
			hnputl(c->out.secret+4, c->acceptid);
		} else {
			hnputl(c->in.secret, c->dialid);
			hnputl(c->in.secret+4, c->acceptid);
			hnputl(c->out.secret, c->acceptid);
			hnputl(c->out.secret+4, c->dialid);
		}
		setalg(c, "hmac_md5_96", authalg, &c->auth);
		break;
	case CLocalClose:
		assert(c->state == CAccept || c->state == COpen);
		convretryinit(c);
		convoconnect(c, ConClose, c->dialid, c->acceptid);
		break;
	case CRemoteClose:
		wakeup(&c->in.controlready);
		wakeup(&c->out.controlready);
		break;
	case CClosed:
		wakeup(&c->in.controlready);
		wakeup(&c->out.controlready);
		if(c->readproc)
			postnote(c->readproc, 1, "interrupt", 0);
		if(c->state != CClosed)
			convderef(c);
		break;
	}
	c->state = state;
}


//assumes c is locked
static void
convderef(Conv *c)
{
	c->ref--;
	if(c->ref > 0) {
		return;
	}
	assert(c->ref == 0);
	assert(c->dataopen == 0);
	assert(c->controlopen == 0);
if(0)print("convderef: %d: ref == 0!\n", c->id);
	c->state = CFree;
	if(c->chan) {	
		cclose(c->chan);
		c->chan = nil;
	}
	if(c->channame) {
		free(c->channame);
		c->channame = nil;
	}
	c->cipher = nil;
	c->auth = nil;
	c->comp = nil;
	strcpy(c->owner, "network");
	c->perm = 0660;
	c->dialid = 0;
	c->acceptid = 0;
	c->timeout = 0;
	c->retries = 0;
	c->drop = 0;
	onewaycleanup(&c->in);
	onewaycleanup(&c->out);
	memset(&c->lstats, 0, sizeof(Stats));
	memset(&c->rstats, 0, sizeof(Stats));
}

static void
onewaycleanup(OneWay *ow)
{
	if(ow->controlpkt)
		freeb(ow->controlpkt);
	if(ow->authstate)
		free(ow->authstate);
	if(ow->cipherstate)
		free(ow->cipherstate);
	if(ow->compstate)
		free(ow->compstate);
	memset(ow, 0, sizeof(OneWay));
}


// assumes conv is locked
static void
convopenchan(Conv *c, char *path)
{
	if(c->state != CInit || c->chan != nil)
		error("already connected");
	c->chan = namec(path, Aopen, ORDWR, 0);
	c->channame = smalloc(strlen(path)+1);
	strcpy(c->channame, path);
	if(waserror()) {
		cclose(c->chan);
		c->chan = nil;
		free(c->channame);
		c->channame = nil;
		nexterror();
	}
	kproc("convreader", convreader, c);

	assert(c->reader == 0 && c->ref > 0);
	// after kproc in case it fails
	c->reader = 1;
	c->ref++;

	poperror();
}

static void
convstats(Conv *c, int local, char *buf, int n)
{
	Stats *stats;
	char *p, *ep;
	int i;

	if(local) {
		stats = &c->lstats;
	} else {
		if(!waserror()) {
			writecontrol(c, 0, 0, 1);
			poperror();
		}
		stats = &c->rstats;
	}

	qlock(c);
	p = buf;
	ep = buf + n;
	p += snprint(p, ep-p, "outPackets: %lud\n", stats->outPackets);
	p += snprint(p, ep-p, "outDataPackets: %lud\n", stats->outDataPackets);
	p += snprint(p, ep-p, "outDataBytes: %lud\n", stats->outDataBytes);
	p += snprint(p, ep-p, "outCompDataBytes: %lud\n", stats->outCompDataBytes);
	for(i=0; i<NCompStats; i++) {
		if(stats->outCompStats[i] == 0)
			continue;
		p += snprint(p, ep-p, "outCompStats[%d]: %lud\n", i, stats->outCompStats[i]);
	}
	p += snprint(p, ep-p, "inPackets: %lud\n", stats->inPackets);
	p += snprint(p, ep-p, "inDataPackets: %lud\n", stats->inDataPackets);
	p += snprint(p, ep-p, "inDataBytes: %lud\n", stats->inDataBytes);
	p += snprint(p, ep-p, "inCompDataBytes: %lud\n", stats->inCompDataBytes);
	p += snprint(p, ep-p, "inMissing: %lud\n", stats->inMissing);
	p += snprint(p, ep-p, "inDup: %lud\n", stats->inDup);
	p += snprint(p, ep-p, "inReorder: %lud\n", stats->inReorder);
	p += snprint(p, ep-p, "inBadComp: %lud\n", stats->inBadComp);
	p += snprint(p, ep-p, "inBadAuth: %lud\n", stats->inBadAuth);
	p += snprint(p, ep-p, "inBadSeq: %lud\n", stats->inBadSeq);
	p += snprint(p, ep-p, "inBadOther: %lud\n", stats->inBadOther);
	USED(p);
	qunlock(c);
}

// c is locked
static void
convack(Conv *c)
{
	Block *b;
	AckPkt *ack;
	Stats *s;
	int i;

	b = allocb(sizeof(AckPkt));
	ack = (AckPkt*)b->wp;
	b->wp += sizeof(AckPkt);
	s = &c->lstats;
	hnputl(ack->cseq, c->in.controlseq);
	hnputl(ack->outPackets, s->outPackets);
	hnputl(ack->outDataPackets, s->outDataPackets);
	hnputl(ack->outDataBytes, s->outDataBytes);
	hnputl(ack->outCompDataBytes, s->outCompDataBytes);
	for(i=0; i<NCompStats; i++)
		hnputl(ack->outCompStats+i*4, s->outCompStats[i]);
	hnputl(ack->inPackets, s->inPackets);
	hnputl(ack->inDataPackets, s->inDataPackets);
	hnputl(ack->inDataBytes, s->inDataBytes);
	hnputl(ack->inCompDataBytes, s->inCompDataBytes);
	hnputl(ack->inMissing, s->inMissing);
	hnputl(ack->inDup, s->inDup);
	hnputl(ack->inReorder, s->inReorder);
	hnputl(ack->inBadComp, s->inBadComp);
	hnputl(ack->inBadAuth, s->inBadAuth);
	hnputl(ack->inBadSeq, s->inBadSeq);
	hnputl(ack->inBadOther, s->inBadOther);
	convoput(c, TControl, ControlAck, b);
}


// assume we hold lock for c
static Block *
conviput(Conv *c, Block *b, int control)
{
	int type, subtype;
	ulong seq, seqwrap;
	long seqdiff;
	int pad;

	c->lstats.inPackets++;

	if(BLEN(b) < 4) {
		c->lstats.inBadOther++;
		freeb(b);
		return nil;
	}
	
	type = b->rp[0] >> 4;
	subtype = b->rp[0] & 0xf;
	b->rp += 1;
	if(type == TConnect) {
		conviconnect(c, subtype, b);
		return nil;
	}

	switch(c->state) {
	case CInit:
	case CDial:
		c->lstats.inBadOther++;
		convoconnect(c, ConReset, c->dialid, c->acceptid);
		convsetstate(c, CClosed);
		break;
	case CAccept:
	case CRemoteClose:
	case CLocalClose:
		c->lstats.inBadOther++;
		freeb(b);
		return nil;
	}

	seq = (b->rp[0]<<16) + (b->rp[1]<<8) + b->rp[2];
	b->rp += 3;

	seqwrap = c->in.seqwrap;
	seqdiff = seq - c->in.seq;
	if(seqdiff < -(SeqMax*3/4)) {
		seqwrap++;
		seqdiff += SeqMax;
	} else if(seqdiff > SeqMax*3/4) {
		seqwrap--;
		seqdiff -= SeqMax;
	}

	if(seqdiff <= 0) {
		if(seqdiff <= -SeqWindow) {
if(0)print("old sequence number: %ld (%ld %ld)\n", seq, c->in.seqwrap, seqdiff);
			c->lstats.inBadSeq++;
			freeb(b);
			return nil;
		}

		if(c->in.window & (1<<-seqdiff)) {
if(0)print("dup sequence number: %ld (%ld %ld)\n", seq, c->in.seqwrap, seqdiff);
			c->lstats.inDup++;
			freeb(b);
			return nil;
		}

		c->lstats.inReorder++;
	}

	// ok the sequence number looks ok
if(0) print("coniput seq=%ulx\n", seq);
	if(c->in.auth != 0) {
		if(!(*c->in.auth)(&c->in, b->rp-4, BLEN(b)+4)) {
if(0)print("bad auth %ld\n", BLEN(b)+4);
			c->lstats.inBadAuth++;
			freeb(b);
			return nil;
		}
		b->wp -= c->in.authlen;
	}

	if(c->in.cipher != 0) {
		if(!(*c->in.cipher)(&c->in, b->rp, BLEN(b))) {
if(0)print("bad cipher\n");
			c->lstats.inBadOther++;
			freeb(b);
			return nil;
		}
		b->rp += c->in.cipherivlen;
		if(c->in.cipherblklen > 1) {
			pad = b->wp[-1];
			if(pad > BLEN(b)) {
if(0)print("pad too big\n");
				c->lstats.inBadOther++;
				freeb(b);
				return nil;
			}
			b->wp -= pad;
		}
	}

	// ok the packet is good
	if(seqdiff > 0) {
		while(seqdiff > 0 && c->in.window != 0) {
			if((c->in.window & (1<<(SeqWindow-1))) == 0) {
				c->lstats.inMissing++;
			}
			c->in.window <<= 1;
			seqdiff--;
		}
		if(seqdiff > 0) {
			c->lstats.inMissing += seqdiff;
			seqdiff = 0;
		}
		c->in.seq = seq;
		c->in.seqwrap = seqwrap;
	}
	c->in.window |= 1<<-seqdiff;
	c->lastrecv = TK2SEC(m->ticks);

	switch(type) {
	case TControl:
		convicontrol(c, subtype, b);
		return nil;
	case TData:
		c->lstats.inDataPackets++;
		c->lstats.inDataBytes += BLEN(b);
		if(control)
			break;
		return b;
	case TCompData:
		c->lstats.inDataPackets++;
		c->lstats.inCompDataBytes += BLEN(b);
		b = convicomp(c, subtype, seq, b);
		if(b == nil) {
			c->lstats.inBadComp++;
			return nil;
		}
		c->lstats.inDataBytes += BLEN(b);
		if(control)
			break;
		return b;
	}
if(0)print("dropping packet id=%d: type=%d n=%ld control=%d\n", c->id, type, BLEN(b), control);
	c->lstats.inBadOther++;
	freeb(b);
	return nil;
}

// assume hold conv lock
static void
conviconnect(Conv *c, int subtype, Block *b)
{
	ulong dialid;
	ulong acceptid;

	if(BLEN(b) != 8) {
		freeb(b);
		return;
	}
	dialid = nhgetl(b->rp);
	acceptid = nhgetl(b->rp + 4);
	freeb(b);

if(0)print("conviconnect: %s: %d %uld %uld\n", convstatename[c->state], subtype, dialid, acceptid);

	if(subtype == ConReset) {
		convsetstate(c, CClosed);
		return;
	}

	switch(c->state) {
	default:
		panic("unknown state: %d", c->state);
	case CInit:
		break;
	case CDial:
		if(dialid != c->dialid)
			goto Reset;
		break;
	case CAccept:
	case COpen:
	case CLocalClose:
	case CRemoteClose:
		if(dialid != c->dialid
		|| subtype != ConOpenRequest && acceptid != c->acceptid)
			goto Reset;
		break;
	case CClosed:
		goto Reset;
	}

	switch(subtype) {
	case ConOpenRequest:
		switch(c->state) {
		case CInit:
			c->dialid = dialid;
			convsetstate(c, CAccept);
			return;
		case CAccept:
		case COpen:
			// duplicate ConOpenRequest that we ignore
			return;
		}
		break;
	case ConOpenAck:
		switch(c->state) {
		case CDial:
			c->acceptid = acceptid;
			convsetstate(c, COpen);
			return;
		case COpen:
			// duplicate that we have to ack
			convoconnect(c, ConOpenAckAck, acceptid, dialid);
			return;
		}
		break;
	case ConOpenAckAck:
		switch(c->state) {
		case CAccept:
			convsetstate(c, COpen);
			return;
		case COpen:
		case CLocalClose:
		case CRemoteClose:
			// duplicate that we ignore
			return;
		}
		break;
	case ConClose:
		switch(c->state) {
		case COpen:
			convoconnect(c, ConCloseAck, dialid, acceptid);
			convsetstate(c, CRemoteClose);
			return;
		case CRemoteClose:
			// duplicate ConClose
			convoconnect(c, ConCloseAck, dialid, acceptid);
			return;
		}
		break;
	case ConCloseAck:
		switch(c->state) {
		case CLocalClose:
			convsetstate(c, CClosed);
			return;
		}
		break;
	}
Reset:
	// invalid connection message - reset to sender
if(1)print("invalid conviconnect - sending reset\n");
	convoconnect(c, ConReset, dialid, acceptid);
	convsetstate(c, CClosed);
}

static void
convicontrol(Conv *c, int subtype, Block *b)
{
	ulong cseq;
	AckPkt *ack;
	int i;

	if(BLEN(b) < 4)
		return;
	cseq = nhgetl(b->rp);
	
	switch(subtype){
	case ControlMesg:
		if(cseq == c->in.controlseq) {
if(0)print("duplicate control packet: %ulx\n", cseq);
			// duplicate control packet
			freeb(b);
			if(c->in.controlpkt == nil)
				convack(c);
			return;
		}

		if(cseq != c->in.controlseq+1)
			return;
		c->in.controlseq = cseq;
		b->rp += 4;
		if(BLEN(b) == 0) {
			// just a ping
			freeb(b);
			convack(c);
		} else {
			c->in.controlpkt = b;
if(0) print("recv %ld size=%ld\n", cseq, BLEN(b));
			wakeup(&c->in.controlready);
		}
		return;
	case ControlAck:
		if(cseq != c->out.controlseq)
			return;
		if(BLEN(b) < sizeof(AckPkt))
			return;
		ack = (AckPkt*)(b->rp);
		c->rstats.outPackets = nhgetl(ack->outPackets);
		c->rstats.outDataPackets = nhgetl(ack->outDataPackets);
		c->rstats.outDataBytes = nhgetl(ack->outDataBytes);
		c->rstats.outCompDataBytes = nhgetl(ack->outCompDataBytes);
		for(i=0; i<NCompStats; i++)
			c->rstats.outCompStats[i] = nhgetl(ack->outCompStats + 4*i);
		c->rstats.inPackets = nhgetl(ack->inPackets);
		c->rstats.inDataPackets = nhgetl(ack->inDataPackets);
		c->rstats.inDataBytes = nhgetl(ack->inDataBytes);
		c->rstats.inCompDataBytes = nhgetl(ack->inCompDataBytes);
		c->rstats.inMissing = nhgetl(ack->inMissing);
		c->rstats.inDup = nhgetl(ack->inDup);
		c->rstats.inReorder = nhgetl(ack->inReorder);
		c->rstats.inBadComp = nhgetl(ack->inBadComp);
		c->rstats.inBadAuth = nhgetl(ack->inBadAuth);
		c->rstats.inBadSeq = nhgetl(ack->inBadSeq);
		c->rstats.inBadOther = nhgetl(ack->inBadOther);
		freeb(b);
		freeb(c->out.controlpkt);
		c->out.controlpkt = nil;
		c->timeout = c->lastrecv + KeepAlive;
		wakeup(&c->out.controlready);
		return;
	}
}

static Block*
convicomp(Conv *c, int subtype, ulong seq, Block *b)
{
	if(c->in.comp == nil) {
		freeb(b);
		return nil;
	}
	if(!(*c->in.comp)(c, subtype, seq, &b))
		return nil;
	return b;
}

// c is locked
static void
convwriteblock(Conv *c, Block *b)
{
	// simulated errors
	if(c->drop && nrand(c->drop) == 0)
		return;

	if(waserror()) {
		convsetstate(c, CClosed);
		nexterror();
	}
	devtab[c->chan->type]->bwrite(c->chan, b, 0);
	poperror();
}


// assume hold conv lock
static void
convoput(Conv *c, int type, int subtype, Block *b)
{
	int pad;
	
	c->lstats.outPackets++;
	/* Make room for sdp trailer */
	if(c->out.cipherblklen > 1)
		pad = c->out.cipherblklen - (BLEN(b) + c->out.cipherivlen) % c->out.cipherblklen;
	else
		pad = 0;

	b = padblock(b, -(pad+c->out.authlen));

	if(pad) {
		memset(b->wp, 0, pad-1);
		b->wp[pad-1] = pad;
		b->wp += pad;
	}

	/* Make space to fit sdp header */
	b = padblock(b, 4 + c->out.cipherivlen);
	b->rp[0] = (type << 4) | subtype;
	c->out.seq++;
	if(c->out.seq == (1<<24)) {
		c->out.seq = 0;
		c->out.seqwrap++;
	}
	b->rp[1] = c->out.seq>>16;
	b->rp[2] = c->out.seq>>8;
	b->rp[3] = c->out.seq;
	
	if(c->out.cipher)
		(*c->out.cipher)(&c->out, b->rp+4, BLEN(b)-4);

	// auth
	if(c->out.auth) {
		b->wp += c->out.authlen;
		(*c->out.auth)(&c->out, b->rp, BLEN(b));
	}
	
	convwriteblock(c, b);
}

// assume hold conv lock
static void
convoconnect(Conv *c, int op, ulong dialid, ulong acceptid)
{
	Block *b;

	c->lstats.outPackets++;
	assert(c->chan != nil);
	b = allocb(9);
	b->wp[0] = (TConnect << 4) | op;
	hnputl(b->wp+1, dialid);
	hnputl(b->wp+5, acceptid);
	b->wp += 9;

	if(!waserror()) {
		convwriteblock(c, b);
		poperror();
	}
}

static Block *
convreadblock(Conv *c, int n)
{
	Block *b;
	Chan *ch;

	qlock(&c->readlk);
	if(waserror()) {
		c->readproc = nil;
		qunlock(&c->readlk);
		nexterror();
	}
	qlock(c);
	if(c->state == CClosed) {
		qunlock(c);
		error("closed");
	}
	c->readproc = up;
	ch = c->chan;
	assert(c->ref > 0);
	qunlock(c);

	b = devtab[ch->type]->bread(ch, n, 0);
	c->readproc = nil;
	poperror();
	qunlock(&c->readlk);

	return b;
}

static int
readready(void *a)
{
	Conv *c = a;

	return c->in.controlpkt != nil || (c->state == CClosed) || (c->state == CRemoteClose);
}

static Block *
readcontrol(Conv *c, int n)
{
	Block *b;

	USED(n);

	qlock(&c->in.controllk);
	if(waserror()) {
		qunlock(&c->in.controllk);
		nexterror();
	}
	qlock(c);	// this lock is not held during the sleep below

	for(;;) {
		if(c->chan == nil || c->state == CClosed) {
			qunlock(c);
if(0)print("readcontrol: return error - state = %s\n", convstatename[c->state]);
			error("conversation closed");
		}

		if(c->in.controlpkt != nil)
			break;

		if(c->state == CRemoteClose) {
			qunlock(c);
if(0)print("readcontrol: return nil - state = %s\n", convstatename[c->state]);
			poperror();
			return nil;
		}
		qunlock(c);
		sleep(&c->in.controlready, readready, c);
		qlock(c);
	}

	convack(c);

	b = c->in.controlpkt;
	c->in.controlpkt = nil;
	qunlock(c);
	poperror();
	qunlock(&c->in.controllk);
	return b;
}


static int
writeready(void *a)
{
	Conv *c = a;

	return c->out.controlpkt == nil || (c->state == CClosed) || (c->state == CRemoteClose);
}

// c is locked
static void
writewait(Conv *c)
{
	for(;;) {
		if(c->state == CFree || c->state == CInit ||
		   c->state == CClosed || c->state == CRemoteClose)
			error("conversation closed");

		if(c->state == COpen && c->out.controlpkt == nil)
			break;

		qunlock(c);
		if(waserror()) {
			qlock(c);
			nexterror();
		}
		sleep(&c->out.controlready, writeready, c);
		poperror();
		qlock(c);
	}
}

static void
writecontrol(Conv *c, void *p, int n, int wait)
{
	Block *b;

	qlock(&c->out.controllk);
	qlock(c);
	if(waserror()) {
		qunlock(c);
		qunlock(&c->out.controllk);
		nexterror();
	}
	writewait(c);
	b = allocb(4+n);
	c->out.controlseq++;
	hnputl(b->wp, c->out.controlseq);
	memmove(b->wp+4, p, n);
	b->wp += 4+n;
	c->out.controlpkt = b;
	convretryinit(c);
	convoput(c, TControl, ControlMesg, copyblock(b, blocklen(b)));
	if(wait)
		writewait(c);
	poperror();
	qunlock(c);
	qunlock(&c->out.controllk);
}

static Block *
readdata(Conv *c, int n)
{
	Block *b;
	int nn;

	for(;;) {

		// some slack for tunneling overhead
		nn = n + 100;

		// make sure size is big enough for control messages
		if(nn < 1000)
			nn = 1000;
		b = convreadblock(c, nn);
		if(b == nil)
			return nil;
		qlock(c);
		if(waserror()) {
			qunlock(c);
			return nil;
		}
		b = conviput(c, b, 0);
		poperror();
		qunlock(c);
		if(b != nil) {
			if(BLEN(b) > n)
				b->wp = b->rp + n;
			return b;
		}
	}
}

static long
writedata(Conv *c, Block *b)
{
	int n;
	ulong seq;
	int subtype;

	qlock(c);
	if(waserror()) {
		qunlock(c);
		nexterror();
	}

	if(c->state != COpen) {
		freeb(b);
		error("conversation not open");
	}

	n = BLEN(b);
	c->lstats.outDataPackets++;
	c->lstats.outDataBytes += n;

	if(c->out.comp != nil) {
		// must generate same value as convoput
		seq = (c->out.seq + 1) & (SeqMax-1);

		subtype = (*c->out.comp)(c, 0, seq, &b);
		c->lstats.outCompDataBytes += BLEN(b);
		convoput(c, TCompData, subtype, b);
	} else
		convoput(c, TData, 0, b);

	poperror();
	qunlock(c);
	return n;
}

static void
convreader(void *a)
{
	Conv *c = a;
	Block *b;

	qlock(c);
	assert(c->reader == 1);
	while(c->dataopen == 0 && c->state != CClosed) {
		qunlock(c);
		b = nil;
		if(!waserror()) {
			b = convreadblock(c, 2000);
			poperror();
		}
		qlock(c);
		if(b == nil) {
			if(strcmp(up->errstr, Eintr) != 0) {
				convsetstate(c, CClosed);
				break;
			}
		} else if(!waserror()) {
			conviput(c, b, 1);
			poperror();
		}
	}
	c->reader = 0;
	convderef(c);
	qunlock(c);
	pexit("hangup", 1);
}


/* ciphers, authenticators, and compressors  */

static void
setalg(Conv *c, char *name, Algorithm *alg, Algorithm **p)
{
	for(; alg->name; alg++)
		if(strcmp(name, alg->name) == 0)
			break;
	if(alg->name == nil)
		error("unknown algorithm");

	*p = alg;
	alg->init(c);
}

static void
setsecret(OneWay *ow, char *secret)
{
	char *p;
	int i, c;
	
	i = 0;
	memset(ow->secret, 0, sizeof(ow->secret));
	for(p=secret; *p; p++) {
		if(i >= sizeof(ow->secret)*2)
			break;
		c = *p;
		if(c >= '0' && c <= '9')
			c -= '0';
		else if(c >= 'a' && c <= 'f')
			c -= 'a'-10;
		else if(c >= 'A' && c <= 'F')
			c -= 'A'-10;
		else
			error("bad character in secret");
		if((i&1) == 0)
			c <<= 4;
		ow->secret[i>>1] |= c;
		i++;
	}
}

static void
setkey(uchar *key, int n, OneWay *ow, char *prefix)
{
	uchar ibuf[SHA1dlen], obuf[MD5dlen], salt[10];
	int i, round = 0;

	while(n > 0){
		for(i=0; i<round+1; i++)
			salt[i] = 'A'+round;
		sha1((uchar*)prefix, strlen(prefix), ibuf, sha1(salt, round+1, nil, nil));
		md5(ibuf, SHA1dlen, obuf, md5(ow->secret, sizeof(ow->secret), nil, nil));
		i = (n<MD5dlen) ? n : MD5dlen;
		memmove(key, obuf, i);
		key += i;
		n -= i;
		if(++round > sizeof salt)
			panic("setkey: you ask too much");
	}
}

static void
cipherfree(Conv *c)
{
	if(c->in.cipherstate) {
		free(c->in.cipherstate);
		c->in.cipherstate = nil;
	}
	if(c->out.cipherstate) {
		free(c->out.cipherstate);
		c->out.cipherstate = nil;
	}
	c->in.cipher = nil;
	c->in.cipherblklen = 0;
	c->out.cipherblklen = 0;
	c->in.cipherivlen = 0;
	c->out.cipherivlen = 0;
}

static void
authfree(Conv *c)
{
	if(c->in.authstate) {
		free(c->in.authstate);
		c->in.authstate = nil;
	}
	if(c->out.authstate) {
		free(c->out.authstate);
		c->out.authstate = nil;
	}
	c->in.auth = nil;
	c->in.authlen = 0;
	c->out.authlen = 0;
}

static void
compfree(Conv *c)
{
	if(c->in.compstate) {
		free(c->in.compstate);
		c->in.compstate = nil;
	}
	if(c->out.compstate) {
		free(c->out.compstate);
		c->out.compstate = nil;
	}
	c->in.comp = nil;
}

static void
nullcipherinit(Conv *c)
{
	cipherfree(c);
}

static int
desencrypt(OneWay *ow, uchar *p, int n)
{
	uchar *pp, *ip, *eip, *ep;
	DESstate *ds = ow->cipherstate;

	if(n < 8 || (n & 0x7 != 0))
		return 0;
	ep = p + n;
	memmove(p, ds->ivec, 8);
	for(p += 8; p < ep; p += 8){
		pp = p;
		ip = ds->ivec;
		for(eip = ip+8; ip < eip; )
			*pp++ ^= *ip++;
		block_cipher(ds->expanded, p, 0);
		memmove(ds->ivec, p, 8);
	}
	return 1;
}

static int
desdecrypt(OneWay *ow, uchar *p, int n)
{
	uchar tmp[8];
	uchar *tp, *ip, *eip, *ep;
	DESstate *ds = ow->cipherstate;

	if(n < 8 || (n & 0x7 != 0))
		return 0;
	ep = p + n;
	memmove(ds->ivec, p, 8);
	p += 8;
	while(p < ep){
		memmove(tmp, p, 8);
		block_cipher(ds->expanded, p, 1);
		tp = tmp;
		ip = ds->ivec;
		for(eip = ip+8; ip < eip; ){
			*p++ ^= *ip;
			*ip++ = *tp++;
		}
	}
	return 1;
}

static void
descipherinit(Conv *c)
{
	uchar key[8];
	uchar ivec[8];
	int i;
	int n = c->cipher->keylen;

	cipherfree(c);
	
	if(n > sizeof(key))
		n = sizeof(key);

	/* in */
	memset(key, 0, sizeof(key));
	setkey(key, n, &c->in, "cipher");
	memset(ivec, 0, sizeof(ivec));
	c->in.cipherblklen = 8;
	c->in.cipherivlen = 8;
	c->in.cipher = desdecrypt;
	c->in.cipherstate = smalloc(sizeof(DESstate));
	setupDESstate(c->in.cipherstate, key, ivec);
	
	/* out */
	memset(key, 0, sizeof(key));
	setkey(key, n, &c->out, "cipher");
	for(i=0; i<8; i++)
		ivec[i] = nrand(256);
	c->out.cipherblklen = 8;
	c->out.cipherivlen = 8;
	c->out.cipher = desencrypt;
	c->out.cipherstate = smalloc(sizeof(DESstate));
	setupDESstate(c->out.cipherstate, key, ivec);
}

static int
rc4encrypt(OneWay *ow, uchar *p, int n)
{
	CipherRc4 *cr = ow->cipherstate;

	if(n < 4)
		return 0;

	hnputl(p, cr->cseq);
	p += 4;
	n -= 4;
	rc4(&cr->current, p, n);
	cr->cseq += n;
	return 1;
}

static int
rc4decrypt(OneWay *ow, uchar *p, int n)
{
	CipherRc4 *cr = ow->cipherstate;
	RC4state tmpstate;
	ulong seq;
	long d, dd;

	if(n < 4)
		return 0;

	seq = nhgetl(p);
	p += 4;
	n -= 4;
	d = seq-cr->cseq;
	if(d == 0) {
		rc4(&cr->current, p, n);
		cr->cseq += n;
		if(cr->ovalid) {
			dd = cr->cseq - cr->lgseq;
			if(dd > RC4back)
				cr->ovalid = 0;
		}
	} else if(d > 0) {
//print("missing packet: %uld %ld\n", seq, d);
		// this link is hosed 
		if(d > RC4forward)
			return 0;
		cr->lgseq = seq;
		if(!cr->ovalid) {
			cr->ovalid = 1;
			cr->oseq = cr->cseq;
			memmove(&cr->old, &cr->current, sizeof(RC4state));
		}
		rc4skip(&cr->current, d);
		rc4(&cr->current, p, n);
		cr->cseq = seq+n;
	} else {
//print("reordered packet: %uld %ld\n", seq, d);
		dd = seq - cr->oseq;
		if(!cr->ovalid || -d > RC4back || dd < 0)
			return 0;
		memmove(&tmpstate, &cr->old, sizeof(RC4state));
		rc4skip(&tmpstate, dd);
		rc4(&tmpstate, p, n);
		return 1;
	}

	// move old state up
	if(cr->ovalid) {
		dd = cr->cseq - RC4back - cr->oseq;
		if(dd > 0) {
			rc4skip(&cr->old, dd);
			cr->oseq += dd;
		}
	}

	return 1;
}

static void
rc4cipherinit(Conv *c)
{
	uchar key[32];
	CipherRc4 *cr;
	int n;

	cipherfree(c);

	n = c->cipher->keylen;
	if(n > sizeof(key))
		n = sizeof(key);

	/* in */
	memset(key, 0, sizeof(key));
	setkey(key, n, &c->in, "cipher");
	c->in.cipherblklen = 1;
	c->in.cipherivlen = 4;
	c->in.cipher = rc4decrypt;
	cr = smalloc(sizeof(CipherRc4));
	memset(cr, 0, sizeof(*cr));
	setupRC4state(&cr->current, key, n);
	c->in.cipherstate = cr;

	/* out */
	memset(key, 0, sizeof(key));
	setkey(key, n, &c->out, "cipher");
	c->out.cipherblklen = 1;
	c->out.cipherivlen = 4;
	c->out.cipher = rc4encrypt;
	cr = smalloc(sizeof(CipherRc4));
	memset(cr, 0, sizeof(*cr));
	setupRC4state(&cr->current, key, n);
	c->out.cipherstate = cr;
}

static void
nullauthinit(Conv *c)
{
	authfree(c);
}

static void
shaauthinit(Conv *c)
{
	authfree(c);
}

static void
seanq_hmac_md5(uchar hash[MD5dlen], ulong wrap, uchar *t, long tlen, uchar *key, long klen)
{
	uchar ipad[65], opad[65], wbuf[4];
	int i;
	DigestState *digest;
	uchar innerhash[MD5dlen];

	for(i=0; i<64; i++){
		ipad[i] = 0x36;
		opad[i] = 0x5c;
	}
	ipad[64] = opad[64] = 0;
	for(i=0; i<klen; i++){
		ipad[i] ^= key[i];
		opad[i] ^= key[i];
	}
	hnputl(wbuf, wrap);
	digest = md5(ipad, 64, nil, nil);
	digest = md5(wbuf, sizeof(wbuf), nil, digest);
	md5(t, tlen, innerhash, digest);
	digest = md5(opad, 64, nil, nil);
	md5(innerhash, MD5dlen, hash, digest);
}

static int
md5auth(OneWay *ow, uchar *t, int tlen)
{
	uchar hash[MD5dlen];
	int r;

	if(tlen < ow->authlen)
		return 0;
	tlen -= ow->authlen;

	memset(hash, 0, MD5dlen);
	seanq_hmac_md5(hash, ow->seqwrap, t, tlen, (uchar*)ow->authstate, 16);
	r = memcmp(t+tlen, hash, ow->authlen) == 0;
	memmove(t+tlen, hash, ow->authlen);
	return r;
}

static void
md5authinit(Conv *c)
{
	int keylen;

	authfree(c);

	keylen = c->auth->keylen;
	if(keylen > 16)
		keylen = 16;

	/* in */
	c->in.authstate = smalloc(16);
	memset(c->in.authstate, 0, 16);
	setkey(c->in.authstate, keylen, &c->in, "auth");
	c->in.authlen = 12;
	c->in.auth = md5auth;
	
	/* out */
	c->out.authstate = smalloc(16);
	memset(c->out.authstate, 0, 16);
	setkey(c->out.authstate, keylen, &c->out, "auth");
	c->out.authlen = 12;
	c->out.auth = md5auth;
}

static void
nullcompinit(Conv *c)
{
	compfree(c);
}

static int
thwackcomp(Conv *c, int, ulong seq, Block **bp)
{
	Block *b, *bb;
	int nn;
	ulong ackseq;
	uchar mask;

	// add ack info
	b = padblock(*bp, 4);

	ackseq = unthwackstate(c->in.compstate, &mask);
	b->rp[0] = mask;
	b->rp[1] = ackseq>>16;
	b->rp[2] = ackseq>>8;
	b->rp[3] = ackseq;

	bb = allocb(BLEN(b));
	nn = thwack(c->out.compstate, bb->wp, b->rp, BLEN(b), seq, c->lstats.outCompStats);
	if(nn < 0) {
		freeb(bb);
		*bp = b;
		return ThwackU;
	} else {
		bb->wp += nn;
		freeb(b);
		*bp = bb;
		return ThwackC;
	}
}

static int
thwackuncomp(Conv *c, int subtype, ulong seq, Block **bp)
{
	Block *b, *bb;
	ulong mask;
	ulong mseq;
	int n;

	switch(subtype) {
	default:
		return 0;
	case ThwackU:
		b = *bp;
		mask = b->rp[0];
		mseq = (b->rp[1]<<16) | (b->rp[2]<<8) | b->rp[3];
		b->rp += 4;
		thwackack(c->out.compstate, mseq, mask);
		return 1;
	case ThwackC:
		bb = *bp;
		b = allocb(ThwMaxBlock);
		n = unthwack(c->in.compstate, b->wp, ThwMaxBlock, bb->rp, BLEN(bb), seq);
		freeb(bb);
		if(n < 0) {
if(0)print("unthwack failed: %d\n", n);
			freeb(b);
			return 0;
		}
		b->wp += n;
		mask = b->rp[0];
		mseq = (b->rp[1]<<16) | (b->rp[2]<<8) | b->rp[3];
		thwackack(c->out.compstate, mseq, mask);
		b->rp += 4;
		*bp = b;
		return 1;
	}
}

static void
thwackcompinit(Conv *c)
{
	compfree(c);

	c->in.compstate = malloc(sizeof(Unthwack));
	if(c->in.compstate == nil)
		error(Enomem);
	unthwackinit(c->in.compstate);
	c->out.compstate = malloc(sizeof(Thwack));
	if(c->out.compstate == nil)
		error(Enomem);
	thwackinit(c->out.compstate);
	c->in.comp = thwackuncomp;
	c->out.comp = thwackcomp;
}
