Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
40 changes: 39 additions & 1 deletion apisix/plugins/proxy-rewrite.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ local ipairs = ipairs
local ngx = ngx
local type = type
local re_sub = ngx.re.sub
local re_gsub = ngx.re.gsub
local re_match = ngx.re.match
local req_set_uri = ngx.req.set_uri
local sub_str = string.sub
local str_gsub = string.gsub
local str_find = core.string.find

local switch_map = {GET = ngx.HTTP_GET, POST = ngx.HTTP_POST, PUT = ngx.HTTP_PUT,
Expand All @@ -44,6 +46,26 @@ local lrucache = core.lrucache.new({
type = "plugin",
})

local nginx_var_pattern = [[(?<!\$)\$(?=[a-zA-Z_]|\{\s*[a-zA-Z_])]]


local function escape_nginx_vars(replacement)
local escaped, _, err = re_gsub(replacement, nginx_var_pattern, function()
return "$$"
end, "jo")
return escaped, err
end


local function preserve_literal_dollars(replacement)
return str_gsub(replacement, "%$%$", "\\$")
end


local function restore_literal_dollars(replacement)
return str_gsub(replacement, "\\%$", "$")
end

core.ctx.register_var("proxy_rewrite_regex_uri_captures", function(ctx)
return ctx.proxy_rewrite_regex_uri_captures
end)
Expand Down Expand Up @@ -225,6 +247,16 @@ function _M.check_schema(conf)
if not secret.is_secret_ref(pattern) then
local test_replacement = secret.is_secret_ref(replacement)
and "" or replacement
if test_replacement ~= "" then
-- Keep validating PCRE captures without treating NGINX
-- variables in the replacement as named captures.
local err
test_replacement, err = escape_nginx_vars(test_replacement)
if err then
return false, "invalid regex_uri replacement(" ..
replacement .. "): " .. err
end
end
Comment on lines +250 to +259

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this needed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is needed because check_schema compiles the replacement with ngx.re.sub to validate both the regex pattern and replacement syntax.

Without escaping NGINX variables, $arg_name is interpreted as a named PCRE capture and route creation fails with "failed to compile the replacement template", which is the original issue.

Escaping only NGINX variable markers as $$ allows PCRE to keep validating regular captures such as $1 and invalid replacement syntax, while treating $arg_name as a literal during schema validation. The existing invalid replacement test also depends on this validation.

I can add a comment here to explain this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added an inline comment in d1b362b explaining that schema validation must keep validating PCRE captures without treating NGINX variables as named captures.

local _, _, err = re_sub("/fake_uri", pattern,
test_replacement, "jo")
if err then
Expand Down Expand Up @@ -355,8 +387,14 @@ function _M.rewrite(conf, ctx)
if captures then
ctx.proxy_rewrite_regex_uri_captures = captures

local replacement = preserve_literal_dollars(conf.regex_uri[i + 1])
replacement = core.utils.resolve_var_with_captures(replacement, captures)
replacement = core.utils.resolve_var(replacement, ctx.var, escape_separator)
replacement = restore_literal_dollars(replacement)
local uri, _, err = re_sub(upstream_uri,
conf.regex_uri[i], conf.regex_uri[i + 1], "jo")
conf.regex_uri[i], function()
return replacement
end, "jo")
Comment on lines +390 to +397

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's too complicated. Is there a simpler way?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The additional steps preserve three different replacement semantics:

  1. $1 and $2 are regex captures.
  2. $arg_name and values from ctx.var are NGINX variables.
  3. $$x must remain the literal string $x.

Resolving NGINX variables directly would also consume $1, while letting ngx.re.sub process the replacement after variable resolution could interpret dollar signs from resolved values again.

I also tested resolving variables after ngx.re.sub, but at that point an escaped NGINX variable and an existing literal $$x both become $..., so they can no longer be distinguished.

I can move these stages into a focused helper to make the rewrite path easier to read. Would that address the concern, or would you prefer a different replacement behavior for literal $$?

if uri then
upstream_uri = uri
else
Expand Down
2 changes: 1 addition & 1 deletion docs/en/latest/plugins/proxy-rewrite.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ The `proxy-rewrite` Plugin offers options to rewrite requests that APISIX forwar
|-----------------------------|---------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| uri | string | False | | | New Upstream URI path. Value supports [NGINX variables](https://nginx.org/en/docs/http/ngx_http_core_module.html). For example, `$arg_name`. |
| method | string | False | | ["GET", "POST", "PUT", "HEAD", "DELETE", "OPTIONS", "MKCOL", "COPY", "MOVE", "PROPFIND", "LOCK", "UNLOCK", "PATCH", "TRACE"] | HTTP method to rewrite requests to use. |
| regex_uri | array[string] | False | | | Regular expressions used to match the URI path from client requests and compose a new Upstream URI path. When both `uri` and `regex_uri` are configured, `uri` has a higher priority. The array should contain one or more **key-value pairs**, with the key being the regular expression to match URI against and value being the new Upstream URI path. For example, with `["^/iresty/(. *)/(. *)", "/$1-$2", ^/theothers/*", "/theothers"]`, if a request is originally sent to `/iresty/hello/world`, the Plugin will rewrite the Upstream URI path to `/iresty/hello-world`; if a request is originally sent to `/theothers/hello/world`, the Plugin will rewrite the Upstream URI path to `/theothers`. |
| regex_uri | array[string] | False | | | Regular expressions used to match the URI path from client requests and compose a new Upstream URI path. The replacement supports both regular expression captures such as `$1` and [NGINX variables](https://nginx.org/en/docs/http/ngx_http_core_module.html) such as `$arg_name`. When both `uri` and `regex_uri` are configured, `uri` has a higher priority. The array should contain one or more **key-value pairs**, with the key being the regular expression to match URI against and value being the new Upstream URI path. For example, with `["^/iresty/(. *)/(. *)", "/$1-$2", ^/theothers/*", "/theothers"]`, if a request is originally sent to `/iresty/hello/world`, the Plugin will rewrite the Upstream URI path to `/iresty/hello-world`; if a request is originally sent to `/theothers/hello/world`, the Plugin will rewrite the Upstream URI path to `/theothers`. |
| host | string | False | | | Set [`Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) request header. |
| headers | object | False | | | Header actions to be executed. Can be set to objects of action verbs `add`, `remove`, and/or `set`; or an object consisting of headers to be `set`. When multiple action verbs are configured, actions are executed in the order of `add`, `remove`, and `set`. |
| headers.add | object | False | | | Headers to append to requests. If a header already present in the request, the header value will be appended. Header value could be set to a constant, one or more [NGINX variables](https://nginx.org/en/docs/http/ngx_http_core_module.html), or the matched result of `regex_uri` using variables such as `$1-$2-$3`. A value could also be an array of such values (e.g. `["val1", "val2"]`) to append the header multiple times, resulting in multiple headers with the same name. |
Expand Down
2 changes: 1 addition & 1 deletion docs/zh/latest/plugins/proxy-rewrite.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ description: proxy-rewrite 插件支持重写 APISIX 转发到上游服务的请
|-----------------------------|-----------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| uri | string | 否 | | | 新的上游 URI 路径。值支持 [NGINX 变量](https://nginx.org/en/docs/http/ngx_http_core_module.html)。例如,`$arg_name`。 |
| method | string | 否 | | ["GET", "POST", "PUT", "HEAD", "DELETE", "OPTIONS", "MKCOL", "COPY", "MOVE", "PROPFIND", "LOCK", "UNLOCK", "PATCH", "TRACE"] | 要使用的重写请求的 HTTP 方法。 |
| regex_uri | array[string] | 否 | | | 用于匹配客户端请求的 URI 路径并组成新的上游 URI 路径的正则表达式。当同时配置 `uri` 和 `regex_uri` 时,`uri` 具有更高的优先级。该数组应包含一个或多个 **键值对**,其中键是用于匹配 URI 的正则表达式,值是新的上游 URI 路径。例如,对于 `["^/iresty/(. *)/(. *)", "/$1-$2", ^/theothers/*", "/theothers"]`,如果请求最初发送到 `/iresty/hello/world`,插件会将上游 URI 路径重写为 `/iresty/hello-world`;如果请求最初发送到 `/theothers/hello/world`,插件会将上游 URI 路径重写为 `/theothers`。|
| regex_uri | array[string] | 否 | | | 用于匹配客户端请求的 URI 路径并组成新的上游 URI 路径的正则表达式。替换字符串同时支持 `$1` 等正则捕获和 `$arg_name` 等 [NGINX 变量](https://nginx.org/en/docs/http/ngx_http_core_module.html)。当同时配置 `uri` 和 `regex_uri` 时,`uri` 具有更高的优先级。该数组应包含一个或多个 **键值对**,其中键是用于匹配 URI 的正则表达式,值是新的上游 URI 路径。例如,对于 `["^/iresty/(. *)/(. *)", "/$1-$2", ^/theothers/*", "/theothers"]`,如果请求最初发送到 `/iresty/hello/world`,插件会将上游 URI 路径重写为 `/iresty/hello-world`;如果请求最初发送到 `/theothers/hello/world`,插件会将上游 URI 路径重写为 `/theothers`。|
| host | string | 否 | | | 设置 [`Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) 请求标头。|
| headers | object | 否 | | | 要执行的标头操作。可以设置为动作动词 `add`、`remove` 和/或 `set` 的对象;或由要 `set` 的标头组成的对象。当配置了多个动作动词时,动作将按照“添加”、“删除”和“设置”的顺序执行。|
| headers.add | object | 否 | | | 要附加到请求的标头。如果请求中已经存在标头,则会附加标头值。标头值可以设置为常量、一个或多个 [NGINX 变量](https://nginx.org/en/docs/http/ngx_http_core_module.html),或者 `regex_uri` 的匹配结果(使用变量,例如 `$1-$2-$3`)。标头值也可以是上述值的数组(例如 `["val1", "val2"]`),从而多次附加该标头,生成多个同名标头。|
Expand Down
82 changes: 82 additions & 0 deletions t/plugin/proxy-rewrite4.t
Original file line number Diff line number Diff line change
@@ -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.
#
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: set route(regex_uri with capture and NGINX variable)
--- 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": {
"proxy-rewrite": {
"regex_uri": ["^/api/(.*)$",
"/plugin_proxy_rewrite_args?c=$1&n=$arg_name&l=$$x"]
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/api/*"
}]]
)

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



=== TEST 2: hit route(regex_uri resolves capture and NGINX variable)
--- request
GET /api/team?name=alice HTTP/1.1
--- response_body
uri: /plugin_proxy_rewrite_args
c: team
l: $x
n: alice
name: alice
Loading