From da853702d68b4b9a42dbdac7c59f4d7d5cb213a7 Mon Sep 17 00:00:00 2001 From: YuanSheng Wang Date: Thu, 31 Oct 2019 10:19:22 +0800 Subject: [PATCH] feature: supported `redirect` plugin. (#732) --- conf/config.yaml | 1 + doc/README.md | 1 + doc/README_CN.md | 1 + doc/plugins/redirect.md | 100 ++++++++++ lua/apisix/plugins/redirect.lua | 119 ++++++++++++ t/admin/plugins.t | 2 +- t/debug/debug-mode.t | 1 + t/plugin/redirect.t | 332 ++++++++++++++++++++++++++++++++ 8 files changed, 556 insertions(+), 1 deletion(-) create mode 100644 doc/plugins/redirect.md create mode 100644 lua/apisix/plugins/redirect.lua create mode 100644 t/plugin/redirect.t diff --git a/conf/config.yaml b/conf/config.yaml index a7d70849..68cf4e65 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -87,6 +87,7 @@ plugins: # plugin list - serverless-post-function - openid-connect - proxy-rewrite + - redirect stream_plugins: - mqtt-proxy diff --git a/doc/README.md b/doc/README.md index 632dc136..56591f06 100644 --- a/doc/README.md +++ b/doc/README.md @@ -53,3 +53,4 @@ Plugins * [serverless](plugins/serverless-cn.md):AllowS to dynamically run Lua code at *different* phase in APISIX. * [ip-restriction](plugins/ip-restriction.md): IP whitelist/blacklist. * openid-connect +* [redirect](plugins/redirect.md): URI redirect. diff --git a/doc/README_CN.md b/doc/README_CN.md index 66360ed4..20e69ef1 100644 --- a/doc/README_CN.md +++ b/doc/README_CN.md @@ -54,3 +54,4 @@ Reference document * [serverless](plugins/serverless-cn.md):允许在 APISIX 中的不同阶段动态运行 Lua 代码。 * [ip-restriction](plugins/ip-restriction-cn.md): IP 黑白名单。 * openid-connect +* [redirect](plugins/redirect-cn.md): URI 重定向。 diff --git a/doc/plugins/redirect.md b/doc/plugins/redirect.md new file mode 100644 index 00000000..7b3f734e --- /dev/null +++ b/doc/plugins/redirect.md @@ -0,0 +1,100 @@ +[中文](redirect-cn.md) + +# redirect + +URI redirect. + +### Parameters + +|Name |Required|Description| +|------- |-----|------| +|uri |required| New uri which can contain Ningx variable, eg: `/test/index.html`, `$uri/index.html`. You can refer to variables in a way similar to `${xxx}` to avoid ambiguity, eg: `${uri}foo/index.html`. If you just need the original `$` character, add `\` in front of it, like this one: `/\$foo/index.html`. If you refer to a variable name that does not exist, this will not produce an error, and it will be used as an empty string.| +|ret_code|option|Response code, the default value is `302`.| + +### Example + +#### Enable Plugin + +Here's a mini example, enable the `redirect` plugin on the specified route: + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d ' +{ + "uri": "/test/index.html", + "plugins": { + "redirect": { + "uri": "/test/default.html", + "ret_code": 301 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:80": 1 + } + } +}' +``` + +And we can use any Nginx built-in variable in the new URI. + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d ' +{ + "uri": "/test", + "plugins": { + "redirect": { + "uri": "$uri/index.html", + "ret_code": 301 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:80": 1 + } + } +}' +``` + +#### Test Plugin + +Testing based on the above examples : + +```shell +$ curl http://127.0.0.1:9080/test/index.html -i +HTTP/1.1 301 Moved Permanently +Date: Wed, 23 Oct 2019 13:48:23 GMT +Content-Type: text/html +Content-Length: 166 +Connection: keep-alive +Location: /test/default.html + +... +``` + +We can check the response code and the response header `Location`. + +It shows that the `redirect` plugin is in effect. + +#### Disable Plugin + +When you want to disable the `redirect` plugin, it is very simple, + you can delete the corresponding json configuration in the plugin configuration, + no need to restart the service, it will take effect immediately : + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d ' +{ + "uri": "/test/index.html", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:80": 1 + } + } +}' +``` + +The `redirect` plugin has been disabled now. It works for other plugins. diff --git a/lua/apisix/plugins/redirect.lua b/lua/apisix/plugins/redirect.lua new file mode 100644 index 00000000..89eec658 --- /dev/null +++ b/lua/apisix/plugins/redirect.lua @@ -0,0 +1,119 @@ +local core = require("apisix.core") +local tab_insert = table.insert +local tab_concat = table.concat +local re_gmatch = ngx.re.gmatch +local ipairs = ipairs + +local lrucache = core.lrucache.new({ + ttl = 300, count = 100 +}) + + +local schema = { + type = "object", + properties = { + ret_code = {type = "integer", minimum = 200, default = 302}, + uri = {type = "string", minLength = 2}, + }, + required = {"uri"}, +} + + +local plugin_name = "rewrite" + +local _M = { + version = 0.1, + priority = 900, + name = plugin_name, + schema = schema, +} + + +local function parse_uri(uri) + + local reg = [[ (\\\$[0-9a-zA-Z_]+) | ]] -- \$host + .. [[ \$\{([0-9a-zA-Z_]+)\} | ]] -- ${host} + .. [[ \$([0-9a-zA-Z_]+) | ]] -- $host + .. [[ (\$|[^$\\]+) ]] -- $ or others + local iterator, err = re_gmatch(uri, reg, "jiox") + if not iterator then + return nil, err + end + + local t = {} + while true do + local m, err = iterator() + if err then + return nil, err + end + + if not m then + break + end + + tab_insert(t, m) + end + + return t +end + + +function _M.check_schema(conf) + local ok, err = core.schema.check(schema, conf) + if not ok then + return false, err + end + + local uri_segs, err = parse_uri(conf.uri) + if not uri_segs then + return false, err + end + core.log.info(core.json.delay_encode(uri_segs)) + + return true +end + + + local tmp = {} +local function concat_new_uri(uri, ctx) + local pased_uri_segs, err = lrucache(uri, nil, parse_uri, uri) + if not pased_uri_segs then + return nil, err + end + + core.table.clear(tmp) + + for _, uri_segs in ipairs(pased_uri_segs) do + local pat1 = uri_segs[1] -- \$host + local pat2 = uri_segs[2] -- ${host} + local pat3 = uri_segs[3] -- $host + local pat4 = uri_segs[4] -- $ or others + core.log.info("parsed uri segs: ", core.json.delay_encode(uri_segs)) + + if pat2 or pat3 then + tab_insert(tmp, ctx.var[pat2 or pat3]) + else + tab_insert(tmp, pat1 or pat4) + end + end + + return tab_concat(tmp, "") +end + + +function _M.rewrite(conf, ctx) + core.log.info("plugin rewrite phase, conf: ", core.json.delay_encode(conf)) + + local new_uri, err = concat_new_uri(conf.uri, ctx) + if not new_uri then + core.log.error("failed to generate new uri by: ", conf.uri, " error: ", + err) + core.response.exit(500) + end + + core.response.set_header("Location", new_uri) + core.response.exit(conf.ret_code) +end + + +return _M diff --git a/t/admin/plugins.t b/t/admin/plugins.t index 6d458dcc..9044d4ba 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -30,6 +30,6 @@ __DATA__ --- request GET /apisix/admin/plugins/list --- response_body_like eval -qr/\["limit-req","limit-count","limit-conn","key-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite"\]/ +qr/\["limit-req","limit-count","limit-conn","key-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect"\]/ --- no_error_log [error] diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t index 49d6b8e3..42b3b1ab 100644 --- a/t/debug/debug-mode.t +++ b/t/debug/debug-mode.t @@ -65,6 +65,7 @@ loaded plugin and sort by priority: 1003 name: limit-conn loaded plugin and sort by priority: 1002 name: limit-count loaded plugin and sort by priority: 1001 name: limit-req loaded plugin and sort by priority: 1000 name: node-status +loaded plugin and sort by priority: 900 name: redirect loaded plugin and sort by priority: 506 name: grpc-transcode loaded plugin and sort by priority: 500 name: prometheus loaded plugin and sort by priority: 0 name: example-plugin diff --git a/t/plugin/redirect.t b/t/plugin/redirect.t new file mode 100644 index 00000000..6edd2158 --- /dev/null +++ b/t/plugin/redirect.t @@ -0,0 +1,332 @@ +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(); +log_level('info'); +run_tests; + +__DATA__ + +=== TEST 1: sanity +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.redirect") + local ok, err = plugin.check_schema({ + ret_code = 302, + uri = '/foo', + }) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- request +GET /t +--- response_body +done +--- no_error_log +[error] + + + +=== TEST 2: default ret_code +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.redirect") + local ok, err = plugin.check_schema({ + -- ret_code = 302, + uri = '/foo', + }) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- request +GET /t +--- response_body +done +--- no_error_log +[error] + + + +=== TEST 3: add plugin with new uri: /test/add +--- 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": { + "redirect": { + "uri": "/test/add", + "ret_code": 301 + } + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 4: redirect +--- request +GET /hello +--- response_headers +Location: /test/add +--- error_code: 301 +--- no_error_log +[error] + + + +=== TEST 5: add plugin with new uri: $uri/test/add +--- 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": { + "redirect": { + "uri": "$uri/test/add", + "ret_code": 301 + } + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 6: redirect +--- request +GET /hello +--- response_headers +Location: /hello/test/add +--- error_code: 301 +--- no_error_log +[error] + + + +=== TEST 7: add plugin with new uri: $uri/test/a${arg_name}c +--- 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": { + "redirect": { + "uri": "$uri/test/a${arg_name}c", + "ret_code": 302 + } + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 8: redirect +--- request +GET /hello?name=json +--- response_headers +Location: /hello/test/ajsonc +--- error_code: 302 +--- no_error_log +[error] + + + +=== TEST 9: add plugin with new uri: /foo$$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, + [[{ + "plugins": { + "redirect": { + "uri": "/foo$$uri", + "ret_code": 302 + } + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 10: redirect +--- request +GET /hello?name=json +--- response_headers +Location: /foo$/hello +--- error_code: 302 +--- no_error_log +[error] + + + +=== TEST 11: add plugin with new uri: \\$uri/foo$uri\\$uri/bar +--- 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": { + "redirect": { + "uri": "\\$uri/foo$uri\\$uri/bar", + "ret_code": 301 + } + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 12: redirect +--- request +GET /hello +--- response_headers +Location: \$uri/foo/hello\$uri/bar +--- error_code: 301 +--- no_error_log +[error] + + + +=== TEST 13: add plugin with new uri: $uri/$bad_var/bar +--- 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": { + "redirect": { + "uri": "$uri/$bad_var/bar", + "ret_code": 301 + } + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 14: redirect +--- request +GET /hello +--- response_headers +Location: /hello//bar +--- error_code: 301 +--- no_error_log +[error]