1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
|
local ngx_re_gmatch = ngx.re.gmatch
local ngx_re_sub = ngx.re.sub
local ngx_re_find = ngx.re.find
local ngx_log = ngx.log
local ngx_WARN = ngx.WARN
--[[
A connection function that incorporates:
- tcp connect
- ssl handshake
- http proxy
Due to this it will be better at setting up a socket pool where connections can
be kept alive.
Call it with a single options table as follows:
client:connect {
scheme = "https" -- scheme to use, or nil for unix domain socket
host = "myhost.com", -- target machine, or a unix domain socket
port = nil, -- port on target machine, will default to 80/443 based on scheme
pool = nil, -- connection pool name, leave blank! this function knows best!
pool_size = nil, -- options as per: https://github.com/openresty/lua-nginx-module#tcpsockconnect
backlog = nil,
-- ssl options as per: https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake
ssl_reused_session = nil
ssl_server_name = nil,
ssl_send_status_req = nil,
ssl_verify = true, -- NOTE: defaults to true
ctx = nil, -- NOTE: not supported
-- mTLS options (experimental!)
--
-- !!! IMPORTANT !!! These options require support for mTLS in cosockets,
-- which is currently only available in the following unmerged PRs.
--
-- * https://github.com/openresty/lua-nginx-module/pull/1602
-- * https://github.com/openresty/lua-resty-core/pull/278
--
-- The details of this feature may change. You have been warned!
--
ssl_client_cert = nil,
ssl_client_priv_key = nil,
proxy_opts, -- proxy opts, defaults to global proxy options
}
]]
local function connect(self, options)
local sock = self.sock
if not sock then
return nil, "not initialized"
end
local ok, err
local request_scheme = options.scheme
local request_host = options.host
local request_port = options.port
local poolname = options.pool
local pool_size = options.pool_size
local backlog = options.backlog
if request_scheme and not request_port then
request_port = (request_scheme == "https" and 443 or 80)
elseif request_port and not request_scheme then
return nil, "'scheme' is required when providing a port"
end
-- ssl settings
local ssl, ssl_reused_session, ssl_server_name
local ssl_verify, ssl_send_status_req, ssl_client_cert, ssl_client_priv_key
if request_scheme == "https" then
ssl = true
ssl_reused_session = options.ssl_reused_session
ssl_server_name = options.ssl_server_name
ssl_send_status_req = options.ssl_send_status_req
ssl_verify = true -- default
if options.ssl_verify == false then
ssl_verify = false
end
ssl_client_cert = options.ssl_client_cert
ssl_client_priv_key = options.ssl_client_priv_key
end
-- proxy related settings
local proxy, proxy_uri, proxy_authorization, proxy_host, proxy_port, path_prefix
proxy = options.proxy_opts or self.proxy_opts
if proxy then
if request_scheme == "https" then
proxy_uri = proxy.https_proxy
proxy_authorization = proxy.https_proxy_authorization
else
proxy_uri = proxy.http_proxy
proxy_authorization = proxy.http_proxy_authorization
-- When a proxy is used, the target URI must be in absolute-form
-- (RFC 7230, Section 5.3.2.). That is, it must be an absolute URI
-- to the remote resource with the scheme, host and an optional port
-- in place.
--
-- Since _format_request() constructs the request line by concatenating
-- params.path and params.query together, we need to modify the path
-- to also include the scheme, host and port so that the final form
-- in conformant to RFC 7230.
path_prefix = "http://" .. request_host .. (request_port == 80 and "" or (":" .. request_port))
end
if not proxy_uri then
proxy = nil
proxy_authorization = nil
path_prefix = nil
end
end
if proxy and proxy.no_proxy then
-- Check if the no_proxy option matches this host. Implementation adapted
-- from lua-http library (https://github.com/daurnimator/lua-http)
if proxy.no_proxy == "*" then
-- all hosts are excluded
proxy = nil
else
local host = request_host
local no_proxy_set = {}
-- wget allows domains in no_proxy list to be prefixed by "."
-- e.g. no_proxy=.mit.edu
for host_suffix in ngx_re_gmatch(proxy.no_proxy, "\\.?([^,]+)") do
no_proxy_set[host_suffix[1]] = true
end
-- From curl docs:
-- matched as either a domain which contains the hostname, or the
-- hostname itself. For example local.com would match local.com,
-- local.com:80, and www.local.com, but not www.notlocal.com.
--
-- Therefore, we keep stripping subdomains from the host, compare
-- them to the ones in the no_proxy list and continue until we find
-- a match or until there's only the TLD left
repeat
if no_proxy_set[host] then
proxy = nil
proxy_uri = nil
proxy_authorization = nil
break
end
-- Strip the next level from the domain and check if that one
-- is on the list
host = ngx_re_sub(host, "^[^.]+\\.", "")
until not ngx_re_find(host, "\\.")
end
end
if proxy then
local proxy_uri_t
proxy_uri_t, err = self:parse_uri(proxy_uri)
if not proxy_uri_t then
return nil, "uri parse error: ", err
end
local proxy_scheme = proxy_uri_t[1]
if proxy_scheme ~= "http" then
return nil, "protocol " .. tostring(proxy_scheme) ..
" not supported for proxy connections"
end
proxy_host = proxy_uri_t[2]
proxy_port = proxy_uri_t[3]
end
-- construct a poolname unique within proxy and ssl info
if not poolname then
poolname = (request_scheme or "")
.. ":" .. request_host
.. ":" .. tostring(request_port)
.. ":" .. tostring(ssl)
.. ":" .. (ssl_server_name or "")
.. ":" .. tostring(ssl_verify)
.. ":" .. (proxy_uri or "")
.. ":" .. (request_scheme == "https" and proxy_authorization or "")
-- in the above we only add the 'proxy_authorization' as part of the poolname
-- when the request is https. Because in that case the CONNECT request (which
-- carries the authorization header) is part of the connect procedure, whereas
-- with a plain http request the authorization is part of the actual request.
end
-- do TCP level connection
local tcp_opts = { pool = poolname, pool_size = pool_size, backlog = backlog }
if proxy then
-- proxy based connection
ok, err = sock:connect(proxy_host, proxy_port, tcp_opts)
if not ok then
return nil, "failed to connect to: " .. (proxy_host or "") ..
":" .. (proxy_port or "") ..
": ", err
end
if ssl and sock:getreusedtimes() == 0 then
-- Make a CONNECT request to create a tunnel to the destination through
-- the proxy. The request-target and the Host header must be in the
-- authority-form of RFC 7230 Section 5.3.3. See also RFC 7231 Section
-- 4.3.6 for more details about the CONNECT request
local destination = request_host .. ":" .. request_port
local res
res, err = self:request({
method = "CONNECT",
path = destination,
headers = {
["Host"] = destination,
["Proxy-Authorization"] = proxy_authorization,
}
})
if not res then
return nil, "failed to issue CONNECT to proxy:", err
end
if res.status < 200 or res.status > 299 then
return nil, "failed to establish a tunnel through a proxy: " .. res.status
end
end
elseif not request_port then
-- non-proxy, without port -> unix domain socket
ok, err = sock:connect(request_host, tcp_opts)
if not ok then
return nil, err
end
else
-- non-proxy, regular network tcp
ok, err = sock:connect(request_host, request_port, tcp_opts)
if not ok then
return nil, err
end
end
local ssl_session
-- Now do the ssl handshake
if ssl and sock:getreusedtimes() == 0 then
-- Experimental mTLS support
if ssl_client_cert and ssl_client_priv_key then
if type(sock.setclientcert) ~= "function" then
ngx_log(ngx_WARN, "cannot use SSL client cert and key without mTLS support")
else
-- currently no return value
ok, err = sock:setclientcert(ssl_client_cert, ssl_client_priv_key)
if not ok then
ngx_log(ngx_WARN, "could not set client certificate: ", err)
end
end
end
ssl_session, err = sock:sslhandshake(ssl_reused_session, ssl_server_name, ssl_verify, ssl_send_status_req)
if not ssl_session then
self:close()
return nil, err
end
end
self.host = request_host
self.port = request_port
self.keepalive = true
self.ssl = ssl
-- set only for http, https has already been handled
self.http_proxy_auth = request_scheme ~= "https" and proxy_authorization or nil
self.path_prefix = path_prefix
return true, nil, ssl_session
end
return connect
|