File

mod_auth_wordpress/mod_auth_wordpress.lua @ 6110:1a6cd0bbb7ab

mod_compliance_2023: Add 2023 Version of the compliance module, basis is the 2021 Version. diff --git a/mod_compliance_2023/README.md b/mod_compliance_2023/README.md new file mode 100644 --- /dev/null +++ b/mod_compliance_2023/README.md @@ -0,0 +1,22 @@ +--- +summary: XMPP Compliance Suites 2023 self-test +labels: +- Stage-Beta +rockspec: + dependencies: + - mod_cloud_notify + +... + +Compare the list of enabled modules with +[XEP-0479: XMPP Compliance Suites 2023] and produce basic report to the +Prosody log file. + +If installed with the Prosody plugin installer then all modules needed for a green checkmark should be included. (With prosody 0.12 only [mod_cloud_notify] is not included with prosody and we need the community module) + +# Compatibility + + Prosody-Version Status + --------------- ---------------------- + trunk Works as of 2024-12-21 + 0.12 Works diff --git a/mod_compliance_2023/mod_compliance_2023.lua b/mod_compliance_2023/mod_compliance_2023.lua new file mode 100644 --- /dev/null +++ b/mod_compliance_2023/mod_compliance_2023.lua @@ -0,0 +1,79 @@ +-- Copyright (c) 2021 Kim Alvefur +-- +-- This module is MIT licensed. + +local hostmanager = require "core.hostmanager"; + +local array = require "util.array"; +local set = require "util.set"; + +local modules_enabled = module:get_option_inherited_set("modules_enabled"); + +for host in pairs(hostmanager.get_children(module.host)) do + local component = module:context(host):get_option_string("component_module"); + if component then + modules_enabled:add(component); + modules_enabled:include(module:context(host):get_option_set("modules_enabled", {})); + end +end + +local function check(suggested, alternate, ...) + if set.intersection(modules_enabled, set.new({suggested; alternate; ...})):empty() then return suggested; end + return false; +end + +local compliance = { + array {"Server"; check("tls"); check("disco")}; + + array {"Advanced Server"; check("pep", "pep_simple")}; + + array {"Web"; check("bosh"); check("websocket")}; + + -- No Server requirements for Advanced Web + + array {"IM"; check("vcard_legacy", "vcard"); check("carbons"); check("http_file_share", "http_upload")}; + + array { + "Advanced IM"; + check("vcard_legacy", "vcard"); + check("blocklist"); + check("muc"); + check("private"); + check("smacks"); + check("mam"); + check("bookmarks"); + }; + + array {"Mobile"; check("smacks"); check("csi_simple", "csi_battery_saver")}; + + array {"Advanced Mobile"; check("cloud_notify")}; + + array {"A/V Calling"; check("turn_external", "external_services", "turncredentials", "extdisco")}; + +}; + +function check_compliance() + local compliant = true; + for _, suite in ipairs(compliance) do + local section = suite:pop(1); + if module:get_option_boolean("compliance_" .. section:lower():gsub("%A", "_"), true) then + local missing = set.new(suite:filter(function(m) return type(m) == "string" end):map(function(m) return "mod_" .. m end)); + if suite[1] then + if compliant then + compliant = false; + module:log("warn", "Missing some modules for XMPP Compliance 2023"); + end + module:log("info", "%s Compliance: %s", section, missing); + end + end + end + + if compliant then module:log("info", "XMPP Compliance 2023: Compliant ✔️"); end +end + +if prosody.start_time then + check_compliance() +else + module:hook_global("server-started", check_compliance); +end +
author Menel <menel@snikket.de>
date Sun, 22 Dec 2024 16:06:28 +0100
parent 1343:7dbde05b48a9
line wrap: on
line source

-- Wordpress authentication backend for Prosody
--
-- Copyright (C) 2011 Waqas Hussain
-- Copyright (C) 2011 Kim Alvefur
--

local log = require "util.logger".init("auth_sql");
local new_sasl = require "util.sasl".new;
local nodeprep = require "util.encodings".stringprep.nodeprep;
local saslprep = require "util.encodings".stringprep.saslprep;
local DBI = require "DBI"
local md5 = require "util.hashes".md5;
local uuid_gen = require "util.uuid".generate;

local connection;
local params = module:get_option("sql");
local table_prefix = module:get_option_string("wordpress_table_prefix", "wp_");

local resolve_relative_path = require "core.configmanager".resolve_relative_path;

local function test_connection()
	if not connection then return nil; end
	if connection:ping() then
		return true;
	else
		module:log("debug", "Database connection closed");
		connection = nil;
	end
end
local function connect()
	if not test_connection() then
		prosody.unlock_globals();
		local dbh, err = DBI.Connect(
			params.driver, params.database,
			params.username, params.password,
			params.host, params.port
		);
		prosody.lock_globals();
		if not dbh then
			module:log("debug", "Database connection failed: %s", tostring(err));
			return nil, err;
		end
		module:log("debug", "Successfully connected to database");
		dbh:autocommit(true); -- don't run in transaction
		connection = dbh;
		return connection;
	end
end

do -- process options to get a db connection
	params = params or { driver = "SQLite3" };

	if params.driver == "SQLite3" then
		params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite");
	end

	assert(params.driver and params.database, "Both the SQL driver and the database need to be specified");

	assert(connect());
end

local function getsql(sql, ...)
	if params.driver == "PostgreSQL" then
		sql = sql:gsub("`", "\"");
	end
	if not test_connection() then connect(); end
	-- do prepared statement stuff
	local stmt, err = connection:prepare(sql);
	if not stmt and not test_connection() then error("connection failed"); end
	if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end
	-- run query
	local ok, err = stmt:execute(...);
	if not ok and not test_connection() then error("connection failed"); end
	if not ok then return nil, err; end

	return stmt;
end
local function setsql(sql, ...)
	local stmt, err = getsql(sql, ...);
	if not stmt then return stmt, err; end
	return stmt:affected();
end

local function get_password(username)
	local stmt, err = getsql("SELECT `user_pass` FROM `"..table_prefix.."users` WHERE `user_login`=?", username);
	if stmt then
		for row in stmt:rows(true) do
			return row.user_pass;
		end
	end
end


local itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
local function hashEncode64(input, count)
	local output = "";
	local i, value = 0, 0;

	while true do
		value = input:byte(i+1)
		i = i+1;
		local idx = value % 0x40 + 1;
		output = output .. itoa64:sub(idx, idx);

		if i < count then
			value = value + input:byte(i+1) * 256;
		end
		local _ = value % (2^6);
		local idx = ((value - _) / (2^6)) % 0x40 + 1
		output = output .. itoa64:sub(idx, idx);

		if i >= count then break; end
		i = i+1;

		if i < count then
			value = value + input:byte(i+1) * 256 * 256;
		end
		local _ = value % (2^12);
		local idx = ((value - _) / (2^12)) % 0x40 + 1
		output = output .. itoa64:sub(idx, idx);

		if i >= count then break; end
		i = i+1;

		local _ = value % (2^18);
		local idx = ((value - _) / (2^18)) % 0x40 + 1
		output = output .. itoa64:sub(idx, idx);

		if not(i < count) then break; end
	end
	return output;
end
local function hashCryptPrivate(password, genSalt)
	local output = "*";
	if not genSalt:match("^%$P%$") then return output; end

	local count_log2 = itoa64:find(genSalt:sub(4,4)) - 1;
	if count_log2 < 7 or count_log2 > 30 then return output; end

	local count = 2 ^ count_log2;
	local salt = genSalt:sub(5, 12);

	if #salt ~= 8 then return output; end

	local hash = md5(salt..password);

	while true do
		hash = md5(hash..password);
		if not(count > 1) then break; end
		count = count-1;
	end

	output = genSalt:sub(1, 12);
	output = output .. hashEncode64(hash, 16);

	return output;
end
local function hashGensaltPrivate(input)
	local iteration_count_log2 = 6;
	local output = "$P$";
	local idx = math.min(iteration_count_log2 + 5, 30) + 1;
	output = output .. itoa64:sub(idx, idx);
	output = output .. hashEncode64(input, 6);
	return output;
end
local function wordpressCheckHash(password, hash)
	if #hash == 32 then return hash == md5(password, true); end
	return #hash == 34 and hashCryptPrivate(password, hash) == hash;
end
local function wordpressCreateHash(password)
	local random = uuid_gen():sub(-6);
	local salt = hashGensaltPrivate(random);
	local hash = hashCryptPrivate(password, salt);
	if #hash == 34 then return hash; end
	return md5(password, true);
end


provider = {};

function provider.test_password(username, password)
	local hash = get_password(username);
	return hash and wordpressCheckHash(password, hash);
end
function provider.user_exists(username)
	module:log("debug", "test user %s existence", username);
	return get_password(username) and true;
end

function provider.get_password(username)
	return nil, "Getting password is not supported.";
end
function provider.set_password(username, password)
	local hash = wordpressCreateHash(password);
	local stmt, err = setsql("UPDATE `"..table_prefix.."users` SET `user_pass`=? WHERE `user_login`=?", hash, username);
	return stmt and true, err;
end
function provider.create_user(username, password)
	return nil, "Account creation/modification not supported.";
end

local escapes = {
	[" "] = "\\20";
	['"'] = "\\22";
	["&"] = "\\26";
	["'"] = "\\27";
	["/"] = "\\2f";
	[":"] = "\\3a";
	["<"] = "\\3c";
	[">"] = "\\3e";
	["@"] = "\\40";
	["\\"] = "\\5c";
};
local unescapes = {};
for k,v in pairs(escapes) do unescapes[v] = k; end
local function jid_escape(s) return s and (s:gsub(".", escapes)); end
local function jid_unescape(s) return s and (s:gsub("\\%x%x", unescapes)); end

function provider.get_sasl_handler()
	local sasl = {};
	function sasl:clean_clone() return provider.get_sasl_handler(); end
	function sasl:mechanisms() return { PLAIN = true; }; end
	function sasl:select(mechanism)
		if not self.selected and mechanism == "PLAIN" then
			self.selected = mechanism;
			return true;
		end
	end
	function sasl:process(message)
		if not message then return "failure", "malformed-request"; end
		local authorization, authentication, password = message:match("^([^%z]*)%z([^%z]+)%z([^%z]+)");
		if not authorization then return "failure", "malformed-request"; end
		authentication = saslprep(authentication);
		password = saslprep(password);
		if (not password) or (password == "") or (not authentication) or (authentication == "") then
			return "failure", "malformed-request", "Invalid username or password.";
		end
		local function test(authentication)
			local prepped = nodeprep(authentication);
			local normalized = jid_unescape(prepped);
			return normalized and provider.test_password(normalized, password) and prepped;
		end
		local username = test(authentication) or test(jid_escape(authentication));
		if username then
			self.username = username;
			return "success";
		end
		return "failure", "not-authorized", "Unable to authorize you with the authentication credentials you've sent.";
	end
	return sasl;
end

module:provides("auth", provider);