shithub: riscv

ref: 61cc0694649a4d4b109d168032f431751f57f083
dir: /sys/src/cmd/ip/torrent.c/

View raw version
#include <u.h>
#include <libc.h>
#include <mp.h>
#include <libsec.h>

typedef struct Dict Dict;
typedef struct Piece Piece;
typedef struct File File;
typedef struct Stats Stats;

struct Dict
{
	Dict	*val;
	Dict	*next;
	char	*start, *end;
	int	len;
	char	typ;	// i, d, s, l
	char	str[];
};

struct Piece
{
	uchar	*hash;
	int	len;
	int	brk;
};

struct File
{
	File	*next;
	char	*name;
	int	rfd;
	int	wfd;
	vlong	off;
	vlong	len;
};

struct Stats
{
	Lock;
	vlong	up;
	vlong	down;
	vlong	left;
};

enum {
	MAXIO = 16*1024,
	SRVPROCS = 16,
	CLIPROCS = 16,
};

int debug;
int nproc = 1;
int killgroup = -1;
int port = 6881;
char *deftrack = "http://exodus.desync.com/announce";
char *mntweb = "/mnt/web";
char *useragent = "torrent";
uchar infohash[SHA1dlen];
uchar peerid[20];
int blocksize;

int npieces;
Piece *pieces;

int nhavemap;
uchar *havemap;
int nhavepieces;

File *files;
Stats stats;

int
finished(void)
{
	return nhavepieces >= npieces;
}

void
freedict(Dict *d)
{
	if(d){
		if(d->val != d)
			freedict(d->val);
		freedict(d->next);
		free(d);
	}
}

char*
bparse(char *s, char *e, Dict **dp)
{
	char *x, t;
	Dict *d;
	int n;

	*dp = nil;
	if(s >= e)
		return e;

	t = *s;
	switch(t){
	case 'd':
	case 'l':
		x = s++;
		d = nil;
		while(s < e){
			if(*s == 'e'){
				s++;
				break;
			}
			if(t == 'd'){
				s = bparse(s, e, dp);
				if((d = *dp) == nil)
					break;
			} else
				d = *dp = mallocz(sizeof(*d), 1);
			d->typ = t;
			d->start = x;
			if(s < e){
				s = bparse(s, e, &d->val);
				dp = &d->next;
				d->end = s;
			}
			x = s;
		}
		if(d)
			d->end = s;
		return s;
	case 'i':
		x = ++s;
		if((s = memchr(x, 'e', e - x)) == nil)
			return e;
		n = s - x;
		s++;
		break;
	default:
		if((x = memchr(s, ':', e - s)) == nil)
			return e;
		x++;
		if((n = atoi(s)) < 0)
			return e;
		s = x + n;
		if((s > e) || (s < x)){
			n = e - x;
			s = e;
		}
		t = 's';
	}
	d = mallocz(sizeof(*d) + n+1, 1);
	d->typ = t;
	memmove(d->str, x, d->len = n);
	d->str[n] = 0;
	*dp = d;
	return s;
}

char*
dstr(Dict *d)
{
	if(d && (d->typ == 's' || d->typ == 'i'))
		return d->str;
	return nil;
}

Dict*
dlook(Dict *d, char *s)
{
	for(; d && d->typ == 'd'; d = d->next)
		if(d->len && strcmp(d->str, s) == 0)
			return d->val;
	return nil;
}

int
readall(int fd, char **p)
{
	int n, r;

	n = 0;
	*p = nil;
	while(*p = realloc(*p, n+1024)){
		if((r = read(fd, *p+n, 1024)) <= 0)
			break;
		n += r;
	}
	return n;
}

int
rwpiece(int wr, int index, uchar *data, int len, int poff)
{
	vlong off;
	int n, m;
	File *f;

	if(len <= 0 || poff < 0 || poff >= pieces[index].len)
		return 0;
	if(len+poff > pieces[index].len)
		len = pieces[index].len - poff;
	off = (vlong)index * (vlong)blocksize;
	off += poff;
	for(f = files; f; f = f->next)
		if((f->off+f->len) > off)
			break;
	off -= f->off;
	n = ((off + len) > f->len) ? f->len - off : len;
	if((n = (wr ? pwrite(f->wfd, data, n, off) : pread(f->rfd, data, n, off))) <= 0)
		return -1;
	if((m = rwpiece(wr, index, data + n, len - n, poff + n)) < 0)
		return -1;
	return n+m;
}

int
havepiece(int x, char *from)
{
	uchar *p, m, hash[SHA1dlen];
	int n;

	m = 0x80>>(x&7);
	if(havemap[x>>3] & m)
		return 1;
	p = malloc(blocksize);
	n = pieces[x].len;
	if(rwpiece(0, x, p, n, 0) != n){
		free(p);
		return 0;
	}
	sha1(p, n, hash, nil);
	free(p);
	if(memcmp(hash, pieces[x].hash, sizeof(hash))){
		if(debug && from != nil)
			fprint(2, "peer %s: damaged piece %d\n", from, x);
		return 0;
	}
	lock(&stats);
	if((havemap[x>>3] & m) == 0){
		havemap[x>>3] |= m;
		nhavepieces++;
		stats.left -= pieces[x].len;
	}
	unlock(&stats);
	if(debug && from != nil)
		fprint(2, "peer %s: completed piece %d\n", from, x);
	return 1;
}

int
pickpiece(uchar *map)
{
	int i, x, r, k;
	uchar m;

	r = -1;
	k = 0;
	for(i = 0; i<nhavemap; i++){
		if(map[i] == 0)
			continue;
		for(x = i<<3, m = 0x80; m; m >>= 1, x++){
			if((~map[i] | havemap[i]) & m)
				continue;
			if(nrand(++k) == 0)
				r = x;
		}
	}
	return r;
}

int
unpack(uchar *s, int n, char *fmt, ...)
{
	va_list arg;
	uchar *b, *e;

	b = s;
	e = b + n;
	va_start(arg, fmt);
	for(; *fmt; fmt++) {
		switch(*fmt){
		case '_':
			s++;
			break;
		case 'b':
			if(s+1 > e) goto Err;
			*va_arg(arg, int*) = *s++;
			break;
		case 'w':
			if(s+2 > e) goto Err;
			*va_arg(arg, int*) = s[0]<<8 | s[1];
			s += 2;
			break;
		case 'l':
			if(s+4 > e) goto Err;
			*va_arg(arg, int*) = s[0]<<24 | s[1]<<16 | s[2]<<8 | s[3];
			s += 4;
			break;
		case 'v':
			if(s+8 > e) goto Err;
			*va_arg(arg, vlong*) = 
				(vlong)s[0]<<56 | 
				(vlong)s[1]<<48 | 
				(vlong)s[2]<<40 |
				(vlong)s[3]<<32 |
				(vlong)s[4]<<24 |
				(vlong)s[5]<<16 | 
				(vlong)s[6]<<8 | 
				(vlong)s[7];
			s += 8;
			break;
		}
	}
	va_end(arg);
	return s - b;
Err:
	va_end(arg);
	return -1;
}

int
pack(uchar *s, int n, char *fmt, ...)
{
	va_list arg;
	uchar *b, *e;
	vlong v;
	int i;

	b = s;
	e = b + n;
	va_start(arg, fmt);
	for(; *fmt; fmt++) {
		switch(*fmt){
		case '_':
			i = 0;
			if(0){
		case 'b':
			i = va_arg(arg, int);
			}
			if(s+1 > e) goto Err;
			*s++ = i & 0xFF;
			break;
		case 'w':
			i = va_arg(arg, int);
			if(s+2 > e) goto Err;
			*s++ = (i>>8) & 0xFF;
			*s++ = i & 0xFF;
			break;
		case 'l':
			i = va_arg(arg, int);
			if(s+4 > e) goto Err;
			*s++ = (i>>24) & 0xFF;
			*s++ = (i>>16) & 0xFF;
			*s++ = (i>>8) & 0xFF;
			*s++ = i & 0xFF;
			break;
		case 'v':
			v = va_arg(arg, vlong);
			if(s+8 > e) goto Err;
			*s++ = (v>>56) & 0xFF;
			*s++ = (v>>48) & 0xFF;
			*s++ = (v>>40) & 0xFF;
			*s++ = (v>>32) & 0xFF;
			*s++ = (v>>24) & 0xFF;
			*s++ = (v>>16) & 0xFF;
			*s++ = (v>>8) & 0xFF;
			*s++ = v & 0xFF;
			break;
		case '*':
			i = va_arg(arg, int);
			if(s+i > e) goto Err;
			memmove(s, va_arg(arg, void*), i);
			s += i;
			break;
		}
	}
	va_end(arg);
	return s - b;
Err:
	va_end(arg);
	return -1;
}



int
peer(int fd, int incoming, char *addr)
{
	uchar buf[64+MAXIO], *map, *told, *p, m;
	int mechoking, hechoking;
	int mewant, hewant;
	int workpiece, workoffset;
	int i, o, l, x, n;

	procsetname("peer %s: %s", addr, incoming ? "incoming" : "outgoing");
	if(debug) fprint(2, "peer %s: %s connected\n", addr, incoming ? "incoming" : "outgoing");

	for(i=0; i<2; i++){
		if((incoming && i) || (!incoming && !i)){
			if(debug) fprint(2, "peer %s: -> handshake\n", addr);
			n = pack(buf, sizeof(buf), "*________**", 
				20, "\x13BitTorrent protocol",
				sizeof(infohash), infohash,
				sizeof(peerid), peerid);
			if(write(fd, buf, n) != n)
				return 1;
		}
		if((incoming && !i) || (!incoming && i)){
			n = 20 + 8 + sizeof(infohash);
			if(readn(fd, buf, n) != n)
				return 1;
			if(memcmp(buf, "\x13BitTorrent protocol", 20))
				return 0;
			if(memcmp(infohash, buf + 20 + 8, sizeof(infohash)))
				return 0;
			if(debug) fprint(2, "peer %s: <- handshake\n", addr);
		}
	}
	if(readn(fd, buf, sizeof(peerid)) != sizeof(peerid))
		return 1;
	if(memcmp(peerid, buf, sizeof(peerid)) == 0)
		return 0;
	if(debug) fprint(2, "peer %s: peerid %.*s\n", addr, sizeof(peerid), (char*)buf);

	mechoking = 1;
	hechoking = 1;
	mewant = 0;
	hewant = 0;
	workpiece = -1;
	workoffset = 0;

	map = mallocz(nhavemap, 1);
	told = malloc(nhavemap);

	if(debug) fprint(2, "peer %s: -> bitfield %d\n", addr, nhavemap);
	memmove(told, havemap, nhavemap);
	n = pack(buf, sizeof(buf), "lb*", nhavemap+1, 0x05, nhavemap, told);
	if(write(fd, buf, n) != n)
		goto Out;

	for(;;){
		for(i=0; i<nhavemap; i++){
			if(told[i] != havemap[i]){
				for(x = i<<3, m = 0x80; m; m >>= 1, x++){
					if((~havemap[i] | told[i] | map[i]) & m)
						continue;
					told[i] |= m;
					if(debug) fprint(2, "peer %s: -> have %d\n", addr, x);
					n = pack(buf, sizeof(buf), "lbl", 1+4, 0x04, x);
					if(write(fd, buf, n) != n)
						goto Out;
				}
			}
			if(!mewant && (map[i] & ~havemap[i])){
				mewant = 1;
				if(debug) fprint(2, "peer %s: -> interested\n", addr);
				n = pack(buf, sizeof(buf), "lb", 1, 0x02);
				if(write(fd, buf, n) != n)
					goto Out;
			}
		}
		if(!hechoking && mewant){
			x = workpiece;
			if(x < 0 || (havemap[x>>3]&(0x80>>(x&7))) != 0 || workoffset >= pieces[x].len)
				x = pickpiece(map);
			if(x >= 0){
				o = workpiece != x ? pieces[x].brk : workoffset;
				l = pieces[x].len - o;
				if(l > MAXIO)
					l = MAXIO;
				workpiece = x;
				workoffset = o + l; 
				if(debug) fprint(2, "peer %s: -> request %d %d %d\n", addr, x, o, l);
				n = pack(buf, sizeof(buf), "lblll", 1+4+4+4, 0x06, x, o, l);
				if(write(fd, buf, n) != n)
					goto Out;
			}
		}
		if(mechoking && hewant){
			mechoking = 0;
			if(debug) fprint(2, "peer %s: -> unchoke\n", addr);
			n = pack(buf, sizeof(buf), "lb", 1, 0x01);
			if(write(fd, buf, n) != n)
				goto Out;
		}

		if(readn(fd, buf, 4) != 4)
			break;
		unpack(buf, 4, "l", &n);
		if(n < 0 || n > sizeof(buf))
			break;
		if(n == 0)
			continue;
		if(readn(fd, buf, n) != n)
			break;

		n--;
		p = buf+1;
		switch(*buf){
		case 0x00:	// Choke
			hechoking = 1;
			workpiece = -1;
			if(debug) fprint(2, "peer %s: <- choke\n", addr);
			break;
		case 0x01:	// Unchoke
			hechoking = 0;
			if(debug) fprint(2, "peer %s: <- unchoke\n", addr);
			break;
		case 0x02:	// Interested
			hewant = 1;
			if(debug) fprint(2, "peer %s: <- interested\n", addr);
			break;
		case 0x03:	// Notinterested
			hewant = 0;
			if(debug) fprint(2, "peer %s: <- notinterested\n", addr);
			break;
		case 0x04:	// Have <piceindex>
			if(unpack(p, n, "l", &x) < 0)
				goto Out;
			if(debug) fprint(2, "peer %s: <- have %d\n", addr, x);
			if(x < 0 || x >= npieces)
				continue;
			map[x>>3] |= 0x80>>(x&7);
			break;
		case 0x05:	// Bitfield
			if(debug) fprint(2, "peer %s: <- bitfield %d\n", addr, n);
			if(n != nhavemap)
				continue;
			memmove(map, p, n);
			break;
		case 0x06:	// Request <index> <begin> <length>
			if(unpack(p, n, "lll", &x, &o, &l) < 0)
				goto Out;
			if(debug) fprint(2, "peer %s: <- request %d %d %d\n", addr, x, o, l);
			if(x < 0 || x >= npieces)
				continue;
			if(!hewant || mechoking || (~havemap[x>>3]&(0x80>>(x&7))))
				continue;
			if(debug) fprint(2, "peer %s: -> piece %d %d\n", addr, x, o);
			n = 4+1+4+4;
			if(l > MAXIO)
				l = MAXIO;
			if((l = rwpiece(0, x, buf + n, l, o)) <= 0)
				continue;
			n = pack(buf, sizeof(buf), "lbll", 1+4+4+l, 0x07, x, o);
			n += l;
			if(write(fd, buf, n) != n)
				goto Out;
			lock(&stats);
			stats.up += n;
			unlock(&stats);
			break;
		case 0x07:	// Piece <index> <begin> <block>
			if(unpack(p, n, "ll", &x, &o) != 8)
				goto Out;
			p += 8;
			n -= 8;
			lock(&stats);
			stats.down += n;
			unlock(&stats);
			if(debug) fprint(2, "peer %s: <- piece %d %d %d\n", addr, x, o, n);
			if(x < 0 || x >= npieces)
				continue;
			if((havemap[x>>3]&(0x80>>(x&7))) != 0)
				continue;
			if(o < 0 || o >= pieces[x].len)
				continue;
			if(o+n > pieces[x].len)
				n = o - pieces[x].len;
			if((o > pieces[x].brk) || (o+n <= pieces[x].brk))
				continue;
			n = rwpiece(1, x, p, n, o);
			if(n <= 0)
				continue;
			pieces[x].brk = o+n;
			if(o+n >= pieces[x].len && !havepiece(x, addr)){
				pieces[x].brk = 0;
				/* backoff from this piece for a while */
				if(x == workpiece)
					workpiece = -1;
			}
			break;
		case 0x08:	// Cancel <index> <begin> <length>
			if(unpack(p, n, "lll", &x, &o, &l) < 0)
				goto Out;
			if(debug) fprint(2, "peer %s: <- cancel %d %d %d\n", addr, x, o, l);
			break;
		case 0x09:	// Port <port>
			if(unpack(p, n, "l", &x) < 0)
				goto Out;
			if(debug) fprint(2, "peer %s: <- port %d\n", addr, x);
			break;
		}
	}

Out:
	free(told);
	free(map);
	return 1;
}

void
server(void)
{
	char addr[64], adir[40], ldir[40];
	int afd, lfd, dfd, pid, nprocs;
	NetConnInfo *ni;

	afd = -1;
	nprocs = 0;
	for(port=6881; port<9000; port++){
		snprint(addr, sizeof(addr), "tcp!*!%d", port);
		if((afd = announce(addr, adir)) >= 0)
			break;
	}
	if(afd < 0){
		fprint(2, "announce: %r");
		return;
	}
	if(rfork(RFFDG|RFPROC|RFMEM))
		return;

	procsetname("server");
	for(;;){
		if((lfd = listen(adir, ldir)) < 0){
			fprint(2, "listen: %r");
			break;
		}
		while(nprocs >= SRVPROCS)
			if(waitpid() > 0)
				nprocs--;
		nprocs++;
		if(pid = rfork(RFFDG|RFPROC|RFMEM)){
			if(pid < 0)
				nprocs--;
			close(lfd);
			continue;
		}
		if((dfd = accept(lfd, ldir)) < 0){
			fprint(2, "accept: %r");
			break;
		}
		ni = getnetconninfo(ldir, dfd);
		peer(dfd, 1, ni ? ni->raddr : "???");
		if(ni) freenetconninfo(ni);
		break;	
	}
	exits(0);
}

void
client(char *ip, char *port)
{
	static Dict *peerqh, *peerqt;
	static QLock peerslk;
	static int nprocs;
	char *addr;
	Dict *d;
	int fd;

	if(ip == nil || port == nil)
		return;

	d = mallocz(sizeof(*d) + 64, 1);
	snprint(addr = d->str, 64, "tcp!%s!%s", ip, port);
	qlock(&peerslk);
	if(dlook(peerqh, addr)){
		qunlock(&peerslk);
		free(d);
		return;
	}
	d->len = strlen(addr);
	d->typ = 'd';
	d->val = d;
	/* enqueue to front */
	if((d->next = peerqh) == nil)
		peerqt = d;
	peerqh = d;
	if(nprocs >= CLIPROCS){
		qunlock(&peerslk);
		return;
	}
	nprocs++;
	qunlock(&peerslk);
	if(rfork(RFFDG|RFPROC|RFMEM|RFNOWAIT))
		return;

	for(;;){
		qlock(&peerslk);
		/* dequeue and put to tail */
		if(d = peerqh){
			if((peerqh = d->next) == nil)
				peerqt = nil;
			d->next = nil;
			if(peerqt)
				peerqt->next = d;
			else
				peerqh = d;
			peerqt = d;
		} else
			nprocs--;
		qunlock(&peerslk);
		if(d == nil)
			exits(0);
		addr = d->str;
		procsetname("client %s", addr);
		if(debug) fprint(2, "client %s\n", addr);
		if((fd = dial(addr, nil, nil, nil)) >= 0){
			peer(fd, 0, addr);
			close(fd);
		}
		sleep(1000+nrand(5000));
	}
}

int
hopen(char *url, ...)
{
	int conn, ctlfd, fd, n;
	char buf[1024+1];
	va_list arg;

	snprint(buf, sizeof buf, "%s/clone", mntweb);
	if((ctlfd = open(buf, ORDWR)) < 0)
		return -1;
	if((n = read(ctlfd, buf, sizeof buf-1)) <= 0){
		close(ctlfd);
		return -1;
	}
	buf[n] = 0;
	conn = atoi(buf);
	va_start(arg, url);
	strcpy(buf, "url ");
	n = 4+vsnprint(buf+4, sizeof(buf)-4, url, arg);
	va_end(arg);
	if(write(ctlfd, buf, n) != n){
	ErrOut:
		close(ctlfd);
		return -1;
	}
	if(useragent != nil && useragent[0] != '\0'){
		n = snprint(buf, sizeof buf, "useragent %s", useragent);
		write(ctlfd, buf, n);
	}
	snprint(buf, sizeof buf, "%s/%d/body", mntweb, conn);
	if((fd = open(buf, OREAD)) < 0)
		goto ErrOut;
	close(ctlfd);
	return fd;
}

void
webseed(Dict *w, File *f)
{
	int fd, err, n, m, o, p, x, y;
	uchar buf[MAXIO];
	vlong off, len;
	Dict *w0;
	char *s;

	if(w == nil || f == nil || finished())
		return;
	if(rfork(RFPROC|RFMEM))
		return;
	w0 = w;
	procsetname("webseed %s %s", w->str, f->name);
Retry:
	if(debug) fprint(2, "webseed %s %s\n", w->str, f->name);
	s = strrchr(w->str, '/');
	if(s && s[1] == 0)
		fd = hopen("%s%s", w->str, f->name);
	else
		fd = hopen("%s", w->str);
	if(fd < 0){
Error:
		if(debug) fprint(2, "webseed %s %s: %r\n", w->str, f->name);
		if(finished())
			exits(0);
		if((w = w->next) == w0)
			exits(0);
		goto Retry;
	}

	err = 0;
	off = f->off;
	len = f->len;
	while(len > 0 && !finished()){
		m = sizeof(buf);
		if(len < m)
			m = len;
		if((n = read(fd, buf, m)) <= 0)
			break;

		x = off / blocksize;
		p = off - (vlong)x*blocksize;
		off += n;
		len -= n;
		y = off / blocksize;

		o = 0;
		while(n > 0){
			m = pieces[x].len - p;
			if(m > n)
				m = n;
			if((havemap[x>>3] & (0x80>>(x&7))) == 0)
				rwpiece(1, x, buf+o, m, p);
			if(x == y)
				break;
			o += m;
			n -= m;
			p = 0;
			if(havepiece(x++, w->str))
				continue;
			if(++err > 10){
				close(fd);
				werrstr("file corrupted");
				goto Error;
			}
		}
	}
	if(off < f->off + f->len)
		havepiece(off / blocksize, w->str);
	havepiece(f->off / blocksize, w->str);
	close(fd);
	exits(0);
}

void
clients4(uchar *p, int len)
{
	char ip[16], port[6];

	while(len >= 6){
		len -= 6;
		snprint(ip, sizeof(ip), "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
		snprint(port, sizeof(port), "%d", p[4]<<8 | p[5]);
		p += 6;
		client(ip, port);
	}
}

void
webtracker(char *url)
{
	char *event, *p;
	Dict *d, *l;
	int n, fd;

	if(rfork(RFPROC|RFMEM))
		return;
	procsetname("webtracker %s", url);
	if(debug) fprint(2, "webtracker %s\n", url);

	event = "&event=started";
	for(;;){
		vlong up, down, left;

		lock(&stats);
		up = stats.up;
		down = stats.down;
		left = stats.left;
		unlock(&stats);

		d = nil;
		if((fd = hopen("%s?info_hash=%.*H&peer_id=%.*H&port=%d&"
			"uploaded=%lld&downloaded=%lld&left=%lld&compact=1&no_peer_id=1%s",
			url, sizeof(infohash), infohash, sizeof(peerid), peerid, port,
			up, down, left, event)) >= 0){
			event = "";
			n = readall(fd, &p);
			close(fd);
			bparse(p, p+n, &d);
			free(p);
		} else if(debug) fprint(2, "tracker %s: %r\n", url);
		/* check errors and warnings */
		if(p = dstr(dlook(d, "failure reason"))) {
			if(debug)
				fprint(2, "tracker failure: %s\n", p);
			exits(0);
		}
		if(p = dstr(dlook(d, "warning message")))
			if(debug)
				fprint(2, "tracker warning: %s\n", p);
		if(l = dlook(d, "peers")){
			if(l->typ == 's')
				clients4((uchar*)l->str, l->len);
			else for(; l && l->typ == 'l'; l = l->next)
				client(dstr(dlook(l->val, "ip")), dstr(dlook(l->val, "port")));
		}
		n = 0;
		if(p = dstr(dlook(d, "interval")))
			n = atoi(p);
		if(n < 10 | n > 60*60)
			n = 2*60;
		freedict(d);
		sleep(n * 1000 + nrand(5000));
	}
}

int
udpaddr(char addr[64], int naddr, char *url)
{
	int port;
	char *x;

	if((url = strchr(url, ':')) == nil)
		return -1;
	url++;
	while(*url == '/')
		url++;
	if(x = strchr(url, ':')){
		port = atoi(x+1);
	} else {
		port = 80;
		if((x = strchr(url, '/')) == nil)
			x = strchr(url, 0);
	}
	snprint(addr, naddr, "udp!%.*s!%d", utfnlen(url, x-url), url, port);
	return 0;
}

void
udptracker(char *url)
{
	int fd, event, n, m, a, i;
	int transid, interval;
	vlong connid;
	uchar buf[MAXIO];
	char addr[64];

	if(udpaddr(addr, sizeof(addr), url) < 0)
		return;
	if(rfork(RFPROC|RFMEM))
		return;
	procsetname("udptracker %s", addr);
	if(debug) fprint(2, "udptracker %s\n", addr);

	event = 1;
	for(;;){
		alarm(30000);
		if((fd = dial(addr, 0, 0, 0)) < 0)
			goto Sleep;

		/* connect */
		transid = rand();
		n = pack(buf, sizeof(buf), "vll", 0x41727101980LL, 0, transid);
		if(write(fd, buf, n) != n)
			goto Sleep;
		for(;;){
			if((n = read(fd, buf, sizeof(buf))) <= 0)
				goto Sleep;
			if(unpack(buf, n, "llv", &a, &i, &connid) < 0)
				continue;
			if(a == 0 && i == transid)
				break;
		}
		alarm(0);

		/* announce */
		transid = rand();
		lock(&stats);
		n = pack(buf, sizeof(buf), "vll**vvvl____llw",
			connid, 1, transid,
			sizeof(infohash), infohash,
			sizeof(peerid), peerid,
			stats.down,
			stats.left,
			stats.up,
			event,
			0, -1,
			port);
		unlock(&stats);

		interval = 0;
		alarm(30000);
		if(write(fd, buf, n) != n)
			goto Sleep;
		for(;;){
			if((n = read(fd, buf, sizeof(buf))) <= 0)
				goto Sleep;
			if((m = unpack(buf, n, "lll________", &a, &i, &interval)) < 0)
				continue;
			if(a == 1 && i == transid){
				clients4(buf+m, n - m);
				break;
			}
		}
		event = 0;
Sleep:
		alarm(0);
		if(fd >= 0)
			close(fd);
		if(interval < 10 | interval > 60*60)
			interval = 2*60;
		sleep(interval * 1000 + nrand(5000));
	}
}

void
tracker(char *url)
{
	static Dict *trackers;
	static QLock trackerslk;
	Dict *d;
	int n;

	if(url == nil)
		return;
	qlock(&trackerslk);
	if(dlook(trackers, url)){
		qunlock(&trackerslk);
		return;
	}
	n = strlen(url);
	d = mallocz(sizeof(*d) + n+1, 1);
	strcpy(d->str, url);
	d->len = n;
	d->typ = 'd';
	d->val = d;
	d->next = trackers;
	trackers = d;
	url = d->str;
	qunlock(&trackerslk);
	if(!cistrncmp(url, "udp:", 4))
		udptracker(url);
	else
		webtracker(url);
}

int
Hfmt(Fmt *f)
{
	uchar *s, *e;
	s = va_arg(f->args, uchar*);
	if(f->flags & FmtPrec)
		e = s + f->prec;
	else
		e = s + strlen((char*)s);
	for(; s < e; s++)
		if(fmtprint(f, *s && ((*s >= '0' && *s <= '9') || 
			(*s >= 'a' && *s <= 'z') ||
			(*s >= 'A' && *s <= 'Z') || 
			strchr(".-_~", *s)) ? "%c" : "%%%.2x", *s) < 0)
			return -1;
	return 0;
}

int
mktorrent(int fd, Dict *alist, Dict *wlist)
{
	uchar *b, h[SHA1dlen];
	Dir *d;
	int n;

	if((d = dirfstat(fd)) == nil)
		return -1;
	if(d->qid.type & QTDIR){
		free(d);
		werrstr("file is a directory");
		return -1;
	}
	if(d->length == 0){
		free(d);
		werrstr("empty file");
		return -1;
	}
	for(blocksize = 256*1024;;blocksize<<=1){
		npieces = (d->length + blocksize-1) / blocksize;
		if(npieces <= 8*1024 || blocksize >= 2*1024*1024)
			break;
	}

	/*
	 * keys in dictionaries have to be ordered alphabetically
	 */
	print("d8:announce%ld:%s", strlen(alist->str), alist->str);
	if(alist->next){
		print("13:announce-listl");
		print("l%ld:%se", strlen(alist->str), alist->str);
		for(alist = alist->next; alist; alist = alist->next)
			print("l%ld:%se", strlen(alist->str), alist->str);
		print("e");
	}

	print("4:infod");
	print("6:lengthi%llde", d->length);
	print("4:name%ld:%s", strlen(d->name), d->name);
	print("12:piece lengthi%de", blocksize);
	print("6:pieces%d:", npieces*sizeof(h));
	free(d);
	b = malloc(blocksize);
	while((n = readn(fd, b, blocksize)) > 0){
		sha1(b, n, h, nil);
		if(write(1, h, sizeof(h)) != sizeof(h)){
			free(b);
			return -1;
		}
		npieces--;
	}
	if(npieces){
		werrstr("read failed: %r");
		return -1;
	}
	free(b);
	print("e");

	if(wlist){
		if(wlist->next){
			print("8:url-listl");
			for(; wlist; wlist = wlist->next)
				print("%ld:%s", strlen(wlist->str), wlist->str);
			print("e");
		} else
			print("8:url-list%ld:%s", strlen(wlist->str), wlist->str);
	}
	print("e");

	return 0;
}

int
mkdirs(char *s)
{
	char *p;
	int f;

	if(access(s, AEXIST) == 0)
		return 0;
	for(p=strchr(s+1, '/'); p; p=strchr(p+1, '/')){
		*p = 0;
		if(access(s, AEXIST)){
			if((f = create(s, OREAD, DMDIR | 0777)) < 0){
				*p = '/';
				return -1;
			}
			close(f);
		}
		*p = '/';
	}
	return 0;
}

char*
fixnamedup(char *s)
{
	int n, l;
	char *d;
	Rune r;

	n = 0;
	d = strdup(s);
	l = strlen(d);
	while(*s){
		s += chartorune(&r, s);
		if(r == ' ')
			r = 0xa0;
		if((n + runelen(r)) >= l){
			l += 64;
			d = realloc(d, l);
		}
		n += runetochar(d + n, &r);
	}
	d[n] = 0;
	return cleanname(d);
}

int
catch(void *, char *msg)
{
	if(strstr(msg, "alarm"))
		return 1;
	postnote(PNGROUP, killgroup, "kill");
	return 0;
}

void
usage(void)
{
	fprint(2, "usage: %s [ -vsdpc ] [ -m mtpt ] [ -t tracker-url ] "
		  "[ -w webseed-url ] [ -i peerid ] [ -A useragent ] [ file ]\n", argv0);
	exits("usage");
}

Dict*
scons(char *s, Dict *t)
{
	Dict *l;

	if(s == nil)
		return t;
	for(l = t; l; l = l->next)
		if(strcmp(l->str, s) == 0)
			return t;
	l = mallocz(sizeof(*l) + strlen(s)+1, 1);
	l->next = t;
	strcpy(l->str, s);
	return l;
}

void
main(int argc, char *argv[])
{
	int sflag, pflag, vflag, cflag, fd, i, n;
	Dict *alist, *wlist, *info, *torrent, *d, *l;
	char *p, *s, *e;
	File **fp, *f;
	vlong len;

	fmtinstall('H', Hfmt);
	alist = wlist = nil;
	sflag = pflag = vflag = cflag = 0;
	ARGBEGIN {
	case 'm':
		mntweb = EARGF(usage());
		break;
	case 't':
		alist = scons(EARGF(usage()), alist);
		break;
	case 'w':
		wlist = scons(EARGF(usage()), wlist);
		break;
	case 's':
		sflag = 1;
		break;
	case 'p':
		pflag = 1;
		break;
	case 'v':
		vflag = 1;
		break;
	case 'c':
		cflag = 1;
		break;
	case 'd':
		debug++;
		break;
	case 'i':
		strncpy((char*)peerid, EARGF(usage()), sizeof(peerid));
		break;
	case 'A':
		useragent = EARGF(usage());
		break;
	default:
		usage();
	} ARGEND;

	if((s = getenv("NPROC")) != 0){
		if((nproc = atoi(s)) <= 0)
			nproc = 1;
		free(s);
	}

	fd = 0;
	if(*argv)
		if((fd = open(*argv, OREAD)) < 0)
			sysfatal("open: %r");
	if(cflag){
		if(alist == nil)
			alist = scons(deftrack, alist);
		if(mktorrent(fd, alist, wlist) < 0)
			sysfatal("%r");
		exits(0);
	}
	if((n = readall(fd, &p)) <= 0)
		sysfatal("read torrent: %r");
	bparse(p, p+n, &torrent);

	alist = scons(dstr(dlook(torrent, "announce")), alist);
	for(d = dlook(torrent, "announce-list"); d && d->typ == 'l'; d = d->next)
		for(l = d->val; l && l->typ == 'l'; l = l->next)
			alist = scons(dstr(l->val), alist);

	if(d = dlook(torrent, "url-list")){
		if(d->typ == 's')
			wlist = scons(dstr(d), wlist);
		else for(l = d; l && l->typ == 'l'; l = l->next)
			wlist = scons(dstr(l->val), wlist);
		/* make wlist into a ring */
		for(l = wlist; l && l->next; l = l->next)
			;
		if(l) l->next = wlist;
	}

	if(alist == nil && wlist == nil)
		sysfatal("no trackers or webseeds in torrent");

	if((d = info = dlook(torrent, "info")) == nil)
		sysfatal("no meta info in torrent");
	for(s = e = d->start; d && d->typ == 'd'; d = d->next)
		e = d->end;
	sha1((uchar*)s, e - s, (uchar*)infohash, nil);
	free(p);

	fp = &files;
	if(d = dlook(info, "files")){		
		for(; d && d->typ == 'l'; d = d->next){
			Dict *di;

			if((s = dstr(dlook(d->val, "length"))) == nil)
				continue;
			f = mallocz(sizeof(*f), 1);
			f->len = atoll(s);
			f->name = dstr(dlook(info, "name"));
			f->rfd = f->wfd = -1;
			for(di = dlook(d->val, "path"); di && di->typ == 'l'; di = di->next)
				if(s = dstr(di->val))
					f->name = f->name ? smprint("%s/%s", f->name, s) : s;
			*fp = f;
			fp = &f->next;
		}
	} else if(s = dstr(dlook(info, "length"))){
		f = mallocz(sizeof(*f), 1);
		f->len = atoll(s);
		f->name = dstr(dlook(info, "name"));
		f->rfd = f->wfd = -1;
		*fp = f;
	}
	len = 0;
	for(f = files; f; f = f->next){
		if(f->name == nil || f->len <= 0)
			sysfatal("bogus file entry in meta info");
		s = fixnamedup(f->name);
		if(vflag) fprint(pflag ? 2 : 1, "%s\n", s);
		if((f->rfd = open(s, OREAD)) < 0){
			if(mkdirs(s) < 0)
				sysfatal("mkdirs: %r");
			if((f->wfd = f->rfd = create(s, ORDWR, 0666)) < 0)
				sysfatal("create: %r");
		}
		f->off = len;
		len += f->len;
	}
	if(len <= 0)
		sysfatal("no files in torrent");

	if((s = dstr(dlook(info, "piece length"))) == nil)
		sysfatal("missing piece length in meta info");
	if((blocksize = atoi(s)) <= 0)
		sysfatal("bogus piece length in meta info");
	d = dlook(info, "pieces");
	if(d == nil || d->typ != 's' || d->len <= 0 || d->len % SHA1dlen)
		sysfatal("bad or no pices in meta info");
	npieces = d->len / SHA1dlen;
	pieces = mallocz(sizeof(Piece) * npieces, 1);
	nhavemap = (npieces+7) / 8;
	havemap = mallocz(nhavemap, 1);
	for(i = 0; i<npieces; i++){
		pieces[i].hash = (uchar*)d->str + i*SHA1dlen;
		if(len < blocksize)
			pieces[i].len = len;
		else
			pieces[i].len = blocksize;
		len -= pieces[i].len;
		stats.left += pieces[i].len;
	}
	if(len)
		sysfatal("pieces do not match file length");

	for(i=0; i<nproc; i++){
		switch(rfork(RFPROC|RFMEM)){
		case -1:
			sysfatal("fork: %r");
		case 0:
			for(; i<npieces; i+=nproc)
				havepiece(i, nil);
			exits(0);
		}
	}
	while(waitpid() >= 0)
		;

	if(finished()){
		if(!sflag)
			exits(nil);
	} else for(f = files; f; f = f->next)
		if(f->wfd < 0 && (open(fixnamedup(f->name), OWRITE) < 0))
			sysfatal("create: %r");

	srand(truerand());
	switch(i = rfork(RFPROC|RFMEM|RFNOTEG)){
	case -1:
		sysfatal("fork: %r");
	case 0:
		if(peerid[0] == 0)
			strncpy((char*)peerid, "-NF9001-", 9);
		for(i=sizeof(peerid)-1; i >= 0 && peerid[i] == 0; i--)
			peerid[i] = nrand(10)+'0';
		server();
		for(; alist; alist = alist->next)
			tracker(alist->str);
		for(f = files, l = wlist; f && l; f = f->next, l = l->next)
			webseed(l, f);
		while(waitpid() != -1)
			;
		break;
	default:
		killgroup = i;
		atnotify(catch, 1);
		do {
			sleep(1000);
			if(pflag)
				print("%d %d\n", nhavepieces, npieces);
		} while(!finished() || sflag);
	}
	postnote(PNGROUP, killgroup, "kill");
	exits(0);
}