From da366208b6315debb5f5b6be7bd3145766838cdf Mon Sep 17 00:00:00 2001 From: mcneb10 Date: Sat, 1 Jun 2024 16:46:19 -0500 Subject: [PATCH] First commit --- .gitignore | 4 + README.md | 17 + clean | 6 + config.lua | 73 + config_private_example.lua | 12 + get_frontends | 6 + main.lua | 65 + run | 51 + utils.lua | 100 + verse.lua | 11230 +++++++++++++++++++++++++++++++++++ 10 files changed, 11564 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 clean create mode 100644 config.lua create mode 100644 config_private_example.lua create mode 100755 get_frontends create mode 100755 main.lua create mode 100755 run create mode 100644 utils.lua create mode 100644 verse.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c856b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# Delete verse.lua to compile from source +#verse.lua +config_private.lua +services.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..aaba2dd --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Lua XMPP Privacy Bot + +This bot replaces links to popular sites such as youtube with privacy respecting front ends such as individuous. It is written in 100% pure lua + +# How to run + +Make sure `make`, `tar`, `gzip`, `lua`, and `luarocks` are installed. + +Then do `luarocks install luasocket luaexpat luasec` + +Next configure the bot to your liking in `config.lua`. Also don't forget to copy `config_private_example.lua` to `config_private.lua` and fill that out as well. + +Then run the `./run` script to run the bot. It will download the farside `services.json` list and compile the `verse.lua` xmpp library. + +# List of supported front ends + +**TODO** \ No newline at end of file diff --git a/clean b/clean new file mode 100755 index 0000000..c4e5514 --- /dev/null +++ b/clean @@ -0,0 +1,6 @@ +#!/usr/bin/env lua + +print("Deleting compiled verse") +os.remove("verse.lua") +print("Deleting services.json") +os.remove("services.json") diff --git a/config.lua b/config.lua new file mode 100644 index 0000000..ef0b8f7 --- /dev/null +++ b/config.lua @@ -0,0 +1,73 @@ +-- Main privacy bot configuration file +config = { + -- Log verbosity + verbosity = 1, + -- Bot nickname + name = "Privacy Link Bot", + --[[ + This will set the type of url to replace the service domain with. Can be: + - clearnet + - onion + - eepsite + - yggdrasil + - TODO: make it work and more types? + ]]-- + prefered_website_medium = "clearnet", + -- Choose random frontend instead of fallback one, will force clearnet + random_frontend = true, + -- List of desired frontends to extract from `services.json` + sites = { + -- Key is domain pattern + ["reddit[.]com"] = { + -- Specify which frontents should be used + frontends = { "libreddit", "redlib" } + }, + ["instagram[.]com"] = { + frontends = { "proxigram" } + }, + ["github[.]com"] = { + frontends = { "gothub" } + }, + ["google[.]com"] = { + frontends = { "searxng" } + }, + ["youtube[.]com"] = { + frontends = { "piped", "invidious"} + }, + ["www[.]youtube[.]com"] = { + frontends = { "piped", "invidious"} + }, + ["youtu[.]be"] = { + frontends = { "piped", "invidious", } + }, + ["twitter[.]com"] = { + frontends = { "nitter", } + }, + ["x[.]com"] = { + frontends = { "nitter", } + }, + ["wikipedia[.]org"] = { + frontends = { "wikiless", } + }, + ["medium[.]com"] = { + frontends = { "scribe", } + }, + ["imgur[.]com"] = { + frontends = { "rimgo", } + }, + ["translate[.]google[.]com"] = { + frontends = { "lingva", } + }, + ["tiktok[.]com"] = { + frontends = { "proxitok", } + }, + ["fandom[.]com"] = { + frontends = { "breezewiki", } + }, + -- TODO: the rest + } +} + +-- Load config file with private information +dofile("config_private.lua") + diff --git a/config_private_example.lua b/config_private_example.lua new file mode 100644 index 0000000..741a290 --- /dev/null +++ b/config_private_example.lua @@ -0,0 +1,12 @@ +-- JID for the bot +config.jid = "bot@server.net" +-- Bot password, stored in plaintext +config.password = "password" + +-- Rooms the bot will attempt to join +config.rooms_to_join = { + -- Normal room + {jid = "room@server.net",}, + -- Password protected room, password stored in plaintext + {jid = "protected@server.net", password = "pass"}, +} \ No newline at end of file diff --git a/get_frontends b/get_frontends new file mode 100755 index 0000000..0fa456d --- /dev/null +++ b/get_frontends @@ -0,0 +1,6 @@ +#!/usr/bin/env lua + +local farside_instance_json_url = "https://git.sr.ht/~benbusby/farside/blob/HEAD/services.json" + +os.remove("services.json") +os.execute(string.format("wget \"%s\"", farside_instance_json_url)) diff --git a/main.lua b/main.lua new file mode 100755 index 0000000..4cb9d4c --- /dev/null +++ b/main.lua @@ -0,0 +1,65 @@ +-- Get the verse lib +local verse = require("verse") +-- Setup logging and config +require("utils") +local log = setup_log(string.format("%s_main", config.name)) +log("info", "Log initialized") +-- Get the client lib +local client_lib = verse.init("client") +-- Make the client +local client = client_lib.new() +-- Load client plugins +client:add_plugin("groupchat") +client:add_plugin("version") +-- Client hooks +client:hook("disconnected", function() + log("error", "XMPP connetion lost. Quitting...") + os.exit(1) +end) +client:hook("authentication-failure", function() + log("error", "Failed to authenticate with XMPP Server. Quitting...") + os.exit(1) +end) +client:hook("authentication-success", function() + log("info", "XMPP authentication sucessful!") +end) +client:hook("ready", function() + log("info", "Client ready") + + -- Main code goes here + for _, room in pairs(config.rooms_to_join) do + local room, err = client:join_room(room.jid, config.name, {}, config.password) + + if room then + -- Run on message events + log("info", "Joined room \"%s\"", room.jid) + room:hook("message", function(event) + if event.stanza.attr.type == "groupchat" and not string.find(event.stanza.attr.from, "/" .. config.name) then + local body = event.stanza:get_child_text("body") + if body then + for site, services in pairs(config.sites) do + local instance = choose_instance(services.frontends) + -- TODO: make it reply using XEP-0461 + for match in string.gmatch(body, string.format("%%s(%s/%%S+)", site)) do + room:send_message(string.format("> %s\nPrivate frontend: %s", match, string.gsub(match, site, instance))) + end + for match in string.gmatch(body, string.format("(https?://%s/%%S+)", site)) do + room:send_message(string.format("> %s\nPrivate frontend: %s", match, string.gsub(match, site, instance))) + end + end + end + end + end) + else + log("error", "Error joining room \"%s\": %s", room.jid, err) + end + end +end) + +log("info", "Connecting to server...") + +client:connect_client(config.jid, config.password) + +verse.loop() + +log("info", "Verse loop exited. Quitting...") diff --git a/run b/run new file mode 100755 index 0000000..80dac12 --- /dev/null +++ b/run @@ -0,0 +1,51 @@ +#!/usr/bin/env lua + +-- TODO: luarocks? + +-- Download frontends list +if not os.execute(string.format("ls services.json 2>/dev/null >/dev/null")) then + dofile("get_frontends") +end + +-- Squish commit hash +local squish_version = "tip" +-- Squish script url +local squish_script = string.format("http://code.matthewwild.co.uk/squish/raw-file/%s/squish.lua", squish_version) + +-- Verse commit hash +local verse_version = "98dc1750584d" +-- Location for various verse files +local verse_folder_name = string.format("verse-%s", verse_version) +local verse_archive_filename = string.format("%s.tar.gz", verse_version) +local verse_dist = string.format("http://code.matthewwild.co.uk/verse/archive/%s", verse_archive_filename) + +-- Options for verse `./configure` script +local verse_config_opts = "" +-- Options for verse `make` +local verse_make_opts = "" + +-- Check if verse module exists and compile if it doesn't +if not os.execute(string.format("ls verse.lua 2>/dev/null >/dev/null")) then + -- Download source archive + os.execute(string.format("wget \"%s\"", verse_dist)) + -- Extract the source archive + os.execute(string.format("tar -xf \"%s\"", verse_archive_filename)) + -- Delete the source archive + os.remove(verse_archive_filename) + -- Compile library + os.execute(string.format( + [[sh -c "cd \"%s\" && # Go to library dir + wget \"%s\" -O ./buildscripts/squish && # Replace squish with stripped down version that actually works + ./configure %s && # Configure the library + make %s # Compile"]], + verse_folder_name, squish_script, verse_config_opts, verse_make_opts + )) + -- Copy file + os.execute(string.format("cp \"%s/verse.lua\" .", verse_folder_name)) + -- Delete folder + os.execute(string.format("rm -rf \"%s\"", verse_folder_name)) +end + + +-- Load main module +require("main") diff --git a/utils.lua b/utils.lua new file mode 100644 index 0000000..5f55003 --- /dev/null +++ b/utils.lua @@ -0,0 +1,100 @@ +-- Various utility functions used by the bot + +function log_callback(source, level, message, ...) + local output = string.format( + "%s %s [%s]: %s", + os.date(), + source, + level, + string.format(message, ...) + ) + if config.verbosity == 0 then + if level ~= "debug" then + print(output) + end + elseif config.verbosity == 1 then + print(output) + end +end + +--[[ + Make a new logger with `name` and setup a handler based on the log level `v` + TODO: `v` needs to be figured out, for now it's 1 to print debug messages 0 to not print them or any other value to print nothing +]]-- +function setup_log(name) + local log_module = require("util.logger") + log_module.add_level_sink("debug", log_callback) + log_module.add_level_sink("info", log_callback) + log_module.add_level_sink("warn", log_callback) + log_module.add_level_sink("error", log_callback) + return log_module.init(name) +end + +-- Read a file in it's entirety, returning an empty string on failure +function read_all_text(file) + local file = io.open(file, "rb") + if not file then + print(string.format("Failed to open file \"%s\"!", file)) + return "" + end + local text = file:read("*all") + file:close() + return text +end + +-- Choose instance from available services +function choose_instance(services) + -- Choose a random service + local service = services[math.random(#services)] + -- Get list of instances for service + local service_instances + for _, service_instance_list in pairs(config.instances) do + if service_instance_list.type == service then + -- Based on config choose instance + if config.random_frontend then + -- TODO: cache this? + local usable_instances = {} + for _, instance_url_list in pairs(service_instance_list.instances) do + -- Instance URLs are split by pipes + for instance in string.gmatch(instance_url_list, "[^|]+") do + if instance.match(instance, "[.]onion$") then + if config.prefered_website_medium == "onion" then + table.insert(usable_instances, instance) + end + elseif instance.match(instance, "[.]i2p$") then + if config.prefered_website_medium == "eepsite" then + table.insert(usable_instances, instance) + end + elseif instance.match(instance, "[[][%d:]+[]]") then + if config.prefered_website_medium == "yggdrasil" then + table.insert(usable_instances, instance) + end + else + -- Assume clearnet + if config.prefered_website_medium == "clearnet" then + table.insert(usable_instances, instance) + end + end + end + end + return string.gsub(usable_instances[math.random(#usable_instances)], "https?://", "") + else + return string.gsub(service_instance_list.fallback, "https?://", "") + end + end + end + return string.format("%s-no-instances-available", service) +end + +-- Config file +dofile("config.lua") + +-- Load subsitutions +local services_text = read_all_text("services.json") +local json = require("util.json") +local services, err = json.decode(services_text) +if services then + config.instances = services +else + print(string.format("Error loading \"services.json\": %s", err)) +end diff --git a/verse.lua b/verse.lua new file mode 100644 index 0000000..cdc7aa0 --- /dev/null +++ b/verse.lua @@ -0,0 +1,11230 @@ +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;