Websocket again, now with style(6) and mkfile. Requires 9webdraw js in /usr/web. Reference: /n/atom/patch/applied/websocket2 Date: Mon Feb 10 21:11:29 CET 2014 Signed-off-by: root@davidrhoskin.com --- /sys/man/8/websocket Thu Jan 1 00:00:00 1970 +++ /sys/man/8/websocket Mon Feb 10 21:10:09 2014 @@ -0,0 +1,65 @@ +.TH WEBSOCKET 8 +.SH NAME +websocket \- a 9P-over-websocket bridge for httpd(8) +.SH SYNOPSIS +.B websocket +.I "magic parameters" ... +.PP +.B +new WebSocket("http://server.example/magic/websocket", "9p"); +.SH DESCRIPTION +.I Websocket +is an +.IR httpd (8) +.I magic +program that tunnels a 9P connection over a WebSocket, allowing +JavaScript programs in a web browser to interact with Plan 9 services. +.PP +Currently, it always mounts the connection over +.B /dev/ +and launches +.IR catclock , +which expects the +.B /dev/draw/ +provided by +.IR 9webdraw . +.SH FILES +.TP +.B /sys/log/websocket +.SH SOURCE +.B /sys/src/cmd/ip/httpd/websocket.c +.PP +.B https://bitbucket.org/dhoskin/weebsocket/ +.SH "SEE ALSO" +.IR intro (5), +.IR httpd (8) +.PP +.B https://bitbucket.org/dhoskin/9webdraw +.SH BUGS +The command +.B /bin/games/catclock +is hardcoded. +.PP +No authentication is performed, and raw 9P is used rather than +.IR cpu (1)'s +protocol. +.PP +More interesting programs such as +.IR acme (1) +cannot run as user +.IR none , +because its default name\%space does not include a writeable +.BR /tmp/ . +.PP +Rather than hardcoding 9P, plugins for different protocols could +be chosen using the WebSocket subprotocol header. +.PP +Rather than running under +.IR httpd (8) +and starting a given command for each connection, +it could be generalised to serve a +.B /net/websocket/ +directory under which arbitrary programs could +.IR announce (2), +with the port field specifying the subprotocol: +.BR "announce(``websocket!*!9p'', nil)" . --- /sys/src/cmd/ip/httpd/mkfile Mon Feb 10 21:10:12 2014 +++ /sys/src/cmd/ip/httpd/mkfile Mon Feb 10 21:10:14 2014 @@ -14,6 +14,7 @@ netlib_history\ webls\ wikipost\ + websocket\ XTARG=\ httpd\ @@ -23,6 +24,7 @@ man2html\ save\ wikipost\ + websocket\ LIB=libhttps.a$O --- /sys/src/cmd/ip/httpd/websocket.c Thu Jan 1 00:00:00 1970 +++ /sys/src/cmd/ip/httpd/websocket.c Mon Feb 10 21:10:15 2014 @@ -0,0 +1,588 @@ +/* Copyright © 2013-2014 David Hoskin */ + +#include +#include +#include +#include +#include +#include +#include +#include "httpd.h" +#include "httpsrv.h" + +enum +{ + /* misc parameters */ + MAXHDRS = 64, + STACKSZ = 32768, + BUFSZ = 16384, + CHANBUF = 8, + + /* packet types */ + /* standard non-control frames */ + Cont = 0x0, + Text = 0x1, + Binary = 0x2, + /* reserved non-control frames */ + /* standard control frames */ + Close = 0x8, + Ping = 0x9, + Pong = 0xA, + /* reserved control frames */ +}; + +typedef struct Procio Procio; +struct Procio +{ + Channel *c; + Biobuf *b; + int fd; + char **argv; +}; + +typedef struct Buf Buf; +struct Buf +{ + uchar *buf; + long n; +}; + +typedef struct Wspkt Wspkt; +struct Wspkt +{ + Buf; + int type; +}; + +/* XXX The default was not enough, but this is just a guess. */ +int mainstacksize = 65536; + +const char wsnoncekey[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; +const char wsversion[] = "13"; + +HSPairs * +parseheaders(char *headers) +{ + char *hdrlines[MAXHDRS], *kv[2]; + HSPairs *h, *t, *tmp; + int nhdr; + int i; + + h = t = nil; + + nhdr = getfields(headers, hdrlines, MAXHDRS, 1, "\r\n"); + + /* + * XXX I think leading whitespaces signifies a continuation line. + * Skip the first line, or else getfields(..., " ") picks up the GET. + */ + for(i = 1; i < nhdr; ++i){ + + if(hdrlines[i] == nil) + continue; + + getfields(hdrlines[i], kv, 2, 1, ": \t"); + + tmp = malloc(sizeof(HSPairs)); + if(tmp == nil) + goto cleanup; + + tmp->s = kv[0]; + tmp->t = kv[1]; + + if(h == nil){ + h = t = tmp; + }else{ + t->next = tmp; + t = tmp; + } + tmp->next = nil; + } + + return h; + +cleanup: + for(t = h->next; h != nil; h = t, t = h->next) + free(h); + return nil; +} + +char * +getheader(HSPairs *h, const char *k) +{ + for(; h != nil; h = h->next) + if(cistrcmp(h->s, k) == 0) + return h->t; + return nil; +} + +int +failhdr(HConnect *c, int code, const char *status, const char *message) +{ + Hio *o; + + o = &c->hout; + hprint(o, "%s %d %s\r\n", hversion, code, status); + hprint(o, "Server: Plan9\r\n"); + hprint(o, "Date: %D\r\n", time(nil)); + hprint(o, "Content-type: text/html\r\n"); + hprint(o, "\r\n"); + hprint(o, "%d %s\n", code, status); + hprint(o, "

%d %s

\n", code, status); + hprint(o, "

Failed to establish websocket connection: %s\n", message); + hprint(o, "\n"); + hflush(o); + return 0; +} + +void +okhdr(HConnect *c, const char *wshashedkey, const char *proto) +{ + Hio *o; + + o = &c->hout; + hprint(o, "%s 101 Switching Protocols\r\n", hversion); + hprint(o, "Upgrade: websocket\r\n"); + hprint(o, "Connection: upgrade\r\n"); + hprint(o, "Sec-WebSocket-Accept: %s\r\n", wshashedkey); + if(proto != nil) + hprint(o, "Sec-WebSocket-Protocol: %s\r\n", proto); + /* we don't handle extensions */ + hprint(o, "\r\n"); + hflush(o); +} + +int +testwsversion(const char *vs) +{ + int i, n; + char *v[16]; + + n = getfields(vs, v, 16, 1, "\t ,"); + for(i = 0; i < n; ++i) + if(strcmp(v[i], wsversion) == 0) + return 1; + return 0; +} + +uvlong +Bgetbe(Biobuf *b, int sz) +{ + uchar buf[8]; + int i; + uvlong x; + + if(Bread(b, buf, sz) != sz) + return -1; + + x = 0; + for(i = 0; i < sz; ++i) + x |= buf[i] << (8 * (sz - 1 - i)); + + return x; +} + +/* Assumptions: +* We will never be masking the data. +* Messages will be atomic: all frames are final. +*/ +int +sendpkt(Biobuf *b, Wspkt *pkt) +{ + uchar hdr[2+8]; + long hdrsz, len; + + hdr[0] = 0x80 | pkt->type; + len = pkt->n; + + /* XXX should use putbe(). */ + if(len >= (1 << 16)){ + hdrsz = 2 + 8; + hdr[1] = 127; + hdr[2] = hdr[3] = hdr[4] = hdr[5] = 0; + hdr[6] = len >> 24; + hdr[7] = len >> 16; + hdr[8] = len >> 8; + hdr[9] = len >> 0; + }else if(len >= 126){ + hdrsz = 2 + 2; + hdr[1] = 126; + hdr[2] = len >> 8; + hdr[3]= len >> 0; + }else{ + hdrsz = 2; + hdr[1] = len; + } + + if(Bwrite(b, hdr, hdrsz) != hdrsz) + return -1; + if(Bwrite(b, pkt->buf, len) != len) + return -1; + if(Bflush(b) < 0) + return -1; + + return 0; +} + +int +recvpkt(Wspkt *pkt, Biobuf *b) +{ + long x; + int masked; + uchar mask[4]; + + pkt->type = Bgetc(b); + if(pkt->type < 0){ + return -1; + } + /* Strip FIN/continuation bit. */ + pkt->type &= 0x0F; + + pkt->n = Bgetc(b); + if(pkt->n < 0){ + return -1; + } + masked = pkt->n & 0x80; + pkt->n &= 0x7F; + + if(pkt->n >= 127){ + pkt->n = Bgetbe(b, 8); + }else if(pkt->n == 126){ + pkt->n = Bgetbe(b, 2); + } + if(pkt->n < 0){ + return -1; + } + + if(masked){ + if(Bread(b, mask, 4) != 4) + return -1; + } + /* allocate appropriate buffer */ + if(pkt->n > BUFSZ){ + /* + * buffer is unacceptably large! + * XXX this should close the connection with a specific error code. + * See websocket spec. + */ + return -1; + }else if(pkt->n == 0){ + pkt->buf = nil; + return 1; + }else{ + pkt->buf = malloc(pkt->n); + if(pkt->buf == nil) + return -1; + + if(Bread(b, pkt->buf, pkt->n) != pkt->n) + return -1; + + if(masked) + for(x = 0; x < pkt->n; ++x) + pkt->buf[x] ^= mask[x % 4]; + + return 1; + } +} + +void +wsreadproc(void *arg) +{ + Procio *pio; + Channel *c; + Biobuf *b; + Wspkt pkt; + + pio = (Procio *)arg; + c = pio->c; + b = pio->b; + + for(;;){ + if(recvpkt(&pkt, b) < 0) + break; + if(send(c, &pkt) < 0) + break; + } + + chanclose(c); + threadexits(nil); +} + +void +wswriteproc(void *arg) +{ + Procio *pio; + Channel *c; + Biobuf *b; + Wspkt pkt; + + pio = (Procio *)arg; + c = pio->c; + b = pio->b; + + for(;;){ + if(recv(c, &pkt) < 0) + break; + if(sendpkt(b, &pkt) < 0) + break; + free(pkt.buf); + } + + chanclose(c); + threadexits(nil); +} + +void +pipereadproc(void *arg) +{ + Procio *pio; + Channel *c; + int fd; + Buf b; + + pio = (Procio *)arg; + c = pio->c; + fd = pio->fd; + + for(;;){ + b.buf = malloc(BUFSZ); + b.n = read(fd, b.buf, BUFSZ); + if(b.n < 1) + break; + if(send(c, &b) < 0) + break; + } + + chanclose(c); + threadexits(nil); +} + +void +pipewriteproc(void *arg) +{ + Procio *pio; + Channel *c; + int fd; + Buf b; + + pio = (Procio *)arg; + c = pio->c; + fd = pio->fd; + + for(;;){ + if(recv(c, &b) != 1) + break; + if(write(fd, b.buf, b.n) != b.n) + break; + free(b.buf); + } + + chanclose(c); + threadexits(nil); +} + +void +mountproc(void *arg) +{ + Procio *pio; + int fd, i; + char **argv; + + pio = (Procio *)arg; + fd = pio->fd; + argv = pio->argv; + + for(i = 0; i < 20; ++i){ + if(i != fd) + close(i); + } + + newns("none", nil); + + if(mount(fd, -1, "/dev/", MBEFORE, "") == -1) + sysfatal("mount failed: %r"); + + procexec(nil, argv[0], argv); +} + +void +echoproc(void *arg) +{ + Procio *pio; + int fd; + char buf[1024]; + int n; + + pio = (Procio *)arg; + fd = pio->fd; + + for(;;){ + n = read(fd, buf, 1024); + if(n > 0) + write(fd, buf, n); + } +} + +int +wscheckhdr(HConnect *c) +{ + HSPairs *hdrs; + char *s, *wsclientkey; + char *rawproto; + char *proto; + char wscatkey[64]; + uchar wshashedkey[SHA1dlen]; + char wsencoded[32]; + + if(strcmp(c->req.meth, "GET") != 0) + return hunallowed(c, "GET"); + + //return failhdr(c, 403, "Forbidden", "my hair is on fire"); + + hdrs = parseheaders((char *)c->header); + + s = getheader(hdrs, "upgrade"); + if(s == nil || !cistrstr(s, "websocket")) + return failhdr(c, 400, "Bad Request", "no upgrade: websocket header."); + s = getheader(hdrs, "connection"); + if(s == nil || !cistrstr(s, "upgrade")) + return failhdr(c, 400, "Bad Request", "no connection: upgrade header."); + wsclientkey = getheader(hdrs, "sec-websocket-key"); + if(wsclientkey == nil || strlen(wsclientkey) != 24) + return failhdr(c, 400, "Bad Request", "invalid websocket nonce key."); + s = getheader(hdrs, "sec-websocket-version"); + if(s == nil || !testwsversion(s)) + return failhdr(c, 426, "Upgrade Required", "could not match websocket version."); + /* XXX should get resource name */ + rawproto = getheader(hdrs, "sec-websocket-protocol"); + proto = rawproto; + /* XXX should test if proto is acceptable" */ + /* should get sec-websocket-extensions */ + + /* OK, we seem to have a valid Websocket request. */ + + /* Hash websocket key. */ + strcpy(wscatkey, wsclientkey); + strcat(wscatkey, wsnoncekey); + sha1((uchar *)wscatkey, strlen(wscatkey), wshashedkey, nil); + enc64(wsencoded, 32, wshashedkey, SHA1dlen); + + okhdr(c, wsencoded, proto); + hflush(&c->hout); + + /* We should now have an open Websocket connection. */ + + return 1; +} + +int +dowebsock(void) +{ + Biobuf bin, bout; + Wspkt pkt; + Buf buf; + int p[2]; + Alt a[] = { + /* c v op */ + {nil, &pkt, CHANRCV}, + {nil, &buf, CHANRCV}, + {nil, nil, CHANEND}, + }; + Procio fromws, tows, frompipe, topipe; + Procio mountp, echop; + char *argv[] = {"/bin/games/catclock", nil}; + + fromws.c = chancreate(sizeof(Wspkt), CHANBUF); + tows.c = chancreate(sizeof(Wspkt), CHANBUF); + frompipe.c = chancreate(sizeof(Buf), CHANBUF); + topipe.c = chancreate(sizeof(Buf), CHANBUF); + + syslog(1, "websocket", "created chans"); + + a[0].c = fromws.c; + a[1].c = frompipe.c; + + Binit(&bin, 0, OREAD); + Binit(&bout, 1, OWRITE); + fromws.b = &bin; + tows.b = &bout; + + pipe(p); + //fd = create("/srv/weebtest", OWRITE, 0666); + //fprint(fd, "%d", p[0]); + //close(fd); + //close(p[0]); + + frompipe.fd = p[1]; + topipe.fd = p[1]; + + mountp.fd = echop.fd = p[0]; + mountp.argv = argv; + + proccreate(wsreadproc, &fromws, STACKSZ); + proccreate(wswriteproc, &tows, STACKSZ); + proccreate(pipereadproc, &frompipe, STACKSZ); + proccreate(pipewriteproc, &topipe, STACKSZ); + + //proccreate(echoproc, &echop, STACKSZ); + procrfork(mountproc, &mountp, STACKSZ, RFNAMEG|RFFDG); + + syslog(1, "websocket", "created procs"); + + for(;;){ + int i; + + i = alt(a); + if(chanclosing(a[i].c) >= 0){ + a[i].op = CHANNOP; + pkt.type = Close; + pkt.buf = nil; + pkt.n = 0; + send(tows.c, &pkt); + goto done; + } + + switch(i){ + case 0: /* from socket */ + if(pkt.type == Ping){ + pkt.type = Pong; + send(tows.c, &pkt); + }else if(pkt.type == Close){ + send(tows.c, &pkt); + goto done; + }else{ + send(topipe.c, &pkt.Buf); + } + break; + case 1: /* from pipe */ + pkt.type = Binary; + pkt.Buf = buf; + send(tows.c, &pkt); + break; + default: + sysfatal("can't happen"); + } + } +done: + syslog(1, "websocket", "closing down cleanly"); + return 1; +} + +void +threadmain(int argc, char **argv) +{ + HConnect *c; + int errfd; + + errfd = open("/sys/log/websocket", OWRITE); + dup(errfd, 2); + + syslog(1, "websocket", "websocket process %d", getpid()); + + c = init(argc, argv); + if(hparseheaders(c, HSTIMEOUT) >= 0) + if(wscheckhdr(c) >= 0) + dowebsock(); + + threadexitsall(nil); +}