Skip to content

Commit 2db11ae

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 2db11ae

File tree

3 files changed

+110
-9
lines changed

3 files changed

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

0 commit comments

Comments
 (0)