Thu, 22 Jun 2023 21:31:56 +0100
http: Add some helpful comments regarding auth/CSRF
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 csrf_token_len = #uuid.generate(); -- Add a CSRF token to the view data and cookie (for verification on next request) 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 -- We already have a CSRF token cookie 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, default_tpl, path_prefix_len, check_auth) return function (event) log("debug", "Check auth..."); local request = event.request; local authed, handler_override = check_auth(request, config); if authed and not request.authenticated then request.authenticated = authed; end 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 h = handler_override or f; local p, custom_tpl; if web.is_response(h) then p = h; else p, custom_tpl = (handler_override or f)(event, sub_path); end -- Process response (may be promises) return promise.join(function (resp, tpl) if type(resp) == "table" and not web.is_response(resp) 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 tpl then if authed or event.needs_csrf then check_csrf(event, resp); end resp.authenticated = event.request.authenticated; resp.config = config; resp = render(tpl, resp); if not headers.content_type then headers.content_type = "text/html; charset=utf-8"; end else return resp; end end return resp; end, p, custom_tpl or default_tpl):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 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, check_auth) 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, check_auth); 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, check_auth); 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.templates = templates; 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); _G.CONFIG = config; local check_auth = require "app.auth".check_auth; 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, nil, check_auth); 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; set_auth_cookie = set_auth_cookie; get_auth_cookie = get_auth_cookie; }