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 = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" }; 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, ""); 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, ""); local tag_format = top_tag_format.."%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 ""; end local css = {}; for code in ansi_codes:gmatch("[^;]+") do t_insert(css, cssmap[tonumber(code)]); end return ""; 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 ""; 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 = ""; 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(""); 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 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 ..."); 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 -- Copyright (C) 2010 Matthew Wild -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local verse = require "verse"; local b64 = require("mime").b64; local sha1 = require("util.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 -- 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 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 -- and rolled into one function pubsub_node:configure(config, callback) if config ~= nil then error("Not implemented yet."); --[[ if config == true then self.stream:send_iq(pubsub_iq("get", self.service, nil, "configure", self.node) , function(reply) local form = reply:get_child("pubsub"):get_child("configure"):get_cild("x"); local config = callback(require"util.dataforms".something(form)) self.stream:send_iq(pubsub_iq("set", config, ...)) end); end --]] -- fetch form and pass it to the callback -- which would process it and pass it back -- and then we submit it -- elseif type(config) == "table" then -- it's a form or stanza that we submit -- end -- this would be done for everything that needs a config end self.stream:send_iq(pubsub_iq("set", self.service, nil, config == nil and "default" or "configure", self.node), callback); end function pubsub_node:publish(id, options, node, callback) if options ~= nil then error("Node configuration is not implemented yet."); end self.stream:send_iq(pubsub_iq("set", self.service, nil, "publish", self.node, nil, id or true) :add_child(node) , callback); end function pubsub_node:subscribe(jid, options, callback) jid = jid or self.stream.jid; if options ~= nil then error("Subscription configuration is not implemented yet."); end self.stream:send_iq(pubsub_iq("set", self.service, nil, "subscribe", self.node, jid) , 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 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 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.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(""); 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(""); 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;