Skip to content

Commit

Permalink
Fix 'Transfer-Encoding: chunked' issue when sending request via proxy
Browse files Browse the repository at this point in the history
When a request with the HTTP "Transfer-Encoding: chunked" header is sent, APIcast
buffers the entire request because by default it does not support sending chunked
requests. However, when sending via proxy, APIcast does not remove the header sent
in the initial request, which tells the server that the client is sending a chunk
request. This then causes an Bad Request error because the upstream will not be able
to determine the end of the chunk from the request.

This commit removes the "Transfer-Encoding: chunked" header from the request when
sending through a proxy.
  • Loading branch information
tkan145 committed Nov 27, 2023
1 parent 3656233 commit 3efa69a
Show file tree
Hide file tree
Showing 5 changed files with 984 additions and 23 deletions.
68 changes: 45 additions & 23 deletions gateway/src/apicast/http_proxy.lua
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
local format = string.format
local tostring = tostring

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 concat = table.concat

local _M = { }
Expand Down Expand Up @@ -86,40 +88,60 @@ local function forward_https_request(proxy_uri, proxy_auth, uri, skip_https_conn
-- This is needed to call ngx.req.get_body_data() below.
ngx.req.read_body()

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

-- 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(),
proxy_uri = proxy_uri,
proxy_auth = proxy_auth
}

if not request.body then
-- 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 encoding = ngx.req.get_headers()["Transfer-Encoding"]

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
local body, err = file_reader(temp_file_path)
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

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)
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
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)
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,
proxy_auth = proxy_auth
}

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

if not httpc then
Expand Down
16 changes: 16 additions & 0 deletions gateway/src/resty/file.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,20 @@ function _M.file_reader(filename)
end)
end

function _M.file_size(filename)
local handle, err = open(filename)

if err then
return nil, err
end

local current = handle:seek()
local size = handle:seek("end")

handle:seek("set", current)
handle:close()

return size
end

return _M
236 changes: 236 additions & 0 deletions t/apicast-policy-camel.t
Original file line number Diff line number Diff line change
Expand Up @@ -506,3 +506,239 @@ using proxy: http://foo:bar\@127.0.0.1:$Test::Nginx::Util::PROXY_SSL_PORT,
EOF
--- no_error_log eval
[qr/\[error\]/, qr/\got header line: Proxy-Authorization: Basic Zm9vOmJhcg==/]
=== TEST 8: API backend connection uses http proxy with chunked request
--- configuration
{
"services": [
{
"id": 42,
"backend_version": 1,
"backend_authentication_type": "service_token",
"backend_authentication_value": "token-value",
"proxy": {
"api_backend": "http://test-upstream.lvh.me:$TEST_NGINX_SERVER_PORT/",
"proxy_rules": [
{ "pattern": "/", "http_method": "POST", "metric_system_name": "hits", "delta": 2 }
],
"policy_chain": [
{
"name": "apicast.policy.apicast"
},
{
"name": "apicast.policy.camel",
"configuration": {
"http_proxy": "$TEST_NGINX_HTTP_PROXY"
}
}
]
}
}
]
}
--- backend
location /transactions/authrep.xml {
content_by_lua_block {
ngx.exit(ngx.OK)
}
}
--- upstream
server_name test-upstream.lvh.me;
location / {
access_by_lua_block {
assert = require('luassert')
local content_length = ngx.req.get_headers()["Content-Length"]
local encoding = ngx.req.get_headers()["Transfer-Encoding"]
assert.equal('12', content_length)
assert.falsy(encoding)
}
echo_read_request_body;
echo $request_body;
}
--- more_headers
Transfer-Encoding: chunked
--- request eval
"POST /?user_key=value
7\r
hello, \r
5\r
world\r
0\r
\r
"
--- response_body
hello, world
--- error_code: 200
--- error_log env
using proxy: $TEST_NGINX_HTTP_PROXY
--- no_error_log
[error]



=== TEST 9: API backend using all_proxy with chunked request
--- configuration
{
"services": [
{
"id": 42,
"backend_version": 1,
"backend_authentication_type": "service_token",
"backend_authentication_value": "token-value",
"proxy": {
"api_backend": "http://test-upstream.lvh.me:$TEST_NGINX_SERVER_PORT/",
"proxy_rules": [
{ "pattern": "/", "http_method": "POST", "metric_system_name": "hits", "delta": 2 }
],
"policy_chain": [
{
"name": "apicast.policy.apicast"
},
{
"name": "apicast.policy.http_proxy",
"configuration": {
"all_proxy": "$TEST_NGINX_HTTP_PROXY"
}
}
]
}
}
]
}
--- backend
location /transactions/authrep.xml {
content_by_lua_block {
local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&user_key=value"
require('luassert').same(ngx.decode_args(expected), ngx.req.get_uri_args(0))
}
}
--- upstream
server_name test-upstream.lvh.me;
location / {
access_by_lua_block {
assert = require('luassert')
local content_length = ngx.req.get_headers()["Content-Length"]
local encoding = ngx.req.get_headers()["Transfer-Encoding"]
assert.equal('12', content_length)
assert.falsy(encoding)
}
echo_read_request_body;
echo $request_body;
}
--- more_headers
Transfer-Encoding: chunked
--- request eval
"POST /?user_key=value
7\r
hello, \r
5\r
world\r
0\r
\r
"
--- response_body
hello, world
--- error_code: 200
--- error_log env
using proxy: $TEST_NGINX_HTTP_PROXY
--- no_error_log
[error]



=== TEST 10: using HTTPS proxy for backend with chunked request
--- init eval
$Test::Nginx::Util::PROXY_SSL_PORT = Test::APIcast::get_random_port();
$Test::Nginx::Util::ENDPOINT_SSL_PORT = Test::APIcast::get_random_port();
--- configuration random_port env eval
<<EOF
{
"services": [
{
"backend_version": 1,
"proxy": {
"api_backend": "https://test-upstream.lvh.me:$Test::Nginx::Util::ENDPOINT_SSL_PORT",
"proxy_rules": [
{ "pattern": "/", "http_method": "POST", "metric_system_name": "hits", "delta": 2 }
],
"policy_chain": [
{
"name": "apicast.policy.apicast"
},
{
"name": "apicast.policy.camel",
"configuration": {
"https_proxy": "http://127.0.0.1:$Test::Nginx::Util::PROXY_SSL_PORT"
}
}
]
}
}
]
}
EOF
--- backend
location /transactions/authrep.xml {
content_by_lua_block {
ngx.exit(ngx.OK)
}
}
--- upstream eval
<<EOF
# Endpoint config
server_name test-upstream.lvh.me;

listen $Test::Nginx::Util::ENDPOINT_SSL_PORT ssl;
ssl_certificate $Test::Nginx::Util::ServRoot/html/server.crt;
ssl_certificate_key $Test::Nginx::Util::ServRoot/html/server.key;

location / {
access_by_lua_block {
assert = require('luassert')
local content_length = ngx.req.get_headers()["Content-Length"]
local encoding = ngx.req.get_headers()["Transfer-Encoding"]
assert.equal('12', content_length)
assert.falsy(encoding)
}
echo_read_request_body;
echo_request_body;
}
}
server {
# Proxy config
listen $Test::Nginx::Util::PROXY_SSL_PORT ssl;

ssl_certificate $Test::Nginx::Util::ServRoot/html/server.crt;
ssl_certificate_key $Test::Nginx::Util::ServRoot/html/server.key;


server_name _ default_server;

location ~ /.* {
proxy_http_version 1.1;
proxy_pass https://\$http_host;
}
EOF
--- more_headers
Transfer-Encoding: chunked
--- request eval
"POST /?user_key=value
7\r
hello, \r
5\r
world\r
0\r
\r
"
--- response_body chomp
hello, world
--- error_code: 200
--- error_log eval
<<EOF
using proxy: http://127.0.0.1:$Test::Nginx::Util::PROXY_SSL_PORT,
EOF
--- no_error_log
[error]
--- user_files fixture=tls.pl eval
Loading

0 comments on commit 3efa69a

Please sign in to comment.