forked from pintsized/lua-resty-rack
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathrack.lua
164 lines (135 loc) · 5.07 KB
/
rack.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
local rack = {}
rack._VERSION = '0.1'
local function process_rack_use_args(args)
local route = table.remove(args, 1)
local mw, options
if type(route) == "table" or type(route) == "function" then
mw = route
route = nil
else
mw = table.remove(args, 1)
end
options = table.remove(args, 1) or {}
return route, mw, options
end
local function get_ngx_middlewares()
ngx.ctx.rack = ngx.ctx.rack or {}
ngx.ctx.rack.middlewares = ngx.ctx.rack.middlewares or {}
return ngx.ctx.rack.middlewares
end
-- uri_relative = /test?arg=true
function get_ngx_uri_relative(query)
return ngx.var.uri .. ngx.var.is_args .. query
end
-- uri_full = http://example.com/test?arg=true
function get_ngx_uri_full(uri_relative)
return ngx.var.scheme .. '://' .. ngx.var.host .. uri_relative
end
local function get_middleware_function(mw, options)
-- If we simply have a function, we can add that instead
if type(mw) == "function" then return mw end
-- If we have a 'call' function, then calling it with options should return a new function with the required params
if type(mw) == "table" and type(mw.call) == "function" then return mw.call(options) end
return nil
end
local function handle_ngx_response_errors(status, body)
assert(status, "Middleware returned with no status. Perhaps you need to call next().")
-- If we have a 5xx or a 3/4xx and no body entity, exit allowing nginx config
-- to generate a response.
if status >= 500 or (status >= 300 and body == nil) then
ngx.exit(status)
end
end
local function normalize(str)
return str:lower():gsub("-", "_")
end
-- creates a metatable that, when applied to a table, makes it normalized, which means:
-- * It lowercases keys, so t.foo and t.FOO return the same
-- * It replaces dashes by underscores, so t['foo-bar'] returns the same as t.foo_bar
-- * When fallback is provided, t['inexisting key'] will return fallback('inexisting key')
-- It is used for immunizing ngx's to browser changes on the headers of requests and responses
local function create_normalizer_mt(fallback)
local normalized = {}
return {
__index = function(t, k)
k = normalize(k)
return normalized[k] or (fallback and fallback(k))
end,
__newindex = function(t, k, v)
rawset(t, k, v)
normalized[normalize(k)] = v
end
}
end
-- Runs the next middleware in the rack.
local function next_middleware()
-- Pick each piece of middleware off in order
local mwf = table.remove(ngx.ctx.rack.middlewares, 1)
local req = ngx.ctx.rack.req
local res = ngx.ctx.rack.res
-- Call the middleware, which may itself call next().
-- The first to return is handling the reponse.
local post_function = mwf(req, res, next_middleware)
if not ngx.headers_sent then
handle_ngx_response_errors(res.status, res.body)
for k,v in pairs(res.header) do ngx.header[k] = v end
ngx.status = res.status
ngx.print(res.body)
ngx.eof()
end
-- Middleware may return a callable object to be called post-EOF.
-- This code will only run for persistent connections, and is not really guaranteed
-- to run, since browser behaviours differ. Also be aware that long running tasks
-- may affect performance by hogging the connection.
if post_function then post_function(req, res) end
end
-- Register some middleware to be used.
--
-- @param string route Optional, dfaults to '/'.
-- @param table middleware The middleware module
-- @param table options Table of options for the middleware.
-- @return void
function rack.use(...)
local route, mw, options = process_rack_use_args({...})
if route and string.sub(ngx.var.uri, 1, route:len()) ~= route then return false end
local mwf = get_middleware_function(mw, options)
if not mwf then return nil, "Invalid middleware" end
local middlewares = get_ngx_middlewares()
table.insert(middlewares, mwf)
return true
end
-- Start the rack.
function rack.run()
-- We need a decent req / res environment to pass around middleware.
if not ngx.ctx.rack or not ngx.ctx.rack.middlewares then
ngx.log(ngx.ERR, "Attempted to run rack without any middleware.")
return
end
local query = ngx.var.query_string or ""
local uri_relative = get_ngx_uri_relative(query)
local uri_full = get_ngx_uri_full(uri_relative)
local req_fallback = function(k) return ngx.var["http_" .. k] end
ngx.ctx.rack.req = {
body = "",
query = query,
uri_full = uri_full,
uri_relative = uri_relative,
method = ngx.var.request_method,
args = ngx.req.get_uri_args(),
scheme = ngx.var.scheme,
uri = ngx.var.uri,
host = ngx.var.host,
header = setmetatable({}, create_normalizer_mt(req_fallback))
}
ngx.ctx.rack.res = {
status = nil,
body = nil,
header = setmetatable({}, create_normalizer_mt())
}
next_middleware()
end
-- to prevent use of casual module global variables
setmetatable(rack, { __newindex = function (table, key, val)
error('attempt to write to undeclared variable "' .. key .. '": ' .. debug.traceback())
end})
return rack