lua_xmpp_privacy_bot/verse.lua

11231 lines
307 KiB
Lua
Raw Normal View History

2024-06-01 16:46:19 -05:00
package.preload['util.encodings'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
local function not_impl()
error("Function not implemented");
end
local mime = require "mime";
module "encodings"
idna = {};
stringprep = {};
base64 = { encode = mime.b64, decode = mime.unb64 };
utf8 = {
valid = (utf8 and utf8.len) and function (s) return not not utf8.len(s); end or function () return true; end;
};
return _M;
end)
package.preload['util.hashes'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
local function not_available(_, method_name)
error("Hash method "..method_name.." not available", 2);
end
local _M = setmetatable({}, { __index = not_available });
local function with(mod, f)
local ok, pkg = pcall(require, mod);
if ok then f(pkg); end
end
with("bgcrypto.md5", function (md5)
_M.md5 = md5.digest;
_M.hmac_md5 = md5.hmac.digest;
end);
with("bgcrypto.sha1", function (sha1)
_M.sha1 = sha1.digest;
_M.hmac_sha1 = sha1.hmac.digest;
_M.scram_Hi_sha1 = function (p, s, i) return sha1.pbkdf2(p, s, i, 20); end;
end);
with("bgcrypto.sha256", function (sha256)
_M.sha256 = sha256.digest;
_M.hmac_sha256 = sha256.hmac.digest;
end);
with("bgcrypto.sha512", function (sha512)
_M.sha512 = sha512.digest;
_M.hmac_sha512 = sha512.hmac.digest;
end);
with("sha1", function (sha1)
_M.sha1 = function (data, hex)
if hex then
return sha1.sha1(data);
else
return (sha1.binary(data));
end
end;
end);
return _M;
end)
package.preload['lib.adhoc'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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 = {};
local 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);
local cmdtag;
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;
end)
package.preload['util.stanza'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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 t_insert = table.insert;
local t_remove = table.remove;
local t_concat = table.concat;
local s_format = string.format;
local s_match = string.match;
local tostring = tostring;
local setmetatable = setmetatable;
local getmetatable = getmetatable;
local pairs = pairs;
local ipairs = ipairs;
local type = type;
local s_gsub = string.gsub;
local s_sub = string.sub;
local s_find = string.find;
local os = os;
local do_pretty_printing = not os.getenv("WINDIR");
local getstyle, getstring;
if do_pretty_printing then
local ok, termcolours = pcall(require, "util.termcolours");
if ok then
getstyle, getstring = termcolours.getstyle, termcolours.getstring;
else
do_pretty_printing = nil;
end
end
local xmlns_stanzas = "urn:ietf:params:xml:ns:xmpp-stanzas";
local _ENV = nil;
local stanza_mt = { __type = "stanza" };
stanza_mt.__index = stanza_mt;
local function new_stanza(name, attr)
local stanza = { name = name, attr = attr or {}, tags = {} };
return setmetatable(stanza, stanza_mt);
end
local function is_stanza(s)
return getmetatable(s) == stanza_mt;
end
function stanza_mt:query(xmlns)
return self:tag("query", { xmlns = xmlns });
end
function stanza_mt:body(text, attr)
return self:tag("body", attr):text(text);
end
function stanza_mt:tag(name, attrs)
local s = new_stanza(name, attrs);
local last_add = self.last_add;
if not last_add then last_add = {}; self.last_add = last_add; end
(last_add[#last_add] or self):add_direct_child(s);
t_insert(last_add, s);
return self;
end
function stanza_mt:text(text)
local last_add = self.last_add;
(last_add and last_add[#last_add] or self):add_direct_child(text);
return self;
end
function stanza_mt:up()
local last_add = self.last_add;
if last_add then t_remove(last_add); end
return self;
end
function stanza_mt:reset()
self.last_add = nil;
return self;
end
function stanza_mt:add_direct_child(child)
if type(child) == "table" then
t_insert(self.tags, child);
end
t_insert(self, child);
end
function stanza_mt:add_child(child)
local last_add = self.last_add;
(last_add and last_add[#last_add] or self):add_direct_child(child);
return self;
end
function stanza_mt:remove_children(name, xmlns)
xmlns = xmlns or self.attr.xmlns;
return self:maptags(function (tag)
if (not name or tag.name == name) and tag.attr.xmlns == xmlns then
return nil;
end
return tag;
end);
end
function stanza_mt:get_child(name, xmlns)
for _, child in ipairs(self.tags) do
if (not name or child.name == name)
and ((not xmlns and self.attr.xmlns == child.attr.xmlns)
or child.attr.xmlns == xmlns) then
return child;
end
end
end
function stanza_mt:get_child_text(name, xmlns)
local tag = self:get_child(name, xmlns);
if tag then
return tag:get_text();
end
return nil;
end
function stanza_mt:child_with_name(name)
for _, child in ipairs(self.tags) do
if child.name == name then return child; end
end
end
function stanza_mt:child_with_ns(ns)
for _, child in ipairs(self.tags) do
if child.attr.xmlns == ns then return child; end
end
end
function stanza_mt:children()
local i = 0;
return function (a)
i = i + 1
return a[i];
end, self, i;
end
function stanza_mt:childtags(name, xmlns)
local tags = self.tags;
local start_i, max_i = 1, #tags;
return function ()
for i = start_i, max_i do
local v = tags[i];
if (not name or v.name == name)
and ((not xmlns and self.attr.xmlns == v.attr.xmlns)
or v.attr.xmlns == xmlns) then
start_i = i+1;
return v;
end
end
end;
end
function stanza_mt:maptags(callback)
local tags, curr_tag = self.tags, 1;
local n_children, n_tags = #self, #tags;
local i = 1;
while curr_tag <= n_tags and n_tags > 0 do
if self[i] == tags[curr_tag] then
local ret = callback(self[i]);
if ret == nil then
t_remove(self, i);
t_remove(tags, curr_tag);
n_children = n_children - 1;
n_tags = n_tags - 1;
i = i - 1;
curr_tag = curr_tag - 1;
else
self[i] = ret;
tags[curr_tag] = ret;
end
curr_tag = curr_tag + 1;
end
i = i + 1;
end
return self;
end
function stanza_mt:find(path)
local pos = 1;
local len = #path + 1;
repeat
local xmlns, name, text;
local char = s_sub(path, pos, pos);
if char == "@" then
return self.attr[s_sub(path, pos + 1)];
elseif char == "{" then
xmlns, pos = s_match(path, "^([^}]+)}()", pos + 1);
end
name, text, pos = s_match(path, "^([^@/#]*)([/#]?)()", pos);
name = name ~= "" and name or nil;
if pos == len then
if text == "#" then
return self:get_child_text(name, xmlns);
end
return self:get_child(name, xmlns);
end
self = self:get_child(name, xmlns);
until not self
end
local escape_table = { ["'"] = "&apos;", ["\""] = "&quot;", ["<"] = "&lt;", [">"] = "&gt;", ["&"] = "&amp;" };
local function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end
local function _dostring(t, buf, self, _xml_escape, parentns)
local nsid = 0;
local name = t.name
t_insert(buf, "<"..name);
for k, v in pairs(t.attr) do
if s_find(k, "\1", 1, true) then
local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$");
nsid = nsid + 1;
t_insert(buf, " xmlns:ns"..nsid.."='".._xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='".._xml_escape(v).."'");
elseif not(k == "xmlns" and v == parentns) then
t_insert(buf, " "..k.."='".._xml_escape(v).."'");
end
end
local len = #t;
if len == 0 then
t_insert(buf, "/>");
else
t_insert(buf, ">");
for n=1,len do
local child = t[n];
if child.name then
self(child, buf, self, _xml_escape, t.attr.xmlns);
else
t_insert(buf, _xml_escape(child));
end
end
t_insert(buf, "</"..name..">");
end
end
function stanza_mt.__tostring(t)
local buf = {};
_dostring(t, buf, _dostring, xml_escape, nil);
return t_concat(buf);
end
function stanza_mt.top_tag(t)
local attr_string = "";
if t.attr then
for k, v in pairs(t.attr) do if type(k) == "string" then attr_string = attr_string .. s_format(" %s='%s'", k, xml_escape(tostring(v))); end end
end
return s_format("<%s%s>", t.name, attr_string);
end
function stanza_mt.get_text(t)
if #t.tags == 0 then
return t_concat(t);
end
end
function stanza_mt.get_error(stanza)
local error_type, condition, text;
local error_tag = stanza:get_child("error");
if not error_tag then
return nil, nil, nil;
end
error_type = error_tag.attr.type;
for _, child in ipairs(error_tag.tags) do
if child.attr.xmlns == xmlns_stanzas then
if not text and child.name == "text" then
text = child:get_text();
elseif not condition then
condition = child.name;
end
if condition and text then
break;
end
end
end
return error_type, condition or "undefined-condition", text;
end
local id = 0;
local function new_id()
id = id + 1;
return "lx"..id;
end
local function preserialize(stanza)
local s = { name = stanza.name, attr = stanza.attr };
for _, child in ipairs(stanza) do
if type(child) == "table" then
t_insert(s, preserialize(child));
else
t_insert(s, child);
end
end
return s;
end
local function deserialize(stanza)
-- Set metatable
if stanza then
local attr = stanza.attr;
for i=1,#attr do attr[i] = nil; end
local attrx = {};
for att in pairs(attr) do
if s_find(att, "|", 1, true) and not s_find(att, "\1", 1, true) then
local ns,na = s_match(att, "^([^|]+)|(.+)$");
attrx[ns.."\1"..na] = attr[att];
attr[att] = nil;
end
end
for a,v in pairs(attrx) do
attr[a] = v;
end
setmetatable(stanza, stanza_mt);
for _, child in ipairs(stanza) do
if type(child) == "table" then
deserialize(child);
end
end
if not stanza.tags then
-- Rebuild tags
local tags = {};
for _, child in ipairs(stanza) do
if type(child) == "table" then
t_insert(tags, child);
end
end
stanza.tags = tags;
end
end
return stanza;
end
local function clone(stanza)
local attr, tags = {}, {};
for k,v in pairs(stanza.attr) do attr[k] = v; end
local new = { name = stanza.name, attr = attr, tags = tags };
for i=1,#stanza do
local child = stanza[i];
if child.name then
child = clone(child);
t_insert(tags, child);
end
t_insert(new, child);
end
return setmetatable(new, stanza_mt);
end
local function message(attr, body)
if not body then
return new_stanza("message", attr);
else
return new_stanza("message", attr):tag("body"):text(body):up();
end
end
local function iq(attr)
if attr and not attr.id then attr.id = new_id(); end
return new_stanza("iq", attr or { id = new_id() });
end
local function reply(orig)
return new_stanza(orig.name, orig.attr and { to = orig.attr.from, from = orig.attr.to, id = orig.attr.id, type = ((orig.name == "iq" and "result") or orig.attr.type) });
end
local xmpp_stanzas_attr = { xmlns = xmlns_stanzas };
local function error_reply(orig, error_type, condition, error_message)
local t = reply(orig);
t.attr.type = "error";
t:tag("error", {type = error_type}) --COMPAT: Some day xmlns:stanzas goes here
:tag(condition, xmpp_stanzas_attr):up();
if error_message then t:tag("text", xmpp_stanzas_attr):text(error_message):up(); end
return t; -- stanza ready for adding app-specific errors
end
local function presence(attr)
return new_stanza("presence", attr);
end
if do_pretty_printing then
local style_attrk = getstyle("yellow");
local style_attrv = getstyle("red");
local style_tagname = getstyle("red");
local style_punc = getstyle("magenta");
local attr_format = " "..getstring(style_attrk, "%s")..getstring(style_punc, "=")..getstring(style_attrv, "'%s'");
local top_tag_format = getstring(style_punc, "<")..getstring(style_tagname, "%s").."%s"..getstring(style_punc, ">");
--local tag_format = getstring(style_punc, "<")..getstring(style_tagname, "%s").."%s"..getstring(style_punc, ">").."%s"..getstring(style_punc, "</")..getstring(style_tagname, "%s")..getstring(style_punc, ">");
local tag_format = top_tag_format.."%s"..getstring(style_punc, "</")..getstring(style_tagname, "%s")..getstring(style_punc, ">");
function stanza_mt.pretty_print(t)
local children_text = "";
for _, child in ipairs(t) do
if type(child) == "string" then
children_text = children_text .. xml_escape(child);
else
children_text = children_text .. child:pretty_print();
end
end
local attr_string = "";
if t.attr then
for k, v in pairs(t.attr) do if type(k) == "string" then attr_string = attr_string .. s_format(attr_format, k, tostring(v)); end end
end
return s_format(tag_format, t.name, attr_string, children_text, t.name);
end
function stanza_mt.pretty_top_tag(t)
local attr_string = "";
if t.attr then
for k, v in pairs(t.attr) do if type(k) == "string" then attr_string = attr_string .. s_format(attr_format, k, tostring(v)); end end
end
return s_format(top_tag_format, t.name, attr_string);
end
else
-- Sorry, fresh out of colours for you guys ;)
stanza_mt.pretty_print = stanza_mt.__tostring;
stanza_mt.pretty_top_tag = stanza_mt.top_tag;
end
return {
stanza_mt = stanza_mt;
stanza = new_stanza;
is_stanza = is_stanza;
new_id = new_id;
preserialize = preserialize;
deserialize = deserialize;
clone = clone;
message = message;
iq = iq;
reply = reply;
error_reply = error_reply;
presence = presence;
xml_escape = xml_escape;
};
end)
package.preload['util.timer'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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 server = require "net.server";
local math_min = math.min
local math_huge = math.huge
local get_time = require "util.time".now
local t_insert = table.insert;
local pairs = pairs;
local type = type;
local data = {};
local new_data = {};
local _ENV = nil;
local _add_task;
if not server.event then
function _add_task(delay, callback)
local current_time = get_time();
delay = delay + current_time;
if delay >= current_time then
t_insert(new_data, {delay, callback});
else
local r = callback(current_time);
if r and type(r) == "number" then
return _add_task(r, callback);
end
end
end
server._addtimer(function()
local current_time = get_time();
if #new_data > 0 then
for _, d in pairs(new_data) do
t_insert(data, d);
end
new_data = {};
end
local next_time = math_huge;
for i, d in pairs(data) do
local t, callback = d[1], d[2];
if t <= current_time then
data[i] = nil;
local r = callback(current_time);
if type(r) == "number" then
_add_task(r, callback);
next_time = math_min(next_time, r);
end
else
next_time = math_min(next_time, t - current_time);
end
end
return next_time;
end);
else
local event = server.event;
local event_base = server.event_base;
local EVENT_LEAVE = (event.core and event.core.LEAVE) or -1;
function _add_task(delay, callback)
local event_handle;
event_handle = event_base:addevent(nil, 0, function ()
local ret = callback(get_time());
if ret then
return 0, ret;
elseif event_handle then
return EVENT_LEAVE;
end
end
, delay);
end
end
return {
add_task = _add_task;
};
end)
package.preload['util.termcolours'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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.
--
--
-- luacheck: ignore 213/i
local t_concat, t_insert = table.concat, table.insert;
local char, format = string.char, string.format;
local tonumber = tonumber;
local ipairs = ipairs;
local io_write = io.write;
local m_floor = math.floor;
local type = type;
local setmetatable = setmetatable;
local pairs = pairs;
local windows;
if os.getenv("WINDIR") then
windows = require "util.windows";
end
local orig_color = windows and windows.get_consolecolor and windows.get_consolecolor();
local _ENV = nil;
local stylemap = {
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 winstylemap = {
["0"] = orig_color, -- reset
["1"] = 7+8, -- bold
["1;33"] = 2+4+8, -- bold yellow
["1;31"] = 4+8 -- bold red
}
local cssmap = {
[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 fmt_string = char(0x1B).."[%sm%s"..char(0x1B).."[0m";
local function getstring(style, text)
if style then
return format(fmt_string, style, text);
else
return text;
end
end
local function gray(n)
return m_floor(n*3/32)+0xe8;
end
local function color(r,g,b)
if r == g and g == b then
return gray(r);
end
r = m_floor(r*3/128);
g = m_floor(g*3/128);
b = m_floor(b*3/128);
return 0x10 + ( r * 36 ) + ( g * 6 ) + ( b );
end
local function hex2rgb(hex)
local r = tonumber(hex:sub(1,2),16);
local g = tonumber(hex:sub(3,4),16);
local b = tonumber(hex:sub(5,6),16);
return r,g,b;
end
setmetatable(stylemap, { __index = function(_, style)
if type(style) == "string" and style:find("%x%x%x%x%x%x") == 1 then
local g = style:sub(7) == " background" and "48;5;" or "38;5;";
return g .. color(hex2rgb(style));
end
end } );
local csscolors = {
red = "ff0000"; fuchsia = "ff00ff"; green = "008000"; white = "ffffff";
lime = "00ff00"; yellow = "ffff00"; purple = "800080"; blue = "0000ff";
aqua = "00ffff"; olive = "808000"; black = "000000"; navy = "000080";
teal = "008080"; silver = "c0c0c0"; maroon = "800000"; gray = "808080";
}
for colorname, rgb in pairs(csscolors) do
stylemap[colorname] = stylemap[colorname] or stylemap[rgb];
colorname, rgb = colorname .. " background", rgb .. " background"
stylemap[colorname] = stylemap[colorname] or stylemap[rgb];
end
local function getstyle(...)
local styles, result = { ... }, {};
for i, style in ipairs(styles) do
style = stylemap[style];
if style then
t_insert(result, style);
end
end
return t_concat(result, ";");
end
local last = "0";
local function setstyle(style)
style = style or "0";
if style ~= last then
io_write("\27["..style.."m");
last = style;
end
end
if windows then
function setstyle(style)
style = style or "0";
if style ~= last then
windows.set_consolecolor(winstylemap[style] or orig_color);
last = style;
end
end
if not orig_color then
function setstyle() end
end
end
local function ansi2css(ansi_codes)
if ansi_codes == "0" then return "</span>"; end
local css = {};
for code in ansi_codes:gmatch("[^;]+") do
t_insert(css, cssmap[tonumber(code)]);
end
return "</span><span style='"..t_concat(css, ";").."'>";
end
local function tohtml(input)
return input:gsub("\027%[(.-)m", ansi2css);
end
return {
getstring = getstring;
getstyle = getstyle;
setstyle = setstyle;
tohtml = tohtml;
};
end)
package.preload['util.uuid'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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 random = require "util.random";
local random_bytes = random.bytes;
local hex = require "util.hex".to;
local m_ceil = math.ceil;
local function get_nibbles(n)
return hex(random_bytes(m_ceil(n/2))):sub(1, n);
end
local function get_twobits()
return ("%x"):format(random_bytes(1):byte() % 4 + 8);
end
local function generate()
-- generate RFC 4122 complaint UUIDs (version 4 - random)
return get_nibbles(8).."-"..get_nibbles(4).."-4"..get_nibbles(3).."-"..(get_twobits())..get_nibbles(3).."-"..get_nibbles(12);
end
return {
get_nibbles=get_nibbles;
generate = generate ;
-- COMPAT
seed = random.seed;
};
end)
package.preload['net.dns'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- Prosody IM
-- This file is included with Prosody IM. It has modifications,
-- which are hereby placed in the public domain.
-- todo: quick (default) header generation
-- todo: nxdomain, error handling
-- todo: cache results of encodeName
-- reference: http://tools.ietf.org/html/rfc1035
-- reference: http://tools.ietf.org/html/rfc1876 (LOC)
local socket = require "socket";
local timer = require "util.timer";
local new_ip = require "util.ip".new_ip;
local _, windows = pcall(require, "util.windows");
local is_windows = (_ and windows) or os.getenv("WINDIR");
local coroutine, io, math, string, table =
coroutine, io, math, string, table;
local ipairs, next, pairs, print, setmetatable, tostring, assert, error, select, type =
ipairs, next, pairs, print, setmetatable, tostring, assert, error, select, type;
local ztact = { -- public domain 20080404 lua@ztact.com
get = function(parent, ...)
local len = select('#', ...);
for i=1,len do
parent = parent[select(i, ...)];
if parent == nil then break; end
end
return parent;
end;
set = function(parent, ...)
local len = select('#', ...);
local key, value = select(len-1, ...);
local cutpoint, cutkey;
for i=1,len-2 do
local key = select (i, ...)
local child = parent[key]
if value == nil then
if child == nil then
return;
elseif next(child, next(child)) then
cutpoint = nil; cutkey = nil;
elseif cutpoint == nil then
cutpoint = parent; cutkey = key;
end
elseif child == nil then
child = {};
parent[key] = child;
end
parent = child
end
if value == nil and cutpoint then
cutpoint[cutkey] = nil;
else
parent[key] = value;
return value;
end
end;
};
local get, set = ztact.get, ztact.set;
local default_timeout = 15;
-------------------------------------------------- module dns
local _ENV = nil;
local dns = {};
-- dns type & class codes ------------------------------ dns type & class codes
local append = table.insert
local function highbyte(i) -- - - - - - - - - - - - - - - - - - - highbyte
return (i-(i%0x100))/0x100;
end
local function augment (t, prefix) -- - - - - - - - - - - - - - - - - augment
local a = {};
for i,s in pairs(t) do
a[i] = s;
a[s] = s;
a[string.lower(s)] = s;
end
setmetatable(a, {
__index = function (_, i)
if type(i) == "number" then
return ("%s%d"):format(prefix, i);
elseif type(i) == "string" then
return i:upper();
end
end;
})
return a;
end
local function encode (t) -- - - - - - - - - - - - - - - - - - - - - encode
local code = {};
for i,s in pairs(t) do
local word = string.char(highbyte(i), i%0x100);
code[i] = word;
code[s] = word;
code[string.lower(s)] = word;
end
return code;
end
dns.types = {
'A', 'NS', 'MD', 'MF', 'CNAME', 'SOA', 'MB', 'MG', 'MR', 'NULL', 'WKS',
'PTR', 'HINFO', 'MINFO', 'MX', 'TXT',
[ 28] = 'AAAA', [ 29] = 'LOC', [ 33] = 'SRV',
[252] = 'AXFR', [253] = 'MAILB', [254] = 'MAILA', [255] = '*' };
dns.classes = { 'IN', 'CS', 'CH', 'HS', [255] = '*' };
dns.type = augment (dns.types, "TYPE");
dns.class = augment (dns.classes, "CLASS");
dns.typecode = encode (dns.types);
dns.classcode = encode (dns.classes);
local function standardize(qname, qtype, qclass) -- - - - - - - standardize
if string.byte(qname, -1) ~= 0x2E then qname = qname..'.'; end
qname = string.lower(qname);
return qname, dns.type[qtype or 'A'], dns.class[qclass or 'IN'];
end
local function prune(rrs, time, soft) -- - - - - - - - - - - - - - - prune
time = time or socket.gettime();
for i,rr in ipairs(rrs) do
if rr.tod then
if rr.tod < time then
rrs[rr[rr.type:lower()]] = nil;
table.remove(rrs, i);
return prune(rrs, time, soft); -- Re-iterate
end
elseif soft == 'soft' then -- What is this? I forget!
assert(rr.ttl == 0);
rrs[rr[rr.type:lower()]] = nil;
table.remove(rrs, i);
end
end
end
-- metatables & co. ------------------------------------------ metatables & co.
local resolver = {};
resolver.__index = resolver;
resolver.timeout = default_timeout;
local function default_rr_tostring(rr)
local rr_val = rr.type and rr[rr.type:lower()];
if type(rr_val) ~= "string" then
return "<UNKNOWN RDATA TYPE>";
end
return rr_val;
end
local special_tostrings = {
LOC = resolver.LOC_tostring;
MX = function (rr)
return string.format('%2i %s', rr.pref, rr.mx);
end;
SRV = function (rr)
local s = rr.srv;
return string.format('%5d %5d %5d %s', s.priority, s.weight, s.port, s.target);
end;
};
local rr_metatable = {}; -- - - - - - - - - - - - - - - - - - - rr_metatable
function rr_metatable.__tostring(rr)
local rr_string = (special_tostrings[rr.type] or default_rr_tostring)(rr);
return string.format('%2s %-5s %6i %-28s %s', rr.class, rr.type, rr.ttl, rr.name, rr_string);
end
local rrs_metatable = {}; -- - - - - - - - - - - - - - - - - - rrs_metatable
function rrs_metatable.__tostring(rrs)
local t = {};
for _, rr in ipairs(rrs) do
append(t, tostring(rr)..'\n');
end
return table.concat(t);
end
local cache_metatable = {}; -- - - - - - - - - - - - - - - - cache_metatable
function cache_metatable.__tostring(cache)
local time = socket.gettime();
local t = {};
for class,types in pairs(cache) do
for type,names in pairs(types) do
for name,rrs in pairs(names) do
prune(rrs, time);
append(t, tostring(rrs));
end
end
end
return table.concat(t);
end
-- packet layer -------------------------------------------------- packet layer
function dns.random(...) -- - - - - - - - - - - - - - - - - - - dns.random
math.randomseed(math.floor(10000*socket.gettime()) % 0x80000000);
dns.random = math.random;
return dns.random(...);
end
local function encodeHeader(o) -- - - - - - - - - - - - - - - encodeHeader
o = o or {};
o.id = o.id or dns.random(0, 0xffff); -- 16b (random) id
o.rd = o.rd or 1; -- 1b 1 recursion desired
o.tc = o.tc or 0; -- 1b 1 truncated response
o.aa = o.aa or 0; -- 1b 1 authoritative response
o.opcode = o.opcode or 0; -- 4b 0 query
-- 1 inverse query
-- 2 server status request
-- 3-15 reserved
o.qr = o.qr or 0; -- 1b 0 query, 1 response
o.rcode = o.rcode or 0; -- 4b 0 no error
-- 1 format error
-- 2 server failure
-- 3 name error
-- 4 not implemented
-- 5 refused
-- 6-15 reserved
o.z = o.z or 0; -- 3b 0 resvered
o.ra = o.ra or 0; -- 1b 1 recursion available
o.qdcount = o.qdcount or 1; -- 16b number of question RRs
o.ancount = o.ancount or 0; -- 16b number of answers RRs
o.nscount = o.nscount or 0; -- 16b number of nameservers RRs
o.arcount = o.arcount or 0; -- 16b number of additional RRs
-- string.char() rounds, so prevent roundup with -0.4999
local header = string.char(
highbyte(o.id), o.id %0x100,
o.rd + 2*o.tc + 4*o.aa + 8*o.opcode + 128*o.qr,
o.rcode + 16*o.z + 128*o.ra,
highbyte(o.qdcount), o.qdcount %0x100,
highbyte(o.ancount), o.ancount %0x100,
highbyte(o.nscount), o.nscount %0x100,
highbyte(o.arcount), o.arcount %0x100
);
return header, o.id;
end
local function encodeName(name) -- - - - - - - - - - - - - - - - encodeName
local t = {};
for part in string.gmatch(name, '[^.]+') do
append(t, string.char(string.len(part)));
append(t, part);
end
append(t, string.char(0));
return table.concat(t);
end
local function encodeQuestion(qname, qtype, qclass) -- - - - encodeQuestion
qname = encodeName(qname);
qtype = dns.typecode[qtype or 'a'];
qclass = dns.classcode[qclass or 'in'];
return qname..qtype..qclass;
end
function resolver:byte(len) -- - - - - - - - - - - - - - - - - - - - - byte
len = len or 1;
local offset = self.offset;
local last = offset + len - 1;
if last > #self.packet then
error(string.format('out of bounds: %i>%i', last, #self.packet));
end
self.offset = offset + len;
return string.byte(self.packet, offset, last);
end
function resolver:word() -- - - - - - - - - - - - - - - - - - - - - - word
local b1, b2 = self:byte(2);
return 0x100*b1 + b2;
end
function resolver:dword () -- - - - - - - - - - - - - - - - - - - - - dword
local b1, b2, b3, b4 = self:byte(4);
--print('dword', b1, b2, b3, b4);
return 0x1000000*b1 + 0x10000*b2 + 0x100*b3 + b4;
end
function resolver:sub(len) -- - - - - - - - - - - - - - - - - - - - - - sub
len = len or 1;
local s = string.sub(self.packet, self.offset, self.offset + len - 1);
self.offset = self.offset + len;
return s;
end
function resolver:header(force) -- - - - - - - - - - - - - - - - - - header
local id = self:word();
--print(string.format(':header id %x', id));
if not self.active[id] and not force then return nil; end
local h = { id = id };
local b1, b2 = self:byte(2);
h.rd = b1 %2;
h.tc = b1 /2%2;
h.aa = b1 /4%2;
h.opcode = b1 /8%16;
h.qr = b1 /128;
h.rcode = b2 %16;
h.z = b2 /16%8;
h.ra = b2 /128;
h.qdcount = self:word();
h.ancount = self:word();
h.nscount = self:word();
h.arcount = self:word();
for k,v in pairs(h) do h[k] = v-v%1; end
return h;
end
function resolver:name() -- - - - - - - - - - - - - - - - - - - - - - name
local remember, pointers = nil, 0;
local len = self:byte();
local n = {};
if len == 0 then return "." end -- Root label
while len > 0 do
if len >= 0xc0 then -- name is "compressed"
pointers = pointers + 1;
if pointers >= 20 then error('dns error: 20 pointers'); end;
local offset = ((len-0xc0)*0x100) + self:byte();
remember = remember or self.offset;
self.offset = offset + 1; -- +1 for lua
else -- name is not compressed
append(n, self:sub(len)..'.');
end
len = self:byte();
end
self.offset = remember or self.offset;
return table.concat(n);
end
function resolver:question() -- - - - - - - - - - - - - - - - - - question
local q = {};
q.name = self:name();
q.type = dns.type[self:word()];
q.class = dns.class[self:word()];
return q;
end
function resolver:A(rr) -- - - - - - - - - - - - - - - - - - - - - - - - A
local b1, b2, b3, b4 = self:byte(4);
rr.a = string.format('%i.%i.%i.%i', b1, b2, b3, b4);
end
function resolver:AAAA(rr)
local addr = {};
for _ = 1, rr.rdlength, 2 do
local b1, b2 = self:byte(2);
table.insert(addr, ("%02x%02x"):format(b1, b2));
end
addr = table.concat(addr, ":"):gsub("%f[%x]0+(%x)","%1");
local zeros = {};
for item in addr:gmatch(":[0:]+:[0:]+:") do
table.insert(zeros, item)
end
if #zeros == 0 then
rr.aaaa = addr;
return
elseif #zeros > 1 then
table.sort(zeros, function(a, b) return #a > #b end);
end
rr.aaaa = addr:gsub(zeros[1], "::", 1):gsub("^0::", "::"):gsub("::0$", "::");
end
function resolver:CNAME(rr) -- - - - - - - - - - - - - - - - - - - - CNAME
rr.cname = self:name();
end
function resolver:MX(rr) -- - - - - - - - - - - - - - - - - - - - - - - MX
rr.pref = self:word();
rr.mx = self:name();
end
function resolver:LOC_nibble_power() -- - - - - - - - - - LOC_nibble_power
local b = self:byte();
--print('nibbles', ((b-(b%0x10))/0x10), (b%0x10));
return ((b-(b%0x10))/0x10) * (10^(b%0x10));
end
function resolver:LOC(rr) -- - - - - - - - - - - - - - - - - - - - - - LOC
rr.version = self:byte();
if rr.version == 0 then
rr.loc = rr.loc or {};
rr.loc.size = self:LOC_nibble_power();
rr.loc.horiz_pre = self:LOC_nibble_power();
rr.loc.vert_pre = self:LOC_nibble_power();
rr.loc.latitude = self:dword();
rr.loc.longitude = self:dword();
rr.loc.altitude = self:dword();
end
end
local function LOC_tostring_degrees(f, pos, neg) -- - - - - - - - - - - - -
f = f - 0x80000000;
if f < 0 then pos = neg; f = -f; end
local deg, min, msec;
msec = f%60000;
f = (f-msec)/60000;
min = f%60;
deg = (f-min)/60;
return string.format('%3d %2d %2.3f %s', deg, min, msec/1000, pos);
end
function resolver.LOC_tostring(rr) -- - - - - - - - - - - - - LOC_tostring
local t = {};
--[[
for k,name in pairs { 'size', 'horiz_pre', 'vert_pre', 'latitude', 'longitude', 'altitude' } do
append(t, string.format('%4s%-10s: %12.0f\n', '', name, rr.loc[name]));
end
--]]
append(t, string.format(
'%s %s %.2fm %.2fm %.2fm %.2fm',
LOC_tostring_degrees (rr.loc.latitude, 'N', 'S'),
LOC_tostring_degrees (rr.loc.longitude, 'E', 'W'),
(rr.loc.altitude - 10000000) / 100,
rr.loc.size / 100,
rr.loc.horiz_pre / 100,
rr.loc.vert_pre / 100
));
return table.concat(t);
end
function resolver:NS(rr) -- - - - - - - - - - - - - - - - - - - - - - - NS
rr.ns = self:name();
end
function resolver:SOA(rr) -- - - - - - - - - - - - - - - - - - - - - - SOA
end
function resolver:SRV(rr) -- - - - - - - - - - - - - - - - - - - - - - SRV
rr.srv = {};
rr.srv.priority = self:word();
rr.srv.weight = self:word();
rr.srv.port = self:word();
rr.srv.target = self:name();
end
function resolver:PTR(rr)
rr.ptr = self:name();
end
function resolver:TXT(rr) -- - - - - - - - - - - - - - - - - - - - - - TXT
rr.txt = self:sub (self:byte());
end
function resolver:rr() -- - - - - - - - - - - - - - - - - - - - - - - - rr
local rr = {};
setmetatable(rr, rr_metatable);
rr.name = self:name(self);
rr.type = dns.type[self:word()] or rr.type;
rr.class = dns.class[self:word()] or rr.class;
rr.ttl = 0x10000*self:word() + self:word();
rr.rdlength = self:word();
rr.tod = self.time + math.max(rr.ttl, 1);
local remember = self.offset;
local rr_parser = self[dns.type[rr.type]];
if rr_parser then rr_parser(self, rr); end
self.offset = remember;
rr.rdata = self:sub(rr.rdlength);
return rr;
end
function resolver:rrs (count) -- - - - - - - - - - - - - - - - - - - - - rrs
local rrs = {};
for _ = 1, count do append(rrs, self:rr()); end
return rrs;
end
function resolver:decode(packet, force) -- - - - - - - - - - - - - - decode
self.packet, self.offset = packet, 1;
local header = self:header(force);
if not header then return nil; end
local response = { header = header };
response.question = {};
local offset = self.offset;
for _ = 1, response.header.qdcount do
append(response.question, self:question());
end
response.question.raw = string.sub(self.packet, offset, self.offset - 1);
if not force then
if not self.active[response.header.id] or not self.active[response.header.id][response.question.raw] then
self.active[response.header.id] = nil;
return nil;
end
end
response.answer = self:rrs(response.header.ancount);
response.authority = self:rrs(response.header.nscount);
response.additional = self:rrs(response.header.arcount);
return response;
end
-- socket layer -------------------------------------------------- socket layer
resolver.delays = { 1, 3 };
function resolver:addnameserver(address) -- - - - - - - - - - addnameserver
self.server = self.server or {};
append(self.server, address);
end
function resolver:setnameserver(address) -- - - - - - - - - - setnameserver
self.server = {};
self:addnameserver(address);
end
function resolver:adddefaultnameservers() -- - - - - adddefaultnameservers
if is_windows then
if windows and windows.get_nameservers then
for _, server in ipairs(windows.get_nameservers()) do
self:addnameserver(server);
end
end
if not self.server or #self.server == 0 then
-- TODO log warning about no nameservers, adding opendns servers as fallback
self:addnameserver("208.67.222.222");
self:addnameserver("208.67.220.220");
end
else -- posix
local resolv_conf = io.open("/etc/resolv.conf");
if resolv_conf then
for line in resolv_conf:lines() do
line = line:gsub("#.*$", "")
:match('^%s*nameserver%s+([%x:%.]*%%?%S*)%s*$');
if line then
local ip = new_ip(line);
if ip then
self:addnameserver(ip.addr);
end
end
end
end
if not self.server or #self.server == 0 then
-- TODO log warning about no nameservers, adding localhost as the default nameserver
self:addnameserver("127.0.0.1");
end
end
end
function resolver:getsocket(servernum) -- - - - - - - - - - - - - getsocket
self.socket = self.socket or {};
self.socketset = self.socketset or {};
local sock = self.socket[servernum];
if sock then return sock; end
local ok, err;
local peer = self.server[servernum];
if peer:find(":") then
sock, err = socket.udp6();
else
sock, err = (socket.udp4 or socket.udp)();
end
if sock and self.socket_wrapper then sock, err = self.socket_wrapper(sock, self); end
if not sock then
return nil, err;
end
sock:settimeout(0);
-- todo: attempt to use a random port, fallback to 0
self.socket[servernum] = sock;
self.socketset[sock] = servernum;
-- set{sock,peer}name can fail, eg because of local routing table
-- if so, try the next server
ok, err = sock:setsockname('*', 0);
if not ok then return self:servfail(sock, err); end
ok, err = sock:setpeername(peer, 53);
if not ok then return self:servfail(sock, err); end
return sock;
end
function resolver:voidsocket(sock)
if self.socket[sock] then
self.socketset[self.socket[sock]] = nil;
self.socket[sock] = nil;
elseif self.socketset[sock] then
self.socket[self.socketset[sock]] = nil;
self.socketset[sock] = nil;
end
sock:close();
end
function resolver:socket_wrapper_set(func) -- - - - - - - socket_wrapper_set
self.socket_wrapper = func;
end
function resolver:closeall () -- - - - - - - - - - - - - - - - - - closeall
for i,sock in ipairs(self.socket) do
self.socket[i] = nil;
self.socketset[sock] = nil;
sock:close();
end
end
function resolver:remember(rr, type) -- - - - - - - - - - - - - - remember
--print ('remember', type, rr.class, rr.type, rr.name)
local qname, qtype, qclass = standardize(rr.name, rr.type, rr.class);
if type ~= '*' then
type = qtype;
local all = get(self.cache, qclass, '*', qname);
--print('remember all', all);
if all then append(all, rr); end
end
self.cache = self.cache or setmetatable({}, cache_metatable);
local rrs = get(self.cache, qclass, type, qname) or
set(self.cache, qclass, type, qname, setmetatable({}, rrs_metatable));
if rr[qtype:lower()] and not rrs[rr[qtype:lower()]] then
rrs[rr[qtype:lower()]] = true;
append(rrs, rr);
end
if type == 'MX' then self.unsorted[rrs] = true; end
end
local function comp_mx(a, b) -- - - - - - - - - - - - - - - - - - - comp_mx
return (a.pref == b.pref) and (a.mx < b.mx) or (a.pref < b.pref);
end
function resolver:peek (qname, qtype, qclass, n) -- - - - - - - - - - - - peek
qname, qtype, qclass = standardize(qname, qtype, qclass);
local rrs = get(self.cache, qclass, qtype, qname);
if not rrs then
if n then if n <= 0 then return end else n = 3 end
rrs = get(self.cache, qclass, "CNAME", qname);
if not (rrs and rrs[1]) then return end
return self:peek(rrs[1].cname, qtype, qclass, n - 1);
end
if prune(rrs, socket.gettime()) and qtype == '*' or not next(rrs) then
set(self.cache, qclass, qtype, qname, nil);
return nil;
end
if self.unsorted[rrs] then table.sort (rrs, comp_mx); self.unsorted[rrs] = nil; end
return rrs;
end
function resolver:purge(soft) -- - - - - - - - - - - - - - - - - - - purge
if soft == 'soft' then
self.time = socket.gettime();
for class,types in pairs(self.cache or {}) do
for type,names in pairs(types) do
for name,rrs in pairs(names) do
prune(rrs, self.time, 'soft')
end
end
end
else self.cache = setmetatable({}, cache_metatable); end
end
function resolver:query(qname, qtype, qclass) -- - - - - - - - - - -- query
qname, qtype, qclass = standardize(qname, qtype, qclass)
local co = coroutine.running();
local q = get(self.wanted, qclass, qtype, qname);
if co and q then
-- We are already waiting for a reply to an identical query.
set(self.wanted, qclass, qtype, qname, co, true);
return true;
end
if not self.server then self:adddefaultnameservers(); end
local question = encodeQuestion(qname, qtype, qclass);
local peek = self:peek (qname, qtype, qclass);
if peek then return peek; end
local header, id = encodeHeader();
--print ('query id', id, qclass, qtype, qname)
local o = {
packet = header..question,
server = self.best_server,
delay = 1,
retry = socket.gettime() + self.delays[1]
};
-- remember the query
self.active[id] = self.active[id] or {};
self.active[id][question] = o;
local conn, err = self:getsocket(o.server)
if not conn then
return nil, err;
end
conn:send (o.packet)
-- remember which coroutine wants the answer
if co then
set(self.wanted, qclass, qtype, qname, co, true);
end
if timer and self.timeout then
local num_servers = #self.server;
local i = 1;
timer.add_task(self.timeout, function ()
if get(self.wanted, qclass, qtype, qname, co) then
if i < num_servers then
i = i + 1;
self:servfail(conn);
o.server = self.best_server;
conn, err = self:getsocket(o.server);
if conn then
conn:send(o.packet);
return self.timeout;
end
end
-- Tried everything, failed
self:cancel(qclass, qtype, qname);
end
end)
end
return true;
end
function resolver:servfail(sock, err)
-- Resend all queries for this server
local num = self.socketset[sock]
-- Socket is dead now
sock = self:voidsocket(sock);
-- Find all requests to the down server, and retry on the next server
self.time = socket.gettime();
for id,queries in pairs(self.active) do
for question,o in pairs(queries) do
if o.server == num then -- This request was to the broken server
o.server = o.server + 1 -- Use next server
if o.server > #self.server then
o.server = 1;
end
o.retries = (o.retries or 0) + 1;
if o.retries >= #self.server then
--print('timeout');
queries[question] = nil;
else
sock, err = self:getsocket(o.server);
if sock then sock:send(o.packet); end
end
end
end
if next(queries) == nil then
self.active[id] = nil;
end
end
if num == self.best_server then
self.best_server = self.best_server + 1;
if self.best_server > #self.server then
-- Exhausted all servers, try first again
self.best_server = 1;
end
end
return sock, err;
end
function resolver:settimeout(seconds)
self.timeout = seconds;
end
function resolver:receive(rset) -- - - - - - - - - - - - - - - - - receive
--print('receive'); print(self.socket);
self.time = socket.gettime();
rset = rset or self.socket;
local response;
for _, sock in pairs(rset) do
if self.socketset[sock] then
local packet = sock:receive();
if packet then
response = self:decode(packet);
if response and self.active[response.header.id]
and self.active[response.header.id][response.question.raw] then
--print('received response');
--self.print(response);
for _, rr in pairs(response.answer) do
if rr.name:sub(-#response.question[1].name, -1) == response.question[1].name then
self:remember(rr, response.question[1].type)
end
end
-- retire the query
local queries = self.active[response.header.id];
queries[response.question.raw] = nil;
if not next(queries) then self.active[response.header.id] = nil; end
if not next(self.active) then self:closeall(); end
-- was the query on the wanted list?
local q = response.question[1];
local cos = get(self.wanted, q.class, q.type, q.name);
if cos then
for co in pairs(cos) do
if coroutine.status(co) == "suspended" then coroutine.resume(co); end
end
set(self.wanted, q.class, q.type, q.name, nil);
end
end
end
end
end
return response;
end
function resolver:feed(sock, packet, force)
--print('receive'); print(self.socket);
self.time = socket.gettime();
local response = self:decode(packet, force);
if response and self.active[response.header.id]
and self.active[response.header.id][response.question.raw] then
--print('received response');
--self.print(response);
for _, rr in pairs(response.answer) do
self:remember(rr, rr.type);
end
for _, rr in pairs(response.additional) do
self:remember(rr, rr.type);
end
-- retire the query
local queries = self.active[response.header.id];
queries[response.question.raw] = nil;
if not next(queries) then self.active[response.header.id] = nil; end
if not next(self.active) then self:closeall(); end
-- was the query on the wanted list?
local q = response.question[1];
if q then
local cos = get(self.wanted, q.class, q.type, q.name);
if cos then
for co in pairs(cos) do
if coroutine.status(co) == "suspended" then coroutine.resume(co); end
end
set(self.wanted, q.class, q.type, q.name, nil);
end
end
end
return response;
end
function resolver:cancel(qclass, qtype, qname)
local cos = get(self.wanted, qclass, qtype, qname);
if cos then
for co in pairs(cos) do
if coroutine.status(co) == "suspended" then coroutine.resume(co); end
end
set(self.wanted, qclass, qtype, qname, nil);
end
end
function resolver:pulse() -- - - - - - - - - - - - - - - - - - - - - pulse
--print(':pulse');
while self:receive() do end
if not next(self.active) then return nil; end
self.time = socket.gettime();
for id,queries in pairs(self.active) do
for question,o in pairs(queries) do
if self.time >= o.retry then
o.server = o.server + 1;
if o.server > #self.server then
o.server = 1;
o.delay = o.delay + 1;
end
if o.delay > #self.delays then
--print('timeout');
queries[question] = nil;
if not next(queries) then self.active[id] = nil; end
if not next(self.active) then return nil; end
else
--print('retry', o.server, o.delay);
local _a = self.socket[o.server];
if _a then _a:send(o.packet); end
o.retry = self.time + self.delays[o.delay];
end
end
end
end
if next(self.active) then return true; end
return nil;
end
function resolver:lookup(qname, qtype, qclass) -- - - - - - - - - - lookup
self:query (qname, qtype, qclass)
while self:pulse() do
local recvt = {}
for i, s in ipairs(self.socket) do
recvt[i] = s
end
socket.select(recvt, nil, 4)
end
--print(self.cache);
return self:peek(qname, qtype, qclass);
end
function resolver:lookupex(handler, qname, qtype, qclass) -- - - - - - - - - - lookup
return self:peek(qname, qtype, qclass) or self:query(qname, qtype, qclass);
end
function resolver:tohostname(ip)
return dns.lookup(ip:gsub("(%d+)%.(%d+)%.(%d+)%.(%d+)", "%4.%3.%2.%1.in-addr.arpa."), "PTR");
end
--print ---------------------------------------------------------------- print
local hints = { -- - - - - - - - - - - - - - - - - - - - - - - - - - - hints
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 = dns.type,
class = dns.class
};
local function hint(p, s) -- - - - - - - - - - - - - - - - - - - - - - hint
return (hints[s] and hints[s][p[s]]) or '';
end
function resolver.print(response) -- - - - - - - - - - - - - resolver.print
for _, s in pairs { 'id', 'qr', 'opcode', 'aa', 'tc', 'rd', 'ra', 'z',
'rcode', 'qdcount', 'ancount', 'nscount', 'arcount' } do
print( string.format('%-30s', 'header.'..s), response.header[s], hint(response.header, s) );
end
for i,question in ipairs(response.question) do
print(string.format ('question[%i].name ', i), question.name);
print(string.format ('question[%i].type ', i), question.type);
print(string.format ('question[%i].class ', i), question.class);
end
local common = { name=1, type=1, class=1, ttl=1, rdlength=1, rdata=1 };
local tmp;
for _, s in pairs({'answer', 'authority', 'additional'}) do
for i,rr in pairs(response[s]) do
for _, t in pairs({ 'name', 'type', 'class', 'ttl', 'rdlength' }) do
tmp = string.format('%s[%i].%s', s, i, t);
print(string.format('%-30s', tmp), rr[t], hint(rr, t));
end
for j,t in pairs(rr) do
if not common[j] then
tmp = string.format('%s[%i].%s', s, i, j);
print(string.format('%-30s %s', tostring(tmp), tostring(t)));
end
end
end
end
end
-- module api ------------------------------------------------------ module api
function dns.resolver () -- - - - - - - - - - - - - - - - - - - - - resolver
local r = { active = {}, cache = {}, unsorted = {}, wanted = {}, best_server = 1 };
setmetatable (r, resolver);
setmetatable (r.cache, cache_metatable);
setmetatable (r.unsorted, { __mode = 'kv' });
return r;
end
local _resolver = dns.resolver();
dns._resolver = _resolver;
function dns.lookup(...) -- - - - - - - - - - - - - - - - - - - - - lookup
return _resolver:lookup(...);
end
function dns.tohostname(...)
return _resolver:tohostname(...);
end
function dns.purge(...) -- - - - - - - - - - - - - - - - - - - - - - purge
return _resolver:purge(...);
end
function dns.peek(...) -- - - - - - - - - - - - - - - - - - - - - - - peek
return _resolver:peek(...);
end
function dns.query(...) -- - - - - - - - - - - - - - - - - - - - - - query
return _resolver:query(...);
end
function dns.feed(...) -- - - - - - - - - - - - - - - - - - - - - - - feed
return _resolver:feed(...);
end
function dns.cancel(...) -- - - - - - - - - - - - - - - - - - - - - - cancel
return _resolver:cancel(...);
end
function dns.settimeout(...)
return _resolver:settimeout(...);
end
function dns.cache()
return _resolver.cache;
end
function dns.socket_wrapper_set(...) -- - - - - - - - - socket_wrapper_set
return _resolver:socket_wrapper_set(...);
end
return dns;
end)
package.preload['net.adns'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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 server = require "net.server";
local new_resolver = require "net.dns".resolver;
local log = require "util.logger".init("adns");
local coroutine, tostring, pcall = coroutine, tostring, pcall;
local setmetatable = setmetatable;
local function dummy_send(sock, data, i, j) return (j-i)+1; end
local _ENV = nil;
local async_resolver_methods = {};
local async_resolver_mt = { __index = async_resolver_methods };
local query_methods = {};
local query_mt = { __index = query_methods };
local function new_async_socket(sock, resolver)
local peername = "<unknown>";
local listener = {};
local handler = {};
local err;
function listener.onincoming(conn, data)
if data then
resolver:feed(handler, data);
end
end
function listener.ondisconnect(conn, err)
if err then
log("warn", "DNS socket for %s disconnected: %s", peername, err);
local servers = resolver.server;
if resolver.socketset[conn] == resolver.best_server and resolver.best_server == #servers then
log("error", "Exhausted all %d configured DNS servers, next lookup will try %s again", #servers, servers[1]);
end
resolver:servfail(conn); -- Let the magic commence
end
end
handler, err = server.wrapclient(sock, "dns", 53, listener);
if not handler then
return nil, err;
end
handler.settimeout = function () end
handler.setsockname = function (_, ...) return sock:setsockname(...); end
handler.setpeername = function (_, ...) peername = (...); local ret, err = sock:setpeername(...); _:set_send(dummy_send); return ret, err; end
handler.connect = function (_, ...) return sock:connect(...) end
--handler.send = function (_, data) _:write(data); return _.sendbuffer and _.sendbuffer(); end
handler.send = function (_, data)
log("debug", "Sending DNS query to %s", peername);
return sock:send(data);
end
return handler;
end
function async_resolver_methods:lookup(handler, qname, qtype, qclass)
local resolver = self._resolver;
return coroutine.wrap(function (peek)
if peek then
log("debug", "Records for %s already cached, using those...", qname);
handler(peek);
return;
end
log("debug", "Records for %s not in cache, sending query (%s)...", qname, tostring(coroutine.running()));
local ok, err = resolver:query(qname, qtype, qclass);
if ok then
coroutine.yield(setmetatable({ resolver, qclass or "IN", qtype or "A", qname, coroutine.running()}, query_mt)); -- Wait for reply
log("debug", "Reply for %s (%s)", qname, tostring(coroutine.running()));
end
if ok then
ok, err = pcall(handler, resolver:peek(qname, qtype, qclass));
else
log("error", "Error sending DNS query: %s", err);
ok, err = pcall(handler, nil, err);
end
if not ok then
log("error", "Error in DNS response handler: %s", tostring(err));
end
end)(resolver:peek(qname, qtype, qclass));
end
function query_methods:cancel(call_handler, reason)
log("warn", "Cancelling DNS lookup for %s", tostring(self[4]));
self[1].cancel(self[2], self[3], self[4], self[5], call_handler);
end
local function new_async_resolver()
local resolver = new_resolver();
resolver:socket_wrapper_set(new_async_socket);
return setmetatable({ _resolver = resolver}, async_resolver_mt);
end
return {
lookup = function (...)
return new_async_resolver():lookup(...);
end;
resolver = new_async_resolver;
new_async_socket = new_async_socket;
};
end)
package.preload['net.server'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
--
-- server.lua by blastbeat of the luadch project
-- Re-used here under the MIT/X Consortium License
--
-- Modifications (C) 2008-2010 Matthew Wild, Waqas Hussain
--
-- // wrapping luadch stuff // --
local use = function( what )
return _G[ what ]
end
local log, table_concat = require ("util.logger").init("socket"), table.concat;
local out_put = function (...) return log("debug", table_concat{...}); end
local out_error = function (...) return log("warn", table_concat{...}); end
----------------------------------// DECLARATION //--
--// constants //--
local STAT_UNIT = 1 -- byte
--// lua functions //--
local type = use "type"
local pairs = use "pairs"
local ipairs = use "ipairs"
local tonumber = use "tonumber"
local tostring = use "tostring"
--// lua libs //--
local table = use "table"
local string = use "string"
local coroutine = use "coroutine"
--// lua lib methods //--
local math_min = math.min
local math_huge = math.huge
local table_concat = table.concat
local string_sub = string.sub
local coroutine_wrap = coroutine.wrap
local coroutine_yield = coroutine.yield
--// extern libs //--
local has_luasec, luasec = pcall ( require , "ssl" )
local luasocket = use "socket" or require "socket"
local luasocket_gettime = luasocket.gettime
local getaddrinfo = luasocket.dns.getaddrinfo
--// extern lib methods //--
local ssl_wrap = ( has_luasec and luasec.wrap )
local socket_bind = luasocket.bind
local socket_sleep = luasocket.sleep
local socket_select = luasocket.select
--// functions //--
local id
local loop
local stats
local idfalse
local closeall
local addsocket
local addserver
local addtimer
local getserver
local wrapserver
local getsettings
local closesocket
local removesocket
local removeserver
local wrapconnection
local changesettings
--// tables //--
local _server
local _readlist
local _timerlist
local _sendlist
local _socketlist
local _closelist
local _readtimes
local _writetimes
local _fullservers
--// simple data types //--
local _
local _readlistlen
local _sendlistlen
local _timerlistlen
local _sendtraffic
local _readtraffic
local _selecttimeout
local _sleeptime
local _tcpbacklog
local _accepretry
local _starttime
local _currenttime
local _maxsendlen
local _maxreadlen
local _checkinterval
local _sendtimeout
local _readtimeout
local _timer
local _maxselectlen
local _maxfd
local _maxsslhandshake
----------------------------------// DEFINITION //--
_server = { } -- key = port, value = table; list of listening servers
_readlist = { } -- array with sockets to read from
_sendlist = { } -- arrary with sockets to write to
_timerlist = { } -- array of timer functions
_socketlist = { } -- key = socket, value = wrapped socket (handlers)
_readtimes = { } -- key = handler, value = timestamp of last data reading
_writetimes = { } -- key = handler, value = timestamp of last data writing/sending
_closelist = { } -- handlers to close
_fullservers = { } -- servers in a paused state while there are too many clients
_readlistlen = 0 -- length of readlist
_sendlistlen = 0 -- length of sendlist
_timerlistlen = 0 -- lenght of timerlist
_sendtraffic = 0 -- some stats
_readtraffic = 0
_selecttimeout = 1 -- timeout of socket.select
_sleeptime = 0 -- time to wait at the end of every loop
_tcpbacklog = 128 -- some kind of hint to the OS
_accepretry = 10 -- seconds to wait until the next attempt of a full server to accept
_maxsendlen = 51000 * 1024 -- max len of send buffer
_maxreadlen = 25000 * 1024 -- max len of read buffer
_checkinterval = 30 -- interval in secs to check idle clients
_sendtimeout = 60000 -- allowed send idle time in secs
_readtimeout = 6 * 60 * 60 -- allowed read idle time in secs
local is_windows = package.config:sub(1,1) == "\\" -- check the directory separator, to detemine whether this is Windows
_maxfd = (is_windows and math.huge) or luasocket._SETSIZE or 1024 -- max fd number, limit to 1024 by default to prevent glibc buffer overflow, but not on Windows
_maxselectlen = luasocket._SETSIZE or 1024 -- But this still applies on Windows
_maxsslhandshake = 30 -- max handshake round-trips
----------------------------------// PRIVATE //--
wrapserver = function( listeners, socket, ip, serverport, pattern, sslctx ) -- this function wraps a server -- FIXME Make sure FD < _maxfd
if socket:getfd() >= _maxfd then
out_error("server.lua: Disallowed FD number: "..socket:getfd())
socket:close()
return nil, "fd-too-large"
end
local connections = 0
local dispatch, disconnect = listeners.onconnect, listeners.ondisconnect
local accept = socket.accept
--// public methods of the object //--
local handler = { }
handler.shutdown = function( ) end
handler.ssl = function( )
return sslctx ~= nil
end
handler.sslctx = function( )
return sslctx
end
handler.remove = function( )
connections = connections - 1
if handler then
handler.resume( )
end
end
handler.close = function()
socket:close( )
_sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
_readlistlen = removesocket( _readlist, socket, _readlistlen )
_server[ip..":"..serverport] = nil;
_socketlist[ socket ] = nil
handler = nil
socket = nil
--mem_free( )
out_put "server.lua: closed server handler and removed sockets from list"
end
handler.pause = function( hard )
if not handler.paused then
_readlistlen = removesocket( _readlist, socket, _readlistlen )
if hard then
_socketlist[ socket ] = nil
socket:close( )
socket = nil;
end
handler.paused = true;
out_put("server.lua: server [", ip, "]:", serverport, " paused")
end
end
handler.resume = function( )
if handler.paused then
if not socket then
socket = socket_bind( ip, serverport, _tcpbacklog );
socket:settimeout( 0 )
end
_readlistlen = addsocket(_readlist, socket, _readlistlen)
_socketlist[ socket ] = handler
_fullservers[ handler ] = nil
handler.paused = false;
out_put("server.lua: server [", ip, "]:", serverport, " resumed")
end
end
handler.ip = function( )
return ip
end
handler.serverport = function( )
return serverport
end
handler.socket = function( )
return socket
end
handler.readbuffer = function( )
if _readlistlen >= _maxselectlen or _sendlistlen >= _maxselectlen then
handler.pause( )
_fullservers[ handler ] = _currenttime
out_put( "server.lua: refused new client connection: server full" )
return false
end
local client, err = accept( socket ) -- try to accept
if client then
local ip, clientport = client:getpeername( )
local handler, client, err = wrapconnection( handler, listeners, client, ip, serverport, clientport, pattern, sslctx ) -- wrap new client socket
if err then -- error while wrapping ssl socket
return false
end
connections = connections + 1
out_put( "server.lua: accepted new client connection from ", tostring(ip), ":", tostring(clientport), " to ", tostring(serverport))
if dispatch and not sslctx then -- SSL connections will notify onconnect when handshake completes
return dispatch( handler );
end
return;
elseif err then -- maybe timeout or something else
out_put( "server.lua: error with new client connection: ", tostring(err) )
handler.pause( )
_fullservers[ handler ] = _currenttime
return false
end
end
return handler
end
wrapconnection = function( server, listeners, socket, ip, serverport, clientport, pattern, sslctx ) -- this function wraps a client to a handler object
if socket:getfd() >= _maxfd then
out_error("server.lua: Disallowed FD number: "..socket:getfd()) -- PROTIP: Switch to libevent
socket:close( ) -- Should we send some kind of error here?
if server then
_fullservers[ server ] = _currenttime
server.pause( )
end
return nil, nil, "fd-too-large"
end
socket:settimeout( 0 )
--// local import of socket methods //--
local send
local receive
local shutdown
--// private closures of the object //--
local ssl
local dispatch = listeners.onincoming
local status = listeners.onstatus
local disconnect = listeners.ondisconnect
local drain = listeners.ondrain
local onreadtimeout = listeners.onreadtimeout;
local detach = listeners.ondetach
local bufferqueue = { } -- buffer array
local bufferqueuelen = 0 -- end of buffer array
local toclose
local fatalerror
local needtls
local bufferlen = 0
local noread = false
local nosend = false
local sendtraffic, readtraffic = 0, 0
local maxsendlen = _maxsendlen
local maxreadlen = _maxreadlen
--// public methods of the object //--
local handler = bufferqueue -- saves a table ^_^
handler.dispatch = function( )
return dispatch
end
handler.disconnect = function( )
return disconnect
end
handler.onreadtimeout = onreadtimeout;
handler.setlistener = function( self, listeners )
if detach then
detach(self) -- Notify listener that it is no longer responsible for this connection
end
dispatch = listeners.onincoming
disconnect = listeners.ondisconnect
status = listeners.onstatus
drain = listeners.ondrain
handler.onreadtimeout = listeners.onreadtimeout
detach = listeners.ondetach
end
handler.getstats = function( )
return readtraffic, sendtraffic
end
handler.ssl = function( )
return ssl
end
handler.sslctx = function ( )
return sslctx
end
handler.send = function( _, data, i, j )
return send( socket, data, i, j )
end
handler.receive = function( pattern, prefix )
return receive( socket, pattern, prefix )
end
handler.shutdown = function( pattern )
return shutdown( socket, pattern )
end
handler.setoption = function (self, option, value)
if socket.setoption then
return socket:setoption(option, value);
end
return false, "setoption not implemented";
end
handler.force_close = function ( self, err )
if bufferqueuelen ~= 0 then
out_put("server.lua: discarding unwritten data for ", tostring(ip), ":", tostring(clientport))
bufferqueuelen = 0;
end
return self:close(err);
end
handler.close = function( self, err )
if not handler then return true; end
_readlistlen = removesocket( _readlist, socket, _readlistlen )
_readtimes[ handler ] = nil
if bufferqueuelen ~= 0 then
handler.sendbuffer() -- Try now to send any outstanding data
if bufferqueuelen ~= 0 then -- Still not empty, so we'll try again later
if handler then
handler.write = nil -- ... but no further writing allowed
end
toclose = true
return false
end
end
if socket then
_ = shutdown and shutdown( socket )
socket:close( )
_sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
_socketlist[ socket ] = nil
socket = nil
else
out_put "server.lua: socket already closed"
end
if handler then
_writetimes[ handler ] = nil
_closelist[ handler ] = nil
local _handler = handler;
handler = nil
if disconnect then
disconnect(_handler, err or false);
disconnect = nil
end
end
if server then
server.remove( )
end
out_put "server.lua: closed client handler and removed socket from list"
return true
end
handler.server = function ( )
return server
end
handler.ip = function( )
return ip
end
handler.serverport = function( )
return serverport
end
handler.clientport = function( )
return clientport
end
handler.port = handler.clientport -- COMPAT server_event
local write = function( self, data )
if not handler then return false end
bufferlen = bufferlen + #data
if bufferlen > maxsendlen then
_closelist[ handler ] = "send buffer exceeded" -- cannot close the client at the moment, have to wait to the end of the cycle
handler.write = idfalse -- dont write anymore
return false
elseif socket and not _sendlist[ socket ] then
_sendlistlen = addsocket(_sendlist, socket, _sendlistlen)
end
bufferqueuelen = bufferqueuelen + 1
bufferqueue[ bufferqueuelen ] = data
if handler then
_writetimes[ handler ] = _writetimes[ handler ] or _currenttime
end
return true
end
handler.write = write
handler.bufferqueue = function( self )
return bufferqueue
end
handler.socket = function( self )
return socket
end
handler.set_mode = function( self, new )
pattern = new or pattern
return pattern
end
handler.set_send = function ( self, newsend )
send = newsend or send
return send
end
handler.bufferlen = function( self, readlen, sendlen )
maxsendlen = sendlen or maxsendlen
maxreadlen = readlen or maxreadlen
return bufferlen, maxreadlen, maxsendlen
end
--TODO: Deprecate
handler.lock_read = function (self, switch)
if switch == true then
local tmp = _readlistlen
_readlistlen = removesocket( _readlist, socket, _readlistlen )
_readtimes[ handler ] = nil
if _readlistlen ~= tmp then
noread = true
end
elseif switch == false then
if noread then
noread = false
_readlistlen = addsocket(_readlist, socket, _readlistlen)
_readtimes[ handler ] = _currenttime
end
end
return noread
end
handler.pause = function (self)
return self:lock_read(true);
end
handler.resume = function (self)
return self:lock_read(false);
end
handler.lock = function( self, switch )
handler.lock_read (switch)
if switch == true then
handler.write = idfalse
local tmp = _sendlistlen
_sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
_writetimes[ handler ] = nil
if _sendlistlen ~= tmp then
nosend = true
end
elseif switch == false then
handler.write = write
if nosend then
nosend = false
write( "" )
end
end
return noread, nosend
end
local _readbuffer = function( ) -- this function reads data
local buffer, err, part = receive( socket, pattern ) -- receive buffer with "pattern"
if not err or (err == "wantread" or err == "timeout") then -- received something
local buffer = buffer or part or ""
local len = #buffer
if len > maxreadlen then
handler:close( "receive buffer exceeded" )
return false
end
local count = len * STAT_UNIT
readtraffic = readtraffic + count
_readtraffic = _readtraffic + count
_readtimes[ handler ] = _currenttime
--out_put( "server.lua: read data '", buffer:gsub("[^%w%p ]", "."), "', error: ", err )
return dispatch( handler, buffer, err )
else -- connections was closed or fatal error
out_put( "server.lua: client ", tostring(ip), ":", tostring(clientport), " read error: ", tostring(err) )
fatalerror = true
_ = handler and handler:force_close( err )
return false
end
end
local _sendbuffer = function( ) -- this function sends data
local succ, err, byte, buffer, count;
if socket then
buffer = table_concat( bufferqueue, "", 1, bufferqueuelen )
succ, err, byte = send( socket, buffer, 1, bufferlen )
count = ( succ or byte or 0 ) * STAT_UNIT
sendtraffic = sendtraffic + count
_sendtraffic = _sendtraffic + count
for i = bufferqueuelen,1,-1 do
bufferqueue[ i ] = nil
end
--out_put( "server.lua: sended '", buffer, "', bytes: ", tostring(succ), ", error: ", tostring(err), ", part: ", tostring(byte), ", to: ", tostring(ip), ":", tostring(clientport) )
else
succ, err, count = false, "unexpected close", 0;
end
if succ then -- sending succesful
bufferqueuelen = 0
bufferlen = 0
_sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) -- delete socket from writelist
_writetimes[ handler ] = nil
if drain then
drain(handler)
end
_ = needtls and handler:starttls(nil)
_ = toclose and handler:force_close( )
return true
elseif byte and ( err == "timeout" or err == "wantwrite" ) then -- want write
buffer = string_sub( buffer, byte + 1, bufferlen ) -- new buffer
bufferqueue[ 1 ] = buffer -- insert new buffer in queue
bufferqueuelen = 1
bufferlen = bufferlen - byte
_writetimes[ handler ] = _currenttime
return true
else -- connection was closed during sending or fatal error
out_put( "server.lua: client ", tostring(ip), ":", tostring(clientport), " write error: ", tostring(err) )
fatalerror = true
_ = handler and handler:force_close( err )
return false
end
end
-- Set the sslctx
local handshake;
function handler.set_sslctx(self, new_sslctx)
sslctx = new_sslctx;
local read, wrote
handshake = coroutine_wrap( function( client ) -- create handshake coroutine
local err
for _ = 1, _maxsslhandshake do
_sendlistlen = ( wrote and removesocket( _sendlist, client, _sendlistlen ) ) or _sendlistlen
_readlistlen = ( read and removesocket( _readlist, client, _readlistlen ) ) or _readlistlen
read, wrote = nil, nil
_, err = client:dohandshake( )
if not err then
out_put( "server.lua: ssl handshake done" )
handler.readbuffer = _readbuffer -- when handshake is done, replace the handshake function with regular functions
handler.sendbuffer = _sendbuffer
_ = status and status( handler, "ssl-handshake-complete" )
if self.autostart_ssl and listeners.onconnect then
listeners.onconnect(self);
if bufferqueuelen ~= 0 then
_sendlistlen = addsocket(_sendlist, client, _sendlistlen)
end
end
_readlistlen = addsocket(_readlist, client, _readlistlen)
return true
else
if err == "wantwrite" then
_sendlistlen = addsocket(_sendlist, client, _sendlistlen)
wrote = true
elseif err == "wantread" then
_readlistlen = addsocket(_readlist, client, _readlistlen)
read = true
else
break;
end
err = nil;
coroutine_yield( ) -- handshake not finished
end
end
err = "ssl handshake error: " .. ( err or "handshake too long" );
out_put( "server.lua: ", err );
_ = handler and handler:force_close(err)
return false, err -- handshake failed
end
)
end
if has_luasec then
handler.starttls = function( self, _sslctx)
if _sslctx then
handler:set_sslctx(_sslctx);
end
if bufferqueuelen > 0 then
out_put "server.lua: we need to do tls, but delaying until send buffer empty"
needtls = true
return
end
out_put( "server.lua: attempting to start tls on " .. tostring( socket ) )
local oldsocket, err = socket
socket, err = ssl_wrap( socket, sslctx ) -- wrap socket
if not socket then
out_put( "server.lua: error while starting tls on client: ", tostring(err or "unknown error") )
return nil, err -- fatal error
end
socket:settimeout( 0 )
-- add the new socket to our system
send = socket.send
receive = socket.receive
shutdown = id
_socketlist[ socket ] = handler
_readlistlen = addsocket(_readlist, socket, _readlistlen)
-- remove traces of the old socket
_readlistlen = removesocket( _readlist, oldsocket, _readlistlen )
_sendlistlen = removesocket( _sendlist, oldsocket, _sendlistlen )
_socketlist[ oldsocket ] = nil
handler.starttls = nil
needtls = nil
-- Secure now (if handshake fails connection will close)
ssl = true
handler.readbuffer = handshake
handler.sendbuffer = handshake
return handshake( socket ) -- do handshake
end
end
handler.readbuffer = _readbuffer
handler.sendbuffer = _sendbuffer
send = socket.send
receive = socket.receive
shutdown = ( ssl and id ) or socket.shutdown
_socketlist[ socket ] = handler
_readlistlen = addsocket(_readlist, socket, _readlistlen)
if sslctx and has_luasec then
out_put "server.lua: auto-starting ssl negotiation..."
handler.autostart_ssl = true;
local ok, err = handler:starttls(sslctx);
if ok == false then
return nil, nil, err
end
end
return handler, socket
end
id = function( )
end
idfalse = function( )
return false
end
addsocket = function( list, socket, len )
if not list[ socket ] then
len = len + 1
list[ len ] = socket
list[ socket ] = len
end
return len;
end
removesocket = function( list, socket, len ) -- this function removes sockets from a list ( copied from copas )
local pos = list[ socket ]
if pos then
list[ socket ] = nil
local last = list[ len ]
list[ len ] = nil
if last ~= socket then
list[ last ] = pos
list[ pos ] = last
end
return len - 1
end
return len
end
closesocket = function( socket )
_sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
_readlistlen = removesocket( _readlist, socket, _readlistlen )
_socketlist[ socket ] = nil
socket:close( )
--mem_free( )
end
local function link(sender, receiver, buffersize)
local sender_locked;
local _sendbuffer = receiver.sendbuffer;
function receiver.sendbuffer()
_sendbuffer();
if sender_locked and receiver.bufferlen() < buffersize then
sender:lock_read(false); -- Unlock now
sender_locked = nil;
end
end
local _readbuffer = sender.readbuffer;
function sender.readbuffer()
_readbuffer();
if not sender_locked and receiver.bufferlen() >= buffersize then
sender_locked = true;
sender:lock_read(true);
end
end
sender:set_mode("*a");
end
----------------------------------// PUBLIC //--
addserver = function( addr, port, listeners, pattern, sslctx ) -- this function provides a way for other scripts to reg a server
addr = addr or "*"
local err
if type( listeners ) ~= "table" then
err = "invalid listener table"
elseif type ( addr ) ~= "string" then
err = "invalid address"
elseif type( port ) ~= "number" or not ( port >= 0 and port <= 65535 ) then
err = "invalid port"
elseif _server[ addr..":"..port ] then
err = "listeners on '[" .. addr .. "]:" .. port .. "' already exist"
elseif sslctx and not has_luasec then
err = "luasec not found"
end
if err then
out_error( "server.lua, [", addr, "]:", port, ": ", err )
return nil, err
end
local server, err = socket_bind( addr, port, _tcpbacklog )
if err then
out_error( "server.lua, [", addr, "]:", port, ": ", err )
return nil, err
end
local handler, err = wrapserver( listeners, server, addr, port, pattern, sslctx ) -- wrap new server socket
if not handler then
server:close( )
return nil, err
end
server:settimeout( 0 )
_readlistlen = addsocket(_readlist, server, _readlistlen)
_server[ addr..":"..port ] = handler
_socketlist[ server ] = handler
out_put( "server.lua: new "..(sslctx and "ssl " or "").."server listener on '[", addr, "]:", port, "'" )
return handler
end
getserver = function ( addr, port )
return _server[ addr..":"..port ];
end
removeserver = function( addr, port )
local handler = _server[ addr..":"..port ]
if not handler then
return nil, "no server found on '[" .. addr .. "]:" .. tostring( port ) .. "'"
end
handler:close( )
_server[ addr..":"..port ] = nil
return true
end
closeall = function( )
for _, handler in pairs( _socketlist ) do
handler:close( )
_socketlist[ _ ] = nil
end
_readlistlen = 0
_sendlistlen = 0
_timerlistlen = 0
_server = { }
_readlist = { }
_sendlist = { }
_timerlist = { }
_socketlist = { }
--mem_free( )
end
getsettings = function( )
return {
select_timeout = _selecttimeout;
select_sleep_time = _sleeptime;
tcp_backlog = _tcpbacklog;
max_send_buffer_size = _maxsendlen;
max_receive_buffer_size = _maxreadlen;
select_idle_check_interval = _checkinterval;
send_timeout = _sendtimeout;
read_timeout = _readtimeout;
max_connections = _maxselectlen;
max_ssl_handshake_roundtrips = _maxsslhandshake;
highest_allowed_fd = _maxfd;
accept_retry_interval = _accepretry;
}
end
changesettings = function( new )
if type( new ) ~= "table" then
return nil, "invalid settings table"
end
_selecttimeout = tonumber( new.select_timeout ) or _selecttimeout
_sleeptime = tonumber( new.select_sleep_time ) or _sleeptime
_maxsendlen = tonumber( new.max_send_buffer_size ) or _maxsendlen
_maxreadlen = tonumber( new.max_receive_buffer_size ) or _maxreadlen
_checkinterval = tonumber( new.select_idle_check_interval ) or _checkinterval
_tcpbacklog = tonumber( new.tcp_backlog ) or _tcpbacklog
_sendtimeout = tonumber( new.send_timeout ) or _sendtimeout
_readtimeout = tonumber( new.read_timeout ) or _readtimeout
_accepretry = tonumber( new.accept_retry_interval ) or _accepretry
_maxselectlen = new.max_connections or _maxselectlen
_maxsslhandshake = new.max_ssl_handshake_roundtrips or _maxsslhandshake
_maxfd = new.highest_allowed_fd or _maxfd
return true
end
addtimer = function( listener )
if type( listener ) ~= "function" then
return nil, "invalid listener function"
end
_timerlistlen = _timerlistlen + 1
_timerlist[ _timerlistlen ] = listener
return true
end
stats = function( )
return _readtraffic, _sendtraffic, _readlistlen, _sendlistlen, _timerlistlen
end
local quitting;
local function setquitting(quit)
quitting = not not quit;
end
loop = function(once) -- this is the main loop of the program
if quitting then return "quitting"; end
if once then quitting = "once"; end
local next_timer_time = math_huge;
repeat
local read, write, err = socket_select( _readlist, _sendlist, math_min(_selecttimeout, next_timer_time) )
for _, socket in ipairs( write ) do -- send data waiting in writequeues
local handler = _socketlist[ socket ]
if handler then
handler.sendbuffer( )
else
closesocket( socket )
out_put "server.lua: found no handler and closed socket (writelist)" -- this should not happen
end
end
for _, socket in ipairs( read ) do -- receive data
local handler = _socketlist[ socket ]
if handler then
handler.readbuffer( )
else
closesocket( socket )
out_put "server.lua: found no handler and closed socket (readlist)" -- this can happen
end
end
for handler, err in pairs( _closelist ) do
handler.disconnect( )( handler, err )
handler:force_close() -- forced disconnect
_closelist[ handler ] = nil;
end
_currenttime = luasocket_gettime( )
-- Check for socket timeouts
if _currenttime - _starttime > _checkinterval then
_starttime = _currenttime
for handler, timestamp in pairs( _writetimes ) do
if _currenttime - timestamp > _sendtimeout then
handler.disconnect( )( handler, "send timeout" )
handler:force_close() -- forced disconnect
end
end
for handler, timestamp in pairs( _readtimes ) do
if _currenttime - timestamp > _readtimeout then
if not(handler.onreadtimeout) or handler:onreadtimeout() ~= true then
handler.disconnect( )( handler, "read timeout" )
handler:close( ) -- forced disconnect?
else
_readtimes[ handler ] = _currenttime -- reset timer
end
end
end
end
-- Fire timers
if _currenttime - _timer >= math_min(next_timer_time, 1) then
next_timer_time = math_huge;
for i = 1, _timerlistlen do
local t = _timerlist[ i ]( _currenttime ) -- fire timers
if t then next_timer_time = math_min(next_timer_time, t); end
end
_timer = _currenttime
else
next_timer_time = next_timer_time - (_currenttime - _timer);
end
for server, paused_time in pairs( _fullservers ) do
if _currenttime - paused_time > _accepretry then
_fullservers[ server ] = nil;
server.resume();
end
end
-- wait some time (0 by default)
socket_sleep( _sleeptime )
until quitting;
if once and quitting == "once" then quitting = nil; return; end
closeall();
return "quitting"
end
local function step()
return loop(true);
end
local function get_backend()
return "select";
end
--// EXPERIMENTAL //--
local wrapclient = function( socket, ip, serverport, listeners, pattern, sslctx )
local handler, socket, err = wrapconnection( nil, listeners, socket, ip, serverport, "clientport", pattern, sslctx )
if not handler then return nil, err end
_socketlist[ socket ] = handler
if not sslctx then
_sendlistlen = addsocket(_sendlist, socket, _sendlistlen)
if listeners.onconnect then
-- When socket is writeable, call onconnect
local _sendbuffer = handler.sendbuffer;
handler.sendbuffer = function ()
handler.sendbuffer = _sendbuffer;
listeners.onconnect(handler);
return _sendbuffer(); -- Send any queued outgoing data
end
end
end
return handler, socket
end
local addclient = function( address, port, listeners, pattern, sslctx, typ )
local err
if type( listeners ) ~= "table" then
err = "invalid listener table"
elseif type ( address ) ~= "string" then
err = "invalid address"
elseif type( port ) ~= "number" or not ( port >= 0 and port <= 65535 ) then
err = "invalid port"
elseif sslctx and not has_luasec then
err = "luasec not found"
end
if not typ then
local addrinfo, err = getaddrinfo(address)
if not addrinfo then return nil, err end
if addrinfo[1] and addrinfo[1].family == "inet6" then
typ = "tcp6"
else
typ = "tcp"
end
end
local create = luasocket[typ]
if type( create ) ~= "function" then
err = "invalid socket type"
end
if err then
out_error( "server.lua, addclient: ", err )
return nil, err
end
local client, err = create( )
if err then
return nil, err
end
client:settimeout( 0 )
local ok, err = client:connect( address, port )
if ok or err == "timeout" then
return wrapclient( client, address, port, listeners, pattern, sslctx )
else
return nil, err
end
end
--// EXPERIMENTAL //--
----------------------------------// BEGIN //--
use "setmetatable" ( _socketlist, { __mode = "k" } )
use "setmetatable" ( _readtimes, { __mode = "k" } )
use "setmetatable" ( _writetimes, { __mode = "k" } )
_timer = luasocket_gettime( )
_starttime = luasocket_gettime( )
local function setlogger(new_logger)
local old_logger = log;
if new_logger then
log = new_logger;
end
return old_logger;
end
----------------------------------// PUBLIC INTERFACE //--
return {
_addtimer = addtimer,
addclient = addclient,
wrapclient = wrapclient,
loop = loop,
link = link,
step = step,
stats = stats,
closeall = closeall,
addserver = addserver,
getserver = getserver,
setlogger = setlogger,
getsettings = getsettings,
setquitting = setquitting,
removeserver = removeserver,
get_backend = get_backend,
changesettings = changesettings,
}
end)
package.preload['util.xmppstream'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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 lxp = require "lxp";
local st = require "util.stanza";
local stanza_mt = st.stanza_mt;
local error = error;
local tostring = tostring;
local t_insert = table.insert;
local t_concat = table.concat;
local t_remove = table.remove;
local setmetatable = setmetatable;
-- COMPAT: w/LuaExpat 1.1.0
local lxp_supports_doctype = pcall(lxp.new, { StartDoctypeDecl = false });
local lxp_supports_xmldecl = pcall(lxp.new, { XmlDecl = false });
local lxp_supports_bytecount = not not lxp.new({}).getcurrentbytecount;
local default_stanza_size_limit = 1024*1024*10; -- 10MB
local _ENV = nil;
local new_parser = lxp.new;
local xml_namespace = {
["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 xmlns_streams = "http://etherx.jabber.org/streams";
local ns_separator = "\1";
local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$";
local function dummy_cb() end
local function new_sax_handlers(session, stream_callbacks, cb_handleprogress)
local xml_handlers = {};
local cb_streamopened = stream_callbacks.streamopened;
local cb_streamclosed = stream_callbacks.streamclosed;
local cb_error = stream_callbacks.error or function(session, e, stanza) error("XML stream error: "..tostring(e)..(stanza and ": "..tostring(stanza) or ""),2); end;
local cb_handlestanza = stream_callbacks.handlestanza;
cb_handleprogress = cb_handleprogress or dummy_cb;
local stream_ns = stream_callbacks.stream_ns or xmlns_streams;
local stream_tag = stream_callbacks.stream_tag or "stream";
if stream_ns ~= "" then
stream_tag = stream_ns..ns_separator..stream_tag;
end
local stream_error_tag = stream_ns..ns_separator..(stream_callbacks.error_tag or "error");
local stream_default_ns = stream_callbacks.default_ns;
local stack = {};
local chardata, stanza = {};
local stanza_size = 0;
local non_streamns_depth = 0;
function xml_handlers:StartElement(tagname, attr)
if stanza and #chardata > 0 then
-- We have some character data in the buffer
t_insert(stanza, t_concat(chardata));
chardata = {};
end
local curr_ns,name = tagname:match(ns_pattern);
if name == "" then
curr_ns, name = "", curr_ns;
end
if curr_ns ~= stream_default_ns or non_streamns_depth > 0 then
attr.xmlns = curr_ns;
non_streamns_depth = non_streamns_depth + 1;
end
for i=1,#attr do
local k = attr[i];
attr[i] = nil;
local xmlk = xml_namespace[k];
if xmlk then
attr[xmlk] = attr[k];
attr[k] = nil;
end
end
if not stanza then --if we are not currently inside a stanza
if lxp_supports_bytecount then
stanza_size = self:getcurrentbytecount();
end
if session.notopen then
if tagname == stream_tag then
non_streamns_depth = 0;
if cb_streamopened then
if lxp_supports_bytecount then
cb_handleprogress(stanza_size);
stanza_size = 0;
end
cb_streamopened(session, attr);
end
else
-- Garbage before stream?
cb_error(session, "no-stream", tagname);
end
return;
end
if curr_ns == "jabber:client" and name ~= "iq" and name ~= "presence" and name ~= "message" then
cb_error(session, "invalid-top-level-element");
end
stanza = setmetatable({ name = name, attr = attr, tags = {} }, stanza_mt);
else -- we are inside a stanza, so add a tag
if lxp_supports_bytecount then
stanza_size = stanza_size + self:getcurrentbytecount();
end
t_insert(stack, stanza);
local oldstanza = stanza;
stanza = setmetatable({ name = name, attr = attr, tags = {} }, stanza_mt);
t_insert(oldstanza, stanza);
t_insert(oldstanza.tags, stanza);
end
end
if lxp_supports_xmldecl then
function xml_handlers:XmlDecl(version, encoding, standalone)
if lxp_supports_bytecount then
cb_handleprogress(self:getcurrentbytecount());
end
end
end
function xml_handlers:StartCdataSection()
if lxp_supports_bytecount then
if stanza then
stanza_size = stanza_size + self:getcurrentbytecount();
else
cb_handleprogress(self:getcurrentbytecount());
end
end
end
function xml_handlers:EndCdataSection()
if lxp_supports_bytecount then
if stanza then
stanza_size = stanza_size + self:getcurrentbytecount();
else
cb_handleprogress(self:getcurrentbytecount());
end
end
end
function xml_handlers:CharacterData(data)
if stanza then
if lxp_supports_bytecount then
stanza_size = stanza_size + self:getcurrentbytecount();
end
t_insert(chardata, data);
elseif lxp_supports_bytecount then
cb_handleprogress(self:getcurrentbytecount());
end
end
function xml_handlers:EndElement(tagname)
if lxp_supports_bytecount then
stanza_size = stanza_size + self:getcurrentbytecount()
end
if non_streamns_depth > 0 then
non_streamns_depth = non_streamns_depth - 1;
end
if stanza then
if #chardata > 0 then
-- We have some character data in the buffer
t_insert(stanza, t_concat(chardata));
chardata = {};
end
-- Complete stanza
if #stack == 0 then
if lxp_supports_bytecount then
cb_handleprogress(stanza_size);
end
stanza_size = 0;
if tagname ~= stream_error_tag then
cb_handlestanza(session, stanza);
else
cb_error(session, "stream-error", stanza);
end
stanza = nil;
else
stanza = t_remove(stack);
end
else
if cb_streamclosed then
cb_streamclosed(session);
end
end
end
local function restricted_handler(parser)
cb_error(session, "parse-error", "restricted-xml", "Restricted XML, see RFC 6120 section 11.1.");
if not parser.stop or not parser:stop() then
error("Failed to abort parsing");
end
end
if lxp_supports_doctype then
xml_handlers.StartDoctypeDecl = restricted_handler;
end
xml_handlers.Comment = restricted_handler;
xml_handlers.ProcessingInstruction = restricted_handler;
local function reset()
stanza, chardata, stanza_size = nil, {}, 0;
stack = {};
end
local function set_session(stream, new_session)
session = new_session;
end
return xml_handlers, { reset = reset, set_session = set_session };
end
local function new(session, stream_callbacks, stanza_size_limit)
-- Used to track parser progress (e.g. to enforce size limits)
local n_outstanding_bytes = 0;
local handle_progress;
if lxp_supports_bytecount then
function handle_progress(n_parsed_bytes)
n_outstanding_bytes = n_outstanding_bytes - n_parsed_bytes;
end
stanza_size_limit = stanza_size_limit or default_stanza_size_limit;
elseif stanza_size_limit then
error("Stanza size limits are not supported on this version of LuaExpat")
end
local handlers, meta = new_sax_handlers(session, stream_callbacks, handle_progress);
local parser = new_parser(handlers, ns_separator, false);
local parse = parser.parse;
function session.open_stream(session, from, to)
local send = session.sends2s or session.send;
local attr = {
["xmlns:stream"] = "http://etherx.jabber.org/streams",
["xml:lang"] = "en",
xmlns = stream_callbacks.default_ns,
version = session.version and (session.version > 0 and "1.0" or nil),
id = session.streamid,
from = from or session.host, to = to,
};
if session.stream_attrs then
session:stream_attrs(from, to, attr)
end
send("<?xml version='1.0'?>");
send(st.stanza("stream:stream", attr):top_tag());
return true;
end
return {
reset = function ()
parser = new_parser(handlers, ns_separator, false);
parse = parser.parse;
n_outstanding_bytes = 0;
meta.reset();
end,
feed = function (self, data)
if lxp_supports_bytecount then
n_outstanding_bytes = n_outstanding_bytes + #data;
end
local ok, err = parse(parser, data);
if lxp_supports_bytecount and n_outstanding_bytes > stanza_size_limit then
return nil, "stanza-too-large";
end
return ok, err;
end,
set_session = meta.set_session;
};
end
return {
ns_separator = ns_separator;
ns_pattern = ns_pattern;
new_sax_handlers = new_sax_handlers;
new = new;
};
end)
package.preload['util.jid'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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 select = select;
local match, sub = string.match, string.sub;
local nodeprep = require "util.encodings".stringprep.nodeprep;
local nameprep = require "util.encodings".stringprep.nameprep;
local resourceprep = require "util.encodings".stringprep.resourceprep;
local escapes = {
[" "] = "\\20"; ['"'] = "\\22";
["&"] = "\\26"; ["'"] = "\\27";
["/"] = "\\2f"; [":"] = "\\3a";
["<"] = "\\3c"; [">"] = "\\3e";
["@"] = "\\40"; ["\\"] = "\\5c";
};
local unescapes = {};
for k,v in pairs(escapes) do unescapes[v] = k; end
local _ENV = nil;
local function split(jid)
if not jid then return; end
local node, nodepos = match(jid, "^([^@/]+)@()");
local host, hostpos = match(jid, "^([^@/]+)()", nodepos)
if node and not host then return nil, nil, nil; end
local resource = match(jid, "^/(.+)$", hostpos);
if (not host) or ((not resource) and #jid >= hostpos) then return nil, nil, nil; end
return node, host, resource;
end
local function bare(jid)
local node, host = split(jid);
if node and host then
return node.."@"..host;
end
return host;
end
local function prepped_split(jid)
local node, host, resource = split(jid);
if host and host ~= "." then
if sub(host, -1, -1) == "." then -- Strip empty root label
host = sub(host, 1, -2);
end
host = nameprep(host);
if not host then return; end
if node then
node = nodeprep(node);
if not node then return; end
end
if resource then
resource = resourceprep(resource);
if not resource then return; end
end
return node, host, resource;
end
end
local function join(node, host, resource)
if not host then return end
if node and resource then
return node.."@"..host.."/"..resource;
elseif node then
return node.."@"..host;
elseif resource then
return host.."/"..resource;
end
return host;
end
local function prep(jid)
local node, host, resource = prepped_split(jid);
return join(node, host, resource);
end
local function compare(jid, acl)
-- compare jid to single acl rule
-- TODO compare to table of rules?
local jid_node, jid_host, jid_resource = split(jid);
local acl_node, acl_host, acl_resource = split(acl);
if ((acl_node ~= nil and acl_node == jid_node) or acl_node == nil) and
((acl_host ~= nil and acl_host == jid_host) or acl_host == nil) and
((acl_resource ~= nil and acl_resource == jid_resource) or acl_resource == nil) then
return true
end
return false
end
local function node(jid)
return (select(1, split(jid)));
end
local function host(jid)
return (select(2, split(jid)));
end
local function resource(jid)
return (select(3, split(jid)));
end
local function escape(s) return s and (s:gsub(".", escapes)); end
local function unescape(s) return s and (s:gsub("\\%x%x", unescapes)); end
return {
split = split;
bare = bare;
prepped_split = prepped_split;
join = join;
prep = prep;
compare = compare;
node = node;
host = host;
resource = resource;
escape = escape;
unescape = unescape;
};
end)
package.preload['util.events'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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 pairs = pairs;
local t_insert = table.insert;
local t_remove = table.remove;
local t_sort = table.sort;
local setmetatable = setmetatable;
local next = next;
local _ENV = nil;
local function new()
-- Map event name to ordered list of handlers (lazily built): handlers[event_name] = array_of_handler_functions
local handlers = {};
-- Array of wrapper functions that wrap all events (nil if empty)
local global_wrappers;
-- Per-event wrappers: wrappers[event_name] = wrapper_function
local wrappers = {};
-- Event map: event_map[handler_function] = priority_number
local event_map = {};
-- Called on-demand to build handlers entries
local function _rebuild_index(handlers, event)
local _handlers = event_map[event];
if not _handlers or next(_handlers) == nil then return; end
local index = {};
for handler in pairs(_handlers) do
t_insert(index, handler);
end
t_sort(index, function(a, b) return _handlers[a] > _handlers[b]; end);
handlers[event] = index;
return index;
end;
setmetatable(handlers, { __index = _rebuild_index });
local function add_handler(event, handler, priority)
local map = event_map[event];
if map then
map[handler] = priority or 0;
else
map = {[handler] = priority or 0};
event_map[event] = map;
end
handlers[event] = nil;
end;
local function remove_handler(event, handler)
local map = event_map[event];
if map then
map[handler] = nil;
handlers[event] = nil;
if next(map) == nil then
event_map[event] = nil;
end
end
end;
local function get_handlers(event)
return handlers[event];
end;
local function add_handlers(handlers)
for event, handler in pairs(handlers) do
add_handler(event, handler);
end
end;
local function remove_handlers(handlers)
for event, handler in pairs(handlers) do
remove_handler(event, handler);
end
end;
local function _fire_event(event_name, event_data)
local h = handlers[event_name];
if h then
for i=1,#h do
local ret = h[i](event_data);
if ret ~= nil then return ret; end
end
end
end;
local function fire_event(event_name, event_data)
local w = wrappers[event_name] or global_wrappers;
if w then
local curr_wrapper = #w;
local function c(event_name, event_data)
curr_wrapper = curr_wrapper - 1;
if curr_wrapper == 0 then
if global_wrappers == nil or w == global_wrappers then
return _fire_event(event_name, event_data);
end
w, curr_wrapper = global_wrappers, #global_wrappers;
return w[curr_wrapper](c, event_name, event_data);
else
return w[curr_wrapper](c, event_name, event_data);
end
end
return w[curr_wrapper](c, event_name, event_data);
end
return _fire_event(event_name, event_data);
end
local function add_wrapper(event_name, wrapper)
local w;
if event_name == false then
w = global_wrappers;
if not w then
w = {};
global_wrappers = w;
end
else
w = wrappers[event_name];
if not w then
w = {};
wrappers[event_name] = w;
end
end
w[#w+1] = wrapper;
end
local function remove_wrapper(event_name, wrapper)
local w;
if event_name == false then
w = global_wrappers;
else
w = wrappers[event_name];
end
if not w then return; end
for i = #w, 1, -1 do
if w[i] == wrapper then
t_remove(w, i);
end
end
if #w == 0 then
if event_name == false then
global_wrappers = nil;
else
wrappers[event_name] = nil;
end
end
end
return {
add_handler = add_handler;
remove_handler = remove_handler;
add_handlers = add_handlers;
remove_handlers = remove_handlers;
get_handlers = get_handlers;
wrappers = {
add_handler = add_wrapper;
remove_handler = remove_wrapper;
};
add_wrapper = add_wrapper;
remove_wrapper = remove_wrapper;
fire_event = fire_event;
_handlers = handlers;
_event_map = event_map;
};
end
return {
new = new;
};
end)
package.preload['util.dataforms'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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 <value> 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 };
}
--]=]
end)
package.preload['util.caps'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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 base64 = require "util.encodings".base64.encode;
local sha1 = require "util.hashes".sha1;
local t_insert, t_sort, t_concat = table.insert, table.sort, table.concat;
local ipairs = ipairs;
local _ENV = nil;
local function calculate_hash(disco_info)
local identities, features, extensions = {}, {}, {};
for _, tag in ipairs(disco_info) do
if tag.name == "identity" then
t_insert(identities, (tag.attr.category or "").."\0"..(tag.attr.type or "").."\0"..(tag.attr["xml:lang"] or "").."\0"..(tag.attr.name or ""));
elseif tag.name == "feature" then
t_insert(features, tag.attr.var or "");
elseif tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then
local form = {};
local FORM_TYPE;
for _, field in ipairs(tag.tags) do
if field.name == "field" and field.attr.var then
local values = {};
for _, val in ipairs(field.tags) do
val = #val.tags == 0 and val:get_text();
if val then t_insert(values, val); end
end
t_sort(values);
if field.attr.var == "FORM_TYPE" then
FORM_TYPE = values[1];
elseif #values > 0 then
t_insert(form, field.attr.var.."\0"..t_concat(values, "<"));
else
t_insert(form, field.attr.var);
end
end
end
t_sort(form);
form = t_concat(form, "<");
if FORM_TYPE then form = FORM_TYPE.."\0"..form; end
t_insert(extensions, form);
end
end
t_sort(identities);
t_sort(features);
t_sort(extensions);
if #identities > 0 then identities = t_concat(identities, "<"):gsub("%z", "/").."<"; else identities = ""; end
if #features > 0 then features = t_concat(features, "<").."<"; else features = ""; end
if #extensions > 0 then extensions = t_concat(extensions, "<"):gsub("%z", "<").."<"; else extensions = ""; end
local S = identities..features..extensions;
local ver = base64(sha1(S));
return ver, S;
end
return {
calculate_hash = calculate_hash;
};
end)
package.preload['util.vcard'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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;
};
end)
package.preload['util.logger'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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.
--
-- luacheck: ignore 213/level
local pairs = pairs;
local _ENV = nil;
local level_sinks = {};
local make_logger;
local function init(name)
local log_debug = make_logger(name, "debug");
local log_info = make_logger(name, "info");
local log_warn = make_logger(name, "warn");
local log_error = make_logger(name, "error");
return function (level, message, ...)
if level == "debug" then
return log_debug(message, ...);
elseif level == "info" then
return log_info(message, ...);
elseif level == "warn" then
return log_warn(message, ...);
elseif level == "error" then
return log_error(message, ...);
end
end
end
function make_logger(source_name, level)
local level_handlers = level_sinks[level];
if not level_handlers then
level_handlers = {};
level_sinks[level] = level_handlers;
end
local logger = function (message, ...)
for i = 1,#level_handlers do
level_handlers[i](source_name, level, message, ...);
end
end
return logger;
end
local function reset()
for level, handler_list in pairs(level_sinks) do
-- Clear all handlers for this level
for i = 1, #handler_list do
handler_list[i] = nil;
end
end
end
local function add_level_sink(level, sink_function)
if not level_sinks[level] then
level_sinks[level] = { sink_function };
else
level_sinks[level][#level_sinks[level] + 1 ] = sink_function;
end
end
return {
init = init;
make_logger = make_logger;
reset = reset;
add_level_sink = add_level_sink;
new = make_logger;
};
end)
package.preload['util.datetime'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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.
--
-- XEP-0082: XMPP Date and Time Profiles
local os_date = os.date;
local os_time = os.time;
local os_difftime = os.difftime;
local tonumber = tonumber;
local _ENV = nil;
local function date(t)
return os_date("!%Y-%m-%d", t);
end
local function datetime(t)
return os_date("!%Y-%m-%dT%H:%M:%SZ", t);
end
local function time(t)
return os_date("!%H:%M:%S", t);
end
local function legacy(t)
return os_date("!%Y%m%dT%H:%M:%S", t);
end
local function parse(s)
if s then
local year, month, day, hour, min, sec, tzd;
year, month, day, hour, min, sec, tzd = s:match("^(%d%d%d%d)%-?(%d%d)%-?(%d%d)T(%d%d):(%d%d):(%d%d)%.?%d*([Z+%-]?.*)$");
if year then
local time_offset = os_difftime(os_time(os_date("*t")), os_time(os_date("!*t"))); -- to deal with local timezone
local tzd_offset = 0;
if tzd ~= "" and tzd ~= "Z" then
local sign, h, m = tzd:match("([+%-])(%d%d):?(%d*)");
if not sign then return; end
if #m ~= 2 then m = "0"; end
h, m = tonumber(h), tonumber(m);
tzd_offset = h * 60 * 60 + m * 60;
if sign == "-" then tzd_offset = -tzd_offset; end
end
sec = (sec + time_offset) - tzd_offset;
return os_time({year=year, month=month, day=day, hour=hour, min=min, sec=sec, isdst=false});
end
end
end
return {
date = date;
datetime = datetime;
time = time;
legacy = legacy;
parse = parse;
};
end)
package.preload['util.json'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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 t_insert, t_concat, t_remove, t_sort = table.insert, table.concat, table.remove, table.sort;
local s_char = string.char;
local tostring, tonumber = tostring, tonumber;
local pairs, ipairs = pairs, ipairs;
local next = next;
local getmetatable, setmetatable = getmetatable, setmetatable;
local print = print;
local has_array, array = pcall(require, "util.array");
local array_mt = has_array and getmetatable(array()) or {};
--module("json")
local module = {};
local null = setmetatable({}, { __tostring = function() return "null"; end; });
module.null = null;
local escapes = {
["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b",
["\f"] = "\\f", ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"};
local unescapes = {
["\""] = "\"", ["\\"] = "\\", ["/"] = "/",
b = "\b", f = "\f", n = "\n", r = "\r", t = "\t"};
for i=0,31 do
local ch = s_char(i);
if not escapes[ch] then escapes[ch] = ("\\u%.4X"):format(i); end
end
local function codepoint_to_utf8(code)
if code < 0x80 then return s_char(code); end
local bits0_6 = code % 64;
if code < 0x800 then
local bits6_5 = (code - bits0_6) / 64;
return s_char(0x80 + 0x40 + bits6_5, 0x80 + bits0_6);
end
local bits0_12 = code % 4096;
local bits6_6 = (bits0_12 - bits0_6) / 64;
local bits12_4 = (code - bits0_12) / 4096;
return s_char(0x80 + 0x40 + 0x20 + bits12_4, 0x80 + bits6_6, 0x80 + bits0_6);
end
local valid_types = {
number = true,
string = true,
table = true,
boolean = true
};
local special_keys = {
__array = true;
__hash = true;
};
local simplesave, tablesave, arraysave, stringsave;
function stringsave(o, buffer)
-- FIXME do proper utf-8 and binary data detection
t_insert(buffer, "\""..(o:gsub(".", escapes)).."\"");
end
function arraysave(o, buffer)
t_insert(buffer, "[");
if next(o) then
for _, v in ipairs(o) do
simplesave(v, buffer);
t_insert(buffer, ",");
end
t_remove(buffer);
end
t_insert(buffer, "]");
end
function tablesave(o, buffer)
local __array = {};
local __hash = {};
local hash = {};
for i,v in ipairs(o) do
__array[i] = v;
end
for k,v in pairs(o) do
local ktype, vtype = type(k), type(v);
if valid_types[vtype] or v == null then
if ktype == "string" and not special_keys[k] then
hash[k] = v;
elseif (valid_types[ktype] or k == null) and __array[k] == nil then
__hash[k] = v;
end
end
end
if next(__hash) ~= nil or next(hash) ~= nil or next(__array) == nil then
t_insert(buffer, "{");
local mark = #buffer;
if buffer.ordered then
local keys = {};
for k in pairs(hash) do
t_insert(keys, k);
end
t_sort(keys);
for _,k in ipairs(keys) do
stringsave(k, buffer);
t_insert(buffer, ":");
simplesave(hash[k], buffer);
t_insert(buffer, ",");
end
else
for k,v in pairs(hash) do
stringsave(k, buffer);
t_insert(buffer, ":");
simplesave(v, buffer);
t_insert(buffer, ",");
end
end
if next(__hash) ~= nil then
t_insert(buffer, "\"__hash\":[");
for k,v in pairs(__hash) do
simplesave(k, buffer);
t_insert(buffer, ",");
simplesave(v, buffer);
t_insert(buffer, ",");
end
t_remove(buffer);
t_insert(buffer, "]");
t_insert(buffer, ",");
end
if next(__array) then
t_insert(buffer, "\"__array\":");
arraysave(__array, buffer);
t_insert(buffer, ",");
end
if mark ~= #buffer then t_remove(buffer); end
t_insert(buffer, "}");
else
arraysave(__array, buffer);
end
end
function simplesave(o, buffer)
local t = type(o);
if o == null then
t_insert(buffer, "null");
elseif t == "number" then
t_insert(buffer, tostring(o));
elseif t == "string" then
stringsave(o, buffer);
elseif t == "table" then
local mt = getmetatable(o);
if mt == array_mt then
arraysave(o, buffer);
else
tablesave(o, buffer);
end
elseif t == "boolean" then
t_insert(buffer, (o and "true" or "false"));
else
t_insert(buffer, "null");
end
end
function module.encode(obj)
local t = {};
simplesave(obj, t);
return t_concat(t);
end
function module.encode_ordered(obj)
local t = { ordered = true };
simplesave(obj, t);
return t_concat(t);
end
function module.encode_array(obj)
local t = {};
arraysave(obj, t);
return t_concat(t);
end
-----------------------------------
local function _skip_whitespace(json, index)
return json:find("[^ \t\r\n]", index) or index; -- no need to check \r\n, we converted those to \t
end
local function _fixobject(obj)
local __array = obj.__array;
if __array then
obj.__array = nil;
for _, v in ipairs(__array) do
t_insert(obj, v);
end
end
local __hash = obj.__hash;
if __hash then
obj.__hash = nil;
local k;
for _, v in ipairs(__hash) do
if k ~= nil then
obj[k] = v; k = nil;
else
k = v;
end
end
end
return obj;
end
local _readvalue, _readstring;
local function _readobject(json, index)
local o = {};
while true do
local key, val;
index = _skip_whitespace(json, index + 1);
if json:byte(index) ~= 0x22 then -- "\""
if json:byte(index) == 0x7d then return o, index + 1; end -- "}"
return nil, "key expected";
end
key, index = _readstring(json, index);
if key == nil then return nil, index; end
index = _skip_whitespace(json, index);
if json:byte(index) ~= 0x3a then return nil, "colon expected"; end -- ":"
val, index = _readvalue(json, index + 1);
if val == nil then return nil, index; end
o[key] = val;
index = _skip_whitespace(json, index);
local b = json:byte(index);
if b == 0x7d then return _fixobject(o), index + 1; end -- "}"
if b ~= 0x2c then return nil, "object eof"; end -- ","
end
end
local function _readarray(json, index)
local a = {};
local oindex = index;
while true do
local val;
val, index = _readvalue(json, index + 1);
if val == nil then
if json:byte(oindex + 1) == 0x5d then return setmetatable(a, array_mt), oindex + 2; end -- "]"
return val, index;
end
t_insert(a, val);
index = _skip_whitespace(json, index);
local b = json:byte(index);
if b == 0x5d then return setmetatable(a, array_mt), index + 1; end -- "]"
if b ~= 0x2c then return nil, "array eof"; end -- ","
end
end
local _unescape_error;
local function _unescape_surrogate_func(x)
local lead, trail = tonumber(x:sub(3, 6), 16), tonumber(x:sub(9, 12), 16);
local codepoint = lead * 0x400 + trail - 0x35FDC00;
local a = codepoint % 64;
codepoint = (codepoint - a) / 64;
local b = codepoint % 64;
codepoint = (codepoint - b) / 64;
local c = codepoint % 64;
codepoint = (codepoint - c) / 64;
return s_char(0xF0 + codepoint, 0x80 + c, 0x80 + b, 0x80 + a);
end
local function _unescape_func(x)
x = x:match("%x%x%x%x", 3);
if x then
--if x >= 0xD800 and x <= 0xDFFF then _unescape_error = true; end -- bad surrogate pair
return codepoint_to_utf8(tonumber(x, 16));
end
_unescape_error = true;
end
function _readstring(json, index)
index = index + 1;
local endindex = json:find("\"", index, true);
if endindex then
local s = json:sub(index, endindex - 1);
--if s:find("[%z-\31]") then return nil, "control char in string"; end
-- FIXME handle control characters
_unescape_error = nil;
--s = s:gsub("\\u[dD][89abAB]%x%x\\u[dD][cdefCDEF]%x%x", _unescape_surrogate_func);
-- FIXME handle escapes beyond BMP
s = s:gsub("\\u.?.?.?.?", _unescape_func);
if _unescape_error then return nil, "invalid escape"; end
return s, endindex + 1;
end
return nil, "string eof";
end
local function _readnumber(json, index)
local m = json:match("[0-9%.%-eE%+]+", index); -- FIXME do strict checking
return tonumber(m), index + #m;
end
local function _readnull(json, index)
local a, b, c = json:byte(index + 1, index + 3);
if a == 0x75 and b == 0x6c and c == 0x6c then
return null, index + 4;
end
return nil, "null parse failed";
end
local function _readtrue(json, index)
local a, b, c = json:byte(index + 1, index + 3);
if a == 0x72 and b == 0x75 and c == 0x65 then
return true, index + 4;
end
return nil, "true parse failed";
end
local function _readfalse(json, index)
local a, b, c, d = json:byte(index + 1, index + 4);
if a == 0x61 and b == 0x6c and c == 0x73 and d == 0x65 then
return false, index + 5;
end
return nil, "false parse failed";
end
function _readvalue(json, index)
index = _skip_whitespace(json, index);
local b = json:byte(index);
-- TODO try table lookup instead of if-else?
if b == 0x7B then -- "{"
return _readobject(json, index);
elseif b == 0x5B then -- "["
return _readarray(json, index);
elseif b == 0x22 then -- "\""
return _readstring(json, index);
elseif b ~= nil and b >= 0x30 and b <= 0x39 or b == 0x2d then -- "0"-"9" or "-"
return _readnumber(json, index);
elseif b == 0x6e then -- "n"
return _readnull(json, index);
elseif b == 0x74 then -- "t"
return _readtrue(json, index);
elseif b == 0x66 then -- "f"
return _readfalse(json, index);
else
return nil, "value expected";
end
end
local first_escape = {
["\\\""] = "\\u0022";
["\\\\"] = "\\u005c";
["\\/" ] = "\\u002f";
["\\b" ] = "\\u0008";
["\\f" ] = "\\u000C";
["\\n" ] = "\\u000A";
["\\r" ] = "\\u000D";
["\\t" ] = "\\u0009";
["\\u" ] = "\\u";
};
function module.decode(json)
json = json:gsub("\\.", first_escape) -- get rid of all escapes except \uXXXX, making string parsing much simpler
--:gsub("[\r\n]", "\t"); -- \r\n\t are equivalent, we care about none of them, and none of them can be in strings
-- TODO do encoding verification
local val, index = _readvalue(json, 1);
if val == nil then return val, index; end
if json:find("[^ \t\r\n]", index) then return nil, "garbage at eof"; end
return val;
end
function module.test(object)
local encoded = module.encode(object);
local decoded = module.decode(encoded);
local recoded = module.encode(decoded);
if encoded ~= recoded then
print("FAILED");
print("encoded:", encoded);
print("recoded:", recoded);
else
print(encoded);
end
return encoded == recoded;
end
return module;
end)
package.preload['util.xml'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
local st = require "util.stanza";
local lxp = require "lxp";
local _ENV = nil;
local parse_xml = (function()
local ns_prefixes = {
["http://www.w3.org/XML/1998/namespace"] = "xml";
};
local ns_separator = "\1";
local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$";
return function(xml)
--luacheck: ignore 212/self
local handler = {};
local stanza = st.stanza("root");
function handler:StartElement(tagname, attr)
local curr_ns,name = tagname:match(ns_pattern);
if name == "" then
curr_ns, name = "", curr_ns;
end
if curr_ns ~= "" then
attr.xmlns = curr_ns;
end
for i=1,#attr do
local k = attr[i];
attr[i] = nil;
local ns, nm = k:match(ns_pattern);
if nm ~= "" then
ns = ns_prefixes[ns];
if ns then
attr[ns..":"..nm] = attr[k];
attr[k] = nil;
end
end
end
stanza:tag(name, attr);
end
function handler:CharacterData(data)
stanza:text(data);
end
function handler:EndElement()
stanza:up();
end
local parser = lxp.new(handler, "\1");
local ok, err, line, col = parser:parse(xml);
if ok then ok, err, line, col = parser:parse(); end
--parser:close();
if ok then
return stanza.tags[1];
else
return ok, err.." (line "..line..", col "..col..")";
end
end;
end)();
return {
parse = parse_xml;
};
end)
package.preload['util.rsm'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
local stanza = require"util.stanza".stanza;
local tostring, tonumber = tostring, tonumber;
local type = type;
local pairs = pairs;
local xmlns_rsm = 'http://jabber.org/protocol/rsm';
local element_parsers = {};
do
local parsers = element_parsers;
local function xs_int(st)
return tonumber((st:get_text()));
end
local function xs_string(st)
return st:get_text();
end
parsers.after = xs_string;
parsers.before = function(st)
local text = st:get_text();
return text == "" or text;
end;
parsers.max = xs_int;
parsers.index = xs_int;
parsers.first = function(st)
return { index = tonumber(st.attr.index); st:get_text() };
end;
parsers.last = xs_string;
parsers.count = xs_int;
end
local element_generators = setmetatable({
first = function(st, data)
if type(data) == "table" then
st:tag("first", { index = data.index }):text(data[1]):up();
else
st:tag("first"):text(tostring(data)):up();
end
end;
before = function(st, data)
if data == true then
st:tag("before"):up();
else
st:tag("before"):text(tostring(data)):up();
end
end
}, {
__index = function(_, name)
return function(st, data)
st:tag(name):text(tostring(data)):up();
end
end;
});
local function parse(set)
local rs = {};
for tag in set:childtags() do
local name = tag.name;
local parser = name and element_parsers[name];
if parser then
rs[name] = parser(tag);
end
end
return rs;
end
local function generate(t)
local st = stanza("set", { xmlns = xmlns_rsm });
for k,v in pairs(t) do
if element_parsers[k] then
element_generators[k](st, v);
end
end
return st;
end
local function get(st)
local set = st:get_child("set", xmlns_rsm);
if set and #set.tags > 0 then
return parse(set);
end
end
return { parse = parse, generate = generate, get = get };
end)
package.preload['util.random'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- Prosody IM
-- Copyright (C) 2008-2014 Matthew Wild
-- Copyright (C) 2008-2014 Waqas Hussain
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local urandom = io.open("/dev/urandom", "r");
if urandom then
return {
seed = function () end;
bytes = function (n) return urandom:read(n); end
};
end
local crypto = require "crypto"
return crypto.rand;
end)
package.preload['util.ip'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- Prosody IM
-- Copyright (C) 2008-2011 Florian Zeitz
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local ip_methods = {};
local ip_mt = { __index = function (ip, key) return (ip_methods[key])(ip); end,
__tostring = function (ip) return ip.addr; end,
__eq = function (ipA, ipB) return ipA.addr == ipB.addr; end};
local hex2bits = { ["0"] = "0000", ["1"] = "0001", ["2"] = "0010", ["3"] = "0011", ["4"] = "0100", ["5"] = "0101", ["6"] = "0110", ["7"] = "0111", ["8"] = "1000", ["9"] = "1001", ["A"] = "1010", ["B"] = "1011", ["C"] = "1100", ["D"] = "1101", ["E"] = "1110", ["F"] = "1111" };
local function new_ip(ipStr, proto)
if not proto then
local sep = ipStr:match("^%x+(.)");
if sep == ":" or (not(sep) and ipStr:sub(1,1) == ":") then
proto = "IPv6"
elseif sep == "." then
proto = "IPv4"
end
if not proto then
return nil, "invalid address";
end
elseif proto ~= "IPv4" and proto ~= "IPv6" then
return nil, "invalid protocol";
end
local zone;
if proto == "IPv6" and ipStr:find('%', 1, true) then
ipStr, zone = ipStr:match("^(.-)%%(.*)");
end
if proto == "IPv6" and ipStr:find('.', 1, true) then
local changed;
ipStr, changed = ipStr:gsub(":(%d+)%.(%d+)%.(%d+)%.(%d+)$", function(a,b,c,d)
return (":%04X:%04X"):format(a*256+b,c*256+d);
end);
if changed ~= 1 then return nil, "invalid-address"; end
end
return setmetatable({ addr = ipStr, proto = proto, zone = zone }, ip_mt);
end
local function toBits(ip)
local result = "";
local fields = {};
if ip.proto == "IPv4" then
ip = ip.toV4mapped;
end
ip = (ip.addr):upper();
ip:gsub("([^:]*):?", function (c) fields[#fields + 1] = c end);
if not ip:match(":$") then fields[#fields] = nil; end
for i, field in ipairs(fields) do
if field:len() == 0 and i ~= 1 and i ~= #fields then
for _ = 1, 16 * (9 - #fields) do
result = result .. "0";
end
else
for _ = 1, 4 - field:len() do
result = result .. "0000";
end
for j = 1, field:len() do
result = result .. hex2bits[field:sub(j, j)];
end
end
end
return result;
end
local function commonPrefixLength(ipA, ipB)
ipA, ipB = toBits(ipA), toBits(ipB);
for i = 1, 128 do
if ipA:sub(i,i) ~= ipB:sub(i,i) then
return i-1;
end
end
return 128;
end
local function v4scope(ip)
local fields = {};
ip:gsub("([^.]*).?", function (c) fields[#fields + 1] = tonumber(c) end);
-- Loopback:
if fields[1] == 127 then
return 0x2;
-- Link-local unicast:
elseif fields[1] == 169 and fields[2] == 254 then
return 0x2;
-- Global unicast:
else
return 0xE;
end
end
local function v6scope(ip)
-- Loopback:
if ip:match("^[0:]*1$") then
return 0x2;
-- Link-local unicast:
elseif ip:match("^[Ff][Ee][89ABab]") then
return 0x2;
-- Site-local unicast:
elseif ip:match("^[Ff][Ee][CcDdEeFf]") then
return 0x5;
-- Multicast:
elseif ip:match("^[Ff][Ff]") then
return tonumber("0x"..ip:sub(4,4));
-- Global unicast:
else
return 0xE;
end
end
local function label(ip)
if commonPrefixLength(ip, new_ip("::1", "IPv6")) == 128 then
return 0;
elseif commonPrefixLength(ip, new_ip("2002::", "IPv6")) >= 16 then
return 2;
elseif commonPrefixLength(ip, new_ip("2001::", "IPv6")) >= 32 then
return 5;
elseif commonPrefixLength(ip, new_ip("fc00::", "IPv6")) >= 7 then
return 13;
elseif commonPrefixLength(ip, new_ip("fec0::", "IPv6")) >= 10 then
return 11;
elseif commonPrefixLength(ip, new_ip("3ffe::", "IPv6")) >= 16 then
return 12;
elseif commonPrefixLength(ip, new_ip("::", "IPv6")) >= 96 then
return 3;
elseif commonPrefixLength(ip, new_ip("::ffff:0:0", "IPv6")) >= 96 then
return 4;
else
return 1;
end
end
local function precedence(ip)
if commonPrefixLength(ip, new_ip("::1", "IPv6")) == 128 then
return 50;
elseif commonPrefixLength(ip, new_ip("2002::", "IPv6")) >= 16 then
return 30;
elseif commonPrefixLength(ip, new_ip("2001::", "IPv6")) >= 32 then
return 5;
elseif commonPrefixLength(ip, new_ip("fc00::", "IPv6")) >= 7 then
return 3;
elseif commonPrefixLength(ip, new_ip("fec0::", "IPv6")) >= 10 then
return 1;
elseif commonPrefixLength(ip, new_ip("3ffe::", "IPv6")) >= 16 then
return 1;
elseif commonPrefixLength(ip, new_ip("::", "IPv6")) >= 96 then
return 1;
elseif commonPrefixLength(ip, new_ip("::ffff:0:0", "IPv6")) >= 96 then
return 35;
else
return 40;
end
end
local function toV4mapped(ip)
local fields = {};
local ret = "::ffff:";
ip:gsub("([^.]*).?", function (c) fields[#fields + 1] = tonumber(c) end);
ret = ret .. ("%02x"):format(fields[1]);
ret = ret .. ("%02x"):format(fields[2]);
ret = ret .. ":"
ret = ret .. ("%02x"):format(fields[3]);
ret = ret .. ("%02x"):format(fields[4]);
return new_ip(ret, "IPv6");
end
function ip_methods:toV4mapped()
if self.proto ~= "IPv4" then return nil, "No IPv4 address" end
local value = toV4mapped(self.addr);
self.toV4mapped = value;
return value;
end
function ip_methods:label()
local value;
if self.proto == "IPv4" then
value = label(self.toV4mapped);
else
value = label(self);
end
self.label = value;
return value;
end
function ip_methods:precedence()
local value;
if self.proto == "IPv4" then
value = precedence(self.toV4mapped);
else
value = precedence(self);
end
self.precedence = value;
return value;
end
function ip_methods:scope()
local value;
if self.proto == "IPv4" then
value = v4scope(self.addr);
else
value = v6scope(self.addr);
end
self.scope = value;
return value;
end
function ip_methods:private()
local private = self.scope ~= 0xE;
if not private and self.proto == "IPv4" then
local ip = self.addr;
local fields = {};
ip:gsub("([^.]*).?", function (c) fields[#fields + 1] = tonumber(c) end);
if fields[1] == 127 or fields[1] == 10 or (fields[1] == 192 and fields[2] == 168)
or (fields[1] == 172 and (fields[2] >= 16 or fields[2] <= 32)) then
private = true;
end
end
self.private = private;
return private;
end
local function parse_cidr(cidr)
local bits;
local ip_len = cidr:find("/", 1, true);
if ip_len then
bits = tonumber(cidr:sub(ip_len+1, -1));
cidr = cidr:sub(1, ip_len-1);
end
return new_ip(cidr), bits;
end
local function match(ipA, ipB, bits)
local common_bits = commonPrefixLength(ipA, ipB);
if bits and ipB.proto == "IPv4" then
common_bits = common_bits - 96; -- v6 mapped addresses always share these bits
end
return common_bits >= (bits or 128);
end
return {new_ip = new_ip,
commonPrefixLength = commonPrefixLength,
parse_cidr = parse_cidr,
match=match};
end)
package.preload['util.time'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- Import gettime() from LuaSocket, as a way to access high-resolution time
-- in a platform-independent way
local socket_gettime = require "socket".gettime;
return {
now = socket_gettime;
}
end)
package.preload['util.sasl.scram'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
local base64, unbase64 = require "mime".b64, require"mime".unb64;
local hashes = require"util.hashes";
local bit = require"bit";
local random = require"util.random";
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 H, HMAC = hashes.sha1, hashes.hmac_sha1;
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(random.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;
local ClientKey;
local ServerKey;
if stream.client_key and stream.server_key then
ClientKey = stream.client_key;
ServerKey = stream.server_key;
else
if stream.salted_password then
SaltedPassword = stream.salted_password;
elseif stream.password then
SaltedPassword = Hi(Normalize(stream.password), salt, i);
end
ServerKey = HMAC(SaltedPassword, "Server Key");
ClientKey = HMAC(SaltedPassword, "Client Key");
end
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 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
end)
package.preload['util.sasl.plain'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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
end)
package.preload['util.sasl.anonymous'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
return function (stream, name)
if name == "ANONYMOUS" then
return function ()
return coroutine.yield() == "success";
end, 0;
end
end
end)
package.preload['verse.plugins.tls'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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 library (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(stream.ssl or {mode="client", protocol="sslv23", options="no_sslv2",capath="/etc/ssl/certs"}, 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
end)
package.preload['verse.plugins.sasl'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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
end)
package.preload['verse.plugins.bind'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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
end)
package.preload['verse.plugins.session'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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 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
end)
package.preload['verse.plugins.legacy'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
local verse = require "verse";
local uuid = require "util.uuid".generate;
local xmlns_auth = "jabber:iq:auth";
function verse.plugins.legacy(stream)
local 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
local 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
end)
package.preload['verse.plugins.compression'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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
end)
package.preload['verse.plugins.smacks'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
local verse = require "verse";
local now = require"socket".gettime;
local xmlns_sm = "urn:xmpp:sm:3";
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
local 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 <r>...");
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
end)
package.preload['verse.plugins.keepalive'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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
end)
package.preload['verse.plugins.disco'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- Verse XMPP Library
-- Copyright (C) 2010 Hubert Chathi <hubert@uhoreg.ca>
-- Copyright (C) 2010 Matthew Wild <mwild1@gmail.com>
--
-- 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.hashes").sha1;
local calculate_hash = require "util.caps".calculate_hash;
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 build_self_disco_info_stanza(query_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.stanza("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
return result;
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(build_self_disco_info_stanza())
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_tag = build_self_disco_info_stanza(query.attr.node);
local result = verse.reply(stanza):add_child(query_tag);
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;
-- Using the disco cache, fires events for each identity of a given JID
local function scan_identities_for_service(service_jid)
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("^(.*)/(.*)$");
print(service_jid, category, type)
stream:event("disco/service-discovered/"..category, {
type = type, jid = service_jid;
});
end
end
end
stream:disco_info(stream.host, nil, function ()
scan_identities_for_service(stream.host);
end);
stream:disco_local_services(function (services)
for _, service in ipairs(services) do
scan_identities_for_service(service.jid);
end
stream:event("ready");
end);
return true;
end, 50);
stream:hook("presence-out", function (presence)
presence:remove_children("c", xmlns_caps);
presence:reset():add_child(stream:caps()):reset();
end, 10);
end
-- end of disco.lua
end)
package.preload['verse.plugins.version'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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 self:event("version/response", version); end
self: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
end)
package.preload['verse.plugins.ping'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
local verse = require "verse";
local gettime = require"socket".gettime;
local xmlns_ping = "urn:xmpp:ping";
function verse.plugins.ping(stream)
function stream:ping(jid, callback)
local t = 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(gettime()-t, jid);
end);
end
stream:hook("iq/"..xmlns_ping, function(stanza)
return stream:send(verse.reply(stanza));
end);
return true;
end
end)
package.preload['verse.plugins.uptime'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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
end)
package.preload['verse.plugins.blocking'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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
end)
package.preload['verse.plugins.jingle'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
local verse = require "verse";
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
-- <transport> 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
end)
package.preload['verse.plugins.jingle_ft'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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:4";
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("file");
local file = {
name = file_tag:get_child_text("name");
size = tonumber(file_tag:get_child_text("size"));
desc = file_tag:get_child_text("desc");
date = file_tag:get_child_text("date");
};
return setmetatable({ jingle = jingle, file = file }, ft_mt);
end);
stream:hook("jingle/describe/file", function (file_info)
-- Return <description/>
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("file")
:tag("name"):text(file_info.filename):up()
:tag("size"):text(tostring(file_info.size)):up()
:tag("date"):text(date):up()
:tag("desc"):text(file_info.description):up()
:up();
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
end)
package.preload['verse.plugins.jingle_s5b'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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.hashes".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
end)
package.preload['verse.plugins.proxy65'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
local verse = require "verse";
local uuid = require "util.uuid";
local sha1 = require "util.hashes".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 activate_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(activate_request, function (activated)
if activated.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(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
end)
package.preload['verse.plugins.jingle_ibb'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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 incoming 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
end)
package.preload['verse.plugins.pubsub'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
local verse = require "verse";
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, op_attr_extra)
local st = verse.iq{ type = iq_type or "get", to = to }
:tag("pubsub", { xmlns = ns or xmlns_pubsub }) -- ns would be ..#owner
local op_attr = { node = node, jid = jid };
if op_attr_extra then
for k, v in pairs(op_attr_extra) do
op_attr[k] = v;
end
end
if op then st:tag(op, op_attr); 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
-- <configure/> and <default/> 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)
, 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, notify, callback)
if type(notify) == "function" then -- COMPAT w/ older versions before 'notify' was added
notify, callback = false, notify;
end
self.stream:send_iq(
pubsub_iq(
"set",
self.service,
nil,
"retract",
self.node,
nil,
id,
{ notify = notify and "1" or nil }
),
callback
);
end
function pubsub_node:purge(notify, callback)
self.stream:send_iq(
pubsub_iq(
"set",
self.service,
xmlns_pubsub_owner,
"purge",
self.node,
nil,
nil,
{ notify = notify and "1" or nil }
),
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
end)
package.preload['verse.plugins.pep'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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, id)
return stream.pubsub:service(nil):node(node or item.attr.xmlns):publish(id or "current", nil, item)
end
end
end)
package.preload['verse.plugins.adhoc'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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 <note/>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
end)
package.preload['verse.plugins.presence'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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 self.last_presence then
stream:send(self.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.priority or opts.prio then
p:tag("priority"):text(tostring(opts.priority or opts.prio)):up();
end
if opts.status or opts.msg then
p:tag("status"):text(opts.status or opts.msg):up();
end
elseif type(opts) == "string" then
p:tag("status"):text(opts):up();
end
stream:send(p);
end
end
end)
package.preload['verse.plugins.private'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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
end)
package.preload['verse.plugins.roster'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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;
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
callback(nil, reply);
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
callback(nil, reply);
end
end);
end
local function add_item(item) -- Takes one roster <item/>
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
callback(nil, result);
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
end)
package.preload['verse.plugins.register'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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
end)
package.preload['verse.plugins.groupchat'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
local verse = require "verse";
local events = require "util.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, password)
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();
if password then
join_st:get_child("x", xmlns_muc):tag("password"):text(password):reset();
end
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
end)
package.preload['verse.plugins.vcard'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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 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
end)
package.preload['verse.plugins.vcard_update'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
local verse = require "verse";
-- local xmlns_vcard = "vcard-temp";
local xmlns_vcard_update = "vcard-temp:x:update";
local sha1 = require("util.hashes").sha1;
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;
local 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
--[[ TODO Complete this, it's probably broken.
-- Maybe better to hook outgoing stanza?
local _set_vcard = stream.set_vcard;
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()
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
end)
package.preload['verse.plugins.carbons'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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
end)
package.preload['verse.plugins.archive'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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:2"
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 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);
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)
self:unhook("message", handle_archived_message);
if reply.attr.type == "error" then
self:warn(table.concat({reply:get_error()}, " "))
callback(false, reply:get_error())
return true;
end
local finished = reply:get_child("fin", xmlns_mam)
if finished then
local rset = rsm.get(finished);
for k,v in pairs(rset or NULL) do results[k]=v; end
end
callback(results);
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
end)
package.preload['util.http'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- Prosody IM
-- Copyright (C) 2013 Florian Zeitz
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local format, char = string.format, string.char;
local pairs, ipairs, tonumber = pairs, ipairs, tonumber;
local t_insert, t_concat = table.insert, table.concat;
local function urlencode(s)
return s and (s:gsub("[^a-zA-Z0-9.~_-]", function (c) return format("%%%02x", c:byte()); end));
end
local function urldecode(s)
return s and (s:gsub("%%(%x%x)", function (c) return char(tonumber(c,16)); end));
end
local function _formencodepart(s)
return s and (s:gsub("%W", function (c)
if c ~= " " then
return format("%%%02x", c:byte());
else
return "+";
end
end));
end
local function formencode(form)
local result = {};
if form[1] then -- Array of ordered { name, value }
for _, field in ipairs(form) do
t_insert(result, _formencodepart(field.name).."=".._formencodepart(field.value));
end
else -- Unordered map of name -> value
for name, value in pairs(form) do
t_insert(result, _formencodepart(name).."=".._formencodepart(value));
end
end
return t_concat(result, "&");
end
local function formdecode(s)
if not s:match("=") then return urldecode(s); end
local r = {};
for k, v in s:gmatch("([^=&]*)=([^&]*)") do
k, v = k:gsub("%+", "%%20"), v:gsub("%+", "%%20");
k, v = urldecode(k), urldecode(v);
t_insert(r, { name = k, value = v });
r[k] = v;
end
return r;
end
local function contains_token(field, token)
field = ","..field:gsub("[ \t]", ""):lower()..",";
return field:find(","..token:lower()..",", 1, true) ~= nil;
end
return {
urlencode = urlencode, urldecode = urldecode;
formencode = formencode, formdecode = formdecode;
contains_token = contains_token;
};
end)
package.preload['net.http.parser'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
local tonumber = tonumber;
local assert = assert;
local t_insert, t_concat = table.insert, table.concat;
local url_parse = require "socket.url".parse;
local urldecode = require "util.http".urldecode;
local function preprocess_path(path)
path = urldecode((path:gsub("//+", "/")));
if path:sub(1,1) ~= "/" then
path = "/"..path;
end
local level = 0;
for component in path:gmatch("([^/]+)/") do
if component == ".." then
level = level - 1;
elseif component ~= "." then
level = level + 1;
end
if level < 0 then
return nil;
end
end
return path;
end
local httpstream = {};
function httpstream.new(success_cb, error_cb, parser_type, options_cb)
local client = true;
if not parser_type or parser_type == "server" then client = false; else assert(parser_type == "client", "Invalid parser type"); end
local buf, buflen, buftable = {}, 0, true;
local bodylimit = tonumber(options_cb and options_cb().body_size_limit) or 10*1024*1024;
local buflimit = tonumber(options_cb and options_cb().buffer_size_limit) or bodylimit * 2;
local chunked, chunk_size, chunk_start;
local state = nil;
local packet;
local len;
local have_body;
local error;
return {
feed = function(_, data)
if error then return nil, "parse has failed"; end
if not data then -- EOF
if buftable then buf, buftable = t_concat(buf), false; end
if state and client and not len then -- reading client body until EOF
packet.body = buf;
success_cb(packet);
elseif buf ~= "" then -- unexpected EOF
error = true; return error_cb("unexpected-eof");
end
return;
end
if buftable then
t_insert(buf, data);
else
buf = { buf, data };
buftable = true;
end
buflen = buflen + #data;
if buflen > buflimit then error = true; return error_cb("max-buffer-size-exceeded"); end
while buflen > 0 do
if state == nil then -- read request
if buftable then buf, buftable = t_concat(buf), false; end
local index = buf:find("\r\n\r\n", nil, true);
if not index then return; end -- not enough data
local method, path, httpversion, status_code, reason_phrase;
local first_line;
local headers = {};
for line in buf:sub(1,index+1):gmatch("([^\r\n]+)\r\n") do -- parse request
if first_line then
local key, val = line:match("^([^%s:]+): *(.*)$");
if not key then error = true; return error_cb("invalid-header-line"); end -- TODO handle multi-line and invalid headers
key = key:lower();
headers[key] = headers[key] and headers[key]..","..val or val;
else
first_line = line;
if client then
httpversion, status_code, reason_phrase = line:match("^HTTP/(1%.[01]) (%d%d%d) (.*)$");
status_code = tonumber(status_code);
if not status_code then error = true; return error_cb("invalid-status-line"); end
have_body = not
( (options_cb and options_cb().method == "HEAD")
or (status_code == 204 or status_code == 304 or status_code == 301)
or (status_code >= 100 and status_code < 200) );
else
method, path, httpversion = line:match("^(%w+) (%S+) HTTP/(1%.[01])$");
if not method then error = true; return error_cb("invalid-status-line"); end
end
end
end
if not first_line then error = true; return error_cb("invalid-status-line"); end
chunked = have_body and headers["transfer-encoding"] == "chunked";
len = tonumber(headers["content-length"]); -- TODO check for invalid len
if len and len > bodylimit then error = true; return error_cb("content-length-limit-exceeded"); end
if client then
-- FIXME handle '100 Continue' response (by skipping it)
if not have_body then len = 0; end
packet = {
code = status_code;
httpversion = httpversion;
headers = headers;
body = have_body and "" or nil;
-- COMPAT the properties below are deprecated
responseversion = httpversion;
responseheaders = headers;
};
else
local parsed_url;
if path:byte() == 47 then -- starts with /
local _path, _query = path:match("([^?]*).?(.*)");
if _query == "" then _query = nil; end
parsed_url = { path = _path, query = _query };
else
parsed_url = url_parse(path);
if not(parsed_url and parsed_url.path) then error = true; return error_cb("invalid-url"); end
end
path = preprocess_path(parsed_url.path);
headers.host = parsed_url.host or headers.host;
len = len or 0;
packet = {
method = method;
url = parsed_url;
path = path;
httpversion = httpversion;
headers = headers;
body = nil;
};
end
buf = buf:sub(index + 4);
buflen = #buf;
state = true;
end
if state then -- read body
if client then
if chunked then
if chunk_start and buflen - chunk_start - 2 < chunk_size then
return;
end -- not enough data
if buftable then buf, buftable = t_concat(buf), false; end
if not buf:find("\r\n", nil, true) then
return;
end -- not enough data
if not chunk_size then
chunk_size, chunk_start = buf:match("^(%x+)[^\r\n]*\r\n()");
chunk_size = chunk_size and tonumber(chunk_size, 16);
if not chunk_size then error = true; return error_cb("invalid-chunk-size"); end
end
if chunk_size == 0 and buf:find("\r\n\r\n", chunk_start-2, true) then
state, chunk_size = nil, nil;
buf = buf:gsub("^.-\r\n\r\n", ""); -- This ensure extensions and trailers are stripped
success_cb(packet);
elseif buflen - chunk_start - 2 >= chunk_size then -- we have a chunk
packet.body = packet.body..buf:sub(chunk_start, chunk_start + (chunk_size-1));
buf = buf:sub(chunk_start + chunk_size + 2);
buflen = buflen - (chunk_start + chunk_size + 2 - 1);
chunk_size, chunk_start = nil, nil;
else -- Partial chunk remaining
break;
end
elseif len and buflen >= len then
if buftable then buf, buftable = t_concat(buf), false; end
if packet.code == 101 then
packet.body, buf, buflen, buftable = buf, {}, 0, true;
else
packet.body, buf = buf:sub(1, len), buf:sub(len + 1);
buflen = #buf;
end
state = nil; success_cb(packet);
else
break;
end
elseif buflen >= len then
if buftable then buf, buftable = t_concat(buf), false; end
packet.body, buf = buf:sub(1, len), buf:sub(len + 1);
buflen = #buf;
state = nil; success_cb(packet);
else
break;
end
end
end
end;
};
end
return httpstream;
end)
package.preload['net.http'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- 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 b64 = require "util.encodings".base64.encode;
local url = require "socket.url"
local httpstream_new = require "net.http.parser".new;
local util_http = require "util.http";
local events = require "util.events";
local verify_identity = require"util.x509".verify_identity;
local ssl_available = pcall(require, "ssl");
local server = require "net.server"
local t_insert, t_concat = table.insert, table.concat;
local pairs = pairs;
local tonumber, tostring, xpcall, traceback =
tonumber, tostring, xpcall, debug.traceback;
local error = error
local setmetatable = setmetatable;
local log = require "util.logger".init("http");
local _ENV = nil;
local requests = {}; -- Open requests
local function make_id(req) return (tostring(req):match("%x+$")); end
local listener = { default_port = 80, default_mode = "*a" };
function listener.onconnect(conn)
local req = requests[conn];
-- Validate certificate
if not req.insecure and conn:ssl() then
local sock = conn:socket();
local chain_valid = sock.getpeerverification and sock:getpeerverification();
if not chain_valid then
req.callback("certificate-chain-invalid", 0, req);
req.callback = nil;
conn:close();
return;
end
local cert = sock.getpeercertificate and sock:getpeercertificate();
if not cert or not verify_identity(req.host, false, cert) then
req.callback("certificate-verify-failed", 0, req);
req.callback = nil;
conn:close();
return;
end
end
-- Send the request
local request_line = { req.method or "GET", " ", req.path, " HTTP/1.1\r\n" };
if req.query then
t_insert(request_line, 4, "?"..req.query);
end
conn:write(t_concat(request_line));
local t = { [2] = ": ", [4] = "\r\n" };
for k, v in pairs(req.headers) do
t[1], t[3] = k, v;
conn:write(t_concat(t));
end
conn:write("\r\n");
if req.body then
conn:write(req.body);
end
end
function listener.onincoming(conn, data)
local request = requests[conn];
if not request then
log("warn", "Received response from connection %s with no request attached!", tostring(conn));
return;
end
if data and request.reader then
request:reader(data);
end
end
function listener.ondisconnect(conn, err)
local request = requests[conn];
if request and request.conn then
request:reader(nil, err or "closed");
end
requests[conn] = nil;
end
function listener.ondetach(conn)
requests[conn] = nil;
end
local function destroy_request(request)
if request.conn then
request.conn = nil;
request.handler:close()
end
end
local function request_reader(request, data, err)
if not request.parser then
local function error_cb(reason)
if request.callback then
request.callback(reason or "connection-closed", 0, request);
request.callback = nil;
end
destroy_request(request);
end
if not data then
error_cb(err);
return;
end
local function success_cb(r)
if request.callback then
request.callback(r.body, r.code, r, request);
request.callback = nil;
end
destroy_request(request);
end
local function options_cb()
return request;
end
request.parser = httpstream_new(success_cb, error_cb, "client", options_cb);
end
request.parser:feed(data);
end
local function handleerr(err) log("error", "Traceback[http]: %s", traceback(tostring(err), 2)); end
local function log_if_failed(id, ret, ...)
if not ret then
log("error", "Request '%s': error in callback: %s", id, tostring((...)));
end
return ...;
end
local function request(self, u, ex, callback)
local req = url.parse(u);
req.url = u;
if not (req and req.host) then
callback("invalid-url", 0, req);
return nil, "invalid-url";
end
if not req.path then
req.path = "/";
end
req.id = ex and ex.id or make_id(req);
do
local event = { http = self, url = u, request = req, options = ex, callback = callback };
local ret = self.events.fire_event("pre-request", event);
if ret then
return ret;
end
req, u, ex, callback = event.request, event.url, event.options, event.callback;
end
local method, headers, body;
local host, port = req.host, req.port;
local host_header = host;
if (port == "80" and req.scheme == "http")
or (port == "443" and req.scheme == "https") then
port = nil;
elseif port then
host_header = host_header..":"..port;
end
headers = {
["Host"] = host_header;
["User-Agent"] = "Prosody XMPP Server";
};
if req.userinfo then
headers["Authorization"] = "Basic "..b64(req.userinfo);
end
if ex then
req.onlystatus = ex.onlystatus;
body = ex.body;
if body then
method = "POST";
headers["Content-Length"] = tostring(#body);
headers["Content-Type"] = "application/x-www-form-urlencoded";
end
if ex.method then method = ex.method; end
if ex.headers then
for k, v in pairs(ex.headers) do
headers[k] = v;
end
end
req.insecure = ex.insecure;
end
log("debug", "Making %s %s request '%s' to %s", req.scheme:upper(), method or "GET", req.id, (ex and ex.suppress_url and host_header) or u);
-- Attach to request object
req.method, req.headers, req.body = method, headers, body;
local using_https = req.scheme == "https";
if using_https and not ssl_available then
error("SSL not available, unable to contact https URL");
end
local port_number = port and tonumber(port) or (using_https and 443 or 80);
local sslctx = false;
if using_https then
sslctx = ex and ex.sslctx or self.options and self.options.sslctx;
end
local handler, conn = server.addclient(host, port_number, listener, "*a", sslctx)
if not handler then
self.events.fire_event("request-connection-error", { http = self, request = req, url = u, err = conn });
callback(conn, 0, req);
return nil, conn;
end
req.handler, req.conn = handler, conn
req.write = function (...) return req.handler:write(...); end
req.callback = function (content, code, response, request)
do
local event = { http = self, url = u, request = req, response = response, content = content, code = code, callback = callback };
self.events.fire_event("response", event);
content, code, response = event.content, event.code, event.response;
end
log("debug", "Request '%s': Calling callback, status %s", req.id, code or "---");
return log_if_failed(req.id, xpcall(function () return callback(content, code, response, request) end, handleerr));
end
req.reader = request_reader;
req.state = "status";
requests[req.handler] = req;
self.events.fire_event("request", { http = self, request = req, url = u });
return req;
end
local function new(options)
local http = {
options = options;
request = request;
new = options and function (new_options)
return new(setmetatable(new_options, { __index = options }));
end or new;
events = events.new();
};
return http;
end
local default_http = new({
sslctx = { mode = "client", protocol = "sslv23", options = { "no_sslv2", "no_sslv3" } };
});
return {
request = function (u, ex, callback)
return default_http:request(u, ex, callback);
end;
default = default_http;
new = new;
events = default_http.events;
-- COMPAT
urlencode = util_http.urlencode;
urldecode = util_http.urldecode;
formencode = util_http.formencode;
formdecode = util_http.formdecode;
};
end)
package.preload['util.x509'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
-- Prosody IM
-- Copyright (C) 2010 Matthew Wild
-- Copyright (C) 2010 Paul Aurich
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
-- TODO: I feel a fair amount of this logic should be integrated into Luasec,
-- so that everyone isn't re-inventing the wheel. Dependencies on
-- IDN libraries complicate that.
-- [TLS-CERTS] - http://tools.ietf.org/html/rfc6125
-- [XMPP-CORE] - http://tools.ietf.org/html/rfc6120
-- [SRV-ID] - http://tools.ietf.org/html/rfc4985
-- [IDNA] - http://tools.ietf.org/html/rfc5890
-- [LDAP] - http://tools.ietf.org/html/rfc4519
-- [PKIX] - http://tools.ietf.org/html/rfc5280
local nameprep = require "util.encodings".stringprep.nameprep;
local idna_to_ascii = require "util.encodings".idna.to_ascii;
local base64 = require "util.encodings".base64;
local log = require "util.logger".init("x509");
local s_format = string.format;
local _ENV = nil;
local oid_commonname = "2.5.4.3"; -- [LDAP] 2.3
local oid_subjectaltname = "2.5.29.17"; -- [PKIX] 4.2.1.6
local oid_xmppaddr = "1.3.6.1.5.5.7.8.5"; -- [XMPP-CORE]
local oid_dnssrv = "1.3.6.1.5.5.7.8.7"; -- [SRV-ID]
-- Compare a hostname (possibly international) with asserted names
-- extracted from a certificate.
-- This function follows the rules laid out in
-- sections 6.4.1 and 6.4.2 of [TLS-CERTS]
--
-- A wildcard ("*") all by itself is allowed only as the left-most label
local function compare_dnsname(host, asserted_names)
-- TODO: Sufficient normalization? Review relevant specs.
local norm_host = idna_to_ascii(host)
if norm_host == nil then
log("info", "Host %s failed IDNA ToASCII operation", host)
return false
end
norm_host = norm_host:lower()
local host_chopped = norm_host:gsub("^[^.]+%.", "") -- everything after the first label
for i=1,#asserted_names do
local name = asserted_names[i]
if norm_host == name:lower() then
log("debug", "Cert dNSName %s matched hostname", name);
return true
end
-- Allow the left most label to be a "*"
if name:match("^%*%.") then
local rest_name = name:gsub("^[^.]+%.", "")
if host_chopped == rest_name:lower() then
log("debug", "Cert dNSName %s matched hostname", name);
return true
end
end
end
return false
end
-- Compare an XMPP domain name with the asserted id-on-xmppAddr
-- identities extracted from a certificate. Both are UTF8 strings.
--
-- Per [XMPP-CORE], matches against asserted identities don't include
-- wildcards, so we just do a normalize on both and then a string comparison
--
-- TODO: Support for full JIDs?
local function compare_xmppaddr(host, asserted_names)
local norm_host = nameprep(host)
for i=1,#asserted_names do
local name = asserted_names[i]
-- We only want to match against bare domains right now, not
-- those crazy full-er JIDs.
if name:match("[@/]") then
log("debug", "Ignoring xmppAddr %s because it's not a bare domain", name)
else
local norm_name = nameprep(name)
if norm_name == nil then
log("info", "Ignoring xmppAddr %s, failed nameprep!", name)
else
if norm_host == norm_name then
log("debug", "Cert xmppAddr %s matched hostname", name)
return true
end
end
end
end
return false
end
-- Compare a host + service against the asserted id-on-dnsSRV (SRV-ID)
-- identities extracted from a certificate.
--
-- Per [SRV-ID], the asserted identities will be encoded in ASCII via ToASCII.
-- Comparison is done case-insensitively, and a wildcard ("*") all by itself
-- is allowed only as the left-most non-service label.
local function compare_srvname(host, service, asserted_names)
local norm_host = idna_to_ascii(host)
if norm_host == nil then
log("info", "Host %s failed IDNA ToASCII operation", host);
return false
end
-- Service names start with a "_"
if service:match("^_") == nil then service = "_"..service end
norm_host = norm_host:lower();
local host_chopped = norm_host:gsub("^[^.]+%.", "") -- everything after the first label
for i=1,#asserted_names do
local asserted_service, name = asserted_names[i]:match("^(_[^.]+)%.(.*)");
if service == asserted_service then
if norm_host == name:lower() then
log("debug", "Cert SRVName %s matched hostname", name);
return true;
end
-- Allow the left most label to be a "*"
if name:match("^%*%.") then
local rest_name = name:gsub("^[^.]+%.", "")
if host_chopped == rest_name:lower() then
log("debug", "Cert SRVName %s matched hostname", name)
return true
end
end
if norm_host == name:lower() then
log("debug", "Cert SRVName %s matched hostname", name);
return true
end
end
end
return false
end
local function verify_identity(host, service, cert)
if cert.setencode then
cert:setencode("utf8");
end
local ext = cert:extensions()
if ext[oid_subjectaltname] then
local sans = ext[oid_subjectaltname];
-- Per [TLS-CERTS] 6.3, 6.4.4, "a client MUST NOT seek a match for a
-- reference identifier if the presented identifiers include a DNS-ID
-- SRV-ID, URI-ID, or any application-specific identifier types"
local had_supported_altnames = false
if sans[oid_xmppaddr] then
had_supported_altnames = true
if service == "_xmpp-client" or service == "_xmpp-server" then
if compare_xmppaddr(host, sans[oid_xmppaddr]) then return true end
end
end
if sans[oid_dnssrv] then
had_supported_altnames = true
-- Only check srvNames if the caller specified a service
if service and compare_srvname(host, service, sans[oid_dnssrv]) then return true end
end
if sans["dNSName"] then
had_supported_altnames = true
if compare_dnsname(host, sans["dNSName"]) then return true end
end
-- We don't need URIs, but [TLS-CERTS] is clear.
if sans["uniformResourceIdentifier"] then
had_supported_altnames = true
end
if had_supported_altnames then return false end
end
-- Extract a common name from the certificate, and check it as if it were
-- a dNSName subjectAltName (wildcards may apply for, and receive,
-- cat treats)
--
-- Per [TLS-CERTS] 1.8, a CN-ID is the Common Name from a cert subject
-- which has one and only one Common Name
local subject = cert:subject()
local cn = nil
for i=1,#subject do
local dn = subject[i]
if dn["oid"] == oid_commonname then
if cn then
log("info", "Certificate has multiple common names")
return false
end
cn = dn["value"];
end
end
if cn then
-- Per [TLS-CERTS] 6.4.4, follow the comparison rules for dNSName SANs.
return compare_dnsname(host, { cn })
end
-- If all else fails, well, why should we be any different?
return false
end
local pat = "%-%-%-%-%-BEGIN ([A-Z ]+)%-%-%-%-%-\r?\n"..
"([0-9A-Za-z+/=\r\n]*)\r?\n%-%-%-%-%-END %1%-%-%-%-%-";
local function pem2der(pem)
local typ, data = pem:match(pat);
if typ and data then
return base64.decode(data), typ;
end
end
local wrap = ('.'):rep(64);
local envelope = "-----BEGIN %s-----\n%s\n-----END %s-----\n"
local function der2pem(data, typ)
typ = typ and typ:upper() or "CERTIFICATE";
data = base64.encode(data);
return s_format(envelope, typ, data:gsub(wrap, '%0\n', (#data-1)/64), typ);
end
return {
verify_identity = verify_identity;
pem2der = pem2der;
der2pem = der2pem;
};
end)
package.preload['verse.bosh'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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
end)
package.preload['verse.client'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
local verse = require "verse";
local stream = verse.stream_mt;
local jid_split = require "util.jid".split;
local adns = require "net.adns";
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:stream>");
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("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("</stream:stream>");
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
end)
package.preload['verse.component'] = (function (...)
local _ENV = _ENV;
local function module(name, ...)
local t = package.loaded[name] or _ENV[name] or { _NAME = name };
package.loaded[name] = t;
for i = 1, select("#", ...) do
(select(i, ...))(t);
end
_ENV = t;
_M = t;
return t;
end
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.hashes".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("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("</stream:stream>");
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");
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";
local verse = {};
verse.server = server;
local stream = {};
stream.__index = stream;
verse.stream_mt = stream;
verse.plugins = {};
function verse.init(...)
for i=1,select("#", ...) do
local ok, err = pcall(require, "verse."..select(i,...));
if not ok then
error("Verse connection module not found: verse."..select(i,...)..err);
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 verse._default_log_handler(name, level, message)
return io.stderr:write(name, "\t", level, "\t", message, "\n");
end
verse.set_log_handler(verse._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("once");
end
function stream:listen(host, port)
host = host or "localhost";
port = port or 0;
local conn, err = server.addserver(host, port, verse.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);
conn:setoption("keepalive", true);
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, verse.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 verse.new_listener(stream)
local conn_listener = {};
function conn_listener.onconnect(conn)
if stream.server then
local client = verse.new();
conn:setlistener(verse.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;