You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by we...@apache.org on 2019/11/21 13:50:01 UTC

[incubator-apisix] branch master updated: plugin: implement plugin `response rewrite`

This is an automated email from the ASF dual-hosted git repository.

wenming pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new fbb51dd  plugin: implement plugin `response rewrite`
fbb51dd is described below

commit fbb51ddadd1ded52da3a04c77e047df4fe25e73c
Author: Lien <li...@users.noreply.github.com>
AuthorDate: Thu Nov 21 21:49:53 2019 +0800

    plugin: implement plugin `response rewrite`
---
 README.md                               |   1 +
 README_CN.md                            |   1 +
 conf/config.yaml                        |   1 +
 doc/README.md                           |   1 +
 doc/README_CN.md                        |   1 +
 doc/plugins/response-rewrite-cn.md      |  82 ++++++++
 doc/plugins/response-rewrite.md         |  82 ++++++++
 lua/apisix/plugins/response-rewrite.lua | 115 +++++++++++
 t/admin/plugins.t                       |   2 +-
 t/debug/debug-mode.t                    |   1 +
 t/lib/server.lua                        |   9 +
 t/plugin/response-rewrite.t             | 356 ++++++++++++++++++++++++++++++++
 12 files changed, 651 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 084633e..5f2d94c 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,7 @@ For more detailed information, see the [White Paper](https://www.iresty.com/down
 - **CLI**: start\stop\reload APISIX through the command line.
 - **REST API**
 - **Proxy Websocket**
+- **[Response Rewrite](doc/plugins/response-rewrite.md)**: Set customized response status code, body and header to the client.
 - **IPv6**: Use IPv6 to match route.
 - **Clustering**: APISIX nodes are stateless, creates clustering of the configuration center, please refer to [etcd Clustering Guide](https://github.com/etcd-io/etcd/blob/master/Documentation/v2/clustering.md).
 - **Scalability**: plug-in mechanism is easy to extend.
diff --git a/README_CN.md b/README_CN.md
index 79d0899..2141831 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -60,6 +60,7 @@ APISIX 通过插件机制,提供动态负载平衡、身份验证、限流限
 - **[限制请求数](doc/plugins/limit-count-cn.md)**
 - **[限制并发](doc/plugins/limit-conn-cn.md)**
 - **[代理请求重写](doc/plugins/proxy-rewrite.md)**: 支持重写请求上游的`host`、`uri`、`schema`、`enable_websocket`、`headers`信息。
+- **[输出内容重写](doc/plugins/response-rewrite.md)**: 支持自定义修改返回内容的 `status code`、`body`、`headers`。
 - **OpenTracing: [支持 Apache Skywalking 和 Zipkin](doc/plugins/zipkin.md)**
 - **监控和指标**: [Prometheus](doc/plugins/prometheus-cn.md)
 - **[gRPC 代理](doc/grpc-proxy-cn.md)**:通过 APISIX 代理 gRPC 连接,并使用 APISIX 的大部分特性管理你的 gRPC 服务。
diff --git a/conf/config.yaml b/conf/config.yaml
index 68cf4e6..8537184 100644
--- a/conf/config.yaml
+++ b/conf/config.yaml
@@ -88,6 +88,7 @@ plugins:                          # plugin list
   - openid-connect
   - proxy-rewrite
   - redirect
+  - response-rewrite
 
 stream_plugins:
   - mqtt-proxy
diff --git a/doc/README.md b/doc/README.md
index 56591f0..7198173 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -54,3 +54,4 @@ Plugins
 * [ip-restriction](plugins/ip-restriction.md): IP whitelist/blacklist.
 * openid-connect
 * [redirect](plugins/redirect.md): URI redirect.
+* [response-rewrite](plugins/response-rewrite.md): Set customized response status code, body and header to the client.
diff --git a/doc/README_CN.md b/doc/README_CN.md
index 20e69ef..0af4da3 100644
--- a/doc/README_CN.md
+++ b/doc/README_CN.md
@@ -55,3 +55,4 @@ Reference document
 * [ip-restriction](plugins/ip-restriction-cn.md): IP 黑白名单。
 * openid-connect
 * [redirect](plugins/redirect-cn.md): URI 重定向。
+* [response-rewrite](plugins/response-rewrite-cn.md): 支持自定义修改返回内容的 `status code`、`body`、`headers`。
diff --git a/doc/plugins/response-rewrite-cn.md b/doc/plugins/response-rewrite-cn.md
new file mode 100644
index 0000000..d81e62b
--- /dev/null
+++ b/doc/plugins/response-rewrite-cn.md
@@ -0,0 +1,82 @@
+<!--
+#
+# 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.
+#
+-->
+
+[English](response-rewrite.md)
+# response-rewrite
+
+该插件支持修改上游服务返回的 body 和 header 信息。
+
+使用场景:
+1、可以设置 `Access-Control-Allow-*` 等 header 信息,来实现 CORS (跨域资源共享)的功能。
+2、另外也可以通过配置 status_code 和 header 里面的 Location 来实现重定向,当然如果只是需要重定向功能,最好使用 [redirect](redirect-cn.md) 插件。
+
+#### 配置参数
+|名字    |可选|说明|
+|------- |-----|------|
+|status_code   |可选| 修改上游返回状态码|
+|body          |可选| 修改上游返回的 `body` 内容,如果设置了新内容,header 里面的 content-length 字段也会被去掉|
+|headers       |可选| 返回给客户端的 `headers`,这里可以设置多个。头信息如果存在将重写,不存在则添加。想要删除某个 header 的话,把对应的值设置为空字符串即可|
+
+
+### 示例
+
+#### 开启插件
+下面是一个示例,在指定的 route 上开启了 `response rewrite` 插件:
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d '
+{
+    "methods": ["GET"],
+    "uri": "/test/index.html",
+    "plugins": {
+        "response-rewrite": {
+            "body": "{\"code\":\"ok\",\"message\":\"new json body\"}",
+            "headers": {
+                "X-Server-id": 3,
+                "X-Server-status": "on"
+            }
+        }
+    },
+    "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+            "127.0.0.1:80": 1
+        }
+    }
+}'
+```
+
+#### 测试插件
+基于上述配置进行测试:
+
+```shell
+curl -X GET -i  http://127.0.0.1:9080/test/index.html
+```
+
+如果看到返回的头部信息和内容都被修改了,即表示 `response rewrite` 插件生效了。
+```
+HTTP/1.1 200 OK
+Date: Sat, 16 Nov 2019 09:15:12 GMT
+Transfer-Encoding: chunked
+Connection: keep-alive
+X-Server-id: 3
+X-Server-status: on
+
+{"code":"ok","message":"new json body"}
+```
diff --git a/doc/plugins/response-rewrite.md b/doc/plugins/response-rewrite.md
new file mode 100644
index 0000000..5a034aa
--- /dev/null
+++ b/doc/plugins/response-rewrite.md
@@ -0,0 +1,82 @@
+<!--
+#
+# 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.
+#
+-->
+
+[中文](response-rewrite-cn.md)
+# response-rewrite
+
+response rewrite plugin, rewrite the content from upstream.
+
+senario:
+1、can set `Access-Control-Allow-*` series field to support CORS(Cross-origin Resource Sharing) .
+2、we can set customized `status_code` and `Location` field in header to achieve redirect, you can alse use [redirect](redirect-cn.md) plugin if you just want a redirect function.
+
+### Parameters
+|Name    |Required|Description|
+|-------         |-----|------|
+|status_code   |No| New `status code` to client|
+|body          |No| New `body` to client, and the content-length will be reset too.|
+|headers             |No| Set the new `headers` for client, can set up multiple. If it exists already from upstream, will rewrite the header, otherwise will add the header. You can set the corresponding value to an empty string to remove a header. |
+
+### Example
+
+#### Enable Plugin
+Here's an example, enable the `response rewrite` plugin on the specified route:
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d '
+{
+    "methods": ["GET"],
+    "uri": "/test/index.html",
+    "plugins": {
+        "response-rewrite": {
+            "body": "{\"code\":\"ok\",\"message\":\"new json body\"}",
+            "headers": {
+                "X-Server-id": 3,
+                "X-Server-status": "on"
+            }
+        }
+    },
+    "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+            "127.0.0.1:80": 1
+        }
+    }
+}'
+```
+
+#### Test Plugin
+Testing based on the above examples :
+```shell
+curl -X GET -i  http://127.0.0.1:9080/test/index.html
+```
+
+It will output like below,no matter what kinf of content from upstream.
+```
+HTTP/1.1 200 OK
+Date: Sat, 16 Nov 2019 09:15:12 GMT
+Transfer-Encoding: chunked
+Connection: keep-alive
+X-Server-id: 3
+X-Server-status: on
+
+{"code":"ok","message":"new json body"}
+```
+
+This means that the `response rewrite` plugin is in effect.
diff --git a/lua/apisix/plugins/response-rewrite.lua b/lua/apisix/plugins/response-rewrite.lua
new file mode 100644
index 0000000..e1f51d3
--- /dev/null
+++ b/lua/apisix/plugins/response-rewrite.lua
@@ -0,0 +1,115 @@
+--
+-- 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.
+--
+local core        = require("apisix.core")
+local plugin_name = "response-rewrite"
+local ngx         = ngx
+local pairs       = pairs
+local type        = type
+
+
+local schema = {
+    type = "object",
+    properties = {
+        headers = {
+            description = "new headers for repsonse",
+            type = "object",
+            minProperties = 1,
+        },
+        body = {
+            description = "new body for repsonse",
+            type = "string",
+        },
+        status_code = {
+            description = "new status code for repsonse",
+            type = "integer",
+            minimum = 200,
+            maximum = 598,
+        }
+    },
+    minProperties = 1,
+}
+
+
+local _M = {
+    version  = 0.1,
+    priority = 899,
+    name     = plugin_name,
+    schema   = schema,
+}
+
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+    if not ok then
+        return false, err
+    end
+
+    --reform header from object into array, so can avoid use pairs, which is NYI
+    if conf.headers then
+        conf.headers_arr = {}
+
+        for field, value in pairs(conf.headers) do
+            if type(field) == 'string'
+                and (type(value) == 'string' or type(value) == 'number') then
+                if #field == 0 then
+                    return false, 'invalid field length in header'
+                end
+                core.table.insert(conf.headers_arr, field)
+                core.table.insert(conf.headers_arr, value)
+            else
+                return false, 'invalid type as header value'
+            end
+
+        end
+    end
+
+    return true
+end
+
+
+do
+
+function _M.body_filter(conf, ctx)
+    if conf.body then
+        ngx.arg[1] = conf.body
+        ngx.arg[2] = true
+    end
+end
+
+function _M.header_filter(conf, ctx)
+    if conf.status_code then
+        ngx.status = conf.status_code
+    end
+
+    if conf.body then
+        ngx.header.content_length = nil
+        -- in case of upstream content is compressed content
+        ngx.header.content_encoding = nil
+    end
+
+    if conf.headers_arr then
+        local field_cnt = #conf.headers_arr
+        for i = 1, field_cnt, 2 do
+            ngx.header[conf.headers_arr[i]] = conf.headers_arr[i+1]
+        end
+    end
+end
+
+end  -- do
+
+
+return _M
diff --git a/t/admin/plugins.t b/t/admin/plugins.t
index 9044d4b..5301619 100644
--- a/t/admin/plugins.t
+++ b/t/admin/plugins.t
@@ -30,6 +30,6 @@ __DATA__
 --- request
 GET /apisix/admin/plugins/list
 --- response_body_like eval
-qr/\["limit-req","limit-count","limit-conn","key-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect"\]/
+qr/\["limit-req","limit-count","limit-conn","key-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite"\]/
 --- no_error_log
 [error]
diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t
index 42b3b1a..a67a01c 100644
--- a/t/debug/debug-mode.t
+++ b/t/debug/debug-mode.t
@@ -66,6 +66,7 @@ loaded plugin and sort by priority: 1002 name: limit-count
 loaded plugin and sort by priority: 1001 name: limit-req
 loaded plugin and sort by priority: 1000 name: node-status
 loaded plugin and sort by priority: 900 name: redirect
+loaded plugin and sort by priority: 899 name: response-rewrite
 loaded plugin and sort by priority: 506 name: grpc-transcode
 loaded plugin and sort by priority: 500 name: prometheus
 loaded plugin and sort by priority: 0 name: example-plugin
diff --git a/t/lib/server.lua b/t/lib/server.lua
index 8cdd346..5c59c35 100644
--- a/t/lib/server.lua
+++ b/t/lib/server.lua
@@ -85,6 +85,15 @@ function _M.opentracing()
 end
 
 
+function _M.with_header()
+    ngx.header['Content-Type'] = 'application/xml'
+    ngx.header['X-Server-id'] = 100
+    --split into multiple chunk
+    ngx.say("hello")
+    ngx.say("world")
+    ngx.say("!")
+end
+
 function _M.mock_zipkin()
     ngx.req.read_body()
     local data = ngx.req.get_body_data()
diff --git a/t/plugin/response-rewrite.t b/t/plugin/response-rewrite.t
new file mode 100644
index 0000000..a8a8228
--- /dev/null
+++ b/t/plugin/response-rewrite.t
@@ -0,0 +1,356 @@
+#
+# 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();
+run_tests;
+
+__DATA__
+
+=== TEST 1:  add plugin
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.response-rewrite")
+            local ok, err = plugin.check_schema({
+                body = 'Hello world',
+                headers = {
+                    ["X-Server-id"] = 3
+                }
+            })
+            if not ok then
+                ngx.say(err)
+            end
+
+            ngx.say("done")
+        }
+    }
+--- request
+GET /t
+--- response_body
+done
+--- no_error_log
+[error]
+
+
+
+=== TEST 2:  add plugin with wrong status_code
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.response-rewrite")
+            local ok, err = plugin.check_schema({
+                status_code = 599
+            })
+            if not ok then
+                ngx.say(err)
+            else
+                ngx.say("done")
+            end
+        }
+    }
+--- request
+GET /t
+--- response_body
+property "status_code" validation failed: expected 599 to be smaller than 598
+--- no_error_log
+[error]
+
+
+
+=== TEST 3:  add plugin fail
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.response-rewrite")
+            local ok, err = plugin.check_schema({
+                body = 2,
+                headers = {
+                    ["X-Server-id"] = "3"
+                }
+            })
+
+            if not ok then
+                ngx.say(err)
+            else
+                ngx.say("done")
+            end
+        }
+    }
+--- request
+GET /t
+--- response_body
+property "body" validation failed: wrong type: expected string, got number
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: set header(rewrite header and body)
+--- 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,
+                 [[{
+                    "plugins": {
+                        "response-rewrite": {
+                            "headers" : {
+                                "X-Server-id": 3,                
+                                "X-Server-status": "on",
+                                "Content-Type": ""
+                            },
+                            "body": "new body\n"
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/with_header"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 5: check body with deleted header
+--- config
+    location /t {
+        content_by_lua_block {
+            local http = require "resty.http"
+            local uri = "http://127.0.0.1:" .. ngx.var.server_port
+                        .. "/with_header"
+
+            local httpc = http.new()
+            local res, err = httpc:request_uri(uri, {method = "GET"})
+            if not res then
+                ngx.say(err)
+                return
+            end 
+
+            if res.headers['Content-Type'] then
+                ngx.say('fail content-type should not be exist, now is'..res.headers['Content-Type'])
+                return
+            end
+
+            if res.headers['X-Server-status'] ~= 'on' then
+                ngx.say('fail X-Server-status needs to be on')
+                return
+            end
+
+            if res.headers['X-Server-id'] ~= '3' then
+                ngx.say('fail X-Server-id needs to be 3')
+                return
+            end
+
+            ngx.print(res.body)
+            ngx.exit(200)
+        }
+    }
+--- request
+GET /t
+--- response_body
+new body
+--- no_error_log
+[error]
+
+
+
+=== TEST 6: set body only and keep header the same
+--- 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,
+                 [[{
+                    "plugins": {
+                        "response-rewrite": {
+                            "body": "new body2\n"
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/with_header"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 7: check body and header not changed
+--- request
+GET /with_header
+--- response_body
+new body2
+--- response_headers
+X-Server-id: 100
+Content-Type: application/xml
+--- no_error_log
+[error]
+
+
+
+=== TEST 8: set location header with 302 code
+--- 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,
+                 [[{
+                    "plugins": {
+                        "response-rewrite": {
+                            "headers": {
+                                "Location":"https://www.iresty.com"
+                            },
+                            "status_code":302
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/hello"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 9: check 302 redirect
+--- request
+GET /hello
+--- error_code eval
+302
+--- response_headers
+Location: https://www.iresty.com
+--- no_error_log
+[error]
+
+
+
+=== TEST 10:  empty string in header field
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.response-rewrite")
+            local ok, err = plugin.check_schema({
+                status_code = 200,
+                headers = {
+                    [""] = 2
+                }
+            })
+            if not ok then
+                ngx.say(err)
+            else
+                ngx.say("done")
+            end
+        }
+    }
+--- request
+GET /t
+--- response_body
+invalid field length in header
+--- no_error_log
+[error]
+
+
+
+=== TEST 11: array in header value
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.response-rewrite")
+            local ok, err = plugin.check_schema({
+                status_code = 200,
+                headers = {
+                    ["X-Name"] = {}
+                }
+            })
+            if not ok then
+                ngx.say(err)
+            else
+                ngx.say("done")
+            end
+        }
+    }
+--- request
+GET /t
+--- response_body
+invalid type as header value
+--- no_error_log
+[error]