kong/spec/01-unit/08-router_spec.lua (3,796 lines of code) (raw):

local Router local atc_compat = require "kong.router.atc_compat" local path_handling_tests = require "spec.fixtures.router_path_handling_tests" local uuid = require("kong.tools.utils").uuid local function reload_router(flavor) _G.kong = { configuration = { router_flavor = flavor, }, } package.loaded["kong.router"] = nil Router = require "kong.router" end local function new_router(cases, old_router) -- add fields expression/priority only for flavor expressions if kong.configuration.router_flavor == "expressions" then for _, v in ipairs(cases) do local r = v.route r.expression = r.expression or atc_compat._get_atc(r) r.priority = r.priority or atc_compat._route_priority(r) end end return Router.new(cases, nil, nil, old_router) end local service = { name = "service-invalid", protocol = "http", } local headers_mt = { __index = function(t, k) local u = rawget(t, string.upper(k)) if u then return u end return rawget(t, string.lower(k)) end } for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" }) do describe("Router (flavor = " .. flavor .. ")", function() reload_router(flavor) local it_trad_only = (flavor == "traditional") and it or pending describe("split_port()", function() it("splits port number", function() for _, case in ipairs({ { { "" }, { "", "", false } }, { { "localhost" }, { "localhost", "localhost", false } }, { { "localhost:" }, { "localhost", "localhost", false } }, { { "localhost:80" }, { "localhost", "localhost:80", true } }, { { "localhost:23h" }, { "localhost:23h", "localhost:23h", false } }, { { "localhost/24" }, { "localhost/24", "localhost/24", false } }, { { "::1" }, { "::1", "::1", false } }, { { "[::1]" }, { "::1", "[::1]", false } }, { { "[::1]:" }, { "::1", "[::1]:", false } }, { { "[::1]:80" }, { "::1", "[::1]:80", true } }, { { "[::1]:80b" }, { "[::1]:80b", "[::1]:80b", false } }, { { "[::1]/96" }, { "[::1]/96", "[::1]/96", false } }, { { "", 88 }, { "", ":88", false } }, { { "localhost", 88 }, { "localhost", "localhost:88", false } }, { { "localhost:", 88 }, { "localhost", "localhost:88", false } }, { { "localhost:80", 88 }, { "localhost", "localhost:80", true } }, { { "localhost:23h", 88 }, { "localhost:23h", "[localhost:23h]:88", false } }, { { "localhost/24", 88 }, { "localhost/24", "localhost/24:88", false } }, { { "::1", 88 }, { "::1", "[::1]:88", false } }, { { "[::1]", 88 }, { "::1", "[::1]:88", false } }, { { "[::1]:", 88 }, { "::1", "[::1]:88", false } }, { { "[::1]:80", 88 }, { "::1", "[::1]:80", true } }, { { "[::1]:80b", 88 }, { "[::1]:80b", "[::1]:80b:88", false } }, { { "[::1]/96", 88 }, { "[::1]/96", "[::1]/96:88", false } }, }) do assert.same(case[2], { Router.split_port(unpack(case[1])) }) end end) end) describe("new()", function() describe("[errors]", function() it("enforces args types", function() assert.error_matches(function() Router.new() end, "expected arg #1 routes to be a table", nil, true) end) it("enforces routes fields types", function() local router, err = Router.new { { route = { }, service = { name = "service-invalid" }, }, } assert.is_nil(router) assert.equal("could not categorize route", err) end) end) end) describe("select()", function() local use_case, router lazy_setup(function() use_case = { -- 1. host { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "domain-1.org", "domain-2.org" }, }, }, -- 2. method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", methods = { "TRACE" }, } }, -- 3. uri { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", paths = { "/my-route" }, } }, -- 4. host + uri { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8104", paths = { "/route-4" }, hosts = { "domain-1.org", "domain-2.org" }, }, }, -- 5. host + method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8105", hosts = { "domain-1.org", "domain-2.org" }, methods = { "POST", "PUT", "PATCH" }, }, }, -- 6. uri + method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8106", methods = { "POST", "PUT", "PATCH", }, paths = { "/route-6" }, } }, -- 7. host + uri + method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8107", hosts = { "domain-with-uri-1.org", "domain-with-uri-2.org" }, methods = { "POST", "PUT", "PATCH", }, paths = { "/my-route-uri" }, }, }, -- 8. serviceless-route { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8108", paths = { "/serviceless" }, } }, -- 9. headers (single) { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8109", headers = { location = { "my-location-1", "my-location-2", }, }, }, }, -- 10. headers (multiple) { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8110", headers = { location = { "my-location-1", }, version = { "v1", "v2", }, }, }, }, -- 11. headers + uri { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8111", headers = { location = { "my-location-1", "my-location-2", }, }, paths = { "/headers-uri" }, }, }, -- 12. host + headers + uri + method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8112", hosts = { "domain-with-headers-1.org", "domain-with-headers-2.org" }, headers = { location = { "my-location-1", "my-location-2", }, }, methods = { "POST", "PUT", "PATCH", }, paths = { "/headers-host-uri-method" }, }, }, -- 13. host + port { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8113", hosts = { "domain-1.org:321", "domain-2.org" }, }, }, -- 14. no "any-port" route { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8114", hosts = { "domain-3.org:321", }, }, }, -- 15. headers (regex) { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8115", headers = { user_agent = { "~*windows|linux|os\\s+x\\s*[\\d\\._]+|solaris|bsd", }, }, }, }, } router = assert(new_router(use_case)) end) it("[host]", function() -- host local match_t = router:select("GET", "/", "domain-1.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same(use_case[1].route.hosts[1], match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("[host] ignores default port", function() -- host local match_t = router:select("GET", "/", "domain-1.org:80") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same(use_case[1].route.hosts[1], match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it_trad_only("[host] weird port matches no-port route", function() local match_t = router:select("GET", "/", "domain-1.org:123") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.same(use_case[1].route.hosts[1], match_t.matches.host) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("[host] matches specific port", function() -- host local match_t = router:select("GET", "/", "domain-1.org:321") assert.truthy(match_t) assert.same(use_case[13].route, match_t.route) if flavor == "traditional" then assert.same(use_case[13].route.hosts[1], match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("[host] matches specific port on port-only route", function() -- host local match_t = router:select("GET", "/", "domain-3.org:321") assert.truthy(match_t) assert.same(use_case[14].route, match_t.route) if flavor == "traditional" then assert.same(use_case[14].route.hosts[1], match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("[host] fails just because of port on port-only route", function() -- host local match_t = router:select("GET", "/", "domain-3.org:123") assert.falsy(match_t) end) it("[uri]", function() -- uri local match_t = router:select("GET", "/my-route", "domain.org") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) assert.same(nil, match_t.matches.host) assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same(use_case[3].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("[uri + empty host]", function() -- uri only (no Host) -- Supported for HTTP/1.0 requests without a Host header local match_t = router:select("GET", "/my-route-uri", "") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) assert.same(nil, match_t.matches.host) assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same(use_case[3].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("[method]", function() -- method local match_t = router:select("TRACE", "/", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) assert.same(nil, match_t.matches.host) if flavor == "traditional" then assert.same(use_case[2].route.methods[1], match_t.matches.method) end assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("[host + uri]", function() -- host + uri local match_t = router:select("GET", "/route-4", "domain-1.org") assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) if flavor == "traditional" then assert.same(use_case[4].route.hosts[1], match_t.matches.host) end assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same(use_case[4].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("[host + method]", function() -- host + method local match_t = router:select("POST", "/", "domain-1.org") assert.truthy(match_t) assert.same(use_case[5].route, match_t.route) if flavor == "traditional" then assert.same(use_case[5].route.hosts[1], match_t.matches.host) assert.same(use_case[5].route.methods[1], match_t.matches.method) end assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("[uri + method]", function() -- uri + method local match_t = router:select("PUT", "/route-6", "domain.org") assert.truthy(match_t) assert.same(use_case[6].route, match_t.route) assert.same(nil, match_t.matches.host) if flavor == "traditional" then assert.same(use_case[6].route.methods[2], match_t.matches.method) assert.same(use_case[6].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("[host + uri + method]", function() -- uri + method local match_t = router:select("PUT", "/my-route-uri", "domain-with-uri-2.org") assert.truthy(match_t) assert.same(use_case[7].route, match_t.route) if flavor == "traditional" then assert.same(use_case[7].route.hosts[2], match_t.matches.host) assert.same(use_case[7].route.methods[2], match_t.matches.method) assert.same(use_case[7].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("single [headers] value", function() -- headers (single) local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = "my-location-1" }) assert.truthy(match_t) assert.same(use_case[9].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-1" }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = "my-location-2" }) assert.truthy(match_t) assert.same(use_case[9].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-2" }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = { "my-location-3", "my-location-2" } }) assert.truthy(match_t) assert.same(use_case[9].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-2" }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = "my-location-3" }) assert.is_nil(match_t) local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = { "my-location-3", "foo" } }) assert.is_nil(match_t) local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" }) assert.truthy(match_t) assert.same(use_case[15].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ user_agent = "mozilla/5.0 (x11; linux x86_64) applewebkit/537.36 (khtml, like gecko) chrome/83.0.4103.116 safari/537.36" }, match_t.matches.headers) end end) it("multiple [headers] values", function() -- headers (multiple) local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = "my-location-1", version = "v1", }) assert.truthy(match_t) assert.same(use_case[10].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-1", version = "v1", }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = "my-location-1", version = "v2", }) assert.truthy(match_t) assert.same(use_case[10].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-1", version = "v2", }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = { "my-location-3", "my-location-1" }, version = "v2", }) assert.truthy(match_t) assert.same(use_case[10].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-1", version = "v2", }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = { "my-location-3", "my-location-2" }, version = "v2", }) -- fallback to Route 9 assert.truthy(match_t) assert.same(use_case[9].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-2" }, match_t.matches.headers) end end) it("[headers + uri]", function() -- headers + uri local match_t = router:select("GET", "/headers-uri", nil, "http", nil, nil, nil, nil, nil, { location = "my-location-2" }) assert.truthy(match_t) assert.same(use_case[11].route, match_t.route) assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same(use_case[11].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-2" }, match_t.matches.headers) end end) it("[host + headers + uri + method]", function() -- host + headers + uri + method local match_t = router:select("PUT", "/headers-host-uri-method", "domain-with-headers-1.org", "http", nil, nil, nil, nil, nil, { location = "my-location-2", }) assert.truthy(match_t) assert.same(use_case[12].route, match_t.route) if flavor == "traditional" then assert.same(use_case[12].route.hosts[1], match_t.matches.host) assert.same(use_case[12].route.methods[2], match_t.matches.method) assert.same(use_case[12].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same(use_case[12].route.headers.location[2], match_t.matches.headers.location) end end) it("[serviceless]", function() local match_t = router:select("GET", "/serviceless") assert.truthy(match_t) assert.is_nil(match_t.service) assert.is_nil(match_t.matches.uri_captures) assert.same(use_case[8].route, match_t.route) if flavor == "traditional" then assert.same(use_case[8].route.paths[1], match_t.matches.uri) end end) if flavor == "traditional" then describe("[IPv6 literal host]", function() local use_case, router lazy_setup(function() use_case = { -- 1: no port, with and without brackets, unique IPs { service = service, route = { hosts = { "::11", "[::12]" }, }, }, -- 2: no port, with and without brackets, same hosts as 4 { service = service, route = { hosts = { "::21", "[::22]" }, }, }, -- 3: unique IPs, with port { service = service, route = { hosts = { "[::31]:321", "[::32]:321" }, }, }, -- 4: same hosts as 2, with port, needs brackets { service = service, route = { hosts = { "[::21]:321", "[::22]:321" }, }, }, } router = assert(new_router(use_case)) end) describe("no-port route is any-port", function() describe("no-port request", function() it("plain match", function() local match_t = assert(router:select("GET", "/", "::11")) assert.same(use_case[1].route, match_t.route) end) it("with brackets", function() local match_t = assert(router:select("GET", "/", "[::11]")) assert.same(use_case[1].route, match_t.route) end) end) it("explicit port still matches", function() local match_t = assert(router:select("GET", "/", "[::11]:654")) assert.same(use_case[1].route, match_t.route) end) end) describe("port-specific route", function() it("matches by port", function() local match_t = assert(router:select("GET", "/", "[::21]:321")) assert.same(use_case[4].route, match_t.route) local match_t = assert(router:select("GET", "/", "[::31]:321")) assert.same(use_case[3].route, match_t.route) end) it("matches other ports to any-port fallback", function() local match_t = assert(router:select("GET", "/", "[::21]:654")) assert.same(use_case[2].route, match_t.route) end) it("fails if there's no any-port route", function() local match_t = router:select("GET", "/", "[::31]:654") assert.falsy(match_t) end) end) end) end describe("[uri prefix]", function() it("matches when given [uri] is in request URI prefix", function() -- uri prefix local match_t = router:select("GET", "/my-route/some/path", "domain.org") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) assert.same(nil, match_t.matches.host) assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same(use_case[3].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("does not supersede another route with a longer [uri]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/my-route/hello" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/my-route" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/my-route/hello", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same("/my-route/hello", match_t.matches.uri) end match_t = router:select("GET", "/my-route/hello/world", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same("/my-route/hello", match_t.matches.uri) end match_t = router:select("GET", "/my-route", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("/my-route", match_t.matches.uri) end match_t = router:select("GET", "/my-route/world", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("/my-route", match_t.matches.uri) end end) it("does not supersede another route with a longer [uri] while [methods] are also defined", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", methods = { "POST", "PUT", "GET" }, paths = { "/my-route" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", methods = { "POST", "PUT", "GET" }, paths = { "/my-route/hello" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/my-route/hello", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) match_t = router:select("GET", "/my-route/hello/world", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) match_t = router:select("GET", "/my-route", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("GET", "/my-route/world", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("does not superseds another route with a longer [uri] while [hosts] are also defined", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "domain.org" }, paths = { "/my-route" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "domain.org" }, paths = { "/my-route/hello" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/my-route/hello", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) match_t = router:select("GET", "/my-route/hello/world", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) match_t = router:select("GET", "/my-route", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("GET", "/my-route/world", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("does not supersede another route with a longer [uri] when a better [uri] match exists for another [host]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "example.com" }, paths = { "/my-route" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "example.com" }, paths = { "/my-route/hello" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "example.net" }, paths = { "/my-route/hello/world" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/my-route/hello/world", "example.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) local match_t = router:select("GET", "/my-route/hello/world/and/goodnight", "example.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("only matches [uri prefix] as a prefix (anchored mode)", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/something/my-route" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "example.com" }, paths = { "/my-route" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/something/my-route", "example.com") assert.truthy(match_t) -- would be route-2 if URI matching was not prefix-only (anchored mode) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same("/something/my-route", match_t.matches.uri) end end) end) describe("[uri regex]", function() it("matches with [uri regex]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/users/\d+/profile]] }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/users/123/profile", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.same(nil, match_t.matches.host) assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same([[/users/\d+/profile]], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("matches the right route when several ones have a [uri regex]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/route/persons/\d{3}]] }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { [[~/route/persons/\d{3}/following]] }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", paths = { [[~/route/persons/\d{3}/[a-z]+]] }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/route/persons/456", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("matches a [uri regex] even if a [prefix uri] got a match", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[/route/persons]] }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { [[~/route/persons/\d+/profile]] }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/route/persons/123/profile", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) assert.same(nil, match_t.matches.host) assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same([[/route/persons/\d+/profile]], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("matches a [uri regex] even if a [uri] got an exact match", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/route/fixture" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "~/route/(fixture)" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/route/fixture", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) assert.same(nil, match_t.matches.host) assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same("/route/(fixture)", match_t.matches.uri) end end) it("matches a [uri regex + host] even if a [prefix uri] got a match", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "route.com" }, paths = { "/pat" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "route.com" }, paths = { "/path" }, methods = { "POST" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "route.com" }, paths = { "~/(path)" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/path", "route.com") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) if flavor == "traditional" then assert.same("route.com", match_t.matches.host) assert.same("/(path)", match_t.matches.uri) end assert.same(nil, match_t.matches.method) end) it("matches from the beginning of the request URI [uri regex]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/prefix/[0-9]+]] } }, }, } local router = assert(new_router(use_case)) -- sanity local match_t = router:select("GET", "/prefix/123", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.same(nil, match_t.matches.host) assert.same(nil, match_t.matches.method) match_t = router:select("GET", "/extra/prefix/123", "domain.org") assert.is_nil(match_t) end) end) describe("[wildcard host]", function() local use_case, router lazy_setup(function() use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "*.route.com" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "route.*" }, }, }, } router = assert(new_router(use_case)) end) it("matches leftmost wildcards", function() local match_t = router:select("GET", "/", "foo.route.com", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same(use_case[1].route.hosts[1], match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("matches rightmost wildcards", function() local match_t = router:select("GET", "/", "route.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("matches any port in request", function() local match_t = router:select("GET", "/", "route.org:123") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) local match_t = router:select("GET", "/", "foo.route.com:123", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("matches port-specific routes", function() table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "*.route.net:123" }, }, }) table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8104", hosts = { "route.*:123" }, -- same as [2] but port-specific }, }) router = assert(new_router(use_case)) finally(function() table.remove(use_case) table.remove(use_case) router = assert(new_router(use_case)) end) -- match the right port local match_t = router:select("GET", "/", "foo.route.net:123") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) -- fail different port assert.is_nil(router:select("GET", "/", "foo.route.net:456")) -- port-specific is higher priority local match_t = router:select("GET", "/", "route.org:123") assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) end) it("prefers port-specific even for http default port", function() table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "route.*:80" }, -- same as [2] but port-specific }, }) router = assert(new_router(use_case)) finally(function() table.remove(use_case) router = assert(new_router(use_case)) end) -- non-port matches any local match_t = assert(router:select("GET", "/", "route.org:123")) assert.same(use_case[2].route, match_t.route) -- port 80 goes to port-specific route local match_t = assert(router:select("GET", "/", "route.org:80")) assert.same(use_case[3].route, match_t.route) -- even if it's implicit port 80 if flavor == "traditional" then local match_t = assert(router:select("GET", "/", "route.org")) assert.same(use_case[3].route, match_t.route) end end) it("prefers port-specific even for https default port", function() table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "route.*:443" }, -- same as [2] but port-specific }, }) router = assert(new_router(use_case)) finally(function() table.remove(use_case) router = assert(new_router(use_case)) end) -- non-port matches any local match_t = assert(router:select("GET", "/", "route.org:123")) assert.same(use_case[2].route, match_t.route) -- port 443 goes to port-specific route local match_t = assert(router:select("GET", "/", "route.org:443")) assert.same(use_case[3].route, match_t.route) -- even if it's implicit port 443 if flavor == "traditional" then local match_t = assert(router:select("GET", "/", "route.org", "https")) assert.same(use_case[3].route, match_t.route) end end) it("does not take precedence over a plain host", function() table.insert(use_case, 1, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "plain.route.com" }, }, }) table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8104", hosts = { "route.com" }, }, }) finally(function() table.remove(use_case, 1) table.remove(use_case) router = assert(new_router(use_case)) end) router = assert(new_router(use_case)) local match_t = router:select("GET", "/", "route.com") assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) if flavor == "traditional" then assert.same("route.com", match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) match_t = router:select("GET", "/", "route.org") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) if flavor == "traditional" then assert.same("route.*", match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) match_t = router:select("GET", "/", "plain.route.com") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same("plain.route.com", match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) match_t = router:select("GET", "/", "foo.route.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("*.route.com", match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("matches [wildcard host + path] even if a similar [plain host] exists", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "*.route.com" }, paths = { "/path1" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "plain.route.com" }, paths = { "/path2" }, }, }, } router = assert(new_router(use_case)) local match_t = router:select("GET", "/path1", "plain.route.com") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same("*.route.com", match_t.matches.host) assert.same("/path1", match_t.matches.uri) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri_captures) end) it("matches [plain host + path] even if a matching [wildcard host] exists", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "*.route.com" }, paths = { "/path1" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "plain.route.com" }, paths = { "/path2" }, }, }, } router = assert(new_router(use_case)) local match_t = router:select("GET", "/path2", "plain.route.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("plain.route.com", match_t.matches.host) assert.same("/path2", match_t.matches.uri) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri_captures) end) it("submatch_weight [wildcard host port] > [wildcard host] ", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "route.*" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "route.*:80", "route.com.*" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/", "route.org:80") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("route.*:80", match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("matches a [wildcard host + port] even if a [wildcard host] matched", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "route.*" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "route.*:123" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "route.*:80" }, }, }, } local router = assert(new_router(use_case)) -- explicit port local match_t = router:select("GET", "/", "route.org:123") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("route.*:123", match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) -- implicit port if flavor == "traditional" then local match_t = router:select("GET", "/", "route.org") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) assert.same("route.*:80", match_t.matches.host) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end end) it("matches [wildcard/plain + uri + method]", function() finally(function() table.remove(use_case) router = assert(new_router(use_case)) end) table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "*.domain.com", "example.com" }, paths = { "/path" }, methods = { "GET", "TRACE" }, }, }) router = assert(new_router(use_case)) local match_t = router:select("POST", "/path", "foo.domain.com") assert.is_nil(match_t) match_t = router:select("GET", "/path", "foo.domain.com") assert.truthy(match_t) assert.same(use_case[#use_case].route, match_t.route) match_t = router:select("TRACE", "/path", "example.com") assert.truthy(match_t) assert.same(use_case[#use_case].route, match_t.route) match_t = router:select("POST", "/path", "foo.domain.com") assert.is_nil(match_t) end) end) describe("[wildcard host] + [uri regex]", function() it("matches", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "*.example.com" }, paths = { [[~/users/\d+/profile]] }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "*.example.com" }, paths = { [[/users]] }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/users/123/profile", "test.example.com") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("GET", "/users", "test.example.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) end) describe("[headers]", function() it("evaluates Routes with more [headers] first", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", headers = { version = { "v1", "v2" }, user_agent = { "foo", "bar" }, }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", headers = { version = { "v1", "v2" }, user_agent = { "foo", "bar" }, location = { "east", "west" }, }, }, } } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { version = "v1", user_agent = "foo", location = { "north", "west" }, }) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("names are case-insensitive", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", headers = { ["USER_AGENT"] = { "foo", "bar" }, }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", headers = { user_agent = { "baz" }, }, }, } } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, setmetatable({ user_agent = "foo", }, headers_mt)) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same({ user_agent = "foo" }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, setmetatable({ ["USER_AGENT"] = "baz", }, headers_mt)) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same({ user_agent = "baz" }, match_t.matches.headers) end end) it("matches values in a case-insensitive way", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", headers = { user_agent = { "foo", "bar" }, }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", headers = { user_agent = { "BAZ" }, }, }, } } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { user_agent = "FOO", }) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same({ user_agent = "foo" }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { user_agent = "baz", }) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same({ user_agent = "baz" }, match_t.matches.headers) end end) end) if flavor ~= "traditional" then describe("incremental rebuild", function() local router local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foo", }, updated_at = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/bar", }, updated_at = 90, }, } } before_each(function() router = assert(new_router(use_case)) end) it("matches initially", function() local match_t = router:select("GET", "/foo") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("GET", "/bar") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("update/remove works", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foo1", }, updated_at = 100, }, }, } local nrouter = assert(new_router(use_case, router)) assert.equal(nrouter, router) local match_t = nrouter:select("GET", "/foo1") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = nrouter:select("GET", "/bar") assert.falsy(match_t) end) it("update with wrong route", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "~/delay/(?<delay>[^\\/]+)$", }, updated_at = 100, }, }, } local ok, nrouter = pcall(new_router, use_case, router) assert(ok) assert.equal(nrouter, router) assert.equal(#nrouter.routes, 0) end) it("update skips routes if updated_at is unchanged", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foo", }, updated_at = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/baz", }, updated_at = 90, }, } } local nrouter = assert(new_router(use_case, router)) assert.equal(nrouter, router) local match_t = nrouter:select("GET", "/baz") assert.falsy(match_t) match_t = nrouter:select("GET", "/bar") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("clears match and negative cache after rebuild", function() local match_t = router:select("GET", "/baz") assert.falsy(match_t) match_t = router:select("GET", "/foo") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foz", }, updated_at = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/baz", }, updated_at = 100, }, } } local nrouter = assert(new_router(use_case, router)) assert.equal(nrouter, router) local match_t = nrouter:select("GET", "/foo") assert.falsy(match_t) match_t = nrouter:select("GET", "/baz") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("detects concurrent incremental builds", function() local use_cases = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foz", }, updated_at = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/baz", }, updated_at = 100, }, } } -- needs to be larger than YIELD_ITERATIONS for i = 1, 1000 do use_cases[i] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb" .. string.format("%04d", i), paths = { "/" .. i, }, updated_at = 100, }, } end local threads = {} -- make sure yield() actually works ngx.IS_CLI = false for i = 1, 10 do threads[i] = ngx.thread.spawn(function() return new_router(use_cases, router) end) end local error_detected = false for i = 1, 10 do local _, _, err = ngx.thread.wait(threads[i]) if err == "concurrent incremental router rebuild without mutex, this is unsafe" then error_detected = true break end end ngx.IS_CLI = true assert.truthy(error_detected) end) it("generates the correct diff", function() local old_router = atc_compat.new({ { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/a" }, expression = 'http.path ^= "/a"', priority = 1, updated_at = 100, }, }, }) local add_matcher = spy.on(old_router.router, "add_matcher") local remove_matcher = spy.on(old_router.router, "remove_matcher") atc_compat.new({ { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/b", }, expression = 'http.path ^= "/b"', priority = 1, updated_at = 101, } }, { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/c", }, expression = 'http.path ^= "/c"', priority = 1, updated_at = 102, }, }, }, nil, nil, old_router) assert.spy(add_matcher).was_called(2) assert.spy(remove_matcher).was_called(1) end) end) end describe("check empty route fields", function() local use_case local _get_atc = atc_compat._get_atc before_each(function() use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", methods = { "GET" }, paths = { "/foo", }, }, }, } end) it("empty methods", function() use_case[1].route.methods = {} assert.equal(_get_atc(use_case[1].route), [[(http.path ^= "/foo")]]) assert(new_router(use_case)) end) it("empty hosts", function() use_case[1].route.hosts = {} assert.equal(_get_atc(use_case[1].route), [[(http.method == "GET") && (http.path ^= "/foo")]]) assert(new_router(use_case)) end) it("empty headers", function() use_case[1].route.headers = {} assert.equal(_get_atc(use_case[1].route), [[(http.method == "GET") && (http.path ^= "/foo")]]) assert(new_router(use_case)) end) it("empty paths", function() use_case[1].route.paths = {} assert.equal(_get_atc(use_case[1].route), [[(http.method == "GET")]]) assert(new_router(use_case)) end) it("empty snis", function() use_case[1].route.snis = {} assert.equal(_get_atc(use_case[1].route), [[(http.method == "GET") && (http.path ^= "/foo")]]) assert(new_router(use_case)) end) end) describe("normalization stopgap measurements", function() local use_case, router lazy_setup(function() use_case = { -- percent encoding with unreserved char, route should be plain text { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/plain/a.b.c", -- /plain/a.b.c "/plain/a.b%25c", -- /plain/a.b.c }, }, }, -- regex. It is no longer normalized since 3.0 { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "~/reg%65x/\\d+", -- /regex/\d+ }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", paths = { "~/regex-meta/%5Cd\\+%2E", -- /regex-meta/\d\+. }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8104", paths = { "~/regex-reserved%2Fabc", -- /regex-reserved/abc }, }, }, } router = assert(new_router(use_case)) end) it("matches against plain text paths", function() local match_t = router:select("GET", "/plain/a.b.c", "example.com") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) -- route no longer normalize user configured path match_t = router:select("GET", "/plain/a.b c", "example.com") assert.falsy(match_t) match_t = router:select("GET", "/plain/a.b%25c", "example.com") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("GET", "/plain/aab.c", "example.com") assert.falsy(match_t) end) it("matches against regex paths", function() local match_t = router:select("GET", "/regex/123", "example.com") assert.falsy(match_t) match_t = router:select("GET", "/reg%65x/123", "example.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) match_t = router:select("GET", "/regex/\\d+", "example.com") assert.falsy(match_t) end) it("escapes meta character after percent decoding from regex paths", function() local match_t = router:select("GET", "/regex-meta/123a", "example.com") assert.falsy(match_t) match_t = router:select("GET", "/regex-meta/\\d+.", "example.com") assert.falsy(match_t) match_t = router:select("GET", "/regex-meta/%5Cd+%2E", "example.com") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) end) it("leave reserved characters alone in regex paths", function() local match_t = router:select("GET", "/regex-reserved/abc", "example.com") assert.falsy(match_t) match_t = router:select("GET", "/regex-reserved%2Fabc", "example.com") assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) end) end) describe("edge-cases", function() it("[host] and [uri] have higher priority than [method]", function() local use_case = { -- 1. host { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "domain-1.org", "domain-2.org" }, }, }, -- 2. method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", methods = { "TRACE" }, } }, -- 3. uri { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", paths = { "/my-route" }, } }, -- 4. host + uri { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8104", paths = { "/route-4" }, hosts = { "domain-1.org", "domain-2.org" }, }, }, -- 5. host + method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8105", hosts = { "domain-1.org", "domain-2.org" }, methods = { "POST", "PUT", "PATCH" }, }, }, -- 6. uri + method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8106", methods = { "POST", "PUT", "PATCH", }, paths = { "/route-6" }, } }, -- 7. host + uri + method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8107", hosts = { "domain-with-uri-1.org", "domain-with-uri-2.org" }, methods = { "POST", "PUT", "PATCH", }, paths = { "/my-route-uri" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("TRACE", "/", "domain-2.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) -- uri local match_t = router:select("TRACE", "/my-route", "domain.org") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) end) it("half [uri] and [host] match does not supersede another route", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "host1.com" }, paths = { "/v1/path" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "host2.com" }, paths = { "/" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/v1/path", "host1.com") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("GET", "/v1/path", "host2.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("half [wildcard host] and [method] match does not supersede another route", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "host.*" }, methods = { "GET" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "host.*" }, methods = { "POST" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/", "host.com") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("POST", "/", "host.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("half [uri regex] and [method] match does not supersede another route", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", methods = { "GET" }, paths = { [[~/users/\d+/profile]] }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", methods = { "POST" }, paths = { [[~/users/\d*/profile]] }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/users/123/profile", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("POST", "/users/123/profile", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("[method] does not supersede [uri prefix]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", methods = { "GET" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/example" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/example", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) match_t = router:select("GET", "/example/status/200", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("[method] does not supersede [wildcard host]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", methods = { "GET" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "domain.*" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/", "nothing.com") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("GET", "/", "domain.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it_trad_only("does not supersede another route with a longer [uri prefix]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/a", "/bbbbbbb" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/a/bb" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/a/bb/foobar", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) describe("root / [uri]", function() lazy_setup(function() table.insert(use_case, 1, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb810f", paths = { "/" }, } }) end) lazy_teardown(function() table.remove(use_case, 1) end) it("request with [method]", function() local router = assert(new_router(use_case)) local match_t = router:select("GET", "/", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("does not supersede another route", function() local router = assert(new_router(use_case)) local match_t = router:select("GET", "/my-route", "domain.org") assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) match_t = router:select("GET", "/my-route/hello/world", "domain.org") assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) end) it("acts as a catch-all route", function() local router = assert(new_router(use_case)) local match_t = router:select("GET", "/foobar/baz", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) end) describe("multiple routes of same category with conflicting values", function() -- reload router to reset combined cached matchers reload_router(flavor) local n = 6 lazy_setup(function() -- all those routes are of the same category: -- [host + uri] for i = 1, n - 1 do table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb811" .. i, hosts = { "domain.org" }, paths = { "/my-uri" }, }, }) end table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8121", hosts = { "domain.org" }, paths = { "/my-target-uri" }, }, }) end) lazy_teardown(function() for _ = 1, n do table.remove(use_case) end end) it("matches correct route", function() local router = assert(new_router(use_case)) local match_t = router:select("GET", "/my-target-uri", "domain.org") assert.truthy(match_t) assert.same(use_case[#use_case].route, match_t.route) end) end) it("more [headers] has priority over longer [paths]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", headers = { version = { "v1" }, }, paths = { "/my-route/hello" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", headers = { version = { "v1" }, location = { "us-east" }, }, paths = { "/my-route" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/my-route/hello", "domain.org", "http", nil, nil, nil, nil, nil, { version = "v1", location = "us-east", }) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("/my-route", match_t.matches.uri) assert.same({ version = "v1", location = "us-east" }, match_t.matches.headers) end local match_t = router:select("GET", "/my-route/hello/world", "http", "domain.org", nil, nil, nil, nil, nil, { version = "v1", location = "us-east", }) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("/my-route", match_t.matches.uri) assert.same({ version = "v1", location = "us-east" }, match_t.matches.headers) end end) end) describe("misses", function() it("invalid [host]", function() assert.is_nil(router:select("GET", "/", "domain-3.org")) end) it("invalid host in [host + uri]", function() assert.is_nil(router:select("GET", "/route-4", "domain-3.org")) end) it("invalid host in [host + method]", function() assert.is_nil(router:select("GET", "/", "domain-3.org")) end) it("invalid method in [host + uri + method]", function() assert.is_nil(router:select("GET", "/some-uri", "domain-with-uri-2.org")) end) it("invalid uri in [host + uri + method]", function() assert.is_nil(router:select("PUT", "/some-uri-foo", "domain-with-uri-2.org")) end) it("does not match when given [uri] is in URI but not in prefix", function() local match_t = router:select("GET", "/some-other-prefix/my-route", "domain.org") assert.is_nil(match_t) end) it("invalid [headers]", function() assert.is_nil(router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = "invalid-location" })) end) it("invalid headers in [headers + uri]", function() assert.is_nil(router:select("GET", "/headers-uri", nil, "http", nil, nil, nil, nil, nil, { location = "invalid-location" })) end) it("invalid headers in [headers + uri + method]", function() assert.is_nil(router:select("PUT", "/headers-uri-method", nil, "http", nil, nil, nil, nil, nil, { location = "invalid-location" })) end) it("invalid headers in [headers + host + uri + method]", function() assert.is_nil(router:select("PUT", "/headers-host-uri-method", nil, "http", nil, nil, nil, nil, nil, { location = "invalid-location", host = "domain-with-headers-1.org" })) end) end) describe("#benchmarks", function() --[[ Run: $ busted --tags=benchmarks <router_spec.lua> To estimate how much time matching an route in a worst-case scenario with a set of ~1000 registered routes would take. We are aiming at sub-ms latency. ]] describe("plain [host]", function() local router local target_domain local benchmark_use_cases = {} lazy_setup(function() for i = 1, 10^5 do benchmark_use_cases[i] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a1" .. string.format("%06d", i), hosts = { "domain-" .. i .. ".org" }, }, } end target_domain = "domain-" .. #benchmark_use_cases .. ".org" router = assert(new_router(benchmark_use_cases)) end) lazy_teardown(function() -- this avoids memory leakage router = nil benchmark_use_cases = nil end) it("takes < 1ms", function() local match_t = router:select("GET", "/", target_domain) assert.truthy(match_t) assert.same(benchmark_use_cases[#benchmark_use_cases].route, match_t.route) end) end) describe("[method + uri + host]", function() local router local target_uri local target_domain local benchmark_use_cases = {} lazy_setup(function() local n = 10^5 for i = 1, n - 1 do -- insert a lot of routes that don't match (missing methods) -- but have conflicting paths and hosts (domain-<n>.org) benchmark_use_cases[i] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a1" .. string.format("%06d", i), hosts = { "domain-" .. n .. ".org" }, paths = { "/my-route-" .. n }, }, } end -- insert our target route, which has the proper method as well benchmark_use_cases[n] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a1" .. string.format("%06d", n), hosts = { "domain-" .. n .. ".org" }, methods = { "POST" }, paths = { "/my-route-" .. n }, }, } target_uri = "/my-route-" .. n target_domain = "domain-" .. n .. ".org" router = assert(new_router(benchmark_use_cases)) end) lazy_teardown(function() -- this avoids memory leakage router = nil benchmark_use_cases = nil end) it("takes < 1ms", function() local match_t = router:select("POST", target_uri, target_domain) assert.truthy(match_t) assert.same(benchmark_use_cases[#benchmark_use_cases].route, match_t.route) end) end) describe("[headers]", function() describe("single key", function() local router local target_location local benchmark_use_cases = {} lazy_setup(function() local n = 10^5 for i = 1, n do benchmark_use_cases[i] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a1" .. string.format("%06d", i), headers = { location = { "somewhere-" .. i }, }, }, } end target_location = "somewhere-" .. n router = assert(new_router(benchmark_use_cases)) end) lazy_teardown(function() -- this avoids memory leakage router = nil benchmark_use_cases = nil end) it("takes < 1ms", function() local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = target_location }) assert.truthy(match_t) assert.same(benchmark_use_cases[#benchmark_use_cases].route, match_t.route) end) end) if flavor == "traditional" then describe("10^4 keys", function() local router local target_val local target_key local benchmark_use_cases = {} lazy_setup(function() local n = 10^5 for i = 1, n do benchmark_use_cases[i] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a1" .. string.format("%06d", i), headers = { ["key-" .. i] = { "somewhere" }, }, }, } end target_key = "key-" .. n target_val = "somewhere" router = assert(new_router(benchmark_use_cases)) end) lazy_teardown(function() -- this avoids memory leakage router = nil benchmark_use_cases = nil end) it("takes < 1ms", function() local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { [target_key] = target_val }) assert.truthy(match_t) assert.same(benchmark_use_cases[#benchmark_use_cases].route, match_t.route) end) end) end end) describe("multiple routes of same category with identical values", function() local router local target_uri local target_domain local benchmark_use_cases = {} lazy_setup(function() local n = 10^5 for i = 1, n - 1 do -- all our routes here use domain.org as the domain -- they all are [host + uri] category benchmark_use_cases[i] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6" .. string.format("%06d", i), hosts = { "domain.org" }, paths = { "/my-route-" .. n }, }, } end -- this one too, but our target will be a -- different URI benchmark_use_cases[n] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6ffffff", hosts = { "domain.org" }, paths = { "/my-real-route" }, }, } target_uri = "/my-real-route" target_domain = "domain.org" router = assert(new_router(benchmark_use_cases)) end) lazy_teardown(function() -- this avoids memory leakage router = nil benchmark_use_cases = nil end) it("takes < 1ms", function() local match_t = router:select("GET", target_uri, target_domain) assert.truthy(match_t) assert.same(benchmark_use_cases[#benchmark_use_cases].route, match_t.route) end) end) describe("[method + uri + host + headers]", function() local router local target_uri local target_domain local target_location local benchmark_use_cases = {} lazy_setup(function() local n = 10^5 for i = 1, n - 1 do -- insert a lot of routes that don't match (missing methods) -- but have conflicting paths and hosts (domain-<n>.org) benchmark_use_cases[i] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6" .. string.format("%06d", i), hosts = { "domain-" .. n .. ".org" }, paths = { "/my-route-" .. n }, headers = { location = { "somewhere-" .. n }, }, }, } end -- insert our target route, which has the proper method as well benchmark_use_cases[n] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6ffffff", hosts = { "domain-" .. n .. ".org" }, headers = { location = { "somewhere-" .. n }, }, methods = { "POST" }, paths = { "/my-route-" .. n }, }, } target_uri = "/my-route-" .. n target_domain = "domain-" .. n .. ".org" target_location = "somewhere-" .. n router = assert(new_router(benchmark_use_cases)) end) lazy_teardown(function() -- this avoids memory leakage router = nil benchmark_use_cases = nil end) it("takes < 1ms", function() local match_t = router:select("POST", target_uri, target_domain, "http", nil, nil, nil, nil, nil, { location = target_location, }) assert.truthy(match_t) assert.same(benchmark_use_cases[#benchmark_use_cases].route, match_t.route) end) end) end) describe("[errors]", function() it("enforces args types", function() assert.error_matches(function() router:select(1) end, "method must be a string", nil, true) assert.error_matches(function() router:select("GET", 1) end, "uri must be a string", nil, true) assert.error_matches(function() router:select("GET", "/", 1) end, "host must be a string", nil, true) assert.error_matches(function() router:select("GET", "/", "", 1) end, "scheme must be a string", nil, true) if flavor == "traditional" then assert.error_matches(function() router:select("GET", "/", "", "http", 1) end, "src_ip must be a string", nil, true) assert.error_matches(function() router:select("GET", "/", "", "http", nil, "") end, "src_port must be a number", nil, true) assert.error_matches(function() router:select("GET", "/", "", "http", nil, nil, 1) end, "dst_ip must be a string", nil, true) assert.error_matches(function() router:select("GET", "/", "", "http", nil, nil, nil, "") end, "dst_port must be a number", nil, true) end assert.error_matches(function() router:select("GET", "/", "", "http", nil, nil, nil, nil, 1) end, "sni must be a string", nil, true) assert.error_matches(function() router:select("GET", "/", "", "http", nil, nil, nil, nil, nil, 1) end, "headers must be a table", nil, true) end) end) end) describe("exec()", function() local spy_stub = { nop = function() end } local function mock_ngx(method, request_uri, headers) local _ngx _ngx = { re = ngx.re, var = setmetatable({ request_uri = request_uri, http_kong_debug = headers.kong_debug }, { __index = function(_, key) if key == "http_host" then spy_stub.nop() return headers.host end end }), req = { get_method = function() return method end, get_headers = function() return setmetatable(headers, headers_mt) end } } return _ngx end it("returns parsed upstream_url + upstream_uri", function() local use_case_routes = { { service = { name = "service-invalid", host = "example.org", protocol = "http" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/my-route" }, }, }, { service = { name = "service-invalid", host = "example.org", protocol = "https" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/my-route-2" }, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) -- upstream_url_t if flavor == "traditional" then assert.equal("http", match_t.upstream_url_t.scheme) end assert.equal("example.org", match_t.upstream_url_t.host) assert.equal(80, match_t.upstream_url_t.port) -- upstream_uri assert.is_nil(match_t.upstream_host) -- only when `preserve_host = true` assert.equal("/my-route", match_t.upstream_uri) _ngx = mock_ngx("GET", "/my-route-2", { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) -- upstream_url_t if flavor == "traditional" then assert.equal("https", match_t.upstream_url_t.scheme) end assert.equal("example.org", match_t.upstream_url_t.host) assert.equal(443, match_t.upstream_url_t.port) -- upstream_uri assert.is_nil(match_t.upstream_host) -- only when `preserve_host = true` assert.equal("/my-route-2", match_t.upstream_uri) end) it("returns matched_host + matched_uri + matched_method + matched_headers", function() local use_case_routes = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "host.com" }, methods = { "GET" }, paths = { "/my-route" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "host.com" }, paths = { "/my-route" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "*.host.com" }, headers = { location = { "my-location-1", "my-location-2" }, }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8104", paths = { [[~/users/\d+/profile]] }, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/my-route", { host = "host.com" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) if flavor == "traditional" then assert.equal("host.com", match_t.matches.host) assert.equal("/my-route", match_t.matches.uri) assert.equal("GET", match_t.matches.method) end assert.is_nil(match_t.matches.headers) _ngx = mock_ngx("GET", "/my-route/prefix/match", { host = "host.com" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) if flavor == "traditional" then assert.equal("host.com", match_t.matches.host) assert.equal("/my-route", match_t.matches.uri) assert.equal("GET", match_t.matches.method) end assert.is_nil(match_t.matches.headers) _ngx = mock_ngx("POST", "/my-route", { host = "host.com" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) if flavor == "traditional" then assert.equal("host.com", match_t.matches.host) assert.equal("/my-route", match_t.matches.uri) end assert.is_nil(match_t.matches.method) assert.is_nil(match_t.matches.headers) _ngx = mock_ngx("GET", "/", { host = "test.host.com", location = "my-location-1" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[3].route, match_t.route) if flavor == "traditional" then assert.equal("*.host.com", match_t.matches.host) assert.same({ location = "my-location-1" }, match_t.matches.headers) end assert.is_nil(match_t.matches.uri) assert.is_nil(match_t.matches.method) _ngx = mock_ngx("GET", "/users/123/profile", { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[4].route, match_t.route) assert.is_nil(match_t.matches.host) if flavor == "traditional" then assert.equal([[/users/\d+/profile]], match_t.matches.uri) end assert.is_nil(match_t.matches.method) assert.is_nil(match_t.matches.headers) end) it("returns uri_captures from a [uri regex]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/users/(?P<user_id>\d+)/profile/?(?P<scope>[a-z]*)]] }, }, }, } local router = assert(new_router(use_case)) local _ngx = mock_ngx("GET", "/users/1984/profile", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.equal("1984", match_t.matches.uri_captures[1]) assert.equal("1984", match_t.matches.uri_captures.user_id) assert.equal("", match_t.matches.uri_captures[2]) assert.equal("", match_t.matches.uri_captures.scope) -- returns the full match as well assert.equal("/users/1984/profile", match_t.matches.uri_captures[0]) -- no stripped_uri capture assert.is_nil(match_t.matches.uri_captures.stripped_uri) assert.equal(2, #match_t.matches.uri_captures) -- again, this time from the LRU cache router._set_ngx(_ngx) match_t = router:exec() assert.equal("1984", match_t.matches.uri_captures[1]) assert.equal("1984", match_t.matches.uri_captures.user_id) assert.equal("", match_t.matches.uri_captures[2]) assert.equal("", match_t.matches.uri_captures.scope) -- returns the full match as well assert.equal("/users/1984/profile", match_t.matches.uri_captures[0]) -- no stripped_uri capture assert.is_nil(match_t.matches.uri_captures.stripped_uri) assert.equal(2, #match_t.matches.uri_captures) _ngx = mock_ngx("GET", "/users/1984/profile/email", { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.equal("1984", match_t.matches.uri_captures[1]) assert.equal("1984", match_t.matches.uri_captures.user_id) assert.equal("email", match_t.matches.uri_captures[2]) assert.equal("email", match_t.matches.uri_captures.scope) -- returns the full match as well assert.equal("/users/1984/profile/email", match_t.matches.uri_captures[0]) -- no stripped_uri capture assert.is_nil(match_t.matches.uri_captures.stripped_uri) assert.equal(2, #match_t.matches.uri_captures) end) it("returns uri_captures normalized, fix #7913", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/users/(?P<fullname>[a-zA-Z\s\d%]+)/profile/?(?P<scope>[a-z]*)]] }, }, }, } local router = assert(new_router(use_case)) local _ngx = mock_ngx("GET", "/users/%6aohn%20doe/profile", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.equal("john doe", match_t.matches.uri_captures[1]) assert.equal("john doe", match_t.matches.uri_captures.fullname) assert.equal("", match_t.matches.uri_captures[2]) assert.equal("", match_t.matches.uri_captures.scope) -- returns the full match as well assert.equal("/users/john doe/profile", match_t.matches.uri_captures[0]) -- no stripped_uri capture assert.is_nil(match_t.matches.uri_captures.stripped_uri) assert.equal(2, #match_t.matches.uri_captures) -- again, this time from the LRU cache local match_t = router:exec() assert.equal("john doe", match_t.matches.uri_captures[1]) assert.equal("john doe", match_t.matches.uri_captures.fullname) assert.equal("", match_t.matches.uri_captures[2]) assert.equal("", match_t.matches.uri_captures.scope) -- returns the full match as well assert.equal("/users/john doe/profile", match_t.matches.uri_captures[0]) -- no stripped_uri capture assert.is_nil(match_t.matches.uri_captures.stripped_uri) assert.equal(2, #match_t.matches.uri_captures) end) it("returns no uri_captures from a [uri prefix] match", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/hello" }, strip_path = true, }, }, } local router = assert(new_router(use_case)) local _ngx = mock_ngx("GET", "/hello/world", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.equal("/world", match_t.upstream_uri) assert.is_nil(match_t.matches.uri_captures) end) it("returns no uri_captures from a [uri regex] match without groups", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/users/\d+/profile]] }, }, }, } local router = assert(new_router(use_case)) local _ngx = mock_ngx("GET", "/users/1984/profile", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.is_nil(match_t.matches.uri_captures) end) it("parses path component from upstream_url property", function() local use_case_routes = { { service = { name = "service-invalid", host = "example.org", path = "/get", protocol = "http" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/my-route" }, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) if flavor == "traditional" then assert.equal("/get", match_t.upstream_url_t.path) end end) it("parses upstream_url port", function() local use_case_routes = { { service = { name = "service-invalid", host = "example.org", port = 8080, protocol = "http" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/my-route" }, }, }, { service = { name = "service-invalid", host = "example.org", port = 8443, protocol = "https" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/my-route-2" }, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.equal(8080, match_t.upstream_url_t.port) _ngx = mock_ngx("GET", "/my-route-2", { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.equal(8443, match_t.upstream_url_t.port) end) it("allows url encoded paths if they are reserved characters", function() local use_case_routes = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/endel%2Fst" }, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/endel%2Fst", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/endel%2Fst", match_t.upstream_uri) end) describe("stripped paths #strip", function() local router local use_case_routes = { { service = service, route = { id = uuid(), paths = { "/my-route", "/this-route" }, strip_path = true } }, -- don't strip this route's matching URI { service = service, route = { id = uuid(), methods = { "POST" }, paths = { "/my-route", "/this-route" }, strip_path = false, }, }, } lazy_setup(function() router = assert(new_router(use_case_routes)) end) it("strips the specified paths from the given uri if matching", function() local _ngx = mock_ngx("GET", "/my-route/hello/world", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/my-route", match_t.prefix) assert.equal("/hello/world", match_t.upstream_uri) end) it("strips if matched URI is plain (not a prefix)", function() local _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/my-route", match_t.prefix) assert.equal("/", match_t.upstream_uri) end) it("doesn't strip if 'strip_uri' is not enabled", function() local _ngx = mock_ngx("POST", "/my-route/hello/world", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) assert.is_nil(match_t.prefix) assert.equal("/my-route/hello/world", match_t.upstream_uri) end) it("normalized client URI before matching and proxying", function() local _ngx = mock_ngx("POST", "/my-route/hello/world", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) assert.is_nil(match_t.prefix) assert.equal("/my-route/hello/world", match_t.upstream_uri) end) it("does not strips root / URI", function() local use_case_routes = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/" }, strip_path = true, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("POST", "/my-route/hello/world", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/", match_t.prefix) assert.equal("/my-route/hello/world", match_t.upstream_uri) end) it("can find an route with stripped URI several times in a row", function() local _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/my-route", match_t.prefix) assert.equal("/", match_t.upstream_uri) _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/my-route", match_t.prefix) assert.equal("/", match_t.upstream_uri) end) it("can proxy an route with stripped URI with different URIs in a row", function() local _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/my-route", match_t.prefix) assert.equal("/", match_t.upstream_uri) _ngx = mock_ngx("GET", "/this-route", { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/this-route", match_t.prefix) assert.equal("/", match_t.upstream_uri) _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/my-route", match_t.prefix) assert.equal("/", match_t.upstream_uri) _ngx = mock_ngx("GET", "/this-route", { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/this-route", match_t.prefix) assert.equal("/", match_t.upstream_uri) end) it("strips url encoded paths", function() local use_case_routes = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/endel%2Fst" }, strip_path = true, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/endel%2Fst", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/endel%2Fst", match_t.prefix) assert.equal("/", match_t.upstream_uri) end) it("strips a [uri regex]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/users/\d+/profile]] }, strip_path = true, }, }, } local router = assert(new_router(use_case)) local _ngx = mock_ngx("GET", "/users/123/profile/hello/world", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.equal("/users/123/profile", match_t.prefix) assert.equal("/hello/world", match_t.upstream_uri) end) it("strips a [uri regex] with a capture group", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/users/(\d+)/profile]] }, strip_path = true, }, }, } local router = assert(new_router(use_case)) local _ngx = mock_ngx("GET", "/users/123/profile/hello/world", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.equal("/users/123/profile", match_t.prefix) assert.equal("/hello/world", match_t.upstream_uri) end) end) describe("preserve Host header", function() local router local use_case_routes = { -- use the request's Host header { service = { name = "service-invalid", host = "example.org", protocol = "http" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", preserve_host = true, hosts = { "preserve.com" }, }, }, -- use the route's upstream_url's Host { service = { name = "service-invalid", host = "example.org", protocol = "http" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", preserve_host = false, hosts = { "discard.com" }, }, }, } lazy_setup(function() router = assert(new_router(use_case_routes)) end) describe("when preserve_host is true", function() local host = "preserve.com" it("uses the request's Host header", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal(host, match_t.upstream_host) end) it("uses the request's Host header incl. port", function() local _ngx = mock_ngx("GET", "/", { host = host .. ":123" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal(host .. ":123", match_t.upstream_host) end) it("does not change the target upstream", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("example.org", match_t.upstream_url_t.host) end) it("uses the request's Host header when `grab_header` is disabled", function() local use_case_routes = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", name = "route-1", preserve_host = true, paths = { "/foo" }, }, upstream_url = "http://example.org", }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/foo", { host = "preserve.com" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("preserve.com", match_t.upstream_host) end) it("uses the request's Host header if an route with no host was cached", function() -- This is a regression test for: -- https://github.com/Kong/kong/issues/2825 -- Ensure cached routes (in the LRU cache) still get proxied with the -- correct Host header when preserve_host = true and no registered -- route has a `hosts` property. local use_case_routes = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", name = "no-host", paths = { "/nohost" }, preserve_host = true, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/nohost", { host = "domain1.com" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("domain1.com", match_t.upstream_host) _ngx = mock_ngx("GET", "/nohost", { host = "domain2.com" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("domain2.com", match_t.upstream_host) end) end) describe("when preserve_host is false", function() local host = "discard.com" it("does not change the target upstream", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) assert.equal("example.org", match_t.upstream_url_t.host) end) it("does not set the host_header", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) assert.is_nil(match_t.upstream_host) end) end) end) describe("preserve Host header #grpc", function() local router local use_case_routes = { -- use the request's Host header { service = { name = "service-invalid", host = "example.org", protocol = "grpc" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", preserve_host = true, hosts = { "preserve.com" }, }, }, -- use the route's upstream_url's Host { service = { name = "service-invalid", host = "example.org", protocol = "grpc" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", preserve_host = false, hosts = { "discard.com" }, }, }, } lazy_setup(function() router = assert(new_router(use_case_routes)) end) describe("when preserve_host is true", function() local host = "preserve.com" it("uses the request's Host header", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal(host, match_t.upstream_host) assert.equal("grpc", match_t.service.protocol) end) it("uses the request's Host header incl. port", function() local _ngx = mock_ngx("GET", "/", { host = host .. ":123" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal(host .. ":123", match_t.upstream_host) assert.equal("grpc", match_t.service.protocol) end) it("does not change the target upstream", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("example.org", match_t.upstream_url_t.host) assert.equal("grpc", match_t.service.protocol) end) it("uses the request's Host header when `grab_header` is disabled", function() local use_case_routes = { { service = { name = "service-invalid", protocol = "grpc", }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", name = "route-1", preserve_host = true, paths = { "/foo" }, }, upstream_url = "http://example.org", }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/foo", { host = "preserve.com" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("preserve.com", match_t.upstream_host) assert.equal("grpc", match_t.service.protocol) end) it("uses the request's Host header if an route with no host was cached", function() -- This is a regression test for: -- https://github.com/Kong/kong/issues/2825 -- Ensure cached routes (in the LRU cache) still get proxied with the -- correct Host header when preserve_host = true and no registered -- route has a `hosts` property. local use_case_routes = { { service = { name = "service-invalid", protocol = "grpc", }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", name = "no-host", paths = { "/nohost" }, preserve_host = true, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/nohost", { host = "domain1.com" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("domain1.com", match_t.upstream_host) assert.equal("grpc", match_t.service.protocol) _ngx = mock_ngx("GET", "/nohost", { host = "domain2.com" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("domain2.com", match_t.upstream_host) assert.equal("grpc", match_t.service.protocol) end) end) describe("when preserve_host is false", function() local host = "discard.com" it("does not change the target upstream", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) assert.equal("example.org", match_t.upstream_url_t.host) assert.equal("grpc", match_t.service.protocol) end) it("does not set the host_header", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) assert.is_nil(match_t.upstream_host) assert.equal("grpc", match_t.service.protocol) end) end) end) describe("#slash handling", function() for i, line in ipairs(path_handling_tests) do for j, test in ipairs(line:expand()) do if flavor == "traditional" or test.path_handling == "v0" then local strip = test.strip_path and "on" or "off" local route_uri_or_host if test.route_path then route_uri_or_host = "uri " .. test.route_path else route_uri_or_host = "host localbin-" .. i .. "-" .. j .. ".com" end local description = string.format("(%d-%d) plain, %s with %s, strip = %s, %s. req: %s", i, j, test.service_path, route_uri_or_host, strip, test.path_handling, test.request_path) it(description, function() local use_case_routes = { { service = { protocol = "http", name = "service-invalid", path = test.service_path, }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6" .. string.format("%03d%03d", i, j), strip_path = test.strip_path, path_handling = test.path_handling, -- only add the header is no path is provided hosts = test.service_path == nil and nil or { "localbin-" .. i .. "-" .. j .. ".com" }, paths = { test.route_path }, }, } } local router = assert(new_router(use_case_routes) ) local _ngx = mock_ngx("GET", test.request_path, { host = "localbin-" .. i .. "-" .. j .. ".com" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) if flavor == "traditional" then assert.same(test.service_path, match_t.upstream_url_t.path) end assert.same(test.expected_path, match_t.upstream_uri) end) end end end -- this is identical to the tests above, except that for the path we match -- with an injected regex sequence, effectively transforming the path -- match into a regex match for i, line in ipairs(path_handling_tests) do if line.route_path then -- skip test cases which match on host for j, test in ipairs(line:expand()) do if flavor == "traditional" or test.path_handling == "v0" then local strip = test.strip_path and "on" or "off" local regex = "~/[0]?" .. test.route_path:sub(2, -1) local description = string.format("(%d-%d) regex, %s with %s, strip = %s, %s. req: %s", i, j, test.service_path, regex, strip, test.path_handling, test.request_path) it(description, function() local use_case_routes = { { service = { protocol = "http", name = "service-invalid", path = test.service_path, }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6" .. string.format("%03d%03d", i, j), strip_path = test.strip_path, -- only add the header is no path is provided path_handling = test.path_handling, hosts = { "localbin-" .. i .. ".com" }, paths = { regex }, }, } } local router = assert(new_router(use_case_routes) ) local _ngx = mock_ngx("GET", test.request_path, { host = "localbin-" .. i .. ".com" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) if flavor == "traditional" then assert.same(test.service_path, match_t.upstream_url_t.path) end assert.same(test.expected_path, match_t.upstream_uri) end) end end end end end) it("works with special characters('\"','\\')", function() local use_case_routes = { { service = { name = "service-invalid", host = "example.org", protocol = "http" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[/\d]] }, }, }, { service = { name = "service-invalid", host = "example.org", protocol = "https" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { [[~/\d+"]] }, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", [[/\d]], { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) -- upstream_url_t if flavor == "traditional" then assert.equal("http", match_t.upstream_url_t.scheme) end assert.equal("example.org", match_t.upstream_url_t.host) assert.equal(80, match_t.upstream_url_t.port) -- upstream_uri assert.is_nil(match_t.upstream_host) -- only when `preserve_host = true` assert.equal([[/\d]], match_t.upstream_uri) _ngx = mock_ngx("GET", [[/123"]], { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) -- upstream_url_t if flavor == "traditional" then assert.equal("https", match_t.upstream_url_t.scheme) end assert.equal("example.org", match_t.upstream_url_t.host) assert.equal(443, match_t.upstream_url_t.port) -- upstream_uri assert.is_nil(match_t.upstream_host) -- only when `preserve_host = true` assert.equal([[/123"]], match_t.upstream_uri) end) end) if flavor == "traditional" then describe("#stream context", function() describe("[sources]", function() local use_case, router lazy_setup(function() use_case = { -- plain { service = service, route = { sources = { { ip = "127.0.0.1" }, { ip = "127.0.0.2" }, } } }, { service = service, route = { sources = { { port = 65001 }, { port = 65002 }, } } }, -- range { service = service, route = { sources = { { ip = "127.168.0.0/8" }, } } }, -- ip + port { service = service, route = { sources = { { ip = "127.0.0.1", port = 65001 }, } } }, { service = service, route = { sources = { { ip = "127.0.0.2", port = 65300 }, { ip = "127.168.0.0/16", port = 65301 }, } } }, } router = assert(new_router(use_case)) end) it("[src_ip]", function() local match_t = router:select(nil, nil, nil, "tcp", "127.0.0.1") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select(nil, nil, nil, "tcp", "127.0.0.1") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("[src_port]", function() local match_t = router:select(nil, nil, nil, "tcp", "127.0.0.3", 65001) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("[src_ip] range match", function() local match_t = router:select(nil, nil, nil, "tcp", "127.168.0.1") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) end) it("[src_ip] + [src_port]", function() local match_t = router:select(nil, nil, nil, "tcp", "127.0.0.1", 65001) assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) end) it("[src_ip] range match + [src_port]", function() local match_t = router:select(nil, nil, nil, "tcp", "127.168.10.1", 65301) assert.truthy(match_t) assert.same(use_case[5].route, match_t.route) end) it("[src_ip] no match", function() local match_t = router:select(nil, nil, nil, "tcp", "10.0.0.1") assert.falsy(match_t) match_t = router:select(nil, nil, nil, "tcp", "10.0.0.2", 65301) assert.falsy(match_t) end) end) describe("[destinations]", function() local use_case, router lazy_setup(function() use_case = { -- plain { service = service, route = { destinations = { { ip = "127.0.0.1" }, { ip = "127.0.0.2" }, } } }, { service = service, route = { destinations = { { port = 65001 }, { port = 65002 }, } } }, -- range { service = service, route = { destinations = { { ip = "127.168.0.0/8" }, } } }, -- ip + port { service = service, route = { destinations = { { ip = "127.0.0.1", port = 65001 }, } } }, { service = service, route = { destinations = { { ip = "127.0.0.2", port = 65300 }, { ip = "127.168.0.0/16", port = 65301 }, } } }, } router = assert(new_router(use_case)) end) it("[dst_ip]", function() local match_t = router:select(nil, nil, nil, "tcp", nil, nil, "127.0.0.1") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select(nil, nil, nil, "tcp", nil, nil, "127.0.0.1") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("[dst_port]", function() local match_t = router:select(nil, nil, nil, "tcp", nil, nil, "127.0.0.3", 65001) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("[dst_ip] range match", function() local match_t = router:select(nil, nil, nil, "tcp", nil, nil, "127.168.0.1") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) end) it("[dst_ip] + [dst_port]", function() local match_t = router:select(nil, nil, nil, "tcp", nil, nil, "127.0.0.1", 65001) assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) end) it("[dst_ip] range match + [dst_port]", function() local match_t = router:select(nil, nil, nil, "tcp", nil, nil, "127.168.10.1", 65301) assert.truthy(match_t) assert.same(use_case[5].route, match_t.route) end) it("[dst_ip] no match", function() local match_t = router:select(nil, nil, nil, "tcp", nil, nil, "10.0.0.1") assert.falsy(match_t) match_t = router:select(nil, nil, nil, "tcp", nil, nil, "10.0.0.2", 65301) assert.falsy(match_t) end) end) describe("[snis]", function() local use_case, use_case_ignore_sni, router, router_ignore_sni lazy_setup(function() use_case = { { service = service, route = { snis = { "www.example.org" } } }, -- see #6425 { service = service, route = { hosts = { "sni.example.com", }, protocols = { "http", "https", }, snis = { "sni.example.com", }, }, }, { service = service, route = { hosts = { "sni.example.com", }, protocols = { "http", }, }, }, } use_case_ignore_sni = { -- see #6425 { service = service, route = { hosts = { "sni.example.com", }, protocols = { "http", "https", }, snis = { "sni.example.com", }, }, }, } router = assert(new_router(use_case)) router_ignore_sni = assert(new_router(use_case_ignore_sni)) end) it("[sni]", function() local match_t = router:select(nil, nil, nil, "tcp", nil, nil, nil, nil, "www.example.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("[sni] is ignored for http request without shadowing routes with `protocols={'http'}`. Fixes #6425", function() local match_t = router_ignore_sni:select(nil, nil, "sni.example.com", "http", nil, nil, nil, nil, nil) assert.truthy(match_t) assert.same(use_case_ignore_sni[1].route, match_t.route) match_t = router:select(nil, nil, "sni.example.com", "http", nil, nil, nil, nil, nil) assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) end) end) it("[sni] has higher priority than [src] or [dst]", function() local use_case = { { service = service, route = { snis = { "www.example.org" }, } }, { service = service, route = { sources = { { ip = "127.0.0.1" }, } } }, { service = service, route = { destinations = { { ip = "172.168.0.1" }, } } }, } local router = assert(new_router(use_case)) local match_t = router:select(nil, nil, nil, "tcp", "127.0.0.1", nil, nil, nil, "www.example.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select(nil, nil, nil, "tcp", nil, nil, "172.168.0.1", nil, "www.example.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("[src] + [dst] has higher priority than [sni]", function() local use_case = { { service = service, route = { snis = { "www.example.org" }, } }, { service = service, route = { sources = { { ip = "127.0.0.1" }, }, destinations = { { ip = "172.168.0.1" }, } } }, } local router = assert(new_router(use_case)) local match_t = router:select(nil, nil, nil, "tcp", "127.0.0.1", nil, "172.168.0.1", nil, "www.example.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) end) end end) end describe("[both regex and prefix with regex_priority]", function() local use_case, router lazy_setup(function() use_case = { -- regex { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/.*" }, hosts = { "domain-1.org", }, }, }, -- prefix { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/" }, hosts = { "domain-2.org", }, regex_priority = 5 }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", paths = { "/v1" }, hosts = { "domain-2.org", }, }, }, } router = assert(new_router(use_case)) end) it("[prefix matching ignore regex_priority]", function() local match_t = router:select("GET", "/v1", "domain-2.org") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) end) end)