Skip to content

Commit

Permalink
feat: proxy-rewrite support config add set and remove header (#8336)
Browse files Browse the repository at this point in the history
Co-authored-by: 罗泽轩 <spacewanderlzx@gmail.com>
Fixes #8239
  • Loading branch information
mscb402 authored Nov 22, 2022
1 parent 6dbf976 commit 01b4b49
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 49 deletions.
14 changes: 14 additions & 0 deletions apisix/core/request.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
185 changes: 150 additions & 35 deletions apisix/plugins/proxy-rewrite.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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.",
Expand All @@ -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)
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
25 changes: 21 additions & 4 deletions docs/en/latest/plugins/proxy-rewrite.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, `[" ^/iresty/(.*)/(.*)/(.*)", "/$1-$2-$3"]`. Here, the first element is the regular expression to match and the second element is the URL template forwarded to the Upstream. |
| 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:
Expand All @@ -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"
]
}
}
},
Expand Down
25 changes: 21 additions & 4 deletions docs/zh/latest/plugins/proxy-rewrite.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

## 启用插件

Expand All @@ -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"
]
}
}
},
Expand Down
Loading

0 comments on commit 01b4b49

Please sign in to comment.