From bba834adeec95f66de642d43106112524aa48195 Mon Sep 17 00:00:00 2001 From: cinap_lenrek Date: Sun, 23 Oct 2011 00:55:56 +0200 Subject: [PATCH] ip/torrent: add minimalistic BitTorrent client --- sys/src/cmd/ip/mkfile | 1 + sys/src/cmd/ip/torrent.c | 817 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 818 insertions(+) create mode 100644 sys/src/cmd/ip/torrent.c diff --git a/sys/src/cmd/ip/mkfile b/sys/src/cmd/ip/mkfile index 0d3fea656..3380e7fdb 100644 --- a/sys/src/cmd/ip/mkfile +++ b/sys/src/cmd/ip/mkfile @@ -20,6 +20,7 @@ TARG = 6in4\ tftpd\ tftpfs\ traceroute\ + torrent\ udpecho\ wol\ diff --git a/sys/src/cmd/ip/torrent.c b/sys/src/cmd/ip/torrent.c new file mode 100644 index 000000000..cf5304274 --- /dev/null +++ b/sys/src/cmd/ip/torrent.c @@ -0,0 +1,817 @@ +#include +#include +#include +#include + +typedef struct Dict Dict; +typedef struct Piece Piece; +typedef struct File File; + +struct Dict +{ + char typ; // i, d, s, l + Dict *val; + Dict *next; + char *start, *end; + int len; + char str[]; +}; + +struct Piece +{ + uchar *hash; + int len; + int brk; +}; + +struct File +{ + File *next; + char *name; + int fd; + vlong off; + vlong len; +}; + +enum { + MAXIO = 16*1024, +}; + +int debug, sflag, pflag, vflag; +int pidgroup = -1; +int port = 48123; +char *mntweb = "/mnt/web"; +uchar infohash[20]; +uchar peerid[20]; +int blocksize; + +int npieces; +Piece *pieces; + +int nhavemap; +uchar *havemap; + +File *files; + +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 >= pieces[index].len) + return 0; + if(len+poff > pieces[index].len) + len = pieces[index].len - poff; + off = (vlong)index * 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->fd, data, n, off) : pread(f->fd, 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) +{ + uchar *p, m, hash[20]; + 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, 20)) + return 0; + havemap[x>>3] |= m; + return 1; +} + +int +pickpiece(uchar *map) +{ + int i, x, r, k; + uchar m; + + r = -1; + k = 0; + for(i = 0; i>= 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 '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; + } + } + 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; + 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 '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 '*': + i = va_arg(arg, int); + if(s+i > e) goto Err; + memmove(s, va_arg(arg, uchar*), i); + s += i; + break; + } + } + va_end(arg); + return s - b; +Err: + va_end(arg); + return -1; +} + +void +peer(char *ip, char *port) +{ + static Dict *peers; + static QLock peerslk; + + uchar buf[64+MAXIO], *map, *told, *p, m; + char *addr; + int retry, i, o, l, x, n, fd; + int mechoking, hechoking; + int mewant, hewant; + int workpiece; + Dict *d; + + 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(peers, addr)){ + qunlock(&peerslk); + free(d); + return; + } + d->len = strlen(addr); + d->typ = 'd'; + d->val = d; + d->next = peers; + peers = d; + qunlock(&peerslk); + + if(rfork(RFFDG|RFPROC|RFMEM) <= 0) + return; + + fd = -1; + retry = 0; + map = malloc(nhavemap); + told = malloc(nhavemap); +Retry: + if(fd >= 0){ + close(fd); + sleep(10000 + nrand(5000)); + } + if(++retry >= 10) + goto Exit; + + if(debug) fprint(2, "dial %s\n", addr); + if((fd = dial(addr, nil, nil, nil)) < 0) + goto Retry; + + 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) + goto Retry; + + if(read(fd, buf, 1) != 1) + goto Retry; + n = buf[0] + 8 + sizeof(infohash) + sizeof(peerid); + if((n = readn(fd, buf+1, n)) != n) + goto Retry; + if(debug) fprint(2, "peer %s: <- handshake %.*s\n", addr, buf[0], (char*)buf+1); + if(memcmp(infohash, buf + 1 + buf[0] + 8, sizeof(infohash))) + goto Exit; + + 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, havemap); + if(write(fd, buf, n) != n) + goto Retry; + + mechoking = 1; + hechoking = 1; + mewant = 0; + hewant = 0; + workpiece = -1; + memset(map, 0, nhavemap); + for(;;){ + for(i=0; i>= 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 Retry; + } + } + 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 Retry; + } + } + if(!hechoking && mewant){ + x = workpiece; + if(x >= 0 && pieces[x].brk < pieces[x].len) + {} + else x = pickpiece(map); + if(x >= 0){ + o = pieces[x].brk; + l = pieces[x].len - o; + if(l > MAXIO) + l = MAXIO; + 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 Retry; + workpiece = x; + } + } + 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 Retry; + } + + if(readn(fd, buf, 4) != 4) + goto Retry; + unpack(buf, 4, "l", &n); + if(n == 0) + continue; + if(n < 0 || n > sizeof(buf)) + goto Retry; + if(readn(fd, buf, n) != n) + goto Retry; + retry = 0; + p = buf+1; + n--; + 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 + if(unpack(p, n, "l", &x) < 0) + goto Retry; + 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 + if(unpack(p, n, "lll", &x, &o, &l) < 0) + goto Retry; + 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 Retry; + break; + case 0x07: // Piece + if(unpack(p, n, "ll", &x, &o) != 8) + goto Retry; + p += 8; + n -= 8; + if(debug) fprint(2, "peer %s: <- piece %d %d %d\n", addr, x, o, n); + if(x < 0 || x >= npieces) + continue; + if((pieces[x].brk != o) || (havemap[x>>3]&(0x80>>(x&7)))) + continue; + if(rwpiece(1, x, p, n, o) == n){ + if((pieces[x].brk = o+n) == pieces[x].len){ + if(!havepiece(x)) + pieces[x].brk = 0; + } + } + break; + case 0x08: // Cancel + if(unpack(p, n, "lll", &x, &o, &l) < 0) + goto Retry; + if(debug) fprint(2, "peer %s: <- cancel %d %d %d\n", addr, x, o, l); + break; + case 0x09: // Port + if(unpack(p, n, "l", &x) < 0) + goto Retry; + if(debug) fprint(2, "peer %s: <- port %d\n", addr, x); + break; + } + } +Exit: + free(told); + free(map); + exits(0); +} + +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; + } + snprint(buf, sizeof buf, "%s/%d/body", mntweb, conn); + if((fd = open(buf, OREAD)) < 0) + goto ErrOut; + close(ctlfd); + return fd; +} + +void +tracker(char *url) +{ + static Dict *trackers; + static QLock trackerslk; + + int n, fd; + char *p; + Dict *d, *l; + + 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(rfork(RFFDG|RFPROC|RFMEM) <= 0) + return; + + for(;;){ + d = nil; + if((fd = hopen("%s?info_hash=%.*H&peer_id=%.*H&port=%d&compact=1", + url, sizeof(infohash), infohash, sizeof(peerid), peerid, port)) >= 0){ + n = readall(fd, &p); + close(fd); + bparse(p, p+n, &d); + free(p); + } + if(l = dlook(d, "peers")){ + if(l->typ == 's'){ + uchar *b, *e; + + b = (uchar*)l->str; + e = b + l->len; + for(; b+6 <= e; b += 6){ + char ip[16], port[6]; + + snprint(ip, sizeof(ip), "%d.%d.%d.%d", b[0], b[1], b[2], b[3]); + snprint(port, sizeof(port), "%d", b[4]<<8 | b[5]); + peer(ip, port); + } + } else for(; l && l->typ == 'l'; l = l->next) + peer(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 +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 >= '0' && *s <= '9') || + (*s >= 'a' && *s <= 'z') || + (*s >= 'A' && *s <= 'Z') || + strchr(".-_~", *s)) ? "%c" : "%%%.2x", *s) < 0) + return -1; + return 0; +} + +int +progress(void) +{ + int i, c; + uchar m; + c = 0; + for(i=0; i>=1) + if(havemap[i] & m) + c++; + if(pflag) + print("%d %d\n", c, npieces); + return c == npieces; +} + +void +killcohort(void) +{ + int i; + for(i=0;i!=3;i++){ /* It's a long way to the kitchen */ + postnote(PNGROUP, pidgroup, "kill"); + sleep(1); + } +} + +int +catchnote(void *, char *msg) +{ + exits(msg); + return 0; +} + +void +usage(void) +{ + fprint(2, "usage: %s [ -vsdp ] [ -m mtpt ] [ torrentfile ]\n", argv0); + exits("usage"); +} + +void +main(int argc, char *argv[]) +{ + Dict *info, *torrent, *d; + File **fp, *f; + char *p, *s, *e; + int fd, i, n; + vlong len; + + fmtinstall('H', Hfmt); + + ARGBEGIN { + case 'm': + mntweb = EARGF(usage()); + break; + case 's': + sflag = 1; + break; + case 'p': + pflag = 1; + break; + case 'v': + vflag = 1; + break; + case 'd': + debug++; + break; + default: + usage(); + } ARGEND; + + fd = 0; + if(*argv) + if((fd = open(*argv, OREAD)) < 0) + sysfatal("open torrent: %r"); + if((n = readall(fd, &p)) <= 0) + sysfatal("read torrent: %r"); + bparse(p, p+n, &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")); + 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")); + *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"); + if(vflag) fprint(pflag ? 2 : 1, "%s\n", f->name); + if((f->fd = open(f->name, ORDWR)) < 0) + if((f->fd = create(f->name, 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 % 20) + sysfatal("bad or no pices in meta info"); + npieces = d->len / 20; + pieces = mallocz(sizeof(Piece) * npieces, 1); + nhavemap = (npieces+7) / 8; + havemap = mallocz(nhavemap, 1); + for(i = 0; istr + i*20; + if(len < blocksize) + pieces[i].len = len; + else + pieces[i].len = blocksize; + len -= pieces[i].len; + } + if(len) + sysfatal("pieces do not match file length"); + + for(i = 0; ityp == 'l'; d = d->next) + if(d->val && d->val->typ == 'l') + tracker(dstr(d->val->val)); + break; + default: + pidgroup = i; + atexit(killcohort); + atnotify(catchnote, 1); + while(!progress() || sflag) + sleep(1000); + } + exits(0); +}