/* * WebSockets daemon. * * Expected usage: "nc -server 'wsd wsd.conf' ", or moral * equivalent (eg, via inetd.conf). * * Config file lines can be * * Comments: blank lines (empty or nothing but whitespace), and * lines whose first nonwhitespace character is NUL, ;, or #. * * Game specs, which are all other lines. They must take the form * of a path, as in the portion of the ws:// URL after the * host/port, and the contact info. The contact info is either * an AF_LOCAL socket rendezvous path or an AF_INET/AF_INET6 * address and port. The distinctin is drawn based on whether * the first character after the whitespace after the path is a * slash (in the AF_LOCAL case) or not (the AF_INET/AF_INET6 * case). In the network case, the line can have two fields, in * which case the address and port are separated with a slash, * or three, in which case the second field is the address and * the third field is the port. * * The path in a game spec line must match the string in the protocol * exactly. There is no support for paths containing whitespace, but, * since no decoding is performed and whitespace has to be encoded on * the GET line, this should not be a problem in practice. * * We use binary messages for everything because the WebSockets spec is * broken; it insists on the use of UTF-8 for text messages, * apparently under the delusion that text in other encodings doesn't * exist. */ #include #include #include #include #include #include #include #include #include #include #include #include #include extern const char *__progname; static const char *conf_file = 0; typedef enum { AT_LOCAL = 1, AT_NETWORK, } ADDRTYPE; typedef struct shake_state SHAKE_STATE; typedef struct ws_state WS_STATE; typedef struct confent CONFENT; struct confent { CONFENT *link; char *path; int pathlen; ADDRTYPE type; union { struct sockaddr_un *sa; // AT_LOCAL struct addrinfo *ai0; // AT_NETWORK } u; } ; struct ws_state { int inhdr; unsigned char hdr[14]; int hw; int hh; unsigned char mask[4]; unsigned char *pb; int pa; int pl; int pw; } ; struct shake_state { char *b; int a; int l; int ll; int aftercr; unsigned int got; #define SSGOT_FIRST 0x00000001 #define SSGOT_CONN 0x00000002 #define SSGOT_UPGRADE 0x00000004 #define SSGOT_KEY 0x00000008 #define SSGOT_VERSION 0x00000010 char nonce[24]; } ; // From RFC 6455: #define WS_OPC_CONT 0x0 #define WS_OPC_TEXT 0x1 #define WS_OPC_BIN 0x2 #define WS_OPC_CLOSE 0x8 #define WS_OPC_PING 0x9 #define WS_OPC_PONG 0xa static CONFENT *config; static AIO_OQ oq; static int (*infn)(const void *, int); static void *instate; #define MAXMESSAGE 1048576 static unsigned char msg[MAXMESSAGE]; static int msgpart; static int msgopc; static int closed; #define CLOSE_SENT 1 #define CLOSE_RCVD 2 static CONFENT *curce; static int backend; static char *backend_str; static AIO_OQ bq; #define Cisspace(x) isspace((unsigned char)(x)) static void handleargs(int ac, char **av) { int skip; int errs; skip = 0; errs = 0; for (ac--,av++;ac;ac--,av++) { if (skip > 0) { skip --; continue; } if (**av != '-') { if (! conf_file) { conf_file = *av; } else { fprintf(stderr,"%s: stray argument `%s'\n",__progname,*av); errs = 1; } continue; } if (0) { needarg:; fprintf(stderr,"%s: %s needs a following argument\n",__progname,*av); errs = 1; continue; } #define WANTARG() do { if (++skip >= ac) goto needarg; } while (0) if (!strcmp(*av,"-cf")) { WANTARG(); if (conf_file) { fprintf(stderr,"%s: config file already specified\n",__progname); errs = 1; } conf_file = av[skip]; continue; } #undef WANTARG fprintf(stderr,"%s: unrecognized option `%s'\n",__progname,*av); errs = 1; } if (errs) exit(1); } static void load_conf(void) { FILE *f; int lno; char *b; int a; int l; int errs; int c; int x; int p0; int p1; CONFENT ce; CONFENT *e; void savec(char ch) { if (l >= a) b = realloc(b,a=l+8); b[l++] = ch; } config = 0; f = fopen(conf_file,"r"); if (! f) { fprintf(stderr,"%s: can't open config file %s: %s\n",__progname,conf_file,strerror(errno)); exit(1); } errs = 0; lno = 0; b = 0; a = 0; while <"readcf"> (1) { l = 0; lno ++; while <"line"> (1) { c = getc(f); switch (c) { case EOF: if (l > 0) { savec('\0'); break <"line">; } break <"readcf">; case '\n': savec('\0'); break <"line">; default: savec(c); break; } } l --; for (x=0;b[x]&&Cisspace(b[x]);x++) ; switch (b[x]) { case '\0': case ';': case '#': continue <"readcf">; } for (p0=x;b[x]&&!Cisspace(b[x]);x++) ; p1 = x; for (;b[x]&&Cisspace(b[x]);x++) ; if (! b[x]) { fprintf(stderr,"%s: %s, line %d: no contact address after path\n",__progname,conf_file,lno); errs = 1; } if (b[x] == '/') { struct sockaddr_un *s; int nb; nb = sizeof(struct sockaddr_un) - sizeof(s->sun_path) + (l-x) + 1; s = malloc(nb); s->sun_len = nb - 1; s->sun_family = AF_LOCAL; bcopy(b+x,&s->sun_path[0],(l-x)+1); ce.type = AT_LOCAL; ce.u.sa = s; } else { int h; int hl; int p; int pl; char *hs; char *ps; int e; struct addrinfo hints; struct addrinfo *ai0; for (h=x;b[x]&&!Cisspace(b[x]);x++) ; hl = x - h; for (;b[x]&&Cisspace(b[x]);x++) ; if (! b[x]) { char *slash; slash = rindex(b+h,'/'); if (! slash) { fprintf(stderr,"%s: %s, line %d: no port number given\n",__progname,conf_file,lno); errs = 1; continue <"readcf">; } hl = slash - (b+h); p = (slash + 1) - b; pl = l - p; } else { for (p=x;b[x]&&!Cisspace(b[x]);x++) ; pl = x - p; } // XXX getaddrinfo API botch: no way to do ptr-and-len! hs = malloc(hl+1); bcopy(b+h,hs,hl); hs[hl] = '\0'; ps = malloc(pl+1); bcopy(b+p,ps,pl); ps[pl] = '\0'; hints.ai_flags = 0; hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; // XXX getaddrinfo API botch: have to clear these! hints.ai_addrlen = 0; hints.ai_canonname = 0; hints.ai_addr = 0; hints.ai_next = 0; e = getaddrinfo(hs,ps,&hints,&ai0); if (e) { fprintf(stderr,"%s:, %s, line %d: can't look up %s/%s: %s\n",__progname,conf_file,lno,hs,ps,gai_strerror(e)); errs = 1; free(hs); free(ps); continue <"readcf">; } free(hs); free(ps); ce.type = AT_NETWORK; ce.u.ai0 = ai0; } ce.pathlen = p1 - p0; ce.path = malloc(p1-p0); bcopy(b+p0,ce.path,p1-p0); e = malloc(sizeof(CONFENT)); *e = ce; e->link = config; config = e; } fclose(f); free(b); if (errs) exit(1); } static void set_nbio(int fd) { fcntl(fd,F_SETFL,fcntl(fd,F_GETFL,0)|O_NONBLOCK); } static void close_drain_exit(void) { if ( ((closed & (CLOSE_SENT|CLOSE_RCVD)) == (CLOSE_SENT|CLOSE_RCVD)) && aio_oq_empty(&oq) ) { exit(0); } } static void rd_stdin(void *arg __attribute__((__unused__))) { unsigned char rbuf[8192]; int nr; int o; int h; nr = read(0,&rbuf[0],sizeof(rbuf)); if (nr < 0) { switch (errno) { case EINTR: case EWOULDBLOCK: return; break; } fprintf(stderr,"%s: read: %s\n",__progname,strerror(errno)); exit(1); } if (nr == 0) { fprintf(stderr,"%s: read EOF\n",__progname); exit(1); } o = 0; while (o < nr) { h = (*infn)(&rbuf[o],nr-o); if (h < 0) abort(); o += h; } } static int wtest_stdout(void *arg __attribute__((__unused__))) { return(aio_oq_nonempty(&oq)); } static void wr_stdout(void *arg __attribute__((__unused__))) { int n; n = aio_oq_writev(&oq,1,-1); if (n == AIO_WRITEV_ERROR) { fprintf(stderr,"%s: stdout writev failed: %s\n",__progname,strerror(errno)); exit(1); } if (n < 0) { fprintf(stderr,"%s: libaio broke: aio_oq_writev returned %d\n",__progname,n); exit(1); } aio_oq_dropdata(&oq,n); close_drain_exit(); } static void ws_newframe(WS_STATE *s) { s->inhdr = 1; s->hw = 2; s->hh = 0; } static void gen_frame(int opc, int fin, const void *data, unsigned long long int len) { unsigned char hdr[10]; int hl; hdr[0] = opc | (fin ? 0x80 : 0); if (len < 126) { hdr[1] = len; hl = 2; } else if (len < 65536) { hdr[1] = 126; hdr[2] = (len >> 8) & 0xff; hdr[3] = len & 0xff; hl = 4; } else { hdr[1] = 127; hdr[2] = (len >> 56) & 0xff; hdr[3] = (len >> 48) & 0xff; hdr[4] = (len >> 40) & 0xff; hdr[5] = (len >> 32) & 0xff; hdr[6] = (len >> 24) & 0xff; hdr[7] = (len >> 16) & 0xff; hdr[8] = (len >> 8) & 0xff; hdr[9] = len & 0xff; hl = 10; } aio_oq_queue_copy(&oq,&hdr[0],hl); aio_oq_queue_copy(&oq,data,len); } static void send_close(int reason) { unsigned char body[2]; body[0] = (reason >> 8) & 0xff; body[1] = reason & 0xff; gen_frame(WS_OPC_CLOSE,1,&body[0],2); closed |= CLOSE_SENT; } static void ws_message(int opc, const void *data, int len) { if (opc != WS_OPC_BIN) { send_close(1003); return; } aio_oq_queue_copy(&bq,data,len); } static void ws_control_frame(WS_STATE *s) { if (! (s->hdr[0] & 0x80)) { fprintf(stderr,"client sent fragmented control frame\n"); exit(1); } switch (s->hdr[0] & 15) { case WS_OPC_CLOSE: closed |= CLOSE_RCVD; if (! (closed & CLOSE_SENT)) { if (s->pl >= 2) { send_close((s->pb[0]*0x100)+s->pb[1]); } else { send_close(1000); } } close_drain_exit(); break; case WS_OPC_PING: if (! (closed & CLOSE_RCVD)) gen_frame(WS_OPC_PONG,1,s->pb,s->pl); break; case WS_OPC_PONG: break; } } static void ws_data_frame(WS_STATE *s) { if (s->hdr[0] & 15) { if (msgpart) { fprintf(stderr,"client sent non-continuation frame (type %d) with partial message pending\n",s->hdr[0]&15); exit(1); } msgopc = s->hdr[0] & 15; } else { if (! msgpart) { fprintf(stderr,"client sent continuation frame with nothing pending\n"); exit(1); } } if ((s->hdr[0] & 0x80) && (msgpart < 1)) { ws_message(msgopc,s->pb,s->pl); } else if (msgpart+s->pl > MAXMESSAGE) { fprintf(stderr,"message too long (%d+%d > %d)\n",msgpart,s->pl,MAXMESSAGE); exit(1); } else { bcopy(s->pb,&msg[msgpart],s->pl); msgpart += s->pl; if (s->hdr[0] & 0x80) { ws_message(msgopc,&msg[0],msgpart); msgpart = 0; } } } static void ws_frame(WS_STATE *s) { if (s->hdr[0] & 8) ws_control_frame(s); else ws_data_frame(s); ws_newframe(s); } static void masked_copy(const void *from, void *to, int nb, const void *mask) { const unsigned char *f; unsigned char *t; const unsigned char *m; int i; f = from; t = to; m = mask; i = 0; for (;nb>0;nb--) { *t++ = *f++ ^ m[i]; i = (i + 1) & 3; } } static int ws_input(const void *data, int len) { WS_STATE *s; const char *dp; int x; int hl; unsigned long long int paylen; int n; dp = data; x = 0; s = instate; while (1) { if (x >= len) return(x); if (s->inhdr) { if (s->hh < s->hw) { n = s->hw - s->hh; if (n > len-x) n = len - x; bcopy(dp+x,s->hdr+s->hh,n); s->hh += n; x += n; continue; } hl = 2; if (s->hdr[0] & 0x70) { fprintf(stderr,"%s: client header has reserved bit(s) set\n",__progname); exit(1); } switch (s->hdr[1] & 0x7f) { case 0x7e: hl += 2; break; case 0x7f: hl += 8; break; } if (s->hdr[1] & 0x80) { hl += 4; } else { fprintf(stderr,"%s: client input isn't masked\n",__progname); exit(1); } if (hl > s->hw) { s->hw = hl; continue; } switch (s->hdr[1] & 0x7f) { default: paylen = s->hdr[1] & 0x7f; break; case 0x7e: paylen = (s->hdr[2] * 0x0100) + s->hdr[3]; if (paylen < 126) { fprintf(stderr,"%s: client input has 16-bit length %llu\n",__progname,paylen); exit(1); } break; case 0x7f: paylen = (s->hdr[2] * 0x0100000000000000ULL) + (s->hdr[3] * 0x0001000000000000ULL) + (s->hdr[4] * 0x0000010000000000ULL) + (s->hdr[5] * 0x0000000100000000ULL) + (s->hdr[6] * 0x0000000001000000ULL) + (s->hdr[7] * 0x0000000000010000ULL) + (s->hdr[8] * 0x0000000000000100ULL) + (s->hdr[9] * 0x0000000000000001ULL); if (paylen < 65536) { fprintf(stderr,"%s: client input has 64-bit length %llu\n",__progname,paylen); exit(1); } if (paylen & 0x8000000000000000ULL) { fprintf(stderr,"%s: client input has 64-bit length with high bit set\n",__progname); exit(1); } break; } if (paylen > MAXMESSAGE) { fprintf(stderr,"%s: client input has excessive length %llu\n",__progname,paylen); exit(1); } s->pw = paylen; s->inhdr = 0; if (s->pw > s->pa) { free(s->pb); s->pa = s->pw; s->pb = malloc(s->pa); } s->pl = 0; } else { if (s->pl < s->pw) { n = s->pw - s->pl; if (n > len-x) n = len - x; masked_copy(dp+x,s->pb+s->pl,n,&s->hdr[s->hw-4]); s->pl += n; x += n; if (s->pl < s->pw) continue; } ws_frame(s); } } } static void ws_init(void) { WS_STATE *s; infn = &ws_input; s = malloc(sizeof(WS_STATE)); s->pb = 0; s->pa = 0; ws_newframe(s); msgpart = 0; closed = 0; instate = s; } static void open_backend_local(void) { int fd; int sl; struct sockaddr_un *s; s = (void *)curce->u.sa; sl = s->sun_len - (sizeof(struct sockaddr_un) - sizeof(s->sun_path)); fd = socket(AF_LOCAL,SOCK_STREAM,0); if (fd < 0) { fprintf(stderr,"%s: connect to %.*s: socket: %s\n",__progname,sl,&s->sun_path[0],strerror(errno)); exit(1); } if (connect(fd,(void *)s,s->sun_len) < 0) { fprintf(stderr,"%s: connect to %.*s: %s\n",__progname,sl,&s->sun_path[0],strerror(errno)); exit(1); } backend_str = malloc(sl+1); bcopy(&s->sun_path[0],backend_str,sl); backend_str[sl]= '\0'; backend = fd; } static void open_backend_network(void) { struct addrinfo *ai; int s; char hbuf[NI_MAXHOST]; char sbuf[NI_MAXSERV]; int e; char *txt; for (ai=curce->u.ai0;ai;ai=ai->ai_next) { e = getnameinfo(ai->ai_addr,ai->ai_addrlen,&hbuf[0],NI_MAXHOST,&sbuf[0],NI_MAXSERV,NI_NUMERICHOST|NI_NUMERICSERV); if (e) { asprintf(&txt,"[af %d, getnameinfo failed: %s]",ai->ai_family,gai_strerror(e)); } else { asprintf(&txt,"%s/%s",&hbuf[0],&sbuf[0]); } do { s = socket(ai->ai_family,ai->ai_socktype,ai->ai_protocol); if (s < 0) { fprintf(stderr,"%s: %s: socket: %s\n",__progname,txt,strerror(errno)); break; } if (connect(s,ai->ai_addr,ai->ai_addrlen) < 0) { fprintf(stderr,"%s: %s: connect: %s\n",__progname,txt,strerror(errno)); close(s); break; } backend_str = txt; backend = s; return; } while (0); free(txt); } exit(1); } static void open_backend_conn(void) { switch (curce->type) { case AT_LOCAL: open_backend_local(); break; case AT_NETWORK: open_backend_network(); break; default: abort(); break; } } static int wtest_backend(void *arg __attribute__((__unused__))) { return(aio_oq_nonempty(&bq)); } static void rd_backend(void *arg __attribute__((__unused__))) { unsigned char rbuf[8192]; int nr; nr = read(backend,&rbuf[0],sizeof(rbuf)); if (nr < 0) { switch (errno) { case EINTR: case EWOULDBLOCK: return; break; } fprintf(stderr,"%s: %s: read: %s\n",__progname,backend_str,strerror(errno)); exit(1); } if (nr == 0) { fprintf(stderr,"%s: %s: read EOF\n",__progname,backend_str); exit(1); } gen_frame(WS_OPC_BIN,1,&rbuf[0],nr); } static void wr_backend(void *arg __attribute__((__unused__))) { int n; n = aio_oq_writev(&bq,backend,-1); if (n == AIO_WRITEV_ERROR) { fprintf(stderr,"%s: %s: writev failed: %s\n",__progname,backend_str,strerror(errno)); exit(1); } if (n < 0) { fprintf(stderr,"%s: libaio broke: aio_oq_writev returned %d\n",__progname,n); exit(1); } aio_oq_dropdata(&bq,n); } static void shake_done(SHAKE_STATE *s) { void *sh; unsigned char hash[20]; void *b64; if (! (s->got & SSGOT_FIRST)) { fprintf(stderr,"Request has no leading line\n"); exit(1); } if (! (s->got & SSGOT_CONN)) { fprintf(stderr,"Request has no Connection: header\n"); exit(1); } if (! (s->got & SSGOT_UPGRADE)) { fprintf(stderr,"Request has no Upgrade: header\n"); exit(1); } if (! (s->got && SSGOT_KEY)) { fprintf(stderr,"Request has no Sec-WebSocket-Key: header\n"); exit(1); } if (! (s->got && SSGOT_VERSION)) { fprintf(stderr,"Request has no Sec-WebSocket-Version: header\n"); exit(1); } open_backend_conn(); aio_oq_init(&bq); aio_add_poll(backend,&aio_rwtest_always,&wtest_backend,&rd_backend,&wr_backend,0); aio_oq_queue_point(&oq,"HTTP/1.1 101 OK\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ",AIO_STRLEN); sh = sha1_init(); sha1_process_bytes(sh,&s->nonce[0],24); sha1_process_bytes(sh,"258EAFA5-E914-47DA-95CA-C5AB0DC85B11",36); sha1_result(sh,&hash[0]); { int o; int t; int p; char rbuf[28]; int r(void *cookie __attribute__((__unused__)), void *data, int len) { int n; n = 20 - o; if (n > len) n = len; if (n > 0) { bcopy(&hash[o],data,n); o += n; } return(n); } o = 0; b64 = base64_e_r_cb(0,&r); t = 0; while (t < 28) { p = base64_r(b64,&rbuf[t],28-t); if (p < 0) { fprintf(stderr,"base64 encoding errored!\n"); exit(1); } if (p == 0) { fprintf(stderr,"base64 encoding broke! p=0 at t=%d\n",t); exit(1); } t += p; } aio_oq_queue_copy(&oq,&rbuf[0],28); } aio_oq_queue_point(&oq,"\r\n\r\n",4); free(s->b); free(s); ws_init(); } static void scan_comma_list(const char *s, int l, void (each)(const char *, int)) { int x; int x0; int c; x = 0; while (1) { for (;(x= l) return; if (s[x] == ',') { x ++; continue; } for (x0=x;(xx0)&&Cisspace(s[x-1]);x++) ; if (x <= x0) abort(); (*each)(s+x0,x-x0); if (c >= l) return; x = c + 1; } } static void shake_line(SHAKE_STATE *s) { int x; int x0; int fl; if (! s->b[0]) { shake_done(s); return; } if (! (s->got & SSGOT_FIRST)) { CONFENT *e; if (strncmp(s->b,"GET ",4)) { fprintf(stderr,"Request isn't GET\n"); exit(1); } for (x=4;(xll)&&Cisspace(s->b[x]);x++) ; for (x0=x;(xll)&&!Cisspace(s->b[x]);x++) ; for (e=config;e;e=e->link) { if ((e->pathlen == x-x0) && !bcmp(s->b+x0,e->path,x-x0)) break; } if (! e) { fprintf(stderr,"Requested path %.*s not found in config\n",x-x0,s->b+x0); exit(1); } curce = e; s->got |= SSGOT_FIRST; return; } if (Cisspace(s->b[0])) { fprintf(stderr,"Request header begins with whitespace\n"); exit(1); } for (x=0;s->b[x]&&(s->b[x]!=':');x++) { if (Cisspace(s->b[x])) { fprintf(stderr,"Request header has whitespace before colon\n"); exit(1); } } fl = x; for (x++;s->b[x]&&Cisspace(s->b[x]);x++) ; x0 = x; x = s->ll; while ((x > x0) && Cisspace(s->b[x-1])) x --; switch (fl) { case 7: if (! strncasecmp(s->b,"upgrade",7)) { if ((x-x0 != 9) || strncmp(s->b+x0,"websocket",9)) { fprintf(stderr,"Request Upgrade: (%.*s) isn't websocket\n",x-x0,s->b+x0); exit(1); } s->got |= SSGOT_UPGRADE; } break; case 10: if (! strncasecmp(s->b,"connection",10)) { int haveupgrade; void opt(const char *o, int l) { if ((l == 7) && !bcmp(o,"Upgrade",7)) haveupgrade = 1; } haveupgrade = 0; scan_comma_list(s->b+x0,x-x0,&opt); if (! haveupgrade) { fprintf(stderr,"Request Connection: (%.*s) doesn't include Upgrade\n",x-x0,s->b+x0); exit(1); } s->got |= SSGOT_CONN; } break; case 17: if (! strncasecmp(s->b,"sec-websocket-key",17)) { if (x-x0 != 24) { fprintf(stderr,"Request Sec-WebSocket-Key: (%.*s) isn't valid\n",x-x0,s->b+x0); exit(1); } bcopy(s->b+x0,&s->nonce[0],24); s->got |= SSGOT_KEY; } break; case 21: if (! strncasecmp(s->b,"sec-websocket-version",21)) { if ((x-x0 != 2) || strncmp(s->b+x0,"13",2)) { fprintf(stderr,"Request Sec-WebSocket-Version: (%.*s) isn't 13\n",x-x0,s->b+x0); exit(1); } s->got |= SSGOT_VERSION; } break; } } static int shake_input(const void *data, int len) { SHAKE_STATE *s; const char *dp; int x; dp = data; x = 0; s = instate; while (1) { if (x >= len) return(x); if (s->aftercr && (dp[x] == '\n')) { s->ll = s->l - 1; s->b[s->ll] = '\0'; s->l = 0; s->aftercr = 0; shake_line(s); return(x+1); } if (s->l >= s->a) s->b = realloc(s->b,s->a=s->l+8); s->b[s->l++] = dp[x]; if (s->l >= 65536) { fprintf(stderr,"%s; ridiculously long handshake line\n",__progname); exit(1); } s->aftercr = (dp[x] == '\r'); x ++; } } static void shake_init(void) { SHAKE_STATE *s; infn = &shake_input; s = malloc(sizeof(SHAKE_STATE)); s->b = 0; s->a = 0; s->l = 0; s->aftercr = 0; s->got = 0; instate = s; } int main(int, char **); int main(int ac, char **av) { handleargs(ac,av); load_conf(); aio_poll_init(); set_nbio(0); set_nbio(1); aio_add_poll(0,&aio_rwtest_always,&aio_rwtest_never,&rd_stdin,0,0); aio_oq_init(&oq); aio_add_poll(1,&aio_rwtest_never,&wtest_stdout,0,&wr_stdout,0); shake_init(); aio_event_loop(); exit(1); }