diff options
Diffstat (limited to 'util')
-rw-r--r-- | util/pluginloader.lua | 15 | ||||
-rw-r--r-- | util/prosodyctl.lua | 86 | ||||
-rw-r--r-- | util/pubsub.lua | 341 | ||||
-rw-r--r-- | util/stanza.lua | 27 | ||||
-rw-r--r-- | util/x509.lua | 211 | ||||
-rw-r--r-- | util/xmppstream.lua | 23 |
6 files changed, 678 insertions, 25 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/stanza.lua b/util/stanza.lua index 7d1f5693..bf944115 100644 --- a/util/stanza.lua +++ b/util/stanza.lua @@ -329,24 +329,21 @@ function deserialize(stanza) return stanza; end -function clone(stanza) - local lookup_table = {}; - local function _copy(object) - if type(object) ~= "table" then - return object; - elseif lookup_table[object] then - return lookup_table[object]; +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 - local new_table = {}; - lookup_table[object] = new_table; - for index, value in pairs(object) do - new_table[_copy(index)] = _copy(value); - end - return setmetatable(new_table, getmetatable(object)); + t_insert(new, child); end - - return _copy(stanza) + return setmetatable(new, stanza_mt); end +clone = _clone; function message(attr, body) if not body then 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) |