--- /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; +}