diff options
-rw-r--r-- | core/certmanager.lua | 4 | ||||
-rw-r--r-- | net/adns.lua | 79 | ||||
-rw-r--r-- | net/dns.lua | 2 | ||||
-rw-r--r-- | plugins/mod_limits.lua | 96 | ||||
-rw-r--r-- | plugins/mod_s2s/mod_s2s.lua | 2 | ||||
-rw-r--r-- | plugins/mod_s2s/s2sout.lib.lua | 10 | ||||
-rw-r--r-- | plugins/mod_server_contact_info.lua | 49 | ||||
-rwxr-xr-x | prosody | 4 | ||||
-rw-r--r-- | prosody.cfg.lua.dist | 90 | ||||
-rwxr-xr-x | prosodyctl | 2 | ||||
-rw-r--r-- | util/dataforms.lua | 40 |
11 files changed, 285 insertions, 93 deletions
diff --git a/core/certmanager.lua b/core/certmanager.lua index 2e237595..288836ce 100644 --- a/core/certmanager.lua +++ b/core/certmanager.lua @@ -58,6 +58,7 @@ local key_try = { "", "/%s.key", "/%s/privkey.pem", "/%s.pem", }; local function find_cert(user_certs, name) local certs = resolve_path(config_path, user_certs or global_certificates); + log("debug", "Searching %s for a key and certificate for %s...", certs, name); for i = 1, #crt_try do local crt_path = certs .. crt_try[i]:format(name); local key_path = certs .. key_try[i]:format(name); @@ -66,13 +67,16 @@ local function find_cert(user_certs, name) if key_path:sub(-4) == ".crt" then key_path = key_path:sub(1, -4) .. "key"; if stat(key_path, "mode") == "file" then + log("debug", "Selecting certificate %s with key %s for %s", crt_path, key_path, name); return { certificate = crt_path, key = key_path }; end elseif stat(key_path, "mode") == "file" then + log("debug", "Selecting certificate %s with key %s for %s", crt_path, key_path, name); return { certificate = crt_path, key = key_path }; end end end + log("debug", "No certificate/key found for %s", name); end local function find_host_cert(host) diff --git a/net/adns.lua b/net/adns.lua index 0b7247ed..f1196a6c 100644 --- a/net/adns.lua +++ b/net/adns.lua @@ -7,7 +7,7 @@ -- local server = require "net.server"; -local dns = require "net.dns"; +local new_resolver = require "net.dns".resolver; local log = require "util.logger".init("adns"); @@ -17,35 +17,11 @@ local function dummy_send(sock, data, i, j) return (j-i)+1; end local _ENV = nil; -local function lookup(handler, qname, qtype, qclass) - 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 = dns.query(qname, qtype, qclass); - if ok then - coroutine.yield({ qclass or "IN", qtype or "A", qname, coroutine.running()}); -- Wait for reply - log("debug", "Reply for %s (%s)", qname, tostring(coroutine.running())); - end - if ok then - ok, err = pcall(handler, dns.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)(dns.peek(qname, qtype, qclass)); -end +local async_resolver_methods = {}; +local async_resolver_mt = { __index = async_resolver_methods }; -local function cancel(handle, call_handler, reason) - log("warn", "Cancelling DNS lookup for %s", tostring(handle[3])); - dns.cancel(handle[1], handle[2], handle[3], handle[4], call_handler); -end +local query_methods = {}; +local query_mt = { __index = query_methods }; local function new_async_socket(sock, resolver) local peername = "<unknown>"; @@ -54,7 +30,7 @@ local function new_async_socket(sock, resolver) local err; function listener.onincoming(conn, data) if data then - dns.feed(handler, data); + resolver:feed(handler, data); end end function listener.ondisconnect(conn, err) @@ -85,10 +61,47 @@ local function new_async_socket(sock, resolver) return handler; end -dns.socket_wrapper_set(new_async_socket); +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 = lookup; - cancel = cancel; + lookup = function (...) + return new_async_resolver():lookup(...); + end; + resolver = new_async_resolver; new_async_socket = new_async_socket; }; diff --git a/net/dns.lua b/net/dns.lua index 188bdf43..5ba3db0e 100644 --- a/net/dns.lua +++ b/net/dns.lua @@ -504,7 +504,7 @@ function resolver:rr() -- - - - - - - - - - - - - - - - - - - - - - - - rr rr.ttl = 0x10000*self:word() + self:word(); rr.rdlength = self:word(); - rr.tod = self.time + math.min(rr.ttl, 1); + rr.tod = self.time + math.max(rr.ttl, 1); local remember = self.offset; local rr_parser = self[dns.type[rr.type]]; diff --git a/plugins/mod_limits.lua b/plugins/mod_limits.lua new file mode 100644 index 00000000..2a6ee8a2 --- /dev/null +++ b/plugins/mod_limits.lua @@ -0,0 +1,96 @@ +-- Because we deal we pre-authed sessions and streams we can't be host-specific +module:set_global(); + +local filters = require "util.filters"; +local throttle = require "util.throttle"; +local timer = require "util.timer"; + +local limits_cfg = module:get_option("limits", {}); +local limits_resolution = module:get_option_number("limits_resolution", 1); + +local default_bytes_per_second = 3000; +local default_burst = 2; + +local rate_units = { b = 1, k = 3, m = 6, g = 9, t = 12 } -- Plan for the future. +local function parse_rate(rate, sess_type) + local quantity, unit, exp; + if rate then + quantity, unit = rate:match("^(%d+) ?([^/]+)/s$"); + exp = quantity and rate_units[unit:sub(1,1):lower()]; + end + if not exp then + module:log("error", "Error parsing rate for %s: %q, using default rate (%d bytes/s)", sess_type, rate, default_bytes_per_second); + return default_bytes_per_second; + end + return quantity*(10^exp); +end + +local function parse_burst(burst, sess_type) + if type(burst) == "string" then + burst = burst:match("^(%d+) ?s$"); + end + local n_burst = tonumber(burst); + if not n_burst then + module:log("error", "Unable to parse burst for %s: %q, using default burst interval (%ds)", sess_type, tostring(burst), default_burst); + end + return n_burst or default_burst; +end + +-- Process config option into limits table: +-- limits = { c2s = { bytes_per_second = X, burst_seconds = Y } } +local limits = {}; + +for sess_type, sess_limits in pairs(limits_cfg) do + limits[sess_type] = { + bytes_per_second = parse_rate(sess_limits.rate, sess_type); + burst_seconds = parse_burst(sess_limits.burst, sess_type); + }; +end + +local default_filter_set = {}; + +function default_filter_set.bytes_in(bytes, session) + local throttle = session.throttle; + if throttle then + local ok, balance, outstanding = throttle:poll(#bytes, true); + if not ok then + session.log("debug", "Session over rate limit (%d) with %d (by %d), pausing", throttle.max, #bytes, outstanding); + session.conn:pause(); -- Read no more data from the connection until there is no outstanding data + local outstanding_data = bytes:sub(-outstanding); + bytes = bytes:sub(1, #bytes-outstanding); + timer.add_task(limits_resolution, function () + if not session.conn then return; end + if throttle:peek(#outstanding_data) then + session.log("debug", "Resuming paused session"); + session.conn:resume(); + end + -- Handle what we can of the outstanding data + session.data(outstanding_data); + end); + end + end + return bytes; +end + +local type_filters = { + c2s = default_filter_set; + s2sin = default_filter_set; + s2sout = default_filter_set; +}; + +local function filter_hook(session) + local session_type = session.type:match("^[^_]+"); + local filter_set, opts = type_filters[session_type], limits[session_type]; + if opts then + session.throttle = throttle.create(opts.bytes_per_second * opts.burst_seconds, opts.burst_seconds); + filters.add_filter(session, "bytes/in", filter_set.bytes_in, 1000); + end +end + +function module.load() + filters.add_filter_hook(filter_hook); +end + +function module.unload() + filters.remove_filter_hook(filter_hook); +end diff --git a/plugins/mod_s2s/mod_s2s.lua b/plugins/mod_s2s/mod_s2s.lua index b46b7e2a..bfd8f9af 100644 --- a/plugins/mod_s2s/mod_s2s.lua +++ b/plugins/mod_s2s/mod_s2s.lua @@ -180,6 +180,7 @@ end -- Stream is authorised, and ready for normal stanzas function mark_connected(session) + local sendq = session.sendq; local from, to = session.from_host, session.to_host; @@ -211,6 +212,7 @@ function mark_connected(session) session.sendq = nil; end + session.resolver = nil; session.ip_hosts = nil; session.srv_hosts = nil; end diff --git a/plugins/mod_s2s/s2sout.lib.lua b/plugins/mod_s2s/s2sout.lib.lua index 61d6086e..cd8553e1 100644 --- a/plugins/mod_s2s/s2sout.lib.lua +++ b/plugins/mod_s2s/s2sout.lib.lua @@ -49,6 +49,8 @@ function s2sout.initiate_connection(host_session) initialize_filters(host_session); host_session.version = 1; + host_session.resolver = adns.resolver(); + -- Kick the connection attempting machine into life if not s2sout.attempt_connection(host_session) then -- Intentionally not returning here, the @@ -84,9 +86,7 @@ function s2sout.attempt_connection(host_session, err) if not err then -- This is our first attempt log("debug", "First attempt to connect to %s, starting with SRV lookup...", to_host); host_session.connecting = true; - local handle; - handle = adns.lookup(function (answer) - handle = nil; + host_session.resolver:lookup(function (answer) local srv_hosts = { answer = answer }; host_session.srv_hosts = srv_hosts; host_session.srv_choice = 0; @@ -168,7 +168,7 @@ function s2sout.try_connect(host_session, connect_host, connect_port, err) local have_other_result = not(has_ipv4) or not(has_ipv6) or false; if has_ipv4 then - handle4 = adns.lookup(function (reply, err) + handle4 = host_session.resolver:lookup(function (reply, err) handle4 = nil; if reply and reply[#reply] and reply[#reply].a then @@ -206,7 +206,7 @@ function s2sout.try_connect(host_session, connect_host, connect_port, err) end if has_ipv6 then - handle6 = adns.lookup(function (reply, err) + handle6 = host_session.resolver:lookup(function (reply, err) handle6 = nil; if reply and reply[#reply] and reply[#reply].aaaa then diff --git a/plugins/mod_server_contact_info.lua b/plugins/mod_server_contact_info.lua new file mode 100644 index 00000000..7ee8a08f --- /dev/null +++ b/plugins/mod_server_contact_info.lua @@ -0,0 +1,49 @@ +-- XEP-0157: Contact Addresses for XMPP Services for Prosody +-- +-- Copyright (C) 2011-2016 Kim Alvefur +-- +-- This file is MIT/X11 licensed. +-- + +local t_insert = table.insert; +local array = require "util.array"; +local df_new = require "util.dataforms".new; + +-- Source: http://xmpp.org/registrar/formtypes.html#http:--jabber.org-network-serverinfo +local valid_types = { + abuse = true; + admin = true; + feedback = true; + sales = true; + security = true; + support = true; +} + +local contact_config = module:get_option("contact_info"); +if not contact_config or not next(contact_config) then -- we'll use admins from the config as default + local admins = module:get_option_inherited_set("admins", {}); + if admins:empty() then + module:log("error", "No contact_info or admins set in config"); + return -- Nothing to attach, so we'll just skip it. + end + module:log("info", "No contact_info in config, using admins as fallback"); + contact_config = { + admin = array.collect( admins / function(admin) return "xmpp:" .. admin; end); + }; +end + +local form_layout = { + { value = "http://jabber.org/network/serverinfo"; type = "hidden"; name = "FORM_TYPE"; }; +}; + +local form_values = {}; + +for t in pairs(valid_types) do + local addresses = contact_config[t]; + if addresses then + t_insert(form_layout, { name = t .. "-addresses", type = "list-multi" }); + form_values[t .. "-addresses"] = addresses; + end +end + +module:add_extension(df_new(form_layout):form(form_values, "result")); @@ -20,8 +20,8 @@ CFG_DATADIR=CFG_DATADIR or os.getenv("PROSODY_DATADIR"); local function is_relative(path) local path_sep = package.config:sub(1,1); - return ((path_sep == "/" and path:sub(1,1) ~= "/") - or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\"))) + return ((path_sep == "/" and path:sub(1,1) ~= "/") + or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\"))) end -- Tell Lua where to find our libraries diff --git a/prosody.cfg.lua.dist b/prosody.cfg.lua.dist index c42e56d0..bd897f74 100644 --- a/prosody.cfg.lua.dist +++ b/prosody.cfg.lua.dist @@ -4,7 +4,8 @@ -- website at https://prosody.im/doc/configure -- -- Tip: You can check that the syntax of this file is correct --- when you have finished by running: prosodyctl check config +-- when you have finished by running this command: +-- prosodyctl check config -- If there are any errors, it will let you know what and where -- they are, otherwise it will keep quiet. -- @@ -26,9 +27,14 @@ admins = { } -- For more information see: https://prosody.im/doc/libevent --use_libevent = true +-- Prosody will always look in its source directory for modules, but +-- this option allows you to specify additional locations where Prosody +-- will look for modules first. For community modules, see https://modules.prosody.im/ +--plugin_paths = {} + -- This is the list of modules Prosody will load on startup. -- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too. --- Documentation on modules can be found at: https://prosody.im/doc/modules +-- Documentation for bundled modules can be found at: https://prosody.im/doc/modules modules_enabled = { -- Generally required @@ -39,20 +45,19 @@ modules_enabled = { "disco"; -- Service discovery -- Not essential, but recommended + "carbons"; -- Keep multiple clients in sync + "pep"; -- Enables users to publish their mood, activity, playing music and more "private"; -- Private XML storage (for room bookmarks, etc.) + "blocklist"; -- Allow users to block communications with other users "vcard"; -- Allow users to set vCards - -- These are commented by default as they have a performance impact - --"blocklist"; -- Allow users to block communications with other users - --"compression"; -- Stream compression - -- Nice to have "version"; -- Replies to server version requests "uptime"; -- Report how long server has been running "time"; -- Let others know the time here on this server "ping"; -- Replies to XMPP pings with pongs - "pep"; -- Enables users to publish their mood, activity, playing music and more "register"; -- Allow users to register on this server using a client and change passwords + --"mam"; -- Store messages in an archive and allow users to access it -- Admin interfaces "admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands @@ -60,15 +65,19 @@ modules_enabled = { -- HTTP modules --"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP" + --"websockets"; -- XMPP over WebSockets --"http_files"; -- Serve static files from a directory over HTTP -- Other specific functionality + --"limits"; -- Enable bandwidth limiting for XMPP connections --"groups"; -- Shared roster support + --"server_contact_info"; -- Publish contact information for this service --"announce"; -- Send announcement to all online users --"welcome"; -- Welcome users who register accounts --"watchregistrations"; -- Alert admins of registrations --"motd"; -- Send a message to users when they log in --"legacyauth"; -- Legacy authentication. Only used by some old clients and bots. + --"proxy65"; -- Enables a file transfer proxy service which clients behind NAT can use } -- These modules are auto-loaded, but should you want @@ -84,18 +93,18 @@ modules_disabled = { -- For more information see https://prosody.im/doc/creating_accounts allow_registration = false --- These are the SSL/TLS-related settings. If you don't want --- to use SSL/TLS, you may comment or remove this -ssl = { - key = "certs/localhost.key"; - certificate = "certs/localhost.crt"; -} - -- Force clients to use encrypted connections? This option will -- prevent clients from authenticating unless they are using encryption. c2s_require_encryption = true +-- Force servers to use encrypted connections? This option will +-- prevent servers from authenticating unless they are using encryption. +-- Note that this is different from authentication + +s2s_require_encryption = true + + -- Force certificate authentication for server-to-server connections? -- This provides ideal security, but requires servers you communicate -- with to support encryption AND present valid, trusted certificates. @@ -104,11 +113,12 @@ c2s_require_encryption = true s2s_secure_auth = false --- Many servers don't support encryption or have invalid or self-signed --- certificates. You can list domains here that will not be required to --- authenticate using certificates. They will be authenticated using DNS. +-- Some servers have invalid or self-signed certificates. You can list +-- remote domains here that will not be required to authenticate using +-- certificates. They will be authenticated using DNS instead, even +-- when s2s_secure_auth is enabled. ---s2s_insecure_domains = { "gmail.com" } +--s2s_insecure_domains = { "insecure.example" } -- Even if you leave s2s_secure_auth disabled, you can still require valid -- certificates for some domains by specifying a list here. @@ -122,7 +132,7 @@ s2s_secure_auth = false -- server please see https://prosody.im/doc/modules/mod_auth_internal_hashed -- for information about using the hashed backend. -authentication = "internal_plain" +authentication = "internal_hashed" -- Select the storage backend to use. By default Prosody uses flat files -- in its configured data directory, but it also supports more backends @@ -136,6 +146,18 @@ authentication = "internal_plain" --sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" } --sql = { driver = "PostgreSQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" } + +-- Archiving configuration +-- If mod_mam is enabled, Prosody will store a copy of every message. This +-- is used to synchronize conversations between multiple clients, even if +-- they are offline. This setting controls how long Prosody will keep +-- messages in the archive before removing them. + +archive_expires_after = "1w" -- Remove archived messages after 1 week + +-- You can also configure messages to be stored in-memory only. For more +-- archiving options, see https://prosody.im/doc/modules/mod_mam + -- Logging configuration -- For advanced logging see https://prosody.im/doc/logging log = { @@ -145,23 +167,28 @@ log = { -- "*console"; -- Log to the console, useful for debugging with daemonize=false } +-- Uncomment to enable statistics +-- For more info see https://prosody.im/doc/statistics +-- statistics = "internal" + +-- Certificates +-- Every virtual host and component needs a certificate so that clients and +-- servers can securely verify its identity. Prosody will automatically load +-- certificates/keys from the directory specified here. +-- For more information, including how to use 'prosodyctl' to auto-import certificates +-- (from e.g. Let's Encrypt) see https://prosody.im/doc/certificates + +-- Location of directory to find certificates in (relative to main config file): +certificates = "certs" + ----------- Virtual hosts ----------- -- You need to add a VirtualHost entry for each domain you wish Prosody to serve. -- Settings under each VirtualHost entry apply *only* to that host. VirtualHost "localhost" -VirtualHost "example.com" - enabled = false -- Remove this line to enable this host - - -- Assign this host a certificate for TLS, otherwise it would use the one - -- set in the global section (if any). - -- Note that old-style SSL on port 5223 only supports one certificate, and will always - -- use the global one. - ssl = { - key = "certs/example.com.key"; - certificate = "certs/example.com.crt"; - } +--VirtualHost "example.com" +-- certificate = "/path/to/example.crt" ------ Components ------ -- You can specify components to add hosts that provide special services, @@ -171,9 +198,6 @@ VirtualHost "example.com" ---Set up a MUC (multi-user chat) room server on conference.example.com: --Component "conference.example.com" "muc" --- Set up a SOCKS5 bytestream proxy for server-proxied file transfers: ---Component "proxy.example.com" "proxy65" - ---Set up an external component (default component port is 5347) -- -- External components allow adding various services, such as gateways/ @@ -1030,7 +1030,7 @@ function commands.check(arg) suggested_global_modules = set.intersection(suggested_global_modules or set.new(options.modules_enabled), set.new(options.modules_enabled)); end end - if not suggested_global_modules:empty() then + if suggested_global_modules and not suggested_global_modules:empty() then print(" Consider moving these modules into modules_enabled in the global section:") print(" "..tostring(suggested_global_modules / function (x) return ("%q"):format(x) end)); end diff --git a/util/dataforms.lua b/util/dataforms.lua index 756f35a7..469ce976 100644 --- a/util/dataforms.lua +++ b/util/dataforms.lua @@ -68,33 +68,37 @@ function form_t.form(layout, data, formtype) form:tag("value"):text(line):up(); end elseif field_type == "list-single" then - local has_default = false; - for _, val in ipairs(field.options or value) do - if type(val) == "table" then - form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up(); - if value == val.value or val.default and (not has_default) then - form:tag("value"):text(val.value):up(); - has_default = true; + if formtype ~= "result" then + local has_default = false; + for _, val in ipairs(field.options or value) do + if type(val) == "table" then + form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up(); + if value == val.value or 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 - else - form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up(); end end - if field.options and value then + if (field.options or formtype == "result") and value then form:tag("value"):text(value):up(); end elseif field_type == "list-multi" then - for _, val in ipairs(field.options or value) do - if type(val) == "table" then - form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up(); - if not field.options and val.default then - form:tag("value"):text(val.value):up(); + if formtype ~= "result" then + for _, val in ipairs(field.options or value) do + if type(val) == "table" then + form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up(); + if not field.options and 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 - else - form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up(); end end - if field.options and value then + if (field.options or formtype == "result") and value then for _, val in ipairs(value) do form:tag("value"):text(val):up(); end |