Commit Diff


commit - b9d67604deb91635f67545a801571b0298a44274
commit + b42d807fdc985cb3182193acd8220e9190857ae2
blob - 17eedaa131fe1124d6e51e5c42ce247881ebf580
blob + d40f9a527fa020bf0e70843b30c6fe2e38585697
--- web/Makefile
+++ web/Makefile
@@ -2,11 +2,13 @@
 
 PROG =		amused-web
 
-SOURCES =	web.c bufio.c http.c ../ev.c ../log.c ../playlist.c ../xmalloc.c
+SOURCES =	web.c bufio.c http.c ws.c \
+		../ev.c ../log.c ../playlist.c ../xmalloc.c
 
 OBJS =		${SOURCES:.c=.o}
 
-DISTFILES =	Makefile amused-web.1 bufio.c bufio.h http.c http.h web.c web.h
+DISTFILES =	Makefile amused-web.1 bufio.c bufio.h http.c http.h \
+		web.c web.h ws.c ws.h
 
 all: ${PROG}
 
blob - 6f9ed2e503f1d40a0a2ccc237fdaeb2faa9f056a
blob + cdb50b8169ff7e99980528db5e4a24a1636eafd5
--- web/http.c
+++ web/http.c
@@ -39,6 +39,7 @@
 #include "bufio.h"
 #include "http.h"
 #include "log.h"
+#include "ws.h"
 #include "xmalloc.h"
 
 #ifndef nitems
@@ -134,6 +135,38 @@ http_parse(struct client *clt)
 			}
 		}
 
+		if (!strncasecmp(line, "Connection:", 11)) {
+			line += 11;
+			line += strspn(line, " \t");
+			if (!strcasecmp(line, "upgrade"))
+				req->flags |= R_CONNUPGR;
+		}
+
+		if (!strncasecmp(line, "Upgrade:", 8)) {
+			line += 8;
+			line += strspn(line, " \t");
+			if (!strcasecmp(line, "websocket"))
+				req->flags |= R_UPGRADEWS;
+		}
+
+		if (!strncasecmp(line, "Sec-WebSocket-Version:", 22)) {
+			line += 22;
+			line += strspn(line, " \t");
+			if (strcmp(line, "13") != 0) {
+				log_warnx("unsupported websocket version %s",
+				    line);
+				errno = EINVAL;
+				return -1;
+			}
+			req->flags |= R_WSVERSION;
+		}
+
+		if (!strncasecmp(line, "Sec-WebSocket-Key:", 18)) {
+			line += 18;
+			line += strspn(line, " \t");
+			req->secret = xstrdup(line);
+		}
+
 		buf_drain(rbuf, endln - rbuf->buf + 2);
 	}
 
@@ -188,8 +221,17 @@ int
 http_reply(struct client *clt, int code, const char *reason, const char *ctype)
 {
 	const char	*version, *location = NULL;
+	char		 b32[32] = "";
 
 	log_debug("> %d %s", code, reason);
+
+	if (code == 101) {
+		if (ws_accept_hdr(clt->req.secret, b32, sizeof(b32)) == -1) {
+			clt->err = 1;
+			return -1;
+		}
+		clt->chunked = 0;
+	}
 
 	if (code >= 300 && code < 400) {
 		location = ctype;
@@ -214,6 +256,12 @@ http_reply(struct client *clt, int code, const char *r
 	if (clt->chunked && bufio_compose_str(&clt->bio,
 	    "Transfer-Encoding: chunked\r\n") == -1)
 		goto err;
+	if (code == 101) {
+		if (bufio_compose_fmt(&clt->bio, "Upgrade: websocket\r\n"
+		    "Connection: Upgrade\r\n"
+		    "Sec-WebSocket-Accept: %s\r\n", b32) == -1)
+			goto err;
+	}
 	if (bufio_compose(&clt->bio, "\r\n", 2) == -1)
 		goto err;
 
@@ -384,6 +432,7 @@ void
 http_free(struct client *clt)
 {
 	free(clt->req.path);
+	free(clt->req.secret);
 	free(clt->req.ctype);
 	free(clt->req.body);
 	bufio_free(&clt->bio);
blob - 1f705f7049092a5e168e49847e307209b0bb8d01
blob + 2403eca6280267892002ab5278333b526468bfef
--- web/http.h
+++ web/http.h
@@ -40,14 +40,21 @@ struct request {
 	char	*path;
 	int	 method;
 	int	 version;
+	char	*secret;
 	char	*ctype;
 	char	*body;
 	size_t	 clen;
+
+#define R_CONNUPGR  0x01
+#define R_UPGRADEWS 0x02
+#define R_WSVERSION 0x04
+	int	 flags;
 };
 
 struct client;
 typedef void (*route_fn)(struct client *);
 
+TAILQ_HEAD(clthead, client);
 struct client {
 	char		buf[1024];
 	size_t		len;
@@ -55,9 +62,12 @@ struct client {
 	struct request	req;
 	int		err;
 	int		chunked;
+	int		ws;		/* if talking ws:// */
 	int		reqdone;	/* done parsing the request */
 	int		done;		/* done handling the client */
 	route_fn	route;
+
+	TAILQ_ENTRY(client) clients;
 };
 
 int	http_init(struct client *, int);
blob - d0e29659553acccbbca336da4bb4e720ee085282
blob + e5c8abce4ceec5881bf44405d08a606365cd653d
--- web/web.c
+++ web/web.c
@@ -41,6 +41,7 @@
 #include "http.h"
 #include "log.h"
 #include "playlist.h"
+#include "ws.h"
 #include "xmalloc.h"
 
 #ifndef nitems
@@ -58,6 +59,7 @@
 #define ICON_TOGGLE		"⏯"
 #define ICON_PLAY		"⏵"
 
+static struct clthead		 clients;
 static struct imsgbuf		 ibuf;
 static struct playlist		 playlist_tmp;
 static struct player_status	 player_status;
@@ -65,6 +67,8 @@ static uint64_t			 position, duration;
 static const char		*prefix = "";
 static size_t			 prefixlen;
 
+static void client_ev(int, int, void *);
+
 const char *head = "<!doctype html>"
 	"<html>"
 	"<head>"
@@ -156,13 +160,84 @@ const char *css = 	"*{box-sizing:border-box}"
 	"}";
 
 const char *js =
+	"var ws;"
+	"let pos=0, dur=0;"
+	"const playlist=document.querySelector('.playlist');"
 	"function cur(e) {"
 	" if (e) {e.preventDefault()}"
 	" let cur = document.querySelector('#current');"
 	" if (cur) {cur.scrollIntoView(); window.scrollBy(0, -100);}"
+	"};"
+	"function b(x){return x=='on'};"
+	"function c(p, c){"
+	" const l=document.createElement('li');"
+	" if(c){l.id='current'};"
+	" const b=document.createElement('button');"
+	" b.type='submit'; b.name='jump'; b.value=p;"
+	" b.innerText=p;"
+	" l.appendChild(b);"
+	" playlist.appendChild(l);"
 	"}"
+	"function d(t){"
+	" const [, type, payload] = t.split(/^(.):(.*)$/);"
+	" if (type=='s'){"
+	"  let s=payload.split(' ');"
+	"  pos=s[0], dur=s[1];"
+	" } else if (type=='S') {"
+	"  const btn=document.querySelector('#toggle');"
+	"  if (payload=='playing') {"
+	"   btn.innerHTML='"ICON_PAUSE"';"
+	"   btn.value='pause';"
+	"  } else {"
+	"   btn.innerHTML='"ICON_PLAY"';"
+	"   btn.value='play';"
+	"  }"
+	" } else if (type=='r') {"
+	"  const btn=document.querySelector('#rone');"
+	"  btn.className=b(payload)?'mode-active':'';"
+	" } else if (type=='R') {"
+	"  const btn=document.querySelector('#rall');"
+	"  btn.className=b(payload)?'mode-active':'';"
+	" } else if (type=='c') {"
+	/* consume */
+	" } else if (type=='x') {"
+	"  playlist.innerHTML='';"
+	" } else if (type=='A') {"
+	"  c(payload, true);"
+	" } else if (type=='a') {"
+	"  c(payload, false);"
+	" } else if (type=='C') {"
+	"  const t=document.querySelector('.controls>p>a');"
+	"  t.innerText = payload.replace(/.*\\//, '');"
+	"  cur();"
+	" } else {"
+	"  console.log('unknown:',t);"
+	" }"
+	"};"
+	"function w(){"
+	" ws = new WebSocket((location.protocol=='http:'?'ws://':'wss://')"
+	"  + location.host + '/ws');"
+	" ws.addEventListener('open', () => console.log('ws: connected'));"
+	" ws.addEventListener('close', () => console.log('ws: closed'));"
+	" ws.addEventListener('message', e => d(e.data))"
+	"};"
+	"w();"
 	"cur();"
-	"document.querySelector('.controls a').addEventListener('click',cur)";
+	"document.querySelector('.controls a').addEventListener('click',cur);"
+	"document.querySelectorAll('form').forEach(f => {"
+	" f.action='/a/'+f.getAttribute('action');"
+	" f.addEventListener('submit', e => {"
+	"  e.preventDefault();"
+	"  const fd = new FormData(f);"
+	"  if (e.submitter && e.submitter.value && e.submitter.value != '')"
+	"   fd.append(e.submitter.name, e.submitter.value);"
+	"  fetch(f.action, {"
+	"   method:'POST',"
+	"   body: new URLSearchParams(fd)"
+	"  })"
+	"  .catch(x => console.log('failed to submit form:', x));"
+	" });"
+	"});";
 
 const char *foot = "<script src='/app.js?v=0'></script></body></html>";
 
@@ -234,17 +309,109 @@ url_decode(char *url)
 	return (0);
 }
 
+static int
+dispatch_event(const char *msg)
+{
+	struct client	*clt;
+	size_t		 len;
+	int		 ret = 0;
+
+	len = strlen(msg);
+	TAILQ_FOREACH(clt, &clients, clients) {
+		if (!clt->ws || clt->done || clt->err)
+			continue;
+
+		if (ws_compose(clt, WST_TEXT, msg, len) == -1)
+			ret = -1;
+
+		ev_add(clt->bio.fd, POLLIN|POLLOUT, client_ev, clt);
+	}
+
+	return (ret);
+}
+
+static int
+dispatch_event_status(void)
+{
+	const char	*status;
+	char		 buf[PATH_MAX + 2];
+	int		 r;
+
+	switch (player_status.status) {
+	case STATE_STOPPED: status = "stopped"; break;
+	case STATE_PLAYING: status = "playing"; break;
+	case STATE_PAUSED:  status = "paused";  break;
+	default: status = "unknown";
+	}
+
+	r = snprintf(buf, sizeof(buf), "S:%s", status);
+	if (r < 0 || (size_t)r >= sizeof(buf)) {
+		log_warn("snprintf");
+		return -1;
+	}
+	dispatch_event(buf);
+
+	r = snprintf(buf, sizeof(buf), "r:%s",
+	    player_status.mode.repeat_one == MODE_ON ? "on" : "off");
+	if (r < 0 || (size_t)r >= sizeof(buf)) {
+		log_warn("snprintf");
+		return -1;
+	}
+	dispatch_event(buf);
+
+	r = snprintf(buf, sizeof(buf), "R:%s",
+	    player_status.mode.repeat_all == MODE_ON ? "on" : "off");
+	if (r < 0 || (size_t)r >= sizeof(buf)) {
+		log_warn("snprintf");
+		return -1;
+	}
+	dispatch_event(buf);
+
+	r = snprintf(buf, sizeof(buf), "c:%s",
+	    player_status.mode.consume == MODE_ON ? "on" : "off");
+	if (r < 0 || (size_t)r >= sizeof(buf)) {
+		log_warn("snprintf");
+		return -1;
+	}
+	dispatch_event(buf);
+
+	r = snprintf(buf, sizeof(buf), "C:%s", player_status.path);
+	if (r < 0 || (size_t)r >= sizeof(buf)) {
+		log_warn("snprintf");
+		return -1;
+	}
+	dispatch_event(buf);
+
+	return 0;
+}
+
+static int
+dispatch_event_track(struct player_status *ps)
+{
+	char		 p[PATH_MAX + 2];
+	int		 r;
+
+	r = snprintf(p, sizeof(p), "%c:%s",
+	    ps->status == STATE_PLAYING ? 'A' : 'a', ps->path);
+	if (r < 0 || (size_t)r >= sizeof(p))
+		return (-1);
+
+	return dispatch_event(p);
+}
+
 static void
 imsg_dispatch(int fd, int ev, void *d)
 {
 	static ssize_t		 off;
 	static int		 off_found;
+	char			 seekmsg[128];
 	struct imsg		 imsg;
 	struct player_status	 ps;
 	struct player_event	 event;
 	const char		*msg;
 	ssize_t			 n;
 	size_t			 datalen;
+	int			 r;
 
 	if (ev & (POLLIN|POLLHUP)) {
 		if ((n = imsg_read(&ibuf)) == -1 && errno != EAGAIN)
@@ -306,6 +473,14 @@ imsg_dispatch(int fd, int ev, void *d)
 			case IMSG_CTL_SEEK:
 				position = event.position;
 				duration = event.duration;
+				r = snprintf(seekmsg, sizeof(seekmsg),
+				    "s:%lld %lld", (long long)position,
+				    (long long)duration);
+				if (r < 0 || (size_t)r >= sizeof(seekmsg)) {
+					log_warn("snprintf failed");
+					break;
+				}
+				dispatch_event(seekmsg);
 				break;
 
 			default:
@@ -316,6 +491,10 @@ imsg_dispatch(int fd, int ev, void *d)
 
 		case IMSG_CTL_SHOW:
 			if (datalen == 0) {
+				if (playlist_tmp.len == 0) {
+					dispatch_event("x:");
+					off = -1;
+				}
 				playlist_swap(&playlist_tmp, off);
 				memset(&playlist_tmp, 0, sizeof(playlist_tmp));
 				off = 0;
@@ -327,6 +506,9 @@ imsg_dispatch(int fd, int ev, void *d)
 			memcpy(&ps, imsg.data, sizeof(ps));
 			if (ps.path[sizeof(ps.path) - 1] != '\0')
 				fatalx("corrupted IMSG_CTL_SHOW");
+			if (playlist_tmp.len == 0)
+				dispatch_event("x:");
+			dispatch_event_track(&ps);
 			playlist_push(&playlist_tmp, ps.path);
 			if (ps.status == STATE_PLAYING)
 				off_found = 1;
@@ -341,6 +523,7 @@ imsg_dispatch(int fd, int ev, void *d)
 			if (player_status.path[sizeof(player_status.path) - 1]
 			    != '\0')
 				fatalx("corrupted IMSG_CTL_STATUS");
+			dispatch_event_status();
 			break;
 		}
 	}
@@ -414,7 +597,7 @@ render_controls(struct client *clt)
 		" enctype='"FORM_URLENCODED"'>") == -1 ||
 	    http_writes(clt, "<button type=submit name=ctl value=prev>"
 		ICON_PREV"</button>") == -1 ||
-	    http_fmt(clt, "<button type=submit name=ctl value=%s>"
+	    http_fmt(clt, "<button id='toggle' type=submit name=ctl value=%s>"
 		"%s</button>", playing ? "pause" : "play",
 		playing ? ICON_PAUSE : ICON_PLAY) == -1 ||
 	    http_writes(clt, "<button type=submit name=ctl value=next>"
@@ -422,9 +605,9 @@ render_controls(struct client *clt)
 	    http_writes(clt, "</form>") == -1 ||
 	    http_writes(clt, "<form action=mode method=post"
 		" enctype='"FORM_URLENCODED"'>") == -1 ||
-	    http_fmt(clt, "<button%s type=submit name=mode value=all>"
+	    http_fmt(clt, "<button%s id=rall type=submit name=mode value=all>"
 		ICON_REPEAT_ALL"</button>", ac) == -1 ||
-	    http_fmt(clt, "<button%s type=submit name=mode value=one>"
+	    http_fmt(clt, "<button%s id=rone type=submit name=mode value=one>"
 		ICON_REPEAT_ONE"</button>", oc) == -1 ||
 	    http_writes(clt, "</form>") == -1 ||
 	    http_writes(clt, "</section>") == -1)
@@ -489,7 +672,10 @@ route_jump(struct client *clt)
 	if (!found)
 		goto badreq;
 
-	http_reply(clt, 302, "See Other", "/");
+	if (!strncmp(clt->req.path, "/a/", 2))
+		http_reply(clt, 200, "OK", "text/plain");
+	else
+		http_reply(clt, 302, "See Other", "/");
 	return;
 
  badreq:
@@ -532,7 +718,10 @@ route_controls(struct client *clt)
 	if (!found)
 		goto badreq;
 
-	http_reply(clt, 302, "See Other", "/");
+	if (!strncmp(clt->req.path, "/a/", 2))
+		http_reply(clt, 200, "OK", "text/plain");
+	else
+		http_reply(clt, 302, "See Other", "/");
 	return;
 
  badreq:
@@ -574,12 +763,65 @@ route_mode(struct client *clt)
 	if (!found)
 		goto badreq;
 
-	http_reply(clt, 302, "See Other", "/");
+	if (!strncmp(clt->req.path, "/a/", 2))
+		http_reply(clt, 200, "OK", "text/plain");
+	else
+		http_reply(clt, 302, "See Other", "/");
 	return;
 
  badreq:
 	http_reply(clt, 400, "Bad Request", "text/plain");
 	http_writes(clt, "Bad Request.\n");
+}
+
+static void
+route_handle_ws(struct client *clt)
+{
+	struct buffer	*rbuf = &clt->bio.rbuf;
+	int		 type;
+	size_t		 len;
+
+	if (ws_read(clt, &type, &len) == -1) {
+		if (errno != EAGAIN) {
+			log_warn("ws_read");
+			clt->done = 1;
+		}
+		return;
+	}
+
+	switch (type) {
+	case WST_PING:
+		ws_compose(clt, WST_PONG, rbuf->buf, len);
+		break;
+	case WST_TEXT:
+		/* log_info("<<< %.*s", (int)len, rbuf->buf); */
+		break;
+	case WST_CLOSE:
+		/* TODO send a close too (ack) */
+		clt->done = 1;
+		break;
+	default:
+		log_info("got unexpected ws frame type 0x%02x", type);
+		break;
+	}
+
+	buf_drain(rbuf, len);
+}
+
+static void
+route_init_ws(struct client *clt)
+{
+	if (!(clt->req.flags & (R_CONNUPGR|R_UPGRADEWS|R_WSVERSION)) ||
+	    clt->req.secret == NULL) {
+		http_reply(clt, 400, "Bad Request", "text/plain");
+		http_writes(clt, "Invalid websocket handshake.\r\n");
+		return;
+	}
+
+	clt->ws = 1;
+	clt->done = 0;
+	clt->route = route_handle_ws;
+	http_reply(clt, 101, "Switching Protocols", NULL);
 }
 
 static void
@@ -609,10 +851,17 @@ route_dispatch(struct client *clt)
 		route_fn	 route;
 	} routes[] = {
 		{ METHOD_GET,	"/",		&route_home },
+
 		{ METHOD_POST,	"/jump",	&route_jump },
 		{ METHOD_POST,	"/ctrls",	&route_controls },
 		{ METHOD_POST,	"/mode",	&route_mode },
 
+		{ METHOD_POST,	"/a/jump",	&route_jump },
+		{ METHOD_POST,	"/a/ctrls",	&route_controls },
+		{ METHOD_POST,	"/a/mode",	&route_mode },
+
+		{ METHOD_GET,	"/ws",		&route_init_ws },
+
 		{ METHOD_GET,	"/style.css",	&route_assets },
 		{ METHOD_GET,	"/app.js",	&route_assets },
 
@@ -693,6 +942,7 @@ client_ev(int fd, int ev, void *d)
 
  err:
 	ev_del(fd);
+	TAILQ_REMOVE(&clients, clt, clients);
 	http_free(clt);
 }
 
@@ -714,6 +964,8 @@ web_accept(int psock, int ev, void *d)
 		return;
 	}
 
+	TAILQ_INSERT_TAIL(&clients, clt, clients);
+
 	client_ev(sock, POLLIN, clt);
 	return;
 }
@@ -738,6 +990,7 @@ main(int argc, char **argv)
 	int		 ch, v, amused_sock, fd;
 	int		 verbose = 0;
 
+	TAILQ_INIT(&clients);
 	setlocale(LC_ALL, NULL);
 
 	log_init(1, LOG_DAEMON);
blob - /dev/null
blob + 16fab184ad65d5ba6cbe6e2c5fa25f9b404af1f1 (mode 644)
--- /dev/null
+++ web/ws.c
@@ -0,0 +1,240 @@
+/*
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <sha1.h>
+
+#include "bufio.h"
+#include "http.h"
+#include "ws.h"
+
+#define WS_GUID	"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+
+static int
+tob64(unsigned char ch)
+{
+	if (ch < 26)
+		return ('A' + ch);
+	ch -= 26;
+	if (ch < 26)
+		return ('a' + ch);
+	ch -= 26;
+	if (ch < 10)
+		return ('0' + ch);
+	ch -= 10;
+	if (ch == 0)
+		return ('+');
+	if (ch == 1)
+		return ('/');
+	errno = EINVAL;
+	return (-1);
+}
+
+static int
+b64encode(unsigned char *in, size_t ilen, char *out, size_t olen)
+{
+	int		 r;
+
+#define SET(x) {				\
+	if ((r = tob64((x) & 0x3F)) == -1)	\
+		return (-1);			\
+	*out++ = r;				\
+}
+
+	while (ilen > 0) {
+		if (olen < 4) {
+			errno = ENOSPC;
+			return (-1);
+		}
+		olen -= 4;
+
+		switch (ilen) {
+		case 1:
+			SET(in[0] >> 2);
+			SET(in[0] << 4);
+			*out++ = '=';
+			*out++ = '=';
+			ilen = 0;
+			break;
+		case 2:
+			SET(in[0] >> 2);
+			SET(in[0] << 4 | in[1] >> 4);
+			SET(in[1] << 2);
+			*out++ = '=';
+			ilen = 0;
+			break;
+		default:
+			SET(in[0] >> 2);
+			SET(in[0] << 4 | in[1] >> 4);
+			SET(in[1] << 2 | in[2] >> 6);
+			SET(in[2]);
+			ilen -= 3;
+			in += 3;
+			break;
+		}
+	}
+
+#undef SET
+
+	if (olen < 1) {
+		errno = ENOSPC;
+		return (-1);
+	}
+	*out = '\0';
+	return (0);
+}
+
+int
+ws_accept_hdr(const char *secret, char *out, size_t olen)
+{
+	SHA1_CTX	 ctx;
+	uint8_t		 hash[SHA1_DIGEST_LENGTH];
+
+	SHA1Init(&ctx);
+	SHA1Update(&ctx, secret, strlen(secret));
+	SHA1Update(&ctx, WS_GUID, strlen(WS_GUID));
+	SHA1Final(hash, &ctx);
+
+	return (b64encode(hash, sizeof(hash), out, olen));
+}
+
+int
+ws_read(struct client *clt, int *type, size_t *len)
+{
+	struct buffer	*rbuf = &clt->bio.rbuf;
+	size_t		 i;
+	uint32_t	 mask;
+	uint8_t		 first, second, op, plen;
+
+	*type = WST_UNKNOWN, *len = 0;
+
+	if (rbuf->len < 2) {
+		errno = EAGAIN;
+		return (-1);
+	}
+
+	memcpy(&first, &rbuf->buf[0], sizeof(first));
+	memcpy(&second, &rbuf->buf[1], sizeof(second));
+
+	/* for the close message this doesn't seem to be the case... */
+#if 0
+	/* the reserved bits must be zero, don't care about FIN */
+	if ((first & 0x0E) != 0) {
+		errno = EINVAL;
+		return (-1);
+	}
+#endif
+
+	/* mask must be set for messages sent by the clients */
+	if ((second >> 7) != 1) {
+		errno = EINVAL;
+		return (-1);
+	}
+
+	op = first & 0x0F;
+	plen = second & 0x7F;
+
+	/* don't support extended payload length for now */
+	if (plen >= 126) {
+		errno = E2BIG;
+		return (-1);
+	}
+
+	*len = plen;
+
+	switch (op) {
+	case WST_CONT:
+	case WST_TEXT:
+	case WST_BINARY:
+	case WST_CLOSE:
+	case WST_PING:
+		*type = op;
+		break;
+	}
+
+	if (rbuf->len < sizeof(first) + sizeof(second) + sizeof(mask) + plen) {
+		errno = EAGAIN;
+		return (-1);
+	}
+
+	buf_drain(rbuf, 2); /* header */
+	memcpy(&mask, rbuf->buf, sizeof(mask));
+	buf_drain(rbuf, 4);
+
+	/* decode the payload */
+	for (i = 0; i < plen; ++i)
+		rbuf->buf[i] ^= mask >> (8 * (i % 4));
+
+	return (0);
+}
+
+int
+ws_compose(struct client *clt, int type, const void *data, size_t len)
+{
+	struct bufio	*bio = &clt->bio;
+	uint16_t	 extlen = 0;
+	uint8_t		 first, second;
+
+	first = (type & 0x0F) | 0x80;
+
+	if (len < 126)
+		second = len;
+	else {
+		second = 126;
+
+		/*
+		 * for the extended length, the most significant bit
+		 * must be zero.  We could use the 64 bit field but
+		 * it's a waste.
+		 */
+		if (len > 0x7FFF) {
+			errno = ERANGE;
+			return (-1);
+		}
+		extlen = htons(len);
+	}
+
+	if (bufio_compose(bio, &first, 1) == -1 ||
+	    bufio_compose(bio, &second, 1) == -1)
+		goto err;
+
+	if (extlen != 0 && bufio_compose(bio, &extlen, sizeof(extlen)) == -1)
+		goto err;
+
+	if (bufio_compose(bio, data, len) == -1)
+		goto err;
+
+	return (0);
+
+ err:
+	clt->err = 1;
+	return (-1);
+}
blob - /dev/null
blob + 9a3e4b238fb0f39f79513fadc36158087f5c15d0 (mode 644)
--- /dev/null
+++ web/ws.h
@@ -0,0 +1,40 @@
+/*
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+enum {
+	WST_UNKNOWN = -1,
+	WST_CONT = 0x00,
+	WST_TEXT = 0x01,
+	WST_BINARY = 0x02,
+	WST_CLOSE = 0x08,
+	WST_PING = 0x09,
+	WST_PONG = 0x0A,
+};
+
+struct client;
+
+int	ws_accept_hdr(const char *, char *, size_t);
+int	ws_read(struct client *, int *, size_t *);
+int	ws_compose(struct client *, int, const void *, size_t);