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