Skip to content

Commit

Permalink
feat: add hosts condition (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
vm-001 authored Jan 3, 2024
1 parent bf9a5de commit 1f56886
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 9 deletions.
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,14 @@ local router, err = Router.new(routes)

Route defines the matching conditions for its handler.

| PROPERTY | DESCRIPTION |
| ----------------------------- | ------------------------------------------------------------ |
| `paths` *required\** | The path list of matching condition. |
| `methods` *optional* | The method list of matching condition. |
| `handler` *required\** | The value of handler will be returned by `router:match()` when the route is matched. |
| `priority` *optional* | The priority of the route in case of radix tree node conflict. |
| `expression` *optional* (TDB) | The `expression` defines a customized matching condition by using expression language. |
| PROPERTY | DESCRIPTION |
|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `paths`</br> *required\** | A list of paths that match the Route.</br> |
| `methods`</br> *optional* | A list of HTTP methods that match the Route. </br> |
| `hosts`</br> *optional* | A list of hostnames that match the Route. Note that the value is case-sensitive. Wildcard hostnames are supported. For example, `*.foo.com` can match with `a.foo.com` or `a.b.foo.com`. |
| `handler`</br> *required\** | The value of handler will be returned by `router:match()` when the route is matched. |
| `priority`</br> *optional* | The priority of the route in case of radix tree node conflict. |
| `expression`</br> *optional* (TDB) | The `expression` defines a customized matching condition by using expression language. |



Expand Down
66 changes: 66 additions & 0 deletions spec/router_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,72 @@ describe("Router", function()
assert.equal("6", router:match("/prefix"))
end)
end)
describe("hosts", function()
it("exact host", function()
local router = Router.new({
{
paths = { "/single-host" },
handler = "1",
hosts = { "example.com" }
},
{
paths = { "/multiple-hosts" },
handler = "2",
hosts = { "foo.com", "bar.com" }
},
})
assert.equal("1", router:match("/single-host", { host = "example.com" }))
assert.equal(nil, router:match("/single-host", { host = "www.example.com" }))
assert.equal(nil, router:match("/single-host", { host = "example1.com" }))
assert.equal(nil, router:match("/single-host", { host = ".com" }))

assert.equal("2", router:match("/multiple-hosts", { host = "foo.com" }))
assert.equal("2", router:match("/multiple-hosts", { host = "bar.com" }))
assert.equal(nil, router:match("/multiple-hosts", { host = "example.com" }))
end)
it("wildcard host", function()
local router = Router.new({
{
paths = { "/" },
handler = "1",
hosts = { "*.example.com" }
},
{
paths = { "/" },
handler = "2",
hosts = { "www.example.*" }
},
{
paths = { "/multiple-hosts" },
handler = "3",
hosts = { "foo.com", "*.foo.com", "*.bar.com" }
},
})

assert.equal("1", router:match("/", { host = "www.example.com" }))
assert.equal("1", router:match("/", { host = "foo.bar.example.com" }))
assert.equal(nil, router:match("/", { host = ".example.com" }))

assert.equal("2", router:match("/", { host = "www.example.org" }))
assert.equal("2", router:match("/", { host = "www.example.foo.bar" }))
assert.equal(nil, router:match("/", { host = "www.example." }))

assert.equal("3", router:match("/multiple-hosts", { host = "foo.com" }))
assert.equal("3", router:match("/multiple-hosts", { host = "www.foo.com" }))
assert.equal("3", router:match("/multiple-hosts", { host = "www.bar.com" }))
end)
it("host value is case-sensitive", function()
local router = Router.new({
{
paths = { "/" },
hosts = { "example.com" },
handler = "1",
}
})
assert.equal("1", router:match("/", { host = "example.com" }))
assert.equal(nil, router:match("/", { host = "examplE.com" }))
end)
end)
end)
describe("match with params binding", function()
it("sanity", function()
Expand Down
15 changes: 15 additions & 0 deletions spec/utils_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ local utils = require "radix-router.utils"

describe("utils", function()
it("starts_with()", function()
assert.is_true(utils.starts_with("/abc", ""))
assert.is_true(utils.starts_with("/abc", "/"))
assert.is_true(utils.starts_with("/abc", "/a"))
assert.is_true(utils.starts_with("/abc", "/ab"))
Expand All @@ -12,6 +13,20 @@ describe("utils", function()
assert.is_false(utils.starts_with("/abc", "/abcd"))
assert.is_false(utils.starts_with("/abc", "/d"))
end)
it("ends_with()", function()
assert.is_true(utils.ends_with("/abc", ""))
assert.is_true(utils.ends_with("/abc", "c"))
assert.is_true(utils.ends_with("/abc", "bc"))
assert.is_true(utils.ends_with("/abc", "abc"))
assert.is_true(utils.ends_with("/abc", "/abc"))

assert.is_false(utils.ends_with("/abc", "0abc"))
assert.is_false(utils.ends_with("/abc", "d"))

assert.is_true(utils.ends_with("example.com", ".com"))
assert.is_true(utils.ends_with("example.com", "*.com", nil, nil, 1))
assert.is_true(utils.ends_with("example.com", "*.com", nil, nil, 5))
end)
it("lcp()", function()
assert.equal(0, utils.lcp("/abc", ""))
assert.equal(1, utils.lcp("/abc", "/"))
Expand Down
64 changes: 64 additions & 0 deletions src/route.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ local bit = utils.is_luajit and require "bit"

local ipairs = ipairs
local str_byte = string.byte
local starts_with = utils.starts_with
local ends_with = utils.ends_with

local BYTE_SLASH = str_byte("/")
local BYTE_ASTERISK = str_byte("*")
local is_luajit = utils.is_luajit
local METHODS = {}
do
Expand Down Expand Up @@ -60,6 +63,30 @@ function Route.new(route, _)
self.method = is_luajit and methods_bit or methods
end

-- route.hosts
if route.hosts then
local hosts = { [0] = 0 }
for _, host in ipairs(route.hosts) do
local host_n = #host
local wildcard_n = 0
for n = 1, host_n do
if str_byte(host, n) == BYTE_ASTERISK then
wildcard_n = wildcard_n + 1
end
end
if wildcard_n > 1 then
return nil, "invalid host"
elseif wildcard_n == 1 then
local n = hosts[0] + 1
hosts[0] = n
hosts[n] = host -- wildcard host
else
hosts[host] = true
end
end
self.hosts = hosts
end

return setmetatable(self, mt)
end

Expand All @@ -81,6 +108,43 @@ function Route:is_match(ctx)
end
end

if self.hosts then
local host = ctx.host
if not host then
return false
end
if not self.hosts[host] then
if self.hosts[0] == 0 then
return false
end

local wildcard_match = false
local host_n = #host
for i = 1, self.hosts[0] do
local wildcard_host = self.hosts[i]
local wildcard_host_n = #wildcard_host
if host_n >= wildcard_host_n then
if str_byte(wildcard_host) == BYTE_ASTERISK then
-- case *.example.com
if ends_with(host, wildcard_host, host_n, wildcard_host_n, 1) then
wildcard_match = true
break
end
else
-- case example.*
if starts_with(host, wildcard_host, host_n, wildcard_host_n - 1) then
wildcard_match = true
break
end
end
end
end
if not wildcard_match then
return false
end
end
end

return true
end

Expand Down
45 changes: 43 additions & 2 deletions src/utils.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local str_byte = string.byte
local math_min = math.min
local type = type

local is_luajit = type(_G.jit) == "table"

Expand All @@ -26,6 +27,7 @@ end


local starts_with
local ends_with
do
if is_luajit then
local ffi = require "ffi"
Expand All @@ -48,6 +50,22 @@ do
local rc = C.memcmp(str, prefix, prefixn)
return rc == 0
end
ends_with = function(str, suffix, strn, suffixn, suffix_skip)
strn = strn or #str
suffix_skip = suffix_skip or 0
suffixn = (suffixn or #suffix) - suffix_skip

if suffixn == 0 then
return true
end

if strn < suffixn then
return false
end

local rc = C.memcmp(ffi.cast("char *", str) + strn - suffixn, ffi.cast("char *", suffix) + suffix_skip, suffixn)
return rc == 0
end
else
local str_sub = string.sub
starts_with = function(str, prefix, strn, prefixn)
Expand All @@ -61,7 +79,29 @@ do
if strn < prefixn then
return false
end
return str_sub(str, 1, prefixn) == prefix

for i = 1, prefixn do
if str_byte(str, i) ~= str_byte(prefix, i) then
return false
end
end

return true
end
ends_with = function(str, suffix, strn, suffixn, suffix_skip)
strn = strn or #str
suffix_skip = suffix_skip or 0
suffixn = (suffixn or #suffix) - suffix_skip

if suffixn == 0 then
return true
end

if strn < suffixn then
return false
end

return str_sub(str, -suffixn) == str_sub(suffix, 1 + suffix_skip)
end
end
end
Expand Down Expand Up @@ -94,8 +134,9 @@ end
return {
lcp = lcp,
starts_with = starts_with,
ends_with = ends_with,
clear_table = clear_table,
new_table = new_table,
is_luajit = is_luajit,
readonly = readonly
readonly = readonly,
}

0 comments on commit 1f56886

Please sign in to comment.