From 5a61a597046644dceadbd31363fa418f759c4e23 Mon Sep 17 00:00:00 2001 From: wingdeans <66850754+wingdeans@users.noreply.github.com> Date: Sun, 26 Nov 2023 21:55:55 -0500 Subject: [PATCH 01/15] Initial websocket experiment --- tool/net/redbean.c | 158 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/tool/net/redbean.c b/tool/net/redbean.c index 93816d1aa..bc70f1892 100644 --- a/tool/net/redbean.c +++ b/tool/net/redbean.c @@ -42,6 +42,7 @@ #include "libc/intrin/atomic.h" #include "libc/intrin/bsr.h" #include "libc/intrin/likely.h" +#include "libc/intrin/newbie.h" #include "libc/intrin/nomultics.h" #include "libc/intrin/safemacros.h" #include "libc/log/appendresourcereport.internal.h" @@ -125,6 +126,7 @@ #include "third_party/mbedtls/net_sockets.h" #include "third_party/mbedtls/oid.h" #include "third_party/mbedtls/san.h" +#include "third_party/mbedtls/sha1.h" #include "third_party/mbedtls/ssl.h" #include "third_party/mbedtls/ssl_ticket.h" #include "third_party/mbedtls/x509.h" @@ -405,6 +407,7 @@ struct ClearedPerMessage { bool hascontenttype; bool gotcachecontrol; bool gotxcontenttypeoptions; + bool iswebsocket; int frags; int statuscode; int isyielding; @@ -5159,6 +5162,106 @@ static bool LuaRunAsset(const char *path, bool mandatory) { return !!a; } +static int LuaUpgradeWS(lua_State *L) { + size_t i; + char *p, *q; + bool haskey; + mbedtls_sha1_context ctx; + unsigned char hash[20]; + OnlyCallDuringRequest(L, "UpgradeWS"); + + haskey = true; + for (i = 0; i < cpm.msg.xheaders.n; ++i) { + if (SlicesEqualCase( + "Sec-WebSocket-Key", strlen("Sec-WebSocket-Key"), + inbuf.p + cpm.msg.xheaders.p[i].k.a, + cpm.msg.xheaders.p[i].k.b - cpm.msg.xheaders.p[i].k.a)) { + mbedtls_sha1_init(&ctx); + mbedtls_sha1_starts_ret(&ctx); + mbedtls_sha1_update_ret( + &ctx, (unsigned char *)inbuf.p + cpm.msg.xheaders.p[i].v.a, + cpm.msg.xheaders.p[i].v.b - cpm.msg.xheaders.p[i].v.a); + haskey = true; + break; + } + } + + if (!haskey) luaL_error(L, "No Sec-WebSocket-Key header"); + + p = SetStatus(101, "Switching Protocols"); + while (p - hdrbuf.p + (20 + 21 + (20 + 28 + 4)) + 512 > hdrbuf.n) { + hdrbuf.n += hdrbuf.n >> 1; + q = xrealloc(hdrbuf.p, hdrbuf.n); + cpm.luaheaderp = p = q + (p - hdrbuf.p); + hdrbuf.p = q; + } + + mbedtls_sha1_update_ret( + &ctx, (unsigned char *)"258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 36); + mbedtls_sha1_finish_ret(&ctx, hash); + char *accept = EncodeBase64((char *)hash, 20, NULL); + + p = stpcpy(p, "Upgrade: websocket\r\n"); + p = stpcpy(p, "Connection: upgrade\r\n"); + p = AppendHeader(p, "Sec-WebSocket-Accept", accept); + + cpm.luaheaderp = p; + cpm.iswebsocket = true; + return 0; +} + +static int LuaReadWS(lua_State *L) { + ssize_t rc; + size_t i, got, amt; + unsigned char wshdr[10], wshdrlen, *extlen, *mask; + uint64_t len; + OnlyCallDuringRequest(L, "ReadWS"); + + got = 0; + do { + if ((rc = reader(client, wshdr + got, 2 - got)) == -1) + luaL_error(L, "Could not read WS header"); + } while ((got += rc) < 2); + + if (!(wshdr[1] | (1 << 7))) luaL_error(L, "Unmasked WS frame"); + + len = wshdr[1] & ~(1 << 7); + wshdrlen = 6; + if (len == 126) { + wshdrlen = 8; + } else if (len == 127) { + wshdrlen = 14; + } + + while (got < wshdrlen) { + if ((rc = reader(client, wshdr + got, wshdrlen - got)) == -1) + luaL_error(L, "Could not read WS extended length"); + got += rc; + } + + extlen = &wshdr[2]; + mask = &wshdr[wshdrlen - 4]; + if (len == 126) { + len = be16toh(*(uint16_t *)extlen); + } else if (len == 127) { + len = be64toh(*(uint64_t *)extlen); + } + + if (len >= inbuf.n - amtread) + luaL_error(L, "Required %d bytes to read WS frame, %d bytes available", len, + inbuf.n - amtread); + + for (got = 0, amt = amtread; got < len; got += rc, amt += rc) { + if ((rc = reader(client, inbuf.p + amt, len - got)) == -1) + luaL_error(L, "Could not read WS data"); + } + + for (i = 0, amt = amtread; i < got; ++i, ++amt) inbuf.p[amt] ^= mask[i & 0x3]; + + lua_pushlstring(L, inbuf.p + amtread, got); + return 1; +} + // // list of functions that can't be run from the repl static const char *const kDontAutoComplete[] = { @@ -5359,6 +5462,7 @@ static const luaL_Reg kLuaFuncs[] = { {"ProgramUid", LuaProgramUid}, // {"ProgramUniprocess", LuaProgramUniprocess}, // {"Rand64", LuaRand64}, // + {"ReadWS", LuaReadWS}, // undocumented {"Rdrand", LuaRdrand}, // {"Rdseed", LuaRdseed}, // {"Rdtsc", LuaRdtsc}, // @@ -5388,6 +5492,7 @@ static const luaL_Reg kLuaFuncs[] = { {"Underlong", LuaUnderlong}, // {"UuidV4", LuaUuidV4}, // {"UuidV7", LuaUuidV7}, // + {"UpgradeWS", LuaUpgradeWS}, // undocumented {"VisualizeControlCodes", LuaVisualizeControlCodes}, // {"Write", LuaWrite}, // {"bin", LuaBin}, // @@ -6479,6 +6584,57 @@ static bool StreamResponse(char *p) { return true; } +static bool StreamWS(char *p) { + ssize_t rc; + struct iovec iov[4]; + char *s, wshdr[10], *extlen; + + p = AppendCrlf(p); + CHECK_LE(p - hdrbuf.p, hdrbuf.n); + if (logmessages) { + LogMessage("sending", hdrbuf.p, p - hdrbuf.p); + } + iov[0].iov_base = hdrbuf.p; + iov[0].iov_len = p - hdrbuf.p; + Send(iov, 1); + + bzero(iov, sizeof(iov)); + iov[0].iov_base = wshdr; + + wshdr[0] = 0x1 | (0x1 << 7); + extlen = &wshdr[2]; + + cpm.isyielding = 2; // skip first YieldGenerator + + for (;;) { + if ((rc = cpm.generator(iov + 1)) <= 0) break; + + if (rc < 126) { + wshdr[1] = rc; + iov[0].iov_len = 2; + } else if (rc <= 0xFFFF) { + wshdr[1] = 126; + *(uint16_t *)extlen = htobe16(rc); + iov[0].iov_len = 4; + } else { + wshdr[1] = 127; + *(uint64_t *)extlen = htobe64(rc); + iov[0].iov_len = 10; + } + if (Send(iov, 4) == -1) break; + } + + if (rc != -1) { + wshdr[0] = 0x8; + wshdr[1] = 0; + iov[0].iov_len = 2; + Send(iov, 1); + } else { + connectionclose = true; + } + return true; +} + static bool HandleMessageActual(void) { int rc; long reqtime, contime; @@ -6543,6 +6699,8 @@ static bool HandleMessageActual(void) { } if (!cpm.generator) { return TransmitResponse(p); + } else if (cpm.iswebsocket) { + return StreamWS(p); } else { return StreamResponse(p); } From 91fdc9a4761d45209fb45a840540a3519e6a9d19 Mon Sep 17 00:00:00 2001 From: wingdeans <66850754+wingdeans@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:36:04 -0500 Subject: [PATCH 02/15] Always close WS, handle close message --- tool/net/redbean.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tool/net/redbean.c b/tool/net/redbean.c index bc70f1892..37bd3dea2 100644 --- a/tool/net/redbean.c +++ b/tool/net/redbean.c @@ -5256,6 +5256,9 @@ static int LuaReadWS(lua_State *L) { luaL_error(L, "Could not read WS data"); } + if ((wshdr[0] & 0xF) == 0x8) + luaL_error(L, "WS connection closed"); + for (i = 0, amt = amtread; i < got; ++i, ++amt) inbuf.p[amt] ^= mask[i & 0x3]; lua_pushlstring(L, inbuf.p + amtread, got); @@ -6624,14 +6627,12 @@ static bool StreamWS(char *p) { if (Send(iov, 4) == -1) break; } - if (rc != -1) { - wshdr[0] = 0x8; - wshdr[1] = 0; - iov[0].iov_len = 2; - Send(iov, 1); - } else { - connectionclose = true; - } + wshdr[0] = 0x8 | (1 << 7); + wshdr[1] = 0; + iov[0].iov_len = 2; + Send(iov, 1); + connectionclose = true; + return true; } From 97fad6062b0dbd9254267540983b66d3b0b4942c Mon Sep 17 00:00:00 2001 From: wingdeans <66850754+wingdeans@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:36:49 -0500 Subject: [PATCH 03/15] Support zero-length messages Copies part of YieldGenerator to StreamWS --- tool/net/redbean.c | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/tool/net/redbean.c b/tool/net/redbean.c index 37bd3dea2..e3ff05b69 100644 --- a/tool/net/redbean.c +++ b/tool/net/redbean.c @@ -6589,8 +6589,9 @@ static bool StreamResponse(char *p) { static bool StreamWS(char *p) { ssize_t rc; - struct iovec iov[4]; + struct iovec iov[2]; char *s, wshdr[10], *extlen; + int nresults, status; p = AppendCrlf(p); CHECK_LE(p - hdrbuf.p, hdrbuf.n); @@ -6604,13 +6605,25 @@ static bool StreamWS(char *p) { bzero(iov, sizeof(iov)); iov[0].iov_base = wshdr; - wshdr[0] = 0x1 | (0x1 << 7); + wshdr[0] = 0x1 | (1 << 7); extlen = &wshdr[2]; - cpm.isyielding = 2; // skip first YieldGenerator - for (;;) { - if ((rc = cpm.generator(iov + 1)) <= 0) break; + if (!YL || lua_status(YL) != LUA_YIELD) break; // done yielding + cpm.contentlength = 0; + status = lua_resume(YL, NULL, 0, &nresults); + if (status != LUA_OK && status != LUA_YIELD) { + LogLuaError("resume", lua_tostring(YL, -1)); + lua_pop(YL, 1); + break; + } + lua_pop(YL, nresults); + if (!cpm.contentlength) UseOutput(); + + DEBUGF("(lua) ws yielded with %ld bytes generated", cpm.contentlength); + + iov[1].iov_base = cpm.content; + iov[1].iov_len = rc = cpm.contentlength; if (rc < 126) { wshdr[1] = rc; @@ -6624,7 +6637,7 @@ static bool StreamWS(char *p) { *(uint64_t *)extlen = htobe64(rc); iov[0].iov_len = 10; } - if (Send(iov, 4) == -1) break; + if (Send(iov, 2) == -1) break; } wshdr[0] = 0x8 | (1 << 7); From 6c02e000bd8530503720af85579ac7d6edff1398 Mon Sep 17 00:00:00 2001 From: wingdeans <66850754+wingdeans@users.noreply.github.com> Date: Wed, 27 Dec 2023 22:21:38 -0500 Subject: [PATCH 04/15] Support pings, binary messages --- tool/net/redbean.c | 54 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/tool/net/redbean.c b/tool/net/redbean.c index e3ff05b69..5d88f643b 100644 --- a/tool/net/redbean.c +++ b/tool/net/redbean.c @@ -407,7 +407,7 @@ struct ClearedPerMessage { bool hascontenttype; bool gotcachecontrol; bool gotxcontenttypeoptions; - bool iswebsocket; + char websockettype; int frags; int statuscode; int isyielding; @@ -5206,7 +5206,7 @@ static int LuaUpgradeWS(lua_State *L) { p = AppendHeader(p, "Sec-WebSocket-Accept", accept); cpm.luaheaderp = p; - cpm.iswebsocket = true; + cpm.websockettype = 1; return 0; } @@ -5215,6 +5215,7 @@ static int LuaReadWS(lua_State *L) { size_t i, got, amt; unsigned char wshdr[10], wshdrlen, *extlen, *mask; uint64_t len; + struct iovec iov[2]; OnlyCallDuringRequest(L, "ReadWS"); got = 0; @@ -5223,9 +5224,13 @@ static int LuaReadWS(lua_State *L) { luaL_error(L, "Could not read WS header"); } while ((got += rc) < 2); - if (!(wshdr[1] | (1 << 7))) luaL_error(L, "Unmasked WS frame"); + if (wshdr[0] & 0x70) goto close; // reserved bit set + if (!(wshdr[1] | (1 << 7))) goto close; // unmasked + if ((wshdr[0] & 0x7) >= 0x3) goto close; // reserved opcode len = wshdr[1] & ~(1 << 7); + if (wshdr[0] & 0x8 && len >= 126) goto close; // long control frame + wshdrlen = 6; if (len == 126) { wshdrlen = 8; @@ -5256,13 +5261,38 @@ static int LuaReadWS(lua_State *L) { luaL_error(L, "Could not read WS data"); } - if ((wshdr[0] & 0xF) == 0x8) - luaL_error(L, "WS connection closed"); - for (i = 0, amt = amtread; i < got; ++i, ++amt) inbuf.p[amt] ^= mask[i & 0x3]; + if ((wshdr[0] & 0xF) == 0x9) { + wshdr[0] = (wshdr[0] & ~0xF) | 0xA; + wshdr[1] = wshdr[1] & ~0x80; + iov[0].iov_base = wshdr; + iov[0].iov_len = wshdrlen - 4; + iov[1].iov_base = inbuf.p + amtread; + iov[1].iov_len = got; + Send(iov, 2); + } + lua_pushlstring(L, inbuf.p + amtread, got); - return 1; + lua_pushnumber(L, wshdr[0]); + + return 2; + +close: + lua_pushnil(L); + lua_pushnumber(L, 0x08); + return 2; +} + +static int LuaSetWSFlags(lua_State *L) { + OnlyCallDuringRequest(L, "SetWSFlags"); + char flags = lround(lua_tonumber(L, 1)); + if (flags & 0x01) { + cpm.websockettype = 1; + } else if (flags & 0x02) { + cpm.websockettype = 2; + } + return 0; } // @@ -5483,6 +5513,7 @@ static const luaL_Reg kLuaFuncs[] = { {"SetHeader", LuaSetHeader}, // {"SetLogLevel", LuaSetLogLevel}, // {"SetStatus", LuaSetStatus}, // + {"SetWSFlags", LuaSetWSFlags}, // undocumented {"Sha1", LuaSha1}, // {"Sha224", LuaSha224}, // {"Sha256", LuaSha256}, // @@ -6605,14 +6636,16 @@ static bool StreamWS(char *p) { bzero(iov, sizeof(iov)); iov[0].iov_base = wshdr; - wshdr[0] = 0x1 | (1 << 7); extlen = &wshdr[2]; for (;;) { if (!YL || lua_status(YL) != LUA_YIELD) break; // done yielding cpm.contentlength = 0; status = lua_resume(YL, NULL, 0, &nresults); - if (status != LUA_OK && status != LUA_YIELD) { + if (status == LUA_OK) { + lua_pop(YL, nresults); + break; + } else if (status != LUA_YIELD) { LogLuaError("resume", lua_tostring(YL, -1)); lua_pop(YL, 1); break; @@ -6637,6 +6670,7 @@ static bool StreamWS(char *p) { *(uint64_t *)extlen = htobe64(rc); iov[0].iov_len = 10; } + wshdr[0] = cpm.websockettype | (1 << 7); if (Send(iov, 2) == -1) break; } @@ -6713,7 +6747,7 @@ static bool HandleMessageActual(void) { } if (!cpm.generator) { return TransmitResponse(p); - } else if (cpm.iswebsocket) { + } else if (cpm.websockettype) { return StreamWS(p); } else { return StreamResponse(p); From 01176f083b05f95b265ba70fd82ea5df6a72ca01 Mon Sep 17 00:00:00 2001 From: wingdeans <66850754+wingdeans@users.noreply.github.com> Date: Thu, 28 Dec 2023 00:49:52 -0500 Subject: [PATCH 05/15] Support fragmentation, utf-8 checks --- tool/net/redbean.c | 71 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/tool/net/redbean.c b/tool/net/redbean.c index 5d88f643b..c54afc540 100644 --- a/tool/net/redbean.c +++ b/tool/net/redbean.c @@ -407,7 +407,7 @@ struct ClearedPerMessage { bool hascontenttype; bool gotcachecontrol; bool gotxcontenttypeoptions; - char websockettype; + char wstype; int frags; int statuscode; int isyielding; @@ -488,6 +488,8 @@ static uint8_t *zmap; static uint8_t *zcdir; static size_t hdrsize; static size_t amtread; +static size_t wsfragread; +static char wsfragtype; static reader_f reader; static writer_f writer; static char *extrahdrs; @@ -5206,14 +5208,15 @@ static int LuaUpgradeWS(lua_State *L) { p = AppendHeader(p, "Sec-WebSocket-Accept", accept); cpm.luaheaderp = p; - cpm.websockettype = 1; + cpm.wstype = 1; return 0; } static int LuaReadWS(lua_State *L) { ssize_t rc; - size_t i, got, amt; - unsigned char wshdr[10], wshdrlen, *extlen, *mask; + size_t i, got, amt, bufsize; + unsigned char wshdr[10], wshdrlen, *extlen, *mask, op; + char *bufstart; uint64_t len; struct iovec iov[2]; OnlyCallDuringRequest(L, "ReadWS"); @@ -5224,12 +5227,19 @@ static int LuaReadWS(lua_State *L) { luaL_error(L, "Could not read WS header"); } while ((got += rc) < 2); + op = wshdr[0] & 0xF; + if (wshdr[0] & 0x70) goto close; // reserved bit set if (!(wshdr[1] | (1 << 7))) goto close; // unmasked if ((wshdr[0] & 0x7) >= 0x3) goto close; // reserved opcode + if (!wsfragtype && !op) goto close; // not in continuation len = wshdr[1] & ~(1 << 7); - if (wshdr[0] & 0x8 && len >= 126) goto close; // long control frame + if (wshdr[0] & 0x8) { // control frame + if (!(wshdr[0] & 0x80) || len >= 126) goto close; // fragmented or too long + } else { + if (op && wsfragtype) goto close; // during fragmented seq + } wshdrlen = 6; if (len == 126) { @@ -5252,29 +5262,54 @@ static int LuaReadWS(lua_State *L) { len = be64toh(*(uint64_t *)extlen); } - if (len >= inbuf.n - amtread) + if (len >= inbuf.n - wsfragread) luaL_error(L, "Required %d bytes to read WS frame, %d bytes available", len, - inbuf.n - amtread); + inbuf.n - wsfragread); - for (got = 0, amt = amtread; got < len; got += rc, amt += rc) { + for (got = 0, amt = wsfragread; got < len; got += rc, amt += rc) { if ((rc = reader(client, inbuf.p + amt, len - got)) == -1) luaL_error(L, "Could not read WS data"); } - for (i = 0, amt = amtread; i < got; ++i, ++amt) inbuf.p[amt] ^= mask[i & 0x3]; + for (i = 0, amt = wsfragread; i < got; ++i, ++amt) + inbuf.p[amt] ^= mask[i & 0x3]; - if ((wshdr[0] & 0xF) == 0x9) { + if (op == 0x9) { wshdr[0] = (wshdr[0] & ~0xF) | 0xA; wshdr[1] = wshdr[1] & ~0x80; iov[0].iov_base = wshdr; iov[0].iov_len = wshdrlen - 4; - iov[1].iov_base = inbuf.p + amtread; + iov[1].iov_base = inbuf.p + wsfragread; iov[1].iov_len = got; Send(iov, 2); } - lua_pushlstring(L, inbuf.p + amtread, got); - lua_pushnumber(L, wshdr[0]); + if (wshdr[0] & 0x80) { + if (op) { + bufstart = inbuf.p + wsfragread; + bufsize = got; + + if (op == 0x1 && !isutf8(bufstart, bufsize)) goto close; + lua_pushlstring(L, bufstart, bufsize); + lua_pushnumber(L, wshdr[0]); + } else { + bufstart = inbuf.p + amtread; + bufsize = (wsfragread - amtread) + got; + + if (wsfragtype == 0x1 && !isutf8(bufstart, bufsize)) goto close; + lua_pushlstring(L, bufstart, bufsize); + lua_pushnumber(L, wsfragtype); + + wsfragread = amtread; + wsfragtype = 0; + } + } else { + lua_pushnil(L); + lua_pushnumber(L, 0); + + if (!wsfragtype) wsfragtype = op; + wsfragread += got; + } return 2; @@ -5288,9 +5323,9 @@ static int LuaSetWSFlags(lua_State *L) { OnlyCallDuringRequest(L, "SetWSFlags"); char flags = lround(lua_tonumber(L, 1)); if (flags & 0x01) { - cpm.websockettype = 1; + cpm.wstype = 1; } else if (flags & 0x02) { - cpm.websockettype = 2; + cpm.wstype = 2; } return 0; } @@ -6637,6 +6672,8 @@ static bool StreamWS(char *p) { iov[0].iov_base = wshdr; extlen = &wshdr[2]; + wsfragread = amtread; + wsfragtype = 0; for (;;) { if (!YL || lua_status(YL) != LUA_YIELD) break; // done yielding @@ -6670,7 +6707,7 @@ static bool StreamWS(char *p) { *(uint64_t *)extlen = htobe64(rc); iov[0].iov_len = 10; } - wshdr[0] = cpm.websockettype | (1 << 7); + wshdr[0] = cpm.wstype | (1 << 7); if (Send(iov, 2) == -1) break; } @@ -6747,7 +6784,7 @@ static bool HandleMessageActual(void) { } if (!cpm.generator) { return TransmitResponse(p); - } else if (cpm.websockettype) { + } else if (cpm.wstype) { return StreamWS(p); } else { return StreamResponse(p); From 83b3c756595d066e91e98509f0c62507613558f2 Mon Sep 17 00:00:00 2001 From: wingdeans <66850754+wingdeans@users.noreply.github.com> Date: Thu, 28 Dec 2023 15:41:29 -0500 Subject: [PATCH 06/15] isutf8: implement RFC 3629 reject surrogate pairs (U+D800 to U+DFFF) reject greater than U+10FFFF --- libc/str/isutf8.c | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/libc/str/isutf8.c b/libc/str/isutf8.c index 30f9600cd..bfdcab7d9 100644 --- a/libc/str/isutf8.c +++ b/libc/str/isutf8.c @@ -26,8 +26,8 @@ static const char kUtf8Dispatch[] = { 1, 1, 1, 1, 1, 1, 1, 1, // 0320 1, 1, 1, 1, 1, 1, 1, 1, // 0330 2, 3, 3, 3, 3, 3, 3, 3, // 0340 utf8-3 - 3, 3, 3, 3, 3, 3, 3, 3, // 0350 - 4, 5, 5, 5, 5, 0, 0, 0, // 0360 utf8-4 + 3, 3, 3, 3, 3, 4, 3, 3, // 0350 + 5, 6, 6, 6, 7, 0, 0, 0, // 0360 utf8-4 0, 0, 0, 0, 0, 0, 0, 0, // 0370 }; @@ -95,6 +95,7 @@ bool32 isutf8(const void *data, size_t size) { } // fallthrough case 3: + case3: if (p + 2 <= e && // (p[0] & 0300) == 0200 && // (p[1] & 0300) == 0200) { // @@ -104,11 +105,17 @@ bool32 isutf8(const void *data, size_t size) { return false; // missing cont } case 4: + if (p < e && (*p & 040)) { + return false; // utf-16 surrogate + } + goto case3; + case 5: if (p < e && (*p & 0377) < 0220) { return false; // overlong } // fallthrough - case 5: + case 6: + case6: if (p + 3 <= e && // (((uint32_t)(p[+2] & 0377) << 030 | // (uint32_t)(p[+1] & 0377) << 020 | // @@ -120,6 +127,11 @@ bool32 isutf8(const void *data, size_t size) { } else { return false; // missing cont } + case 7: + if (p < e && (*p & 0x3F) > 0xF) { + return false; // over limit + } + goto case6; default: __builtin_unreachable(); } From 6550c5fae1e7674841f84f9bef757e3176b984db Mon Sep 17 00:00:00 2001 From: wingdeans <66850754+wingdeans@users.noreply.github.com> Date: Thu, 28 Dec 2023 18:39:19 -0500 Subject: [PATCH 07/15] Add Sec-WebSocket-Key HTTP header --- net/http/gethttpheader.gperf | 1 + net/http/gethttpheader.inc | 7 +++++-- net/http/gethttpheadername.c | 2 ++ net/http/http.h | 3 ++- tool/net/redbean.c | 23 +++++++---------------- 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/net/http/gethttpheader.gperf b/net/http/gethttpheader.gperf index 26c3c2dd0..fad88e6bb 100644 --- a/net/http/gethttpheader.gperf +++ b/net/http/gethttpheader.gperf @@ -104,3 +104,4 @@ CF-Visitor, kHttpCfVisitor CF-Connecting-IP, kHttpCfConnectingIp CF-IPCountry, kHttpCfIpcountry CDN-Loop, kHttpCdnLoop +Sec-WebSocket-Key, kHttpWebsocketKey diff --git a/net/http/gethttpheader.inc b/net/http/gethttpheader.inc index 8e58c9279..3deb185f0 100644 --- a/net/http/gethttpheader.inc +++ b/net/http/gethttpheader.inc @@ -39,7 +39,7 @@ #line 12 "gethttpheader.gperf" struct thatispacked HttpHeaderSlot { char *name; char code; }; -#define TOTAL_KEYWORDS 93 +#define TOTAL_KEYWORDS 94 #define MIN_WORD_LENGTH 2 #define MAX_WORD_LENGTH 32 #define MIN_HASH_VALUE 3 @@ -387,7 +387,10 @@ LookupHttpHeader (register const char *str, register size_t len) #line 87 "gethttpheader.gperf" {"Strict-Transport-Security", kHttpStrictTransportSecurity}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, - {""}, {""}, {""}, {""}, {""}, + {""}, {""}, +#line 107 "gethttpheader.gperf" + {"Sec-WebSocket-Key", kHttpWebsocketKey}, + {""}, {""}, #line 22 "gethttpheader.gperf" {"X-Forwarded-For", kHttpXForwardedFor}, {""}, diff --git a/net/http/gethttpheadername.c b/net/http/gethttpheadername.c index ced63607c..f41d31018 100644 --- a/net/http/gethttpheadername.c +++ b/net/http/gethttpheadername.c @@ -206,6 +206,8 @@ const char *GetHttpHeaderName(int h) { return "CDN-Loop"; case kHttpSecChUaPlatform: return "Sec-CH-UA-Platform"; + case kHttpWebsocketKey: + return "Sec-WebSocket-Key"; default: return NULL; } diff --git a/net/http/http.h b/net/http/http.h index c1dbbb13b..e4c3f6535 100644 --- a/net/http/http.h +++ b/net/http/http.h @@ -138,7 +138,8 @@ #define kHttpCfIpcountry 90 #define kHttpSecChUaPlatform 91 #define kHttpCdnLoop 92 -#define kHttpHeadersMax 93 +#define kHttpWebsocketKey 93 +#define kHttpHeadersMax 94 COSMOPOLITAN_C_START_ diff --git a/tool/net/redbean.c b/tool/net/redbean.c index c54afc540..d3f5ea8b3 100644 --- a/tool/net/redbean.c +++ b/tool/net/redbean.c @@ -5172,23 +5172,14 @@ static int LuaUpgradeWS(lua_State *L) { unsigned char hash[20]; OnlyCallDuringRequest(L, "UpgradeWS"); - haskey = true; - for (i = 0; i < cpm.msg.xheaders.n; ++i) { - if (SlicesEqualCase( - "Sec-WebSocket-Key", strlen("Sec-WebSocket-Key"), - inbuf.p + cpm.msg.xheaders.p[i].k.a, - cpm.msg.xheaders.p[i].k.b - cpm.msg.xheaders.p[i].k.a)) { - mbedtls_sha1_init(&ctx); - mbedtls_sha1_starts_ret(&ctx); - mbedtls_sha1_update_ret( - &ctx, (unsigned char *)inbuf.p + cpm.msg.xheaders.p[i].v.a, - cpm.msg.xheaders.p[i].v.b - cpm.msg.xheaders.p[i].v.a); - haskey = true; - break; - } - } + if (!HasHeader(kHttpWebsocketKey)) + luaL_error(L, "No Sec-WebSocket-Key header"); - if (!haskey) luaL_error(L, "No Sec-WebSocket-Key header"); + mbedtls_sha1_init(&ctx); + mbedtls_sha1_starts_ret(&ctx); + mbedtls_sha1_update_ret(&ctx, (unsigned char*) + HeaderData(kHttpWebsocketKey), + HeaderLength(kHttpWebsocketKey)); p = SetStatus(101, "Switching Protocols"); while (p - hdrbuf.p + (20 + 21 + (20 + 28 + 4)) + 512 > hdrbuf.n) { From b5993e4e1df259e827b7a40b85a5ba09c55a90e8 Mon Sep 17 00:00:00 2001 From: wingdeans <66850754+wingdeans@users.noreply.github.com> Date: Thu, 28 Dec 2023 23:28:34 -0500 Subject: [PATCH 08/15] Implement new API --- tool/net/redbean.c | 66 +++++++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/tool/net/redbean.c b/tool/net/redbean.c index d3f5ea8b3..b295d7a41 100644 --- a/tool/net/redbean.c +++ b/tool/net/redbean.c @@ -5164,13 +5164,15 @@ static bool LuaRunAsset(const char *path, bool mandatory) { return !!a; } -static int LuaUpgradeWS(lua_State *L) { +static int LuaWSUpgrade(lua_State *L) { size_t i; char *p, *q; bool haskey; mbedtls_sha1_context ctx; unsigned char hash[20]; - OnlyCallDuringRequest(L, "UpgradeWS"); + + if (cpm.generator) + luaL_error(L, "Cannot upgrade to websocket after yielding normally"); if (!HasHeader(kHttpWebsocketKey)) luaL_error(L, "No Sec-WebSocket-Key header"); @@ -5200,17 +5202,18 @@ static int LuaUpgradeWS(lua_State *L) { cpm.luaheaderp = p; cpm.wstype = 1; + return 0; } -static int LuaReadWS(lua_State *L) { +static int LuaWSRead(lua_State *L) { ssize_t rc; size_t i, got, amt, bufsize; unsigned char wshdr[10], wshdrlen, *extlen, *mask, op; char *bufstart; uint64_t len; struct iovec iov[2]; - OnlyCallDuringRequest(L, "ReadWS"); + OnlyCallDuringRequest(L, "ws.Read"); got = 0; do { @@ -5282,21 +5285,21 @@ static int LuaReadWS(lua_State *L) { if (op == 0x1 && !isutf8(bufstart, bufsize)) goto close; lua_pushlstring(L, bufstart, bufsize); - lua_pushnumber(L, wshdr[0]); + lua_pushinteger(L, op); } else { bufstart = inbuf.p + amtread; bufsize = (wsfragread - amtread) + got; if (wsfragtype == 0x1 && !isutf8(bufstart, bufsize)) goto close; lua_pushlstring(L, bufstart, bufsize); - lua_pushnumber(L, wsfragtype); + lua_pushinteger(L, wsfragtype); wsfragread = amtread; wsfragtype = 0; } } else { lua_pushnil(L); - lua_pushnumber(L, 0); + lua_pushinteger(L, 0); if (!wsfragtype) wsfragtype = op; wsfragread += got; @@ -5306,21 +5309,50 @@ static int LuaReadWS(lua_State *L) { close: lua_pushnil(L); - lua_pushnumber(L, 0x08); + lua_pushinteger(L, 0x08); return 2; } -static int LuaSetWSFlags(lua_State *L) { - OnlyCallDuringRequest(L, "SetWSFlags"); - char flags = lround(lua_tonumber(L, 1)); - if (flags & 0x01) { - cpm.wstype = 1; - } else if (flags & 0x02) { - cpm.wstype = 2; +static int LuaWSWrite(lua_State *L) { + int type; + size_t size; + const char *data; + + OnlyCallDuringRequest(L, "ws.Write"); + if (!cpm.wstype) + LuaWSUpgrade(L); + + type = luaL_optinteger(L, 2, -1); + if (type == 1 || type == 2) { + cpm.wstype = type; + } else if (type != -1) { + luaL_error(L, "Invalid WS type"); + } + + if (!lua_isnil(L, 1)) { + data = luaL_checklstring(L, 1, &size); + appendd(&cpm.outbuf, data, size); } return 0; } +static const luaL_Reg kLuaWS[] = { + {"Read", LuaWSRead}, // + {"Write", LuaWSWrite}, // + {0} // +}; + +int LuaWS(lua_State *L) { + luaL_newlib(L, kLuaWS); + lua_pushinteger(L, 0); lua_setfield(L, -2, "CONT"); + lua_pushinteger(L, 1); lua_setfield(L, -2, "TEXT"); + lua_pushinteger(L, 2); lua_setfield(L, -2, "BIN"); + lua_pushinteger(L, 8); lua_setfield(L, -2, "CLOSE"); + lua_pushinteger(L, 9); lua_setfield(L, -2, "PING"); + lua_pushinteger(L, 10); lua_setfield(L, -2, "PONG"); + return 1; +} + // // list of functions that can't be run from the repl static const char *const kDontAutoComplete[] = { @@ -5521,7 +5553,6 @@ static const luaL_Reg kLuaFuncs[] = { {"ProgramUid", LuaProgramUid}, // {"ProgramUniprocess", LuaProgramUniprocess}, // {"Rand64", LuaRand64}, // - {"ReadWS", LuaReadWS}, // undocumented {"Rdrand", LuaRdrand}, // {"Rdseed", LuaRdseed}, // {"Rdtsc", LuaRdtsc}, // @@ -5539,7 +5570,6 @@ static const luaL_Reg kLuaFuncs[] = { {"SetHeader", LuaSetHeader}, // {"SetLogLevel", LuaSetLogLevel}, // {"SetStatus", LuaSetStatus}, // - {"SetWSFlags", LuaSetWSFlags}, // undocumented {"Sha1", LuaSha1}, // {"Sha224", LuaSha224}, // {"Sha256", LuaSha256}, // @@ -5552,7 +5582,6 @@ static const luaL_Reg kLuaFuncs[] = { {"Underlong", LuaUnderlong}, // {"UuidV4", LuaUuidV4}, // {"UuidV7", LuaUuidV7}, // - {"UpgradeWS", LuaUpgradeWS}, // undocumented {"VisualizeControlCodes", LuaVisualizeControlCodes}, // {"Write", LuaWrite}, // {"bin", LuaBin}, // @@ -5591,6 +5620,7 @@ static const luaL_Reg kLuaLibs[] = { {"path", LuaPath}, // {"re", LuaRe}, // {"unix", LuaUnix}, // + {"ws", LuaWS} // }; static void LuaSetArgv(lua_State *L) { From c6002e00fbd79389afb58888d2fff42aba362290 Mon Sep 17 00:00:00 2001 From: Derek Meer Date: Wed, 19 Mar 2025 02:29:08 -0700 Subject: [PATCH 09/15] redbean: clean up websockets support --- libc/str/isutf8.c | 2 + net/http/gethttpheader.gperf | 2 +- net/http/gethttpheader.inc | 2 +- net/http/gethttpheadername.c | 2 +- net/http/http.h | 2 +- tool/net/redbean.c | 187 +++++++++++++++++++++-------------- 6 files changed, 118 insertions(+), 79 deletions(-) diff --git a/libc/str/isutf8.c b/libc/str/isutf8.c index bfdcab7d9..8c385a55e 100644 --- a/libc/str/isutf8.c +++ b/libc/str/isutf8.c @@ -45,6 +45,8 @@ static const char kUtf8Dispatch[] = { * - Incorrect sequencing of 0300 (FIRST) and 0200 (CONT) chars * - Thompson-Pike varint sequence not encodable as UTF-16 * - Overlong UTF-8 encoding + * - any UTF-16 surrogate code points + * - last character in a multi-byte UTF-8 sequence exceeds the valid limit * * @param size if -1 implies strlen */ diff --git a/net/http/gethttpheader.gperf b/net/http/gethttpheader.gperf index fad88e6bb..850a3dab6 100644 --- a/net/http/gethttpheader.gperf +++ b/net/http/gethttpheader.gperf @@ -104,4 +104,4 @@ CF-Visitor, kHttpCfVisitor CF-Connecting-IP, kHttpCfConnectingIp CF-IPCountry, kHttpCfIpcountry CDN-Loop, kHttpCdnLoop -Sec-WebSocket-Key, kHttpWebsocketKey +Sec-WebSocket-Key, kHttpWebSocketKey diff --git a/net/http/gethttpheader.inc b/net/http/gethttpheader.inc index 3deb185f0..b90d279cc 100644 --- a/net/http/gethttpheader.inc +++ b/net/http/gethttpheader.inc @@ -389,7 +389,7 @@ LookupHttpHeader (register const char *str, register size_t len) {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, #line 107 "gethttpheader.gperf" - {"Sec-WebSocket-Key", kHttpWebsocketKey}, + {"Sec-WebSocket-Key", kHttpWebSocketKey}, {""}, {""}, #line 22 "gethttpheader.gperf" {"X-Forwarded-For", kHttpXForwardedFor}, diff --git a/net/http/gethttpheadername.c b/net/http/gethttpheadername.c index f41d31018..ee661b9fb 100644 --- a/net/http/gethttpheadername.c +++ b/net/http/gethttpheadername.c @@ -206,7 +206,7 @@ const char *GetHttpHeaderName(int h) { return "CDN-Loop"; case kHttpSecChUaPlatform: return "Sec-CH-UA-Platform"; - case kHttpWebsocketKey: + case kHttpWebSocketKey: return "Sec-WebSocket-Key"; default: return NULL; diff --git a/net/http/http.h b/net/http/http.h index e4c3f6535..77c0c8bf0 100644 --- a/net/http/http.h +++ b/net/http/http.h @@ -138,7 +138,7 @@ #define kHttpCfIpcountry 90 #define kHttpSecChUaPlatform 91 #define kHttpCdnLoop 92 -#define kHttpWebsocketKey 93 +#define kHttpWebSocketKey 93 #define kHttpHeadersMax 94 COSMOPOLITAN_C_START_ diff --git a/tool/net/redbean.c b/tool/net/redbean.c index b295d7a41..e400ff611 100644 --- a/tool/net/redbean.c +++ b/tool/net/redbean.c @@ -5165,131 +5165,159 @@ static bool LuaRunAsset(const char *path, bool mandatory) { } static int LuaWSUpgrade(lua_State *L) { - size_t i; - char *p, *q; - bool haskey; mbedtls_sha1_context ctx; unsigned char hash[20]; + char *accept, *p, *q; + if (cpm.generator) { + return luaL_error(L, "Cannot upgrade to websocket after yielding normally"); + } - if (cpm.generator) - luaL_error(L, "Cannot upgrade to websocket after yielding normally"); - - if (!HasHeader(kHttpWebsocketKey)) - luaL_error(L, "No Sec-WebSocket-Key header"); - + if (!HasHeader(kHttpWebSocketKey)) { + return luaL_error(L, "No Sec-WebSocket-Key header"); + } + // Prepare Sec-WebSocket-Accept response header (See RFC6455 1.3) mbedtls_sha1_init(&ctx); mbedtls_sha1_starts_ret(&ctx); - mbedtls_sha1_update_ret(&ctx, (unsigned char*) - HeaderData(kHttpWebsocketKey), - HeaderLength(kHttpWebsocketKey)); + mbedtls_sha1_update_ret(&ctx, + (unsigned char*)HeaderData(kHttpWebSocketKey), + HeaderLength(kHttpWebSocketKey)); + mbedtls_sha1_update_ret(&ctx, + (unsigned char*)"258EAFA5-E914-47DA-95CA-C5AB0DC85B11", + 36); + mbedtls_sha1_finish_ret(&ctx, hash); + accept = EncodeBase64((char *)hash, 20, NULL); + // prepare response p = SetStatus(101, "Switching Protocols"); - while (p - hdrbuf.p + (20 + 21 + (20 + 28 + 4)) + 512 > hdrbuf.n) { + // make enough space for the handshake message: + // "Upgrade: websocket\r\n" (20 bytes) + // "Connection: Upgrade\r\n" (21 bytes) + // "Sec-WebSocket-Accept: \r\n" (54 bytes) + // will always be 28 bytes, as len(b64(hash)) = 4*ceil(20/3) = 28 + while (p - hdrbuf.p + 95 + 512 > hdrbuf.n) { hdrbuf.n += hdrbuf.n >> 1; q = xrealloc(hdrbuf.p, hdrbuf.n); cpm.luaheaderp = p = q + (p - hdrbuf.p); hdrbuf.p = q; } - - mbedtls_sha1_update_ret( - &ctx, (unsigned char *)"258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 36); - mbedtls_sha1_finish_ret(&ctx, hash); - char *accept = EncodeBase64((char *)hash, 20, NULL); - p = stpcpy(p, "Upgrade: websocket\r\n"); - p = stpcpy(p, "Connection: upgrade\r\n"); + p = stpcpy(p, "Connection: Upgrade\r\n"); p = AppendHeader(p, "Sec-WebSocket-Accept", accept); - cpm.luaheaderp = p; cpm.wstype = 1; return 0; } +// see RFC6455 5.2 for details on the websocket data frame structure static int LuaWSRead(lua_State *L) { ssize_t rc; size_t i, got, amt, bufsize; - unsigned char wshdr[10], wshdrlen, *extlen, *mask, op; + unsigned char header[10], headerlen, opcode, *extlen, *mask; char *bufstart; uint64_t len; struct iovec iov[2]; OnlyCallDuringRequest(L, "ws.Read"); got = 0; + // read 2 bytes of the frame header do { - if ((rc = reader(client, wshdr + got, 2 - got)) == -1) - luaL_error(L, "Could not read WS header"); + if ((rc = reader(client, header + got, 2 - got)) == -1) { + return luaL_error(L, "Could not read WS header"); + } } while ((got += rc) < 2); - op = wshdr[0] & 0xF; + // reserved bit set + if (header[0] & 0x70) goto close; + // reserved opcode + if ((header[0] & 0x7) > 0x3) goto close; + // payload data is unmasked + if (!(header[1] | (1 << 7))) goto close; - if (wshdr[0] & 0x70) goto close; // reserved bit set - if (!(wshdr[1] | (1 << 7))) goto close; // unmasked - if ((wshdr[0] & 0x7) >= 0x3) goto close; // reserved opcode - if (!wsfragtype && !op) goto close; // not in continuation + opcode = header[0] & 0xF; + // not in continuation + if (!wsfragtype && !opcode) goto close; - len = wshdr[1] & ~(1 << 7); - if (wshdr[0] & 0x8) { // control frame - if (!(wshdr[0] & 0x80) || len >= 126) goto close; // fragmented or too long + len = header[1] & ~(1 << 7); + if (header[0] & 0x8) { + // control frame is fragmented or too long + if (!(header[0] & 0x80) || len >= 126) goto close; } else { - if (op && wsfragtype) goto close; // during fragmented seq + // control frame during fragmented sequence + if (opcode && wsfragtype) goto close; } - wshdrlen = 6; + headerlen = 6; if (len == 126) { - wshdrlen = 8; + headerlen = 8; } else if (len == 127) { - wshdrlen = 14; + headerlen = 14; } - while (got < wshdrlen) { - if ((rc = reader(client, wshdr + got, wshdrlen - got)) == -1) - luaL_error(L, "Could not read WS extended length"); + // read rest of header, if necessary + while (got < headerlen) { + if ((rc = reader(client, header + got, headerlen - got)) == -1) { + return luaL_error(L, "Could not read WS extended length"); + } got += rc; } - extlen = &wshdr[2]; - mask = &wshdr[wshdrlen - 4]; + extlen = &header[2]; + mask = &header[headerlen - 4]; + // multibyte length quantities are expressed in network byte order if (len == 126) { len = be16toh(*(uint16_t *)extlen); } else if (len == 127) { len = be64toh(*(uint64_t *)extlen); } - if (len >= inbuf.n - wsfragread) - luaL_error(L, "Required %d bytes to read WS frame, %d bytes available", len, - inbuf.n - wsfragread); - - for (got = 0, amt = wsfragread; got < len; got += rc, amt += rc) { - if ((rc = reader(client, inbuf.p + amt, len - got)) == -1) - luaL_error(L, "Could not read WS data"); + if (len >= inbuf.n - wsfragread) { + return luaL_error(L, + "Required %d bytes to read WS frame, %d bytes available", + len, + inbuf.n - wsfragread); } - for (i = 0, amt = wsfragread; i < got; ++i, ++amt) - inbuf.p[amt] ^= mask[i & 0x3]; + // read in frame data + for (got = 0, amt = wsfragread; got < len; got += rc, amt += rc) { + if ((rc = reader(client, inbuf.p + amt, len - got)) == -1) { + return luaL_error(L, "Could not read WS data"); + } + } - if (op == 0x9) { - wshdr[0] = (wshdr[0] & ~0xF) | 0xA; - wshdr[1] = wshdr[1] & ~0x80; - iov[0].iov_base = wshdr; - iov[0].iov_len = wshdrlen - 4; + // unmask data + for (i = 0, amt = wsfragread; i < got; ++i, ++amt) { + inbuf.p[amt] ^= mask[i & 0x3]; + } + + // ping received, respond with pong + if (opcode == 0x9) { + header[0] = (header[0] & ~0xF) | 0xA; + header[1] = header[1] & ~0x80; + // pong data must be identical to ping + iov[0].iov_base = header; + iov[0].iov_len = headerlen - 4; iov[1].iov_base = inbuf.p + wsfragread; iov[1].iov_len = got; Send(iov, 2); } - if (wshdr[0] & 0x80) { - if (op) { + // final fragment + if (header[0] & 0x80) { + // non-continuation frame + if (opcode) { bufstart = inbuf.p + wsfragread; bufsize = got; - if (op == 0x1 && !isutf8(bufstart, bufsize)) goto close; + // text frame with invalid text + if (opcode == 0x1 && !isutf8(bufstart, bufsize)) goto close; lua_pushlstring(L, bufstart, bufsize); - lua_pushinteger(L, op); + lua_pushinteger(L, opcode); } else { bufstart = inbuf.p + amtread; bufsize = (wsfragread - amtread) + got; + // text frame with invalid text if (wsfragtype == 0x1 && !isutf8(bufstart, bufsize)) goto close; lua_pushlstring(L, bufstart, bufsize); lua_pushinteger(L, wsfragtype); @@ -5301,7 +5329,7 @@ static int LuaWSRead(lua_State *L) { lua_pushnil(L); lua_pushinteger(L, 0); - if (!wsfragtype) wsfragtype = op; + if (!wsfragtype) wsfragtype = opcode; wsfragread += got; } @@ -5319,20 +5347,22 @@ static int LuaWSWrite(lua_State *L) { const char *data; OnlyCallDuringRequest(L, "ws.Write"); - if (!cpm.wstype) + if (!cpm.wstype) { LuaWSUpgrade(L); + } type = luaL_optinteger(L, 2, -1); if (type == 1 || type == 2) { cpm.wstype = type; } else if (type != -1) { - luaL_error(L, "Invalid WS type"); + return luaL_error(L, "Invalid WS type"); } if (!lua_isnil(L, 1)) { data = luaL_checklstring(L, 1, &size); appendd(&cpm.outbuf, data, size); } + return 0; } @@ -5620,7 +5650,7 @@ static const luaL_Reg kLuaLibs[] = { {"path", LuaPath}, // {"re", LuaRe}, // {"unix", LuaUnix}, // - {"ws", LuaWS} // + {"ws", LuaWS}, // }; static void LuaSetArgv(lua_State *L) { @@ -6677,7 +6707,7 @@ static bool StreamResponse(char *p) { static bool StreamWS(char *p) { ssize_t rc; struct iovec iov[2]; - char *s, wshdr[10], *extlen; + char header[10], *s, *extlen; int nresults, status; p = AppendCrlf(p); @@ -6690,14 +6720,17 @@ static bool StreamWS(char *p) { Send(iov, 1); bzero(iov, sizeof(iov)); - iov[0].iov_base = wshdr; + iov[0].iov_base = header; - extlen = &wshdr[2]; + extlen = &header[2]; wsfragread = amtread; wsfragtype = 0; for (;;) { - if (!YL || lua_status(YL) != LUA_YIELD) break; // done yielding + // done yielding + if (!YL || lua_status(YL) != LUA_YIELD) { + break; + } cpm.contentlength = 0; status = lua_resume(YL, NULL, 0, &nresults); if (status == LUA_OK) { @@ -6709,7 +6742,9 @@ static bool StreamWS(char *p) { break; } lua_pop(YL, nresults); - if (!cpm.contentlength) UseOutput(); + if (!cpm.contentlength) { + UseOutput(); + } DEBUGF("(lua) ws yielded with %ld bytes generated", cpm.contentlength); @@ -6717,23 +6752,25 @@ static bool StreamWS(char *p) { iov[1].iov_len = rc = cpm.contentlength; if (rc < 126) { - wshdr[1] = rc; + header[1] = rc; iov[0].iov_len = 2; } else if (rc <= 0xFFFF) { - wshdr[1] = 126; + header[1] = 126; *(uint16_t *)extlen = htobe16(rc); iov[0].iov_len = 4; } else { - wshdr[1] = 127; + header[1] = 127; *(uint64_t *)extlen = htobe64(rc); iov[0].iov_len = 10; } - wshdr[0] = cpm.wstype | (1 << 7); - if (Send(iov, 2) == -1) break; + header[0] = cpm.wstype | (1 << 7); + if (Send(iov, 2) == -1) { + break; + } } - wshdr[0] = 0x8 | (1 << 7); - wshdr[1] = 0; + header[0] = 0x8 | (1 << 7); + header[1] = 0; iov[0].iov_len = 2; Send(iov, 1); connectionclose = true; From 4a10293f841be16945e38c4e1b569e7354b51096 Mon Sep 17 00:00:00 2001 From: Derek Meer Date: Wed, 19 Mar 2025 04:13:28 -0700 Subject: [PATCH 10/15] redbean: fix restricted websockets opcode checks --- tool/net/redbean.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tool/net/redbean.c b/tool/net/redbean.c index e400ff611..93d714bf9 100644 --- a/tool/net/redbean.c +++ b/tool/net/redbean.c @@ -5229,12 +5229,12 @@ static int LuaWSRead(lua_State *L) { // reserved bit set if (header[0] & 0x70) goto close; - // reserved opcode - if ((header[0] & 0x7) > 0x3) goto close; - // payload data is unmasked - if (!(header[1] | (1 << 7))) goto close; opcode = header[0] & 0xF; + // reserved opcode + if ((opcode & 0x7) >= 0x3 || opcode > 0xA) goto close; + // payload data is unmasked + if (!(header[1] | (1 << 7))) goto close; // not in continuation if (!wsfragtype && !opcode) goto close; From a0429ccf05f6398166047a96449e568cfed39ef1 Mon Sep 17 00:00:00 2001 From: Derek Meer Date: Wed, 19 Mar 2025 13:54:02 -0700 Subject: [PATCH 11/15] redbean: return early from LuaWSWrite if upgrade fails --- tool/net/redbean.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tool/net/redbean.c b/tool/net/redbean.c index 93d714bf9..a755fec96 100644 --- a/tool/net/redbean.c +++ b/tool/net/redbean.c @@ -5342,13 +5342,16 @@ close: } static int LuaWSWrite(lua_State *L) { - int type; + int type, retval; size_t size; const char *data; OnlyCallDuringRequest(L, "ws.Write"); if (!cpm.wstype) { - LuaWSUpgrade(L); + retval = LuaWSUpgrade(L); + if (retval != 0) { + return retval; + } } type = luaL_optinteger(L, 2, -1); From 8f4dc5a2afac99447a35f91ef8e69cbc31886869 Mon Sep 17 00:00:00 2001 From: Derek Meer Date: Wed, 19 Mar 2025 16:11:53 -0700 Subject: [PATCH 12/15] add test cases for new isutf8 checks --- test/libc/str/isutf8_test.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/libc/str/isutf8_test.c b/test/libc/str/isutf8_test.c index e9b0690c1..e24fd5873 100644 --- a/test/libc/str/isutf8_test.c +++ b/test/libc/str/isutf8_test.c @@ -42,10 +42,16 @@ TEST(isutf8, good) { } TEST(isutf8, bad) { - ASSERT_FALSE(isutf8("\300\200", -1)); // overlong nul - ASSERT_FALSE(isutf8("\200\300", -1)); // latin1 c1 control code - ASSERT_FALSE(isutf8("\300\300", -1)); // missing continuation - ASSERT_FALSE(isutf8("\377\200\200\200\200", -1)); // thompson-pike varint + ASSERT_FALSE(isutf8("\300\200", -1)); // overlong nul + ASSERT_FALSE(isutf8("\200\300", -1)); // latin1 c1 control code + ASSERT_FALSE(isutf8("\300\300", -1)); // missing continuation + ASSERT_FALSE(isutf8("\377\200\200\200\200", -1)); // thompson-pike varint + ASSERT_FALSE(isutf8("\355\240\200", -1)); // single utf-16 surrogate (high) + ASSERT_FALSE(isutf8("\355\277\277", -1)); // single utf-16 surrogate (low) + ASSERT_FALSE(isutf8("\355\240\200\355\260\200", -1)); // paired utf-16 surrogates (range begin) + ASSERT_FALSE(isutf8("\355\257\277\355\277\277", -1)); // paired utf-16 surrogates (range end) + ASSERT_FALSE(isutf8("\364\220\200\200", -1)); // boundary condition + ASSERT_FALSE(isutf8("\220", -1)); // boundary condition } TEST(isutf8, oob) { From ecb47384f8cdfd45955c06621f197b2bf7a2f9a2 Mon Sep 17 00:00:00 2001 From: Derek Meer Date: Fri, 21 Mar 2025 14:13:44 -0700 Subject: [PATCH 13/15] add websockets echo server to redbean-tester --- test/tool/net/BUILD.mk | 6 ++++++ tool/net/tester/.init.lua | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 tool/net/tester/.init.lua diff --git a/test/tool/net/BUILD.mk b/test/tool/net/BUILD.mk index c1a7ebd4f..a1ac357b1 100644 --- a/test/tool/net/BUILD.mk +++ b/test/tool/net/BUILD.mk @@ -71,11 +71,17 @@ o/$(MODE)/test/tool/net/%.dbg: \ $(APE_NO_MODIFY_SELF) @$(APELINK) + +o/$(MODE)/tool/net/tester/.init.lua.zip.o: private \ + ZIPOBJ_FLAGS += \ + -B + .PRECIOUS: o/$(MODE)/test/tool/net/redbean-tester o/$(MODE)/test/tool/net/redbean-tester.dbg: \ $(TOOL_NET_DEPS) \ o/$(MODE)/tool/net/redbean.o \ $(TOOL_NET_REDBEAN_LUA_MODULES) \ + o/$(MODE)/tool/net/tester/.init.lua.zip.o \ o/$(MODE)/tool/net/demo/seekable.txt.zip.o \ o/$(MODE)/tool/net/net.pkg \ $(CRT) \ diff --git a/tool/net/tester/.init.lua b/tool/net/tester/.init.lua new file mode 100644 index 000000000..6fa68a912 --- /dev/null +++ b/tool/net/tester/.init.lua @@ -0,0 +1,33 @@ +-- special script called by main redbean process at startup +HidePath('/usr/share/zoneinfo/') +HidePath('/usr/share/ssl/') + +-- 20Ki, for certain autobahn tests +ProgramMaxPayloadSize(20 * 1024 * 1024) + +function OnHttpRequest() + if GetPath() == "/ws" then + ws.Write(nil) -- upgrade without sending a response + coroutine.yield() + + local fds = {[GetClientFd()] = unix.POLLIN} + -- simple echo server + while true do + unix.poll(fds) + local s, t = ws.Read() + if t == ws.CLOSE then + return + elseif t == ws.TEXT then + ws.Write(s, ws.TEXT) + coroutine.yield() + elseif t == ws.BIN then + ws.Write(s, ws.BIN) + coroutine.yield() + end + end + else + Route() + end +end + + From 195961caca927a5a81e18b22034ee00e41be78c0 Mon Sep 17 00:00:00 2001 From: Derek Meer Date: Fri, 21 Mar 2025 19:04:19 -0700 Subject: [PATCH 14/15] redbean_test: add websocket test --- test/tool/net/redbean_test.c | 42 ++++++++++++++++++++++++++++++++++++ tool/net/tester/.init.lua | 8 +++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/test/tool/net/redbean_test.c b/test/tool/net/redbean_test.c index e13c3cfdd..06996f0d0 100644 --- a/test/tool/net/redbean_test.c +++ b/test/tool/net/redbean_test.c @@ -291,3 +291,45 @@ Z\n", EXPECT_NE(-1, wait(0)); EXPECT_NE(-1, sigprocmask(SIG_SETMASK, &savemask, 0)); } + +TEST(redbean, testWebSockets) { + if (IsWindows()) + return; + char portbuf[16]; + int pid, pipefds[2]; + sigset_t chldmask, savemask; + sigaddset(&chldmask, SIGCHLD); + EXPECT_NE(-1, sigprocmask(SIG_BLOCK, &chldmask, &savemask)); + ASSERT_NE(-1, pipe(pipefds)); + ASSERT_NE(-1, (pid = fork())); + if (!pid) { + setpgrp(); + close(0); + open("/dev/null", O_RDWR); + close(pipefds[0]); + dup2(pipefds[1], 1); + sigprocmask(SIG_SETMASK, &savemask, NULL); + execv("bin/redbean-tester", + (char *const[]){"bin/redbean-tester", "-vvszXp0", "-l127.0.0.1", // "-L/tmp/redbean-tester.log", + __strace > 0 ? "--strace" : 0, 0}); + _exit(127); + } + EXPECT_NE(-1, close(pipefds[1])); + EXPECT_NE(-1, read(pipefds[0], portbuf, sizeof(portbuf))); + port = atoi(portbuf); + EXPECT_TRUE(Matches("HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" + "Date: .*\r\n" + "Server: redbean/.*\r\n" + "\r\n", + gc(SendHttpRequest("GET /ws HTTP/1.1\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n")))); + EXPECT_EQ(0, close(pipefds[0])); + EXPECT_NE(-1, kill(pid, SIGTERM)); + EXPECT_NE(-1, wait(0)); + EXPECT_NE(-1, sigprocmask(SIG_SETMASK, &savemask, 0)); +} diff --git a/tool/net/tester/.init.lua b/tool/net/tester/.init.lua index 6fa68a912..ccdd769fa 100644 --- a/tool/net/tester/.init.lua +++ b/tool/net/tester/.init.lua @@ -10,10 +10,14 @@ function OnHttpRequest() ws.Write(nil) -- upgrade without sending a response coroutine.yield() - local fds = {[GetClientFd()] = unix.POLLIN} + local fd = GetClientFd() + local fds = {[fd] = unix.POLLIN | unix.POLLHUP | unix.POLLRDHUP} -- simple echo server while true do - unix.poll(fds) + res = unix.poll(fds) + if (res[fd] & unix.POLLHUP == unix.POLLHUP) or (res[fd] & unix.POLLRDHUP == unix.POLLRDHUP) then + return + end local s, t = ws.Read() if t == ws.CLOSE then return From 2309b74448d3e7905b8598186d89114e06430214 Mon Sep 17 00:00:00 2001 From: Derek Meer Date: Fri, 21 Mar 2025 20:11:16 -0700 Subject: [PATCH 15/15] OnHttpRequest: simplify early exit condition --- tool/net/tester/.init.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tool/net/tester/.init.lua b/tool/net/tester/.init.lua index ccdd769fa..077843882 100644 --- a/tool/net/tester/.init.lua +++ b/tool/net/tester/.init.lua @@ -11,11 +11,12 @@ function OnHttpRequest() coroutine.yield() local fd = GetClientFd() - local fds = {[fd] = unix.POLLIN | unix.POLLHUP | unix.POLLRDHUP} + local client_exit = unix.POLLHUP | unix.POLLRDHUP | unix.POLLERR + local fds = {[fd] = unix.POLLIN | client_exit} -- simple echo server while true do res = unix.poll(fds) - if (res[fd] & unix.POLLHUP == unix.POLLHUP) or (res[fd] & unix.POLLRDHUP == unix.POLLRDHUP) then + if res[fd] & client_exit > 0 then return end local s, t = ws.Read()