diff --git a/gateway/src/apicast/configuration/service.lua b/gateway/src/apicast/configuration/service.lua index 566abc0bb..06be193d4 100644 --- a/gateway/src/apicast/configuration/service.lua +++ b/gateway/src/apicast/configuration/service.lua @@ -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() diff --git a/gateway/src/apicast/http_proxy.lua b/gateway/src/apicast/http_proxy.lua index 66d5dca07..f91256c43 100644 --- a/gateway/src/apicast/http_proxy.lua +++ b/gateway/src/apicast/http_proxy.lua @@ -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 = { } @@ -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 @@ -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 @@ -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 diff --git a/gateway/src/resty/http/chunked.lua b/gateway/src/resty/http/chunked.lua new file mode 100644 index 000000000..2c47d21a7 --- /dev/null +++ b/gateway/src/resty/http/chunked.lua @@ -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