From 01b4b49eb2ba642b337f7a1fbe1894a77942910b Mon Sep 17 00:00:00 2001 From: Joanthan Chen Date: Tue, 22 Nov 2022 14:36:13 +0800 Subject: [PATCH] feat: proxy-rewrite support config add set and remove header (#8336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 罗泽轩 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 8fb947305538..173fafc56928 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 7b9a99f0b872..fdb8c81840d0 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 49b915cc4055..b4c14d7d5a99 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, `[" ^/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: @@ -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 2b1d0e2a4a2a..765b5f5678fa 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 7ddedefd1a95..621c06557c56 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