Skip to content

Latest commit

 

History

History
757 lines (562 loc) · 20.4 KB

README.md

File metadata and controls

757 lines (562 loc) · 20.4 KB

lua-resty-route

lua-resty-route is a URL routing library for OpenResty supporting multiple route matchers, middleware, and HTTP and WebSockets handlers to mention a few of its features.

Matchers

lua-resty-route supports multiple different matchers on routing. Right now we support these:

  • Prefix (case-sensitive and case-insensitive)
  • Equals (case-sensitive and case-insensitive)
  • Match (using Lua's string.match function)
  • Regex (case-sensitive and case-insensitive)
  • Simple (case-sensitive and case-insensitive)

Matcher is selected by a prefix in a route's pattern, and they do somewhat follow the Nginx's location block prefixes:

Prefix Matcher Case-sensitive Used by Default
[none] Prefix
* Prefix
= Equals
=* Equals
# Match ¹
~ Regex
~* Regex
@ Simple
@* Simple

¹ Lua string.match can be case-sensitive or case-insensitive.

Prefix Matcher

Prefix, as the name tells, matches only the prefix of the actual location. Prefix matcher takes only static string prefixes. If you need anything more fancy, take a look at regex matcher. Prefix can be matched case-insensitively by prefixing the prefix with *, :-). Let's see this in action:

route "/users" (function(self) end)

This route matches locations like:

  • /users
  • /users/edit
  • /users_be_aware

But it doesn't match location paths like:

  • /Users
  • /USERS/EDIT

But those can be still be matched in case-insensitive way:

route "*/users" (function(self) end)

Equals Matcher

This works the same as the prefix matcher, but with this we match the exact location, to use this matcher, prefix the route with =:

route "=/users" {
    get = function(self) end
}

This route matches only this location:

  • /users

Case-insensitive variant can be used also:

route "=*/users" {
    get = function(self) end
}

And this of course matches locations like:

  • /users
  • /USERS
  • /usErs

Match Matcher

This matcher matches patters using Lua's string.match function. Nice thing about this matcher is that it accepts patterns and also provides captures. Check Lua's documentation about possible ways to define patterns. Here are some examples:

route "#/files/(%w+)[.](%w+)" {
    get = function(self, file, ext) end
}

This will match location paths like:

  • /files/test.txt etc.

In that case the provided function (that answers only HTTP GET requests in this example), will be called also with these captures: "test" (function argument file) and txt (function argument ext).

For many, the regular expressions are more familiar and more powerfull. That is what we will look next.

Regex Matcher

Regex or regular expressions is a common way to do pattern matching. OpenResty has support for PCRE compatible regualar expressions, and this matcher in particular, uses ngx.re.match function:

route [[~^/files/(\w+)[.](\w+)$]] {
    get = function(self, file, ext) end
}

As with the Match matcher example above, the end results are the same and the function will be called with the captures.

For Regex matcher we also have case-insensitive version:

route [[~*^/files/(\w+)[.](\w+)$]] {
    get = function(self, file, ext) end
}

Simple Matcher

This matcher is a specialized and limited version of a Regex matcher with one advantage. It handles type conversions automatically, right now it only supports integer conversion to Lua number. For example:

route:get "@/users/:number" (function(self, id) end)

You could have location path like:

  • /users/45

The function above will get 45 as a Lua number.

Supported simple capturers are:

  • :string, that is equal to this regex [^/]+ (one or more chars, not including /)
  • :number, that is equal to this regex \d+ (one or more digits that can be turned to Lua number using tonumber function)

In future, we may add other capture shortcuts.

Of course there is a case-insensitive version for this matcher as well:

route:get "@*/users/:number" (function(self, id) end)

The simple matcher always matches the location from the beginning to end (partial matches are not considered).

Routing

There are many different ways to define routes in lua-resty-route. It can be said that it is somewhat a Lua DSL for defining routes.

To define routes, you first need a new instance of route. This instance can be shared with different requests. You may create the routes in init_by_lua*. Here we define a new route instance:

local route = require "resty.route".new()

Now that we do have this route instance, we may continue to a next section, HTTP Routing.

Note: Routes are tried in the order they are added when dispatched. This differs from how Nginx itself handles the location blocks.

HTTP Routing

HTTP routing is the most common thing to do in web related routing. That's why HTTP routing is the default way to route in lua-resty-route. Other types of routing include e.g. WebSockets routing.

The most common HTTP request methods (sometimes referred to as verbs) are:

Method Definition
GET Read
POST Create
PUT Update or Replace
PATCH Update or Modify
DELETE Delete

While these are the most common ones, lua-resty-route is not by any means restricted to these. You may use whatever request methods there is just like these common ones. But to keep things simple here, we will just use these in the examples.

The General Pattern in Routing

route(...)
route:method(...)

or

route(method, pattern, func)
route:method(pattern, func)

e.g.:

route("get", "/", function(self) end)
route:get("/", function(self) end)

Only the first function argument is mandatory. That's why we can call these functions in a quite flexible ways. For some methods, e.g. websocket, we can pass a table instead of a function as a route handler. Next we look at different ways to call these functions.

Defining Routes as a Table

route "=/users" {
    get  = function(self) end,
    post = function(self) end
}
local users = {
    get  = function(self) end,
    post = function(self) end
}
route "=/users" (users)
route("=/users", users)

Using Lua Packages for Routing

route "=/users"  "controllers.users"
route("=/users", "controllers.users")

These are same as:

route("=/users", require "controllers.users")

Defining Multiple Methods at Once

route { "get", "head" } "=/users" (function(self) end)

Defining Multiple Routes at Once

route {
    ["/"] = function(self) end,
    ["=/users"] = {
        get  = function(self) end,
        post = function(self) end
    }
}

Routing all the HTTP Request Methods

route "/" (function(self) end)
route("/", function(self) end)

The Catch all Route

route(function(self) end)

Going Crazy with Routing

route:as "@home" (function(self) end)
route {
    get = {
        ["=/"] = "@home",
        ["=/users"] = function(self) end
    },
    ["=/help"] = function(self) end,
    [{ "post", "put"}] = {
        ["=/me"] = function(self)
        end
    },
    ["=/you"] = {
        [{ "get", "head" }] = function(self) end
    },
    [{ "/files", "/cache" }] = {
        -- requiring controllers.filesystem returns a function
        [{"get", "head" }] = "controllers.filesystem"
    }
}

As you may see this is pretty freaky. But it doesn't actually stop here. I haven't even mentioned things like callable Lua tables (aka tables with metamethod __call) or web sockets routing. They are supported as well.

WebSockets Routing

File System Routing

File system routing is based on a file system tree. This could be considered as a routing by a convention. File system routing depends on either LuaFileSystem module or a preferred and LFS compatible ljsyscall.

As an example, let's consider that we do have this kind of file tree:

/routing/
 ├─ index.lua 
 ├─ users.lua
 └─ users/
 │  ├─ [email protected]
 │  ├─ [email protected]
 │  └─ #/
 │     └─ index.lua
 └─ page/
    └─ #.lua

This file tree will provide you with the following routes:

  • @*/index.lua
  • @*/usersusers.lua
  • @*/users/viewusers/[email protected] (only GET requests are routed here)
  • @*/users/editusers/[email protected] (only POST requests are routed here)
  • @*/users/:numberusers/#/index.lua
  • @*/page/:numberpage/#.lua

The files could look like this (just an example):

index.lua:

return {
    get  = function(self) end,
    post = function(self) end
}

users.lua:

return {
    get    = function(self) end,
    post   = function(self) end,
    delete = function(self) end
}   

users/[email protected]:

return function(self) end

users/[email protected]:

return function(self) end

users/#/index.lua:

return {
    get    = function(self, id) end,
    put    = function(self, id) end,
    post   = function(self, id) end,
    delete = function(self, id) end
}

page/#.lua:

return {
    get    = function(self, id) end,
    put    = function(self, id) end,
    post   = function(self, id) end,
    delete = function(self, id) end
}

To define routes based on file system tree you will need to call route:fs function:

-- Here we assume that you do have /routing directory
-- on your file system. You may use whatever path you
-- like, absolute or relative.
route:fs "/routing"

Using file system routing you can just add new files to file system tree, and they will be added automatically as a routes.

Named Routes

You can define named route handlers, and then reuse them in actual routes.

route:as "@home" (function(self) end)

(the use of @ as a prefix for a named route is optional)

And here we actually attach it to a route:

route:get "/" "@home"

You can also define multiple named routes in a one go:

route:as {
    home    = function(self) end,
    signin  = function(self) end,
    signout = function(self) end
}

or if you want to use prefixes:

route:as {
    ["@home"]    = function(self) end,
    ["@signin"]  = function(self) end,
    ["@signout"] = function(self) end
}

Named routes must be defined before referencing them in routes. There are or will be other uses to named routers as well. On todo list there are things like reverse routing and route forwarding to a named route.

Middleware

Middleware in lua-resty-route can be defined on either on per request or per route basis. Middleware are filters that you can add to the request processing pipeline. As lua-resty-route tries to be as unopionated as possible we don't really restrict what the filters do or how they have to be written. Middleware can be inserted just flexible as routes, and they actually do share much of the logic. With one impotant difference. You can have multiple middleware on the pipeline whereas only one matchin route will be executed. The middleware can also be yielded (coroutine.yield), and that allows code to be run before and after the router (you can yield a router as well, but that will never be resumed). If you don't yield, then the middleware is considered as a before filter.

The most common type of Middleware is request level middleware:

route:use(function(self)
    -- This code will be run before router:
    -- ...
    self.yield() -- or coroutine.yield()
    -- This code will be run after the router:
    -- ...
end)

Now, as you were already hinted, you may add filters to specific routes as well:

route.filter "=/" (function(self)
    -- this middleware will only be called on a specific route
end)

You can use the same rules as with routing there, e.g.

route.filter:post "middleware.csrf"

Of course you can also do things like:

route.filter:delete "@/users/:number" (function(self, id)
    -- here we can say prevent deleting the user who
    -- issued the request or something.
end)

All the matching middleware is run on every request, unless one of them decides to exit, but we do always try to run after filters for those middleware that already did run, and yielded. But we will call them in reverse order:

  1. middleware 1 runs and yields
  2. middleware 2 runs (and finishes)
  3. middleware 3 runs and yields
  4. router runs
  5. middleware 3 resumes
  6. middleware 1 resumes

The order of middleware is by scope:

  1. request level middleware is executed first
  2. router level middleware is executed second

If there are multiple requet or router level middleware, then they will be executed the same order they were added to a specific scope. Yielded middleware is executed in reverse order. Yielded middleware will only be resumed once.

Internally we do use Lua's great coroutines.

We are going to support a bunch of predefined middleware in a future.

Events

Events allow you to register specialized handlers for different HTTP status codes or other predefined event codes. There can be only one handler for each code or code group.

You can for example define 404 aka route not found handler like this:

route:on(404, function(self) end)

Some groups are predefined, e.g.:

  • info, status codes 100 – 199
  • success, status codes 200 – 299
  • redirect, status codes 300 – 399
  • client error, status codes 400 – 499
  • server error, status codes 500 – 599
  • error, status codes 400 – 599

You may use groups like this:

route:on "error" (function(self, code) end)

You can also define multiple event handlers in a one go:

route:on {
    error   = function(self, code) end,
    success = function(self, code) end,
    [302]   = function(self) end
}

Then there is a generic catch-all event handler:

route:on(function(self, code) end)

We will find the right event handler in this order:

  1. if there is a specific handler for a specific code, we will call that
  2. if there is a group handler for specific code, we will call that
  3. if there is a catch-all handler, we will call that

Only one of these is called per event.

It is possible that we will add other handlers in a future where you could hook on.

Router API

You may have seen in previous examples functions get as a first parameter a self. The self represents a router that contains many nice functions documented below.

While the above so called Route API is for defining the routes, the Router API is actually about running the routes.

router.context

This is really powerful concept here to share data between different routes and functions. Many middleware will be inserted to context.

E.g. a redis middleware could add redis object to context so that you could just:

local ok, err = self.redis:set("cat", "tommy")

Opening and closing the Redis connection is something that the middleware does automatically before scenes. It means that you don't need to initiate or close the connections to Redis server, but this small framework takes care of this. As you see, this self parameter is automatically passed around different layers of this framework, and this context makes it easy to pass data between them.

router.yield()

Is similar to coroutine.yield() but as you have seen above in middlewares section, it is quite nice to just call self.yield() instead to split middleware to before and after filters, it also makes us possible to add e.g. debugging / profiling code in a future. self.yield() is more self explaining what happens and makes code easier to read (may be subjective opinion).

router:redirect(uri, code)

Similar to ngx.redirect but runs redirect event handler and after filters before actually calling ngx.redirect with code (or ngx.HTTP_MOVED_TEMPORARILY if not specified) and ending the handler.

router:exit(uri, code)

Similar to ngx.exit but runs event handler and after filters before actually calling ngx.exit with code (or ngx.OK if not specified) and ending the handler.

router:exec(uri, args)

Similar to ngx.exec but runs event handler and after filters before actually calling ngx.exec and ending the handler.

router:done()

Similar to ngx.exit with ngx.HTTP_OK but runs event handler and after filters before actually calling ngx.exit and ending the handler.

router:abort()

This is reserved for ngx.on_abort usage (NYI). Right now only calls ngx.exit(499) after running event handler and after filters.

router:fail(error, code)

If error is a string, then logs it to error log. Otherwise it is similar to ngx.exit(code) (by default the code is ngx.HTTP_INTERNAL_SERVER_ERROR) but runs event handler and after filters before actually calling ngx.exitand ending the handler.

router:to(location, method)

Allows you to execute another route (defined by route).

router:render(content, context)

Writes content to output stream. If there is a context.template then it will call context.template.render(content, context or self.context).

router:json(data)

Encodes data as JSON, adds application/json content-type header and outputs the JSON.

router:*

A lot more can be added here to make writing code less repetive, but a lot can be done with injecting into self.context as well.

Roadmap

This is a small collection of ideas that may or may not be implemented as a part of lua-resty-route.

  • Add documentation
  • Add tests
  • Rewrite current middleware and add new ones
  • Rewrite current websocket handler
  • Add route statistics
  • Add an automatic route cleaning and redirecting (possibly configurable) (clean function is already written)
  • Add an automatic slash handling and redirecting (possibly configurable)
  • Add a more automated way to define redirects
  • Add a support for route caching
  • Add a support to route by host
  • Add a support to route by headers
  • Add a support for Nginx phases
  • Add a support for easy way to define Web Hooks routes
  • Add a support for easy way to define Server Sent Events routes
  • Add a support for "provides", e.g. renderers (?)
  • Add a support for conditions, e.g. content negotiation
  • Add a support for route grouping (already possible on Nginx at config level)
  • Add a support for reverse routing
  • Add a support for form method spoofing
  • Add a support for client connection abort event handler (ngx.on_abort)
  • Add a support for host (and possibly) other headers filtering
  • Add a support for basic authentication
  • Add a support for JWT / OpenID Connect authentication
  • Add bootstrapping functionality from Nginx configs
  • Add support for resources (or view sets) (a more automated REST-routing)
  • Add filesystem routing support for resources (or view sets)

See Also

License

lua-resty-route uses two clause BSD license.

Copyright (c) 2015 – 2017, Aapo Talvensaari
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice, this
  list of conditions and the following disclaimer in the documentation and/or
  other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES`