commit d0e23d516bf6a44da81e589e54a20c4303315024 Author: moparisthebest Date: Wed Nov 25 13:42:19 2015 -0500 initial diff --git a/dev/mod_ircd.old_comments b/dev/mod_ircd.old_comments new file mode 100644 index 0000000..063acc0 --- /dev/null +++ b/dev/mod_ircd.old_comments @@ -0,0 +1,508 @@ +-- README +-- Squish verse into this dir, then squish them into one, which you move +-- and rename to mod_ircd.lua in your prosody modules/plugins dir. +-- +-- IRC spec: +-- http://tools.ietf.org/html/rfc2812 +local _module = module +module = _G.module +local module = _module +-- +local component_jid, component_secret, muc_server, port_number = + module.host, nil, module:get_option_string("conference_server"), module:get_option_number("listener_port", 7000); + +if not muc_server then + module:log ("error", "You need to set the MUC server! halting.") + return false; +end + +package.loaded["util.sha1"] = require "util.encodings"; +local verse = require "verse" +require "verse.component" +require "socket" +c = verse.new();--verse.logger()) +c:add_plugin("groupchat"); + +local function verse2prosody(e) + return c:event("stanza", e.stanza) or true; +end +module:hook("message/bare", verse2prosody); +module:hook("message/full", verse2prosody); +module:hook("presence/bare", verse2prosody); +module:hook("presence/full", verse2prosody); +c.type = "component"; +c.send = core_post_stanza; + +-- This plugin is actually a verse based component, but that mode is currently commented out + +-- Add some hooks for debugging +--c:hook("opened", function () print("Stream opened!") end); +--c:hook("closed", function () print("Stream closed!") end); +--c:hook("stanza", function (stanza) print("Stanza:", stanza) end); + +-- This one prints all received data +--c:hook("incoming-raw", print, 1000); +--c:hook("stanza", print, 1000); +--c:hook("outgoing-raw", print, 1000); + +-- Print a message after authentication +--c:hook("authentication-success", function () print("Logged in!"); end); +--c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +--c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +--c.connect_host = "127.0.0.1" +--c:connect_component(component_jid, component_secret); + +local jid = require "util.jid"; +local nodeprep = require "util.encodings".stringprep.nodeprep; + +local function utf8_clean (s) + local push, join = table.insert, table.concat; + local r, i = {}, 1; + if not(s and #s > 0) then + return "" + end + while true do + local c = s:sub(i,i) + local b = c:byte(); + local w = ( + (b >= 9 and b <= 10 and 0) or + (b >= 32 and b <= 126 and 0) or + (b >= 192 and b <= 223 and 1) or + (b >= 224 and b <= 239 and 2) or + (b >= 240 and b <= 247 and 3) or + (b >= 248 and b <= 251 and 4) or + (b >= 251 and b <= 252 and 5) or nil + ) + if not w then + push(r, "?") + else + local n = i + w; + if w == 0 then + push(r, c); + elseif n > #s then + push(r, ("?"):format(b)); + else + local e = s:sub(i+1,n); + if e:match('^[\128-\191]*$') then + push(r, c); + push(r, e); + i = n; + else + push(r, ("?"):format(b)); + end + end + end + i = i + 1; + if i > #s then + break + end + end + return join(r); +end + +local function parse_line(line) + local ret = {}; + if line:sub(1,1) == ":" then + ret.from, line = line:match("^:(%w+)%s+(.*)$"); + end + for part in line:gmatch("%S+") do + if part:sub(1,1) == ":" then + ret[#ret+1] = line:match(":(.*)$"); + break + end + ret[#ret+1]=part; + end + return ret; +end + +local function build_line(parts) + if #parts > 1 then + parts[#parts] = ":" .. parts[#parts]; + end + return (parts.from and ":"..parts.from.." " or "")..table.concat(parts, " "); +end + +local function irc2muc(channel, nick) + local room = channel and nodeprep(channel:match("^#(%w+)")) or nil; + return jid.join(room, muc_server, nick) +end +local function muc2irc(room) + local channel, _, nick = jid.split(room); + return "#"..channel, nick; +end +local role_map = { + moderator = "@", + participant = "", + visitor = "", + none = "" +} +local aff_map = { + owner = "~", + administrator = "&", + member = "+", + none = "" +} +local role_modemap = { + moderator = "o", + participant = "", + visitor = "", + none = "" +} +local aff_modemap = { + owner = "q", + administrator = "a", + member = "v", + none = "" +} + +local irc_listener = { default_port = port_number, default_mode = "*l" }; + +local sessions = {}; +local jids = {}; +local commands = {}; + +local nicks = {}; + +local st = require "util.stanza"; + +local conference_server = muc_server; + +local function irc_close_session(session) + session.conn:close(); +end + +function irc_listener.onincoming(conn, data) + local session = sessions[conn]; + if not session then + session = { conn = conn, host = component_jid, reset_stream = function () end, + close = irc_close_session, log = logger.init("irc"..(conn.id or "1")), + rooms = {}, + roster = {} }; + sessions[conn] = session; + function session.data(data) + local parts = parse_line(data); + module:log("debug", require"util.serialization".serialize(parts)); + local command = table.remove(parts, 1); + if not command then + return; + end + command = command:upper(); + if not session.nick then + if not (command == "USER" or command == "NICK") then + module:log("debug", "Client tried to send command %s before registering", command); + return session.send{from=muc_server, "451", command, "You have not registered"} + end + end + if commands[command] then + local ret = commands[command](session, parts); + if ret then + return session.send(ret); + end + else + session.send{from=muc_server, "421", session.nick, command, "Unknown command"}; + return module:log("debug", "Unknown command: %s", command); + end + end + function session.send(data) + if type(data) == "string" then + return conn:write(data.."\r\n"); + elseif type(data) == "table" then + local line = build_line(data); + module:log("debug", line); + conn:write(line.."\r\n"); + end + end + end + if data then + session.data(data); + end +end + +function irc_listener.ondisconnect(conn, error) + local session = sessions[conn]; + if session then + for _, room in pairs(session.rooms) do + room:leave("Disconnected"); + end + if session.nick then + nicks[session.nick] = nil; + end + if session.full_jid then + jids[session.full_jid] = nil; + end + end + sessions[conn] = nil; +end + +function commands.NICK(session, args) + if session.nick then + session.send{from = muc_server, "484", "*", nick, "I'm afraid I can't let you do that"}; + --TODO Loop throug all rooms and change nick, with help from Verse. + return; + end + local nick = args[1]; + nick = nick:gsub("[^%w_]",""); + if nicks[nick] then + session.send{from=muc_server, "433", nick, "The nickname "..nick.." is already in use"}; + return; + end + local full_jid = jid.join(nick, component_jid, "ircd"); + jids[full_jid] = session; + jids[full_jid]["ar_last"] = {}; + nicks[nick] = session; + session.nick = nick; + session.full_jid = full_jid; + session.type = "c2s"; + + session.send{from = muc_server, "001", nick, "Welcome in the IRC to MUC XMPP Gateway, "..nick}; + session.send{from = muc_server, "002", nick, "Your host is "..muc_server.." running Prosody "..prosody.version}; + session.send{from = muc_server, "003", nick, "This server was created the "..os.date(nil, prosody.start_time)} + session.send{from = muc_server, "004", nick, table.concat({muc_server, "mod_ircd(alpha-0.8)", "i", "aoqv"}, " ")}; + session.send{from = muc_server, "375", nick, "- "..muc_server.." Message of the day -"}; + session.send{from = muc_server, "372", nick, "-"}; + session.send{from = muc_server, "372", nick, "- Please be warned that this is only a partial irc implementation,"}; + session.send{from = muc_server, "372", nick, "- it's made to facilitate users transiting away from irc to XMPP."}; + session.send{from = muc_server, "372", nick, "-"}; + session.send{from = muc_server, "372", nick, "- Prosody is _NOT_ an IRC Server and it never will."}; + session.send{from = muc_server, "372", nick, "- We also would like to remind you that this plugin is provided as is,"}; + session.send{from = muc_server, "372", nick, "- it's still an Alpha and it's still a work in progress, use it at your sole"}; + session.send{from = muc_server, "372", nick, "- risk as there's a not so little chance something will break."}; + + session.send{from = nick, "MODE", nick, "+i"}; -- why -> Invisible mode setting, + -- enforce by default on most servers (since the source host doesn't show it's sensible to have it "set") +end + +function commands.USER(session, params) + -- FIXME + -- Empty command for now +end + +local function mode_map(am, rm, nicks) + local rnick; + local c_modes; + c_modes = aff_modemap[am]..role_modemap[rm] + rnick = string.rep(nicks.." ", c_modes:len()) + if c_modes == "" then return nil, nil end + return c_modes, rnick +end + +function commands.JOIN(session, args) + local channel = args[1]; + if not channel then return end + local room_jid = irc2muc(channel); + print(session.full_jid); + if not jids[session.full_jid].ar_last[room_jid] then jids[session.full_jid].ar_last[room_jid] = {}; end + local room, err = c:join_room(room_jid, session.nick, { source = session.full_jid } ); + if not room then + return ":"..muc_server.." ERR :Could not join room: "..err + end + session.rooms[channel] = room; + room.channel = channel; + room.session = session; + session.send{from=session.nick, "JOIN", channel}; + if room.subject then + session.send{from=muc_server, 332, session.nick, channel ,room.subject}; + end + commands.NAMES(session, channel); + + room:hook("subject-changed", function(changed) + session.send((":%s TOPIC %s :%s"):format(changed.by.nick, channel, changed.to or "")); + end); + + room:hook("message", function(event) + if not event.body then return end + local nick, body = event.nick, event.body; + if nick ~= session.nick then + if body:sub(1,4) == "/me " then + body = "\1ACTION ".. body:sub(5) .. "\1" + end + local type = event.stanza.attr.type; + session.send{from=nick, "PRIVMSG", type == "groupchat" and channel or nick, body}; + --FIXME PM's probably won't work + end + end); + + room:hook("presence", function(ar) + local c_modes; + local rnick; + if ar.nick and not jids[session.full_jid].ar_last[ar.room_jid][ar.nick] then jids[session.full_jid].ar_last[ar.room_jid][ar.nick] = {} end + local x_ar = ar.stanza:get_child("x", "http://jabber.org/protocol/muc#user") + if x_ar then + local xar_item = x_ar:get_child("item") + if xar_item and xar_item.attr and ar.stanza.attr.type ~= "unavailable" then + if xar_item.attr.affiliation and xar_item.attr.role then + if not jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"] and + not jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"] then + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"] = xar_item.attr.affiliation + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"] = xar_item.attr.role + c_modes, rnick = mode_map(xar_item.attr.affiliation, xar_item.attr.role, ar.nick); + if c_modes and rnick then session.send((":%s MODE %s +%s"):format(muc_server, channel, c_modes.." "..rnick)); end + else + c_modes, rnick = mode_map(jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"], jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"], ar.nick); + if c_modes and rnick then session.send((":%s MODE %s -%s"):format(muc_server, channel, c_modes.." "..rnick)); end + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"] = xar_item.attr.affiliation + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"] = xar_item.attr.role + c_modes, rnick = mode_map(xar_item.attr.affiliation, xar_item.attr.role, ar.nick); + if c_modes and rnick then session.send((":%s MODE %s +%s"):format(muc_server, channel, c_modes.." "..rnick)); end + end + end + end + end + end, -1); +end + +c:hook("groupchat/joined", function(room) + local session = room.session or jids[room.opts.source]; + local channel = "#"..room.jid:match("^(.*)@"); + session.send{from=session.nick.."!"..session.nick, "JOIN", channel}; + if room.topic then + session.send{from=muc_server, 332, room.topic}; + end + commands.NAMES(session, channel) + room:hook("occupant-joined", function(nick) + session.send{from=nick.nick.."!"..nick.nick, "JOIN", channel}; + end); + room:hook("occupant-left", function(nick) + jids[session.full_jid].ar_last[nick.jid:match("^(.*)/")][nick.nick] = nil; -- ugly + session.send{from=nick.nick.."!"..nick.nick, "PART", channel}; + end); +end); + +function commands.NAMES(session, channel) + local nicks = { }; + local room = session.rooms[channel]; + local symbols_map = { + owner = "~", + administrator = "&", + moderator = "@", + member = "+" + } + + if not room then return end + -- TODO Break this out into commands.NAMES + for nick, n in pairs(room.occupants) do + if n.affiliation == "owner" and n.role == "moderator" then + nick = symbols_map[n.affiliation]..nick; + elseif n.affiliation == "administrator" and n.role == "moderator" then + nick = symbols_map[n.affiliation]..nick; + elseif n.affiliation == "member" and n.role == "moderator" then + nick = symbols_map[n.role]..nick; + elseif n.affiliation == "member" and n.role == "partecipant" then + nick = symbols_map[n.affiliation]..nick; + elseif n.affiliation == "none" and n.role == "moderator" then + nick = symbols_map[n.role]..nick; + end + table.insert(nicks, nick); + end + nicks = table.concat(nicks, " "); + session.send((":%s 353 %s = %s :%s"):format(muc_server, session.nick, channel, nicks)); + session.send((":%s 366 %s %s :End of /NAMES list."):format(muc_server, session.nick, channel)); + session.send(":"..muc_server.." 353 "..session.nick.." = "..channel.." :"..nicks); +end + +function commands.PART(session, args) + local channel, part_message = unpack(args); + local room = channel and nodeprep(channel:match("^#(%w+)")) or nil; + if not room then return end + channel = channel:match("^([%S]*)"); + session.rooms[channel]:leave(part_message); + jids[session.full_jid].ar_last[room.."@"..muc_server] = nil; + session.send(":"..session.nick.." PART :"..channel); +end + +function commands.PRIVMSG(session, args) + local channel, message = unpack(args); + if message and #message > 0 then + if message:sub(1,8) == "\1ACTION " then + message = "/me ".. message:sub(9,-2) + end + message = utf8_clean(message); + if channel:sub(1,1) == "#" then + if session.rooms[channel] then + module:log("debug", "%s sending PRIVMSG \"%s\" to %s", session.nick, message, channel); + session.rooms[channel]:send_message(message); + end + else -- private message + local nick = channel; + module:log("debug", "PM to %s", nick); + for channel, room in pairs(session.rooms) do + module:log("debug", "looking for %s in %s", nick, channel); + if room.occupants[nick] then + module:log("debug", "found %s in %s", nick, channel); + local who = room.occupants[nick]; + -- FIXME PMs in verse + --room:send_private_message(nick, message); + local pm = st.message({type="chat",to=who.jid}, message); + module:log("debug", "sending PM to %s: %s", nick, tostring(pm)); + room:send(pm) + break + end + end + end + end +end + +function commands.PING(session, args) + session.send{from=muc_server, "PONG", args[1]}; +end + +function commands.TOPIC(session, message) + if not message then return end + local channel, topic = message[1], message[2]; + channel = utf8_clean(channel); + topic = utf8_clean(topic); + if not channel then return end + local room = session.rooms[channel]; + + if topic then room:set_subject(topic); end +end + +function commands.WHO(session, args) + local channel = args[1]; + if session.rooms[channel] then + local room = session.rooms[channel] + for nick in pairs(room.occupants) do + --n=MattJ 91.85.191.50 irc.freenode.net MattJ H :0 Matthew Wild + session.send{from=muc_server, 352, session.nick, channel, nick, nick, muc_server, nick, "H", "0 "..nick} + end + session.send{from=muc_server, 315, session.nick, channel, "End of /WHO list"}; + end +end + +function commands.MODE(session, args) -- FIXME + -- emptied for the time being, until something sane which works is available. +end + +function commands.QUIT(session, args) + session.send{"ERROR", "Closing Link: "..session.nick}; + for _, room in pairs(session.rooms) do + room:leave(args[1]); + end + jids[session.full_jid] = nil; + nicks[session.nick] = nil; + sessions[session.conn] = nil; + session:close(); +end + +function commands.RAW(session, data) + --c:send(data) +end + +local function desetup() + require "net.connlisteners".deregister("irc"); +end + +--c:hook("ready", function () + require "net.connlisteners".register("irc", irc_listener); + require "net.connlisteners".start("irc"); +--end); + +module:hook("module-unloaded", desetup) + + +--print("Starting loop...") +--verse.loop() diff --git a/ircd.sh b/ircd.sh new file mode 100755 index 0000000..23b4a8c --- /dev/null +++ b/ircd.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e +squish +echo -e 'config:reload()\nmodule:reload("ircd", "burtrum.org")' | telnet localhost 5582 + +exit + +echo -e 'module:load("ircd", "burtrum.org")' | telnet localhost 5582 \ No newline at end of file diff --git a/mod_ircd.in.lua b/mod_ircd.in.lua new file mode 100644 index 0000000..5b5b014 --- /dev/null +++ b/mod_ircd.in.lua @@ -0,0 +1,598 @@ +-- README +-- Squish verse into this dir, then squish them into one, which you move +-- and rename to mod_ircd.lua in your prosody modules/plugins dir. +-- +-- IRC spec: +-- http://tools.ietf.org/html/rfc2812 + +local client_xmlns = "jabber:client" + +--module:set_global(); + +local server = require "net.server"; +local portmanager = require "core.portmanager"; + +local component_jid, component_secret, muc_server, port_number = + module.host, nil, module:get_option_string("conference_server"), module:get_option_number("listener_port", 7000); + +if not muc_server then + module:log ("error", "You need to set the MUC server in the configuration (conference_server)!") + module:log ("error", "Be a good boy or girl and go read the wiki at: http://code.google.com/p/prosody-modules/wiki/mod_ircd") + return false; +end + +package.loaded["util.sha1"] = require "util.encodings"; +--local verse = require "verse" +--require "verse.component" +--require "socket" +require "verse".init("component"); + +c = verse.new(); -- something interferes with prosody's console logging +c:add_plugin("groupchat"); + +local function verse2prosody(e) + return c:event("stanza", e.stanza) or true; +end +module:hook("message/bare", verse2prosody); +module:hook("message/full", verse2prosody); +module:hook("presence/bare", verse2prosody); +module:hook("presence/full", verse2prosody); +c.type = "component"; +c.send = core_post_stanza; + +local jid = require "util.jid"; +local nodeprep = require "util.encodings".stringprep.nodeprep; + +local function utf8_clean (s) + local push, join = table.insert, table.concat; + local r, i = {}, 1; + if not(s and #s > 0) then + return "" + end + while true do + local c = s:sub(i,i) + local b = c:byte(); + local w = ( + (b >= 9 and b <= 10 and 0) or + (b >= 32 and b <= 126 and 0) or + (b >= 192 and b <= 223 and 1) or + (b >= 224 and b <= 239 and 2) or + (b >= 240 and b <= 247 and 3) or + (b >= 248 and b <= 251 and 4) or + (b >= 251 and b <= 252 and 5) or nil + ) + if not w then + push(r, "?") + else + local n = i + w; + if w == 0 then + push(r, c); + elseif n > #s then + push(r, ("?"):format(b)); + else + local e = s:sub(i+1,n); + if e:match('^[\128-\191]*$') then + push(r, c); + push(r, e); + i = n; + else + push(r, ("?"):format(b)); + end + end + end + i = i + 1; + if i > #s then + break + end + end + return join(r); +end + +local function parse_line(line) + local ret = {}; + if line:sub(1,1) == ":" then + ret.from, line = line:match("^:(%w+)%s+(.*)$"); + end + for part in line:gmatch("%S+") do + if part:sub(1,1) == ":" then + ret[#ret+1] = line:match(":(.*)$"); + break + end + ret[#ret+1]=part; + end + return ret; +end + +local function build_line(parts) + if #parts > 1 then + parts[#parts] = ":" .. parts[#parts]; + end + return (parts.from and ":"..parts.from.." " or "")..table.concat(parts, " "); +end + +local function irc2muc(channel, nick) + local room = channel and nodeprep(channel:match("^#(%w+)")) or nil; + if not nick then + return jid.join(room, muc_server); + else + return jid.join(room, muc_server, nick); + end +end +local function muc2irc(room) + local channel, _, nick = jid.split(room); + return "#"..channel, nick; +end +local role_map = { + moderator = "@", + participant = "", + visitor = "", + none = "" +} +local aff_map = { + owner = "~", + administrator = "&", + member = "+", + none = "" +} +local role_modemap = { + moderator = "o", + participant = "", + visitor = "", + none = "" +} +local aff_modemap = { + owner = "q", + administrator = "a", + member = "v", + none = "" +} + +local irc_listener = { default_port = port_number, default_mode = "*l"; interface = "192.168.1.3" }; + +local sessions = module:shared("sessions"); +local jids = {}; +local commands = {}; + +local nicks = {}; +local usernames = {}; + +local st = require "util.stanza"; + +local conference_server = muc_server; + +local function irc_close_session(session) + session.conn:close(); +end + +function irc_listener.onincoming(conn, data) + local session = sessions[conn]; + if not session then + session = { conn = conn, host = component_jid, reset_stream = function () end, + close = irc_close_session, log = logger.init("irc"..(conn.id or "1")), + rooms = {}, roster = {}, has_un = false }; + sessions[conn] = session; + + function session.data(data) + local parts = parse_line(data); + module:log("debug", require"util.serialization".serialize(parts)); + local command = table.remove(parts, 1); + if not command then + return; + end + command = command:upper(); + if not session.username and not session.nick then + if not (command == "USER" or command == "NICK") then + module:log("debug", "Client tried to send command %s before registering", command); + return session.send{from=muc_server, "451", command, "You have not completed the registration."} + end + end + if commands[command] then + local ret = commands[command](session, parts); + if ret then + return session.send(ret); + end + else + session.send{from=muc_server, "421", session.nick, command, "Unknown command"}; + return module:log("debug", "Unknown command: %s", command); + end + end + + function session.send(data) + if type(data) == "string" then + return conn:write(data.."\r\n"); + elseif type(data) == "table" then + local line = build_line(data); + module:log("debug", line); + conn:write(line.."\r\n"); + end + end + end + + if data then + session.data(data); + end +end + +function irc_listener.ondisconnect(conn, error) + local session = sessions[conn]; + + if session then + for _, room in pairs(session.rooms) do + room:leave("Disconnected"); + end + if session.nick then + nicks[session.nick] = nil; + end + if session.full_jid then + jids[session.full_jid] = nil; + end + if session.username then + usernames[session.username] = nil; + end + end + sessions[conn] = nil; +end + +local function nick_inuse(nick) + if nicks[nick] then return true else return false end +end +local function check_username(un) + local count = 0; + local result; + + for name, given in pairs(usernames) do + if un == given then count = count + 1; end + end + + result = count + 1; + + if count > 0 then return tostring(un)..tostring(result); else return tostring(un); end +end +local function set_t_data(session, full_jid) + session.full_jid = full_jid; + jids[full_jid] = session; + jids[full_jid]["ar_last"] = {}; + jids[full_jid]["nicks_changing"] = {}; + + if session.nick then nicks[session.nick] = session; end +end +local function send_motd(session) + local nick = session.nick; + + if session.username and session.nick then -- send MOTD only if username and nick are set + session.send{from = muc_server, "001", nick, "Welcome in the IRC to MUC XMPP Gateway, "..nick}; + session.send{from = muc_server, "002", nick, "Your host is "..muc_server.." running Prosody "..prosody.version}; + session.send{from = muc_server, "003", nick, "This server was created the "..os.date(nil, prosody.start_time)} + session.send{from = muc_server, "004", nick, table.concat({muc_server, "mod_ircd(alpha-0.8)", "i", "aoqv"}, " ")}; + session.send((":%s %s %s %s :%s"):format(muc_server, "005", nick, "CHANTYPES=# PREFIX=(qaov)~&@+", "are supported by this server")); + session.send((":%s %s %s %s :%s"):format(muc_server, "005", nick, "STATUSMSG=~&@+", "are supported by this server")); + session.send{from = muc_server, "375", nick, "- "..muc_server.." Message of the day -"}; + session.send{from = muc_server, "372", nick, "-"}; + session.send{from = muc_server, "372", nick, "- Please be warned that this is only a partial irc implementation,"}; + session.send{from = muc_server, "372", nick, "- it's made to facilitate users transiting away from irc to XMPP."}; + session.send{from = muc_server, "372", nick, "-"}; + session.send{from = muc_server, "372", nick, "- Prosody is _NOT_ an IRC Server and it never will."}; + session.send{from = muc_server, "372", nick, "- We also would like to remind you that this plugin is provided as is,"}; + session.send{from = muc_server, "372", nick, "- it's still an Alpha and it's still a work in progress, use it at your sole"}; + session.send{from = muc_server, "372", nick, "- risk as there's a not so little chance something will break."}; + session.send{from = nick, "MODE", nick, "+i"}; -- why -> Invisible mode setting, + end -- enforce by default on most servers (since the source host doesn't show it's sensible to have it "set") +end + +function commands.NICK(session, args) + local nick = args[1]; + nick = nick:gsub("[^%w_]",""); + + if session.nick and not nick_inuse(nick) then -- changing nick + local oldnick = session.nick; + + -- update and replace session data + session.nick = nick; + nicks[oldnick] = nil; + nicks[nick] = session; + + session.send{from=oldnick.."!"..nicks[nick].username, "NICK", nick}; + + -- broadcast changes if required + if session.rooms then + session.nicks_changing[nick] = { oldnick, session.username }; + + for id, room in pairs(session.rooms) do room:change_nick(nick); end + + session.nicks_changing[nick] = nil; + end + + return; + elseif nick_inuse(nick) then + session.send{from=muc_server, "433", nick, "The nickname "..nick.." is already in use"}; return; + end + + session.nick = nick; + session.type = "c2s"; + nicks[nick] = session; + + -- Some choppy clients send in NICK before USER, that needs to be handled + if session.username then + set_t_data(session, jid.join(session.username, component_jid, "ircd")); + end + + send_motd(session); +end + +function commands.USER(session, params) + local username = params[1]; + + if not session.has_un then + local un_checked = check_username(username); + + usernames[un_checked] = username; + session.username = un_checked; + session.has_un = true; + + if not session.full_jid then + set_t_data(session, jid.join(session.username, component_jid, "ircd")); + end + else + return session.send{from=muc_server, "462", "USER", "You may not re-register."} + end + + send_motd(session); +end + +function commands.USERHOST(session, params) -- can show only users on the gateway. Needed for some clients to determinate self hostmask. + local nick = params[1]; + + if not nick then session.send{from=muc_server, "461", "USERHOST", "Not enough parameters"}; return; end + + if nicks[nick] and nicks[nick].nick and nicks[nick].username then + session.send{from=muc_server, "302", session.nick, nick.."=+"..nicks[nick].username}; return; + else + return; + end +end + +local function mode_map(am, rm, nicks) + local rnick; + local c_modes; + c_modes = aff_modemap[am]..role_modemap[rm] + rnick = string.rep(nicks.." ", c_modes:len()) + if c_modes == "" then return nil, nil end + return c_modes, rnick +end + +function commands.JOIN(session, args) + local channel = args[1]; + if not channel then return end + local room_jid = irc2muc(channel); + + if not jids[session.full_jid].ar_last[room_jid] then jids[session.full_jid].ar_last[room_jid] = {}; end + local room, err = c:join_room(room_jid, session.nick, { source = session.full_jid } ); + if not room then + return ":"..muc_server.." ERR :Could not join room: "..err + end + + session.rooms[channel] = room; + room.session = session; + + if session.nicks_changing[session.nick] then -- my own nick is changing + commands.NAMES(session, channel); + else + session.send{from=session.nick.."!"..session.username, "JOIN", channel}; + if room.subject then + session.send{from=muc_server, 332, session.nick, channel, room.subject}; + end + commands.NAMES(session, channel); + end + + room:hook("subject-changed", function(changed) + session.send{from=changed.by.nick, "TOPIC", channel, changed.to or ""} + end); + + room:hook("message", function(event) + if not event.body then return end + local nick, body = event.nick, event.body; + if nick ~= session.nick then + if body:sub(1,4) == "/me " then + body = "\1ACTION ".. body:sub(5) .. "\1" + end + local type = event.stanza.attr.type; + session.send{from=nick, "PRIVMSG", type == "groupchat" and channel or nick, body}; + --FIXME PM's probably won't work + end + end); + + room:hook("presence", function(ar) + local c_modes; + local rnick; + if ar.nick and not jids[session.full_jid].ar_last[ar.room_jid][ar.nick] then jids[session.full_jid].ar_last[ar.room_jid][ar.nick] = {} end + local x_ar = ar.stanza:get_child("x", "http://jabber.org/protocol/muc#user") + if x_ar then + local xar_item = x_ar:get_child("item") + if xar_item and xar_item.attr and ar.stanza.attr.type ~= "unavailable" then + if xar_item.attr.affiliation and xar_item.attr.role then + if not jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"] and + not jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"] then + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"] = xar_item.attr.affiliation + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"] = xar_item.attr.role + n_self_changing = nicks[ar.nick] and nicks[ar.nick].nicks_changing and nicks[ar.nick].nicks_changing[ar.nick] + if n_self_changing then return; end + c_modes, rnick = mode_map(xar_item.attr.affiliation, xar_item.attr.role, ar.nick); + if c_modes and rnick then session.send((":%s MODE %s +%s"):format(muc_server, channel, c_modes.." "..rnick)); end + else + c_modes, rnick = mode_map(jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"], jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"], ar.nick); + if c_modes and rnick then session.send((":%s MODE %s -%s"):format(muc_server, channel, c_modes.." "..rnick)); end + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"] = xar_item.attr.affiliation + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"] = xar_item.attr.role + n_self_changing = nicks[ar.nick] and nicks[ar.nick].nicks_changing and nicks[ar.nick].nicks_changing[ar.nick] + if n_self_changing then return; end + c_modes, rnick = mode_map(xar_item.attr.affiliation, xar_item.attr.role, ar.nick); + if c_modes and rnick then session.send((":%s MODE %s +%s"):format(muc_server, channel, c_modes.." "..rnick)); end + end + end + end + end + end, -1); +end + +c:hook("groupchat/joined", function(room) + local session = room.session or jids[room.opts.source]; + local channel = "#"..room.jid:match("^(.*)@"); + + room:hook("occupant-joined", function(nick) + if session.nicks_changing[nick.nick] then + session.send{from=session.nicks_changing[nick.nick][1].."!"..(session.nicks_changing[nick.nick][2] or "xmpp"), "NICK", nick.nick}; + session.nicks_changing[nick.nick] = nil; + else + session.send{from=nick.nick.."!"..(nicks[nick.nick] and nicks[nick.nick].username or "xmpp"), "JOIN", channel}; + end + end); + room:hook("occupant-left", function(nick) + if jids[session.full_jid] then jids[session.full_jid].ar_last[nick.jid:match("^(.*)/")][nick.nick] = nil; end + local status_code = + nick.presence:get_child("x","http://jabber.org/protocol/muc#user") and + nick.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("status") and + nick.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("status").attr.code; + + + if status_code == "303" then + local newnick = + nick.presence:get_child("x","http://jabber.org/protocol/muc#user") and + nick.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("item") and + nick.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("item").attr.nick; + + session.nicks_changing[newnick] = { nick.nick, (nicks[nick.nick] and nicks[nick.nick].username or "xmpp") }; return; + end + + for id, data in pairs(session.nicks_changing) do + if data[1] == nick.nick then return; end + end + session.send{from=nick.nick.."!"..(nicks[nick.nick] and nicks[nick.nick].username or "xmpp"), "PART", channel}; + end); +end); + +function commands.NAMES(session, channel) + local nicks = { }; + if type(channel) == "table" then channel = channel[1] end + + local room = session.rooms[channel]; + + local symbols_map = { + owner = "~", + administrator = "&", + moderator = "@", + member = "+" + } + + if not room then return end + -- TODO Break this out into commands.NAMES + for nick, n in pairs(room.occupants) do + if n.affiliation == "owner" and n.role == "moderator" then + nick = symbols_map[n.affiliation]..nick; + elseif n.affiliation == "administrator" and n.role == "moderator" then + nick = symbols_map[n.affiliation]..nick; + elseif n.affiliation == "member" and n.role == "moderator" then + nick = symbols_map[n.role]..nick; + elseif n.affiliation == "member" and n.role == "partecipant" then + nick = symbols_map[n.affiliation]..nick; + elseif n.affiliation == "none" and n.role == "moderator" then + nick = symbols_map[n.role]..nick; + end + table.insert(nicks, nick); + end + nicks = table.concat(nicks, " "); + session.send((":%s 353 %s = %s :%s"):format(muc_server, session.nick, channel, nicks)); + session.send((":%s 366 %s %s :End of /NAMES list."):format(muc_server, session.nick, channel)); + session.send(":"..muc_server.." 353 "..session.nick.." = "..channel.." :"..nicks); +end + +function commands.PART(session, args) + local channel, part_message = unpack(args); + local room = channel and nodeprep(channel:match("^#(%w+)")) or nil; + if not room then return end + channel = channel:match("^([%S]*)"); + session.rooms[channel]:leave(part_message); + jids[session.full_jid].ar_last[room.."@"..muc_server] = nil; + session.send{from=session.nick.."!"..session.username, "PART", channel}; +end + +function commands.PRIVMSG(session, args) + local channel, message = unpack(args); + if message and #message > 0 then + if message:sub(1,8) == "\1ACTION " then + message = "/me ".. message:sub(9,-2) + end + message = utf8_clean(message); + if channel:sub(1,1) == "#" then + if session.rooms[channel] then + module:log("debug", "%s sending PRIVMSG \"%s\" to %s", session.nick, message, channel); + session.rooms[channel]:send_message(message); + end + else -- private message + local nick = channel; + module:log("debug", "PM to %s", nick); + for channel, room in pairs(session.rooms) do + module:log("debug", "looking for %s in %s", nick, channel); + if room.occupants[nick] then + module:log("debug", "found %s in %s", nick, channel); + local who = room.occupants[nick]; + -- FIXME PMs in verse + --room:send_private_message(nick, message); + local pm = st.message({type="chat",to=who.jid}, message); + module:log("debug", "sending PM to %s: %s", nick, tostring(pm)); + room:send(pm) + break + end + end + end + end +end + +function commands.PING(session, args) + session.send{from=muc_server, "PONG", args[1]}; +end + +function commands.TOPIC(session, message) + if not message then return end + local channel, topic = message[1], message[2]; + channel = utf8_clean(channel); + topic = utf8_clean(topic); + if not channel then return end + local room = session.rooms[channel]; + + if topic then room:set_subject(topic); end +end + +function commands.WHO(session, args) + local channel = args[1]; + if session.rooms[channel] then + local room = session.rooms[channel] + for nick in pairs(room.occupants) do + session.send{from=muc_server, 352, session.nick, channel, nick, nick, muc_server, nick, "H", "0 "..nick} + end + session.send{from=muc_server, 315, session.nick, channel, "End of /WHO list"}; + end +end + +function commands.MODE(session, args) -- Empty command +end + +function commands.QUIT(session, args) + session.send{"ERROR", "Closing Link: "..session.nick}; + for _, room in pairs(session.rooms) do + room:leave(args[1]); + end + jids[session.full_jid] = nil; + nicks[session.nick] = nil; + usernames[session.username] = nil; + sessions[session.conn] = nil; + session:close(); +end + +function commands.RAW(session, data) -- Empty command +end + +module:provides("net", { + name = "ircd"; + listener = irc_listener; + default_port = 7000; +}); diff --git a/mod_ircd.lua b/mod_ircd.lua new file mode 100644 index 0000000..f7507a4 --- /dev/null +++ b/mod_ircd.lua @@ -0,0 +1,8174 @@ +package.preload['verse']=(function(...) +package.preload['util.encodings']=(function(...) +local function e() +error("Function not implemented"); +end +local t=require"mime"; +module"encodings" +stringprep={}; +base64={encode=t.b64,decode=e}; +return _M; +end) +package.preload['util.hashes']=(function(...) +local e=require"util.sha1"; +return{sha1=e.sha1}; +end) +package.preload['util.sha1']=(function(...) +local c=string.len +local a=string.char +local b=string.byte +local g=string.sub +local m=math.floor +local t=require"bit" +local k=t.bnot +local e=t.band +local y=t.bor +local n=t.bxor +local i=t.lshift +local o=t.rshift +local u,l,d,h,r +local function p(e,t) +return i(e,t)+o(e,32-t) +end +local function s(i) +local t,o +local t="" +for n=1,8 do +o=e(i,15) +if(o<10)then +t=a(o+48)..t +else +t=a(o+87)..t +end +i=m(i/16) +end +return t +end +local function j(t) +local i,o +local n="" +i=c(t)*8 +t=t..a(128) +o=56-e(c(t),63) +if(o<0)then +o=o+64 +end +for e=1,o do +t=t..a(0) +end +for t=1,8 do +n=a(e(i,255))..n +i=m(i/256) +end +return t..n +end +local function q(w) +local m,t,i,o,f,s,c,v +local a,a +local a={} +while(w~="")do +for e=0,15 do +a[e]=0 +for t=1,4 do +a[e]=a[e]*256+b(w,e*4+t) +end +end +for e=16,79 do +a[e]=p(n(n(a[e-3],a[e-8]),n(a[e-14],a[e-16])),1) +end +m=u +t=l +i=d +o=h +f=r +for h=0,79 do +if(h<20)then +s=y(e(t,i),e(k(t),o)) +c=1518500249 +elseif(h<40)then +s=n(n(t,i),o) +c=1859775393 +elseif(h<60)then +s=y(y(e(t,i),e(t,o)),e(i,o)) +c=2400959708 +else +s=n(n(t,i),o) +c=3395469782 +end +v=p(m,5)+s+f+c+a[h] +f=o +o=i +i=p(t,30) +t=m +m=v +end +u=e(u+m,4294967295) +l=e(l+t,4294967295) +d=e(d+i,4294967295) +h=e(h+o,4294967295) +r=e(r+f,4294967295) +w=g(w,65) +end +end +local function a(e,t) +e=j(e) +u=1732584193 +l=4023233417 +d=2562383102 +h=271733878 +r=3285377520 +q(e) +local e=s(u)..s(l)..s(d) +..s(h)..s(r); +if t then +return e; +else +return(e:gsub("..",function(e) +return string.char(tonumber(e,16)); +end)); +end +end +_G.sha1={sha1=a}; +return _G.sha1; +end) +package.preload['lib.adhoc']=(function(...) +local n,r=require"util.stanza",require"util.uuid"; +local h="http://jabber.org/protocol/commands"; +local i={} +local s={}; +function _cmdtag(e,o,t,a) +local e=n.stanza("command",{xmlns=h,node=e.node,status=o}); +if t then e.attr.sessionid=t;end +if a then e.attr.action=a;end +return e; +end +function s.new(e,a,t,o) +return{name=e,node=a,handler=t,cmdtag=_cmdtag,permission=(o or"user")}; +end +function s.handle_cmd(o,s,a) +local e=a.tags[1].attr.sessionid or r.generate(); +local t={}; +t.to=a.attr.to; +t.from=a.attr.from; +t.action=a.tags[1].attr.action or"execute"; +t.form=a.tags[1]:child_with_ns("jabber:x:data"); +local t,h=o:handler(t,i[e]); +i[e]=h; +local a=n.reply(a); +if t.status=="completed"then +i[e]=nil; +cmdtag=o:cmdtag("completed",e); +elseif t.status=="canceled"then +i[e]=nil; +cmdtag=o:cmdtag("canceled",e); +elseif t.status=="error"then +i[e]=nil; +a=n.error_reply(a,t.error.type,t.error.condition,t.error.message); +s.send(a); +return true; +else +cmdtag=o:cmdtag("executing",e); +end +for t,e in pairs(t)do +if t=="info"then +cmdtag:tag("note",{type="info"}):text(e):up(); +elseif t=="warn"then +cmdtag:tag("note",{type="warn"}):text(e):up(); +elseif t=="error"then +cmdtag:tag("note",{type="error"}):text(e.message):up(); +elseif t=="actions"then +local t=n.stanza("actions"); +for a,e in ipairs(e)do +if(e=="prev")or(e=="next")or(e=="complete")then +t:tag(e):up(); +else +module:log("error",'Command "'..o.name.. +'" at node "'..o.node..'" provided an invalid action "'..e..'"'); +end +end +cmdtag:add_child(t); +elseif t=="form"then +cmdtag:add_child((e.layout or e):form(e.values)); +elseif t=="result"then +cmdtag:add_child((e.layout or e):form(e.values,"result")); +elseif t=="other"then +cmdtag:add_child(e); +end +end +a:add_child(cmdtag); +s.send(a); +return true; +end +return s; +end) +package.preload['util.rsm']=(function(...) +local h=require"util.stanza".stanza; +local a,i=tostring,tonumber; +local n=type; +local r=pairs; +local s='http://jabber.org/protocol/rsm'; +local o={}; +do +local e=o; +local function t(e) +return i((e:get_text())); +end +local function a(t) +return t:get_text(); +end +e.after=a; +e.before=function(e) +local e=e:get_text(); +return e==""or e; +end; +e.max=t; +e.index=t; +e.first=function(e) +return{index=i(e.attr.index);e:get_text()}; +end; +e.last=a; +e.count=t; +end +local d=setmetatable({ +first=function(t,e) +if n(e)=="table"then +t:tag("first",{index=e.index}):text(e[1]):up(); +else +t:tag("first"):text(a(e)):up(); +end +end; +before=function(t,e) +if e==true then +t:tag("before"):up(); +else +t:tag("before"):text(a(e)):up(); +end +end +},{ +__index=function(t,e) +return function(t,o) +t:tag(e):text(a(o)):up(); +end +end; +}); +local function n(e) +local i={}; +for t in e:childtags()do +local e=t.name; +local a=e and o[e]; +if a then +i[e]=a(t); +end +end +return i; +end +local function i(e) +local t=h("set",{xmlns=s}); +for e,a in r(e)do +if o[e]then +d[e](t,a); +end +end +return t; +end +local function t(e) +local e=e:get_child("set",s); +if e and#e.tags>0 then +return n(e); +end +end +return{parse=n,generate=i,get=t}; +end) +package.preload['util.stanza']=(function(...) +local t=table.insert; +local d=table.remove; +local w=table.concat; +local s=string.format; +local l=string.match; +local f=tostring; +local u=setmetatable; +local n=pairs; +local i=ipairs; +local o=type; +local v=string.gsub; +local y=string.sub; +local c=string.find; +local e=os; +local m=not e.getenv("WINDIR"); +local r,a; +if m then +local t,e=pcall(require,"util.termcolours"); +if t then +r,a=e.getstyle,e.getstring; +else +m=nil; +end +end +local p="urn:ietf:params:xml:ns:xmpp-stanzas"; +module"stanza" +stanza_mt={__type="stanza"}; +stanza_mt.__index=stanza_mt; +local e=stanza_mt; +function stanza(t,a) +local t={name=t,attr=a or{},tags={}}; +return u(t,e); +end +local h=stanza; +function e:query(e) +return self:tag("query",{xmlns=e}); +end +function e:body(t,e) +return self:tag("body",e):text(t); +end +function e:tag(a,e) +local a=h(a,e); +local e=self.last_add; +if not e then e={};self.last_add=e;end +(e[#e]or self):add_direct_child(a); +t(e,a); +return self; +end +function e:text(t) +local e=self.last_add; +(e and e[#e]or self):add_direct_child(t); +return self; +end +function e:up() +local e=self.last_add; +if e then d(e);end +return self; +end +function e:reset() +self.last_add=nil; +return self; +end +function e:add_direct_child(e) +if o(e)=="table"then +t(self.tags,e); +end +t(self,e); +end +function e:add_child(t) +local e=self.last_add; +(e and e[#e]or self):add_direct_child(t); +return self; +end +function e:get_child(a,t) +for o,e in i(self.tags)do +if(not a or e.name==a) +and((not t and self.attr.xmlns==e.attr.xmlns) +or e.attr.xmlns==t)then +return e; +end +end +end +function e:get_child_text(e,t) +local e=self:get_child(e,t); +if e then +return e:get_text(); +end +return nil; +end +function e:child_with_name(t) +for a,e in i(self.tags)do +if e.name==t then return e;end +end +end +function e:child_with_ns(t) +for a,e in i(self.tags)do +if e.attr.xmlns==t then return e;end +end +end +function e:children() +local e=0; +return function(t) +e=e+1 +return t[e]; +end,self,e; +end +function e:childtags(i,t) +local e=self.tags; +local a,o=1,#e; +return function() +for o=a,o do +local e=e[o]; +if(not i or e.name==i) +and((not t and self.attr.xmlns==e.attr.xmlns) +or e.attr.xmlns==t)then +a=o+1; +return e; +end +end +end; +end +function e:maptags(i) +local a,t=self.tags,1; +local n,o=#self,#a; +local e=1; +while t<=o and o>0 do +if self[e]==a[t]then +local i=i(self[e]); +if i==nil then +d(self,e); +d(a,t); +n=n-1; +o=o-1; +e=e-1; +t=t-1; +else +self[e]=i; +a[t]=i; +end +t=t+1; +end +e=e+1; +end +return self; +end +function e:find(a) +local e=1; +local s=#a+1; +repeat +local o,t,i; +local n=y(a,e,e); +if n=="@"then +return self.attr[y(a,e+1)]; +elseif n=="{"then +o,e=l(a,"^([^}]+)}()",e+1); +end +t,i,e=l(a,"^([^@/#]*)([/#]?)()",e); +t=t~=""and t or nil; +if e==s then +if i=="#"then +return self:get_child_text(t,o); +end +return self:get_child(t,o); +end +self=self:get_child(t,o); +until not self +end +local d +do +local t={["'"]="'",["\""]=""",["<"]="<",[">"]=">",["&"]="&"}; +function d(e)return(v(e,"['&<>\"]",t));end +_M.xml_escape=d; +end +local function y(a,e,h,o,r) +local i=0; +local s=a.name +t(e,"<"..s); +for a,n in n(a.attr)do +if c(a,"\1",1,true)then +local a,s=l(a,"^([^\1]*)\1?(.*)$"); +i=i+1; +t(e," xmlns:ns"..i.."='"..o(a).."' ".."ns"..i..":"..s.."='"..o(n).."'"); +elseif not(a=="xmlns"and n==r)then +t(e," "..a.."='"..o(n).."'"); +end +end +local i=#a; +if i==0 then +t(e,"/>"); +else +t(e,">"); +for i=1,i do +local i=a[i]; +if i.name then +h(i,e,h,o,a.attr.xmlns); +else +t(e,o(i)); +end +end +t(e,""); +end +end +function e.__tostring(t) +local e={}; +y(t,e,y,d,nil); +return w(e); +end +function e.top_tag(e) +local t=""; +if e.attr then +for e,a in n(e.attr)do if o(e)=="string"then t=t..s(" %s='%s'",e,d(f(a)));end end +end +return s("<%s%s>",e.name,t); +end +function e.get_text(e) +if#e.tags==0 then +return w(e); +end +end +function e.get_error(a) +local o,e,t; +local a=a:get_child("error"); +if not a then +return nil,nil,nil; +end +o=a.attr.type; +for o,a in i(a.tags)do +if a.attr.xmlns==p then +if not t and a.name=="text"then +t=a:get_text(); +elseif not e then +e=a.name; +end +if e and t then +break; +end +end +end +return o,e or"undefined-condition",t; +end +do +local e=0; +function new_id() +e=e+1; +return"lx"..e; +end +end +function preserialize(e) +local a={name=e.name,attr=e.attr}; +for i,e in i(e)do +if o(e)=="table"then +t(a,preserialize(e)); +else +t(a,e); +end +end +return a; +end +function deserialize(a) +if a then +local s=a.attr; +for e=1,#s do s[e]=nil;end +local h={}; +for e in n(s)do +if c(e,"|",1,true)and not c(e,"\1",1,true)then +local t,a=l(e,"^([^|]+)|(.+)$"); +h[t.."\1"..a]=s[e]; +s[e]=nil; +end +end +for t,e in n(h)do +s[t]=e; +end +u(a,e); +for t,e in i(a)do +if o(e)=="table"then +deserialize(e); +end +end +if not a.tags then +local n={}; +for i,e in i(a)do +if o(e)=="table"then +t(n,e); +end +end +a.tags=n; +end +end +return a; +end +local function l(a) +local o,i={},{}; +for t,e in n(a.attr)do o[t]=e;end +local o={name=a.name,attr=o,tags=i}; +for e=1,#a do +local e=a[e]; +if e.name then +e=l(e); +t(i,e); +end +t(o,e); +end +return u(o,e); +end +clone=l; +function message(e,t) +if not t then +return h("message",e); +else +return h("message",e):tag("body"):text(t):up(); +end +end +function iq(e) +if e and not e.id then e.id=new_id();end +return h("iq",e or{id=new_id()}); +end +function reply(e) +return h(e.name,e.attr and{to=e.attr.from,from=e.attr.to,id=e.attr.id,type=((e.name=="iq"and"result")or e.attr.type)}); +end +do +local a={xmlns=p}; +function error_reply(e,o,i,t) +local e=reply(e); +e.attr.type="error"; +e:tag("error",{type=o}) +:tag(i,a):up(); +if(t)then e:tag("text",a):text(t):up();end +return e; +end +end +function presence(e) +return h("presence",e); +end +if m then +local u=r("yellow"); +local h=r("red"); +local l=r("red"); +local t=r("magenta"); +local r=" "..a(u,"%s")..a(t,"=")..a(h,"'%s'"); +local h=a(t,"<")..a(l,"%s").."%s"..a(t,">"); +local l=h.."%s"..a(t,""); +function e.pretty_print(e) +local t=""; +for a,e in i(e)do +if o(e)=="string"then +t=t..d(e); +else +t=t..e:pretty_print(); +end +end +local a=""; +if e.attr then +for e,t in n(e.attr)do if o(e)=="string"then a=a..s(r,e,f(t));end end +end +return s(l,e.name,a,t,e.name); +end +function e.pretty_top_tag(t) +local e=""; +if t.attr then +for t,a in n(t.attr)do if o(t)=="string"then e=e..s(r,t,f(a));end end +end +return s(h,t.name,e); +end +else +e.pretty_print=e.__tostring; +e.pretty_top_tag=e.top_tag; +end +return _M; +end) +package.preload['util.timer']=(function(...) +local a=require"net.server"; +local d=math.min +local l=math.huge +local n=require"socket".gettime; +local r=table.insert; +local h=pairs; +local s=type; +local i={}; +local e={}; +module"timer" +local t; +if not a.event then +function t(o,i) +local n=n(); +o=o+n; +if o>=n then +r(e,{o,i}); +else +local e=i(n); +if e and s(e)=="number"then +return t(e,i); +end +end +end +a._addtimer(function() +local a=n(); +if#e>0 then +for a,t in h(e)do +r(i,t); +end +e={}; +end +local e=l; +for h,o in h(i)do +local o,n=o[1],o[2]; +if o<=a then +i[h]=nil; +local a=n(a); +if s(a)=="number"then +t(a,n); +e=d(e,a); +end +else +e=d(e,o-a); +end +end +return e; +end); +else +local e=a.event; +local a=a.event_base; +local o=(e.core and e.core.LEAVE)or-1; +function t(i,e) +local t; +t=a:addevent(nil,0,function() +local e=e(n()); +if e then +return 0,e; +elseif t then +return o; +end +end +,i); +end +end +add_task=t; +return _M; +end) +package.preload['util.termcolours']=(function(...) +local i,n=table.concat,table.insert; +local t,a=string.char,string.format; +local h=tonumber; +local s=ipairs; +local r=io.write; +local e; +if os.getenv("WINDIR")then +e=require"util.windows"; +end +local o=e and e.get_consolecolor and e.get_consolecolor(); +module"termcolours" +local d={ +reset=0;bright=1,dim=2,underscore=4,blink=5,reverse=7,hidden=8; +black=30;red=31;green=32;yellow=33;blue=34;magenta=35;cyan=36;white=37; +["black background"]=40;["red background"]=41;["green background"]=42;["yellow background"]=43;["blue background"]=44;["magenta background"]=45;["cyan background"]=46;["white background"]=47; +bold=1,dark=2,underline=4,underlined=4,normal=0; +} +local u={ +["0"]=o, +["1"]=7+8, +["1;33"]=2+4+8, +["1;31"]=4+8 +} +local l={ +[1]="font-weight: bold",[2]="opacity: 0.5",[4]="text-decoration: underline",[8]="visibility: hidden", +[30]="color:black",[31]="color:red",[32]="color:green",[33]="color:#FFD700", +[34]="color:blue",[35]="color: magenta",[36]="color:cyan",[37]="color: white", +[40]="background-color:black",[41]="background-color:red",[42]="background-color:green", +[43]="background-color:yellow",[44]="background-color:blue",[45]="background-color: magenta", +[46]="background-color:cyan",[47]="background-color: white"; +}; +local c=t(27).."[%sm%s"..t(27).."[0m"; +function getstring(t,e) +if t then +return a(c,t,e); +else +return e; +end +end +function getstyle(...) +local e,t={...},{}; +for a,e in s(e)do +e=d[e]; +if e then +n(t,e); +end +end +return i(t,";"); +end +local a="0"; +function setstyle(e) +e=e or"0"; +if e~=a then +r("\27["..e.."m"); +a=e; +end +end +if e then +function setstyle(t) +t=t or"0"; +if t~=a then +e.set_consolecolor(u[t]or o); +a=t; +end +end +if not o then +function setstyle(e)end +end +end +local function a(e) +if e=="0"then return"";end +local t={}; +for e in e:gmatch("[^;]+")do +n(t,l[h(e)]); +end +return""; +end +function tohtml(e) +return e:gsub("\027%[(.-)m",a); +end +return _M; +end) +package.preload['util.uuid']=(function(...) +local e=math.random; +local o=tostring; +local e=os.time; +local n=os.clock; +local i=require"util.hashes".sha1; +module"uuid" +local t=0; +local function a() +local e=e(); +if t>=e then e=t+1;end +t=e; +return e; +end +local function t(e) +return i(e..n()..o({}),true); +end +local e=t(a()); +local function o(a) +e=t(e..a); +end +local function t(t) +if#e"; +end +return e; +end +local k={ +LOC=e.LOC_tostring; +MX=function(e) +return a.format('%2i %s',e.pref,e.mx); +end; +SRV=function(e) +local e=e.srv; +return a.format('%5d %5d %5d %s',e.priority,e.weight,e.port,e.target); +end; +}; +local q={}; +function q.__tostring(e) +local t=(k[e.type]or w)(e); +return a.format('%2s %-5s %6i %-28s %s',e.class,e.type,e.ttl,e.name,t); +end +local k={}; +function k.__tostring(t) +local e={}; +for a,t in c(t)do +h(e,p(t)..'\n'); +end +return i.concat(e); +end +local w={}; +function w.__tostring(t) +local a=s.gettime(); +local e={}; +for i,t in o(t)do +for i,t in o(t)do +for o,t in o(t)do +y(t,a); +h(e,p(t)); +end +end +end +return i.concat(e); +end +function e:new() +local t={active={},cache={},unsorted={}}; +r(t,e); +r(t.cache,w); +r(t.unsorted,{__mode='kv'}); +return t; +end +function t.random(...) +b.randomseed(b.floor(1e4*s.gettime())%2147483648); +t.random=b.random; +return t.random(...); +end +local function E(e) +e=e or{}; +e.id=e.id or t.random(0,65535); +e.rd=e.rd or 1; +e.tc=e.tc or 0; +e.aa=e.aa or 0; +e.opcode=e.opcode or 0; +e.qr=e.qr or 0; +e.rcode=e.rcode or 0; +e.z=e.z or 0; +e.ra=e.ra or 0; +e.qdcount=e.qdcount or 1; +e.ancount=e.ancount or 0; +e.nscount=e.nscount or 0; +e.arcount=e.arcount or 0; +local t=a.char( +m(e.id),e.id%256, +e.rd+2*e.tc+4*e.aa+8*e.opcode+128*e.qr, +e.rcode+16*e.z+128*e.ra, +m(e.qdcount),e.qdcount%256, +m(e.ancount),e.ancount%256, +m(e.nscount),e.nscount%256, +m(e.arcount),e.arcount%256 +); +return t,e.id; +end +local function m(t) +local e={}; +for t in a.gmatch(t,'[^.]+')do +h(e,a.char(a.len(t))); +h(e,t); +end +h(e,a.char(0)); +return i.concat(e); +end +local function b(o,a,e) +o=m(o); +a=t.typecode[a or'a']; +e=t.classcode[e or'in']; +return o..a..e; +end +function e:byte(e) +e=e or 1; +local o=self.offset; +local t=o+e-1; +if t>#self.packet then +x(a.format('out of bounds: %i>%i',t,#self.packet)); +end +self.offset=o+e; +return a.byte(self.packet,o,t); +end +function e:word() +local t,e=self:byte(2); +return 256*t+e; +end +function e:dword() +local o,t,a,e=self:byte(4); +return 16777216*o+65536*t+256*a+e; +end +function e:sub(e) +e=e or 1; +local t=a.sub(self.packet,self.offset,self.offset+e-1); +self.offset=self.offset+e; +return t; +end +function e:header(t) +local e=self:word(); +if not self.active[e]and not t then return nil;end +local e={id=e}; +local t,a=self:byte(2); +e.rd=t%2; +e.tc=t/2%2; +e.aa=t/4%2; +e.opcode=t/8%16; +e.qr=t/128; +e.rcode=a%16; +e.z=a/16%8; +e.ra=a/128; +e.qdcount=self:word(); +e.ancount=self:word(); +e.nscount=self:word(); +e.arcount=self:word(); +for a,t in o(e)do e[a]=t-t%1;end +return e; +end +function e:name() +local a,t=nil,0; +local e=self:byte(); +local o={}; +if e==0 then return"."end +while e>0 do +if e>=192 then +t=t+1; +if t>=20 then x('dns error: 20 pointers');end; +local e=((e-192)*256)+self:byte(); +a=a or self.offset; +self.offset=e+1; +else +h(o,self:sub(e)..'.'); +end +e=self:byte(); +end +self.offset=a or self.offset; +return i.concat(o); +end +function e:question() +local e={}; +e.name=self:name(); +e.type=t.type[self:word()]; +e.class=t.class[self:word()]; +return e; +end +function e:A(e) +local o,t,n,i=self:byte(4); +e.a=a.format('%i.%i.%i.%i',o,t,n,i); +end +function e:AAAA(a) +local e={}; +for t=1,a.rdlength,2 do +local a,t=self:byte(2); +i.insert(e,("%02x%02x"):format(a,t)); +end +e=i.concat(e,":"):gsub("%f[%x]0+(%x)","%1"); +local t={}; +for e in e:gmatch(":[0:]+:")do +i.insert(t,e) +end +if#t==0 then +a.aaaa=e; +return +elseif#t>1 then +i.sort(t,function(t,e)return#t>#e end); +end +a.aaaa=e:gsub(t[1],"::",1):gsub("^0::","::"):gsub("::0$","::"); +end +function e:CNAME(e) +e.cname=self:name(); +end +function e:MX(e) +e.pref=self:word(); +e.mx=self:name(); +end +function e:LOC_nibble_power() +local e=self:byte(); +return((e-(e%16))/16)*(10^(e%16)); +end +function e:LOC(e) +e.version=self:byte(); +if e.version==0 then +e.loc=e.loc or{}; +e.loc.size=self:LOC_nibble_power(); +e.loc.horiz_pre=self:LOC_nibble_power(); +e.loc.vert_pre=self:LOC_nibble_power(); +e.loc.latitude=self:dword(); +e.loc.longitude=self:dword(); +e.loc.altitude=self:dword(); +end +end +local function m(e,i,t) +e=e-2147483648; +if e<0 then i=t;e=-e;end +local n,t,o; +o=e%6e4; +e=(e-o)/6e4; +t=e%60; +n=(e-t)/60; +return a.format('%3d %2d %2.3f %s',n,t,o/1e3,i); +end +function e.LOC_tostring(e) +local t={}; +h(t,a.format( +'%s %s %.2fm %.2fm %.2fm %.2fm', +m(e.loc.latitude,'N','S'), +m(e.loc.longitude,'E','W'), +(e.loc.altitude-1e7)/100, +e.loc.size/100, +e.loc.horiz_pre/100, +e.loc.vert_pre/100 +)); +return i.concat(t); +end +function e:NS(e) +e.ns=self:name(); +end +function e:SOA(e) +end +function e:SRV(e) +e.srv={}; +e.srv.priority=self:word(); +e.srv.weight=self:word(); +e.srv.port=self:word(); +e.srv.target=self:name(); +end +function e:PTR(e) +e.ptr=self:name(); +end +function e:TXT(e) +e.txt=self:sub(self:byte()); +end +function e:rr() +local e={}; +r(e,q); +e.name=self:name(self); +e.type=t.type[self:word()]or e.type; +e.class=t.class[self:word()]or e.class; +e.ttl=65536*self:word()+self:word(); +e.rdlength=self:word(); +if e.ttl<=0 then +e.tod=self.time+30; +else +e.tod=self.time+e.ttl; +end +local a=self.offset; +local t=self[t.type[e.type]]; +if t then t(self,e);end +self.offset=a; +e.rdata=self:sub(e.rdlength); +return e; +end +function e:rrs(t) +local e={}; +for t=1,t do h(e,self:rr());end +return e; +end +function e:decode(t,o) +self.packet,self.offset=t,1; +local t=self:header(o); +if not t then return nil;end +local t={header=t}; +t.question={}; +local i=self.offset; +for e=1,t.header.qdcount do +h(t.question,self:question()); +end +t.question.raw=a.sub(self.packet,i,self.offset-1); +if not o then +if not self.active[t.header.id]or not self.active[t.header.id][t.question.raw]then +self.active[t.header.id]=nil; +return nil; +end +end +t.answer=self:rrs(t.header.ancount); +t.authority=self:rrs(t.header.nscount); +t.additional=self:rrs(t.header.arcount); +return t; +end +e.delays={1,3}; +function e:addnameserver(e) +self.server=self.server or{}; +h(self.server,e); +end +function e:setnameserver(e) +self.server={}; +self:addnameserver(e); +end +function e:adddefaultnameservers() +if _ then +if v and v.get_nameservers then +for t,e in c(v.get_nameservers())do +self:addnameserver(e); +end +end +if not self.server or#self.server==0 then +self:addnameserver("208.67.222.222"); +self:addnameserver("208.67.220.220"); +end +else +local e=z.open("/etc/resolv.conf"); +if e then +for e in e:lines()do +e=e:gsub("#.*$","") +:match('^%s*nameserver%s+(.*)%s*$'); +if e then +e:gsub("%f[%d.](%d+%.%d+%.%d+%.%d+)%f[^%d.]",function(e) +self:addnameserver(e) +end); +end +end +end +if not self.server or#self.server==0 then +self:addnameserver("127.0.0.1"); +end +end +end +function e:getsocket(a) +self.socket=self.socket or{}; +self.socketset=self.socketset or{}; +local e=self.socket[a]; +if e then return e;end +local o,t; +e,t=s.udp(); +if e and self.socket_wrapper then e,t=self.socket_wrapper(e,self);end +if not e then +return nil,t; +end +e:settimeout(0); +self.socket[a]=e; +self.socketset[e]=a; +o,t=e:setsockname('*',0); +if not o then return self:servfail(e,t);end +o,t=e:setpeername(self.server[a],53); +if not o then return self:servfail(e,t);end +return e; +end +function e:voidsocket(e) +if self.socket[e]then +self.socketset[self.socket[e]]=nil; +self.socket[e]=nil; +elseif self.socketset[e]then +self.socket[self.socketset[e]]=nil; +self.socketset[e]=nil; +end +e:close(); +end +function e:socket_wrapper_set(e) +self.socket_wrapper=e; +end +function e:closeall() +for t,e in c(self.socket)do +self.socket[t]=nil; +self.socketset[e]=nil; +e:close(); +end +end +function e:remember(e,t) +local i,o,a=g(e.name,e.type,e.class); +if t~='*'then +t=o; +local t=d(self.cache,a,'*',i); +if t then h(t,e);end +end +self.cache=self.cache or r({},w); +local a=d(self.cache,a,t,i)or +l(self.cache,a,t,i,r({},k)); +if not a[e[o:lower()]]then +a[e[o:lower()]]=true; +h(a,e); +end +if t=='MX'then self.unsorted[a]=true;end +end +local function m(e,t) +return(e.pref==t.pref)and(e.mx#self.server then +e.server=1; +end +e.retries=(e.retries or 0)+1; +if e.retries>=#self.server then +a[o]=nil; +else +t,i=self:getsocket(e.server); +if t then t:send(e.packet);end +end +end +end +if n(a)==nil then +self.active[s]=nil; +end +end +if h==self.best_server then +self.best_server=self.best_server+1; +if self.best_server>#self.server then +self.best_server=1; +end +end +return t,i; +end +function e:settimeout(e) +self.timeout=e; +end +function e:receive(t) +self.time=s.gettime(); +t=t or self.socket; +local e; +for a,t in o(t)do +if self.socketset[t]then +local t=t:receive(); +if t then +e=self:decode(t); +if e and self.active[e.header.id] +and self.active[e.header.id][e.question.raw]then +for a,t in o(e.answer)do +self:remember(t,e.question[1].type) +end +local t=self.active[e.header.id]; +t[e.question.raw]=nil; +if not n(t)then self.active[e.header.id]=nil;end +if not n(self.active)then self:closeall();end +local e=e.question[1]; +local t=d(self.wanted,e.class,e.type,e.name); +if t then +for e in o(t)do +if u.status(e)=="suspended"then u.resume(e);end +end +l(self.wanted,e.class,e.type,e.name,nil); +end +end +end +end +end +return e; +end +function e:feed(a,e,t) +self.time=s.gettime(); +local e=self:decode(e,t); +if e and self.active[e.header.id] +and self.active[e.header.id][e.question.raw]then +for a,t in o(e.answer)do +self:remember(t,e.question[1].type); +end +local t=self.active[e.header.id]; +t[e.question.raw]=nil; +if not n(t)then self.active[e.header.id]=nil;end +if not n(self.active)then self:closeall();end +local e=e.question[1]; +if e then +local t=d(self.wanted,e.class,e.type,e.name); +if t then +for e in o(t)do +if u.status(e)=="suspended"then u.resume(e);end +end +l(self.wanted,e.class,e.type,e.name,nil); +end +end +end +return e; +end +function e:cancel(i,t,e) +local a=d(self.wanted,i,t,e); +if a then +for e in o(a)do +if u.status(e)=="suspended"then u.resume(e);end +end +l(self.wanted,i,t,e,nil); +end +end +function e:pulse() +while self:receive()do end +if not n(self.active)then return nil;end +self.time=s.gettime(); +for i,t in o(self.active)do +for a,e in o(t)do +if self.time>=e.retry then +e.server=e.server+1; +if e.server>#self.server then +e.server=1; +e.delay=e.delay+1; +end +if e.delay>#self.delays then +t[a]=nil; +if not n(t)then self.active[i]=nil;end +if not n(self.active)then return nil;end +else +local t=self.socket[e.server]; +if t then t:send(e.packet);end +e.retry=self.time+self.delays[e.delay]; +end +end +end +end +if n(self.active)then return true;end +return nil; +end +function e:lookup(t,o,a) +self:query(t,o,a) +while self:pulse()do +local e={} +for a,t in c(self.socket)do +e[a]=t +end +s.select(e,nil,4) +end +return self:peek(t,o,a); +end +function e:lookupex(o,e,t,a) +return self:peek(e,t,a)or self:query(e,t,a); +end +function e:tohostname(e) +return t.lookup(e:gsub("(%d+)%.(%d+)%.(%d+)%.(%d+)","%4.%3.%2.%1.in-addr.arpa."),"PTR"); +end +local i={ +qr={[0]='query','response'}, +opcode={[0]='query','inverse query','server status request'}, +aa={[0]='non-authoritative','authoritative'}, +tc={[0]='complete','truncated'}, +rd={[0]='recursion not desired','recursion desired'}, +ra={[0]='recursion not available','recursion available'}, +z={[0]='(reserved)'}, +rcode={[0]='no error','format error','server failure','name error','not implemented'}, +type=t.type, +class=t.class +}; +local function n(t,e) +return(i[e]and i[e][t[e]])or''; +end +function e.print(e) +for o,t in o{'id','qr','opcode','aa','tc','rd','ra','z', +'rcode','qdcount','ancount','nscount','arcount'}do +f(a.format('%-30s','header.'..t),e.header[t],n(e.header,t)); +end +for t,e in c(e.question)do +f(a.format('question[%i].name ',t),e.name); +f(a.format('question[%i].type ',t),e.type); +f(a.format('question[%i].class ',t),e.class); +end +local h={name=1,type=1,class=1,ttl=1,rdlength=1,rdata=1}; +local t; +for s,i in o({'answer','authority','additional'})do +for s,e in o(e[i])do +for h,o in o({'name','type','class','ttl','rdlength'})do +t=a.format('%s[%i].%s',i,s,o); +f(a.format('%-30s',t),e[o],n(e,o)); +end +for e,o in o(e)do +if not h[e]then +t=a.format('%s[%i].%s',i,s,e); +f(a.format('%-30s %s',p(t),p(o))); +end +end +end +end +end +function t.resolver() +local t={active={},cache={},unsorted={},wanted={},best_server=1}; +r(t,e); +r(t.cache,w); +r(t.unsorted,{__mode='kv'}); +return t; +end +local e=t.resolver(); +t._resolver=e; +function t.lookup(...) +return e:lookup(...); +end +function t.tohostname(...) +return e:tohostname(...); +end +function t.purge(...) +return e:purge(...); +end +function t.peek(...) +return e:peek(...); +end +function t.query(...) +return e:query(...); +end +function t.feed(...) +return e:feed(...); +end +function t.cancel(...) +return e:cancel(...); +end +function t.settimeout(...) +return e:settimeout(...); +end +function t.cache() +return e.cache; +end +function t.socket_wrapper_set(...) +return e:socket_wrapper_set(...); +end +return t; +end) +package.preload['net.adns']=(function(...) +local c=require"net.server"; +local a=require"net.dns"; +local e=require"util.logger".init("adns"); +local t,t=table.insert,table.remove; +local o,r,l=coroutine,tostring,pcall; +local function u(a,a,e,t)return(t-e)+1;end +module"adns" +function lookup(d,t,h,s) +return o.wrap(function(i) +if i then +e("debug","Records for %s already cached, using those...",t); +d(i); +return; +end +e("debug","Records for %s not in cache, sending query (%s)...",t,r(o.running())); +local i,n=a.query(t,h,s); +if i then +o.yield({s or"IN",h or"A",t,o.running()}); +e("debug","Reply for %s (%s)",t,r(o.running())); +end +if i then +i,n=l(d,a.peek(t,h,s)); +else +e("error","Error sending DNS query: %s",n); +i,n=l(d,nil,n); +end +if not i then +e("error","Error in DNS response handler: %s",r(n)); +end +end)(a.peek(t,h,s)); +end +function cancel(t,o,i) +e("warn","Cancelling DNS lookup for %s",r(t[3])); +a.cancel(t[1],t[2],t[3],t[4],o); +end +function new_async_socket(o,i) +local n=""; +local s={}; +local t={}; +local h; +function s.onincoming(o,e) +if e then +a.feed(t,e); +end +end +function s.ondisconnect(a,o) +if o then +e("warn","DNS socket for %s disconnected: %s",n,o); +local t=i.server; +if i.socketset[a]==i.best_server and i.best_server==#t then +e("error","Exhausted all %d configured DNS servers, next lookup will try %s again",#t,t[1]); +end +i:servfail(a); +end +end +t,h=c.wrapclient(o,"dns",53,s); +if not t then +return nil,h; +end +t.settimeout=function()end +t.setsockname=function(e,...)return o:setsockname(...);end +t.setpeername=function(i,...)n=(...);local a,e=o:setpeername(...);i:set_send(u);return a,e;end +t.connect=function(e,...)return o:connect(...)end +t.send=function(a,t) +e("debug","Sending DNS query to %s",n); +return o:send(t); +end +return t; +end +a.socket_wrapper_set(new_async_socket); +return _M; +end) +package.preload['net.server']=(function(...) +local l=function(e) +return _G[e] +end +local M,e=require("util.logger").init("socket"),table.concat; +local i=function(...)return M("debug",e{...});end +local H=function(...)return M("warn",e{...});end +local ce=1 +local F=l"type" +local W=l"pairs" +local de=l"ipairs" +local y=l"tonumber" +local h=l"tostring" +local t=l"os" +local o=l"table" +local a=l"string" +local e=l"coroutine" +local Z=t.difftime +local X=math.min +local re=math.huge +local fe=o.concat +local me=a.sub +local we=e.wrap +local ye=e.yield +local x=l"ssl" +local b=l"socket"or require"socket" +local J=b.gettime +local ve=(x and x.wrap) +local he=b.bind +local pe=b.sleep +local be=b.select +local G +local B +local ae +local K +local te +local c +local ee +local oe +local ne +local ie +local se +local Q +local r +local le +local D +local ue +local p +local s +local R +local d +local n +local S +local g +local f +local m +local a +local o +local v +local U +local C +local T +local E +local k +local V +local u +local _ +local z +local A +local I +local O +local L +local j +local q +local N +p={} +s={} +d={} +R={} +n={} +g={} +f={} +S={} +a=0 +o=0 +v=0 +U=0 +C=0 +T=1 +E=0 +k=128 +_=51e3*1024 +z=25e3*1024 +A=12e5 +I=6e4 +O=6*60*60 +local e=package.config:sub(1,1)=="\\" +q=(e and math.huge)or b._SETSIZE or 1024 +j=b._SETSIZE or 1024 +N=30 +ie=function(w,t,f,l,v,u) +if t:getfd()>=q then +H("server.lua: Disallowed FD number: "..t:getfd()) +t:close() +return nil,"fd-too-large" +end +local m=0 +local y,e=w.onconnect,w.ondisconnect +local b=t.accept +local e={} +e.shutdown=function()end +e.ssl=function() +return u~=nil +end +e.sslctx=function() +return u +end +e.remove=function() +m=m-1 +if e then +e.resume() +end +end +e.close=function() +t:close() +o=r(d,t,o) +a=r(s,t,a) +p[f..":"..l]=nil; +n[t]=nil +e=nil +t=nil +i"server.lua: closed server handler and removed sockets from list" +end +e.pause=function(o) +if not e.paused then +a=r(s,t,a) +if o then +n[t]=nil +t:close() +t=nil; +end +e.paused=true; +end +end +e.resume=function() +if e.paused then +if not t then +t=he(f,l,k); +t:settimeout(0) +end +a=c(s,t,a) +n[t]=e +e.paused=false; +end +end +e.ip=function() +return f +end +e.serverport=function() +return l +end +e.socket=function() +return t +end +e.readbuffer=function() +if a>=j or o>=j then +e.pause() +i("server.lua: refused new client connection: server full") +return false +end +local t,o=b(t) +if t then +local o,a=t:getpeername() +local t,n,e=D(e,w,t,o,l,a,v,u) +if e then +return false +end +m=m+1 +i("server.lua: accepted new client connection from ",h(o),":",h(a)," to ",h(l)) +if y and not u then +return y(t); +end +return; +elseif o then +i("server.lua: error with new client connection: ",h(o)) +return false +end +end +return e +end +D=function(E,y,t,I,Q,O,T,j) +if t:getfd()>=q then +H("server.lua: Disallowed FD number: "..t:getfd()) +t:close() +if E then +E.pause() +end +return nil,nil,"fd-too-large" +end +t:settimeout(0) +local p +local A +local k +local P +local W=y.onincoming +local Y=y.onstatus +local b=y.ondisconnect +local F=y.ondrain +local M=y.ondetach +local v={} +local l=0 +local V +local B +local L +local w=0 +local q=false +local H=false +local D,R=0,0 +local _=_ +local z=z +local e=v +e.dispatch=function() +return W +end +e.disconnect=function() +return b +end +e.setlistener=function(a,t) +if M then +M(a) +end +W=t.onincoming +b=t.ondisconnect +Y=t.onstatus +F=t.ondrain +M=t.ondetach +end +e.getstats=function() +return R,D +end +e.ssl=function() +return P +end +e.sslctx=function() +return j +end +e.send=function(n,o,i,a) +return p(t,o,i,a) +end +e.receive=function(o,a) +return A(t,o,a) +end +e.shutdown=function(a) +return k(t,a) +end +e.setoption=function(i,a,o) +if t.setoption then +return t:setoption(a,o); +end +return false,"setoption not implemented"; +end +e.force_close=function(t,a) +if l~=0 then +i("server.lua: discarding unwritten data for ",h(I),":",h(O)) +l=0; +end +return t:close(a); +end +e.close=function(u,h) +if not e then return true;end +a=r(s,t,a) +g[e]=nil +if l~=0 then +e.sendbuffer() +if l~=0 then +if e then +e.write=nil +end +V=true +return false +end +end +if t then +m=k and k(t) +t:close() +o=r(d,t,o) +n[t]=nil +t=nil +else +i"server.lua: socket already closed" +end +if e then +f[e]=nil +S[e]=nil +local t=e; +e=nil +if b then +b(t,h or false); +b=nil +end +end +if E then +E.remove() +end +i"server.lua: closed client handler and removed socket from list" +return true +end +e.ip=function() +return I +end +e.serverport=function() +return Q +end +e.clientport=function() +return O +end +e.port=e.clientport +local b=function(i,a) +w=w+#a +if w>_ then +S[e]="send buffer exceeded" +e.write=K +return false +elseif t and not d[t]then +o=c(d,t,o) +end +l=l+1 +v[l]=a +if e then +f[e]=f[e]or u +end +return true +end +e.write=b +e.bufferqueue=function(t) +return v +end +e.socket=function(a) +return t +end +e.set_mode=function(a,t) +T=t or T +return T +end +e.set_send=function(a,t) +p=t or p +return p +end +e.bufferlen=function(o,a,t) +_=t or _ +z=a or z +return w,z,_ +end +e.lock_read=function(i,o) +if o==true then +local o=a +a=r(s,t,a) +g[e]=nil +if a~=o then +q=true +end +elseif o==false then +if q then +q=false +a=c(s,t,a) +g[e]=u +end +end +return q +end +e.pause=function(t) +return t:lock_read(true); +end +e.resume=function(t) +return t:lock_read(false); +end +e.lock=function(i,a) +e.lock_read(a) +if a==true then +e.write=K +local a=o +o=r(d,t,o) +f[e]=nil +if o~=a then +H=true +end +elseif a==false then +e.write=b +if H then +H=false +b("") +end +end +return q,H +end +local b=function() +local a,t,o=A(t,T) +if not t or(t=="wantread"or t=="timeout")then +local o=a or o or"" +local a=#o +if a>z then +e:close("receive buffer exceeded") +return false +end +local a=a*ce +R=R+a +C=C+a +g[e]=u +return W(e,o,t) +else +i("server.lua: client ",h(I),":",h(O)," read error: ",h(t)) +B=true +m=e and e:force_close(t) +return false +end +end +local w=function() +local c,a,n,s,y; +if t then +s=fe(v,"",1,l) +c,a,n=p(t,s,1,w) +y=(c or n or 0)*ce +D=D+y +U=U+y +for e=l,1,-1 do +v[e]=nil +end +else +c,a,y=false,"unexpected close",0; +end +if c then +l=0 +w=0 +o=r(d,t,o) +f[e]=nil +if F then +F(e) +end +m=L and e:starttls(nil) +m=V and e:force_close() +return true +elseif n and(a=="timeout"or a=="wantwrite")then +s=me(s,n+1,w) +v[1]=s +l=1 +w=w-n +f[e]=u +return true +else +i("server.lua: client ",h(I),":",h(O)," write error: ",h(a)) +B=true +m=e and e:force_close(a) +return false +end +end +local u; +function e.set_sslctx(p,t) +j=t; +local l,f +u=we(function(n) +local t +for h=1,N do +o=(f and r(d,n,o))or o +a=(l and r(s,n,a))or a +l,f=nil,nil +m,t=n:dohandshake() +if not t then +i("server.lua: ssl handshake done") +e.readbuffer=b +e.sendbuffer=w +m=Y and Y(e,"ssl-handshake-complete") +if p.autostart_ssl and y.onconnect then +y.onconnect(p); +end +a=c(s,n,a) +return true +else +if t=="wantwrite"then +o=c(d,n,o) +f=true +elseif t=="wantread"then +a=c(s,n,a) +l=true +else +break; +end +t=nil; +ye() +end +end +i("server.lua: ssl handshake error: ",h(t or"handshake too long")) +m=e and e:force_close("ssl handshake failed") +return false,t +end +) +end +if x then +e.starttls=function(f,m) +if m then +e:set_sslctx(m); +end +if l>0 then +i"server.lua: we need to do tls, but delaying until send buffer empty" +L=true +return +end +i("server.lua: attempting to start tls on "..h(t)) +local m,l=t +t,l=ve(t,j) +if not t then +i("server.lua: error while starting tls on client: ",h(l or"unknown error")) +return nil,l +end +t:settimeout(0) +p=t.send +A=t.receive +k=G +n[t]=e +a=c(s,t,a) +a=r(s,m,a) +o=r(d,m,o) +n[m]=nil +e.starttls=nil +L=nil +P=true +e.readbuffer=u +e.sendbuffer=u +return u(t) +end +end +e.readbuffer=b +e.sendbuffer=w +p=t.send +A=t.receive +k=(P and G)or t.shutdown +n[t]=e +a=c(s,t,a) +if j and x then +i"server.lua: auto-starting ssl negotiation..." +e.autostart_ssl=true; +local t,e=e:starttls(j); +if t==false then +return nil,nil,e +end +end +return e,t +end +G=function() +end +K=function() +return false +end +c=function(t,a,e) +if not t[a]then +e=e+1 +t[e]=a +t[a]=e +end +return e; +end +r=function(e,a,t) +local o=e[a] +if o then +e[a]=nil +local i=e[t] +e[t]=nil +if i~=a then +e[i]=o +e[o]=i +end +return t-1 +end +return t +end +Q=function(e) +o=r(d,e,o) +a=r(s,e,a) +n[e]=nil +e:close() +end +local function w(e,a,o) +local t; +local i=a.sendbuffer; +function a.sendbuffer() +i(); +if t and a.bufferlen()=o then +t=true; +e:lock_read(true); +end +end +e:set_mode("*a"); +end +ee=function(t,e,d,l,r) +local o +if F(d)~="table"then +o="invalid listener table" +end +if F(e)~="number"or not(e>=0 and e<=65535)then +o="invalid port" +elseif p[t..":"..e]then +o="listeners on '["..t.."]:"..e.."' already exist" +elseif r and not x then +o="luasec not found" +end +if o then +H("server.lua, [",t,"]:",e,": ",o) +return nil,o +end +t=t or"*" +local o,h=he(t,e,k) +if h then +H("server.lua, [",t,"]:",e,": ",h) +return nil,h +end +local h,d=ie(d,o,t,e,l,r) +if not h then +o:close() +return nil,d +end +o:settimeout(0) +a=c(s,o,a) +p[t..":"..e]=h +n[o]=h +i("server.lua: new "..(r and"ssl "or"").."server listener on '[",t,"]:",e,"'") +return h +end +ne=function(t,e) +return p[t..":"..e]; +end +le=function(t,e) +local a=p[t..":"..e] +if not a then +return nil,"no server found on '["..t.."]:"..h(e).."'" +end +a:close() +p[t..":"..e]=nil +return true +end +te=function() +for e,t in W(n)do +t:close() +n[e]=nil +end +a=0 +o=0 +v=0 +p={} +s={} +d={} +R={} +n={} +end +se=function() +return{ +select_timeout=T; +select_sleep_time=E; +tcp_backlog=k; +max_send_buffer_size=_; +max_receive_buffer_size=z; +select_idle_check_interval=A; +send_timeout=I; +read_timeout=O; +max_connections=j; +max_ssl_handshake_roundtrips=N; +highest_allowed_fd=q; +} +end +ue=function(e) +if F(e)~="table"then +return nil,"invalid settings table" +end +T=y(e.select_timeout)or T +E=y(e.select_sleep_time)or E +_=y(e.max_send_buffer_size)or _ +z=y(e.max_receive_buffer_size)or z +A=y(e.select_idle_check_interval)or A +k=y(e.tcp_backlog)or k +I=y(e.send_timeout)or I +O=y(e.read_timeout)or O +j=e.max_connections or j +N=e.max_ssl_handshake_roundtrips or N +q=e.highest_allowed_fd or q +return true +end +oe=function(e) +if F(e)~="function"then +return nil,"invalid listener function" +end +v=v+1 +R[v]=e +return true +end +ae=function() +return C,U,a,o,v +end +local t; +local function h(e) +t=not not e; +end +B=function(a) +if t then return"quitting";end +if a then t="once";end +local e=re; +repeat +local o,a,s=be(s,d,X(T,e)) +for t,e in de(a)do +local t=n[e] +if t then +t.sendbuffer() +else +Q(e) +i"server.lua: found no handler and closed socket (writelist)" +end +end +for e,t in de(o)do +local e=n[t] +if e then +e.readbuffer() +else +Q(t) +i"server.lua: found no handler and closed socket (readlist)" +end +end +for e,t in W(S)do +e.disconnect()(e,t) +e:force_close() +S[e]=nil; +end +u=J() +local a=Z(u-V) +if a>A then +V=u +for e,t in W(f)do +if Z(u-t)>I then +e.disconnect()(e,"send timeout") +e:force_close() +end +end +for e,t in W(g)do +if Z(u-t)>O then +e.disconnect()(e,"read timeout") +e:close() +end +end +end +if u-L>=X(e,1)then +e=re; +for t=1,v do +local t=R[t](u) +if t then e=X(e,t);end +end +L=u +else +e=e-(u-L); +end +pe(E) +until t; +if a and t=="once"then t=nil;return;end +return"quitting" +end +local function u() +return B(true); +end +local function r() +return"select"; +end +local i=function(e,t,s,a,h,i) +local e,t,s=D(nil,a,e,t,s,"clientport",h,i) +if not e then return nil,s end +n[t]=e +if not i then +o=c(d,t,o) +if a.onconnect then +local t=e.sendbuffer; +e.sendbuffer=function() +e.sendbuffer=t; +a.onconnect(e); +return t(); +end +end +end +return e,t +end +local o=function(a,o,n,h,s) +local e,t=b.tcp() +if t then +return nil,t +end +e:settimeout(0) +m,t=e:connect(a,o) +if t then +local e=i(e,a,o,n) +else +D(nil,n,e,a,o,"clientport",h,s) +end +end +l"setmetatable"(n,{__mode="k"}) +l"setmetatable"(g,{__mode="k"}) +l"setmetatable"(f,{__mode="k"}) +L=J() +V=J() +local function a(e) +local t=M; +if e then +M=e; +end +return t; +end +return{ +_addtimer=oe, +addclient=o, +wrapclient=i, +loop=B, +link=w, +step=u, +stats=ae, +closeall=te, +addserver=ee, +getserver=ne, +setlogger=a, +getsettings=se, +setquitting=h, +removeserver=le, +get_backend=r, +changesettings=ue, +} +end) +package.preload['util.xmppstream']=(function(...) +local e=require"lxp"; +local t=require"util.stanza"; +local b=t.stanza_mt; +local f=error; +local t=tostring; +local l=table.insert; +local y=table.concat; +local x=table.remove; +local v=setmetatable; +local j=pcall(e.new,{StartDoctypeDecl=false}); +local k=pcall(e.new,{XmlDecl=false}); +local a=not not e.new({}).getcurrentbytecount; +local q=1024*1024*10; +module"xmppstream" +local p=e.new; +local z={ +["http://www.w3.org/XML/1998/namespace\1lang"]="xml:lang"; +["http://www.w3.org/XML/1998/namespace\1space"]="xml:space"; +["http://www.w3.org/XML/1998/namespace\1base"]="xml:base"; +["http://www.w3.org/XML/1998/namespace\1id"]="xml:id"; +}; +local h="http://etherx.jabber.org/streams"; +local d="\1"; +local g="^([^"..d.."]*)"..d.."?(.*)$"; +_M.ns_separator=d; +_M.ns_pattern=g; +local function o()end +function new_sax_handlers(n,e,s) +local i={}; +local p=e.streamopened; +local w=e.streamclosed; +local u=e.error or function(o,a,e)f("XML stream error: "..t(a)..(e and": "..t(e)or""),2);end; +local q=e.handlestanza; +s=s or o; +local t=e.stream_ns or h; +local m=e.stream_tag or"stream"; +if t~=""then +m=t..d..m; +end +local _=t..d..(e.error_tag or"error"); +local E=e.default_ns; +local d={}; +local h,e={}; +local t=0; +local r=0; +function i:StartElement(c,o) +if e and#h>0 then +l(e,y(h)); +h={}; +end +local h,i=c:match(g); +if i==""then +h,i="",h; +end +if h~=E or r>0 then +o.xmlns=h; +r=r+1; +end +for t=1,#o do +local e=o[t]; +o[t]=nil; +local t=z[e]; +if t then +o[t]=o[e]; +o[e]=nil; +end +end +if not e then +if a then +t=self:getcurrentbytecount(); +end +if n.notopen then +if c==m then +r=0; +if p then +if a then +s(t); +t=0; +end +p(n,o); +end +else +u(n,"no-stream",c); +end +return; +end +if h=="jabber:client"and i~="iq"and i~="presence"and i~="message"then +u(n,"invalid-top-level-element"); +end +e=v({name=i,attr=o,tags={}},b); +else +if a then +t=t+self:getcurrentbytecount(); +end +l(d,e); +local t=e; +e=v({name=i,attr=o,tags={}},b); +l(t,e); +l(t.tags,e); +end +end +if k then +function i:XmlDecl(e,e,e) +if a then +s(self:getcurrentbytecount()); +end +end +end +function i:StartCdataSection() +if a then +if e then +t=t+self:getcurrentbytecount(); +else +s(self:getcurrentbytecount()); +end +end +end +function i:EndCdataSection() +if a then +if e then +t=t+self:getcurrentbytecount(); +else +s(self:getcurrentbytecount()); +end +end +end +function i:CharacterData(o) +if e then +if a then +t=t+self:getcurrentbytecount(); +end +l(h,o); +elseif a then +s(self:getcurrentbytecount()); +end +end +function i:EndElement(o) +if a then +t=t+self:getcurrentbytecount() +end +if r>0 then +r=r-1; +end +if e then +if#h>0 then +l(e,y(h)); +h={}; +end +if#d==0 then +if a then +s(t); +end +t=0; +if o~=_ then +q(n,e); +else +u(n,"stream-error",e); +end +e=nil; +else +e=x(d); +end +else +if w then +w(n); +end +end +end +local function a(e) +u(n,"parse-error","restricted-xml","Restricted XML, see RFC 6120 section 11.1."); +if not e.stop or not e:stop()then +f("Failed to abort parsing"); +end +end +if j then +i.StartDoctypeDecl=a; +end +i.Comment=a; +i.ProcessingInstruction=a; +local function a() +e,h,t=nil,{},0; +d={}; +end +local function e(t,e) +n=e; +end +return i,{reset=a,set_session=e}; +end +function new(n,i,o) +local e=0; +local t; +if a then +function t(t) +e=e-t; +end +o=o or q; +elseif o then +f("Stanza size limits are not supported on this version of LuaExpat") +end +local i,n=new_sax_handlers(n,i,t); +local t=p(i,d,false); +local s=t.parse; +return{ +reset=function() +t=p(i,d,false); +s=t.parse; +e=0; +n.reset(); +end, +feed=function(n,i) +if a then +e=e+#i; +end +local i,t=s(t,i); +if a and e>o then +return nil,"stanza-too-large"; +end +return i,t; +end, +set_session=n.set_session; +}; +end +return _M; +end) +package.preload['util.jid']=(function(...) +local t,i=string.match,string.sub; +local d=require"util.encodings".stringprep.nodeprep; +local r=require"util.encodings".stringprep.nameprep; +local h=require"util.encodings".stringprep.resourceprep; +local n={ +[" "]="\\20";['"']="\\22"; +["&"]="\\26";["'"]="\\27"; +["/"]="\\2f";[":"]="\\3a"; +["<"]="\\3c";[">"]="\\3e"; +["@"]="\\40";["\\"]="\\5c"; +}; +local s={}; +for t,e in pairs(n)do s[e]=t;end +module"jid" +local function a(e) +if not e then return;end +local i,a=t(e,"^([^@/]+)@()"); +local a,o=t(e,"^([^@/]+)()",a) +if i and not a then return nil,nil,nil;end +local t=t(e,"^/(.+)$",o); +if(not a)or((not t)and#e>=o)then return nil,nil,nil;end +return i,a,t; +end +split=a; +function bare(e) +local t,e=a(e); +if t and e then +return t.."@"..e; +end +return e; +end +local function o(e) +local a,e,t=a(e); +if e then +if i(e,-1,-1)=="."then +e=i(e,1,-2); +end +e=r(e); +if not e then return;end +if a then +a=d(a); +if not a then return;end +end +if t then +t=h(t); +if not t then return;end +end +return a,e,t; +end +end +prepped_split=o; +function prep(e) +local t,e,a=o(e); +if e then +if t then +e=t.."@"..e; +end +if a then +e=e.."/"..a; +end +end +return e; +end +function join(t,e,a) +if t and e and a then +return t.."@"..e.."/"..a; +elseif t and e then +return t.."@"..e; +elseif e and a then +return e.."/"..a; +elseif e then +return e; +end +return nil; +end +function compare(e,t) +local o,i,n=a(e); +local e,t,a=a(t); +if((e~=nil and e==o)or e==nil)and +((t~=nil and t==i)or t==nil)and +((a~=nil and a==n)or a==nil)then +return true +end +return false +end +function escape(e)return e and(e:gsub(".",n));end +function unescape(e)return e and(e:gsub("\\%x%x",s));end +return _M; +end) +package.preload['util.events']=(function(...) +local i=pairs; +local s=table.insert; +local o=table.sort; +local d=setmetatable; +local n=next; +module"events" +function new() +local t={}; +local e={}; +local function r(h,a) +local e=e[a]; +if not e or n(e)==nil then return;end +local t={}; +for e in i(e)do +s(t,e); +end +o(t,function(t,a)return e[t]>e[a];end); +h[a]=t; +return t; +end; +d(t,{__index=r}); +local function s(o,i,n) +local a=e[o]; +if a then +a[i]=n or 0; +else +a={[i]=n or 0}; +e[o]=a; +end +t[o]=nil; +end; +local function h(a,i) +local o=e[a]; +if o then +o[i]=nil; +t[a]=nil; +if n(o)==nil then +e[a]=nil; +end +end +end; +local function n(e) +for e,t in i(e)do +s(e,t); +end +end; +local function o(e) +for t,e in i(e)do +h(t,e); +end +end; +local function a(e,...) +local e=t[e]; +if e then +for t=1,#e do +local e=e[t](...); +if e~=nil then return e;end +end +end +end; +return{ +add_handler=s; +remove_handler=h; +add_handlers=n; +remove_handlers=o; +fire_event=a; +_handlers=t; +_event_map=e; +}; +end +return _M; +end) +package.preload['util.dataforms']=(function(...) +local a=setmetatable; +local e,i=pairs,ipairs; +local r,h,l=tostring,type,next; +local n=table.concat; +local c=require"util.stanza"; +local d=require"util.jid".prep; +module"dataforms" +local u='jabber:x:data'; +local s={}; +local e={__index=s}; +function new(t) +return a(t,e); +end +function from_stanza(e) +local o={ +title=e:get_child_text("title"); +instructions=e:get_child_text("instructions"); +}; +for e in e:childtags("field")do +local a={ +name=e.attr.var; +label=e.attr.label; +type=e.attr.type; +required=e:get_child("required")and true or nil; +value=e:get_child_text("value"); +}; +o[#o+1]=a; +if a.type then +local t={}; +if a.type:match"list%-"then +for e in e:childtags("option")do +t[#t+1]={label=e.attr.label,value=e:get_child_text("value")}; +end +for e in e:childtags("value")do +t[#t+1]={label=e.attr.label,value=e:get_text(),default=true}; +end +elseif a.type:match"%-multi"then +for e in e:childtags("value")do +t[#t+1]=e.attr.label and{label=e.attr.label,value=e:get_text()}or e:get_text(); +end +if a.type=="text-multi"then +a.value=n(t,"\n"); +else +a.value=t; +end +end +end +end +return new(o); +end +function s.form(t,a,e) +local e=c.stanza("x",{xmlns=u,type=e or"form"}); +if t.title then +e:tag("title"):text(t.title):up(); +end +if t.instructions then +e:tag("instructions"):text(t.instructions):up(); +end +for t,o in i(t)do +local t=o.type or"text-single"; +e:tag("field",{type=t,var=o.name,label=o.label}); +local a=(a and a[o.name])or o.value; +if a then +if t=="hidden"then +if h(a)=="table"then +e:tag("value") +:add_child(a) +:up(); +else +e:tag("value"):text(r(a)):up(); +end +elseif t=="boolean"then +e:tag("value"):text((a and"1")or"0"):up(); +elseif t=="fixed"then +elseif t=="jid-multi"then +for a,t in i(a)do +e:tag("value"):text(t):up(); +end +elseif t=="jid-single"then +e:tag("value"):text(a):up(); +elseif t=="text-single"or t=="text-private"then +e:tag("value"):text(a):up(); +elseif t=="text-multi"then +for t in a:gmatch("([^\r\n]+)\r?\n*")do +e:tag("value"):text(t):up(); +end +elseif t=="list-single"then +local o=false; +for a,t in i(a)do +if h(t)=="table"then +e:tag("option",{label=t.label}):tag("value"):text(t.value):up():up(); +if t.default and(not o)then +e:tag("value"):text(t.value):up(); +o=true; +end +else +e:tag("option",{label=t}):tag("value"):text(r(t)):up():up(); +end +end +elseif t=="list-multi"then +for a,t in i(a)do +if h(t)=="table"then +e:tag("option",{label=t.label}):tag("value"):text(t.value):up():up(); +if t.default then +e:tag("value"):text(t.value):up(); +end +else +e:tag("option",{label=t}):tag("value"):text(r(t)):up():up(); +end +end +end +end +if o.required then +e:tag("required"):up(); +end +e:up(); +end +return e; +end +local e={}; +function s.data(t,n) +local o={}; +local a={}; +for i,t in i(t)do +local i; +for e in n:childtags()do +if t.name==e.attr.var then +i=e; +break; +end +end +if not i then +if t.required then +a[t.name]="Required value missing"; +end +else +local e=e[t.type]; +if e then +o[t.name],a[t.name]=e(i,t.required); +end +end +end +if l(a)then +return o,a; +end +return o; +end +e["text-single"]= +function(t,a) +local t=t:get_child_text("value"); +if t and#t>0 then +return t +elseif a then +return nil,"Required value missing"; +end +end +e["text-private"]= +e["text-single"]; +e["jid-single"]= +function(t,o) +local a=t:get_child_text("value") +local t=d(a); +if t and#t>0 then +return t +elseif a then +return nil,"Invalid JID: "..a; +elseif o then +return nil,"Required value missing"; +end +end +e["jid-multi"]= +function(o,i) +local t={}; +local a={}; +for e in o:childtags("value")do +local e=e:get_text(); +local o=d(e); +t[#t+1]=o; +if e and not o then +a[#a+1]=("Invalid JID: "..e); +end +end +if#t>0 then +return t,(#a>0 and n(a,"\n")or nil); +elseif i then +return nil,"Required value missing"; +end +end +e["list-multi"]= +function(o,a) +local t={}; +for e in o:childtags("value")do +t[#t+1]=e:get_text(); +end +return t,(a and#t==0 and"Required value missing"or nil); +end +e["text-multi"]= +function(a,t) +local t,a=e["list-multi"](a,t); +if t then +t=n(t,"\n"); +end +return t,a; +end +e["list-single"]= +e["text-single"]; +local a={ +["1"]=true,["true"]=true, +["0"]=false,["false"]=false, +}; +e["boolean"]= +function(t,o) +local t=t:get_child_text("value"); +local a=a[t~=nil and t]; +if a~=nil then +return a; +elseif t then +return nil,"Invalid boolean representation"; +elseif o then +return nil,"Required value missing"; +end +end +e["hidden"]= +function(e) +return e:get_child_text("value"); +end +return _M; +end) +package.preload['util.caps']=(function(...) +local l=require"util.encodings".base64.encode; +local d=require"util.hashes".sha1; +local n,s,h=table.insert,table.sort,table.concat; +local r=ipairs; +module"caps" +function calculate_hash(e) +local i,o,a={},{},{}; +for t,e in r(e)do +if e.name=="identity"then +n(i,(e.attr.category or"").."\0"..(e.attr.type or"").."\0"..(e.attr["xml:lang"]or"").."\0"..(e.attr.name or"")); +elseif e.name=="feature"then +n(o,e.attr.var or""); +elseif e.name=="x"and e.attr.xmlns=="jabber:x:data"then +local t={}; +local o; +for a,e in r(e.tags)do +if e.name=="field"and e.attr.var then +local a={}; +for t,e in r(e.tags)do +e=#e.tags==0 and e:get_text(); +if e then n(a,e);end +end +s(a); +if e.attr.var=="FORM_TYPE"then +o=a[1]; +elseif#a>0 then +n(t,e.attr.var.."\0"..h(a,"<")); +else +n(t,e.attr.var); +end +end +end +s(t); +t=h(t,"<"); +if o then t=o.."\0"..t;end +n(a,t); +end +end +s(i); +s(o); +s(a); +if#i>0 then i=h(i,"<"):gsub("%z","/").."<";else i="";end +if#o>0 then o=h(o,"<").."<";else o="";end +if#a>0 then a=h(a,"<"):gsub("%z","<").."<";else a="";end +local e=i..o..a; +local t=l(d(e)); +return t,e; +end +return _M; +end) +package.preload['util.vcard']=(function(...) +local i=require"util.stanza"; +local a,r=table.insert,table.concat; +local s=type; +local e,n,c=next,pairs,ipairs; +local d,h,l,u; +local f="\n"; +local o; +local function e() +error"Not implemented" +end +local function e() +error"Not implemented" +end +local function w(e) +return e:gsub("[,:;\\]","\\%1"):gsub("\n","\\n"); +end +local function y(e) +return e:gsub("\\?[\\nt:;,]",{ +["\\\\"]="\\", +["\\n"]="\n", +["\\r"]="\r", +["\\t"]="\t", +["\\:"]=":", +["\\;"]=";", +["\\,"]=",", +[":"]="\29", +[";"]="\30", +[","]="\31", +}); +end +local function p(e) +local a=i.stanza(e.name,{xmlns="vcard-temp"}); +local t=o[e.name]; +if t=="text"then +a:text(e[1]); +elseif s(t)=="table"then +if t.types and e.TYPE then +if s(e.TYPE)=="table"then +for o,t in n(t.types)do +for o,e in n(e.TYPE)do +if e:upper()==t then +a:tag(t):up(); +break; +end +end +end +else +a:tag(e.TYPE:upper()):up(); +end +end +if t.props then +for o,t in n(t.props)do +if e[t]then +a:tag(t):up(); +end +end +end +if t.value then +a:tag(t.value):text(e[1]):up(); +elseif t.values then +local o=t.values; +local i=o.behaviour=="repeat-last"and o[#o]; +for o=1,#e do +a:tag(t.values[o]or i):text(e[o]):up(); +end +end +end +return a; +end +local function m(t) +local e=i.stanza("vCard",{xmlns="vcard-temp"}); +for a=1,#t do +e:add_child(p(t[a])); +end +return e; +end +function u(e) +if not e[1]or e[1].name then +return m(e) +else +local t=i.stanza("xCard",{xmlns="vcard-temp"}); +for a=1,#e do +t:add_child(m(e[a])); +end +return t; +end +end +function d(t) +t=t +:gsub("\r\n","\n") +:gsub("\n ","") +:gsub("\n\n+","\n"); +local s={}; +local e; +for t in t:gmatch("[^\n]+")do +local t=y(t); +local n,t,i=t:match("^([-%a]+)(\30?[^\29]*)\29(.*)$"); +i=i:gsub("\29",":"); +if#t>0 then +local o={}; +for a,n,i in t:gmatch("\30([^=]+)(=?)([^\30]*)")do +a=a:upper(); +local e={}; +for t in i:gmatch("[^\31]+")do +e[#e+1]=t +e[t]=true; +end +if n=="="then +o[a]=e; +else +o[a]=true; +end +end +t=o; +end +if n=="BEGIN"and i=="VCARD"then +e={}; +s[#s+1]=e; +elseif n=="END"and i=="VCARD"then +e=nil; +elseif e and o[n]then +local o=o[n]; +local n={name=n}; +e[#e+1]=n; +local s=e; +e=n; +if o.types then +for o,a in c(o.types)do +local a=a:lower(); +if(t.TYPE and t.TYPE[a]==true) +or t[a]==true then +e.TYPE=a; +end +end +end +if o.props then +for o,a in c(o.props)do +if t[a]then +if t[a]==true then +e[a]=true; +else +for o,t in c(t[a])do +e[a]=t; +end +end +end +end +end +if o=="text"or o.value then +a(e,i); +elseif o.values then +local t="\30"..i; +for t in t:gmatch("\30([^\30]*)")do +a(e,t); +end +end +e=s; +end +end +return s; +end +local function c(t) +local e={}; +for a=1,#t do +e[a]=w(t[a]); +end +e=r(e,";"); +local a=""; +for e,t in n(t)do +if s(e)=="string"and e~="name"then +a=a..(";%s=%s"):format(e,s(t)=="table"and r(t,",")or t); +end +end +return("%s%s:%s"):format(t.name,a,e) +end +local function i(t) +local e={}; +a(e,"BEGIN:VCARD") +for o=1,#t do +a(e,c(t[o])); +end +a(e,"END:VCARD") +return r(e,f); +end +function h(e) +if e[1]and e[1].name then +return i(e) +else +local t={}; +for a=1,#e do +t[a]=i(e[a]); +end +return r(t,f); +end +end +local function r(i) +local e=i.name; +local t=o[e]; +local e={name=e}; +if t=="text"then +e[1]=i:get_text(); +elseif s(t)=="table"then +if t.value then +e[1]=i:get_child_text(t.value)or""; +elseif t.values then +local t=t.values; +if t.behaviour=="repeat-last"then +for t=1,#i.tags do +a(e,i.tags[t]:get_text()or""); +end +else +for o=1,#t do +a(e,i:get_child_text(t[o])or""); +end +end +elseif t.names then +local t=t.names; +for a=1,#t do +if i:get_child(t[a])then +e[1]=t[a]; +break; +end +end +end +if t.props_verbatim then +for a,t in n(t.props_verbatim)do +e[a]=t; +end +end +if t.types then +local t=t.types; +e.TYPE={}; +for o=1,#t do +if i:get_child(t[o])then +a(e.TYPE,t[o]:lower()); +end +end +if#e.TYPE==0 then +e.TYPE=nil; +end +end +if t.props then +local t=t.props; +for o=1,#t do +local t=t[o] +local o=i:get_child_text(t); +if o then +e[t]=e[t]or{}; +a(e[t],o); +end +end +end +else +return nil +end +return e; +end +local function i(e) +local e=e.tags; +local t={}; +for o=1,#e do +a(t,r(e[o])); +end +return t +end +function l(e) +if e.attr.xmlns~="vcard-temp"then +return nil,"wrong-xmlns"; +end +if e.name=="xCard"then +local t={}; +local a=e.tags; +for e=1,#a do +t[e]=i(a[e]); +end +return t +elseif e.name=="vCard"then +return i(e) +end +end +o={ +VERSION="text", +FN="text", +N={ +values={ +"FAMILY", +"GIVEN", +"MIDDLE", +"PREFIX", +"SUFFIX", +}, +}, +NICKNAME="text", +PHOTO={ +props_verbatim={ENCODING={"b"}}, +props={"TYPE"}, +value="BINVAL", +}, +BDAY="text", +ADR={ +types={ +"HOME", +"WORK", +"POSTAL", +"PARCEL", +"DOM", +"INTL", +"PREF", +}, +values={ +"POBOX", +"EXTADD", +"STREET", +"LOCALITY", +"REGION", +"PCODE", +"CTRY", +} +}, +LABEL={ +types={ +"HOME", +"WORK", +"POSTAL", +"PARCEL", +"DOM", +"INTL", +"PREF", +}, +value="LINE", +}, +TEL={ +types={ +"HOME", +"WORK", +"VOICE", +"FAX", +"PAGER", +"MSG", +"CELL", +"VIDEO", +"BBS", +"MODEM", +"ISDN", +"PCS", +"PREF", +}, +value="NUMBER", +}, +EMAIL={ +types={ +"HOME", +"WORK", +"INTERNET", +"PREF", +"X400", +}, +value="USERID", +}, +JABBERID="text", +MAILER="text", +TZ="text", +GEO={ +values={ +"LAT", +"LON", +}, +}, +TITLE="text", +ROLE="text", +LOGO="copy of PHOTO", +AGENT="text", +ORG={ +values={ +behaviour="repeat-last", +"ORGNAME", +"ORGUNIT", +} +}, +CATEGORIES={ +values="KEYWORD", +}, +NOTE="text", +PRODID="text", +REV="text", +SORTSTRING="text", +SOUND="copy of PHOTO", +UID="text", +URL="text", +CLASS={ +names={ +"PUBLIC", +"PRIVATE", +"CONFIDENTIAL", +}, +}, +KEY={ +props={"TYPE"}, +value="CRED", +}, +DESC="text", +}; +o.LOGO=o.PHOTO; +o.SOUND=o.PHOTO; +return{ +from_text=d; +to_text=h; +from_xep54=l; +to_xep54=u; +lua_to_text=h; +lua_to_xep54=u; +text_to_lua=d; +text_to_xep54=function(...)return u(d(...));end; +xep54_to_lua=l; +xep54_to_text=function(...)return h(l(...))end; +}; +end) +package.preload['util.logger']=(function(...) +local e=pcall; +local e=string.find; +local e,n,e=ipairs,pairs,setmetatable; +module"logger" +local e={}; +local t; +function init(e) +local a=t(e,"debug"); +local i=t(e,"info"); +local o=t(e,"warn"); +local n=t(e,"error"); +return function(e,t,...) +if e=="debug"then +return a(t,...); +elseif e=="info"then +return i(t,...); +elseif e=="warn"then +return o(t,...); +elseif e=="error"then +return n(t,...); +end +end +end +function t(i,a) +local t=e[a]; +if not t then +t={}; +e[a]=t; +end +local e=function(o,...) +for e=1,#t do +t[e](i,a,o,...); +end +end +return e; +end +function reset() +for t,e in n(e)do +for t=1,#e do +e[t]=nil; +end +end +end +function add_level_sink(t,a) +if not e[t]then +e[t]={a}; +else +e[t][#e[t]+1]=a; +end +end +_M.new=t; +return _M; +end) +package.preload['util.datetime']=(function(...) +local e=os.date; +local i=os.time; +local u=os.difftime; +local t=error; +local l=tonumber; +module"datetime" +function date(t) +return e("!%Y-%m-%d",t); +end +function datetime(t) +return e("!%Y-%m-%dT%H:%M:%SZ",t); +end +function time(t) +return e("!%H:%M:%S",t); +end +function legacy(t) +return e("!%Y%m%dT%H:%M:%S",t); +end +function parse(t) +if t then +local n,r,s,h,d,a,o; +n,r,s,h,d,a,o=t:match("^(%d%d%d%d)%-?(%d%d)%-?(%d%d)T(%d%d):(%d%d):(%d%d)%.?%d*([Z+%-]?.*)$"); +if n then +local u=u(i(e("*t")),i(e("!*t"))); +local t=0; +if o~=""and o~="Z"then +local o,a,e=o:match("([+%-])(%d%d):?(%d*)"); +if not o then return;end +if#e~=2 then e="0";end +a,e=l(a),l(e); +t=a*60*60+e*60; +if o=="-"then t=-t;end +end +a=(a+u)-t; +return i({year=n,month=r,day=s,hour=h,min=d,sec=a,isdst=false}); +end +end +end +return _M; +end) +package.preload['util.sasl.scram']=(function(...) +local i,c=require"mime".b64,require"mime".unb64; +local a=require"crypto"; +local t=require"bit"; +local p=tonumber; +local o,e=string.char,string.byte; +local n=string.gsub; +local s=t.bxor; +local function m(t,a) +return(n(t,"()(.)",function(i,t) +return o(s(e(t),e(a,i))) +end)); +end +local function w(e) +return a.digest("sha1",e,true); +end +local t=a.hmac.digest; +local function e(e,a) +return t("sha1",a,e,true); +end +local function y(o,t,i) +local t=e(o,t.."\0\0\0\1"); +local a=t; +for i=2,i do +t=e(o,t); +a=m(a,t); +end +return a; +end +local function f(e) +return e; +end +local function t(e) +return(n(e,"[,=]",{[","]="=2C",["="]="=3D"})); +end +local function n(o,d) +local t="n="..t(o.username); +local n=i(a.rand.bytes(15)); +local r="r="..n; +local h=t..","..r; +local s=""; +local t=o.conn:ssl()and"y"or"n"; +if d=="SCRAM-SHA-1-PLUS"then +s=o.conn:socket():getfinished(); +t="p=tls-unique"; +end +local d=t..",,"; +local t=d..h; +local t,u=coroutine.yield(t); +if t~="challenge"then return false end +local a,t,l=u:match("(r=[^,]+),s=([^,]*),i=(%d+)"); +local l=p(l); +t=c(t); +if not a or not t or not l then +return false,"Could not parse server_first_message"; +elseif a:find(n,3,true)~=3 then +return false,"nonce sent by server does not match our nonce"; +elseif a==r then +return false,"server did not append s-nonce to nonce"; +end +local n=d..s; +local n="c="..i(n); +local n=n..","..a; +local o=y(f(o.password),t,l); +local t=e(o,"Client Key"); +local s=w(t); +local a=h..","..u..","..n; +local s=e(s,a); +local t=m(t,s); +local o=e(o,"Server Key"); +local e=e(o,a); +local t="p="..i(t); +local t=n..","..t; +local t,a=coroutine.yield(t); +if t~="success"then return false,"success-expected"end +local t=a:match("v=([^,]+)"); +if c(t)~=e then +return false,"server signature did not match"; +end +return true; +end +return function(e,t) +if e.username and(e.password or(e.client_key or e.server_key))then +if t=="SCRAM-SHA-1"then +return n,99; +elseif t=="SCRAM-SHA-1-PLUS"then +local e=e.conn:ssl()and e.conn:socket(); +if e and e.getfinished then +return n,100; +end +end +end +end +end) +package.preload['util.sasl.plain']=(function(...) +return function(e,t) +if t=="PLAIN"and e.username and e.password then +return function(e) +return"success"==coroutine.yield("\0"..e.username.."\0"..e.password); +end,5; +end +end +end) +package.preload['util.sasl.anonymous']=(function(...) +return function(t,e) +if e=="ANONYMOUS"then +return function() +return coroutine.yield()=="success"; +end,0; +end +end +end) +package.preload['verse.plugins.tls']=(function(...) +local a=require"verse"; +local t="urn:ietf:params:xml:ns:xmpp-tls"; +function a.plugins.tls(e) +local function i(o) +if e.authenticated then return;end +if o:get_child("starttls",t)and e.conn.starttls then +e:debug("Negotiating TLS..."); +e:send(a.stanza("starttls",{xmlns=t})); +return true; +elseif not e.conn.starttls and not e.secure then +e:warn("SSL libary (LuaSec) not loaded, so TLS not available"); +elseif not e.secure then +e:debug("Server doesn't offer TLS :("); +end +end +local function o(t) +if t.name=="proceed"then +e:debug("Server says proceed, handshake starting..."); +e.conn:starttls({mode="client",protocol="sslv23",options="no_sslv2"},true); +end +end +local function a(t) +if t=="ssl-handshake-complete"then +e.secure=true; +e:debug("Re-opening stream..."); +e:reopen(); +end +end +e:hook("stream-features",i,400); +e:hook("stream/"..t,o); +e:hook("status",a,400); +return true; +end +end) +package.preload['verse.plugins.sasl']=(function(...) +local n,h=require"mime".b64,require"mime".unb64; +local a="urn:ietf:params:xml:ns:xmpp-sasl"; +function verse.plugins.sasl(e) +local function s(t) +if e.authenticated then return;end +e:debug("Authenticating with SASL..."); +local t=t:get_child("mechanisms",a); +if not t then return end +local o={}; +local i={}; +for t in t:childtags("mechanism")do +t=t:get_text(); +e:debug("Server offers %s",t); +if not o[t]then +local n=t:match("[^-]+"); +local s,a=pcall(require,"util.sasl."..n:lower()); +if s then +e:debug("Loaded SASL %s module",n); +o[t],i[t]=a(e,t); +elseif not tostring(a):match("not found")then +e:debug("Loading failed: %s",tostring(a)); +end +end +end +local t={}; +for e in pairs(o)do +table.insert(t,e); +end +if not t[1]then +e:event("authentication-failure",{condition="no-supported-sasl-mechanisms"}); +e:close(); +return; +end +table.sort(t,function(e,t)return i[e]>i[t];end); +local t,i=t[1]; +e:debug("Selecting %s mechanism...",t); +e.sasl_mechanism=coroutine.wrap(o[t]); +i=e:sasl_mechanism(t); +local t=verse.stanza("auth",{xmlns=a,mechanism=t}); +if i then +t:text(n(i)); +end +e:send(t); +return true; +end +local function o(t) +if t.name=="failure"then +local a=t.tags[1]; +local t=t:get_child_text("text"); +e:event("authentication-failure",{condition=a.name,text=t}); +e:close(); +return false; +end +local t,o=e.sasl_mechanism(t.name,h(t:get_text())); +if not t then +e:event("authentication-failure",{condition=o}); +e:close(); +return false; +elseif t==true then +e:event("authentication-success"); +e.authenticated=true +e:reopen(); +else +e:send(verse.stanza("response",{xmlns=a}):text(n(t))); +end +return true; +end +e:hook("stream-features",s,300); +e:hook("stream/"..a,o); +return true; +end +end) +package.preload['verse.plugins.bind']=(function(...) +local t=require"verse"; +local i=require"util.jid"; +local a="urn:ietf:params:xml:ns:xmpp-bind"; +function t.plugins.bind(e) +local function o(o) +if e.bound then return;end +e:debug("Binding resource..."); +e:send_iq(t.iq({type="set"}):tag("bind",{xmlns=a}):tag("resource"):text(e.resource), +function(t) +if t.attr.type=="result"then +local t=t +:get_child("bind",a) +:get_child_text("jid"); +e.username,e.host,e.resource=i.split(t); +e.jid,e.bound=t,true; +e:event("bind-success",{jid=t}); +elseif t.attr.type=="error"then +local a=t:child_with_name("error"); +local o,t,a=t:get_error(); +e:event("bind-failure",{error=t,text=a,type=o}); +end +end); +end +e:hook("stream-features",o,200); +return true; +end +end) +package.preload['verse.plugins.session']=(function(...) +local o=require"verse"; +local t="urn:ietf:params:xml:ns:xmpp-session"; +function o.plugins.session(e) +local function n(a) +local a=a:get_child("session",t); +if a and not a:get_child("optional")then +local function i(a) +e:debug("Establishing Session..."); +e:send_iq(o.iq({type="set"}):tag("session",{xmlns=t}), +function(t) +if t.attr.type=="result"then +e:event("session-success"); +elseif t.attr.type=="error"then +local a=t:child_with_name("error"); +local o,t,a=t:get_error(); +e:event("session-failure",{error=t,text=a,type=o}); +end +end); +return true; +end +e:hook("bind-success",i); +end +end +e:hook("stream-features",n); +return true; +end +end) +package.preload['verse.plugins.legacy']=(function(...) +local i=require"verse"; +local n=require"util.uuid".generate; +local o="jabber:iq:auth"; +function i.plugins.legacy(e) +function handle_auth_form(t) +local a=t:get_child("query",o); +if t.attr.type~="result"or not a then +local a,o,t=t:get_error(); +e:debug("warn","%s %s: %s",a,o,t); +end +local t={ +username=e.username; +password=e.password; +resource=e.resource or n(); +digest=false,sequence=false,token=false; +}; +local o=i.iq({to=e.host,type="set"}) +:tag("query",{xmlns=o}); +if#a>0 then +for a in a:childtags()do +local a=a.name; +local i=t[a]; +if i then +o:tag(a):text(t[a]):up(); +elseif i==nil then +local t="feature-not-implemented"; +e:event("authentication-failure",{condition=t}); +return false; +end +end +else +for t,e in pairs(t)do +if e then +o:tag(t):text(e):up(); +end +end +end +e:send_iq(o,function(a) +if a.attr.type=="result"then +e.resource=t.resource; +e.jid=t.username.."@"..e.host.."/"..t.resource; +e:event("authentication-success"); +e:event("bind-success",e.jid); +else +local a,t,a=a:get_error(); +e:event("authentication-failure",{condition=t}); +end +end); +end +function handle_opened(t) +if not t.version then +e:send_iq(i.iq({type="get"}) +:tag("query",{xmlns="jabber:iq:auth"}) +:tag("username"):text(e.username), +handle_auth_form); +end +end +e:hook("opened",handle_opened); +end +end) +package.preload['verse.plugins.compression']=(function(...) +local a=require"verse"; +local o=require"zlib"; +local e="http://jabber.org/features/compress" +local t="http://jabber.org/protocol/compress" +local e="http://etherx.jabber.org/streams"; +local i=9; +local function s(e) +local i,o=pcall(o.deflate,i); +if i==false then +local t=a.stanza("failure",{xmlns=t}):tag("setup-failed"); +e:send(t); +e:error("Failed to create zlib.deflate filter: %s",tostring(o)); +return +end +return o +end +local function h(e) +local i,o=pcall(o.inflate); +if i==false then +local t=a.stanza("failure",{xmlns=t}):tag("setup-failed"); +e:send(t); +e:error("Failed to create zlib.inflate filter: %s",tostring(o)); +return +end +return o +end +local function r(e,i) +function e:send(o) +local i,o,n=pcall(i,tostring(o),'sync'); +if i==false then +e:close({ +condition="undefined-condition"; +text=o; +extra=a.stanza("failure",{xmlns=t}):tag("processing-failed"); +}); +e:warn("Compressed send failed: %s",tostring(o)); +return; +end +e.conn:write(o); +end; +end +local function d(e,o) +local n=e.data +e.data=function(s,i) +e:debug("Decompressing data..."); +local i,o,h=pcall(o,i); +if i==false then +e:close({ +condition="undefined-condition"; +text=o; +extra=a.stanza("failure",{xmlns=t}):tag("processing-failed"); +}); +stream:warn("%s",tostring(o)); +return; +end +return n(s,o); +end; +end +function a.plugins.compression(e) +local function n(o) +if not e.compressed then +local o=o:child_with_name("compression"); +if o then +for o in o:children()do +local o=o[1] +if o=="zlib"then +e:send(a.stanza("compress",{xmlns=t}):tag("method"):text("zlib")) +e:debug("Enabled compression using zlib.") +return true; +end +end +session:debug("Remote server supports no compression algorithm we support.") +end +end +end +local function i(o) +if o.name=="compressed"then +e:debug("Activating compression...") +local a=s(e); +if not a then return end +local t=h(e); +if not t then return end +r(e,a); +d(e,t); +e.compressed=true; +e:reopen(); +elseif o.name=="failure"then +e:warn("Failed to establish compression"); +end +end +e:hook("stream-features",n,250); +e:hook("stream/"..t,i); +end +end) +package.preload['verse.plugins.smacks']=(function(...) +local i=require"verse"; +local h=socket.gettime; +local n="urn:xmpp:sm:2"; +function i.plugins.smacks(e) +local t={}; +local a=0; +local r=h(); +local o; +local s=0; +local function d(t) +if t.attr.xmlns=="jabber:client"or not t.attr.xmlns then +s=s+1; +e:debug("Increasing handled stanzas to %d for %s",s,t:top_tag()); +end +end +function outgoing_stanza(a) +if a.name and not a.attr.xmlns then +t[#t+1]=tostring(a); +r=h(); +if not o then +o=true; +e:debug("Waiting to send ack request..."); +i.add_task(1,function() +if#t==0 then +o=false; +return; +end +local a=h()-r; +if a<1 and#t<10 then +return 1-a; +end +e:debug("Time up, sending ..."); +o=false; +e:send(i.stanza("r",{xmlns=n})); +end); +end +end +end +local function h() +e:debug("smacks: connection lost"); +e.stream_management_supported=nil; +if e.resumption_token then +e:debug("smacks: have resumption token, reconnecting in 1s..."); +e.authenticated=nil; +i.add_task(1,function() +e:connect(e.connect_host or e.host,e.connect_port or 5222); +end); +return true; +end +end +local function r() +e.resumption_token=nil; +e:unhook("disconnected",h); +end +local function l(o) +if o.name=="r"then +e:debug("Ack requested... acking %d handled stanzas",s); +e:send(i.stanza("a",{xmlns=n,h=tostring(s)})); +elseif o.name=="a"then +local o=tonumber(o.attr.h); +if o>a then +local i=#t; +for a=a+1,o do +table.remove(t,1); +end +e:debug("Received ack: New ack: "..o.." Last ack: "..a.." Unacked stanzas now: "..#t.." (was "..i..")"); +a=o; +else +e:warn("Received bad ack for "..o.." when last ack was "..a); +end +elseif o.name=="enabled"then +if o.attr.id then +e.resumption_token=o.attr.id; +e:hook("closed",r,100); +e:hook("disconnected",h,100); +end +elseif o.name=="resumed"then +local o=tonumber(o.attr.h); +if o>a then +local i=#t; +for a=a+1,o do +table.remove(t,1); +end +e:debug("Received ack: New ack: "..o.." Last ack: "..a.." Unacked stanzas now: "..#t.." (was "..i..")"); +a=o; +end +for a=1,#t do +e:send(t[a]); +end +t={}; +e:debug("Resumed successfully"); +e:event("resumed"); +else +e:warn("Don't know how to handle "..n.."/"..o.name); +end +end +local function a() +if not e.smacks then +e:debug("smacks: sending enable"); +e:send(i.stanza("enable",{xmlns=n,resume="true"})); +e.smacks=true; +e:hook("stanza",d); +e:hook("outgoing",outgoing_stanza); +end +end +local function t(t) +if t:get_child("sm",n)then +e.stream_management_supported=true; +if e.smacks and e.bound then +e:debug("Resuming stream with %d handled stanzas",s); +e:send(i.stanza("resume",{xmlns=n, +h=s,previd=e.resumption_token})); +return true; +else +e:hook("bind-success",a,1); +end +end +end +e:hook("stream-features",t,250); +e:hook("stream/"..n,l); +end +end) +package.preload['verse.plugins.keepalive']=(function(...) +local t=require"verse"; +function t.plugins.keepalive(e) +e.keepalive_timeout=e.keepalive_timeout or 300; +t.add_task(e.keepalive_timeout,function() +e.conn:write(" "); +return e.keepalive_timeout; +end); +end +end) +package.preload['verse.plugins.disco']=(function(...) +local a=require"verse"; +local h=require("mime").b64; +local n=require("util.sha1").sha1; +local s="http://jabber.org/protocol/caps"; +local e="http://jabber.org/protocol/disco"; +local o=e.."#info"; +local i=e.."#items"; +function a.plugins.disco(e) +e:add_plugin("presence"); +local t={ +__index=function(a,e) +local t={identities={},features={}}; +if e=="identities"or e=="features"then +return a[false][e] +end +a[e]=t; +return t; +end, +}; +local r={ +__index=function(t,a) +local e={}; +t[a]=e; +return e; +end, +}; +e.disco={ +cache={}, +info=setmetatable({ +[false]={ +identities={ +{category='client',type='pc',name='Verse'}, +}, +features={ +[s]=true, +[o]=true, +[i]=true, +}, +}, +},t); +items=setmetatable({[false]={}},r); +}; +e.caps={} +e.caps.node='http://code.matthewwild.co.uk/verse/' +local function d(t,e) +if t.category0 then +self.connecting_peer_candidates=true; +local function n(e,t) +self.jingle:send_command("transport-info",a.stanza("content",{creator=self.creator,name=self.name}) +:tag("transport",{xmlns=o,sid=self.s5b_sid}) +:tag("candidate-used",{cid=e.cid})); +self.onconnect_callback=i; +self.conn=t; +end +local e=s(self.s5b_sid..self.peer..e.jid,true); +h(n,t,e); +else +e:warn("Actually, I'm going to wait for my peer to tell me its streamhost..."); +self.onconnect_callback=i; +end +end +function t:info_received(t) +e:warn("Info received"); +local n=t:child_with_name("content"); +local i=n:child_with_name("transport"); +if i:get_child("candidate-used")and not self.connecting_peer_candidates then +local t=i:child_with_name("candidate-used"); +if t then +local function d(i,e) +if self.jingle.role=="initiator"then +self.jingle.stream:send_iq(a.iq({to=i.jid,type="set"}) +:tag("query",{xmlns=r,sid=self.s5b_sid}) +:tag("activate"):text(self.jingle.peer),function(i) +if i.attr.type=="result"then +self.jingle:send_command("transport-info",a.stanza("content",n.attr) +:tag("transport",{xmlns=o,sid=self.s5b_sid}) +:tag("activated",{cid=t.attr.cid})); +self.conn=e; +self.onconnect_callback(e); +else +self.jingle.stream:error("Failed to activate bytestream"); +end +end); +end +end +self.jingle.stream:debug("CID: %s",self.jingle.stream.proxy65.available_streamhosts[t.attr.cid]); +local t={ +self.jingle.stream.proxy65.available_streamhosts[t.attr.cid]; +}; +local e=s(self.s5b_sid..e.jid..self.peer,true); +h(d,t,e); +end +elseif i:get_child("activated")then +self.onconnect_callback(self.conn); +end +end +function t:disconnect() +if self.conn then +self.conn:close(); +end +end +function t:handle_accepted(e) +end +local t={__index=t}; +e:hook("jingle/transport/"..o,function(e) +return setmetatable({ +role=e.role, +peer=e.peer, +stream=e.stream, +jingle=e, +},t); +end); +end +end) +package.preload['verse.plugins.proxy65']=(function(...) +local e=require"util.events"; +local r=require"util.uuid"; +local h=require"util.sha1"; +local i={}; +i.__index=i; +local o="http://jabber.org/protocol/bytestreams"; +local n; +function verse.plugins.proxy65(t) +t.proxy65=setmetatable({stream=t},i); +t.proxy65.available_streamhosts={}; +local e=0; +t:hook("disco/service-discovered/proxy",function(a) +if a.type=="bytestreams"then +e=e+1; +t:send_iq(verse.iq({to=a.jid,type="get"}) +:tag("query",{xmlns=o}),function(a) +e=e-1; +if a.attr.type=="result"then +local e=a:get_child("query",o) +:get_child("streamhost").attr; +t.proxy65.available_streamhosts[e.jid]={ +jid=e.jid; +host=e.host; +port=tonumber(e.port); +}; +end +if e==0 then +t:event("proxy65/discovered-proxies",t.proxy65.available_streamhosts); +end +end); +end +end); +t:hook("iq/"..o,function(a) +local e=verse.new(nil,{ +initiator_jid=a.attr.from, +streamhosts={}, +current_host=0; +}); +for t in a.tags[1]:childtags()do +if t.name=="streamhost"then +table.insert(e.streamhosts,t.attr); +end +end +local function o() +if e.current_host<#e.streamhosts then +e.current_host=e.current_host+1; +e:connect( +e.streamhosts[e.current_host].host, +e.streamhosts[e.current_host].port +); +n(t,e,a.tags[1].attr.sid,a.attr.from,t.jid); +return true; +end +e:unhook("disconnected",o); +t:send(verse.error_reply(a,"cancel","item-not-found")); +end +function e:accept() +e:hook("disconnected",o,100); +e:hook("connected",function() +e:unhook("disconnected",o); +local e=verse.reply(a) +:tag("query",a.tags[1].attr) +:tag("streamhost-used",{jid=e.streamhosts[e.current_host].jid}); +t:send(e); +end,100); +o(); +end +function e:refuse() +end +t:event("proxy65/request",e); +end); +end +function i:new(t,s) +local e=verse.new(nil,{ +target_jid=t; +bytestream_sid=r.generate(); +}); +local a=verse.iq{type="set",to=t} +:tag("query",{xmlns=o,mode="tcp",sid=e.bytestream_sid}); +for t,e in ipairs(s or self.proxies)do +a:tag("streamhost",e):up(); +end +self.stream:send_iq(a,function(a) +if a.attr.type=="error"then +local t,a,o=a:get_error(); +e:event("connection-failed",{conn=e,type=t,condition=a,text=o}); +else +local a=a.tags[1]:get_child("streamhost-used"); +if not a then +end +e.streamhost_jid=a.attr.jid; +local a,i; +for o,t in ipairs(s or self.proxies)do +if t.jid==e.streamhost_jid then +a,i=t.host,t.port; +break; +end +end +if not(a and i)then +end +e:connect(a,i); +local function a() +e:unhook("connected",a); +local t=verse.iq{to=e.streamhost_jid,type="set"} +:tag("query",{xmlns=o,sid=e.bytestream_sid}) +:tag("activate"):text(t); +self.stream:send_iq(t,function(t) +if t.attr.type=="result"then +e:event("connected",e); +else +end +end); +return true; +end +e:hook("connected",a,100); +n(self.stream,e,e.bytestream_sid,self.stream.jid,t); +end +end); +return e; +end +function n(i,e,o,a,t) +local a=h.sha1(o..a..t); +local function t() +e:unhook("connected",t); +return true; +end +local function n(t) +e:unhook("incoming-raw",n); +if t:sub(1,2)~="\005\000"then +return e:event("error","connection-failure"); +end +e:event("connected"); +return true; +end +local function o(i) +e:unhook("incoming-raw",o); +if i~="\005\000"then +local t="version-mismatch"; +if i:sub(1,1)=="\005"then +t="authentication-failure"; +end +return e:event("error",t); +end +e:send(string.char(5,1,0,3,#a)..a.."\0\0"); +e:hook("incoming-raw",n,100); +return true; +end +e:hook("connected",t,200); +e:hook("incoming-raw",o,100); +e:send("\005\001\000"); +end +end) +package.preload['verse.plugins.jingle_ibb']=(function(...) +local e=require"verse"; +local i=require"util.encodings".base64; +local s=require"util.uuid".generate; +local n="urn:xmpp:jingle:transports:ibb:1"; +local o="http://jabber.org/protocol/ibb"; +assert(i.encode("This is a test.")=="VGhpcyBpcyBhIHRlc3Qu","Base64 encoding failed"); +assert(i.decode("VGhpcyBpcyBhIHRlc3Qu")=="This is a test.","Base64 decoding failed"); +local t=table.concat +local a={}; +local t={__index=a}; +local function h(a) +local t=setmetatable({stream=a},t) +t=e.eventable(t); +return t; +end +function a:initiate(e,a,t) +self.block=2048; +self.stanza=t or'iq'; +self.peer=e; +self.sid=a or tostring(self):match("%x+$"); +self.iseq=0; +self.oseq=0; +local e=function(e) +return self:feed(e) +end +self.feeder=e; +print("Hooking incomming IQs"); +local a=self.stream; +a:hook("iq/"..o,e) +if t=="message"then +a:hook("message",e) +end +end +function a:open(t) +self.stream:send_iq(e.iq{to=self.peer,type="set"} +:tag("open",{ +xmlns=o, +["block-size"]=self.block, +sid=self.sid, +stanza=self.stanza +}) +,function(e) +if t then +if e.attr.type~="error"then +t(true) +else +t(false,e:get_error()) +end +end +end); +end +function a:send(n) +local a=self.stanza; +local t; +if a=="iq"then +t=e.iq{type="set",to=self.peer} +elseif a=="message"then +t=e.message{to=self.peer} +end +local e=self.oseq; +self.oseq=e+1; +t:tag("data",{xmlns=o,sid=self.sid,seq=e}) +:text(i.encode(n)); +if a=="iq"then +self.stream:send_iq(t,function(e) +self:event(e.attr.type=="result"and"drained"or"error"); +end) +else +stream:send(t) +self:event("drained"); +end +end +function a:feed(t) +if t.attr.from~=self.peer then return end +local a=t[1]; +if a.attr.sid~=self.sid then return end +local n; +if a.name=="open"then +self:event("connected"); +self.stream:send(e.reply(t)) +return true +elseif a.name=="data"then +local o=t:get_child_text("data",o); +local a=tonumber(a.attr.seq); +local n=self.iseq; +if o and a then +if a~=n then +self.stream:send(e.error_reply(t,"cancel","not-acceptable","Wrong sequence. Packet lost?")) +self:close(); +self:event("error"); +return true; +end +self.iseq=a+1; +local a=i.decode(o); +if self.stanza=="iq"then +self.stream:send(e.reply(t)) +end +self:event("incoming-raw",a); +return true; +end +elseif a.name=="close"then +self.stream:send(e.reply(t)) +self:close(); +return true +end +end +function a:close() +self.stream:unhook("iq/"..o,self.feeder) +self:event("disconnected"); +end +function e.plugins.jingle_ibb(a) +a:hook("ready",function() +a:add_disco_feature(n); +end,10); +local t={}; +function t:_setup() +local e=h(self.stream); +e.sid=self.sid or e.sid; +e.stanza=self.stanza or e.stanza; +e.block=self.block or e.block; +e:initiate(self.peer,self.sid,self.stanza); +self.conn=e; +end +function t:generate_initiate() +print("ibb:generate_initiate() as "..self.role); +local t=s(); +self.sid=t; +self.stanza='iq'; +self.block=2048; +local e=e.stanza("transport",{xmlns=n, +sid=self.sid,stanza=self.stanza,["block-size"]=self.block}); +return e; +end +function t:generate_accept(t) +print("ibb:generate_accept() as "..self.role); +local e=t.attr; +self.sid=e.sid or self.sid; +self.stanza=e.stanza or self.stanza; +self.block=e["block-size"]or self.block; +self:_setup(); +return t; +end +function t:connect(t) +if not self.conn then +self:_setup(); +end +local e=self.conn; +print("ibb:connect() as "..self.role); +if self.role=="initiator"then +e:open(function(a,...) +assert(a,table.concat({...},", ")); +t(e); +end); +else +t(e); +end +end +function t:info_received(e) +print("ibb:info_received()"); +end +function t:disconnect() +if self.conn then +self.conn:close() +end +end +function t:handle_accepted(e)end +local t={__index=t}; +a:hook("jingle/transport/"..n,function(e) +return setmetatable({ +role=e.role, +peer=e.peer, +stream=e.stream, +jingle=e, +},t); +end); +end +end) +package.preload['verse.plugins.pubsub']=(function(...) +local h=require"verse"; +local e=require"util.jid".bare; +local s=table.insert; +local o="http://jabber.org/protocol/pubsub"; +local n="http://jabber.org/protocol/pubsub#owner"; +local a="http://jabber.org/protocol/pubsub#event"; +local e="http://jabber.org/protocol/pubsub#errors"; +local e={}; +local i={__index=e}; +function h.plugins.pubsub(e) +e.pubsub=setmetatable({stream=e},i); +e:hook("message",function(t) +local o=t.attr.from; +for t in t:childtags("event",a)do +local t=t:get_child("items"); +if t then +local a=t.attr.node; +for t in t:childtags("item")do +e:event("pubsub/event",{ +from=o; +node=a; +item=t; +}); +end +end +end +end); +return true; +end +function e:create(t,e,a) +return self:service(t):node(e):create(nil,a); +end +function e:subscribe(e,t,a,o) +return self:service(e):node(t):subscribe(a,nil,o); +end +function e:publish(a,e,i,o,t) +return self:service(a):node(e):publish(i,nil,o,t); +end +local a={}; +local t={__index=a}; +function e:service(e) +return setmetatable({stream=self.stream,service=e},t) +end +local function t(r,e,s,a,i,n,t) +local e=h.iq{type=r or"get",to=e} +:tag("pubsub",{xmlns=s or o}) +if a then e:tag(a,{node=i,jid=n});end +if t then e:tag("item",{id=t~=true and t or nil});end +return e; +end +function a:subscriptions(a) +self.stream:send_iq(t(nil,self.service,nil,"subscriptions") +,a and function(e) +if e.attr.type=="result"then +local e=e:get_child("pubsub",o); +local e=e and e:get_child("subscriptions"); +local o={}; +if e then +for e in e:childtags("subscription")do +local t=self:node(e.attr.node) +t.subscription=e; +t.subscribed_jid=e.attr.jid; +s(o,t); +end +end +a(o); +else +a(false,e:get_error()); +end +end or nil); +end +function a:affiliations(a) +self.stream:send_iq(t(nil,self.service,nil,"affiliations") +,a and function(e) +if e.attr.type=="result"then +local e=e:get_child("pubsub",o); +local e=e and e:get_child("affiliations")or{}; +local o={}; +if e then +for t in e:childtags("affiliation")do +local e=self:node(t.attr.node) +e.affiliation=t; +s(o,e); +end +end +a(o); +else +a(false,e:get_error()); +end +end or nil); +end +function a:nodes(a) +self.stream:disco_items(self.service,nil,function(e,...) +if e then +for t=1,#e do +e[t]=self:node(e[t].node); +end +end +a(e,...) +end); +end +local e={}; +local o={__index=e}; +function a:node(e) +return setmetatable({stream=self.stream,service=self.service,node=e},o) +end +function i:__call(t,e) +local t=self:service(t); +return e and t:node(e)or t; +end +function e:hook(a,o) +self._hooks=self._hooks or setmetatable({},{__mode='kv'}); +local function t(e) +if(not e.service or e.from==self.service)and e.node==self.node then +return a(e) +end +end +self._hooks[a]=t; +self.stream:hook("pubsub/event",t,o); +return t; +end +function e:unhook(e) +if e then +local e=self._hooks[e]; +self.stream:unhook("pubsub/event",e); +elseif self._hooks then +for e in pairs(self._hooks)do +self.stream:unhook("pubsub/event",e); +end +end +end +function e:create(a,e) +if a~=nil then +error("Not implemented yet."); +else +self.stream:send_iq(t("set",self.service,nil,"create",self.node),e); +end +end +function e:configure(e,a) +if e~=nil then +error("Not implemented yet."); +end +self.stream:send_iq(t("set",self.service,nil,e==nil and"default"or"configure",self.node),a); +end +function e:publish(i,o,e,a) +if o~=nil then +error("Node configuration is not implemented yet."); +end +self.stream:send_iq(t("set",self.service,nil,"publish",self.node,nil,i or true) +:add_child(e) +,a); +end +function e:subscribe(e,o,a) +e=e or self.stream.jid; +if o~=nil then +error("Subscription configuration is not implemented yet."); +end +self.stream:send_iq(t("set",self.service,nil,"subscribe",self.node,e,id) +,a); +end +function e:subscription(e) +error("Not implemented yet."); +end +function e:affiliation(e) +error("Not implemented yet."); +end +function e:unsubscribe(e,a) +e=e or self.subscribed_jid or self.stream.jid; +self.stream:send_iq(t("set",self.service,nil,"unsubscribe",self.node,e) +,a); +end +function e:configure_subscription(e,e) +error("Not implemented yet."); +end +function e:items(a,e) +if a then +self.stream:send_iq(t("get",self.service,nil,"items",self.node) +,e); +else +self.stream:disco_items(self.service,self.node,e); +end +end +function e:item(e,a) +self.stream:send_iq(t("get",self.service,nil,"items",self.node,nil,e) +,a); +end +function e:retract(a,e) +self.stream:send_iq(t("set",self.service,nil,"retract",self.node,nil,a) +,e); +end +function e:purge(e,a) +assert(not e,"Not implemented yet."); +self.stream:send_iq(t("set",self.service,n,"purge",self.node) +,a); +end +function e:delete(e,a) +assert(not e,"Not implemented yet."); +self.stream:send_iq(t("set",self.service,n,"delete",self.node) +,a); +end +end) +package.preload['verse.plugins.pep']=(function(...) +local e=require"verse"; +local t="http://jabber.org/protocol/pubsub"; +local t=t.."#event"; +function e.plugins.pep(e) +e:add_plugin("disco"); +e:add_plugin("pubsub"); +e.pep={}; +e:hook("pubsub/event",function(t) +return e:event("pep/"..t.node,{from=t.from,item=t.item.tags[1]}); +end); +function e:hook_pep(t,o,i) +local a=e.events._handlers["pep/"..t]; +if not(a)or#a==0 then +e:add_disco_feature(t.."+notify"); +end +e:hook("pep/"..t,o,i); +end +function e:unhook_pep(t,a) +e:unhook("pep/"..t,a); +local a=e.events._handlers["pep/"..t]; +if not(a)or#a==0 then +e:remove_disco_feature(t.."+notify"); +end +end +function e:publish_pep(t,a) +return e.pubsub:service(nil):node(a or t.attr.xmlns):publish(nil,nil,t) +end +end +end) +package.preload['verse.plugins.adhoc']=(function(...) +local o=require"verse"; +local n=require"lib.adhoc"; +local t="http://jabber.org/protocol/commands"; +local h="jabber:x:data"; +local a={}; +a.__index=a; +local i={}; +function o.plugins.adhoc(e) +e:add_plugin("disco"); +e:add_disco_feature(t); +function e:query_commands(a,o) +e:disco_items(a,t,function(t) +e:debug("adhoc list returned") +local a={}; +for o,t in ipairs(t)do +a[t.node]=t.name; +end +e:debug("adhoc calling callback") +return o(a); +end); +end +function e:execute_command(t,o,i) +local e=setmetatable({ +stream=e,jid=t, +command=o,callback=i +},a); +return e:execute(); +end +local function s(t,e) +if not(e)or e=="user"then return true;end +if type(e)=="function"then +return e(t); +end +end +function e:add_adhoc_command(o,a,s,h) +i[a]=n.new(o,a,s,h); +e:add_disco_item({jid=e.jid,node=a,name=o},t); +return i[a]; +end +local function h(a) +local t=a.tags[1]; +local t=t.attr.node; +local t=i[t]; +if not t then return;end +if not s(a.attr.from,t.permission)then +e:send(o.error_reply(a,"auth","forbidden","You don't have permission to execute this command"):up() +:add_child(t:cmdtag("canceled") +:tag("note",{type="error"}):text("You don't have permission to execute this command"))); +return true +end +return n.handle_cmd(t,{send=function(t)return e:send(t)end},a); +end +e:hook("iq/"..t,function(e) +local t=e.attr.type; +local a=e.tags[1].name; +if t=="set"and a=="command"then +return h(e); +end +end); +end +function a:_process_response(e) +if e.attr.type=="error"then +self.status="canceled"; +self.callback(self,{}); +return; +end +local e=e:get_child("command",t); +self.status=e.attr.status; +self.sessionid=e.attr.sessionid; +self.form=e:get_child("x",h); +self.note=e:get_child("note"); +self.callback(self); +end +function a:execute() +local e=o.iq({to=self.jid,type="set"}) +:tag("command",{xmlns=t,node=self.command}); +self.stream:send_iq(e,function(e) +self:_process_response(e); +end); +end +function a:next(a) +local e=o.iq({to=self.jid,type="set"}) +:tag("command",{ +xmlns=t, +node=self.command, +sessionid=self.sessionid +}); +if a then e:add_child(a);end +self.stream:send_iq(e,function(e) +self:_process_response(e); +end); +end +end) +package.preload['verse.plugins.presence']=(function(...) +local a=require"verse"; +function a.plugins.presence(e) +e.last_presence=nil; +e:hook("presence-out",function(t) +if not t.attr.to then +e.last_presence=t; +end +end,1); +function e:resend_presence() +if last_presence then +e:send(last_presence); +end +end +function e:set_status(t) +local a=a.presence(); +if type(t)=="table"then +if t.show then +a:tag("show"):text(t.show):up(); +end +if t.prio then +a:tag("priority"):text(tostring(t.prio)):up(); +end +if t.msg then +a:tag("status"):text(t.msg):up(); +end +end +e:send(a); +end +end +end) +package.preload['verse.plugins.private']=(function(...) +local o=require"verse"; +local a="jabber:iq:private"; +function o.plugins.private(i) +function i:private_set(i,n,e,s) +local t=o.iq({type="set"}) +:tag("query",{xmlns=a}); +if e then +if e.name==i and e.attr and e.attr.xmlns==n then +t:add_child(e); +else +t:tag(i,{xmlns=n}) +:add_child(e); +end +end +self:send_iq(t,s); +end +function i:private_get(e,t,i) +self:send_iq(o.iq({type="get"}) +:tag("query",{xmlns=a}) +:tag(e,{xmlns=t}), +function(o) +if o.attr.type=="result"then +local a=o:get_child("query",a); +local e=a:get_child(e,t); +i(e); +end +end); +end +end +end) +package.preload['verse.plugins.roster']=(function(...) +local o=require"verse"; +local r=require"util.jid".bare; +local a="jabber:iq:roster"; +local i="urn:xmpp:features:rosterver"; +local n=table.insert; +function o.plugins.roster(t) +local s=false; +local e={ +items={}; +ver=""; +}; +t.roster=e; +t:hook("stream-features",function(e) +if e:get_child("ver",i)then +s=true; +end +end); +local function h(t) +local e=o.stanza("item",{xmlns=a}); +for a,t in pairs(t)do +if a~="groups"then +e.attr[a]=t; +else +for a=1,#t do +e:tag("group"):text(t[a]):up(); +end +end +end +return e; +end +local function d(e) +local t={}; +local a={}; +t.groups=a; +local o=e.attr.jid; +for e,a in pairs(e.attr)do +if e~="xmlns"then +t[e]=a +end +end +for e in e:childtags("group")do +n(a,e:get_text()) +end +return t; +end +function e:load(t) +e.ver,e.items=t.ver,t.items; +end +function e:dump() +return{ +ver=e.ver, +items=e.items, +}; +end +function e:add_contact(s,i,n,e) +local i={jid=s,name=i,groups=n}; +local a=o.iq({type="set"}) +:tag("query",{xmlns=a}) +:add_child(h(i)); +t:send_iq(a,function(t) +if not e then return end +if t.attr.type=="result"then +e(true); +else +local t,a,o=t:get_error(); +e(nil,{t,a,o}); +end +end); +end +function e:delete_contact(i,n) +i=(type(i)=="table"and i.jid)or i; +local s={jid=i,subscription="remove"} +if not e.items[i]then return false,"item-not-found";end +t:send_iq(o.iq({type="set"}) +:tag("query",{xmlns=a}) +:add_child(h(s)), +function(e) +if not n then return end +if e.attr.type=="result"then +n(true); +else +local a,t,e=e:get_error(); +n(nil,{a,t,e}); +end +end); +end +local function h(t) +local t=d(t); +e.items[t.jid]=t; +end +local function d(t) +local a=e.items[t]; +e.items[t]=nil; +return a; +end +function e:fetch(i) +t:send_iq(o.iq({type="get"}):tag("query",{xmlns=a,ver=s and e.ver or nil}), +function(t) +if t.attr.type=="result"then +local t=t:get_child("query",a); +if t then +e.items={}; +for t in t:childtags("item")do +h(t) +end +e.ver=t.attr.ver or""; +end +i(e); +else +local t,e,a=stanza:get_error(); +i(nil,{t,e,a}); +end +end); +end +t:hook("iq/"..a,function(i) +local s,n=i.attr.type,i.attr.from; +if s=="set"and(not n or n==r(t.jid))then +local s=i:get_child("query",a); +local n=s and s:get_child("item"); +if n then +local i,a; +local o=n.attr.jid; +if n.attr.subscription=="remove"then +i="removed" +a=d(o); +else +i=e.items[o]and"changed"or"added"; +h(n) +a=e.items[o]; +end +e.ver=s.attr.ver; +if a then +t:event("roster/item-"..i,a); +end +end +t:send(o.reply(i)) +return true; +end +end); +end +end) +package.preload['verse.plugins.register']=(function(...) +local t=require"verse"; +local i="jabber:iq:register"; +function t.plugins.register(e) +local function a(o) +if o:get_child("register","http://jabber.org/features/iq-register")then +local t=t.iq({to=e.host_,type="set"}) +:tag("query",{xmlns=i}) +:tag("username"):text(e.username):up() +:tag("password"):text(e.password):up(); +if e.register_email then +t:tag("email"):text(e.register_email):up(); +end +e:send_iq(t,function(t) +if t.attr.type=="result"then +e:event("registration-success"); +else +local o,t,a=t:get_error(); +e:debug("Registration failed: %s",t); +e:event("registration-failure",{type=o,condition=t,text=a}); +end +end); +else +e:debug("In-band registration not offered by server"); +e:event("registration-failure",{condition="service-unavailable"}); +end +e:unhook("stream-features",a); +return true; +end +e:hook("stream-features",a,310); +end +end) +package.preload['verse.plugins.groupchat']=(function(...) +local i=require"verse"; +local e=require"events"; +local n=require"util.jid"; +local a={}; +a.__index=a; +local h="urn:xmpp:delay"; +local s="http://jabber.org/protocol/muc"; +function i.plugins.groupchat(o) +o:add_plugin("presence") +o.rooms={}; +o:hook("stanza",function(e) +local a=n.bare(e.attr.from); +if not a then return end +local t=o.rooms[a] +if not t and e.attr.to and a then +t=o.rooms[e.attr.to.." "..a] +end +if t and t.opts.source and e.attr.to~=t.opts.source then return end +if t then +local i=select(3,n.split(e.attr.from)); +local n=e:get_child_text("body"); +local o=e:get_child("delay",h); +local a={ +room_jid=a; +room=t; +sender=t.occupants[i]; +nick=i; +body=n; +stanza=e; +delay=(o and o.attr.stamp); +}; +local t=t:event(e.name,a); +return t or(e.name=="message")or nil; +end +end,500); +function o:join_room(n,h,t) +if not h then +return false,"no nickname supplied" +end +t=t or{}; +local e=setmetatable(i.eventable{ +stream=o,jid=n,nick=h, +subject=nil, +occupants={}, +opts=t, +},a); +if t.source then +self.rooms[t.source.." "..n]=e; +else +self.rooms[n]=e; +end +local a=e.occupants; +e:hook("presence",function(o) +local t=o.nick or h; +if not a[t]and o.stanza.attr.type~="unavailable"then +a[t]={ +nick=t; +jid=o.stanza.attr.from; +presence=o.stanza; +}; +local o=o.stanza:get_child("x",s.."#user"); +if o then +local e=o:get_child("item"); +if e and e.attr then +a[t].real_jid=e.attr.jid; +a[t].affiliation=e.attr.affiliation; +a[t].role=e.attr.role; +end +end +if t==e.nick then +e.stream:event("groupchat/joined",e); +else +e:event("occupant-joined",a[t]); +end +elseif a[t]and o.stanza.attr.type=="unavailable"then +if t==e.nick then +e.stream:event("groupchat/left",e); +if e.opts.source then +self.rooms[e.opts.source.." "..n]=nil; +else +self.rooms[n]=nil; +end +else +a[t].presence=o.stanza; +e:event("occupant-left",a[t]); +a[t]=nil; +end +end +end); +e:hook("message",function(a) +local t=a.stanza:get_child_text("subject"); +if not t then return end +t=#t>0 and t or nil; +if t~=e.subject then +local o=e.subject; +e.subject=t; +return e:event("subject-changed",{from=o,to=t,by=a.sender,event=a}); +end +end,2e3); +local t=i.presence():tag("x",{xmlns=s}):reset(); +self:event("pre-groupchat/joining",t); +e:send(t) +self:event("groupchat/joining",e); +return e; +end +o:hook("presence-out",function(e) +if not e.attr.to then +for a,t in pairs(o.rooms)do +t:send(e); +end +e.attr.to=nil; +end +end); +end +function a:send(e) +if e.name=="message"and not e.attr.type then +e.attr.type="groupchat"; +end +if e.name=="presence"then +e.attr.to=self.jid.."/"..self.nick; +end +if e.attr.type=="groupchat"or not e.attr.to then +e.attr.to=self.jid; +end +if self.opts.source then +e.attr.from=self.opts.source +end +self.stream:send(e); +end +function a:send_message(e) +self:send(i.message():tag("body"):text(e)); +end +function a:set_subject(e) +self:send(i.message():tag("subject"):text(e)); +end +function a:leave(t) +self.stream:event("groupchat/leaving",self); +local e=i.presence({type="unavailable"}); +if t then +e:tag("status"):text(t); +end +self:send(e); +end +function a:admin_set(e,t,a,o) +self:send(i.iq({type="set"}) +:query(s.."#admin") +:tag("item",{nick=e,[t]=a}) +:tag("reason"):text(o or"")); +end +function a:set_role(e,t,a) +self:admin_set(e,"role",t,a); +end +function a:set_affiliation(a,t,e) +self:admin_set(a,"affiliation",t,e); +end +function a:kick(e,t) +self:set_role(e,"none",t); +end +function a:ban(t,e) +self:set_affiliation(t,"outcast",e); +end +end) +package.preload['verse.plugins.vcard']=(function(...) +local i=require"verse"; +local o=require"util.vcard"; +local n="vcard-temp"; +function i.plugins.vcard(a) +function a:get_vcard(t,e) +a:send_iq(i.iq({to=t,type="get"}) +:tag("vCard",{xmlns=n}),e and function(t) +local a,a; +vCard=t:get_child("vCard",n); +if t.attr.type=="result"and vCard then +vCard=o.from_xep54(vCard) +e(vCard) +else +e(false) +end +end or nil); +end +function a:set_vcard(e,n) +local t; +if type(e)=="table"and e.name then +t=e; +elseif type(e)=="string"then +t=o.to_xep54(o.from_text(e)[1]); +elseif type(e)=="table"then +t=o.to_xep54(e); +error("Converting a table to vCard not implemented") +end +if not t then return false end +a:debug("setting vcard to %s",tostring(t)); +a:send_iq(i.iq({type="set"}) +:add_child(t),n); +end +end +end) +package.preload['verse.plugins.vcard_update']=(function(...) +local i=require"verse"; +local e,n="vcard-temp","vcard-temp:x:update"; +local e,t=pcall(function()return require("util.hashes").sha1;end); +if not e then +e,t=pcall(function()return require("util.sha1").sha1;end); +if not e then +error("Could not find a sha1()") +end +end +local h=t; +local e,t=pcall(function() +local e=require("util.encodings").base64.decode; +assert(e("SGVsbG8=")=="Hello") +return e; +end); +if not e then +e,t=pcall(function()return require("mime").unb64;end); +if not e then +error("Could not find a base64 decoder") +end +end +local s=t; +function i.plugins.vcard_update(e) +e:add_plugin("vcard"); +e:add_plugin("presence"); +local t; +function update_vcard_photo(a) +local o; +for e=1,#a do +if a[e].name=="PHOTO"then +o=a[e][1]; +break +end +end +if o then +local a=h(s(o),true); +t=i.stanza("x",{xmlns=n}) +:tag("photo"):text(a); +e:resend_presence() +else +t=nil; +end +end +local a=e.set_vcard; +local a; +e:hook("ready",function(t) +if a then return;end +a=true; +e:get_vcard(nil,function(t) +if t then +update_vcard_photo(t) +end +e:event("ready"); +end); +return true; +end,3); +e:hook("presence-out",function(e) +if t and not e:get_child("x",n)then +e:add_child(t); +end +end,10); +end +end) +package.preload['verse.plugins.carbons']=(function(...) +local a=require"verse"; +local o="urn:xmpp:carbons:2"; +local h="urn:xmpp:forward:0"; +local r=os.time; +local s=require"util.datetime".parse; +local n=require"util.jid".bare; +function a.plugins.carbons(e) +local t={}; +t.enabled=false; +e.carbons=t; +function t:enable(i) +e:send_iq(a.iq{type="set"} +:tag("enable",{xmlns=o}) +,function(e) +local e=e.attr.type=="result"; +if e then +t.enabled=true; +end +if i then +i(e); +end +end or nil); +end +function t:disable(i) +e:send_iq(a.iq{type="set"} +:tag("disable",{xmlns=o}) +,function(e) +local e=e.attr.type=="result"; +if e then +t.enabled=false; +end +if i then +i(e); +end +end or nil); +end +local a; +e:hook("bind-success",function() +a=n(e.jid); +end); +e:hook("message",function(i) +local t=i:get_child(nil,o); +if i.attr.from==a and t then +local o=t.name; +local t=t:get_child("forwarded",h); +local a=t and t:get_child("message","jabber:client"); +local t=t:get_child("delay","urn:xmpp:delay"); +local t=t and t.attr.stamp; +t=t and s(t); +if a then +return e:event("carbon",{ +dir=o, +stanza=a, +timestamp=t or r(), +}); +end +end +end,1); +end +end) +package.preload['verse.plugins.archive']=(function(...) +local e=require"verse"; +local a=require"util.stanza"; +local t="urn:xmpp:mam:0" +local r="urn:xmpp:forward:0"; +local l="urn:xmpp:delay"; +local s=require"util.uuid".generate; +local u=require"util.datetime".parse; +local i=require"util.datetime".datetime; +local o=require"util.dataforms".new; +local d=require"util.rsm"; +local m={}; +local c=o{ +{name="FORM_TYPE";type="hidden";value=t;}; +{name="with";type="jid-single";}; +{name="start";type="text-single"}; +{name="end";type="text-single";}; +}; +function e.plugins.archive(n) +function n:query_archive(o,e,h) +local s=s(); +local n=a.iq{type="set",to=o} +:tag("query",{xmlns=t,queryid=s}); +local o,a=tonumber(e["start"]),tonumber(e["end"]); +e["start"]=o and i(o); +e["end"]=a and i(a); +n:add_child(c:form(e,"submit")); +n:add_child(d.generate(e)); +local a={}; +local function o(i) +local e=i:get_child("fin",t) +if e and e.attr.queryid==s then +local e=d.get(e); +for e,t in pairs(e or m)do a[e]=t;end +self:unhook("message",o); +h(a); +return true +end +local e=i:get_child("result",t); +if e and e.attr.queryid==s then +local t=e:get_child("forwarded",r); +t=t or i:get_child("forwarded",r); +local o=e.attr.id; +local e=t:get_child("delay",l); +local i=e and u(e.attr.stamp)or nil; +local e=t:get_child("message","jabber:client") +a[#a+1]={id=o,stamp=i,message=e}; +return true +end +end +self:hook("message",o,1); +self:send_iq(n,function(e) +if e.attr.type=="error"then +self:warn(table.concat({e:get_error()}," ")) +self:unhook("message",o); +h(false,e:get_error()) +end +return true +end); +end +local i={ +always=true,[true]="always", +never=false,[false]="never", +roster="roster", +} +local function h(t) +local e={}; +local a=t.attr.default; +if a then +e[false]=i[a]; +end +local a=t:get_child("always"); +if a then +for t in a:childtags("jid")do +local t=t:get_text(); +e[t]=true; +end +end +local t=t:get_child("never"); +if t then +for t in t:childtags("jid")do +local t=t:get_text(); +e[t]=false; +end +end +return e; +end +local function s(o) +local e +e,o[false]=o[false],nil; +if e~=nil then +e=i[e]; +end +local i=a.stanza("prefs",{xmlns=t,default=e}) +local t=a.stanza("always"); +local e=a.stanza("never"); +for o,a in pairs(o)do +(a and t or e):tag("jid"):text(o):up(); +end +return i:add_child(t):add_child(e); +end +function n:archive_prefs_get(o) +self:send_iq(a.iq{type="get"}:tag("prefs",{xmlns=t}), +function(e) +if e and e.attr.type=="result"and e.tags[1]then +local t=h(e.tags[1]); +o(t,e); +else +o(nil,e); +end +end); +end +function n:archive_prefs_set(t,e) +self:send_iq(a.iq{type="set"}:add_child(s(t)),e); +end +end +end) +package.preload['util.http']=(function(...) +local t,s=string.format,string.char; +local i,n,h=pairs,ipairs,tonumber; +local o,r=table.insert,table.concat; +local function d(e) +return e and(e:gsub("[^a-zA-Z0-9.~_-]",function(e)return t("%%%02x",e:byte());end)); +end +local function a(e) +return e and(e:gsub("%%(%x%x)",function(e)return s(h(e,16));end)); +end +local function e(e) +return e and(e:gsub("%W",function(e) +if e~=" "then +return t("%%%02x",e:byte()); +else +return"+"; +end +end)); +end +local function s(t) +local a={}; +if t[1]then +for i,t in n(t)do +o(a,e(t.name).."="..e(t.value)); +end +else +for t,i in i(t)do +o(a,e(t).."="..e(i)); +end +end +return r(a,"&"); +end +local function n(e) +if not e:match("=")then return a(e);end +local i={}; +for e,t in e:gmatch("([^=&]*)=([^&]*)")do +e,t=e:gsub("%+","%%20"),t:gsub("%+","%%20"); +e,t=a(e),a(t); +o(i,{name=e,value=t}); +i[e]=t; +end +return i; +end +local function o(e,t) +e=","..e:gsub("[ \t]",""):lower()..","; +return e:find(","..t:lower()..",",1,true)~=nil; +end +return{ +urlencode=d,urldecode=a; +formencode=s,formdecode=n; +contains_token=o; +}; +end) +package.preload['net.http.parser']=(function(...) +local w=tonumber; +local a=assert; +local b=require"socket.url".parse; +local t=require"util.http".urldecode; +local function g(e) +e=t((e:gsub("//+","/"))); +if e:sub(1,1)~="/"then +e="/"..e; +end +local t=0; +for e in e:gmatch("([^/]+)/")do +if e==".."then +t=t-1; +elseif e~="."then +t=t+1; +end +if t<0 then +return nil; +end +end +return e; +end +local v={}; +function v.new(u,s,e,y) +local d=true; +if not e or e=="server"then d=false;else a(e=="client","Invalid parser type");end +local e=""; +local p,a,r; +local h=nil; +local t; +local o; +local c; +local n; +return{ +feed=function(l,i) +if n then return nil,"parse has failed";end +if not i then +if h and d and not o then +t.body=e; +u(t); +elseif e~=""then +n=true;return s(); +end +return; +end +e=e..i; +while#e>0 do +if h==nil then +local f=e:find("\r\n\r\n",nil,true); +if not f then return;end +local m,r,l,i,v; +local u; +local a={}; +for t in e:sub(1,f+1):gmatch("([^\r\n]+)\r\n")do +if u then +local e,t=t:match("^([^%s:]+): *(.*)$"); +if not e then n=true;return s("invalid-header-line");end +e=e:lower(); +a[e]=a[e]and a[e]..","..t or t; +else +u=t; +if d then +l,i,v=t:match("^HTTP/(1%.[01]) (%d%d%d) (.*)$"); +i=w(i); +if not i then n=true;return s("invalid-status-line");end +c=not +((y and y().method=="HEAD") +or(i==204 or i==304 or i==301) +or(i>=100 and i<200)); +else +m,r,l=t:match("^(%w+) (%S+) HTTP/(1%.[01])$"); +if not m then n=true;return s("invalid-status-line");end +end +end +end +if not u then n=true;return s("invalid-status-line");end +p=c and a["transfer-encoding"]=="chunked"; +o=w(a["content-length"]); +if d then +if not c then o=0;end +t={ +code=i; +httpversion=l; +headers=a; +body=c and""or nil; +responseversion=l; +responseheaders=a; +}; +else +local e; +if r:byte()==47 then +local a,t=r:match("([^?]*).?(.*)"); +if t==""then t=nil;end +e={path=a,query=t}; +else +e=b(r); +if not(e and e.path)then n=true;return s("invalid-url");end +end +r=g(e.path); +a.host=e.host or a.host; +o=o or 0; +t={ +method=m; +url=e; +path=r; +httpversion=l; +headers=a; +body=nil; +}; +end +e=e:sub(f+4); +h=true; +end +if h then +if d then +if p then +if not e:find("\r\n",nil,true)then +return; +end +if not a then +a,r=e:match("^(%x+)[^\r\n]*\r\n()"); +a=a and w(a,16); +if not a then n=true;return s("invalid-chunk-size");end +end +if a==0 and e:find("\r\n\r\n",r-2,true)then +h,a=nil,nil; +e=e:gsub("^.-\r\n\r\n",""); +u(t); +elseif#e-r-2>=a then +t.body=t.body..e:sub(r,r+(a-1)); +e=e:sub(r+a+2); +a,r=nil,nil; +else +break; +end +elseif o and#e>=o then +if t.code==101 then +t.body,e=e,""; +else +t.body,e=e:sub(1,o),e:sub(o+1); +end +h=nil;u(t); +else +break; +end +elseif#e>=o then +t.body,e=e:sub(1,o),e:sub(o+1); +h=nil;u(t); +else +break; +end +end +end +end; +}; +end +return v; +end) +package.preload['net.http']=(function(...) +local x=require"socket" +local j=require"util.encodings".base64.encode; +local c=require"socket.url" +local d=require"net.http.parser".new; +local h=require"util.http"; +local w=pcall(require,"ssl"); +local f=require"net.server" +local r,i=table.insert,table.concat; +local m=pairs; +local y,u,b,p,s= +tonumber,tostring,xpcall,select,debug.traceback; +local g,v=assert,error +local l=require"util.logger".init("http"); +module"http" +local o={}; +local n={default_port=80,default_mode="*a"}; +function n.onconnect(t) +local e=o[t]; +local a={e.method or"GET"," ",e.path," HTTP/1.1\r\n"}; +if e.query then +r(a,4,"?"..e.query); +end +t:write(i(a)); +local a={[2]=": ",[4]="\r\n"}; +for o,e in m(e.headers)do +a[1],a[3]=o,e; +t:write(i(a)); +end +t:write("\r\n"); +if e.body then +t:write(e.body); +end +end +function n.onincoming(t,a) +local e=o[t]; +if not e then +l("warn","Received response from connection %s with no request attached!",u(t)); +return; +end +if a and e.reader then +e:reader(a); +end +end +function n.ondisconnect(t,a) +local e=o[t]; +if e and e.conn then +e:reader(nil,a); +end +o[t]=nil; +end +function n.ondetach(e) +o[e]=nil; +end +local function q(e,a,i) +if not e.parser then +local function o(t) +if e.callback then +e.callback(t or"connection-closed",0,e); +e.callback=nil; +end +destroy_request(e); +end +if not a then +o(i); +return; +end +local function a(t) +if e.callback then +e.callback(t.body,t.code,t,e); +e.callback=nil; +end +destroy_request(e); +end +local function t() +return e; +end +e.parser=d(a,o,"client",t); +end +e.parser:feed(a); +end +local function k(e)l("error","Traceback[http]: %s",s(u(e),2));end +function request(e,t,d) +local e=c.parse(e); +if not(e and e.host)then +d(nil,0,e); +return nil,"invalid-url"; +end +if not e.path then +e.path="/"; +end +local r,i,s; +local c,a=e.host,e.port; +local h=c; +if(a=="80"and e.scheme=="http") +or(a=="443"and e.scheme=="https")then +a=nil; +elseif a then +h=h..":"..a; +end +i={ +["Host"]=h; +["User-Agent"]="Prosody XMPP Server"; +}; +if e.userinfo then +i["Authorization"]="Basic "..j(e.userinfo); +end +if t then +e.onlystatus=t.onlystatus; +s=t.body; +if s then +r="POST"; +i["Content-Length"]=u(#s); +i["Content-Type"]="application/x-www-form-urlencoded"; +end +if t.method then r=t.method;end +if t.headers then +for e,t in m(t.headers)do +i[e]=t; +end +end +end +e.method,e.headers,e.body=r,i,s; +local i=e.scheme=="https"; +if i and not w then +v("SSL not available, unable to contact https URL"); +end +local h=a and y(a)or(i and 443 or 80); +local a=x.tcp(); +a:settimeout(10); +local r,s=a:connect(c,h); +if not r and s~="timeout"then +d(nil,0,e); +return nil,s; +end +local s=false; +if i then +s=t and t.sslctx or{mode="client",protocol="sslv23",options={"no_sslv2","no_sslv3"}}; +end +e.handler,e.conn=g(f.wrapclient(a,c,h,n,"*a",s)); +e.write=function(...)return e.handler:write(...);end +e.callback=function(o,t,i,a)l("debug","Calling callback, status %s",t or"---");return p(2,b(function()return d(o,t,i,a)end,k));end +e.reader=q; +e.state="status"; +o[e.handler]=e; +return e; +end +function destroy_request(e) +if e.conn then +e.conn=nil; +e.handler:close() +end +end +local t,o=h.urlencode,h.urldecode; +local a,e=h.formencode,h.formdecode; +_M.urlencode,_M.urldecode=t,o; +_M.formencode,_M.formdecode=a,e; +return _M; +end) +package.preload['verse.bosh']=(function(...) +local n=require"util.xmppstream".new; +local i=require"util.stanza"; +require"net.httpclient_listener"; +local o=require"net.http"; +local e=setmetatable({},{__index=verse.stream_mt}); +e.__index=e; +local s="http://etherx.jabber.org/streams"; +local h="http://jabber.org/protocol/httpbind"; +local a=5; +function verse.new_bosh(a,t) +local t={ +bosh_conn_pool={}; +bosh_waiting_requests={}; +bosh_rid=math.random(1,999999); +bosh_outgoing_buffer={}; +bosh_url=t; +conn={}; +}; +function t:reopen() +self.bosh_need_restart=true; +self:flush(); +end +local t=verse.new(a,t); +return setmetatable(t,e); +end +function e:connect() +self:_send_session_request(); +end +function e:send(e) +self:debug("Putting into BOSH send buffer: %s",tostring(e)); +self.bosh_outgoing_buffer[#self.bosh_outgoing_buffer+1]=i.clone(e); +self:flush(); +end +function e:flush() +if self.connected +and#self.bosh_waiting_requests0 +or self.bosh_need_restart)then +self:debug("Flushing..."); +local t=self:_make_body(); +local e=self.bosh_outgoing_buffer; +for a,o in ipairs(e)do +t:add_child(o); +e[a]=nil; +end +self:_make_request(t); +else +self:debug("Decided not to flush."); +end +end +function e:_make_request(i) +local e,t=o.request(self.bosh_url,{body=tostring(i)},function(o,e,t) +if e~=0 then +self.inactive_since=nil; +return self:_handle_response(o,e,t); +end +local e=os.time(); +if not self.inactive_since then +self.inactive_since=e; +elseif e-self.inactive_since>self.bosh_max_inactivity then +return self:_disconnected(); +else +self:debug("%d seconds left to reconnect, retrying in %d seconds...", +self.bosh_max_inactivity-(e-self.inactive_since),a); +end +timer.add_task(a,function() +self:debug("Retrying request..."); +for e,a in ipairs(self.bosh_waiting_requests)do +if a==t then +table.remove(self.bosh_waiting_requests,e); +break; +end +end +self:_make_request(i); +end); +end); +if e then +table.insert(self.bosh_waiting_requests,e); +else +self:warn("Request failed instantly: %s",t); +end +end +function e:_disconnected() +self.connected=nil; +self:event("disconnected"); +end +function e:_send_session_request() +local e=self:_make_body(); +e.attr.hold="1"; +e.attr.wait="60"; +e.attr["xml:lang"]="en"; +e.attr.ver="1.6"; +e.attr.from=self.jid; +e.attr.to=self.host; +e.attr.secure='true'; +o.request(self.bosh_url,{body=tostring(e)},function(e,t) +if t==0 then +return self:_disconnected(); +end +local e=self:_parse_response(e) +if not e then +self:warn("Invalid session creation response"); +self:_disconnected(); +return; +end +self.bosh_sid=e.attr.sid; +self.bosh_wait=tonumber(e.attr.wait); +self.bosh_hold=tonumber(e.attr.hold); +self.bosh_max_inactivity=tonumber(e.attr.inactivity); +self.bosh_max_requests=tonumber(e.attr.requests)or self.bosh_hold; +self.connected=true; +self:event("connected"); +self:_handle_response_payload(e); +end); +end +function e:_handle_response(t,a,e) +if self.bosh_waiting_requests[1]~=e then +self:warn("Server replied to request that wasn't the oldest"); +for a,t in ipairs(self.bosh_waiting_requests)do +if t==e then +self.bosh_waiting_requests[a]=nil; +break; +end +end +else +table.remove(self.bosh_waiting_requests,1); +end +local e=self:_parse_response(t); +if e then +self:_handle_response_payload(e); +end +self:flush(); +end +function e:_handle_response_payload(t) +local e=t.tags; +for t=1,#e do +local e=e[t]; +if e.attr.xmlns==s then +self:event("stream-"..e.name,e); +elseif e.attr.xmlns then +self:event("stream/"..e.attr.xmlns,e); +else +self:event("stanza",e); +end +end +if t.attr.type=="terminate"then +self:_disconnected({reason=t.attr.condition}); +end +end +local a={ +stream_ns="http://jabber.org/protocol/httpbind",stream_tag="body", +default_ns="jabber:client", +streamopened=function(e,t)e.notopen=nil;e.payload=verse.stanza("body",t);return true;end; +handlestanza=function(e,t)e.payload:add_child(t);end; +}; +function e:_parse_response(e) +self:debug("Parsing response: %s",e); +if e==nil then +self:debug("%s",debug.traceback()); +self:_disconnected(); +return; +end +local t={notopen=true,stream=self}; +local a=n(t,a); +a:feed(e); +return t.payload; +end +function e:_make_body() +self.bosh_rid=self.bosh_rid+1; +local e=verse.stanza("body",{ +xmlns=h; +content="text/xml; charset=utf-8"; +sid=self.bosh_sid; +rid=self.bosh_rid; +}); +if self.bosh_need_restart then +self.bosh_need_restart=nil; +e.attr.restart='true'; +end +return e; +end +end) +package.preload['verse.client']=(function(...) +local t=require"verse"; +local i=t.stream_mt; +local s=require"util.jid".split; +local r=require"net.adns"; +local e=require"lxp"; +local a=require"util.stanza"; +t.message,t.presence,t.iq,t.stanza,t.reply,t.error_reply= +a.message,a.presence,a.iq,a.stanza,a.reply,a.error_reply; +local h=require"util.xmppstream".new; +local n="http://etherx.jabber.org/streams"; +local function d(t,e) +return t.prioritye.weight); +end +local o={ +stream_ns=n, +stream_tag="stream", +default_ns="jabber:client"}; +function o.streamopened(e,t) +e.stream_id=t.id; +if not e:event("opened",t)then +e.notopen=nil; +end +return true; +end +function o.streamclosed(e) +e.notopen=true; +if not e.closed then +e:send(""); +e.closed=true; +end +e:event("closed"); +return e:close("stream closed") +end +function o.handlestanza(t,e) +if e.attr.xmlns==n then +return t:event("stream-"..e.name,e); +elseif e.attr.xmlns then +return t:event("stream/"..e.attr.xmlns,e); +end +return t:event("stanza",e); +end +function o.error(a,t,e) +if a:event(t,e)==nil then +if e then +local t=e:get_child(nil,"urn:ietf:params:xml:ns:xmpp-streams"); +local e=e:get_child_text("text","urn:ietf:params:xml:ns:xmpp-streams"); +error(t.name..(e and": "..e or"")); +else +error(e and e.name or t or"unknown-error"); +end +end +end +function i:reset() +if self.stream then +self.stream:reset(); +else +self.stream=h(self,o); +end +self.notopen=true; +return true; +end +function i:connect_client(e,a) +self.jid,self.password=e,a; +self.username,self.host,self.resource=s(e); +self:add_plugin("tls"); +self:add_plugin("sasl"); +self:add_plugin("bind"); +self:add_plugin("session"); +function self.data(t,e) +local t,a=self.stream:feed(e); +if t then return;end +self:debug("debug","Received invalid XML (%s) %d bytes: %s",tostring(a),#e,e:sub(1,300):gsub("[\r\n]+"," ")); +self:close("xml-not-well-formed"); +end +self:hook("connected",function()self:reopen();end); +self:hook("incoming-raw",function(e)return self.data(self.conn,e);end); +self.curr_id=0; +self.tracked_iqs={}; +self:hook("stanza",function(e) +local t,a=e.attr.id,e.attr.type; +if t and e.name=="iq"and(a=="result"or a=="error")and self.tracked_iqs[t]then +self.tracked_iqs[t](e); +self.tracked_iqs[t]=nil; +return true; +end +end); +self:hook("stanza",function(e) +local a; +if e.attr.xmlns==nil or e.attr.xmlns=="jabber:client"then +if e.name=="iq"and(e.attr.type=="get"or e.attr.type=="set")then +local o=e.tags[1]and e.tags[1].attr.xmlns; +if o then +a=self:event("iq/"..o,e); +if not a then +a=self:event("iq",e); +end +end +if a==nil then +self:send(t.error_reply(e,"cancel","service-unavailable")); +return true; +end +else +a=self:event(e.name,e); +end +end +return a; +end,-1); +self:hook("outgoing",function(e) +if e.name then +self:event("stanza-out",e); +end +end); +self:hook("stanza-out",function(e) +if not e.attr.xmlns then +self:event(e.name.."-out",e); +end +end); +local function e() +self:event("ready"); +end +self:hook("session-success",e,-1) +self:hook("bind-success",e,-1); +local e=self.close; +function self:close(t) +self.close=e; +if not self.closed then +self:send(""); +self.closed=true; +else +return self:close(t); +end +end +local function t() +self:connect(self.connect_host or self.host,self.connect_port or 5222); +end +if not(self.connect_host or self.connect_port)then +r.lookup(function(a) +if a then +local e={}; +self.srv_hosts=e; +for a,t in ipairs(a)do +table.insert(e,t.srv); +end +table.sort(e,d); +local a=e[1]; +self.srv_choice=1; +if a then +self.connect_host,self.connect_port=a.target,a.port; +self:debug("Best record found, will connect to %s:%d",self.connect_host or self.host,self.connect_port or 5222); +end +self:hook("disconnected",function() +if self.srv_hosts and self.srv_choice<#self.srv_hosts then +self.srv_choice=self.srv_choice+1; +local e=e[self.srv_choice]; +self.connect_host,self.connect_port=e.target,e.port; +t(); +return true; +end +end,1e3); +self:hook("connected",function() +self.srv_hosts=nil; +end,1e3); +end +t(); +end,"_xmpp-client._tcp."..(self.host)..".","SRV"); +else +t(); +end +end +function i:reopen() +self:reset(); +self:send(a.stanza("stream:stream",{to=self.host,["xmlns:stream"]='http://etherx.jabber.org/streams', +xmlns="jabber:client",version="1.0"}):top_tag()); +end +function i:send_iq(t,a) +local e=self:new_id(); +self.tracked_iqs[e]=a; +t.attr.id=e; +self:send(t); +end +function i:new_id() +self.curr_id=self.curr_id+1; +return tostring(self.curr_id); +end +end) +package.preload['verse.component']=(function(...) +local o=require"verse"; +local a=o.stream_mt; +local h=require"util.jid".split; +local e=require"lxp"; +local t=require"util.stanza"; +local d=require"util.sha1".sha1; +o.message,o.presence,o.iq,o.stanza,o.reply,o.error_reply= +t.message,t.presence,t.iq,t.stanza,t.reply,t.error_reply; +local r=require"util.xmppstream".new; +local s="http://etherx.jabber.org/streams"; +local i="jabber:component:accept"; +local n={ +stream_ns=s, +stream_tag="stream", +default_ns=i}; +function n.streamopened(e,t) +e.stream_id=t.id; +if not e:event("opened",t)then +e.notopen=nil; +end +return true; +end +function n.streamclosed(e) +return e:event("closed"); +end +function n.handlestanza(t,e) +if e.attr.xmlns==s then +return t:event("stream-"..e.name,e); +elseif e.attr.xmlns or e.name=="handshake"then +return t:event("stream/"..(e.attr.xmlns or i),e); +end +return t:event("stanza",e); +end +function a:reset() +if self.stream then +self.stream:reset(); +else +self.stream=r(self,n); +end +self.notopen=true; +return true; +end +function a:connect_component(e,n) +self.jid,self.password=e,n; +self.username,self.host,self.resource=h(e); +function self.data(t,e) +local o,t=self.stream:feed(e); +if o then return;end +a:debug("debug","Received invalid XML (%s) %d bytes: %s",tostring(t),#e,e:sub(1,300):gsub("[\r\n]+"," ")); +a:close("xml-not-well-formed"); +end +self:hook("incoming-raw",function(e)return self.data(self.conn,e);end); +self.curr_id=0; +self.tracked_iqs={}; +self:hook("stanza",function(t) +local e,a=t.attr.id,t.attr.type; +if e and t.name=="iq"and(a=="result"or a=="error")and self.tracked_iqs[e]then +self.tracked_iqs[e](t); +self.tracked_iqs[e]=nil; +return true; +end +end); +self:hook("stanza",function(e) +local t; +if e.attr.xmlns==nil or e.attr.xmlns=="jabber:client"then +if e.name=="iq"and(e.attr.type=="get"or e.attr.type=="set")then +local a=e.tags[1]and e.tags[1].attr.xmlns; +if a then +t=self:event("iq/"..a,e); +if not t then +t=self:event("iq",e); +end +end +if t==nil then +self:send(o.error_reply(e,"cancel","service-unavailable")); +return true; +end +else +t=self:event(e.name,e); +end +end +return t; +end,-1); +self:hook("opened",function(e) +print(self.jid,self.stream_id,e.id); +local e=d(self.stream_id..n,true); +self:send(t.stanza("handshake",{xmlns=i}):text(e)); +self:hook("stream/"..i,function(e) +if e.name=="handshake"then +self:event("authentication-success"); +end +end); +end); +local function e() +self:event("ready"); +end +self:hook("authentication-success",e,-1); +self:connect(self.connect_host or self.host,self.connect_port or 5347); +self:reopen(); +end +function a:reopen() +self:reset(); +self:send(t.stanza("stream:stream",{to=self.jid,["xmlns:stream"]='http://etherx.jabber.org/streams', +xmlns=i,version="1.0"}):top_tag()); +end +function a:close(t) +if not self.notopen then +self:send(""); +end +local e=self.conn.disconnect(); +self.conn:close(); +e(conn,t); +end +function a:send_iq(t,a) +local e=self:new_id(); +self.tracked_iqs[e]=a; +t.attr.id=e; +self:send(t); +end +function a:new_id() +self.curr_id=self.curr_id+1; +return tostring(self.curr_id); +end +end) +pcall(require,"luarocks.require"); +local s=require"socket"; +pcall(require,"ssl"); +local a=require"net.server"; +local n=require"util.events"; +local o=require"util.logger"; +module("verse",package.seeall); +local e=_M; +_M.server=a; +local t={}; +t.__index=t; +stream_mt=t; +e.plugins={}; +function e.init(...) +for e=1,select("#",...)do +local t=pcall(require,"verse."..select(e,...)); +if not t then +error("Verse connection module not found: verse."..select(e,...)); +end +end +return e; +end +local i=0; +function e.new(a,o) +local t=setmetatable(o or{},t); +i=i+1; +t.id=tostring(i); +t.logger=a or e.new_logger("stream"..t.id); +t.events=n.new(); +t.plugins={}; +t.verse=e; +return t; +end +e.add_task=require"util.timer".add_task; +e.logger=o.init; +e.new_logger=o.init; +e.log=e.logger("verse"); +local function h(o,...) +local e,t,a=0,{...},select('#',...); +return(o:gsub("%%(.)",function(o)if e<=a then e=e+1;return tostring(t[e]);end end)); +end +function e.set_log_handler(e,t) +t=t or{"debug","info","warn","error"}; +o.reset(); +if io.type(e)=="file"then +local o=e; +function e(a,t,e) +o:write(a,"\t",t,"\t",e,"\n"); +end +end +if e then +local function i(a,t,o,...) +return e(a,t,h(o,...)); +end +for t,e in ipairs(t)do +o.add_level_sink(e,i); +end +end +end +function _default_log_handler(a,t,o) +return io.stderr:write(a,"\t",t,"\t",o,"\n"); +end +e.set_log_handler(_default_log_handler,{"error"}); +local function o(t) +e.log("error","Error: %s",t); +e.log("error","Traceback: %s",debug.traceback()); +end +function e.set_error_handler(e) +o=e; +end +function e.loop() +return xpcall(a.loop,o); +end +function e.step() +return xpcall(a.step,o); +end +function e.quit() +return a.setquitting(true); +end +function t:listen(e,t) +e=e or"localhost"; +t=t or 0; +local a,o=a.addserver(e,t,new_listener(self,"server"),"*a"); +if a then +self:debug("Bound to %s:%s",e,t); +self.server=a; +end +return a,o; +end +function t:connect(t,o) +t=t or"localhost"; +o=tonumber(o)or 5222; +local i=s.tcp() +i:settimeout(0); +local n,e=i:connect(t,o); +if not n and e~="timeout"then +self:warn("connect() to %s:%d failed: %s",t,o,e); +return self:event("disconnected",{reason=e})or false,e; +end +local t=a.wrapclient(i,t,o,new_listener(self),"*a"); +if not t then +self:warn("connection initialisation failed: %s",e); +return self:event("disconnected",{reason=e})or false,e; +end +self:set_conn(t); +return true; +end +function t:set_conn(t) +self.conn=t; +self.send=function(a,e) +self:event("outgoing",e); +e=tostring(e); +self:event("outgoing-raw",e); +return t:write(e); +end; +end +function t:close(t) +if not self.conn then +e.log("error","Attempt to close disconnected connection - possibly a bug"); +return; +end +local e=self.conn.disconnect(); +self.conn:close(); +e(self.conn,t); +end +function t:debug(...) +return self.logger("debug",...); +end +function t:info(...) +return self.logger("info",...); +end +function t:warn(...) +return self.logger("warn",...); +end +function t:error(...) +return self.logger("error",...); +end +function t:event(e,...) +self:debug("Firing event: "..tostring(e)); +return self.events.fire_event(e,...); +end +function t:hook(e,...) +return self.events.add_handler(e,...); +end +function t:unhook(e,t) +return self.events.remove_handler(e,t); +end +function e.eventable(e) +e.events=n.new(); +e.hook,e.unhook=t.hook,t.unhook; +local t=e.events.fire_event; +function e:event(e,...) +return t(e,...); +end +return e; +end +function t:add_plugin(t) +if self.plugins[t]then return true;end +if require("verse.plugins."..t)then +local e,a=e.plugins[t](self); +if e~=false then +self:debug("Loaded %s plugin",t); +self.plugins[t]=true; +else +self:warn("Failed to load %s plugin: %s",t,a); +end +end +return self; +end +function new_listener(t) +local a={}; +function a.onconnect(a) +if t.server then +local e=e.new(); +a:setlistener(new_listener(e)); +e:set_conn(a); +t:event("connected",{client=e}); +else +t.connected=true; +t:event("connected"); +end +end +function a.onincoming(a,e) +t:event("incoming-raw",e); +end +function a.ondisconnect(e,a) +if e~=t.conn then return end +t.connected=false; +t:event("disconnected",{reason=a}); +end +function a.ondrain(e) +t:event("drained"); +end +function a.onstatus(a,e) +t:event("status",e); +end +return a; +end +return e; +end) +local e="jabber:client" +local e=require"net.server"; +local e=require"core.portmanager"; +local h,t,e,d= +module.host,nil,module:get_option_string("conference_server"),module:get_option_number("listener_port",7e3); +if not e then +module:log("error","You need to set the MUC server in the configuration (conference_server)!") +module:log("error","Be a good boy or girl and go read the wiki at: http://code.google.com/p/prosody-modules/wiki/mod_ircd") +return false; +end +package.loaded["util.sha1"]=require"util.encodings"; +require"verse".init("component"); +c=verse.new(); +c:add_plugin("groupchat"); +local function t(e) +return c:event("stanza",e.stanza)or true; +end +module:hook("message/bare",t); +module:hook("message/full",t); +module:hook("presence/bare",t); +module:hook("presence/full",t); +c.type="component"; +c.send=core_post_stanza; +local s=require"util.jid"; +local w=require"util.encodings".stringprep.nodeprep; +local function u(o) +local i,r=table.insert,table.concat; +local a,t={},1; +if not(o and#o>0)then +return"" +end +while true do +local n=o:sub(t,t) +local e=n:byte(); +local s=( +(e>=9 and e<=10 and 0)or +(e>=32 and e<=126 and 0)or +(e>=192 and e<=223 and 1)or +(e>=224 and e<=239 and 2)or +(e>=240 and e<=247 and 3)or +(e>=248 and e<=251 and 4)or +(e>=251 and e<=252 and 5)or nil +) +if not s then +i(a,"?") +else +local h=t+s; +if s==0 then +i(a,n); +elseif h>#o then +i(a,("?"):format(e)); +else +local o=o:sub(t+1,h); +if o:match('^[\128-\191]*$')then +i(a,n); +i(a,o); +t=h; +else +i(a,("?"):format(e)); +end +end +end +t=t+1; +if t>#o then +break +end +end +return r(a); +end +local function r(t) +local e={}; +if t:sub(1,1)==":"then +e.from,t=t:match("^:(%w+)%s+(.*)$"); +end +for a in t:gmatch("%S+")do +if a:sub(1,1)==":"then +e[#e+1]=t:match(":(.*)$"); +break +end +e[#e+1]=a; +end +return e; +end +local function m(e) +if#e>1 then +e[#e]=":"..e[#e]; +end +return(e.from and":"..e.from.." "or"")..table.concat(e," "); +end +local function b(t,a) +local t=t and w(t:match("^#(%w+)"))or nil; +if not a then +return s.join(t,e); +else +return s.join(t,e,a); +end +end +local function t(e) +local e,a,t=s.split(e); +return"#"..e,t; +end +local t={ +moderator="@", +participant="", +visitor="", +none="" +} +local t={ +owner="~", +administrator="&", +member="+", +none="" +} +local k={ +moderator="o", +participant="", +visitor="", +none="" +} +local g={ +owner="q", +administrator="a", +member="v", +none="" +} +local f={default_port=d,default_mode="*l";interface="192.168.1.3"}; +local d=module:shared("sessions"); +local i={}; +local n={}; +local o={}; +local l={}; +local p=require"util.stanza"; +local t=e; +local function a(e) +e.conn:close(); +end +function f.onincoming(o,i) +local t=d[o]; +if not t then +t={conn=o,host=h,reset_stream=function()end, +close=a,log=logger.init("irc"..(o.id or"1")), +rooms={},roster={},has_un=false}; +d[o]=t; +function t.data(a) +local o=r(a); +module:log("debug",require"util.serialization".serialize(o)); +local a=table.remove(o,1); +if not a then +return; +end +a=a:upper(); +if not t.username and not t.nick then +if not(a=="USER"or a=="NICK")then +module:log("debug","Client tried to send command %s before registering",a); +return t.send{from=e,"451",a,"You have not completed the registration."} +end +end +if n[a]then +local e=n[a](t,o); +if e then +return t.send(e); +end +else +t.send{from=e,"421",t.nick,a,"Unknown command"}; +return module:log("debug","Unknown command: %s",a); +end +end +function t.send(e) +if type(e)=="string"then +return o:write(e.."\r\n"); +elseif type(e)=="table"then +local e=m(e); +module:log("debug",e); +o:write(e.."\r\n"); +end +end +end +if i then +t.data(i); +end +end +function f.ondisconnect(t,e) +local e=d[t]; +if e then +for t,e in pairs(e.rooms)do +e:leave("Disconnected"); +end +if e.nick then +o[e.nick]=nil; +end +if e.full_jid then +i[e.full_jid]=nil; +end +if e.username then +l[e.username]=nil; +end +end +d[t]=nil; +end +local function r(e) +if o[e]then return true else return false end +end +local function v(t) +local e=0; +local a; +for i,o in pairs(l)do +if t==o then e=e+1;end +end +a=e+1; +if e>0 then return tostring(t)..tostring(a);else return tostring(t);end +end +local function m(e,t) +e.full_jid=t; +i[t]=e; +i[t]["ar_last"]={}; +i[t]["nicks_changing"]={}; +if e.nick then o[e.nick]=e;end +end +local function y(t) +local a=t.nick; +if t.username and t.nick then +t.send{from=e,"001",a,"Welcome in the IRC to MUC XMPP Gateway, "..a}; +t.send{from=e,"002",a,"Your host is "..e.." running Prosody "..prosody.version}; +t.send{from=e,"003",a,"This server was created the "..os.date(nil,prosody.start_time)} +t.send{from=e,"004",a,table.concat({e,"mod_ircd(alpha-0.8)","i","aoqv"}," ")}; +t.send((":%s %s %s %s :%s"):format(e,"005",a,"CHANTYPES=# PREFIX=(qaov)~&@+","are supported by this server")); +t.send((":%s %s %s %s :%s"):format(e,"005",a,"STATUSMSG=~&@+","are supported by this server")); +t.send{from=e,"375",a,"- "..e.." Message of the day -"}; +t.send{from=e,"372",a,"-"}; +t.send{from=e,"372",a,"- Please be warned that this is only a partial irc implementation,"}; +t.send{from=e,"372",a,"- it's made to facilitate users transiting away from irc to XMPP."}; +t.send{from=e,"372",a,"-"}; +t.send{from=e,"372",a,"- Prosody is _NOT_ an IRC Server and it never will."}; +t.send{from=e,"372",a,"- We also would like to remind you that this plugin is provided as is,"}; +t.send{from=e,"372",a,"- it's still an Alpha and it's still a work in progress, use it at your sole"}; +t.send{from=e,"372",a,"- risk as there's a not so little chance something will break."}; +t.send{from=a,"MODE",a,"+i"}; +end +end +function n.NICK(t,a) +local a=a[1]; +a=a:gsub("[^%w_]",""); +if t.nick and not r(a)then +local e=t.nick; +t.nick=a; +o[e]=nil; +o[a]=t; +t.send{from=e.."!"..o[a].username,"NICK",a}; +if t.rooms then +t.nicks_changing[a]={e,t.username}; +for o,e in pairs(t.rooms)do e:change_nick(a);end +t.nicks_changing[a]=nil; +end +return; +elseif r(a)then +t.send{from=e,"433",a,"The nickname "..a.." is already in use"};return; +end +t.nick=a; +t.type="c2s"; +o[a]=t; +if t.username then +m(t,s.join(t.username,h,"ircd")); +end +y(t); +end +function n.USER(t,a) +local a=a[1]; +if not t.has_un then +local e=v(a); +l[e]=a; +t.username=e; +t.has_un=true; +if not t.full_jid then +m(t,s.join(t.username,h,"ircd")); +end +else +return t.send{from=e,"462","USER","You may not re-register."} +end +y(t); +end +function n.USERHOST(a,t) +local t=t[1]; +if not t then a.send{from=e,"461","USERHOST","Not enough parameters"};return;end +if o[t]and o[t].nick and o[t].username then +a.send{from=e,"302",a.nick,t.."=+"..o[t].username};return; +else +return; +end +end +local function m(a,o,i) +local t; +local e; +e=g[a]..k[o] +t=string.rep(i.." ",e:len()) +if e==""then return nil,nil end +return e,t +end +function n.JOIN(a,t) +local s=t[1]; +if not s then return end +local t=b(s); +if not i[a.full_jid].ar_last[t]then i[a.full_jid].ar_last[t]={};end +local t,h=c:join_room(t,a.nick,{source=a.full_jid}); +if not t then +return":"..e.." ERR :Could not join room: "..h +end +a.rooms[s]=t; +t.session=a; +if a.nicks_changing[a.nick]then +n.NAMES(a,s); +else +a.send{from=a.nick.."!"..a.username,"JOIN",s}; +if t.subject then +a.send{from=e,332,a.nick,s,t.subject}; +end +n.NAMES(a,s); +end +t:hook("subject-changed",function(e) +a.send{from=e.by.nick,"TOPIC",s,e.to or""} +end); +t:hook("message",function(e) +if not e.body then return end +local o,t=e.nick,e.body; +if o~=a.nick then +if t:sub(1,4)=="/me "then +t="\1ACTION "..t:sub(5).."\1" +end +local e=e.stanza.attr.type; +a.send{from=o,"PRIVMSG",e=="groupchat"and s or o,t}; +end +end); +t:hook("presence",function(t) +local h; +local r; +if t.nick and not i[a.full_jid].ar_last[t.room_jid][t.nick]then i[a.full_jid].ar_last[t.room_jid][t.nick]={}end +local n=t.stanza:get_child("x","http://jabber.org/protocol/muc#user") +if n then +local n=n:get_child("item") +if n and n.attr and t.stanza.attr.type~="unavailable"then +if n.attr.affiliation and n.attr.role then +if not i[a.full_jid].ar_last[t.room_jid][t.nick]["affiliation"]and +not i[a.full_jid].ar_last[t.room_jid][t.nick]["role"]then +i[a.full_jid].ar_last[t.room_jid][t.nick]["affiliation"]=n.attr.affiliation +i[a.full_jid].ar_last[t.room_jid][t.nick]["role"]=n.attr.role +n_self_changing=o[t.nick]and o[t.nick].nicks_changing and o[t.nick].nicks_changing[t.nick] +if n_self_changing then return;end +h,r=m(n.attr.affiliation,n.attr.role,t.nick); +if h and r then a.send((":%s MODE %s +%s"):format(e,s,h.." "..r));end +else +h,r=m(i[a.full_jid].ar_last[t.room_jid][t.nick]["affiliation"],i[a.full_jid].ar_last[t.room_jid][t.nick]["role"],t.nick); +if h and r then a.send((":%s MODE %s -%s"):format(e,s,h.." "..r));end +i[a.full_jid].ar_last[t.room_jid][t.nick]["affiliation"]=n.attr.affiliation +i[a.full_jid].ar_last[t.room_jid][t.nick]["role"]=n.attr.role +n_self_changing=o[t.nick]and o[t.nick].nicks_changing and o[t.nick].nicks_changing[t.nick] +if n_self_changing then return;end +h,r=m(n.attr.affiliation,n.attr.role,t.nick); +if h and r then a.send((":%s MODE %s +%s"):format(e,s,h.." "..r));end +end +end +end +end +end,-1); +end +c:hook("groupchat/joined",function(a) +local t=a.session or i[a.opts.source]; +local n="#"..a.jid:match("^(.*)@"); +a:hook("occupant-joined",function(e) +if t.nicks_changing[e.nick]then +t.send{from=t.nicks_changing[e.nick][1].."!"..(t.nicks_changing[e.nick][2]or"xmpp"),"NICK",e.nick}; +t.nicks_changing[e.nick]=nil; +else +t.send{from=e.nick.."!"..(o[e.nick]and o[e.nick].username or"xmpp"),"JOIN",n}; +end +end); +a:hook("occupant-left",function(e) +if i[t.full_jid]then i[t.full_jid].ar_last[e.jid:match("^(.*)/")][e.nick]=nil;end +local a= +e.presence:get_child("x","http://jabber.org/protocol/muc#user")and +e.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("status")and +e.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("status").attr.code; +if a=="303"then +local a= +e.presence:get_child("x","http://jabber.org/protocol/muc#user")and +e.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("item")and +e.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("item").attr.nick; +t.nicks_changing[a]={e.nick,(o[e.nick]and o[e.nick].username or"xmpp")};return; +end +for o,a in pairs(t.nicks_changing)do +if a[1]==e.nick then return;end +end +t.send{from=e.nick.."!"..(o[e.nick]and o[e.nick].username or"xmpp"),"PART",n}; +end); +end); +function n.NAMES(a,o) +local i={}; +if type(o)=="table"then o=o[1]end +local t=a.rooms[o]; +local n={ +owner="~", +administrator="&", +moderator="@", +member="+" +} +if not t then return end +for t,e in pairs(t.occupants)do +if e.affiliation=="owner"and e.role=="moderator"then +t=n[e.affiliation]..t; +elseif e.affiliation=="administrator"and e.role=="moderator"then +t=n[e.affiliation]..t; +elseif e.affiliation=="member"and e.role=="moderator"then +t=n[e.role]..t; +elseif e.affiliation=="member"and e.role=="partecipant"then +t=n[e.affiliation]..t; +elseif e.affiliation=="none"and e.role=="moderator"then +t=n[e.role]..t; +end +table.insert(i,t); +end +i=table.concat(i," "); +a.send((":%s 353 %s = %s :%s"):format(e,a.nick,o,i)); +a.send((":%s 366 %s %s :End of /NAMES list."):format(e,a.nick,o)); +a.send(":"..e.." 353 "..a.nick.." = "..o.." :"..i); +end +function n.PART(a,t) +local t,n=unpack(t); +local o=t and w(t:match("^#(%w+)"))or nil; +if not o then return end +t=t:match("^([%S]*)"); +a.rooms[t]:leave(n); +i[a.full_jid].ar_last[o.."@"..e]=nil; +a.send{from=a.nick.."!"..a.username,"PART",t}; +end +function n.PRIVMSG(a,e) +local t,e=unpack(e); +if e and#e>0 then +if e:sub(1,8)=="\1ACTION "then +e="/me "..e:sub(9,-2) +end +e=u(e); +if t:sub(1,1)=="#"then +if a.rooms[t]then +module:log("debug","%s sending PRIVMSG \"%s\" to %s",a.nick,e,t); +a.rooms[t]:send_message(e); +end +else +local t=t; +module:log("debug","PM to %s",t); +for o,a in pairs(a.rooms)do +module:log("debug","looking for %s in %s",t,o); +if a.occupants[t]then +module:log("debug","found %s in %s",t,o); +local o=a.occupants[t]; +local e=p.message({type="chat",to=o.jid},e); +module:log("debug","sending PM to %s: %s",t,tostring(e)); +a:send(e) +break +end +end +end +end +end +function n.PING(a,t) +a.send{from=e,"PONG",t[1]}; +end +function n.TOPIC(a,e) +if not e then return end +local e,t=e[1],e[2]; +e=u(e); +t=u(t); +if not e then return end +local e=a.rooms[e]; +if t then e:set_subject(t);end +end +function n.WHO(t,a) +local a=a[1]; +if t.rooms[a]then +local o=t.rooms[a] +for o in pairs(o.occupants)do +t.send{from=e,352,t.nick,a,o,o,e,o,"H","0 "..o} +end +t.send{from=e,315,t.nick,a,"End of /WHO list"}; +end +end +function n.MODE(e,e) +end +function n.QUIT(e,t) +e.send{"ERROR","Closing Link: "..e.nick}; +for a,e in pairs(e.rooms)do +e:leave(t[1]); +end +i[e.full_jid]=nil; +o[e.nick]=nil; +l[e.username]=nil; +d[e.conn]=nil; +e:close(); +end +function n.RAW(e,e) +end +module:provides("net",{ +name="ircd"; +listener=f; +default_port=7e3; +}); diff --git a/squishy b/squishy new file mode 100644 index 0000000..507b03e --- /dev/null +++ b/squishy @@ -0,0 +1,12 @@ +Output "mod_ircd.lua" + +verse_path = "verse" + +Module "verse" (verse_path.."/verse.lua") + +--Module "verse" (verse_path.."/init.lua") +--Module "verse.component" (verse_path.."/component.lua") +--Module "verse.plugins.groupchat" (verse_path.."/plugins/groupchat.lua") +--Module "verse.plugins.presence" (verse_path.."/plugins/presence.lua") + +Main "mod_ircd.in.lua" diff --git a/verse.orig/verse.lua b/verse.orig/verse.lua new file mode 100644 index 0000000..a968b7c --- /dev/null +++ b/verse.orig/verse.lua @@ -0,0 +1,580 @@ +package.preload['verse.plugins.presence'] = (function (...) +function verse.plugins.presence(stream) + stream.last_presence = nil; + + stream:hook("presence-out", function (presence) + if not presence.attr.to then + stream.last_presence = presence; -- Cache non-directed presence + end + end, 1); + + function stream:resend_presence() + if last_presence then + stream:send(last_presence); + end + end + + function stream:set_status(opts) + local p = verse.presence(); + if type(opts) == "table" then + if opts.show then + p:tag("show"):text(opts.show):up(); + end + if opts.prio then + p:tag("priority"):text(tostring(opts.prio)):up(); + end + if opts.msg then + p:tag("status"):text(opts.msg):up(); + end + end + -- TODO maybe use opts as prio if it's a int, + -- or as show or status if it's a string? + + stream:send(p); + end +end + end) +package.preload['verse.plugins.groupchat'] = (function (...) +local events = require "util.events"; + +local room_mt = {}; +room_mt.__index = room_mt; + +local xmlns_delay = "urn:xmpp:delay"; +local xmlns_muc = "http://jabber.org/protocol/muc"; + +function verse.plugins.groupchat(stream) + stream:add_plugin("presence") + stream.rooms = {}; + + stream:hook("stanza", function (stanza) + local room_jid = jid.bare(stanza.attr.from); + if not room_jid then return end + local room = stream.rooms[room_jid] + if not room and stanza.attr.to and room_jid then + room = stream.rooms[stanza.attr.to.." "..room_jid] + end + if room and room.opts.source and stanza.attr.to ~= room.opts.source then return end + if room then + local nick = select(3, jid.split(stanza.attr.from)); + local body = stanza:get_child_text("body"); + local delay = stanza:get_child("delay", xmlns_delay); + local event = { + room_jid = room_jid; + room = room; + sender = room.occupants[nick]; + nick = nick; + body = body; + stanza = stanza; + delay = (delay and delay.attr.stamp); + }; + local ret = room:event(stanza.name, event); + return ret or (stanza.name == "message") or nil; + end + end, 500); + + function stream:join_room(jid, nick, opts) + if not nick then + return false, "no nickname supplied" + end + opts = opts or {}; + local room = setmetatable({ + stream = stream, jid = jid, nick = nick, + subject = nil, + occupants = {}, + opts = opts, + events = events.new() + }, room_mt); + if opts.source then + self.rooms[opts.source.." "..jid] = room; + else + self.rooms[jid] = room; + end + local occupants = room.occupants; + room:hook("presence", function (presence) + local nick = presence.nick or nick; + if not occupants[nick] and presence.stanza.attr.type ~= "unavailable" then + occupants[nick] = { + nick = nick; + jid = presence.stanza.attr.from; + presence = presence.stanza; + }; + local x = presence.stanza:get_child("x", xmlns_muc .. "#user"); + if x then + local x_item = x:get_child("item"); + if x_item and x_item.attr then + occupants[nick].real_jid = x_item.attr.jid; + occupants[nick].affiliation = x_item.attr.affiliation; + occupants[nick].role = x_item.attr.role; + end + --TODO Check for status 100? + end + if nick == room.nick then + room.stream:event("groupchat/joined", room); + else + room:event("occupant-joined", occupants[nick]); + end + elseif occupants[nick] and presence.stanza.attr.type == "unavailable" then + if nick == room.nick then + room.stream:event("groupchat/left", room); + if room.opts.source then + self.rooms[room.opts.source.." "..jid] = nil; + else + self.rooms[jid] = nil; + end + else + occupants[nick].presence = presence.stanza; + room:event("occupant-left", occupants[nick]); + occupants[nick] = nil; + end + end + end); + room:hook("message", function(event) + local subject = event.stanza:get_child_text("subject"); + if not subject then return end + subject = #subject > 0 and subject or nil; + if subject ~= room.subject then + local old_subject = room.subject; + room.subject = subject; + return room:event("subject-changed", { from = old_subject, to = subject, by = event.sender, event = event }); + end + end, 2000); + local join_st = verse.presence():tag("x",{xmlns = xmlns_muc}):reset(); + self:event("pre-groupchat/joining", join_st); + room:send(join_st) + self:event("groupchat/joining", room); + return room; + end + + stream:hook("presence-out", function(presence) + if not presence.attr.to then + for _, room in pairs(stream.rooms) do + room:send(presence); + end + presence.attr.to = nil; + end + end); +end + +function room_mt:send(stanza) + if stanza.name == "message" and not stanza.attr.type then + stanza.attr.type = "groupchat"; + end + if stanza.name == "presence" then + stanza.attr.to = self.jid .."/"..self.nick; + end + if stanza.attr.type == "groupchat" or not stanza.attr.to then + stanza.attr.to = self.jid; + end + if self.opts.source then + stanza.attr.from = self.opts.source + end + self.stream:send(stanza); +end + +function room_mt:send_message(text) + self:send(verse.message():tag("body"):text(text)); +end + +function room_mt:set_subject(text) + self:send(verse.message():tag("subject"):text(text)); +end + +function room_mt:change_nick(new) + self.nick = new; + self:send(verse.presence()); +end + +function room_mt:leave(message) + self.stream:event("groupchat/leaving", self); + self:send(verse.presence({type="unavailable"})); +end + +function room_mt:admin_set(nick, what, value, reason) + self:send(verse.iq({type="set"}) + :query(xmlns_muc .. "#admin") + :tag("item", {nick = nick, [what] = value}) + :tag("reason"):text(reason or "")); +end + +function room_mt:set_role(nick, role, reason) + self:admin_set(nick, "role", role, reason); +end + +function room_mt:set_affiliation(nick, affiliation, reason) + self:admin_set(nick, "affiliation", affiliation, reason); +end + +function room_mt:kick(nick, reason) + self:set_role(nick, "none", reason); +end + +function room_mt:ban(nick, reason) + self:set_affiliation(nick, "outcast", reason); +end + +function room_mt:event(name, arg) + self.stream:debug("Firing room event: %s", name); + return self.events.fire_event(name, arg); +end + +function room_mt:hook(name, callback, priority) + return self.events.add_handler(name, callback, priority); +end + end) +package.preload['verse.component'] = (function (...) +local verse = require "verse"; +local stream = verse.stream_mt; + +local jid_split = require "util.jid".split; +local lxp = require "lxp"; +local st = require "util.stanza"; +local sha1 = require "util.sha1".sha1; + +-- Shortcuts to save having to load util.stanza +verse.message, verse.presence, verse.iq, verse.stanza, verse.reply, verse.error_reply = + st.message, st.presence, st.iq, st.stanza, st.reply, st.error_reply; + +local new_xmpp_stream = require "util.xmppstream".new; + +local xmlns_stream = "http://etherx.jabber.org/streams"; +local xmlns_component = "jabber:component:accept"; + +local stream_callbacks = { + stream_ns = xmlns_stream, + stream_tag = "stream", + default_ns = xmlns_component }; + +function stream_callbacks.streamopened(stream, attr) + stream.stream_id = attr.id; + if not stream:event("opened", attr) then + stream.notopen = nil; + end + return true; +end + +function stream_callbacks.streamclosed(stream) + return stream:event("closed"); +end + +function stream_callbacks.handlestanza(stream, stanza) + if stanza.attr.xmlns == xmlns_stream then + return stream:event("stream-"..stanza.name, stanza); + elseif stanza.attr.xmlns or stanza.name == "handshake" then + return stream:event("stream/"..(stanza.attr.xmlns or xmlns_component), stanza); + end + + return stream:event("stanza", stanza); +end + +function stream:reset() + if self.stream then + self.stream:reset(); + else + self.stream = new_xmpp_stream(self, stream_callbacks); + end + self.notopen = true; + return true; +end + +function stream:connect_component(jid, pass) + self.jid, self.password = jid, pass; + self.username, self.host, self.resource = jid_split(jid); + + function self.data(conn, data) + local ok, err = self.stream:feed(data); + if ok then return; end + stream:debug("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " ")); + stream:close("xml-not-well-formed"); + end + + self:hook("incoming-raw", function (data) return self.data(self.conn, data); end); + + self.curr_id = 0; + + self.tracked_iqs = {}; + self:hook("stanza", function (stanza) + local id, type = stanza.attr.id, stanza.attr.type; + if id and stanza.name == "iq" and (type == "result" or type == "error") and self.tracked_iqs[id] then + self.tracked_iqs[id](stanza); + self.tracked_iqs[id] = nil; + return true; + end + end); + + self:hook("stanza", function (stanza) + if stanza.attr.xmlns == nil or stanza.attr.xmlns == "jabber:client" then + if stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then + local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns; + if xmlns then + ret = self:event("iq/"..xmlns, stanza); + if not ret then + ret = self:event("iq", stanza); + end + end + if ret == nil then + self:send(verse.error_reply(stanza, "cancel", "service-unavailable")); + return true; + end + else + ret = self:event(stanza.name, stanza); + end + end + return ret; + end, -1); + + self:hook("opened", function (attr) + print(self.jid, self.stream_id, attr.id); + local token = sha1(self.stream_id..pass, true); + + self:send(st.stanza("handshake", { xmlns = xmlns_component }):text(token)); + self:hook("stream/"..xmlns_component, function (stanza) + if stanza.name == "handshake" then + self:event("authentication-success"); + end + end); + end); + + local function stream_ready() + self:event("ready"); + end + self:hook("authentication-success", stream_ready, -1); + + -- Initialise connection + self:connect(self.connect_host or self.host, self.connect_port or 5347); + self:reopen(); +end + +function stream:reopen() + self:reset(); + self:send(st.stanza("stream:stream", { to = self.host, ["xmlns:stream"]='http://etherx.jabber.org/streams', + xmlns = xmlns_component, version = "1.0" }):top_tag()); +end + +function stream:close(reason) + if not self.notopen then + self:send(""); + end + local on_disconnect = self.conn.disconnect(); + self.conn:close(); + on_disconnect(conn, reason); +end + +function stream:send_iq(iq, callback) + local id = self:new_id(); + self.tracked_iqs[id] = callback; + iq.attr.id = id; + self:send(iq); +end + +function stream:new_id() + self.curr_id = self.curr_id + 1; + return tostring(self.curr_id); +end + end) + +-- Use LuaRocks if available +pcall(require, "luarocks.require"); + +-- Load LuaSec if available +pcall(require, "ssl"); + +local server = require "net.server"; +local events = require "util.events"; +local logger = require "util.logger"; + +module("verse", package.seeall); +local verse = _M; +_M.server = server; + +local stream = {}; +stream.__index = stream; +stream_mt = stream; + +verse.plugins = {}; + +local max_id = 0; + +function verse.new(logger, base) + local t = setmetatable(base or {}, stream); + max_id = max_id + 1; + t.id = tostring(max_id); + t.logger = logger or verse.new_logger("stream"..t.id); + t.events = events.new(); + t.plugins = {}; + t.verse = verse; + return t; +end + +verse.add_task = require "util.timer".add_task; + +verse.logger = logger.init; -- COMPAT: Deprecated +verse.new_logger = logger.init; +verse.log = verse.logger("verse"); + +local function format(format, ...) + local n, arg, maxn = 0, { ... }, select('#', ...); + return (format:gsub("%%(.)", function (c) if n <= maxn then n = n + 1; return tostring(arg[n]); end end)); +end + +function verse.set_log_handler(log_handler, levels) + levels = levels or { "debug", "info", "warn", "error" }; + logger.reset(); + local function _log_handler(name, level, message, ...) + return log_handler(name, level, format(message, ...)); + end + if log_handler then + for i, level in ipairs(levels) do + logger.add_level_sink(level, _log_handler); + end + end +end + +function _default_log_handler(name, level, message) + return io.stderr:write(name, "\t", level, "\t", message, "\n"); +end +verse.set_log_handler(_default_log_handler, { "error" }); + +local function error_handler(err) + verse.log("error", "Error: %s", err); + verse.log("error", "Traceback: %s", debug.traceback()); +end + +function verse.set_error_handler(new_error_handler) + error_handler = new_error_handler; +end + +function verse.loop() + return xpcall(server.loop, error_handler); +end + +function verse.step() + return xpcall(server.step, error_handler); +end + +function verse.quit() + return server.setquitting(true); +end + +function stream:connect(connect_host, connect_port) + connect_host = connect_host or "localhost"; + connect_port = tonumber(connect_port) or 5222; + + -- Create and initiate connection + local conn = socket.tcp() + conn:settimeout(0); + local success, err = conn:connect(connect_host, connect_port); + + if not success and err ~= "timeout" then + self:warn("connect() to %s:%d failed: %s", connect_host, connect_port, err); + return self:event("disconnected", { reason = err }) or false, err; + end + + local conn = server.wrapclient(conn, connect_host, connect_port, new_listener(self), "*a"); + if not conn then + self:warn("connection initialisation failed: %s", err); + return self:event("disconnected", { reason = err }) or false, err; + end + + self.conn = conn; + self.send = function (stream, data) + self:event("outgoing", data); + data = tostring(data); + self:event("outgoing-raw", data); + return conn:write(data); + end; + return true; +end + +function stream:close() + if not self.conn then + verse.log("error", "Attempt to close disconnected connection - possibly a bug"); + return; + end + local on_disconnect = self.conn.disconnect(); + self.conn:close(); + on_disconnect(conn, reason); +end + +-- Logging functions +function stream:debug(...) + return self.logger("debug", ...); +end + +function stream:warn(...) + return self.logger("warn", ...); +end + +function stream:error(...) + return self.logger("error", ...); +end + +-- Event handling +function stream:event(name, ...) + self:debug("Firing event: "..tostring(name)); + return self.events.fire_event(name, ...); +end + +function stream:hook(name, ...) + return self.events.add_handler(name, ...); +end + +function stream:unhook(name, handler) + return self.events.remove_handler(name, handler); +end + +function verse.eventable(object) + object.events = events.new(); + object.hook, object.unhook = stream.hook, stream.unhook; + local fire_event = object.events.fire_event; + function object:event(name, ...) + return fire_event(name, ...); + end + return object; +end + +function stream:add_plugin(name) + if self.plugins[name] then return true; end + if require("verse.plugins."..name) then + local ok, err = verse.plugins[name](self); + if ok ~= false then + self:debug("Loaded %s plugin", name); + self.plugins[name] = true; + else + self:warn("Failed to load %s plugin: %s", name, err); + end + end + return self; +end + +-- Listener factory +function new_listener(stream) + local conn_listener = {}; + + function conn_listener.onconnect(conn) + stream.connected = true; + stream:event("connected"); + end + + function conn_listener.onincoming(conn, data) + stream:event("incoming-raw", data); + end + + function conn_listener.ondisconnect(conn, err) + stream.connected = false; + stream:event("disconnected", { reason = err }); + end + + function conn_listener.ondrain(conn) + stream:event("drained"); + end + + function conn_listener.onstatus(conn, new_status) + stream:event("status", new_status); + end + + return conn_listener; +end + +return verse; + diff --git a/verse/.hg/00changelog.i b/verse/.hg/00changelog.i new file mode 100644 index 0000000..d3a8311 Binary files /dev/null and b/verse/.hg/00changelog.i differ diff --git a/verse/.hg/branch b/verse/.hg/branch new file mode 100644 index 0000000..4ad96d5 --- /dev/null +++ b/verse/.hg/branch @@ -0,0 +1 @@ +default diff --git a/verse/.hg/cache/branchheads-served b/verse/.hg/cache/branchheads-served new file mode 100644 index 0000000..1ecc197 --- /dev/null +++ b/verse/.hg/cache/branchheads-served @@ -0,0 +1,2 @@ +154c2f04d73b0a3ba1d4e0b12295c804f0fc3927 368 +154c2f04d73b0a3ba1d4e0b12295c804f0fc3927 default diff --git a/verse/.hg/dirstate b/verse/.hg/dirstate new file mode 100644 index 0000000..92d506d Binary files /dev/null and b/verse/.hg/dirstate differ diff --git a/verse/.hg/hgrc b/verse/.hg/hgrc new file mode 100644 index 0000000..9513652 --- /dev/null +++ b/verse/.hg/hgrc @@ -0,0 +1,2 @@ +[paths] +default = http://code.matthewwild.co.uk/verse diff --git a/verse/.hg/requires b/verse/.hg/requires new file mode 100644 index 0000000..f634f66 --- /dev/null +++ b/verse/.hg/requires @@ -0,0 +1,4 @@ +dotencode +fncache +revlogv1 +store diff --git a/verse/.hg/store/00changelog.i b/verse/.hg/store/00changelog.i new file mode 100644 index 0000000..edb4179 Binary files /dev/null and b/verse/.hg/store/00changelog.i differ diff --git a/verse/.hg/store/00manifest.i b/verse/.hg/store/00manifest.i new file mode 100644 index 0000000..fba09a2 Binary files /dev/null and b/verse/.hg/store/00manifest.i differ diff --git a/verse/.hg/store/data/_l_i_c_e_n_s_e.i b/verse/.hg/store/data/_l_i_c_e_n_s_e.i new file mode 100644 index 0000000..a0adb67 Binary files /dev/null and b/verse/.hg/store/data/_l_i_c_e_n_s_e.i differ diff --git a/verse/.hg/store/data/bosh.lua.i b/verse/.hg/store/data/bosh.lua.i new file mode 100644 index 0000000..49eca4d Binary files /dev/null and b/verse/.hg/store/data/bosh.lua.i differ diff --git a/verse/.hg/store/data/client.lua.i b/verse/.hg/store/data/client.lua.i new file mode 100644 index 0000000..e92aa86 Binary files /dev/null and b/verse/.hg/store/data/client.lua.i differ diff --git a/verse/.hg/store/data/component.lua.i b/verse/.hg/store/data/component.lua.i new file mode 100644 index 0000000..ba36a4b Binary files /dev/null and b/verse/.hg/store/data/component.lua.i differ diff --git a/verse/.hg/store/data/doc/example.lua.i b/verse/.hg/store/data/doc/example.lua.i new file mode 100644 index 0000000..f854951 Binary files /dev/null and b/verse/.hg/store/data/doc/example.lua.i differ diff --git a/verse/.hg/store/data/doc/example__adhoc.lua.i b/verse/.hg/store/data/doc/example__adhoc.lua.i new file mode 100644 index 0000000..37e4735 Binary files /dev/null and b/verse/.hg/store/data/doc/example__adhoc.lua.i differ diff --git a/verse/.hg/store/data/doc/example__bosh.lua.i b/verse/.hg/store/data/doc/example__bosh.lua.i new file mode 100644 index 0000000..fe31057 Binary files /dev/null and b/verse/.hg/store/data/doc/example__bosh.lua.i differ diff --git a/verse/.hg/store/data/doc/example__carbons.lua.i b/verse/.hg/store/data/doc/example__carbons.lua.i new file mode 100644 index 0000000..1aa9c13 Binary files /dev/null and b/verse/.hg/store/data/doc/example__carbons.lua.i differ diff --git a/verse/.hg/store/data/doc/example__component.lua.i b/verse/.hg/store/data/doc/example__component.lua.i new file mode 100644 index 0000000..566422a Binary files /dev/null and b/verse/.hg/store/data/doc/example__component.lua.i differ diff --git a/verse/.hg/store/data/doc/example__jingle.lua.i b/verse/.hg/store/data/doc/example__jingle.lua.i new file mode 100644 index 0000000..d68c629 Binary files /dev/null and b/verse/.hg/store/data/doc/example__jingle.lua.i differ diff --git a/verse/.hg/store/data/doc/example__jingle__send.lua.i b/verse/.hg/store/data/doc/example__jingle__send.lua.i new file mode 100644 index 0000000..f74d874 Binary files /dev/null and b/verse/.hg/store/data/doc/example__jingle__send.lua.i differ diff --git a/verse/.hg/store/data/doc/example__pep.lua.i b/verse/.hg/store/data/doc/example__pep.lua.i new file mode 100644 index 0000000..4c4e2ba Binary files /dev/null and b/verse/.hg/store/data/doc/example__pep.lua.i differ diff --git a/verse/.hg/store/data/doc/example__pubsub.lua.i b/verse/.hg/store/data/doc/example__pubsub.lua.i new file mode 100644 index 0000000..5b89bd0 Binary files /dev/null and b/verse/.hg/store/data/doc/example__pubsub.lua.i differ diff --git a/verse/.hg/store/data/init.lua.i b/verse/.hg/store/data/init.lua.i new file mode 100644 index 0000000..e83d430 Binary files /dev/null and b/verse/.hg/store/data/init.lua.i differ diff --git a/verse/.hg/store/data/libs/adhoc.lib.lua.i b/verse/.hg/store/data/libs/adhoc.lib.lua.i new file mode 100644 index 0000000..7e5e191 Binary files /dev/null and b/verse/.hg/store/data/libs/adhoc.lib.lua.i differ diff --git a/verse/.hg/store/data/libs/bit.lua.i b/verse/.hg/store/data/libs/bit.lua.i new file mode 100644 index 0000000..e992575 Binary files /dev/null and b/verse/.hg/store/data/libs/bit.lua.i differ diff --git a/verse/.hg/store/data/libs/encodings.lua.i b/verse/.hg/store/data/libs/encodings.lua.i new file mode 100644 index 0000000..aa9905e Binary files /dev/null and b/verse/.hg/store/data/libs/encodings.lua.i differ diff --git a/verse/.hg/store/data/libs/hashes.lua.i b/verse/.hg/store/data/libs/hashes.lua.i new file mode 100644 index 0000000..20ee870 Binary files /dev/null and b/verse/.hg/store/data/libs/hashes.lua.i differ diff --git a/verse/.hg/store/data/libs/logger.lua.i b/verse/.hg/store/data/libs/logger.lua.i new file mode 100644 index 0000000..1b130df Binary files /dev/null and b/verse/.hg/store/data/libs/logger.lua.i differ diff --git a/verse/.hg/store/data/libs/xstanza.lua.i b/verse/.hg/store/data/libs/xstanza.lua.i new file mode 100644 index 0000000..6b756e8 Binary files /dev/null and b/verse/.hg/store/data/libs/xstanza.lua.i differ diff --git a/verse/.hg/store/data/plugins/adhoc.lua.i b/verse/.hg/store/data/plugins/adhoc.lua.i new file mode 100644 index 0000000..fab5633 Binary files /dev/null and b/verse/.hg/store/data/plugins/adhoc.lua.i differ diff --git a/verse/.hg/store/data/plugins/archive.lua.i b/verse/.hg/store/data/plugins/archive.lua.i new file mode 100644 index 0000000..59fb467 Binary files /dev/null and b/verse/.hg/store/data/plugins/archive.lua.i differ diff --git a/verse/.hg/store/data/plugins/bind.lua.i b/verse/.hg/store/data/plugins/bind.lua.i new file mode 100644 index 0000000..a820c8d Binary files /dev/null and b/verse/.hg/store/data/plugins/bind.lua.i differ diff --git a/verse/.hg/store/data/plugins/blocking.lua.i b/verse/.hg/store/data/plugins/blocking.lua.i new file mode 100644 index 0000000..f3ac101 Binary files /dev/null and b/verse/.hg/store/data/plugins/blocking.lua.i differ diff --git a/verse/.hg/store/data/plugins/carbons.lua.i b/verse/.hg/store/data/plugins/carbons.lua.i new file mode 100644 index 0000000..760d226 Binary files /dev/null and b/verse/.hg/store/data/plugins/carbons.lua.i differ diff --git a/verse/.hg/store/data/plugins/compression.lua.i b/verse/.hg/store/data/plugins/compression.lua.i new file mode 100644 index 0000000..42b73ee Binary files /dev/null and b/verse/.hg/store/data/plugins/compression.lua.i differ diff --git a/verse/.hg/store/data/plugins/disco.lua.i b/verse/.hg/store/data/plugins/disco.lua.i new file mode 100644 index 0000000..35bfd5d Binary files /dev/null and b/verse/.hg/store/data/plugins/disco.lua.i differ diff --git a/verse/.hg/store/data/plugins/groupchat.lua.i b/verse/.hg/store/data/plugins/groupchat.lua.i new file mode 100644 index 0000000..4ca9bde Binary files /dev/null and b/verse/.hg/store/data/plugins/groupchat.lua.i differ diff --git a/verse/.hg/store/data/plugins/jingle.lua.i b/verse/.hg/store/data/plugins/jingle.lua.i new file mode 100644 index 0000000..4bfa049 Binary files /dev/null and b/verse/.hg/store/data/plugins/jingle.lua.i differ diff --git a/verse/.hg/store/data/plugins/jingle__ft.lua.i b/verse/.hg/store/data/plugins/jingle__ft.lua.i new file mode 100644 index 0000000..c6f0c0d Binary files /dev/null and b/verse/.hg/store/data/plugins/jingle__ft.lua.i differ diff --git a/verse/.hg/store/data/plugins/jingle__ibb.lua.i b/verse/.hg/store/data/plugins/jingle__ibb.lua.i new file mode 100644 index 0000000..3a78439 Binary files /dev/null and b/verse/.hg/store/data/plugins/jingle__ibb.lua.i differ diff --git a/verse/.hg/store/data/plugins/jingle__s5b.lua.i b/verse/.hg/store/data/plugins/jingle__s5b.lua.i new file mode 100644 index 0000000..c0b2590 Binary files /dev/null and b/verse/.hg/store/data/plugins/jingle__s5b.lua.i differ diff --git a/verse/.hg/store/data/plugins/keepalive.lua.i b/verse/.hg/store/data/plugins/keepalive.lua.i new file mode 100644 index 0000000..281b5a0 Binary files /dev/null and b/verse/.hg/store/data/plugins/keepalive.lua.i differ diff --git a/verse/.hg/store/data/plugins/legacy.lua.i b/verse/.hg/store/data/plugins/legacy.lua.i new file mode 100644 index 0000000..4b196d0 Binary files /dev/null and b/verse/.hg/store/data/plugins/legacy.lua.i differ diff --git a/verse/.hg/store/data/plugins/pep.lua.i b/verse/.hg/store/data/plugins/pep.lua.i new file mode 100644 index 0000000..e0dc561 Binary files /dev/null and b/verse/.hg/store/data/plugins/pep.lua.i differ diff --git a/verse/.hg/store/data/plugins/ping.lua.i b/verse/.hg/store/data/plugins/ping.lua.i new file mode 100644 index 0000000..24a5a86 Binary files /dev/null and b/verse/.hg/store/data/plugins/ping.lua.i differ diff --git a/verse/.hg/store/data/plugins/presence.lua.i b/verse/.hg/store/data/plugins/presence.lua.i new file mode 100644 index 0000000..8ab39a6 Binary files /dev/null and b/verse/.hg/store/data/plugins/presence.lua.i differ diff --git a/verse/.hg/store/data/plugins/private.lua.i b/verse/.hg/store/data/plugins/private.lua.i new file mode 100644 index 0000000..0219852 Binary files /dev/null and b/verse/.hg/store/data/plugins/private.lua.i differ diff --git a/verse/.hg/store/data/plugins/proxy65.lua.i b/verse/.hg/store/data/plugins/proxy65.lua.i new file mode 100644 index 0000000..1c34633 Binary files /dev/null and b/verse/.hg/store/data/plugins/proxy65.lua.i differ diff --git a/verse/.hg/store/data/plugins/pubsub.lua.i b/verse/.hg/store/data/plugins/pubsub.lua.i new file mode 100644 index 0000000..13d97e7 Binary files /dev/null and b/verse/.hg/store/data/plugins/pubsub.lua.i differ diff --git a/verse/.hg/store/data/plugins/receipts.lua.i b/verse/.hg/store/data/plugins/receipts.lua.i new file mode 100644 index 0000000..bf949e8 Binary files /dev/null and b/verse/.hg/store/data/plugins/receipts.lua.i differ diff --git a/verse/.hg/store/data/plugins/register.lua.i b/verse/.hg/store/data/plugins/register.lua.i new file mode 100644 index 0000000..8e628d7 Binary files /dev/null and b/verse/.hg/store/data/plugins/register.lua.i differ diff --git a/verse/.hg/store/data/plugins/roster.lua.i b/verse/.hg/store/data/plugins/roster.lua.i new file mode 100644 index 0000000..0404e57 Binary files /dev/null and b/verse/.hg/store/data/plugins/roster.lua.i differ diff --git a/verse/.hg/store/data/plugins/sasl.lua.i b/verse/.hg/store/data/plugins/sasl.lua.i new file mode 100644 index 0000000..c5f04cf Binary files /dev/null and b/verse/.hg/store/data/plugins/sasl.lua.i differ diff --git a/verse/.hg/store/data/plugins/session.lua.i b/verse/.hg/store/data/plugins/session.lua.i new file mode 100644 index 0000000..813cae4 Binary files /dev/null and b/verse/.hg/store/data/plugins/session.lua.i differ diff --git a/verse/.hg/store/data/plugins/smacks.lua.i b/verse/.hg/store/data/plugins/smacks.lua.i new file mode 100644 index 0000000..4189751 Binary files /dev/null and b/verse/.hg/store/data/plugins/smacks.lua.i differ diff --git a/verse/.hg/store/data/plugins/tls.lua.i b/verse/.hg/store/data/plugins/tls.lua.i new file mode 100644 index 0000000..2a0feda Binary files /dev/null and b/verse/.hg/store/data/plugins/tls.lua.i differ diff --git a/verse/.hg/store/data/plugins/uptime.lua.i b/verse/.hg/store/data/plugins/uptime.lua.i new file mode 100644 index 0000000..dfc5cef Binary files /dev/null and b/verse/.hg/store/data/plugins/uptime.lua.i differ diff --git a/verse/.hg/store/data/plugins/vcard.lua.i b/verse/.hg/store/data/plugins/vcard.lua.i new file mode 100644 index 0000000..cd03de7 Binary files /dev/null and b/verse/.hg/store/data/plugins/vcard.lua.i differ diff --git a/verse/.hg/store/data/plugins/vcard__update.lua.i b/verse/.hg/store/data/plugins/vcard__update.lua.i new file mode 100644 index 0000000..b354621 Binary files /dev/null and b/verse/.hg/store/data/plugins/vcard__update.lua.i differ diff --git a/verse/.hg/store/data/plugins/version.lua.i b/verse/.hg/store/data/plugins/version.lua.i new file mode 100644 index 0000000..81680cf Binary files /dev/null and b/verse/.hg/store/data/plugins/version.lua.i differ diff --git a/verse/.hg/store/data/squishy.i b/verse/.hg/store/data/squishy.i new file mode 100644 index 0000000..941c09a Binary files /dev/null and b/verse/.hg/store/data/squishy.i differ diff --git a/verse/.hg/store/data/util/dataforms.lua.i b/verse/.hg/store/data/util/dataforms.lua.i new file mode 100644 index 0000000..0c7d626 Binary files /dev/null and b/verse/.hg/store/data/util/dataforms.lua.i differ diff --git a/verse/.hg/store/data/util/sasl/anonymous.lua.i b/verse/.hg/store/data/util/sasl/anonymous.lua.i new file mode 100644 index 0000000..055bc56 Binary files /dev/null and b/verse/.hg/store/data/util/sasl/anonymous.lua.i differ diff --git a/verse/.hg/store/data/util/sasl/plain.lua.i b/verse/.hg/store/data/util/sasl/plain.lua.i new file mode 100644 index 0000000..b76b2c5 Binary files /dev/null and b/verse/.hg/store/data/util/sasl/plain.lua.i differ diff --git a/verse/.hg/store/data/util/sasl/scram.lua.i b/verse/.hg/store/data/util/sasl/scram.lua.i new file mode 100644 index 0000000..2f06ab1 Binary files /dev/null and b/verse/.hg/store/data/util/sasl/scram.lua.i differ diff --git a/verse/.hg/store/data/util/sha1.lua.i b/verse/.hg/store/data/util/sha1.lua.i new file mode 100644 index 0000000..bf68b61 Binary files /dev/null and b/verse/.hg/store/data/util/sha1.lua.i differ diff --git a/verse/.hg/store/data/util/vcard.lua.i b/verse/.hg/store/data/util/vcard.lua.i new file mode 100644 index 0000000..cd45d5a Binary files /dev/null and b/verse/.hg/store/data/util/vcard.lua.i differ diff --git a/verse/.hg/store/fncache b/verse/.hg/store/fncache new file mode 100644 index 0000000..0108cf5 --- /dev/null +++ b/verse/.hg/store/fncache @@ -0,0 +1,58 @@ +data/client.lua.i +data/util/sasl/plain.lua.i +data/plugins/private.lua.i +data/plugins/version.lua.i +data/util/sasl/scram.lua.i +data/plugins/legacy.lua.i +data/plugins/jingle_s5b.lua.i +data/bosh.lua.i +data/doc/example_adhoc.lua.i +data/plugins/roster.lua.i +data/plugins/jingle_ft.lua.i +data/doc/example_bosh.lua.i +data/doc/example_component.lua.i +data/plugins/vcard_update.lua.i +data/plugins/vcard.lua.i +data/plugins/ping.lua.i +data/plugins/session.lua.i +data/plugins/adhoc.lua.i +data/libs/logger.lua.i +data/plugins/uptime.lua.i +data/plugins/pubsub.lua.i +data/libs/xstanza.lua.i +data/doc/example_jingle_send.lua.i +data/plugins/jingle.lua.i +data/plugins/smacks.lua.i +data/doc/example_jingle.lua.i +data/plugins/register.lua.i +data/plugins/carbons.lua.i +data/plugins/jingle_ibb.lua.i +data/init.lua.i +data/plugins/presence.lua.i +data/plugins/blocking.lua.i +data/plugins/pep.lua.i +data/libs/hashes.lua.i +data/squishy.i +data/plugins/bind.lua.i +data/plugins/tls.lua.i +data/doc/example_pep.lua.i +data/util/sha1.lua.i +data/doc/example.lua.i +data/libs/bit.lua.i +data/doc/example_pubsub.lua.i +data/doc/example_carbons.lua.i +data/plugins/receipts.lua.i +data/libs/adhoc.lib.lua.i +data/LICENSE.i +data/plugins/disco.lua.i +data/plugins/archive.lua.i +data/util/sasl/anonymous.lua.i +data/component.lua.i +data/plugins/sasl.lua.i +data/plugins/groupchat.lua.i +data/util/vcard.lua.i +data/plugins/compression.lua.i +data/plugins/proxy65.lua.i +data/util/dataforms.lua.i +data/libs/encodings.lua.i +data/plugins/keepalive.lua.i diff --git a/verse/.hg/store/phaseroots b/verse/.hg/store/phaseroots new file mode 100644 index 0000000..e69de29 diff --git a/verse/.hg/store/undo b/verse/.hg/store/undo new file mode 100644 index 0000000..2583bfc Binary files /dev/null and b/verse/.hg/store/undo differ diff --git a/verse/.hg/store/undo.phaseroots b/verse/.hg/store/undo.phaseroots new file mode 100644 index 0000000..e69de29 diff --git a/verse/.hg/undo.bookmarks b/verse/.hg/undo.bookmarks new file mode 100644 index 0000000..e69de29 diff --git a/verse/.hg/undo.branch b/verse/.hg/undo.branch new file mode 100644 index 0000000..331d858 --- /dev/null +++ b/verse/.hg/undo.branch @@ -0,0 +1 @@ +default \ No newline at end of file diff --git a/verse/.hg/undo.desc b/verse/.hg/undo.desc new file mode 100644 index 0000000..421e491 --- /dev/null +++ b/verse/.hg/undo.desc @@ -0,0 +1,3 @@ +0 +pull +http://code.matthewwild.co.uk/verse diff --git a/verse/.hg/undo.dirstate b/verse/.hg/undo.dirstate new file mode 100644 index 0000000..e69de29 diff --git a/verse/LICENSE b/verse/LICENSE new file mode 100644 index 0000000..a60125d --- /dev/null +++ b/verse/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2009-2010 Matthew Wild + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/verse/bosh.lua b/verse/bosh.lua new file mode 100644 index 0000000..ebedfd9 --- /dev/null +++ b/verse/bosh.lua @@ -0,0 +1,209 @@ + +local new_xmpp_stream = require "util.xmppstream".new; +local st = require "util.stanza"; +require "net.httpclient_listener"; -- Required for net.http to work +local http = require "net.http"; + +local stream_mt = setmetatable({}, { __index = verse.stream_mt }); +stream_mt.__index = stream_mt; + +local xmlns_stream = "http://etherx.jabber.org/streams"; +local xmlns_bosh = "http://jabber.org/protocol/httpbind"; + +local reconnect_timeout = 5; + +function verse.new_bosh(logger, url) + local stream = { + bosh_conn_pool = {}; + bosh_waiting_requests = {}; + bosh_rid = math.random(1,999999); + bosh_outgoing_buffer = {}; + bosh_url = url; + conn = {}; + }; + function stream:reopen() + self.bosh_need_restart = true; + self:flush(); + end + local conn = verse.new(logger, stream); + return setmetatable(conn, stream_mt); +end + +function stream_mt:connect() + self:_send_session_request(); +end + +function stream_mt:send(data) + self:debug("Putting into BOSH send buffer: %s", tostring(data)); + self.bosh_outgoing_buffer[#self.bosh_outgoing_buffer+1] = st.clone(data); + self:flush(); --TODO: Optimize by doing this on next tick (give a chance for data to buffer) +end + +function stream_mt:flush() + if self.connected + and #self.bosh_waiting_requests < self.bosh_max_requests + and (#self.bosh_waiting_requests == 0 + or #self.bosh_outgoing_buffer > 0 + or self.bosh_need_restart) then + self:debug("Flushing..."); + local payload = self:_make_body(); + local buffer = self.bosh_outgoing_buffer; + for i, stanza in ipairs(buffer) do + payload:add_child(stanza); + buffer[i] = nil; + end + self:_make_request(payload); + else + self:debug("Decided not to flush."); + end +end + +function stream_mt:_make_request(payload) + local request, err = http.request(self.bosh_url, { body = tostring(payload) }, function (response, code, request) + if code ~= 0 then + self.inactive_since = nil; + return self:_handle_response(response, code, request); + end + + -- Connection issues, we need to retry this request + local time = os.time(); + if not self.inactive_since then + self.inactive_since = time; -- So we know when it is time to give up + elseif time - self.inactive_since > self.bosh_max_inactivity then + return self:_disconnected(); + else + self:debug("%d seconds left to reconnect, retrying in %d seconds...", + self.bosh_max_inactivity - (time - self.inactive_since), reconnect_timeout); + end + + -- Set up reconnect timer + timer.add_task(reconnect_timeout, function () + self:debug("Retrying request..."); + -- Remove old request + for i, waiting_request in ipairs(self.bosh_waiting_requests) do + if waiting_request == request then + table.remove(self.bosh_waiting_requests, i); + break; + end + end + self:_make_request(payload); + end); + end); + if request then + table.insert(self.bosh_waiting_requests, request); + else + self:warn("Request failed instantly: %s", err); + end +end + +function stream_mt:_disconnected() + self.connected = nil; + self:event("disconnected"); +end + +function stream_mt:_send_session_request() + local body = self:_make_body(); + + -- XEP-0124 + body.attr.hold = "1"; + body.attr.wait = "60"; + body.attr["xml:lang"] = "en"; + body.attr.ver = "1.6"; + + -- XEP-0206 + body.attr.from = self.jid; + body.attr.to = self.host; + body.attr.secure = 'true'; + + http.request(self.bosh_url, { body = tostring(body) }, function (response, code) + if code == 0 then + -- Failed to connect + return self:_disconnected(); + end + -- Handle session creation response + local payload = self:_parse_response(response) + if not payload then + self:warn("Invalid session creation response"); + self:_disconnected(); + return; + end + self.bosh_sid = payload.attr.sid; -- Session id + self.bosh_wait = tonumber(payload.attr.wait); -- How long the server may hold connections for + self.bosh_hold = tonumber(payload.attr.hold); -- How many connections the server may hold + self.bosh_max_inactivity = tonumber(payload.attr.inactivity); -- Max amount of time with no connections + self.bosh_max_requests = tonumber(payload.attr.requests) or self.bosh_hold; -- Max simultaneous requests we can make + self.connected = true; + self:event("connected"); + self:_handle_response_payload(payload); + end); +end + +function stream_mt:_handle_response(response, code, request) + if self.bosh_waiting_requests[1] ~= request then + self:warn("Server replied to request that wasn't the oldest"); + for i, waiting_request in ipairs(self.bosh_waiting_requests) do + if waiting_request == request then + self.bosh_waiting_requests[i] = nil; + break; + end + end + else + table.remove(self.bosh_waiting_requests, 1); + end + local payload = self:_parse_response(response); + if payload then + self:_handle_response_payload(payload); + end + self:flush(); +end + +function stream_mt:_handle_response_payload(payload) + local stanzas = payload.tags; + for i = 1, #stanzas do + local stanza = stanzas[i]; + if stanza.attr.xmlns == xmlns_stream then + self:event("stream-"..stanza.name, stanza); + elseif stanza.attr.xmlns then + self:event("stream/"..stanza.attr.xmlns, stanza); + else + self:event("stanza", stanza); + end + end + if payload.attr.type == "terminate" then + self:_disconnected({reason = payload.attr.condition}); + end +end + +local stream_callbacks = { + stream_ns = "http://jabber.org/protocol/httpbind", stream_tag = "body", + default_ns = "jabber:client", + streamopened = function (session, attr) session.notopen = nil; session.payload = verse.stanza("body", attr); return true; end; + handlestanza = function (session, stanza) session.payload:add_child(stanza); end; +}; +function stream_mt:_parse_response(response) + self:debug("Parsing response: %s", response); + if response == nil then + self:debug("%s", debug.traceback()); + self:_disconnected(); + return; + end + local session = { notopen = true, stream = self }; + local stream = new_xmpp_stream(session, stream_callbacks); + stream:feed(response); + return session.payload; +end + +function stream_mt:_make_body() + self.bosh_rid = self.bosh_rid + 1; + local body = verse.stanza("body", { + xmlns = xmlns_bosh; + content = "text/xml; charset=utf-8"; + sid = self.bosh_sid; + rid = self.bosh_rid; + }); + if self.bosh_need_restart then + self.bosh_need_restart = nil; + body.attr.restart = 'true'; + end + return body; +end diff --git a/verse/client.lua b/verse/client.lua new file mode 100644 index 0000000..fd2eae6 --- /dev/null +++ b/verse/client.lua @@ -0,0 +1,219 @@ +local verse = require "verse"; +local stream = verse.stream_mt; + +local jid_split = require "util.jid".split; +local adns = require "net.adns"; +local lxp = require "lxp"; +local st = require "util.stanza"; + +-- Shortcuts to save having to load util.stanza +verse.message, verse.presence, verse.iq, verse.stanza, verse.reply, verse.error_reply = + st.message, st.presence, st.iq, st.stanza, st.reply, st.error_reply; + +local new_xmpp_stream = require "util.xmppstream".new; + +local xmlns_stream = "http://etherx.jabber.org/streams"; + +local function compare_srv_priorities(a,b) + return a.priority < b.priority or (a.priority == b.priority and a.weight > b.weight); +end + +local stream_callbacks = { + stream_ns = xmlns_stream, + stream_tag = "stream", + default_ns = "jabber:client" }; + +function stream_callbacks.streamopened(stream, attr) + stream.stream_id = attr.id; + if not stream:event("opened", attr) then + stream.notopen = nil; + end + return true; +end + +function stream_callbacks.streamclosed(stream) + stream.notopen = true; + if not stream.closed then + stream:send(""); + stream.closed = true; + end + stream:event("closed"); + return stream:close("stream closed") +end + +function stream_callbacks.handlestanza(stream, stanza) + if stanza.attr.xmlns == xmlns_stream then + return stream:event("stream-"..stanza.name, stanza); + elseif stanza.attr.xmlns then + return stream:event("stream/"..stanza.attr.xmlns, stanza); + end + + return stream:event("stanza", stanza); +end + +function stream_callbacks.error(stream, e, stanza) + if stream:event(e, stanza) == nil then + if stanza then + local err = stanza:get_child(nil, "urn:ietf:params:xml:ns:xmpp-streams"); + local text = stanza:get_child_text("text", "urn:ietf:params:xml:ns:xmpp-streams"); + error(err.name..(text and ": "..text or "")); + else + error(stanza and stanza.name or e or "unknown-error"); + end + end +end + +function stream:reset() + if self.stream then + self.stream:reset(); + else + self.stream = new_xmpp_stream(self, stream_callbacks); + end + self.notopen = true; + return true; +end + +function stream:connect_client(jid, pass) + self.jid, self.password = jid, pass; + self.username, self.host, self.resource = jid_split(jid); + + -- Required XMPP features + self:add_plugin("tls"); + self:add_plugin("sasl"); + self:add_plugin("bind"); + self:add_plugin("session"); + + function self.data(conn, data) + local ok, err = self.stream:feed(data); + if ok then return; end + self:debug("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " ")); + self:close("xml-not-well-formed"); + end + + self:hook("connected", function () self:reopen(); end); + self:hook("incoming-raw", function (data) return self.data(self.conn, data); end); + + self.curr_id = 0; + + self.tracked_iqs = {}; + self:hook("stanza", function (stanza) + local id, type = stanza.attr.id, stanza.attr.type; + if id and stanza.name == "iq" and (type == "result" or type == "error") and self.tracked_iqs[id] then + self.tracked_iqs[id](stanza); + self.tracked_iqs[id] = nil; + return true; + end + end); + + self:hook("stanza", function (stanza) + local ret; + if stanza.attr.xmlns == nil or stanza.attr.xmlns == "jabber:client" then + if stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then + local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns; + if xmlns then + ret = self:event("iq/"..xmlns, stanza); + if not ret then + ret = self:event("iq", stanza); + end + end + if ret == nil then + self:send(verse.error_reply(stanza, "cancel", "service-unavailable")); + return true; + end + else + ret = self:event(stanza.name, stanza); + end + end + return ret; + end, -1); + + self:hook("outgoing", function (data) + if data.name then + self:event("stanza-out", data); + end + end); + + self:hook("stanza-out", function (stanza) + if not stanza.attr.xmlns then + self:event(stanza.name.."-out", stanza); + end + end); + + local function stream_ready() + self:event("ready"); + end + self:hook("session-success", stream_ready, -1) + self:hook("bind-success", stream_ready, -1); + + local _base_close = self.close; + function self:close(reason) + self.close = _base_close; + if not self.closed then + self:send(""); + self.closed = true; + else + return self:close(reason); + end + end + + local function start_connect() + -- Initialise connection + self:connect(self.connect_host or self.host, self.connect_port or 5222); + end + + if not (self.connect_host or self.connect_port) then + -- Look up SRV records + adns.lookup(function (answer) + if answer then + local srv_hosts = {}; + self.srv_hosts = srv_hosts; + for _, record in ipairs(answer) do + table.insert(srv_hosts, record.srv); + end + table.sort(srv_hosts, compare_srv_priorities); + + local srv_choice = srv_hosts[1]; + self.srv_choice = 1; + if srv_choice then + self.connect_host, self.connect_port = srv_choice.target, srv_choice.port; + self:debug("Best record found, will connect to %s:%d", self.connect_host or self.host, self.connect_port or 5222); + end + + self:hook("disconnected", function () + if self.srv_hosts and self.srv_choice < #self.srv_hosts then + self.srv_choice = self.srv_choice + 1; + local srv_choice = srv_hosts[self.srv_choice]; + self.connect_host, self.connect_port = srv_choice.target, srv_choice.port; + start_connect(); + return true; + end + end, 1000); + + self:hook("connected", function () + self.srv_hosts = nil; + end, 1000); + end + start_connect(); + end, "_xmpp-client._tcp."..(self.host)..".", "SRV"); + else + start_connect(); + end +end + +function stream:reopen() + self:reset(); + self:send(st.stanza("stream:stream", { to = self.host, ["xmlns:stream"]='http://etherx.jabber.org/streams', + xmlns = "jabber:client", version = "1.0" }):top_tag()); +end + +function stream:send_iq(iq, callback) + local id = self:new_id(); + self.tracked_iqs[id] = callback; + iq.attr.id = id; + self:send(iq); +end + +function stream:new_id() + self.curr_id = self.curr_id + 1; + return tostring(self.curr_id); +end diff --git a/verse/component.lua b/verse/component.lua new file mode 100644 index 0000000..3a6f3e0 --- /dev/null +++ b/verse/component.lua @@ -0,0 +1,149 @@ +local verse = require "verse"; +local stream = verse.stream_mt; + +local jid_split = require "util.jid".split; +local lxp = require "lxp"; +local st = require "util.stanza"; +local sha1 = require "util.sha1".sha1; + +-- Shortcuts to save having to load util.stanza +verse.message, verse.presence, verse.iq, verse.stanza, verse.reply, verse.error_reply = + st.message, st.presence, st.iq, st.stanza, st.reply, st.error_reply; + +local new_xmpp_stream = require "util.xmppstream".new; + +local xmlns_stream = "http://etherx.jabber.org/streams"; +local xmlns_component = "jabber:component:accept"; + +local stream_callbacks = { + stream_ns = xmlns_stream, + stream_tag = "stream", + default_ns = xmlns_component }; + +function stream_callbacks.streamopened(stream, attr) + stream.stream_id = attr.id; + if not stream:event("opened", attr) then + stream.notopen = nil; + end + return true; +end + +function stream_callbacks.streamclosed(stream) + return stream:event("closed"); +end + +function stream_callbacks.handlestanza(stream, stanza) + if stanza.attr.xmlns == xmlns_stream then + return stream:event("stream-"..stanza.name, stanza); + elseif stanza.attr.xmlns or stanza.name == "handshake" then + return stream:event("stream/"..(stanza.attr.xmlns or xmlns_component), stanza); + end + + return stream:event("stanza", stanza); +end + +function stream:reset() + if self.stream then + self.stream:reset(); + else + self.stream = new_xmpp_stream(self, stream_callbacks); + end + self.notopen = true; + return true; +end + +function stream:connect_component(jid, pass) + self.jid, self.password = jid, pass; + self.username, self.host, self.resource = jid_split(jid); + + function self.data(conn, data) + local ok, err = self.stream:feed(data); + if ok then return; end + stream:debug("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " ")); + stream:close("xml-not-well-formed"); + end + + self:hook("incoming-raw", function (data) return self.data(self.conn, data); end); + + self.curr_id = 0; + + self.tracked_iqs = {}; + self:hook("stanza", function (stanza) + local id, type = stanza.attr.id, stanza.attr.type; + if id and stanza.name == "iq" and (type == "result" or type == "error") and self.tracked_iqs[id] then + self.tracked_iqs[id](stanza); + self.tracked_iqs[id] = nil; + return true; + end + end); + + self:hook("stanza", function (stanza) + local ret; + if stanza.attr.xmlns == nil or stanza.attr.xmlns == "jabber:client" then + if stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then + local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns; + if xmlns then + ret = self:event("iq/"..xmlns, stanza); + if not ret then + ret = self:event("iq", stanza); + end + end + if ret == nil then + self:send(verse.error_reply(stanza, "cancel", "service-unavailable")); + return true; + end + else + ret = self:event(stanza.name, stanza); + end + end + return ret; + end, -1); + + self:hook("opened", function (attr) + print(self.jid, self.stream_id, attr.id); + local token = sha1(self.stream_id..pass, true); + + self:send(st.stanza("handshake", { xmlns = xmlns_component }):text(token)); + self:hook("stream/"..xmlns_component, function (stanza) + if stanza.name == "handshake" then + self:event("authentication-success"); + end + end); + end); + + local function stream_ready() + self:event("ready"); + end + self:hook("authentication-success", stream_ready, -1); + + -- Initialise connection + self:connect(self.connect_host or self.host, self.connect_port or 5347); + self:reopen(); +end + +function stream:reopen() + self:reset(); + self:send(st.stanza("stream:stream", { to = self.jid, ["xmlns:stream"]='http://etherx.jabber.org/streams', + xmlns = xmlns_component, version = "1.0" }):top_tag()); +end + +function stream:close(reason) + if not self.notopen then + self:send(""); + end + local on_disconnect = self.conn.disconnect(); + self.conn:close(); + on_disconnect(conn, reason); +end + +function stream:send_iq(iq, callback) + local id = self:new_id(); + self.tracked_iqs[id] = callback; + iq.attr.id = id; + self:send(iq); +end + +function stream:new_id() + self.curr_id = self.curr_id + 1; + return tostring(self.curr_id); +end diff --git a/verse/doc/example.lua b/verse/doc/example.lua new file mode 100644 index 0000000..06be3e2 --- /dev/null +++ b/verse/doc/example.lua @@ -0,0 +1,39 @@ +-- Change these: +local jid, password = "user@example.com", "secret"; + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none verse"); + +require "verse".init("client"); + +c = verse.new(); +c:add_plugin("version"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end); + +-- This one prints all received data +c:hook("incoming-raw", print, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c:connect_client(jid, password); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + print("Stream ready!"); + c.version:set{ name = "verse example client" }; + c:query_version(c.jid, function (v) print("I am using "..(v.name or "")); end); +end); + +print("Starting loop...") +verse.loop() diff --git a/verse/doc/example_adhoc.lua b/verse/doc/example_adhoc.lua new file mode 100644 index 0000000..1e1b60e --- /dev/null +++ b/verse/doc/example_adhoc.lua @@ -0,0 +1,48 @@ +-- Change these: +local jid, password = "user@example.com", "secret"; + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none"); + +require "verse".init("client"); + +c = verse.new(verse.logger()); +c:add_plugin("version"); +c:add_plugin("disco"); +c:add_plugin("adhoc"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end); + +-- This one prints all received data +c:hook("incoming-raw", print, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c:connect_client(jid, password); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + print("Stream ready!"); + c.version:set{ name = "verse example client" }; + + local function random_handler() + return { info = tostring(math.random(1,100)), status = "completed" }; + end + + c:add_adhoc_command("Get random number", "random", random_handler); + + c:send(verse.presence():add_child(c:caps())); +end); + +print("Starting loop...") +verse.loop() diff --git a/verse/doc/example_bosh.lua b/verse/doc/example_bosh.lua new file mode 100644 index 0000000..075051c --- /dev/null +++ b/verse/doc/example_bosh.lua @@ -0,0 +1,40 @@ +-- Change these: +local jid, password = "user@example.com", "secret"; +local url = "http://example.com:80/http-bind"; + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none verse"); + +require "verse".init("client", "bosh"); + +c = verse.new_bosh(nil, url); +c:add_plugin("version"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end); + +-- This one prints all received data +c:hook("incoming-raw", print, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c:connect_client(jid, password); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + print("Stream ready!"); + c.version:set{ name = "Verse example BOSH client" }; + c:query_version(c.jid, function (v) print("I am using "..(v.name or "")); end); +end); + +print("Starting loop...") +verse.loop() diff --git a/verse/doc/example_carbons.lua b/verse/doc/example_carbons.lua new file mode 100644 index 0000000..985f347 --- /dev/null +++ b/verse/doc/example_carbons.lua @@ -0,0 +1,46 @@ +local xmlns_carbons = "urn:xmpp:carbons:1"; +local xmlns_forward = "urn:xmpp:forward:0"; + +local function datetime(t) return os_date("!%Y-%m-%dT%H:%M:%SZ", t); end + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none verse"); + +require "verse".init("client"); + +c = verse.new();--verse.logger()); +c:add_plugin "carbons" + +c:hook("disconnected", verse.quit); +local jid, password = unpack(arg); +assert(jid and password, "You need to supply JID and password as arguments"); +c:connect_client(jid, password); + +-- Print a message after authentication +c:hook("authentication-success", function () c:debug("Logged in!"); end); +c:hook("authentication-failure", function (err) + c:error("Failed to log in! Error: "..tostring(err.condition)); + c:close(); +end); + +c:hook("carbon", function(carbon) + local dir, ts, st = carbon.dir, carbon.timestamp, carbon.stanza; + print("", datetime(ts), dir:upper()); + print(st); +end); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + c:debug("Connected"); + c.carbons:enable(function(ok) + if ok then + c:debug("Carbons enabled") + else + c:error("Could not enable carbons, aborting"); + c:close(); + end + end); +end); + +verse.loop() diff --git a/verse/doc/example_component.lua b/verse/doc/example_component.lua new file mode 100644 index 0000000..398e2fe --- /dev/null +++ b/verse/doc/example_component.lua @@ -0,0 +1,43 @@ +local jid, password = "echo.localhost", "hellohello"; + +--os.execute("squish --minify-level=none verse"); + +require "verse".init("component"); + +c = verse.new(verse.logger()) +c:add_plugin("version"); +c:add_plugin("ping"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end); + +-- This one prints all received data +c:hook("incoming-raw", print, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c.connect_host = "127.0.0.1" +c:connect_component(jid, password); + +-- Catch binding-success which is (currently) how you know when a stream is ready +c:hook("ready", function () + print("Stream ready!"); + c.version:set{ name = "verse example component" }; +end); + +-- Echo, echo, echo, echo... +c:hook("stanza", function (stanza) + stanza.attr.from, stanza.attr.to = stanza.attr.to, stanza.attr.from; + c:send(stanza); +end) + +print("Starting loop...") +verse.loop() diff --git a/verse/doc/example_jingle.lua b/verse/doc/example_jingle.lua new file mode 100644 index 0000000..22dadc1 --- /dev/null +++ b/verse/doc/example_jingle.lua @@ -0,0 +1,56 @@ +-- Change these: +local jid, password = "user@example.com/receiver", "secret"; + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none"); + +require "verse".init("client"); + +c = verse.new(verse.logger()) +c:add_plugin("version"); +c:add_plugin("disco"); +c:add_plugin("proxy65"); +c:add_plugin("jingle"); +c:add_plugin("jingle_ft"); +c:add_plugin("jingle_s5b"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end, 15); + +-- This one prints all received data +--c:hook("incoming-raw", function (...) print("<<", ...) end, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c:connect_client(jid, password); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + print("Stream ready!"); + + -- Handle incoming Jingle requests + c:hook("jingle", function (jingle) + if jingle.content.type == "file" then + print("File offer from "..jingle.peer.."!"); + print("Filename: "..jingle.content.file.name.." Size: "..jingle.content.file.size); + jingle:accept({ save_file = jingle.content.file.name..".received" }); + end + end); + + c:send(verse.presence() + :tag("status"):text("Send me a file!"):up() + :add_child(c:caps()) + ); +end); + +print("Starting loop...") +verse.loop() diff --git a/verse/doc/example_jingle_send.lua b/verse/doc/example_jingle_send.lua new file mode 100644 index 0000000..880009a --- /dev/null +++ b/verse/doc/example_jingle_send.lua @@ -0,0 +1,52 @@ +-- Change these: +local jid, password = "user@example.com/sender", "secret"; +local receiver = "user@example.com/receiver"; + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none"); + +require "verse".init("client"); + +c = verse.new(verse.logger()) +c:add_plugin("version"); +c:add_plugin("disco"); +c:add_plugin("proxy65"); +c:add_plugin("jingle"); +c:add_plugin("jingle_ft"); +c:add_plugin("jingle_s5b"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end, 15); + +-- This one prints all received data +--c:hook("incoming-raw", function (...) print("<<", ...) end, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c:connect_client(jid, password); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + print("Stream ready!"); + + c:send(verse.presence() + :tag("status"):text("Let me send you a file!"):up() + :add_child(c:caps()) + ); + + c:hook("proxy65/discovered-proxies", function () + print(c:send_file(receiver, "jingle.txt")); + end); +end); + +print("Starting loop...") +verse.loop() diff --git a/verse/doc/example_pep.lua b/verse/doc/example_pep.lua new file mode 100644 index 0000000..2f6c521 --- /dev/null +++ b/verse/doc/example_pep.lua @@ -0,0 +1,56 @@ +-- Change these: +local jid, password = "user@example.com", "secret"; + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none"); + +require "verse".init("client"); + +c = verse.new(); +c:add_plugin("version"); +c:add_plugin("pep"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end); + +-- This one prints all received data +c:hook("incoming-raw", print, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c:connect_client(jid, password); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + print("Stream ready!"); + c.version:set{ name = "verse example client" }; + c:hook_pep("http://jabber.org/protocol/mood", function (event) + print(event.from.." is "..event.item.tags[1].name); + end); + + c:hook_pep("http://jabber.org/protocol/tune", function (event) + print(event.from.." is listening to "..event.item:get_child_text("title")); + end); + + c:send(verse.presence()); + + c:publish_pep(verse.stanza("tune", { xmlns = "http://jabber.org/protocol/tune" }) + :tag("title"):text("Beautiful Cedars"):up() + :tag("artist"):text("The Spinners"):up() + :tag("source"):text("Not Quite Folk"):up() + :tag("track"):text("4"):up() + ); + +end); + +print("Starting loop...") +verse.loop() diff --git a/verse/doc/example_pubsub.lua b/verse/doc/example_pubsub.lua new file mode 100644 index 0000000..bafd2b7 --- /dev/null +++ b/verse/doc/example_pubsub.lua @@ -0,0 +1,56 @@ +-- Change these: +local jid, password = "user@example.com", "secret"; + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none"); + +require "verse".init("client"); + +c = verse.new(); +c:add_plugin("pubsub"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end); + +-- This one prints all received data +c:hook("incoming-raw", print, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c:connect_client(jid, password); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + print("Stream ready!"); + + -- Create a reference to a node + local node = c.pubsub("pubsub.shakespeare.lit", "princely_musings"); + + -- Callback for when something is published to the node + node:hook(function(event) + print(event.item) + end); + node:subscribe() -- so we actually get the notifications that above callback would get + + node:publish( + nil, -- no id, so the service should give us one + nil, -- no options (not supported at the time of this writing) + verse.stanza("something", { xmlns = "http://example.com/pubsub-thingy" }) -- the actual payload, would turn up in event.item above + :tag("foobar"), + function(success) -- callback + print("publish", success and "success" or "failure") + end) +end); + +print("Starting loop...") +verse.loop() + diff --git a/verse/init.lua b/verse/init.lua new file mode 100644 index 0000000..4166aed --- /dev/null +++ b/verse/init.lua @@ -0,0 +1,250 @@ + +-- Use LuaRocks if available +pcall(require, "luarocks.require"); + +local socket = require"socket"; + +-- Load LuaSec if available +pcall(require, "ssl"); + +local server = require "net.server"; +local events = require "util.events"; +local logger = require "util.logger"; + +module("verse", package.seeall); +local verse = _M; +_M.server = server; + +local stream = {}; +stream.__index = stream; +stream_mt = stream; + +verse.plugins = {}; + +function verse.init(...) + for i=1,select("#", ...) do + local ok = pcall(require, "verse."..select(i,...)); + if not ok then + error("Verse connection module not found: verse."..select(i,...)); + end + end + return verse; +end + + +local max_id = 0; + +function verse.new(logger, base) + local t = setmetatable(base or {}, stream); + max_id = max_id + 1; + t.id = tostring(max_id); + t.logger = logger or verse.new_logger("stream"..t.id); + t.events = events.new(); + t.plugins = {}; + t.verse = verse; + return t; +end + +verse.add_task = require "util.timer".add_task; + +verse.logger = logger.init; -- COMPAT: Deprecated +verse.new_logger = logger.init; +verse.log = verse.logger("verse"); + +local function format(format, ...) + local n, arg, maxn = 0, { ... }, select('#', ...); + return (format:gsub("%%(.)", function (c) if n <= maxn then n = n + 1; return tostring(arg[n]); end end)); +end + +function verse.set_log_handler(log_handler, levels) + levels = levels or { "debug", "info", "warn", "error" }; + logger.reset(); + if io.type(log_handler) == "file" then + local f = log_handler; + function log_handler(name, level, message) + f:write(name, "\t", level, "\t", message, "\n"); + end + end + if log_handler then + local function _log_handler(name, level, message, ...) + return log_handler(name, level, format(message, ...)); + end + for i, level in ipairs(levels) do + logger.add_level_sink(level, _log_handler); + end + end +end + +function _default_log_handler(name, level, message) + return io.stderr:write(name, "\t", level, "\t", message, "\n"); +end +verse.set_log_handler(_default_log_handler, { "error" }); + +local function error_handler(err) + verse.log("error", "Error: %s", err); + verse.log("error", "Traceback: %s", debug.traceback()); +end + +function verse.set_error_handler(new_error_handler) + error_handler = new_error_handler; +end + +function verse.loop() + return xpcall(server.loop, error_handler); +end + +function verse.step() + return xpcall(server.step, error_handler); +end + +function verse.quit() + return server.setquitting(true); +end + +function stream:listen(host, port) + host = host or "localhost"; + port = port or 0; + local conn, err = server.addserver(host, port, new_listener(self, "server"), "*a"); + if conn then + self:debug("Bound to %s:%s", host, port); + self.server = conn; + end + return conn, err; +end + +function stream:connect(connect_host, connect_port) + connect_host = connect_host or "localhost"; + connect_port = tonumber(connect_port) or 5222; + + -- Create and initiate connection + local conn = socket.tcp() + conn:settimeout(0); + local success, err = conn:connect(connect_host, connect_port); + + if not success and err ~= "timeout" then + self:warn("connect() to %s:%d failed: %s", connect_host, connect_port, err); + return self:event("disconnected", { reason = err }) or false, err; + end + + local conn = server.wrapclient(conn, connect_host, connect_port, new_listener(self), "*a"); + if not conn then + self:warn("connection initialisation failed: %s", err); + return self:event("disconnected", { reason = err }) or false, err; + end + self:set_conn(conn); + return true; +end + +function stream:set_conn(conn) + self.conn = conn; + self.send = function (stream, data) + self:event("outgoing", data); + data = tostring(data); + self:event("outgoing-raw", data); + return conn:write(data); + end; +end + +function stream:close(reason) + if not self.conn then + verse.log("error", "Attempt to close disconnected connection - possibly a bug"); + return; + end + local on_disconnect = self.conn.disconnect(); + self.conn:close(); + on_disconnect(self.conn, reason); +end + +-- Logging functions +function stream:debug(...) + return self.logger("debug", ...); +end + +function stream:info(...) + return self.logger("info", ...); +end + +function stream:warn(...) + return self.logger("warn", ...); +end + +function stream:error(...) + return self.logger("error", ...); +end + +-- Event handling +function stream:event(name, ...) + self:debug("Firing event: "..tostring(name)); + return self.events.fire_event(name, ...); +end + +function stream:hook(name, ...) + return self.events.add_handler(name, ...); +end + +function stream:unhook(name, handler) + return self.events.remove_handler(name, handler); +end + +function verse.eventable(object) + object.events = events.new(); + object.hook, object.unhook = stream.hook, stream.unhook; + local fire_event = object.events.fire_event; + function object:event(name, ...) + return fire_event(name, ...); + end + return object; +end + +function stream:add_plugin(name) + if self.plugins[name] then return true; end + if require("verse.plugins."..name) then + local ok, err = verse.plugins[name](self); + if ok ~= false then + self:debug("Loaded %s plugin", name); + self.plugins[name] = true; + else + self:warn("Failed to load %s plugin: %s", name, err); + end + end + return self; +end + +-- Listener factory +function new_listener(stream) + local conn_listener = {}; + + function conn_listener.onconnect(conn) + if stream.server then + local client = verse.new(); + conn:setlistener(new_listener(client)); + client:set_conn(conn); + stream:event("connected", { client = client }); + else + stream.connected = true; + stream:event("connected"); + end + end + + function conn_listener.onincoming(conn, data) + stream:event("incoming-raw", data); + end + + function conn_listener.ondisconnect(conn, err) + if conn ~= stream.conn then return end + stream.connected = false; + stream:event("disconnected", { reason = err }); + end + + function conn_listener.ondrain(conn) + stream:event("drained"); + end + + function conn_listener.onstatus(conn, new_status) + stream:event("status", new_status); + end + + return conn_listener; +end + +return verse; diff --git a/verse/libs/adhoc.lib.lua b/verse/libs/adhoc.lib.lua new file mode 100644 index 0000000..0cb4efe --- /dev/null +++ b/verse/libs/adhoc.lib.lua @@ -0,0 +1,85 @@ +-- Copyright (C) 2009-2010 Florian Zeitz +-- +-- This file is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local st, uuid = require "util.stanza", require "util.uuid"; + +local xmlns_cmd = "http://jabber.org/protocol/commands"; + +local states = {} + +local _M = {}; + +function _cmdtag(desc, status, sessionid, action) + local cmd = st.stanza("command", { xmlns = xmlns_cmd, node = desc.node, status = status }); + if sessionid then cmd.attr.sessionid = sessionid; end + if action then cmd.attr.action = action; end + + return cmd; +end + +function _M.new(name, node, handler, permission) + return { name = name, node = node, handler = handler, cmdtag = _cmdtag, permission = (permission or "user") }; +end + +function _M.handle_cmd(command, origin, stanza) + local sessionid = stanza.tags[1].attr.sessionid or uuid.generate(); + local dataIn = {}; + dataIn.to = stanza.attr.to; + dataIn.from = stanza.attr.from; + dataIn.action = stanza.tags[1].attr.action or "execute"; + dataIn.form = stanza.tags[1]:child_with_ns("jabber:x:data"); + + local data, state = command:handler(dataIn, states[sessionid]); + states[sessionid] = state; + local stanza = st.reply(stanza); + if data.status == "completed" then + states[sessionid] = nil; + cmdtag = command:cmdtag("completed", sessionid); + elseif data.status == "canceled" then + states[sessionid] = nil; + cmdtag = command:cmdtag("canceled", sessionid); + elseif data.status == "error" then + states[sessionid] = nil; + stanza = st.error_reply(stanza, data.error.type, data.error.condition, data.error.message); + origin.send(stanza); + return true; + else + cmdtag = command:cmdtag("executing", sessionid); + end + + for name, content in pairs(data) do + if name == "info" then + cmdtag:tag("note", {type="info"}):text(content):up(); + elseif name == "warn" then + cmdtag:tag("note", {type="warn"}):text(content):up(); + elseif name == "error" then + cmdtag:tag("note", {type="error"}):text(content.message):up(); + elseif name =="actions" then + local actions = st.stanza("actions"); + for _, action in ipairs(content) do + if (action == "prev") or (action == "next") or (action == "complete") then + actions:tag(action):up(); + else + module:log("error", 'Command "'..command.name.. + '" at node "'..command.node..'" provided an invalid action "'..action..'"'); + end + end + cmdtag:add_child(actions); + elseif name == "form" then + cmdtag:add_child((content.layout or content):form(content.values)); + elseif name == "result" then + cmdtag:add_child((content.layout or content):form(content.values, "result")); + elseif name == "other" then + cmdtag:add_child(content); + end + end + stanza:add_child(cmdtag); + origin.send(stanza); + + return true; +end + +return _M; diff --git a/verse/libs/bit.lua b/verse/libs/bit.lua new file mode 100644 index 0000000..2482c47 --- /dev/null +++ b/verse/libs/bit.lua @@ -0,0 +1,145 @@ +-- 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. +-- + + +local type = type; +local tonumber = tonumber; +local setmetatable = setmetatable; +local error = error; +local tostring = tostring; +local print = print; + +local xor_map = {[0]=0;[1]=1;[2]=2;[3]=3;[4]=4;[5]=5;[6]=6;[7]=7;[8]=8;[9]=9;[10]=10;[11]=11;[12]=12;[13]=13;[14]=14;[15]=15;[16]=1;[17]=0;[18]=3;[19]=2;[20]=5;[21]=4;[22]=7;[23]=6;[24]=9;[25]=8;[26]=11;[27]=10;[28]=13;[29]=12;[30]=15;[31]=14;[32]=2;[33]=3;[34]=0;[35]=1;[36]=6;[37]=7;[38]=4;[39]=5;[40]=10;[41]=11;[42]=8;[43]=9;[44]=14;[45]=15;[46]=12;[47]=13;[48]=3;[49]=2;[50]=1;[51]=0;[52]=7;[53]=6;[54]=5;[55]=4;[56]=11;[57]=10;[58]=9;[59]=8;[60]=15;[61]=14;[62]=13;[63]=12;[64]=4;[65]=5;[66]=6;[67]=7;[68]=0;[69]=1;[70]=2;[71]=3;[72]=12;[73]=13;[74]=14;[75]=15;[76]=8;[77]=9;[78]=10;[79]=11;[80]=5;[81]=4;[82]=7;[83]=6;[84]=1;[85]=0;[86]=3;[87]=2;[88]=13;[89]=12;[90]=15;[91]=14;[92]=9;[93]=8;[94]=11;[95]=10;[96]=6;[97]=7;[98]=4;[99]=5;[100]=2;[101]=3;[102]=0;[103]=1;[104]=14;[105]=15;[106]=12;[107]=13;[108]=10;[109]=11;[110]=8;[111]=9;[112]=7;[113]=6;[114]=5;[115]=4;[116]=3;[117]=2;[118]=1;[119]=0;[120]=15;[121]=14;[122]=13;[123]=12;[124]=11;[125]=10;[126]=9;[127]=8;[128]=8;[129]=9;[130]=10;[131]=11;[132]=12;[133]=13;[134]=14;[135]=15;[136]=0;[137]=1;[138]=2;[139]=3;[140]=4;[141]=5;[142]=6;[143]=7;[144]=9;[145]=8;[146]=11;[147]=10;[148]=13;[149]=12;[150]=15;[151]=14;[152]=1;[153]=0;[154]=3;[155]=2;[156]=5;[157]=4;[158]=7;[159]=6;[160]=10;[161]=11;[162]=8;[163]=9;[164]=14;[165]=15;[166]=12;[167]=13;[168]=2;[169]=3;[170]=0;[171]=1;[172]=6;[173]=7;[174]=4;[175]=5;[176]=11;[177]=10;[178]=9;[179]=8;[180]=15;[181]=14;[182]=13;[183]=12;[184]=3;[185]=2;[186]=1;[187]=0;[188]=7;[189]=6;[190]=5;[191]=4;[192]=12;[193]=13;[194]=14;[195]=15;[196]=8;[197]=9;[198]=10;[199]=11;[200]=4;[201]=5;[202]=6;[203]=7;[204]=0;[205]=1;[206]=2;[207]=3;[208]=13;[209]=12;[210]=15;[211]=14;[212]=9;[213]=8;[214]=11;[215]=10;[216]=5;[217]=4;[218]=7;[219]=6;[220]=1;[221]=0;[222]=3;[223]=2;[224]=14;[225]=15;[226]=12;[227]=13;[228]=10;[229]=11;[230]=8;[231]=9;[232]=6;[233]=7;[234]=4;[235]=5;[236]=2;[237]=3;[238]=0;[239]=1;[240]=15;[241]=14;[242]=13;[243]=12;[244]=11;[245]=10;[246]=9;[247]=8;[248]=7;[249]=6;[250]=5;[251]=4;[252]=3;[253]=2;[254]=1;[255]=0;}; +local or_map = {[0]=0;[1]=1;[2]=2;[3]=3;[4]=4;[5]=5;[6]=6;[7]=7;[8]=8;[9]=9;[10]=10;[11]=11;[12]=12;[13]=13;[14]=14;[15]=15;[16]=1;[17]=1;[18]=3;[19]=3;[20]=5;[21]=5;[22]=7;[23]=7;[24]=9;[25]=9;[26]=11;[27]=11;[28]=13;[29]=13;[30]=15;[31]=15;[32]=2;[33]=3;[34]=2;[35]=3;[36]=6;[37]=7;[38]=6;[39]=7;[40]=10;[41]=11;[42]=10;[43]=11;[44]=14;[45]=15;[46]=14;[47]=15;[48]=3;[49]=3;[50]=3;[51]=3;[52]=7;[53]=7;[54]=7;[55]=7;[56]=11;[57]=11;[58]=11;[59]=11;[60]=15;[61]=15;[62]=15;[63]=15;[64]=4;[65]=5;[66]=6;[67]=7;[68]=4;[69]=5;[70]=6;[71]=7;[72]=12;[73]=13;[74]=14;[75]=15;[76]=12;[77]=13;[78]=14;[79]=15;[80]=5;[81]=5;[82]=7;[83]=7;[84]=5;[85]=5;[86]=7;[87]=7;[88]=13;[89]=13;[90]=15;[91]=15;[92]=13;[93]=13;[94]=15;[95]=15;[96]=6;[97]=7;[98]=6;[99]=7;[100]=6;[101]=7;[102]=6;[103]=7;[104]=14;[105]=15;[106]=14;[107]=15;[108]=14;[109]=15;[110]=14;[111]=15;[112]=7;[113]=7;[114]=7;[115]=7;[116]=7;[117]=7;[118]=7;[119]=7;[120]=15;[121]=15;[122]=15;[123]=15;[124]=15;[125]=15;[126]=15;[127]=15;[128]=8;[129]=9;[130]=10;[131]=11;[132]=12;[133]=13;[134]=14;[135]=15;[136]=8;[137]=9;[138]=10;[139]=11;[140]=12;[141]=13;[142]=14;[143]=15;[144]=9;[145]=9;[146]=11;[147]=11;[148]=13;[149]=13;[150]=15;[151]=15;[152]=9;[153]=9;[154]=11;[155]=11;[156]=13;[157]=13;[158]=15;[159]=15;[160]=10;[161]=11;[162]=10;[163]=11;[164]=14;[165]=15;[166]=14;[167]=15;[168]=10;[169]=11;[170]=10;[171]=11;[172]=14;[173]=15;[174]=14;[175]=15;[176]=11;[177]=11;[178]=11;[179]=11;[180]=15;[181]=15;[182]=15;[183]=15;[184]=11;[185]=11;[186]=11;[187]=11;[188]=15;[189]=15;[190]=15;[191]=15;[192]=12;[193]=13;[194]=14;[195]=15;[196]=12;[197]=13;[198]=14;[199]=15;[200]=12;[201]=13;[202]=14;[203]=15;[204]=12;[205]=13;[206]=14;[207]=15;[208]=13;[209]=13;[210]=15;[211]=15;[212]=13;[213]=13;[214]=15;[215]=15;[216]=13;[217]=13;[218]=15;[219]=15;[220]=13;[221]=13;[222]=15;[223]=15;[224]=14;[225]=15;[226]=14;[227]=15;[228]=14;[229]=15;[230]=14;[231]=15;[232]=14;[233]=15;[234]=14;[235]=15;[236]=14;[237]=15;[238]=14;[239]=15;[240]=15;[241]=15;[242]=15;[243]=15;[244]=15;[245]=15;[246]=15;[247]=15;[248]=15;[249]=15;[250]=15;[251]=15;[252]=15;[253]=15;[254]=15;[255]=15;}; +local and_map = {[0]=0;[1]=0;[2]=0;[3]=0;[4]=0;[5]=0;[6]=0;[7]=0;[8]=0;[9]=0;[10]=0;[11]=0;[12]=0;[13]=0;[14]=0;[15]=0;[16]=0;[17]=1;[18]=0;[19]=1;[20]=0;[21]=1;[22]=0;[23]=1;[24]=0;[25]=1;[26]=0;[27]=1;[28]=0;[29]=1;[30]=0;[31]=1;[32]=0;[33]=0;[34]=2;[35]=2;[36]=0;[37]=0;[38]=2;[39]=2;[40]=0;[41]=0;[42]=2;[43]=2;[44]=0;[45]=0;[46]=2;[47]=2;[48]=0;[49]=1;[50]=2;[51]=3;[52]=0;[53]=1;[54]=2;[55]=3;[56]=0;[57]=1;[58]=2;[59]=3;[60]=0;[61]=1;[62]=2;[63]=3;[64]=0;[65]=0;[66]=0;[67]=0;[68]=4;[69]=4;[70]=4;[71]=4;[72]=0;[73]=0;[74]=0;[75]=0;[76]=4;[77]=4;[78]=4;[79]=4;[80]=0;[81]=1;[82]=0;[83]=1;[84]=4;[85]=5;[86]=4;[87]=5;[88]=0;[89]=1;[90]=0;[91]=1;[92]=4;[93]=5;[94]=4;[95]=5;[96]=0;[97]=0;[98]=2;[99]=2;[100]=4;[101]=4;[102]=6;[103]=6;[104]=0;[105]=0;[106]=2;[107]=2;[108]=4;[109]=4;[110]=6;[111]=6;[112]=0;[113]=1;[114]=2;[115]=3;[116]=4;[117]=5;[118]=6;[119]=7;[120]=0;[121]=1;[122]=2;[123]=3;[124]=4;[125]=5;[126]=6;[127]=7;[128]=0;[129]=0;[130]=0;[131]=0;[132]=0;[133]=0;[134]=0;[135]=0;[136]=8;[137]=8;[138]=8;[139]=8;[140]=8;[141]=8;[142]=8;[143]=8;[144]=0;[145]=1;[146]=0;[147]=1;[148]=0;[149]=1;[150]=0;[151]=1;[152]=8;[153]=9;[154]=8;[155]=9;[156]=8;[157]=9;[158]=8;[159]=9;[160]=0;[161]=0;[162]=2;[163]=2;[164]=0;[165]=0;[166]=2;[167]=2;[168]=8;[169]=8;[170]=10;[171]=10;[172]=8;[173]=8;[174]=10;[175]=10;[176]=0;[177]=1;[178]=2;[179]=3;[180]=0;[181]=1;[182]=2;[183]=3;[184]=8;[185]=9;[186]=10;[187]=11;[188]=8;[189]=9;[190]=10;[191]=11;[192]=0;[193]=0;[194]=0;[195]=0;[196]=4;[197]=4;[198]=4;[199]=4;[200]=8;[201]=8;[202]=8;[203]=8;[204]=12;[205]=12;[206]=12;[207]=12;[208]=0;[209]=1;[210]=0;[211]=1;[212]=4;[213]=5;[214]=4;[215]=5;[216]=8;[217]=9;[218]=8;[219]=9;[220]=12;[221]=13;[222]=12;[223]=13;[224]=0;[225]=0;[226]=2;[227]=2;[228]=4;[229]=4;[230]=6;[231]=6;[232]=8;[233]=8;[234]=10;[235]=10;[236]=12;[237]=12;[238]=14;[239]=14;[240]=0;[241]=1;[242]=2;[243]=3;[244]=4;[245]=5;[246]=6;[247]=7;[248]=8;[249]=9;[250]=10;[251]=11;[252]=12;[253]=13;[254]=14;[255]=15;} + +local not_map = {[0]=15;[1]=14;[2]=13;[3]=12;[4]=11;[5]=10;[6]=9;[7]=8;[8]=7;[9]=6;[10]=5;[11]=4;[12]=3;[13]=2;[14]=1;[15]=0;}; +local rshift1_map = {[0]=0;[1]=0;[2]=1;[3]=1;[4]=2;[5]=2;[6]=3;[7]=3;[8]=4;[9]=4;[10]=5;[11]=5;[12]=6;[13]=6;[14]=7;[15]=7;}; +local rshift1carry_map = {[0]=0;[1]=8;[2]=0;[3]=8;[4]=0;[5]=8;[6]=0;[7]=8;[8]=0;[9]=8;[10]=0;[11]=8;[12]=0;[13]=8;[14]=0;[15]=8;}; +local lshift1_map = {[0]=0;[1]=2;[2]=4;[3]=6;[4]=8;[5]=10;[6]=12;[7]=14;[8]=0;[9]=2;[10]=4;[11]=6;[12]=8;[13]=10;[14]=12;[15]=14;}; +local lshift1carry_map = {[0]=0;[1]=0;[2]=0;[3]=0;[4]=0;[5]=0;[6]=0;[7]=0;[8]=1;[9]=1;[10]=1;[11]=1;[12]=1;[13]=1;[14]=1;[15]=1;}; +local arshift1carry_map = {[0]=0;[1]=0;[2]=0;[3]=0;[4]=0;[5]=0;[6]=0;[7]=0;[8]=8;[9]=8;[10]=8;[11]=8;[12]=8;[13]=8;[14]=8;[15]=8;}; + +module "bit" + +local bit_mt = {__tostring = function(t) return ("%x%x%x%x%x%x%x%x"):format(t[1],t[2],t[3],t[4],t[5],t[6],t[7],t[8]); end}; +local function do_bop(a, b, op) + return setmetatable({ + op[a[1]*16+b[1]]; + op[a[2]*16+b[2]]; + op[a[3]*16+b[3]]; + op[a[4]*16+b[4]]; + op[a[5]*16+b[5]]; + op[a[6]*16+b[6]]; + op[a[7]*16+b[7]]; + op[a[8]*16+b[8]]; + }, bit_mt); +end +local function do_uop(a, op) + return setmetatable({ + op[a[1]]; + op[a[2]]; + op[a[3]]; + op[a[4]]; + op[a[5]]; + op[a[6]]; + op[a[7]]; + op[a[8]]; + }, bit_mt); +end + +function bxor(a, b) return do_bop(a, b, xor_map); end +function bor(a, b) return do_bop(a, b, or_map); end +function band(a, b) return do_bop(a, b, and_map); end + +function bnot(a) return do_uop(a, not_map); end +local function _rshift1(t) + local carry = 0; + for i=1,8 do + local t_i = rshift1_map[t[i]] + carry; + carry = rshift1carry_map[t[i]]; + t[i] = t_i; + end +end +function rshift(a, i) + local t = {a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]}; + for n = 1,i do _rshift1(t); end + return setmetatable(t, bit_mt); +end +local function _arshift1(t) + local carry = arshift1carry_map[t[1]]; + for i=1,8 do + local t_i = rshift1_map[t[i]] + carry; + carry = rshift1carry_map[t[i]]; + t[i] = t_i; + end +end +function arshift(a, i) + local t = {a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]}; + for n = 1,i do _arshift1(t); end + return setmetatable(t, bit_mt); +end +local function _lshift1(t) + local carry = 0; + for i=8,1,-1 do + local t_i = lshift1_map[t[i]] + carry; + carry = lshift1carry_map[t[i]]; + t[i] = t_i; + end +end +function lshift(a, i) + local t = {a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]}; + for n = 1,i do _lshift1(t); end + return setmetatable(t, bit_mt); +end + +local function _cast(a) + if type(a) == "number" then a = ("%x"):format(a); + elseif type(a) == "table" then return a; + elseif type(a) ~= "string" then error("string expected, got "..type(a), 2); end + local t = {0,0,0,0,0,0,0,0}; + a = "00000000"..a; + a = a:sub(-8); + for i = 1,8 do + t[i] = tonumber(a:sub(i,i), 16) or error("Number format error", 2); + end + return setmetatable(t, bit_mt); +end + +local function wrap1(f) + return function(a, ...) + if type(a) ~= "table" then a = _cast(a); end + a = f(a, ...); + a = tonumber(tostring(a), 16); + if a > 0x7fffffff then a = a - 1 - 0xffffffff; end + return a; + end; +end +local function wrap2(f) + return function(a, b, ...) + if type(a) ~= "table" then a = _cast(a); end + if type(b) ~= "table" then b = _cast(b); end + a = f(a, b, ...); + a = tonumber(tostring(a), 16); + if a > 0x7fffffff then a = a - 1 - 0xffffffff; end + return a; + end; +end + +bxor = wrap2(bxor); +bor = wrap2(bor); +band = wrap2(band); +bnot = wrap1(bnot); +lshift = wrap1(lshift); +rshift = wrap1(rshift); +arshift = wrap1(arshift); +cast = wrap1(_cast); + +bits = 32; + +return _M; diff --git a/verse/libs/encodings.lua b/verse/libs/encodings.lua new file mode 100644 index 0000000..4364221 --- /dev/null +++ b/verse/libs/encodings.lua @@ -0,0 +1,12 @@ +local function not_impl() + error("Function not implemented"); +end + +local mime = require "mime"; + +module "encodings" + +stringprep = {}; +base64 = { encode = mime.b64, decode = not_impl }; --mime.unb64 is buggy with \0 + +return _M; diff --git a/verse/libs/hashes.lua b/verse/libs/hashes.lua new file mode 100644 index 0000000..6a0e0ef --- /dev/null +++ b/verse/libs/hashes.lua @@ -0,0 +1,3 @@ +local sha1 = require "util.sha1"; + +return { sha1 = sha1.sha1 }; diff --git a/verse/libs/xstanza.lua b/verse/libs/xstanza.lua new file mode 100644 index 0000000..3b12aac --- /dev/null +++ b/verse/libs/xstanza.lua @@ -0,0 +1,27 @@ +local stanza_mt = getmetatable(require "util.stanza".stanza()); + +local xmlns_stanzas = "urn:ietf:params:xml:ns:xmpp-stanzas"; + +function stanza_mt:get_error() + local type, condition, text; + + local error_tag = self:get_child("error"); + if not error_tag then + return nil, nil; + end + type = error_tag.attr.type; + + for child in error_tag:children() do + if child.attr.xmlns == xmlns_stanzas then + if child.name == "text" then + text = child:get_text(); + else + condition = child.name; + end + if condition and text then + break; + end + end + end + return type, condition, text; +end diff --git a/verse/plugins/adhoc.lua b/verse/plugins/adhoc.lua new file mode 100644 index 0000000..372e453 --- /dev/null +++ b/verse/plugins/adhoc.lua @@ -0,0 +1,115 @@ +local verse = require "verse"; +local adhoc = require "lib.adhoc"; + +local xmlns_commands = "http://jabber.org/protocol/commands"; +local xmlns_data = "jabber:x:data"; + +local command_mt = {}; +command_mt.__index = command_mt; + +-- Table of commands we provide +local commands = {}; + +function verse.plugins.adhoc(stream) + stream:add_plugin("disco"); + stream:add_disco_feature(xmlns_commands); + + function stream:query_commands(jid, callback) + stream:disco_items(jid, xmlns_commands, function (items) + stream:debug("adhoc list returned") + local command_list = {}; + for _, item in ipairs(items) do + command_list[item.node] = item.name; + end + stream:debug("adhoc calling callback") + return callback(command_list); + end); + end + + function stream:execute_command(jid, command, callback) + local cmd = setmetatable({ + stream = stream, jid = jid, + command = command, callback = callback + }, command_mt); + return cmd:execute(); + end + + -- ACL checker for commands we provide + local function has_affiliation(jid, aff) + if not(aff) or aff == "user" then return true; end + if type(aff) == "function" then + return aff(jid); + end + -- TODO: Support 'roster', etc. + end + + function stream:add_adhoc_command(name, node, handler, permission) + commands[node] = adhoc.new(name, node, handler, permission); + stream:add_disco_item({ jid = stream.jid, node = node, name = name }, xmlns_commands); + return commands[node]; + end + + local function handle_command(stanza) + local command_tag = stanza.tags[1]; + local node = command_tag.attr.node; + + local handler = commands[node]; + if not handler then return; end + + if not has_affiliation(stanza.attr.from, handler.permission) then + stream:send(verse.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up() + :add_child(handler:cmdtag("canceled") + :tag("note", {type="error"}):text("You don't have permission to execute this command"))); + return true + end + + -- User has permission now execute the command + return adhoc.handle_cmd(handler, { send = function (d) return stream:send(d) end }, stanza); + end + + stream:hook("iq/"..xmlns_commands, function (stanza) + local type = stanza.attr.type; + local name = stanza.tags[1].name; + if type == "set" and name == "command" then + return handle_command(stanza); + end + end); +end + +function command_mt:_process_response(result) + if result.attr.type == "error" then + self.status = "canceled"; + self.callback(self, {}); + return; + end + local command = result:get_child("command", xmlns_commands); + self.status = command.attr.status; + self.sessionid = command.attr.sessionid; + self.form = command:get_child("x", xmlns_data); + self.note = command:get_child("note"); --FIXME handle multiple s + self.callback(self); +end + +-- Initial execution of a command +function command_mt:execute() + local iq = verse.iq({ to = self.jid, type = "set" }) + :tag("command", { xmlns = xmlns_commands, node = self.command }); + self.stream:send_iq(iq, function (result) + self:_process_response(result); + end); +end + +function command_mt:next(form) + local iq = verse.iq({ to = self.jid, type = "set" }) + :tag("command", { + xmlns = xmlns_commands, + node = self.command, + sessionid = self.sessionid + }); + + if form then iq:add_child(form); end + + self.stream:send_iq(iq, function (result) + self:_process_response(result); + end); +end diff --git a/verse/plugins/archive.lua b/verse/plugins/archive.lua new file mode 100644 index 0000000..2405e8f --- /dev/null +++ b/verse/plugins/archive.lua @@ -0,0 +1,139 @@ +-- This implements XEP-0313: Message Archive Management +-- http://xmpp.org/extensions/xep-0313.html +-- (ie not XEP-0136) + +local verse = require "verse"; +local st = require "util.stanza"; +local xmlns_mam = "urn:xmpp:mam:0" +local xmlns_forward = "urn:xmpp:forward:0"; +local xmlns_delay = "urn:xmpp:delay"; +local uuid = require "util.uuid".generate; +local parse_datetime = require "util.datetime".parse; +local datetime = require "util.datetime".datetime; +local dataform = require"util.dataforms".new; +local rsm = require "util.rsm"; +local NULL = {}; + +local query_form = dataform { + { name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam; }; + { name = "with"; type = "jid-single"; }; + { name = "start"; type = "text-single" }; + { name = "end"; type = "text-single"; }; +}; + +function verse.plugins.archive(stream) + function stream:query_archive(where, query_params, callback) + local queryid = uuid(); + local query_st = st.iq{ type="set", to = where } + :tag("query", { xmlns = xmlns_mam, queryid = queryid }); + + + local qstart, qend = tonumber(query_params["start"]), tonumber(query_params["end"]); + query_params["start"] = qstart and datetime(qstart); + query_params["end"] = qend and datetime(qend); + + query_st:add_child(query_form:form(query_params, "submit")); + -- query_st:up(); + query_st:add_child(rsm.generate(query_params)); + + local results = {}; + local function handle_archived_message(message) + + local finnished = message:get_child("fin", xmlns_mam) + if finnished and finnished.attr.queryid == queryid then + local rset = rsm.get(finnished); + for k,v in pairs(rset or NULL) do results[k]=v; end + self:unhook("message", handle_archived_message); + callback(results); + return true + end + local result_tag = message:get_child("result", xmlns_mam); + if result_tag and result_tag.attr.queryid == queryid then + local forwarded = result_tag:get_child("forwarded", xmlns_forward); + forwarded = forwarded or message:get_child("forwarded", xmlns_forward); -- COMPAT XEP-0313 pre 2013-05-31 + + local id = result_tag.attr.id; + local delay = forwarded:get_child("delay", xmlns_delay); + local stamp = delay and parse_datetime(delay.attr.stamp) or nil; + + local message = forwarded:get_child("message", "jabber:client") + + results[#results+1] = { id = id, stamp = stamp, message = message }; + return true + end + end + + self:hook("message", handle_archived_message, 1); + self:send_iq(query_st, function(reply) + if reply.attr.type == "error" then + self:warn(table.concat({reply:get_error()}, " ")) + self:unhook("message", handle_archived_message); + callback(false, reply:get_error()) + end + return true + end); + end + + local default_attrs = { + always = true, [true] = "always", + never = false, [false] = "never", + roster = "roster", + } + + local function prefs_decode(stanza) -- from XML + local prefs = {}; + local default = stanza.attr.default; + + if default then + prefs[false] = default_attrs[default]; + end + + local always = stanza:get_child("always"); + if always then + for rule in always:childtags("jid") do + local jid = rule:get_text(); + prefs[jid] = true; + end + end + + local never = stanza:get_child("never"); + if never then + for rule in never:childtags("jid") do + local jid = rule:get_text(); + prefs[jid] = false; + end + end + return prefs; + end + + local function prefs_encode(prefs) -- into XML + local default + default, prefs[false] = prefs[false], nil; + if default ~= nil then + default = default_attrs[default]; + end + local reply = st.stanza("prefs", { xmlns = xmlns_mam, default = default }) + local always = st.stanza("always"); + local never = st.stanza("never"); + for k,v in pairs(prefs) do + (v and always or never):tag("jid"):text(k):up(); + end + return reply:add_child(always):add_child(never); + end + + function stream:archive_prefs_get(callback) + self:send_iq(st.iq{ type="get" }:tag("prefs", { xmlns = xmlns_mam }), + function(result) + if result and result.attr.type == "result" and result.tags[1] then + local prefs = prefs_decode(result.tags[1]); + callback(prefs, result); + else + callback(nil, result); + end + end); + end + + function stream:archive_prefs_set(prefs, callback) + self:send_iq(st.iq{ type="set" }:add_child(prefs_encode(prefs)), callback); + end +end diff --git a/verse/plugins/bind.lua b/verse/plugins/bind.lua new file mode 100644 index 0000000..d53b234 --- /dev/null +++ b/verse/plugins/bind.lua @@ -0,0 +1,28 @@ +local verse = require "verse"; +local jid = require "util.jid"; + +local xmlns_bind = "urn:ietf:params:xml:ns:xmpp-bind"; + +function verse.plugins.bind(stream) + local function handle_features(features) + if stream.bound then return; end + stream:debug("Binding resource..."); + stream:send_iq(verse.iq({ type = "set" }):tag("bind", {xmlns=xmlns_bind}):tag("resource"):text(stream.resource), + function (reply) + if reply.attr.type == "result" then + local result_jid = reply + :get_child("bind", xmlns_bind) + :get_child_text("jid"); + stream.username, stream.host, stream.resource = jid.split(result_jid); + stream.jid, stream.bound = result_jid, true; + stream:event("bind-success", { jid = result_jid }); + elseif reply.attr.type == "error" then + local err = reply:child_with_name("error"); + local type, condition, text = reply:get_error(); + stream:event("bind-failure", { error = condition, text = text, type = type }); + end + end); + end + stream:hook("stream-features", handle_features, 200); + return true; +end diff --git a/verse/plugins/blocking.lua b/verse/plugins/blocking.lua new file mode 100644 index 0000000..017462b --- /dev/null +++ b/verse/plugins/blocking.lua @@ -0,0 +1,46 @@ +local verse = require "verse"; + +local xmlns_blocking = "urn:xmpp:blocking"; + +function verse.plugins.blocking(stream) + -- FIXME: Disco + stream.blocking = {}; + function stream.blocking:block_jid(jid, callback) + stream:send_iq(verse.iq{type="set"} + :tag("block", { xmlns = xmlns_blocking }) + :tag("item", { jid = jid }) + , function () return callback and callback(true); end + , function () return callback and callback(false); end + ); + end + function stream.blocking:unblock_jid(jid, callback) + stream:send_iq(verse.iq{type="set"} + :tag("unblock", { xmlns = xmlns_blocking }) + :tag("item", { jid = jid }) + , function () return callback and callback(true); end + , function () return callback and callback(false); end + ); + end + function stream.blocking:unblock_all_jids(callback) + stream:send_iq(verse.iq{type="set"} + :tag("unblock", { xmlns = xmlns_blocking }) + , function () return callback and callback(true); end + , function () return callback and callback(false); end + ); + end + function stream.blocking:get_blocked_jids(callback) + stream:send_iq(verse.iq{type="get"} + :tag("blocklist", { xmlns = xmlns_blocking }) + , function (result) + local list = result:get_child("blocklist", xmlns_blocking); + if not list then return callback and callback(false); end + local jids = {}; + for item in list:childtags() do + jids[#jids+1] = item.attr.jid; + end + return callback and callback(jids); + end + , function (result) return callback and callback(false); end + ); + end +end diff --git a/verse/plugins/carbons.lua b/verse/plugins/carbons.lua new file mode 100644 index 0000000..10d651b --- /dev/null +++ b/verse/plugins/carbons.lua @@ -0,0 +1,67 @@ +local verse = require "verse"; + +local xmlns_carbons = "urn:xmpp:carbons:2"; +local xmlns_forward = "urn:xmpp:forward:0"; +local os_time = os.time; +local parse_datetime = require "util.datetime".parse; +local bare_jid = require "util.jid".bare; + +-- TODO Check disco for support + +function verse.plugins.carbons(stream) + local carbons = {}; + carbons.enabled = false; + stream.carbons = carbons; + + function carbons:enable(callback) + stream:send_iq(verse.iq{type="set"} + :tag("enable", { xmlns = xmlns_carbons }) + , function(result) + local success = result.attr.type == "result"; + if success then + carbons.enabled = true; + end + if callback then + callback(success); + end + end or nil); + end + + function carbons:disable(callback) + stream:send_iq(verse.iq{type="set"} + :tag("disable", { xmlns = xmlns_carbons }) + , function(result) + local success = result.attr.type == "result"; + if success then + carbons.enabled = false; + end + if callback then + callback(success); + end + end or nil); + end + + local my_bare; + stream:hook("bind-success", function() + my_bare = bare_jid(stream.jid); + end); + + stream:hook("message", function(stanza) + local carbon = stanza:get_child(nil, xmlns_carbons); + if stanza.attr.from == my_bare and carbon then + local carbon_dir = carbon.name; + local fwd = carbon:get_child("forwarded", xmlns_forward); + local fwd_stanza = fwd and fwd:get_child("message", "jabber:client"); + local delay = fwd:get_child("delay", "urn:xmpp:delay"); + local stamp = delay and delay.attr.stamp; + stamp = stamp and parse_datetime(stamp); + if fwd_stanza then + return stream:event("carbon", { + dir = carbon_dir, + stanza = fwd_stanza, + timestamp = stamp or os_time(), + }); + end + end + end, 1); +end diff --git a/verse/plugins/compression.lua b/verse/plugins/compression.lua new file mode 100644 index 0000000..e45ae7c --- /dev/null +++ b/verse/plugins/compression.lua @@ -0,0 +1,122 @@ +-- Copyright (C) 2009-2010 Matthew Wild +-- Copyright (C) 2009-2010 Tobias Markmann +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local verse = require "verse"; +local zlib = require "zlib"; + +local xmlns_compression_feature = "http://jabber.org/features/compress" +local xmlns_compression_protocol = "http://jabber.org/protocol/compress" +local xmlns_stream = "http://etherx.jabber.org/streams"; + +local compression_level = 9; + +-- returns either nil or a fully functional ready to use inflate stream +local function get_deflate_stream(session) + local status, deflate_stream = pcall(zlib.deflate, compression_level); + if status == false then + local error_st = verse.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"); + session:send(error_st); + session:error("Failed to create zlib.deflate filter: %s", tostring(deflate_stream)); + return + end + return deflate_stream +end + +-- returns either nil or a fully functional ready to use inflate stream +local function get_inflate_stream(session) + local status, inflate_stream = pcall(zlib.inflate); + if status == false then + local error_st = verse.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"); + session:send(error_st); + session:error("Failed to create zlib.inflate filter: %s", tostring(inflate_stream)); + return + end + return inflate_stream +end + +-- setup compression for a stream +local function setup_compression(session, deflate_stream) + function session:send(t) + --TODO: Better code injection in the sending process + local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync'); + if status == false then + session:close({ + condition = "undefined-condition"; + text = compressed; + extra = verse.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("processing-failed"); + }); + session:warn("Compressed send failed: %s", tostring(compressed)); + return; + end + session.conn:write(compressed); + end; +end + +-- setup decompression for a stream +local function setup_decompression(session, inflate_stream) + local old_data = session.data + session.data = function(conn, data) + session:debug("Decompressing data..."); + local status, decompressed, eof = pcall(inflate_stream, data); + if status == false then + session:close({ + condition = "undefined-condition"; + text = decompressed; + extra = verse.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("processing-failed"); + }); + stream:warn("%s", tostring(decompressed)); + return; + end + return old_data(conn, decompressed); + end; +end + +function verse.plugins.compression(stream) + local function handle_features(features) + if not stream.compressed then + -- does remote server support compression? + local comp_st = features:child_with_name("compression"); + if comp_st then + -- do we support the mechanism + for a in comp_st:children() do + local algorithm = a[1] + if algorithm == "zlib" then + stream:send(verse.stanza("compress", {xmlns=xmlns_compression_protocol}):tag("method"):text("zlib")) + stream:debug("Enabled compression using zlib.") + return true; + end + end + session:debug("Remote server supports no compression algorithm we support.") + end + end + end + local function handle_compressed(stanza) + if stanza.name == "compressed" then + stream:debug("Activating compression...") + + -- create deflate and inflate streams + local deflate_stream = get_deflate_stream(stream); + if not deflate_stream then return end + + local inflate_stream = get_inflate_stream(stream); + if not inflate_stream then return end + + -- setup compression for stream.w + setup_compression(stream, deflate_stream); + + -- setup decompression for stream.data + setup_decompression(stream, inflate_stream); + + stream.compressed = true; + stream:reopen(); + elseif stanza.name == "failure" then + stream:warn("Failed to establish compression"); + end + end + stream:hook("stream-features", handle_features, 250); + stream:hook("stream/"..xmlns_compression_protocol, handle_compressed); +end diff --git a/verse/plugins/disco.lua b/verse/plugins/disco.lua new file mode 100644 index 0000000..45e3f9a --- /dev/null +++ b/verse/plugins/disco.lua @@ -0,0 +1,369 @@ +-- Verse XMPP Library +-- Copyright (C) 2010 Hubert Chathi +-- Copyright (C) 2010 Matthew Wild +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local verse = require "verse"; +local b64 = require("mime").b64; +local sha1 = require("util.sha1").sha1; + +local xmlns_caps = "http://jabber.org/protocol/caps"; +local xmlns_disco = "http://jabber.org/protocol/disco"; +local xmlns_disco_info = xmlns_disco.."#info"; +local xmlns_disco_items = xmlns_disco.."#items"; + +function verse.plugins.disco(stream) + stream:add_plugin("presence"); + local disco_info_mt = { + __index = function(t, k) + local node = { identities = {}, features = {} }; + if k == "identities" or k == "features" then + return t[false][k] + end + t[k] = node; + return node; + end, + }; + local disco_items_mt = { + __index = function(t, k) + local node = { }; + t[k] = node; + return node; + end, + }; + stream.disco = { + cache = {}, + info = setmetatable({ + [false] = { + identities = { + {category = 'client', type='pc', name='Verse'}, + }, + features = { + [xmlns_caps] = true, + [xmlns_disco_info] = true, + [xmlns_disco_items] = true, + }, + }, + }, disco_info_mt); + items = setmetatable({[false]={}}, disco_items_mt); + }; + + stream.caps = {} + stream.caps.node = 'http://code.matthewwild.co.uk/verse/' + + local function cmp_identity(item1, item2) + if item1.category < item2.category then + return true; + elseif item2.category < item1.category then + return false; + end + if item1.type < item2.type then + return true; + elseif item2.type < item1.type then + return false; + end + if (not item1['xml:lang'] and item2['xml:lang']) or + (item2['xml:lang'] and item1['xml:lang'] < item2['xml:lang']) then + return true + end + return false + end + + local function cmp_feature(item1, item2) + return item1.var < item2.var + end + + local function calculate_hash(node) + local identities = stream.disco.info[node or false].identities; + table.sort(identities, cmp_identity) + local features = {}; + for var in pairs(stream.disco.info[node or false].features) do + features[#features+1] = { var = var }; + end + table.sort(features, cmp_feature) + local S = {}; + for key,identity in pairs(identities) do + S[#S+1] = table.concat({ + identity.category, identity.type or '', + identity['xml:lang'] or '', identity.name or '' + }, '/'); + end + for key,feature in pairs(features) do + S[#S+1] = feature.var + end + S[#S+1] = ''; + S = table.concat(S,'<'); + -- FIXME: make sure S is utf8-encoded + --stream:debug("Computed hash string: "..S); + --stream:debug("Computed hash string (sha1): "..sha1(S, true)); + --stream:debug("Computed hash string (sha1+b64): "..b64(sha1(S))); + return (b64(sha1(S))) + end + + setmetatable(stream.caps, { + __call = function (...) -- vararg: allow calling as function or member + -- retrieve the c stanza to insert into the + -- presence stanza + local hash = calculate_hash() + stream.caps.hash = hash; + -- TODO proper caching.... some day + return verse.stanza('c', { + xmlns = xmlns_caps, + hash = 'sha-1', + node = stream.caps.node, + ver = hash + }) + end + }) + + function stream:set_identity(identity, node) + self.disco.info[node or false].identities = { identity }; + stream:resend_presence(); + end + + function stream:add_identity(identity, node) + local identities = self.disco.info[node or false].identities; + identities[#identities + 1] = identity; + stream:resend_presence(); + end + + function stream:add_disco_feature(feature, node) + local feature = feature.var or feature; + self.disco.info[node or false].features[feature] = true; + stream:resend_presence(); + end + + function stream:remove_disco_feature(feature, node) + local feature = feature.var or feature; + self.disco.info[node or false].features[feature] = nil; + stream:resend_presence(); + end + + function stream:add_disco_item(item, node) + local items = self.disco.items[node or false]; + items[#items +1] = item; + end + + function stream:remove_disco_item(item, node) + local items = self.disco.items[node or false]; + for i=#items,1,-1 do + if items[i] == item then + table.remove(items, i); + end + end + end + + -- TODO Node? + function stream:jid_has_identity(jid, category, type) + local cached_disco = self.disco.cache[jid]; + if not cached_disco then + return nil, "no-cache"; + end + local identities = self.disco.cache[jid].identities; + if type then + return identities[category.."/"..type] or false; + end + -- Check whether we have any identities with this category instead + for identity in pairs(identities) do + if identity:match("^(.*)/") == category then + return true; + end + end + end + + function stream:jid_supports(jid, feature) + local cached_disco = self.disco.cache[jid]; + if not cached_disco or not cached_disco.features then + return nil, "no-cache"; + end + return cached_disco.features[feature] or false; + end + + function stream:get_local_services(category, type) + local host_disco = self.disco.cache[self.host]; + if not(host_disco) or not(host_disco.items) then + return nil, "no-cache"; + end + + local results = {}; + for _, service in ipairs(host_disco.items) do + if self:jid_has_identity(service.jid, category, type) then + table.insert(results, service.jid); + end + end + return results; + end + + function stream:disco_local_services(callback) + self:disco_items(self.host, nil, function (items) + if not items then + return callback({}); + end + local n_items = 0; + local function item_callback() + n_items = n_items - 1; + if n_items == 0 then + return callback(items); + end + end + + for _, item in ipairs(items) do + if item.jid then + n_items = n_items + 1; + self:disco_info(item.jid, nil, item_callback); + end + end + if n_items == 0 then + return callback(items); + end + end); + end + + function stream:disco_info(jid, node, callback) + local disco_request = verse.iq({ to = jid, type = "get" }) + :tag("query", { xmlns = xmlns_disco_info, node = node }); + self:send_iq(disco_request, function (result) + if result.attr.type == "error" then + return callback(nil, result:get_error()); + end + + local identities, features = {}, {}; + + for tag in result:get_child("query", xmlns_disco_info):childtags() do + if tag.name == "identity" then + identities[tag.attr.category.."/"..tag.attr.type] = tag.attr.name or true; + elseif tag.name == "feature" then + features[tag.attr.var] = true; + end + end + + + if not self.disco.cache[jid] then + self.disco.cache[jid] = { nodes = {} }; + end + + if node then + if not self.disco.cache[jid].nodes[node] then + self.disco.cache[jid].nodes[node] = { nodes = {} }; + end + self.disco.cache[jid].nodes[node].identities = identities; + self.disco.cache[jid].nodes[node].features = features; + else + self.disco.cache[jid].identities = identities; + self.disco.cache[jid].features = features; + end + return callback(self.disco.cache[jid]); + end); + end + + function stream:disco_items(jid, node, callback) + local disco_request = verse.iq({ to = jid, type = "get" }) + :tag("query", { xmlns = xmlns_disco_items, node = node }); + self:send_iq(disco_request, function (result) + if result.attr.type == "error" then + return callback(nil, result:get_error()); + end + local disco_items = { }; + for tag in result:get_child("query", xmlns_disco_items):childtags() do + if tag.name == "item" then + table.insert(disco_items, { + name = tag.attr.name; + jid = tag.attr.jid; + node = tag.attr.node; + }); + end + end + + if not self.disco.cache[jid] then + self.disco.cache[jid] = { nodes = {} }; + end + + if node then + if not self.disco.cache[jid].nodes[node] then + self.disco.cache[jid].nodes[node] = { nodes = {} }; + end + self.disco.cache[jid].nodes[node].items = disco_items; + else + self.disco.cache[jid].items = disco_items; + end + return callback(disco_items); + end); + end + + stream:hook("iq/"..xmlns_disco_info, function (stanza) + local query = stanza.tags[1]; + if stanza.attr.type == 'get' and query.name == "query" then + local query_node = query.attr.node; + local node = stream.disco.info[query_node or false]; + if query_node and query_node == stream.caps.node .. "#" .. stream.caps.hash then + node = stream.disco.info[false]; + end + local identities, features = node.identities, node.features + + -- construct the response + local result = verse.reply(stanza):tag("query", { + xmlns = xmlns_disco_info, + node = query_node, + }); + for _,identity in pairs(identities) do + result:tag('identity', identity):up() + end + for feature in pairs(features) do + result:tag('feature', { var = feature }):up() + end + stream:send(result); + return true + end + end); + + stream:hook("iq/"..xmlns_disco_items, function (stanza) + local query = stanza.tags[1]; + if stanza.attr.type == 'get' and query.name == "query" then + -- figure out what items to send + local items = stream.disco.items[query.attr.node or false]; + + -- construct the response + local result = verse.reply(stanza):tag('query',{ + xmlns = xmlns_disco_items, + node = query.attr.node + }) + for i=1,#items do + result:tag('item', items[i]):up() + end + stream:send(result); + return true + end + end); + + local initial_disco_started; + stream:hook("ready", function () + if initial_disco_started then return; end + initial_disco_started = true; + stream:disco_local_services(function (services) + for _, service in ipairs(services) do + local service_disco_info = stream.disco.cache[service.jid]; + if service_disco_info then + for identity in pairs(service_disco_info.identities) do + local category, type = identity:match("^(.*)/(.*)$"); + stream:event("disco/service-discovered/"..category, { + type = type, jid = service.jid; + }); + end + end + end + stream:event("ready"); + end); + return true; + end, 50); + + stream:hook("presence-out", function (presence) + if not presence:get_child("c", xmlns_caps) then + presence:reset():add_child(stream:caps()):reset(); + end + end, 10); +end + +-- end of disco.lua diff --git a/verse/plugins/groupchat.lua b/verse/plugins/groupchat.lua new file mode 100644 index 0000000..b9e54f9 --- /dev/null +++ b/verse/plugins/groupchat.lua @@ -0,0 +1,177 @@ +local verse = require "verse"; +local events = require "events"; +local jid = require "util.jid"; + +local room_mt = {}; +room_mt.__index = room_mt; + +local xmlns_delay = "urn:xmpp:delay"; +local xmlns_muc = "http://jabber.org/protocol/muc"; + +function verse.plugins.groupchat(stream) + stream:add_plugin("presence") + stream.rooms = {}; + + stream:hook("stanza", function (stanza) + local room_jid = jid.bare(stanza.attr.from); + if not room_jid then return end + local room = stream.rooms[room_jid] + if not room and stanza.attr.to and room_jid then + room = stream.rooms[stanza.attr.to.." "..room_jid] + end + if room and room.opts.source and stanza.attr.to ~= room.opts.source then return end + if room then + local nick = select(3, jid.split(stanza.attr.from)); + local body = stanza:get_child_text("body"); + local delay = stanza:get_child("delay", xmlns_delay); + local event = { + room_jid = room_jid; + room = room; + sender = room.occupants[nick]; + nick = nick; + body = body; + stanza = stanza; + delay = (delay and delay.attr.stamp); + }; + local ret = room:event(stanza.name, event); + return ret or (stanza.name == "message") or nil; + end + end, 500); + + function stream:join_room(jid, nick, opts) + if not nick then + return false, "no nickname supplied" + end + opts = opts or {}; + local room = setmetatable(verse.eventable{ + stream = stream, jid = jid, nick = nick, + subject = nil, + occupants = {}, + opts = opts, + }, room_mt); + if opts.source then + self.rooms[opts.source.." "..jid] = room; + else + self.rooms[jid] = room; + end + local occupants = room.occupants; + room:hook("presence", function (presence) + local nick = presence.nick or nick; + if not occupants[nick] and presence.stanza.attr.type ~= "unavailable" then + occupants[nick] = { + nick = nick; + jid = presence.stanza.attr.from; + presence = presence.stanza; + }; + local x = presence.stanza:get_child("x", xmlns_muc .. "#user"); + if x then + local x_item = x:get_child("item"); + if x_item and x_item.attr then + occupants[nick].real_jid = x_item.attr.jid; + occupants[nick].affiliation = x_item.attr.affiliation; + occupants[nick].role = x_item.attr.role; + end + --TODO Check for status 100? + end + if nick == room.nick then + room.stream:event("groupchat/joined", room); + else + room:event("occupant-joined", occupants[nick]); + end + elseif occupants[nick] and presence.stanza.attr.type == "unavailable" then + if nick == room.nick then + room.stream:event("groupchat/left", room); + if room.opts.source then + self.rooms[room.opts.source.." "..jid] = nil; + else + self.rooms[jid] = nil; + end + else + occupants[nick].presence = presence.stanza; + room:event("occupant-left", occupants[nick]); + occupants[nick] = nil; + end + end + end); + room:hook("message", function(event) + local subject = event.stanza:get_child_text("subject"); + if not subject then return end + subject = #subject > 0 and subject or nil; + if subject ~= room.subject then + local old_subject = room.subject; + room.subject = subject; + return room:event("subject-changed", { from = old_subject, to = subject, by = event.sender, event = event }); + end + end, 2000); + local join_st = verse.presence():tag("x",{xmlns = xmlns_muc}):reset(); + self:event("pre-groupchat/joining", join_st); + room:send(join_st) + self:event("groupchat/joining", room); + return room; + end + + stream:hook("presence-out", function(presence) + if not presence.attr.to then + for _, room in pairs(stream.rooms) do + room:send(presence); + end + presence.attr.to = nil; + end + end); +end + +function room_mt:send(stanza) + if stanza.name == "message" and not stanza.attr.type then + stanza.attr.type = "groupchat"; + end + if stanza.name == "presence" then + stanza.attr.to = self.jid .."/"..self.nick; + end + if stanza.attr.type == "groupchat" or not stanza.attr.to then + stanza.attr.to = self.jid; + end + if self.opts.source then + stanza.attr.from = self.opts.source + end + self.stream:send(stanza); +end + +function room_mt:send_message(text) + self:send(verse.message():tag("body"):text(text)); +end + +function room_mt:set_subject(text) + self:send(verse.message():tag("subject"):text(text)); +end + +function room_mt:leave(message) + self.stream:event("groupchat/leaving", self); + local presence = verse.presence({type="unavailable"}); + if message then + presence:tag("status"):text(message); + end + self:send(presence); +end + +function room_mt:admin_set(nick, what, value, reason) + self:send(verse.iq({type="set"}) + :query(xmlns_muc .. "#admin") + :tag("item", {nick = nick, [what] = value}) + :tag("reason"):text(reason or "")); +end + +function room_mt:set_role(nick, role, reason) + self:admin_set(nick, "role", role, reason); +end + +function room_mt:set_affiliation(nick, affiliation, reason) + self:admin_set(nick, "affiliation", affiliation, reason); +end + +function room_mt:kick(nick, reason) + self:set_role(nick, "none", reason); +end + +function room_mt:ban(nick, reason) + self:set_affiliation(nick, "outcast", reason); +end diff --git a/verse/plugins/jingle.lua b/verse/plugins/jingle.lua new file mode 100644 index 0000000..fedcf10 --- /dev/null +++ b/verse/plugins/jingle.lua @@ -0,0 +1,301 @@ +local verse = require "verse"; +local sha1 = require "util.sha1".sha1; +local timer = require "util.timer"; +local uuid_generate = require "util.uuid".generate; + +local xmlns_jingle = "urn:xmpp:jingle:1"; +local xmlns_jingle_errors = "urn:xmpp:jingle:errors:1"; + +local jingle_mt = {}; +jingle_mt.__index = jingle_mt; + +local registered_transports = {}; +local registered_content_types = {}; + +function verse.plugins.jingle(stream) + stream:hook("ready", function () + stream:add_disco_feature(xmlns_jingle); + end, 10); + + function stream:jingle(to) + return verse.eventable(setmetatable(base or { + role = "initiator"; + peer = to; + sid = uuid_generate(); + stream = stream; + }, jingle_mt)); + end + + function stream:register_jingle_transport(transport) + -- transport is a function that receives a + -- element, and returns a connection + -- We wait for 'connected' on that connection, + -- and use :send() and 'incoming-raw'. + end + + function stream:register_jingle_content_type(content) + -- Call content() for every 'incoming-raw'? + -- I think content() returns the object we return + -- on jingle:accept() + end + + local function handle_incoming_jingle(stanza) + local jingle_tag = stanza:get_child("jingle", xmlns_jingle); + local sid = jingle_tag.attr.sid; + local action = jingle_tag.attr.action; + local result = stream:event("jingle/"..sid, stanza); + if result == true then + -- Ack + stream:send(verse.reply(stanza)); + return true; + end + -- No existing Jingle object handled this action, our turn... + if action ~= "session-initiate" then + -- Trying to send a command to a session we don't know + local reply = verse.error_reply(stanza, "cancel", "item-not-found") + :tag("unknown-session", { xmlns = xmlns_jingle_errors }):up(); + stream:send(reply); + return; + end + + -- Ok, session-initiate, new session + + -- Create new Jingle object + local sid = jingle_tag.attr.sid; + + local jingle = verse.eventable{ + role = "receiver"; + peer = stanza.attr.from; + sid = sid; + stream = stream; + }; + + setmetatable(jingle, jingle_mt); + + local content_tag; + local content, transport; + for tag in jingle_tag:childtags() do + if tag.name == "content" and tag.attr.xmlns == xmlns_jingle then + local description_tag = tag:child_with_name("description"); + local description_xmlns = description_tag.attr.xmlns; + if description_xmlns then + local desc_handler = stream:event("jingle/content/"..description_xmlns, jingle, description_tag); + if desc_handler then + content = desc_handler; + end + end + + local transport_tag = tag:child_with_name("transport"); + local transport_xmlns = transport_tag.attr.xmlns; + + transport = stream:event("jingle/transport/"..transport_xmlns, jingle, transport_tag); + if content and transport then + content_tag = tag; + break; + end + end + end + if not content then + -- FIXME: Fail, no content + stream:send(verse.error_reply(stanza, "cancel", "feature-not-implemented", "The specified content is not supported")); + return true; + end + + if not transport then + -- FIXME: Refuse session, no transport + stream:send(verse.error_reply(stanza, "cancel", "feature-not-implemented", "The specified transport is not supported")); + return true; + end + + stream:send(verse.reply(stanza)); + + jingle.content_tag = content_tag; + jingle.creator, jingle.name = content_tag.attr.creator, content_tag.attr.name; + jingle.content, jingle.transport = content, transport; + + function jingle:decline() + -- FIXME: Decline session + end + + stream:hook("jingle/"..sid, function (stanza) + if stanza.attr.from ~= jingle.peer then + return false; + end + local jingle_tag = stanza:get_child("jingle", xmlns_jingle); + return jingle:handle_command(jingle_tag); + end); + + stream:event("jingle", jingle); + return true; + end + + function jingle_mt:handle_command(jingle_tag) + local action = jingle_tag.attr.action; + stream:debug("Handling Jingle command: %s", action); + if action == "session-terminate" then + self:destroy(); + elseif action == "session-accept" then + -- Yay! + self:handle_accepted(jingle_tag); + elseif action == "transport-info" then + stream:debug("Handling transport-info"); + self.transport:info_received(jingle_tag); + elseif action == "transport-replace" then + -- FIXME: Used for IBB fallback + stream:error("Peer wanted to swap transport, not implemented"); + else + -- FIXME: Reply unhandled command + stream:warn("Unhandled Jingle command: %s", action); + return nil; + end + return true; + end + + function jingle_mt:send_command(command, element, callback) + local stanza = verse.iq({ to = self.peer, type = "set" }) + :tag("jingle", { + xmlns = xmlns_jingle, + sid = self.sid, + action = command, + initiator = self.role == "initiator" and self.stream.jid or nil, + responder = self.role == "responder" and self.jid or nil, + }):add_child(element); + if not callback then + self.stream:send(stanza); + else + self.stream:send_iq(stanza, callback); + end + end + + function jingle_mt:accept(options) + local accept_stanza = verse.iq({ to = self.peer, type = "set" }) + :tag("jingle", { + xmlns = xmlns_jingle, + sid = self.sid, + action = "session-accept", + responder = stream.jid, + }) + :tag("content", { creator = self.creator, name = self.name }); + + local content_accept_tag = self.content:generate_accept(self.content_tag:child_with_name("description"), options); + accept_stanza:add_child(content_accept_tag); + + local transport_accept_tag = self.transport:generate_accept(self.content_tag:child_with_name("transport"), options); + accept_stanza:add_child(transport_accept_tag); + + local jingle = self; + stream:send_iq(accept_stanza, function (result) + if result.attr.type == "error" then + local type, condition, text = result:get_error(); + stream:error("session-accept rejected: %s", condition); -- FIXME: Notify + return false; + end + jingle.transport:connect(function (conn) + stream:warn("CONNECTED (receiver)!!!"); + jingle.state = "active"; + jingle:event("connected", conn); + end); + end); + end + + + stream:hook("iq/"..xmlns_jingle, handle_incoming_jingle); + return true; +end + +function jingle_mt:offer(name, content) + local session_initiate = verse.iq({ to = self.peer, type = "set" }) + :tag("jingle", { xmlns = xmlns_jingle, action = "session-initiate", + initiator = self.stream.jid, sid = self.sid }); + + -- Content tag + session_initiate:tag("content", { creator = self.role, name = name }); + + -- Need description element from someone who can turn 'content' into XML + local description = self.stream:event("jingle/describe/"..name, content); + + if not description then + return false, "Unknown content type"; + end + + session_initiate:add_child(description); + + -- FIXME: Sort transports by 1) recipient caps 2) priority (SOCKS vs IBB, etc.) + -- Fixed to s5b in the meantime + local transport = self.stream:event("jingle/transport/".."urn:xmpp:jingle:transports:s5b:1", self); + self.transport = transport; + + session_initiate:add_child(transport:generate_initiate()); + + self.stream:debug("Hooking %s", "jingle/"..self.sid); + self.stream:hook("jingle/"..self.sid, function (stanza) + if stanza.attr.from ~= self.peer then + return false; + end + local jingle_tag = stanza:get_child("jingle", xmlns_jingle); + return self:handle_command(jingle_tag) + end); + + self.stream:send_iq(session_initiate, function (result) + if result.attr.type == "error" then + self.state = "terminated"; + local type, condition, text = result:get_error(); + return self:event("error", { type = type, condition = condition, text = text }); + end + end); + self.state = "pending"; +end + +function jingle_mt:terminate(reason) + local reason_tag = verse.stanza("reason"):tag(reason or "success"); + self:send_command("session-terminate", reason_tag, function (result) + self.state = "terminated"; + self.transport:disconnect(); + self:destroy(); + end); +end + +function jingle_mt:destroy() + self:event("terminated"); + self.stream:unhook("jingle/"..self.sid, self.handle_command); +end + +function jingle_mt:handle_accepted(jingle_tag) + local transport_tag = jingle_tag:child_with_name("transport"); + self.transport:handle_accepted(transport_tag); + self.transport:connect(function (conn) + self.stream:debug("CONNECTED (initiator)!") + -- Connected, send file + self.state = "active"; + self:event("connected", conn); + end); +end + +function jingle_mt:set_source(source, auto_close) + local function pump() + local chunk, err = source(); + if chunk and chunk ~= "" then + self.transport.conn:send(chunk); + elseif chunk == "" then + return pump(); -- We need some data! + elseif chunk == nil then + if auto_close then + self:terminate(); + end + self.transport.conn:unhook("drained", pump); + source = nil; + end + end + self.transport.conn:hook("drained", pump); + pump(); +end + +function jingle_mt:set_sink(sink) + self.transport.conn:hook("incoming-raw", sink); + self.transport.conn:hook("disconnected", function (event) + self.stream:debug("Closing sink..."); + local reason = event.reason; + if reason == "closed" then reason = nil; end + sink(nil, reason); + end); +end diff --git a/verse/plugins/jingle_ft.lua b/verse/plugins/jingle_ft.lua new file mode 100644 index 0000000..1a52a3a --- /dev/null +++ b/verse/plugins/jingle_ft.lua @@ -0,0 +1,74 @@ +local verse = require "verse"; +local ltn12 = require "ltn12"; + +local dirsep = package.config:sub(1,1); + +local xmlns_jingle_ft = "urn:xmpp:jingle:apps:file-transfer:1"; +local xmlns_si_file_transfer = "http://jabber.org/protocol/si/profile/file-transfer"; + +function verse.plugins.jingle_ft(stream) + stream:hook("ready", function () + stream:add_disco_feature(xmlns_jingle_ft); + end, 10); + + local ft_content = { type = "file" }; + + function ft_content:generate_accept(description, options) + if options and options.save_file then + self.jingle:hook("connected", function () + local sink = ltn12.sink.file(io.open(options.save_file, "w+")); + self.jingle:set_sink(sink); + end); + end + + return description; + end + + local ft_mt = { __index = ft_content }; + stream:hook("jingle/content/"..xmlns_jingle_ft, function (jingle, description_tag) + local file_tag = description_tag:get_child("offer"):get_child("file", xmlns_si_file_transfer); + local file = { + name = file_tag.attr.name; + size = tonumber(file_tag.attr.size); + }; + + return setmetatable({ jingle = jingle, file = file }, ft_mt); + end); + + stream:hook("jingle/describe/file", function (file_info) + -- Return + local date; + if file_info.timestamp then + date = os.date("!%Y-%m-%dT%H:%M:%SZ", file_info.timestamp); + end + return verse.stanza("description", { xmlns = xmlns_jingle_ft }) + :tag("offer") + :tag("file", { xmlns = xmlns_si_file_transfer, + name = file_info.filename, -- Mandatory + size = file_info.size, -- Mandatory + date = date, + hash = file_info.hash, + }) + :tag("desc"):text(file_info.description or ""); + end); + + function stream:send_file(to, filename) + local file, err = io.open(filename); + if not file then return file, err; end + + local file_size = file:seek("end", 0); + file:seek("set", 0); + + local source = ltn12.source.file(file); + + local jingle = self:jingle(to); + jingle:offer("file", { + filename = filename:match("[^"..dirsep.."]+$"); + size = file_size; + }); + jingle:hook("connected", function () + jingle:set_source(source, true); + end); + return jingle; + end +end diff --git a/verse/plugins/jingle_ibb.lua b/verse/plugins/jingle_ibb.lua new file mode 100644 index 0000000..9c96a14 --- /dev/null +++ b/verse/plugins/jingle_ibb.lua @@ -0,0 +1,214 @@ +local verse = require "verse"; +local base64 = require "util.encodings".base64; +local uuid_generate = require "util.uuid".generate; + +local xmlns_jingle_ibb = "urn:xmpp:jingle:transports:ibb:1"; +local xmlns_ibb = "http://jabber.org/protocol/ibb"; +assert(base64.encode("This is a test.") == "VGhpcyBpcyBhIHRlc3Qu", "Base64 encoding failed"); +assert(base64.decode("VGhpcyBpcyBhIHRlc3Qu") == "This is a test.", "Base64 decoding failed"); +local t_concat = table.concat + +local ibb_conn = {}; +local ibb_conn_mt = { __index = ibb_conn }; + +local function new_ibb(stream) + local conn = setmetatable({ stream = stream }, ibb_conn_mt) + conn = verse.eventable(conn); + return conn; +end + +function ibb_conn:initiate(peer, sid, stanza) + self.block = 2048; -- ignored for now + self.stanza = stanza or 'iq'; + self.peer = peer; + self.sid = sid or tostring(self):match("%x+$"); + self.iseq = 0; + self.oseq = 0; + local feeder = function(stanza) + return self:feed(stanza) + end + self.feeder = feeder; + print("Hooking incomming IQs"); + local stream = self.stream; + stream:hook("iq/".. xmlns_ibb, feeder) + if stanza == "message" then + stream:hook("message", feeder) + end +end + +function ibb_conn:open(callback) + self.stream:send_iq(verse.iq{ to = self.peer, type = "set" } + :tag("open", { + xmlns = xmlns_ibb, + ["block-size"] = self.block, + sid = self.sid, + stanza = self.stanza + }) + , function(reply) + if callback then + if reply.attr.type ~= "error" then + callback(true) + else + callback(false, reply:get_error()) + end + end + end); +end + +function ibb_conn:send(data) + local stanza = self.stanza; + local st; + if stanza == "iq" then + st = verse.iq{ type = "set", to = self.peer } + elseif stanza == "message" then + st = verse.message{ to = self.peer } + end + + local seq = self.oseq; + self.oseq = seq + 1; + + st:tag("data", { xmlns = xmlns_ibb, sid = self.sid, seq = seq }) + :text(base64.encode(data)); + + if stanza == "iq" then + self.stream:send_iq(st, function(reply) + self:event(reply.attr.type == "result" and "drained" or "error"); + end) + else + stream:send(st) + self:event("drained"); + end +end + +function ibb_conn:feed(stanza) + if stanza.attr.from ~= self.peer then return end + local child = stanza[1]; + if child.attr.sid ~= self.sid then return end + local ok; + if child.name == "open" then + self:event("connected"); + self.stream:send(verse.reply(stanza)) + return true + elseif child.name == "data" then + local bdata = stanza:get_child_text("data", xmlns_ibb); + local seq = tonumber(child.attr.seq); + local expected_seq = self.iseq; + if bdata and seq then + if seq ~= expected_seq then + self.stream:send(verse.error_reply(stanza, "cancel", "not-acceptable", "Wrong sequence. Packet lost?")) + self:close(); + self:event("error"); + return true; + end + self.iseq = seq + 1; + local data = base64.decode(bdata); + if self.stanza == "iq" then + self.stream:send(verse.reply(stanza)) + end + self:event("incoming-raw", data); + return true; + end + elseif child.name == "close" then + self.stream:send(verse.reply(stanza)) + self:close(); + return true + end +end + +--[[ FIXME some day +function ibb_conn:receive(patt) + -- is this even used? + print("ibb_conn:receive("..tostring(patt)..")"); + assert(patt == "*a" or tonumber(patt)); + local data = t_concat(self.ibuffer):sub(self.pos, tonumber(patt) or nil); + self.pos = self.pos + #data; + return data +end + +function ibb_conn:dirty() + print("ibb_conn:dirty()"); + return false -- ???? +end +function ibb_conn:getfd() + return 0 +end +function ibb_conn:settimeout(n) + -- ignore? +end +-]] + +function ibb_conn:close() + self.stream:unhook("iq/".. xmlns_ibb, self.feeder) + self:event("disconnected"); +end + +function verse.plugins.jingle_ibb(stream) + stream:hook("ready", function () + stream:add_disco_feature(xmlns_jingle_ibb); + end, 10); + + local ibb = {}; + + function ibb:_setup() + local conn = new_ibb(self.stream); + conn.sid = self.sid or conn.sid; + conn.stanza = self.stanza or conn.stanza; + conn.block = self.block or conn.block; + conn:initiate(self.peer, self.sid, self.stanza); + self.conn = conn; + end + function ibb:generate_initiate() + print("ibb:generate_initiate() as ".. self.role); + local sid = uuid_generate(); + self.sid = sid; + self.stanza = 'iq'; + self.block = 2048; + local transport = verse.stanza("transport", { xmlns = xmlns_jingle_ibb, + sid = self.sid, stanza = self.stanza, ["block-size"] = self.block }); + return transport; + end + function ibb:generate_accept(initiate_transport) + print("ibb:generate_accept() as ".. self.role); + local attr = initiate_transport.attr; + self.sid = attr.sid or self.sid; + self.stanza = attr.stanza or self.stanza; + self.block = attr["block-size"] or self.block; + self:_setup(); + return initiate_transport; + end + function ibb:connect(callback) + if not self.conn then + self:_setup(); + end + local conn = self.conn; + print("ibb:connect() as ".. self.role); + if self.role == "initiator" then + conn:open(function(ok, ...) + assert(ok, table.concat({...}, ", ")); + callback(conn); + end); + else + callback(conn); + end + end + function ibb:info_received(jingle_tag) + print("ibb:info_received()"); + -- TODO, what exactly? + end + function ibb:disconnect() + if self.conn then + self.conn:close() + end + end + function ibb:handle_accepted(jingle_tag) end + + local ibb_mt = { __index = ibb }; + stream:hook("jingle/transport/"..xmlns_jingle_ibb, function (jingle) + return setmetatable({ + role = jingle.role, + peer = jingle.peer, + stream = jingle.stream, + jingle = jingle, + }, ibb_mt); + end); +end diff --git a/verse/plugins/jingle_s5b.lua b/verse/plugins/jingle_s5b.lua new file mode 100644 index 0000000..4453272 --- /dev/null +++ b/verse/plugins/jingle_s5b.lua @@ -0,0 +1,220 @@ +local verse = require "verse"; + +local xmlns_s5b = "urn:xmpp:jingle:transports:s5b:1"; +local xmlns_bytestreams = "http://jabber.org/protocol/bytestreams"; +local sha1 = require "util.sha1".sha1; +local uuid_generate = require "util.uuid".generate; + +local function negotiate_socks5(conn, hash) + local function suppress_connected() + conn:unhook("connected", suppress_connected); + return true; + end + local function receive_connection_response(data) + conn:unhook("incoming-raw", receive_connection_response); + + if data:sub(1, 2) ~= "\005\000" then + return conn:event("error", "connection-failure"); + end + conn:event("connected"); + return true; + end + local function receive_auth_response(data) + conn:unhook("incoming-raw", receive_auth_response); + if data ~= "\005\000" then -- SOCKSv5; "NO AUTHENTICATION" + -- Server is not SOCKSv5, or does not allow no auth + local err = "version-mismatch"; + if data:sub(1,1) == "\005" then + err = "authentication-failure"; + end + return conn:event("error", err); + end + -- Request SOCKS5 connection + conn:send(string.char(0x05, 0x01, 0x00, 0x03, #hash)..hash.."\0\0"); --FIXME: Move to "connected"? + conn:hook("incoming-raw", receive_connection_response, 100); + return true; + end + conn:hook("connected", suppress_connected, 200); + conn:hook("incoming-raw", receive_auth_response, 100); + conn:send("\005\001\000"); -- SOCKSv5; 1 mechanism; "NO AUTHENTICATION" +end + +local function connect_to_usable_streamhost(callback, streamhosts, auth_token) + local conn = verse.new(nil, { + streamhosts = streamhosts, + current_host = 0; + }); + --Attempt to connect to the next host + local function attempt_next_streamhost(event) + if event then + return callback(nil, event.reason); + end + -- First connect, or the last connect failed + if conn.current_host < #conn.streamhosts then + conn.current_host = conn.current_host + 1; + conn:debug("Attempting to connect to "..conn.streamhosts[conn.current_host].host..":"..conn.streamhosts[conn.current_host].port.."..."); + local ok, err = conn:connect( + conn.streamhosts[conn.current_host].host, + conn.streamhosts[conn.current_host].port + ); + if not ok then + conn:debug("Error connecting to proxy (%s:%s): %s", + conn.streamhosts[conn.current_host].host, + conn.streamhosts[conn.current_host].port, + err + ); + else + conn:debug("Connecting..."); + end + negotiate_socks5(conn, auth_token); + return true; -- Halt processing of disconnected event + end + -- All streamhosts tried, none successful + conn:unhook("disconnected", attempt_next_streamhost); + return callback(nil); + -- Let disconnected event fall through to user handlers... + end + conn:hook("disconnected", attempt_next_streamhost, 100); + -- When this event fires, we're connected to a streamhost + conn:hook("connected", function () + conn:unhook("disconnected", attempt_next_streamhost); + callback(conn.streamhosts[conn.current_host], conn); + end, 100); + attempt_next_streamhost(); -- Set it in motion + return conn; +end + +function verse.plugins.jingle_s5b(stream) + stream:hook("ready", function () + stream:add_disco_feature(xmlns_s5b); + end, 10); + + local s5b = {}; + + function s5b:generate_initiate() + self.s5b_sid = uuid_generate(); + local transport = verse.stanza("transport", { xmlns = xmlns_s5b, + mode = "tcp", sid = self.s5b_sid }); + local p = 0; + for jid, streamhost in pairs(stream.proxy65.available_streamhosts) do + p = p + 1; + transport:tag("candidate", { jid = jid, host = streamhost.host, + port = streamhost.port, cid=jid, priority = p, type = "proxy" }):up(); + end + stream:debug("Have %d proxies", p) + return transport; + end + + function s5b:generate_accept(initiate_transport) + local candidates = {}; + self.s5b_peer_candidates = candidates; + self.s5b_mode = initiate_transport.attr.mode or "tcp"; + self.s5b_sid = initiate_transport.attr.sid or self.jingle.sid; + + -- Import the list of candidates the initiator offered us + for candidate in initiate_transport:childtags() do + --if candidate.attr.jid == "asterix4@jabber.lagaule.org/Gajim" + --and candidate.attr.host == "82.246.25.239" then + candidates[candidate.attr.cid] = { + type = candidate.attr.type; + jid = candidate.attr.jid; + host = candidate.attr.host; + port = tonumber(candidate.attr.port) or 0; + priority = tonumber(candidate.attr.priority) or 0; + cid = candidate.attr.cid; + }; + --end + end + + -- Import our own candidates + -- TODO ^ + local transport = verse.stanza("transport", { xmlns = xmlns_s5b }); + return transport; + end + + function s5b:connect(callback) + stream:warn("Connecting!"); + + local streamhost_array = {}; + for cid, streamhost in pairs(self.s5b_peer_candidates or {}) do + streamhost_array[#streamhost_array+1] = streamhost; + end + + if #streamhost_array > 0 then + self.connecting_peer_candidates = true; + local function onconnect(streamhost, conn) + self.jingle:send_command("transport-info", verse.stanza("content", { creator = self.creator, name = self.name }) + :tag("transport", { xmlns = xmlns_s5b, sid = self.s5b_sid }) + :tag("candidate-used", { cid = streamhost.cid })); + self.onconnect_callback = callback; + self.conn = conn; + end + local auth_token = sha1(self.s5b_sid..self.peer..stream.jid, true); + connect_to_usable_streamhost(onconnect, streamhost_array, auth_token); + else + stream:warn("Actually, I'm going to wait for my peer to tell me its streamhost..."); + self.onconnect_callback = callback; + end + end + + function s5b:info_received(jingle_tag) + stream:warn("Info received"); + local content_tag = jingle_tag:child_with_name("content"); + local transport_tag = content_tag:child_with_name("transport"); + if transport_tag:get_child("candidate-used") and not self.connecting_peer_candidates then + local candidate_used = transport_tag:child_with_name("candidate-used"); + if candidate_used then + -- Connect straight away to candidate used, we weren't trying any anyway + local function onconnect(streamhost, conn) + if self.jingle.role == "initiator" then -- More correct would be - "is this a candidate we offered?" + -- Activate the stream + self.jingle.stream:send_iq(verse.iq({ to = streamhost.jid, type = "set" }) + :tag("query", { xmlns = xmlns_bytestreams, sid = self.s5b_sid }) + :tag("activate"):text(self.jingle.peer), function (result) + + if result.attr.type == "result" then + self.jingle:send_command("transport-info", verse.stanza("content", content_tag.attr) + :tag("transport", { xmlns = xmlns_s5b, sid = self.s5b_sid }) + :tag("activated", { cid = candidate_used.attr.cid })); + self.conn = conn; + self.onconnect_callback(conn); + else + self.jingle.stream:error("Failed to activate bytestream"); + end + end); + end + end + + -- FIXME: Another assumption that cid==jid, and that it was our candidate + self.jingle.stream:debug("CID: %s", self.jingle.stream.proxy65.available_streamhosts[candidate_used.attr.cid]); + local streamhost_array = { + self.jingle.stream.proxy65.available_streamhosts[candidate_used.attr.cid]; + }; + + local auth_token = sha1(self.s5b_sid..stream.jid..self.peer, true); + connect_to_usable_streamhost(onconnect, streamhost_array, auth_token); + end + elseif transport_tag:get_child("activated") then + self.onconnect_callback(self.conn); + end + end + + function s5b:disconnect() + if self.conn then + self.conn:close(); + end + end + + function s5b:handle_accepted(jingle_tag) + end + + local s5b_mt = { __index = s5b }; + stream:hook("jingle/transport/"..xmlns_s5b, function (jingle) + return setmetatable({ + role = jingle.role, + peer = jingle.peer, + stream = jingle.stream, + jingle = jingle, + }, s5b_mt); + end); +end diff --git a/verse/plugins/keepalive.lua b/verse/plugins/keepalive.lua new file mode 100644 index 0000000..7afd812 --- /dev/null +++ b/verse/plugins/keepalive.lua @@ -0,0 +1,9 @@ +local verse = require "verse"; + +function verse.plugins.keepalive(stream) + stream.keepalive_timeout = stream.keepalive_timeout or 300; + verse.add_task(stream.keepalive_timeout, function () + stream.conn:write(" "); + return stream.keepalive_timeout; + end); +end diff --git a/verse/plugins/legacy.lua b/verse/plugins/legacy.lua new file mode 100644 index 0000000..f12517d --- /dev/null +++ b/verse/plugins/legacy.lua @@ -0,0 +1,65 @@ +local verse = require "verse"; +local uuid = require "util.uuid".generate; + +local xmlns_auth = "jabber:iq:auth"; + +function verse.plugins.legacy(stream) + function handle_auth_form(result) + local query = result:get_child("query", xmlns_auth); + if result.attr.type ~= "result" or not query then + local type, cond, text = result:get_error(); + stream:debug("warn", "%s %s: %s", type, cond, text); + --stream:event("authentication-failure", { condition = cond }); + -- COMPAT continue anyways + end + local auth_data = { + username = stream.username; + password = stream.password; + resource = stream.resource or uuid(); + digest = false, sequence = false, token = false; + }; + local request = verse.iq({ to = stream.host, type = "set" }) + :tag("query", { xmlns = xmlns_auth }); + if #query > 0 then + for tag in query:childtags() do + local field = tag.name; + local value = auth_data[field]; + if value then + request:tag(field):text(auth_data[field]):up(); + elseif value == nil then + local cond = "feature-not-implemented"; + stream:event("authentication-failure", { condition = cond }); + return false; + end + end + else -- COMPAT for servers not following XEP 78 + for field, value in pairs(auth_data) do + if value then + request:tag(field):text(value):up(); + end + end + end + stream:send_iq(request, function (response) + if response.attr.type == "result" then + stream.resource = auth_data.resource; + stream.jid = auth_data.username.."@"..stream.host.."/"..auth_data.resource; + stream:event("authentication-success"); + stream:event("bind-success", stream.jid); + else + local type, cond, text = response:get_error(); + stream:event("authentication-failure", { condition = cond }); + end + end); + end + + function handle_opened(attr) + if not attr.version then + stream:send_iq(verse.iq({type="get"}) + :tag("query", { xmlns = "jabber:iq:auth" }) + :tag("username"):text(stream.username), + handle_auth_form); + + end + end + stream:hook("opened", handle_opened); +end diff --git a/verse/plugins/pep.lua b/verse/plugins/pep.lua new file mode 100644 index 0000000..2a6c7ef --- /dev/null +++ b/verse/plugins/pep.lua @@ -0,0 +1,34 @@ +local verse = require "verse"; + +local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; +local xmlns_pubsub_event = xmlns_pubsub.."#event"; + +function verse.plugins.pep(stream) + stream:add_plugin("disco"); + stream:add_plugin("pubsub"); + stream.pep = {}; + + stream:hook("pubsub/event", function(event) + return stream:event("pep/"..event.node, { from = event.from, item = event.item.tags[1] } ); + end); + + function stream:hook_pep(node, callback, priority) + local handlers = stream.events._handlers["pep/"..node]; + if not(handlers) or #handlers == 0 then + stream:add_disco_feature(node.."+notify"); + end + stream:hook("pep/"..node, callback, priority); + end + + function stream:unhook_pep(node, callback) + stream:unhook("pep/"..node, callback); + local handlers = stream.events._handlers["pep/"..node]; + if not(handlers) or #handlers == 0 then + stream:remove_disco_feature(node.."+notify"); + end + end + + function stream:publish_pep(item, node) + return stream.pubsub:service(nil):node(node or item.attr.xmlns):publish(nil, nil, item) + end +end diff --git a/verse/plugins/ping.lua b/verse/plugins/ping.lua new file mode 100644 index 0000000..b89a671 --- /dev/null +++ b/verse/plugins/ping.lua @@ -0,0 +1,24 @@ +local verse = require "verse"; + +local xmlns_ping = "urn:xmpp:ping"; + +function verse.plugins.ping(stream) + function stream:ping(jid, callback) + local t = socket.gettime(); + stream:send_iq(verse.iq{ to = jid, type = "get" }:tag("ping", { xmlns = xmlns_ping }), + function (reply) + if reply.attr.type == "error" then + local type, condition, text = reply:get_error(); + if condition ~= "service-unavailable" and condition ~= "feature-not-implemented" then + callback(nil, jid, { type = type, condition = condition, text = text }); + return; + end + end + callback(socket.gettime()-t, jid); + end); + end + stream:hook("iq/"..xmlns_ping, function(stanza) + return stream:send(verse.reply(stanza)); + end); + return true; +end diff --git a/verse/plugins/presence.lua b/verse/plugins/presence.lua new file mode 100644 index 0000000..898e27e --- /dev/null +++ b/verse/plugins/presence.lua @@ -0,0 +1,36 @@ +local verse = require "verse"; + +function verse.plugins.presence(stream) + stream.last_presence = nil; + + stream:hook("presence-out", function (presence) + if not presence.attr.to then + stream.last_presence = presence; -- Cache non-directed presence + end + end, 1); + + function stream:resend_presence() + if last_presence then + stream:send(last_presence); + end + end + + function stream:set_status(opts) + local p = verse.presence(); + if type(opts) == "table" then + if opts.show then + p:tag("show"):text(opts.show):up(); + end + if opts.prio then + p:tag("priority"):text(tostring(opts.prio)):up(); + end + if opts.msg then + p:tag("status"):text(opts.msg):up(); + end + end + -- TODO maybe use opts as prio if it's a int, + -- or as show or status if it's a string? + + stream:send(p); + end +end diff --git a/verse/plugins/private.lua b/verse/plugins/private.lua new file mode 100644 index 0000000..473a8f1 --- /dev/null +++ b/verse/plugins/private.lua @@ -0,0 +1,35 @@ +local verse = require "verse"; + +-- Implements XEP-0049: Private XML Storage + +local xmlns_private = "jabber:iq:private"; + +function verse.plugins.private(stream) + function stream:private_set(name, xmlns, data, callback) + local iq = verse.iq({ type = "set" }) + :tag("query", { xmlns = xmlns_private }); + if data then + if data.name == name and data.attr and data.attr.xmlns == xmlns then + iq:add_child(data); + else + iq:tag(name, { xmlns = xmlns }) + :add_child(data); + end + end + self:send_iq(iq, callback); + end + + function stream:private_get(name, xmlns, callback) + self:send_iq(verse.iq({type="get"}) + :tag("query", { xmlns = xmlns_private }) + :tag(name, { xmlns = xmlns }), + function (reply) + if reply.attr.type == "result" then + local query = reply:get_child("query", xmlns_private); + local result = query:get_child(name, xmlns); + callback(result); + end + end); + end +end + diff --git a/verse/plugins/proxy65.lua b/verse/plugins/proxy65.lua new file mode 100644 index 0000000..377ac2d --- /dev/null +++ b/verse/plugins/proxy65.lua @@ -0,0 +1,186 @@ +local events = require "util.events"; +local uuid = require "util.uuid"; +local sha1 = require "util.sha1"; + +local proxy65_mt = {}; +proxy65_mt.__index = proxy65_mt; + +local xmlns_bytestreams = "http://jabber.org/protocol/bytestreams"; + +local negotiate_socks5; + +function verse.plugins.proxy65(stream) + stream.proxy65 = setmetatable({ stream = stream }, proxy65_mt); + stream.proxy65.available_streamhosts = {}; + local outstanding_proxies = 0; + stream:hook("disco/service-discovered/proxy", function (service) + -- Fill list with available proxies + if service.type == "bytestreams" then + outstanding_proxies = outstanding_proxies + 1; + stream:send_iq(verse.iq({ to = service.jid, type = "get" }) + :tag("query", { xmlns = xmlns_bytestreams }), function (result) + + outstanding_proxies = outstanding_proxies - 1; + if result.attr.type == "result" then + local streamhost = result:get_child("query", xmlns_bytestreams) + :get_child("streamhost").attr; + + stream.proxy65.available_streamhosts[streamhost.jid] = { + jid = streamhost.jid; + host = streamhost.host; + port = tonumber(streamhost.port); + }; + end + if outstanding_proxies == 0 then + stream:event("proxy65/discovered-proxies", stream.proxy65.available_streamhosts); + end + end); + end + end); + stream:hook("iq/"..xmlns_bytestreams, function (request) + local conn = verse.new(nil, { + initiator_jid = request.attr.from, + streamhosts = {}, + current_host = 0; + }); + + -- Parse hosts from request + for tag in request.tags[1]:childtags() do + if tag.name == "streamhost" then + table.insert(conn.streamhosts, tag.attr); + end + end + + --Attempt to connect to the next host + local function attempt_next_streamhost() + -- First connect, or the last connect failed + if conn.current_host < #conn.streamhosts then + conn.current_host = conn.current_host + 1; + conn:connect( + conn.streamhosts[conn.current_host].host, + conn.streamhosts[conn.current_host].port + ); + negotiate_socks5(stream, conn, request.tags[1].attr.sid, request.attr.from, stream.jid); + return true; -- Halt processing of disconnected event + end + -- All streamhosts tried, none successful + conn:unhook("disconnected", attempt_next_streamhost); + stream:send(verse.error_reply(request, "cancel", "item-not-found")); + -- Let disconnected event fall through to user handlers... + end + + function conn:accept() + conn:hook("disconnected", attempt_next_streamhost, 100); + -- When this event fires, we're connected to a streamhost + conn:hook("connected", function () + conn:unhook("disconnected", attempt_next_streamhost); + -- Send XMPP success notification + local reply = verse.reply(request) + :tag("query", request.tags[1].attr) + :tag("streamhost-used", { jid = conn.streamhosts[conn.current_host].jid }); + stream:send(reply); + end, 100); + attempt_next_streamhost(); + end + function conn:refuse() + -- FIXME: XMPP refused reply + end + stream:event("proxy65/request", conn); + end); +end + +function proxy65_mt:new(target_jid, proxies) + local conn = verse.new(nil, { + target_jid = target_jid; + bytestream_sid = uuid.generate(); + }); + + local request = verse.iq{type="set", to = target_jid} + :tag("query", { xmlns = xmlns_bytestreams, mode = "tcp", sid = conn.bytestream_sid }); + for _, proxy in ipairs(proxies or self.proxies) do + request:tag("streamhost", proxy):up(); + end + + + self.stream:send_iq(request, function (reply) + if reply.attr.type == "error" then + local type, condition, text = reply:get_error(); + conn:event("connection-failed", { conn = conn, type = type, condition = condition, text = text }); + else + -- Target connected to streamhost, connect ourselves + local streamhost_used = reply.tags[1]:get_child("streamhost-used"); + if not streamhost_used then + --FIXME: Emit error + end + conn.streamhost_jid = streamhost_used.attr.jid; + local host, port; + for _, proxy in ipairs(proxies or self.proxies) do + if proxy.jid == conn.streamhost_jid then + host, port = proxy.host, proxy.port; + break; + end + end + if not (host and port) then + --FIXME: Emit error + end + + conn:connect(host, port); + + local function handle_proxy_connected() + conn:unhook("connected", handle_proxy_connected); + -- Both of us connected, tell proxy to activate connection + local request = verse.iq{to = conn.streamhost_jid, type="set"} + :tag("query", { xmlns = xmlns_bytestreams, sid = conn.bytestream_sid }) + :tag("activate"):text(target_jid); + self.stream:send_iq(request, function (reply) + if reply.attr.type == "result" then + -- Connection activated, ready to use + conn:event("connected", conn); + else + --FIXME: Emit error + end + end); + return true; + end + conn:hook("connected", handle_proxy_connected, 100); + + negotiate_socks5(self.stream, conn, conn.bytestream_sid, self.stream.jid, target_jid); + end + end); + return conn; +end + +function negotiate_socks5(stream, conn, sid, requester_jid, target_jid) + local hash = sha1.sha1(sid..requester_jid..target_jid); + local function suppress_connected() + conn:unhook("connected", suppress_connected); + return true; + end + local function receive_connection_response(data) + conn:unhook("incoming-raw", receive_connection_response); + + if data:sub(1, 2) ~= "\005\000" then + return conn:event("error", "connection-failure"); + end + conn:event("connected"); + return true; + end + local function receive_auth_response(data) + conn:unhook("incoming-raw", receive_auth_response); + if data ~= "\005\000" then -- SOCKSv5; "NO AUTHENTICATION" + -- Server is not SOCKSv5, or does not allow no auth + local err = "version-mismatch"; + if data:sub(1,1) == "\005" then + err = "authentication-failure"; + end + return conn:event("error", err); + end + -- Request SOCKS5 connection + conn:send(string.char(0x05, 0x01, 0x00, 0x03, #hash)..hash.."\0\0"); --FIXME: Move to "connected"? + conn:hook("incoming-raw", receive_connection_response, 100); + return true; + end + conn:hook("connected", suppress_connected, 200); + conn:hook("incoming-raw", receive_auth_response, 100); + conn:send("\005\001\000"); -- SOCKSv5; 1 mechanism; "NO AUTHENTICATION" +end diff --git a/verse/plugins/pubsub.lua b/verse/plugins/pubsub.lua new file mode 100644 index 0000000..5ff05fd --- /dev/null +++ b/verse/plugins/pubsub.lua @@ -0,0 +1,267 @@ +local verse = require "verse"; +local jid_bare = require "util.jid".bare; + +local t_insert = table.insert; + +local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; +local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; +local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event"; +local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors"; + +local pubsub = {}; +local pubsub_mt = { __index = pubsub }; + +function verse.plugins.pubsub(stream) + stream.pubsub = setmetatable({ stream = stream }, pubsub_mt); + stream:hook("message", function (message) + local m_from = message.attr.from; + for pubsub_event in message:childtags("event", xmlns_pubsub_event) do + local items = pubsub_event:get_child("items"); + if items then + local node = items.attr.node; + for item in items:childtags("item") do + stream:event("pubsub/event", { + from = m_from; + node = node; + item = item; + }); + end + end + end + end); + return true; +end + +-- COMPAT +function pubsub:create(server, node, callback) + return self:service(server):node(node):create(nil, callback); +end + +function pubsub:subscribe(server, node, jid, callback) + return self:service(server):node(node):subscribe(jid, nil, callback); +end + +function pubsub:publish(server, node, id, item, callback) + return self:service(server):node(node):publish(id, nil, item, callback); +end + +-------------------------------------------------------------------------- +---------------------New and improved PubSub interface-------------------- +-------------------------------------------------------------------------- + +local pubsub_service = {}; +local pubsub_service_mt = { __index = pubsub_service }; + +-- TODO should the property be named 'jid' instead? +function pubsub:service(service) + return setmetatable({ stream = self.stream, service = service }, pubsub_service_mt) +end + +-- Helper function for iq+pubsub tags + +local function pubsub_iq(iq_type, to, ns, op, node, jid, item_id) + local st = verse.iq{ type = iq_type or "get", to = to } + :tag("pubsub", { xmlns = ns or xmlns_pubsub }) -- ns would be ..#owner + if op then st:tag(op, { node = node, jid = jid }); end + if item_id then st:tag("item", { id = item_id ~= true and item_id or nil }); end + return st; +end + +-- http://xmpp.org/extensions/xep-0060.html#entity-subscriptions +function pubsub_service:subscriptions(callback) + self.stream:send_iq(pubsub_iq(nil, self.service, nil, "subscriptions") + , callback and function (result) + if result.attr.type == "result" then + local ps = result:get_child("pubsub", xmlns_pubsub); + local subs = ps and ps:get_child("subscriptions"); + local nodes = {}; + if subs then + for sub in subs:childtags("subscription") do + local node = self:node(sub.attr.node) + node.subscription = sub; + node.subscribed_jid = sub.attr.jid; + t_insert(nodes, node); + -- FIXME Good enough? + -- Or how about: + -- nodes[node] = sub; + end + end + callback(nodes); + else + callback(false, result:get_error()); + end + end or nil); +end + +-- http://xmpp.org/extensions/xep-0060.html#entity-affiliations +function pubsub_service:affiliations(callback) + self.stream:send_iq(pubsub_iq(nil, self.service, nil, "affiliations") + , callback and function (result) + if result.attr.type == "result" then + local ps = result:get_child("pubsub", xmlns_pubsub); + local affils = ps and ps:get_child("affiliations") or {}; + local nodes = {}; + if affils then + for affil in affils:childtags("affiliation") do + local node = self:node(affil.attr.node) + node.affiliation = affil; + t_insert(nodes, node); + -- nodes[node] = affil; + end + end + callback(nodes); + else + callback(false, result:get_error()); + end + end or nil); +end + +function pubsub_service:nodes(callback) + self.stream:disco_items(self.service, nil, function(items, ...) + if items then + for i=1,#items do + items[i] = self:node(items[i].node); + end + end + callback(items, ...) + end); +end + +local pubsub_node = {}; +local pubsub_node_mt = { __index = pubsub_node }; + +function pubsub_service:node(node) + return setmetatable({ stream = self.stream, service = self.service, node = node }, pubsub_node_mt) +end + +function pubsub_mt:__call(service, node) + local s = self:service(service); + return node and s:node(node) or s; +end + +function pubsub_node:hook(callback, prio) + self._hooks = self._hooks or setmetatable({}, { __mode = 'kv' }); + local function hook(event) + -- FIXME service == nil would mean anyone, + -- publishing would be go to your bare jid. + -- So if you're only interestied in your own + -- events, hook your own bare jid. + if (not event.service or event.from == self.service) and event.node == self.node then + return callback(event) + end + end + self._hooks[callback] = hook; + self.stream:hook("pubsub/event", hook, prio); + return hook; +end + +function pubsub_node:unhook(callback) + if callback then + local hook = self._hooks[callback]; + self.stream:unhook("pubsub/event", hook); + elseif self._hooks then + for hook in pairs(self._hooks) do + self.stream:unhook("pubsub/event", hook); + end + end +end + +function pubsub_node:create(config, callback) + if config ~= nil then + error("Not implemented yet."); + else + self.stream:send_iq(pubsub_iq("set", self.service, nil, "create", self.node), callback); + end +end + +-- and rolled into one +function pubsub_node:configure(config, callback) + if config ~= nil then + error("Not implemented yet."); + --[[ + if config == true then + self.stream:send_iq(pubsub_iq("get", self.service, nil, "configure", self.node) + , function(reply) + local form = reply:get_child("pubsub"):get_child("configure"):get_cild("x"); + local config = callback(require"util.dataforms".something(form)) + self.stream:send_iq(pubsub_iq("set", config, ...)) + end); + end + --]] + -- fetch form and pass it to the callback + -- which would process it and pass it back + -- and then we submit it + -- elseif type(config) == "table" then + -- it's a form or stanza that we submit + -- end + -- this would be done for everything that needs a config + end + self.stream:send_iq(pubsub_iq("set", self.service, nil, config == nil and "default" or "configure", self.node), callback); +end + +function pubsub_node:publish(id, options, node, callback) + if options ~= nil then + error("Node configuration is not implemented yet."); + end + self.stream:send_iq(pubsub_iq("set", self.service, nil, "publish", self.node, nil, id or true) + :add_child(node) + , callback); +end + +function pubsub_node:subscribe(jid, options, callback) + jid = jid or self.stream.jid; + if options ~= nil then + error("Subscription configuration is not implemented yet."); + end + self.stream:send_iq(pubsub_iq("set", self.service, nil, "subscribe", self.node, jid, id) + , callback); +end + +function pubsub_node:subscription(callback) + error("Not implemented yet."); +end + +function pubsub_node:affiliation(callback) + error("Not implemented yet."); +end + +function pubsub_node:unsubscribe(jid, callback) + jid = jid or self.subscribed_jid or self.stream.jid; + self.stream:send_iq(pubsub_iq("set", self.service, nil, "unsubscribe", self.node, jid) + , callback); +end + +function pubsub_node:configure_subscription(options, callback) + error("Not implemented yet."); +end + +function pubsub_node:items(full, callback) + if full then + self.stream:send_iq(pubsub_iq("get", self.service, nil, "items", self.node) + , callback); + else + self.stream:disco_items(self.service, self.node, callback); + end +end + +function pubsub_node:item(id, callback) + self.stream:send_iq(pubsub_iq("get", self.service, nil, "items", self.node, nil, id) + , callback); +end + +function pubsub_node:retract(id, callback) + self.stream:send_iq(pubsub_iq("set", self.service, nil, "retract", self.node, nil, id) + , callback); +end + +function pubsub_node:purge(notify, callback) + assert(not notify, "Not implemented yet."); + self.stream:send_iq(pubsub_iq("set", self.service, xmlns_pubsub_owner, "purge", self.node) + , callback); +end + +function pubsub_node:delete(redirect_uri, callback) + assert(not redirect_uri, "Not implemented yet."); + self.stream:send_iq(pubsub_iq("set", self.service, xmlns_pubsub_owner, "delete", self.node) + , callback); +end diff --git a/verse/plugins/receipts.lua b/verse/plugins/receipts.lua new file mode 100644 index 0000000..3d049fc --- /dev/null +++ b/verse/plugins/receipts.lua @@ -0,0 +1,16 @@ +local verse = require"verse"; +local xmlns_receipts = "urn:xmpp:receipts"; + +function verse.plugins.receipts(stream) + stream:add_plugin("disco"); + local function send_receipt(stanza) + if stanza:get_child("request", xmlns_receipts) then + stream:send(verse.reply(stanza) + :tag("received", { xmlns = xmlns_receipts, id = stanza.attr.id })); + end + end + + stream:add_disco_feature(xmlns_receipts); + stream:hook("message", send_receipt, 1000); +end + diff --git a/verse/plugins/register.lua b/verse/plugins/register.lua new file mode 100644 index 0000000..c4e9e6d --- /dev/null +++ b/verse/plugins/register.lua @@ -0,0 +1,32 @@ +local verse = require "verse"; + +local xmlns_register = "jabber:iq:register"; + +function verse.plugins.register(stream) + local function handle_features(features_stanza) + if features_stanza:get_child("register", "http://jabber.org/features/iq-register") then + local request = verse.iq({ to = stream.host_, type = "set" }) + :tag("query", { xmlns = xmlns_register }) + :tag("username"):text(stream.username):up() + :tag("password"):text(stream.password):up(); + if stream.register_email then + request:tag("email"):text(stream.register_email):up(); + end + stream:send_iq(request, function (result) + if result.attr.type == "result" then + stream:event("registration-success"); + else + local type, condition, text = result:get_error(); + stream:debug("Registration failed: %s", condition); + stream:event("registration-failure", { type = type, condition = condition, text = text }); + end + end); + else + stream:debug("In-band registration not offered by server"); + stream:event("registration-failure", { condition = "service-unavailable" }); + end + stream:unhook("stream-features", handle_features); + return true; + end + stream:hook("stream-features", handle_features, 310); +end diff --git a/verse/plugins/roster.lua b/verse/plugins/roster.lua new file mode 100644 index 0000000..1f7bb53 --- /dev/null +++ b/verse/plugins/roster.lua @@ -0,0 +1,161 @@ +local verse = require "verse"; +local bare_jid = require "util.jid".bare; + +local xmlns_roster = "jabber:iq:roster"; +local xmlns_rosterver = "urn:xmpp:features:rosterver"; +local t_insert = table.insert; + +function verse.plugins.roster(stream) + local ver_supported = false; + local roster = { + items = {}; + ver = ""; + -- TODO: + -- groups = {}; + }; + stream.roster = roster; + + stream:hook("stream-features", function(features_stanza) + if features_stanza:get_child("ver", xmlns_rosterver) then + ver_supported = true; + end + end); + + local function item_lua2xml(item_table) + local xml_item = verse.stanza("item", { xmlns = xmlns_roster }); + for k, v in pairs(item_table) do + if k ~= "groups" then + xml_item.attr[k] = v; + else + for i = 1,#v do + xml_item:tag("group"):text(v[i]):up(); + end + end + end + return xml_item; + end + + local function item_xml2lua(xml_item) + local item_table = { }; + local groups = {}; + item_table.groups = groups; + local jid = xml_item.attr.jid; + + for k, v in pairs(xml_item.attr) do + if k ~= "xmlns" then + item_table[k] = v + end + end + + for group in xml_item:childtags("group") do + t_insert(groups, group:get_text()) + end + return item_table; + end + + function roster:load(r) + roster.ver, roster.items = r.ver, r.items; + end + + function roster:dump() + return { + ver = roster.ver, + items = roster.items, + }; + end + + -- should this be add_contact(item, callback) instead? + function roster:add_contact(jid, name, groups, callback) + local item = { jid = jid, name = name, groups = groups }; + local stanza = verse.iq({ type = "set" }) + :tag("query", { xmlns = xmlns_roster }) + :add_child(item_lua2xml(item)); + stream:send_iq(stanza, function (reply) + if not callback then return end + if reply.attr.type == "result" then + callback(true); + else + local type, condition, text = reply:get_error(); + callback(nil, { type, condition, text }); + end + end); + end + -- What about subscriptions? + + function roster:delete_contact(jid, callback) + jid = (type(jid) == "table" and jid.jid) or jid; + local item = { jid = jid, subscription = "remove" } + if not roster.items[jid] then return false, "item-not-found"; end + stream:send_iq(verse.iq({ type = "set" }) + :tag("query", { xmlns = xmlns_roster }) + :add_child(item_lua2xml(item)), + function (reply) + if not callback then return end + if reply.attr.type == "result" then + callback(true); + else + local type, condition, text = reply:get_error(); + callback(nil, { type, condition, text }); + end + end); + end + + local function add_item(item) -- Takes one roster + local roster_item = item_xml2lua(item); + roster.items[roster_item.jid] = roster_item; + end + + -- Private low level + local function delete_item(jid) + local deleted_item = roster.items[jid]; + roster.items[jid] = nil; + return deleted_item; + end + + function roster:fetch(callback) + stream:send_iq(verse.iq({type="get"}):tag("query", { xmlns = xmlns_roster, ver = ver_supported and roster.ver or nil }), + function (result) + if result.attr.type == "result" then + local query = result:get_child("query", xmlns_roster); + if query then + roster.items = {}; + for item in query:childtags("item") do + add_item(item) + end + roster.ver = query.attr.ver or ""; + end + callback(roster); + else + local type, condition, text = stanza:get_error(); + callback(nil, { type, condition, text }); --FIXME + end + end); + end + + stream:hook("iq/"..xmlns_roster, function(stanza) + local type, from = stanza.attr.type, stanza.attr.from; + if type == "set" and (not from or from == bare_jid(stream.jid)) then + local query = stanza:get_child("query", xmlns_roster); + local item = query and query:get_child("item"); + if item then + local event, target; + local jid = item.attr.jid; + if item.attr.subscription == "remove" then + event = "removed" + target = delete_item(jid); + else + event = roster.items[jid] and "changed" or "added"; + add_item(item) + target = roster.items[jid]; + end + roster.ver = query.attr.ver; + if target then + stream:event("roster/item-"..event, target); + end + -- TODO else return error? Events? + end + stream:send(verse.reply(stanza)) + return true; + end + end); +end diff --git a/verse/plugins/sasl.lua b/verse/plugins/sasl.lua new file mode 100644 index 0000000..eef8131 --- /dev/null +++ b/verse/plugins/sasl.lua @@ -0,0 +1,80 @@ +-- local verse = require"verse"; +local base64, unbase64 = require "mime".b64, require"mime".unb64; +local xmlns_sasl = "urn:ietf:params:xml:ns:xmpp-sasl"; + +function verse.plugins.sasl(stream) + local function handle_features(features_stanza) + if stream.authenticated then return; end + stream:debug("Authenticating with SASL..."); + local sasl_mechanisms = features_stanza:get_child("mechanisms", xmlns_sasl); + if not sasl_mechanisms then return end + + local mechanisms = {}; + local preference = {}; + + for mech in sasl_mechanisms:childtags("mechanism") do + mech = mech:get_text(); + stream:debug("Server offers %s", mech); + if not mechanisms[mech] then + local name = mech:match("[^-]+"); + local ok, impl = pcall(require, "util.sasl."..name:lower()); + if ok then + stream:debug("Loaded SASL %s module", name); + mechanisms[mech], preference[mech] = impl(stream, mech); + elseif not tostring(impl):match("not found") then + stream:debug("Loading failed: %s", tostring(impl)); + end + end + end + + local supported = {}; -- by the server + for mech in pairs(mechanisms) do + table.insert(supported, mech); + end + if not supported[1] then + stream:event("authentication-failure", { condition = "no-supported-sasl-mechanisms" }); + stream:close(); + return; + end + table.sort(supported, function (a, b) return preference[a] > preference[b]; end); + local mechanism, initial_data = supported[1]; + stream:debug("Selecting %s mechanism...", mechanism); + stream.sasl_mechanism = coroutine.wrap(mechanisms[mechanism]); + initial_data = stream:sasl_mechanism(mechanism); + local auth_stanza = verse.stanza("auth", { xmlns = xmlns_sasl, mechanism = mechanism }); + if initial_data then + auth_stanza:text(base64(initial_data)); + end + stream:send(auth_stanza); + return true; + end + + local function handle_sasl(sasl_stanza) + if sasl_stanza.name == "failure" then + local err = sasl_stanza.tags[1]; + local text = sasl_stanza:get_child_text("text"); + stream:event("authentication-failure", { condition = err.name, text = text }); + stream:close(); + return false; + end + local ok, err = stream.sasl_mechanism(sasl_stanza.name, unbase64(sasl_stanza:get_text())); + if not ok then + stream:event("authentication-failure", { condition = err }); + stream:close(); + return false; + elseif ok == true then + stream:event("authentication-success"); + stream.authenticated = true + stream:reopen(); + else + stream:send(verse.stanza("response", { xmlns = xmlns_sasl }):text(base64(ok))); + end + return true; + end + + stream:hook("stream-features", handle_features, 300); + stream:hook("stream/"..xmlns_sasl, handle_sasl); + + return true; +end + diff --git a/verse/plugins/session.lua b/verse/plugins/session.lua new file mode 100644 index 0000000..0520610 --- /dev/null +++ b/verse/plugins/session.lua @@ -0,0 +1,30 @@ +local verse = require "verse"; + +local xmlns_session = "urn:ietf:params:xml:ns:xmpp-session"; + +function verse.plugins.session(stream) + + local function handle_features(features) + local session_feature = features:get_child("session", xmlns_session); + if session_feature and not session_feature:get_child("optional") then + local function handle_binding(jid) + stream:debug("Establishing Session..."); + stream:send_iq(verse.iq({ type = "set" }):tag("session", {xmlns=xmlns_session}), + function (reply) + if reply.attr.type == "result" then + stream:event("session-success"); + elseif reply.attr.type == "error" then + local err = reply:child_with_name("error"); + local type, condition, text = reply:get_error(); + stream:event("session-failure", { error = condition, text = text, type = type }); + end + end); + return true; + end + stream:hook("bind-success", handle_binding); + end + end + stream:hook("stream-features", handle_features); + + return true; +end diff --git a/verse/plugins/smacks.lua b/verse/plugins/smacks.lua new file mode 100644 index 0000000..81eaef7 --- /dev/null +++ b/verse/plugins/smacks.lua @@ -0,0 +1,144 @@ +local verse = require "verse"; +local now = socket.gettime; + +local xmlns_sm = "urn:xmpp:sm:2"; + +function verse.plugins.smacks(stream) + -- State for outgoing stanzas + local outgoing_queue = {}; + local last_ack = 0; + local last_stanza_time = now(); + local timer_active; + + -- State for incoming stanzas + local handled_stanza_count = 0; + + -- Catch incoming stanzas + local function incoming_stanza(stanza) + if stanza.attr.xmlns == "jabber:client" or not stanza.attr.xmlns then + handled_stanza_count = handled_stanza_count + 1; + stream:debug("Increasing handled stanzas to %d for %s", handled_stanza_count, stanza:top_tag()); + end + end + + -- Catch outgoing stanzas + function outgoing_stanza(stanza) + -- NOTE: This will not behave nice if stanzas are serialized before this point + if stanza.name and not stanza.attr.xmlns then + -- serialize stanzas in order to bypass this on resumption + outgoing_queue[#outgoing_queue+1] = tostring(stanza); + last_stanza_time = now(); + if not timer_active then + timer_active = true; + stream:debug("Waiting to send ack request..."); + verse.add_task(1, function() + if #outgoing_queue == 0 then + timer_active = false; + return; + end + local time_since_last_stanza = now() - last_stanza_time; + if time_since_last_stanza < 1 and #outgoing_queue < 10 then + return 1 - time_since_last_stanza; + end + stream:debug("Time up, sending ..."); + timer_active = false; + stream:send(verse.stanza("r", { xmlns = xmlns_sm })); + end); + end + end + end + + local function on_disconnect() + stream:debug("smacks: connection lost"); + stream.stream_management_supported = nil; + if stream.resumption_token then + stream:debug("smacks: have resumption token, reconnecting in 1s..."); + stream.authenticated = nil; + verse.add_task(1, function () + stream:connect(stream.connect_host or stream.host, stream.connect_port or 5222); + end); + return true; + end + end + + -- Graceful shutdown + local function on_close() + stream.resumption_token = nil; + stream:unhook("disconnected", on_disconnect); + end + + local function handle_sm_command(stanza) + if stanza.name == "r" then -- Request for acks for stanzas we received + stream:debug("Ack requested... acking %d handled stanzas", handled_stanza_count); + stream:send(verse.stanza("a", { xmlns = xmlns_sm, h = tostring(handled_stanza_count) })); + elseif stanza.name == "a" then -- Ack for stanzas we sent + local new_ack = tonumber(stanza.attr.h); + if new_ack > last_ack then + local old_unacked = #outgoing_queue; + for i=last_ack+1,new_ack do + table.remove(outgoing_queue, 1); + end + stream:debug("Received ack: New ack: "..new_ack.." Last ack: "..last_ack.." Unacked stanzas now: "..#outgoing_queue.." (was "..old_unacked..")"); + last_ack = new_ack; + else + stream:warn("Received bad ack for "..new_ack.." when last ack was "..last_ack); + end + elseif stanza.name == "enabled" then + + if stanza.attr.id then + stream.resumption_token = stanza.attr.id; + stream:hook("closed", on_close, 100); + stream:hook("disconnected", on_disconnect, 100); + end + elseif stanza.name == "resumed" then + local new_ack = tonumber(stanza.attr.h); + if new_ack > last_ack then + local old_unacked = #outgoing_queue; + for i=last_ack+1,new_ack do + table.remove(outgoing_queue, 1); + end + stream:debug("Received ack: New ack: "..new_ack.." Last ack: "..last_ack.." Unacked stanzas now: "..#outgoing_queue.." (was "..old_unacked..")"); + last_ack = new_ack; + end + for i=1,#outgoing_queue do + stream:send(outgoing_queue[i]); + end + outgoing_queue = {}; + stream:debug("Resumed successfully"); + stream:event("resumed"); + else + stream:warn("Don't know how to handle "..xmlns_sm.."/"..stanza.name); + end + end + + local function on_bind_success() + if not stream.smacks then + --stream:unhook("bind-success", on_bind_success); + stream:debug("smacks: sending enable"); + stream:send(verse.stanza("enable", { xmlns = xmlns_sm, resume = "true" })); + stream.smacks = true; + + -- Catch stanzas + stream:hook("stanza", incoming_stanza); + stream:hook("outgoing", outgoing_stanza); + end + end + + local function on_features(features) + if features:get_child("sm", xmlns_sm) then + stream.stream_management_supported = true; + if stream.smacks and stream.bound then -- Already enabled in a previous session - resume + stream:debug("Resuming stream with %d handled stanzas", handled_stanza_count); + stream:send(verse.stanza("resume", { xmlns = xmlns_sm, + h = handled_stanza_count, previd = stream.resumption_token })); + return true; + else + stream:hook("bind-success", on_bind_success, 1); + end + end + end + + stream:hook("stream-features", on_features, 250); + stream:hook("stream/"..xmlns_sm, handle_sm_command); + --stream:hook("ready", on_stream_ready, 500); +end diff --git a/verse/plugins/tls.lua b/verse/plugins/tls.lua new file mode 100644 index 0000000..92abd45 --- /dev/null +++ b/verse/plugins/tls.lua @@ -0,0 +1,36 @@ +local verse = require "verse"; + +local xmlns_tls = "urn:ietf:params:xml:ns:xmpp-tls"; + +function verse.plugins.tls(stream) + local function handle_features(features_stanza) + if stream.authenticated then return; end + if features_stanza:get_child("starttls", xmlns_tls) and stream.conn.starttls then + stream:debug("Negotiating TLS..."); + stream:send(verse.stanza("starttls", { xmlns = xmlns_tls })); + return true; + elseif not stream.conn.starttls and not stream.secure then + stream:warn("SSL libary (LuaSec) not loaded, so TLS not available"); + elseif not stream.secure then + stream:debug("Server doesn't offer TLS :("); + end + end + local function handle_tls(tls_status) + if tls_status.name == "proceed" then + stream:debug("Server says proceed, handshake starting..."); + stream.conn:starttls({mode="client", protocol="sslv23", options="no_sslv2"}, true); + end + end + local function handle_status(new_status) + if new_status == "ssl-handshake-complete" then + stream.secure = true; + stream:debug("Re-opening stream..."); + stream:reopen(); + end + end + stream:hook("stream-features", handle_features, 400); + stream:hook("stream/"..xmlns_tls, handle_tls); + stream:hook("status", handle_status, 400); + + return true; +end diff --git a/verse/plugins/uptime.lua b/verse/plugins/uptime.lua new file mode 100644 index 0000000..326af27 --- /dev/null +++ b/verse/plugins/uptime.lua @@ -0,0 +1,42 @@ +local verse = require "verse"; + +local xmlns_last = "jabber:iq:last"; + +local function set_uptime(self, uptime_info) + self.starttime = uptime_info.starttime; +end + +function verse.plugins.uptime(stream) + stream.uptime = { set = set_uptime }; + stream:hook("iq/"..xmlns_last, function (stanza) + if stanza.attr.type ~= "get" then return; end + local reply = verse.reply(stanza) + :tag("query", { seconds = tostring(os.difftime(os.time(), stream.uptime.starttime)), xmlns = xmlns_last }); + stream:send(reply); + return true; + end); + + function stream:query_uptime(target_jid, callback) + callback = callback or function (uptime) return stream:event("uptime/response", uptime); end + stream:send_iq(verse.iq({ type = "get", to = target_jid }) + :tag("query", { xmlns = xmlns_last }), + function (reply) + local query = reply:get_child("query", xmlns_last); + if reply.attr.type == "result" then + local seconds = tonumber(query.attr.seconds); + callback({ + seconds = seconds or nil; + }); + else + local type, condition, text = reply:get_error(); + callback({ + error = true; + condition = condition; + text = text; + type = type; + }); + end + end); + end + return true; +end diff --git a/verse/plugins/vcard.lua b/verse/plugins/vcard.lua new file mode 100644 index 0000000..50f644c --- /dev/null +++ b/verse/plugins/vcard.lua @@ -0,0 +1,36 @@ +local verse = require "verse"; +local vcard = require "util.vcard"; + +local xmlns_vcard = "vcard-temp"; + +function verse.plugins.vcard(stream) + function stream:get_vcard(jid, callback) --jid = nil for self + stream:send_iq(verse.iq({to = jid, type="get"}) + :tag("vCard", {xmlns=xmlns_vcard}), callback and function(stanza) + local lCard, xCard; + vCard = stanza:get_child("vCard", xmlns_vcard); + if stanza.attr.type == "result" and vCard then + vCard = vcard.from_xep54(vCard) + callback(vCard) + else + callback(false) -- FIXME add error + end + end or nil); + end + + function stream:set_vcard(aCard, callback) + local xCard; + if type(aCard) == "table" and aCard.name then + xCard = aCard; + elseif type(aCard) == "string" then + xCard = vcard.to_xep54(vcard.from_text(aCard)[1]); + elseif type(aCard) == "table" then + xCard = vcard.to_xep54(aCard); + error("Converting a table to vCard not implemented") + end + if not xCard then return false end + stream:debug("setting vcard to %s", tostring(xCard)); + stream:send_iq(verse.iq({type="set"}) + :add_child(xCard), callback); + end +end diff --git a/verse/plugins/vcard_update.lua b/verse/plugins/vcard_update.lua new file mode 100644 index 0000000..87f6cc5 --- /dev/null +++ b/verse/plugins/vcard_update.lua @@ -0,0 +1,103 @@ +local verse = require "verse"; + +local xmlns_vcard, xmlns_vcard_update = "vcard-temp", "vcard-temp:x:update"; + +-- MMMmmmm.. hacky +local ok, fun = pcall(function() return require("util.hashes").sha1; end); +if not ok then + ok, fun = pcall(function() return require("util.sha1").sha1; end); + if not ok then + error("Could not find a sha1()") + end +end +local sha1 = fun; + +local ok, fun = pcall(function() + local unb64 = require("util.encodings").base64.decode; + assert(unb64("SGVsbG8=") == "Hello") + return unb64; +end); +if not ok then + ok, fun = pcall(function() return require("mime").unb64; end); + if not ok then + error("Could not find a base64 decoder") + end +end +local unb64 = fun; + +function verse.plugins.vcard_update(stream) + stream:add_plugin("vcard"); + stream:add_plugin("presence"); + + + local x_vcard_update; + + function update_vcard_photo(vCard) + local data; + for i=1,#vCard do + if vCard[i].name == "PHOTO" then + data = vCard[i][1]; + break + end + end + if data then + local hash = sha1(unb64(data), true); + x_vcard_update = verse.stanza("x", { xmlns = xmlns_vcard_update }) + :tag("photo"):text(hash); + + stream:resend_presence() + else + x_vcard_update = nil; + end + end + + local _set_vcard = stream.set_vcard; + + --[[ TODO Complete this, it's probably broken. + -- Maybe better to hook outgoing stanza? + function stream:set_vcard(vCard, callback) + _set_vcard(vCard, function(event, ...) + if event.attr.type == "result" then + local vCard_ = response:get_child("vCard", xmlns_vcard); + if vCard_ then + update_vcard_photo(vCard_); + end -- Or fetch it again? Seems wasteful, but if the server overrides stuff? :/ + end + if callback then + return callback(event, ...); + end + end); + end + --]] + + local initial_vcard_fetch_started; + stream:hook("ready", function(event) + if initial_vcard_fetch_started then return; end + initial_vcard_fetch_started = true; + -- if stream:jid_supports(nil, xmlns_vcard) then TODO this, correctly + stream:get_vcard(nil, function(response) + if response then + update_vcard_photo(response) + end + stream:event("ready"); + end); + return true; + end, 3); + + stream:hook("presence-out", function(presence) + if x_vcard_update and not presence:get_child("x", xmlns_vcard_update) then + presence:add_child(x_vcard_update); + end + end, 10); + + --[[ + stream:hook("presence", function(presence) + local x_vcard_update = presence:get_child("x", xmlns_vcard_update); + local photo_hash = x_vcard_update and x_vcard_update:get_child("photo"); + :get_child_text("photo"); + if x_vcard_update then + -- TODO Cache peoples avatars here + end + end); + --]] +end diff --git a/verse/plugins/version.lua b/verse/plugins/version.lua new file mode 100644 index 0000000..822fd59 --- /dev/null +++ b/verse/plugins/version.lua @@ -0,0 +1,57 @@ +local verse = require "verse"; + +local xmlns_version = "jabber:iq:version"; + +local function set_version(self, version_info) + self.name = version_info.name; + self.version = version_info.version; + self.platform = version_info.platform; +end + +function verse.plugins.version(stream) + stream.version = { set = set_version }; + stream:hook("iq/"..xmlns_version, function (stanza) + if stanza.attr.type ~= "get" then return; end + local reply = verse.reply(stanza) + :tag("query", { xmlns = xmlns_version }); + if stream.version.name then + reply:tag("name"):text(tostring(stream.version.name)):up(); + end + if stream.version.version then + reply:tag("version"):text(tostring(stream.version.version)):up() + end + if stream.version.platform then + reply:tag("os"):text(stream.version.platform); + end + stream:send(reply); + return true; + end); + + function stream:query_version(target_jid, callback) + callback = callback or function (version) return stream:event("version/response", version); end + stream:send_iq(verse.iq({ type = "get", to = target_jid }) + :tag("query", { xmlns = xmlns_version }), + function (reply) + if reply.attr.type == "result" then + local query = reply:get_child("query", xmlns_version); + local name = query and query:get_child_text("name"); + local version = query and query:get_child_text("version"); + local os = query and query:get_child_text("os"); + callback({ + name = name; + version = version; + platform = os; + }); + else + local type, condition, text = reply:get_error(); + callback({ + error = true; + condition = condition; + text = text; + type = type; + }); + end + end); + end + return true; +end diff --git a/verse/squishy b/verse/squishy new file mode 100644 index 0000000..5f849bb --- /dev/null +++ b/verse/squishy @@ -0,0 +1,84 @@ +Output "verse.lua" + +-- Verse-specific versions of libraries +Module "util.encodings" "libs/encodings.lua" +Module "util.hashes" "libs/hashes.lua" +Module "util.sha1" "util/sha1.lua" +Module "lib.adhoc" "libs/adhoc.lib.lua" + +AutoFetchURL("http://hg.prosody.im/prosody-modules/raw-file/tip/mod_mam/?"); +Module "util.rsm" "rsm.lib.lua" +-- Prosody libraries +if not GetOption("prosody") then + AutoFetchURL "http://hg.prosody.im/0.9/raw-file/tip/?" +else + AutoFetchURL(GetOption("prosody").."/?") +end + +Module "util.stanza" "util/stanza.lua" +Module "util.timer" "util/timer.lua" +Module "util.termcolours" "util/termcolours.lua" +Module "util.uuid" "util/uuid.lua" +Module "net.dns" "net/dns.lua" +Module "net.adns" "net/adns.lua" +Module "net.server" "net/server_select.lua" +Module "util.xmppstream" "util/xmppstream.lua" +Module "util.jid" "util/jid.lua" +Module "util.events" "util/events.lua" +Module "util.dataforms" "util/dataforms.lua" +Module "util.caps" "util/caps.lua" +Module "util.vcard" "util/vcard.lua" +Module "util.logger" "util/logger.lua" +Module "util.datetime" "util/datetime.lua" + +Module "util.sasl.scram" "util/sasl/scram.lua" +Module "util.sasl.plain" "util/sasl/plain.lua" +Module "util.sasl.anonymous" "util/sasl/anonymous.lua" + +-- Verse plugins +plugins = { + -- Login + "tls", "sasl", "bind", "session", "legacy", "compression"; + -- Reliability + "smacks", "keepalive"; + -- Queries + "disco", "version", "ping", "uptime"; + -- Privacy control + "blocking"; + -- Jingle / file transfer + "jingle", "jingle_ft", "jingle_s5b", "proxy65", "jingle_ibb"; + -- Pubsub + "pubsub", "pep"; + -- Command and control + "adhoc"; + -- Basics + "presence", "private", "roster", "register"; + -- MUC + "groupchat"; + -- vCard + "vcard", "vcard_update"; + -- Carbons + "carbons"; + + "archive"; +} + +for _, plugin in ipairs(plugins) do + Module("verse.plugins."..plugin)("plugins/"..plugin..".lua") +end + +Module "util.http" "util/http.lua" +Module "net.http.parser" "net/http/parser.lua" +Module "net.http" "net/http.lua" + +Module "verse.bosh" "bosh.lua" + +if GetOption "internal-bit-module" then + Module "bit" "libs/bit.lua" +end + +Module "verse.client" "client.lua" +Module "verse.component" "component.lua" + +-- Main verse file +Main "init.lua" diff --git a/verse/util/dataforms.lua b/verse/util/dataforms.lua new file mode 100644 index 0000000..b2540f4 --- /dev/null +++ b/verse/util/dataforms.lua @@ -0,0 +1,282 @@ +-- 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. +-- + +local setmetatable = setmetatable; +local pairs, ipairs = pairs, ipairs; +local tostring, type, next = tostring, type, next; +local t_concat = table.concat; +local st = require "util.stanza"; +local jid_prep = require "util.jid".prep; + +module "dataforms" + +local xmlns_forms = 'jabber:x:data'; + +local form_t = {}; +local form_mt = { __index = form_t }; + +function new(layout) + return setmetatable(layout, form_mt); +end + +function from_stanza(stanza) + local layout = { + title = stanza:get_child_text("title"); + instructions = stanza:get_child_text("instructions"); + }; + for tag in stanza:childtags("field") do + local field = { + name = tag.attr.var; + label = tag.attr.label; + type = tag.attr.type; + required = tag:get_child("required") and true or nil; + value = tag:get_child_text("value"); + }; + layout[#layout+1] = field; + if field.type then + local value = {}; + if field.type:match"list%-" then + for tag in tag:childtags("option") do + value[#value+1] = { label = tag.attr.label, value = tag:get_child_text("value") }; + end + for tag in tag:childtags("value") do + value[#value+1] = { label = tag.attr.label, value = tag:get_text(), default = true }; + end + elseif field.type:match"%-multi" then + for tag in tag:childtags("value") do + value[#value+1] = tag.attr.label and { label = tag.attr.label, value = tag:get_text() } or tag:get_text(); + end + if field.type == "text-multi" then + field.value = t_concat(value, "\n"); + else + field.value = value; + end + end + end + end + return new(layout); +end + +function form_t.form(layout, data, formtype) + local form = st.stanza("x", { xmlns = xmlns_forms, type = formtype or "form" }); + if layout.title then + form:tag("title"):text(layout.title):up(); + end + if layout.instructions then + form:tag("instructions"):text(layout.instructions):up(); + end + for n, field in ipairs(layout) do + local field_type = field.type or "text-single"; + -- Add field tag + form:tag("field", { type = field_type, var = field.name, label = field.label }); + + local value = (data and data[field.name]) or field.value; + + if value then + -- Add value, depending on type + if field_type == "hidden" then + if type(value) == "table" then + -- Assume an XML snippet + form:tag("value") + :add_child(value) + :up(); + else + form:tag("value"):text(tostring(value)):up(); + end + elseif field_type == "boolean" then + form:tag("value"):text((value and "1") or "0"):up(); + elseif field_type == "fixed" then + + elseif field_type == "jid-multi" then + for _, jid in ipairs(value) do + form:tag("value"):text(jid):up(); + end + elseif field_type == "jid-single" then + form:tag("value"):text(value):up(); + elseif field_type == "text-single" or field_type == "text-private" then + form:tag("value"):text(value):up(); + elseif field_type == "text-multi" then + -- Split into multiple tags, one for each line + for line in value:gmatch("([^\r\n]+)\r?\n*") do + form:tag("value"):text(line):up(); + end + elseif field_type == "list-single" then + local has_default = false; + for _, val in ipairs(value) do + if type(val) == "table" then + form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up(); + if val.default and (not has_default) then + form:tag("value"):text(val.value):up(); + has_default = true; + end + else + form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up(); + end + end + elseif field_type == "list-multi" then + for _, val in ipairs(value) do + if type(val) == "table" then + form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up(); + if val.default then + form:tag("value"):text(val.value):up(); + end + else + form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up(); + end + end + end + end + + if field.required then + form:tag("required"):up(); + end + + -- Jump back up to list of fields + form:up(); + end + return form; +end + +local field_readers = {}; + +function form_t.data(layout, stanza) + local data = {}; + local errors = {}; + + for _, field in ipairs(layout) do + local tag; + for field_tag in stanza:childtags() do + if field.name == field_tag.attr.var then + tag = field_tag; + break; + end + end + + if not tag then + if field.required then + errors[field.name] = "Required value missing"; + end + else + local reader = field_readers[field.type]; + if reader then + data[field.name], errors[field.name] = reader(tag, field.required); + end + end + end + if next(errors) then + return data, errors; + end + return data; +end + +field_readers["text-single"] = + function (field_tag, required) + local data = field_tag:get_child_text("value"); + if data and #data > 0 then + return data + elseif required then + return nil, "Required value missing"; + end + end + +field_readers["text-private"] = + field_readers["text-single"]; + +field_readers["jid-single"] = + function (field_tag, required) + local raw_data = field_tag:get_child_text("value") + local data = jid_prep(raw_data); + if data and #data > 0 then + return data + elseif raw_data then + return nil, "Invalid JID: " .. raw_data; + elseif required then + return nil, "Required value missing"; + end + end + +field_readers["jid-multi"] = + function (field_tag, required) + local result = {}; + local err = {}; + for value_tag in field_tag:childtags("value") do + local raw_value = value_tag:get_text(); + local value = jid_prep(raw_value); + result[#result+1] = value; + if raw_value and not value then + err[#err+1] = ("Invalid JID: " .. raw_value); + end + end + if #result > 0 then + return result, (#err > 0 and t_concat(err, "\n") or nil); + elseif required then + return nil, "Required value missing"; + end + end + +field_readers["list-multi"] = + function (field_tag, required) + local result = {}; + for value in field_tag:childtags("value") do + result[#result+1] = value:get_text(); + end + return result, (required and #result == 0 and "Required value missing" or nil); + end + +field_readers["text-multi"] = + function (field_tag, required) + local data, err = field_readers["list-multi"](field_tag, required); + if data then + data = t_concat(data, "\n"); + end + return data, err; + end + +field_readers["list-single"] = + field_readers["text-single"]; + +local boolean_values = { + ["1"] = true, ["true"] = true, + ["0"] = false, ["false"] = false, +}; + +field_readers["boolean"] = + function (field_tag, required) + local raw_value = field_tag:get_child_text("value"); + local value = boolean_values[raw_value ~= nil and raw_value]; + if value ~= nil then + return value; + elseif raw_value then + return nil, "Invalid boolean representation"; + elseif required then + return nil, "Required value missing"; + end + end + +field_readers["hidden"] = + function (field_tag) + return field_tag:get_child_text("value"); + end + +return _M; + + +--[=[ + +Layout: +{ + + title = "MUC Configuration", + instructions = [[Use this form to configure options for this MUC room.]], + + { name = "FORM_TYPE", type = "hidden", required = true }; + { name = "field-name", type = "field-type", required = false }; +} + + +--]=] diff --git a/verse/util/sasl/anonymous.lua b/verse/util/sasl/anonymous.lua new file mode 100644 index 0000000..8eca054 --- /dev/null +++ b/verse/util/sasl/anonymous.lua @@ -0,0 +1,8 @@ + +return function (stream, name) + if name == "ANONYMOUS" then + return function () + return coroutine.yield() == "success"; + end, 0; + end +end diff --git a/verse/util/sasl/plain.lua b/verse/util/sasl/plain.lua new file mode 100644 index 0000000..c5aa8ab --- /dev/null +++ b/verse/util/sasl/plain.lua @@ -0,0 +1,9 @@ + +return function (stream, name) + if name == "PLAIN" and stream.username and stream.password then + return function (stream) + return "success" == coroutine.yield("\0"..stream.username.."\0"..stream.password); + end, 5; + end +end + diff --git a/verse/util/sasl/scram.lua b/verse/util/sasl/scram.lua new file mode 100644 index 0000000..ffbe10b --- /dev/null +++ b/verse/util/sasl/scram.lua @@ -0,0 +1,109 @@ + +local base64, unbase64 = require "mime".b64, require"mime".unb64; +local crypto = require"crypto"; +local bit = require"bit"; + +local tonumber = tonumber; +local char, byte = string.char, string.byte; +local gsub = string.gsub; +local xor = bit.bxor; + +local function XOR(a, b) + return (gsub(a, "()(.)", function(i, c) + return char(xor(byte(c), byte(b, i))) + end)); +end + +local function H(str) + return crypto.digest("sha1", str, true); +end + +local _hmac_digest = crypto.hmac.digest; +local function HMAC(key, str) + return _hmac_digest("sha1", str, key, true); +end + +local function Hi(str, salt, i) + local U = HMAC(str, salt .. "\0\0\0\1"); + local ret = U; + for _ = 2, i do + U = HMAC(str, U); + ret = XOR(ret, U); + end + return ret; +end + +local function Normalize(str) + return str; -- TODO +end + +local function value_safe(str) + return (gsub(str, "[,=]", { [","] = "=2C", ["="] = "=3D" })); +end + +local function scram(stream, name) + local username = "n=" .. value_safe(stream.username); + local c_nonce = base64(crypto.rand.bytes(15)); + local our_nonce = "r=" .. c_nonce; + local client_first_message_bare = username .. "," .. our_nonce; + local cbind_data = ""; + local gs2_cbind_flag = stream.conn:ssl() and "y" or "n"; + if name == "SCRAM-SHA-1-PLUS" then + cbind_data = stream.conn:socket():getfinished(); + gs2_cbind_flag = "p=tls-unique"; + end + local gs2_header = gs2_cbind_flag .. ",,"; + local client_first_message = gs2_header .. client_first_message_bare; + local cont, server_first_message = coroutine.yield(client_first_message); + if cont ~= "challenge" then return false end + + local nonce, salt, iteration_count = server_first_message:match("(r=[^,]+),s=([^,]*),i=(%d+)"); + local i = tonumber(iteration_count); + salt = unbase64(salt); + if not nonce or not salt or not i then + return false, "Could not parse server_first_message"; + elseif nonce:find(c_nonce, 3, true) ~= 3 then + return false, "nonce sent by server does not match our nonce"; + elseif nonce == our_nonce then + return false, "server did not append s-nonce to nonce"; + end + + local cbind_input = gs2_header .. cbind_data; + local channel_binding = "c=" .. base64(cbind_input); + local client_final_message_without_proof = channel_binding .. "," .. nonce; + + local SaltedPassword = Hi(Normalize(stream.password), salt, i); + local ClientKey = HMAC(SaltedPassword, "Client Key"); + local StoredKey = H(ClientKey); + local AuthMessage = client_first_message_bare .. "," .. server_first_message .. "," .. client_final_message_without_proof; + local ClientSignature = HMAC(StoredKey, AuthMessage); + local ClientProof = XOR(ClientKey, ClientSignature); + local ServerKey = HMAC(SaltedPassword, "Server Key"); + local ServerSignature = HMAC(ServerKey, AuthMessage); + + local proof = "p=" .. base64(ClientProof); + local client_final_message = client_final_message_without_proof .. "," .. proof; + + local ok, server_final_message = coroutine.yield(client_final_message); + if ok ~= "success" then return false, "success-expected" end + + local verifier = server_final_message:match("v=([^,]+)"); + if unbase64(verifier) ~= ServerSignature then + return false, "server signature did not match"; + end + return true; +end + +return function (stream, name) + if stream.username and (stream.password or (stream.client_key or stream.server_key)) then + if name == "SCRAM-SHA-1" then + return scram, 99; + elseif name == "SCRAM-SHA-1-PLUS" then + local sock = stream.conn:ssl() and stream.conn:socket(); + if sock and sock.getfinished then + return scram, 100; + end + end + end +end + diff --git a/verse/util/sha1.lua b/verse/util/sha1.lua new file mode 100644 index 0000000..1e854c4 --- /dev/null +++ b/verse/util/sha1.lua @@ -0,0 +1,146 @@ +------------------------------------------------- +--- *** SHA-1 algorithm for Lua *** --- +------------------------------------------------- +--- Author: Martin Huesser --- +--- Date: 2008-06-16 --- +--- License: You may use this code in your --- +--- projects as long as this header --- +--- stays intact. --- +------------------------------------------------- + +local strlen = string.len +local strchar = string.char +local strbyte = string.byte +local strsub = string.sub +local floor = math.floor +local bit = require "bit" +local bnot = bit.bnot +local band = bit.band +local bor = bit.bor +local bxor = bit.bxor +local shl = bit.lshift +local shr = bit.rshift +local h0, h1, h2, h3, h4 + +------------------------------------------------- + +local function LeftRotate(val, nr) + return shl(val, nr) + shr(val, 32 - nr) +end + +------------------------------------------------- + +local function ToHex(num) + local i, d + local str = "" + for i = 1, 8 do + d = band(num, 15) + if (d < 10) then + str = strchar(d + 48) .. str + else + str = strchar(d + 87) .. str + end + num = floor(num / 16) + end + return str +end + +------------------------------------------------- + +local function PreProcess(str) + local bitlen, i + local str2 = "" + bitlen = strlen(str) * 8 + str = str .. strchar(128) + i = 56 - band(strlen(str), 63) + if (i < 0) then + i = i + 64 + end + for i = 1, i do + str = str .. strchar(0) + end + for i = 1, 8 do + str2 = strchar(band(bitlen, 255)) .. str2 + bitlen = floor(bitlen / 256) + end + return str .. str2 +end + +------------------------------------------------- + +local function MainLoop(str) + local a, b, c, d, e, f, k, t + local i, j + local w = {} + while (str ~= "") do + for i = 0, 15 do + w[i] = 0 + for j = 1, 4 do + w[i] = w[i] * 256 + strbyte(str, i * 4 + j) + end + end + for i = 16, 79 do + w[i] = LeftRotate(bxor(bxor(w[i - 3], w[i - 8]), bxor(w[i - 14], w[i - 16])), 1) + end + a = h0 + b = h1 + c = h2 + d = h3 + e = h4 + for i = 0, 79 do + if (i < 20) then + f = bor(band(b, c), band(bnot(b), d)) + k = 1518500249 + elseif (i < 40) then + f = bxor(bxor(b, c), d) + k = 1859775393 + elseif (i < 60) then + f = bor(bor(band(b, c), band(b, d)), band(c, d)) + k = 2400959708 + else + f = bxor(bxor(b, c), d) + k = 3395469782 + end + t = LeftRotate(a, 5) + f + e + k + w[i] + e = d + d = c + c = LeftRotate(b, 30) + b = a + a = t + end + h0 = band(h0 + a, 4294967295) + h1 = band(h1 + b, 4294967295) + h2 = band(h2 + c, 4294967295) + h3 = band(h3 + d, 4294967295) + h4 = band(h4 + e, 4294967295) + str = strsub(str, 65) + end +end + +------------------------------------------------- + +local function sha1(str, hexres) + str = PreProcess(str) + h0 = 1732584193 + h1 = 4023233417 + h2 = 2562383102 + h3 = 0271733878 + h4 = 3285377520 + MainLoop(str) + local hex = ToHex(h0)..ToHex(h1)..ToHex(h2) + ..ToHex(h3)..ToHex(h4); + if hexres then + return hex; + else + return (hex:gsub("..", function (byte) + return string.char(tonumber(byte, 16)); + end)); + end +end + +_G.sha1 = {sha1 = sha1}; +return _G.sha1; + +------------------------------------------------- +------------------------------------------------- +------------------------------------------------- diff --git a/verse/util/vcard.lua b/verse/util/vcard.lua new file mode 100644 index 0000000..58b25dc --- /dev/null +++ b/verse/util/vcard.lua @@ -0,0 +1,464 @@ +-- Copyright (C) 2011-2012 Kim Alvefur +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +-- TODO +-- Fix folding. + +local st = require "util.stanza"; +local t_insert, t_concat = table.insert, table.concat; +local type = type; +local next, pairs, ipairs = next, pairs, ipairs; + +local from_text, to_text, from_xep54, to_xep54; + +local line_sep = "\n"; + +local vCard_dtd; -- See end of file + +local function fold_line() + error "Not implemented" --TODO +end +local function unfold_line() + error "Not implemented" + -- gsub("\r?\n[ \t]([^\r\n])", "%1"); +end + +local function vCard_esc(s) + return s:gsub("[,:;\\]", "\\%1"):gsub("\n","\\n"); +end + +local function vCard_unesc(s) + return s:gsub("\\?[\\nt:;,]", { + ["\\\\"] = "\\", + ["\\n"] = "\n", + ["\\r"] = "\r", + ["\\t"] = "\t", + ["\\:"] = ":", -- FIXME Shouldn't need to espace : in values, just params + ["\\;"] = ";", + ["\\,"] = ",", + [":"] = "\29", + [";"] = "\30", + [","] = "\31", + }); +end + +local function item_to_xep54(item) + local t = st.stanza(item.name, { xmlns = "vcard-temp" }); + + local prop_def = vCard_dtd[item.name]; + if prop_def == "text" then + t:text(item[1]); + elseif type(prop_def) == "table" then + if prop_def.types and item.TYPE then + if type(item.TYPE) == "table" then + for _,v in pairs(prop_def.types) do + for _,typ in pairs(item.TYPE) do + if typ:upper() == v then + t:tag(v):up(); + break; + end + end + end + else + t:tag(item.TYPE:upper()):up(); + end + end + + if prop_def.props then + for _,v in pairs(prop_def.props) do + if item[v] then + t:tag(v):up(); + end + end + end + + if prop_def.value then + t:tag(prop_def.value):text(item[1]):up(); + elseif prop_def.values then + local prop_def_values = prop_def.values; + local repeat_last = prop_def_values.behaviour == "repeat-last" and prop_def_values[#prop_def_values]; + for i=1,#item do + t:tag(prop_def.values[i] or repeat_last):text(item[i]):up(); + end + end + end + + return t; +end + +local function vcard_to_xep54(vCard) + local t = st.stanza("vCard", { xmlns = "vcard-temp" }); + for i=1,#vCard do + t:add_child(item_to_xep54(vCard[i])); + end + return t; +end + +function to_xep54(vCards) + if not vCards[1] or vCards[1].name then + return vcard_to_xep54(vCards) + else + local t = st.stanza("xCard", { xmlns = "vcard-temp" }); + for i=1,#vCards do + t:add_child(vcard_to_xep54(vCards[i])); + end + return t; + end +end + +function from_text(data) + data = data -- unfold and remove empty lines + :gsub("\r\n","\n") + :gsub("\n ", "") + :gsub("\n\n+","\n"); + local vCards = {}; + local c; -- current item + for line in data:gmatch("[^\n]+") do + local line = vCard_unesc(line); + local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$"); + value = value:gsub("\29",":"); + if #params > 0 then + local _params = {}; + for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do + k = k:upper(); + local _vt = {}; + for _p in v:gmatch("[^\31]+") do + _vt[#_vt+1]=_p + _vt[_p]=true; + end + if isval == "=" then + _params[k]=_vt; + else + _params[k]=true; + end + end + params = _params; + end + if name == "BEGIN" and value == "VCARD" then + c = {}; + vCards[#vCards+1] = c; + elseif name == "END" and value == "VCARD" then + c = nil; + elseif c and vCard_dtd[name] then + local dtd = vCard_dtd[name]; + local p = { name = name }; + c[#c+1]=p; + --c[name]=p; + local up = c; + c = p; + if dtd.types then + for _, t in ipairs(dtd.types) do + local t = t:lower(); + if ( params.TYPE and params.TYPE[t] == true) + or params[t] == true then + c.TYPE=t; + end + end + end + if dtd.props then + for _, p in ipairs(dtd.props) do + if params[p] then + if params[p] == true then + c[p]=true; + else + for _, prop in ipairs(params[p]) do + c[p]=prop; + end + end + end + end + end + if dtd == "text" or dtd.value then + t_insert(c, value); + elseif dtd.values then + local value = "\30"..value; + for p in value:gmatch("\30([^\30]*)") do + t_insert(c, p); + end + end + c = up; + end + end + return vCards; +end + +local function item_to_text(item) + local value = {}; + for i=1,#item do + value[i] = vCard_esc(item[i]); + end + value = t_concat(value, ";"); + + local params = ""; + for k,v in pairs(item) do + if type(k) == "string" and k ~= "name" then + params = params .. (";%s=%s"):format(k, type(v) == "table" and t_concat(v,",") or v); + end + end + + return ("%s%s:%s"):format(item.name, params, value) +end + +local function vcard_to_text(vcard) + local t={}; + t_insert(t, "BEGIN:VCARD") + for i=1,#vcard do + t_insert(t, item_to_text(vcard[i])); + end + t_insert(t, "END:VCARD") + return t_concat(t, line_sep); +end + +function to_text(vCards) + if vCards[1] and vCards[1].name then + return vcard_to_text(vCards) + else + local t = {}; + for i=1,#vCards do + t[i]=vcard_to_text(vCards[i]); + end + return t_concat(t, line_sep); + end +end + +local function from_xep54_item(item) + local prop_name = item.name; + local prop_def = vCard_dtd[prop_name]; + + local prop = { name = prop_name }; + + if prop_def == "text" then + prop[1] = item:get_text(); + elseif type(prop_def) == "table" then + if prop_def.value then --single item + prop[1] = item:get_child_text(prop_def.value) or ""; + elseif prop_def.values then --array + local value_names = prop_def.values; + if value_names.behaviour == "repeat-last" then + for i=1,#item.tags do + t_insert(prop, item.tags[i]:get_text() or ""); + end + else + for i=1,#value_names do + t_insert(prop, item:get_child_text(value_names[i]) or ""); + end + end + elseif prop_def.names then + local names = prop_def.names; + for i=1,#names do + if item:get_child(names[i]) then + prop[1] = names[i]; + break; + end + end + end + + if prop_def.props_verbatim then + for k,v in pairs(prop_def.props_verbatim) do + prop[k] = v; + end + end + + if prop_def.types then + local types = prop_def.types; + prop.TYPE = {}; + for i=1,#types do + if item:get_child(types[i]) then + t_insert(prop.TYPE, types[i]:lower()); + end + end + if #prop.TYPE == 0 then + prop.TYPE = nil; + end + end + + -- A key-value pair, within a key-value pair? + if prop_def.props then + local params = prop_def.props; + for i=1,#params do + local name = params[i] + local data = item:get_child_text(name); + if data then + prop[name] = prop[name] or {}; + t_insert(prop[name], data); + end + end + end + else + return nil + end + + return prop; +end + +local function from_xep54_vCard(vCard) + local tags = vCard.tags; + local t = {}; + for i=1,#tags do + t_insert(t, from_xep54_item(tags[i])); + end + return t +end + +function from_xep54(vCard) + if vCard.attr.xmlns ~= "vcard-temp" then + return nil, "wrong-xmlns"; + end + if vCard.name == "xCard" then -- A collection of vCards + local t = {}; + local vCards = vCard.tags; + for i=1,#vCards do + t[i] = from_xep54_vCard(vCards[i]); + end + return t + elseif vCard.name == "vCard" then -- A single vCard + return from_xep54_vCard(vCard) + end +end + +-- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd +vCard_dtd = { + VERSION = "text", --MUST be 3.0, so parsing is redundant + FN = "text", + N = { + values = { + "FAMILY", + "GIVEN", + "MIDDLE", + "PREFIX", + "SUFFIX", + }, + }, + NICKNAME = "text", + PHOTO = { + props_verbatim = { ENCODING = { "b" } }, + props = { "TYPE" }, + value = "BINVAL", --{ "EXTVAL", }, + }, + BDAY = "text", + ADR = { + types = { + "HOME", + "WORK", + "POSTAL", + "PARCEL", + "DOM", + "INTL", + "PREF", + }, + values = { + "POBOX", + "EXTADD", + "STREET", + "LOCALITY", + "REGION", + "PCODE", + "CTRY", + } + }, + LABEL = { + types = { + "HOME", + "WORK", + "POSTAL", + "PARCEL", + "DOM", + "INTL", + "PREF", + }, + value = "LINE", + }, + TEL = { + types = { + "HOME", + "WORK", + "VOICE", + "FAX", + "PAGER", + "MSG", + "CELL", + "VIDEO", + "BBS", + "MODEM", + "ISDN", + "PCS", + "PREF", + }, + value = "NUMBER", + }, + EMAIL = { + types = { + "HOME", + "WORK", + "INTERNET", + "PREF", + "X400", + }, + value = "USERID", + }, + JABBERID = "text", + MAILER = "text", + TZ = "text", + GEO = { + values = { + "LAT", + "LON", + }, + }, + TITLE = "text", + ROLE = "text", + LOGO = "copy of PHOTO", + AGENT = "text", + ORG = { + values = { + behaviour = "repeat-last", + "ORGNAME", + "ORGUNIT", + } + }, + CATEGORIES = { + values = "KEYWORD", + }, + NOTE = "text", + PRODID = "text", + REV = "text", + SORTSTRING = "text", + SOUND = "copy of PHOTO", + UID = "text", + URL = "text", + CLASS = { + names = { -- The item.name is the value if it's one of these. + "PUBLIC", + "PRIVATE", + "CONFIDENTIAL", + }, + }, + KEY = { + props = { "TYPE" }, + value = "CRED", + }, + DESC = "text", +}; +vCard_dtd.LOGO = vCard_dtd.PHOTO; +vCard_dtd.SOUND = vCard_dtd.PHOTO; + +return { + from_text = from_text; + to_text = to_text; + + from_xep54 = from_xep54; + to_xep54 = to_xep54; + + -- COMPAT: + lua_to_text = to_text; + lua_to_xep54 = to_xep54; + + text_to_lua = from_text; + text_to_xep54 = function (...) return to_xep54(from_text(...)); end; + + xep54_to_lua = from_xep54; + xep54_to_text = function (...) return to_text(from_xep54(...)) end; +}; diff --git a/verse/verse.lua b/verse/verse.lua new file mode 100644 index 0000000..78fe5d0 --- /dev/null +++ b/verse/verse.lua @@ -0,0 +1,7669 @@ +package.preload['util.encodings']=(function(...) +local function e() +error("Function not implemented"); +end +local t=require"mime"; +module"encodings" +stringprep={}; +base64={encode=t.b64,decode=e}; +return _M; +end) +package.preload['util.hashes']=(function(...) +local e=require"util.sha1"; +return{sha1=e.sha1}; +end) +package.preload['util.sha1']=(function(...) +local m=string.len +local o=string.char +local g=string.byte +local k=string.sub +local c=math.floor +local t=require"bit" +local q=t.bnot +local e=t.band +local y=t.bor +local n=t.bxor +local a=t.lshift +local i=t.rshift +local l,u,d,h,s +local function p(t,e) +return a(t,e)+i(t,32-e) +end +local function r(i) +local t,a +local t="" +for n=1,8 do +a=e(i,15) +if(a<10)then +t=o(a+48)..t +else +t=o(a+87)..t +end +i=c(i/16) +end +return t +end +local function j(t) +local i,a +local n="" +i=m(t)*8 +t=t..o(128) +a=56-e(m(t),63) +if(a<0)then +a=a+64 +end +for e=1,a do +t=t..o(0) +end +for t=1,8 do +n=o(e(i,255))..n +i=c(i/256) +end +return t..n +end +local function b(w) +local r,t,i,a,f,m,c,v +local o,o +local o={} +while(w~="")do +for e=0,15 do +o[e]=0 +for t=1,4 do +o[e]=o[e]*256+g(w,e*4+t) +end +end +for e=16,79 do +o[e]=p(n(n(o[e-3],o[e-8]),n(o[e-14],o[e-16])),1) +end +r=l +t=u +i=d +a=h +f=s +for s=0,79 do +if(s<20)then +m=y(e(t,i),e(q(t),a)) +c=1518500249 +elseif(s<40)then +m=n(n(t,i),a) +c=1859775393 +elseif(s<60)then +m=y(y(e(t,i),e(t,a)),e(i,a)) +c=2400959708 +else +m=n(n(t,i),a) +c=3395469782 +end +v=p(r,5)+m+f+c+o[s] +f=a +a=i +i=p(t,30) +t=r +r=v +end +l=e(l+r,4294967295) +u=e(u+t,4294967295) +d=e(d+i,4294967295) +h=e(h+a,4294967295) +s=e(s+f,4294967295) +w=k(w,65) +end +end +local function a(e,t) +e=j(e) +l=1732584193 +u=4023233417 +d=2562383102 +h=271733878 +s=3285377520 +b(e) +local e=r(l)..r(u)..r(d) +..r(h)..r(s); +if t then +return e; +else +return(e:gsub("..",function(e) +return string.char(tonumber(e,16)); +end)); +end +end +_G.sha1={sha1=a}; +return _G.sha1; +end) +package.preload['lib.adhoc']=(function(...) +local n,h=require"util.stanza",require"util.uuid"; +local e="http://jabber.org/protocol/commands"; +local i={} +local s={}; +function _cmdtag(o,i,t,a) +local e=n.stanza("command",{xmlns=e,node=o.node,status=i}); +if t then e.attr.sessionid=t;end +if a then e.attr.action=a;end +return e; +end +function s.new(t,e,o,a) +return{name=t,node=e,handler=o,cmdtag=_cmdtag,permission=(a or"user")}; +end +function s.handle_cmd(a,s,o) +local e=o.tags[1].attr.sessionid or h.generate(); +local t={}; +t.to=o.attr.to; +t.from=o.attr.from; +t.action=o.tags[1].attr.action or"execute"; +t.form=o.tags[1]:child_with_ns("jabber:x:data"); +local t,h=a:handler(t,i[e]); +i[e]=h; +local o=n.reply(o); +if t.status=="completed"then +i[e]=nil; +cmdtag=a:cmdtag("completed",e); +elseif t.status=="canceled"then +i[e]=nil; +cmdtag=a:cmdtag("canceled",e); +elseif t.status=="error"then +i[e]=nil; +o=n.error_reply(o,t.error.type,t.error.condition,t.error.message); +s.send(o); +return true; +else +cmdtag=a:cmdtag("executing",e); +end +for t,e in pairs(t)do +if t=="info"then +cmdtag:tag("note",{type="info"}):text(e):up(); +elseif t=="warn"then +cmdtag:tag("note",{type="warn"}):text(e):up(); +elseif t=="error"then +cmdtag:tag("note",{type="error"}):text(e.message):up(); +elseif t=="actions"then +local t=n.stanza("actions"); +for o,e in ipairs(e)do +if(e=="prev")or(e=="next")or(e=="complete")then +t:tag(e):up(); +else +module:log("error",'Command "'..a.name.. +'" at node "'..a.node..'" provided an invalid action "'..e..'"'); +end +end +cmdtag:add_child(t); +elseif t=="form"then +cmdtag:add_child((e.layout or e):form(e.values)); +elseif t=="result"then +cmdtag:add_child((e.layout or e):form(e.values,"result")); +elseif t=="other"then +cmdtag:add_child(e); +end +end +o:add_child(cmdtag); +s.send(o); +return true; +end +return s; +end) +package.preload['util.rsm']=(function(...) +local n=require"util.stanza".stanza; +local t,i=tostring,tonumber; +local h=type; +local s=pairs; +local o='http://jabber.org/protocol/rsm'; +local a={}; +do +local e=a; +local function t(e) +return i((e:get_text())); +end +local function a(t) +return t:get_text(); +end +e.after=a; +e.before=function(e) +local e=e:get_text(); +return e==""or e; +end; +e.max=t; +e.index=t; +e.first=function(e) +return{index=i(e.attr.index);e:get_text()}; +end; +e.last=a; +e.count=t; +end +local h=setmetatable({ +first=function(a,e) +if h(e)=="table"then +a:tag("first",{index=e.index}):text(e[1]):up(); +else +a:tag("first"):text(t(e)):up(); +end +end; +before=function(a,e) +if e==true then +a:tag("before"):up(); +else +a:tag("before"):text(t(e)):up(); +end +end +},{ +__index=function(e,a) +return function(e,o) +e:tag(a):text(t(o)):up(); +end +end; +}); +local function t(e) +local t={}; +for o in e:childtags()do +local e=o.name; +local a=e and a[e]; +if a then +t[e]=a(o); +end +end +return t; +end +local function i(e) +local t=n("set",{xmlns=o}); +for e,o in s(e)do +if a[e]then +h[e](t,o); +end +end +return t; +end +local function a(e) +local e=e:get_child("set",o); +if e and#e.tags>0 then +return t(e); +end +end +return{parse=t,generate=i,get=a}; +end) +package.preload['util.stanza']=(function(...) +local t=table.insert; +local d=table.remove; +local p=table.concat; +local h=string.format; +local l=string.match; +local c=tostring; +local m=setmetatable; +local n=pairs; +local o=ipairs; +local i=type; +local v=string.gsub; +local w=string.sub; +local f=string.find; +local e=os; +local u=not e.getenv("WINDIR"); +local r,a; +if u then +local t,e=pcall(require,"util.termcolours"); +if t then +r,a=e.getstyle,e.getstring; +else +u=nil; +end +end +local y="urn:ietf:params:xml:ns:xmpp-stanzas"; +module"stanza" +stanza_mt={__type="stanza"}; +stanza_mt.__index=stanza_mt; +local e=stanza_mt; +function stanza(a,t) +local t={name=a,attr=t or{},tags={}}; +return m(t,e); +end +local s=stanza; +function e:query(e) +return self:tag("query",{xmlns=e}); +end +function e:body(t,e) +return self:tag("body",e):text(t); +end +function e:tag(a,e) +local a=s(a,e); +local e=self.last_add; +if not e then e={};self.last_add=e;end +(e[#e]or self):add_direct_child(a); +t(e,a); +return self; +end +function e:text(t) +local e=self.last_add; +(e and e[#e]or self):add_direct_child(t); +return self; +end +function e:up() +local e=self.last_add; +if e then d(e);end +return self; +end +function e:reset() +self.last_add=nil; +return self; +end +function e:add_direct_child(e) +if i(e)=="table"then +t(self.tags,e); +end +t(self,e); +end +function e:add_child(t) +local e=self.last_add; +(e and e[#e]or self):add_direct_child(t); +return self; +end +function e:get_child(a,t) +for o,e in o(self.tags)do +if(not a or e.name==a) +and((not t and self.attr.xmlns==e.attr.xmlns) +or e.attr.xmlns==t)then +return e; +end +end +end +function e:get_child_text(e,t) +local e=self:get_child(e,t); +if e then +return e:get_text(); +end +return nil; +end +function e:child_with_name(t) +for a,e in o(self.tags)do +if e.name==t then return e;end +end +end +function e:child_with_ns(t) +for a,e in o(self.tags)do +if e.attr.xmlns==t then return e;end +end +end +function e:children() +local e=0; +return function(t) +e=e+1 +return t[e]; +end,self,e; +end +function e:childtags(i,o) +local e=self.tags; +local t,a=1,#e; +return function() +for a=t,a do +local e=e[a]; +if(not i or e.name==i) +and((not o and self.attr.xmlns==e.attr.xmlns) +or e.attr.xmlns==o)then +t=a+1; +return e; +end +end +end; +end +function e:maptags(i) +local o,e=self.tags,1; +local n,a=#self,#o; +local t=1; +while e<=a and a>0 do +if self[t]==o[e]then +local i=i(self[t]); +if i==nil then +d(self,t); +d(o,e); +n=n-1; +a=a-1; +t=t-1; +e=e-1; +else +self[t]=i; +o[e]=i; +end +e=e+1; +end +t=t+1; +end +return self; +end +function e:find(a) +local e=1; +local s=#a+1; +repeat +local o,t,i; +local n=w(a,e,e); +if n=="@"then +return self.attr[w(a,e+1)]; +elseif n=="{"then +o,e=l(a,"^([^}]+)}()",e+1); +end +t,i,e=l(a,"^([^@/#]*)([/#]?)()",e); +t=t~=""and t or nil; +if e==s then +if i=="#"then +return self:get_child_text(t,o); +end +return self:get_child(t,o); +end +self=self:get_child(t,o); +until not self +end +local d +do +local e={["'"]="'",["\""]=""",["<"]="<",[">"]=">",["&"]="&"}; +function d(t)return(v(t,"['&<>\"]",e));end +_M.xml_escape=d; +end +local function w(o,e,s,a,r) +local i=0; +local h=o.name +t(e,"<"..h); +for o,n in n(o.attr)do +if f(o,"\1",1,true)then +local s,o=l(o,"^([^\1]*)\1?(.*)$"); +i=i+1; +t(e," xmlns:ns"..i.."='"..a(s).."' ".."ns"..i..":"..o.."='"..a(n).."'"); +elseif not(o=="xmlns"and n==r)then +t(e," "..o.."='"..a(n).."'"); +end +end +local i=#o; +if i==0 then +t(e,"/>"); +else +t(e,">"); +for i=1,i do +local i=o[i]; +if i.name then +s(i,e,s,a,o.attr.xmlns); +else +t(e,a(i)); +end +end +t(e,""); +end +end +function e.__tostring(t) +local e={}; +w(t,e,w,d,nil); +return p(e); +end +function e.top_tag(t) +local e=""; +if t.attr then +for t,a in n(t.attr)do if i(t)=="string"then e=e..h(" %s='%s'",t,d(c(a)));end end +end +return h("<%s%s>",t.name,e); +end +function e.get_text(e) +if#e.tags==0 then +return p(e); +end +end +function e.get_error(t) +local i,e,a; +local t=t:get_child("error"); +if not t then +return nil,nil,nil; +end +i=t.attr.type; +for o,t in o(t.tags)do +if t.attr.xmlns==y then +if not a and t.name=="text"then +a=t:get_text(); +elseif not e then +e=t.name; +end +if e and a then +break; +end +end +end +return i,e or"undefined-condition",a; +end +do +local e=0; +function new_id() +e=e+1; +return"lx"..e; +end +end +function preserialize(e) +local a={name=e.name,attr=e.attr}; +for o,e in o(e)do +if i(e)=="table"then +t(a,preserialize(e)); +else +t(a,e); +end +end +return a; +end +function deserialize(a) +if a then +local s=a.attr; +for e=1,#s do s[e]=nil;end +local h={}; +for e in n(s)do +if f(e,"|",1,true)and not f(e,"\1",1,true)then +local a,t=l(e,"^([^|]+)|(.+)$"); +h[a.."\1"..t]=s[e]; +s[e]=nil; +end +end +for e,t in n(h)do +s[e]=t; +end +m(a,e); +for t,e in o(a)do +if i(e)=="table"then +deserialize(e); +end +end +if not a.tags then +local n={}; +for o,e in o(a)do +if i(e)=="table"then +t(n,e); +end +end +a.tags=n; +end +end +return a; +end +local function l(a) +local o,i={},{}; +for e,t in n(a.attr)do o[e]=t;end +local o={name=a.name,attr=o,tags=i}; +for e=1,#a do +local e=a[e]; +if e.name then +e=l(e); +t(i,e); +end +t(o,e); +end +return m(o,e); +end +clone=l; +function message(e,t) +if not t then +return s("message",e); +else +return s("message",e):tag("body"):text(t):up(); +end +end +function iq(e) +if e and not e.id then e.id=new_id();end +return s("iq",e or{id=new_id()}); +end +function reply(e) +return s(e.name,e.attr and{to=e.attr.from,from=e.attr.to,id=e.attr.id,type=((e.name=="iq"and"result")or e.attr.type)}); +end +do +local t={xmlns=y}; +function error_reply(e,i,o,a) +local e=reply(e); +e.attr.type="error"; +e:tag("error",{type=i}) +:tag(o,t):up(); +if(a)then e:tag("text",t):text(a):up();end +return e; +end +end +function presence(e) +return s("presence",e); +end +if u then +local u=r("yellow"); +local s=r("red"); +local l=r("red"); +local t=r("magenta"); +local s=" "..a(u,"%s")..a(t,"=")..a(s,"'%s'"); +local r=a(t,"<")..a(l,"%s").."%s"..a(t,">"); +local l=r.."%s"..a(t,""); +function e.pretty_print(e) +local t=""; +for a,e in o(e)do +if i(e)=="string"then +t=t..d(e); +else +t=t..e:pretty_print(); +end +end +local a=""; +if e.attr then +for e,t in n(e.attr)do if i(e)=="string"then a=a..h(s,e,c(t));end end +end +return h(l,e.name,a,t,e.name); +end +function e.pretty_top_tag(t) +local e=""; +if t.attr then +for t,a in n(t.attr)do if i(t)=="string"then e=e..h(s,t,c(a));end end +end +return h(r,t.name,e); +end +else +e.pretty_print=e.__tostring; +e.pretty_top_tag=e.top_tag; +end +return _M; +end) +package.preload['util.timer']=(function(...) +local a=require"net.server"; +local h=math.min +local u=math.huge +local i=require"socket".gettime; +local r=table.insert; +local d=pairs; +local l=type; +local s={}; +local t={}; +module"timer" +local e; +if not a.event then +function e(o,n) +local i=i(); +o=o+i; +if o>=i then +r(t,{o,n}); +else +local t=n(i); +if t and l(t)=="number"then +return e(t,n); +end +end +end +a._addtimer(function() +local o=i(); +if#t>0 then +for a,e in d(t)do +r(s,e); +end +t={}; +end +local t=u; +for r,a in d(s)do +local i,n=a[1],a[2]; +if i<=o then +s[r]=nil; +local a=n(o); +if l(a)=="number"then +e(a,n); +t=h(t,a); +end +else +t=h(t,i-o); +end +end +return t; +end); +else +local t=a.event; +local a=a.event_base; +local o=(t.core and t.core.LEAVE)or-1; +function e(n,e) +local t; +t=a:addevent(nil,0,function() +local e=e(i()); +if e then +return 0,e; +elseif t then +return o; +end +end +,n); +end +end +add_task=e; +return _M; +end) +package.preload['util.termcolours']=(function(...) +local i,n=table.concat,table.insert; +local t,a=string.char,string.format; +local r=tonumber; +local h=ipairs; +local s=io.write; +local e; +if os.getenv("WINDIR")then +e=require"util.windows"; +end +local o=e and e.get_consolecolor and e.get_consolecolor(); +module"termcolours" +local d={ +reset=0;bright=1,dim=2,underscore=4,blink=5,reverse=7,hidden=8; +black=30;red=31;green=32;yellow=33;blue=34;magenta=35;cyan=36;white=37; +["black background"]=40;["red background"]=41;["green background"]=42;["yellow background"]=43;["blue background"]=44;["magenta background"]=45;["cyan background"]=46;["white background"]=47; +bold=1,dark=2,underline=4,underlined=4,normal=0; +} +local u={ +["0"]=o, +["1"]=7+8, +["1;33"]=2+4+8, +["1;31"]=4+8 +} +local l={ +[1]="font-weight: bold",[2]="opacity: 0.5",[4]="text-decoration: underline",[8]="visibility: hidden", +[30]="color:black",[31]="color:red",[32]="color:green",[33]="color:#FFD700", +[34]="color:blue",[35]="color: magenta",[36]="color:cyan",[37]="color: white", +[40]="background-color:black",[41]="background-color:red",[42]="background-color:green", +[43]="background-color:yellow",[44]="background-color:blue",[45]="background-color: magenta", +[46]="background-color:cyan",[47]="background-color: white"; +}; +local c=t(27).."[%sm%s"..t(27).."[0m"; +function getstring(e,t) +if e then +return a(c,e,t); +else +return t; +end +end +function getstyle(...) +local e,t={...},{}; +for a,e in h(e)do +e=d[e]; +if e then +n(t,e); +end +end +return i(t,";"); +end +local a="0"; +function setstyle(e) +e=e or"0"; +if e~=a then +s("\27["..e.."m"); +a=e; +end +end +if e then +function setstyle(t) +t=t or"0"; +if t~=a then +e.set_consolecolor(u[t]or o); +a=t; +end +end +if not o then +function setstyle(e)end +end +end +local function a(t) +if t=="0"then return"";end +local e={}; +for t in t:gmatch("[^;]+")do +n(e,l[r(t)]); +end +return""; +end +function tohtml(e) +return e:gsub("\027%[(.-)m",a); +end +return _M; +end) +package.preload['util.uuid']=(function(...) +local e=math.random; +local n=tostring; +local e=os.time; +local i=os.clock; +local o=require"util.hashes".sha1; +module"uuid" +local t=0; +local function a() +local e=e(); +if t>=e then e=t+1;end +t=e; +return e; +end +local function t(e) +return o(e..i()..n({}),true); +end +local e=t(a()); +local function o(a) +e=t(e..a); +end +local function t(t) +if#e"; +end +return e; +end +local w={ +LOC=e.LOC_tostring; +MX=function(e) +return a.format('%2i %s',e.pref,e.mx); +end; +SRV=function(e) +local e=e.srv; +return a.format('%5d %5d %5d %s',e.priority,e.weight,e.port,e.target); +end; +}; +local x={}; +function x.__tostring(e) +local t=(w[e.type]or j)(e); +return a.format('%2s %-5s %6i %-28s %s',e.class,e.type,e.ttl,e.name,t); +end +local j={}; +function j.__tostring(t) +local e={}; +for a,t in m(t)do +n(e,b(t)..'\n'); +end +return i.concat(e); +end +local w={}; +function w.__tostring(e) +local a=s.gettime(); +local t={}; +for i,e in o(e)do +for i,e in o(e)do +for o,e in o(e)do +v(e,a); +n(t,b(e)); +end +end +end +return i.concat(t); +end +function e:new() +local t={active={},cache={},unsorted={}}; +r(t,e); +r(t.cache,w); +r(t.unsorted,{__mode='kv'}); +return t; +end +function t.random(...) +p.randomseed(p.floor(1e4*s.gettime())%2147483648); +t.random=p.random; +return t.random(...); +end +local function E(e) +e=e or{}; +e.id=e.id or t.random(0,65535); +e.rd=e.rd or 1; +e.tc=e.tc or 0; +e.aa=e.aa or 0; +e.opcode=e.opcode or 0; +e.qr=e.qr or 0; +e.rcode=e.rcode or 0; +e.z=e.z or 0; +e.ra=e.ra or 0; +e.qdcount=e.qdcount or 1; +e.ancount=e.ancount or 0; +e.nscount=e.nscount or 0; +e.arcount=e.arcount or 0; +local t=a.char( +c(e.id),e.id%256, +e.rd+2*e.tc+4*e.aa+8*e.opcode+128*e.qr, +e.rcode+16*e.z+128*e.ra, +c(e.qdcount),e.qdcount%256, +c(e.ancount),e.ancount%256, +c(e.nscount),e.nscount%256, +c(e.arcount),e.arcount%256 +); +return t,e.id; +end +local function c(t) +local e={}; +for t in a.gmatch(t,'[^.]+')do +n(e,a.char(a.len(t))); +n(e,t); +end +n(e,a.char(0)); +return i.concat(e); +end +local function p(o,a,e) +o=c(o); +a=t.typecode[a or'a']; +e=t.classcode[e or'in']; +return o..a..e; +end +function e:byte(e) +e=e or 1; +local t=self.offset; +local o=t+e-1; +if o>#self.packet then +q(a.format('out of bounds: %i>%i',o,#self.packet)); +end +self.offset=t+e; +return a.byte(self.packet,t,o); +end +function e:word() +local t,e=self:byte(2); +return 256*t+e; +end +function e:dword() +local t,e,o,a=self:byte(4); +return 16777216*t+65536*e+256*o+a; +end +function e:sub(e) +e=e or 1; +local t=a.sub(self.packet,self.offset,self.offset+e-1); +self.offset=self.offset+e; +return t; +end +function e:header(t) +local e=self:word(); +if not self.active[e]and not t then return nil;end +local e={id=e}; +local t,a=self:byte(2); +e.rd=t%2; +e.tc=t/2%2; +e.aa=t/4%2; +e.opcode=t/8%16; +e.qr=t/128; +e.rcode=a%16; +e.z=a/16%8; +e.ra=a/128; +e.qdcount=self:word(); +e.ancount=self:word(); +e.nscount=self:word(); +e.arcount=self:word(); +for a,t in o(e)do e[a]=t-t%1;end +return e; +end +function e:name() +local t,a=nil,0; +local e=self:byte(); +local o={}; +if e==0 then return"."end +while e>0 do +if e>=192 then +a=a+1; +if a>=20 then q('dns error: 20 pointers');end; +local e=((e-192)*256)+self:byte(); +t=t or self.offset; +self.offset=e+1; +else +n(o,self:sub(e)..'.'); +end +e=self:byte(); +end +self.offset=t or self.offset; +return i.concat(o); +end +function e:question() +local e={}; +e.name=self:name(); +e.type=t.type[self:word()]; +e.class=t.class[self:word()]; +return e; +end +function e:A(o) +local e,t,i,n=self:byte(4); +o.a=a.format('%i.%i.%i.%i',e,t,i,n); +end +function e:AAAA(a) +local e={}; +for t=1,a.rdlength,2 do +local a,t=self:byte(2); +i.insert(e,("%02x%02x"):format(a,t)); +end +e=i.concat(e,":"):gsub("%f[%x]0+(%x)","%1"); +local t={}; +for e in e:gmatch(":[0:]+:")do +i.insert(t,e) +end +if#t==0 then +a.aaaa=e; +return +elseif#t>1 then +i.sort(t,function(e,t)return#e>#t end); +end +a.aaaa=e:gsub(t[1],"::",1):gsub("^0::","::"):gsub("::0$","::"); +end +function e:CNAME(e) +e.cname=self:name(); +end +function e:MX(e) +e.pref=self:word(); +e.mx=self:name(); +end +function e:LOC_nibble_power() +local e=self:byte(); +return((e-(e%16))/16)*(10^(e%16)); +end +function e:LOC(e) +e.version=self:byte(); +if e.version==0 then +e.loc=e.loc or{}; +e.loc.size=self:LOC_nibble_power(); +e.loc.horiz_pre=self:LOC_nibble_power(); +e.loc.vert_pre=self:LOC_nibble_power(); +e.loc.latitude=self:dword(); +e.loc.longitude=self:dword(); +e.loc.altitude=self:dword(); +end +end +local function c(e,n,t) +e=e-2147483648; +if e<0 then n=t;e=-e;end +local i,t,o; +o=e%6e4; +e=(e-o)/6e4; +t=e%60; +i=(e-t)/60; +return a.format('%3d %2d %2.3f %s',i,t,o/1e3,n); +end +function e.LOC_tostring(e) +local t={}; +n(t,a.format( +'%s %s %.2fm %.2fm %.2fm %.2fm', +c(e.loc.latitude,'N','S'), +c(e.loc.longitude,'E','W'), +(e.loc.altitude-1e7)/100, +e.loc.size/100, +e.loc.horiz_pre/100, +e.loc.vert_pre/100 +)); +return i.concat(t); +end +function e:NS(e) +e.ns=self:name(); +end +function e:SOA(e) +end +function e:SRV(e) +e.srv={}; +e.srv.priority=self:word(); +e.srv.weight=self:word(); +e.srv.port=self:word(); +e.srv.target=self:name(); +end +function e:PTR(e) +e.ptr=self:name(); +end +function e:TXT(e) +e.txt=self:sub(self:byte()); +end +function e:rr() +local e={}; +r(e,x); +e.name=self:name(self); +e.type=t.type[self:word()]or e.type; +e.class=t.class[self:word()]or e.class; +e.ttl=65536*self:word()+self:word(); +e.rdlength=self:word(); +if e.ttl<=0 then +e.tod=self.time+30; +else +e.tod=self.time+e.ttl; +end +local a=self.offset; +local t=self[t.type[e.type]]; +if t then t(self,e);end +self.offset=a; +e.rdata=self:sub(e.rdlength); +return e; +end +function e:rrs(t) +local e={}; +for t=1,t do n(e,self:rr());end +return e; +end +function e:decode(t,o) +self.packet,self.offset=t,1; +local t=self:header(o); +if not t then return nil;end +local t={header=t}; +t.question={}; +local i=self.offset; +for e=1,t.header.qdcount do +n(t.question,self:question()); +end +t.question.raw=a.sub(self.packet,i,self.offset-1); +if not o then +if not self.active[t.header.id]or not self.active[t.header.id][t.question.raw]then +self.active[t.header.id]=nil; +return nil; +end +end +t.answer=self:rrs(t.header.ancount); +t.authority=self:rrs(t.header.nscount); +t.additional=self:rrs(t.header.arcount); +return t; +end +e.delays={1,3}; +function e:addnameserver(e) +self.server=self.server or{}; +n(self.server,e); +end +function e:setnameserver(e) +self.server={}; +self:addnameserver(e); +end +function e:adddefaultnameservers() +if z then +if y and y.get_nameservers then +for t,e in m(y.get_nameservers())do +self:addnameserver(e); +end +end +if not self.server or#self.server==0 then +self:addnameserver("208.67.222.222"); +self:addnameserver("208.67.220.220"); +end +else +local e=_.open("/etc/resolv.conf"); +if e then +for e in e:lines()do +e=e:gsub("#.*$","") +:match('^%s*nameserver%s+(.*)%s*$'); +if e then +e:gsub("%f[%d.](%d+%.%d+%.%d+%.%d+)%f[^%d.]",function(e) +self:addnameserver(e) +end); +end +end +end +if not self.server or#self.server==0 then +self:addnameserver("127.0.0.1"); +end +end +end +function e:getsocket(a) +self.socket=self.socket or{}; +self.socketset=self.socketset or{}; +local e=self.socket[a]; +if e then return e;end +local o,t; +e,t=s.udp(); +if e and self.socket_wrapper then e,t=self.socket_wrapper(e,self);end +if not e then +return nil,t; +end +e:settimeout(0); +self.socket[a]=e; +self.socketset[e]=a; +o,t=e:setsockname('*',0); +if not o then return self:servfail(e,t);end +o,t=e:setpeername(self.server[a],53); +if not o then return self:servfail(e,t);end +return e; +end +function e:voidsocket(e) +if self.socket[e]then +self.socketset[self.socket[e]]=nil; +self.socket[e]=nil; +elseif self.socketset[e]then +self.socket[self.socketset[e]]=nil; +self.socketset[e]=nil; +end +e:close(); +end +function e:socket_wrapper_set(e) +self.socket_wrapper=e; +end +function e:closeall() +for t,e in m(self.socket)do +self.socket[t]=nil; +self.socketset[e]=nil; +e:close(); +end +end +function e:remember(e,t) +local a,o,i=g(e.name,e.type,e.class); +if t~='*'then +t=o; +local t=d(self.cache,i,'*',a); +if t then n(t,e);end +end +self.cache=self.cache or r({},w); +local a=d(self.cache,i,t,a)or +u(self.cache,i,t,a,r({},j)); +if not a[e[o:lower()]]then +a[e[o:lower()]]=true; +n(a,e); +end +if t=='MX'then self.unsorted[a]=true;end +end +local function c(t,e) +return(t.pref==e.pref)and(t.mx#self.server then +e.server=1; +end +e.retries=(e.retries or 0)+1; +if e.retries>=#self.server then +a[o]=nil; +else +t,n=self:getsocket(e.server); +if t then t:send(e.packet);end +end +end +end +if h(a)==nil then +self.active[s]=nil; +end +end +if i==self.best_server then +self.best_server=self.best_server+1; +if self.best_server>#self.server then +self.best_server=1; +end +end +return t,n; +end +function e:settimeout(e) +self.timeout=e; +end +function e:receive(t) +self.time=s.gettime(); +t=t or self.socket; +local e; +for a,t in o(t)do +if self.socketset[t]then +local t=t:receive(); +if t then +e=self:decode(t); +if e and self.active[e.header.id] +and self.active[e.header.id][e.question.raw]then +for a,t in o(e.answer)do +self:remember(t,e.question[1].type) +end +local t=self.active[e.header.id]; +t[e.question.raw]=nil; +if not h(t)then self.active[e.header.id]=nil;end +if not h(self.active)then self:closeall();end +local e=e.question[1]; +local t=d(self.wanted,e.class,e.type,e.name); +if t then +for e in o(t)do +if l.status(e)=="suspended"then l.resume(e);end +end +u(self.wanted,e.class,e.type,e.name,nil); +end +end +end +end +end +return e; +end +function e:feed(a,e,t) +self.time=s.gettime(); +local e=self:decode(e,t); +if e and self.active[e.header.id] +and self.active[e.header.id][e.question.raw]then +for a,t in o(e.answer)do +self:remember(t,e.question[1].type); +end +local t=self.active[e.header.id]; +t[e.question.raw]=nil; +if not h(t)then self.active[e.header.id]=nil;end +if not h(self.active)then self:closeall();end +local e=e.question[1]; +if e then +local t=d(self.wanted,e.class,e.type,e.name); +if t then +for e in o(t)do +if l.status(e)=="suspended"then l.resume(e);end +end +u(self.wanted,e.class,e.type,e.name,nil); +end +end +end +return e; +end +function e:cancel(t,e,a) +local i=d(self.wanted,t,e,a); +if i then +for e in o(i)do +if l.status(e)=="suspended"then l.resume(e);end +end +u(self.wanted,t,e,a,nil); +end +end +function e:pulse() +while self:receive()do end +if not h(self.active)then return nil;end +self.time=s.gettime(); +for i,t in o(self.active)do +for a,e in o(t)do +if self.time>=e.retry then +e.server=e.server+1; +if e.server>#self.server then +e.server=1; +e.delay=e.delay+1; +end +if e.delay>#self.delays then +t[a]=nil; +if not h(t)then self.active[i]=nil;end +if not h(self.active)then return nil;end +else +local t=self.socket[e.server]; +if t then t:send(e.packet);end +e.retry=self.time+self.delays[e.delay]; +end +end +end +end +if h(self.active)then return true;end +return nil; +end +function e:lookup(e,t,a) +self:query(e,t,a) +while self:pulse()do +local e={} +for a,t in m(self.socket)do +e[a]=t +end +s.select(e,nil,4) +end +return self:peek(e,t,a); +end +function e:lookupex(o,a,t,e) +return self:peek(a,t,e)or self:query(a,t,e); +end +function e:tohostname(e) +return t.lookup(e:gsub("(%d+)%.(%d+)%.(%d+)%.(%d+)","%4.%3.%2.%1.in-addr.arpa."),"PTR"); +end +local i={ +qr={[0]='query','response'}, +opcode={[0]='query','inverse query','server status request'}, +aa={[0]='non-authoritative','authoritative'}, +tc={[0]='complete','truncated'}, +rd={[0]='recursion not desired','recursion desired'}, +ra={[0]='recursion not available','recursion available'}, +z={[0]='(reserved)'}, +rcode={[0]='no error','format error','server failure','name error','not implemented'}, +type=t.type, +class=t.class +}; +local function s(t,e) +return(i[e]and i[e][t[e]])or''; +end +function e.print(e) +for o,t in o{'id','qr','opcode','aa','tc','rd','ra','z', +'rcode','qdcount','ancount','nscount','arcount'}do +f(a.format('%-30s','header.'..t),e.header[t],s(e.header,t)); +end +for e,t in m(e.question)do +f(a.format('question[%i].name ',e),t.name); +f(a.format('question[%i].type ',e),t.type); +f(a.format('question[%i].class ',e),t.class); +end +local h={name=1,type=1,class=1,ttl=1,rdlength=1,rdata=1}; +local t; +for n,i in o({'answer','authority','additional'})do +for n,e in o(e[i])do +for h,o in o({'name','type','class','ttl','rdlength'})do +t=a.format('%s[%i].%s',i,n,o); +f(a.format('%-30s',t),e[o],s(e,o)); +end +for e,o in o(e)do +if not h[e]then +t=a.format('%s[%i].%s',i,n,e); +f(a.format('%-30s %s',b(t),b(o))); +end +end +end +end +end +function t.resolver() +local t={active={},cache={},unsorted={},wanted={},best_server=1}; +r(t,e); +r(t.cache,w); +r(t.unsorted,{__mode='kv'}); +return t; +end +local e=t.resolver(); +t._resolver=e; +function t.lookup(...) +return e:lookup(...); +end +function t.tohostname(...) +return e:tohostname(...); +end +function t.purge(...) +return e:purge(...); +end +function t.peek(...) +return e:peek(...); +end +function t.query(...) +return e:query(...); +end +function t.feed(...) +return e:feed(...); +end +function t.cancel(...) +return e:cancel(...); +end +function t.settimeout(...) +return e:settimeout(...); +end +function t.cache() +return e.cache; +end +function t.socket_wrapper_set(...) +return e:socket_wrapper_set(...); +end +return t; +end) +package.preload['net.adns']=(function(...) +local c=require"net.server"; +local a=require"net.dns"; +local e=require"util.logger".init("adns"); +local t,t=table.insert,table.remove; +local n,s,l=coroutine,tostring,pcall; +local function u(a,a,e,t)return(t-e)+1;end +module"adns" +function lookup(d,t,h,r) +return n.wrap(function(o) +if o then +e("debug","Records for %s already cached, using those...",t); +d(o); +return; +end +e("debug","Records for %s not in cache, sending query (%s)...",t,s(n.running())); +local i,o=a.query(t,h,r); +if i then +n.yield({r or"IN",h or"A",t,n.running()}); +e("debug","Reply for %s (%s)",t,s(n.running())); +end +if i then +i,o=l(d,a.peek(t,h,r)); +else +e("error","Error sending DNS query: %s",o); +i,o=l(d,nil,o); +end +if not i then +e("error","Error in DNS response handler: %s",s(o)); +end +end)(a.peek(t,h,r)); +end +function cancel(t,o,i) +e("warn","Cancelling DNS lookup for %s",s(t[3])); +a.cancel(t[1],t[2],t[3],t[4],o); +end +function new_async_socket(o,i) +local s=""; +local n={}; +local t={}; +local h; +function n.onincoming(o,e) +if e then +a.feed(t,e); +end +end +function n.ondisconnect(a,o) +if o then +e("warn","DNS socket for %s disconnected: %s",s,o); +local t=i.server; +if i.socketset[a]==i.best_server and i.best_server==#t then +e("error","Exhausted all %d configured DNS servers, next lookup will try %s again",#t,t[1]); +end +i:servfail(a); +end +end +t,h=c.wrapclient(o,"dns",53,n); +if not t then +return nil,h; +end +t.settimeout=function()end +t.setsockname=function(e,...)return o:setsockname(...);end +t.setpeername=function(e,...)s=(...);local o,a=o:setpeername(...);e:set_send(u);return o,a;end +t.connect=function(e,...)return o:connect(...)end +t.send=function(a,t) +e("debug","Sending DNS query to %s",s); +return o:send(t); +end +return t; +end +a.socket_wrapper_set(new_async_socket); +return _M; +end) +package.preload['net.server']=(function(...) +local d=function(e) +return _G[e] +end +local M,e=require("util.logger").init("socket"),table.concat; +local i=function(...)return M("debug",e{...});end +local U=function(...)return M("warn",e{...});end +local ne=1 +local H=d"type" +local D=d"pairs" +local ie=d"ipairs" +local p=d"tonumber" +local h=d"tostring" +local a=d"os" +local t=d"table" +local o=d"string" +local e=d"coroutine" +local V=a.difftime +local Y=math.min +local oe=math.huge +local ve=t.concat +local ye=o.sub +local pe=e.wrap +local we=e.yield +local T=d"ssl" +local v=d"socket"or require"socket" +local P=v.gettime +local fe=(T and T.wrap) +local le=v.bind +local me=v.sleep +local ce=v.select +local K +local G +local ue +local Q +local re +local c +local he +local se +local de +local ae +local ee +local X +local r +local Z +local F +local te +local y +local s +local R +local l +local n +local S +local b +local w +local m +local a +local o +local g +local L +local C +local O +local N +local j +local J +local u +local I +local E +local A +local _ +local z +local W +local q +local k +local x +y={} +s={} +l={} +R={} +n={} +b={} +w={} +S={} +a=0 +o=0 +g=0 +L=0 +C=0 +O=1 +N=0 +j=128 +I=51e3*1024 +E=25e3*1024 +A=12e5 +_=6e4 +z=6*60*60 +local e=package.config:sub(1,1)=="\\" +k=(e and math.huge)or v._SETSIZE or 1024 +q=v._SETSIZE or 1024 +x=30 +ae=function(w,t,f,d,v,u) +if t:getfd()>=k then +U("server.lua: Disallowed FD number: "..t:getfd()) +t:close() +return nil,"fd-too-large" +end +local m=0 +local p,e=w.onconnect,w.ondisconnect +local b=t.accept +local e={} +e.shutdown=function()end +e.ssl=function() +return u~=nil +end +e.sslctx=function() +return u +end +e.remove=function() +m=m-1 +if e then +e.resume() +end +end +e.close=function() +t:close() +o=r(l,t,o) +a=r(s,t,a) +y[f..":"..d]=nil; +n[t]=nil +e=nil +t=nil +i"server.lua: closed server handler and removed sockets from list" +end +e.pause=function(o) +if not e.paused then +a=r(s,t,a) +if o then +n[t]=nil +t:close() +t=nil; +end +e.paused=true; +end +end +e.resume=function() +if e.paused then +if not t then +t=le(f,d,j); +t:settimeout(0) +end +a=c(s,t,a) +n[t]=e +e.paused=false; +end +end +e.ip=function() +return f +end +e.serverport=function() +return d +end +e.socket=function() +return t +end +e.readbuffer=function() +if a>=q or o>=q then +e.pause() +i("server.lua: refused new client connection: server full") +return false +end +local t,o=b(t) +if t then +local a,o=t:getpeername() +local e,n,t=F(e,w,t,a,d,o,v,u) +if t then +return false +end +m=m+1 +i("server.lua: accepted new client connection from ",h(a),":",h(o)," to ",h(d)) +if p and not u then +return p(e); +end +return; +elseif o then +i("server.lua: error with new client connection: ",h(o)) +return false +end +end +return e +end +F=function(D,y,t,H,X,N,O,g) +if t:getfd()>=k then +U("server.lua: Disallowed FD number: "..t:getfd()) +t:close() +if D then +D.pause() +end +return nil,nil,"fd-too-large" +end +t:settimeout(0) +local p +local R +local k +local W +local F=y.onincoming +local U=y.onstatus +local q=y.ondisconnect +local M=y.ondrain +local Y=y.ondetach +local v={} +local d=0 +local G +local J +local P +local f=0 +local j=false +local A=false +local V,B=0,0 +local _=I +local z=E +local e=v +e.dispatch=function() +return F +end +e.disconnect=function() +return q +end +e.setlistener=function(a,t) +if Y then +Y(a) +end +F=t.onincoming +q=t.ondisconnect +U=t.onstatus +M=t.ondrain +Y=t.ondetach +end +e.getstats=function() +return B,V +end +e.ssl=function() +return W +end +e.sslctx=function() +return g +end +e.send=function(n,i,o,a) +return p(t,i,o,a) +end +e.receive=function(o,a) +return R(t,o,a) +end +e.shutdown=function(a) +return k(t,a) +end +e.setoption=function(i,a,o) +if t.setoption then +return t:setoption(a,o); +end +return false,"setoption not implemented"; +end +e.force_close=function(t,a) +if d~=0 then +i("server.lua: discarding unwritten data for ",h(H),":",h(N)) +d=0; +end +return t:close(a); +end +e.close=function(u,h) +if not e then return true;end +a=r(s,t,a) +b[e]=nil +if d~=0 then +e.sendbuffer() +if d~=0 then +if e then +e.write=nil +end +G=true +return false +end +end +if t then +m=k and k(t) +t:close() +o=r(l,t,o) +n[t]=nil +t=nil +else +i"server.lua: socket already closed" +end +if e then +w[e]=nil +S[e]=nil +local t=e; +e=nil +if q then +q(t,h or false); +q=nil +end +end +if D then +D.remove() +end +i"server.lua: closed client handler and removed socket from list" +return true +end +e.ip=function() +return H +end +e.serverport=function() +return X +end +e.clientport=function() +return N +end +e.port=e.clientport +local q=function(i,a) +f=f+#a +if f>_ then +S[e]="send buffer exceeded" +e.write=Q +return false +elseif t and not l[t]then +o=c(l,t,o) +end +d=d+1 +v[d]=a +if e then +w[e]=w[e]or u +end +return true +end +e.write=q +e.bufferqueue=function(t) +return v +end +e.socket=function(a) +return t +end +e.set_mode=function(a,t) +O=t or O +return O +end +e.set_send=function(a,t) +p=t or p +return p +end +e.bufferlen=function(o,a,t) +_=t or _ +z=a or z +return f,z,_ +end +e.lock_read=function(i,o) +if o==true then +local o=a +a=r(s,t,a) +b[e]=nil +if a~=o then +j=true +end +elseif o==false then +if j then +j=false +a=c(s,t,a) +b[e]=u +end +end +return j +end +e.pause=function(t) +return t:lock_read(true); +end +e.resume=function(t) +return t:lock_read(false); +end +e.lock=function(i,a) +e.lock_read(a) +if a==true then +e.write=Q +local a=o +o=r(l,t,o) +w[e]=nil +if o~=a then +A=true +end +elseif a==false then +e.write=q +if A then +A=false +q("") +end +end +return j,A +end +local b=function() +local o,t,a=R(t,O) +if not t or(t=="wantread"or t=="timeout")then +local o=o or a or"" +local a=#o +if a>z then +e:close("receive buffer exceeded") +return false +end +local a=a*ne +B=B+a +C=C+a +b[e]=u +return F(e,o,t) +else +i("server.lua: client ",h(H),":",h(N)," read error: ",h(t)) +J=true +m=e and e:force_close(t) +return false +end +end +local v=function() +local y,a,s,n,c; +if t then +n=ve(v,"",1,d) +y,a,s=p(t,n,1,f) +c=(y or s or 0)*ne +V=V+c +L=L+c +for e=d,1,-1 do +v[e]=nil +end +else +y,a,c=false,"unexpected close",0; +end +if y then +d=0 +f=0 +o=r(l,t,o) +w[e]=nil +if M then +M(e) +end +m=P and e:starttls(nil) +m=G and e:force_close() +return true +elseif s and(a=="timeout"or a=="wantwrite")then +n=ye(n,s+1,f) +v[1]=n +d=1 +f=f-s +w[e]=u +return true +else +i("server.lua: client ",h(H),":",h(N)," write error: ",h(a)) +J=true +m=e and e:force_close(a) +return false +end +end +local u; +function e.set_sslctx(w,t) +g=t; +local f,d +u=pe(function(n) +local t +for h=1,x do +o=(d and r(l,n,o))or o +a=(f and r(s,n,a))or a +f,d=nil,nil +m,t=n:dohandshake() +if not t then +i("server.lua: ssl handshake done") +e.readbuffer=b +e.sendbuffer=v +m=U and U(e,"ssl-handshake-complete") +if w.autostart_ssl and y.onconnect then +y.onconnect(w); +end +a=c(s,n,a) +return true +else +if t=="wantwrite"then +o=c(l,n,o) +d=true +elseif t=="wantread"then +a=c(s,n,a) +f=true +else +break; +end +t=nil; +we() +end +end +i("server.lua: ssl handshake error: ",h(t or"handshake too long")) +m=e and e:force_close("ssl handshake failed") +return false,t +end +) +end +if T then +e.starttls=function(f,m) +if m then +e:set_sslctx(m); +end +if d>0 then +i"server.lua: we need to do tls, but delaying until send buffer empty" +P=true +return +end +i("server.lua: attempting to start tls on "..h(t)) +local d,m=t +t,m=fe(t,g) +if not t then +i("server.lua: error while starting tls on client: ",h(m or"unknown error")) +return nil,m +end +t:settimeout(0) +p=t.send +R=t.receive +k=K +n[t]=e +a=c(s,t,a) +a=r(s,d,a) +o=r(l,d,o) +n[d]=nil +e.starttls=nil +P=nil +W=true +e.readbuffer=u +e.sendbuffer=u +return u(t) +end +end +e.readbuffer=b +e.sendbuffer=v +p=t.send +R=t.receive +k=(W and K)or t.shutdown +n[t]=e +a=c(s,t,a) +if g and T then +i"server.lua: auto-starting ssl negotiation..." +e.autostart_ssl=true; +local e,t=e:starttls(g); +if e==false then +return nil,nil,t +end +end +return e,t +end +K=function() +end +Q=function() +return false +end +c=function(t,a,e) +if not t[a]then +e=e+1 +t[e]=a +t[a]=e +end +return e; +end +r=function(e,i,t) +local a=e[i] +if a then +e[i]=nil +local o=e[t] +e[t]=nil +if o~=i then +e[o]=a +e[a]=o +end +return t-1 +end +return t +end +X=function(e) +o=r(l,e,o) +a=r(s,e,a) +n[e]=nil +e:close() +end +local function f(e,t,o) +local a; +local i=t.sendbuffer; +function t.sendbuffer() +i(); +if a and t.bufferlen()=o then +a=true; +e:lock_read(true); +end +end +e:set_mode("*a"); +end +he=function(t,e,d,l,r) +local o +if H(d)~="table"then +o="invalid listener table" +end +if H(e)~="number"or not(e>=0 and e<=65535)then +o="invalid port" +elseif y[t..":"..e]then +o="listeners on '["..t.."]:"..e.."' already exist" +elseif r and not T then +o="luasec not found" +end +if o then +U("server.lua, [",t,"]:",e,": ",o) +return nil,o +end +t=t or"*" +local o,h=le(t,e,j) +if h then +U("server.lua, [",t,"]:",e,": ",h) +return nil,h +end +local h,d=ae(d,o,t,e,l,r) +if not h then +o:close() +return nil,d +end +o:settimeout(0) +a=c(s,o,a) +y[t..":"..e]=h +n[o]=h +i("server.lua: new "..(r and"ssl "or"").."server listener on '[",t,"]:",e,"'") +return h +end +de=function(e,t) +return y[e..":"..t]; +end +Z=function(e,t) +local a=y[e..":"..t] +if not a then +return nil,"no server found on '["..e.."]:"..h(t).."'" +end +a:close() +y[e..":"..t]=nil +return true +end +re=function() +for e,t in D(n)do +t:close() +n[e]=nil +end +a=0 +o=0 +g=0 +y={} +s={} +l={} +R={} +n={} +end +ee=function() +return{ +select_timeout=O; +select_sleep_time=N; +tcp_backlog=j; +max_send_buffer_size=I; +max_receive_buffer_size=E; +select_idle_check_interval=A; +send_timeout=_; +read_timeout=z; +max_connections=q; +max_ssl_handshake_roundtrips=x; +highest_allowed_fd=k; +} +end +te=function(e) +if H(e)~="table"then +return nil,"invalid settings table" +end +O=p(e.select_timeout)or O +N=p(e.select_sleep_time)or N +I=p(e.max_send_buffer_size)or I +E=p(e.max_receive_buffer_size)or E +A=p(e.select_idle_check_interval)or A +j=p(e.tcp_backlog)or j +_=p(e.send_timeout)or _ +z=p(e.read_timeout)or z +q=e.max_connections or q +x=e.max_ssl_handshake_roundtrips or x +k=e.highest_allowed_fd or k +return true +end +se=function(e) +if H(e)~="function"then +return nil,"invalid listener function" +end +g=g+1 +R[g]=e +return true +end +ue=function() +return C,L,a,o,g +end +local t; +local function y(e) +t=not not e; +end +G=function(a) +if t then return"quitting";end +if a then t="once";end +local e=oe; +repeat +local o,a,s=ce(s,l,Y(O,e)) +for t,e in ie(a)do +local t=n[e] +if t then +t.sendbuffer() +else +X(e) +i"server.lua: found no handler and closed socket (writelist)" +end +end +for e,t in ie(o)do +local e=n[t] +if e then +e.readbuffer() +else +X(t) +i"server.lua: found no handler and closed socket (readlist)" +end +end +for e,t in D(S)do +e.disconnect()(e,t) +e:force_close() +S[e]=nil; +end +u=P() +local a=V(u-J) +if a>A then +J=u +for e,t in D(w)do +if V(u-t)>_ then +e.disconnect()(e,"send timeout") +e:force_close() +end +end +for e,t in D(b)do +if V(u-t)>z then +e.disconnect()(e,"read timeout") +e:close() +end +end +end +if u-W>=Y(e,1)then +e=oe; +for t=1,g do +local t=R[t](u) +if t then e=Y(e,t);end +end +W=u +else +e=e-(u-W); +end +me(N) +until t; +if a and t=="once"then t=nil;return;end +return"quitting" +end +local function h() +return G(true); +end +local function r() +return"select"; +end +local s=function(e,s,a,t,h,i) +local e,a,s=F(nil,t,e,s,a,"clientport",h,i) +if not e then return nil,s end +n[a]=e +if not i then +o=c(l,a,o) +if t.onconnect then +local a=e.sendbuffer; +e.sendbuffer=function() +e.sendbuffer=a; +t.onconnect(e); +return a(); +end +end +end +return e,a +end +local t=function(a,o,i,n,h) +local t,e=v.tcp() +if e then +return nil,e +end +t:settimeout(0) +m,e=t:connect(a,o) +if e then +local e=s(t,a,o,i) +else +F(nil,i,t,a,o,"clientport",n,h) +end +end +d"setmetatable"(n,{__mode="k"}) +d"setmetatable"(b,{__mode="k"}) +d"setmetatable"(w,{__mode="k"}) +W=P() +J=P() +local function a(e) +local t=M; +if e then +M=e; +end +return t; +end +return{ +_addtimer=se, +addclient=t, +wrapclient=s, +loop=G, +link=f, +step=h, +stats=ue, +closeall=re, +addserver=he, +getserver=de, +setlogger=a, +getsettings=ee, +setquitting=y, +removeserver=Z, +get_backend=r, +changesettings=te, +} +end) +package.preload['util.xmppstream']=(function(...) +local e=require"lxp"; +local t=require"util.stanza"; +local b=t.stanza_mt; +local f=error; +local t=tostring; +local d=table.insert; +local p=table.concat; +local _=table.remove; +local v=setmetatable; +local z=pcall(e.new,{StartDoctypeDecl=false}); +local T=pcall(e.new,{XmlDecl=false}); +local a=not not e.new({}).getcurrentbytecount; +local E=1024*1024*10; +module"xmppstream" +local k=e.new; +local x={ +["http://www.w3.org/XML/1998/namespace\1lang"]="xml:lang"; +["http://www.w3.org/XML/1998/namespace\1space"]="xml:space"; +["http://www.w3.org/XML/1998/namespace\1base"]="xml:base"; +["http://www.w3.org/XML/1998/namespace\1id"]="xml:id"; +}; +local s="http://etherx.jabber.org/streams"; +local r="\1"; +local g="^([^"..r.."]*)"..r.."?(.*)$"; +_M.ns_separator=r; +_M.ns_pattern=g; +local function o()end +function new_sax_handlers(n,e,h) +local i={}; +local y=e.streamopened; +local w=e.streamclosed; +local l=e.error or function(o,a,e)f("XML stream error: "..t(a)..(e and": "..t(e)or""),2);end; +local j=e.handlestanza; +h=h or o; +local t=e.stream_ns or s; +local c=e.stream_tag or"stream"; +if t~=""then +c=t..r..c; +end +local q=t..r..(e.error_tag or"error"); +local k=e.default_ns; +local u={}; +local s,e={}; +local t=0; +local r=0; +function i:StartElement(m,o) +if e and#s>0 then +d(e,p(s)); +s={}; +end +local s,i=m:match(g); +if i==""then +s,i="",s; +end +if s~=k or r>0 then +o.xmlns=s; +r=r+1; +end +for t=1,#o do +local e=o[t]; +o[t]=nil; +local t=x[e]; +if t then +o[t]=o[e]; +o[e]=nil; +end +end +if not e then +if a then +t=self:getcurrentbytecount(); +end +if n.notopen then +if m==c then +r=0; +if y then +if a then +h(t); +t=0; +end +y(n,o); +end +else +l(n,"no-stream",m); +end +return; +end +if s=="jabber:client"and i~="iq"and i~="presence"and i~="message"then +l(n,"invalid-top-level-element"); +end +e=v({name=i,attr=o,tags={}},b); +else +if a then +t=t+self:getcurrentbytecount(); +end +d(u,e); +local t=e; +e=v({name=i,attr=o,tags={}},b); +d(t,e); +d(t.tags,e); +end +end +if T then +function i:XmlDecl(e,e,e) +if a then +h(self:getcurrentbytecount()); +end +end +end +function i:StartCdataSection() +if a then +if e then +t=t+self:getcurrentbytecount(); +else +h(self:getcurrentbytecount()); +end +end +end +function i:EndCdataSection() +if a then +if e then +t=t+self:getcurrentbytecount(); +else +h(self:getcurrentbytecount()); +end +end +end +function i:CharacterData(o) +if e then +if a then +t=t+self:getcurrentbytecount(); +end +d(s,o); +elseif a then +h(self:getcurrentbytecount()); +end +end +function i:EndElement(o) +if a then +t=t+self:getcurrentbytecount() +end +if r>0 then +r=r-1; +end +if e then +if#s>0 then +d(e,p(s)); +s={}; +end +if#u==0 then +if a then +h(t); +end +t=0; +if o~=q then +j(n,e); +else +l(n,"stream-error",e); +end +e=nil; +else +e=_(u); +end +else +if w then +w(n); +end +end +end +local function a(e) +l(n,"parse-error","restricted-xml","Restricted XML, see RFC 6120 section 11.1."); +if not e.stop or not e:stop()then +f("Failed to abort parsing"); +end +end +if z then +i.StartDoctypeDecl=a; +end +i.Comment=a; +i.ProcessingInstruction=a; +local function a() +e,s,t=nil,{},0; +u={}; +end +local function e(t,e) +n=e; +end +return i,{reset=a,set_session=e}; +end +function new(i,n,t) +local e=0; +local o; +if a then +function o(a) +e=e-a; +end +t=t or E; +elseif t then +f("Stanza size limits are not supported on this version of LuaExpat") +end +local n,i=new_sax_handlers(i,n,o); +local o=k(n,r,false); +local s=o.parse; +return{ +reset=function() +o=k(n,r,false); +s=o.parse; +e=0; +i.reset(); +end, +feed=function(n,i) +if a then +e=e+#i; +end +local i,o=s(o,i); +if a and e>t then +return nil,"stanza-too-large"; +end +return i,o; +end, +set_session=i.set_session; +}; +end +return _M; +end) +package.preload['util.jid']=(function(...) +local a,i=string.match,string.sub; +local r=require"util.encodings".stringprep.nodeprep; +local d=require"util.encodings".stringprep.nameprep; +local l=require"util.encodings".stringprep.resourceprep; +local n={ +[" "]="\\20";['"']="\\22"; +["&"]="\\26";["'"]="\\27"; +["/"]="\\2f";[":"]="\\3a"; +["<"]="\\3c";[">"]="\\3e"; +["@"]="\\40";["\\"]="\\5c"; +}; +local s={}; +for e,t in pairs(n)do s[t]=e;end +module"jid" +local function o(e) +if not e then return;end +local o,t=a(e,"^([^@/]+)@()"); +local t,i=a(e,"^([^@/]+)()",t) +if o and not t then return nil,nil,nil;end +local a=a(e,"^/(.+)$",i); +if(not t)or((not a)and#e>=i)then return nil,nil,nil;end +return o,t,a; +end +split=o; +function bare(e) +local t,e=o(e); +if t and e then +return t.."@"..e; +end +return e; +end +local function h(e) +local a,e,t=o(e); +if e then +if i(e,-1,-1)=="."then +e=i(e,1,-2); +end +e=d(e); +if not e then return;end +if a then +a=r(a); +if not a then return;end +end +if t then +t=l(t); +if not t then return;end +end +return a,e,t; +end +end +prepped_split=h; +function prep(e) +local t,e,a=h(e); +if e then +if t then +e=t.."@"..e; +end +if a then +e=e.."/"..a; +end +end +return e; +end +function join(a,e,t) +if a and e and t then +return a.."@"..e.."/"..t; +elseif a and e then +return a.."@"..e; +elseif e and t then +return e.."/"..t; +elseif e then +return e; +end +return nil; +end +function compare(t,e) +local n,i,s=o(t); +local e,t,a=o(e); +if((e~=nil and e==n)or e==nil)and +((t~=nil and t==i)or t==nil)and +((a~=nil and a==s)or a==nil)then +return true +end +return false +end +function escape(e)return e and(e:gsub(".",n));end +function unescape(e)return e and(e:gsub("\\%x%x",s));end +return _M; +end) +package.preload['util.events']=(function(...) +local o=pairs; +local s=table.insert; +local n=table.sort; +local d=setmetatable; +local h=next; +module"events" +function new() +local t={}; +local e={}; +local function r(i,a) +local e=e[a]; +if not e or h(e)==nil then return;end +local t={}; +for e in o(e)do +s(t,e); +end +n(t,function(a,t)return e[a]>e[t];end); +i[a]=t; +return t; +end; +d(t,{__index=r}); +local function s(o,i,n) +local a=e[o]; +if a then +a[i]=n or 0; +else +a={[i]=n or 0}; +e[o]=a; +end +t[o]=nil; +end; +local function i(a,i) +local o=e[a]; +if o then +o[i]=nil; +t[a]=nil; +if h(o)==nil then +e[a]=nil; +end +end +end; +local function a(e) +for e,t in o(e)do +s(e,t); +end +end; +local function n(e) +for t,e in o(e)do +i(t,e); +end +end; +local function o(e,...) +local e=t[e]; +if e then +for t=1,#e do +local e=e[t](...); +if e~=nil then return e;end +end +end +end; +return{ +add_handler=s; +remove_handler=i; +add_handlers=a; +remove_handlers=n; +fire_event=o; +_handlers=t; +_event_map=e; +}; +end +return _M; +end) +package.preload['util.dataforms']=(function(...) +local e=setmetatable; +local t,i=pairs,ipairs; +local r,h,c=tostring,type,next; +local s=table.concat; +local l=require"util.stanza"; +local d=require"util.jid".prep; +module"dataforms" +local u='jabber:x:data'; +local n={}; +local t={__index=n}; +function new(a) +return e(a,t); +end +function from_stanza(e) +local o={ +title=e:get_child_text("title"); +instructions=e:get_child_text("instructions"); +}; +for e in e:childtags("field")do +local a={ +name=e.attr.var; +label=e.attr.label; +type=e.attr.type; +required=e:get_child("required")and true or nil; +value=e:get_child_text("value"); +}; +o[#o+1]=a; +if a.type then +local t={}; +if a.type:match"list%-"then +for e in e:childtags("option")do +t[#t+1]={label=e.attr.label,value=e:get_child_text("value")}; +end +for e in e:childtags("value")do +t[#t+1]={label=e.attr.label,value=e:get_text(),default=true}; +end +elseif a.type:match"%-multi"then +for e in e:childtags("value")do +t[#t+1]=e.attr.label and{label=e.attr.label,value=e:get_text()}or e:get_text(); +end +if a.type=="text-multi"then +a.value=s(t,"\n"); +else +a.value=t; +end +end +end +end +return new(o); +end +function n.form(t,n,e) +local e=l.stanza("x",{xmlns=u,type=e or"form"}); +if t.title then +e:tag("title"):text(t.title):up(); +end +if t.instructions then +e:tag("instructions"):text(t.instructions):up(); +end +for t,o in i(t)do +local a=o.type or"text-single"; +e:tag("field",{type=a,var=o.name,label=o.label}); +local t=(n and n[o.name])or o.value; +if t then +if a=="hidden"then +if h(t)=="table"then +e:tag("value") +:add_child(t) +:up(); +else +e:tag("value"):text(r(t)):up(); +end +elseif a=="boolean"then +e:tag("value"):text((t and"1")or"0"):up(); +elseif a=="fixed"then +elseif a=="jid-multi"then +for a,t in i(t)do +e:tag("value"):text(t):up(); +end +elseif a=="jid-single"then +e:tag("value"):text(t):up(); +elseif a=="text-single"or a=="text-private"then +e:tag("value"):text(t):up(); +elseif a=="text-multi"then +for t in t:gmatch("([^\r\n]+)\r?\n*")do +e:tag("value"):text(t):up(); +end +elseif a=="list-single"then +local a=false; +for o,t in i(t)do +if h(t)=="table"then +e:tag("option",{label=t.label}):tag("value"):text(t.value):up():up(); +if t.default and(not a)then +e:tag("value"):text(t.value):up(); +a=true; +end +else +e:tag("option",{label=t}):tag("value"):text(r(t)):up():up(); +end +end +elseif a=="list-multi"then +for a,t in i(t)do +if h(t)=="table"then +e:tag("option",{label=t.label}):tag("value"):text(t.value):up():up(); +if t.default then +e:tag("value"):text(t.value):up(); +end +else +e:tag("option",{label=t}):tag("value"):text(r(t)):up():up(); +end +end +end +end +if o.required then +e:tag("required"):up(); +end +e:up(); +end +return e; +end +local e={}; +function n.data(t,s) +local n={}; +local a={}; +for o,t in i(t)do +local o; +for e in s:childtags()do +if t.name==e.attr.var then +o=e; +break; +end +end +if not o then +if t.required then +a[t.name]="Required value missing"; +end +else +local e=e[t.type]; +if e then +n[t.name],a[t.name]=e(o,t.required); +end +end +end +if c(a)then +return n,a; +end +return n; +end +e["text-single"]= +function(t,a) +local t=t:get_child_text("value"); +if t and#t>0 then +return t +elseif a then +return nil,"Required value missing"; +end +end +e["text-private"]= +e["text-single"]; +e["jid-single"]= +function(t,o) +local t=t:get_child_text("value") +local a=d(t); +if a and#a>0 then +return a +elseif t then +return nil,"Invalid JID: "..t; +elseif o then +return nil,"Required value missing"; +end +end +e["jid-multi"]= +function(o,i) +local a={}; +local t={}; +for e in o:childtags("value")do +local e=e:get_text(); +local o=d(e); +a[#a+1]=o; +if e and not o then +t[#t+1]=("Invalid JID: "..e); +end +end +if#a>0 then +return a,(#t>0 and s(t,"\n")or nil); +elseif i then +return nil,"Required value missing"; +end +end +e["list-multi"]= +function(a,o) +local t={}; +for e in a:childtags("value")do +t[#t+1]=e:get_text(); +end +return t,(o and#t==0 and"Required value missing"or nil); +end +e["text-multi"]= +function(t,a) +local t,a=e["list-multi"](t,a); +if t then +t=s(t,"\n"); +end +return t,a; +end +e["list-single"]= +e["text-single"]; +local a={ +["1"]=true,["true"]=true, +["0"]=false,["false"]=false, +}; +e["boolean"]= +function(t,o) +local t=t:get_child_text("value"); +local a=a[t~=nil and t]; +if a~=nil then +return a; +elseif t then +return nil,"Invalid boolean representation"; +elseif o then +return nil,"Required value missing"; +end +end +e["hidden"]= +function(e) +return e:get_child_text("value"); +end +return _M; +end) +package.preload['util.caps']=(function(...) +local d=require"util.encodings".base64.encode; +local l=require"util.hashes".sha1; +local n,h,s=table.insert,table.sort,table.concat; +local r=ipairs; +module"caps" +function calculate_hash(e) +local i,o,a={},{},{}; +for t,e in r(e)do +if e.name=="identity"then +n(i,(e.attr.category or"").."\0"..(e.attr.type or"").."\0"..(e.attr["xml:lang"]or"").."\0"..(e.attr.name or"")); +elseif e.name=="feature"then +n(o,e.attr.var or""); +elseif e.name=="x"and e.attr.xmlns=="jabber:x:data"then +local t={}; +local o; +for a,e in r(e.tags)do +if e.name=="field"and e.attr.var then +local a={}; +for t,e in r(e.tags)do +e=#e.tags==0 and e:get_text(); +if e then n(a,e);end +end +h(a); +if e.attr.var=="FORM_TYPE"then +o=a[1]; +elseif#a>0 then +n(t,e.attr.var.."\0"..s(a,"<")); +else +n(t,e.attr.var); +end +end +end +h(t); +t=s(t,"<"); +if o then t=o.."\0"..t;end +n(a,t); +end +end +h(i); +h(o); +h(a); +if#i>0 then i=s(i,"<"):gsub("%z","/").."<";else i="";end +if#o>0 then o=s(o,"<").."<";else o="";end +if#a>0 then a=s(a,"<"):gsub("%z","<").."<";else a="";end +local e=i..o..a; +local t=d(l(e)); +return t,e; +end +return _M; +end) +package.preload['util.vcard']=(function(...) +local i=require"util.stanza"; +local a,r=table.insert,table.concat; +local s=type; +local e,h,m=next,pairs,ipairs; +local c,l,u,d; +local f="\n"; +local o; +local function e() +error"Not implemented" +end +local function e() +error"Not implemented" +end +local function w(e) +return e:gsub("[,:;\\]","\\%1"):gsub("\n","\\n"); +end +local function y(e) +return e:gsub("\\?[\\nt:;,]",{ +["\\\\"]="\\", +["\\n"]="\n", +["\\r"]="\r", +["\\t"]="\t", +["\\:"]=":", +["\\;"]=";", +["\\,"]=",", +[":"]="\29", +[";"]="\30", +[","]="\31", +}); +end +local function p(t) +local a=i.stanza(t.name,{xmlns="vcard-temp"}); +local e=o[t.name]; +if e=="text"then +a:text(t[1]); +elseif s(e)=="table"then +if e.types and t.TYPE then +if s(t.TYPE)=="table"then +for o,e in h(e.types)do +for o,t in h(t.TYPE)do +if t:upper()==e then +a:tag(e):up(); +break; +end +end +end +else +a:tag(t.TYPE:upper()):up(); +end +end +if e.props then +for o,e in h(e.props)do +if t[e]then +a:tag(e):up(); +end +end +end +if e.value then +a:tag(e.value):text(t[1]):up(); +elseif e.values then +local o=e.values; +local i=o.behaviour=="repeat-last"and o[#o]; +for o=1,#t do +a:tag(e.values[o]or i):text(t[o]):up(); +end +end +end +return a; +end +local function n(t) +local e=i.stanza("vCard",{xmlns="vcard-temp"}); +for a=1,#t do +e:add_child(p(t[a])); +end +return e; +end +function d(e) +if not e[1]or e[1].name then +return n(e) +else +local t=i.stanza("xCard",{xmlns="vcard-temp"}); +for a=1,#e do +t:add_child(n(e[a])); +end +return t; +end +end +function c(t) +t=t +:gsub("\r\n","\n") +:gsub("\n ","") +:gsub("\n\n+","\n"); +local s={}; +local e; +for t in t:gmatch("[^\n]+")do +local t=y(t); +local n,t,i=t:match("^([-%a]+)(\30?[^\29]*)\29(.*)$"); +i=i:gsub("\29",":"); +if#t>0 then +local a={}; +for e,o,i in t:gmatch("\30([^=]+)(=?)([^\30]*)")do +e=e:upper(); +local t={}; +for e in i:gmatch("[^\31]+")do +t[#t+1]=e +t[e]=true; +end +if o=="="then +a[e]=t; +else +a[e]=true; +end +end +t=a; +end +if n=="BEGIN"and i=="VCARD"then +e={}; +s[#s+1]=e; +elseif n=="END"and i=="VCARD"then +e=nil; +elseif e and o[n]then +local o=o[n]; +local n={name=n}; +e[#e+1]=n; +local s=e; +e=n; +if o.types then +for o,a in m(o.types)do +local a=a:lower(); +if(t.TYPE and t.TYPE[a]==true) +or t[a]==true then +e.TYPE=a; +end +end +end +if o.props then +for o,a in m(o.props)do +if t[a]then +if t[a]==true then +e[a]=true; +else +for o,t in m(t[a])do +e[a]=t; +end +end +end +end +end +if o=="text"or o.value then +a(e,i); +elseif o.values then +local t="\30"..i; +for t in t:gmatch("\30([^\30]*)")do +a(e,t); +end +end +e=s; +end +end +return s; +end +local function n(e) +local t={}; +for a=1,#e do +t[a]=w(e[a]); +end +t=r(t,";"); +local a=""; +for e,t in h(e)do +if s(e)=="string"and e~="name"then +a=a..(";%s=%s"):format(e,s(t)=="table"and r(t,",")or t); +end +end +return("%s%s:%s"):format(e.name,a,t) +end +local function i(t) +local e={}; +a(e,"BEGIN:VCARD") +for o=1,#t do +a(e,n(t[o])); +end +a(e,"END:VCARD") +return r(e,f); +end +function l(e) +if e[1]and e[1].name then +return i(e) +else +local t={}; +for a=1,#e do +t[a]=i(e[a]); +end +return r(t,f); +end +end +local function n(i) +local t=i.name; +local e=o[t]; +local t={name=t}; +if e=="text"then +t[1]=i:get_text(); +elseif s(e)=="table"then +if e.value then +t[1]=i:get_child_text(e.value)or""; +elseif e.values then +local e=e.values; +if e.behaviour=="repeat-last"then +for e=1,#i.tags do +a(t,i.tags[e]:get_text()or""); +end +else +for o=1,#e do +a(t,i:get_child_text(e[o])or""); +end +end +elseif e.names then +local e=e.names; +for a=1,#e do +if i:get_child(e[a])then +t[1]=e[a]; +break; +end +end +end +if e.props_verbatim then +for a,e in h(e.props_verbatim)do +t[a]=e; +end +end +if e.types then +local e=e.types; +t.TYPE={}; +for o=1,#e do +if i:get_child(e[o])then +a(t.TYPE,e[o]:lower()); +end +end +if#t.TYPE==0 then +t.TYPE=nil; +end +end +if e.props then +local e=e.props; +for o=1,#e do +local e=e[o] +local o=i:get_child_text(e); +if o then +t[e]=t[e]or{}; +a(t[e],o); +end +end +end +else +return nil +end +return t; +end +local function i(e) +local t=e.tags; +local e={}; +for o=1,#t do +a(e,n(t[o])); +end +return e +end +function u(e) +if e.attr.xmlns~="vcard-temp"then +return nil,"wrong-xmlns"; +end +if e.name=="xCard"then +local t={}; +local a=e.tags; +for e=1,#a do +t[e]=i(a[e]); +end +return t +elseif e.name=="vCard"then +return i(e) +end +end +o={ +VERSION="text", +FN="text", +N={ +values={ +"FAMILY", +"GIVEN", +"MIDDLE", +"PREFIX", +"SUFFIX", +}, +}, +NICKNAME="text", +PHOTO={ +props_verbatim={ENCODING={"b"}}, +props={"TYPE"}, +value="BINVAL", +}, +BDAY="text", +ADR={ +types={ +"HOME", +"WORK", +"POSTAL", +"PARCEL", +"DOM", +"INTL", +"PREF", +}, +values={ +"POBOX", +"EXTADD", +"STREET", +"LOCALITY", +"REGION", +"PCODE", +"CTRY", +} +}, +LABEL={ +types={ +"HOME", +"WORK", +"POSTAL", +"PARCEL", +"DOM", +"INTL", +"PREF", +}, +value="LINE", +}, +TEL={ +types={ +"HOME", +"WORK", +"VOICE", +"FAX", +"PAGER", +"MSG", +"CELL", +"VIDEO", +"BBS", +"MODEM", +"ISDN", +"PCS", +"PREF", +}, +value="NUMBER", +}, +EMAIL={ +types={ +"HOME", +"WORK", +"INTERNET", +"PREF", +"X400", +}, +value="USERID", +}, +JABBERID="text", +MAILER="text", +TZ="text", +GEO={ +values={ +"LAT", +"LON", +}, +}, +TITLE="text", +ROLE="text", +LOGO="copy of PHOTO", +AGENT="text", +ORG={ +values={ +behaviour="repeat-last", +"ORGNAME", +"ORGUNIT", +} +}, +CATEGORIES={ +values="KEYWORD", +}, +NOTE="text", +PRODID="text", +REV="text", +SORTSTRING="text", +SOUND="copy of PHOTO", +UID="text", +URL="text", +CLASS={ +names={ +"PUBLIC", +"PRIVATE", +"CONFIDENTIAL", +}, +}, +KEY={ +props={"TYPE"}, +value="CRED", +}, +DESC="text", +}; +o.LOGO=o.PHOTO; +o.SOUND=o.PHOTO; +return{ +from_text=c; +to_text=l; +from_xep54=u; +to_xep54=d; +lua_to_text=l; +lua_to_xep54=d; +text_to_lua=c; +text_to_xep54=function(...)return d(c(...));end; +xep54_to_lua=u; +xep54_to_text=function(...)return l(u(...))end; +}; +end) +package.preload['util.logger']=(function(...) +local e=pcall; +local e=string.find; +local e,s,e=ipairs,pairs,setmetatable; +module"logger" +local e={}; +local t; +function init(e) +local n=t(e,"debug"); +local i=t(e,"info"); +local o=t(e,"warn"); +local a=t(e,"error"); +return function(t,e,...) +if t=="debug"then +return n(e,...); +elseif t=="info"then +return i(e,...); +elseif t=="warn"then +return o(e,...); +elseif t=="error"then +return a(e,...); +end +end +end +function t(o,a) +local t=e[a]; +if not t then +t={}; +e[a]=t; +end +local e=function(e,...) +for i=1,#t do +t[i](o,a,e,...); +end +end +return e; +end +function reset() +for t,e in s(e)do +for t=1,#e do +e[t]=nil; +end +end +end +function add_level_sink(t,a) +if not e[t]then +e[t]={a}; +else +e[t][#e[t]+1]=a; +end +end +_M.new=t; +return _M; +end) +package.preload['util.datetime']=(function(...) +local e=os.date; +local n=os.time; +local u=os.difftime; +local t=error; +local r=tonumber; +module"datetime" +function date(t) +return e("!%Y-%m-%d",t); +end +function datetime(t) +return e("!%Y-%m-%dT%H:%M:%SZ",t); +end +function time(t) +return e("!%H:%M:%S",t); +end +function legacy(t) +return e("!%Y%m%dT%H:%M:%S",t); +end +function parse(o) +if o then +local i,s,h,d,l,t,a; +i,s,h,d,l,t,a=o:match("^(%d%d%d%d)%-?(%d%d)%-?(%d%d)T(%d%d):(%d%d):(%d%d)%.?%d*([Z+%-]?.*)$"); +if i then +local u=u(n(e("*t")),n(e("!*t"))); +local o=0; +if a~=""and a~="Z"then +local a,t,e=a:match("([+%-])(%d%d):?(%d*)"); +if not a then return;end +if#e~=2 then e="0";end +t,e=r(t),r(e); +o=t*60*60+e*60; +if a=="-"then o=-o;end +end +t=(t+u)-o; +return n({year=i,month=s,day=h,hour=d,min=l,sec=t,isdst=false}); +end +end +end +return _M; +end) +package.preload['util.sasl.scram']=(function(...) +local i,c=require"mime".b64,require"mime".unb64; +local a=require"crypto"; +local t=require"bit"; +local n=tonumber; +local s,e=string.char,string.byte; +local o=string.gsub; +local h=t.bxor; +local function m(a,t) +return(o(a,"()(.)",function(o,a) +return s(h(e(a),e(t,o))) +end)); +end +local function y(e) +return a.digest("sha1",e,true); +end +local s=a.hmac.digest; +local function e(e,t) +return s("sha1",t,e,true); +end +local function w(o,t,i) +local t=e(o,t.."\0\0\0\1"); +local a=t; +for i=2,i do +t=e(o,t); +a=m(a,t); +end +return a; +end +local function f(e) +return e; +end +local function t(e) +return(o(e,"[,=]",{[","]="=2C",["="]="=3D"})); +end +local function r(o,h) +local t="n="..t(o.username); +local s=i(a.rand.bytes(15)); +local d="r="..s; +local r=t..","..d; +local u=""; +local t=o.conn:ssl()and"y"or"n"; +if h=="SCRAM-SHA-1-PLUS"then +u=o.conn:socket():getfinished(); +t="p=tls-unique"; +end +local l=t..",,"; +local t=l..r; +local t,h=coroutine.yield(t); +if t~="challenge"then return false end +local a,t,p=h:match("(r=[^,]+),s=([^,]*),i=(%d+)"); +local n=n(p); +t=c(t); +if not a or not t or not n then +return false,"Could not parse server_first_message"; +elseif a:find(s,3,true)~=3 then +return false,"nonce sent by server does not match our nonce"; +elseif a==d then +return false,"server did not append s-nonce to nonce"; +end +local s=l..u; +local s="c="..i(s); +local s=s..","..a; +local a=w(f(o.password),t,n); +local t=e(a,"Client Key"); +local n=y(t); +local o=r..","..h..","..s; +local n=e(n,o); +local t=m(t,n); +local a=e(a,"Server Key"); +local e=e(a,o); +local t="p="..i(t); +local t=s..","..t; +local t,a=coroutine.yield(t); +if t~="success"then return false,"success-expected"end +local t=a:match("v=([^,]+)"); +if c(t)~=e then +return false,"server signature did not match"; +end +return true; +end +return function(e,t) +if e.username and(e.password or(e.client_key or e.server_key))then +if t=="SCRAM-SHA-1"then +return r,99; +elseif t=="SCRAM-SHA-1-PLUS"then +local e=e.conn:ssl()and e.conn:socket(); +if e and e.getfinished then +return r,100; +end +end +end +end +end) +package.preload['util.sasl.plain']=(function(...) +return function(e,t) +if t=="PLAIN"and e.username and e.password then +return function(e) +return"success"==coroutine.yield("\0"..e.username.."\0"..e.password); +end,5; +end +end +end) +package.preload['util.sasl.anonymous']=(function(...) +return function(t,e) +if e=="ANONYMOUS"then +return function() +return coroutine.yield()=="success"; +end,0; +end +end +end) +package.preload['verse.plugins.tls']=(function(...) +local a=require"verse"; +local t="urn:ietf:params:xml:ns:xmpp-tls"; +function a.plugins.tls(e) +local function i(o) +if e.authenticated then return;end +if o:get_child("starttls",t)and e.conn.starttls then +e:debug("Negotiating TLS..."); +e:send(a.stanza("starttls",{xmlns=t})); +return true; +elseif not e.conn.starttls and not e.secure then +e:warn("SSL libary (LuaSec) not loaded, so TLS not available"); +elseif not e.secure then +e:debug("Server doesn't offer TLS :("); +end +end +local function o(t) +if t.name=="proceed"then +e:debug("Server says proceed, handshake starting..."); +e.conn:starttls({mode="client",protocol="sslv23",options="no_sslv2"},true); +end +end +local function a(t) +if t=="ssl-handshake-complete"then +e.secure=true; +e:debug("Re-opening stream..."); +e:reopen(); +end +end +e:hook("stream-features",i,400); +e:hook("stream/"..t,o); +e:hook("status",a,400); +return true; +end +end) +package.preload['verse.plugins.sasl']=(function(...) +local n,s=require"mime".b64,require"mime".unb64; +local o="urn:ietf:params:xml:ns:xmpp-sasl"; +function verse.plugins.sasl(e) +local function h(t) +if e.authenticated then return;end +e:debug("Authenticating with SASL..."); +local t=t:get_child("mechanisms",o); +if not t then return end +local a={}; +local i={}; +for t in t:childtags("mechanism")do +t=t:get_text(); +e:debug("Server offers %s",t); +if not a[t]then +local n=t:match("[^-]+"); +local s,o=pcall(require,"util.sasl."..n:lower()); +if s then +e:debug("Loaded SASL %s module",n); +a[t],i[t]=o(e,t); +elseif not tostring(o):match("not found")then +e:debug("Loading failed: %s",tostring(o)); +end +end +end +local t={}; +for e in pairs(a)do +table.insert(t,e); +end +if not t[1]then +e:event("authentication-failure",{condition="no-supported-sasl-mechanisms"}); +e:close(); +return; +end +table.sort(t,function(e,t)return i[e]>i[t];end); +local t,i=t[1]; +e:debug("Selecting %s mechanism...",t); +e.sasl_mechanism=coroutine.wrap(a[t]); +i=e:sasl_mechanism(t); +local t=verse.stanza("auth",{xmlns=o,mechanism=t}); +if i then +t:text(n(i)); +end +e:send(t); +return true; +end +local function i(t) +if t.name=="failure"then +local a=t.tags[1]; +local t=t:get_child_text("text"); +e:event("authentication-failure",{condition=a.name,text=t}); +e:close(); +return false; +end +local t,a=e.sasl_mechanism(t.name,s(t:get_text())); +if not t then +e:event("authentication-failure",{condition=a}); +e:close(); +return false; +elseif t==true then +e:event("authentication-success"); +e.authenticated=true +e:reopen(); +else +e:send(verse.stanza("response",{xmlns=o}):text(n(t))); +end +return true; +end +e:hook("stream-features",h,300); +e:hook("stream/"..o,i); +return true; +end +end) +package.preload['verse.plugins.bind']=(function(...) +local t=require"verse"; +local o=require"util.jid"; +local a="urn:ietf:params:xml:ns:xmpp-bind"; +function t.plugins.bind(e) +local function i(i) +if e.bound then return;end +e:debug("Binding resource..."); +e:send_iq(t.iq({type="set"}):tag("bind",{xmlns=a}):tag("resource"):text(e.resource), +function(t) +if t.attr.type=="result"then +local t=t +:get_child("bind",a) +:get_child_text("jid"); +e.username,e.host,e.resource=o.split(t); +e.jid,e.bound=t,true; +e:event("bind-success",{jid=t}); +elseif t.attr.type=="error"then +local a=t:child_with_name("error"); +local o,t,a=t:get_error(); +e:event("bind-failure",{error=t,text=a,type=o}); +end +end); +end +e:hook("stream-features",i,200); +return true; +end +end) +package.preload['verse.plugins.session']=(function(...) +local a=require"verse"; +local o="urn:ietf:params:xml:ns:xmpp-session"; +function a.plugins.session(e) +local function n(t) +local t=t:get_child("session",o); +if t and not t:get_child("optional")then +local function i(t) +e:debug("Establishing Session..."); +e:send_iq(a.iq({type="set"}):tag("session",{xmlns=o}), +function(t) +if t.attr.type=="result"then +e:event("session-success"); +elseif t.attr.type=="error"then +local a=t:child_with_name("error"); +local t,o,a=t:get_error(); +e:event("session-failure",{error=o,text=a,type=t}); +end +end); +return true; +end +e:hook("bind-success",i); +end +end +e:hook("stream-features",n); +return true; +end +end) +package.preload['verse.plugins.legacy']=(function(...) +local a=require"verse"; +local n=require"util.uuid".generate; +local i="jabber:iq:auth"; +function a.plugins.legacy(e) +function handle_auth_form(t) +local o=t:get_child("query",i); +if t.attr.type~="result"or not o then +local o,a,t=t:get_error(); +e:debug("warn","%s %s: %s",o,a,t); +end +local t={ +username=e.username; +password=e.password; +resource=e.resource or n(); +digest=false,sequence=false,token=false; +}; +local i=a.iq({to=e.host,type="set"}) +:tag("query",{xmlns=i}); +if#o>0 then +for a in o:childtags()do +local a=a.name; +local o=t[a]; +if o then +i:tag(a):text(t[a]):up(); +elseif o==nil then +local t="feature-not-implemented"; +e:event("authentication-failure",{condition=t}); +return false; +end +end +else +for t,e in pairs(t)do +if e then +i:tag(t):text(e):up(); +end +end +end +e:send_iq(i,function(a) +if a.attr.type=="result"then +e.resource=t.resource; +e.jid=t.username.."@"..e.host.."/"..t.resource; +e:event("authentication-success"); +e:event("bind-success",e.jid); +else +local a,t,a=a:get_error(); +e:event("authentication-failure",{condition=t}); +end +end); +end +function handle_opened(t) +if not t.version then +e:send_iq(a.iq({type="get"}) +:tag("query",{xmlns="jabber:iq:auth"}) +:tag("username"):text(e.username), +handle_auth_form); +end +end +e:hook("opened",handle_opened); +end +end) +package.preload['verse.plugins.compression']=(function(...) +local a=require"verse"; +local e=require"zlib"; +local t="http://jabber.org/features/compress" +local t="http://jabber.org/protocol/compress" +local o="http://etherx.jabber.org/streams"; +local i=9; +local function l(o) +local i,e=pcall(e.deflate,i); +if i==false then +local t=a.stanza("failure",{xmlns=t}):tag("setup-failed"); +o:send(t); +o:error("Failed to create zlib.deflate filter: %s",tostring(e)); +return +end +return e +end +local function d(o) +local i,e=pcall(e.inflate); +if i==false then +local t=a.stanza("failure",{xmlns=t}):tag("setup-failed"); +o:send(t); +o:error("Failed to create zlib.inflate filter: %s",tostring(e)); +return +end +return e +end +local function h(e,o) +function e:send(i) +local i,o,n=pcall(o,tostring(i),'sync'); +if i==false then +e:close({ +condition="undefined-condition"; +text=o; +extra=a.stanza("failure",{xmlns=t}):tag("processing-failed"); +}); +e:warn("Compressed send failed: %s",tostring(o)); +return; +end +e.conn:write(o); +end; +end +local function r(e,i) +local s=e.data +e.data=function(n,o) +e:debug("Decompressing data..."); +local i,o,h=pcall(i,o); +if i==false then +e:close({ +condition="undefined-condition"; +text=o; +extra=a.stanza("failure",{xmlns=t}):tag("processing-failed"); +}); +stream:warn("%s",tostring(o)); +return; +end +return s(n,o); +end; +end +function a.plugins.compression(e) +local function i(o) +if not e.compressed then +local o=o:child_with_name("compression"); +if o then +for o in o:children()do +local o=o[1] +if o=="zlib"then +e:send(a.stanza("compress",{xmlns=t}):tag("method"):text("zlib")) +e:debug("Enabled compression using zlib.") +return true; +end +end +session:debug("Remote server supports no compression algorithm we support.") +end +end +end +local function o(t) +if t.name=="compressed"then +e:debug("Activating compression...") +local a=l(e); +if not a then return end +local t=d(e); +if not t then return end +h(e,a); +r(e,t); +e.compressed=true; +e:reopen(); +elseif t.name=="failure"then +e:warn("Failed to establish compression"); +end +end +e:hook("stream-features",i,250); +e:hook("stream/"..t,o); +end +end) +package.preload['verse.plugins.smacks']=(function(...) +local s=require"verse"; +local h=socket.gettime; +local n="urn:xmpp:sm:2"; +function s.plugins.smacks(e) +local t={}; +local a=0; +local r=h(); +local o; +local i=0; +local function d(t) +if t.attr.xmlns=="jabber:client"or not t.attr.xmlns then +i=i+1; +e:debug("Increasing handled stanzas to %d for %s",i,t:top_tag()); +end +end +function outgoing_stanza(a) +if a.name and not a.attr.xmlns then +t[#t+1]=tostring(a); +r=h(); +if not o then +o=true; +e:debug("Waiting to send ack request..."); +s.add_task(1,function() +if#t==0 then +o=false; +return; +end +local a=h()-r; +if a<1 and#t<10 then +return 1-a; +end +e:debug("Time up, sending ..."); +o=false; +e:send(s.stanza("r",{xmlns=n})); +end); +end +end +end +local function h() +e:debug("smacks: connection lost"); +e.stream_management_supported=nil; +if e.resumption_token then +e:debug("smacks: have resumption token, reconnecting in 1s..."); +e.authenticated=nil; +s.add_task(1,function() +e:connect(e.connect_host or e.host,e.connect_port or 5222); +end); +return true; +end +end +local function r() +e.resumption_token=nil; +e:unhook("disconnected",h); +end +local function l(o) +if o.name=="r"then +e:debug("Ack requested... acking %d handled stanzas",i); +e:send(s.stanza("a",{xmlns=n,h=tostring(i)})); +elseif o.name=="a"then +local o=tonumber(o.attr.h); +if o>a then +local i=#t; +for a=a+1,o do +table.remove(t,1); +end +e:debug("Received ack: New ack: "..o.." Last ack: "..a.." Unacked stanzas now: "..#t.." (was "..i..")"); +a=o; +else +e:warn("Received bad ack for "..o.." when last ack was "..a); +end +elseif o.name=="enabled"then +if o.attr.id then +e.resumption_token=o.attr.id; +e:hook("closed",r,100); +e:hook("disconnected",h,100); +end +elseif o.name=="resumed"then +local o=tonumber(o.attr.h); +if o>a then +local i=#t; +for a=a+1,o do +table.remove(t,1); +end +e:debug("Received ack: New ack: "..o.." Last ack: "..a.." Unacked stanzas now: "..#t.." (was "..i..")"); +a=o; +end +for a=1,#t do +e:send(t[a]); +end +t={}; +e:debug("Resumed successfully"); +e:event("resumed"); +else +e:warn("Don't know how to handle "..n.."/"..o.name); +end +end +local function t() +if not e.smacks then +e:debug("smacks: sending enable"); +e:send(s.stanza("enable",{xmlns=n,resume="true"})); +e.smacks=true; +e:hook("stanza",d); +e:hook("outgoing",outgoing_stanza); +end +end +local function o(a) +if a:get_child("sm",n)then +e.stream_management_supported=true; +if e.smacks and e.bound then +e:debug("Resuming stream with %d handled stanzas",i); +e:send(s.stanza("resume",{xmlns=n, +h=i,previd=e.resumption_token})); +return true; +else +e:hook("bind-success",t,1); +end +end +end +e:hook("stream-features",o,250); +e:hook("stream/"..n,l); +end +end) +package.preload['verse.plugins.keepalive']=(function(...) +local t=require"verse"; +function t.plugins.keepalive(e) +e.keepalive_timeout=e.keepalive_timeout or 300; +t.add_task(e.keepalive_timeout,function() +e.conn:write(" "); +return e.keepalive_timeout; +end); +end +end) +package.preload['verse.plugins.disco']=(function(...) +local a=require"verse"; +local r=require("mime").b64; +local s=require("util.sha1").sha1; +local n="http://jabber.org/protocol/caps"; +local e="http://jabber.org/protocol/disco"; +local i=e.."#info"; +local o=e.."#items"; +function a.plugins.disco(e) +e:add_plugin("presence"); +local h={ +__index=function(a,e) +local t={identities={},features={}}; +if e=="identities"or e=="features"then +return a[false][e] +end +a[e]=t; +return t; +end, +}; +local t={ +__index=function(t,a) +local e={}; +t[a]=e; +return e; +end, +}; +e.disco={ +cache={}, +info=setmetatable({ +[false]={ +identities={ +{category='client',type='pc',name='Verse'}, +}, +features={ +[n]=true, +[i]=true, +[o]=true, +}, +}, +},h); +items=setmetatable({[false]={}},t); +}; +e.caps={} +e.caps.node='http://code.matthewwild.co.uk/verse/' +local function h(t,e) +if t.category0 then +self.connecting_peer_candidates=true; +local function n(t,e) +self.jingle:send_command("transport-info",a.stanza("content",{creator=self.creator,name=self.name}) +:tag("transport",{xmlns=o,sid=self.s5b_sid}) +:tag("candidate-used",{cid=t.cid})); +self.onconnect_callback=i; +self.conn=e; +end +local e=h(self.s5b_sid..self.peer..e.jid,true); +s(n,t,e); +else +e:warn("Actually, I'm going to wait for my peer to tell me its streamhost..."); +self.onconnect_callback=i; +end +end +function t:info_received(t) +e:warn("Info received"); +local n=t:child_with_name("content"); +local i=n:child_with_name("transport"); +if i:get_child("candidate-used")and not self.connecting_peer_candidates then +local t=i:child_with_name("candidate-used"); +if t then +local function r(i,e) +if self.jingle.role=="initiator"then +self.jingle.stream:send_iq(a.iq({to=i.jid,type="set"}) +:tag("query",{xmlns=l,sid=self.s5b_sid}) +:tag("activate"):text(self.jingle.peer),function(i) +if i.attr.type=="result"then +self.jingle:send_command("transport-info",a.stanza("content",n.attr) +:tag("transport",{xmlns=o,sid=self.s5b_sid}) +:tag("activated",{cid=t.attr.cid})); +self.conn=e; +self.onconnect_callback(e); +else +self.jingle.stream:error("Failed to activate bytestream"); +end +end); +end +end +self.jingle.stream:debug("CID: %s",self.jingle.stream.proxy65.available_streamhosts[t.attr.cid]); +local t={ +self.jingle.stream.proxy65.available_streamhosts[t.attr.cid]; +}; +local e=h(self.s5b_sid..e.jid..self.peer,true); +s(r,t,e); +end +elseif i:get_child("activated")then +self.onconnect_callback(self.conn); +end +end +function t:disconnect() +if self.conn then +self.conn:close(); +end +end +function t:handle_accepted(e) +end +local t={__index=t}; +e:hook("jingle/transport/"..o,function(e) +return setmetatable({ +role=e.role, +peer=e.peer, +stream=e.stream, +jingle=e, +},t); +end); +end +end) +package.preload['verse.plugins.proxy65']=(function(...) +local e=require"util.events"; +local r=require"util.uuid"; +local h=require"util.sha1"; +local i={}; +i.__index=i; +local o="http://jabber.org/protocol/bytestreams"; +local n; +function verse.plugins.proxy65(t) +t.proxy65=setmetatable({stream=t},i); +t.proxy65.available_streamhosts={}; +local e=0; +t:hook("disco/service-discovered/proxy",function(a) +if a.type=="bytestreams"then +e=e+1; +t:send_iq(verse.iq({to=a.jid,type="get"}) +:tag("query",{xmlns=o}),function(a) +e=e-1; +if a.attr.type=="result"then +local e=a:get_child("query",o) +:get_child("streamhost").attr; +t.proxy65.available_streamhosts[e.jid]={ +jid=e.jid; +host=e.host; +port=tonumber(e.port); +}; +end +if e==0 then +t:event("proxy65/discovered-proxies",t.proxy65.available_streamhosts); +end +end); +end +end); +t:hook("iq/"..o,function(a) +local e=verse.new(nil,{ +initiator_jid=a.attr.from, +streamhosts={}, +current_host=0; +}); +for t in a.tags[1]:childtags()do +if t.name=="streamhost"then +table.insert(e.streamhosts,t.attr); +end +end +local function o() +if e.current_host<#e.streamhosts then +e.current_host=e.current_host+1; +e:connect( +e.streamhosts[e.current_host].host, +e.streamhosts[e.current_host].port +); +n(t,e,a.tags[1].attr.sid,a.attr.from,t.jid); +return true; +end +e:unhook("disconnected",o); +t:send(verse.error_reply(a,"cancel","item-not-found")); +end +function e:accept() +e:hook("disconnected",o,100); +e:hook("connected",function() +e:unhook("disconnected",o); +local e=verse.reply(a) +:tag("query",a.tags[1].attr) +:tag("streamhost-used",{jid=e.streamhosts[e.current_host].jid}); +t:send(e); +end,100); +o(); +end +function e:refuse() +end +t:event("proxy65/request",e); +end); +end +function i:new(t,s) +local e=verse.new(nil,{ +target_jid=t; +bytestream_sid=r.generate(); +}); +local a=verse.iq{type="set",to=t} +:tag("query",{xmlns=o,mode="tcp",sid=e.bytestream_sid}); +for t,e in ipairs(s or self.proxies)do +a:tag("streamhost",e):up(); +end +self.stream:send_iq(a,function(a) +if a.attr.type=="error"then +local o,t,a=a:get_error(); +e:event("connection-failed",{conn=e,type=o,condition=t,text=a}); +else +local a=a.tags[1]:get_child("streamhost-used"); +if not a then +end +e.streamhost_jid=a.attr.jid; +local a,i; +for o,t in ipairs(s or self.proxies)do +if t.jid==e.streamhost_jid then +a,i=t.host,t.port; +break; +end +end +if not(a and i)then +end +e:connect(a,i); +local function a() +e:unhook("connected",a); +local t=verse.iq{to=e.streamhost_jid,type="set"} +:tag("query",{xmlns=o,sid=e.bytestream_sid}) +:tag("activate"):text(t); +self.stream:send_iq(t,function(t) +if t.attr.type=="result"then +e:event("connected",e); +else +end +end); +return true; +end +e:hook("connected",a,100); +n(self.stream,e,e.bytestream_sid,self.stream.jid,t); +end +end); +return e; +end +function n(i,e,a,o,t) +local t=h.sha1(a..o..t); +local function o() +e:unhook("connected",o); +return true; +end +local function a(t) +e:unhook("incoming-raw",a); +if t:sub(1,2)~="\005\000"then +return e:event("error","connection-failure"); +end +e:event("connected"); +return true; +end +local function i(o) +e:unhook("incoming-raw",i); +if o~="\005\000"then +local t="version-mismatch"; +if o:sub(1,1)=="\005"then +t="authentication-failure"; +end +return e:event("error",t); +end +e:send(string.char(5,1,0,3,#t)..t.."\0\0"); +e:hook("incoming-raw",a,100); +return true; +end +e:hook("connected",o,200); +e:hook("incoming-raw",i,100); +e:send("\005\001\000"); +end +end) +package.preload['verse.plugins.jingle_ibb']=(function(...) +local e=require"verse"; +local i=require"util.encodings".base64; +local s=require"util.uuid".generate; +local n="urn:xmpp:jingle:transports:ibb:1"; +local o="http://jabber.org/protocol/ibb"; +assert(i.encode("This is a test.")=="VGhpcyBpcyBhIHRlc3Qu","Base64 encoding failed"); +assert(i.decode("VGhpcyBpcyBhIHRlc3Qu")=="This is a test.","Base64 decoding failed"); +local t=table.concat +local a={}; +local t={__index=a}; +local function h(a) +local t=setmetatable({stream=a},t) +t=e.eventable(t); +return t; +end +function a:initiate(a,e,t) +self.block=2048; +self.stanza=t or'iq'; +self.peer=a; +self.sid=e or tostring(self):match("%x+$"); +self.iseq=0; +self.oseq=0; +local e=function(e) +return self:feed(e) +end +self.feeder=e; +print("Hooking incomming IQs"); +local a=self.stream; +a:hook("iq/"..o,e) +if t=="message"then +a:hook("message",e) +end +end +function a:open(t) +self.stream:send_iq(e.iq{to=self.peer,type="set"} +:tag("open",{ +xmlns=o, +["block-size"]=self.block, +sid=self.sid, +stanza=self.stanza +}) +,function(e) +if t then +if e.attr.type~="error"then +t(true) +else +t(false,e:get_error()) +end +end +end); +end +function a:send(n) +local a=self.stanza; +local t; +if a=="iq"then +t=e.iq{type="set",to=self.peer} +elseif a=="message"then +t=e.message{to=self.peer} +end +local e=self.oseq; +self.oseq=e+1; +t:tag("data",{xmlns=o,sid=self.sid,seq=e}) +:text(i.encode(n)); +if a=="iq"then +self.stream:send_iq(t,function(e) +self:event(e.attr.type=="result"and"drained"or"error"); +end) +else +stream:send(t) +self:event("drained"); +end +end +function a:feed(t) +if t.attr.from~=self.peer then return end +local a=t[1]; +if a.attr.sid~=self.sid then return end +local n; +if a.name=="open"then +self:event("connected"); +self.stream:send(e.reply(t)) +return true +elseif a.name=="data"then +local o=t:get_child_text("data",o); +local a=tonumber(a.attr.seq); +local n=self.iseq; +if o and a then +if a~=n then +self.stream:send(e.error_reply(t,"cancel","not-acceptable","Wrong sequence. Packet lost?")) +self:close(); +self:event("error"); +return true; +end +self.iseq=a+1; +local a=i.decode(o); +if self.stanza=="iq"then +self.stream:send(e.reply(t)) +end +self:event("incoming-raw",a); +return true; +end +elseif a.name=="close"then +self.stream:send(e.reply(t)) +self:close(); +return true +end +end +function a:close() +self.stream:unhook("iq/"..o,self.feeder) +self:event("disconnected"); +end +function e.plugins.jingle_ibb(a) +a:hook("ready",function() +a:add_disco_feature(n); +end,10); +local t={}; +function t:_setup() +local e=h(self.stream); +e.sid=self.sid or e.sid; +e.stanza=self.stanza or e.stanza; +e.block=self.block or e.block; +e:initiate(self.peer,self.sid,self.stanza); +self.conn=e; +end +function t:generate_initiate() +print("ibb:generate_initiate() as "..self.role); +local t=s(); +self.sid=t; +self.stanza='iq'; +self.block=2048; +local e=e.stanza("transport",{xmlns=n, +sid=self.sid,stanza=self.stanza,["block-size"]=self.block}); +return e; +end +function t:generate_accept(t) +print("ibb:generate_accept() as "..self.role); +local e=t.attr; +self.sid=e.sid or self.sid; +self.stanza=e.stanza or self.stanza; +self.block=e["block-size"]or self.block; +self:_setup(); +return t; +end +function t:connect(t) +if not self.conn then +self:_setup(); +end +local e=self.conn; +print("ibb:connect() as "..self.role); +if self.role=="initiator"then +e:open(function(a,...) +assert(a,table.concat({...},", ")); +t(e); +end); +else +t(e); +end +end +function t:info_received(e) +print("ibb:info_received()"); +end +function t:disconnect() +if self.conn then +self.conn:close() +end +end +function t:handle_accepted(e)end +local t={__index=t}; +a:hook("jingle/transport/"..n,function(e) +return setmetatable({ +role=e.role, +peer=e.peer, +stream=e.stream, +jingle=e, +},t); +end); +end +end) +package.preload['verse.plugins.pubsub']=(function(...) +local o=require"verse"; +local e=require"util.jid".bare; +local n=table.insert; +local i="http://jabber.org/protocol/pubsub"; +local s="http://jabber.org/protocol/pubsub#owner"; +local a="http://jabber.org/protocol/pubsub#event"; +local e="http://jabber.org/protocol/pubsub#errors"; +local e={}; +local h={__index=e}; +function o.plugins.pubsub(e) +e.pubsub=setmetatable({stream=e},h); +e:hook("message",function(t) +local o=t.attr.from; +for t in t:childtags("event",a)do +local t=t:get_child("items"); +if t then +local a=t.attr.node; +for t in t:childtags("item")do +e:event("pubsub/event",{ +from=o; +node=a; +item=t; +}); +end +end +end +end); +return true; +end +function e:create(e,a,t) +return self:service(e):node(a):create(nil,t); +end +function e:subscribe(a,o,t,e) +return self:service(a):node(o):subscribe(t,nil,e); +end +function e:publish(e,t,a,o,i) +return self:service(e):node(t):publish(a,nil,o,i); +end +local a={}; +local t={__index=a}; +function e:service(e) +return setmetatable({stream=self.stream,service=e},t) +end +local function t(e,h,r,a,s,n,t) +local e=o.iq{type=e or"get",to=h} +:tag("pubsub",{xmlns=r or i}) +if a then e:tag(a,{node=s,jid=n});end +if t then e:tag("item",{id=t~=true and t or nil});end +return e; +end +function a:subscriptions(a) +self.stream:send_iq(t(nil,self.service,nil,"subscriptions") +,a and function(o) +if o.attr.type=="result"then +local e=o:get_child("pubsub",i); +local e=e and e:get_child("subscriptions"); +local o={}; +if e then +for t in e:childtags("subscription")do +local e=self:node(t.attr.node) +e.subscription=t; +e.subscribed_jid=t.attr.jid; +n(o,e); +end +end +a(o); +else +a(false,o:get_error()); +end +end or nil); +end +function a:affiliations(a) +self.stream:send_iq(t(nil,self.service,nil,"affiliations") +,a and function(e) +if e.attr.type=="result"then +local e=e:get_child("pubsub",i); +local e=e and e:get_child("affiliations")or{}; +local t={}; +if e then +for e in e:childtags("affiliation")do +local a=self:node(e.attr.node) +a.affiliation=e; +n(t,a); +end +end +a(t); +else +a(false,e:get_error()); +end +end or nil); +end +function a:nodes(a) +self.stream:disco_items(self.service,nil,function(e,...) +if e then +for t=1,#e do +e[t]=self:node(e[t].node); +end +end +a(e,...) +end); +end +local e={}; +local o={__index=e}; +function a:node(e) +return setmetatable({stream=self.stream,service=self.service,node=e},o) +end +function h:__call(e,t) +local e=self:service(e); +return t and e:node(t)or e; +end +function e:hook(a,o) +self._hooks=self._hooks or setmetatable({},{__mode='kv'}); +local function t(e) +if(not e.service or e.from==self.service)and e.node==self.node then +return a(e) +end +end +self._hooks[a]=t; +self.stream:hook("pubsub/event",t,o); +return t; +end +function e:unhook(e) +if e then +local e=self._hooks[e]; +self.stream:unhook("pubsub/event",e); +elseif self._hooks then +for e in pairs(self._hooks)do +self.stream:unhook("pubsub/event",e); +end +end +end +function e:create(a,e) +if a~=nil then +error("Not implemented yet."); +else +self.stream:send_iq(t("set",self.service,nil,"create",self.node),e); +end +end +function e:configure(e,a) +if e~=nil then +error("Not implemented yet."); +end +self.stream:send_iq(t("set",self.service,nil,e==nil and"default"or"configure",self.node),a); +end +function e:publish(e,o,a,i) +if o~=nil then +error("Node configuration is not implemented yet."); +end +self.stream:send_iq(t("set",self.service,nil,"publish",self.node,nil,e or true) +:add_child(a) +,i); +end +function e:subscribe(e,a,o) +e=e or self.stream.jid; +if a~=nil then +error("Subscription configuration is not implemented yet."); +end +self.stream:send_iq(t("set",self.service,nil,"subscribe",self.node,e,id) +,o); +end +function e:subscription(e) +error("Not implemented yet."); +end +function e:affiliation(e) +error("Not implemented yet."); +end +function e:unsubscribe(e,a) +e=e or self.subscribed_jid or self.stream.jid; +self.stream:send_iq(t("set",self.service,nil,"unsubscribe",self.node,e) +,a); +end +function e:configure_subscription(e,e) +error("Not implemented yet."); +end +function e:items(a,e) +if a then +self.stream:send_iq(t("get",self.service,nil,"items",self.node) +,e); +else +self.stream:disco_items(self.service,self.node,e); +end +end +function e:item(e,a) +self.stream:send_iq(t("get",self.service,nil,"items",self.node,nil,e) +,a); +end +function e:retract(a,e) +self.stream:send_iq(t("set",self.service,nil,"retract",self.node,nil,a) +,e); +end +function e:purge(a,e) +assert(not a,"Not implemented yet."); +self.stream:send_iq(t("set",self.service,s,"purge",self.node) +,e); +end +function e:delete(e,a) +assert(not e,"Not implemented yet."); +self.stream:send_iq(t("set",self.service,s,"delete",self.node) +,a); +end +end) +package.preload['verse.plugins.pep']=(function(...) +local e=require"verse"; +local t="http://jabber.org/protocol/pubsub"; +local t=t.."#event"; +function e.plugins.pep(e) +e:add_plugin("disco"); +e:add_plugin("pubsub"); +e.pep={}; +e:hook("pubsub/event",function(t) +return e:event("pep/"..t.node,{from=t.from,item=t.item.tags[1]}); +end); +function e:hook_pep(t,o,i) +local a=e.events._handlers["pep/"..t]; +if not(a)or#a==0 then +e:add_disco_feature(t.."+notify"); +end +e:hook("pep/"..t,o,i); +end +function e:unhook_pep(t,a) +e:unhook("pep/"..t,a); +local a=e.events._handlers["pep/"..t]; +if not(a)or#a==0 then +e:remove_disco_feature(t.."+notify"); +end +end +function e:publish_pep(t,a) +return e.pubsub:service(nil):node(a or t.attr.xmlns):publish(nil,nil,t) +end +end +end) +package.preload['verse.plugins.adhoc']=(function(...) +local o=require"verse"; +local n=require"lib.adhoc"; +local t="http://jabber.org/protocol/commands"; +local r="jabber:x:data"; +local a={}; +a.__index=a; +local i={}; +function o.plugins.adhoc(e) +e:add_plugin("disco"); +e:add_disco_feature(t); +function e:query_commands(a,o) +e:disco_items(a,t,function(t) +e:debug("adhoc list returned") +local a={}; +for o,t in ipairs(t)do +a[t.node]=t.name; +end +e:debug("adhoc calling callback") +return o(a); +end); +end +function e:execute_command(i,t,o) +local e=setmetatable({ +stream=e,jid=i, +command=t,callback=o +},a); +return e:execute(); +end +local function h(t,e) +if not(e)or e=="user"then return true;end +if type(e)=="function"then +return e(t); +end +end +function e:add_adhoc_command(o,a,s,h) +i[a]=n.new(o,a,s,h); +e:add_disco_item({jid=e.jid,node=a,name=o},t); +return i[a]; +end +local function s(a) +local t=a.tags[1]; +local t=t.attr.node; +local t=i[t]; +if not t then return;end +if not h(a.attr.from,t.permission)then +e:send(o.error_reply(a,"auth","forbidden","You don't have permission to execute this command"):up() +:add_child(t:cmdtag("canceled") +:tag("note",{type="error"}):text("You don't have permission to execute this command"))); +return true +end +return n.handle_cmd(t,{send=function(t)return e:send(t)end},a); +end +e:hook("iq/"..t,function(e) +local a=e.attr.type; +local t=e.tags[1].name; +if a=="set"and t=="command"then +return s(e); +end +end); +end +function a:_process_response(e) +if e.attr.type=="error"then +self.status="canceled"; +self.callback(self,{}); +return; +end +local e=e:get_child("command",t); +self.status=e.attr.status; +self.sessionid=e.attr.sessionid; +self.form=e:get_child("x",r); +self.note=e:get_child("note"); +self.callback(self); +end +function a:execute() +local e=o.iq({to=self.jid,type="set"}) +:tag("command",{xmlns=t,node=self.command}); +self.stream:send_iq(e,function(e) +self:_process_response(e); +end); +end +function a:next(e) +local t=o.iq({to=self.jid,type="set"}) +:tag("command",{ +xmlns=t, +node=self.command, +sessionid=self.sessionid +}); +if e then t:add_child(e);end +self.stream:send_iq(t,function(e) +self:_process_response(e); +end); +end +end) +package.preload['verse.plugins.presence']=(function(...) +local a=require"verse"; +function a.plugins.presence(t) +t.last_presence=nil; +t:hook("presence-out",function(e) +if not e.attr.to then +t.last_presence=e; +end +end,1); +function t:resend_presence() +if last_presence then +t:send(last_presence); +end +end +function t:set_status(e) +local a=a.presence(); +if type(e)=="table"then +if e.show then +a:tag("show"):text(e.show):up(); +end +if e.prio then +a:tag("priority"):text(tostring(e.prio)):up(); +end +if e.msg then +a:tag("status"):text(e.msg):up(); +end +end +t:send(a); +end +end +end) +package.preload['verse.plugins.private']=(function(...) +local a=require"verse"; +local o="jabber:iq:private"; +function a.plugins.private(s) +function s:private_set(n,i,e,s) +local t=a.iq({type="set"}) +:tag("query",{xmlns=o}); +if e then +if e.name==n and e.attr and e.attr.xmlns==i then +t:add_child(e); +else +t:tag(n,{xmlns=i}) +:add_child(e); +end +end +self:send_iq(t,s); +end +function s:private_get(i,t,n) +self:send_iq(a.iq({type="get"}) +:tag("query",{xmlns=o}) +:tag(i,{xmlns=t}), +function(e) +if e.attr.type=="result"then +local e=e:get_child("query",o); +local e=e:get_child(i,t); +n(e); +end +end); +end +end +end) +package.preload['verse.plugins.roster']=(function(...) +local i=require"verse"; +local r=require"util.jid".bare; +local a="jabber:iq:roster"; +local o="urn:xmpp:features:rosterver"; +local n=table.insert; +function i.plugins.roster(t) +local s=false; +local e={ +items={}; +ver=""; +}; +t.roster=e; +t:hook("stream-features",function(e) +if e:get_child("ver",o)then +s=true; +end +end); +local function h(t) +local e=i.stanza("item",{xmlns=a}); +for a,t in pairs(t)do +if a~="groups"then +e.attr[a]=t; +else +for a=1,#t do +e:tag("group"):text(t[a]):up(); +end +end +end +return e; +end +local function d(t) +local e={}; +local a={}; +e.groups=a; +local o=t.attr.jid; +for t,a in pairs(t.attr)do +if t~="xmlns"then +e[t]=a +end +end +for e in t:childtags("group")do +n(a,e:get_text()) +end +return e; +end +function e:load(t) +e.ver,e.items=t.ver,t.items; +end +function e:dump() +return{ +ver=e.ver, +items=e.items, +}; +end +function e:add_contact(o,s,n,e) +local o={jid=o,name=s,groups=n}; +local a=i.iq({type="set"}) +:tag("query",{xmlns=a}) +:add_child(h(o)); +t:send_iq(a,function(t) +if not e then return end +if t.attr.type=="result"then +e(true); +else +local o,t,a=t:get_error(); +e(nil,{o,t,a}); +end +end); +end +function e:delete_contact(o,n) +o=(type(o)=="table"and o.jid)or o; +local s={jid=o,subscription="remove"} +if not e.items[o]then return false,"item-not-found";end +t:send_iq(i.iq({type="set"}) +:tag("query",{xmlns=a}) +:add_child(h(s)), +function(e) +if not n then return end +if e.attr.type=="result"then +n(true); +else +local a,t,e=e:get_error(); +n(nil,{a,t,e}); +end +end); +end +local function h(t) +local t=d(t); +e.items[t.jid]=t; +end +local function d(t) +local a=e.items[t]; +e.items[t]=nil; +return a; +end +function e:fetch(o) +t:send_iq(i.iq({type="get"}):tag("query",{xmlns=a,ver=s and e.ver or nil}), +function(t) +if t.attr.type=="result"then +local t=t:get_child("query",a); +if t then +e.items={}; +for t in t:childtags("item")do +h(t) +end +e.ver=t.attr.ver or""; +end +o(e); +else +local e,t,a=stanza:get_error(); +o(nil,{e,t,a}); +end +end); +end +t:hook("iq/"..a,function(o) +local s,n=o.attr.type,o.attr.from; +if s=="set"and(not n or n==r(t.jid))then +local s=o:get_child("query",a); +local n=s and s:get_child("item"); +if n then +local o,a; +local i=n.attr.jid; +if n.attr.subscription=="remove"then +o="removed" +a=d(i); +else +o=e.items[i]and"changed"or"added"; +h(n) +a=e.items[i]; +end +e.ver=s.attr.ver; +if a then +t:event("roster/item-"..o,a); +end +end +t:send(i.reply(o)) +return true; +end +end); +end +end) +package.preload['verse.plugins.register']=(function(...) +local t=require"verse"; +local i="jabber:iq:register"; +function t.plugins.register(e) +local function a(o) +if o:get_child("register","http://jabber.org/features/iq-register")then +local t=t.iq({to=e.host_,type="set"}) +:tag("query",{xmlns=i}) +:tag("username"):text(e.username):up() +:tag("password"):text(e.password):up(); +if e.register_email then +t:tag("email"):text(e.register_email):up(); +end +e:send_iq(t,function(t) +if t.attr.type=="result"then +e:event("registration-success"); +else +local a,t,o=t:get_error(); +e:debug("Registration failed: %s",t); +e:event("registration-failure",{type=a,condition=t,text=o}); +end +end); +else +e:debug("In-band registration not offered by server"); +e:event("registration-failure",{condition="service-unavailable"}); +end +e:unhook("stream-features",a); +return true; +end +e:hook("stream-features",a,310); +end +end) +package.preload['verse.plugins.groupchat']=(function(...) +local i=require"verse"; +local e=require"events"; +local n=require"util.jid"; +local a={}; +a.__index=a; +local s="urn:xmpp:delay"; +local h="http://jabber.org/protocol/muc"; +function i.plugins.groupchat(o) +o:add_plugin("presence") +o.rooms={}; +o:hook("stanza",function(e) +local a=n.bare(e.attr.from); +if not a then return end +local t=o.rooms[a] +if not t and e.attr.to and a then +t=o.rooms[e.attr.to.." "..a] +end +if t and t.opts.source and e.attr.to~=t.opts.source then return end +if t then +local o=select(3,n.split(e.attr.from)); +local n=e:get_child_text("body"); +local i=e:get_child("delay",s); +local a={ +room_jid=a; +room=t; +sender=t.occupants[o]; +nick=o; +body=n; +stanza=e; +delay=(i and i.attr.stamp); +}; +local t=t:event(e.name,a); +return t or(e.name=="message")or nil; +end +end,500); +function o:join_room(n,s,t) +if not s then +return false,"no nickname supplied" +end +t=t or{}; +local e=setmetatable(i.eventable{ +stream=o,jid=n,nick=s, +subject=nil, +occupants={}, +opts=t, +},a); +if t.source then +self.rooms[t.source.." "..n]=e; +else +self.rooms[n]=e; +end +local a=e.occupants; +e:hook("presence",function(o) +local t=o.nick or s; +if not a[t]and o.stanza.attr.type~="unavailable"then +a[t]={ +nick=t; +jid=o.stanza.attr.from; +presence=o.stanza; +}; +local o=o.stanza:get_child("x",h.."#user"); +if o then +local e=o:get_child("item"); +if e and e.attr then +a[t].real_jid=e.attr.jid; +a[t].affiliation=e.attr.affiliation; +a[t].role=e.attr.role; +end +end +if t==e.nick then +e.stream:event("groupchat/joined",e); +else +e:event("occupant-joined",a[t]); +end +elseif a[t]and o.stanza.attr.type=="unavailable"then +if t==e.nick then +e.stream:event("groupchat/left",e); +if e.opts.source then +self.rooms[e.opts.source.." "..n]=nil; +else +self.rooms[n]=nil; +end +else +a[t].presence=o.stanza; +e:event("occupant-left",a[t]); +a[t]=nil; +end +end +end); +e:hook("message",function(a) +local t=a.stanza:get_child_text("subject"); +if not t then return end +t=#t>0 and t or nil; +if t~=e.subject then +local o=e.subject; +e.subject=t; +return e:event("subject-changed",{from=o,to=t,by=a.sender,event=a}); +end +end,2e3); +local t=i.presence():tag("x",{xmlns=h}):reset(); +self:event("pre-groupchat/joining",t); +e:send(t) +self:event("groupchat/joining",e); +return e; +end +o:hook("presence-out",function(e) +if not e.attr.to then +for a,t in pairs(o.rooms)do +t:send(e); +end +e.attr.to=nil; +end +end); +end +function a:send(e) +if e.name=="message"and not e.attr.type then +e.attr.type="groupchat"; +end +if e.name=="presence"then +e.attr.to=self.jid.."/"..self.nick; +end +if e.attr.type=="groupchat"or not e.attr.to then +e.attr.to=self.jid; +end +if self.opts.source then +e.attr.from=self.opts.source +end +self.stream:send(e); +end +function a:send_message(e) +self:send(i.message():tag("body"):text(e)); +end +function a:set_subject(e) +self:send(i.message():tag("subject"):text(e)); +end +function a:leave(e) +self.stream:event("groupchat/leaving",self); +local t=i.presence({type="unavailable"}); +if e then +t:tag("status"):text(e); +end +self:send(t); +end +function a:admin_set(e,t,o,a) +self:send(i.iq({type="set"}) +:query(h.."#admin") +:tag("item",{nick=e,[t]=o}) +:tag("reason"):text(a or"")); +end +function a:set_role(e,t,a) +self:admin_set(e,"role",t,a); +end +function a:set_affiliation(t,a,e) +self:admin_set(t,"affiliation",a,e); +end +function a:kick(t,e) +self:set_role(t,"none",e); +end +function a:ban(t,e) +self:set_affiliation(t,"outcast",e); +end +end) +package.preload['verse.plugins.vcard']=(function(...) +local i=require"verse"; +local o=require"util.vcard"; +local n="vcard-temp"; +function i.plugins.vcard(a) +function a:get_vcard(t,e) +a:send_iq(i.iq({to=t,type="get"}) +:tag("vCard",{xmlns=n}),e and function(t) +local a,a; +vCard=t:get_child("vCard",n); +if t.attr.type=="result"and vCard then +vCard=o.from_xep54(vCard) +e(vCard) +else +e(false) +end +end or nil); +end +function a:set_vcard(e,n) +local t; +if type(e)=="table"and e.name then +t=e; +elseif type(e)=="string"then +t=o.to_xep54(o.from_text(e)[1]); +elseif type(e)=="table"then +t=o.to_xep54(e); +error("Converting a table to vCard not implemented") +end +if not t then return false end +a:debug("setting vcard to %s",tostring(t)); +a:send_iq(i.iq({type="set"}) +:add_child(t),n); +end +end +end) +package.preload['verse.plugins.vcard_update']=(function(...) +local n=require"verse"; +local e,i="vcard-temp","vcard-temp:x:update"; +local e,t=pcall(function()return require("util.hashes").sha1;end); +if not e then +e,t=pcall(function()return require("util.sha1").sha1;end); +if not e then +error("Could not find a sha1()") +end +end +local h=t; +local e,t=pcall(function() +local e=require("util.encodings").base64.decode; +assert(e("SGVsbG8=")=="Hello") +return e; +end); +if not e then +e,t=pcall(function()return require("mime").unb64;end); +if not e then +error("Could not find a base64 decoder") +end +end +local s=t; +function n.plugins.vcard_update(e) +e:add_plugin("vcard"); +e:add_plugin("presence"); +local t; +function update_vcard_photo(a) +local o; +for e=1,#a do +if a[e].name=="PHOTO"then +o=a[e][1]; +break +end +end +if o then +local a=h(s(o),true); +t=n.stanza("x",{xmlns=i}) +:tag("photo"):text(a); +e:resend_presence() +else +t=nil; +end +end +local a=e.set_vcard; +local a; +e:hook("ready",function(t) +if a then return;end +a=true; +e:get_vcard(nil,function(t) +if t then +update_vcard_photo(t) +end +e:event("ready"); +end); +return true; +end,3); +e:hook("presence-out",function(e) +if t and not e:get_child("x",i)then +e:add_child(t); +end +end,10); +end +end) +package.preload['verse.plugins.carbons']=(function(...) +local o=require"verse"; +local a="urn:xmpp:carbons:2"; +local n="urn:xmpp:forward:0"; +local s=os.time; +local r=require"util.datetime".parse; +local h=require"util.jid".bare; +function o.plugins.carbons(e) +local t={}; +t.enabled=false; +e.carbons=t; +function t:enable(i) +e:send_iq(o.iq{type="set"} +:tag("enable",{xmlns=a}) +,function(e) +local e=e.attr.type=="result"; +if e then +t.enabled=true; +end +if i then +i(e); +end +end or nil); +end +function t:disable(i) +e:send_iq(o.iq{type="set"} +:tag("disable",{xmlns=a}) +,function(e) +local e=e.attr.type=="result"; +if e then +t.enabled=false; +end +if i then +i(e); +end +end or nil); +end +local o; +e:hook("bind-success",function() +o=h(e.jid); +end); +e:hook("message",function(i) +local t=i:get_child(nil,a); +if i.attr.from==o and t then +local o=t.name; +local t=t:get_child("forwarded",n); +local a=t and t:get_child("message","jabber:client"); +local t=t:get_child("delay","urn:xmpp:delay"); +local t=t and t.attr.stamp; +t=t and r(t); +if a then +return e:event("carbon",{ +dir=o, +stanza=a, +timestamp=t or s(), +}); +end +end +end,1); +end +end) +package.preload['verse.plugins.archive']=(function(...) +local e=require"verse"; +local t=require"util.stanza"; +local a="urn:xmpp:mam:0" +local d="urn:xmpp:forward:0"; +local l="urn:xmpp:delay"; +local i=require"util.uuid".generate; +local u=require"util.datetime".parse; +local o=require"util.datetime".datetime; +local n=require"util.dataforms".new; +local h=require"util.rsm"; +local c={}; +local m=n{ +{name="FORM_TYPE";type="hidden";value=a;}; +{name="with";type="jid-single";}; +{name="start";type="text-single"}; +{name="end";type="text-single";}; +}; +function e.plugins.archive(n) +function n:query_archive(n,e,r) +local s=i(); +local i=t.iq{type="set",to=n} +:tag("query",{xmlns=a,queryid=s}); +local n,t=tonumber(e["start"]),tonumber(e["end"]); +e["start"]=n and o(n); +e["end"]=t and o(t); +i:add_child(m:form(e,"submit")); +i:add_child(h.generate(e)); +local o={}; +local function n(i) +local e=i:get_child("fin",a) +if e and e.attr.queryid==s then +local e=h.get(e); +for t,e in pairs(e or c)do o[t]=e;end +self:unhook("message",n); +r(o); +return true +end +local t=i:get_child("result",a); +if t and t.attr.queryid==s then +local e=t:get_child("forwarded",d); +e=e or i:get_child("forwarded",d); +local a=t.attr.id; +local t=e:get_child("delay",l); +local t=t and u(t.attr.stamp)or nil; +local e=e:get_child("message","jabber:client") +o[#o+1]={id=a,stamp=t,message=e}; +return true +end +end +self:hook("message",n,1); +self:send_iq(i,function(e) +if e.attr.type=="error"then +self:warn(table.concat({e:get_error()}," ")) +self:unhook("message",n); +r(false,e:get_error()) +end +return true +end); +end +local i={ +always=true,[true]="always", +never=false,[false]="never", +roster="roster", +} +local function h(t) +local e={}; +local a=t.attr.default; +if a then +e[false]=i[a]; +end +local a=t:get_child("always"); +if a then +for t in a:childtags("jid")do +local t=t:get_text(); +e[t]=true; +end +end +local t=t:get_child("never"); +if t then +for t in t:childtags("jid")do +local t=t:get_text(); +e[t]=false; +end +end +return e; +end +local function s(o) +local e +e,o[false]=o[false],nil; +if e~=nil then +e=i[e]; +end +local i=t.stanza("prefs",{xmlns=a,default=e}) +local a=t.stanza("always"); +local e=t.stanza("never"); +for t,o in pairs(o)do +(o and a or e):tag("jid"):text(t):up(); +end +return i:add_child(a):add_child(e); +end +function n:archive_prefs_get(o) +self:send_iq(t.iq{type="get"}:tag("prefs",{xmlns=a}), +function(e) +if e and e.attr.type=="result"and e.tags[1]then +local t=h(e.tags[1]); +o(t,e); +else +o(nil,e); +end +end); +end +function n:archive_prefs_set(a,e) +self:send_iq(t.iq{type="set"}:add_child(s(a)),e); +end +end +end) +package.preload['util.http']=(function(...) +local t,s=string.format,string.char; +local d,r,n=pairs,ipairs,tonumber; +local i,o=table.insert,table.concat; +local function h(e) +return e and(e:gsub("[^a-zA-Z0-9.~_-]",function(e)return t("%%%02x",e:byte());end)); +end +local function a(e) +return e and(e:gsub("%%(%x%x)",function(e)return s(n(e,16));end)); +end +local function e(e) +return e and(e:gsub("%W",function(e) +if e~=" "then +return t("%%%02x",e:byte()); +else +return"+"; +end +end)); +end +local function s(t) +local a={}; +if t[1]then +for o,t in r(t)do +i(a,e(t.name).."="..e(t.value)); +end +else +for o,t in d(t)do +i(a,e(o).."="..e(t)); +end +end +return o(a,"&"); +end +local function n(e) +if not e:match("=")then return a(e);end +local o={}; +for e,t in e:gmatch("([^=&]*)=([^&]*)")do +e,t=e:gsub("%+","%%20"),t:gsub("%+","%%20"); +e,t=a(e),a(t); +i(o,{name=e,value=t}); +o[e]=t; +end +return o; +end +local function o(e,t) +e=","..e:gsub("[ \t]",""):lower()..","; +return e:find(","..t:lower()..",",1,true)~=nil; +end +return{ +urlencode=h,urldecode=a; +formencode=s,formdecode=n; +contains_token=o; +}; +end) +package.preload['net.http.parser']=(function(...) +local m=tonumber; +local a=assert; +local v=require"socket.url".parse; +local t=require"util.http".urldecode; +local function b(e) +e=t((e:gsub("//+","/"))); +if e:sub(1,1)~="/"then +e="/"..e; +end +local t=0; +for e in e:gmatch("([^/]+)/")do +if e==".."then +t=t-1; +elseif e~="."then +t=t+1; +end +if t<0 then +return nil; +end +end +return e; +end +local y={}; +function y.new(u,r,e,y) +local d=true; +if not e or e=="server"then d=false;else a(e=="client","Invalid parser type");end +local e=""; +local p,o,h; +local s=nil; +local t; +local a; +local c; +local n; +return{ +feed=function(l,i) +if n then return nil,"parse has failed";end +if not i then +if s and d and not a then +t.body=e; +u(t); +elseif e~=""then +n=true;return r(); +end +return; +end +e=e..i; +while#e>0 do +if s==nil then +local f=e:find("\r\n\r\n",nil,true); +if not f then return;end +local w,h,l,i,g; +local u; +local o={}; +for t in e:sub(1,f+1):gmatch("([^\r\n]+)\r\n")do +if u then +local e,t=t:match("^([^%s:]+): *(.*)$"); +if not e then n=true;return r("invalid-header-line");end +e=e:lower(); +o[e]=o[e]and o[e]..","..t or t; +else +u=t; +if d then +l,i,g=t:match("^HTTP/(1%.[01]) (%d%d%d) (.*)$"); +i=m(i); +if not i then n=true;return r("invalid-status-line");end +c=not +((y and y().method=="HEAD") +or(i==204 or i==304 or i==301) +or(i>=100 and i<200)); +else +w,h,l=t:match("^(%w+) (%S+) HTTP/(1%.[01])$"); +if not w then n=true;return r("invalid-status-line");end +end +end +end +if not u then n=true;return r("invalid-status-line");end +p=c and o["transfer-encoding"]=="chunked"; +a=m(o["content-length"]); +if d then +if not c then a=0;end +t={ +code=i; +httpversion=l; +headers=o; +body=c and""or nil; +responseversion=l; +responseheaders=o; +}; +else +local e; +if h:byte()==47 then +local a,t=h:match("([^?]*).?(.*)"); +if t==""then t=nil;end +e={path=a,query=t}; +else +e=v(h); +if not(e and e.path)then n=true;return r("invalid-url");end +end +h=b(e.path); +o.host=e.host or o.host; +a=a or 0; +t={ +method=w; +url=e; +path=h; +httpversion=l; +headers=o; +body=nil; +}; +end +e=e:sub(f+4); +s=true; +end +if s then +if d then +if p then +if not e:find("\r\n",nil,true)then +return; +end +if not o then +o,h=e:match("^(%x+)[^\r\n]*\r\n()"); +o=o and m(o,16); +if not o then n=true;return r("invalid-chunk-size");end +end +if o==0 and e:find("\r\n\r\n",h-2,true)then +s,o=nil,nil; +e=e:gsub("^.-\r\n\r\n",""); +u(t); +elseif#e-h-2>=o then +t.body=t.body..e:sub(h,h+(o-1)); +e=e:sub(h+o+2); +o,h=nil,nil; +else +break; +end +elseif a and#e>=a then +if t.code==101 then +t.body,e=e,""; +else +t.body,e=e:sub(1,a),e:sub(a+1); +end +s=nil;u(t); +else +break; +end +elseif#e>=a then +t.body,e=e:sub(1,a),e:sub(a+1); +s=nil;u(t); +else +break; +end +end +end +end; +}; +end +return y; +end) +package.preload['net.http']=(function(...) +local b=require"socket" +local y=require"util.encodings".base64.encode; +local l=require"socket.url" +local d=require"net.http.parser".new; +local h=require"util.http"; +local j=pcall(require,"ssl"); +local x=require"net.server" +local u,o=table.insert,table.concat; +local m=pairs; +local k,c,q,g,s= +tonumber,tostring,xpcall,select,debug.traceback; +local v,p=assert,error +local r=require"util.logger".init("http"); +module"http" +local i={}; +local n={default_port=80,default_mode="*a"}; +function n.onconnect(t) +local e=i[t]; +local a={e.method or"GET"," ",e.path," HTTP/1.1\r\n"}; +if e.query then +u(a,4,"?"..e.query); +end +t:write(o(a)); +local a={[2]=": ",[4]="\r\n"}; +for e,i in m(e.headers)do +a[1],a[3]=e,i; +t:write(o(a)); +end +t:write("\r\n"); +if e.body then +t:write(e.body); +end +end +function n.onincoming(a,t) +local e=i[a]; +if not e then +r("warn","Received response from connection %s with no request attached!",c(a)); +return; +end +if t and e.reader then +e:reader(t); +end +end +function n.ondisconnect(t,a) +local e=i[t]; +if e and e.conn then +e:reader(nil,a); +end +i[t]=nil; +end +function n.ondetach(e) +i[e]=nil; +end +local function w(e,o,i) +if not e.parser then +local function a(t) +if e.callback then +e.callback(t or"connection-closed",0,e); +e.callback=nil; +end +destroy_request(e); +end +if not o then +a(i); +return; +end +local function o(t) +if e.callback then +e.callback(t.body,t.code,t,e); +e.callback=nil; +end +destroy_request(e); +end +local function t() +return e; +end +e.parser=d(o,a,"client",t); +end +e.parser:feed(o); +end +local function f(e)r("error","Traceback[http]: %s",s(c(e),2));end +function request(e,t,u) +local e=l.parse(e); +if not(e and e.host)then +u(nil,0,e); +return nil,"invalid-url"; +end +if not e.path then +e.path="/"; +end +local d,o,s; +local l,a=e.host,e.port; +local h=l; +if(a=="80"and e.scheme=="http") +or(a=="443"and e.scheme=="https")then +a=nil; +elseif a then +h=h..":"..a; +end +o={ +["Host"]=h; +["User-Agent"]="Prosody XMPP Server"; +}; +if e.userinfo then +o["Authorization"]="Basic "..y(e.userinfo); +end +if t then +e.onlystatus=t.onlystatus; +s=t.body; +if s then +d="POST"; +o["Content-Length"]=c(#s); +o["Content-Type"]="application/x-www-form-urlencoded"; +end +if t.method then d=t.method;end +if t.headers then +for t,e in m(t.headers)do +o[t]=e; +end +end +end +e.method,e.headers,e.body=d,o,s; +local o=e.scheme=="https"; +if o and not j then +p("SSL not available, unable to contact https URL"); +end +local h=a and k(a)or(o and 443 or 80); +local a=b.tcp(); +a:settimeout(10); +local d,s=a:connect(l,h); +if not d and s~="timeout"then +u(nil,0,e); +return nil,s; +end +local s=false; +if o then +s=t and t.sslctx or{mode="client",protocol="sslv23",options={"no_sslv2","no_sslv3"}}; +end +e.handler,e.conn=v(x.wrapclient(a,l,h,n,"*a",s)); +e.write=function(...)return e.handler:write(...);end +e.callback=function(i,t,o,a)r("debug","Calling callback, status %s",t or"---");return g(2,q(function()return u(i,t,o,a)end,f));end +e.reader=w; +e.state="status"; +i[e.handler]=e; +return e; +end +function destroy_request(e) +if e.conn then +e.conn=nil; +e.handler:close() +end +end +local e,t=h.urlencode,h.urldecode; +local o,a=h.formencode,h.formdecode; +_M.urlencode,_M.urldecode=e,t; +_M.formencode,_M.formdecode=o,a; +return _M; +end) +package.preload['verse.bosh']=(function(...) +local n=require"util.xmppstream".new; +local a=require"util.stanza"; +require"net.httpclient_listener"; +local i=require"net.http"; +local e=setmetatable({},{__index=verse.stream_mt}); +e.__index=e; +local s="http://etherx.jabber.org/streams"; +local h="http://jabber.org/protocol/httpbind"; +local o=5; +function verse.new_bosh(a,t) +local t={ +bosh_conn_pool={}; +bosh_waiting_requests={}; +bosh_rid=math.random(1,999999); +bosh_outgoing_buffer={}; +bosh_url=t; +conn={}; +}; +function t:reopen() +self.bosh_need_restart=true; +self:flush(); +end +local t=verse.new(a,t); +return setmetatable(t,e); +end +function e:connect() +self:_send_session_request(); +end +function e:send(e) +self:debug("Putting into BOSH send buffer: %s",tostring(e)); +self.bosh_outgoing_buffer[#self.bosh_outgoing_buffer+1]=a.clone(e); +self:flush(); +end +function e:flush() +if self.connected +and#self.bosh_waiting_requests0 +or self.bosh_need_restart)then +self:debug("Flushing..."); +local t=self:_make_body(); +local e=self.bosh_outgoing_buffer; +for o,a in ipairs(e)do +t:add_child(a); +e[o]=nil; +end +self:_make_request(t); +else +self:debug("Decided not to flush."); +end +end +function e:_make_request(a) +local e,t=i.request(self.bosh_url,{body=tostring(a)},function(i,e,t) +if e~=0 then +self.inactive_since=nil; +return self:_handle_response(i,e,t); +end +local e=os.time(); +if not self.inactive_since then +self.inactive_since=e; +elseif e-self.inactive_since>self.bosh_max_inactivity then +return self:_disconnected(); +else +self:debug("%d seconds left to reconnect, retrying in %d seconds...", +self.bosh_max_inactivity-(e-self.inactive_since),o); +end +timer.add_task(o,function() +self:debug("Retrying request..."); +for a,e in ipairs(self.bosh_waiting_requests)do +if e==t then +table.remove(self.bosh_waiting_requests,a); +break; +end +end +self:_make_request(a); +end); +end); +if e then +table.insert(self.bosh_waiting_requests,e); +else +self:warn("Request failed instantly: %s",t); +end +end +function e:_disconnected() +self.connected=nil; +self:event("disconnected"); +end +function e:_send_session_request() +local e=self:_make_body(); +e.attr.hold="1"; +e.attr.wait="60"; +e.attr["xml:lang"]="en"; +e.attr.ver="1.6"; +e.attr.from=self.jid; +e.attr.to=self.host; +e.attr.secure='true'; +i.request(self.bosh_url,{body=tostring(e)},function(t,e) +if e==0 then +return self:_disconnected(); +end +local e=self:_parse_response(t) +if not e then +self:warn("Invalid session creation response"); +self:_disconnected(); +return; +end +self.bosh_sid=e.attr.sid; +self.bosh_wait=tonumber(e.attr.wait); +self.bosh_hold=tonumber(e.attr.hold); +self.bosh_max_inactivity=tonumber(e.attr.inactivity); +self.bosh_max_requests=tonumber(e.attr.requests)or self.bosh_hold; +self.connected=true; +self:event("connected"); +self:_handle_response_payload(e); +end); +end +function e:_handle_response(t,a,e) +if self.bosh_waiting_requests[1]~=e then +self:warn("Server replied to request that wasn't the oldest"); +for a,t in ipairs(self.bosh_waiting_requests)do +if t==e then +self.bosh_waiting_requests[a]=nil; +break; +end +end +else +table.remove(self.bosh_waiting_requests,1); +end +local e=self:_parse_response(t); +if e then +self:_handle_response_payload(e); +end +self:flush(); +end +function e:_handle_response_payload(t) +local e=t.tags; +for t=1,#e do +local e=e[t]; +if e.attr.xmlns==s then +self:event("stream-"..e.name,e); +elseif e.attr.xmlns then +self:event("stream/"..e.attr.xmlns,e); +else +self:event("stanza",e); +end +end +if t.attr.type=="terminate"then +self:_disconnected({reason=t.attr.condition}); +end +end +local a={ +stream_ns="http://jabber.org/protocol/httpbind",stream_tag="body", +default_ns="jabber:client", +streamopened=function(e,t)e.notopen=nil;e.payload=verse.stanza("body",t);return true;end; +handlestanza=function(t,e)t.payload:add_child(e);end; +}; +function e:_parse_response(e) +self:debug("Parsing response: %s",e); +if e==nil then +self:debug("%s",debug.traceback()); +self:_disconnected(); +return; +end +local t={notopen=true,stream=self}; +local a=n(t,a); +a:feed(e); +return t.payload; +end +function e:_make_body() +self.bosh_rid=self.bosh_rid+1; +local e=verse.stanza("body",{ +xmlns=h; +content="text/xml; charset=utf-8"; +sid=self.bosh_sid; +rid=self.bosh_rid; +}); +if self.bosh_need_restart then +self.bosh_need_restart=nil; +e.attr.restart='true'; +end +return e; +end +end) +package.preload['verse.client']=(function(...) +local t=require"verse"; +local o=t.stream_mt; +local s=require"util.jid".split; +local h=require"net.adns"; +local e=require"lxp"; +local a=require"util.stanza"; +t.message,t.presence,t.iq,t.stanza,t.reply,t.error_reply= +a.message,a.presence,a.iq,a.stanza,a.reply,a.error_reply; +local d=require"util.xmppstream".new; +local n="http://etherx.jabber.org/streams"; +local function r(e,t) +return e.priorityt.weight); +end +local i={ +stream_ns=n, +stream_tag="stream", +default_ns="jabber:client"}; +function i.streamopened(e,t) +e.stream_id=t.id; +if not e:event("opened",t)then +e.notopen=nil; +end +return true; +end +function i.streamclosed(e) +e.notopen=true; +if not e.closed then +e:send(""); +e.closed=true; +end +e:event("closed"); +return e:close("stream closed") +end +function i.handlestanza(t,e) +if e.attr.xmlns==n then +return t:event("stream-"..e.name,e); +elseif e.attr.xmlns then +return t:event("stream/"..e.attr.xmlns,e); +end +return t:event("stanza",e); +end +function i.error(a,t,e) +if a:event(t,e)==nil then +if e then +local t=e:get_child(nil,"urn:ietf:params:xml:ns:xmpp-streams"); +local e=e:get_child_text("text","urn:ietf:params:xml:ns:xmpp-streams"); +error(t.name..(e and": "..e or"")); +else +error(e and e.name or t or"unknown-error"); +end +end +end +function o:reset() +if self.stream then +self.stream:reset(); +else +self.stream=d(self,i); +end +self.notopen=true; +return true; +end +function o:connect_client(e,a) +self.jid,self.password=e,a; +self.username,self.host,self.resource=s(e); +self:add_plugin("tls"); +self:add_plugin("sasl"); +self:add_plugin("bind"); +self:add_plugin("session"); +function self.data(t,e) +local a,t=self.stream:feed(e); +if a then return;end +self:debug("debug","Received invalid XML (%s) %d bytes: %s",tostring(t),#e,e:sub(1,300):gsub("[\r\n]+"," ")); +self:close("xml-not-well-formed"); +end +self:hook("connected",function()self:reopen();end); +self:hook("incoming-raw",function(e)return self.data(self.conn,e);end); +self.curr_id=0; +self.tracked_iqs={}; +self:hook("stanza",function(t) +local e,a=t.attr.id,t.attr.type; +if e and t.name=="iq"and(a=="result"or a=="error")and self.tracked_iqs[e]then +self.tracked_iqs[e](t); +self.tracked_iqs[e]=nil; +return true; +end +end); +self:hook("stanza",function(e) +local a; +if e.attr.xmlns==nil or e.attr.xmlns=="jabber:client"then +if e.name=="iq"and(e.attr.type=="get"or e.attr.type=="set")then +local o=e.tags[1]and e.tags[1].attr.xmlns; +if o then +a=self:event("iq/"..o,e); +if not a then +a=self:event("iq",e); +end +end +if a==nil then +self:send(t.error_reply(e,"cancel","service-unavailable")); +return true; +end +else +a=self:event(e.name,e); +end +end +return a; +end,-1); +self:hook("outgoing",function(e) +if e.name then +self:event("stanza-out",e); +end +end); +self:hook("stanza-out",function(e) +if not e.attr.xmlns then +self:event(e.name.."-out",e); +end +end); +local function e() +self:event("ready"); +end +self:hook("session-success",e,-1) +self:hook("bind-success",e,-1); +local e=self.close; +function self:close(t) +self.close=e; +if not self.closed then +self:send(""); +self.closed=true; +else +return self:close(t); +end +end +local function t() +self:connect(self.connect_host or self.host,self.connect_port or 5222); +end +if not(self.connect_host or self.connect_port)then +h.lookup(function(a) +if a then +local e={}; +self.srv_hosts=e; +for a,t in ipairs(a)do +table.insert(e,t.srv); +end +table.sort(e,r); +local a=e[1]; +self.srv_choice=1; +if a then +self.connect_host,self.connect_port=a.target,a.port; +self:debug("Best record found, will connect to %s:%d",self.connect_host or self.host,self.connect_port or 5222); +end +self:hook("disconnected",function() +if self.srv_hosts and self.srv_choice<#self.srv_hosts then +self.srv_choice=self.srv_choice+1; +local e=e[self.srv_choice]; +self.connect_host,self.connect_port=e.target,e.port; +t(); +return true; +end +end,1e3); +self:hook("connected",function() +self.srv_hosts=nil; +end,1e3); +end +t(); +end,"_xmpp-client._tcp."..(self.host)..".","SRV"); +else +t(); +end +end +function o:reopen() +self:reset(); +self:send(a.stanza("stream:stream",{to=self.host,["xmlns:stream"]='http://etherx.jabber.org/streams', +xmlns="jabber:client",version="1.0"}):top_tag()); +end +function o:send_iq(t,a) +local e=self:new_id(); +self.tracked_iqs[e]=a; +t.attr.id=e; +self:send(t); +end +function o:new_id() +self.curr_id=self.curr_id+1; +return tostring(self.curr_id); +end +end) +package.preload['verse.component']=(function(...) +local o=require"verse"; +local t=o.stream_mt; +local h=require"util.jid".split; +local e=require"lxp"; +local a=require"util.stanza"; +local r=require"util.sha1".sha1; +o.message,o.presence,o.iq,o.stanza,o.reply,o.error_reply= +a.message,a.presence,a.iq,a.stanza,a.reply,a.error_reply; +local d=require"util.xmppstream".new; +local s="http://etherx.jabber.org/streams"; +local i="jabber:component:accept"; +local n={ +stream_ns=s, +stream_tag="stream", +default_ns=i}; +function n.streamopened(e,t) +e.stream_id=t.id; +if not e:event("opened",t)then +e.notopen=nil; +end +return true; +end +function n.streamclosed(e) +return e:event("closed"); +end +function n.handlestanza(t,e) +if e.attr.xmlns==s then +return t:event("stream-"..e.name,e); +elseif e.attr.xmlns or e.name=="handshake"then +return t:event("stream/"..(e.attr.xmlns or i),e); +end +return t:event("stanza",e); +end +function t:reset() +if self.stream then +self.stream:reset(); +else +self.stream=d(self,n); +end +self.notopen=true; +return true; +end +function t:connect_component(e,n) +self.jid,self.password=e,n; +self.username,self.host,self.resource=h(e); +function self.data(a,e) +local o,a=self.stream:feed(e); +if o then return;end +t:debug("debug","Received invalid XML (%s) %d bytes: %s",tostring(a),#e,e:sub(1,300):gsub("[\r\n]+"," ")); +t:close("xml-not-well-formed"); +end +self:hook("incoming-raw",function(e)return self.data(self.conn,e);end); +self.curr_id=0; +self.tracked_iqs={}; +self:hook("stanza",function(t) +local e,a=t.attr.id,t.attr.type; +if e and t.name=="iq"and(a=="result"or a=="error")and self.tracked_iqs[e]then +self.tracked_iqs[e](t); +self.tracked_iqs[e]=nil; +return true; +end +end); +self:hook("stanza",function(e) +local t; +if e.attr.xmlns==nil or e.attr.xmlns=="jabber:client"then +if e.name=="iq"and(e.attr.type=="get"or e.attr.type=="set")then +local a=e.tags[1]and e.tags[1].attr.xmlns; +if a then +t=self:event("iq/"..a,e); +if not t then +t=self:event("iq",e); +end +end +if t==nil then +self:send(o.error_reply(e,"cancel","service-unavailable")); +return true; +end +else +t=self:event(e.name,e); +end +end +return t; +end,-1); +self:hook("opened",function(e) +print(self.jid,self.stream_id,e.id); +local e=r(self.stream_id..n,true); +self:send(a.stanza("handshake",{xmlns=i}):text(e)); +self:hook("stream/"..i,function(e) +if e.name=="handshake"then +self:event("authentication-success"); +end +end); +end); +local function e() +self:event("ready"); +end +self:hook("authentication-success",e,-1); +self:connect(self.connect_host or self.host,self.connect_port or 5347); +self:reopen(); +end +function t:reopen() +self:reset(); +self:send(a.stanza("stream:stream",{to=self.jid,["xmlns:stream"]='http://etherx.jabber.org/streams', +xmlns=i,version="1.0"}):top_tag()); +end +function t:close(t) +if not self.notopen then +self:send(""); +end +local e=self.conn.disconnect(); +self.conn:close(); +e(conn,t); +end +function t:send_iq(e,a) +local t=self:new_id(); +self.tracked_iqs[t]=a; +e.attr.id=t; +self:send(e); +end +function t:new_id() +self.curr_id=self.curr_id+1; +return tostring(self.curr_id); +end +end) +pcall(require,"luarocks.require"); +local h=require"socket"; +pcall(require,"ssl"); +local a=require"net.server"; +local n=require"util.events"; +local o=require"util.logger"; +module("verse",package.seeall); +local e=_M; +_M.server=a; +local t={}; +t.__index=t; +stream_mt=t; +e.plugins={}; +function e.init(...) +for e=1,select("#",...)do +local t=pcall(require,"verse."..select(e,...)); +if not t then +error("Verse connection module not found: verse."..select(e,...)); +end +end +return e; +end +local i=0; +function e.new(o,a) +local t=setmetatable(a or{},t); +i=i+1; +t.id=tostring(i); +t.logger=o or e.new_logger("stream"..t.id); +t.events=n.new(); +t.plugins={}; +t.verse=e; +return t; +end +e.add_task=require"util.timer".add_task; +e.logger=o.init; +e.new_logger=o.init; +e.log=e.logger("verse"); +local function s(o,...) +local e,a,t=0,{...},select('#',...); +return(o:gsub("%%(.)",function(o)if e<=t then e=e+1;return tostring(a[e]);end end)); +end +function e.set_log_handler(e,t) +t=t or{"debug","info","warn","error"}; +o.reset(); +if io.type(e)=="file"then +local t=e; +function e(e,a,o) +t:write(e,"\t",a,"\t",o,"\n"); +end +end +if e then +local function i(a,o,t,...) +return e(a,o,s(t,...)); +end +for t,e in ipairs(t)do +o.add_level_sink(e,i); +end +end +end +function _default_log_handler(a,t,o) +return io.stderr:write(a,"\t",t,"\t",o,"\n"); +end +e.set_log_handler(_default_log_handler,{"error"}); +local function o(t) +e.log("error","Error: %s",t); +e.log("error","Traceback: %s",debug.traceback()); +end +function e.set_error_handler(e) +o=e; +end +function e.loop() +return xpcall(a.loop,o); +end +function e.step() +return xpcall(a.step,o); +end +function e.quit() +return a.setquitting(true); +end +function t:listen(t,e) +t=t or"localhost"; +e=e or 0; +local a,o=a.addserver(t,e,new_listener(self,"server"),"*a"); +if a then +self:debug("Bound to %s:%s",t,e); +self.server=a; +end +return a,o; +end +function t:connect(t,o) +t=t or"localhost"; +o=tonumber(o)or 5222; +local i=h.tcp() +i:settimeout(0); +local n,e=i:connect(t,o); +if not n and e~="timeout"then +self:warn("connect() to %s:%d failed: %s",t,o,e); +return self:event("disconnected",{reason=e})or false,e; +end +local t=a.wrapclient(i,t,o,new_listener(self),"*a"); +if not t then +self:warn("connection initialisation failed: %s",e); +return self:event("disconnected",{reason=e})or false,e; +end +self:set_conn(t); +return true; +end +function t:set_conn(t) +self.conn=t; +self.send=function(a,e) +self:event("outgoing",e); +e=tostring(e); +self:event("outgoing-raw",e); +return t:write(e); +end; +end +function t:close(t) +if not self.conn then +e.log("error","Attempt to close disconnected connection - possibly a bug"); +return; +end +local e=self.conn.disconnect(); +self.conn:close(); +e(self.conn,t); +end +function t:debug(...) +return self.logger("debug",...); +end +function t:info(...) +return self.logger("info",...); +end +function t:warn(...) +return self.logger("warn",...); +end +function t:error(...) +return self.logger("error",...); +end +function t:event(e,...) +self:debug("Firing event: "..tostring(e)); +return self.events.fire_event(e,...); +end +function t:hook(e,...) +return self.events.add_handler(e,...); +end +function t:unhook(t,e) +return self.events.remove_handler(t,e); +end +function e.eventable(e) +e.events=n.new(); +e.hook,e.unhook=t.hook,t.unhook; +local t=e.events.fire_event; +function e:event(e,...) +return t(e,...); +end +return e; +end +function t:add_plugin(t) +if self.plugins[t]then return true;end +if require("verse.plugins."..t)then +local e,a=e.plugins[t](self); +if e~=false then +self:debug("Loaded %s plugin",t); +self.plugins[t]=true; +else +self:warn("Failed to load %s plugin: %s",t,a); +end +end +return self; +end +function new_listener(t) +local a={}; +function a.onconnect(a) +if t.server then +local e=e.new(); +a:setlistener(new_listener(e)); +e:set_conn(a); +t:event("connected",{client=e}); +else +t.connected=true; +t:event("connected"); +end +end +function a.onincoming(a,e) +t:event("incoming-raw",e); +end +function a.ondisconnect(e,a) +if e~=t.conn then return end +t.connected=false; +t:event("disconnected",{reason=a}); +end +function a.ondrain(e) +t:event("drained"); +end +function a.onstatus(a,e) +t:event("status",e); +end +return a; +end +return e;