├── .luacheckrc ├── LICENSE ├── Makefile ├── README.md └── lib └── resty ├── route.lua └── route ├── handlers ├── http.lua ├── sse.lua └── websocket.lua ├── matchers ├── equals.lua ├── match.lua ├── prefix.lua ├── regex.lua └── simple.lua ├── middleware ├── ajax.lua ├── form.lua ├── pjax.lua ├── redis.lua ├── reqargs.lua └── template.lua └── router.lua /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "ngx_lua" 2 | 3 | globals = { 4 | "table.unpack", 5 | } 6 | 7 | self = false 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 – 2017, Aapo Talvensaari 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint 2 | 3 | lint: 4 | @luacheck -q ./lib/resty 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-resty-route 2 | 3 | **lua-resty-route** is a URL routing library for OpenResty supporting 4 | multiple route matchers, middleware, and HTTP and WebSockets handlers 5 | to mention a few of its features. 6 | 7 | ## Matchers 8 | 9 | `lua-resty-route` supports multiple different matchers on routing. Right now 10 | we support these: 11 | 12 | * Prefix (case-sensitive and case-insensitive) 13 | * Equals (case-sensitive and case-insensitive) 14 | * Match (using Lua's `string.match` function) 15 | * Regex (case-sensitive and case-insensitive) 16 | * Simple (case-sensitive and case-insensitive) 17 | 18 | Matcher is selected by a prefix in a route's pattern, and they do somewhat 19 | follow the Nginx's `location` block prefixes: 20 | 21 | Prefix | Matcher | Case-sensitive | Used by Default 22 | ---------|---------|----------------|---------------- 23 | `[none]` | Prefix | ✓ | ✓ 24 | `*` | Prefix | | 25 | `=` | Equals | ✓ | 26 | `=*` | Equals | | 27 | `#` | Match | ¹ | 28 | `~` | Regex | ✓ | 29 | `~*` | Regex | | 30 | `@` | Simple | ✓ | 31 | `@*` | Simple | | 32 | 33 | ¹ Lua `string.match` can be case-sensitive or case-insensitive. 34 | 35 | ### Prefix Matcher 36 | 37 | Prefix, as the name tells, matches only the prefix of the actual location. 38 | Prefix matcher takes only static string prefixes. If you need anything more 39 | fancy, take a look at regex matcher. Prefix can be matched case-insensitively 40 | by prefixing the prefix with `*`, :-). Let's see this in action: 41 | 42 | ```lua 43 | route "/users" (function(self) end) 44 | ``` 45 | 46 | This route matches locations like: 47 | 48 | * `/users` 49 | * `/users/edit` 50 | * `/users_be_aware` 51 | 52 | But it **doesn't** match location paths like: 53 | 54 | * `/Users` 55 | * `/USERS/EDIT` 56 | 57 | But those can be still be matched in case-insensitive way: 58 | 59 | ```lua 60 | route "*/users" (function(self) end) 61 | ``` 62 | 63 | ### Equals Matcher 64 | 65 | This works the same as the prefix matcher, but with this 66 | we match the exact location, to use this matcher, prefix 67 | the route with `=`: 68 | 69 | ```lua 70 | route "=/users" { 71 | get = function(self) end 72 | } 73 | ``` 74 | 75 | This route matches only this location: 76 | 77 | * `/users` 78 | 79 | 80 | Case-insensitive variant can be used also: 81 | 82 | ```lua 83 | route "=*/users" { 84 | get = function(self) end 85 | } 86 | ``` 87 | 88 | And this of course matches locations like: 89 | 90 | * `/users` 91 | * `/USERS` 92 | * `/usErs` 93 | 94 | ### Match Matcher 95 | 96 | This matcher matches patters using Lua's `string.match` function. Nice 97 | thing about this matcher is that it accepts patterns and also provides 98 | captures. Check Lua's documentation about possible ways to define 99 | [patterns](https://www.lua.org/manual/5.1/manual.html#5.4.1). Here are 100 | some examples: 101 | 102 | ```lua 103 | route "#/files/(%w+)[.](%w+)" { 104 | get = function(self, file, ext) end 105 | } 106 | ``` 107 | 108 | This will match location paths like: 109 | 110 | * `/files/test.txt` etc. 111 | 112 | In that case the provided function (that answers only HTTP `GET` 113 | requests in this example), will be called also with these captures: 114 | `"test"` (function argument `file`) and `txt` (function argument `ext`). 115 | 116 | For many, the regular expressions are more familiar and more powerfull. 117 | That is what we will look next. 118 | 119 | ### Regex Matcher 120 | 121 | Regex or regular expressions is a common way to do pattern matching. 122 | OpenResty has support for PCRE compatible regualar expressions, and 123 | this matcher in particular, uses `ngx.re.match` function: 124 | 125 | ```lua 126 | route [[~^/files/(\w+)[.](\w+)$]] { 127 | get = function(self, file, ext) end 128 | } 129 | ``` 130 | 131 | As with the Match matcher example above, the end results are the same 132 | and the function will be called with the captures. 133 | 134 | For Regex matcher we also have case-insensitive version: 135 | 136 | ```lua 137 | route [[~*^/files/(\w+)[.](\w+)$]] { 138 | get = function(self, file, ext) end 139 | } 140 | ``` 141 | 142 | ### Simple Matcher 143 | 144 | This matcher is a specialized and limited version of a Regex matcher 145 | with one advantage. It handles type conversions automatically, right 146 | now it only supports integer conversion to Lua number. For example: 147 | 148 | ```lua 149 | route:get "@/users/:number" (function(self, id) end) 150 | ``` 151 | 152 | You could have location path like: 153 | 154 | * `/users/45` 155 | 156 | The function above will get `45` as a Lua `number`. 157 | 158 | Supported simple capturers are: 159 | 160 | * `:string`, that is equal to this regex `[^/]+` (one or more chars, not including `/`) 161 | * `:number`, that is equal to this regex `\d+` (one or more digits that can be turned to Lua number using `tonumber` function) 162 | 163 | In future, we may add other capture shortcuts. 164 | 165 | Of course there is a case-insensitive version for this matcher as well: 166 | 167 | ```lua 168 | route:get "@*/users/:number" (function(self, id) end) 169 | ``` 170 | 171 | The simple matcher always matches the location from the beginning to end (partial 172 | matches are not considered). 173 | 174 | ## Routing 175 | 176 | There are many different ways to define routes in `lua-resty-route`. 177 | It can be said that it is somewhat a Lua DSL for defining routes. 178 | 179 | To define routes, you first need a new instance of route. This instance 180 | can be shared with different requests. You may create the routes in 181 | `init_by_lua*`. Here we define a new route instance: 182 | 183 | ```lua 184 | local route = require "resty.route".new() 185 | ``` 186 | 187 | Now that we do have this `route` instance, we may continue to a next 188 | section, [HTTP Routing](#http-routing). 189 | 190 | **Note:** Routes are tried in the order they are added when dispatched. 191 | This differs from how Nginx itself handles the `location` blocks. 192 | 193 | ### HTTP Routing 194 | 195 | HTTP routing is the most common thing to do in web related routing. That's 196 | why HTTP routing is the default way to route in `lua-resty-route`. Other 197 | types of routing include e.g. [WebSockets routing](#websockets-routing). 198 | 199 | The most common HTTP request methods (sometimes referred to as verbs) are: 200 | 201 | Method | Definition 202 | ---------|----------- 203 | `GET` | Read 204 | `POST` | Create 205 | `PUT` | Update or Replace 206 | `PATCH` | Update or Modify 207 | `DELETE` | Delete 208 | 209 | While these are the most common ones, `lua-resty-route` is not by any means 210 | restricted to these. You may use whatever request methods there is just like 211 | these common ones. But to keep things simple here, we will just use these in 212 | the examples. 213 | 214 | #### The General Pattern in Routing 215 | 216 | ```lua 217 | route(...) 218 | route:method(...) 219 | ``` 220 | 221 | or 222 | 223 | ```lua 224 | route(method, pattern, func) 225 | route:method(pattern, func) 226 | ``` 227 | 228 | e.g.: 229 | 230 | ```lua 231 | route("get", "/", function(self) end) 232 | route:get("/", function(self) end) 233 | ``` 234 | 235 | Only the first function argument is mandatory. That's why we can 236 | call these functions in a quite flexible ways. For some `methods`, 237 | e.g. websocket, we can pass a `table` instead of a `function` as 238 | a route handler. Next we look at different ways to call these 239 | functions. 240 | 241 | #### Defining Routes as a Table 242 | 243 | ```lua 244 | route "=/users" { 245 | get = function(self) end, 246 | post = function(self) end 247 | } 248 | local users = { 249 | get = function(self) end, 250 | post = function(self) end 251 | } 252 | route "=/users" (users) 253 | route("=/users", users) 254 | ``` 255 | 256 | #### Using Lua Packages for Routing 257 | 258 | ```lua 259 | route "=/users" "controllers.users" 260 | route("=/users", "controllers.users") 261 | ``` 262 | 263 | These are same as: 264 | 265 | ```lua 266 | route("=/users", require "controllers.users") 267 | ``` 268 | 269 | #### Defining Multiple Methods at Once 270 | 271 | ```lua 272 | route { "get", "head" } "=/users" (function(self) end) 273 | ``` 274 | 275 | #### Defining Multiple Routes at Once 276 | 277 | ```lua 278 | route { 279 | ["/"] = function(self) end, 280 | ["=/users"] = { 281 | get = function(self) end, 282 | post = function(self) end 283 | } 284 | } 285 | ``` 286 | 287 | #### Routing all the HTTP Request Methods 288 | 289 | ```lua 290 | route "/" (function(self) end) 291 | route("/", function(self) end) 292 | ``` 293 | 294 | #### The Catch all Route 295 | 296 | ```lua 297 | route(function(self) end) 298 | ``` 299 | 300 | #### Going Crazy with Routing 301 | 302 | ```lua 303 | route:as "@home" (function(self) end) 304 | route { 305 | get = { 306 | ["=/"] = "@home", 307 | ["=/users"] = function(self) end 308 | }, 309 | ["=/help"] = function(self) end, 310 | [{ "post", "put"}] = { 311 | ["=/me"] = function(self) 312 | end 313 | }, 314 | ["=/you"] = { 315 | [{ "get", "head" }] = function(self) end 316 | }, 317 | [{ "/files", "/cache" }] = { 318 | -- requiring controllers.filesystem returns a function 319 | [{"get", "head" }] = "controllers.filesystem" 320 | } 321 | } 322 | ``` 323 | 324 | As you may see this is pretty freaky. But it doesn't actually 325 | stop here. I haven't even mentioned things like callable Lua 326 | tables (aka tables with metamethod `__call`) or web sockets 327 | routing. They are supported as well. 328 | 329 | ### WebSockets Routing 330 | 331 | ### File System Routing 332 | 333 | File system routing is based on a file system tree. This could be 334 | considered as a routing by a convention. File system routing depends 335 | on either [LuaFileSystem](https://github.com/keplerproject/luafilesystem) 336 | module or a preferred and LFS compatible 337 | [ljsyscall](https://github.com/justincormack/ljsyscall). 338 | 339 | As an example, let's consider that we do have this kind of file tree: 340 | 341 | ``` 342 | /routing/ 343 | ├─ index.lua 344 | ├─ users.lua 345 | └─ users/ 346 | │ ├─ view@get.lua 347 | │ ├─ edit@post.lua 348 | │ └─ #/ 349 | │ └─ index.lua 350 | └─ page/ 351 | └─ #.lua 352 | ``` 353 | 354 | This file tree will provide you with the following routes: 355 | 356 | - `@*/` → `index.lua` 357 | - `@*/users` → `users.lua` 358 | - `@*/users/view` → `users/view@get.lua` (only GET requests are routed here) 359 | - `@*/users/edit` → `users/edit@post.lua` (only POST requests are routed here) 360 | - `@*/users/:number` → `users/#/index.lua` 361 | - `@*/page/:number` → `page/#.lua` 362 | 363 | The files could look like this (just an example): 364 | 365 | `index.lua`: 366 | 367 | ```lua 368 | return { 369 | get = function(self) end, 370 | post = function(self) end 371 | } 372 | ``` 373 | 374 | `users.lua`: 375 | 376 | ```lua 377 | return { 378 | get = function(self) end, 379 | post = function(self) end, 380 | delete = function(self) end 381 | } 382 | ``` 383 | 384 | `users/view@get.lua`: 385 | 386 | ```lua 387 | return function(self) end 388 | ``` 389 | 390 | `users/edit@post.lua`: 391 | 392 | ```lua 393 | return function(self) end 394 | ``` 395 | 396 | `users/#/index.lua`: 397 | 398 | ```lua 399 | return { 400 | get = function(self, id) end, 401 | put = function(self, id) end, 402 | post = function(self, id) end, 403 | delete = function(self, id) end 404 | } 405 | ``` 406 | 407 | `page/#.lua`: 408 | 409 | ```lua 410 | return { 411 | get = function(self, id) end, 412 | put = function(self, id) end, 413 | post = function(self, id) end, 414 | delete = function(self, id) end 415 | } 416 | ``` 417 | 418 | To define routes based on file system tree you will need to call `route:fs` 419 | function: 420 | 421 | ```lua 422 | -- Here we assume that you do have /routing directory 423 | -- on your file system. You may use whatever path you 424 | -- like, absolute or relative. 425 | route:fs "/routing" 426 | ``` 427 | 428 | Using file system routing you can just add new files to file system tree, 429 | and they will be added automatically as a routes. 430 | 431 | ### Named Routes 432 | 433 | You can define named route handlers, and then reuse them in actual routes. 434 | 435 | ```lua 436 | route:as "@home" (function(self) end) 437 | ``` 438 | 439 | (the use of `@` as a prefix for a named route is optional) 440 | 441 | And here we actually attach it to a route: 442 | 443 | ```lua 444 | route:get "/" "@home" 445 | ``` 446 | 447 | You can also define multiple named routes in a one go: 448 | 449 | ```lua 450 | route:as { 451 | home = function(self) end, 452 | signin = function(self) end, 453 | signout = function(self) end 454 | } 455 | ``` 456 | 457 | or if you want to use prefixes: 458 | 459 | ```lua 460 | route:as { 461 | ["@home"] = function(self) end, 462 | ["@signin"] = function(self) end, 463 | ["@signout"] = function(self) end 464 | } 465 | ``` 466 | 467 | Named routes must be defined before referencing them in routes. 468 | There are or will be other uses to named routers as well. On todo 469 | list there are things like reverse routing and route forwarding to 470 | a named route. 471 | 472 | ## Middleware 473 | 474 | Middleware in `lua-resty-route` can be defined on either on per request 475 | or per route basis. Middleware are filters that you can add to the request 476 | processing pipeline. As `lua-resty-route` tries to be as unopionated as 477 | possible we don't really restrict what the filters do or how they have to 478 | be written. Middleware can be inserted just flexible as routes, and they 479 | actually do share much of the logic. With one impotant difference. You can 480 | have multiple middleware on the pipeline whereas only one matchin route 481 | will be executed. The middleware can also be yielded (`coroutine.yield`), 482 | and that allows code to be run before and after the router (you can yield 483 | a router as well, but that will never be resumed). If you don't yield, 484 | then the middleware is considered as a before filter. 485 | 486 | The most common type of Middleware is request level middleware: 487 | 488 | ```lua 489 | route:use(function(self) 490 | -- This code will be run before router: 491 | -- ... 492 | self.yield() -- or coroutine.yield() 493 | -- This code will be run after the router: 494 | -- ... 495 | end) 496 | ``` 497 | 498 | Now, as you were already hinted, you may add filters to specific routes as well: 499 | 500 | ```lua 501 | route.filter "=/" (function(self) 502 | -- this middleware will only be called on a specific route 503 | end) 504 | ``` 505 | 506 | You can use the same rules as with routing there, e.g. 507 | 508 | ```lua 509 | route.filter:post "middleware.csrf" 510 | ``` 511 | 512 | Of course you can also do things like: 513 | 514 | ```lua 515 | route.filter:delete "@/users/:number" (function(self, id) 516 | -- here we can say prevent deleting the user who 517 | -- issued the request or something. 518 | end) 519 | ``` 520 | 521 | All the matching middleware is run on every request, unless one of them 522 | decides to `exit`, but we do always try to run after filters for those 523 | middleware that already did run, and yielded. But we will call them in 524 | reverse order: 525 | 526 | 1. middleware 1 runs and yields 527 | 3. middleware 2 runs (and finishes) 528 | 4. middleware 3 runs and yields 529 | 5. router runs 530 | 6. middleware 3 resumes 531 | 7. middleware 1 resumes 532 | 533 | The order of middleware is by scope: 534 | 535 | 1. request level middleware is executed first 536 | 2. router level middleware is executed second 537 | 538 | If there are multiple requet or router level middleware, then they will be 539 | executed the same order they were added to a specific scope. Yielded middleware 540 | is executed in reverse order. Yielded middleware will only be resumed once. 541 | 542 | Internally we do use Lua's great `coroutines`. 543 | 544 | We are going to support a bunch of predefined middleware in a future. 545 | 546 | ## Events 547 | 548 | Events allow you to register specialized handlers for different HTTP status 549 | codes or other predefined event codes. There can be only one handler for each 550 | code or code group. 551 | 552 | You can for example define `404` aka route not found handler like this: 553 | 554 | ```lua 555 | route:on(404, function(self) end) 556 | ``` 557 | 558 | Some groups are predefined, e.g.: 559 | 560 | * `info`, status codes 100 – 199 561 | * `success`, status codes 200 – 299 562 | * `redirect`, status codes 300 – 399 563 | * `client error`, status codes 400 – 499 564 | * `server error`, status codes 500 – 599 565 | * `error`, status codes 400 – 599 566 | 567 | You may use groups like this: 568 | 569 | ```lua 570 | route:on "error" (function(self, code) end) 571 | ``` 572 | 573 | You can also define multiple event handlers in a one go: 574 | 575 | ```lua 576 | route:on { 577 | error = function(self, code) end, 578 | success = function(self, code) end, 579 | [302] = function(self) end 580 | } 581 | ``` 582 | 583 | Then there is a generic catch-all event handler: 584 | 585 | ```lua 586 | route:on(function(self, code) end) 587 | ``` 588 | 589 | We will find the right event handler in this order: 590 | 591 | 1. if there is a specific handler for a specific code, we will call that 592 | 2. if there is a group handler for specific code, we will call that 593 | 3. if there is a catch-all handler, we will call that 594 | 595 | Only one of these is called per event. 596 | 597 | It is possible that we will add other handlers in a future where you could 598 | hook on. 599 | 600 | ### Router API 601 | 602 | You may have seen in previous examples functions get as a first 603 | parameter a `self`. The `self` represents a `router` that contains 604 | many nice functions documented below. 605 | 606 | While the above so called `Route API` is for defining the routes, 607 | the `Router API` is actually about running the routes. 608 | 609 | #### router.context 610 | 611 | This is really powerful concept here to share data between 612 | different routes and functions. Many middleware will be 613 | inserted to context. 614 | 615 | E.g. a redis middleware could add `redis` object to `context` 616 | so that you could just: 617 | 618 | ```lua 619 | local ok, err = self.redis:set("cat", "tommy") 620 | ``` 621 | 622 | Opening and closing the Redis connection is something that the 623 | middleware does automatically before scenes. It means that you 624 | don't need to initiate or close the connections to Redis server, 625 | but this small `framework` takes care of this. As you see, this 626 | `self` parameter is automatically passed around different layers 627 | of this framework, and this context makes it easy to pass data 628 | between them. 629 | 630 | #### router.yield() 631 | 632 | Is similar to `coroutine.yield()` but as you have seen above 633 | in middlewares section, it is quite nice to just call `self.yield()` 634 | instead to split middleware to before and after `filters`, 635 | it also makes us possible to add e.g. debugging / profiling code 636 | in a future. `self.yield()` is more self explaining what happens 637 | and makes code easier to read (may be subjective opinion). 638 | 639 | #### router:redirect(uri, code) 640 | 641 | Similar to `ngx.redirect` but runs redirect event handler and 642 | after filters before actually calling `ngx.redirect` with `code` 643 | (or `ngx.HTTP_MOVED_TEMPORARILY` if not specified) and ending 644 | the handler. 645 | 646 | #### router:exit(uri, code) 647 | 648 | Similar to `ngx.exit` but runs event handler and after filters 649 | before actually calling `ngx.exit` with `code` (or `ngx.OK` 650 | if not specified) and ending the handler. 651 | 652 | #### router:exec(uri, args) 653 | 654 | Similar to `ngx.exec` but runs event handler and after filters 655 | before actually calling `ngx.exec` and ending the handler. 656 | 657 | #### router:done() 658 | 659 | Similar to `ngx.exit` with `ngx.HTTP_OK` but runs event handler 660 | and after filters before actually calling `ngx.exit` and ending 661 | the handler. 662 | 663 | #### router:abort() 664 | 665 | This is reserved for `ngx.on_abort` usage (NYI). Right now only 666 | calls `ngx.exit(499)` after running event handler and after 667 | filters. 668 | 669 | #### router:fail(error, code) 670 | 671 | If `error` is a string, then logs it to error log. Otherwise it 672 | is similar to `ngx.exit(code)` (by default the `code` is 673 | `ngx.HTTP_INTERNAL_SERVER_ERROR`) but runs event handler and 674 | after filters before actually calling `ngx.exit`and ending 675 | the handler. 676 | 677 | #### router:to(location, method) 678 | 679 | Allows you to execute another route (defined by `route`). 680 | 681 | #### router:render(content, context) 682 | 683 | Writes content to output stream. If there is a `context.template` 684 | then it will call `context.template.render(content, context or self.context)`. 685 | 686 | #### router:json(data) 687 | 688 | Encodes data as JSON, adds `application/json` content-type 689 | header and outputs the JSON. 690 | 691 | #### router:* 692 | 693 | A lot more can be added here to make writing code less repetive, 694 | but a lot can be done with injecting into `self.context` as well. 695 | 696 | ## Roadmap 697 | 698 | This is a small collection of ideas that may or may not be implemented as 699 | a part of `lua-resty-route`. 700 | 701 | * Add documentation 702 | * Add tests 703 | * Rewrite current middleware and add new ones 704 | * Rewrite current websocket handler 705 | * Add route statistics 706 | * Add an automatic route cleaning and redirecting (possibly configurable) (clean function is already written) 707 | * Add an automatic slash handling and redirecting (possibly configurable) 708 | * Add a more automated way to define redirects 709 | * Add a support for route caching 710 | * Add a support to route by host 711 | * Add a support to route by headers 712 | * Add a support for Nginx phases 713 | * Add a support for easy way to define Web Hooks routes 714 | * Add a support for easy way to define Server Sent Events routes 715 | * Add a support for "provides", e.g. renderers (?) 716 | * Add a support for conditions, e.g. content negotiation 717 | * Add a support for route grouping (already possible on Nginx at config level) 718 | * Add a support for reverse routing 719 | * Add a support for form method spoofing 720 | * Add a support for client connection abort event handler (`ngx.on_abort`) 721 | * Add a support for host (and possibly) other headers filtering 722 | * Add a support for basic authentication 723 | * Add a support for JWT / OpenID Connect authentication 724 | * Add bootstrapping functionality from Nginx configs 725 | * Add support for resources (or view sets) (a more automated REST-routing) 726 | * Add filesystem routing support for resources (or view sets) 727 | 728 | ## See Also 729 | 730 | * [lua-resty-reqargs](https://github.com/bungle/lua-resty-reqargs) — Request arguments parser 731 | * [lua-resty-session](https://github.com/bungle/lua-resty-session) — Session library 732 | * [lua-resty-template](https://github.com/bungle/lua-resty-template) — Templating engine 733 | * [lua-resty-validation](https://github.com/bungle/lua-resty-validation) — Validation and filtering library 734 | 735 | ## License 736 | 737 | `lua-resty-route` uses two clause BSD license. 738 | 739 | ``` 740 | Copyright (c) 2015 – 2017, Aapo Talvensaari 741 | All rights reserved. 742 | 743 | Redistribution and use in source and binary forms, with or without modification, 744 | are permitted provided that the following conditions are met: 745 | 746 | * Redistributions of source code must retain the above copyright notice, this 747 | list of conditions and the following disclaimer. 748 | 749 | * Redistributions in binary form must reproduce the above copyright notice, this 750 | list of conditions and the following disclaimer in the documentation and/or 751 | other materials provided with the distribution. 752 | 753 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 754 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 755 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 756 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 757 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES` 758 | -------------------------------------------------------------------------------- /lib/resty/route.lua: -------------------------------------------------------------------------------- 1 | local require = require 2 | local router = require "resty.route.router" 3 | local setmetatable = setmetatable 4 | local getmetatable = getmetatable 5 | local reverse = string.reverse 6 | local create = coroutine.create 7 | local select = select 8 | local dofile = dofile 9 | local assert = assert 10 | local error = error 11 | local concat = table.concat 12 | local unpack = table.unpack or unpack 13 | local ipairs = ipairs 14 | local pairs = pairs 15 | local lower = string.lower 16 | local floor = math.floor 17 | local pcall = pcall 18 | local type = type 19 | local find = string.find 20 | local byte = string.byte 21 | local max = math.max 22 | local sub = string.sub 23 | local var = ngx.var 24 | local S = byte "*" 25 | local H = byte "#" 26 | local E = byte "=" 27 | local T = byte "~" 28 | local F = byte "/" 29 | local A = byte "@" 30 | local lfs 31 | do 32 | local o, l = pcall(require, "syscall.lfs") 33 | if not o then o, l = pcall(require, "lfs") end 34 | if o then lfs = l end 35 | end 36 | local matchers = { 37 | prefix = require "resty.route.matchers.prefix", 38 | equals = require "resty.route.matchers.equals", 39 | match = require "resty.route.matchers.match", 40 | regex = require "resty.route.matchers.regex", 41 | simple = require "resty.route.matchers.simple", 42 | } 43 | local selectors = { 44 | [E] = matchers.equals, 45 | [H] = matchers.match, 46 | [T] = matchers.regex, 47 | [A] = matchers.simple 48 | } 49 | local http = require "resty.route.handlers.http" 50 | local handlers = { 51 | -- Common 52 | delete = http, 53 | get = http, 54 | head = http, 55 | post = http, 56 | put = http, 57 | -- Pathological 58 | connect = http, 59 | options = http, 60 | trace = http, 61 | -- WebDAV 62 | copy = http, 63 | lock = http, 64 | mkcol = http, 65 | move = http, 66 | propfind = http, 67 | proppatch = http, 68 | search = http, 69 | unlock = http, 70 | bind = http, 71 | rebind = http, 72 | unbind = http, 73 | acl = http, 74 | -- Subversion 75 | report = http, 76 | mkactivity = http, 77 | checkout = http, 78 | merge = http, 79 | -- UPnP 80 | msearch = http, 81 | notify = http, 82 | subscribe = http, 83 | unsubscribe = http, 84 | -- RFC5789 85 | patch = http, 86 | purge = http, 87 | -- CalDAV 88 | mkcalendar = http, 89 | -- RFC2068 90 | link = http, 91 | unlink = http, 92 | -- Special 93 | sse = require "resty.route.handlers.sse", 94 | websocket = require "resty.route.handlers.websocket" 95 | } 96 | local function location(l) 97 | return l or var.uri 98 | end 99 | local function method(m) 100 | local t = type(m) 101 | if t == "string" then 102 | local h = lower(m) 103 | return handlers[h] and h or m 104 | elseif var.http_accept == "text/event-stream" then 105 | return "sse" 106 | elseif var.http_upgrade == "websocket" then 107 | return "websocket" 108 | else 109 | return lower(var.request_method) 110 | end 111 | end 112 | local function array(t) 113 | if type(t) ~= "table" then return false end 114 | local m, c = 0, 0 115 | for k, _ in pairs(t) do 116 | if type(k) ~= "number" or k < 0 or floor(k) ~= k then return false end 117 | m = max(m, k) 118 | c = c + 1 119 | end 120 | return c == m 121 | end 122 | local function callable(f) 123 | if type(f) == "function" then 124 | return true 125 | end 126 | local m = getmetatable(f) 127 | return m and type(m.__call) == "function" 128 | end 129 | local function methods(m) 130 | local t = type(m) 131 | if t == "table" and array(m) and #m > 0 then 132 | for _, n in ipairs(m) do 133 | if not handlers[n] then 134 | return false 135 | end 136 | end 137 | return true 138 | elseif t == "string" then 139 | return not not handlers[m] 140 | end 141 | return false 142 | end 143 | local function routing(p) 144 | local t = type(p) 145 | if t == "table" and array(p) and #p > 0 then 146 | for _, q in ipairs(p) do 147 | if type(q) ~= "string" then 148 | return false 149 | end 150 | local b = byte(q) 151 | if not selectors[b] and S ~= b and F ~= b then 152 | return false 153 | end 154 | end 155 | return true 156 | elseif t == "string" then 157 | local b = byte(p) 158 | return selectors[b] or S == b or F == b 159 | else 160 | return false 161 | end 162 | end 163 | local function resolve(p) 164 | local b = byte(p) 165 | if b == S then return matchers.prefix, sub(p, 2), true end 166 | local s = selectors[b] 167 | if s then 168 | if b == H or byte(p, 2) ~= S then return s, sub(p, 2) end 169 | return s, sub(p, 3), true 170 | end 171 | return matchers.prefix, p 172 | end 173 | local function named(self, i, c, f) 174 | local l = self[i] 175 | if f then 176 | local t = type(f) 177 | if t == "function" then 178 | l[c] = f 179 | elseif t == "table" then 180 | if callable[f[c]] then 181 | l[c] = f[c] 182 | elseif callable(f) then 183 | l[c] = f 184 | else 185 | error "Invalid handler" 186 | end 187 | else 188 | error "Invalid handler" 189 | end 190 | else 191 | local t = type(c) 192 | if t == "function" then 193 | l[-1] = c 194 | elseif t == "table" then 195 | for n, x in pairs(c) do 196 | if callable(x) then 197 | l[n] = x 198 | end 199 | end 200 | if callable(c) then 201 | l[-1] = c 202 | end 203 | else 204 | return function(x) 205 | return named(self, i, c, x) 206 | end 207 | end 208 | end 209 | return self 210 | end 211 | local function matcher(h, ...) 212 | if select(1, ...) then 213 | return create(h), ... 214 | end 215 | end 216 | local function locator(l, m, p, f) 217 | local n = l.n + 1 218 | l.n = n 219 | if m then 220 | if p then 221 | local match, pattern, insensitive = resolve(p) 222 | l[n] = function(request_method, request_location) 223 | if m == request_method then 224 | return matcher(f, match(request_location, pattern, insensitive)) 225 | end 226 | end 227 | else 228 | l[n] = function(request_method) 229 | if m == request_method then 230 | return create(f) 231 | end 232 | end 233 | end 234 | elseif p then 235 | local match, pattern, insensitive = resolve(p) 236 | l[n] = function(_, request_location) 237 | return matcher(f, match(request_location, pattern, insensitive)) 238 | end 239 | else 240 | l[n] = function() 241 | return create(f) 242 | end 243 | end 244 | return true 245 | end 246 | local function append(l, m, p, f) 247 | local o 248 | local mt = type(m) 249 | local pt = type(p) 250 | if mt == "table" and pt == "table" then 251 | for _, a in ipairs(m) do 252 | for _, b in ipairs(p) do 253 | local c = handlers[a](f) 254 | if type(c) == "function" then 255 | o = locator(l, a, b, c) 256 | end 257 | end 258 | end 259 | elseif mt == "table" then 260 | for _, a in ipairs(m) do 261 | local b = handlers[a](f) 262 | if type(b) == "function" then 263 | o = locator(l, a, p, b) 264 | end 265 | end 266 | elseif pt == "table" then 267 | for _, a in ipairs(p) do 268 | if m then 269 | local b = handlers[m](f) 270 | if type(b) == "function" then 271 | o = locator(l, m, a, b) 272 | end 273 | else 274 | o = locator(l, nil, a, f) 275 | end 276 | end 277 | else 278 | if m then 279 | local a = handlers[m](f) 280 | if type(a) == "function" then 281 | o = locator(l, m, p, a) 282 | end 283 | else 284 | o = locator(l, m, p, f) 285 | end 286 | end 287 | return o 288 | end 289 | local function call(self, ...) 290 | local n = select("#", ...) 291 | assert(n == 1 or n == 2 or n == 3, "Invalid number of arguments") 292 | if n == 3 then 293 | local m, p, f, o = ... 294 | local l = not self.filter and p and self[1] or self[2] 295 | assert(m == nil or methods(m), "Invalid method") 296 | assert(p == nil or routing(p), "Invalid pattern") 297 | f = l[f] or f 298 | local t = type(f) 299 | if t == "function" then 300 | o = append(l, m, p, f) 301 | elseif t == "table" then 302 | if m and p then 303 | o = append(l, m, p, f) 304 | elseif m then 305 | for x, r in pairs(f) do 306 | if routing(x) then 307 | o = self(m, x, r) 308 | end 309 | end 310 | o = append(l, m, p, f) or o 311 | elseif p then 312 | for x, r in pairs(f) do 313 | if methods(x) then 314 | o = self(x, p, r) or o 315 | end 316 | end 317 | else 318 | for x, r in pairs(f) do 319 | if methods(x) then 320 | o = self(x, nil, r) or o 321 | elseif routing(x) then 322 | o = self(nil, x, r) or o 323 | end 324 | end 325 | end 326 | if callable(f) then 327 | o = append(l, m, p, f) or o 328 | end 329 | elseif t == "string" then 330 | o, f = pcall(require, f) 331 | assert(o, f) 332 | o = self(m, p, f) and true 333 | end 334 | if o then 335 | return self 336 | end 337 | error "Invalid function" 338 | elseif n == 2 then 339 | local m, p = ... 340 | if methods(m) then 341 | if routing(p) then 342 | return function(...) 343 | return self(m, p, ...) 344 | end 345 | else 346 | return self(m, nil, p) 347 | end 348 | elseif routing(m) then 349 | return self(nil, ...) 350 | elseif routing(p) then 351 | assert(m == nil, "Invalid method") 352 | return function(...) 353 | return self(nil, p, ...) 354 | end 355 | else 356 | assert(m == nil, "Invalid method") 357 | assert(p == nil, "Invalid pattern") 358 | return function(...) 359 | return self(nil, nil, ...) 360 | end 361 | end 362 | elseif n == 1 then 363 | local m = ... 364 | if methods(m) then 365 | return function(...) 366 | return self(m, ...) 367 | end 368 | elseif routing(m) then 369 | return self(nil, ...) 370 | else 371 | return self(nil, nil, ...) 372 | end 373 | end 374 | end 375 | local filter = {} 376 | filter.__index = filter 377 | filter.__call = call 378 | function filter.new(...) 379 | return setmetatable({ ... }, filter) 380 | end 381 | local route = {} 382 | route.__index = route 383 | route.__call = call 384 | function route.new() 385 | local a, b = { n = 0 }, { n = 0 } 386 | return setmetatable({ {}, { n = 0 }, a, b, filter = filter.new(a, b) }, route) 387 | end 388 | function route:match(l, p) 389 | local m, q, i = resolve(p) 390 | return m(l, q, i) 391 | end 392 | function route:clean(l) 393 | if type(l) ~= "string" or l == "" or l == "/" or l == "." or l == ".." then return "/" end 394 | local s = find(l, "/", 1, true) 395 | if not s then return "/" .. l end 396 | local i, n, t = 1, 1, {} 397 | while s do 398 | if i < s then 399 | local f = sub(l, i, s - 1) 400 | if f == ".." then 401 | n = n > 1 and n - 1 or 1 402 | t[n] = nil 403 | elseif f ~= "." then 404 | t[n] = f 405 | n = n + 1 406 | end 407 | end 408 | i = s + 1 409 | s = find(l, "/", i, true) 410 | end 411 | local f = sub(l, i) 412 | if f == ".." then 413 | n = n > 1 and n - 1 or 1 414 | t[n] = nil 415 | elseif f ~= "." then 416 | t[n] = f 417 | end 418 | return "/" .. concat(t, "/") 419 | end 420 | function route:use(...) 421 | return self.filter(...) 422 | end 423 | function route:fs(p, l) 424 | assert(lfs, "Lua file system (LFS) library was not found") 425 | p = p or var.document_root 426 | if not p then return end 427 | if byte(p, -1) == F then 428 | p = sub(p, 1, #p - 1) 429 | end 430 | l = l or "" 431 | if byte(l) == F then 432 | l = sub(l, 2) 433 | end 434 | if byte(l, -1) == F then 435 | l = sub(l, 1, #l - 1) 436 | end 437 | local dir = lfs.dir 438 | local attributes = lfs.attributes 439 | local dirs = { n = 0 } 440 | for file in dir(p) do 441 | if file ~= "." and file ~= ".." then 442 | local f = concat{ p, "/", file} 443 | local mode = attributes(f).mode 444 | if mode == "directory" then 445 | local x = { l, "/" } 446 | x[3] = file == "#" and ":number" or file 447 | dirs.n = dirs.n + 1 448 | dirs[dirs.n] = { f, concat(x) } 449 | elseif (mode == "file" or mode == "link") and sub(file, -4) == ".lua" then 450 | local b = sub(file, 1, #file - 4) 451 | local m 452 | local i = find(reverse(b), "@", 1, true) 453 | if i then 454 | m = sub(b, -i+1) 455 | b = sub(b, 1, -i-1) 456 | end 457 | local x = { "@*/" } 458 | if l ~= "" then 459 | x[2] = l 460 | if b ~= "index" then 461 | if b == "#" then 462 | x[3] = "/:number" 463 | else 464 | x[3] = "/" 465 | x[4] = b 466 | end 467 | end 468 | else 469 | if b ~= "index" then 470 | if b == "#" then 471 | x[2] = ":number" 472 | else 473 | x[2] = b 474 | end 475 | end 476 | end 477 | f = dofile(f) 478 | self(m, concat(x), f) 479 | end 480 | end 481 | end 482 | for i=1, dirs.n do 483 | self:fs(dirs[i][1], dirs[i][2]) 484 | end 485 | return self 486 | end 487 | function route:on(...) 488 | return named(self, 1, ...) 489 | end 490 | function route:as(...) 491 | return named(self, 2, ...) 492 | end 493 | function route:dispatch(l, m) 494 | router.new(unpack(self)):to(location(l), method(m)) 495 | end 496 | for h in pairs(handlers) do 497 | local f = function(self, ...) 498 | return self(h, ...) 499 | end 500 | route[h] = f 501 | filter[h] = f 502 | end 503 | return route 504 | -------------------------------------------------------------------------------- /lib/resty/route/handlers/http.lua: -------------------------------------------------------------------------------- 1 | return function(f) 2 | return f 3 | end -------------------------------------------------------------------------------- /lib/resty/route/handlers/sse.lua: -------------------------------------------------------------------------------- 1 | return function(f) 2 | return f 3 | end -------------------------------------------------------------------------------- /lib/resty/route/handlers/websocket.lua: -------------------------------------------------------------------------------- 1 | local server = require "resty.websocket.server" 2 | local setmetatable = setmetatable 3 | local type = type 4 | local websocket = {} 5 | return function(f) 6 | return function() 7 | end 8 | end 9 | -- TODO: Rewrite needed 10 | --[[ 11 | local require = require 12 | local server = require "resty.websocket.server" 13 | local setmetatable = setmetatable 14 | local ngx = ngx 15 | local var = ngx.var 16 | local flush = ngx.flush 17 | local abort = ngx.on_abort 18 | local kill = ngx.thread.kill 19 | local spawn = ngx.thread.spawn 20 | local exiting = ngx.worker.exiting 21 | local sub = string.sub 22 | local ipairs = ipairs 23 | local select = select 24 | local type = type 25 | local mt, handler = {}, {} 26 | local noop = function() end 27 | local function find(func) 28 | local t = type(func) 29 | if t == "function" then 30 | return { receive = func } 31 | elseif t == "table" then 32 | return func 33 | end 34 | return nil 35 | end 36 | function mt:__call(func) 37 | local self = find(func) 38 | return function(context, ...) 39 | local self = setmetatable(self, handler) 40 | self.n = select("#", ...) 41 | self.args = { ... } 42 | self.context = context 43 | self:upgrade() 44 | local websocket, e = server:new(self) 45 | if not websocket then self:fail(e) end 46 | self.websocket = websocket 47 | abort(self.abort(self)) 48 | self:connect() 49 | flush(true) 50 | local d, t = websocket:recv_frame() 51 | while not websocket.fatal and not exiting() do 52 | if not d then 53 | self:timeout() 54 | else 55 | if self.receive then 56 | self:receive(d, t) 57 | else 58 | if not t then t = "unknown" end 59 | if self[t] then self[t](self, d) end 60 | end 61 | end 62 | d, t = websocket:recv_frame() 63 | end 64 | self:close() 65 | end 66 | end 67 | handler.__index = handler 68 | function handler:upgrading() end 69 | function handler:upgrade() 70 | self.upgrade = noop 71 | self:upgrading(); 72 | local host = var.host 73 | local s = #var.scheme + 4 74 | local e = #host + s - 1 75 | if sub(var.http_origin or "", s, e) ~= host then 76 | return self:forbidden() 77 | end 78 | self:upgraded() 79 | end 80 | function handler:upgraded() end 81 | function handler:connect() end 82 | function handler:timeout() 83 | local websocket = self.websocket 84 | local _, e = websocket:send_ping() 85 | if websocket.fatal then 86 | self:error(e) 87 | end 88 | end 89 | function handler:continuation() end 90 | function handler:text() end 91 | function handler:binary() end 92 | function handler:closign() end 93 | function handler:close() 94 | self.close = noop 95 | self:closing(); 96 | local threads = self.threads 97 | if threads then 98 | for _, v in ipairs(self.threads) do 99 | kill(v) 100 | end 101 | end 102 | self.threads = {} 103 | if not self.websocket.fatal then 104 | local b, e = self.websocket:send_close() 105 | if not b and self.websocket.fatal then 106 | return self:error(e) 107 | else 108 | return self.websocket.fatal and self:error(e) or self:exit() 109 | end 110 | end 111 | self:closed(); 112 | end 113 | function handler:closed() end 114 | function handler:forbidden() 115 | return self.route:forbidden() 116 | end 117 | function handler:error(message) 118 | local threads = self.threads 119 | if threads then 120 | for _, v in ipairs(self.threads) do 121 | kill(v) 122 | end 123 | end 124 | self.threads = {} 125 | if not self.websocket.fatal then 126 | local d, e = self.websocket:send_close() 127 | if not d and self.websocket.fatal then 128 | return self:fail(message or e) 129 | else 130 | return self.websocket.fatal and self:fail(message or e) or self:exit() 131 | end 132 | end 133 | end 134 | function handler.abort(self) 135 | return function() self:close() end 136 | end 137 | function handler:ping() 138 | local b, e = self.websocket:send_pong() 139 | if not b and self.websocket.fatal then 140 | if not b then return self:error(e) end 141 | end 142 | end 143 | function handler:pong() end 144 | function handler:unknown() end 145 | function handler:send(text) 146 | local b, e = self.websocket:send_text(text) 147 | if not b and self.websocket.fatal then 148 | return self:error(e) 149 | end 150 | end 151 | function handler:spawn(...) 152 | if not self.threads then self.threads = {} end 153 | self.threads[#self.threads+1] = spawn(...) 154 | end 155 | ]] -------------------------------------------------------------------------------- /lib/resty/route/matchers/equals.lua: -------------------------------------------------------------------------------- 1 | local lower = string.lower 2 | return function(location, pattern, insensitive) 3 | if location and insensitive then 4 | location = lower(location) 5 | pattern = lower(pattern) 6 | end 7 | return location == pattern 8 | end 9 | -------------------------------------------------------------------------------- /lib/resty/route/matchers/match.lua: -------------------------------------------------------------------------------- 1 | local match = string.match 2 | return function(location, pattern) 3 | return match(location, pattern) 4 | end -------------------------------------------------------------------------------- /lib/resty/route/matchers/prefix.lua: -------------------------------------------------------------------------------- 1 | local sub = string.sub 2 | local lower = string.lower 3 | return function(location, pattern, insensitive) 4 | local prefix = sub(location, 1, #pattern) 5 | if insensitive then 6 | prefix = lower(prefix) 7 | pattern = lower(pattern) 8 | end 9 | return prefix == pattern 10 | end -------------------------------------------------------------------------------- /lib/resty/route/matchers/regex.lua: -------------------------------------------------------------------------------- 1 | local match = ngx.re.match 2 | local unpack = table.unpack or unpack 3 | return function(location, pattern, insensitive) 4 | local m = match(location, pattern, insensitive and "ijosu" or "josu") 5 | if m then 6 | if m[1] then 7 | return unpack(m) 8 | end 9 | return m[0] 10 | end 11 | return nil 12 | end -------------------------------------------------------------------------------- /lib/resty/route/matchers/simple.lua: -------------------------------------------------------------------------------- 1 | local match = ngx.re.match 2 | local unpack = table.unpack or unpack 3 | local concat = table.concat 4 | local find = string.find 5 | local sub = string.sub 6 | local huge = math.huge 7 | local tonumber = tonumber 8 | local unescape = ngx.unescape_uri 9 | local cache = {} 10 | return function(location, pattern, insensitive) 11 | if not cache[pattern] then 12 | local i, c, p, j, n = 1, {}, {}, 0, 1 13 | local s = find(pattern, ":", 1, true) 14 | while s do 15 | if s > i then 16 | p[n] = [[\Q]] 17 | p[n+1] = sub(pattern, i, s - 1) 18 | p[n+2] = [[\E]] 19 | n=n+3 20 | end 21 | local x = sub(pattern, s, s + 6) 22 | if x == ":number" then 23 | p[n] = [[(\d+)]] 24 | s, j, n = s + 7, j + 1, n + 1 25 | c[j] = tonumber 26 | elseif x == ":string" then 27 | p[n] = [[([^/]+)]] 28 | s, j, n = s + 7, j + 1, n + 1 29 | c[j] = unescape 30 | end 31 | i = s 32 | s = find(pattern, ":", s + 1, true) 33 | end 34 | if j > 0 then 35 | local rest = sub(pattern, i) 36 | if #rest > 0 then 37 | p[n] = [[\Q]] 38 | p[n+1] = rest 39 | p[n+2] = [[\E$]] 40 | else 41 | p[n] = "$" 42 | end 43 | else 44 | p[1] = pattern 45 | p[2] = "$" 46 | end 47 | cache[pattern] = { concat(p), j, c } 48 | end 49 | local p, j, c = unpack(cache[pattern]) 50 | local m = match(location, p, insensitive and "aijosu" or "ajosu") 51 | if m then 52 | if m[1] then 53 | for i = 1, j do 54 | m[i] = c[i](m[i]) 55 | if m[i] == huge then return nil end 56 | end 57 | return unpack(m) 58 | end 59 | return m[0] 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/resty/route/middleware/ajax.lua: -------------------------------------------------------------------------------- 1 | -- TODO: Rewrite needed 2 | local var = ngx.var 3 | return function(self) 4 | self.ajax = var.http_x_requested_with == "XMLHttpRequest" 5 | end 6 | -------------------------------------------------------------------------------- /lib/resty/route/middleware/form.lua: -------------------------------------------------------------------------------- 1 | -- TODO: Rewrite needed 2 | local form = require "resty.validation".fields 3 | return function(self) 4 | self.form = form 5 | end 6 | -------------------------------------------------------------------------------- /lib/resty/route/middleware/pjax.lua: -------------------------------------------------------------------------------- 1 | -- TODO: Rewrite needed 2 | local var = ngx.var 3 | return function(self) 4 | if not not var.http_x_pjax then 5 | self.pjax = { 6 | container = var.http_x_pjax_container, 7 | version = var.http_x_pjax_version 8 | } 9 | end 10 | end -------------------------------------------------------------------------------- /lib/resty/route/middleware/redis.lua: -------------------------------------------------------------------------------- 1 | -- TODO: Rewrite needed 2 | local redis = require "resty.redis" 3 | return function(self) 4 | return function(options) 5 | local route = self.route 6 | local r, e = redis:new() 7 | if not r then 8 | return route:error(e) 9 | end 10 | local o, e = r:connect(options.host or "127.0.0.1", options.port or 6379) 11 | if not o then 12 | return route:error(e) 13 | end 14 | if options.timeout then 15 | r:set_timeout(options.timeout) 16 | end 17 | self[options.name or "redis"] = r 18 | route:after(function() 19 | if options.max_idle_timeout and options.pool_size then 20 | r:set_keepalive(options.max_idle_timeout, options.pool_size) 21 | else 22 | r:close() 23 | end 24 | end) 25 | end 26 | end -------------------------------------------------------------------------------- /lib/resty/route/middleware/reqargs.lua: -------------------------------------------------------------------------------- 1 | -- TODO: Rewrite needed 2 | local reqargs = require "resty.reqargs" 3 | local remove = os.remove 4 | local pairs = pairs 5 | local function cleanup(self) 6 | local files = self.files 7 | for _, f in pairs(files) do 8 | if f.n then 9 | for i = 1, f.n do 10 | remove(f[i].temp) 11 | end 12 | else 13 | remove(f.temp) 14 | end 15 | end 16 | self.files = {} 17 | end 18 | return function(self) 19 | return function(options) 20 | local get, post, files = reqargs(options) 21 | self.route:after(cleanup) 22 | self.get = get 23 | self.post = post 24 | self.files = files 25 | end 26 | end -------------------------------------------------------------------------------- /lib/resty/route/middleware/template.lua: -------------------------------------------------------------------------------- 1 | -- TODO: Rewrite needed 2 | return function(self) 3 | self.template = require "resty.template" 4 | end 5 | -------------------------------------------------------------------------------- /lib/resty/route/router.lua: -------------------------------------------------------------------------------- 1 | local encode = require "cjson.safe".encode 2 | local setmetatable = setmetatable 3 | local create = coroutine.create 4 | local resume = coroutine.resume 5 | local status = coroutine.status 6 | local yield = coroutine.yield 7 | local pcall = pcall 8 | local type = type 9 | local next = next 10 | local ngx = ngx 11 | local log = ngx.log 12 | local redirect = ngx.redirect 13 | local exit = ngx.exit 14 | local exec = ngx.exec 15 | local header = ngx.header 16 | local print = ngx.print 17 | local OK = ngx.OK 18 | local ERR = ngx.ERR 19 | local WARN = ngx.WARN 20 | local HTTP_200 = ngx.HTTP_OK 21 | local HTTP_302 = ngx.HTTP_MOVED_TEMPORARILY 22 | local HTTP_500 = ngx.HTTP_INTERNAL_SERVER_ERROR 23 | local HTTP_404 = ngx.HTTP_NOT_FOUND 24 | local function process(self, i, t, ok, ...) 25 | if ok then 26 | if i == 3 then return self:done(...) end 27 | if status(t) == "suspended" then 28 | local f = self[1] 29 | local n = f.n + 1 30 | f.n = n 31 | f[n] = t 32 | end 33 | else 34 | return self:fail(...) 35 | end 36 | end 37 | local function execute(self, i, t, ...) 38 | if t then process(self, i, t, resume(t, self.context, ...)) end 39 | end 40 | local function go(self, i) 41 | local a = self[i] 42 | local n = a.n 43 | for j=1,n do 44 | execute(self, i, a[j](self.method, self.location)) 45 | end 46 | end 47 | local function finish(self, code, func, ...) 48 | if code then 49 | local t = self[2] 50 | if next(t) then 51 | local f 52 | if t[code] then 53 | f = t[code] 54 | elseif code == OK and t.ok then 55 | f = t.ok 56 | elseif code == 499 and t.abort then 57 | f = t.abort 58 | elseif code >= 100 and code <= 199 and t.info then 59 | f = t.info 60 | elseif code >= 200 and code <= 299 and t.success then 61 | f = t.success 62 | elseif code >= 300 and code <= 399 and t.redirect then 63 | f = t.redirect 64 | elseif code >= 400 and code <= 499 and t["client error"] then 65 | f = t["client error"] 66 | elseif code >= 500 and code <= 599 and t["server error"] then 67 | f = t["server error"] 68 | elseif code >= 400 and code <= 599 and t.error then 69 | f = t.error 70 | elseif t[-1] then 71 | f = t[-1] 72 | end 73 | if f then 74 | local o, e = pcall(f, self.context, code) 75 | if not o then log(WARN, e) end 76 | end 77 | end 78 | end 79 | local f = self[1] 80 | local n = f.n 81 | for i=n,1,-1 do 82 | local t = f[i] 83 | f[i] = nil 84 | f.n = i - 1 85 | local o, e = resume(t) 86 | if not o then log(WARN, e) end 87 | end 88 | return func(...) 89 | end 90 | local router = { yield = yield } 91 | router.__index = router 92 | function router.new(...) 93 | local self = setmetatable({ { n = 0 }, ... }, router) 94 | self.context = setmetatable({ route = self }, { __index = self }) 95 | self.context.context = self.context 96 | return self 97 | end 98 | function router:redirect(uri, code) 99 | code = code or HTTP_302 100 | return finish(self, code, redirect, uri, code) 101 | end 102 | function router:exit(code) 103 | code = code or OK 104 | return finish(self, code, exit, code) 105 | end 106 | function router:exec(uri, args) 107 | return finish(self, OK, exec, uri, args) 108 | end 109 | function router:done() 110 | return self:exit(HTTP_200) 111 | end 112 | function router:abort() 113 | return self:exit(499) 114 | end 115 | function router:fail(error, code) 116 | if type(error) == "string" then 117 | log(ERR, error) 118 | end 119 | return self:exit(code or type(error) == "number" and error or HTTP_500) 120 | end 121 | function router:to(location, method) 122 | method = method or "get" 123 | self.location = location 124 | self.method = method 125 | if self[5] then 126 | go(self, 5) 127 | self[5] = nil 128 | end 129 | go(self, 4) 130 | local named = self[3][location] 131 | if named then 132 | execute(self, 3, type(named) == "function" and create(named) or create(function(...) named(...) end)) 133 | else 134 | go(self, 3) 135 | end 136 | self:fail(HTTP_404) 137 | end 138 | function router:render(content, context) 139 | local template = self.context.template 140 | if template then 141 | template.render(content, context or self.context) 142 | else 143 | print(content) 144 | end 145 | return self 146 | end 147 | function router:json(data) 148 | if type(data) == "table" then 149 | data = encode(data) 150 | end 151 | header.content_type = "application/json" 152 | print(data) 153 | return self 154 | end 155 | return router 156 | --------------------------------------------------------------------------------