You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by sp...@apache.org on 2022/11/22 06:36:19 UTC

[apisix] branch master updated: feat: proxy-rewrite support config add set and remove header (#8336)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 01b4b49eb feat: proxy-rewrite support config add set and remove header (#8336)
01b4b49eb is described below

commit 01b4b49eb2ba642b337f7a1fbe1894a77942910b
Author: Joanthan Chen <ms...@gmail.com>
AuthorDate: Tue Nov 22 14:36:13 2022 +0800

    feat: proxy-rewrite support config add set and remove header (#8336)
    
    Co-authored-by: 罗泽轩 <sp...@gmail.com>
    Fixes https://github.com/apache/apisix/issues/8239
---
 apisix/core/request.lua                 |  14 +++
 apisix/plugins/proxy-rewrite.lua        | 185 ++++++++++++++++++++++++++------
 docs/en/latest/plugins/proxy-rewrite.md |  25 ++++-
 docs/zh/latest/plugins/proxy-rewrite.md |  25 ++++-
 t/plugin/proxy-rewrite3.t               | 130 ++++++++++++++++++++--
 5 files changed, 330 insertions(+), 49 deletions(-)

diff --git a/apisix/core/request.lua b/apisix/core/request.lua
index 8fb947305..173fafc56 100644
--- a/apisix/core/request.lua
+++ b/apisix/core/request.lua
@@ -22,6 +22,11 @@
 local lfs = require("lfs")
 local log = require("apisix.core.log")
 local io = require("apisix.core.io")
+local req_add_header
+if ngx.config.subsystem == "http" then
+    local ngx_req = require "ngx.req"
+    req_add_header = ngx_req.add_header
+end
 local is_apisix_or, a6_request = pcall(require, "resty.apisix.request")
 local ngx = ngx
 local get_headers = ngx.req.get_headers
@@ -138,6 +143,15 @@ function _M.set_header(ctx, header_name, header_value)
     end
 end
 
+function _M.add_header(header_name, header_value)
+    local err
+    header_name, err = _validate_header_name(header_name)
+    if err then
+        error(err)
+    end
+
+    req_add_header(header_name, header_value)
+end
 
 -- return the remote address of client which directly connecting to APISIX.
 -- so if there is a load balancer between downstream client and APISIX,
diff --git a/apisix/plugins/proxy-rewrite.lua b/apisix/plugins/proxy-rewrite.lua
index 7b9a99f0b..fdb8c8184 100644
--- a/apisix/plugins/proxy-rewrite.lua
+++ b/apisix/plugins/proxy-rewrite.lua
@@ -37,6 +37,10 @@ for key in pairs(switch_map) do
     core.table.insert(schema_method_enum, key)
 end
 
+local lrucache = core.lrucache.new({
+    type = "plugin",
+})
+
 local schema = {
     type = "object",
     properties = {
@@ -70,8 +74,61 @@ local schema = {
         },
         headers = {
             description = "new headers for request",
-            type = "object",
-            minProperties = 1,
+            oneOf = {
+                {
+                    type = "object",
+                    minProperties = 1,
+                    additionalProperties = false,
+                    properties = {
+                        add = {
+                            type = "object",
+                            minProperties = 1,
+                            patternProperties = {
+                                ["^[^:]+$"] = {
+                                    oneOf = {
+                                        { type = "string" },
+                                        { type = "number" }
+                                    }
+                                }
+                            },
+                        },
+                        set = {
+                            type = "object",
+                            minProperties = 1,
+                            patternProperties = {
+                                ["^[^:]+$"] = {
+                                    oneOf = {
+                                        { type = "string" },
+                                        { type = "number" },
+                                    }
+                                }
+                            },
+                        },
+                        remove = {
+                            type = "array",
+                            minItems = 1,
+                            items = {
+                                type = "string",
+                                -- "Referer"
+                                pattern = "^[^:]+$"
+                            }
+                        },
+                    },
+                },
+                {
+                    type = "object",
+                    minProperties = 1,
+                    patternProperties = {
+                        ["^[^:]+$"] = {
+                            oneOf = {
+                                { type = "string" },
+                                { type = "number" }
+                            }
+                        }
+                    },
+                }
+            },
+
         },
         use_real_request_uri_unsafe = {
             description = "use real_request_uri instead, THIS IS VERY UNSAFE.",
@@ -90,6 +147,37 @@ local _M = {
     schema   = schema,
 }
 
+local function is_new_headers_conf(headers)
+    return (headers.add and type(headers.add) == "table") or
+        (headers.set and type(headers.set) == "table") or
+        (headers.remove and type(headers.remove) == "table")
+end
+
+local function check_set_headers(headers)
+    for field, value in pairs(headers) do
+        if type(field) ~= 'string' then
+            return false, 'invalid type as header field'
+        end
+
+        if type(value) ~= 'string' and type(value) ~= 'number' then
+            return false, 'invalid type as header value'
+        end
+
+        if #field == 0 then
+            return false, 'invalid field length in header'
+        end
+
+        core.log.info("header field: ", field)
+        if not core.utils.validate_header_field(field) then
+            return false, 'invalid field character in header'
+        end
+        if not core.utils.validate_header_value(value) then
+            return false, 'invalid value character in header'
+        end
+    end
+
+    return true
+end
 
 function _M.check_schema(conf)
     local ok, err = core.schema.check(schema, conf)
@@ -111,27 +199,12 @@ function _M.check_schema(conf)
         return true
     end
 
-    for field, value in pairs(conf.headers) do
-        if type(field) ~= 'string' then
-            return false, 'invalid type as header field'
-        end
-
-        if type(value) ~= 'string' and type(value) ~= 'number' then
-            return false, 'invalid type as header value'
-        end
-
-        if #field == 0 then
-            return false, 'invalid field length in header'
-        end
-
-        core.log.info("header field: ", field)
-
-        if not core.utils.validate_header_field(field) then
-            return false, 'invalid field character in header'
-        end
-
-        if not core.utils.validate_header_value(value) then
-            return false, 'invalid value character in header'
+    if conf.headers then
+        if not is_new_headers_conf(conf.headers) then
+            ok, err = check_set_headers(conf.headers)
+            if not ok then
+                return false, err
+            end
         end
     end
 
@@ -150,13 +223,43 @@ do
         core.table.insert(upstream_names, name)
     end
 
-function _M.rewrite(conf, ctx)
-    for _, name in ipairs(upstream_names) do
-        if conf[name] then
-            ctx.var[upstream_vars[name]] = conf[name]
+    local function create_header_operation(hdr_conf)
+        local set = {}
+        local add = {}
+
+        if is_new_headers_conf(hdr_conf) then
+            if hdr_conf.add then
+                for field, value in pairs(hdr_conf.add) do
+                    core.table.insert_tail(add, field, value)
+                end
+            end
+            if hdr_conf.set then
+                for field, value in pairs(hdr_conf.set) do
+                    core.table.insert_tail(set, field, value)
+                end
+            end
+
+        else
+            for field, value in pairs(hdr_conf) do
+                core.table.insert_tail(set, field, value)
+            end
         end
+
+        return {
+            add = add,
+            set = set,
+            remove = hdr_conf.remove or {},
+        }
     end
 
+
+    function _M.rewrite(conf, ctx)
+        for _, name in ipairs(upstream_names) do
+            if conf[name] then
+                ctx.var[upstream_vars[name]] = conf[name]
+            end
+        end
+
     local upstream_uri = ctx.var.uri
     if conf.use_real_request_uri_unsafe then
         upstream_uri = ctx.var.real_request_uri
@@ -197,19 +300,31 @@ function _M.rewrite(conf, ctx)
     end
 
     if conf.headers then
-        if not conf.headers_arr then
-            conf.headers_arr = {}
+        local hdr_op, err = core.lrucache.plugin_ctx(lrucache, ctx, nil,
+                                    create_header_operation, conf.headers)
+        if not hdr_op then
+            core.log.error("failed to create header operation: ", err)
+            return
+        end
 
-            for field, value in pairs(conf.headers) do
-                core.table.insert_tail(conf.headers_arr, field, value)
-            end
+        local field_cnt = #hdr_op.add
+        for i = 1, field_cnt, 2 do
+            local val = core.utils.resolve_var(hdr_op.add[i + 1], ctx.var)
+            local header = hdr_op.add[i]
+            core.request.add_header(header, val)
         end
 
-        local field_cnt = #conf.headers_arr
+        local field_cnt = #hdr_op.set
         for i = 1, field_cnt, 2 do
-            core.request.set_header(ctx, conf.headers_arr[i],
-                                    core.utils.resolve_var(conf.headers_arr[i+1], ctx.var))
+            local val = core.utils.resolve_var(hdr_op.set[i + 1], ctx.var)
+            core.request.set_header(hdr_op.set[i], val)
+        end
+
+        local field_cnt = #hdr_op.remove
+        for i = 1, field_cnt do
+            core.request.set_header(hdr_op.remove[i], nil)
         end
+
     end
 
     if conf.method then
diff --git a/docs/en/latest/plugins/proxy-rewrite.md b/docs/en/latest/plugins/proxy-rewrite.md
index 49b915cc4..b4c14d7d5 100644
--- a/docs/en/latest/plugins/proxy-rewrite.md
+++ b/docs/en/latest/plugins/proxy-rewrite.md
@@ -39,9 +39,18 @@ The `proxy-rewrite` Plugin rewrites Upstream proxy information such as `scheme`,
 | method                      | string        | False    |         | ["GET", "POST", "PUT", "HEAD", "DELETE", "OPTIONS","MKCOL", "COPY", "MOVE", "PROPFIND", "PROPFIND","LOCK", "UNLOCK", "PATCH", "TRACE"] | Rewrites the HTTP method.                                                                                                                                                                                                                                                                      [...]
 | regex_uri                   | array[string] | False    |         |                                                                                                                                        | New upstream forwarding address. Regular expressions can be used to match the URL from client. If it matches, the URL template is forwarded to the Upstream otherwise, the URL from the client is forwarded. When both `uri` and `regex_uri` are configured, `uri` is used first. For example, [...]
 | host                        | string        | False    |         |                                                                                                                                        | New Upstream host address.                                                                                                                                                                                                                                                                     [...]
-| headers                     | object        | False    |         |                                                                                                                                        | New Upstream headers. Headers are overwritten if they are already present otherwise, they are added to the present headers. To remove a header, set the header value to an empty string. The values in the header can contain Nginx variables like `$remote_addr` and `$client_addr`.          [...]
+| headers                     | object        | False    |         |                                                                                                                                   |                   |
+| headers.add     | object   | false     |        |                 | Append the new headers. The format is `{"name: value",...}`. The values in the header can contain Nginx variables like $remote_addr and $balancer_ip.                                                                                              |
+| headers.set     | object  | false     |        |                 | Overwrite the headers. If header is not exist, will add it. The format is  `{"name": "value", ...}`. The values in the header can contain Nginx variables like $remote_addr and $balancer_ip.                                                                                                |
+| headers.remove  | array   | false     |        |                 | Remove the headers. The format is `["name", ...]`.
 | use_real_request_uri_unsafe | boolean       | False    | false   |                                                                                                                                        | Use real_request_uri (original $request_uri in nginx) to bypass URI normalization. **Enabling this is considered unsafe as it bypasses all URI normalization steps**.                                                                                                                          [...]
 
+## Header Priority
+
+Header configurations are executed according to the following priorities:
+
+`add` > `remove` > `set`
+
 ## Enabling the Plugin
 
 The example below enables the `proxy-rewrite` Plugin on a specific Route:
@@ -56,9 +65,17 @@ curl http://127.0.0.1:9180/apisix/admin/routes/1  -H 'X-API-KEY: edd1c9f034335f1
             "uri": "/test/home.html",
             "host": "iresty.com",
             "headers": {
-                "X-Api-Version": "v1",
-                "X-Api-Engine": "apisix",
-                "X-Api-useless": ""
+               "set": {
+                    "X-Api-Version": "v1",
+                    "X-Api-Engine": "apisix",
+                    "X-Api-useless": ""
+                },
+                "add": {
+                    "X-Request-ID": "112233"
+                },
+                "remove":[
+                    "X-test"
+                ]
             }
         }
     },
diff --git a/docs/zh/latest/plugins/proxy-rewrite.md b/docs/zh/latest/plugins/proxy-rewrite.md
index 2b1d0e2a4..765b5f567 100644
--- a/docs/zh/latest/plugins/proxy-rewrite.md
+++ b/docs/zh/latest/plugins/proxy-rewrite.md
@@ -39,7 +39,16 @@ description: 本文介绍了关于 Apache APISIX `proxy-rewrite` 插件的基本
 | method    | string        | 否    |         | ["GET", "POST", "PUT", "HEAD", "DELETE", "OPTIONS","MKCOL", "COPY", "MOVE", "PROPFIND", "PROPFIND","LOCK", "UNLOCK", "PATCH", "TRACE"] | 将路由的请求方法代理为该请求方法。 |
 | regex_uri | array[string] | 否    |         |                                                                                                                                        | 转发到上游的新 `uri` 地址。使用正则表达式匹配来自客户端的 `uri`,如果匹配成功,则使用模板替换转发到上游的 `uri`,如果没有匹配成功,则将客户端请求的 `uri` 转发至上游。当同时配置 `uri` 和 `regex_uri` 属性时,优先使用 `uri`。例如:["^/iresty/(.*)/(.*)/(.*)","/$1-$2-$3"] 第一个元素代表匹配来自客户端请求的 `uri` 正则表达式,第二个元素代表匹配成功后转发到上游的 `uri` 模板。但是目前 APISIX 仅支持一个 `regex_uri`,所以 `regex_uri` 数组的长度是 `2`。 |
 | host      | string        | 否    |         |                   | 转发到上游的新 `host` 地址,例如:`iresty.com`。|
-| headers   | object        | 否    |         |                   | 转发到上游的新 `headers`,可以设置多个。如果 header 存在将进行重写,如果不存在则会添加到 header 中。如果你想要删除某个 header,请把对应的值设置为空字符串即可。支持使用 NGINX 的变量,例如 `client_addr` 和`$remote_addr`。|
+| headers   | object        | 否    |         |                   |   |
+| headers.add     | object   | 否     |        |                 | 添加新的请求头,如果头已经存在,会追加到末尾。格式为 `{"name: value", ...}`。这个值能够以 `$var` 的格式包含 NGINX 变量,比如 `$remote_addr $balancer_ip`。                                                                                              |
+| headers.set     | object  | 否     |        |                 | 改写请求头,如果请求头不存在,则会添加这个请求头。格式为 `{"name": "value", ...}`。这个值能够以 `$var` 的格式包含 NGINX 变量,比如 `$remote_addr $balancer_ip`。                                                                                                |
+| headers.remove  | array   | 否     |        |                 | 移除响应头。格式为 `["name", ...]`。
+
+## Header 优先级
+
+Header 头的相关配置,遵循如下优先级进行执行:
+
+`add` > `remove` > `set`
 
 ## 启用插件
 
@@ -56,9 +65,17 @@ curl http://127.0.0.1:9180/apisix/admin/routes/1  \
             "uri": "/test/home.html",
             "host": "iresty.com",
             "headers": {
-                "X-Api-Version": "v1",
-                "X-Api-Engine": "apisix",
-                "X-Api-useless": ""
+                "set": {
+                    "X-Api-Version": "v1",
+                    "X-Api-Engine": "apisix",
+                    "X-Api-useless": ""
+                },
+                "add": {
+                    "X-Request-ID": "112233"
+                },
+                "remove":[
+                    "X-test"
+                ]
             }
         }
     },
diff --git a/t/plugin/proxy-rewrite3.t b/t/plugin/proxy-rewrite3.t
index 7ddedefd1..621c06557 100644
--- a/t/plugin/proxy-rewrite3.t
+++ b/t/plugin/proxy-rewrite3.t
@@ -319,12 +319,8 @@ ngx.var.request_uri: /print_uri_detailed
             ngx.say(body)
         }
     }
---- request
-GET /t
 --- response_body
 passed
---- no_error_log
-[error]
 
 
 
@@ -335,5 +331,127 @@ GET /echo HTTP/1.1
 X-Forwarded-Host: apisix.ai
 --- response_headers
 X-Forwarded-Host: test.com
---- no_error_log
-[error]
+
+
+
+=== TEST 14: set route header test
+--- 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,
+                 [[{
+                        "methods": ["GET"],
+                        "plugins": {
+                            "proxy-rewrite": {
+                                "headers": {
+                                    "add":{"test": "123"},
+                                    "set":{"test2": "2233"},
+                                    "remove":["hello"]
+                                }
+                            }
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "127.0.0.1:1980": 1
+                            },
+                            "type": "roundrobin"
+                        },
+                        "uri": "/echo"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 15: add exist header in muti-header
+--- request
+GET /echo HTTP/1.1
+--- more_headers
+test: sssss
+test: bbb
+--- response_headers
+test: sssss, bbb, 123
+
+
+
+=== TEST 16: add header to exist header
+--- request
+GET /echo HTTP/1.1
+--- more_headers
+test: sssss
+--- response_headers
+test: sssss, 123
+
+
+
+=== TEST 17: remove header
+--- request
+GET /echo HTTP/1.1
+--- more_headers
+hello: word
+--- response_headers
+hello:
+
+
+
+=== TEST 18: set header success
+--- request
+GET /echo HTTP/1.1
+--- response_headers
+test2: 2233
+
+
+
+=== TEST 19: header priority test
+--- 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,
+                 [[{
+                        "methods": ["GET"],
+                        "plugins": {
+                            "proxy-rewrite": {
+                                "headers": {
+                                    "add":{"test": "test_in_add"},
+                                    "set":{"test": "test_in_set"}
+                                }
+                            }
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "127.0.0.1:1980": 1
+                            },
+                            "type": "roundrobin"
+                        },
+                        "uri": "/echo"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 20: set and test priority test
+--- request
+GET /echo HTTP/1.1
+--- response_headers
+test: test_in_set