commit - b9d67604deb91635f67545a801571b0298a44274
commit + b42d807fdc985cb3182193acd8220e9190857ae2
blob - 17eedaa131fe1124d6e51e5c42ce247881ebf580
blob + d40f9a527fa020bf0e70843b30c6fe2e38585697
--- web/Makefile
+++ web/Makefile
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
#include "bufio.h"
#include "http.h"
#include "log.h"
+#include "ws.h"
#include "xmalloc.h"
#ifndef nitems
}
}
+ 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);
}
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;
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;
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
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;
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
#include "http.h"
#include "log.h"
#include "playlist.h"
+#include "ws.h"
#include "xmalloc.h"
#ifndef nitems
#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;
static const char *prefix = "";
static size_t prefixlen;
+static void client_ev(int, int, void *);
+
const char *head = "<!doctype html>"
"<html>"
"<head>"
"}";
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>";
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)
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:
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;
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;
if (player_status.path[sizeof(player_status.path) - 1]
!= '\0')
fatalx("corrupted IMSG_CTL_STATUS");
+ dispatch_event_status();
break;
}
}
" 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>"
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)
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:
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:
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
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 },
err:
ev_del(fd);
+ TAILQ_REMOVE(&clients, clt, clients);
http_free(clt);
}
return;
}
+ TAILQ_INSERT_TAIL(&clients, clt, clients);
+
client_ev(sock, POLLIN, clt);
return;
}
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
+/*
+ * 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
+/*
+ * 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);