Skip to content

Commit addb342

Browse files
committed
Support to proxy request with Transfer-Encoding: chunked and proxy_buffering: off
1 parent 3e25e57 commit addb342

File tree

4 files changed

+253
-36
lines changed

4 files changed

+253
-36
lines changed

gateway/src/apicast/http_proxy.lua

Lines changed: 130 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,22 @@ local round_robin = require 'resty.balancer.round_robin'
77
local http_proxy = require 'resty.http.proxy'
88
local file_reader = require("resty.file").file_reader
99
local file_size = require("resty.file").file_size
10+
local get_client_body_reader = require('resty.http.raw_request').get_client_body_reader
11+
local send_response = require('resty.http.raw_response').send_response
12+
local http_ver = ngx.req.http_version
13+
local ngx_send_headers = ngx.send_headers
14+
local ngx_req = ngx.req
15+
local ngx_flush = ngx.flush
16+
local ngx_var = ngx.var
1017

1118
local _M = { }
1219

20+
local http_methods_with_body = {
21+
POST = true,
22+
PUT = true,
23+
PATCH = true
24+
}
25+
1326
function _M.reset()
1427
_M.balancer = round_robin.new()
1528
_M.resolver = resty_resolver
@@ -103,17 +116,70 @@ local function modify_chunked_request_headers(req, file)
103116
set_header(req.headers, "Content-Length", contentLength)
104117
end
105118

119+
local function handle_expect()
120+
local expect = ngx_var.http_expect
121+
if type(expect) == "table" then
122+
expect = expect[1]
123+
end
106124

107-
local function forward_https_request(proxy_uri, uri, skip_https_connect)
108-
-- This is needed to call ngx.req.get_body_data() below.
109-
ngx.req.read_body()
125+
if expect and expect:lower() == "100-continue" then
126+
ngx.status = 100
127+
local ok, err = ngx_send_headers()
110128

111-
local request = {
112-
uri = uri,
113-
method = ngx.req.get_method(),
114-
headers = ngx.req.get_headers(0, true),
115-
path = format('%s%s%s', ngx.var.uri, ngx.var.is_args, ngx.var.query_string or ''),
129+
if not ok then
130+
return nil, "failed to send response header: " .. (err or "unknown")
131+
end
132+
133+
ok, err = ngx_flush(true)
134+
if not ok then
135+
return nil, "failed to flush response header: " .. (err or "unknown")
136+
end
137+
end
138+
end
139+
140+
local function get_request_body(req, opts)
141+
local chunksize = 32 * 1024
142+
local sock, err
143+
local body = nil
144+
local encoding = ngx_var.http_transfer_encoding
145+
146+
if not opts.request_buffering then
147+
if http_ver() ~= 1.1 then
148+
ngx.log(ngx.ERR, "bad http version")
149+
ngx.exit(ngx.HTTP_BAD_REQUEST)
150+
end
151+
152+
if type(encoding) == "table" then
153+
encoding = encoding[1]
154+
end
155+
156+
_, err = handle_expect()
157+
if err then
158+
ngx.log(ngx.ERR, err)
159+
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
160+
end
116161

162+
if encoding and encoding:lower() == "chunked" then
163+
-- The default ngx reader does not support chunked request
164+
-- so we will need to get the raw request socket and manually
165+
-- decode the chunked request
166+
sock, err = ngx.req.socket(true)
167+
body = get_client_body_reader(sock, chunksize, true)
168+
else
169+
sock, err = ngx.req.socket()
170+
body = get_client_body_reader(sock, chunksize)
171+
end
172+
173+
if not sock then
174+
ngx.log(ngx.ERR, "unable to obtain request socket: ", err)
175+
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
176+
end
177+
178+
req.body = body
179+
req.sock = sock
180+
else
181+
-- This is needed to call ngx.req.get_body_data() below.
182+
ngx.req.read_body()
117183
-- We cannot use resty.http's .get_client_body_reader().
118184
-- In POST requests with HTTPS, the result of that call is nil, and it
119185
-- results in a time-out.
@@ -123,37 +189,53 @@ local function forward_https_request(proxy_uri, uri, skip_https_connect)
123189
-- read and need to be cached in a local file. This request will return
124190
-- nil, so after this we need to read the temp file.
125191
-- https://github.com/openresty/lua-nginx-module#ngxreqget_body_data
126-
body = ngx.req.get_body_data(),
127-
proxy_uri = proxy_uri
128-
}
192+
req.body = ngx_req.get_body_data()
193+
194+
if not req.body then
195+
local temp_file_path = ngx.req.get_body_file()
196+
ngx.log(ngx.INFO, "HTTPS Proxy: Request body is bigger than client_body_buffer_size, read the content from path='", temp_file_path, "'")
197+
198+
if temp_file_path then
199+
body, err = file_reader(temp_file_path)
200+
if err then
201+
ngx.log(ngx.ERR, "HTTPS proxy: Failed to read temp body file, err: ", err)
202+
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
203+
end
204+
if encoding == "chunked" then
205+
-- If the body is smaller than "client_boby_buffer_size" the Content-Length header is
206+
-- set based on the size of the buffer. However, when the body is rendered to a file,
207+
-- we will need to calculate and manually set the Content-Length header based on the
208+
-- file size
209+
modify_chunked_request_headers(req, temp_file_path)
210+
end
211+
req.body = body
212+
end
213+
end
129214

130-
if not request.body then
131-
local temp_file_path = ngx.req.get_body_file()
132-
ngx.log(ngx.INFO, "HTTPS Proxy: Request body is bigger than client_body_buffer_size, read the content from path='", temp_file_path, "'")
133-
134-
if temp_file_path then
135-
local body, err = file_reader(temp_file_path)
136-
if err then
137-
ngx.log(ngx.ERR, "HTTPS proxy: Failed to read temp body file, err: ", err)
138-
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
139-
end
140-
if ngx.var.http_transfer_encoding == "chunked" then
141-
-- If the body is smaller than "client_boby_buffer_size" the Content-Length header is
142-
-- set based on the size of the buffer. However, when the body is rendered to a file,
143-
-- we will need to calculate and manually set the Content-Length header based on the
144-
-- file size
145-
modify_chunked_request_headers(request, temp_file_path)
146-
end
147-
request.body = body
215+
-- The whole request is buffered in the memory so remove the Transfer-Encoding: chunked
216+
if ngx.var.http_transfer_encoding == "chunked" then
217+
set_header(req.headers, "Transfer-Encoding", nil)
148218
end
149219
end
220+
end
221+
222+
local function forward_https_request(proxy_uri, uri, proxy_opts)
223+
local method = ngx.req.get_method()
224+
local chunksize = 32 * 1024
150225

151-
-- The whole request is buffered in the memory so remove the Transfer-Encoding: chunked
152-
if ngx.var.http_transfer_encoding == "chunked" then
153-
set_header(request.headers, "Transfer-Encoding", nil)
226+
local request = {
227+
uri = uri,
228+
method = ngx.req.get_method(),
229+
headers = ngx.req.get_headers(0, true),
230+
path = format('%s%s%s', ngx.var.uri, ngx.var.is_args, ngx.var.query_string or ''),
231+
proxy_uri = proxy_uri
232+
}
233+
234+
if http_methods_with_body[method] then
235+
get_request_body(request, proxy_opts)
154236
end
155237

156-
local httpc, err = http_proxy.new(request, skip_https_connect)
238+
local httpc, err = http_proxy.new(request, proxy_opts.skip_https_connect)
157239

158240
if not httpc then
159241
ngx.log(ngx.ERR, 'could not connect to proxy: ', proxy_uri, ' err: ', err)
@@ -165,8 +247,16 @@ local function forward_https_request(proxy_uri, uri, skip_https_connect)
165247
res, err = httpc:request(request)
166248

167249
if res then
168-
httpc:proxy_response(res)
169-
httpc:set_keepalive()
250+
if not proxy_opts.request_buffering then
251+
local bytes, err = send_response(request.sock, res, chunksize)
252+
if not bytes then
253+
ngx.log(ngx.ERR, "failed to send response: ", err)
254+
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
255+
end
256+
else
257+
httpc:proxy_response(res)
258+
httpc:set_keepalive()
259+
end
170260
else
171261
ngx.log(ngx.ERR, 'failed to proxy request to: ', proxy_uri, ' err : ', err)
172262
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
@@ -204,7 +294,11 @@ function _M.request(upstream, proxy_uri)
204294
return
205295
elseif uri.scheme == 'https' then
206296
upstream:rewrite_request()
207-
forward_https_request(proxy_uri, uri, upstream.skip_https_connect)
297+
local proxy_opts = {
298+
skip_https_connect = upstream.skip_https_connect,
299+
request_buffering = upstream.request_buffering
300+
}
301+
forward_https_request(proxy_uri, uri, proxy_opts)
208302
return ngx.exit(ngx.OK) -- terminate phase
209303
else
210304
ngx.log(ngx.ERR, 'could not connect to proxy: ', proxy_uri, ' err: ', 'invalid request scheme')

gateway/src/apicast/upstream.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ function _M:call(context)
241241
self:set_skip_https_connect_on_proxy();
242242
end
243243

244+
self.request_buffering = context.request_buffering
245+
244246
http_proxy.request(self, proxy_uri)
245247
else
246248
local err = self:rewrite_request()
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
local httpc = require "resty.resolver.http"
2+
3+
local _M = {
4+
}
5+
6+
local cr_lf = "\r\n"
7+
8+
-- chunked_reader return a body reader that translates the data read from
9+
-- lua-resty-http client_body_reader to HTTP "chunked" format before returning it
10+
--
11+
-- The chunked reader return nil when the final 0-length chunk is read
12+
local function chunked_reader(sock, chunksize)
13+
chunksize = chunksize or 65536
14+
local eof = false
15+
local reader = httpc:get_client_body_reader(chunksize, sock)
16+
if not reader then
17+
return nil
18+
end
19+
20+
return function()
21+
if eof then
22+
return nil
23+
end
24+
25+
local buffer, err = reader()
26+
if err then
27+
return nil, err
28+
end
29+
if buffer then
30+
local chunk = string.format("%x\r\n", #buffer) .. buffer .. cr_lf
31+
return chunk
32+
else
33+
eof = true
34+
return "0\r\n\r\n"
35+
end
36+
end
37+
end
38+
39+
function _M.get_client_body_reader(sock, chunksize, is_chunked)
40+
if is_chunked then
41+
return chunked_reader(sock, chunksize)
42+
else
43+
return httpc:get_client_body_reader(chunksize, sock)
44+
end
45+
end
46+
47+
return _M
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
local _M = {
2+
}
3+
4+
local cr_lf = "\r\n"
5+
6+
local function send(socket, data)
7+
if not data or data == '' then
8+
ngx.log(ngx.DEBUG, 'skipping sending nil')
9+
return
10+
end
11+
12+
return socket:send(data)
13+
end
14+
15+
-- write_response writes response body reader to sock in the HTTP/1.x server response format,
16+
-- including the status line, headers, body, and optional trailer.
17+
-- The connection is closed if send() fails or when returning a non-zero
18+
function _M.send_response(sock, res, chunksize)
19+
local bytes, err
20+
chunksize = chunksize or 65536
21+
22+
if ngx.headers_sent then
23+
return nil, "headers have already been sent"
24+
end
25+
26+
if not sock then
27+
return nil, "socket not initialized yet"
28+
end
29+
30+
-- Status line
31+
-- FIXME: should get protocol version from res?
32+
local status = "HTTP/1.1 " .. res.status .. " " .. res.reason .. cr_lf
33+
bytes, err = send(sock, status)
34+
if not bytes then
35+
return nil, "failed to send status line, err: " .. (err or "unknown")
36+
end
37+
38+
-- Rest of header
39+
for k, v in pairs(res.headers) do
40+
local header = k .. ": " .. v .. cr_lf
41+
bytes, err = send(sock, header)
42+
if not bytes then
43+
return nil, "failed to send header, err: " .. (err or "unknown")
44+
end
45+
end
46+
47+
-- End-of-header
48+
bytes, err = send(sock, cr_lf)
49+
if not bytes then
50+
return nil, "failed to send end of header, err: " .. (err or "unknown")
51+
end
52+
53+
-- Write body
54+
if res.has_body then
55+
local reader = res.body_reader
56+
repeat
57+
local chunk, read_err
58+
59+
chunk, read_err = reader(chunksize)
60+
if read_err then
61+
return nil, "failed to read response body, err: " .. (err or "unknown")
62+
end
63+
64+
if chunk then
65+
bytes, err = send(sock, chunk)
66+
if not bytes then
67+
return nil, "failed to send response body, err: " .. (err or "unknown")
68+
end
69+
end
70+
until not chunk
71+
end
72+
end
73+
74+
return _M

0 commit comments

Comments
 (0)