diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d931a1d..4e69163 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,7 @@ jobs: git clone https://github.com/openresty/nginx-devel-utils.git git clone https://github.com/simpl/ngx_devel_kit.git ../ndk-nginx-module git clone https://github.com/openresty/lua-nginx-module.git ../lua-nginx-module + git clone https://github.com/openresty/stream-lua-nginx-module.git ../stream-lua-nginx-module git clone https://github.com/openresty/lua-resty-core.git ../lua-resty-core git clone https://github.com/openresty/lua-resty-lrucache.git ../lua-resty-lrucache git clone https://github.com/openresty/no-pool-nginx.git ../no-pool-nginx @@ -76,7 +77,7 @@ jobs: export LD_LIBRARY_PATH=$PWD/mockeagain:$LD_LIBRARY_PATH export TEST_NGINX_RESOLVER=8.8.4.4 export NGX_BUILD_CC=$CC - ngx-build $NGINX_VERSION --with-ipv6 --with-http_realip_module --with-http_ssl_module --with-cc-opt="-I$PCRE_INC -I$OPENSSL_INC" --with-ld-opt="-L$PCRE_LIB -L$OPENSSL_LIB -Wl,-rpath,$PCRE_LIB:$OPENSSL_LIB" --add-module=../ndk-nginx-module --add-module=../lua-nginx-module --with-debug > build.log 2>&1 || (cat build.log && exit 1) + ngx-build $NGINX_VERSION --with-ipv6 --with-http_realip_module --with-http_ssl_module --with-stream --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt="-I$PCRE_INC -I$OPENSSL_INC" --with-ld-opt="-L$PCRE_LIB -L$OPENSSL_LIB -Wl,-rpath,$PCRE_LIB:$OPENSSL_LIB" --add-module=../ndk-nginx-module --add-module=../lua-nginx-module --add-module=../stream-lua-nginx-module --with-debug > build.log 2>&1 || (cat build.log && exit 1) nginx -V ldd `which nginx`|grep -E 'luajit|ssl|pcre' prove -I. -r t diff --git a/lib/resty/websocket/client.lua b/lib/resty/websocket/client.lua index e24cc05..bb95958 100644 --- a/lib/resty/websocket/client.lua +++ b/lib/resty/websocket/client.lua @@ -15,10 +15,13 @@ local new_tab = wbproto.new_tab local tcp = ngx.socket.tcp local re_match = ngx.re.match local re_find = ngx.re.find +local re_gmatch = ngx.re.gmatch local encode_base64 = ngx.encode_base64 local concat = table.concat +local insert = table.insert local char = string.char local str_find = string.find +local str_sub = string.sub local rand = math.random local rshift = bit.rshift local band = bit.band @@ -27,6 +30,7 @@ local type = type local debug = ngx.config.debug local ngx_log = ngx.log local ngx_DEBUG = ngx.DEBUG +local tostring = tostring local assert = assert local ssl_support = true @@ -79,6 +83,13 @@ end function _M.connect(self, uri, opts) + -- a client instance may be reused across multiple connect() attempts + -- (e.g. a failed connect followed by a retry); clear any response + -- metadata from a previous attempt so callers never observe stale data. + self.resp_status_code = nil + self.resp_header = nil + self.resp_headers = nil + local sock = self.sock if not sock then return nil, "not initialized" @@ -191,11 +202,59 @@ function _M.connect(self, uri, opts) end end + local connect_addr, connect_port = addr, port + local connect_is_unix = is_unix + local proxy_opts = opts and opts.proxy_opts + local proxy_url + + if scheme == "wss" and proxy_opts and proxy_opts.wss_proxy then + proxy_url = proxy_opts.wss_proxy + end + + if proxy_url then + if str_sub(proxy_url, 1, 6) == "unix:/" then + connect_addr = proxy_url + connect_port = nil + connect_is_unix = true + + else + -- https://github.com/ledgetech/lua-resty-http/blob/master/lib/resty/http.lua + local m, err = re_match( + proxy_url, + [[^(?:(http[s]?):)?//((?:[^\[\]:/\?]+)|(?:\[.+\]))(?::(\d+))?([^\?]*)\??(.*)]], + "jo" + ) + if err then + return nil, "error parsing proxy_url: " .. err + + elseif not m then + return nil, "invalid proxy url" + + elseif m[1] == "https" then + -- TLS to the proxy itself (as opposed to the tunnelled TLS + -- handshake with the target once CONNECT succeeds) is not + -- implemented; fail loudly instead of silently sending the + -- CONNECT request (and any Proxy-Authorization) in the clear. + return nil, "https proxy (TLS to the proxy itself) is not implemented" + + elseif m[1] ~= "http" then + return nil, "only proxy with scheme \"http\" is supported" + end + + connect_addr = m[2] + connect_port = m[3] or 80 + end + + if not connect_addr then + return nil, "invalid proxy url" + end + end + local ok, err - if is_unix then - ok, err = sock:connect(addr, sock_opts) + if connect_is_unix then + ok, err = sock:connect(connect_addr, sock_opts) else - ok, err = sock:connect(addr, port, sock_opts) + ok, err = sock:connect(connect_addr, connect_port, sock_opts) end if not ok then return nil, "failed to connect: " .. err @@ -213,6 +272,42 @@ function _M.connect(self, uri, opts) end if ssl then + if proxy_url then + local req = "CONNECT " .. addr .. ":" .. port .. " HTTP/1.1" + .. "\r\nHost: " .. addr .. ":" .. port + .. "\r\nProxy-Connection: Keep-Alive" + + if proxy_opts.wss_proxy_authorization then + req = req .. "\r\nProxy-Authorization: " .. proxy_opts.wss_proxy_authorization + end + + req = req .. "\r\n\r\n" + + local bytes, err = sock:send(req) + if not bytes then + return nil, "failed to send the handshake request: " .. err + end + + local header_reader = sock:receiveuntil("\r\n\r\n") + -- FIXME: check for too big response headers + local header, err, _ = header_reader() + if not header then + return nil, "failed to receive response header: " .. err + end + + -- error("header: " .. header) + + -- FIXME: verify the response headers + + local m, _ = re_match(header, [[^\s*HTTP/1\.[01]\s+(\d+)]], "jo") + if not m then + return nil, "bad HTTP response status line: " .. header + elseif m[1] ~= "200" then + return nil, "error establishing a connection to ".. + "the proxy server, got status " .. tostring(m[1]) + end + end + if client_cert then ok, err = sock:setclientcert(client_cert, client_priv_key) if not ok then @@ -281,11 +376,23 @@ function _M.connect(self, uri, opts) return nil, "bad HTTP response status line: " .. header end - -- RFC 6455 section 4.1: a status code other than 101 means the server - -- has not accepted the upgrade, so the client must fail the connection - if m[1] ~= "101" then - return nil, "failed websocket handshake: unexpected response status: " - .. m[1], header + self.resp_status_code = m[1] + self.resp_header = header + if self.resp_status_code ~= "101" then + -- RFC 6455 §4.1: a non-101 response means the WebSocket connection + -- was never established; mark fatal unconditionally so that no WS + -- frames can be sent on what is still a plain HTTP connection. + -- When keep_response=true the caller intends to read the HTTP + -- response body, so leave the raw socket open for that purpose. + if not (opts and opts.keep_response) then + local closing_ok, closing_err = sock:close() + if not closing_ok then + ngx_log(ngx_DEBUG, "failed to close the underlying socket: ", + closing_err, " when handling a non-101 response") + end + end + self.fatal = true + return nil, "unexpected HTTP response code: " .. m[1], header end return 1, nil, header @@ -423,4 +530,57 @@ function _M.set_keepalive(self, ...) end +function _M.get_resp_headers(self) + if self.resp_headers then + return self.resp_headers + end + + if not self.resp_header then + return nil, "response header not available" + end + + local iter, err = re_gmatch(self.resp_header .. "\r\n", "([^:\\s]+):\\s*(.*?)\r\n", "jo") + if err then + return nil, "failed to parse response header: " .. err + end + + -- gather all response headers + + local resp_headers = {} + + while true do + local m, err = iter() + if err then + return nil, "failed to parse response header: " .. err + end + + if not m then + -- no match found (any more) + break + end + + local key = m[1]:lower():gsub("-", "_") + local val = m[2] + + if resp_headers[key] then + if type(resp_headers[key]) ~= "table" then + resp_headers[key] = { resp_headers[key] } + end + + insert(resp_headers[key], tostring(val)) + + else + resp_headers[key] = tostring(val) + end + end + + self.resp_headers = resp_headers + + return resp_headers +end + +function _M.get_resp_status_code(self) + return self.resp_status_code +end + return _M diff --git a/t/count.t b/t/count.t index f0be407..eab91fa 100644 --- a/t/count.t +++ b/t/count.t @@ -63,7 +63,7 @@ size: 5 --- request GET /t --- response_body -size: 13 +size: 15 --- no_error_log [error] diff --git a/t/cs.t b/t/cs.t index d65918c..c3f437a 100644 --- a/t/cs.t +++ b/t/cs.t @@ -2695,3 +2695,456 @@ received text frame: reused connection --- no_error_log [error] [warn] + + + +=== TEST 41: SSL with forward proxy +--- no_check_leak +--- http_config eval: $::HttpConfig +--- main_config + stream { + server { + listen 16796; + + error_log logs/error.log debug; + content_by_lua_block { + require("t.forward-proxy-server").connect() + } + } + } +--- config + listen 12345 ssl; + server_name test.com; + ssl_certificate ../../cert/test.crt; + ssl_certificate_key ../../cert/test.key; + server_tokens off; + + location = /c { + content_by_lua ' + local client = require "resty.websocket.client" + local wb, err = client:new() + + local uri = "wss://127.0.0.1:12345/s" + local ok, err = wb:connect(uri, { + proxy_opts = { + wss_proxy = "http://127.0.0.1:16796", + }, + }) + if not ok then + ngx.say("failed to connect: " .. err) + return + end + + local data = "hello" + local bytes, err = wb:send_text(data) + if not bytes then + ngx.say("failed to send frame: ", err) + return + end + + local typ + data, typ, err = wb:recv_frame() + if not data then + ngx.say("failed to receive 2nd frame: ", err) + return + end + + ngx.say("received: ", data, " (", typ, ")") + + -- note our mock forward proxy server does not support + -- keepalive, so we must close it here + local ok, err = wb:close() + if not ok then + ngx.say("failed to close conn: ", err) + return + end + '; + } + + location = /s { + content_by_lua ' + local server = require "resty.websocket.server" + local wb, err = server:new() + if not wb then + ngx.log(ngx.ERR, "failed to new websocket: ", err) + return ngx.exit(444) + end + + while true do + local data, typ, err = wb:recv_frame() + if not data then + -- ngx.log(ngx.ERR, "failed to receive a frame: ", err) + return ngx.exit(444) + end + + -- send it back! + local bytes, err = wb:send_text(data) + if not bytes then + ngx.log(ngx.ERR, "failed to send the 2nd text: ", err) + return ngx.exit(444) + end + end + '; + } +--- request +GET /c +--- response_body +received: hello (text) + +--- no_error_log +[error] +[warn] + + + +=== TEST 42: SSL with forward proxy with auth +--- no_check_leak +--- http_config eval: $::HttpConfig +--- main_config + stream { + server { + listen 16796; + + error_log logs/error.log debug; + content_by_lua_block { + require("t.forward-proxy-server").connect({ + basic_auth = ngx.encode_base64("user:pass"), + }) + } + } + } +--- config + listen 12345 ssl; + server_name test.com; + ssl_certificate ../../cert/test.crt; + ssl_certificate_key ../../cert/test.key; + server_tokens off; + + location = /c { + content_by_lua ' + local client = require "resty.websocket.client" + local wb, err = client:new() + + local uri = "wss://127.0.0.1:12345/s" + local ok, err = wb:connect(uri, { + proxy_opts = { + wss_proxy = "http://127.0.0.1:16796", + }, + }) + if ok then + ngx.say("connect ok") + return + end + ngx.say("failed to connect without auth: " .. err) + + local uri = "wss://127.0.0.1:12345/s" + local ok, err = wb:connect(uri, { + proxy_opts = { + wss_proxy = "http://127.0.0.1:16796", + wss_proxy_authorization = "Basic " .. ngx.encode_base64("user:pass") + }, + }) + if not ok then + ngx.say("failed to connect: " .. err) + return + end + + local data = "hello" + local bytes, err = wb:send_text(data) + if not bytes then + ngx.say("failed to send frame: ", err) + return + end + + local typ + data, typ, err = wb:recv_frame() + if not data then + ngx.say("failed to receive 2nd frame: ", err) + return + end + + ngx.say("received: ", data, " (", typ, ")") + + -- note our mock forward proxy server does not support + -- keepalive, so we must close it here + local ok, err = wb:close() + if not ok then + ngx.say("failed to close conn: ", err) + return + end + '; + } + + location = /s { + content_by_lua ' + local server = require "resty.websocket.server" + local wb, err = server:new() + if not wb then + ngx.log(ngx.ERR, "failed to new websocket: ", err) + return ngx.exit(444) + end + + while true do + local data, typ, err = wb:recv_frame() + if not data then + -- ngx.log(ngx.ERR, "failed to receive a frame: ", err) + return ngx.exit(444) + end + + -- send it back! + local bytes, err = wb:send_text(data) + if not bytes then + ngx.log(ngx.ERR, "failed to send the 2nd text: ", err) + return ngx.exit(444) + end + end + '; + } +--- request +GET /c +--- response_body +failed to connect without auth: error establishing a connection to the proxy server, got status 401 +received: hello (text) + +--- no_error_log +[error] +[warn] + + +=== TEST 43: client:get_resp_headers +--- http_config eval: $::HttpConfig +--- config + location = /c { + content_by_lua ' + local client = require "resty.websocket.client" + local wb, err = client:new() + local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/s" + -- ngx.say("uri: ", uri) + local ok, err = wb:connect(uri) + if not ok then + ngx.say("failed to connect: " .. err) + return + end + + local data, typ, err = wb:recv_frame() + if not data then + ngx.say("failed to receive 1st frame: ", err) + return + end + + ngx.say("1: received: ", data, " (", typ, ")") + + local resp_headers = wb:get_resp_headers() + + ngx.say(resp_headers.upgrade) + ngx.say(resp_headers.connection) + ngx.say(resp_headers.x_foo) + '; + } + + location = /s { + content_by_lua ' + local server = require "resty.websocket.server" + + ngx.header["x-foo"] = "bar" + + local wb, err = server:new() + if not wb then + ngx.log(ngx.ERR, "failed to new websocket: ", err) + return ngx.exit(444) + end + + local bytes, err = wb:send_text("你好, WebSocket!") + if not bytes then + ngx.log(ngx.ERR, "failed to send the 1st text: ", err) + return ngx.exit(444) + end + '; + } +--- request +GET /c +--- response_body +1: received: 你好, WebSocket! (text) +websocket +upgrade +bar +--- no_error_log +[error] +[warn] + + + +=== TEST 44: SSL with forward proxy and legacy HTTP version +--- no_check_leak +--- http_config eval: $::HttpConfig +--- main_config + stream { + server { + listen 16796; + + error_log logs/error.log debug; + content_by_lua_block { + require("t.forward-proxy-server").connect({ + legacy_http_version = true + }) + } + } + } +--- config + listen 12345 ssl; + server_name test.com; + ssl_certificate ../../cert/test.crt; + ssl_certificate_key ../../cert/test.key; + server_tokens off; + + location = /c { + content_by_lua ' + local client = require "resty.websocket.client" + local wb, err = client:new() + + local uri = "wss://127.0.0.1:12345/s" + local ok, err = wb:connect(uri, { + proxy_opts = { + wss_proxy = "http://127.0.0.1:16796", + }, + }) + if not ok then + ngx.say("failed to connect: " .. err) + return + end + + local data = "hello" + local bytes, err = wb:send_text(data) + if not bytes then + ngx.say("failed to send frame: ", err) + return + end + + local typ + data, typ, err = wb:recv_frame() + if not data then + ngx.say("failed to receive 2nd frame: ", err) + return + end + + ngx.say("received: ", data, " (", typ, ")") + + -- note our mock forward proxy server does not support + -- keepalive, so we must close it here + local ok, err = wb:close() + if not ok then + ngx.say("failed to close conn: ", err) + return + end + '; + } + + location = /s { + content_by_lua ' + local server = require "resty.websocket.server" + local wb, err = server:new() + if not wb then + ngx.log(ngx.ERR, "failed to new websocket: ", err) + return ngx.exit(444) + end + + while true do + local data, typ, err = wb:recv_frame() + if not data then + -- ngx.log(ngx.ERR, "failed to receive a frame: ", err) + return ngx.exit(444) + end + + -- send it back! + local bytes, err = wb:send_text(data) + if not bytes then + ngx.log(ngx.ERR, "failed to send the 2nd text: ", err) + return ngx.exit(444) + end + end + '; + } +--- request +GET /c +--- response_body +received: hello (text) + +--- no_error_log +[error] +[warn] + + + +=== TEST 45: get_resp_status_code and headers when error connect +--- http_config eval: $::HttpConfig +--- config + location = /c { + content_by_lua ' + local client = require "resty.websocket.client" + local wb, err = client:new() + local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/s" + -- ngx.say("uri: ", uri) + local ok, err = wb:connect(uri) + if not ok then + local headers = wb:get_resp_headers() + local status_code = wb:get_resp_status_code() + ngx.say("1: status code: ", status_code) + ngx.say("2: retry-after: ", headers.retry_after) + return + else + ngx.say("websocket should fail") + end + '; + } + + location = /s { + content_by_lua ' + ngx.header["retry-after"] = "30" + return ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS) + '; + } +--- request +GET /c +--- response_body +1: status code: 429 +2: retry-after: 30 +--- no_error_log +[error] +[warn] + + + +=== TEST 46: get_resp_headers and get_resp_status_code return nil if connect failed without response +--- http_config eval: $::HttpConfig +--- config + location = /c { + content_by_lua ' + local client = require "resty.websocket.client" + local wb, err = client:new() + local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/s" + local ok, err = wb:connect(uri) + if not ok then + local headers, err = wb:get_resp_headers() + local status_code = wb:get_resp_status_code() + ngx.say("1: status code: ", status_code) + ngx.say("2: headers: ", headers) + ngx.say("3: error: ", err) + return + end + '; + } + + location = /s { + content_by_lua ' + return ngx.exit(ngx.HTTP_CLOSE) + '; + } +--- request +GET /c +--- response_body +1: status code: nil +2: headers: nil +3: error: response header not available +--- no_error_log +[error] +[warn] diff --git a/t/forward-proxy-server.lua b/t/forward-proxy-server.lua new file mode 100644 index 0000000..94beabc --- /dev/null +++ b/t/forward-proxy-server.lua @@ -0,0 +1,133 @@ +local _M = {} + +local fmt = string.format +local split = require("ngx.re").split + +local header_mt = { + __index = function(self, name) + name = name:lower():gsub("_", "-") + return rawget(self, name) + end, + + __newindex = function(self, name, value) + name = name:lower():gsub("_", "-") + rawset(self, name, value) + end, +} + +local function new_headers() + return setmetatable({}, header_mt) +end + +local function respond(msg, http_version) + ngx.print(fmt("HTTP/%s %s\r\n\r\n", http_version or "1.1", msg)) +end + + +-- This is a very naive forward proxy, which accepts a CONNECT over HTTP, and +-- then starts tunnelling the bytes blind (for end-to-end SSL). +function _M.connect(opts) + local req_sock = ngx.req.socket(true) + req_sock:settimeouts(1000, 1000, 1000) + + -- receive request line + local req_line = req_sock:receive() + ngx.log(ngx.DEBUG, "request line: ", req_line) + + local method, host_port = unpack(split(req_line, " ")) + if method ~= "CONNECT" then + return ngx.exit(400) + end + + local upstream_host, upstream_port = unpack(split(host_port, ":")) + + local headers = new_headers() + + -- receive headers + repeat + local line = req_sock:receive("*l") + local name, value = line:match("^([^:]+):%s*(.+)$") + if name and value then + -- don't log header values: this includes Proxy-Authorization, which + -- carries the client's credentials + ngx.log(ngx.DEBUG, "header: ", name) + headers[name] = value + end + until ngx.re.find(line, "^\\s*$", "jo") + + + local http_version = opts and opts.legacy_http_version and "1.0" or "1.1" + local basic_auth = opts and opts.basic_auth + if basic_auth then + ngx.log(ngx.DEBUG, "checking proxy-authorization...") + + local found = headers["proxy-authorization"] + if not found then + ngx.log(ngx.NOTICE, "client did not send proxy-authorization header") + respond("401 Unauthorized", http_version) + return ngx.exit(ngx.OK) + end + + local auth = ngx.re.gsub(found, [[^Basic\s*]], "", "oji") + + if auth ~= basic_auth then + ngx.log(ngx.NOTICE, "client sent incorrect proxy-authorization") + respond("403 Forbidden", http_version) + return ngx.exit(ngx.OK) + end + + ngx.log(ngx.DEBUG, "accepted basic proxy-authorization") + end + + + -- Connect to requested upstream + local upstream_sock = ngx.socket.tcp() + upstream_sock:settimeouts(1000, 1000, 1000) + local ok, err = upstream_sock:connect(upstream_host, upstream_port) + if not ok then + ngx.log(ngx.ERR, "connect to upstream ", upstream_host, ":", upstream_port, + " failed: ", err) + return ngx.exit(504) + end + + -- Tell the client we are good to go + respond("200 OK", http_version) + ngx.flush() + + ngx.log(ngx.DEBUG, "tunneling started") + + -- 10Kb in either direction should be plenty + local max_bytes = 10 * 1024 + + repeat + local req_data = req_sock:receiveany(max_bytes) + if req_data then + ngx.log(ngx.DEBUG, "client RCV ", #req_data, " bytes") + + local bytes, err = upstream_sock:send(req_data) + if bytes then + ngx.log(ngx.DEBUG, "upstream SND ", bytes, " bytes") + elseif err then + ngx.log(ngx.ERR, "upstream SND failed: ", err) + end + end + + local res_data = upstream_sock:receiveany(max_bytes) + if res_data then + ngx.log(ngx.DEBUG, "upstream RCV ", #res_data, " bytes") + + local bytes, err = req_sock:send(res_data) + if bytes then + ngx.log(ngx.DEBUG, "client SND: ", bytes, " bytes") + elseif err then + ngx.log(ngx.ERR, "client SND failed: ", err) + end + end + until not req_data and not res_data -- request socket should be closed + + upstream_sock:close() + + ngx.log(ngx.DEBUG, "tunneling ended") +end + +return _M diff --git a/t/handshake.t b/t/handshake.t index 4de53dc..71a7fef 100644 --- a/t/handshake.t +++ b/t/handshake.t @@ -49,7 +49,7 @@ __DATA__ --- request GET /t --- response_body -failed to connect: failed websocket handshake: unexpected response status: 403 +failed to connect: unexpected HTTP response code: 403 --- no_error_log [error] @@ -84,7 +84,7 @@ failed to connect: failed websocket handshake: unexpected response status: 403 --- request GET /t --- response_body -failed to connect: failed websocket handshake: unexpected response status: 301 +failed to connect: unexpected HTTP response code: 301 --- no_error_log [error] diff --git a/t/sanity.t b/t/sanity.t index 46216c6..5827546 100644 --- a/t/sanity.t +++ b/t/sanity.t @@ -6,7 +6,7 @@ use Protocol::WebSocket::Frame; repeat_each(2); -plan tests => repeat_each() * 162; +plan tests => repeat_each() * 162 + 18; my $pwd = cwd(); @@ -943,3 +943,118 @@ Sec-WebSocket-Protocol: chat --- no_error_log [error] --- error_code: 101 + + + +=== TEST 23: client should not send a close frame when server responds non-101 +--- http_config eval: $::HttpConfig +--- config + location = /c { + content_by_lua_block { + local client = require "resty.websocket.client" + local wb, err = client:new() + local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/plain" + local ok, err, header = wb:connect(uri) + if ok then + ngx.say("unexpected ok: ", header) + return + end + + ngx.say("connect result: ", err) + } + } + + location = /plain { + default_type text/plain; + return 200 'plain http'; + } +--- request +GET /c +--- response_body +connect result: unexpected HTTP response code: 200 +--- no_error_log +[error] + + + +=== TEST 24: fatal is set and socket is closed after non-101 (default, no keep_response) +--- http_config eval: $::HttpConfig +--- config + location = /c { + content_by_lua_block { + local client = require "resty.websocket.client" + local wb, err = client:new() + local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/plain" + local ok, err = wb:connect(uri) + if ok then + ngx.say("unexpected ok") + return + end + + ngx.say("fatal: ", tostring(wb.fatal)) + + local _, send_err = wb:send_text("hello") + ngx.say("send_text: ", send_err) + + local _, _, recv_err = wb:recv_frame() + ngx.say("recv_frame: ", recv_err) + } + } + + location = /plain { + default_type text/plain; + return 200 'plain http'; + } +--- request +GET /c +--- response_body +fatal: true +send_text: fatal error already happened +recv_frame: fatal error already happened +--- no_error_log +[error] + + + +=== TEST 25: keep_response=true leaves socket open for body reading; fatal still set +--- http_config eval: $::HttpConfig +--- config + location = /c { + content_by_lua_block { + local client = require "resty.websocket.client" + local wb, err = client:new() + local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/plain" + local ok, err = wb:connect(uri, { keep_response = true }) + if ok then + ngx.say("unexpected ok") + return + end + + ngx.say("fatal: ", tostring(wb.fatal)) + + local _, send_err = wb:send_text("hello") + ngx.say("send_text: ", send_err) + + -- raw socket is still open; read the HTTP response body directly + local body, read_err = wb.sock:receive(10) + if read_err then + ngx.say("body read failed: ", read_err) + else + ngx.say("body: ", body) + end + wb.sock:close() + } + } + + location = /plain { + default_type text/plain; + return 200 'plain http'; + } +--- request +GET /c +--- response_body +fatal: true +send_text: fatal error already happened +body: plain http +--- no_error_log +[error]