summaryrefslogtreecommitdiff
path: root/cgi.lua
blob: 942b26fbeacc85ac3301ae1be1966f448aaac222 (plain)
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