aboutsummaryrefslogtreecommitdiffstats
path: root/util
diff options
context:
space:
mode:
Diffstat (limited to 'util')
-rw-r--r--util/pluginloader.lua15
-rw-r--r--util/prosodyctl.lua86
-rw-r--r--util/pubsub.lua341
-rw-r--r--util/x509.lua211
-rw-r--r--util/xmppstream.lua23
5 files changed, 666 insertions, 10 deletions
diff --git a/util/pluginloader.lua b/util/pluginloader.lua
index 31ab1e88..1aedd630 100644
--- a/util/pluginloader.lua
+++ b/util/pluginloader.lua
@@ -6,8 +6,13 @@
-- COPYING file in the source package for more information.
--
-
-local plugin_dir = CFG_PLUGINDIR or "./plugins/";
+local dir_sep, path_sep = package.config:match("^(%S+)%s(%S+)");
+local plugin_dir = {};
+for path in (CFG_PLUGINDIR or "./plugins/"):gsub("[/\\]", dir_sep):gmatch("[^"..path_sep.."]+") do
+ path = path..dir_sep; -- add path separator to path end
+ path = path:gsub(dir_sep..dir_sep.."+", dir_sep); -- coalesce multiple separaters
+ plugin_dir[#plugin_dir + 1] = path;
+end
local io_open, os_time = io.open, os.time;
local loadstring, pairs = loadstring, pairs;
@@ -15,7 +20,11 @@ local loadstring, pairs = loadstring, pairs;
module "pluginloader"
local function load_file(name)
- local file, err = io_open(plugin_dir..name);
+ local file, err;
+ for i=1,#plugin_dir do
+ file, err = io_open(plugin_dir[i]..name);
+ if file then break; end
+ end
if not file then return file, err; end
local content = file:read("*a");
file:close();
diff --git a/util/prosodyctl.lua b/util/prosodyctl.lua
index 40d21be8..aa1850b2 100644
--- a/util/prosodyctl.lua
+++ b/util/prosodyctl.lua
@@ -15,18 +15,104 @@ local usermanager = require "core.usermanager";
local signal = require "util.signal";
local set = require "util.set";
local lfs = require "lfs";
+local pcall = pcall;
local nodeprep, nameprep = stringprep.nodeprep, stringprep.nameprep;
local io, os = io, os;
+local print = print;
local tostring, tonumber = tostring, tonumber;
local CFG_SOURCEDIR = _G.CFG_SOURCEDIR;
+local _G = _G;
local prosody = prosody;
module "prosodyctl"
+-- UI helpers
+function show_message(msg, ...)
+ print(msg:format(...));
+end
+
+function show_warning(msg, ...)
+ print(msg:format(...));
+end
+
+function show_usage(usage, desc)
+ print("Usage: ".._G.arg[0].." "..usage);
+ if desc then
+ print(" "..desc);
+ end
+end
+
+function getchar(n)
+ local stty_ret = os.execute("stty raw -echo 2>/dev/null");
+ local ok, char;
+ if stty_ret == 0 then
+ ok, char = pcall(io.read, n or 1);
+ os.execute("stty sane");
+ else
+ ok, char = pcall(io.read, "*l");
+ if ok then
+ char = char:sub(1, n or 1);
+ end
+ end
+ if ok then
+ return char;
+ end
+end
+
+function getpass()
+ local stty_ret = os.execute("stty -echo 2>/dev/null");
+ if stty_ret ~= 0 then
+ io.write("\027[08m"); -- ANSI 'hidden' text attribute
+ end
+ local ok, pass = pcall(io.read, "*l");
+ if stty_ret == 0 then
+ os.execute("stty sane");
+ else
+ io.write("\027[00m");
+ end
+ io.write("\n");
+ if ok then
+ return pass;
+ end
+end
+
+function show_yesno(prompt)
+ io.write(prompt, " ");
+ local choice = getchar():lower();
+ io.write("\n");
+ if not choice:match("%a") then
+ choice = prompt:match("%[.-(%U).-%]$");
+ if not choice then return nil; end
+ end
+ return (choice == "y");
+end
+
+function read_password()
+ local password;
+ while true do
+ io.write("Enter new password: ");
+ password = getpass();
+ if not password then
+ show_message("No password - cancelled");
+ return;
+ end
+ io.write("Retype new password: ");
+ if getpass() ~= password then
+ if not show_yesno [=[Passwords did not match, try again? [Y/n]]=] then
+ return;
+ end
+ else
+ break;
+ end
+ end
+ return password;
+end
+
+-- Server control
function adduser(params)
local user, host, password = nodeprep(params.user), nameprep(params.host), params.password;
if not user then
diff --git a/util/pubsub.lua b/util/pubsub.lua
new file mode 100644
index 00000000..69e79acc
--- /dev/null
+++ b/util/pubsub.lua
@@ -0,0 +1,341 @@
+module("pubsub", package.seeall);
+
+local service = {};
+local service_mt = { __index = service };
+
+local default_config = {
+ broadcaster = function () end;
+ get_affiliation = function () end;
+ capabilities = {};
+};
+
+function new(config)
+ config = config or {};
+ return setmetatable({
+ config = setmetatable(config, { __index = default_config });
+ affiliations = {};
+ subscriptions = {};
+ nodes = {};
+ }, service_mt);
+end
+
+function service:jids_equal(jid1, jid2)
+ local normalize = self.config.normalize_jid;
+ return normalize(jid1) == normalize(jid2);
+end
+
+function service:may(node, actor, action)
+ if actor == true then return true; end
+
+ local node_obj = self.nodes[node];
+ local node_aff = node_obj and node_obj.affiliations[actor];
+ local service_aff = self.affiliations[actor]
+ or self.config.get_affiliation(actor, node, action)
+ or "none";
+
+ -- Check if node allows/forbids it
+ local node_capabilities = node_obj and node_obj.capabilities;
+ if node_capabilities then
+ local caps = node_capabilities[node_aff or service_aff];
+ if caps then
+ local can = caps[action];
+ if can ~= nil then
+ return can;
+ end
+ end
+ end
+
+ -- Check service-wide capabilities instead
+ local service_capabilities = self.config.capabilities;
+ local caps = service_capabilities[node_aff or service_aff];
+ if caps then
+ local can = caps[action];
+ if can ~= nil then
+ return can;
+ end
+ end
+
+ return false;
+end
+
+function service:set_affiliation(node, actor, jid, affiliation)
+ -- Access checking
+ if not self:may(node, actor, "set_affiliation") then
+ return false, "forbidden";
+ end
+ --
+ local node_obj = self.nodes[node];
+ if not node_obj then
+ return false, "item-not-found";
+ end
+ node_obj.affiliations[jid] = affiliation;
+ local _, jid_sub = self:get_subscription(node, true, jid);
+ if not jid_sub and not self:may(node, jid, "be_unsubscribed") then
+ local ok, err = self:add_subscription(node, true, jid);
+ if not ok then
+ return ok, err;
+ end
+ elseif jid_sub and not self:may(node, jid, "be_subscribed") then
+ local ok, err = self:add_subscription(node, true, jid);
+ if not ok then
+ return ok, err;
+ end
+ end
+ return true;
+end
+
+function service:add_subscription(node, actor, jid, options)
+ -- Access checking
+ local cap;
+ if actor == true or jid == actor or self:jids_equal(actor, jid) then
+ cap = "subscribe";
+ else
+ cap = "subscribe_other";
+ end
+ if not self:may(node, actor, cap) then
+ return false, "forbidden";
+ end
+ if not self:may(node, jid, "be_subscribed") then
+ return false, "forbidden";
+ end
+ --
+ local node_obj = self.nodes[node];
+ if not node_obj then
+ if not self.config.autocreate_on_subscribe then
+ return false, "item-not-found";
+ else
+ local ok, err = self:create(node, actor);
+ if not ok then
+ return ok, err;
+ end
+ node_obj = self.nodes[node];
+ end
+ end
+ node_obj.subscribers[jid] = options or true;
+ local normal_jid = self.config.normalize_jid(jid);
+ local subs = self.subscriptions[normal_jid];
+ if subs then
+ if not subs[jid] then
+ subs[jid] = { [node] = true };
+ else
+ subs[jid][node] = true;
+ end
+ else
+ self.subscriptions[normal_jid] = { [jid] = { [node] = true } };
+ end
+ return true;
+end
+
+function service:remove_subscription(node, actor, jid)
+ -- Access checking
+ local cap;
+ if actor == true or jid == actor or self:jids_equal(actor, jid) then
+ cap = "unsubscribe";
+ else
+ cap = "unsubscribe_other";
+ end
+ if not self:may(node, actor, cap) then
+ return false, "forbidden";
+ end
+ if not self:may(node, jid, "be_unsubscribed") then
+ return false, "forbidden";
+ end
+ --
+ local node_obj = self.nodes[node];
+ if not node_obj then
+ return false, "item-not-found";
+ end
+ if not node_obj.subscribers[jid] then
+ return false, "not-subscribed";
+ end
+ node_obj.subscribers[jid] = nil;
+ local normal_jid = self.config.normalize_jid(jid);
+ local subs = self.subscriptions[normal_jid];
+ if subs then
+ local jid_subs = subs[jid];
+ if jid_subs then
+ jid_subs[node] = nil;
+ if next(jid_subs) == nil then
+ subs[jid] = nil;
+ end
+ end
+ if next(subs) == nil then
+ self.subscriptions[normal_jid] = nil;
+ end
+ end
+ return true;
+end
+
+function service:get_subscription(node, actor, jid)
+ -- Access checking
+ local cap;
+ if actor == true or jid == actor or self:jids_equal(actor, jid) then
+ cap = "get_subscription";
+ else
+ cap = "get_subscription_other";
+ end
+ if not self:may(node, actor, cap) then
+ return false, "forbidden";
+ end
+ --
+ local node_obj = self.nodes[node];
+ if not node_obj then
+ return false, "item-not-found";
+ end
+ return true, node_obj.subscribers[jid];
+end
+
+function service:create(node, actor)
+ -- Access checking
+ if not self:may(node, actor, "create") then
+ return false, "forbidden";
+ end
+ --
+ if self.nodes[node] then
+ return false, "conflict";
+ end
+
+ self.nodes[node] = {
+ name = node;
+ subscribers = {};
+ config = {};
+ data = {};
+ affiliations = {};
+ };
+ local ok, err = self:set_affiliation(node, true, actor, "owner");
+ if not ok then
+ self.nodes[node] = nil;
+ end
+ return ok, err;
+end
+
+function service:publish(node, actor, id, item)
+ -- Access checking
+ if not self:may(node, actor, "publish") then
+ return false, "forbidden";
+ end
+ --
+ local node_obj = self.nodes[node];
+ if not node_obj then
+ if not self.config.autocreate_on_publish then
+ return false, "item-not-found";
+ end
+ local ok, err = self:create(node, actor);
+ if not ok then
+ return ok, err;
+ end
+ node_obj = self.nodes[node];
+ end
+ node_obj.data[id] = item;
+ self.config.broadcaster(node, node_obj.subscribers, item);
+ return true;
+end
+
+function service:retract(node, actor, id, retract)
+ -- Access checking
+ if not self:may(node, actor, "retract") then
+ return false, "forbidden";
+ end
+ --
+ local node_obj = self.nodes[node];
+ if (not node_obj) or (not node_obj.data[id]) then
+ return false, "item-not-found";
+ end
+ node_obj.data[id] = nil;
+ if retract then
+ self.config.broadcaster(node, node_obj.subscribers, retract);
+ end
+ return true
+end
+
+function service:get_items(node, actor, id)
+ -- Access checking
+ if not self:may(node, actor, "get_items") then
+ return false, "forbidden";
+ end
+ --
+ local node_obj = self.nodes[node];
+ if not node_obj then
+ return false, "item-not-found";
+ end
+ if id then -- Restrict results to a single specific item
+ return true, { [id] = node_obj.data[id] };
+ else
+ return true, node_obj.data;
+ end
+end
+
+function service:get_nodes(actor)
+ -- Access checking
+ if not self:may(nil, actor, "get_nodes") then
+ return false, "forbidden";
+ end
+ --
+ return true, self.nodes;
+end
+
+function service:get_subscriptions(node, actor, jid)
+ -- Access checking
+ local cap;
+ if actor == true or jid == actor or self:jids_equal(actor, jid) then
+ cap = "get_subscriptions";
+ else
+ cap = "get_subscriptions_other";
+ end
+ if not self:may(node, actor, cap) then
+ return false, "forbidden";
+ end
+ --
+ local node_obj;
+ if node then
+ node_obj = self.nodes[node];
+ if not node_obj then
+ return false, "item-not-found";
+ end
+ end
+ local normal_jid = self.config.normalize_jid(jid);
+ local subs = self.subscriptions[normal_jid];
+ -- We return the subscription object from the node to save
+ -- a get_subscription() call for each node.
+ local ret = {};
+ if subs then
+ for jid, subscribed_nodes in pairs(subs) do
+ if node then -- Return only subscriptions to this node
+ if subscribed_nodes[node] then
+ ret[#ret+1] = {
+ node = subscribed_node;
+ jid = jid;
+ subscription = node_obj.subscribers[jid];
+ };
+ end
+ else -- Return subscriptions to all nodes
+ local nodes = self.nodes;
+ for subscribed_node in pairs(subscribed_nodes) do
+ ret[#ret+1] = {
+ node = subscribed_node;
+ jid = jid;
+ subscription = nodes[subscribed_node].subscribers[jid];
+ };
+ end
+ end
+ end
+ end
+ return true, ret;
+end
+
+-- Access models only affect 'none' affiliation caps, service/default access level...
+function service:set_node_capabilities(node, actor, capabilities)
+ -- Access checking
+ if not self:may(node, actor, "configure") then
+ return false, "forbidden";
+ end
+ --
+ local node_obj = self.nodes[node];
+ if not node_obj then
+ return false, "item-not-found";
+ end
+ node_obj.capabilities = capabilities;
+ return true;
+end
+
+return _M;
diff --git a/util/x509.lua b/util/x509.lua
new file mode 100644
index 00000000..11f231a0
--- /dev/null
+++ b/util/x509.lua
@@ -0,0 +1,211 @@
+-- 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/draft-saintandre-tls-server-id-check-10
+-- [XMPP-CORE] - http://tools.ietf.org/html/draft-ietf-xmpp-3920bis-18
+-- [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 log = require "util.logger".init("x509");
+
+module "x509"
+
+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 4.4.1 and 4.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
+
+function verify_identity(host, service, cert)
+ local ext = cert:extensions()
+ if ext[oid_subjectaltname] then
+ local sans = ext[oid_subjectaltname];
+
+ -- Per [TLS-CERTS] 4.3, 4.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 compare_xmppaddr(host, sans[oid_xmppaddr]) then return true 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.5, 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] 4.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
+
+return _M;
diff --git a/util/xmppstream.lua b/util/xmppstream.lua
index cbdadd9b..a13e9d32 100644
--- a/util/xmppstream.lua
+++ b/util/xmppstream.lua
@@ -9,10 +9,13 @@
local lxp = require "lxp";
local st = require "util.stanza";
+local stanza_mt = st.stanza_mt;
local tostring = tostring;
local t_insert = table.insert;
local t_concat = table.concat;
+local t_remove = table.remove;
+local setmetatable = setmetatable;
local default_log = require "util.logger".init("xmppstream");
@@ -53,12 +56,13 @@ function new_sax_handlers(session, stream_callbacks)
local stream_default_ns = stream_callbacks.default_ns;
+ local stack = {};
local chardata, stanza = {};
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
- stanza:text(t_concat(chardata));
+ t_insert(stanza, t_concat(chardata));
chardata = {};
end
local curr_ns,name = tagname:match(ns_pattern);
@@ -102,9 +106,13 @@ function new_sax_handlers(session, stream_callbacks)
cb_error(session, "invalid-top-level-element");
end
- stanza = st.stanza(name, attr);
+ stanza = setmetatable({ name = name, attr = attr, tags = {} }, stanza_mt);
else -- we are inside a stanza, so add a tag
- stanza:tag(name, attr);
+ 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
function xml_handlers:CharacterData(data)
@@ -119,12 +127,11 @@ function new_sax_handlers(session, stream_callbacks)
if stanza then
if #chardata > 0 then
-- We have some character data in the buffer
- stanza:text(t_concat(chardata));
+ t_insert(stanza, t_concat(chardata));
chardata = {};
end
-- Complete stanza
- local last_add = stanza.last_add;
- if not last_add or #last_add == 0 then
+ if #stack == 0 then
if tagname ~= stream_error_tag then
cb_handlestanza(session, stanza);
else
@@ -132,7 +139,7 @@ function new_sax_handlers(session, stream_callbacks)
end
stanza = nil;
else
- stanza:up();
+ stanza = t_remove(stack);
end
else
if tagname == stream_tag then
@@ -147,11 +154,13 @@ function new_sax_handlers(session, stream_callbacks)
cb_error(session, "parse-error", "unexpected-element-close", name);
end
stanza, chardata = nil, {};
+ stack = {};
end
end
local function reset()
stanza, chardata = nil, {};
+ stack = {};
end
local function set_session(stream, new_session)