src/http.lua

changeset 0
6279a7d40ae7
child 12
dfa7cb60647e
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/http.lua	Tue Mar 09 12:16:56 2021 +0000
@@ -0,0 +1,210 @@
+local http_server = require"net.http.server";
+local files = require "net.http.files";
+local http_codes = require"net.http.codes";
+local promise = require "util.promise";
+local url = require"socket.url";
+local uuid = require"util.uuid";
+local web = require "util.web";
+local render = require "render".render;
+local ok, json = pcall(require, "cjson");
+if not ok then json = require"util.json"; end
+local errors = require "util.error".init("web", {});
+
+local templates;
+local config;
+
+local log = require "util.logger".init("web");
+
+local usercookie = require"util.usercookie";
+local secret = uuid.generate();
+
+local check_auth = require "app.auth".check_auth;
+
+local function set_auth_cookie(username, response)
+	local expires = config.cookie_ttl or 604800;
+	local cookie = usercookie.generate(username, os.time()+expires, secret);
+	cookie = "remember=".. cookie .. "; Path="..config.base_path
+		.."; Max-Age="..tostring(expires).."; HttpOnly";
+	return web.set_cookie(response.headers, cookie);
+end
+
+local csrf_token_len = #uuid.generate();
+
+local function check_csrf(event, viewdata)
+	local request, response = event.request, event.response;
+	web.unpack_cookies(request);
+	local csrf_token = request.cookies.csrf_token;
+	log("debug", "csrf_token=%s", tostring(csrf_token));
+	if csrf_token and #csrf_token == csrf_token_len then
+		viewdata.csrf_token = csrf_token;
+	else
+		csrf_token = uuid.generate();
+		viewdata.csrf_token = csrf_token;
+		web.set_cookie(response.headers, "csrf_token=" .. csrf_token .. "; Path="..config.base_path.."; HttpOnly");
+	end
+end
+
+local function wrap_handler(f, t, path_prefix_len)
+	return function (event)
+		log("debug", "Check auth...");
+		local authed = check_auth(event.request, config);
+		log("debug", "Checked, %s", authed);
+		event.config = config;
+		local sub_path = nil;
+		if path_prefix_len then
+			sub_path = event.request.path:sub(path_prefix_len+2);
+		end
+		local p = f(event, sub_path);
+		return promise.resolve(p):next(function (resp)
+			if type(resp) == "table" then
+				local headers = event.response.headers;
+				local accept = event.request.headers.accept or "";
+				web.add_header(headers, "vary", "Accept, Cookie");
+				if accept:find"application/json" then
+					headers.content_type = "application/json";
+					return json.encode(resp);
+				elseif t then
+					if authed or event.needs_csrf then check_csrf(event, resp); end
+					resp.authenticated = event.request.authenticated;
+					resp.config = config;
+					resp = render(t, resp);
+					if not headers.content_type then
+						headers.content_type = "text/html; charset=utf-8";
+					end
+				else
+					return resp;
+				end
+			end
+			return resp;
+		end):catch(function (err)
+			log("error", "Failed inside handler wrapper: %s", json.encode(err));
+			return promise.reject(errors.wrap(err));
+		end);
+	end
+end
+
+local function handle_error(error, config)
+	if not error.response then
+		-- Top-level error, return text string
+		if config.debug then
+			return error.private_message;
+		end
+		return "An internal error occurred.";
+	end
+
+	error.response.headers.content_type = "text/html; charset=utf-8";
+
+	log("warn", "HTTP error handler triggered (debug: %s): %d %s", not not config.debug, error.code, json.encode(error.error));
+
+	local err = errors.wrap(error.err);
+
+	local r = render(templates.error or "{text}", {
+		config = config;
+		code = err.code or error.code;
+		long = err.condition or http_codes[error.code];
+		text = err.text or "An unexpected error occurred. Please try again later.";
+		id = err.instance_id;
+		internal_error = config.debug and error.error and error.error.context.wrapped_error;
+	});
+	return r;
+end
+
+local function redirect(to, code)
+	code = code or 303;
+	return function (event)
+		if event.request.url.query then
+			event.response.headers.location = to.."?"..event.request.url.query;
+		else
+			event.response.headers.location = to;
+		end
+		return code;
+	end
+end
+
+local function size_only(request, data)
+	request.headers.content_size = #data;
+	return 200;
+end
+
+local function head_handler(handler)
+	return function (event)
+		event.send = size_only;
+		return handler(event);
+	end
+end
+
+local function register_handlers(handlers, event_base, path_prefix)
+	for method_path, handler in pairs(handlers) do
+		if type(handler) == "table" then
+			register_handlers(handler, event_base, (path_prefix and (path_prefix.."/") or "")..method_path);
+		else
+			local method, path, wildcard = method_path:match"^(%w+)_(.-)(_?)$";
+			method = method:upper();
+			wildcard = wildcard == "_" and (path == "" and "*" or "/*") or "";
+			local template_name = path;
+			if path == "" then
+				template_name = "index";
+			end
+
+			if path_prefix then
+				path = path_prefix .. "/" .. path;
+			end
+
+			if templates[template_name] then
+				log("debug", "handler for %s %s (wildcard: %s) with template %s", method, path, wildcard, template_name);
+			else
+				log("debug", "No template (%s) for %s", template_name, path);
+			end
+
+			local path_prefix_len;
+			if wildcard == "/*" then
+				path_prefix_len = #path+1;
+			end
+
+			handler = wrap_handler(handler, templates[template_name], path_prefix_len);
+			local head_event = event_base:format("HEAD", path, wildcard);
+			http_server.add_handler(head_event, head_handler(handler));
+
+			local event_name = event_base:format(method, path, wildcard);
+			http_server.add_handler(event_name, handler);
+			log("debug", "Handler for %s added with template %s", event_name, template_name);
+		end
+	end
+end
+
+local function init(_config, events)
+	config = _config or {};
+
+	templates = require "templates".init(config);
+
+	local base_url = url.parse(config.base_url or "http://localhost:8006/");
+	local base_path = url.parse_path(config.base_path or base_url.path or "/");
+	base_path.is_directory = true;
+	base_url.path = url.build_path(base_path);
+	config.base_host = base_url.host;
+	config.base_path = base_url.path;
+	config.base_url = url.build(base_url);
+	local event_base = ("%%s %s%s%%s%%s"):format(base_url.host, base_url.path);
+
+	local handlers = require "app.routes";
+
+	handlers.get_static_ = files.serve { path = "./static", http_base = base_url.path.."static" };
+
+	http_server.add_host(base_url.host);
+	http_server.set_default_host(base_url.host);
+	http_server.add_handler("http-error", function (error)
+		return handle_error(error, config)
+	end);
+
+	register_handlers(handlers, event_base);
+
+	local listen_port = config.listen_port or 8007;
+	local listen_interface = config.listen_interface or "*";
+	assert(http_server.listen_on(listen_port, listen_interface));
+	log("info", "Serving web interface on http://localhost:%d/", listen_port);
+end
+
+return {
+	init = init;
+	render = render;
+}

mercurial