summaryrefslogtreecommitdiff
path: root/web/server.lua
blob: a0cc5058e4020169ac299ef4a006e08695734c43 (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
local cqueues = require 'cqueues'
local cq_condition = require'cqueues.condition'
local http_server = require 'http.server'
local http_headers = require 'http.headers'
local http_util = require 'http.util'
local http_cookie = require 'http.cookie'
local pprint=require'pprint'

local function make_req(app, server, stream)
	local req = {server=server, stream=stream, app=app}
	req.headers = assert(req.stream:get_headers())
	req.method = req.headers:get":method"

	local path = req.headers:get":path"
	local sel, qs = path:match"^(.-)%?(.*)$"
	if sel then path = sel end
	req.path, req.qs = path, qs
	req.args = {}
	if req.qs then
		for k,v in http_util.query_args(req.qs) do req.args[k] = v end
	end

	req.cookies = assert(http_cookie.parse_cookies(req.headers))

	if req.method == 'POST' then
		req.form = {}
		local content_type = req.headers:get'content-type'
		local is_form = content_type:match'^application/x%-www%-form%-urlencoded'
		if is_form then
			local body = stream:get_body_as_string()
			-- i'm like 96% sure this is correct
			body = body:gsub('+',' ')
			for k,v in http_util.query_args(body) do
				req.form[k] = v
			end
		end
	end

	req.app:run_hooks('pre_req',req)
	return req
end

-- take return values of view function, construct and send http response
local function make_resp(req, body, header_dict, status_code)
	if body == false then return end -- assume view sent its own response

	local rheaders
	if rawequal(getmetatable(header_dict), http_headers.mt) then
		rheaders = header_dict
	else
		rheaders = http_headers.new()
		for k,v in pairs(header_dict or {}) do
			rheaders:append(k:gsub("_","-"), v)
		end
		if not rheaders:has':status' then
			rheaders:append(':status',tostring(status_code or 200)) end
		if not rheaders:has'content-type' then
			rheaders:append('content-type','text/html') end
	end

	req.app:run_hooks('post_req',req,body,rheaders,status_code)

	assert(req.stream:write_headers(rheaders, false))

	if type(body) == 'string' then
		assert(req.stream:write_body_from_string(body))
	elseif io.type(body) == 'file' then
		assert(req.stream:write_body_from_file(body))
		body:close()
	elseif type(body) == 'table' and getmetatable(body).__tostring then
		assert(req.stream:write_body_from_string(tostring(body)))
	else
		error('unsupported response type: '..type(body))
	end -- todo maybe: chunk iterators, if i need them
end

local function handle_request(app, server, stream)
	local req = make_req(app, server, stream)
	print('handling',app.version,req.method,req.path)
	local ok, body, headers, status = xpcall(app.view, debug.traceback, req)
	if not ok then
		if type(body)=='table' and body.fail then
			status,headers = body.status, nil
			body = '<h1>'..status..' page</h1><pre>'..body.tb..'</pre>'
		else
			status, headers = 500, nil
			body = '<h1>500 internal server explosion</h1><pre>'..body..'</pre>'
		end
	end
	return make_resp(req, body, headers, status)
end

local function onerror(server, ctx, op, err, errno)
	local msg = op .. " on " .. tostring(ctx) .. " failed"
	if err then msg = msg.. ": " .. tostring(err) end
	io.stderr:write(msg, "\n")
end

local function run(cq, app)
	local server = assert(http_server.listen {
		cq = cq, host = '0.0.0.0', port = 8082, onerror = onerror,
		onstream = function(server, stream)
			local ok, err = xpcall(handle_request, debug.traceback, app, server, stream)
			if not ok then error(err, 0) end
		end
	})
	assert(server:listen())
	local _,addr,port = server:localname()
	print(("listening on http://%s:%s"):format(addr,port))
end

return {
	run = run,
}