Skip to content

Commit

Permalink
Initial support to proxy request with Transfer-Encoding: chunked
Browse files Browse the repository at this point in the history
  • Loading branch information
tkan145 committed Jun 26, 2023
1 parent fdf5b2e commit f58329b
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 24 deletions.
2 changes: 2 additions & 0 deletions gateway/src/apicast/configuration/service.lua
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ local function get_request_params(method)

if method == "GET" then
return params
elseif ngx.req.get_headers()["Transfer-Encoding"] == "chunked" then
return params
else
ngx.req.read_body()
local body_params, err = ngx.req.get_post_args()
Expand Down
114 changes: 90 additions & 24 deletions gateway/src/apicast/http_proxy.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ 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 chunked_reader = require('resty.http.chunked').chunked_reader
local chunked_writer = require('resty.http.chunked').chunked_writer
local req_headers = ngx.req.get_headers
local http_ver = ngx.req.http_version
local ngx_send_headers = ngx.send_headers
local ngx_flush = ngx.flush

local _M = { }

Expand Down Expand Up @@ -82,14 +88,58 @@ local function absolute_url(uri)
end

local function forward_https_request(proxy_uri, uri, skip_https_connect)
-- This is needed to call ngx.req.get_body_data() below.
ngx.req.read_body()
local sock, ok, err
local chunksize = 32 * 1024
local body
local headers = req_headers()

local encoding = headers.transfer_encoding
if type(encoding) == "table" then
encoding = encoding[1]
end

local request = {
uri = uri,
method = ngx.req.get_method(),
headers = ngx.req.get_headers(0, true),
path = format('%s%s%s', ngx.var.uri, ngx.var.is_args, ngx.var.query_string or ''),
if encoding and encoding:lower() == "chunked" then
if http_ver() ~= 1.1 then
ngx.log(ngx.ERR, "bad http version")
ngx.exit(ngx.HTTP_BAD_REQUEST)
end
local expect = headers["Expect"]

if type(expect) == "table" then
expect = expect[1]
end

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

if not ok then
ngx.log(ngx.ERR, "failed to send response header: " .. (err or "unknown"))
end

ok, err = ngx_flush(true)
if not ok then
ngx.log(ngx.ERR, "failed to flush response header: " .. (err or "unknown"))
end
end

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

if not sock then
if err == "no body" then
body = nil
else
return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end
else
body = chunked_reader(sock, chunksize)
end
else
-- 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
Expand All @@ -100,24 +150,31 @@ local function forward_https_request(proxy_uri, uri, skip_https_connect)
-- 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(),
proxy_uri = proxy_uri
}

if not request.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
local 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
request.body = body
body = ngx.req.get_body_data()

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

local request = {
uri = uri,
method = ngx.req.get_method(),
headers = ngx.req.get_headers(0, true),
path = format('%s%s%s', ngx.var.uri, ngx.var.is_args, ngx.var.query_string or ''),
body = body,
proxy_uri = proxy_uri
}

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

if not httpc then
Expand All @@ -130,11 +187,20 @@ local function forward_https_request(proxy_uri, uri, skip_https_connect)
res, err = httpc:request(request)

if res then
httpc:proxy_response(res)
-- if we are using raw socket we will need to send the response back with sock:send
if sock then
chunked_writer(sock, res ,chunksize)
else
httpc:proxy_response(res)
end
httpc:set_keepalive()
else
ngx.log(ngx.ERR, 'failed to proxy request to: ', proxy_uri, ' err : ', err)
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
if sock then
sock:send("HTTP/1.1 502 Bad Gateway\\r\\n")
else
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
end
end
end

Expand Down
166 changes: 166 additions & 0 deletions gateway/src/resty/http/chunked.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
local co_wrap_iter = require("resty.coroutines").co_wrap_iter
local co_yield = coroutine._yield

local _M = {
}

local cr_lf = "\r\n"
local default_max_chunk_size = 32 * 1024 -- 32K

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

return socket:send(data)
end

local function print_err(error_code, ...)
ngx.log(ngx.ERR, ...)
return ngx.exit(error_code)
end

-- This is a copy of lua-resty-http _chunked_body_reader function but with
-- extra bits to make chunked encoding work with raw socket
-- https://github.com/ledgetech/lua-resty-http/blob/v0.16.1/lib/resty/http.lua#L418

-- chunked_reader return a body reader that translates the data read from sock
-- out of HTTP "chunked" format before returning it
--
-- The chunked reader return nil when the final 0-length chunk is read
function _M.chunked_reader(sock, max_chunk_size)
max_chunk_size = max_chunk_size or default_max_chunk_size

if not sock then
return nil, "chunked_reader: invalid sock"
end

return co_wrap_iter(function()
local eof = false
local remaining = 0
local size = 0
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
size = max_chunk_size
remaining = remaining - max_chunk_size
else
-- Consume all remaining
size = remaining
remaining = 0
end
else
-- read a line from socket
-- chunk-size CRLF
local line, err = sock:receive()
if not line then
co_yield(nil, "chunked_reader: failed to receive chunk size, err: " .. (err or "unknown"))
end

size = tonumber(line, 16)
if not size then
co_yield(nil, "chunked_reader: unable to read chunksize")
end

if max_chunk_size and size > max_chunk_size then
-- Consume up to max_chunk_size
remaining = size - max_chunk_size
size = max_chunk_size
end
end


if size > 0 then
-- Receive the chunk
local chunk, err = sock:receive(size)
if not chunk then
co_yield(nil, "chunked_reader: failed to receive chunk of size " .. size .. " err: " .. (err or "unknown"))
end

if remaining == 0 then
-- We're at the end of a chunk, read the next two bytes
-- and verify they are "\r\n"
local data, err = sock:receive(2)
if not data then
co_yield(nil, "chunked_reader: failed to receive chunk terminator, err: " .. (err or "unknown"))
end
end

chunk = string.format("%x\r\n", size) .. chunk .. cr_lf

co_yield(chunk)
else
-- we're at the end of a chunk, read the next two
-- bytes to verify they are "\r\n".
local chunk, err = sock:receive(2)
if not chunk then
co_yield(nil, "chunked_reader: failed to receive chunk terminator, err: " .. (err or "unknown"))
end

if chunk ~= "\r\n" then
co_yield(nil, "chunked_reader: bad chunk terminator")
end

eof = true
co_yield("0\r\n\r\n")
break
end
until eof
end)
end

-- chunked_writer writes response body reader to sock in the HTTP/1.x server response format,
-- including the status line, headers, body, and optional trailer.
function _M.chunked_writer(sock, res, chunksize)
local bytes, err
chunksize = chunksize or 65536

-- Status line
-- FIXME: should get protocol version from res?
local status = "HTTP/1.1 " .. res.status .. " " .. res.reason .. cr_lf
bytes, err = send(sock, status)
if not bytes then
print_err(503, "chunked_writer: failed to send status line, err: " .. (err or "unknown"))
end

-- Rest of header
for k, v in pairs(res.headers) do
local header = k .. ": " .. v .. cr_lf
bytes, err = send(sock, header)
if not bytes then
print_err(503, "chunked_writer: failed to send header, err: " .. (err or "unknown"))
end
end

-- End-of-header
bytes, err = send(sock, cr_lf)
if not bytes then
print_err(503, "chunked_writer: failed to send end of header, err: " .. (err or "unknown"))
end

-- Write body and trailer
-- TODO: handle trailer
if res.has_body then
local reader = res.body_reader
repeat
local chunk, read_err

chunk, read_err = reader(chunksize)
if read_err then
print_err(503, "chunked_writer: failed to read body, err: " .. (err or "unknown"))
end

if chunk then
bytes, err = send(sock, chunk)
if not bytes then
print_err(503, "chunked_writer: failed to send body, err: " .. (err or "unknown"))
end
end
until not chunk
end
end

return _M

0 comments on commit f58329b

Please sign in to comment.