Skip to content

Commit 9710364

Browse files
committed
feat: add max_header_len & validate_handshake options
- Added `max_header_len` option to limit the maximum allowed header size during the WebSocket upgrade process. - Added `validate_handshake` option to enforce that the WebSocket handshake response must return HTTP 101. - Improved HTTP response parsing by checking the status line and extracting response headers properly. - Added new test cases
1 parent 5400587 commit 9710364

File tree

3 files changed

+117
-9
lines changed

3 files changed

+117
-9
lines changed

README.markdown

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,18 @@ An optional options table can be specified. The following options are as follows
185185
* `max_send_len`
186186

187187
Specifies the maximal length of payload allowed when sending WebSocket frames. Defaults to the value of `max_payload_len`.
188+
* `max_header_len`
189+
190+
Specifies the maximal length of payload allowed when receiving headers during the WebSocket upgrade process. Defaults to `0`, disabling the check allowing unlimited length.
188191
* `send_masked`
189192

190193
Specifies whether to send out masked WebSocket frames. When it is `true`, masked frames are always sent. Default to `false`.
191194
* `timeout`
192195

193196
Specifies the network timeout threshold in milliseconds. You can change this setting later via the `set_timeout` method call. Note that this timeout setting does not affect the HTTP response header sending process for the websocket handshake; you need to configure the [send_timeout](http://nginx.org/en/docs/http/ngx_http_core_module.html#send_timeout) directive at the same time.
197+
* `validate_handshake`
198+
199+
Specifies whether to ensure the WebSocket upgrade returned an HTTP 101 status code. When the handshake fails, both the HTTP status code & response body will be captured in the returned error message. Default to `false`.
194200

195201
[Back to TOC](#table-of-contents)
196202

lib/resty/websocket/client.lua

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,14 @@ function _M.new(self, opts)
5151
end
5252

5353
local max_payload_len, send_unmasked, timeout
54-
local max_recv_len, max_send_len
54+
local max_recv_len, max_send_len, max_header_len
55+
local validate_handshake
5556
if opts then
5657
max_payload_len = opts.max_payload_len
5758
max_recv_len = opts.max_recv_len
5859
max_send_len = opts.max_send_len
60+
max_header_len = opts.max_header_len
61+
validate_handshake = opts.validate_handshake
5962

6063
send_unmasked = opts.send_unmasked
6164
timeout = opts.timeout
@@ -68,12 +71,16 @@ function _M.new(self, opts)
6871
max_payload_len = max_payload_len or 65535
6972
max_recv_len = max_recv_len or max_payload_len
7073
max_send_len = max_send_len or max_payload_len
74+
max_header_len = max_header_len or 0
75+
validate_handshake = validate_handshake or false
7176

7277
return setmetatable({
7378
sock = sock,
7479
max_recv_len = max_recv_len,
7580
max_send_len = max_send_len,
81+
max_header_len = max_header_len,
7682
send_unmasked = send_unmasked,
83+
validate_handshake = validate_handshake,
7784
}, mt)
7885
end
7986

@@ -265,21 +272,40 @@ function _M.connect(self, uri, opts)
265272
return nil, "failed to send the handshake request: " .. err
266273
end
267274

275+
-- Parse request up to end of headers.
276+
local header, err
268277
local header_reader = sock:receiveuntil("\r\n\r\n")
269-
-- FIXME: check for too big response headers
270-
local header, err, partial = header_reader()
278+
if self.max_header_len > 0 then
279+
header, err = header_reader(self.max_header_len + 1)
280+
if string.len(header) > self.max_header_len then
281+
return nil, "response headers too large (limit: " .. self.max_header_len .. " bytes)"
282+
end
283+
else
284+
header, err = header_reader()
285+
end
271286
if not header then
272287
return nil, "failed to receive response header: " .. err
273288
end
274289

275-
-- error("header: " .. header)
276-
277-
-- FIXME: verify the response headers
278-
279-
m, err = re_match(header, [[^\s*HTTP/1\.1\s+]], "jo")
280-
if not m then
290+
-- Validate HTTP status line.
291+
local status_line_end = header:find("\r?\n")
292+
local status_line
293+
if not status_line_end then
281294
return nil, "bad HTTP response status line: " .. header
282295
end
296+
status_line = header:sub(1, status_line_end - 1)
297+
local status_code = status_line:match("^HTTP/1%.1 (%d+)")
298+
if not status_code then
299+
return nil, "bad HTTP response status code line: " .. header
300+
end
301+
302+
-- Ensure the status code is 101 (Switching Protocols) per RFC 6455.
303+
-- This status code check is optional for backward compatibility.
304+
if self.validate_handshake and status_code ~= "101" then
305+
local body, body_err = sock:receive("*a")
306+
body = body or "(no body received)"
307+
return nil, "unexpected HTTP response, code: " .. status_code .. ", body: " .. body
308+
end
283309

284310
return 1, nil, header
285311
end

t/cs.t

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2695,3 +2695,79 @@ received text frame: reused connection
26952695
--- no_error_log
26962696
[error]
26972697
[warn]
2698+
2699+
2700+
=== TEST 40: return full response body when handshake fails
2701+
--- http_config eval: $::HttpConfig
2702+
--- config
2703+
error_page 400 = /400.html;
2704+
2705+
location = /c {
2706+
content_by_lua_block {
2707+
local client = require "resty.websocket.client"
2708+
local wb, err = client:new{ validate_handshake = true }
2709+
local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/s"
2710+
local ok, err, res = wb:connect(uri)
2711+
if ok then
2712+
ngx.say("unexpected connection success")
2713+
return
2714+
end
2715+
2716+
ngx.say("error: \"", err, "\"")
2717+
}
2718+
}
2719+
2720+
location = /s {
2721+
return 400;
2722+
}
2723+
2724+
location /400.html {
2725+
internal;
2726+
return 400 "";
2727+
}
2728+
--- request
2729+
GET /c
2730+
--- response_body_like
2731+
^error: "unexpected HTTP response, code: 400, body: <html>.*"
2732+
--- no_error_log
2733+
[error]
2734+
[warn]
2735+
2736+
2737+
=== TEST 41: response headers exceed max_header_len
2738+
--- http_config eval: $::HttpConfig
2739+
--- config
2740+
location = /c {
2741+
content_by_lua_block {
2742+
local client = require "resty.websocket.client"
2743+
local wb, err = client:new{ max_header_len = 1024 }
2744+
local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/s"
2745+
local ok, err = wb:connect(uri)
2746+
if ok then
2747+
ngx.say("unexpected connection success")
2748+
return
2749+
end
2750+
2751+
ngx.say("error: \"", err, "\"")
2752+
}
2753+
}
2754+
2755+
location = /s {
2756+
content_by_lua_block {
2757+
ngx.header["X-Custom-1"] = string.rep("X", 5000)
2758+
2759+
local server = require "resty.websocket.server"
2760+
local wb, err = server:new()
2761+
if not wb then
2762+
ngx.log(ngx.ERR, "failed to new websocket: ", err)
2763+
return ngx.exit(444)
2764+
end
2765+
}
2766+
}
2767+
--- request
2768+
GET /c
2769+
--- response_body_like
2770+
^error: "response headers too large \(limit: 1024 bytes\)"
2771+
--- no_error_log
2772+
[error]
2773+
[warn]

0 commit comments

Comments
 (0)