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
|
local html = require 'html'
local md5 = require 'md5'
local M = {}
M.env = os.getenv
local header_written
local function header(h, v)
header_written = true
assert(not h:match "[: \n]")
assert(h:lower() == h)
assert(not (tostring(v)):match "\n")
print(("%s: %s"):format(h, v))
end
local function serve(headers, body)
assert(not header_written)
if type(headers) == 'string' then
headers = {content_type = headers}
end
headers.etag = ('"%s"'):format(md5.sumhexa(body))
headers.content_length = #body
for h, v in pairs(headers) do
if type(v) == 'table' then
for _, v in ipairs(v) do
header(h:gsub("_", "-"), v)
end
else
header(h:gsub("_", "-"), v)
end
end
print()
io.stdout:write(body)
end
function M.abort(...)
coroutine.yield(M.abort, ...)
end
function M.redirect(...)
coroutine.yield(M.redirect, ...)
end
function M.decode_path(path)
local p = {}
for location in path:gmatch '([^/]+)' do
table.insert(p, html.url_decode(location))
end
return p
end
function M.parse_qs(str,sep)
sep = sep or '&'
local values = {}
for key, val in str:gmatch(('([^%q=]+)(=*[^%q=]*)'):format(sep, sep)) do
local key = html.url_decode(key)
local keys = {}
key = key:gsub('%[([^%]]*)%]', function(v)
-- extract keys between balanced brackets
if string.find(v, "^-?%d+$") then
v = tonumber(v)
else
v = html.url_decode(v)
end
table.insert(keys, v)
return "="
end)
key = key:gsub('=+.*$', "")
key = key:gsub('%s', "_") -- remove spaces in parameter name
val = val:gsub('^=+', "")
if not values[key] then
values[key] = {}
end
if #keys > 0 and type(values[key]) ~= 'table' then
values[key] = {}
elseif #keys == 0 and type(values[key]) == 'table' then
values[key] = html.url_decode(val)
end
local t = values[key]
for i,k in ipairs(keys) do
if type(t) ~= 'table' then
t = {}
end
if k == "" then
k = #t+1
end
if not t[k] then
t[k] = {}
end
if i == #keys then
t[k] = html.url_decode(val)
end
t = t[k]
end
end
return values
end
local redir_statuses = {
[301] = "301 Moved Permanently",
[301] = "302 Found",
[302] = "302 See Other",
[307] = "307 Temporary Redirect",
[308] = "308 Permanent Redirect",
}
local function redirect_page(code, url)
return {
content = 'text/html', status = redir_statuses[code] or code,
location = url
}, html.render_doc(function()
html.h1 "redirect"
html.p(function()
html.a({href = url}, url)
end)
end)
end
function M.serve(methods, error_page)
local co = coroutine.create(function()
local path = M.env 'PATH_INFO'
local query = M.parse_qs(M.env 'QUERY_STRING')
local form
local content_type = M.env 'CONTENT_TYPE'
if content_type == 'application/x-www-form-urlencoded' then
form = M.parse_qs(io.read 'a')
end
local cookie = {}
if M.env 'HTTP_COOKIE' then
cookie = M.parse_qs(M.env 'HTTP_COOKIE', '; ')
end
if methods[M.env 'REQUEST_METHOD'] then
local info = {query = query, form = form, cookie = cookie}
serve(methods[M.env 'REQUEST_METHOD'](info, path))
else
M.abort(405)
end
end)
local ok, err, code, arg = coroutine.resume(co)
if ok and coroutine.status(co) ~= "dead" then
if err == M.abort then
serve(error_page(code, arg))
elseif err == M.redirect then
serve(redirect_page(code, arg))
else
ok = false err = "invalid yield"
end
end
if not ok then
io.stderr:write('\n'..debug.traceback(co, err)..'\n')
serve(error_page(500))
end
end
return M
|