src/http.lua

changeset 0
6279a7d40ae7
child 12
dfa7cb60647e
equal deleted inserted replaced
-1:000000000000 0:6279a7d40ae7
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 }

mercurial