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: support variable when rewrite header in proxy rewrite plugin #9112

Merged
Show file tree
Hide file tree
Changes from 2 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
41 changes: 41 additions & 0 deletions apisix/core/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -335,4 +335,45 @@ end
_M.resolve_var = resolve_var


local resolve_var_with_captures
do
local _captures
-- escape is not supported very well, like there si a redundant '\' after escape "$1"
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
-- escape is not supported very well, like there si a redundant '\' after escape "$1"
-- escape is not supported very well, like there is a redundant '\' after escape "$1"

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

local pat = [[ (?<! \\) \$ \{? (\d+) \}? ]]

local function resolve(m)
local v = _captures[tonumber(m[1])]
leslie-tsang marked this conversation as resolved.
Show resolved Hide resolved
if not v then
v = ""
end
return v
end

-- captures is the match result of regex uri in proxy-rewrite plugin
function resolve_var_with_captures(tpl, captures)
if not tpl then
return tpl, nil
end

local from = core_str.find(tpl, "$")
if not from then
return tpl, nil
end

captures = captures or {}

_captures = captures
local res, _, err = re_gsub(tpl, pat, resolve, "jox")
_captures = nil
if not res then
return nil, err
end

return res, nil
end
end
-- Resolve {$1, $2, ...} in the given string
_M.resolve_var_with_captures = resolve_var_with_captures


return _M
29 changes: 24 additions & 5 deletions apisix/plugins/proxy-rewrite.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ local ipairs = ipairs
local ngx = ngx
local type = type
local re_sub = ngx.re.sub
local re_match = ngx.re.match
local sub_str = string.sub
local str_find = core.string.find

Expand All @@ -41,6 +42,10 @@ local lrucache = core.lrucache.new({
type = "plugin",
})

core.ctx.register_var("proxy_rewrite_regex_uri_captures", function(ctx)
return ctx.proxy_rewrite_regex_uri_captures
soulbird marked this conversation as resolved.
Show resolved Hide resolved
end)

local schema = {
type = "object",
properties = {
Expand Down Expand Up @@ -257,6 +262,7 @@ do
return re_sub(s, [[\?]], "%3F", "jo")
end


function _M.rewrite(conf, ctx)
for _, name in ipairs(upstream_names) do
if conf[name] then
Expand All @@ -278,15 +284,24 @@ function _M.rewrite(conf, ctx)

local uri, _, err = re_sub(upstream_uri, conf.regex_uri[1],
conf.regex_uri[2], "jo")
if uri then
upstream_uri = uri
else
if not uri then
local msg = "failed to substitute the uri " .. ctx.var.uri ..
" (" .. conf.regex_uri[1] .. ") with " ..
conf.regex_uri[2] .. " : " .. err
core.log.error(msg)
return 500, {message = msg}
end

local m, err = re_match(upstream_uri, conf.regex_uri[1], "jo")
if not m then
if err then
Copy link
Member

Choose a reason for hiding this comment

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

We can merge the two conditions above?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

how? use the re_match first, then use the match result to replace?

Copy link
Member

Choose a reason for hiding this comment

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

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

leslie-tsang marked this conversation as resolved.
Show resolved Hide resolved
core.log.error("match error in proxy-rewrite plugin, please check: ", err)
return
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to return a 503 for this error?

Copy link
Contributor Author

@monkeyDluffy6017 monkeyDluffy6017 Mar 22, 2023

Choose a reason for hiding this comment

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

I prefer 500, it's an unexpected condition

end
end
ctx.proxy_rewrite_regex_uri_captures = m

upstream_uri = uri
end

if not conf.use_real_request_uri_unsafe then
Expand Down Expand Up @@ -325,14 +340,18 @@ function _M.rewrite(conf, ctx)

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 val = core.utils.resolve_var_with_captures(hdr_op.add[i + 1],
ctx.proxy_rewrite_regex_uri_captures)
val = core.utils.resolve_var(val, ctx.var)
local header = hdr_op.add[i]
core.request.add_header(ctx, header, val)
end

local field_cnt = #hdr_op.set
for i = 1, field_cnt, 2 do
local val = core.utils.resolve_var(hdr_op.set[i + 1], ctx.var)
local val = core.utils.resolve_var_with_captures(hdr_op.set[i + 1],
ctx.proxy_rewrite_regex_uri_captures)
val = core.utils.resolve_var(val, ctx.var)
core.request.set_header(ctx, hdr_op.set[i], val)
end

Expand Down
4 changes: 2 additions & 2 deletions docs/en/latest/plugins/proxy-rewrite.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ The `proxy-rewrite` Plugin rewrites Upstream proxy information such as `scheme`,
| 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 | | | |
| 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 the header does not exist, it will be added. The format is `{"name": "value", ...}`. The values in the header can contain Nginx variables like $remote_addr and $balancer_ip. |
| 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`. It also supports referencing the match result of `regex_uri` as a variable like `$1-$2-$3`. |
| headers.set | object | false | | | Overwrite the headers. If the header does not exist, it will be added. The format is `{"name": "value", ...}`. The values in the header can contain Nginx variables like `$remote_addr` and `$balancer_ip`. It also supports referencing the match result of `regex_uri` as a variable like `$1-$2-$3`. |
| 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**. |

Expand Down
4 changes: 2 additions & 2 deletions docs/zh/latest/plugins/proxy-rewrite.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ description: 本文介绍了关于 Apache APISIX `proxy-rewrite` 插件的基本
| 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.add | object | 否 | | | 添加新的请求头,如果头已经存在,会追加到末尾。格式为 `{"name: value", ...}`。这个值能够以 `$var` 的格式包含 NGINX 变量,比如 `$remote_addr $balancer_ip`。 |
| headers.set | object | 否 | | | 改写请求头,如果请求头不存在,则会添加这个请求头。格式为 `{"name": "value", ...}`。这个值能够以 `$var` 的格式包含 NGINX 变量,比如 `$remote_addr $balancer_ip`。 |
| headers.add | object | 否 | | | 添加新的请求头,如果头已经存在,会追加到末尾。格式为 `{"name: value", ...}`。这个值能够以 `$var` 的格式包含 NGINX 变量,比如 `$remote_addr $balancer_ip`。也支持以变量的形式引用 `regex_uri` 的匹配结果,比如 `$1-$2-$3`。 |
| headers.set | object | 否 | | | 改写请求头,如果请求头不存在,则会添加这个请求头。格式为 `{"name": "value", ...}`。这个值能够以 `$var` 的格式包含 NGINX 变量,比如 `$remote_addr $balancer_ip`。也支持以变量的形式引用 `regex_uri` 的匹配结果,比如 `$1-$2-$3`。 |
| headers.remove | array | 否 | | | 移除响应头。格式为 `["name", ...]`。

## Header 优先级
Expand Down
149 changes: 149 additions & 0 deletions t/plugin/proxy-rewrite3.t
Original file line number Diff line number Diff line change
Expand Up @@ -622,3 +622,152 @@ GET /test/plugin/proxy/rewrite HTTP/1.1
}
--- response_body
/plugin_proxy_rewrite?a=c



=== TEST 27: use variables in headers when captured by regex_uri
--- 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": "/test/*",
"plugins": {
"proxy-rewrite": {
"regex_uri": ["^/test/(.*)/(.*)/(.*)", "/echo"],
"headers": {
"add": {
"X-Request-ID": "$1/$2/$3"
}
}
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
}
}]]
)

if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- request
GET /t
--- response_body
passed



=== TEST 28: hit
--- request
GET /test/plugin/proxy/rewrite HTTP/1.1
--- response_headers
X-Request-ID: plugin/proxy/rewrite



=== TEST 29: use variables in header when not matched regex_uri
--- 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*",
"plugins": {
"proxy-rewrite": {
"regex_uri": ["^/test/(.*)/(.*)/(.*)", "/echo"],
"headers": {
"add": {
"X-Request-ID": "$1/$2/$3"
}
}
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
}
}]]
)

if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- request
GET /t
--- response_body
passed



=== TEST 30: hit
--- request
GET /echo HTTP/1.1
--- more_headers
X-Foo: Foo
--- response_headers
X-Foo: Foo



=== TEST 31: use variables in headers when captured by regex_uri
--- 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": "/test/*",
"plugins": {
"proxy-rewrite": {
"regex_uri": ["^/test/(not_matched)?.*", "/echo"],
"headers": {
"add": {
"X-Request-ID": "test1/$1/$2/test2"
}
}
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
}
}]]
)

if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- request
GET /t
--- response_body
passed



=== TEST 32: hit
--- request
GET /test/plugin/proxy/rewrite HTTP/1.1
--- response_headers
X-Request-ID: test1///test2