|
1 local http_server = require"net.http.server"; |
|
2 local files = require "net.http.files"; |
|
3 local http_codes = require"net.http.codes"; |
|
4 local promise = require "util.promise"; |
|
5 local url = require"socket.url"; |
|
6 local uuid = require"util.uuid"; |
|
7 local web = require "util.web"; |
|
8 local render = require "render".render; |
|
9 local ok, json = pcall(require, "cjson"); |
|
10 if not ok then json = require"util.json"; end |
|
11 local errors = require "util.error".init("web", {}); |
|
12 |
|
13 local templates; |
|
14 local config; |
|
15 |
|
16 local log = require "util.logger".init("web"); |
|
17 |
|
18 local usercookie = require"util.usercookie"; |
|
19 local secret = uuid.generate(); |
|
20 |
|
21 local check_auth = require "app.auth".check_auth; |
|
22 |
|
23 local function set_auth_cookie(username, response) |
|
24 local expires = config.cookie_ttl or 604800; |
|
25 local cookie = usercookie.generate(username, os.time()+expires, secret); |
|
26 cookie = "remember=".. cookie .. "; Path="..config.base_path |
|
27 .."; Max-Age="..tostring(expires).."; HttpOnly"; |
|
28 return web.set_cookie(response.headers, cookie); |
|
29 end |
|
30 |
|
31 local csrf_token_len = #uuid.generate(); |
|
32 |
|
33 local function check_csrf(event, viewdata) |
|
34 local request, response = event.request, event.response; |
|
35 web.unpack_cookies(request); |
|
36 local csrf_token = request.cookies.csrf_token; |
|
37 log("debug", "csrf_token=%s", tostring(csrf_token)); |
|
38 if csrf_token and #csrf_token == csrf_token_len then |
|
39 viewdata.csrf_token = csrf_token; |
|
40 else |
|
41 csrf_token = uuid.generate(); |
|
42 viewdata.csrf_token = csrf_token; |
|
43 web.set_cookie(response.headers, "csrf_token=" .. csrf_token .. "; Path="..config.base_path.."; HttpOnly"); |
|
44 end |
|
45 end |
|
46 |
|
47 local function wrap_handler(f, t, path_prefix_len) |
|
48 return function (event) |
|
49 log("debug", "Check auth..."); |
|
50 local authed = check_auth(event.request, config); |
|
51 log("debug", "Checked, %s", authed); |
|
52 event.config = config; |
|
53 local sub_path = nil; |
|
54 if path_prefix_len then |
|
55 sub_path = event.request.path:sub(path_prefix_len+2); |
|
56 end |
|
57 local p = f(event, sub_path); |
|
58 return promise.resolve(p):next(function (resp) |
|
59 if type(resp) == "table" then |
|
60 local headers = event.response.headers; |
|
61 local accept = event.request.headers.accept or ""; |
|
62 web.add_header(headers, "vary", "Accept, Cookie"); |
|
63 if accept:find"application/json" then |
|
64 headers.content_type = "application/json"; |
|
65 return json.encode(resp); |
|
66 elseif t then |
|
67 if authed or event.needs_csrf then check_csrf(event, resp); end |
|
68 resp.authenticated = event.request.authenticated; |
|
69 resp.config = config; |
|
70 resp = render(t, resp); |
|
71 if not headers.content_type then |
|
72 headers.content_type = "text/html; charset=utf-8"; |
|
73 end |
|
74 else |
|
75 return resp; |
|
76 end |
|
77 end |
|
78 return resp; |
|
79 end):catch(function (err) |
|
80 log("error", "Failed inside handler wrapper: %s", json.encode(err)); |
|
81 return promise.reject(errors.wrap(err)); |
|
82 end); |
|
83 end |
|
84 end |
|
85 |
|
86 local function handle_error(error, config) |
|
87 if not error.response then |
|
88 -- Top-level error, return text string |
|
89 if config.debug then |
|
90 return error.private_message; |
|
91 end |
|
92 return "An internal error occurred."; |
|
93 end |
|
94 |
|
95 error.response.headers.content_type = "text/html; charset=utf-8"; |
|
96 |
|
97 log("warn", "HTTP error handler triggered (debug: %s): %d %s", not not config.debug, error.code, json.encode(error.error)); |
|
98 |
|
99 local err = errors.wrap(error.err); |
|
100 |
|
101 local r = render(templates.error or "{text}", { |
|
102 config = config; |
|
103 code = err.code or error.code; |
|
104 long = err.condition or http_codes[error.code]; |
|
105 text = err.text or "An unexpected error occurred. Please try again later."; |
|
106 id = err.instance_id; |
|
107 internal_error = config.debug and error.error and error.error.context.wrapped_error; |
|
108 }); |
|
109 return r; |
|
110 end |
|
111 |
|
112 local function redirect(to, code) |
|
113 code = code or 303; |
|
114 return function (event) |
|
115 if event.request.url.query then |
|
116 event.response.headers.location = to.."?"..event.request.url.query; |
|
117 else |
|
118 event.response.headers.location = to; |
|
119 end |
|
120 return code; |
|
121 end |
|
122 end |
|
123 |
|
124 local function size_only(request, data) |
|
125 request.headers.content_size = #data; |
|
126 return 200; |
|
127 end |
|
128 |
|
129 local function head_handler(handler) |
|
130 return function (event) |
|
131 event.send = size_only; |
|
132 return handler(event); |
|
133 end |
|
134 end |
|
135 |
|
136 local function register_handlers(handlers, event_base, path_prefix) |
|
137 for method_path, handler in pairs(handlers) do |
|
138 if type(handler) == "table" then |
|
139 register_handlers(handler, event_base, (path_prefix and (path_prefix.."/") or "")..method_path); |
|
140 else |
|
141 local method, path, wildcard = method_path:match"^(%w+)_(.-)(_?)$"; |
|
142 method = method:upper(); |
|
143 wildcard = wildcard == "_" and (path == "" and "*" or "/*") or ""; |
|
144 local template_name = path; |
|
145 if path == "" then |
|
146 template_name = "index"; |
|
147 end |
|
148 |
|
149 if path_prefix then |
|
150 path = path_prefix .. "/" .. path; |
|
151 end |
|
152 |
|
153 if templates[template_name] then |
|
154 log("debug", "handler for %s %s (wildcard: %s) with template %s", method, path, wildcard, template_name); |
|
155 else |
|
156 log("debug", "No template (%s) for %s", template_name, path); |
|
157 end |
|
158 |
|
159 local path_prefix_len; |
|
160 if wildcard == "/*" then |
|
161 path_prefix_len = #path+1; |
|
162 end |
|
163 |
|
164 handler = wrap_handler(handler, templates[template_name], path_prefix_len); |
|
165 local head_event = event_base:format("HEAD", path, wildcard); |
|
166 http_server.add_handler(head_event, head_handler(handler)); |
|
167 |
|
168 local event_name = event_base:format(method, path, wildcard); |
|
169 http_server.add_handler(event_name, handler); |
|
170 log("debug", "Handler for %s added with template %s", event_name, template_name); |
|
171 end |
|
172 end |
|
173 end |
|
174 |
|
175 local function init(_config, events) |
|
176 config = _config or {}; |
|
177 |
|
178 templates = require "templates".init(config); |
|
179 |
|
180 local base_url = url.parse(config.base_url or "http://localhost:8006/"); |
|
181 local base_path = url.parse_path(config.base_path or base_url.path or "/"); |
|
182 base_path.is_directory = true; |
|
183 base_url.path = url.build_path(base_path); |
|
184 config.base_host = base_url.host; |
|
185 config.base_path = base_url.path; |
|
186 config.base_url = url.build(base_url); |
|
187 local event_base = ("%%s %s%s%%s%%s"):format(base_url.host, base_url.path); |
|
188 |
|
189 local handlers = require "app.routes"; |
|
190 |
|
191 handlers.get_static_ = files.serve { path = "./static", http_base = base_url.path.."static" }; |
|
192 |
|
193 http_server.add_host(base_url.host); |
|
194 http_server.set_default_host(base_url.host); |
|
195 http_server.add_handler("http-error", function (error) |
|
196 return handle_error(error, config) |
|
197 end); |
|
198 |
|
199 register_handlers(handlers, event_base); |
|
200 |
|
201 local listen_port = config.listen_port or 8007; |
|
202 local listen_interface = config.listen_interface or "*"; |
|
203 assert(http_server.listen_on(listen_port, listen_interface)); |
|
204 log("info", "Serving web interface on http://localhost:%d/", listen_port); |
|
205 end |
|
206 |
|
207 return { |
|
208 init = init; |
|
209 render = render; |
|
210 } |