diff --git a/apisix-master-0.rockspec b/apisix-master-0.rockspec index 6779af88f33f..b65d7a7656e6 100644 --- a/apisix-master-0.rockspec +++ b/apisix-master-0.rockspec @@ -80,7 +80,8 @@ dependencies = { "lua-resty-mediador = 0.1.2-1", "lua-resty-ldap = 0.1.0-0", "lua-resty-t1k = 1.1.0", - "brotli-ffi = 0.3-1" + "brotli-ffi = 0.3-1", + "lua-ffi-zlib = 0.6-0" } build = { diff --git a/apisix/plugins/response-rewrite.lua b/apisix/plugins/response-rewrite.lua index 20458c4fcf82..d9aa816ce3de 100644 --- a/apisix/plugins/response-rewrite.lua +++ b/apisix/plugins/response-rewrite.lua @@ -19,6 +19,7 @@ local expr = require("resty.expr.v1") local re_compile = require("resty.core.regex").re_match_compile local plugin_name = "response-rewrite" local ngx = ngx +local ngx_header = ngx.header local re_match = ngx.re.match local re_sub = ngx.re.sub local re_gsub = ngx.re.gsub @@ -26,6 +27,8 @@ local pairs = pairs local ipairs = ipairs local type = type local pcall = pcall +local zlib = require("ffi-zlib") +local str_buffer = require("string.buffer") local lrucache = core.lrucache.new({ @@ -199,6 +202,31 @@ local function check_set_headers(headers) end +local function inflate_gzip(data) + local inputs = str_buffer.new():set(data) + local outputs = str_buffer.new() + + local read_inputs = function(size) + local data = inputs:get(size) + if data == "" then + return nil + end + return data + end + + local write_outputs = function(data) + return outputs:put(data) + end + + local ok, err = zlib.inflateGzip(read_inputs, write_outputs) + if not ok then + return nil, err + end + + return outputs:get() +end + + function _M.check_schema(conf) local ok, err = core.schema.check(schema, conf) if not ok then @@ -260,6 +288,19 @@ function _M.body_filter(conf, ctx) end local err + if ctx.response_encoding == "gzip" then + body, err = inflate_gzip(body) + if err ~= nil then + core.log.error("filters may not work as expected, inflate gzip err: ", err) + return + end + elseif ctx.response_encoding ~= nil then + core.log.error("filters may not work as expected ", + "due to unsupported compression encoding type: ", + ctx.response_encoding) + return + end + for _, filter in ipairs(conf.filters) do if filter.scope == "once" then body, _, err = re_sub(body, filter.regex, filter.replace, filter.options) @@ -333,7 +374,9 @@ function _M.header_filter(conf, ctx) -- if filters have no any match, response body won't be modified. if conf.filters or conf.body then + local response_encoding = ngx_header["Content-Encoding"] core.response.clear_header_as_body_modified() + ctx.response_encoding = response_encoding end if not conf.headers then diff --git a/t/plugin/response-rewrite3.t b/t/plugin/response-rewrite3.t new file mode 100644 index 000000000000..904508cc8963 --- /dev/null +++ b/t/plugin/response-rewrite3.t @@ -0,0 +1,435 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +BEGIN { + if ($ENV{TEST_NGINX_CHECK_LEAK}) { + $SkipReason = "unavailable for the hup tests"; + + } else { + $ENV{TEST_NGINX_USE_HUP} = 1; + undef $ENV{TEST_NGINX_USE_STAP}; + } +} + +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_shuffle(); +no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + + my $http_config = $block->http_config // <<_EOC_; + server { + listen 11451; + gzip on; + gzip_types *; + gzip_min_length 1; + location /gzip_hello { + content_by_lua_block { + ngx.req.read_body() + local s = "hello world" + ngx.header['Content-Length'] = #s + 1 + ngx.say(s) + } + } + } +_EOC_ + + $block->set_value("http_config", $http_config); +}); + +run_tests; + +__DATA__ + +=== TEST 1: set route use gzip upstream +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/gzip_hello", + "upstream": { + "nodes": { + "127.0.0.1:11451": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 2: should return gzip body +--- request +GET /gzip_hello +--- more_headers +Accept-Encoding: gzip +--- response_headers +Content-Encoding: gzip + + + +=== TEST 3: set route use gzip upstream and response-rewrite body conf +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/gzip_hello", + "upstream": { + "nodes": { + "127.0.0.1:11451": 1 + }, + "type": "roundrobin" + }, + "plugins": { + "response-rewrite": { + "vars": [ + ["status","==",200] + ], + "body": "new body\n" + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 4: should rewrite body and clear Content-Encoding header +--- request +GET /gzip_hello +--- more_headers +Accept-Encoding: gzip +--- response_body +new body +--- response_headers +Content-Encoding: + + + +=== TEST 5: set route use gzip upstream and response-rewrite filter conf +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/gzip_hello", + "upstream": { + "nodes": { + "127.0.0.1:11451": 1 + }, + "type": "roundrobin" + }, + "plugins": { + "response-rewrite": { + "vars": [ + ["status","==",200] + ], + "filters": [ + { + "regex": "hello", + "replace": "test" + } + ] + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 6: gzip decode support, should rewrite body and clear Content-Encoding header +--- request +GET /gzip_hello +--- more_headers +Accept-Encoding: gzip +--- response_body +test world +--- response_headers +Content-Encoding: + + + +=== TEST 7: set route use response-write body conf, and mock unsupported compression encoding type +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/echo", + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "plugins": { + "response-rewrite": { + "vars": [ + ["status","==",200] + ], + "body": "new body\n" + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + return + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 8: use body conf will ignore encoding, should rewrite body and clear Content-Encoding header +--- request +POST /echo +fake body with mock content encoding header +--- more_headers +Content-Encoding: deflate +--- response_body +new body +--- response_headers +Content-Encoding: + + + +=== TEST 9: set route use response-write filter conf, and mock unsupported compression encoding type +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/echo", + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "plugins": { + "response-rewrite": { + "vars": [ + ["status","==",200] + ], + "filters": [ + { + "regex": "hello", + "replace": "test" + } + ] + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + return + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 10: use filter conf will report unsupported encoding type error +--- request +POST /echo +fake body with mock content encoding header +--- more_headers +Content-Encoding: deflate +--- response_headers +Content-Encoding: +--- error_log +filters may not work as expected due to unsupported compression encoding type: deflate + + + +=== TEST 11: set route use response-write plugin but not use filter conf or body conf +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/gzip_hello", + "upstream": { + "nodes": { + "127.0.0.1:11451": 1 + }, + "type": "roundrobin" + }, + "plugins": { + "response-rewrite": { + "vars": [ + ["status","==",200] + ], + "headers": { + "set": { + "X-Server-id": 3, + "X-Server-status": "on", + "Content-Type": "" + } + } + } + } + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 12: should keep Content-Encoding +--- request +GET /gzip_hello +--- more_headers +Accept-Encoding: gzip +--- response_headers +Content-Encoding: gzip +X-Server-id: 3 +X-Server-status: on +Content-Type: + + + +=== TEST 13: response-write without filter conf or body conf, and mock unsupported compression encoding type +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/echo", + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "plugins": { + "response-rewrite": { + "vars": [ + ["status","==",200] + ], + "headers": { + "set": { + "X-Server-id": 3, + "X-Server-status": "on", + "Content-Type": "" + } + } + } + } + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 14: should keep Content-Encoding +--- request +POST /echo +fake body with mock content encoding header +--- more_headers +Content-Encoding: deflate +--- response_headers +Content-Encoding: deflate +X-Server-id: 3 +X-Server-status: on +Content-Type: