diff options
-rw-r--r-- | core/offlinemessage.lua | 13 | ||||
-rw-r--r-- | core/rostermanager.lua | 19 | ||||
-rw-r--r-- | core/stanza_dispatch.lua | 103 | ||||
-rw-r--r-- | core/usermanager.lua | 11 | ||||
-rw-r--r-- | main.lua | 275 | ||||
-rw-r--r-- | util/datamanager.lua | 79 | ||||
-rw-r--r-- | util/jid.lua | 8 | ||||
-rw-r--r-- | util/stanza.lua | 104 |
8 files changed, 612 insertions, 0 deletions
diff --git a/core/offlinemessage.lua b/core/offlinemessage.lua new file mode 100644 index 00000000..dda9b7d8 --- /dev/null +++ b/core/offlinemessage.lua @@ -0,0 +1,13 @@ + +require "util.datamanager" + +local datamanager = datamanager; +local t_insert = table.insert; + +module "offlinemessage" + +function new(user, host, stanza) + local offlinedata = datamanager.load(user, host, "offlinemsg") or {}; + t_insert(offlinedata, stanza); + return datamanager.store(user, host, "offlinemsg", offlinedata); +end diff --git a/core/rostermanager.lua b/core/rostermanager.lua new file mode 100644 index 00000000..7a1e7d4e --- /dev/null +++ b/core/rostermanager.lua @@ -0,0 +1,19 @@ + +local mainlog = log; +local function log(type, message) + mainlog(type, "rostermanager", message); +end + +local setmetatable = setmetatable; +local format = string.format; +local loadfile, setfenv, pcall = loadfile, setfenv, pcall; + +require "util.datamanager" + +local datamanager = datamanager; + +module "rostermanager" + +function getroster(username, host) + return datamanager.load(username, host, "roster") or {}; +end diff --git a/core/stanza_dispatch.lua b/core/stanza_dispatch.lua new file mode 100644 index 00000000..b7428ecd --- /dev/null +++ b/core/stanza_dispatch.lua @@ -0,0 +1,103 @@ + +require "util.stanza" + +local st = stanza; + +local t_concat = table.concat; +local format = string.format; + +function init_stanza_dispatcher(session) + local iq_handlers = {}; + + local session_log = session.log; + local log = function (type, msg) session_log(type, "stanza_dispatcher", msg); end + local send = session.send; + + + + iq_handlers["jabber:iq:auth"] = + function (stanza) + local username = stanza[1]:child_with_name("username"); + local password = stanza[1]:child_with_name("password"); + local resource = stanza[1]:child_with_name("resource"); + if not (username and password and resource) then + local reply = st.reply(stanza); + send(reply:query("jabber:iq:auth") + :tag("username"):up() + :tag("password"):up() + :tag("resource"):up()); + return true; + else + username, password, resource = t_concat(username), t_concat(password), t_concat(resource); + print(username, password, resource) + local reply = st.reply(stanza); + require "core.usermanager" + if usermanager.validate_credentials(session.host, username, password) then + -- Authentication successful! + session.username = username; + session.resource = resource; + if not hosts[session.host].sessions[username] then + hosts[session.host].sessions[username] = { sessions = {} }; + end + hosts[session.host].sessions[username].sessions[resource] = session; + send(st.reply(stanza)); + return true; + else + local reply = st.reply(stanza); + reply.attr.type = "error"; + reply:tag("error", { code = "401", type = "auth" }) + :tag("not-authorized", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" }); + send(reply); + return true; + end + end + + end + + iq_handlers["jabber:iq:roster"] = + function (stanza) + if stanza.attr.type == "get" then + session.roster = session.roster or rostermanager.getroster(session.username, session.host); + if session.roster == false then + send(st.reply(stanza) + :tag("error", { type = "wait" }) + :tag("internal-server-error", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"})); + return true; + else session.roster = session.roster or {}; + end + local roster = st.reply(stanza) + :query("jabber:iq:roster"); + for jid in pairs(session.roster) do + roster:tag("item", { jid = jid, subscription = "none" }):up(); + end + send(roster); + return true; + end + end + + + return function (stanza) + log("info", "--> "..tostring(stanza)); + if stanza.name == "iq" then + if not stanza[1] then log("warn", "<iq> without child is invalid"); return; end + if not stanza.attr.id then log("warn", "<iq> without id attribute is invalid"); end + local xmlns = stanza[1].attr.xmlns; + if not xmlns then log("warn", "Child of <iq> has no xmlns - invalid"); return; end + if (((not stanza.attr.to) or stanza.attr.to == session.host or stanza.attr.to:match("@[^/]+$")) and (stanza.attr.type == "get" or stanza.attr.type == "set")) then -- Stanza sent to us + if iq_handlers[xmlns] then + if iq_handlers[xmlns](stanza) then return; end; + end + log("warn", "Unhandled namespace: "..xmlns); + send(format("<iq type='error' id='%s'><error type='cancel'><service-unavailable/></error></iq>", stanza.attr.id)); + end + + end + -- Need to route stanza + if stanza.attr.to and ((not hosts[stanza.attr.to]) or hosts[stanza.attr.to].type ~= "local") then + stanza.attr.from = session.username.."@"..session.host; + session.send_to(stanza.attr.to, stanza); + end + end + +end + diff --git a/core/usermanager.lua b/core/usermanager.lua new file mode 100644 index 00000000..c98a1918 --- /dev/null +++ b/core/usermanager.lua @@ -0,0 +1,11 @@ + +require "util.datamanager" +local datamanager = datamanager; + +module "usermanager" + +function validate_credentials(host, username, password) + local credentials = datamanager.load(username, host, "accounts") or {}; + if password == credentials.password then return true; end + return false; +end diff --git a/main.lua b/main.lua new file mode 100644 index 00000000..cb6e03fd --- /dev/null +++ b/main.lua @@ -0,0 +1,275 @@ +require "luarocks.require" + +require "copas" +require "socket" +require "ssl" +require "lxp" + +function log(type, area, message) + print(type, area, message); +end + +require "core.stanza_dispatch" +require "core.rostermanager" +require "core.offlinemessage" +require "util.stanza" +require "util.jid" + +-- Locals for faster access -- +local t_insert = table.insert; +local t_concat = table.concat; +local t_concatall = function (t, sep) local tt = {}; for _, s in ipairs(t) do t_insert(tt, tostring(s)); end return t_concat(tt, sep); end +local m_random = math.random; +local format = string.format; +local st = stanza; +------------------------------ + +users = {}; +hosts = { + ["localhost"] = { + type = "local"; + connected = true; + sessions = {}; + }; + ["getjabber.ath.cx"] = { + type = "local"; + connected = true; + sessions = {}; + }; + } + +local hosts, users = hosts, users; + +local ssl_ctx, msg = ssl.newcontext { mode = "server", protocol = "sslv23", key = "/home/matthew/ssl_cert/server.key", + certificate = "/home/matthew/ssl_cert/server.crt", capath = "/etc/ssl/certs", verify = "none", } + +if not ssl_ctx then error("Failed to initialise SSL/TLS support: "..tostring(msg)); end + + +function connect_host(host) + hosts[host] = { type = "remote", sendbuffer = {} }; +end + +function handler(conn) + local copas_receive, copas_send = copas.receive, copas.send; + local reqdata, sktmsg; + local session = { sendbuffer = { external = {} }, conn = conn, notopen = true, priority = 0 } + + + -- Logging functions -- + + local mainlog, log = log; + do + local conn_name = tostring(conn):match("%w+$"); + log = function (type, area, message) mainlog(type, conn_name, message); end + end + local print = function (...) log("info", "core", t_concatall({...}, "\t")); end + session.log = log; + + -- -- -- + + -- Send buffers -- + + local sendbuffer = session.sendbuffer; + local send = function (data) return t_insert(sendbuffer, tostring(data)); end; + local send_to = function (to, stanza) + local node, host, resource = jid.split(to); + print("Routing stanza to "..to..":", node, host, resource); + if not hosts[host] then + print(" ...but host offline, establishing connection"); + connect_host(host); + t_insert(hosts[host].sendbuffer, stanza); -- This will be sent when s2s connection succeeds + elseif hosts[host].connected then + print(" ...putting in our external send buffer"); + t_insert(sendbuffer.external, { node = node, host = host, resource = resource, data = stanza}); + print(" ...there are now "..tostring(#sendbuffer.external).." stanzas in the external send buffer"); + end + end + session.send, session.send_to = send, send_to; + + -- -- -- + print("Client connected"); + conn = ssl.wrap(copas.wrap(conn), ssl_ctx); + + do + local succ, msg + conn:settimeout(15) + while not succ do + succ, msg = conn:dohandshake() + if not succ then + print("SSL: "..tostring(msg)); + if msg == 'wantread' then + socket.select({conn}, nil) + elseif msg == 'wantwrite' then + socket.select(nil, {conn}) + else + -- other error + end + end + end + end + print("SSL handshake complete"); + -- XML parser initialisation -- + + local parser; + local stanza; + + local stanza_dispatch = init_stanza_dispatcher(session); + + local xml_handlers = {}; + + do + local ns_stack = { "" }; + local curr_ns = ""; + local curr_tag; + function xml_handlers:StartElement(name, attr) + curr_ns,name = name:match("^(.+):(%w+)$"); + print("Tag received:", name, tostring(curr_ns)); + if not stanza then + if session.notopen then + if name == "stream" then + session.host = attr.to or error("Client failed to specify destination hostname"); + session.version = attr.version or 0; + session.streamid = m_random(1000000, 99999999); + print(session, session.host, "Client opened stream"); + send("<?xml version='1.0'?>"); + send(format("<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='%s' from='%s' >", session.streamid, session.host)); + --send("<stream:features>"); + --send("<mechanism>PLAIN</mechanism>"); + --send [[<register xmlns="http://jabber.org/features/iq-register"/> ]] + --send("</stream:features>"); + log("info", "core", "Stream opened successfully"); + session.notopen = nil; + return; + end + error("Client failed to open stream successfully"); + end + if name ~= "iq" and name ~= "presence" and name ~= "message" then + error("Client sent invalid top-level stanza"); + end + stanza = st.stanza(name, { to = attr.to, type = attr.type, id = attr.id, xmlns = curr_ns }); + curr_tag = stanza; + else + attr.xmlns = curr_ns; + stanza:tag(name, attr); + end + end + function xml_handlers:CharacterData(data) + if data:match("%S") then + stanza:text(data); + end + end + function xml_handlers:EndElement(name) + curr_ns,name = name:match("^(.+):(%w+)$"); + --print("<"..name.."/>", tostring(stanza), tostring(#stanza.last_add < 1), tostring(stanza.last_add[#stanza.last_add].name)); + if (not stanza) or #stanza.last_add < 0 or (#stanza.last_add > 0 and name ~= stanza.last_add[#stanza.last_add].name) then error("XML parse error in client stream"); end + -- Complete stanza + print(name, tostring(#stanza.last_add)); + if #stanza.last_add == 0 then + stanza_dispatch(stanza); + stanza = nil; + else + stanza:up(); + end + end +--[[ function xml_handlers:StartNamespaceDecl(namespace) + table.insert(ns_stack, namespace); + curr_ns = namespace; + log("debug", "parser", "Entering namespace "..tostring(curr_ns)); + end + function xml_handlers:EndNamespaceDecl(namespace) + table.remove(ns_stack); + log("debug", "parser", "Leaving namespace "..tostring(curr_ns)); + curr_ns = ns_stack[#ns_stack]; + log("debug", "parser", "Entering namespace "..tostring(curr_ns)); + end +]] + end + parser = lxp.new(xml_handlers, ":"); + + -- -- -- + + -- Main loop -- + print "Receiving..." + reqdata = copas_receive(conn, 1); + print "Received" + while reqdata do + parser:parse(reqdata); + if #sendbuffer.external > 0 then + -- Stanzas queued to go to other places, from us + -- ie. other local users, or remote hosts that weren't connected before + print(#sendbuffer.external.." stanzas queued for other recipients, sending now..."); + for n, packet in pairs(sendbuffer.external) do + if not hosts[packet.host] then + connect_host(packet.host); + t_insert(hosts[packet.host].sendbuffer, packet.data); + elseif hosts[packet.host].type == "local" then + print(" ...is to a local user") + local destuser = hosts[packet.host].sessions[packet.node]; + if destuser and destuser.sessions then + if not destuser.sessions[packet.resource] then + local best_resource; + for resource, session in pairs(destuser.sessions) do + if not best_session then best_session = session; + elseif session.priority >= best_session.priority and session.priority >= 0 then + best_session = session; + end + end + if not best_session then + offlinemessage.new(packet.node, packet.host, packet.data); + else + print("resource '"..packet.resource.."' was not online, have chosen to send to '"..best_session.username.."@"..best_session.host.."/"..best_session.resource.."'"); + packet.resource = best_session.resource; + end + end + if destuser.sessions[packet.resource] == session then + log("warn", "core", "Attempt to send stanza to self, dropping..."); + else + print("...sending..."); + copas_send(destuser.sessions[packet.resource].conn, tostring(packet.data)); + print("...sent") + end + elseif packet.data.name == "message" then + print(" ...will be stored offline"); + offlinemessage.new(packet.node, packet.host, packet.data); + elseif packet.data.name == "iq" then + print(" ...is an iq"); + send(st.reply(packet.data) + :tag("error", { type = "cancel" }) + :tag("service-unavailable", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" })); + end + print(" ...removing from send buffer"); + sendbuffer.external[n] = nil; + end + end + end + + if #sendbuffer > 0 then + for n, data in ipairs(sendbuffer) do + print "Sending..." + copas_send(conn, data); + print "Sent" + sendbuffer[n] = nil; + end + end + print "Receiving..." + repeat + reqdata, sktmsg = copas_receive(conn, 1); + if sktmsg == 'wantread' then + print("Received... wantread"); + --socket.select({conn}, nil) + --print("Socket ready now..."); + elseif sktmsg then + print("Received socket message:", sktmsg); + end + until reqdata or sktmsg == "closed"; + print("Received", tostring(reqdata)); + end + log("info", "core", "Client disconnected, connection closed"); +end + +server = socket.bind("*", 5223) +assert(server, "Failed to bind to socket") +copas.addserver(server, handler) + +copas.loop(); diff --git a/util/datamanager.lua b/util/datamanager.lua new file mode 100644 index 00000000..be63673e --- /dev/null +++ b/util/datamanager.lua @@ -0,0 +1,79 @@ +local format = string.format; +local setmetatable, type = setmetatable, type; +local pairs = pairs; +local char = string.char; +local loadfile, setfenv, pcall = loadfile, setfenv, pcall; +local log = log; +local io_open = io.open; + +module "datamanager" + + +---- utils ----- +local encode, decode; + +local log = function (type, msg) return log(type, "datamanager", msg); end + +do + local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = char(tonumber("0x"..k)); return t[k]; end }); + + decode = function (s) + return s and (s:gsub("+", " "):gsub("%%([a-fA-F0-9][a-fA-F0-9])", urlcodes)); + end + + encode = function (s) + return s and (s:gsub("%W", function (c) return format("%%%x", c:byte()); end)); + end +end + +local function basicSerialize (o) + if type(o) == "number" or type(o) == "boolean" then + return tostring(o) + else -- assume it is a string + return string.format("%q", tostring(o)) + end +end + + +local function simplesave (f, o) + if type(o) == "number" then + f:write(o) + elseif type(o) == "string" then + f:write(format("%q", o)) + elseif type(o) == "table" then + f:write("{\n") + for k,v in pairs(o) do + f:write(" [", format("%q", k), "] = ") + simplesave(f, v) + f:write(",\n") + end + f:write("}\n") + else + error("cannot serialize a " .. type(o)) + end + end + +------- API ------------- + +function getpath(username, host, datastore) + return format("data/%s/%s/%s.dat", encode(host), datastore, encode(username)); +end + +function load(username, host, datastore) + local data, ret = loadfile(getpath(username, host, datastore)); + if not data then log("warn", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..username.."@"..host); return nil; end + setfenv(data, {}); + local success, ret = pcall(data); + if not success then log("error", "Unable to load "..datastore.." storage ('"..ret.."') for user: "..username.."@"..host); return nil; end + return ret; +end + +function store(username, host, datastore, data) + local f, msg = io_open(getpath(username, host, datastore), "w+"); + if not f then log("error", "Unable to write to "..datastore.." storage ('"..msg.."') for user: "..username.."@"..host); return nil; end + f:write("return "); + simplesave(f, data); + f:close(); + return true; +end + diff --git a/util/jid.lua b/util/jid.lua new file mode 100644 index 00000000..9d01e2af --- /dev/null +++ b/util/jid.lua @@ -0,0 +1,8 @@ + +local match = string.match; + +module "jid" + +function split(jid) + return match(jid, "^([^@]+)@([^/]+)/?(.*)$"); +end diff --git a/util/stanza.lua b/util/stanza.lua new file mode 100644 index 00000000..88d0609f --- /dev/null +++ b/util/stanza.lua @@ -0,0 +1,104 @@ +local t_insert = table.insert; +local t_remove = table.remove; +local format = string.format; +local tostring = tostring; +local setmetatable= setmetatable; +local pairs = pairs; +local ipairs = ipairs; + +module "stanza" + +stanza_mt = {}; +stanza_mt.__index = stanza_mt; + +function stanza(name, attr) + local stanza = { name = name, attr = attr or {}, last_add = {}}; + return setmetatable(stanza, stanza_mt); +end + +function stanza_mt:iq(attrs) + return self + stanza("iq", attrs) +end +function stanza_mt:message(attrs) + return self + stanza("message", attrs) +end +function stanza_mt:presence(attrs) + return self + stanza("presence", attrs) +end +function stanza_mt:query(xmlns) + return self:tag("query", { xmlns = xmlns }); +end +function stanza_mt:tag(name, attrs) + local s = stanza(name, attrs); + (self.last_add[#self.last_add] or self):add_child(s); + t_insert(self.last_add, s); + return self; +end + +function stanza_mt:text(text) + (self.last_add[#self.last_add] or self):add_child(text); + return self; +end + +function stanza_mt:up() + t_remove(self.last_add); + return self; +end + +function stanza_mt:add_child(child) + t_insert(self, child); +end + +function stanza_mt:child_with_name(name) + for _, child in ipairs(self) do + if child.name == name then return child; end + end +end + +function stanza_mt.__tostring(t) + local children_text = ""; + for n, child in ipairs(t) do + children_text = children_text .. tostring(child); + end + + local attr_string = ""; + if t.attr then + for k, v in pairs(t.attr) do attr_string = attr_string .. format(" %s='%s'", k, tostring(v)); end + end + + return format("<%s%s>%s</%s>", t.name, attr_string, children_text, t.name); +end + +function stanza_mt.__add(s1, s2) + return s:add_child(s2); +end + + +do + local id = 0; + function new_id() + id = id + 1; + return "lx"..id; + end +end + +function message(attr, body) + if not body then + return stanza("message", attr); + else + return stanza("message", attr):tag("body"):text(body); + end +end +function iq(attr) + if attr and not attr.id then attr.id = new_id(); end + return stanza("iq", attr or { id = new_id() }); +end + +function reply(orig) + return stanza(orig.name, orig.attr and { to = orig.attr.from, from = orig.attr.to, id = orig.attr.id, type = ((orig.name == "iq" and "result") or nil) }); +end + +function presence(attr) + return stanza("presence", attr); +end + |