From 949713d191377b83c5e9dee7524f356b3200f9e2 Mon Sep 17 00:00:00 2001 From: flrgh Date: Tue, 4 Jun 2024 09:03:16 +0800 Subject: [PATCH 1/2] feature: support custom host header in client. This allows callers of `client:connect()` to specify a custom host header. This option also overrides the default SNI if not explicitly set. It also adds documentation and tests for the recently-added `server_name` option. --- README.markdown | 8 + lib/resty/websocket/client.lua | 24 ++- t/cs.t | 299 ++++++++++++++++++++++++++++++++- 3 files changed, 325 insertions(+), 6 deletions(-) diff --git a/README.markdown b/README.markdown index a425f69..8ed3233 100644 --- a/README.markdown +++ b/README.markdown @@ -447,6 +447,14 @@ SSL handshake if the `wss://` scheme is used. [ngx.ssl.parse_pem_priv_key](https://github.com/openresty/lua-resty-core/blob/master/lib/ngx/ssl.md#parse_pem_priv_key) function provided by lua-resty-core. +* `host` + + Specifies the value of the `Host` header sent in the handshake request. If not provided, the `Host` header will be derived from the hostname/address and port in the connection URI. + +* `server_name` + + Specifies the server name (SNI) to use when performing the TLS handshake with the server. If not provided, the `host` value or the `:` from the connection URI will be used. + The SSL connection mode (`wss://`) requires at least `ngx_lua` 0.9.11 or OpenResty 1.7.4.1. [Back to TOC](#table-of-contents) diff --git a/lib/resty/websocket/client.lua b/lib/resty/websocket/client.lua index a256c78..37eb860 100644 --- a/lib/resty/websocket/client.lua +++ b/lib/resty/websocket/client.lua @@ -94,7 +94,7 @@ function _M.connect(self, uri, opts) end local scheme = m[1] - local host = m[2] + local addr = m[2] local port = m[3] local path = m[4] @@ -117,6 +117,7 @@ function _M.connect(self, uri, opts) local ssl_verify, server_name, headers, proto_header, origin_header local sock_opts = {} local client_cert, client_priv_key + local host if opts then local protos = opts.protocols @@ -155,9 +156,11 @@ function _M.connect(self, uri, opts) "client_priv_key must be provided with client_cert") end - if opts.ssl_verify or opts.server_name then - ssl_verify = opts.ssl_verify - server_name = opts.server_name or host + ssl_verify = opts.ssl_verify + + server_name = opts.server_name + if server_name ~= nil and type(server_name) ~= "string" then + return nil, "SSL server_name must be a string" end if opts.headers then @@ -166,6 +169,11 @@ function _M.connect(self, uri, opts) return nil, "custom headers must be a table" end end + + host = opts.host + if host ~= nil and type(host) ~= "string" then + return nil, "custom host header must be a string" + end end local ok, err @@ -196,6 +204,9 @@ function _M.connect(self, uri, opts) return nil, "failed to set TLS client certificate: " .. err end end + + server_name = server_name or host or addr + ok, err = sock:sslhandshake(false, server_name, ssl_verify) if not ok then return nil, "ssl handshake failed: " .. err @@ -218,8 +229,11 @@ function _M.connect(self, uri, opts) rand(256) - 1) local key = encode_base64(bytes) + + local host_header = host or (is_unix and "unix_sock" or addr .. ":" .. port) + local req = "GET " .. path .. " HTTP/1.1\r\nUpgrade: websocket\r\nHost: " - .. (is_unix and "unix_sock" or host .. ":" .. port) + .. host_header .. "\r\nSec-WebSocket-Key: " .. key .. (proto_header or "") .. "\r\nSec-WebSocket-Version: 13" diff --git a/t/cs.t b/t/cs.t index a296d77..0ed82fc 100644 --- a/t/cs.t +++ b/t/cs.t @@ -6,7 +6,7 @@ use Protocol::WebSocket::Frame; repeat_each(2); -plan tests => repeat_each() * (blocks() * 4 + 13); +plan tests => repeat_each() * (blocks() * 4 + 3); my $pwd = cwd(); @@ -2191,3 +2191,300 @@ received: hello (text) [warn] --- timeout: 10 + + + +=== TEST 30: handshake with default host header +--- 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 .. "/s" + local ok, err = wb:connect(uri) + if not ok then + ngx.say("failed to connect: ", err) + return + end + } + } + + location = /s { + content_by_lua_block { + 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 + ngx.log(ngx.INFO, "host: <", ngx.var.http_host, ">") + } + } +--- request +GET /c +--- error_log eval +qr/host: <127.0.0.1:\d+>/ + + + +=== TEST 31: handshake with custom host header (without port number) +--- 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 .. "/s" + local ok, err = wb:connect(uri, { host = "client.test" }) + if not ok then + ngx.say("failed to connect: ", err) + return + end + } + } + + location = /s { + content_by_lua_block { + 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 + ngx.log(ngx.INFO, "host: <", ngx.var.http_host, ">") + } + } +--- request +GET /c +--- error_log +host: + + + +=== TEST 32: handshake with custom host header (with port number) +--- 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 .. "/s" + local ok, err = wb:connect(uri, { host = "client.test:8080" }) + if not ok then + ngx.say("failed to connect: ", err) + return + end + } + } + + location = /s { + content_by_lua_block { + 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 + ngx.log(ngx.INFO, "host: <", ngx.var.http_host, ">") + } + } +--- request +GET /c +--- error_log +host: + + + +=== TEST 33: SNI derived from custom host header (without port number) +--- http_config eval: $::HttpConfig +--- 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_block { + local client = require "resty.websocket.client" + local wb, err = client:new() + + local uri = "wss://127.0.0.1:12345/s" + local opts = { + host = "test.com", + ssl_verify = false, + } + local ok, err = wb:connect(uri, opts) + if not ok then + ngx.say("failed to connect: ", err) + return + end + } + } + + location = /s { + content_by_lua_block { + 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 + + ngx.log(ngx.INFO, "host: <", ngx.var.http_host, ">") + ngx.log(ngx.INFO, "SSL server name: <", ngx.var.ssl_server_name, ">") + } + } +--- request +GET /c +--- error_log +host: +SSL server name: + + + +=== TEST 34: SNI derived from custom host header (with port number) +--- http_config eval: $::HttpConfig +--- 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_block { + local client = require "resty.websocket.client" + local wb, err = client:new() + + local uri = "wss://127.0.0.1:12345/s" + local opts = { + host = "test.com:8443", + ssl_verify = false, + } + local ok, err = wb:connect(uri, opts) + if not ok then + ngx.say("failed to connect: ", err) + return + end + } + } + + location = /s { + content_by_lua_block { + 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 + + ngx.log(ngx.INFO, "host: <", ngx.var.http_host, ">") + ngx.log(ngx.INFO, "SSL server name: <", ngx.var.ssl_server_name, ">") + } + } +--- request +GET /c +--- error_log +host: +SSL server name: + + + +=== TEST 35: custom SNI +--- http_config eval: $::HttpConfig +--- config + listen 12345 ssl; + server_name test.com client.test; + ssl_certificate ../../cert/test.crt; + ssl_certificate_key ../../cert/test.key; + server_tokens off; + + location = /c { + content_by_lua_block { + local client = require "resty.websocket.client" + local wb, err = client:new() + + local uri = "wss://127.0.0.1:12345/s" + local opts = { + server_name = "test.com", + ssl_verify = false, + } + local ok, err = wb:connect(uri, opts) + if not ok then + ngx.say("failed to connect: ", err) + return + end + } + } + + location = /s { + content_by_lua_block { + 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 + + ngx.log(ngx.INFO, "host: <", ngx.var.http_host, ">") + ngx.log(ngx.INFO, "SSL server name: <", ngx.var.ssl_server_name, ">") + } + } +--- request +GET /c +--- error_log eval +[ + qr/host: <127.0.0.1:\d+>/, + "SSL server name: ", +] + + + +=== TEST 36: custom SNI and host +--- http_config eval: $::HttpConfig +--- config + listen 12345 ssl; + server_name test.com client.test; + ssl_certificate ../../cert/test.crt; + ssl_certificate_key ../../cert/test.key; + server_tokens off; + + location = /c { + content_by_lua_block { + local client = require "resty.websocket.client" + local wb, err = client:new() + + local uri = "wss://127.0.0.1:12345/s" + local opts = { + host = "client.test", + server_name = "test.com", + ssl_verify = false, + } + local ok, err = wb:connect(uri, opts) + if not ok then + ngx.say("failed to connect: ", err) + return + end + } + } + + location = /s { + content_by_lua_block { + 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 + + ngx.log(ngx.INFO, "host: <", ngx.var.http_host, ">") + ngx.log(ngx.INFO, "SSL server name: <", ngx.var.ssl_server_name, ">") + } + } +--- request +GET /c +--- error_log +host: +SSL server name: From 8aba92936c5214510b512b7b14ae32a3852acb80 Mon Sep 17 00:00:00 2001 From: lijunlong Date: Tue, 4 Jun 2024 09:24:52 +0800 Subject: [PATCH 2/2] more fixes --- lib/resty/websocket/client.lua | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/resty/websocket/client.lua b/lib/resty/websocket/client.lua index 37eb860..a91e538 100644 --- a/lib/resty/websocket/client.lua +++ b/lib/resty/websocket/client.lua @@ -117,7 +117,7 @@ function _M.connect(self, uri, opts) local ssl_verify, server_name, headers, proto_header, origin_header local sock_opts = {} local client_cert, client_priv_key - local host + local header_host if opts then local protos = opts.protocols @@ -170,17 +170,17 @@ function _M.connect(self, uri, opts) end end - host = opts.host - if host ~= nil and type(host) ~= "string" then + header_host = opts.host + if header_host ~= nil and type(header_host) ~= "string" then return nil, "custom host header must be a string" end end local ok, err if is_unix then - ok, err = sock:connect(host, sock_opts) + ok, err = sock:connect(addr, sock_opts) else - ok, err = sock:connect(host, port, sock_opts) + ok, err = sock:connect(addr, port, sock_opts) end if not ok then return nil, "failed to connect: " .. err @@ -205,7 +205,7 @@ function _M.connect(self, uri, opts) end end - server_name = server_name or host or addr + server_name = server_name or header_host or addr ok, err = sock:sslhandshake(false, server_name, ssl_verify) if not ok then @@ -230,7 +230,8 @@ function _M.connect(self, uri, opts) local key = encode_base64(bytes) - local host_header = host or (is_unix and "unix_sock" or addr .. ":" .. port) + local host_header = header_host + or (is_unix and "unix_sock" or addr .. ":" .. port) local req = "GET " .. path .. " HTTP/1.1\r\nUpgrade: websocket\r\nHost: " .. host_header