From 3e63246668525a42a3b6c220c017bf0e3aae6228 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 | 18 ++++++++++-------- 2 files changed, 34 insertions(+), 8 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..1efc1abb78a3 100644 --- a/connectd/websocketd.c +++ b/connectd/websocketd.c @@ -90,7 +90,7 @@ bad_http(int fd, const char *fmt, ...) static const char *get_http_hdr(const tal_t *ctx, const u8 *buf, size_t buflen, const char *hdrname) { - size_t hdrlen; + size_t hdrlen, hdrnamelen = strlen(hdrname); for (;;) { const u8 *end = memmem(buf, buflen, "\r\n", 2); @@ -99,16 +99,18 @@ 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)) - && buf[strlen(hdrname)] == ':') + /* Header name (ASCII case-insensitive, RFC 9110 #5.1) + * followed by ':'. */ + if (hdrlen > hdrnamelen + && strncasecmp((const char *)buf, hdrname, hdrnamelen) == 0 + && buf[hdrnamelen] == ':') break; buf = end + 2; buflen -= hdrlen + 2; } - buf += strlen(hdrname) + 1; - hdrlen -= strlen(hdrname) + 1; + buf += hdrnamelen + 1; + hdrlen -= hdrnamelen + 1; /* Ignore leading whitespace (technically, they can split * fields over multiple lines, but that's silly for the fields @@ -160,10 +162,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 || !strcasestr(hdr, "websocket")) bad_http(fd, "Upgrade: websocket missing"); hdr = get_http_hdr(tmpctx, buf, len, "Connection"); - if (!hdr || !strstr(hdr, "Upgrade")) + if (!hdr || !strcasestr(hdr, "Upgrade")) bad_http(fd, "Connection: Upgrade missing"); hdr = get_http_hdr(tmpctx, buf, len, "Sec-WebSocket-Version"); if (!hdr || !streq(hdr, "13"))