Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: proxy-rewrite support config add set and remove header #8336

Merged
merged 22 commits into from
Nov 22, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 174 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,58 @@ local schema = {
},
headers = {
description = "new headers for request",
type = "object",
minProperties = 1,
anyOf = {
tokers marked this conversation as resolved.
Show resolved Hide resolved
{
type = "object",
minProperties = 1,
patternProperties = {
["^[^:]+$"] = {
oneOf = {
{ type = "string" },
{ type = "number" }
}
}
},
},
{
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",
-- "Set-Cookie"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set-Cookie is a response header. Better to use another example.

pattern = "^[^:]+$"
}
},
},
}
},

},
use_real_request_uri_unsafe = {
description = "use real_request_uri instead, THIS IS VERY UNSAFE.",
Expand All @@ -90,6 +144,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)
tokers marked this conversation as resolved.
Show resolved Hide resolved
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 +196,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 +220,69 @@ 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

local function addHeader(ctx, header_name, header_value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
local function addHeader(ctx, header_name, header_value)
local function add_header(ctx, header_name, header_value)

Copy link
Contributor Author

@mscb402 mscb402 Nov 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

local oldHeader = core.request.header(ctx, header_name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
local oldHeader = core.request.header(ctx, header_name)
local old_header = core.request.header(ctx, header_name)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if oldHeader == nil then
return header_value
end

if type(oldHeader) == "string" then
return oldHeader .. ", " .. header_value
end

if type(oldHeader) == "table" then
-- core.log.error("oldHeader",#oldHeader)
local result = ""
local field_cnt = #oldHeader
for i = 1, field_cnt, 1 do
if result == "" then
result = oldHeader[i]
else
result = result .. ", " .. oldHeader[i]
end
end
return result .. ", " .. header_value
end

return ""
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 +323,32 @@ 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]
val = addHeader(ctx, header, val)
core.request.set_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", ...]`.
tokers marked this conversation as resolved.
Show resolved Hide resolved
| 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 Configures the header according to the following priorities:

`remove` > `set` > `add`

## 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 头的相关配置,遵循如下优先级:

`remove` > `set` > `add`

## 启用插件

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