File

mod_json_streams/mod_json_streams.lua @ 5418:f2c7bb3af600

mod_http_oauth2: Add role selector to consent page List includes all roles available to the user, if more than one. Defaults to either the first role in the scope string or the users primary role. Earlier draft listed all roles, but having options that can't be selected is bad UX and the entire list of all roles on the server could be long, and perhaps even sensitive. Allows e.g. picking a role with fewer permissions than what might otherwise have been selected. UX wise, doing this with more checkboxes or possibly radio buttons would have been confusion and/or looked messier. Fixes the previous situation where unselecting a role would default to the primary role, which could be more permissions than requested.
author Kim Alvefur <zash@zash.se>
date Fri, 05 May 2023 01:23:13 +0200
parent 350:98569ec25ac2
line wrap: on
line source

--
-- XEP-0295: JSON Encodings for XMPP
--

module.host = "*"

local httpserver = require "net.httpserver";
local filters = require "util.filters"
local json = require "util.json"

local json_escapes = {
	["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f",
	["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"};

local s_char = string.char;
for i=0,31 do
	local ch = s_char(i);
	if not json_escapes[ch] then json_escapes[ch] = ("\\u%.4X"):format(i); end
end

local state_out = 0;
local state_key_before = 1;
local state_key_in = 2;
local state_key_escape = 3;
local state_key_after = 4;
local state_val_before = 5;
local state_val_in = 6;
local state_val_escape = 7;
local state_val_after = 8;

local whitespace = { [" "] = true, ["\n"] = true, ["\r"] = true, ["\t"] = true };
function json_decoder()
	local state = state_out;
	local quote;
	local output = "";
	local buffer = "";
	return function(input)
		for ch in input:gmatch(".") do
			module:log("debug", "%s | %d", ch, state)
			local final = false;
			if state == state_out then
				if whitespace[ch] then
				elseif ch ~= "{" then return nil, "{ expected";
				else state = state_key_before end
			elseif state == state_key_before then
				if whitespace[ch] then
				elseif ch ~= "'" and ch ~= "\"" then return nil, "\" expected";
				else quote = ch; state = state_key_in; end
			elseif state == state_key_in then
				if ch == quote then state = state_key_after;
				elseif ch ~= "s" then return nil, "invalid key, 's' expected"; -- only s as key allowed
				else end -- ignore key
			elseif state == state_key_after then
				if whitespace[ch] then
				elseif ch ~= ":" then return nil, ": expected";
				else state = state_val_before; end
			elseif state == state_val_before then
				if whitespace[ch] then
				elseif ch ~= "'" and ch ~= "\"" then return nil, "\" expected";
				else quote = ch; state = state_val_in; end
			elseif state == state_val_in then
				if ch == quote then state = state_val_after;
				elseif ch == "\\" then state = state_val_escape;
				else end
			elseif state == state_val_after then
				if whitespace[ch] then
				elseif ch ~= "}" then return nil, "} expected";
				else state = state_out;
					final = true;
				end
			elseif state == state_val_escape then
				state = state_val_in;
			else
				module:log("error", "Unhandled state: "..state);
				return nil, "Unhandled state in parser"
			end
			buffer = buffer..ch;
			if final then
				module:log("debug", "%s", buffer)
				local tmp;
				pcall(function() tmp = json.decode(buffer); end);
				if not tmp then return nil, "Invalid JSON"; end
				output, buffer = output..tmp.s, "";
			end
		end
		local _ = output; output = "";
		return _;
	end;
end

function filter_hook(session)
	local determined = false;
	local is_json = false;
	local function in_filter(t)
		if not determined then
			is_json = (t:sub(1,1) == "{") and json_decoder();
			determined = true;
		end
		if is_json then
			local s, err = is_json(t);
			if not err then return s; end
			session:close("not-well-formed");
			return;
		end
		return t;
	end
	local function out_filter(t)
		if is_json then
			return '{"s":"' .. t:gsub(".", json_escapes) .. '"}'; -- encode
		end
		return t;
	end
	filters.add_filter(session, "bytes/in", in_filter,   100);
	filters.add_filter(session, "bytes/out", out_filter, 100);
end

function module.load()
	filters.add_filter_hook(filter_hook);
end
function module.unload()
	filters.remove_filter_hook(filter_hook);
end

function encode(data)
	if type(data) == "string" then
		data = json.encode({ s = data });
	elseif type(data) == "table" and data.body then
		data.body = json.encode({ s = data.body });
		data.headers["Content-Type"] = "application/json";
	end
	return data;
end
function handle_request(method, body, request)
	local mod_bosh = modulemanager.get_module("*", "bosh")
	if mod_bosh then
		if body and method == "POST" then
			pcall(function() body = json.decode(body).s; end);
		end
		local _send = request.send;
		function request:send(data) return _send(self, encode(data)); end
		return encode(mod_bosh.handle_request(method, body, request));
	end
	return "<html><body>mod_bosh not loaded</body></html>";
end

local function setup()
	local ports = module:get_option("jsonstreams_ports") or { 5280 };
	httpserver.new_from_config(ports, handle_request, { base = "jsonstreams" });
end
if prosody.start_time then -- already started
	setup();
else
	prosody.events.add_handler("server-started", setup);
end