Skip to content

Commit

Permalink
Support chunked requests when talking to proxy servers with request b…
Browse files Browse the repository at this point in the history
…uffering disabled
  • Loading branch information
tkan145 committed Nov 27, 2023
1 parent 3efa69a commit b02a888
Show file tree
Hide file tree
Showing 7 changed files with 1,439 additions and 45 deletions.
171 changes: 126 additions & 45 deletions gateway/src/apicast/http_proxy.lua
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
local format = string.format
local tostring = tostring
local ngx_flush = ngx.flush
local ngx_get_method = ngx.req.get_method
local ngx_http_version = ngx.req.http_version
local ngx_send_headers = ngx.send_headers

local resty_url = require "resty.url"
local resty_resolver = require 'resty.resolver'
local round_robin = require 'resty.balancer.round_robin'
local http_proxy = require 'resty.http.proxy'
local file_reader = require("resty.file").file_reader
local file_size = require("resty.file").file_size
local client_body_reader = require("resty.http.request_reader").get_client_body_reader
local send_response = require("resty.http.response_writer").send_response
local concat = table.concat

local _M = { }

local http_methods_with_body = {
POST = true,
PUT = true,
PATCH = true

Check warning on line 23 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L23

Added line #L23 was not covered by tests
}

local DEFAULT_CHUNKSIZE = 32 * 1024

function _M.reset()
_M.balancer = round_robin.new()
_M.resolver = resty_resolver
Expand Down Expand Up @@ -84,52 +98,105 @@ local function absolute_url(uri)
)
end

local function forward_https_request(proxy_uri, proxy_auth, uri, skip_https_connect)
-- This is needed to call ngx.req.get_body_data() below.
ngx.req.read_body()

-- We cannot use resty.http's .get_client_body_reader().
-- In POST requests with HTTPS, the result of that call is nil, and it
-- results in a time-out.
--
--
-- If ngx.req.get_body_data is nil, can be that the body is too big to
-- read and need to be cached in a local file. This request will return
-- nil, so after this we need to read the temp file.
-- /~https://github.com/openresty/lua-nginx-module#ngxreqget_body_data
local body = ngx.req.get_body_data()
local function handle_expect()
local expect = ngx.req.get_headers()["Expect"]
if type(expect) == "table" then
expect = expect[1]

Check warning on line 104 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L102-L104

Added lines #L102 - L104 were not covered by tests
end

if expect and expect:lower() == "100-continue" then
ngx.status = 100
local ok, err = ngx_send_headers()

Check warning on line 109 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L107-L109

Added lines #L107 - L109 were not covered by tests

if not ok then
return nil, "failed to send response header: " .. (err or "unknown")

Check warning on line 112 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L111-L112

Added lines #L111 - L112 were not covered by tests
end

ok, err = ngx_flush(true)
if not ok then
return nil, "failed to flush response header: " .. (err or "unknown")

Check warning on line 117 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L115-L117

Added lines #L115 - L117 were not covered by tests
end
end
end

local function forward_https_request(proxy_uri, uri, proxy_opts)
local body, err
local sock
local opts = proxy_opts or {}
local req_method = ngx_get_method()
local encoding = ngx.req.get_headers()["Transfer-Encoding"]
local is_chunked = encoding and encoding:lower() == "chunked"

Check warning on line 128 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L125-L128

Added lines #L125 - L128 were not covered by tests

if http_methods_with_body[req_method] then
if opts.request_unbuffered and ngx_http_version() == 1.1 then
local _, err = handle_expect()
if err then
ngx.log(ngx.ERR, "failed to handle expect header, err: ", err)
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)

Check warning on line 135 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L130-L135

Added lines #L130 - L135 were not covered by tests
end

if not body then
local temp_file_path = ngx.req.get_body_file()
ngx.log(ngx.INFO, "HTTPS Proxy: Request body is bigger than client_body_buffer_size, read the content from path='", temp_file_path, "'")

if temp_file_path then
body, err = file_reader(temp_file_path)
if err then
ngx.log(ngx.ERR, "HTTPS proxy: Failed to read temp body file, err: ", err)
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end

if encoding == "chunked" then
-- If the body is smaller than "client_boby_buffer_size" the Content-Length header is
-- set based on the size of the buffer. However, when the body is rendered to a file,
-- we will need to calculate and manually set the Content-Length header based on the
-- file size
local contentLength, err = file_size(temp_file_path)
if err then
ngx.log(ngx.ERR, "HTTPS proxy: Failed to set content length, err: ", err)
if is_chunked then

Check warning on line 138 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L138

Added line #L138 was not covered by tests
-- The default ngx reader does not support chunked request
-- so we will need to get the raw request socket and manually
-- decode the chunked request
sock, err = ngx.req.socket(true)

Check warning on line 142 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L142

Added line #L142 was not covered by tests
else
sock, err = ngx.req.socket()

Check warning on line 144 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L144

Added line #L144 was not covered by tests
end

if not sock then
ngx.log(ngx.ERR, "unable to obtain request socket: ", err)
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)

Check warning on line 149 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L147-L149

Added lines #L147 - L149 were not covered by tests
end

body = client_body_reader(sock, DEFAULT_CHUNKSIZE, is_chunked)

Check warning on line 152 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L152

Added line #L152 was not covered by tests
else
-- This is needed to call ngx.req.get_body_data() below.
ngx.req.read_body()

Check warning on line 155 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L155

Added line #L155 was not covered by tests

-- We cannot use resty.http's .get_client_body_reader().
-- In POST requests with HTTPS, the result of that call is nil, and it
-- results in a time-out.
--
--
-- If ngx.req.get_body_data is nil, can be that the body is too big to
-- read and need to be cached in a local file. This request will return
-- nil, so after this we need to read the temp file.
-- /~https://github.com/openresty/lua-nginx-module#ngxreqget_body_data
body = ngx.req.get_body_data()

Check warning on line 166 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L166

Added line #L166 was not covered by tests

if not body then
local temp_file_path = ngx.req.get_body_file()
ngx.log(ngx.INFO, "HTTPS Proxy: Request body is bigger than client_body_buffer_size, read the content from path='", temp_file_path, "'")

Check warning on line 170 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L168-L170

Added lines #L168 - L170 were not covered by tests

if temp_file_path then
body, err = file_reader(temp_file_path)
if err then
ngx.log(ngx.ERR, "HTTPS proxy: Failed to read temp body file, err: ", err)
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)

Check warning on line 176 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L172-L176

Added lines #L172 - L176 were not covered by tests
end

if is_chunked then

Check warning on line 179 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L179

Added line #L179 was not covered by tests
-- If the body is smaller than "client_boby_buffer_size" the Content-Length header is
-- set based on the size of the buffer. However, when the body is rendered to a file,
-- we will need to calculate and manually set the Content-Length header based on the
-- file size
local contentLength, err = file_size(temp_file_path)
if err then
ngx.log(ngx.ERR, "HTTPS proxy: Failed to set content length, err: ", err)
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)

Check warning on line 187 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L184-L187

Added lines #L184 - L187 were not covered by tests
end

ngx.req.set_header("Content-Length", tostring(contentLength))

Check warning on line 190 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L190

Added line #L190 was not covered by tests
end
end

ngx.req.set_header("Content-Length", tostring(contentLength))
end
end
end

-- The whole request is buffered in the memory so remove the Transfer-Encoding: chunked
if ngx.var.http_transfer_encoding == "chunked" then
ngx.req.set_header("Transfer-Encoding", nil)
-- The whole request is buffered in the memory so remove the Transfer-Encoding: chunked
if is_chunked then
ngx.req.set_header("Transfer-Encoding", nil)

Check warning on line 197 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L196-L197

Added lines #L196 - L197 were not covered by tests
end
end
end

local request = {

Check warning on line 202 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L202

Added line #L202 was not covered by tests
Expand All @@ -139,10 +206,10 @@ local function forward_https_request(proxy_uri, proxy_auth, uri, skip_https_conn
path = format('%s%s%s', ngx.var.uri, ngx.var.is_args, ngx.var.query_string or ''),
body = body,
proxy_uri = proxy_uri,
proxy_auth = proxy_auth
proxy_auth = opts.proxy_auth

Check warning on line 209 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L209

Added line #L209 was not covered by tests
}

local httpc, err = http_proxy.new(request, skip_https_connect)
local httpc, err = http_proxy.new(request, opts.skip_https_connect)

Check warning on line 212 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L212

Added line #L212 was not covered by tests

if not httpc then
ngx.log(ngx.ERR, 'could not connect to proxy: ', proxy_uri, ' err: ', err)
Expand All @@ -154,8 +221,16 @@ local function forward_https_request(proxy_uri, proxy_auth, uri, skip_https_conn
res, err = httpc:request(request)

if res then
httpc:proxy_response(res)
httpc:set_keepalive()
if opts.request_unbuffered and is_chunked then
local bytes, err = send_response(sock, res, DEFAULT_CHUNKSIZE)
if not bytes then
ngx.log(ngx.ERR, "failed to send response: ", err)
return sock:send("HTTP/1.1 502 Bad Gateway")

Check warning on line 228 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L224-L228

Added lines #L224 - L228 were not covered by tests
end
else
httpc:proxy_response(res)
httpc:set_keepalive()

Check warning on line 232 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L231-L232

Added lines #L231 - L232 were not covered by tests
end
else
ngx.log(ngx.ERR, 'failed to proxy request to: ', proxy_uri, ' err : ', err)
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
Expand Down Expand Up @@ -208,7 +283,13 @@ function _M.request(upstream, proxy_uri)
return
elseif uri.scheme == 'https' then
upstream:rewrite_request()
forward_https_request(proxy_uri, proxy_auth, uri, upstream.skip_https_connect)
local proxy_opts = {

Check warning on line 286 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L286

Added line #L286 was not covered by tests
proxy_auth = proxy_auth,
skip_https_connect = upstream.skip_https_connect,
request_unbuffered = upstream.request_unbuffered

Check warning on line 289 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L289

Added line #L289 was not covered by tests
}

forward_https_request(proxy_uri, uri, proxy_opts)

Check warning on line 292 in gateway/src/apicast/http_proxy.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/http_proxy.lua#L292

Added line #L292 was not covered by tests
return ngx.exit(ngx.OK) -- terminate phase
else
ngx.log(ngx.ERR, 'could not connect to proxy: ', proxy_uri, ' err: ', 'invalid request scheme')
Expand Down
1 change: 1 addition & 0 deletions gateway/src/apicast/upstream.lua
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ function _M:call(context)
self:set_skip_https_connect_on_proxy();
end

self.request_unbuffered = context.request_unbuffered
http_proxy.request(self, proxy_uri)
else
local err = self:rewrite_request()
Expand Down
47 changes: 47 additions & 0 deletions gateway/src/resty/http/request_reader.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
local httpc = require "resty.resolver.http"

local _M = {
}

local cr_lf = "\r\n"

-- chunked_reader return a body reader that translates the data read from
-- lua-resty-http client_body_reader to HTTP "chunked" format before returning it
--
-- The chunked reader return nil when the final 0-length chunk is read
local function chunked_reader(sock, chunksize)
chunksize = chunksize or 65536
local eof = false
local reader = httpc:get_client_body_reader(chunksize, sock)
if not reader then
return nil

Check warning on line 17 in gateway/src/resty/http/request_reader.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/request_reader.lua#L13-L17

Added lines #L13 - L17 were not covered by tests
end

return function()
if eof then
return nil

Check warning on line 22 in gateway/src/resty/http/request_reader.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/request_reader.lua#L21-L22

Added lines #L21 - L22 were not covered by tests
end

local buffer, err = reader()
if err then
return nil, err

Check warning on line 27 in gateway/src/resty/http/request_reader.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/request_reader.lua#L25-L27

Added lines #L25 - L27 were not covered by tests
end
if buffer then
local chunk = string.format("%x\r\n", #buffer) .. buffer .. cr_lf
return chunk

Check warning on line 31 in gateway/src/resty/http/request_reader.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/request_reader.lua#L29-L31

Added lines #L29 - L31 were not covered by tests
else
eof = true
return "0\r\n\r\n"

Check warning on line 34 in gateway/src/resty/http/request_reader.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/request_reader.lua#L33-L34

Added lines #L33 - L34 were not covered by tests
end
end
end

function _M.get_client_body_reader(sock, chunksize, is_chunked)
if is_chunked then
return chunked_reader(sock, chunksize)

Check warning on line 41 in gateway/src/resty/http/request_reader.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/request_reader.lua#L40-L41

Added lines #L40 - L41 were not covered by tests
else
return httpc:get_client_body_reader(chunksize, sock)

Check warning on line 43 in gateway/src/resty/http/request_reader.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/request_reader.lua#L43

Added line #L43 was not covered by tests
end
end

return _M
58 changes: 58 additions & 0 deletions gateway/src/resty/http/response_writer.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
local _M = {
}

local cr_lf = "\r\n"

local function send(socket, data)
if not data or data == '' then
ngx.log(ngx.DEBUG, 'skipping sending nil')
return

Check warning on line 9 in gateway/src/resty/http/response_writer.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/response_writer.lua#L7-L9

Added lines #L7 - L9 were not covered by tests
end

return socket:send(data)

Check warning on line 12 in gateway/src/resty/http/response_writer.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/response_writer.lua#L12

Added line #L12 was not covered by tests
end

-- write_response writes response body reader to sock in the HTTP/1.x server response format,
-- The connection is closed if send() fails or when returning a non-zero
function _M.send_response(sock, response, chunksize)
local bytes, err
chunksize = chunksize or 65536

Check warning on line 19 in gateway/src/resty/http/response_writer.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/response_writer.lua#L19

Added line #L19 was not covered by tests

if not response then
ngx.log(ngx.ERR, "no response provided")
return

Check warning on line 23 in gateway/src/resty/http/response_writer.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/response_writer.lua#L21-L23

Added lines #L21 - L23 were not covered by tests
end

if not sock then
return nil, "socket not initialized yet"

Check warning on line 27 in gateway/src/resty/http/response_writer.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/response_writer.lua#L26-L27

Added lines #L26 - L27 were not covered by tests
end

-- Status line
local status = "HTTP/1.1 " .. response.status .. " " .. response.reason .. cr_lf
bytes, err = send(sock, status)
if not bytes then
return nil, "failed to send status line, err: " .. (err or "unknown")

Check warning on line 34 in gateway/src/resty/http/response_writer.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/response_writer.lua#L31-L34

Added lines #L31 - L34 were not covered by tests
end

-- Write body
local reader = response.body_reader

Check warning on line 38 in gateway/src/resty/http/response_writer.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/response_writer.lua#L38

Added line #L38 was not covered by tests
repeat
local chunk, read_err

chunk, read_err = reader(chunksize)
if read_err then
return nil, "failed to read response body, err: " .. (err or "unknown")

Check warning on line 44 in gateway/src/resty/http/response_writer.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/response_writer.lua#L42-L44

Added lines #L42 - L44 were not covered by tests
end

if chunk then
bytes, err = send(sock, chunk)
if not bytes then
return nil, "failed to send response body, err: " .. (err or "unknown")

Check warning on line 50 in gateway/src/resty/http/response_writer.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/response_writer.lua#L47-L50

Added lines #L47 - L50 were not covered by tests
end
end
until not chunk

Check warning on line 53 in gateway/src/resty/http/response_writer.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/response_writer.lua#L53

Added line #L53 was not covered by tests

return true, nil

Check warning on line 55 in gateway/src/resty/http/response_writer.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/http/response_writer.lua#L55

Added line #L55 was not covered by tests
end

return _M
Loading

0 comments on commit b02a888

Please sign in to comment.