File

tools/ejabberdsql2prosody.lua @ 13652:a08065207ef0

net.server_epoll: Call :shutdown() on TLS sockets when supported Comment from Matthew: This fixes a potential issue where the Prosody process gets blocked on sockets waiting for them to close. Unlike non-TLS sockets, closing a TLS socket sends layer 7 data, and this can cause problems for sockets which are in the process of being cleaned up. This depends on LuaSec changes which are not yet upstream. From Martijn's original email: So first my analysis of luasec. in ssl.c the socket is put into blocking mode right before calling SSL_shutdown() inside meth_destroy(). My best guess to why this is is because meth_destroy is linked to the __close and __gc methods, which can't exactly be called multiple times and luasec does want to make sure that a tls session is shutdown as clean as possible. I can't say I disagree with this reasoning and don't want to change this behaviour. My solution to this without changing the current behaviour is to introduce a shutdown() method. I am aware that this overlaps in a conflicting way with tcp's shutdown method, but it stays close to the OpenSSL name. This method calls SSL_shutdown() in the current (non)blocking mode of the underlying socket and returns a boolean whether or not the shutdown is completed (matching SSL_shutdown()'s 0 or 1 return values), and returns the familiar ssl_ioerror() strings on error with a false for completion. This error can then be used to determine if we have wantread/wantwrite to finalize things. Once meth_shutdown() has been called once a shutdown flag will be set, which indicates to meth_destroy() that the SSL_shutdown() has been handled by the application and it shouldn't be needed to set the socket to blocking mode. I've left the SSL_shutdown() call in the LSEC_STATE_CONNECTED to prevent TOCTOU if the application reaches a timeout for the shutdown code, which might allow SSL_shutdown() to clean up anyway at the last possible moment. Another thing I've changed to luasec is the call to socket_setblocking() right before calling close(2) in socket_destroy() in usocket.c. According to the latest POSIX[0]: Note that the requirement for close() on a socket to block for up to the current linger interval is not conditional on the O_NONBLOCK setting. Which I read to mean that removing O_NONBLOCK on the socket before close doesn't impact the behaviour and only causes noise in system call tracers. I didn't touch the windows bits of this, since I don't do windows. For the prosody side of things I've made the TLS shutdown bits resemble interface:onwritable(), and put it under a combined guard of self._tls and self.conn.shutdown. The self._tls bit is there to prevent getting stuck on this condition, and self.conn.shutdown is there to prevent the code being called by instances where the patched luasec isn't deployed. The destroy() method can be called from various places and is read by me as the "we give up" error path. To accommodate for these unexpected entrypoints I've added a single call to self.conn:shutdown() to prevent the socket being put into blocking mode. I have no expectations that there is any other use here. Same as previous, the self.conn.shutdown check is there to make sure it's not called on unpatched luasec deployments and self._tls is there to make sure we don't call shutdown() on tcp sockets. I wouldn't recommend logging of the conn:shutdown() error inside close(), since a lot of clients simply close the connection before SSL_shutdown() is done.
author Martijn van Duren <martijn@openbsd.org>
date Thu, 06 Feb 2025 15:04:38 +0000
parent 13142:879a6a33c21b
line wrap: on
line source

#!/usr/bin/env lua
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--

prosody = {};

package.path = package.path ..";../?.lua";

local my_name = arg[0];
if my_name:match("[/\\]") then
	package.path = package.path..";"..my_name:gsub("[^/\\]+$", "../?.lua");
	package.cpath = package.cpath..";"..my_name:gsub("[^/\\]+$", "../?.so");
end
if not pcall(require, "prosody.loader") then
	pcall(require, "loader");
end


local serialize = require "prosody.util.serialization".serialize;
local st = require "prosody.util.stanza";
local parse_xml = require "prosody.util.xml".parse;
package.loaded["prosody.util.logger"] = {init = function() return function() end; end}
local dm = require "prosody.util.datamanager"
dm.set_data_path("data");

function parseFile(filename)
------

local file = nil;
local last = nil;
local line = 1;
local function read(expected)
	local ch;
	if last then
		ch = last; last = nil;
	else
		ch = file:read(1);
		if ch == "\n" then line = line + 1; end
	end
	if expected and ch ~= expected then error("expected: "..expected.."; got: "..(ch or "nil").." on line "..line); end
	return ch;
end
local function peek()
	if not last then last = read(); end
	return last;
end

local escapes = {
	["\\0"] = "\0";
	["\\'"] = "'";
	["\\\""] = "\"";
	["\\b"] = "\b";
	["\\n"] = "\n";
	["\\r"] = "\r";
	["\\t"] = "\t";
	["\\Z"] = "\26";
	["\\\\"] = "\\";
	["\\%"] = "%";
	["\\_"] = "_";
}
local function unescape(s)
	return escapes[s] or error("Unknown escape sequence: "..s);
end
local function readString()
	read("'");
	local s = "";
	while true do
		local ch = peek();
		if ch == "\\" then
			s = s..unescape(read()..read());
		elseif ch == "'" then
			break;
		else
			s = s..read();
		end
	end
	read("'");
	return s;
end
local function readNonString()
	local s = "";
	while true do
		if peek() == "," or peek() == ")" then
			break;
		else
			s = s..read();
		end
	end
	return tonumber(s);
end
local function readItem()
	if peek() == "'" then
		return readString();
	else
		return readNonString();
	end
end
local function readTuple()
	local items = {}
	read("(");
	while peek() ~= ")" do
		table.insert(items, readItem());
		if peek() == ")" then break; end
		read(",");
	end
	read(")");
	return items;
end
local function readTuples()
	if peek() ~= "(" then read("("); end
	local tuples = {};
	while true do
		table.insert(tuples, readTuple());
		if peek() == "," then read() end
		if peek() == ";" then break; end
	end
	return tuples;
end
local function readTableName()
	local tname = "";
	while peek() ~= "`" do tname = tname..read(); end
	return tname;
end
local function readInsert()
	if peek() == nil then return nil; end
	for ch in ("INSERT INTO `"):gmatch(".") do -- find line starting with this
		if peek() == ch then
			read(); -- found
		else -- match failed, skip line
			while peek() and read() ~= "\n" do end
			return nil;
		end
	end
	local tname = readTableName();
	read("`"); read(" ") -- expect this
	if peek() == "(" then -- skip column list
		repeat until read() == ")";
		read(" ");
	end
	for ch in ("VALUES "):gmatch(".") do read(ch); end -- expect this
	local tuples = readTuples();
	read(";"); read("\n");
	return tname, tuples;
end

local function readFile(filename)
	file = io.open(filename);
	if not file then error("File not found: "..filename); os.exit(0); end
	local t = {};
	while true do
		local tname, tuples = readInsert();
		if tname then
			if t[tname] then
				local t_name = t[tname];
				for i=1,#tuples do
					table.insert(t_name, tuples[i]);
				end
			else
				t[tname] = tuples;
			end
		elseif peek() == nil then
			break;
		end
	end
	return t;
end

return readFile(filename);

------
end

local arg, hostname = ...;
local help = "/? -? ? /h -h /help -help --help";
if not(arg and hostname) or help:find(arg, 1, true) then
	print([[ejabberd SQL DB dump importer for Prosody

  Usage: ejabberdsql2prosody.lua filename.txt hostname

The file can be generated using mysqldump:
  mysqldump db_name > filename.txt]]);
	os.exit(1);
end
local map = {
	["last"] = {"username", "seconds", "state"};
	["privacy_default_list"] = {"username", "name"};
	["privacy_list"] = {"username", "name", "id"};
	["privacy_list_data"] = {"id", "t", "value", "action", "ord", "match_all", "match_iq", "match_message", "match_presence_in", "match_presence_out"};
	["private_storage"] = {"username", "namespace", "data"};
	["rostergroups"] = {"username", "jid", "grp"};
	["rosterusers"] = {"username", "jid", "nick", "subscription", "ask", "askmessage", "server", "subscribe", "type"};
	["spool"] = {"username", "xml", "seq"};
	["users"] = {"username", "password"};
	["vcard"] = {"username", "vcard"};
	--["vcard_search"] = {};
}
local NULL = {};
local parsed = parseFile(arg);
for name, data in pairs(parsed) do
	local m = map[name];
	if m then
		if #data > 0 and #data[1] ~= #m then
			print("[warning] expected "..#m.." columns for table `"..name.."`, found "..#data[1]);
		end
		for i=1,#data do
			local row = data[i];
			for j=1,#m do
				row[m[j]] = row[j];
				row[j] = nil;
			end
		end
	end
end
--print(serialize(t));

for _, row in ipairs(parsed["users"] or NULL) do
	local node, password = row.username, row.password;
	local ret, err = dm.store(node, hostname, "accounts", {password = password});
	print("["..(err or "success").."] accounts: "..node.."@"..hostname);
end

function roster(node, host, jid, item)
	local roster = dm.load(node, host, "roster") or {};
	roster[jid] = item;
	local ret, err = dm.store(node, host, "roster", roster);
	print("["..(err or "success").."] roster: " ..node.."@"..host.." - "..jid);
end
function roster_pending(node, host, jid)
	local roster = dm.load(node, host, "roster") or {};
	roster.pending = roster.pending or {};
	roster.pending[jid] = true;
	local ret, err = dm.store(node, host, "roster", roster);
	print("["..(err or "success").."] roster-pending: " ..node.."@"..host.." - "..jid);
end
function roster_group(node, host, jid, group)
	local roster = dm.load(node, host, "roster") or {};
	local item = roster[jid];
	if not item then print("Warning: No roster item "..jid.." for user "..node..", can't put in group "..group); return; end
	item.groups[group] = true;
	local ret, err = dm.store(node, host, "roster", roster);
	print("["..(err or "success").."] roster-group: " ..node.."@"..host.." - "..jid.." - "..group);
end
function private_storage(node, host, xmlns, stanza)
	local private = dm.load(node, host, "private") or {};
	private[stanza.name..":"..xmlns] = st.preserialize(stanza);
	local ret, err = dm.store(node, host, "private", private);
	print("["..(err or "success").."] private: " ..node.."@"..host.." - "..xmlns);
end
function offline_msg(node, host, t, stanza)
	stanza.attr.stamp = os.date("!%Y-%m-%dT%H:%M:%SZ", t);
	stanza.attr.stamp_legacy = os.date("!%Y%m%dT%H:%M:%S", t);
	local ret, err = dm.list_append(node, host, "offline", st.preserialize(stanza));
	print("["..(err or "success").."] offline: " ..node.."@"..host.." - "..os.date("!%Y-%m-%dT%H:%M:%SZ", t));
end
for _, row in ipairs(parsed["rosterusers"] or NULL) do
	local node, contact = row.username, row.jid;
	local name = row.nick;
	if name == "" then name = nil; end
	local subscription = row.subscription;
	if subscription == "N" then
		subscription = "none"
	elseif subscription == "B" then
		subscription = "both"
	elseif subscription == "F" then
		subscription = "from"
	elseif subscription == "T" then
		subscription = "to"
	else error("Unknown subscription type: "..subscription) end;
	local ask = row.ask;
	if ask == "N" then
		ask = nil;
	elseif ask == "O" then
		ask = "subscribe";
	elseif ask == "I" then
		roster_pending(node, hostname, contact);
		ask = nil;
	elseif ask == "B" then
		roster_pending(node, hostname, contact);
		ask = "subscribe";
	else error("Unknown ask type: "..ask); end
	local item = {name = name, ask = ask, subscription = subscription, groups = {}};
	roster(node, hostname, contact, item);
end
for _, row in ipairs(parsed["rostergroups"] or NULL) do
	roster_group(row.username, hostname, row.jid, row.grp);
end
for _, row in ipairs(parsed["vcard"] or NULL) do
	local stanza, err = parse_xml(row.vcard);
	if stanza then
		local ret, err = dm.store(row.username, hostname, "vcard", st.preserialize(stanza));
		print("["..(err or "success").."] vCard: "..row.username.."@"..hostname);
	else
		print("[error] vCard XML parse failed: "..row.username.."@"..hostname);
	end
end
for _, row in ipairs(parsed["private_storage"] or NULL) do
	local stanza, err = parse_xml(row.data);
	if stanza then
		private_storage(row.username, hostname, row.namespace, stanza);
	else
		print("[error] Private XML parse failed: "..row.username.."@"..hostname);
	end
end
table.sort(parsed["spool"] or NULL, function(a,b) return a.seq < b.seq; end); -- sort by sequence number, just in case
local time_offset = os.difftime(os.time(os.date("!*t")), os.time(os.date("*t"))) -- to deal with timezones
local date_parse = function(s)
	local year, month, day, hour, min, sec = s:match("(....)-?(..)-?(..)T(..):(..):(..)");
	return os.time({year=year, month=month, day=day, hour=hour, min=min, sec=sec-time_offset});
end
for _, row in ipairs(parsed["spool"] or NULL) do
	local stanza, err = parse_xml(row.xml);
	if stanza then
		local last_child = stanza.tags[#stanza.tags];
		if not last_child or last_child ~= stanza[#stanza] then error("Last child of offline message is not a tag"); end
		if last_child.name ~= "x" and last_child.attr.xmlns ~= "jabber:x:delay" then error("Last child of offline message is not a timestamp"); end
		stanza[#stanza], stanza.tags[#stanza.tags] = nil, nil;
		local t = date_parse(last_child.attr.stamp);
		offline_msg(row.username, hostname, t, stanza);
	else
		print("[error] Offline message XML parsing failed: "..row.username.."@"..hostname);
	end
end