blob: d9e05e6fc3aa0937bbd3813eec18ce3bffd5f4a6 [file] [log] [blame]
local http_headers = require "resty.http_headers"
local ngx = ngx
local ngx_socket_tcp = ngx.socket.tcp
local ngx_req = ngx.req
local ngx_req_socket = ngx_req.socket
local ngx_req_get_headers = ngx_req.get_headers
local ngx_req_get_method = ngx_req.get_method
local str_lower = string.lower
local str_upper = string.upper
local str_find = string.find
local str_sub = string.sub
local tbl_concat = table.concat
local tbl_insert = table.insert
local ngx_encode_args = ngx.encode_args
local ngx_re_match = ngx.re.match
local ngx_re_gmatch = ngx.re.gmatch
local ngx_re_sub = ngx.re.sub
local ngx_re_gsub = ngx.re.gsub
local ngx_re_find = ngx.re.find
local ngx_log = ngx.log
local ngx_DEBUG = ngx.DEBUG
local ngx_ERR = ngx.ERR
local ngx_var = ngx.var
local ngx_print = ngx.print
local ngx_header = ngx.header
local co_yield = coroutine.yield
local co_create = coroutine.create
local co_status = coroutine.status
local co_resume = coroutine.resume
local setmetatable = setmetatable
local tonumber = tonumber
local tostring = tostring
local unpack = unpack
local rawget = rawget
local select = select
local ipairs = ipairs
local pairs = pairs
local pcall = pcall
local type = type
-- http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
local HOP_BY_HOP_HEADERS = {
["connection"] = true,
["keep-alive"] = true,
["proxy-authenticate"] = true,
["proxy-authorization"] = true,
["te"] = true,
["trailers"] = true,
["transfer-encoding"] = true,
["upgrade"] = true,
["content-length"] = true, -- Not strictly hop-by-hop, but Nginx will deal
-- with this (may send chunked for example).
}
local EXPECTING_BODY = {
POST = true,
PUT = true,
PATCH = true,
}
-- Reimplemented coroutine.wrap, returning "nil, err" if the coroutine cannot
-- be resumed. This protects user code from inifite loops when doing things like
-- repeat
-- local chunk, err = res.body_reader()
-- if chunk then -- <-- This could be a string msg in the core wrap function.
-- ...
-- end
-- until not chunk
local co_wrap = function(func)
local co = co_create(func)
if not co then
return nil, "could not create coroutine"
else
return function(...)
if co_status(co) == "suspended" then
return select(2, co_resume(co, ...))
else
return nil, "can't resume a " .. co_status(co) .. " coroutine"
end
end
end
end
-- Returns a new table, recursively copied from the one given.
--
-- @param table table to be copied
-- @return table
local function tbl_copy(orig)
local orig_type = type(orig)
local copy
if orig_type == "table" then
copy = {}
for orig_key, orig_value in next, orig, nil do
copy[tbl_copy(orig_key)] = tbl_copy(orig_value)
end
else -- number, string, boolean, etc
copy = orig
end
return copy
end
local _M = {
_VERSION = '0.14',
}
_M._USER_AGENT = "lua-resty-http/" .. _M._VERSION .. " (Lua) ngx_lua/" .. ngx.config.ngx_lua_version
local mt = { __index = _M }
local HTTP = {
[1.0] = " HTTP/1.0\r\n",
[1.1] = " HTTP/1.1\r\n",
}
local DEFAULT_PARAMS = {
method = "GET",
path = "/",
version = 1.1,
}
local DEBUG = false
function _M.new(_)
local sock, err = ngx_socket_tcp()
if not sock then
return nil, err
end
return setmetatable({ sock = sock, keepalive = true }, mt)
end
function _M.debug(d)
DEBUG = (d == true)
end
function _M.set_timeout(self, timeout)
local sock = self.sock
if not sock then
return nil, "not initialized"
end
return sock:settimeout(timeout)
end
function _M.set_timeouts(self, connect_timeout, send_timeout, read_timeout)
local sock = self.sock
if not sock then
return nil, "not initialized"
end
return sock:settimeouts(connect_timeout, send_timeout, read_timeout)
end
function _M.ssl_handshake(self, ...)
local sock = self.sock
if not sock then
return nil, "not initialized"
end
self.ssl = true
return sock:sslhandshake(...)
end
function _M.connect(self, ...)
local sock = self.sock
if not sock then
return nil, "not initialized"
end
self.host = select(1, ...)
self.port = select(2, ...)
-- If port is not a number, this is likely a unix domain socket connection.
if type(self.port) ~= "number" then
self.port = nil
end
self.keepalive = true
return sock:connect(...)
end
function _M.set_keepalive(self, ...)
local sock = self.sock
if not sock then
return nil, "not initialized"
end
if self.keepalive == true then
return sock:setkeepalive(...)
else
-- The server said we must close the connection, so we cannot setkeepalive.
-- If close() succeeds we return 2 instead of 1, to differentiate between
-- a normal setkeepalive() failure and an intentional close().
local res, err = sock:close()
if res then
return 2, "connection must be closed"
else
return res, err
end
end
end
function _M.get_reused_times(self)
local sock = self.sock
if not sock then
return nil, "not initialized"
end
return sock:getreusedtimes()
end
function _M.close(self)
local sock = self.sock
if not sock then
return nil, "not initialized"
end
return sock:close()
end
local function _should_receive_body(method, code)
if method == "HEAD" then return nil end
if code == 204 or code == 304 then return nil end
if code >= 100 and code < 200 then return nil end
return true
end
function _M.parse_uri(_, uri, query_in_path)
if query_in_path == nil then query_in_path = true end
local m, err = ngx_re_match(uri, [[^(?:(http[s]?):)?//([^:/\?]+)(?::(\d+))?([^\?]*)\??(.*)]], "jo")
if not m then
if err then
return nil, "failed to match the uri: " .. uri .. ", " .. err
end
return nil, "bad uri: " .. uri
else
-- If the URI is schemaless (i.e. //example.com) try to use our current
-- request scheme.
if not m[1] then
local scheme = ngx_var.scheme
if scheme == "http" or scheme == "https" then
m[1] = scheme
else
return nil, "schemaless URIs require a request context: " .. uri
end
end
if m[3] then
m[3] = tonumber(m[3])
else
if m[1] == "https" then
m[3] = 443
else
m[3] = 80
end
end
if not m[4] or "" == m[4] then m[4] = "/" end
if query_in_path and m[5] and m[5] ~= "" then
m[4] = m[4] .. "?" .. m[5]
m[5] = nil
end
return m, nil
end
end
local function _format_request(params)
local version = params.version
local headers = params.headers or {}
local query = params.query or ""
if type(query) == "table" then
query = "?" .. ngx_encode_args(query)
elseif query ~= "" and str_sub(query, 1, 1) ~= "?" then
query = "?" .. query
end
-- Initialize request
local req = {
str_upper(params.method),
" ",
params.path,
query,
HTTP[version],
-- Pre-allocate slots for minimum headers and carriage return.
true,
true,
true,
}
local c = 6 -- req table index it's faster to do this inline vs table.insert
-- Append headers
for key, values in pairs(headers) do
key = tostring(key)
if type(values) == "table" then
for _, value in pairs(values) do
req[c] = key .. ": " .. tostring(value) .. "\r\n"
c = c + 1
end
else
req[c] = key .. ": " .. tostring(values) .. "\r\n"
c = c + 1
end
end
-- Close headers
req[c] = "\r\n"
return tbl_concat(req)
end
local function _receive_status(sock)
local line, err = sock:receive("*l")
if not line then
return nil, nil, nil, err
end
return tonumber(str_sub(line, 10, 12)), tonumber(str_sub(line, 6, 8)), str_sub(line, 14)
end
local function _receive_headers(sock)
local headers = http_headers.new()
repeat
local line, err = sock:receive("*l")
if not line then
return nil, err
end
local m, err = ngx_re_match(line, "([^:\\s]+):\\s*(.*)", "jo")
if err then ngx_log(ngx_ERR, err) end
if not m then
break
end
local key = m[1]
local val = m[2]
if headers[key] then
if type(headers[key]) ~= "table" then
headers[key] = { headers[key] }
end
tbl_insert(headers[key], tostring(val))
else
headers[key] = tostring(val)
end
until ngx_re_find(line, "^\\s*$", "jo")
return headers, nil
end
local function _chunked_body_reader(sock, default_chunk_size)
return co_wrap(function(max_chunk_size)
local remaining = 0
local length
max_chunk_size = max_chunk_size or default_chunk_size
repeat
-- If we still have data on this chunk
if max_chunk_size and remaining > 0 then
if remaining > max_chunk_size then
-- Consume up to max_chunk_size
length = max_chunk_size
remaining = remaining - max_chunk_size
else
-- Consume all remaining
length = remaining
remaining = 0
end
else -- This is a fresh chunk
-- Receive the chunk size
local str, err = sock:receive("*l")
if not str then
co_yield(nil, err)
end
length = tonumber(str, 16)
if not length then
co_yield(nil, "unable to read chunksize")
end
if max_chunk_size and length > max_chunk_size then
-- Consume up to max_chunk_size
remaining = length - max_chunk_size
length = max_chunk_size
end
end
if length > 0 then
local str, err = sock:receive(length)
if not str then
co_yield(nil, err)
end
max_chunk_size = co_yield(str) or default_chunk_size
-- If we're finished with this chunk, read the carriage return.
if remaining == 0 then
sock:receive(2) -- read \r\n
end
else
-- Read the last (zero length) chunk's carriage return
sock:receive(2) -- read \r\n
end
until length == 0
end)
end
local function _body_reader(sock, content_length, default_chunk_size)
return co_wrap(function(max_chunk_size)
max_chunk_size = max_chunk_size or default_chunk_size
if not content_length and max_chunk_size then
-- We have no length, but wish to stream.
-- HTTP 1.0 with no length will close connection, so read chunks to the end.
repeat
local str, err, partial = sock:receive(max_chunk_size)
if not str and err == "closed" then
co_yield(partial, err)
end
max_chunk_size = tonumber(co_yield(str) or default_chunk_size)
if max_chunk_size and max_chunk_size < 0 then max_chunk_size = nil end
if not max_chunk_size then
ngx_log(ngx_ERR, "Buffer size not specified, bailing")
break
end
until not str
elseif not content_length then
-- We have no length but don't wish to stream.
-- HTTP 1.0 with no length will close connection, so read to the end.
co_yield(sock:receive("*a"))
elseif not max_chunk_size then
-- We have a length and potentially keep-alive, but want everything.
co_yield(sock:receive(content_length))
else
-- We have a length and potentially a keep-alive, and wish to stream
-- the response.
local received = 0
repeat
local length = max_chunk_size
if received + length > content_length then
length = content_length - received
end
if length > 0 then
local str, err = sock:receive(length)
if not str then
co_yield(nil, err)
end
received = received + length
max_chunk_size = tonumber(co_yield(str) or default_chunk_size)
if max_chunk_size and max_chunk_size < 0 then max_chunk_size = nil end
if not max_chunk_size then
ngx_log(ngx_ERR, "Buffer size not specified, bailing")
break
end
end
until length == 0
end
end)
end
local function _no_body_reader()
return nil
end
local function _read_body(res)
local reader = res.body_reader
if not reader then
-- Most likely HEAD or 304 etc.
return nil, "no body to be read"
end
local chunks = {}
local c = 1
local chunk, err
repeat
chunk, err = reader()
if err then
return nil, err, tbl_concat(chunks) -- Return any data so far.
end
if chunk then
chunks[c] = chunk
c = c + 1
end
until not chunk
return tbl_concat(chunks)
end
local function _trailer_reader(sock)
return co_wrap(function()
co_yield(_receive_headers(sock))
end)
end
local function _read_trailers(res)
local reader = res.trailer_reader
if not reader then
return nil, "no trailers"
end
local trailers = reader()
setmetatable(res.headers, { __index = trailers })
end
local function _send_body(sock, body)
if type(body) == 'function' then
repeat
local chunk, err, partial = body()
if chunk then
local ok, err = sock:send(chunk)
if not ok then
return nil, err
end
elseif err ~= nil then
return nil, err, partial
end
until chunk == nil
elseif body ~= nil then
local bytes, err = sock:send(body)
if not bytes then
return nil, err
end
end
return true, nil
end
local function _handle_continue(sock, body)
local status, version, reason, err = _receive_status(sock) --luacheck: no unused
if not status then
return nil, nil, err
end
-- Only send body if we receive a 100 Continue
if status == 100 then
local ok, err = sock:receive("*l") -- Read carriage return
if not ok then
return nil, nil, err
end
_send_body(sock, body)
end
return status, version, err
end
function _M.send_request(self, params)
-- Apply defaults
setmetatable(params, { __index = DEFAULT_PARAMS })
local sock = self.sock
local body = params.body
local headers = http_headers.new()
local params_headers = params.headers
if params_headers then
-- We assign one by one so that the metatable can handle case insensitivity
-- for us. You can blame the spec for this inefficiency.
for k, v in pairs(params_headers) do
headers[k] = v
end
end
-- Ensure minimal headers are set
if not headers["Content-Length"] then
if type(body) == 'string' then
headers["Content-Length"] = #body
elseif body == nil and EXPECTING_BODY[str_upper(params.method)] then
headers["Content-Length"] = 0
end
end
if not headers["Host"] then
if (str_sub(self.host, 1, 5) == "unix:") then
return nil, "Unable to generate a useful Host header for a unix domain socket. Please provide one."
end
-- If we have a port (i.e. not connected to a unix domain socket), and this
-- port is non-standard, append it to the Host heaer.
if self.port then
if self.ssl and self.port ~= 443 then
headers["Host"] = self.host .. ":" .. self.port
elseif not self.ssl and self.port ~= 80 then
headers["Host"] = self.host .. ":" .. self.port
else
headers["Host"] = self.host
end
else
headers["Host"] = self.host
end
end
if not headers["User-Agent"] then
headers["User-Agent"] = _M._USER_AGENT
end
if params.version == 1.0 and not headers["Connection"] then
headers["Connection"] = "Keep-Alive"
end
params.headers = headers
-- Format and send request
local req = _format_request(params)
if DEBUG then ngx_log(ngx_DEBUG, "\n", req) end
local bytes, err = sock:send(req)
if not bytes then
return nil, err
end
-- Send the request body, unless we expect: continue, in which case
-- we handle this as part of reading the response.
if headers["Expect"] ~= "100-continue" then
local ok, err, partial = _send_body(sock, body)
if not ok then
return nil, err, partial
end
end
return true
end
function _M.read_response(self, params)
local sock = self.sock
local status, version, reason, err
-- If we expect: continue, we need to handle this, sending the body if allowed.
-- If we don't get 100 back, then status is the actual status.
if params.headers["Expect"] == "100-continue" then
local _status, _version, _err = _handle_continue(sock, params.body)
if not _status then
return nil, _err
elseif _status ~= 100 then
status, version, err = _status, _version, _err -- luacheck: no unused
end
end
-- Just read the status as normal.
if not status then
status, version, reason, err = _receive_status(sock)
if not status then
return nil, err
end
end
local res_headers, err = _receive_headers(sock)
if not res_headers then
return nil, err
end
-- keepalive is true by default. Determine if this is correct or not.
local ok, connection = pcall(str_lower, res_headers["Connection"])
if ok then
if (version == 1.1 and str_find(connection, "close", 1, true)) or
(version == 1.0 and not str_find(connection, "keep-alive", 1, true)) then
self.keepalive = false
end
else
-- no connection header
if version == 1.0 then
self.keepalive = false
end
end
local body_reader = _no_body_reader
local trailer_reader, err
local has_body = false
-- Receive the body_reader
if _should_receive_body(params.method, status) then
has_body = true
local te = res_headers["Transfer-Encoding"]
-- Handle duplicate headers
-- This shouldn't happen but can in the real world
if type(te) == "table" then
te = tbl_concat(te, "")
end
local ok, encoding = pcall(str_lower, te)
if not ok then
encoding = ""
end
if version == 1.1 and str_find(encoding, "chunked", 1, true) ~= nil then
body_reader, err = _chunked_body_reader(sock)
else
local ok, length = pcall(tonumber, res_headers["Content-Length"])
if not ok then
-- No content-length header, read until connection is closed by server
length = nil
end
body_reader, err = _body_reader(sock, length)
end
end
if res_headers["Trailer"] then
trailer_reader, err = _trailer_reader(sock)
end
if err then
return nil, err
else
return {
status = status,
reason = reason,
headers = res_headers,
has_body = has_body,
body_reader = body_reader,
read_body = _read_body,
trailer_reader = trailer_reader,
read_trailers = _read_trailers,
}
end
end
function _M.request(self, params)
params = tbl_copy(params) -- Take by value
local res, err = self:send_request(params)
if not res then
return res, err
else
return self:read_response(params)
end
end
function _M.request_pipeline(self, requests)
requests = tbl_copy(requests) -- Take by value
for _, params in ipairs(requests) do
if params.headers and params.headers["Expect"] == "100-continue" then
return nil, "Cannot pipeline request specifying Expect: 100-continue"
end
local res, err = self:send_request(params)
if not res then
return res, err
end
end
local responses = {}
for i, params in ipairs(requests) do
responses[i] = setmetatable({
params = params,
response_read = false,
}, {
-- Read each actual response lazily, at the point the user tries
-- to access any of the fields.
__index = function(t, k)
local res, err
if t.response_read == false then
res, err = _M.read_response(self, t.params)
t.response_read = true
if not res then
ngx_log(ngx_ERR, err)
else
for rk, rv in pairs(res) do
t[rk] = rv
end
end
end
return rawget(t, k)
end,
})
end
return responses
end
function _M.request_uri(self, uri, params)
params = tbl_copy(params or {}) -- Take by value
local parsed_uri, err = self:parse_uri(uri, false)
if not parsed_uri then
return nil, err
end
local scheme, host, port, path, query = unpack(parsed_uri)
if not params.path then params.path = path end
if not params.query then params.query = query end
-- See if we should use a proxy to make this request
local proxy_uri = self:get_proxy_uri(scheme, host)
-- Make the connection either through the proxy or directly
-- to the remote host
local c, err
if proxy_uri then
local proxy_authorization
if scheme == "https" then
if params.headers and params.headers["Proxy-Authorization"] then
proxy_authorization = params.headers["Proxy-Authorization"]
else
proxy_authorization = self.proxy_opts.https_proxy_authorization
end
end
c, err = self:connect_proxy(proxy_uri, scheme, host, port, proxy_authorization)
else
c, err = self:connect(host, port)
end
if not c then
return nil, err
end
if proxy_uri then
if scheme == "http" then
-- 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.
if port == 80 then
params.path = scheme .. "://" .. host .. path
else
params.path = scheme .. "://" .. host .. ":" .. port .. path
end
if self.proxy_opts.http_proxy_authorization then
if not params.headers then
params.headers = {}
end
if not params.headers["Proxy-Authorization"] then
params.headers["Proxy-Authorization"] = self.proxy_opts.http_proxy_authorization
end
end
elseif scheme == "https" then
-- don't keep this connection alive as the next request could target
-- any host and re-using the proxy tunnel for that is not possible
self.keepalive = false
end
-- self:connect_uri() set the host and port to point to the proxy server. As
-- the connection to the proxy has been established, set the host and port
-- to point to the actual remote endpoint at the other end of the tunnel to
-- ensure the correct Host header added to the requests.
self.host = host
self.port = port
end
if scheme == "https" then
local verify = true
if params.ssl_verify == false then
verify = false
end
local ok, err = self:ssl_handshake(nil, host, verify)
if not ok then
self:close()
return nil, err
end
end
local res, err = self:request(params)
if not res then
self:close()
return nil, err
end
local body, err = res:read_body()
if not body then
self:close()
return nil, err
end
res.body = body
if params.keepalive == false then
local ok, err = self:close()
if not ok then
ngx_log(ngx_ERR, err)
end
else
local ok, err = self:set_keepalive(params.keepalive_timeout, params.keepalive_pool)
if not ok then
ngx_log(ngx_ERR, err)
end
end
return res, nil
end
function _M.get_client_body_reader(_, chunksize, sock)
chunksize = chunksize or 65536
if not sock then
local ok, err
ok, sock, err = pcall(ngx_req_socket)
if not ok then
return nil, sock -- pcall err
end
if not sock then
if err == "no body" then
return nil
else
return nil, err
end
end
end
local headers = ngx_req_get_headers()
local length = headers.content_length
local encoding = headers.transfer_encoding
if length then
return _body_reader(sock, tonumber(length), chunksize)
elseif encoding and str_lower(encoding) == 'chunked' then
-- Not yet supported by ngx_lua but should just work...
return _chunked_body_reader(sock, chunksize)
else
return nil
end
end
function _M.proxy_request(self, chunksize)
return self:request({
method = ngx_req_get_method(),
path = ngx_re_gsub(ngx_var.uri, "\\s", "%20", "jo") .. ngx_var.is_args .. (ngx_var.query_string or ""),
body = self:get_client_body_reader(chunksize),
headers = ngx_req_get_headers(),
})
end
function _M.proxy_response(_, response, chunksize)
if not response then
ngx_log(ngx_ERR, "no response provided")
return
end
ngx.status = response.status
-- Filter out hop-by-hop headeres
for k, v in pairs(response.headers) do
if not HOP_BY_HOP_HEADERS[str_lower(k)] then
ngx_header[k] = v
end
end
local reader = response.body_reader
repeat
local chunk, err = reader(chunksize)
if err then
ngx_log(ngx_ERR, err)
break
end
if chunk then
local res, err = ngx_print(chunk)
if not res then
ngx_log(ngx_ERR, err)
break
end
end
until not chunk
end
function _M.set_proxy_options(self, opts)
self.proxy_opts = tbl_copy(opts) -- Take by value
end
function _M.get_proxy_uri(self, scheme, host)
if not self.proxy_opts then
return nil
end
-- Check if the no_proxy option matches this host. Implementation adapted
-- from lua-http library (https://github.com/daurnimator/lua-http)
if self.proxy_opts.no_proxy then
if self.proxy_opts.no_proxy == "*" then
-- all hosts are excluded
return nil
end
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(self.proxy_opts.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
return nil
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
if scheme == "http" and self.proxy_opts.http_proxy then
return self.proxy_opts.http_proxy
end
if scheme == "https" and self.proxy_opts.https_proxy then
return self.proxy_opts.https_proxy
end
return nil
end
function _M.connect_proxy(self, proxy_uri, scheme, host, port, proxy_authorization)
-- Parse the provided proxy URI
local parsed_proxy_uri, err = self:parse_uri(proxy_uri, false)
if not parsed_proxy_uri then
return nil, err
end
-- Check that the scheme is http (https is not supported for
-- connections between the client and the proxy)
local proxy_scheme = parsed_proxy_uri[1]
if proxy_scheme ~= "http" then
return nil, "protocol " .. proxy_scheme .. " not supported for proxy connections"
end
-- Make the connection to the given proxy
local proxy_host, proxy_port = parsed_proxy_uri[2], parsed_proxy_uri[3]
local c, err = self:connect(proxy_host, proxy_port)
if not c then
return nil, err
end
if scheme == "https" 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 = host .. ":" .. port
local res, err = self:request({
method = "CONNECT",
path = destination,
headers = {
["Host"] = destination,
["Proxy-Authorization"] = proxy_authorization,
}
})
if not res then
return nil, 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
return c, nil
end
return _M