File

util/prosodyctl/shell.lua @ 13843:87dd8639f08f 13.0

mod_invites_register: Stricter validation of registration events This fixes two problems: 1) Account invites that were created with a specific username were not in fact restricted to that username. 2) Password reset invites were not restricted to resetting passwords, but could be used to create an arbitrary new account if the client or registration frontend (e.g. mod_invites_register_web) doesn't handle/enforce the username. This new validation ensures that registrations and resets are always for the username specified in the invitation.
author Matthew Wild <mwild1@gmail.com>
date Thu, 10 Apr 2025 16:07:32 +0100
parent 13796:c8e534b4f2e2
line wrap: on
line source

local config = require "prosody.core.configmanager";
local human_io = require "prosody.util.human.io";
local server = require "prosody.net.server";
local st = require "prosody.util.stanza";
local path = require "prosody.util.paths";
local parse_args = require "prosody.util.argparse".parse;
local tc = require "prosody.util.termcolours";
local isatty = require "prosody.util.pposix".isatty;
local term_width = require"prosody.util.human.io".term_width;

local have_readline, readline = pcall(require, "readline");

local adminstream = require "prosody.util.adminstream";

if have_readline then
	readline.set_readline_name("prosody");
	readline.set_options({
			histfile = path.join(prosody.paths.data, ".shell_history");
			ignoredups = true;
		});
end

local function read_line(prompt_string)
	if have_readline then
		return readline.readline(prompt_string);
	else
		io.write(prompt_string);
		return io.read("*line");
	end
end

local function send_line(client, line, interactive)
	client.send(st.stanza("repl-input", { width = tostring(term_width()), repl = interactive == false and "0" or "1" }):text(line));
end

local function repl(client)
	local line = read_line(client.prompt_string or "prosody> ");
	if not line or line == "quit" or line == "exit" or line == "bye" then
		if not line then
			print("");
		end
		if have_readline then
			readline.save_history();
		end
		os.exit(0, true);
	end
	send_line(client, line);
end

local function printbanner()
	local banner = config.get("*", "console_banner");
	if banner then return print(banner); end
	print([[
                     ____                \   /     _
                    |  _ \ _ __ ___  ___  _-_   __| |_   _
                    | |_) | '__/ _ \/ __|/ _ \ / _` | | | |
                    |  __/| | | (_) \__ \ |_| | (_| | |_| |
                    |_|   |_|  \___/|___/\___/ \__,_|\__, |
                    A study in simplicity            |___/

]]);
	print("Welcome to the Prosody administration console. For a list of commands, type: help");
	print("You may find more help on using this console in our online documentation at ");
	print("https://prosody.im/doc/console\n");
end

local function check()
	local lfs = require "lfs";
	local socket_path = path.resolve_relative_path(prosody.paths.data, config.get("*", "admin_socket") or "prosody.sock");
	local state = lfs.attributes(socket_path, "mode");
	return state == "socket";
end

local function start(arg) --luacheck: ignore 212/arg
	local client = adminstream.client();
	local opts, err, where = parse_args(arg);
	local ttyout = isatty(io.stdout);

	if not opts then
		if err == "param-not-found" then
			print("Unknown command-line option: "..tostring(where));
		elseif err == "missing-value" then
			print("Expected a value to follow command-line option: "..where);
		end
		os.exit(1);
	end

	if arg[1] then
		if arg[2] then
			arg[1] = ("{"..string.rep("%q", #arg, ", ").."}"):format(table.unpack(arg, 1, #arg));
		end

		client.events.add_handler("connected", function()
			send_line(client, arg[1], false);
			return true;
		end, 1);

		local errors = 0; -- TODO This is weird, but works for now.
		client.events.add_handler("received", function(stanza)
			if stanza.name == "repl-output" or stanza.name == "repl-result" then
				local dest = io.stdout;
				if stanza.attr.type == "error" then
					errors = errors + 1;
					dest = io.stderr;
				end
				if stanza.attr.eol == "0" then
					dest:write(stanza:get_text());
				else
					dest:write(stanza:get_text(), "\n");
				end
			end
			if stanza.name == "repl-result" then
				os.exit(errors);
			end
			return true;
		end, 1);
	end

	client.events.add_handler("connected", function ()
		if not opts.quiet then
			printbanner();
		end
		repl(client);
	end);

	client.events.add_handler("disconnected", function ()
		print("--- session closed ---");
		os.exit(0, true);
	end);

	client.events.add_handler("received", function (stanza)
		if stanza.name ~= "repl-request-input" then
			return;
		end
		if stanza.attr.type == "password" then
			local password = human_io.read_password();
			client.send(st.stanza("repl-requested-input", {
				type = stanza.attr.type;
				id = stanza.attr.id;
				status = password and "submit" or "cancel";
			}):text(password or ""));
		else
			io.stderr:write("Internal error - unexpected input request type "..tostring(stanza.attr.type).."\n");
			os.exit(1);
		end
		return true;
	end, 2);


	client.events.add_handler("received", function (stanza)
		if stanza.name == "repl-output" or stanza.name == "repl-result" then
			local result_prefix = stanza.attr.type == "error" and "!" or "|";
			local out = result_prefix.." "..stanza:get_text();
			if ttyout and stanza.attr.type == "error" then
				out = tc.getstring(tc.getstyle("red"), out);
			end
			print(out);
		end
		if stanza.name == "repl-result" then
			repl(client);
		end
	end);

	client.prompt_string = config.get("*", "admin_shell_prompt");

	local socket_path = path.resolve_relative_path(prosody.paths.data, opts.socket or config.get("*", "admin_socket") or "prosody.sock");
	local conn = adminstream.connection(socket_path, client.listeners);
	local ok, err = conn:connect();
	if not ok then
		if err == "no unix socket support" then
			print("** LuaSocket unix socket support not available or incompatible, ensure your");
			print("** version is up to date.");
		else
			print("** Unable to connect to server - is it running? Is mod_admin_shell enabled?");
			print("** Connection error: "..err);
		end
		os.exit(1);
	end
	server.loop();
end

return {
	shell = start;
	available = check;
};