From 880909cfe7d0b920fa00d4930c7c53cc1160b38b Mon Sep 17 00:00:00 2001 From: Corey Phillips Date: Sat, 11 Jul 2026 11:09:25 -0400 Subject: [PATCH] connectd: treat websocket handshake headers as case-insensitive HTTP header field names are case-insensitive (RFC 9110 section 5.1) and RFC 6455 says the Upgrade and Connection values are treated as ASCII case-insensitive, as the comment in websocketd.c already quotes. The handshake parser matched header names byte-exactly and the values with case-sensitive strstr, so any client that normalizes header casing got 400: notably Node.js's built-in WebSocket (undici) lowercases all request headers and can never connect to a bind-addr=ws: listener, while browsers happen to send RFC casing and work. Match header names case-insensitively in get_http_hdr and use a case-insensitive search for the Upgrade and Connection values. The Sec-WebSocket-Version value comparison stays exact and the key is base64 data, unchanged. Extends the unit test with a lowercased handshake as sent by Node. Fixes #9307 Changelog-Fixed: connectd: the WebSocket port now accepts handshakes from clients that send lowercase HTTP header names, such as Node.js's built-in WebSocket. Signed-off-by: Corey Phillips --- connectd/test/run-websocket.c | 24 ++++++++++++++++++++++++ connectd/websocketd.c | 24 ++++++++++++++++++++---- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/connectd/test/run-websocket.c b/connectd/test/run-websocket.c index 2d133f062663..7001f3578b3e 100644 --- a/connectd/test/run-websocket.c +++ b/connectd/test/run-websocket.c @@ -69,6 +69,30 @@ int main(int argc, char *argv[]) my_rbuf = tal_strdup(tmpctx, hdr); my_wbuf = tal_arr(tmpctx, char, 0); + http_upgrade(STDIN_FILENO); + assert(streq(tal_strndup(tmpctx, my_wbuf, tal_bytelen(my_wbuf)), + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" + "\r\n")); + + /* Header names are case-insensitive (RFC 9110 5.1) and the Upgrade + * and Connection values are ASCII case-insensitive (RFC 6455 4.2.1). + * Node's built-in WebSocket sends lowercased header names. */ + hdr = "GET /chat HTTP/1.1\r\n" + "host: server.example.com\r\n" + "upgrade: WebSocket\r\n" + "connection: upgrade\r\n" + "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + "origin: http://example.com\r\n" + "sec-websocket-protocol: chat, superchat\r\n" + "sec-websocket-version: 13\r\n\r\n"; + + my_rbuf = tal_strdup(tmpctx, hdr); + my_rbuf_off = 0; + my_wbuf = tal_arr(tmpctx, char, 0); + http_upgrade(STDIN_FILENO); assert(streq(tal_strndup(tmpctx, my_wbuf, tal_bytelen(my_wbuf)), "HTTP/1.1 101 Switching Protocols\r\n" diff --git a/connectd/websocketd.c b/connectd/websocketd.c index d139199f6553..23158b3625fd 100644 --- a/connectd/websocketd.c +++ b/connectd/websocketd.c @@ -99,8 +99,11 @@ static const char *get_http_hdr(const tal_t *ctx, const u8 *buf, size_t buflen, /* Empty line? End of headers. */ if (hdrlen == 0) return NULL; - /* header name followed by : */ - if (memstarts(buf, hdrlen, hdrname, strlen(hdrname)) + /* header name (ASCII case-insensitive, RFC 9110 #5.1) + * followed by : */ + if (hdrlen > strlen(hdrname) + && strncasecmp((const char *)buf, hdrname, + strlen(hdrname)) == 0 && buf[strlen(hdrname)] == ':') break; buf = end + 2; @@ -126,6 +129,19 @@ static bool http_headers_complete(const u8 *buf, size_t len) return memmem(buf, len, "\r\n\r\n", 4) != NULL; } +/* Like strstr, but ASCII case-insensitive: RFC 6455 says the Upgrade and + * Connection values are "treated as an ASCII case-insensitive value". */ +static bool contains_ci(const char *haystack, const char *needle) +{ + size_t n = strlen(needle); + + for (const char *p = haystack; *p; p++) { + if (strncasecmp(p, needle, n) == 0) + return true; + } + return false; +} + static void http_respond(int fd, const u8 *buf, size_t len) { const char *hdr; @@ -160,10 +176,10 @@ static void http_respond(int fd, const u8 *buf, size_t len) 6. A |Sec-WebSocket-Version| header field, with a value of 13. */ hdr = get_http_hdr(tmpctx, buf, len, "Upgrade"); - if (!hdr || !strstr(hdr, "websocket")) + if (!hdr || !contains_ci(hdr, "websocket")) bad_http(fd, "Upgrade: websocket missing"); hdr = get_http_hdr(tmpctx, buf, len, "Connection"); - if (!hdr || !strstr(hdr, "Upgrade")) + if (!hdr || !contains_ci(hdr, "Upgrade")) bad_http(fd, "Connection: Upgrade missing"); hdr = get_http_hdr(tmpctx, buf, len, "Sec-WebSocket-Version"); if (!hdr || !streq(hdr, "13"))