diff --git a/apisix/admin/init.lua b/apisix/admin/init.lua index b5a20afa1211..12f63f44a922 100644 --- a/apisix/admin/init.lua +++ b/apisix/admin/init.lua @@ -16,7 +16,7 @@ -- local require = require local core = require("apisix.core") -local route = require("resty.radixtree") +local route = require("apisix.utils.router") local plugin = require("apisix.plugin") local ngx = ngx local get_method = ngx.req.get_method diff --git a/apisix/api_router.lua b/apisix/api_router.lua index 3f1cebc46326..f382258dc974 100644 --- a/apisix/api_router.lua +++ b/apisix/api_router.lua @@ -15,7 +15,7 @@ -- limitations under the License. -- local require = require -local router = require("resty.radixtree") +local router = require("apisix.utils.router") local plugin_mod = require("apisix.plugin") local ip_restriction = require("apisix.plugins.ip-restriction") local core = require("apisix.core") diff --git a/apisix/control/router.lua b/apisix/control/router.lua index 86ea7694b745..821cee864874 100644 --- a/apisix/control/router.lua +++ b/apisix/control/router.lua @@ -15,7 +15,7 @@ -- limitations under the License. -- local require = require -local router = require("resty.radixtree") +local router = require("apisix.utils.router") local builtin_v1_routes = require("apisix.control.v1") local plugin_mod = require("apisix.plugin") local core = require("apisix.core") diff --git a/apisix/http/router/base.lua b/apisix/http/router/base.lua new file mode 100644 index 000000000000..1cef301116e3 --- /dev/null +++ b/apisix/http/router/base.lua @@ -0,0 +1,118 @@ +-- +-- 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. +-- +local require = require +local radixtree = require("resty.radixtree") +local router = require("apisix.utils.router") +local core = require("apisix.core") +local http_route = require("apisix.http.route") +local ipairs = ipairs +local type = type +local error = error +local loadstring = loadstring + + +local _M = {} + + +function _M.create_radixtree_uri_router(routes, uri_routes, with_parameter) + routes = routes or {} + + core.table.clear(uri_routes) + + for _, route in ipairs(routes) do + if type(route) == "table" then + local status = core.table.try_read_attr(route, "value", "status") + -- check the status + if status and status == 0 then + goto CONTINUE + end + + local filter_fun, err + if route.value.filter_func then + filter_fun, err = loadstring( + "return " .. route.value.filter_func, + "router#" .. route.value.id) + if not filter_fun then + core.log.error("failed to load filter function: ", err, + " route id: ", route.value.id) + goto CONTINUE + end + + filter_fun = filter_fun() + end + + core.log.info("insert uri route: ", + core.json.delay_encode(route.value)) + core.table.insert(uri_routes, { + paths = route.value.uris or route.value.uri, + methods = route.value.methods, + priority = route.value.priority, + hosts = route.value.hosts or route.value.host, + remote_addrs = route.value.remote_addrs + or route.value.remote_addr, + vars = route.value.vars, + filter_fun = filter_fun, + handler = function (api_ctx, match_opts) + api_ctx.matched_params = nil + api_ctx.matched_route = route + api_ctx.curr_req_matched = match_opts.matched + end + }) + + ::CONTINUE:: + end + end + + core.log.info("route items: ", core.json.delay_encode(uri_routes, true)) + + if with_parameter then + return radixtree.new(uri_routes) + else + return router.new(uri_routes) + end +end + + +function _M.match_uri(uri_router, match_opts, api_ctx) + core.table.clear(match_opts) + match_opts.method = api_ctx.var.request_method + match_opts.host = api_ctx.var.host + match_opts.remote_addr = api_ctx.var.remote_addr + match_opts.vars = api_ctx.var + match_opts.matched = core.tablepool.fetch("matched_route_record", 0, 4) + + local ok = uri_router:dispatch(api_ctx.var.uri, match_opts, api_ctx, match_opts) + return ok +end + + +function _M.init_worker(filter) + local user_routes, err = core.config.new("/routes", { + automatic = true, + item_schema = core.schema.route, + checker = http_route.check_route, + filter = filter, + }) + if not user_routes then + error("failed to create etcd instance for fetching /routes : " .. err) + end + + return user_routes +end + + +return _M diff --git a/apisix/http/router/radixtree_host_uri.lua b/apisix/http/router/radixtree_host_uri.lua index ad2b31e93192..08e0655c7bee 100644 --- a/apisix/http/router/radixtree_host_uri.lua +++ b/apisix/http/router/radixtree_host_uri.lua @@ -15,16 +15,13 @@ -- limitations under the License. -- local require = require -local router = require("resty.radixtree") +local router = require("apisix.utils.router") local core = require("apisix.core") -local http_route = require("apisix.http.route") local ipairs = ipairs local type = type -local error = error local tab_insert = table.insert local loadstring = loadstring local pairs = pairs -local user_routes local cached_version local host_router local only_uri_router @@ -126,6 +123,7 @@ end local match_opts = {} function _M.match(api_ctx) + local user_routes = _M.user_routes if not cached_version or cached_version ~= user_routes.conf_version then create_radixtree_router(user_routes.values) cached_version = user_routes.conf_version @@ -151,27 +149,4 @@ function _M.match(api_ctx) end -function _M.routes() - if not user_routes then - return nil, nil - end - - return user_routes.values, user_routes.conf_version -end - - -function _M.init_worker(filter) - local err - user_routes, err = core.config.new("/routes", { - automatic = true, - item_schema = core.schema.route, - checker = http_route.check_route, - filter = filter, - }) - if not user_routes then - error("failed to create etcd instance for fetching /routes : " .. err) - end -end - - return _M diff --git a/apisix/http/router/radixtree_uri.lua b/apisix/http/router/radixtree_uri.lua index 41ceb6841c61..2ed9a894cbdd 100644 --- a/apisix/http/router/radixtree_uri.lua +++ b/apisix/http/router/radixtree_uri.lua @@ -15,14 +15,8 @@ -- limitations under the License. -- local require = require -local router = require("resty.radixtree") local core = require("apisix.core") -local http_route = require("apisix.http.route") -local ipairs = ipairs -local type = type -local error = error -local loadstring = loadstring -local user_routes +local base_router = require("apisix.http.router.base") local cached_version @@ -31,64 +25,12 @@ local _M = {version = 0.2} local uri_routes = {} local uri_router -local function create_radixtree_router(routes) - routes = routes or {} - - core.table.clear(uri_routes) - - for _, route in ipairs(routes) do - if type(route) == "table" then - local status = core.table.try_read_attr(route, "value", "status") - -- check the status - if status and status == 0 then - goto CONTINUE - end - - local filter_fun, err - if route.value.filter_func then - filter_fun, err = loadstring( - "return " .. route.value.filter_func, - "router#" .. route.value.id) - if not filter_fun then - core.log.error("failed to load filter function: ", err, - " route id: ", route.value.id) - goto CONTINUE - end - - filter_fun = filter_fun() - end - - core.log.info("insert uri route: ", - core.json.delay_encode(route.value)) - core.table.insert(uri_routes, { - paths = route.value.uris or route.value.uri, - methods = route.value.methods, - priority = route.value.priority, - hosts = route.value.hosts or route.value.host, - remote_addrs = route.value.remote_addrs - or route.value.remote_addr, - vars = route.value.vars, - filter_fun = filter_fun, - handler = function (api_ctx, match_opts) - api_ctx.matched_params = nil - api_ctx.matched_route = route - api_ctx.curr_req_matched = match_opts.matched - end - }) - - ::CONTINUE:: - end - end - - core.log.info("route items: ", core.json.delay_encode(uri_routes, true)) - uri_router = router.new(uri_routes) -end - - local match_opts = {} function _M.match(api_ctx) + local user_routes = _M.user_routes if not cached_version or cached_version ~= user_routes.conf_version then - create_radixtree_router(user_routes.values) + uri_router = base_router.create_radixtree_uri_router(user_routes.values, + uri_routes, false) cached_version = user_routes.conf_version end @@ -97,38 +39,7 @@ function _M.match(api_ctx) return true end - core.table.clear(match_opts) - match_opts.method = api_ctx.var.request_method - match_opts.host = api_ctx.var.host - match_opts.remote_addr = api_ctx.var.remote_addr - match_opts.vars = api_ctx.var - match_opts.matched = core.tablepool.fetch("matched_route_record", 0, 4) - - local ok = uri_router:dispatch(api_ctx.var.uri, match_opts, api_ctx, match_opts) - return ok -end - - -function _M.routes() - if not user_routes then - return nil, nil - end - - return user_routes.values, user_routes.conf_version -end - - -function _M.init_worker(filter) - local err - user_routes, err = core.config.new("/routes", { - automatic = true, - item_schema = core.schema.route, - checker = http_route.check_route, - filter = filter, - }) - if not user_routes then - error("failed to create etcd instance for fetching /routes : " .. err) - end + return base_router.match_uri(uri_router, match_opts, api_ctx) end diff --git a/apisix/http/router/radixtree_uri_with_parameter.lua b/apisix/http/router/radixtree_uri_with_parameter.lua new file mode 100644 index 000000000000..3fb7610802db --- /dev/null +++ b/apisix/http/router/radixtree_uri_with_parameter.lua @@ -0,0 +1,46 @@ +-- +-- 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. +-- +local require = require +local core = require("apisix.core") +local base_router = require("apisix.http.router.base") +local cached_version + + +local _M = {} + + + local uri_routes = {} + local uri_router + local match_opts = {} +function _M.match(api_ctx) + local user_routes = _M.user_routes + if not cached_version or cached_version ~= user_routes.conf_version then + uri_router = base_router.create_radixtree_uri_router(user_routes.values, + uri_routes, true) + cached_version = user_routes.conf_version + end + + if not uri_router then + core.log.error("failed to fetch valid `uri_with_parameter` router: ") + return true + end + + return base_router.match_uri(uri_router, match_opts, api_ctx) +end + + +return _M diff --git a/apisix/router.lua b/apisix/router.lua index b76b16d98729..c8e1c0cbe751 100644 --- a/apisix/router.lua +++ b/apisix/router.lua @@ -15,6 +15,7 @@ -- limitations under the License. -- local require = require +local base_router = require("apisix.http.router.base") local core = require("apisix.core") local plugin_checker = require("apisix.plugin").plugin_checker local error = error @@ -67,6 +68,27 @@ local function filter(route) end +-- attach common methods if the router doesn't provide its custom implementation +local function attach_http_router_common_methods(http_router) + if http_router.routes == nil then + http_router.routes = function () + if not http_router.user_routes then + return nil, nil + end + + local user_routes = http_router.user_routes + return user_routes.values, user_routes.conf_version + end + end + + if http_router.init_worker == nil then + http_router.init_worker = function (filter) + http_router.user_routes = base_router.init_worker(filter) + end + end +end + + function _M.http_init_worker() local conf = core.config.local_conf() local router_http_name = "radixtree_uri" @@ -78,6 +100,7 @@ function _M.http_init_worker() end local router_http = require("apisix.http.router." .. router_http_name) + attach_http_router_common_methods(router_http) router_http.init_worker(filter) _M.router_http = router_http diff --git a/apisix/ssl/router/radixtree_sni.lua b/apisix/ssl/router/radixtree_sni.lua index 2aa01d2de730..0790627f6de2 100644 --- a/apisix/ssl/router/radixtree_sni.lua +++ b/apisix/ssl/router/radixtree_sni.lua @@ -15,7 +15,7 @@ -- limitations under the License. -- local get_request = require("resty.core.base").get_request -local radixtree_new = require("resty.radixtree").new +local router_new = require("apisix.utils.router").new local core = require("apisix.core") local apisix_ssl = require("apisix.ssl") local ngx_ssl = require("ngx.ssl") @@ -118,7 +118,7 @@ local function create_router(ssl_items) if #route_items > 1 then core.log.info("we have more than 1 ssl certs now") end - local router, err = radixtree_new(route_items) + local router, err = router_new(route_items) if not router then return nil, err end diff --git a/apisix/utils/router.lua b/apisix/utils/router.lua new file mode 100644 index 000000000000..8b6b60405e1e --- /dev/null +++ b/apisix/utils/router.lua @@ -0,0 +1,34 @@ +-- +-- 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. +-- +local resty_router = require("resty.radixtree") + + +local _M = {} + +do + local router_opts = { + no_param_match = true + } + +function _M.new(routes) + return resty_router.new(routes, router_opts) +end + +end + + +return _M diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 1029da4a74b4..7638f048a352 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -88,6 +88,9 @@ apisix: router: http: 'radixtree_uri' # radixtree_uri: match route by uri(base on radixtree) # radixtree_host_uri: match route by host + uri(base on radixtree) + # radixtree_uri_with_parameter: like radixtree_uri but match uri with parameters, + # see https://github.com/api7/lua-resty-radixtree/#parameters-in-path for + # more details. ssl: 'radixtree_sni' # radixtree_sni: match route by SNI(base on radixtree) # stream_proxy: # TCP/UDP proxy # tcp: # TCP proxy port list diff --git a/doc/architecture-design.md b/doc/architecture-design.md index 2166845f40d3..a434d97083c9 100644 --- a/doc/architecture-design.md +++ b/doc/architecture-design.md @@ -461,6 +461,7 @@ Set the route that best suits your business needs in the local configuration `co * `Prefix match`: Use `*` at the end to represent the given `uri` as a prefix match. For example, `/foo*` allows matching `/foo/`, `/foo/a` and `/foo/b`. * `match priority`: first try absolute match, if you can't hit absolute match, try prefix match. * `Any filter attribute`: Allows you to specify any Nginx built-in variable as a filter, such as URL request parameters, request headers, cookies, and so on. + * `radixtree_uri_with_parameter`: Like `radixtree_uri` but also support parameter match. * `radixtree_host_uri`: Use `host + uri` as the primary index (based on the `radixtree` engine), matching both host and URL for the current request. * `apisix.router.ssl`: SSL loads the matching route. diff --git a/doc/router-radixtree.md b/doc/router-radixtree.md index aaeda5d1bffc..648d0fe655d7 100644 --- a/doc/router-radixtree.md +++ b/doc/router-radixtree.md @@ -68,6 +68,26 @@ Here are the rules: |/blog/foo/gloo | `/blog/foo/*` | |/blog/bar | not match | +#### 4. Parameter match + +When `radixtree_uri_with_parameter` is used, we can match routes with parameters. + +For example, with configuration: +```yaml +apisix: + router: + http: 'radixtree_uri_with_parameter' +``` + +route like +``` +/blog/:name +``` + +will match both `/blog/dog` and `/blog/cat`. + +For more details, see https://github.com/api7/lua-resty-radixtree/#parameters-in-path. + ### How to filter route by Nginx builtin variable Please take a look at [radixtree-new](https://github.com/iresty/lua-resty-radixtree#new), diff --git a/doc/zh-cn/architecture-design.md b/doc/zh-cn/architecture-design.md index d867e9bb2abb..cedfe90db5e5 100644 --- a/doc/zh-cn/architecture-design.md +++ b/doc/zh-cn/architecture-design.md @@ -472,6 +472,7 @@ APISIX 区别于其他 API 网关的一大特点是允许用户选择不同 Rout * `前缀匹配`:末尾使用 `*` 代表给定的 `uri` 是前缀匹配。比如 `/foo*`,则允许匹配 `/foo/`、`/foo/a`和`/foo/b`等。 * `匹配优先级`:优先尝试绝对匹配,若无法命中绝对匹配,再尝试前缀匹配。 * `任意过滤属性`:允许指定任何 Nginx 内置变量作为过滤条件,比如 URL 请求参数、请求头、cookie 等。 + * `radixtree_uri_with_parameter`: 同 `radixtree_uri` 但额外有参数匹配的功能。 * `radixtree_host_uri`: 使用 `host + uri` 作为主索引(基于 `radixtree` 引擎),对当前请求会同时匹配 host 和 uri,支持的匹配条件与 `radixtree_uri` 基本一致。 * `apisix.router.ssl`: SSL 加载匹配路由。 diff --git a/rockspec/apisix-master-0.rockspec b/rockspec/apisix-master-0.rockspec index bf2912a17c09..a5fc0c8d2d41 100644 --- a/rockspec/apisix-master-0.rockspec +++ b/rockspec/apisix-master-0.rockspec @@ -42,7 +42,7 @@ dependencies = { "lua-resty-cookie = 0.1.0", "lua-resty-session = 2.24", "opentracing-openresty = 0.1", - "lua-resty-radixtree = 2.5", + "lua-resty-radixtree = 2.6", "lua-protobuf = 0.3.1", "lua-resty-openidc = 1.7.2-1", "luafilesystem = 1.7.0-2", diff --git a/t/router/radixtree-host-uri2.t b/t/router/radixtree-host-uri2.t index eb49091931c0..df8a88d3dad3 100644 --- a/t/router/radixtree-host-uri2.t +++ b/t/router/radixtree-host-uri2.t @@ -195,3 +195,142 @@ qr/1981/ use config_center: yaml --- no_error_log [error] + + + +=== TEST 6: set route with ':' +--- yaml_config +apisix: + node_listen: 1984 + router: + http: 'radixtree_host_uri' + admin_key: null +--- 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": "/file:listReputationHistories", + "plugins":{"proxy-rewrite":{"uri":"/hello"}}, + "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 +--- no_error_log +[error] + + + +=== TEST 7: hit routes +--- yaml_config +apisix: + router: + http: 'radixtree_host_uri' +--- request +GET /file:listReputationHistories +--- response_body +hello world +--- no_error_log +[error] + + + +=== TEST 8: not hit +--- yaml_config +apisix: + router: + http: 'radixtree_host_uri' +--- request +GET /file:xx +--- error_code: 404 +--- no_error_log +[error] + + + +=== TEST 9: set route with ':' & host +--- yaml_config +apisix: + node_listen: 1984 + router: + http: 'radixtree_host_uri' + admin_key: null +--- 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": "/do:listReputationHistories", + "hosts": ["t.com"], + "plugins":{"proxy-rewrite":{"uri":"/hello"}}, + "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 +--- no_error_log +[error] + + + +=== TEST 10: hit routes +--- yaml_config +apisix: + router: + http: 'radixtree_host_uri' +--- request +GET /do:listReputationHistories +--- more_headers +Host: t.com +--- response_body +hello world +--- no_error_log +[error] + + + +=== TEST 11: not hit +--- yaml_config +apisix: + router: + http: 'radixtree_host_uri' +--- request +GET /do:xx +--- more_headers +Host: t.com +--- error_code: 404 +--- no_error_log +[error] diff --git a/t/router/radixtree-uri-host.t b/t/router/radixtree-uri-host.t index 7dab68c18fe3..a0d114122501 100644 --- a/t/router/radixtree-uri-host.t +++ b/t/router/radixtree-uri-host.t @@ -246,3 +246,56 @@ GET /hello?name=json hello world --- no_error_log [error] + + + +=== TEST 15: set route with ':' +--- 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": "/file:listReputationHistories", + "plugins":{"proxy-rewrite":{"uri":"/hello"}}, + "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 +--- no_error_log +[error] + + + +=== TEST 16: hit routes +--- request +GET /file:listReputationHistories +--- response_body +hello world +--- no_error_log +[error] + + + +=== TEST 17: not hit +--- request +GET /file:xx +--- error_code: 404 +--- no_error_log +[error] diff --git a/t/node/route-parameter-uri.t b/t/router/radixtree-uri-with-parameter.t similarity index 93% rename from t/node/route-parameter-uri.t rename to t/router/radixtree-uri-with-parameter.t index 8da02c10129a..70fd59a5d361 100644 --- a/t/node/route-parameter-uri.t +++ b/t/router/radixtree-uri-with-parameter.t @@ -22,6 +22,22 @@ worker_connections(256); no_root_location(); no_shuffle(); +our $yaml_config = <<_EOC_; +apisix: + node_listen: 1984 + admin_key: null + router: + http: 'radixtree_uri_with_parameter' +_EOC_ + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->yaml_config) { + $block->set_value("yaml_config", $yaml_config); + } +}); + run_tests(); __DATA__