diff --git a/apisix/plugin.lua b/apisix/plugin.lua index b3dadcb49588..342bd9680e47 100644 --- a/apisix/plugin.lua +++ b/apisix/plugin.lua @@ -34,6 +34,9 @@ local type = type local local_plugins = core.table.new(32, 0) local tostring = tostring local error = error +-- make linter happy to avoid error: getting the Lua global "load" +-- luacheck: globals load, ignore lua_load +local lua_load = load local is_http = ngx.config.subsystem == "http" local local_plugins_hash = core.table.new(0, 32) local stream_local_plugins = core.table.new(32, 0) @@ -49,6 +52,9 @@ local merged_stream_route = core.lrucache.new({ local expr_lrucache = core.lrucache.new({ ttl = 300, count = 512 }) +local meta_pre_func_load_lrucache = core.lrucache.new({ + ttl = 300, count = 512 +}) local local_conf local check_plugin_metadata @@ -906,10 +912,23 @@ local function check_single_plugin_schema(name, plugin_conf, schema_type, skip_d .. name .. " err: " .. err end - if plugin_conf._meta and plugin_conf._meta.filter then - ok, err = expr.new(plugin_conf._meta.filter) - if not ok then - return nil, "failed to validate the 'vars' expression: " .. err + if plugin_conf._meta then + if plugin_conf._meta.filter then + ok, err = expr.new(plugin_conf._meta.filter) + if not ok then + return nil, "failed to validate the 'vars' expression: " .. err + end + end + + if plugin_conf._meta.pre_function then + local pre_function, err = meta_pre_func_load_lrucache(plugin_conf._meta.pre_function + , "", + lua_load, + plugin_conf._meta.pre_function, "meta pre_function") + if not pre_function then + return nil, "failed to load _meta.pre_function in plugin " .. name .. ": " + .. err + end end end end @@ -1130,6 +1149,17 @@ function _M.stream_plugin_checker(item, in_cp) return true end +local function run_meta_pre_function(conf, api_ctx, name) + if conf._meta and conf._meta.pre_function then + local _, pre_function = pcall(meta_pre_func_load_lrucache(conf._meta.pre_function, "", + lua_load, + conf._meta.pre_function, "meta pre_function")) + local ok, err = pcall(pre_function, conf, api_ctx) + if not ok then + core.log.error("pre_function execution for plugin ", name, " failed: ", err) + end + end +end function _M.run_plugin(phase, plugins, api_ctx) local plugin_run = false @@ -1169,6 +1199,7 @@ function _M.run_plugin(phase, plugins, api_ctx) goto CONTINUE end + run_meta_pre_function(conf, api_ctx, plugins[i]["name"]) plugin_run = true api_ctx._plugin_name = plugins[i]["name"] local code, body = phase_func(conf, api_ctx) @@ -1207,6 +1238,7 @@ function _M.run_plugin(phase, plugins, api_ctx) local conf = plugins[i + 1] if phase_func and meta_filter(api_ctx, plugins[i]["name"], conf) then plugin_run = true + run_meta_pre_function(conf, api_ctx, plugins[i]["name"]) api_ctx._plugin_name = plugins[i]["name"] phase_func(conf, api_ctx) api_ctx._plugin_name = nil diff --git a/apisix/plugins/example-plugin.lua b/apisix/plugins/example-plugin.lua index 16086ddcd38e..767ccfae72a9 100644 --- a/apisix/plugins/example-plugin.lua +++ b/apisix/plugins/example-plugin.lua @@ -107,6 +107,10 @@ function _M.access(conf, ctx) return end +function _M.header_filter(conf, ctx) + core.log.warn("plugin header_filter phase, conf: ", core.json.encode(conf)) +end + function _M.body_filter(conf, ctx) core.log.warn("plugin body_filter phase, eof: ", ngx.arg[2], @@ -119,6 +123,10 @@ function _M.delayed_body_filter(conf, ctx) ", conf: ", core.json.encode(conf)) end +function _M.log(conf, ctx) + core.log.warn("plugin log phase, conf: ", core.json.encode(conf)) +end + local function hello() local args = ngx.req.get_uri_args() diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua index b4241ff2d9f5..a19f3fac7235 100644 --- a/apisix/schema_def.lua +++ b/apisix/schema_def.lua @@ -1027,8 +1027,15 @@ _M.plugin_injected_schema = { description = "filter determines whether the plugin ".. "needs to be executed at runtime", type = "array", - } - } + }, + pre_function = { + description = "function to be executed in each phase " .. + "before execution of plugins. The pre_function will have access " .. + "to two arguments: `conf` and `ctx`.", + type = "string", + }, + }, + additionalProperties = false, } } diff --git a/t/admin/plugins.t b/t/admin/plugins.t index 713d59d4cf41..6c574c2a4673 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -281,7 +281,7 @@ plugins: } } --- response_body eval -qr/\{"metadata_schema":\{"properties":\{"ikey":\{"minimum":0,"type":"number"\},"skey":\{"type":"string"\}\},"required":\["ikey","skey"\],"type":"object"\},"priority":0,"schema":\{"\$comment":"this is a mark for our injected plugin schema","properties":\{"_meta":\{"properties":\{"disable":\{"type":"boolean"\},"error_response":\{"oneOf":\[\{"type":"string"\},\{"type":"object"\}\]\},"filter":\{"description":"filter determines whether the plugin needs to be executed at runtime","type":"array"\},"priority":\{"description":"priority of plugins by customized order","type":"integer"\}\},"type":"object"\},"i":\{"minimum":0,"type":"number"\},"ip":\{"type":"string"\},"port":\{"type":"integer"\},"s":\{"type":"string"\},"t":\{"minItems":1,"type":"array"\}\},"required":\["i"\],"type":"object"\},"version":0.1\}/ +qr/\{"metadata_schema":\{"properties":\{"ikey":\{"minimum":0,"type":"number"\},"skey":\{"type":"string"\}\},"required":\["ikey","skey"\],"type":"object"\},"priority":0,"schema":\{"\$comment":"this is a mark for our injected plugin schema","properties":\{"_meta":\{"additionalProperties":false,"properties":\{"disable":\{"type":"boolean"\},"error_response":\{"oneOf":\[\{"type":"string"\},\{"type":"object"\}\]\},"filter":\{"description":"filter determines whether the plugin needs to be executed at runtime","type":"array"\},"pre_function":\{"description":"function to be executed in each phase before execution of plugins. The pre_function will have access to two arguments: `conf` and `ctx`.","type":"string"\},"priority":\{"description":"priority of plugins by customized order","type":"integer"\}\},"type":"object"\},"i":\{"minimum":0,"type":"number"\},"ip":\{"type":"string"\},"port":\{"type":"integer"\},"s":\{"type":"string"\},"t":\{"minItems":1,"type":"array"\}\},"required":\["i"\],"type":"object"\},"version":0.1\}/ @@ -382,7 +382,7 @@ qr/\{"encrypt_fields":\["password"\],"properties":\{"password":\{"type":"string" } } --- response_body -{"priority":1003,"schema":{"$comment":"this is a mark for our injected plugin schema","properties":{"_meta":{"properties":{"disable":{"type":"boolean"},"error_response":{"oneOf":[{"type":"string"},{"type":"object"}]},"filter":{"description":"filter determines whether the plugin needs to be executed at runtime","type":"array"},"priority":{"description":"priority of plugins by customized order","type":"integer"}},"type":"object"},"burst":{"minimum":0,"type":"integer"},"conn":{"exclusiveMinimum":0,"type":"integer"},"default_conn_delay":{"exclusiveMinimum":0,"type":"number"},"key":{"type":"string"},"key_type":{"default":"var","enum":["var","var_combination"],"type":"string"},"only_use_default_delay":{"default":false,"type":"boolean"}},"required":["conn","burst","default_conn_delay","key"],"type":"object"},"version":0.1} +{"priority":1003,"schema":{"$comment":"this is a mark for our injected plugin schema","properties":{"_meta":{"additionalProperties":false,"properties":{"disable":{"type":"boolean"},"error_response":{"oneOf":[{"type":"string"},{"type":"object"}]},"filter":{"description":"filter determines whether the plugin needs to be executed at runtime","type":"array"},"pre_function":{"description":"function to be executed in each phase before execution of plugins. The pre_function will have access to two arguments: `conf` and `ctx`.","type":"string"},"priority":{"description":"priority of plugins by customized order","type":"integer"}},"type":"object"},"burst":{"minimum":0,"type":"integer"},"conn":{"exclusiveMinimum":0,"type":"integer"},"default_conn_delay":{"exclusiveMinimum":0,"type":"number"},"key":{"type":"string"},"key_type":{"default":"var","enum":["var","var_combination"],"type":"string"},"only_use_default_delay":{"default":false,"type":"boolean"}},"required":["conn","burst","default_conn_delay","key"],"type":"object"},"version":0.1} diff --git a/t/misc/pre-function.t b/t/misc/pre-function.t new file mode 100644 index 000000000000..316c93ccbd8c --- /dev/null +++ b/t/misc/pre-function.t @@ -0,0 +1,325 @@ +# +# 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. +# +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_root_location(); +log_level("info"); + +$ENV{TEST_NGINX_HTML_DIR} ||= html_dir(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: invalid pre_function +--- 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": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr", + "_meta": { + "pre_function": "not a function" + } + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.print(body) + } + } +--- error_code: 400 +--- response_body +{"error_msg":"failed to load _meta.pre_function in plugin limit-count: [string \"meta pre_function\"]:1: unexpected symbol near 'not'"} + + + +=== TEST 2: attempt setting pre_function in _meta with a typo in `pre_function` +# this is to test the case where user (or CP) would attempt configuring pre_function +# using incorrect field name, this validation is achieved by setting `additionalProperties = false` +# in schema_def.lua +--- 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": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr", + "_meta": { + "prefunction": "not a function" + } + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.print(body) + } + } +--- error_code: 400 +--- response_body +{"error_msg":"failed to check the configuration of plugin limit-count err: property \"_meta\" validation failed: additional properties forbidden, found prefunction"} + + + +=== TEST 3: pre_function with error in code +--- 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": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr", + "_meta": { + "pre_function": "return function() print(invalid.index) end" + } + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- error_code: 200 +--- response_body +passed + + + +=== TEST 4: sending request will execute erroneous code and print error log +--- request +GET /hello +--- error_log +pre_function execution for plugin limit-count failed: [string "meta pre_function"]:1: attempt to index global 'invalid' (a nil value), + + + +=== TEST 5: test pre_function sanity: correct function +--- 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": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr", + "_meta": { + "pre_function": "return function(conf, ctx) ngx.log(ngx.WARN, 'hello ', ngx.req.get_headers()[\"User-Agent\"]) end" + } + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- error_code: 200 +--- response_body +passed + + + +=== TEST 6: request +--- request +GET /hello +--- more_headers +User-Agent: test-nginx +--- error_log +hello test-nginx + + + +=== TEST 7: pre_function is executed in all phases +--- 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": { + "example-plugin": { + "i": 11, + "_meta": { + "pre_function": "return function(conf, ctx) ngx.log(ngx.WARN, 'hello: ', ngx.get_phase()) end" + } + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- error_code: 200 +--- response_body +passed + + + +=== TEST 8: request +--- request +GET /hello +--- error_log +hello: access +hello: header_filter +hello: body_filter +hello: log + + + +=== TEST 9: test pre-function with proxy-rewrite, (rewrite phase) +--- 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": { + "uri": "/uri", + "headers": { + "x-api": "$example_var_name" + }, + "_meta": { + "pre_function": "return function(conf, ctx) local core = require \"apisix.core\" core.ctx.register_var(\"example_var_name\", function(ctx) return \"example_var_value\" end) end" + } + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 10: hit route(header supports nginx variables) +--- request +GET /hello +--- response_body +uri: /uri +host: localhost +x-api: example_var_value +x-real-ip: 127.0.0.1 diff --git a/utils/lj-releng b/utils/lj-releng index b2c5040d0f09..182738241a0b 100755 --- a/utils/lj-releng +++ b/utils/lj-releng @@ -1,4 +1,5 @@ #!/usr/bin/env perl + # Copyright (c) 2011-2017, Yichun "agentzh" Zhang (章亦春) agentzh@gmail.com, OpenResty Inc. # This module is licensed under the terms of the BSD license.