src/web/web.lua

Thu, 22 Jun 2023 21:31:36 +0100

author
Matthew Wild <mwild1@gmail.com>
date
Thu, 22 Jun 2023 21:31:36 +0100
changeset 17
b284dc4816cd
parent 0
6279a7d40ae7
permissions
-rw-r--r--

web: Add a few new helper functions

local http_util = require "util.http";
local usercookie = require"util.usercookie";
local uuid = require "util.uuid";

local function unpack_cookies(request)
	if not request.cookies then
		request.cookies = usercookie.decode(request.headers.cookie);
	end
end

local post_parsers = {
	["application/x-www-form-urlencoded"] = http_util.formdecode;
};

local function parse_body(request)
	local content_type = request.headers.content_type;
	if not content_type then
		--log("warn", "No Content-Type header sent");
		return nil, 400;
	end
	local post_parser = post_parsers[content_type];
	if not post_parser then
		--log("warn", "Don't know how to parse %s", content_type);
		return nil, 415;
	end
	local post_body = post_parser(request.body);
	if type(post_body) ~= "table" then
		--log("warn", "Could not parse %s %q, got %s", content_type, request.body, type(post_body));
		return nil, 415;
	end
	return post_body;
end

local csrf_token_len = #uuid.generate();

local function validate_csrf(csrf_token, request)
	if request.headers.origin == nil and request.headers.referer == nil then
		return true; -- Probably a non-browser request
	end
	if not (csrf_token and #csrf_token == csrf_token_len) then
		return false;
	end
	unpack_cookies(request);
	return request.cookies.csrf_token == csrf_token;
end

local function parse_body_and_csrf(request)
	local post_body, err = parse_body(request);
	if not post_body then return nil, err; end
	if not validate_csrf(post_body.csrf_token, request) then
		--log("warn", "CSRF error (token: '%s', cookie: '%s')",
		--	tostring(post_body.csrf_token), tostring(request.headers.cookie));
		return nil, 400;
	end
	return post_body;
end

local function parse_query(request)
	local q = request.url.query;
	return q and http_util.formdecode(q) or nil;
end

-- Cookies

local function add_header(headers, header, value)
	if headers[header] then
		headers[header] = headers[header] .. ", " .. value;
	else
		headers[header] = value;
	end
end

local function prefix_header(headers, header, value)
	if headers[header] then
		headers[header] = value .. ", " .. headers[header];
	else
		headers[header] = value;
	end
end

local response_mt = {};

local function redirect(to, code)
	return setmetatable({
		status_code = code or 303;
		headers = {
			Location = to;
		}
	}, response_mt);
end

local function is_response(obj)
	return getmetatable(obj) == response_mt;
end

local function set_cookie(headers, cookie, opts)
	if opts then
		local params = {""};
		if opts.path then
			table.insert(params, "Path="..opts.path);
		end
		if opts.ttl then
			table.insert(params, ("Max-Age=%d"):format(opts.ttl));
		end
		if opts.http_only then
			table.insert(params, "HttpOnly");
		end
		if opts.secure then
			table.insert(params, "Secure");
		end

		if #params > 1 then
			cookie = cookie .. table.concat(params, "; ");
		end
	end

	prefix_header(headers, "set_cookie", cookie);
end

local function set_auth_cookie(username, response, secret)
	local expires = config.cookie_ttl or 604800;
	local cookie = usercookie.generate(username, os.time()+expires, secret);
	cookie = "__Host-auth=".. cookie .. "; Path="..config.base_path
		.."; Max-Age="..tostring(expires).."; Secure; HttpOnly";
	return set_cookie(response.headers, cookie);
end

local function verify_auth_cookie(request, secret)
	unpack_cookies(request);
	request.cookies.auth = usercookie.verify(request.cookies["__Host-auth"], secret);
end

return {
	unpack_cookies = unpack_cookies;
	validate_csrf = validate_csrf;
	parse_body_and_csrf = parse_body_and_csrf;
	parse_body = parse_body;
	parse_query = parse_query;
	add_header = add_header;
	prefix_header = prefix_header;
	redirect = redirect;
	set_cookie = set_cookie;
	set_auth_cookie = set_auth_cookie;
	verify_auth_cookie = verify_auth_cookie;
	is_response = is_response;
};

mercurial