Mon, 22 Nov 2021 10:40:32 +0000
Pin to latest Prosody trunk revision (eventually aiming for 0.12)
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; }