diff options
193 files changed, 11155 insertions, 6066 deletions
diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 00000000..590f9c37 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,12 @@ +cache = true +read_globals = { "prosody", "hosts", "import" } +globals = { "_M" } +allow_defined_top = true +module = true +unused_secondaries = false +codes = true +ignore = { "411/err", "421/err", "411/ok", "421/ok", "211/_ENV" } + +files["plugins/"] = { + ignore = { "122/module" }; +} diff --git a/CHANGES b/CHANGES new file mode 100644 index 00000000..3d277bfd --- /dev/null +++ b/CHANGES @@ -0,0 +1,25 @@ +0.10.not-released-yet +===================== + +**YYYY-MM-DD** + +New features +------------ + +- Rewritten SQL storage module with Archive support +- SCRAM-SHA-1-PLUS +- `prosodyctl check` +- Statistics +- Improved TLS configuration +- Lua 5.2 support +- mod\_blocklist (XEP-0191) +- mod\_carbons (XEP-0280) +- Asynchronous operations +- Pluggable connection timeout handling +- mod\_websocket (RFC 7395) + +Removed +------- + +- mod\_privacy (XEP-0016) + @@ -18,7 +18,7 @@ INSTALLEDDATA = $(DATADIR) all: prosody.install prosodyctl.install prosody.cfg.lua.install prosody.version $(MAKE) -C util-src install ifeq ($(EXCERTS),yes) - $(MAKE) -C certs localhost.crt example.com.crt || true + -$(MAKE) -C certs localhost.crt example.com.crt endif install: prosody.install prosodyctl.install prosody.cfg.lua.install util/encodings.so util/encodings.so util/pposix.so util/signal.so @@ -31,8 +31,9 @@ install: prosody.install prosodyctl.install prosody.cfg.lua.install util/encodin install -m755 ./prosodyctl.install $(BIN)/prosodyctl install -m644 core/*.lua $(SOURCE)/core install -m644 net/*.lua $(SOURCE)/net - install -d $(SOURCE)/net/http + install -d $(SOURCE)/net/http $(SOURCE)/net/websocket install -m644 net/http/*.lua $(SOURCE)/net/http + install -m644 net/websocket/*.lua $(SOURCE)/net/websocket install -m644 util/*.lua $(SOURCE)/util install -m644 util/*.so $(SOURCE)/util install -d $(SOURCE)/util/sasl @@ -40,8 +41,8 @@ install: prosody.install prosodyctl.install prosody.cfg.lua.install util/encodin umask 0022 && cp -r plugins/* $(MODULES) install -m644 certs/* $(CONFIG)/certs install -m644 man/prosodyctl.man $(MAN)/man1/prosodyctl.1 - test -e $(CONFIG)/prosody.cfg.lua || install -m644 prosody.cfg.lua.install $(CONFIG)/prosody.cfg.lua - test -e prosody.version && install -m644 prosody.version $(SOURCE)/prosody.version || true + test -f $(CONFIG)/prosody.cfg.lua || install -m644 prosody.cfg.lua.install $(CONFIG)/prosody.cfg.lua + -test -f prosody.version && install -m644 prosody.version $(SOURCE)/prosody.version $(MAKE) install -C util-src clean: @@ -51,6 +52,9 @@ clean: rm -f prosody.version $(MAKE) clean -C util-src +test: + cd tests && $(RUNWITH) test.lua + util/%.so: $(MAKE) install -C util-src @@ -64,8 +68,16 @@ util/%.so: prosody.cfg.lua.install: prosody.cfg.lua.dist sed 's|certs/|$(INSTALLEDCONFIG)/certs/|' $^ > $@ -prosody.version: $(wildcard prosody.release .hg/dirstate) - test -e .hg/dirstate && \ - hexdump -n6 -e'6/1 "%02x"' .hg/dirstate > $@ || true - test -f prosody.release && \ - cp prosody.release $@ || true +%.version: %.release + cp $^ $@ + +%.version: .hg_archival.txt + sed -n 's/^node: \(............\).*/\1/p' $^ > $@ + +%.version: .hg/dirstate + hexdump -n6 -e'6/1 "%02x"' $^ > $@ + +%.version: + echo unknown > $@ + + diff --git a/certs/Makefile b/certs/Makefile index c709ff91..b3011a89 100644 --- a/certs/Makefile +++ b/certs/Makefile @@ -15,16 +15,52 @@ keysize=2048 # To request a cert %.csr: %.cnf %.key - openssl req -new -key $(lastword $^) -out $@ -utf8 -config $(firstword $^) + openssl req -new -key $(lastword $^) \ + -sha256 -utf8 -config $(firstword $^) -out $@ + +%.csr: %.cnf + umask 0077 && touch $*.key + openssl req -new -newkey rsa:$(keysize) -nodes -keyout $*.key \ + -sha256 -utf8 -config $^ -out $@ + @chmod 400 $*.key -c + +%.csr: %.key + openssl req -new -key $^ -utf8 -subj /CN=$* -out $@ + +%.csr: + umask 0077 && touch $*.key + openssl req -new -newkey rsa:$(keysize) -nodes -keyout $*.key \ + -utf8 -subj /CN=$* -out $@ + @chmod 400 $*.key -c # Self signed %.crt: %.cnf %.key - openssl req -new -x509 -nodes -key $(lastword $^) -days 365 \ - -sha1 -out $@ -utf8 -config $(firstword $^) + openssl req -new -x509 -key $(lastword $^) -days 365 -sha256 -utf8 \ + -config $(firstword $^) -out $@ + +%.crt: %.cnf + umask 0077 && touch $*.key + openssl req -new -x509 -newkey rsa:$(keysize) -nodes -keyout $*.key \ + -days 365 -sha256 -utf8 -config $(firstword $^) -out $@ + @chmod 400 $*.key -c +%.crt: %.key + openssl req -new -x509 -key $^ -days 365 -sha256 -utf8 -subj /CN=$* -out $@ + +%.crt: + umask 0077 && touch $*.key + openssl req -new -x509 -newkey rsa:$(keysize) -nodes -keyout $*.key \ + -days 365 -sha256 -out $@ -utf8 -subj /CN=$* + @chmod 400 $*.key -c + +# Generate a config from the example %.cnf: sed 's,example\.com,$*,g' openssl.cnf > $@ %.key: umask 0077 && openssl genrsa -out $@ $(keysize) @chmod 400 $@ -c + +# Generate Diffie-Hellman parameters +dh-%.pem: + openssl dhparam -out $@ $* @@ -19,6 +19,8 @@ CXX=g++ LD=gcc RUNWITH=lua EXCERTS=yes +PRNG= +PRNGLIBS= CFLAGS="-fPIC -Wall" LDFLAGS="-shared" @@ -32,7 +34,7 @@ Configure Prosody prior to building. --help This help. --ostype=OS Use one of the OS presets. - May be one of: debian, macosx, linux, freebsd + May be one of: debian, macosx, linux, freebsd, openbsd --prefix=DIR Prefix where Prosody should be installed. Default is $PREFIX --sysconfdir=DIR Location where the config file should be installed. @@ -58,6 +60,11 @@ Configure Prosody prior to building. icu: use ICU from IBM --with-ssl=LIB The name of the SSL to link with. Default is $OPENSSL_LIB +--with-random=METHOD CSPRNG backend to use. One of + getrandom: Linux kernel + arc4random: OpenBSD kernel + openssl: OpenSSL RAND method + Default is to use /dev/urandom --cflags=FLAGS Flags to pass to the compiler Default is $CFLAGS --ldflags=FLAGS Flags to pass to the linker @@ -99,32 +106,32 @@ do --ostype=*) OSTYPE="$value" OSTYPE_SET=yes - if [ "$OSTYPE" = "debian" ] - then LUA_SUFFIX="5.1"; - LUA_SUFFIX_SET=yes - RUNWITH="lua5.1" - LUA_INCDIR=/usr/include/lua5.1; - LUA_INCDIR_SET=yes - CFLAGS="$CFLAGS -D_GNU_SOURCE" - fi - if [ "$OSTYPE" = "macosx" ] - then LUA_INCDIR=/usr/local/include; - LUA_INCDIR_SET=yes - LUA_LIBDIR=/usr/local/lib - LUA_LIBDIR_SET=yes - LDFLAGS="-bundle -undefined dynamic_lookup" - fi - if [ "$OSTYPE" = "linux" ] - then LUA_INCDIR=/usr/local/include; + if [ "$OSTYPE" = "debian" ]; then + LUA_SUFFIX="5.1"; + LUA_SUFFIX_SET=yes + RUNWITH="lua5.1" + LUA_INCDIR=/usr/include/lua5.1; + LUA_INCDIR_SET=yes + CFLAGS="$CFLAGS -D_GNU_SOURCE" + fi + if [ "$OSTYPE" = "macosx" ]; then + LUA_INCDIR=/usr/local/include; + LUA_INCDIR_SET=yes + LUA_LIBDIR=/usr/local/lib + LUA_LIBDIR_SET=yes + CFLAGS="$CFLAGS -mmacosx-version-min=10.3" + LDFLAGS="-bundle -undefined dynamic_lookup" + fi + if [ "$OSTYPE" = "linux" ]; then + LUA_INCDIR=/usr/local/include; LUA_INCDIR_SET=yes LUA_LIBDIR=/usr/local/lib LUA_LIBDIR_SET=yes - CFLAGS="-Wall -fPIC" - CFLAGS="$CFLAGS -D_GNU_SOURCE" + CFLAGS="-Wall -fPIC -D_GNU_SOURCE" LDFLAGS="-shared" - fi - if [ "$OSTYPE" = "freebsd" -o "$OSTYPE" = "openbsd" ] - then LUA_INCDIR="/usr/local/include/lua51" + fi + if [ "$OSTYPE" = "freebsd" -o "$OSTYPE" = "openbsd" ]; then + LUA_INCDIR="/usr/local/include/lua51" LUA_INCDIR_SET=yes CFLAGS="-Wall -fPIC -I/usr/local/include" LDFLAGS="-I/usr/local/include -L/usr/local/lib -shared" @@ -132,10 +139,12 @@ do LUA_SUFFIX_SET=yes LUA_DIR=/usr/local LUA_DIR_SET=yes - fi - if [ "$OSTYPE" = "openbsd" ] - then LUA_INCDIR="/usr/local/include"; - fi + CC=cc + LD=ld + fi + if [ "$OSTYPE" = "openbsd" ]; then + LUA_INCDIR="/usr/local/include"; + fi ;; --libdir=*) LIBDIR="$value" @@ -172,6 +181,16 @@ do --with-ssl=*) OPENSSL_LIB="$value" ;; + --with-random=getrandom) + PRNG=GETRANDOM + ;; + --with-random=openssl) + PRNG=OPENSSL + PRNGLIBS=-lcrypto + ;; + --with-random=arc4random) + PRNG=ARC4RANDOM + ;; --cflags=*) CFLAGS="$value" ;; @@ -226,7 +245,7 @@ find_program() { found="no" while [ "$item" ] do - if [ -e "$item/$1" ] + if [ -f "$item/$1" ] then found="yes" break @@ -249,7 +268,7 @@ then LUA_SUFFIX="$suffix" if [ "$LUA_DIR_SET" = "yes" ] then - if [ -e "$LUA_DIR/bin/lua$suffix" ] + if [ -f "$LUA_DIR/bin/lua$suffix" ] then find_lua="$LUA_DIR" fi @@ -264,7 +283,7 @@ then done fi -if ! [ "$LUA_DIR_SET" = "yes" ] +if [ "$LUA_DIR_SET" != "yes" ] then echo -n "Looking for Lua... " if [ ! "$find_lua" ] @@ -283,12 +302,12 @@ then fi fi -if ! [ "$LUA_INCDIR_SET" = "yes" ] +if [ "$LUA_INCDIR_SET" != "yes" ] then LUA_INCDIR="$LUA_DIR/include" fi -if ! [ "$LUA_LIBDIR_SET" = "yes" ] +if [ "$LUA_LIBDIR_SET" != "yes" ] then LUA_LIBDIR="$LUA_DIR/lib" fi @@ -303,14 +322,16 @@ then IDNA_LIBS="$ICU_FLAGS" CFLAGS="$CFLAGS -DUSE_STRINGPREP_ICU" fi -if [ "$IDN_LIBRARY" = "idn" ] +if [ "$IDN_LIBRARY" = "idn" ] then IDNA_LIBS="-l$IDN_LIB" fi +OPENSSL_LIBS="-l$OPENSSL_LIB" + echo -n "Checking Lua includes... " lua_h="$LUA_INCDIR/lua.h" -if [ -e "$lua_h" ] +if [ -f "$lua_h" ] then echo "lua.h found in $lua_h" else @@ -360,7 +381,7 @@ LUA_BINDIR=$LUA_BINDIR REQUIRE_CONFIG=$REQUIRE_CONFIG IDN_LIB=$IDN_LIB IDNA_LIBS=$IDNA_LIBS -OPENSSL_LIB=$OPENSSL_LIB +OPENSSL_LIBS=$OPENSSL_LIBS CFLAGS=$CFLAGS LDFLAGS=$LDFLAGS CC=$CC @@ -368,6 +389,9 @@ CXX=$CXX LD=$LD RUNWITH=$RUNWITH EXCERTS=$EXCERTS +RANDOM=$PRNG +RANDOM_LIBS=$PRNGLIBS + EOF diff --git a/core/certmanager.lua b/core/certmanager.lua index 624bd841..a4c9d891 100644 --- a/core/certmanager.lua +++ b/core/certmanager.lua @@ -1,97 +1,173 @@ -- 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 softreq = require"util.dependencies".softreq; +local ssl = softreq"ssl"; +if not ssl then + return { + create_context = function () + return nil, "LuaSec (required for encryption) was not found"; + end; + reload_ssl_config = function () end; + } +end + local configmanager = require "core.configmanager"; local log = require "util.logger".init("certmanager"); -local ssl = ssl; -local ssl_newcontext = ssl and ssl.newcontext; - -local tostring = tostring; +local ssl_context = ssl.context or softreq"ssl.context"; +local ssl_x509 = ssl.x509 or softreq"ssl.x509"; +local ssl_newcontext = ssl.newcontext; +local new_config = require"util.sslconfig".new; +local stat = require "lfs".attributes; + +local tonumber, tostring = tonumber, tostring; +local pairs = pairs; local type = type; local io_open = io.open; +local select = select; local prosody = prosody; -local resolve_path = configmanager.resolve_relative_path; +local resolve_path = require"util.paths".resolve_relative_path; local config_path = prosody.paths.config; -local luasec_has_noticket, luasec_has_verifyext, luasec_has_no_compression; -if ssl then - local luasec_major, luasec_minor = ssl._VERSION:match("^(%d+)%.(%d+)"); - luasec_has_noticket = tonumber(luasec_major)>0 or tonumber(luasec_minor)>=4; - luasec_has_verifyext = tonumber(luasec_major)>0 or tonumber(luasec_minor)>=5; - luasec_has_no_compression = tonumber(luasec_major)>0 or tonumber(luasec_minor)>=5; -end +local luasec_major, luasec_minor = ssl._VERSION:match("^(%d+)%.(%d+)"); +local luasec_version = luasec_major * 100 + luasec_minor; +local luasec_has = { + -- TODO If LuaSec ever starts exposing these things itself, use that instead + cipher_server_preference = luasec_version >= 2; + no_ticket = luasec_version >= 4; + no_compression = luasec_version >= 5; + single_dh_use = luasec_version >= 2; + single_ecdh_use = luasec_version >= 2; +}; -module "certmanager" +local _ENV = nil; -- Global SSL options if not overridden per-host -local default_ssl_config = configmanager.get("*", "ssl"); -local default_capath = "/etc/ssl/certs"; -local default_verify = (ssl and ssl.x509 and { "peer", "client_once", }) or "none"; -local default_options = { "no_sslv2", "no_sslv3", "cipher_server_preference", luasec_has_noticket and "no_ticket" or nil }; -local default_verifyext = { "lsec_continue", "lsec_ignore_purpose" }; - -if ssl and not luasec_has_verifyext and ssl.x509 then - -- COMPAT mw/luasec-hg - for i=1,#default_verifyext do -- Remove lsec_ prefix - default_verify[#default_verify+1] = default_verifyext[i]:sub(6); +local global_ssl_config = configmanager.get("*", "ssl"); + +local global_certificates = configmanager.get("*", "certificates") or "certs"; + +local crt_try = { "", "/%s.crt", "/%s/fullchain.pem", "/%s.pem", }; +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); + for i = 1, #crt_try do + local crt_path = certs .. crt_try[i]:format(name); + local key_path = certs .. key_try[i]:format(name); + + if stat(crt_path, "mode") == "file" then + if key_path:sub(-4) == ".crt" then + key_path = key_path:sub(1, -4) .. "key"; + if stat(key_path, "mode") == "file" then + return { certificate = crt_path, key = key_path }; + end + elseif stat(key_path, "mode") == "file" then + return { certificate = crt_path, key = key_path }; + end + end end end -if luasec_has_no_compression and configmanager.get("*", "ssl_compression") ~= true then - default_options[#default_options+1] = "no_compression"; + +local function find_host_cert(host) + if not host then return nil; end + return find_cert(configmanager.get(host, "certificate"), host) or find_host_cert(host:match("%.(.+)$")); end -if luasec_has_no_compression then -- Has no_compression? Then it has these too... - default_options[#default_options+1] = "single_dh_use"; - default_options[#default_options+1] = "single_ecdh_use"; +local function find_service_cert(service, port) + local cert_config = configmanager.get("*", service.."_certificate"); + if type(cert_config) == "table" then + cert_config = cert_config[port] or cert_config.default; + end + return find_cert(cert_config, service); end -function create_context(host, mode, user_ssl_config) - user_ssl_config = user_ssl_config or default_ssl_config; - - if not ssl then return nil, "LuaSec (required for encryption) was not found"; end - if not user_ssl_config then return nil, "No SSL/TLS configuration present for "..host; end - - local ssl_config = { - mode = mode; - protocol = user_ssl_config.protocol or "sslv23"; - key = resolve_path(config_path, user_ssl_config.key); - password = user_ssl_config.password or function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host); end; - certificate = resolve_path(config_path, user_ssl_config.certificate); - capath = resolve_path(config_path, user_ssl_config.capath or default_capath); - cafile = resolve_path(config_path, user_ssl_config.cafile); - verify = user_ssl_config.verify or default_verify; - verifyext = user_ssl_config.verifyext or default_verifyext; - options = user_ssl_config.options or default_options; - depth = user_ssl_config.depth; - curve = user_ssl_config.curve or "secp384r1"; - ciphers = user_ssl_config.ciphers or "HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK:!SRP:!3DES:!aNULL"; - dhparam = user_ssl_config.dhparam; +-- Built-in defaults +local core_defaults = { + capath = "/etc/ssl/certs"; + depth = 9; + protocol = "tlsv1+"; + verify = (ssl_x509 and { "peer", "client_once", }) or "none"; + options = { + cipher_server_preference = luasec_has.cipher_server_preference; + no_ticket = luasec_has.no_ticket; + no_compression = luasec_has.no_compression and configmanager.get("*", "ssl_compression") ~= true; + single_dh_use = luasec_has.single_dh_use; + single_ecdh_use = luasec_has.single_ecdh_use; }; + verifyext = { "lsec_continue", "lsec_ignore_purpose" }; + curve = "secp384r1"; + ciphers = "HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK:!SRP:!3DES:!aNULL"; +} +local path_options = { -- These we pass through resolve_path() + key = true, certificate = true, cafile = true, capath = true, dhparam = true +} + +if luasec_version < 5 and ssl_x509 then + -- COMPAT mw/luasec-hg + for i=1,#core_defaults.verifyext do -- Remove lsec_ prefix + core_defaults.verify[#core_defaults.verify+1] = core_defaults.verifyext[i]:sub(6); + end +end + +local function create_context(host, mode, ...) + local cfg = new_config(); + cfg:apply(core_defaults); + local service_name, port = host:match("^(%w+) port (%d+)$"); + if service_name then + cfg:apply(find_service_cert(service_name, tonumber(port))); + else + cfg:apply(find_host_cert(host)); + end + cfg:apply({ + mode = mode, + -- We can't read the password interactively when daemonized + password = function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host); end; + }); + cfg:apply(global_ssl_config); + + for i = select('#', ...), 1, -1 do + cfg:apply(select(i, ...)); + end + local user_ssl_config = cfg:final(); + + if mode == "server" then + if not user_ssl_config.key then return nil, "No key present in SSL/TLS configuration for "..host; end + if not user_ssl_config.certificate then return nil, "No certificate present in SSL/TLS configuration for "..host; end + end + + for option in pairs(path_options) do + if type(user_ssl_config[option]) == "string" then + user_ssl_config[option] = resolve_path(config_path, user_ssl_config[option]); + else + user_ssl_config[option] = nil; + end + end -- LuaSec expects dhparam to be a callback that takes two arguments. -- We ignore those because it is mostly used for having a separate -- set of params for EXPORT ciphers, which we don't have by default. - if type(ssl_config.dhparam) == "string" then - local f, err = io_open(resolve_path(config_path, ssl_config.dhparam)); + if type(user_ssl_config.dhparam) == "string" then + local f, err = io_open(user_ssl_config.dhparam); if not f then return nil, "Could not open DH parameters: "..err end local dhparam = f:read("*a"); f:close(); - ssl_config.dhparam = function() return dhparam; end + user_ssl_config.dhparam = function() return dhparam; end end - local ctx, err = ssl_newcontext(ssl_config); + local ctx, err = ssl_newcontext(user_ssl_config); - -- COMPAT: LuaSec 0.4.1 ignores the cipher list from the config, so we have to take - -- care of it ourselves... - if ctx and ssl_config.ciphers then + -- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care + -- of it ourselves (W/A for #x) + if ctx and user_ssl_config.ciphers then local success; - success, err = ssl.context.setcipher(ctx, ssl_config.ciphers); + success, err = ssl_context.setcipher(ctx, user_ssl_config.ciphers); if not success then ctx = nil; end end @@ -100,9 +176,9 @@ function create_context(host, mode, user_ssl_config) local file = err:match("^error loading (.-) %("); if file then if file == "private key" then - file = ssl_config.key or "your private key"; + file = user_ssl_config.key or "your private key"; elseif file == "certificate" then - file = ssl_config.certificate or "your certificate file"; + file = user_ssl_config.certificate or "your certificate file"; end local reason = err:match("%((.+)%)$") or "some reason"; if reason == "Permission denied" then @@ -121,13 +197,19 @@ function create_context(host, mode, user_ssl_config) log("error", "SSL/TLS: Error initialising for %s: %s", host, err); end end - return ctx, err; + return ctx, err, user_ssl_config; end -function reload_ssl_config() - default_ssl_config = configmanager.get("*", "ssl"); +local function reload_ssl_config() + global_ssl_config = configmanager.get("*", "ssl"); + if luasec_has.no_compression then + core_defaults.options.no_compression = configmanager.get("*", "ssl_compression") ~= true; + end end prosody.events.add_handler("config-reloaded", reload_ssl_config); -return _M; +return { + create_context = create_context; + reload_ssl_config = reload_ssl_config; +}; diff --git a/core/configmanager.lua b/core/configmanager.lua index c8aa7b9a..16d4b8e2 100644 --- a/core/configmanager.lua +++ b/core/configmanager.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -15,26 +15,31 @@ local fire_event = prosody and prosody.events.fire_event or function () end; local envload = require"util.envload".envload; local deps = require"util.dependencies"; +local resolve_relative_path = require"util.paths".resolve_relative_path; +local glob_to_pattern = require"util.paths".glob_to_pattern; local path_sep = package.config:sub(1,1); -local have_encodings, encodings = pcall(require, "util.encodings"); -local nameprep = have_encodings and encodings.stringprep.nameprep or function (host) return host:lower(); end +local encodings = deps.softreq"util.encodings"; +local nameprep = encodings and encodings.stringprep.nameprep or function (host) return host:lower(); end + +local _M = {}; +local _ENV = nil; -module "configmanager" +_M.resolve_relative_path = resolve_relative_path; -- COMPAT local parsers = {}; -local config_mt = { __index = function (t, k) return rawget(t, "*"); end}; +local config_mt = { __index = function (t, _) return rawget(t, "*"); end}; local config = setmetatable({ ["*"] = { } }, config_mt); -- When host not found, use global local host_mt = { __index = function(_, k) return config["*"][k] end } -function getconfig() +function _M.getconfig() return config; end -function get(host, key, _oldkey) +function _M.get(host, key, _oldkey) if key == "core" then key = _oldkey; -- COMPAT with code that still uses "core" end @@ -50,11 +55,11 @@ function _M.rawget(host, key, _oldkey) end end -local function set(config, host, key, value) +local function set(config_table, host, key, value) if host and key then - local hostconfig = rawget(config, host); + local hostconfig = rawget(config_table, host); if not hostconfig then - hostconfig = rawset(config, host, setmetatable({}, host_mt))[host]; + hostconfig = rawset(config_table, host, setmetatable({}, host_mt))[host]; end hostconfig[key] = value; return true; @@ -69,55 +74,20 @@ function _M.set(host, key, value, _oldvalue) return set(config, host, key, value); end --- Helper function to resolve relative paths (needed by config) -do - function resolve_relative_path(parent_path, path) - if path then - -- Some normalization - parent_path = parent_path:gsub("%"..path_sep.."+$", ""); - path = path:gsub("^%.%"..path_sep.."+", ""); - - local is_relative; - if path_sep == "/" and path:sub(1,1) ~= "/" then - is_relative = true; - elseif path_sep == "\\" and (path:sub(1,1) ~= "/" and (path:sub(2,3) ~= ":\\" and path:sub(2,3) ~= ":/")) then - is_relative = true; - end - if is_relative then - return parent_path..path_sep..path; - end - end - return path; - end -end +function _M.load(filename, config_format) + config_format = config_format or filename:match("%w+$"); --- Helper function to convert a glob to a Lua pattern -local function glob_to_pattern(glob) - return "^"..glob:gsub("[%p*?]", function (c) - if c == "*" then - return ".*"; - elseif c == "?" then - return "."; - else - return "%"..c; - end - end).."$"; -end - -function load(filename, format) - format = format or filename:match("%w+$"); - - if parsers[format] and parsers[format].load then + if parsers[config_format] and parsers[config_format].load then local f, err = io.open(filename); if f then local new_config = setmetatable({ ["*"] = { } }, config_mt); - local ok, err = parsers[format].load(f:read("*a"), filename, new_config); + local ok, err = parsers[config_format].load(f:read("*a"), filename, new_config); f:close(); if ok then config = new_config; fire_event("config-reloaded", { filename = filename, - format = format, + format = config_format, config = config }); end @@ -126,98 +96,95 @@ function load(filename, format) return f, "file", err; end - if not format then + if not config_format then return nil, "file", "no parser specified"; else - return nil, "file", "no parser for "..(format); + return nil, "file", "no parser for "..(config_format); end end -function save(filename, format) -end - -function addparser(format, parser) - if format and parser then - parsers[format] = parser; +function _M.addparser(config_format, parser) + if config_format and parser then + parsers[config_format] = parser; end end -- _M needed to avoid name clash with local 'parsers' function _M.parsers() local p = {}; - for format in pairs(parsers) do - table.insert(p, format); + for config_format in pairs(parsers) do + table.insert(p, config_format); end return p; end -- Built-in Lua parser do - local pcall, setmetatable = _G.pcall, _G.setmetatable; - local rawget = _G.rawget; + local pcall = _G.pcall; parsers.lua = {}; - function parsers.lua.load(data, config_file, config) + function parsers.lua.load(data, config_file, config_table) local env; -- The ' = true' are needed so as not to set off __newindex when we assign the functions below env = setmetatable({ Host = true, host = true, VirtualHost = true, Component = true, component = true, Include = true, include = true, RunScript = true }, { - __index = function (t, k) + __index = function (_, k) return rawget(_G, k); end, - __newindex = function (t, k, v) - set(config, env.__currenthost or "*", k, v); + __newindex = function (_, k, v) + set(config_table, env.__currenthost or "*", k, v); end }); - + rawset(env, "__currenthost", "*") -- Default is global function env.VirtualHost(name) name = nameprep(name); - if rawget(config, name) and rawget(config[name], "component_module") then + if rawget(config_table, name) and rawget(config_table[name], "component_module") then error(format("Host %q clashes with previously defined %s Component %q, for services use a sub-domain like conference.%s", - name, config[name].component_module:gsub("^%a+$", { component = "external", muc = "MUC"}), name, name), 0); + name, config_table[name].component_module:gsub("^%a+$", { component = "external", muc = "MUC"}), name, name), 0); end rawset(env, "__currenthost", name); -- Needs at least one setting to logically exist :) - set(config, name or "*", "defined", true); + set(config_table, name or "*", "defined", true); return function (config_options) rawset(env, "__currenthost", "*"); -- Return to global scope for option_name, option_value in pairs(config_options) do - set(config, name or "*", option_name, option_value); + set(config_table, name or "*", option_name, option_value); end end; end env.Host, env.host = env.VirtualHost, env.VirtualHost; - + function env.Component(name) name = nameprep(name); - if rawget(config, name) and rawget(config[name], "defined") and not rawget(config[name], "component_module") then + if rawget(config_table, name) and rawget(config_table[name], "defined") and not rawget(config_table[name], "component_module") then error(format("Component %q clashes with previously defined Host %q, for services use a sub-domain like conference.%s", name, name, name), 0); end - set(config, name, "component_module", "component"); + set(config_table, name, "component_module", "component"); -- Don't load the global modules by default - set(config, name, "load_global_modules", false); + set(config_table, name, "load_global_modules", false); rawset(env, "__currenthost", name); local function handle_config_options(config_options) rawset(env, "__currenthost", "*"); -- Return to global scope for option_name, option_value in pairs(config_options) do - set(config, name or "*", option_name, option_value); + set(config_table, name or "*", option_name, option_value); end end - + return function (module) if type(module) == "string" then - set(config, name, "component_module", module); + set(config_table, name, "component_module", module); return handle_config_options; end return handle_config_options(module); end end env.component = env.Component; - + function env.Include(file) + -- Check whether this is a wildcard Include if file:match("[*?]") then local lfs = deps.softreq "lfs"; if not lfs then @@ -237,38 +204,39 @@ do env.Include(path..path_sep..f); end end - else - local file = resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file); - local f, err = io.open(file); - if f then - local ret, err = parsers.lua.load(f:read("*a"), file, config); - if not ret then error(err:gsub("%[string.-%]", file), 0); end - end - if not f then error("Error loading included "..file..": "..err, 0); end - return f, err; + return; + end + -- Not a wildcard, so resolve (potentially) relative path and run through config parser + file = resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file); + local f, err = io.open(file); + if f then + local ret, err = parsers.lua.load(f:read("*a"), file, config_table); + if not ret then error(err:gsub("%[string.-%]", file), 0); end end + if not f then error("Error loading included "..file..": "..err, 0); end + return f, err; end env.include = env.Include; - + function env.RunScript(file) return dofile(resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file)); end - + local chunk, err = envload(data, "@"..config_file, env); - + if not chunk then return nil, err; end - + local ok, err = pcall(chunk); - + if not ok then return nil, err; end - + return true; end - + end return _M; diff --git a/core/hostmanager.lua b/core/hostmanager.lua index 06ba72a1..53c1cd4e 100644 --- a/core/hostmanager.lua +++ b/core/hostmanager.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -13,7 +13,6 @@ local disco_items = require "util.multitable".new(); local NULL = {}; local jid_split = require "util.jid".split; -local uuid_gen = require "util.uuid".generate; local log = require "util.logger".init("hostmanager"); @@ -27,15 +26,31 @@ local core_route_stanza = _G.prosody.core_route_stanza; local pairs, select, rawget = pairs, select, rawget; local tostring, type = tostring, type; +local setmetatable = setmetatable; + +local _ENV = nil; -module "hostmanager" +local host_mt = { } +function host_mt:__tostring() + if self.type == "component" then + local typ = configmanager.get(self.host, "component_module"); + if typ == "component" then + return ("Component %q"):format(self.host); + end + return ("Component %q %q"):format(self.host, typ); + elseif self.type == "local" then + return ("VirtualHost %q"):format(self.host); + end +end local hosts_loaded_once; +local activate, deactivate; + local function load_enabled_hosts(config) local defined_hosts = config or configmanager.getconfig(); local activated_any_host; - + for host, host_config in pairs(defined_hosts) do if host ~= "*" and host_config.enabled ~= false then if not host_config.component_module then @@ -44,11 +59,11 @@ local function load_enabled_hosts(config) activate(host, host_config); end end - + if not activated_any_host then log("error", "No active VirtualHost entries in the config file. This may cause unexpected behaviour as no modules will be loaded."); end - + prosody_events.fire_event("hosts-activated", defined_hosts); hosts_loaded_once = true; end @@ -56,8 +71,8 @@ end prosody_events.add_handler("server-starting", load_enabled_hosts); local function host_send(stanza) - local name, type = stanza.name, stanza.attr.type; - if type == "error" or (name == "iq" and type == "result") then + local name, stanza_type = stanza.name, stanza.attr.type; + if stanza_type == "error" or (name == "iq" and stanza_type == "result") then local dest_host_name = select(2, jid_split(stanza.attr.to)); local dest_host = hosts[dest_host_name] or { type = "unknown" }; log("warn", "Unhandled response sent to %s host %s: %s", dest_host.type, dest_host_name, tostring(stanza)); @@ -74,10 +89,10 @@ function activate(host, host_config) host = host; s2sout = {}; events = events_new(); - dialback_secret = configmanager.get(host, "dialback_secret") or uuid_gen(); send = host_send; modules = {}; }; + setmetatable(host_session, host_mt); if not host_config.component_module then -- host host_session.type = "local"; host_session.sessions = {}; @@ -93,7 +108,7 @@ function activate(host, host_config) log("warn", "%s: Option '%s' has no effect for virtual hosts - put it in the server-wide section instead", host, option_name); end end - + log((hosts_loaded_once and "info") or "debug", "Activated host: %s", host); prosody_events.fire_event("host-activated", host); return true; @@ -104,11 +119,11 @@ function deactivate(host, reason) if not host_session then return nil, "The host "..tostring(host).." is not activated"; end log("info", "Deactivating host: %s", host); prosody_events.fire_event("host-deactivating", { host = host, host_session = host_session, reason = reason }); - + if type(reason) ~= "table" then reason = { condition = "host-gone", text = tostring(reason or "This server has stopped serving "..host) }; end - + -- Disconnect local users, s2s connections -- TODO: These should move to mod_c2s and mod_s2s (how do they know they're being unloaded and not reloaded?) if host_session.sessions then @@ -151,8 +166,12 @@ function deactivate(host, reason) return true; end -function get_children(host) +local function get_children(host) return disco_items:get(host) or NULL; end -return _M; +return { + activate = activate; + deactivate = deactivate; + get_children = get_children; +} diff --git a/core/loggingmanager.lua b/core/loggingmanager.lua index c69dede8..e3a83817 100644 --- a/core/loggingmanager.lua +++ b/core/loggingmanager.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -10,29 +10,28 @@ local format = string.format; local setmetatable, rawset, pairs, ipairs, type = setmetatable, rawset, pairs, ipairs, type; -local io_open, io_write = io.open, io.write; +local stdout = io.stdout; +local io_open = io.open; local math_max, rep = math.max, string.rep; local os_date = os.date; -local getstyle, setstyle = require "util.termcolours".getstyle, require "util.termcolours".setstyle; - -if os.getenv("__FLUSH_LOG") then - local io_flush = io.flush; - local _io_write = io_write; - io_write = function(...) _io_write(...); io_flush(); end -end +local getstyle, getstring = require "util.termcolours".getstyle, require "util.termcolours".getstring; +local tostring = tostring; +local select = select; +local unpack = table.unpack or unpack; --luacheck: ignore 113 local config = require "core.configmanager"; local logger = require "util.logger"; local prosody = prosody; _G.log = logger.init("general"); +prosody.log = logger.init("general"); -module "loggingmanager" +local _ENV = nil; -- The log config used if none specified in the config file (see reload_logging for initialization) local default_logging; local default_file_logging; -local default_timestamp = "%b %d %H:%M:%S"; +local default_timestamp = "%b %d %H:%M:%S "; -- The actual config loggingmanager is using local logging_config; @@ -45,16 +44,16 @@ local logging_levels = { "debug", "info", "warn", "error" } -- This function is called automatically when a new sink type is added [see apply_sink_rules()] local function add_rule(sink_config) local sink_maker = log_sink_types[sink_config.to]; - if sink_maker then - -- Create sink - local sink = sink_maker(sink_config); - - -- Set sink for all chosen levels - for level in pairs(get_levels(sink_config.levels or logging_levels)) do - logger.add_level_sink(level, sink); - end - else - -- No such sink type + if not sink_maker then + return; -- No such sink type + end + + -- Create sink + local sink = sink_maker(sink_config); + + -- Set sink for all chosen levels + for level in pairs(get_levels(sink_config.levels or logging_levels)) do + logger.add_level_sink(level, sink); end end @@ -63,7 +62,7 @@ end -- the log_sink_types table. function apply_sink_rules(sink_type) if type(logging_config) == "table" then - + for _, level in ipairs(logging_levels) do if type(logging_config[level]) == "string" then local value = logging_config[level]; @@ -82,7 +81,7 @@ function apply_sink_rules(sink_type) end end end - + for _, sink_config in ipairs(logging_config) do if (type(sink_config) == "table" and sink_config.to == sink_type) then add_rule(sink_config); @@ -128,7 +127,7 @@ function get_levels(criteria, set) end end end - + for _, level in ipairs(criteria) do set[level] = true; end @@ -136,14 +135,14 @@ function get_levels(criteria, set) end -- Initialize config, etc. -- -function reload_logging() +local function reload_logging() local old_sink_types = {}; - + for name, sink_maker in pairs(log_sink_types) do old_sink_types[name] = sink_maker; log_sink_types[name] = nil; end - + logger.reset(); local debug_mode = config.get("*", "debug"); @@ -152,15 +151,13 @@ function reload_logging() default_file_logging = { { to = "file", levels = { min = (debug_mode and "debug") or "info" }, timestamps = true } }; - default_timestamp = "%b %d %H:%M:%S"; logging_config = config.get("*", "log") or default_logging; - - + for name, sink_maker in pairs(old_sink_types) do log_sink_types[name] = sink_maker; end - + prosody.events.fire_event("logging-reloaded"); end @@ -170,107 +167,98 @@ prosody.events.add_handler("config-reloaded", reload_logging); --- Definition of built-in logging sinks --- -- Null sink, must enter log_sink_types *first* -function log_sink_types.nowhere() +local function log_to_nowhere() return function () return false; end; end +log_sink_types.nowhere = log_to_nowhere; + +local function log_to_file(sink_config, logfile) + logfile = logfile or io_open(sink_config.filename, "a+"); + if not logfile then + return log_to_nowhere(sink_config); + end + local write = logfile.write; --- Column width for "source" (used by stdout and console) -local sourcewidth = 20; + local timestamps = sink_config.timestamps; -function log_sink_types.stdout(config) - local timestamps = config.timestamps; - if timestamps == true then timestamps = default_timestamp; -- Default format + elseif timestamps then + timestamps = timestamps .. " "; end - - return function (name, level, message, ...) - sourcewidth = math_max(#name+2, sourcewidth); - local namelen = #name; - if timestamps then - io_write(os_date(timestamps), " "); - end - if ... then - io_write(name, rep(" ", sourcewidth-namelen), level, "\t", format(message, ...), "\n"); - else - io_write(name, rep(" ", sourcewidth-namelen), level, "\t", message, "\n"); - end - end -end -do - local do_pretty_printing = true; - - local logstyles = {}; - if do_pretty_printing then - logstyles["info"] = getstyle("bold"); - logstyles["warn"] = getstyle("bold", "yellow"); - logstyles["error"] = getstyle("bold", "red"); + if sink_config.buffer_mode ~= false then + logfile:setvbuf(sink_config.buffer_mode or "line"); end - function log_sink_types.console(config) - -- Really if we don't want pretty colours then just use plain stdout - if not do_pretty_printing then - return log_sink_types.stdout(config); - end - - local timestamps = config.timestamps; - if timestamps == true then - timestamps = default_timestamp; -- Default format + -- Column width for "source" (used by stdout and console) + local sourcewidth = sink_config.source_width; + + return function (name, level, message, ...) + local n = select('#', ...); + if n ~= 0 then + local arg = { ... }; + for i = 1, n do + arg[i] = tostring(arg[i]); + end + message = format(message, unpack(arg, 1, n)); end - return function (name, level, message, ...) + if sourcewidth then sourcewidth = math_max(#name+2, sourcewidth); - local namelen = #name; - - if timestamps then - io_write(os_date(timestamps), " "); - end - io_write(name, rep(" ", sourcewidth-namelen)); - setstyle(logstyles[level]); - io_write(level); - setstyle(); - if ... then - io_write("\t", format(message, ...), "\n"); - else - io_write("\t", message, "\n"); - end + name = name .. rep(" ", sourcewidth-#name); + else + name = name .. "\t"; end + write(logfile, timestamps and os_date(timestamps) or "", name, level, "\t", message, "\n"); end end +log_sink_types.file = log_to_file; -local empty_function = function () end; -function log_sink_types.file(config) - local log = config.filename; - local logfile = io_open(log, "a+"); - if not logfile then - return empty_function; +local function log_to_stdout(sink_config) + if not sink_config.timestamps then + sink_config.timestamps = false; + end + if sink_config.source_width == nil then + sink_config.source_width = 20; end - local write, flush = logfile.write, logfile.flush; + return log_to_file(sink_config, stdout); +end +log_sink_types.stdout = log_to_stdout; - local timestamps = config.timestamps; +local do_pretty_printing = true; - if timestamps == nil or timestamps == true then - timestamps = default_timestamp; -- Default format - end +local logstyles; +if do_pretty_printing then + logstyles = {}; + logstyles["info"] = getstyle("bold"); + logstyles["warn"] = getstyle("bold", "yellow"); + logstyles["error"] = getstyle("bold", "red"); +end +local function log_to_console(sink_config) + -- Really if we don't want pretty colours then just use plain stdout + local logstdout = log_to_stdout(sink_config); + if not do_pretty_printing then + return logstdout; + end return function (name, level, message, ...) - if timestamps then - write(logfile, os_date(timestamps), " "); + local logstyle = logstyles[level]; + if logstyle then + level = getstring(logstyle, level); end - if ... then - write(logfile, name, "\t", level, "\t", format(message, ...), "\n"); - else - write(logfile, name, "\t" , level, "\t", message, "\n"); - end - flush(logfile); - end; + return logstdout(name, level, message, ...); + end end +log_sink_types.console = log_to_console; -function register_sink_type(name, sink_maker) +local function register_sink_type(name, sink_maker) local old_sink_maker = log_sink_types[name]; log_sink_types[name] = sink_maker; return old_sink_maker; end -return _M; +return { + reload_logging = reload_logging; + register_sink_type = register_sink_type; +} diff --git a/core/moduleapi.lua b/core/moduleapi.lua index ed75669b..402c7927 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -1,23 +1,28 @@ -- Prosody IM -- Copyright (C) 2008-2012 Matthew Wild -- Copyright (C) 2008-2012 Waqas Hussain --- +-- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local config = require "core.configmanager"; -local modulemanager = require "modulemanager"; -- This is necessary to avoid require loops local array = require "util.array"; local set = require "util.set"; +local it = require "util.iterators"; local logger = require "util.logger"; local pluginloader = require "util.pluginloader"; local timer = require "util.timer"; +local resolve_relative_path = require"util.paths".resolve_relative_path; +local measure = require "core.statsmanager".measure; +local st = require "util.stanza"; local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat; local error, setmetatable, type = error, setmetatable, type; -local ipairs, pairs, select, unpack = ipairs, pairs, select, unpack; +local ipairs, pairs, select = ipairs, pairs, select; +local unpack = table.unpack or unpack; --luacheck: ignore 113 local tonumber, tostring = tonumber, tostring; +local require = require; local prosody = prosody; local hosts = prosody.hosts; @@ -44,14 +49,14 @@ function api:get_host() end function api:get_host_type() - return self.host ~= "*" and hosts[self.host].type or nil; + return (self.host == "*" and "global") or hosts[self.host].type or "local"; end function api:set_global() self.host = "*"; -- Update the logger local _log = logger.init("mod_"..self.name); - self.log = function (self, ...) return _log(...); end; + self.log = function (self, ...) return _log(...); end; --luacheck: ignore self self._log = _log; self.global = true; end @@ -59,8 +64,8 @@ end function api:add_feature(xmlns) self:add_item("feature", xmlns); end -function api:add_identity(category, type, name) - self:add_item("identity", {category = category, type = type, name = name}); +function api:add_identity(category, identity_type, name) + self:add_item("identity", {category = category, type = identity_type, name = name}); end function api:add_extension(data) self:add_item("extension", data); @@ -71,10 +76,10 @@ function api:has_feature(xmlns) end return false; end -function api:has_identity(category, type, name) +function api:has_identity(category, identity_type, name) for _, id in ipairs(self:get_host_items("identity")) do - if id.category == category and id.type == type and id.name == name then - return true; + if id.category == category and id.type == identity_type and id.name == name then + return true; end end return false; @@ -90,6 +95,7 @@ function api:hook_object_event(object, event, handler, priority) end function api:unhook_object_event(object, event, handler) + self.event_handlers:set(object, event, handler, nil); return object.remove_handler(event, handler); end @@ -113,16 +119,30 @@ function api:hook_tag(xmlns, name, handler, priority) end api.hook_stanza = api.hook_tag; -- COMPAT w/pre-0.9 +function api:unhook(event, handler) + return self:unhook_object_event((hosts[self.host] or prosody).events, event, handler); +end + +function api:wrap_object_event(events_object, event, handler) + return self:hook_object_event(assert(events_object.wrappers, "no wrappers"), event, handler); +end + +function api:wrap_event(event, handler) + return self:wrap_object_event((hosts[self.host] or prosody).events, event, handler); +end + +function api:wrap_global(event, handler) + return self:hook_object_event(prosody.events, event, handler); +end + function api:require(lib) - local f, n = pluginloader.load_code(self.name, lib..".lib.lua", self.environment); - if not f then - f, n = pluginloader.load_code(lib, lib..".lib.lua", self.environment); - end + local f, n = pluginloader.load_code_ext(self.name, lib, "lib.lua", self.environment); if not f then error("Failed to load plugin library '"..lib.."', error: "..n); end -- FIXME better error message return f(); end function api:depends(name) + local modulemanager = require"core.modulemanager"; if not self.dependencies then self.dependencies = {}; self:hook("module-reloaded", function (event) @@ -252,21 +272,21 @@ function api:get_option_array(name, ...) if value == nil then return nil; end - + if type(value) ~= "table" then return array{ value }; -- Assume any non-list is a single-item list end - + return array():append(value); -- Clone end function api:get_option_set(name, ...) local value = self:get_option_array(name, ...); - + if value == nil then return nil; end - + return set.new(value); end @@ -282,6 +302,20 @@ function api:get_option_inherited_set(name, ...) return value; end +function api:get_option_path(name, default, parent) + if parent == nil then + parent = parent or self:get_directory(); + elseif prosody.paths[parent] then + parent = prosody.paths[parent]; + end + local value = self:get_option_string(name, default); + if value == nil then + return nil; + end + return resolve_relative_path(parent, value); +end + + function api:context(host) return setmetatable({host=host or "*"}, {__index=self,__newindex=self}); end @@ -304,15 +338,16 @@ function api:remove_item(key, value) end function api:get_host_items(key) + local modulemanager = require"core.modulemanager"; local result = modulemanager.get_items(key, self.host) or {}; return result; end -function api:handle_items(type, added_cb, removed_cb, existing) - self:hook("item-added/"..type, added_cb); - self:hook("item-removed/"..type, removed_cb); +function api:handle_items(item_type, added_cb, removed_cb, existing) + self:hook("item-added/"..item_type, added_cb); + self:hook("item-removed/"..item_type, removed_cb); if existing ~= false then - for _, item in ipairs(self:get_host_items(type)) do + for _, item in ipairs(self:get_host_items(item_type)) do added_cb({ item = item }); end end @@ -343,6 +378,14 @@ function api:send(stanza) return core_post_stanza(hosts[self.host], stanza); end +function api:broadcast(jids, stanza, iter) + for jid in (iter or it.values)(jids) do + local new_stanza = st.clone(stanza); + new_stanza.attr.to = jid; + core_post_stanza(hosts[self.host], new_stanza); + end +end + function api:add_timer(delay, callback) return timer.add_task(delay, function (t) if self.loaded == false then return; end @@ -356,12 +399,35 @@ function api:get_directory() end function api:load_resource(path, mode) - path = config.resolve_relative_path(self:get_directory(), path); + path = resolve_relative_path(self:get_directory(), path); return io.open(path, mode); end -function api:open_store(name, type) - return storagemanager.open(self.host, name or self.name, type); +function api:open_store(name, store_type) + return require"core.storagemanager".open(self.host, name or self.name, store_type); +end + +function api:measure(name, stat_type) + return measure(stat_type, "/"..self.host.."/mod_"..self.name.."/"..name); +end + +function api:measure_object_event(events_object, event_name, stat_name) + local m = self:measure(stat_name or event_name, "duration"); + local function handler(handlers, _event_name, _event_data) + local finished = m(); + local ret = handlers(_event_name, _event_data); + finished(); + return ret; + end + return self:hook_object_event(events_object, event_name, handler); +end + +function api:measure_event(event_name, stat_name) + return self:measure_object_event((hosts[self.host] or prosody).events.wrappers, event_name, stat_name); +end + +function api:measure_global_event(event_name, stat_name) + return self:measure_object_event(prosody.events.wrappers, event_name, stat_name); end return api; diff --git a/core/modulemanager.lua b/core/modulemanager.lua index 4df95069..65542f9a 100644 --- a/core/modulemanager.lua +++ b/core/modulemanager.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -13,31 +13,33 @@ local pluginloader = require "util.pluginloader"; local set = require "util.set"; local new_multitable = require "util.multitable".new; +local api = require "core.moduleapi"; -- Module API container local hosts = hosts; local prosody = prosody; -local pcall, xpcall = pcall, xpcall; +local xpcall = xpcall; local setmetatable, rawget = setmetatable, rawget; local ipairs, pairs, type, tostring, t_insert = ipairs, pairs, type, tostring, table.insert; local debug_traceback = debug.traceback; -local unpack, select = unpack, select; -pcall = function(f, ...) +local select = select; +local unpack = table.unpack or unpack; --luacheck: ignore 113 +local pcall = function(f, ...) local n = select("#", ...); local params = {...}; return xpcall(function() return f(unpack(params, 1, n)) end, function(e) return tostring(e).."\n"..debug_traceback(); end); end -local autoload_modules = {"presence", "message", "iq", "offline", "c2s", "s2s"}; +local autoload_modules = {prosody.platform, "presence", "message", "iq", "offline", "c2s", "s2s", "s2s_auth_certs"}; local component_inheritable_modules = {"tls", "saslauth", "dialback", "iq", "s2s"}; -- We need this to let modules access the real global namespace local _G = _G; -module "modulemanager" +local _ENV = nil; -local api = _G.require "core.moduleapi"; -- Module API container +local load_modules_for_host, load, unload, reload, get_module, get_items, get_modules, is_loaded, module_has_method, call_module_method; -- [host] = { [module] = module_env } local modulemap = { ["*"] = {} }; @@ -45,28 +47,28 @@ local modulemap = { ["*"] = {} }; -- Load modules when a host is activated function load_modules_for_host(host) local component = config.get(host, "component_module"); - + local global_modules_enabled = config.get("*", "modules_enabled"); local global_modules_disabled = config.get("*", "modules_disabled"); local host_modules_enabled = config.get(host, "modules_enabled"); local host_modules_disabled = config.get(host, "modules_disabled"); - + if host_modules_enabled == global_modules_enabled then host_modules_enabled = nil; end if host_modules_disabled == global_modules_disabled then host_modules_disabled = nil; end - + local global_modules = set.new(autoload_modules) + set.new(global_modules_enabled) - set.new(global_modules_disabled); if component then global_modules = set.intersection(set.new(component_inheritable_modules), global_modules); end local modules = (global_modules + set.new(host_modules_enabled)) - set.new(host_modules_disabled); - + -- COMPAT w/ pre 0.8 if modules:contains("console") then log("error", "The mod_console plugin has been renamed to mod_admin_telnet. Please update your config."); modules:remove("console"); modules:add("admin_telnet"); end - + if component then load(host, component); end @@ -84,18 +86,18 @@ end); local function do_unload_module(host, name) local mod = get_module(host, name); if not mod then return nil, "module-not-loaded"; end - + if module_has_method(mod, "unload") then local ok, err = call_module_method(mod, "unload"); if (not ok) and err then log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err); end end - + for object, event, handler in mod.module.event_handlers:iter(nil, nil, nil) do object.remove_handler(event, handler); end - + if mod.module.items then -- remove items local events = (host == "*" and prosody.events) or hosts[host].events; for key,t in pairs(mod.module.items) do @@ -117,11 +119,11 @@ local function do_load_module(host, module_name, state) elseif not hosts[host] and host ~= "*"then return nil, "unknown-host"; end - + if not modulemap[host] then modulemap[host] = hosts[host].modules; end - + if modulemap[host][module_name] then log("debug", "%s is already loaded for %s, so not loading again", module_name, host); return nil, "module-already-loaded"; @@ -131,7 +133,7 @@ local function do_load_module(host, module_name, state) local _log = logger.init(host..":"..module_name); local host_module_api = setmetatable({ host = host, event_handlers = new_multitable(), items = {}; - _log = _log, log = function (self, ...) return _log(...); end; + _log = _log, log = function (self, ...) return _log(...); end; --luacheck: ignore 212/self },{ __index = modulemap["*"][module_name].module; }); @@ -147,18 +149,19 @@ local function do_load_module(host, module_name, state) end return nil, "global-module-already-loaded"; end - + local _log = logger.init(host..":"..module_name); local api_instance = setmetatable({ name = module_name, host = host, - _log = _log, log = function (self, ...) return _log(...); end, event_handlers = new_multitable(), - reloading = not not state, saved_state = state~=true and state or nil } + _log = _log, log = function (self, ...) return _log(...); end, --luacheck: ignore 212/self + event_handlers = new_multitable(), reloading = not not state, + saved_state = state~=true and state or nil } , { __index = api }); local pluginenv = setmetatable({ module = api_instance }, { __index = _G }); api_instance.environment = pluginenv; - + local mod, err = pluginloader.load_code(module_name, nil, pluginenv); if not mod then log("error", "Unable to load module '%s': %s", module_name or "nil", err or "nil"); @@ -316,4 +319,15 @@ function call_module_method(module, method, ...) end end -return _M; +return { + load_modules_for_host = load_modules_for_host; + load = load; + unload = unload; + reload = reload; + get_module = get_module; + get_items = get_items; + get_modules = get_modules; + is_loaded = is_loaded; + module_has_method = module_has_method; + call_module_method = call_module_method; +}; diff --git a/core/portmanager.lua b/core/portmanager.lua index 37442a31..60f7731d 100644 --- a/core/portmanager.lua +++ b/core/portmanager.lua @@ -9,12 +9,12 @@ local set = require "util.set"; local table = table; local setmetatable, rawset, rawget = setmetatable, rawset, rawget; -local type, tonumber, tostring, ipairs, pairs = type, tonumber, tostring, ipairs, pairs; +local type, tonumber, tostring, ipairs = type, tonumber, tostring, ipairs; local prosody = prosody; local fire_event = prosody.events.fire_event; -module "portmanager"; +local _ENV = nil; --- Config @@ -41,7 +41,7 @@ local active_services = multitable.new(); --- Private helpers -local function error_to_friendly_message(service_name, port, err) +local function error_to_friendly_message(service_name, port, err) --luacheck: ignore 212/service_name local friendly_message = err; if err:match(" in use") then -- FIXME: Use service_name here @@ -63,33 +63,14 @@ local function error_to_friendly_message(service_name, port, err) return friendly_message; end -prosody.events.add_handler("item-added/net-provider", function (event) - local item = event.item; - register_service(item.name, item); -end); -prosody.events.add_handler("item-removed/net-provider", function (event) - local item = event.item; - unregister_service(item.name, item); -end); - -local function duplicate_ssl_config(ssl_config) - local ssl_config = type(ssl_config) == "table" and ssl_config or {}; - - local _config = {}; - for k, v in pairs(ssl_config) do - _config[k] = v; - end - return _config; -end - --- Public API -function activate(service_name) +local function activate(service_name) local service_info = services[service_name][1]; if not service_info then return nil, "Unknown service: "..service_name; end - + local listener = service_info.listener; local config_prefix = (service_info.config_prefix or service_name).."_"; @@ -105,7 +86,7 @@ function activate(service_name) or listener.default_interface -- COMPAT w/pre0.9 or default_interfaces bind_interfaces = set.new(type(bind_interfaces)~="table" and {bind_interfaces} or bind_interfaces); - + local bind_ports = config.get("*", config_prefix.."ports") or service_info.default_ports or {service_info.default_port @@ -115,7 +96,7 @@ function activate(service_name) local mode, ssl = listener.default_mode or default_mode; local hooked_ports = {}; - + for interface in bind_interfaces do for port in bind_ports do local port_number = tonumber(port); @@ -127,24 +108,15 @@ function activate(service_name) local err; -- Create SSL context for this service/port if service_info.encryption == "ssl" then - local ssl_config = duplicate_ssl_config((config.get("*", config_prefix.."ssl") and config.get("*", config_prefix.."ssl")[interface]) - or (config.get("*", config_prefix.."ssl") and config.get("*", config_prefix.."ssl")[port]) - or config.get("*", config_prefix.."ssl") - or (config.get("*", "ssl") and config.get("*", "ssl")[interface]) - or (config.get("*", "ssl") and config.get("*", "ssl")[port]) - or config.get("*", "ssl")); - -- add default entries for, or override ssl configuration - if ssl_config and service_info.ssl_config then - for key, value in pairs(service_info.ssl_config) do - if not service_info.ssl_config_override and not ssl_config[key] then - ssl_config[key] = value; - elseif service_info.ssl_config_override then - ssl_config[key] = value; - end - end - end - - ssl, err = certmanager.create_context(service_info.name.." port "..port, "server", ssl_config); + local global_ssl_config = config.get("*", "ssl") or {}; + local prefix_ssl_config = config.get("*", config_prefix.."ssl") or global_ssl_config; + ssl, err = certmanager.create_context(service_info.name.." port "..port, "server", + prefix_ssl_config[interface], + prefix_ssl_config[port], + prefix_ssl_config, + service_info.ssl_config or {}, + global_ssl_config[interface], + global_ssl_config[port]); if not ssl then log("error", "Error binding encrypted port for %s: %s", service_info.name, error_to_friendly_message(service_name, port_number, err) or "unknown error"); end @@ -170,8 +142,10 @@ function activate(service_name) return true; end -function deactivate(service_name, service_info) - for name, interface, port, n, active_service +local close; -- forward declaration + +local function deactivate(service_name, service_info) + for name, interface, port, n, active_service --luacheck: ignore 213/name 213/n in active_services:iter(service_name or service_info and service_info.name, nil, nil, nil) do if service_info == nil or active_service.service == service_info then close(interface, port); @@ -180,7 +154,7 @@ function deactivate(service_name, service_info) log("info", "Deactivated service '%s'", service_name or service_info.name); end -function register_service(service_name, service_info) +local function register_service(service_name, service_info) table.insert(services[service_name], service_info); if not active_services:get(service_name) then @@ -190,12 +164,12 @@ function register_service(service_name, service_info) log("error", "Failed to activate service '%s': %s", service_name, err or "unknown error"); end end - + fire_event("service-added", { name = service_name, service = service_info }); return true; end -function unregister_service(service_name, service_info) +local function unregister_service(service_name, service_info) log("debug", "Unregistering service: %s", service_name); local service_info_list = services[service_name]; for i, service in ipairs(service_info_list) do @@ -210,12 +184,14 @@ function unregister_service(service_name, service_info) fire_event("service-removed", { name = service_name, service = service_info }); end +local get_service_at -- forward declaration + function close(interface, port) - local service, server = get_service_at(interface, port); + local service, service_server = get_service_at(interface, port); if not service then return false, "port-not-open"; end - server:close(); + service_server:close(); active_services:remove(service.name, interface, port); log("debug", "Removed listening service %s from [%s]:%d", service.name, interface, port); return true; @@ -226,16 +202,37 @@ function get_service_at(interface, port) return data.service, data.server; end -function get_service(service_name) +local function get_service(service_name) return (services[service_name] or {})[1]; end -function get_active_services(...) +local function get_active_services() return active_services; end -function get_registered_services() +local function get_registered_services() return services; end -return _M; +-- Event handlers + +prosody.events.add_handler("item-added/net-provider", function (event) + local item = event.item; + register_service(item.name, item); +end); +prosody.events.add_handler("item-removed/net-provider", function (event) + local item = event.item; + unregister_service(item.name, item); +end); + +return { + activate = activate; + deactivate = deactivate; + register_service = register_service; + unregister_service = unregister_service; + close = close; + get_service_at = get_service_at; + get_service = get_service; + get_active_services = get_active_services; + get_registered_services = get_registered_services; +}; diff --git a/core/rostermanager.lua b/core/rostermanager.lua index 5e06e3f7..7f6fb82a 100644 --- a/core/rostermanager.lua +++ b/core/rostermanager.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -13,21 +13,24 @@ local log = require "util.logger".init("rostermanager"); local pairs = pairs; local tostring = tostring; +local type = type; local hosts = hosts; -local bare_sessions = bare_sessions; +local bare_sessions = prosody.bare_sessions; -local datamanager = require "util.datamanager" local um_user_exists = require "core.usermanager".user_exists; local st = require "util.stanza"; +local storagemanager = require "core.storagemanager"; + +local _ENV = nil; -module "rostermanager" +local save_roster; -- forward declaration -function add_to_roster(session, jid, item) +local function add_to_roster(session, jid, item) if session.roster then local old_item = session.roster[jid]; session.roster[jid] = item; - if save_roster(session.username, session.host) then + if save_roster(session.username, session.host, nil, jid) then return true; else session.roster[jid] = old_item; @@ -38,11 +41,11 @@ function add_to_roster(session, jid, item) end end -function remove_from_roster(session, jid) +local function remove_from_roster(session, jid) if session.roster then local old_item = session.roster[jid]; session.roster[jid] = nil; - if save_roster(session.username, session.host) then + if save_roster(session.username, session.host, nil, jid) then return true; else session.roster[jid] = old_item; @@ -53,8 +56,8 @@ function remove_from_roster(session, jid) end end -function roster_push(username, host, jid) - local roster = jid and jid ~= "pending" and hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster; +local function roster_push(username, host, jid) + local roster = jid and hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster; if roster then local item = hosts[host].sessions[username].roster[jid]; local stanza = st.iq({type="set"}); @@ -79,7 +82,22 @@ function roster_push(username, host, jid) end end -function load_roster(username, host) +local function roster_metadata(roster, err) + local metadata = roster[false]; + if not metadata then + metadata = { broken = err or nil }; + roster[false] = metadata; + end + if roster.pending and type(roster.pending.subscription) ~= "string" then + metadata.pending = roster.pending; + roster.pending = nil; + elseif not metadata.pending then + metadata.pending = {}; + end + return metadata; +end + +local function load_roster(username, host) local jid = username.."@"..host; log("debug", "load_roster: asked for: %s", jid); local user = bare_sessions[jid]; @@ -91,27 +109,28 @@ function load_roster(username, host) else -- Attempt to load roster for non-loaded user log("debug", "load_roster: loading for offline user: %s@%s", username, host); end - local data, err = datamanager.load(username, host, "roster"); + local roster_store = storagemanager.open(host, "roster", "keyval"); + local data, err = roster_store:get(username); roster = data or {}; if user then user.roster = roster; end - if not roster[false] then roster[false] = { broken = err or nil }; end + roster_metadata(roster, err); if roster[jid] then roster[jid] = nil; log("warn", "roster for %s has a self-contact", jid); end if not err then - hosts[host].events.fire_event("roster-load", username, host, roster); + hosts[host].events.fire_event("roster-load", { username = username, host = host, roster = roster }); end return roster, err; end -function save_roster(username, host, roster) +function save_roster(username, host, roster, jid) if not um_user_exists(username, host) then log("debug", "not saving roster for %s@%s: the user doesn't exist", username, host); return nil; end - log("debug", "save_roster: saving roster for %s@%s", username, host); + log("debug", "save_roster: saving roster for %s@%s, (%s)", username, host, jid or "all contacts"); if not roster then roster = hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster; --if not roster then @@ -120,22 +139,24 @@ function save_roster(username, host, roster) --end end if roster then - local metadata = roster[false]; - if not metadata then - metadata = {}; - roster[false] = metadata; - end + local metadata = roster_metadata(roster); if metadata.version ~= true then metadata.version = (metadata.version or 0) + 1; end - if roster[false].broken then return nil, "Not saving broken roster" end - return datamanager.store(username, host, "roster", roster); + if metadata.broken then return nil, "Not saving broken roster" end + if jid == nil then + local roster_store = storagemanager.open(host, "roster", "keyval"); + return roster_store:set(username, roster); + else + local roster_store = storagemanager.open(host, "roster", "map"); + return roster_store:set_keys(username, { [false] = metadata, [jid] = roster[jid] or roster_store.remove }); + end end log("warn", "save_roster: user had no roster to save"); return nil; end -function process_inbound_subscription_approval(username, host, jid) +local function process_inbound_subscription_approval(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; if item and item.ask then @@ -145,11 +166,13 @@ function process_inbound_subscription_approval(username, host, jid) item.subscription = "both"; end item.ask = nil; - return save_roster(username, host, roster); + return save_roster(username, host, roster, jid); end end -function process_inbound_subscription_cancellation(username, host, jid) +local is_contact_pending_out -- forward declaration + +local function process_inbound_subscription_cancellation(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; local changed = nil; @@ -167,16 +190,18 @@ function process_inbound_subscription_cancellation(username, host, jid) end end if changed then - return save_roster(username, host, roster); + return save_roster(username, host, roster, jid); end end -function process_inbound_unsubscribe(username, host, jid) +local is_contact_pending_in -- forward declaration + +local function process_inbound_unsubscribe(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; local changed = nil; if is_contact_pending_in(username, host, jid) then - roster.pending[jid] = nil; -- TODO maybe delete roster.pending if empty? + roster[false].pending[jid] = nil; changed = true; end if item then @@ -189,7 +214,7 @@ function process_inbound_unsubscribe(username, host, jid) end end if changed then - return save_roster(username, host, roster); + return save_roster(username, host, roster, jid); end end @@ -198,13 +223,13 @@ local function _get_online_roster_subscription(jidA, jidB) local item = user and (user.roster[jidB] or { subscription = "none" }); return item and item.subscription; end -function is_contact_subscribed(username, host, jid) +local function is_contact_subscribed(username, host, jid) do local selfjid = username.."@"..host; - local subscription = _get_online_roster_subscription(selfjid, jid); - if subscription then return (subscription == "both" or subscription == "from"); end - local subscription = _get_online_roster_subscription(jid, selfjid); - if subscription then return (subscription == "both" or subscription == "to"); end + local user_subscription = _get_online_roster_subscription(selfjid, jid); + if user_subscription then return (user_subscription == "both" or user_subscription == "from"); end + local contact_subscription = _get_online_roster_subscription(jid, selfjid); + if contact_subscription then return (contact_subscription == "both" or contact_subscription == "to"); end end local roster, err = load_roster(username, host); local item = roster[jid]; @@ -213,24 +238,23 @@ end function is_contact_pending_in(username, host, jid) local roster = load_roster(username, host); - return roster.pending and roster.pending[jid]; + return roster[false].pending[jid]; end -function set_contact_pending_in(username, host, jid, pending) +local function set_contact_pending_in(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; if item and (item.subscription == "from" or item.subscription == "both") then return; -- false end - if not roster.pending then roster.pending = {}; end - roster.pending[jid] = true; - return save_roster(username, host, roster); + roster[false].pending[jid] = true; + return save_roster(username, host, roster, jid); end function is_contact_pending_out(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; return item and item.ask; end -function set_contact_pending_out(username, host, jid) -- subscribe +local function set_contact_pending_out(username, host, jid) -- subscribe local roster = load_roster(username, host); local item = roster[jid]; if item and (item.ask or item.subscription == "to" or item.subscription == "both") then @@ -242,9 +266,9 @@ function set_contact_pending_out(username, host, jid) -- subscribe end item.ask = "subscribe"; log("debug", "set_contact_pending_out: saving roster; set %s@%s.roster[%q].ask=subscribe", username, host, jid); - return save_roster(username, host, roster); + return save_roster(username, host, roster, jid); end -function unsubscribe(username, host, jid) +local function unsubscribe(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; if not item then return false; end @@ -257,9 +281,9 @@ function unsubscribe(username, host, jid) elseif item.subscription == "to" then item.subscription = "none"; end - return save_roster(username, host, roster); + return save_roster(username, host, roster, jid); end -function subscribed(username, host, jid) +local function subscribed(username, host, jid) if is_contact_pending_in(username, host, jid) then local roster = load_roster(username, host); local item = roster[jid]; @@ -272,38 +296,37 @@ function subscribed(username, host, jid) else -- subscription == to item.subscription = "both"; end - roster.pending[jid] = nil; - -- TODO maybe remove roster.pending if empty - return save_roster(username, host, roster); + roster[false].pending[jid] = nil; + return save_roster(username, host, roster, jid); end -- TODO else implement optional feature pre-approval (ask = subscribed) end -function unsubscribed(username, host, jid) +local function unsubscribed(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; local pending = is_contact_pending_in(username, host, jid); if pending then - roster.pending[jid] = nil; -- TODO maybe delete roster.pending if empty? + roster[false].pending[jid] = nil; end - local subscribed; + local is_subscribed; if item then if item.subscription == "from" then item.subscription = "none"; - subscribed = true; + is_subscribed = true; elseif item.subscription == "both" then item.subscription = "to"; - subscribed = true; + is_subscribed = true; end end - local success = (pending or subscribed) and save_roster(username, host, roster); + local success = (pending or is_subscribed) and save_roster(username, host, roster, jid); return success, pending, subscribed; end -function process_outbound_subscription_request(username, host, jid) +local function process_outbound_subscription_request(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; if item and (item.subscription == "none" or item.subscription == "from") then item.ask = "subscribe"; - return save_roster(username, host, roster); + return save_roster(username, host, roster, jid); end end @@ -318,4 +341,22 @@ end]] -return _M; +return { + add_to_roster = add_to_roster; + remove_from_roster = remove_from_roster; + roster_push = roster_push; + load_roster = load_roster; + save_roster = save_roster; + process_inbound_subscription_approval = process_inbound_subscription_approval; + process_inbound_subscription_cancellation = process_inbound_subscription_cancellation; + process_inbound_unsubscribe = process_inbound_unsubscribe; + is_contact_subscribed = is_contact_subscribed; + is_contact_pending_in = is_contact_pending_in; + set_contact_pending_in = set_contact_pending_in; + is_contact_pending_out = is_contact_pending_out; + set_contact_pending_out = set_contact_pending_out; + unsubscribe = unsubscribe; + subscribed = subscribed; + unsubscribed = unsubscribed; + process_outbound_subscription_request = process_outbound_subscription_request; +}; diff --git a/core/s2smanager.lua b/core/s2smanager.lua index fb5c4299..a8d399d2 100644 --- a/core/s2smanager.lua +++ b/core/s2smanager.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -22,16 +22,16 @@ prosody.incoming_s2s = incoming_s2s; local incoming_s2s = incoming_s2s; local fire_event = prosody.events.fire_event; -module "s2smanager" +local _ENV = nil; -function new_incoming(conn) +local function new_incoming(conn) local session = { conn = conn, type = "s2sin_unauthed", direction = "incoming", hosts = {} }; session.log = logger_init("s2sin"..tostring(session):match("[a-f0-9]+$")); incoming_s2s[session] = true; return session; end -function new_outgoing(from_host, to_host) +local function new_outgoing(from_host, to_host) local host_session = { to_host = to_host, from_host = from_host, host = from_host, notopen = true, type = "s2sout_unauthed", direction = "outgoing" }; hosts[from_host].s2sout[to_host] = host_session; @@ -49,11 +49,11 @@ local resting_session = { -- Resting, not dead close = function (session) session.log("debug", "Attempt to close already-closed session"); end; - filter = function (type, data) return data; end; + filter = function (type, data) return data; end; --luacheck: ignore 212/type }; resting_session.__index = resting_session; -function retire_session(session, reason) - local log = session.log or log; +local function retire_session(session, reason) + local log = session.log or log; --luacheck: ignore 431/log for k in pairs(session) do if k ~= "log" and k ~= "id" and k ~= "conn" then session[k] = nil; @@ -68,17 +68,17 @@ function retire_session(session, reason) return setmetatable(session, resting_session); end -function destroy_session(session, reason) +local function destroy_session(session, reason) if session.destroyed then return; end (session.log or log)("debug", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host)..(reason and (": "..reason) or "")); - + if session.direction == "outgoing" then hosts[session.from_host].s2sout[session.to_host] = nil; session:bounce_sendq(reason); elseif session.direction == "incoming" then incoming_s2s[session] = nil; end - + local event_data = { session = session, reason = reason }; if session.type == "s2sout" then fire_event("s2sout-destroyed", event_data); @@ -91,9 +91,15 @@ function destroy_session(session, reason) hosts[session.to_host].events.fire_event("s2sin-destroyed", event_data); end end - + retire_session(session, reason); -- Clean session until it is GC'd return true; end -return _M; +return { + incoming_s2s = incoming_s2s; + new_incoming = new_incoming; + new_outgoing = new_outgoing; + retire_session = retire_session; + destroy_session = destroy_session; +}; diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua index 4b014d18..6aa0a4f0 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -10,8 +10,8 @@ local tostring, setmetatable = tostring, setmetatable; local pairs, next= pairs, next; local hosts = hosts; -local full_sessions = full_sessions; -local bare_sessions = bare_sessions; +local full_sessions = prosody.full_sessions; +local bare_sessions = prosody.bare_sessions; local logger = require "util.logger"; local log = logger.init("sessionmanager"); @@ -24,9 +24,9 @@ local uuid_generate = require "util.uuid".generate; local initialize_filters = require "util.filters".initialize; local gettime = require "socket".gettime; -module "sessionmanager" +local _ENV = nil; -function new_session(conn) +local function new_session(conn) local session = { conn = conn, type = "c2s_unauthed", conntime = gettime() }; local filter = initialize_filters(session); local w = conn.write; @@ -37,14 +37,19 @@ function new_session(conn) if t then t = filter("bytes/out", tostring(t)); if t then - return w(conn, t); + local ret, err = w(conn, t); + if not ret then + session.log("debug", "Error writing to connection: %s", tostring(err)); + return false, err; + end end end + return true; end session.ip = conn:ip(); local conn_name = "c2s"..tostring(session):match("[a-f0-9]+$"); session.log = logger.init(conn_name); - + return session; end @@ -54,11 +59,11 @@ local resting_session = { -- Resting, not dead close = function (session) session.log("debug", "Attempt to close already-closed session"); end; - filter = function (type, data) return data; end; + filter = function (type, data) return data; end; --luacheck: ignore 212/type }; resting_session.__index = resting_session; -function retire_session(session) - local log = session.log or log; +local function retire_session(session) + local log = session.log or log; --luacheck: ignore 431/log for k in pairs(session) do if k ~= "log" and k ~= "id" then session[k] = nil; @@ -67,25 +72,26 @@ function retire_session(session) function session.send(data) log("debug", "Discarding data sent to resting session: %s", tostring(data)); return false; end function session.data(data) log("debug", "Discarding data received from resting session: %s", tostring(data)); end + session.thread = { run = function (_, data) return session.data(data) end }; return setmetatable(session, resting_session); end -function destroy_session(session, err) +local function destroy_session(session, err) (session.log or log)("debug", "Destroying session for %s (%s@%s)%s", session.full_jid or "(unknown)", session.username or "(unknown)", session.host or "(unknown)", err and (": "..err) or ""); if session.destroyed then return; end - + -- Remove session/resource from user's session list if session.full_jid then local host_session = hosts[session.host]; - + -- Allow plugins to prevent session destruction if host_session.events.fire_event("pre-resource-unbind", {session=session, error=err}) then return; end - + host_session.sessions[session.username].sessions[session.resource] = nil; full_sessions[session.full_jid] = nil; - + if not next(host_session.sessions[session.username].sessions) then log("debug", "All resources of %s are now offline", session.username); host_session.sessions[session.username] = nil; @@ -94,11 +100,11 @@ function destroy_session(session, err) host_session.events.fire_event("resource-unbind", {session=session, error=err}); end - + retire_session(session); end -function make_authenticated(session, username) +local function make_authenticated(session, username) username = nodeprep(username); if not username or #username == 0 then return nil, "Invalid username"; end session.username = username; @@ -111,15 +117,25 @@ end -- returns true, nil on success -- returns nil, err_type, err, err_message on failure -function bind_resource(session, resource) +local function bind_resource(session, resource) if not session.username then return nil, "auth", "not-authorized", "Cannot bind resource before authentication"; end if session.resource then return nil, "cancel", "not-allowed", "Cannot bind multiple resources on a single connection"; end -- We don't support binding multiple resources + local event_payload = { session = session, resource = resource }; + if hosts[session.host].events.fire_event("pre-resource-bind", event_payload) == false then + local err = event_payload.error; + if err then return nil, err.type, err.condition, err.text; end + return nil, "cancel", "not-allowed"; + else + -- In case a plugin wants to poke at it + resource = event_payload.resource; + end + resource = resourceprep(resource); resource = resource ~= "" and resource or uuid_generate(); --FIXME: Randomly-generated resources must be unique per-user, and never conflict with existing - + if not hosts[session.host].sessions[session.username] then local sessions = { sessions = {} }; hosts[session.host].sessions[session.username] = sessions; @@ -156,12 +172,12 @@ function bind_resource(session, resource) end end end - + session.resource = resource; session.full_jid = session.username .. '@' .. session.host .. '/' .. resource; hosts[session.host].sessions[session.username].sessions[resource] = session; full_sessions[session.full_jid] = session; - + local err; session.roster, err = rm_load_roster(session.username, session.host); if err then @@ -176,18 +192,18 @@ function bind_resource(session, resource) session.log("error", "Roster loading failed: %s", err); return nil, "cancel", "internal-server-error", "Error loading roster"; end - + hosts[session.host].events.fire_event("resource-bind", {session=session}); - + return true; end -function send_to_available_resources(user, host, stanza) - local jid = user.."@"..host; +local function send_to_available_resources(username, host, stanza) + local jid = username.."@"..host; local count = 0; local user = bare_sessions[jid]; if user then - for k, session in pairs(user.sessions) do + for _, session in pairs(user.sessions) do if session.presence then session.send(stanza); count = count + 1; @@ -197,12 +213,12 @@ function send_to_available_resources(user, host, stanza) return count; end -function send_to_interested_resources(user, host, stanza) - local jid = user.."@"..host; +local function send_to_interested_resources(username, host, stanza) + local jid = username.."@"..host; local count = 0; local user = bare_sessions[jid]; if user then - for k, session in pairs(user.sessions) do + for _, session in pairs(user.sessions) do if session.interested then session.send(stanza); count = count + 1; @@ -212,4 +228,12 @@ function send_to_interested_resources(user, host, stanza) return count; end -return _M; +return { + new_session = new_session; + retire_session = retire_session; + destroy_session = destroy_session; + make_authenticated = make_authenticated; + bind_resource = bind_resource; + send_to_available_resources = send_to_available_resources; + send_to_interested_resources = send_to_interested_resources; +}; diff --git a/core/stanza_router.lua b/core/stanza_router.lua index a2c7b396..55c94bf4 100644 --- a/core/stanza_router.lua +++ b/core/stanza_router.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -30,7 +30,7 @@ deprecated_warning"core_process_stanza"; deprecated_warning"core_route_stanza"; local valid_stanzas = { message = true, presence = true, iq = true }; -local function handle_unhandled_stanza(host, origin, stanza) +local function handle_unhandled_stanza(host, origin, stanza) --luacheck: ignore 212/host local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns or "jabber:client", origin.type; if xmlns == "jabber:client" and valid_stanzas[name] then -- A normal stanza @@ -46,7 +46,7 @@ local function handle_unhandled_stanza(host, origin, stanza) if origin.send then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end - elseif not((name == "features" or name == "error") and xmlns == "http://etherx.jabber.org/streams") then -- FIXME remove check once we handle S2S features + else log("warn", "Unhandled %s stream element or stanza: %s; xmlns=%s: %s", origin_type, name, xmlns, tostring(stanza)); -- we didn't handle it origin:close("unsupported-stanza-type"); end @@ -199,7 +199,7 @@ function core_route_stanza(origin, stanza) -- Auto-detect origin if not specified origin = origin or hosts[from_host]; if not origin then return false; end - + if hosts[host] then -- old stanza routing code removed core_post_stanza(origin, stanza); @@ -221,6 +221,8 @@ function core_route_stanza(origin, stanza) end end end + +--luacheck: ignore 122/prosody prosody.core_process_stanza = core_process_stanza; prosody.core_post_stanza = core_post_stanza; prosody.core_route_stanza = core_route_stanza; diff --git a/core/statsmanager.lua b/core/statsmanager.lua new file mode 100644 index 00000000..7771a2b3 --- /dev/null +++ b/core/statsmanager.lua @@ -0,0 +1,72 @@ + +local stats = require "util.statistics".new(); +local config = require "core.configmanager"; +local log = require "util.logger".init("stats"); +local timer = require "util.timer"; +local fire_event = prosody.events.fire_event; + +local stats_config = config.get("*", "statistics_interval"); +local stats_interval = tonumber(stats_config); +if stats_config and not stats_interval then + log("error", "Invalid 'statistics_interval' setting, statistics will be disabled"); +end + +local measure, collect; +local latest_stats = {}; +local changed_stats = {}; +local stats_extra = {}; + +if stats_interval then + log("debug", "Statistics collection is enabled every %d seconds", stats_interval); + function measure(type, name) + local f = assert(stats[type], "unknown stat type: "..type); + return f(name); + end + + local mark_collection_start = measure("times", "stats.collection"); + local mark_processing_start = measure("times", "stats.processing"); + + function collect() + local mark_collection_done = mark_collection_start(); + fire_event("stats-update"); + changed_stats, stats_extra = {}, {}; + for stat_name, getter in pairs(stats.get_stats()) do + local type, value, extra = getter(); + local old_value = latest_stats[stat_name]; + latest_stats[stat_name] = value; + if value ~= old_value then + changed_stats[stat_name] = value; + end + if extra then + stats_extra[stat_name] = extra; + end + end + mark_collection_done(); + local mark_processing_done = mark_processing_start(); + fire_event("stats-updated", { stats = latest_stats, changed_stats = changed_stats, stats_extra = stats_extra }); + mark_processing_done(); + return stats_interval; + end + + timer.add_task(stats_interval, collect); + prosody.events.add_handler("server-started", function () collect() end, -1); +else + log("debug", "Statistics collection is disabled"); + -- nop + function measure() + return measure; + end + function collect() + end +end + +return { + measure = measure; + collect = collect; + get_stats = function () + return latest_stats, changed_stats, stats_extra; + end; + get = function (name) + return latest_stats[name], stats_extra[name]; + end; +}; diff --git a/core/storagemanager.lua b/core/storagemanager.lua index 1c82af6d..4f04e594 100644 --- a/core/storagemanager.lua +++ b/core/storagemanager.lua @@ -1,5 +1,5 @@ -local error, type, pairs = error, type, pairs; +local type, pairs = type, pairs; local setmetatable = setmetatable; local config = require "core.configmanager"; @@ -11,11 +11,10 @@ local log = require "util.logger".init("storagemanager"); local prosody = prosody; -module("storagemanager") +local _ENV = nil; local olddm = {}; -- maintain old datamanager, for backwards compatibility for k,v in pairs(datamanager) do olddm[k] = v; end -_M.olddm = olddm; local null_storage_method = function () return false, "no data storage active"; end local null_storage_driver = setmetatable( @@ -23,7 +22,7 @@ local null_storage_driver = setmetatable( name = "null", open = function (self) return self; end }, { - __index = function (self, method) + __index = function (self, method) --luacheck: ignore 212 return null_storage_method; end } @@ -31,13 +30,13 @@ local null_storage_driver = setmetatable( local stores_available = multitable.new(); -function initialize_host(host) +local function initialize_host(host) local host_session = hosts[host]; host_session.events.add_handler("item-added/storage-provider", function (event) local item = event.item; stores_available:set(host, item.name, item); end); - + host_session.events.add_handler("item-removed/storage-provider", function (event) local item = event.item; stores_available:set(host, item.name, nil); @@ -45,7 +44,7 @@ function initialize_host(host) end prosody.events.add_handler("host-activated", initialize_host, 101); -function load_driver(host, driver_name) +local function load_driver(host, driver_name) if driver_name == "null" then return null_storage_driver; end @@ -58,8 +57,28 @@ function load_driver(host, driver_name) return stores_available:get(host, driver_name); end -function get_driver(host, store) - local storage = config.get(host, "storage"); +local function get_storage_config(host) + -- COMPAT w/ unreleased Prosody 0.10 and the once-experimental mod_storage_sql2 in peoples' config files + local storage_config = config.get(host, "storage"); + local found_sql2; + if storage_config == "sql2" then + storage_config, found_sql2 = "sql", true; + elseif type(storage_config) == "table" then + for store_name, driver_name in pairs(storage_config) do + if driver_name == "sql2" then + storage_config[store_name] = "sql"; + found_sql2 = true; + end + end + end + if found_sql2 then + log("error", "The temporary 'sql2' storage module has now been renamed to 'sql', please update your config file: https://prosody.im/doc/modules/mod_storage_sql2"); + end + return storage_config; +end + +local function get_driver(host, store) + local storage = get_storage_config(host); local driver_name; local option_type = type(storage); if option_type == "string" then @@ -70,7 +89,7 @@ function get_driver(host, store) if not driver_name then driver_name = config.get(host, "default_storage") or "internal"; end - + local driver = load_driver(host, driver_name); if not driver then log("warn", "Falling back to null driver for %s storage on %s", store, host); @@ -80,11 +99,64 @@ function get_driver(host, store) return driver, driver_name; end +local map_shim_mt = { + __index = { + get = function(self, username, key) + local ret, err = self.keyval_store:get(username); + if ret == nil then return nil, err end + return ret[key]; + end; + set = function(self, username, key, data) + local current, err = self.keyval_store:get(username); + if current == nil then + if err then + return nil, err; + else + current = {}; + end + end + current[key] = data; + return self.keyval_store:set(username, current); + end; + set_keys = function (self, username, keydatas) + local current, err = self.keyval_store:get(username); + if current == nil then + if err then + return nil, err; + else + current = keydatas; + end + else + for k,v in pairs(keydatas) do + if v == self.remove then v = nil; end + current[k] = v; + end + end + return self.keyval_store:set(username, current); + end; + remove = {}; + }; +} + +local open; + +local function create_map_shim(host, store) + local keyval_store, err = open(host, store, "keyval"); + if keyval_store == nil then return nil, err end + return setmetatable({ + keyval_store = keyval_store; + }, map_shim_mt); +end + function open(host, store, typ) local driver, driver_name = get_driver(host, store); local ret, err = driver:open(store, typ); if not ret then if err == "unsupported-store" then + if typ == "map" then -- Use shim on top of keyval store + log("debug", "map storage driver unavailable, using shim on top of keyval store."); + return create_map_shim(host, store); + end log("debug", "Storage driver %s does not support store %s (%s), falling back to null driver", driver_name, store, typ or "<nil>"); ret = null_storage_driver; @@ -94,14 +166,19 @@ function open(host, store, typ) return ret, err; end -function purge(user, host) - local storage = config.get(host, "storage"); +local function purge(user, host) + local storage = get_storage_config(host); if type(storage) == "table" then -- multiple storage backends in use that we need to purge local purged = {}; - for store, driver in pairs(storage) do - if not purged[driver] then - purged[driver] = get_driver(host, store):purge(user); + for store, driver_name in pairs(storage) do + if not purged[driver_name] then + local driver = get_driver(host, store); + if driver.purge then + purged[driver_name] = driver:purge(user); + else + log("warn", "Storage driver %s does not support removing all user data, you may need to delete it manually", driver_name); + end end end end @@ -132,4 +209,12 @@ function datamanager.purge(username, host) return purge(username, host); end -return _M; +return { + initialize_host = initialize_host; + load_driver = load_driver; + get_driver = get_driver; + open = open; + purge = purge; + + olddm = olddm; +}; diff --git a/core/usermanager.lua b/core/usermanager.lua index 08343bee..d5132662 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -10,7 +10,6 @@ local modulemanager = require "core.modulemanager"; local log = require "util.logger".init("usermanager"); local type = type; local ipairs = ipairs; -local pairs = pairs; local jid_bare = require "util.jid".bare; local jid_prep = require "util.jid".prep; local config = require "core.configmanager"; @@ -24,22 +23,22 @@ local setmetatable = setmetatable; local default_provider = "internal_plain"; -module "usermanager" +local _ENV = nil; -function new_null_provider() +local function new_null_provider() local function dummy() return nil, "method not implemented"; end; local function dummy_get_sasl_handler() return sasl_new(nil, {}); end return setmetatable({name = "null", get_sasl_handler = dummy_get_sasl_handler}, { - __index = function(self, method) return dummy; end + __index = function(self, method) return dummy; end --luacheck: ignore 212 }); end local provider_mt = { __index = new_null_provider() }; -function initialize_host(host) +local function initialize_host(host) local host_session = hosts[host]; if host_session.type ~= "local" then return; end - + host_session.events.add_handler("item-added/auth-provider", function (event) local provider = event.item; local auth_provider = config.get(host, "authentication") or default_provider; @@ -51,7 +50,7 @@ function initialize_host(host) host_session.users = setmetatable(provider, provider_mt); end if host_session.users ~= nil and host_session.users.name ~= nil then - log("debug", "host '%s' now set to use user provider '%s'", host, host_session.users.name); + log("debug", "Host '%s' now set to use user provider '%s'", host, host_session.users.name); end end); host_session.events.add_handler("item-removed/auth-provider", function (event) @@ -69,87 +68,98 @@ function initialize_host(host) end; prosody.events.add_handler("host-activated", initialize_host, 100); -function test_password(username, host, password) +local function test_password(username, host, password) return hosts[host].users.test_password(username, password); end -function get_password(username, host) +local function get_password(username, host) return hosts[host].users.get_password(username); end -function set_password(username, password, host) +local function set_password(username, password, host) return hosts[host].users.set_password(username, password); end -function user_exists(username, host) +local function user_exists(username, host) + if hosts[host].sessions[username] then return true; end return hosts[host].users.user_exists(username); end -function create_user(username, password, host) +local function create_user(username, password, host) return hosts[host].users.create_user(username, password); end -function delete_user(username, host) +local function delete_user(username, host) local ok, err = hosts[host].users.delete_user(username); if not ok then return nil, err; end prosody.events.fire_event("user-deleted", { username = username, host = host }); return storagemanager.purge(username, host); end -function users(host) +local function users(host) return hosts[host].users.users(); end -function get_sasl_handler(host, session) +local function get_sasl_handler(host, session) return hosts[host].users.get_sasl_handler(session); end -function get_provider(host) +local function get_provider(host) return hosts[host].users; end -function is_admin(jid, host) +local function is_admin(jid, host) if host and not hosts[host] then return false; end if type(jid) ~= "string" then return false; end - local is_admin; jid = jid_bare(jid); host = host or "*"; - + local host_admins = config.get(host, "admins"); local global_admins = config.get("*", "admins"); - + if host_admins and host_admins ~= global_admins then if type(host_admins) == "table" then for _,admin in ipairs(host_admins) do if jid_prep(admin) == jid then - is_admin = true; - break; + return true; end end elseif host_admins then log("error", "Option 'admins' for host '%s' is not a list", host); end end - - if not is_admin and global_admins then + + if global_admins then if type(global_admins) == "table" then for _,admin in ipairs(global_admins) do if jid_prep(admin) == jid then - is_admin = true; - break; + return true; end end elseif global_admins then log("error", "Global option 'admins' is not a list"); end end - + -- Still not an admin, check with auth provider - if not is_admin and host ~= "*" and hosts[host].users and hosts[host].users.is_admin then - is_admin = hosts[host].users.is_admin(jid); + if host ~= "*" and hosts[host].users and hosts[host].users.is_admin then + return hosts[host].users.is_admin(jid); end - return is_admin or false; + return false; end -return _M; +return { + new_null_provider = new_null_provider; + initialize_host = initialize_host; + test_password = test_password; + get_password = get_password; + set_password = set_password; + user_exists = user_exists; + create_user = create_user; + delete_user = delete_user; + users = users; + get_sasl_handler = get_sasl_handler; + get_provider = get_provider; + is_admin = is_admin; +}; diff --git a/fallbacks/bit.lua b/fallbacks/bit.lua index 2482c473..28dca4e6 100644 --- a/fallbacks/bit.lua +++ b/fallbacks/bit.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/fallbacks/lxp.lua b/fallbacks/lxp.lua index 6d3297d1..ac1c9a03 100644 --- a/fallbacks/lxp.lua +++ b/fallbacks/lxp.lua @@ -61,7 +61,7 @@ local function parser(data, handlers, ns_separator) while #data == 0 do data = coroutine.yield(); end return data:sub(1,1); end - + local ns = { xml = "http://www.w3.org/XML/1998/namespace" }; ns.__index = ns; local function apply_ns(name, dodefault) @@ -100,7 +100,7 @@ local function parser(data, handlers, ns_separator) ns = getmetatable(ns); return tag; end - + while true do if peek() == "<" then local elem = read_until(">"):sub(2,-2); diff --git a/man/Makefile b/man/Makefile new file mode 100644 index 00000000..79bdd90a --- /dev/null +++ b/man/Makefile @@ -0,0 +1,4 @@ +all: prosodyctl.man + +%.man: %.markdown + pandoc -s -t man -o $@ $^ diff --git a/man/prosodyctl.man b/man/prosodyctl.man index 6dcb04cd..b91502a8 100644 --- a/man/prosodyctl.man +++ b/man/prosodyctl.man @@ -1,83 +1,140 @@ -.TH PROSODYCTL 1 "2009-07-02" - +.\" Automatically generated by Pandoc 1.15.2 +.\" +.hy +.TH "PROSODYCTL" "1" "2015\-12\-23" "" "" .SH NAME +.PP prosodyctl \- Manage a Prosody XMPP server - .SH SYNOPSIS -\fBprosodyctl\fP \fIcommand\fP [\fI--help\fP] - +.IP +.nf +\f[C] +prosodyctl\ command\ [\-\-help] +\f[] +.fi .SH DESCRIPTION -\fBprosodyctl\fP is the control tool for the Prosody XMPP server. It may be -used to control the server daemon and manage users. - -\fBprosodyctl\fP needs to be executed with sufficient privileges to perform -its commands. This typically means executing \fBprosodyctl\fP as the root user. -If a user named "prosody" is found then \fBprosodyctl\fP will change to that +.PP +prosodyctl is the control tool for the Prosody XMPP server. +It may be used to control the server daemon and manage users. +.PP +prosodyctl needs to be executed with sufficient privileges to perform +its commands. +This typically means executing prosodyctl as the root user. +If a user named "prosody" is found then prosodyctl will change to that user before executing its commands. - .SH COMMANDS .SS User Management -In the following commands users are identified by a Jabber ID, \fIjid\fP, of the -usual form: user@domain. - -.IP "\fBadduser\fP \fIjid\fP" -Adds a user with Jabber ID, \fIjid\fP, to the server. You will be -prompted to enter the user's password. - -.IP "\fBpasswd\fP \fIjid\fP" -Changes the password of an existing user with Jabber ID, \fIjid\fP. You will be -prompted to enter the user's new password. - -.IP "\fBdeluser\fP \fIjid\fP" -Deletes an existing user with Jabber ID, \fIjid\fP, from the server. - +.PP +In the following commands users are identified by a Jabber ID, jid, of +the usual form: user\@domain. +.TP +.B adduser jid +Adds a user with Jabber ID, jid, to the server. +You will be prompted to enter the user\[aq]s password. +.RS +.RE +.TP +.B passwd jid +Changes the password of an existing user with Jabber ID, jid. +You will be prompted to enter the user\[aq]s new password. +.RS +.RE +.TP +.B deluser jid +Deletes an existing user with Jabber ID, jid, from the server. +.RS +.RE .SS Daemon Management -Although \fBprosodyctl\fP has commands to manage the \fBprosody\fP daemon it is -recommended that you utilize your distributions daemon management features if -you attained Prosody through a package. - -To perform daemon control commands \fBprosodyctl\fP needs a \fIpidfile\fP value -specified in \fI/etc/prosody/prosody.cfg.lua\fP. Failure to do so will cause -\fBprosodyctl\fP to complain. - -.IP \fBstart\fP -Starts the \fBprosody\fP server daemon. If run as root \fBprosodyctl\fP will -attempt to change to a user named "prosody" before executing. This operation -will block for up to five seconds to wait for the server to execute. - -.IP \fBstop\fP -Stops the \fBprosody\fP server daemon. This operation will block for up to five -seconds to wait for the server to stop executing. - -.IP \fBrestart\fP -Restarts the \fBprosody\fP server daemon. Equivalent to running \fBprosodyctl -stop\fP followed by \fBprosodyctl start\fP. - -.IP \fBstatus\fP -Prints the current execution status of the \fBprosody\fP server daemon. - +.PP +Although prosodyctl has commands to manage the prosody daemon it is +recommended that you utilize your distributions daemon management +features if you attained Prosody through a package. +.PP +To perform daemon control commands prosodyctl needs a pidfile value +specified in \f[C]/etc/prosody/prosody.cfg.lua\f[]. +Failure to do so will cause prosodyctl to complain. +.TP +.B start +Starts the prosody server daemon. +If run as root prosodyctl will attempt to change to a user named +"prosody" before executing. +This operation will block for up to five seconds to wait for the server +to execute. +.RS +.RE +.TP +.B stop +Stops the prosody server daemon. +This operation will block for up to five seconds to wait for the server +to stop executing. +.RS +.RE +.TP +.B restart +Restarts the prosody server daemon. +Equivalent to running prosodyctl stop followed by prosodyctl start. +.RS +.RE +.TP +.B reload +Signals the prosody server daemon to reload configuration and reopen log +files. +.RS +.RE +.TP +.B status +Prints the current execution status of the prosody server daemon. +.RS +.RE +.SS Debugging +.PP +prosodyctl can also show some information about the environment, +dependencies and such to aid in debugging. +.TP +.B about +Shows environment, various paths used by Prosody and installed +dependencies. +.RS +.RE +.TP +.B check [what] +Performs various sanity checks on the configuration, DNS setup and +configured TLS certificates. +\f[C]what\f[] can be one of \f[C]config\f[], \f[C]dns\f[] and +\f[C]certs\f[] to run only that check. +.RS +.RE .SS Ejabberd Compatibility -\fBejabberd\fP is another XMPP server which provides a comparable control tool, -\fBejabberdctl\fP, to control its server's operations. \fBprosodyctl\fP -implements some commands which are compatible with \fBejabberdctl\fP. For -details of how these commands work you should see -.BR ejabberdctl (8). - -.IP "\fBregister\fP \fIuser server password\fP" -.IP "\fBunregister\fP \fIuser server\fP" - +.PP +ejabberd is another XMPP server which provides a comparable control +tool, ejabberdctl, to control its server\[aq]s operations. +prosodyctl implements some commands which are compatible with +ejabberdctl. +For details of how these commands work you should see ejabberdctl(8). +.IP +.nf +\f[C] +register\ user\ server\ password + +unregister\ user\ server +\f[] +.fi .SH OPTIONS -.IP \fI--help\fP +.TP +.B \f[C]\-\-help\f[] Display help text for the specified command. - +.RS +.RE .SH FILES -.IP \fI/etc/prosody/prosody.cfg.lua\fP -The main \fBprosody\fP configuration file. \fBprosodyctl\fP reads this to -determine the process ID file of the \fBprosody\fP server daemon and to -determine if a host has been configured. - +.TP +.B \f[C]/etc/prosody/prosody.cfg.lua\f[] +The main prosody configuration file. +prosodyctl reads this to determine the process ID file of the prosody +server daemon and to determine if a host has been configured. +.RS +.RE .SH ONLINE -More information may be found online at: \fIhttp://prosody.im/\fP - +.PP +More information may be found online at: <https://prosody.im/> .SH AUTHORS -Dwayne Bent <dbb.1@liqd.org> +Dwayne Bent <dbb.1@liqd.org>; Kim Alvefur. diff --git a/man/prosodyctl.markdown b/man/prosodyctl.markdown new file mode 100644 index 00000000..217dfd3d --- /dev/null +++ b/man/prosodyctl.markdown @@ -0,0 +1,127 @@ +--- +author: +- 'Dwayne Bent <dbb.1@liqd.org>' +- Kim Alvefur +date: '2015-12-23' +section: 1 +title: PROSODYCTL +... + +NAME +==== + +prosodyctl - Manage a Prosody XMPP server + +SYNOPSIS +======== + + prosodyctl command [--help] + +DESCRIPTION +=========== + +prosodyctl is the control tool for the Prosody XMPP server. It may be +used to control the server daemon and manage users. + +prosodyctl needs to be executed with sufficient privileges to perform +its commands. This typically means executing prosodyctl as the root +user. If a user named "prosody" is found then prosodyctl will change to +that user before executing its commands. + +COMMANDS +======== + +User Management +--------------- + +In the following commands users are identified by a Jabber ID, jid, of +the usual form: user@domain. + +adduser jid +: Adds a user with Jabber ID, jid, to the server. You will be prompted + to enter the user's password. + +passwd jid +: Changes the password of an existing user with Jabber ID, jid. You + will be prompted to enter the user's new password. + +deluser jid +: Deletes an existing user with Jabber ID, jid, from the server. + +Daemon Management +----------------- + +Although prosodyctl has commands to manage the prosody daemon it is +recommended that you utilize your distributions daemon management +features if you attained Prosody through a package. + +To perform daemon control commands prosodyctl needs a pidfile value +specified in `/etc/prosody/prosody.cfg.lua`. Failure to do so will cause +prosodyctl to complain. + +start +: Starts the prosody server daemon. If run as root prosodyctl will + attempt to change to a user named "prosody" before executing. This + operation will block for up to five seconds to wait for the server + to execute. + +stop +: Stops the prosody server daemon. This operation will block for up to + five seconds to wait for the server to stop executing. + +restart +: Restarts the prosody server daemon. Equivalent to running prosodyctl + stop followed by prosodyctl start. + +reload +: Signals the prosody server daemon to reload configuration and reopen + log files. + +status +: Prints the current execution status of the prosody server daemon. + +Debugging +--------- + +prosodyctl can also show some information about the environment, +dependencies and such to aid in debugging. + +about +: Shows environment, various paths used by Prosody and + installed dependencies. + +check \[what\] +: Performs various sanity checks on the configuration, DNS setup and + configured TLS certificates. `what` can be one of `config`, `dns` + and `certs` to run only that check. + +Ejabberd Compatibility +---------------------- + +ejabberd is another XMPP server which provides a comparable control +tool, ejabberdctl, to control its server's operations. prosodyctl +implements some commands which are compatible with ejabberdctl. For +details of how these commands work you should see ejabberdctl(8). + + register user server password + + unregister user server + +OPTIONS +======= + +`--help` +: Display help text for the specified command. + +FILES +===== + +`/etc/prosody/prosody.cfg.lua` +: The main prosody configuration file. prosodyctl reads this to + determine the process ID file of the prosody server daemon and to + determine if a host has been configured. + +ONLINE +====== + +More information may be found online at: <https://prosody.im/> diff --git a/net/adns.lua b/net/adns.lua index 3fc958f4..d3da2065 100644 --- a/net/adns.lua +++ b/net/adns.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -16,9 +16,9 @@ local coroutine, tostring, pcall = coroutine, tostring, pcall; local function dummy_send(sock, data, i, j) return (j-i)+1; end -module "adns" +local _ENV = nil; -function lookup(handler, qname, qtype, qclass) +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); @@ -43,12 +43,12 @@ function lookup(handler, qname, qtype, qclass) end)(dns.peek(qname, qtype, qclass)); end -function cancel(handle, call_handler, reason) +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 -function new_async_socket(sock, resolver) +local function new_async_socket(sock, resolver) local peername = "<unknown>"; local listener = {}; local handler = {}; @@ -65,7 +65,7 @@ function new_async_socket(sock, resolver) 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 @@ -73,7 +73,7 @@ function new_async_socket(sock, resolver) 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 @@ -88,4 +88,8 @@ end dns.socket_wrapper_set(new_async_socket); -return _M; +return { + lookup = lookup; + cancel = cancel; + new_async_socket = new_async_socket; +}; diff --git a/net/connlisteners.lua b/net/connlisteners.lua index 99ddc720..000bfa63 100644 --- a/net/connlisteners.lua +++ b/net/connlisteners.lua @@ -2,14 +2,17 @@ local log = require "util.logger".init("net.connlisteners"); local traceback = debug.traceback; -module "httpserver" +local _ENV = nil; -function fail() +local function fail() log("error", "Attempt to use legacy connlisteners API. For more info see http://prosody.im/doc/developers/network"); log("error", "Legacy connlisteners API usage, %s", traceback("", 2)); end -register, deregister = fail, fail; -get, start = fail, fail, epic_fail; - -return _M; +return { + register = fail; + register = fail; + get = fail; + start = fail; + -- epic fail +}; diff --git a/net/dns.lua b/net/dns.lua index d123731c..b047ec54 100644 --- a/net/dns.lua +++ b/net/dns.lua @@ -71,8 +71,8 @@ local get, set = ztact.get, ztact.set; local default_timeout = 15; -------------------------------------------------- module dns -module('dns') -local dns = _M; +local _ENV = nil; +local dns = {}; -- dns type & class codes ------------------------------ dns type & class codes @@ -213,15 +213,6 @@ function cache_metatable.__tostring(cache) end -function resolver:new() -- - - - - - - - - - - - - - - - - - - - - resolver - local r = { active = {}, cache = {}, unsorted = {} }; - setmetatable(r, resolver); - setmetatable(r.cache, cache_metatable); - setmetatable(r.unsorted, { __mode = 'kv' }); - return r; -end - - -- packet layer -------------------------------------------------- packet layer @@ -629,7 +620,7 @@ function resolver:getsocket(servernum) -- - - - - - - - - - - - - getsocket if peer:find(":") then sock, err = socket.udp6(); else - sock, err = socket.udp(); + 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 @@ -1054,8 +1045,6 @@ end function dns.resolver () -- - - - - - - - - - - - - - - - - - - - - resolver - -- this function seems to be redundant with resolver.new () - local r = { active = {}, cache = {}, unsorted = {}, wanted = {}, best_server = 1 }; setmetatable (r, resolver); setmetatable (r.cache, cache_metatable); diff --git a/net/http.lua b/net/http.lua index 8ce47494..b78f8438 100644 --- a/net/http.lua +++ b/net/http.lua @@ -1,12 +1,11 @@ -- 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 socket = require "socket" local b64 = require "util.encodings".base64.encode; local url = require "socket.url" local httpstream_new = require "net.http.parser".new; @@ -24,7 +23,7 @@ local assert, error = assert, error local log = require "util.logger".init("http"); -module "http" +local _ENV = nil; local requests = {}; -- Open requests @@ -37,7 +36,7 @@ function listener.onconnect(conn) 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 @@ -45,7 +44,7 @@ function listener.onconnect(conn) conn:write(t_concat(t)); end conn:write("\r\n"); - + if req.body then conn:write(req.body); end @@ -76,6 +75,13 @@ 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) @@ -85,12 +91,12 @@ local function request_reader(request, data, err) 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); @@ -107,20 +113,20 @@ local function request_reader(request, data, err) end local function handleerr(err) log("error", "Traceback[http]: %s", traceback(tostring(err), 2)); end -function request(u, ex, callback) +local function request(u, ex, callback) local req = url.parse(u); - + if not (req and req.host) then callback(nil, 0, req); return nil, "invalid-url"; end - + if not req.path then req.path = "/"; end - + local method, headers, body; - + local host, port = req.host, req.port; local host_header = host; if (port == "80" and req.scheme == "http") @@ -134,7 +140,7 @@ function request(u, ex, callback) ["Host"] = host_header; ["User-Agent"] = "Prosody XMPP Server"; }; - + if req.userinfo then headers["Authorization"] = "Basic "..b64(req.userinfo); end @@ -154,33 +160,29 @@ function request(u, ex, callback) end end end - + -- 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); - - -- Connect the socket, and wrap it with net.server - local conn = socket.tcp(); - conn:settimeout(10); - local ok, err = conn:connect(host, port_number); - if not ok and err ~= "timeout" then - callback(nil, 0, req); - return nil, err; - end - + local sslctx = false; if using_https then sslctx = ex and ex.sslctx or { mode = "client", protocol = "sslv23", options = { "no_sslv2", "no_sslv3" } }; end - req.handler, req.conn = assert(server.wrapclient(conn, host, port_number, listener, "*a", sslctx)); + local handler, conn = server.addclient(host, port_number, listener, "*a", sslctx) + if not handler then + callback(nil, 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, request, response) log("debug", "Calling callback, status %s", code or "---"); return select(2, xpcall(function () return callback(content, code, request, response) end, handleerr)); end req.reader = request_reader; req.state = "status"; @@ -189,17 +191,12 @@ function request(u, ex, callback) return req; end -function destroy_request(request) - if request.conn then - request.conn = nil; - request.handler:close() - end -end - -local urlencode, urldecode = util_http.urlencode, util_http.urldecode; -local formencode, formdecode = util_http.formencode, util_http.formdecode; - -_M.urlencode, _M.urldecode = urlencode, urldecode; -_M.formencode, _M.formdecode = formencode, formdecode; - -return _M; +return { + request = request; + + -- COMPAT + urlencode = util_http.urlencode; + urldecode = util_http.urldecode; + formencode = util_http.formencode; + formdecode = util_http.formdecode; +}; diff --git a/net/http/codes.lua b/net/http/codes.lua index 0cadd079..bc31c7dd 100644 --- a/net/http/codes.lua +++ b/net/http/codes.lua @@ -25,6 +25,7 @@ local response_codes = { [305] = "Use Proxy"; -- The 306 status code was used in a previous version of [RFC2616], is no longer used, and the code is reserved. [307] = "Temporary Redirect"; + [308] = "Permanent Redirect"; [400] = "Bad Request"; [401] = "Unauthorized"; @@ -39,17 +40,21 @@ local response_codes = { [410] = "Gone"; [411] = "Length Required"; [412] = "Precondition Failed"; - [413] = "Request Entity Too Large"; - [414] = "Request-URI Too Long"; + [413] = "Payload Too Large"; + [414] = "URI Too Long"; [415] = "Unsupported Media Type"; - [416] = "Requested Range Not Satisfiable"; + [416] = "Range Not Satisfiable"; [417] = "Expectation Failed"; [418] = "I'm a teapot"; + [421] = "Misdirected Request"; [422] = "Unprocessable Entity"; [423] = "Locked"; [424] = "Failed Dependency"; -- The 425 status code is reserved for the WebDAV advanced collections expired proposal [RFC2817] [426] = "Upgrade Required"; + [428] = "Precondition Required"; + [429] = "Too Many Requests"; + [431] = "Request Header Fields Too Large"; [500] = "Internal Server Error"; [501] = "Not Implemented"; @@ -61,6 +66,7 @@ local response_codes = { [507] = "Insufficient Storage"; [508] = "Loop Detected"; [510] = "Not Extended"; + [511] = "Network Authentication Required"; }; for k,v in pairs(response_codes) do response_codes[k] = k.." "..v; end diff --git a/net/http/server.lua b/net/http/server.lua index f091595c..aeaa7416 100644 --- a/net/http/server.lua +++ b/net/http/server.lua @@ -11,6 +11,7 @@ local setmetatable = setmetatable; local xpcall = xpcall; local traceback = debug.traceback; local tostring = tostring; +local cache = require "util.cache"; local codes = require "net.http.codes"; local _M = {}; @@ -27,7 +28,10 @@ local function is_wildcard_match(wildcard_event, event) return wildcard_event:sub(1, -2) == event:sub(1, #wildcard_event-1); end -local recent_wildcard_events, max_cached_wildcard_events = {}, 10000; +local _handlers = events._handlers; +local recent_wildcard_events = cache.new(10000, function (key, value) + rawset(_handlers, key, nil); +end); local event_map = events._event_map; setmetatable(events._handlers, { @@ -62,10 +66,7 @@ setmetatable(events._handlers, { end rawset(handlers, curr_event, handlers_array); if not event_map[curr_event] then -- Only wildcard handlers match, if any - table.insert(recent_wildcard_events, curr_event); - if #recent_wildcard_events > max_cached_wildcard_events then - rawset(handlers, table.remove(recent_wildcard_events, 1), nil); - end + recent_wildcard_events:set(curr_event, true); end return handlers_array; end; @@ -189,6 +190,7 @@ function handle_request(conn, request, finish_cb) persistent = persistent; conn = conn; send = _M.send_response; + done = _M.finish_response; finish_cb = finish_cb; }; conn._http_open_response = response; @@ -208,7 +210,7 @@ function handle_request(conn, request, finish_cb) err_code, err = 400, "Missing or invalid 'Host' header"; end end - + if err then response.status_code = err_code; response:send(events.fire_event("http-error", { code = err_code, message = err })); @@ -250,24 +252,30 @@ function handle_request(conn, request, finish_cb) response.status_code = 404; response:send(events.fire_event("http-error", { code = 404 })); end -function _M.send_response(response, body) - if response.finished then return; end - response.finished = true; - response.conn._http_open_response = nil; - +local function prepare_header(response) local status_line = "HTTP/"..response.request.httpversion.." "..(response.status or codes[response.status_code]); local headers = response.headers; - body = body or response.body or ""; - headers.content_length = #body; - local output = { status_line }; for k,v in pairs(headers) do t_insert(output, headerfix[k]..v); end t_insert(output, "\r\n\r\n"); + return output; +end +_M.prepare_header = prepare_header; +function _M.send_response(response, body) + if response.finished then return; end + body = body or response.body or ""; + response.headers.content_length = #body; + local output = prepare_header(response); t_insert(output, body); - response.conn:write(t_concat(output)); + response:done(); +end +function _M.finish_response(response) + if response.finished then return; end + response.finished = true; + response.conn._http_open_response = nil; if response.on_destroy then response:on_destroy(); response.on_destroy = nil; @@ -286,7 +294,7 @@ function _M.remove_handler(event, handler) end function _M.listen_on(port, interface, ssl) - addserver(interface or "*", port, listener, "*a", ssl); + return addserver(interface or "*", port, listener, "*a", ssl); end function _M.add_host(host) hosts[host] = true; diff --git a/net/httpserver.lua b/net/httpserver.lua index 7d574788..6e2e31b9 100644 --- a/net/httpserver.lua +++ b/net/httpserver.lua @@ -2,14 +2,15 @@ local log = require "util.logger".init("net.httpserver"); local traceback = debug.traceback; -module "httpserver" +local _ENV = nil; function fail() log("error", "Attempt to use legacy HTTP API. For more info see http://prosody.im/doc/developers/legacy_http"); log("error", "Legacy HTTP API usage, %s", traceback("", 2)); end -new, new_from_config = fail, fail; -set_default_handler = fail; - -return _M; +return { + new = fail; + new_from_config = fail; + set_default_handler = fail; +}; diff --git a/net/server.lua b/net/server.lua index 9b0d27e1..41e180fa 100644 --- a/net/server.lua +++ b/net/server.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/net/server_event.lua b/net/server_event.lua index 45938a13..e5705ab5 100644 --- a/net/server_event.lua +++ b/net/server_event.lua @@ -11,6 +11,7 @@ -- when using luasec, there are 4 cases of timeout errors: wantread or wantwrite during reading or writing --]] +-- luacheck: ignore 212/self 431/err 211/ret local SCRIPT_NAME = "server_event.lua" local SCRIPT_VERSION = "0.05" @@ -32,27 +33,32 @@ local cfg = { DEBUG = true, -- show debug messages } -local function use(x) return rawget(_G, x); end -local ipairs = use "ipairs" -local string = use "string" -local select = use "select" -local require = use "require" -local tostring = use "tostring" -local coroutine = use "coroutine" -local setmetatable = use "setmetatable" +local pairs = pairs +local select = select +local require = require +local tostring = tostring +local setmetatable = setmetatable local t_insert = table.insert local t_concat = table.concat +local s_sub = string.sub -local ssl = use "ssl" -local socket = use "socket" or require "socket" +local coroutine_wrap = coroutine.wrap +local coroutine_yield = coroutine.yield + +local has_luasec, ssl = pcall ( require , "ssl" ) +local socket = require "socket" +local levent = require "luaevent.core" + +local socket_gettime = socket.gettime +local getaddrinfo = socket.dns.getaddrinfo local log = require ("util.logger").init("socket") local function debug(...) return log("debug", ("%s "):rep(select('#', ...)), ...) end -local vdebug = debug; +-- local vdebug = debug; local bitor = ( function( ) -- thx Rici Lake local hasbit = function( x, p ) @@ -72,737 +78,673 @@ local bitor = ( function( ) -- thx Rici Lake end end )( ) -local event = require "luaevent.core" -local base = event.new( ) -local EV_READ = event.EV_READ -local EV_WRITE = event.EV_WRITE -local EV_TIMEOUT = event.EV_TIMEOUT -local EV_SIGNAL = event.EV_SIGNAL +local base = levent.new( ) +local addevent = base.addevent +local EV_READ = levent.EV_READ +local EV_WRITE = levent.EV_WRITE +local EV_TIMEOUT = levent.EV_TIMEOUT +local EV_SIGNAL = levent.EV_SIGNAL local EV_READWRITE = bitor( EV_READ, EV_WRITE ) -local interfacelist = ( function( ) -- holds the interfaces for sockets - local array = { } - local len = 0 - return function( method, arg ) - if "add" == method then - len = len + 1 - array[ len ] = arg - arg:_position( len ) - return len - elseif "delete" == method then - if len <= 0 then - return nil, "array is already empty" +local interfacelist = { } + +-- Client interface methods +local interface_mt = {}; interface_mt.__index = interface_mt; + +-- Private methods +function interface_mt:_close() + return self:_destroy(); +end + +function interface_mt:_start_connection(plainssl) -- should be called from addclient + local callback = function( event ) + if EV_TIMEOUT == event then -- timeout during connection + self.fatalerror = "connection timeout" + self:ontimeout() -- call timeout listener + self:_close() + debug( "new connection failed. id:", self.id, "error:", self.fatalerror ) + else + if plainssl and has_luasec then -- start ssl session + self:starttls(self._sslctx, true) + else -- normal connection + self:_start_session(true) end - local position = arg:_position() -- get position in array - if position ~= len then - local interface = array[ len ] -- get last interface - array[ position ] = interface -- copy it into free position - array[ len ] = nil -- free last position - interface:_position( position ) -- set new position in array - else -- free last position - array[ len ] = nil + debug( "new connection established. id:", self.id ) + end + self.eventconnect = nil + return -1 + end + self.eventconnect = addevent( base, self.conn, EV_WRITE, callback, cfg.CONNECT_TIMEOUT ) + return true +end +function interface_mt:_start_session(call_onconnect) -- new session, for example after startssl + if self.type == "client" then + local callback = function( ) + self:_lock( false, false, false ) + --vdebug( "start listening on client socket with id:", self.id ) + self.eventread = addevent( base, self.conn, EV_READ, self.readcallback, cfg.READ_TIMEOUT ); -- register callback + if call_onconnect then + self:onconnect() end - len = len - 1 - return len - else - return array + self.eventsession = nil + return -1 end + self.eventsession = addevent( base, nil, EV_TIMEOUT, callback, 0 ) + else + self:_lock( false ) + --vdebug( "start listening on server socket with id:", self.id ) + self.eventread = addevent( base, self.conn, EV_READ, self.readcallback ) -- register callback end -end )( ) - --- Client interface methods -local interface_mt -do - interface_mt = {}; interface_mt.__index = interface_mt; - - local addevent = base.addevent - local coroutine_wrap, coroutine_yield = coroutine.wrap,coroutine.yield - - -- Private methods - function interface_mt:_position(new_position) - self.position = new_position or self.position - return self.position; - end - function interface_mt:_close() - return self:_destroy(); - end - - function interface_mt:_start_connection(plainssl) -- should be called from addclient - local callback = function( event ) - if EV_TIMEOUT == event then -- timeout during connection - self.fatalerror = "connection timeout" - self:ontimeout() -- call timeout listener - self:_close() - debug( "new connection failed. id:", self.id, "error:", self.fatalerror ) - else - if plainssl and ssl then -- start ssl session - self:starttls(self._sslctx, true) - else -- normal connection - self:_start_session(true) + return true +end +function interface_mt:_start_ssl(call_onconnect) -- old socket will be destroyed, therefore we have to close read/write events first + --vdebug( "starting ssl session with client id:", self.id ) + local _ + _ = self.eventread and self.eventread:close( ) -- close events; this must be called outside of the event callbacks! + _ = self.eventwrite and self.eventwrite:close( ) + self.eventread, self.eventwrite = nil, nil + local err + self.conn, err = ssl.wrap( self.conn, self._sslctx ) + if err then + self.fatalerror = err + self.conn = nil -- cannot be used anymore + if call_onconnect then + self.ondisconnect = nil -- dont call this when client isnt really connected + end + self:_close() + debug( "fatal error while ssl wrapping:", err ) + return false + end + self.conn:settimeout( 0 ) -- set non blocking + local handshakecallback = coroutine_wrap(function( event ) + local _, err + local attempt = 0 + local maxattempt = cfg.MAX_HANDSHAKE_ATTEMPTS + while attempt < maxattempt do -- no endless loop + attempt = attempt + 1 + debug( "ssl handshake of client with id:"..tostring(self)..", attempt:"..attempt ) + if attempt > maxattempt then + self.fatalerror = "max handshake attempts exceeded" + elseif EV_TIMEOUT == event then + self.fatalerror = "timeout during handshake" + else + _, err = self.conn:dohandshake( ) + if not err then + self:_lock( false, false, false ) -- unlock the interface; sending, closing etc allowed + self.send = self.conn.send -- caching table lookups with new client object + self.receive = self.conn.receive + if not call_onconnect then -- trigger listener + self:onstatus("ssl-handshake-complete"); end - debug( "new connection established. id:", self.id ) + self:_start_session( call_onconnect ) + debug( "ssl handshake done" ) + self.eventhandshake = nil + return -1 end - self.eventconnect = nil - return -1 - end - self.eventconnect = addevent( base, self.conn, EV_WRITE, callback, cfg.CONNECT_TIMEOUT ) - return true - end - function interface_mt:_start_session(call_onconnect) -- new session, for example after startssl - if self.type == "client" then - local callback = function( ) - self:_lock( false, false, false ) - --vdebug( "start listening on client socket with id:", self.id ) - self.eventread = addevent( base, self.conn, EV_READ, self.readcallback, cfg.READ_TIMEOUT ); -- register callback - if call_onconnect then - self:onconnect() + if err == "wantwrite" then + event = EV_WRITE + elseif err == "wantread" then + event = EV_READ + else + debug( "ssl handshake error:", err ) + self.fatalerror = err end - self.eventsession = nil - return -1 end - self.eventsession = addevent( base, nil, EV_TIMEOUT, callback, 0 ) - else - self:_lock( false ) - --vdebug( "start listening on server socket with id:", self.id ) - self.eventread = addevent( base, self.conn, EV_READ, self.readcallback ) -- register callback - end - return true - end - function interface_mt:_start_ssl(call_onconnect) -- old socket will be destroyed, therefore we have to close read/write events first - --vdebug( "starting ssl session with client id:", self.id ) - local _ - _ = self.eventread and self.eventread:close( ) -- close events; this must be called outside of the event callbacks! - _ = self.eventwrite and self.eventwrite:close( ) - self.eventread, self.eventwrite = nil, nil - local err - self.conn, err = ssl.wrap( self.conn, self._sslctx ) - if err then - self.fatalerror = err - self.conn = nil -- cannot be used anymore + if self.fatalerror then if call_onconnect then self.ondisconnect = nil -- dont call this when client isnt really connected end self:_close() - debug( "fatal error while ssl wrapping:", err ) - return false - end - self.conn:settimeout( 0 ) -- set non blocking - local handshakecallback = coroutine_wrap( - function( event ) - local _, err - local attempt = 0 - local maxattempt = cfg.MAX_HANDSHAKE_ATTEMPTS - while attempt < maxattempt do -- no endless loop - attempt = attempt + 1 - debug( "ssl handshake of client with id:"..tostring(self)..", attempt:"..attempt ) - if attempt > maxattempt then - self.fatalerror = "max handshake attempts exceeded" - elseif EV_TIMEOUT == event then - self.fatalerror = "timeout during handshake" - else - _, err = self.conn:dohandshake( ) - if not err then - self:_lock( false, false, false ) -- unlock the interface; sending, closing etc allowed - self.send = self.conn.send -- caching table lookups with new client object - self.receive = self.conn.receive - if not call_onconnect then -- trigger listener - self:onstatus("ssl-handshake-complete"); - end - self:_start_session( call_onconnect ) - debug( "ssl handshake done" ) - self.eventhandshake = nil - return -1 - end - if err == "wantwrite" then - event = EV_WRITE - elseif err == "wantread" then - event = EV_READ - else - debug( "ssl handshake error:", err ) - self.fatalerror = err - end - end - if self.fatalerror then - if call_onconnect then - self.ondisconnect = nil -- dont call this when client isnt really connected - end - self:_close() - debug( "handshake failed because:", self.fatalerror ) - self.eventhandshake = nil - return -1 - end - event = coroutine_yield( event, cfg.HANDSHAKE_TIMEOUT ) -- yield this monster... - end - end - ) - debug "starting handshake..." - self:_lock( false, true, true ) -- unlock read/write events, but keep interface locked - self.eventhandshake = addevent( base, self.conn, EV_READWRITE, handshakecallback, cfg.HANDSHAKE_TIMEOUT ) - return true - end - function interface_mt:_destroy() -- close this interface + events and call last listener - debug( "closing client with id:", self.id, self.fatalerror ) - self:_lock( true, true, true ) -- first of all, lock the interface to avoid further actions - local _ - _ = self.eventread and self.eventread:close( ) - if self.type == "client" then - _ = self.eventwrite and self.eventwrite:close( ) - _ = self.eventhandshake and self.eventhandshake:close( ) - _ = self.eventstarthandshake and self.eventstarthandshake:close( ) - _ = self.eventconnect and self.eventconnect:close( ) - _ = self.eventsession and self.eventsession:close( ) - _ = self.eventwritetimeout and self.eventwritetimeout:close( ) - _ = self.eventreadtimeout and self.eventreadtimeout:close( ) - _ = self.ondisconnect and self:ondisconnect( self.fatalerror ~= "client to close" and self.fatalerror) -- call ondisconnect listener (wont be the case if handshake failed on connect) - _ = self.conn and self.conn:close( ) -- close connection - _ = self._server and self._server:counter(-1); - self.eventread, self.eventwrite = nil, nil - self.eventstarthandshake, self.eventhandshake, self.eventclose = nil, nil, nil - self.readcallback, self.writecallback = nil, nil - else - self.conn:close( ) - self.eventread, self.eventclose = nil, nil - self.interface, self.readcallback = nil, nil + debug( "handshake failed because:", self.fatalerror ) + self.eventhandshake = nil + return -1 end - interfacelist( "delete", self ) - return true - end - - function interface_mt:_lock(nointerface, noreading, nowriting) -- lock or unlock this interface or events - self.nointerface, self.noreading, self.nowriting = nointerface, noreading, nowriting - return nointerface, noreading, nowriting - end - - --TODO: Deprecate - function interface_mt:lock_read(switch) - if switch then - return self:pause(); - else - return self:resume(); + event = coroutine_yield( event, cfg.HANDSHAKE_TIMEOUT ) -- yield this monster... end end + ) + debug "starting handshake..." + self:_lock( false, true, true ) -- unlock read/write events, but keep interface locked + self.eventhandshake = addevent( base, self.conn, EV_READWRITE, handshakecallback, cfg.HANDSHAKE_TIMEOUT ) + return true +end +function interface_mt:_destroy() -- close this interface + events and call last listener + debug( "closing client with id:", self.id, self.fatalerror ) + self:_lock( true, true, true ) -- first of all, lock the interface to avoid further actions + local _ + _ = self.eventread and self.eventread:close( ) + if self.type == "client" then + _ = self.eventwrite and self.eventwrite:close( ) + _ = self.eventhandshake and self.eventhandshake:close( ) + _ = self.eventstarthandshake and self.eventstarthandshake:close( ) + _ = self.eventconnect and self.eventconnect:close( ) + _ = self.eventsession and self.eventsession:close( ) + _ = self.eventwritetimeout and self.eventwritetimeout:close( ) + _ = self.eventreadtimeout and self.eventreadtimeout:close( ) + _ = self.ondisconnect and self:ondisconnect( self.fatalerror ~= "client to close" and self.fatalerror) -- call ondisconnect listener (wont be the case if handshake failed on connect) + _ = self.conn and self.conn:close( ) -- close connection + _ = self._server and self._server:counter(-1); + self.eventread, self.eventwrite = nil, nil + self.eventstarthandshake, self.eventhandshake, self.eventclose = nil, nil, nil + self.readcallback, self.writecallback = nil, nil + else + self.conn:close( ) + self.eventread, self.eventclose = nil, nil + self.interface, self.readcallback = nil, nil + end + interfacelist[ self ] = nil + return true +end - function interface_mt:pause() - return self:_lock(self.nointerface, true, self.nowriting); - end +function interface_mt:_lock(nointerface, noreading, nowriting) -- lock or unlock this interface or events + self.nointerface, self.noreading, self.nowriting = nointerface, noreading, nowriting + return nointerface, noreading, nowriting +end - function interface_mt:resume() - self:_lock(self.nointerface, false, self.nowriting); - if not self.eventread then - self.eventread = addevent( base, self.conn, EV_READ, self.readcallback, cfg.READ_TIMEOUT ); -- register callback - end +--TODO: Deprecate +function interface_mt:lock_read(switch) + if switch then + return self:pause(); + else + return self:resume(); end +end - function interface_mt:counter(c) - if c then - self._connections = self._connections + c - end - return self._connections - end - - -- Public methods - function interface_mt:write(data) - if self.nowriting then return nil, "locked" end - --vdebug( "try to send data to client, id/data:", self.id, data ) - data = tostring( data ) - local len = #data - local total = len + self.writebufferlen - if total > cfg.MAX_SEND_LENGTH then -- check buffer length - local err = "send buffer exceeded" - debug( "error:", err ) -- to much, check your app - return nil, err - end - t_insert(self.writebuffer, data) -- new buffer - self.writebufferlen = total - if not self.eventwrite then -- register new write event - --vdebug( "register new write event" ) - self.eventwrite = addevent( base, self.conn, EV_WRITE, self.writecallback, cfg.WRITE_TIMEOUT ) - end - return true - end - function interface_mt:close() - if self.nointerface then return nil, "locked"; end - debug( "try to close client connection with id:", self.id ) - if self.type == "client" then - self.fatalerror = "client to close" - if self.eventwrite then -- wait for incomplete write request - self:_lock( true, true, false ) - debug "closing delayed until writebuffer is empty" - return nil, "writebuffer not empty, waiting" - else -- close now - self:_lock( true, true, true ) - self:_close() - return true - end - else - debug( "try to close server with id:", tostring(self.id)) - self.fatalerror = "server to close" - self:_lock( true ) - self:_close( 0 ) - return true - end - end - - function interface_mt:socket() - return self.conn - end - - function interface_mt:server() - return self._server or self; - end - - function interface_mt:port() - return self._port - end - - function interface_mt:serverport() - return self._serverport - end - - function interface_mt:ip() - return self._ip - end - - function interface_mt:ssl() - return self._usingssl - end - interface_mt.clientport = interface_mt.port -- COMPAT server_select +function interface_mt:pause() + return self:_lock(self.nointerface, true, self.nowriting); +end - function interface_mt:type() - return self._type or "client" +function interface_mt:resume() + self:_lock(self.nointerface, false, self.nowriting); + if not self.eventread then + self.eventread = addevent( base, self.conn, EV_READ, self.readcallback, cfg.READ_TIMEOUT ); -- register callback end - - function interface_mt:connections() - return self._connections - end - - function interface_mt:address() - return self.addr - end - - function interface_mt:set_sslctx(sslctx) - self._sslctx = sslctx; - if sslctx then - self.starttls = nil; -- use starttls() of interface_mt - else - self.starttls = false; -- prevent starttls() - end +end + +function interface_mt:counter(c) + if c then + self._connections = self._connections + c end + return self._connections +end - function interface_mt:set_mode(pattern) - if pattern then - self._pattern = pattern; - end - return self._pattern; - end - - function interface_mt:set_send(new_send) - -- No-op, we always use the underlying connection's send - end - - function interface_mt:starttls(sslctx, call_onconnect) - debug( "try to start ssl at client id:", self.id ) - local err - self._sslctx = sslctx; - if self._usingssl then -- startssl was already called - err = "ssl already active" - end - if err then - debug( "error:", err ) - return nil, err - end - self._usingssl = true - self.startsslcallback = function( ) -- we have to start the handshake outside of a read/write event - self.startsslcallback = nil - self:_start_ssl(call_onconnect); - self.eventstarthandshake = nil - return -1 - end - if not self.eventwrite then - self:_lock( true, true, true ) -- lock the interface, to not disturb the handshake - self.eventstarthandshake = addevent( base, nil, EV_TIMEOUT, self.startsslcallback, 0 ) -- add event to start handshake - else -- wait until writebuffer is empty +-- Public methods +function interface_mt:write(data) + if self.nowriting then return nil, "locked" end + --vdebug( "try to send data to client, id/data:", self.id, data ) + data = tostring( data ) + local len = #data + local total = len + self.writebufferlen + if total > cfg.MAX_SEND_LENGTH then -- check buffer length + local err = "send buffer exceeded" + debug( "error:", err ) -- to much, check your app + return nil, err + end + t_insert(self.writebuffer, data) -- new buffer + self.writebufferlen = total + if not self.eventwrite then -- register new write event + --vdebug( "register new write event" ) + self.eventwrite = addevent( base, self.conn, EV_WRITE, self.writecallback, cfg.WRITE_TIMEOUT ) + end + return true +end +function interface_mt:close() + if self.nointerface then return nil, "locked"; end + debug( "try to close client connection with id:", self.id ) + if self.type == "client" then + self.fatalerror = "client to close" + if self.eventwrite then -- wait for incomplete write request self:_lock( true, true, false ) - debug "ssl session delayed until writebuffer is empty..." + debug "closing delayed until writebuffer is empty" + return nil, "writebuffer not empty, waiting" + else -- close now + self:_lock( true, true, true ) + self:_close() + return true end - self.starttls = false; + else + debug( "try to close server with id:", tostring(self.id)) + self.fatalerror = "server to close" + self:_lock( true ) + self:_close( 0 ) return true end - - function interface_mt:setoption(option, value) - if self.conn.setoption then - return self.conn:setoption(option, value); - end - return false, "setoption not implemented"; - end - - function interface_mt:setlistener(listener) - self:ondetach(); -- Notify listener that it is no longer responsible for this connection - self.onconnect, self.ondisconnect, self.onincoming, - self.ontimeout, self.onstatus, self.ondetach - = listener.onconnect, listener.ondisconnect, listener.onincoming, - listener.ontimeout, listener.onstatus, listener.ondetach; - end - - -- Stub handlers - function interface_mt:onconnect() - end - function interface_mt:onincoming() - end - function interface_mt:ondisconnect() - end - function interface_mt:ontimeout() - end - function interface_mt:ondrain() +end + +function interface_mt:socket() + return self.conn +end + +function interface_mt:server() + return self._server or self; +end + +function interface_mt:port() + return self._port +end + +function interface_mt:serverport() + return self._serverport +end + +function interface_mt:ip() + return self._ip +end + +function interface_mt:ssl() + return self._usingssl +end +interface_mt.clientport = interface_mt.port -- COMPAT server_select + +function interface_mt:type() + return self._type or "client" +end + +function interface_mt:connections() + return self._connections +end + +function interface_mt:address() + return self.addr +end + +function interface_mt:set_sslctx(sslctx) + self._sslctx = sslctx; + if sslctx then + self.starttls = nil; -- use starttls() of interface_mt + else + self.starttls = false; -- prevent starttls() end - function interface_mt:ondetach() +end + +function interface_mt:set_mode(pattern) + if pattern then + self._pattern = pattern; end - function interface_mt:onstatus() + return self._pattern; +end + +function interface_mt:set_send(new_send) -- luacheck: ignore 212 + -- No-op, we always use the underlying connection's send +end + +function interface_mt:starttls(sslctx, call_onconnect) + debug( "try to start ssl at client id:", self.id ) + local err + self._sslctx = sslctx; + if self._usingssl then -- startssl was already called + err = "ssl already active" + end + if err then + debug( "error:", err ) + return nil, err + end + self._usingssl = true + self.startsslcallback = function( ) -- we have to start the handshake outside of a read/write event + self.startsslcallback = nil + self:_start_ssl(call_onconnect); + self.eventstarthandshake = nil + return -1 + end + if not self.eventwrite then + self:_lock( true, true, true ) -- lock the interface, to not disturb the handshake + self.eventstarthandshake = addevent( base, nil, EV_TIMEOUT, self.startsslcallback, 0 ) -- add event to start handshake + else -- wait until writebuffer is empty + self:_lock( true, true, false ) + debug "ssl session delayed until writebuffer is empty..." + end + self.starttls = false; + return true +end + +function interface_mt:setoption(option, value) + if self.conn.setoption then + return self.conn:setoption(option, value); end + return false, "setoption not implemented"; +end + +function interface_mt:setlistener(listener) + self:ondetach(); -- Notify listener that it is no longer responsible for this connection + self.onconnect, self.ondisconnect, self.onincoming, self.ontimeout, + self.onreadtimeout, self.onstatus, self.ondetach + = listener.onconnect, listener.ondisconnect, listener.onincoming, listener.ontimeout, + listener.onreadtimeout, listener.onstatus, listener.ondetach; +end + +-- Stub handlers +function interface_mt:onconnect() +end +function interface_mt:onincoming() +end +function interface_mt:ondisconnect() +end +function interface_mt:ontimeout() +end +function interface_mt:onreadtimeout() + self.fatalerror = "timeout during receiving" + debug( "connection failed:", self.fatalerror ) + self:_close() + self.eventread = nil +end +function interface_mt:ondrain() +end +function interface_mt:ondetach() +end +function interface_mt:onstatus() end -- End of client interface methods -local handleclient; -do - local string_sub = string.sub -- caching table lookups - local addevent = base.addevent - local socket_gettime = socket.gettime - function handleclient( client, ip, port, server, pattern, listener, sslctx ) -- creates an client interface - --vdebug("creating client interfacce...") - local interface = { - type = "client"; - conn = client; - currenttime = socket_gettime( ); -- safe the origin - writebuffer = {}; -- writebuffer - writebufferlen = 0; -- length of writebuffer - send = client.send; -- caching table lookups - receive = client.receive; - onconnect = listener.onconnect; -- will be called when client disconnects - ondisconnect = listener.ondisconnect; -- will be called when client disconnects - onincoming = listener.onincoming; -- will be called when client sends data - ontimeout = listener.ontimeout; -- called when fatal socket timeout occurs - ondrain = listener.ondrain; -- called when writebuffer is empty - ondetach = listener.ondetach; -- called when disassociating this listener from this connection - onstatus = listener.onstatus; -- called for status changes (e.g. of SSL/TLS) - eventread = false, eventwrite = false, eventclose = false, - eventhandshake = false, eventstarthandshake = false; -- event handler - eventconnect = false, eventsession = false; -- more event handler... - eventwritetimeout = false; -- even more event handler... - eventreadtimeout = false; - fatalerror = false; -- error message - writecallback = false; -- will be called on write events - readcallback = false; -- will be called on read events - nointerface = true; -- lock/unlock parameter of this interface - noreading = false, nowriting = false; -- locks of the read/writecallback - startsslcallback = false; -- starting handshake callback - position = false; -- position of client in interfacelist - - -- Properties - _ip = ip, _port = port, _server = server, _pattern = pattern, - _serverport = (server and server:port() or nil), - _sslctx = sslctx; -- parameters - _usingssl = false; -- client is using ssl; - } - if not ssl then interface.starttls = false; end - interface.id = tostring(interface):match("%x+$"); - interface.writecallback = function( event ) -- called on write events - --vdebug( "new client write event, id/ip/port:", interface, ip, port ) - if interface.nowriting or ( interface.fatalerror and ( "client to close" ~= interface.fatalerror ) ) then -- leave this event - --vdebug( "leaving this event because:", interface.nowriting or interface.fatalerror ) - interface.eventwrite = false - return -1 - end - if EV_TIMEOUT == event then -- took too long to write some data to socket -> disconnect - interface.fatalerror = "timeout during writing" - debug( "writing failed:", interface.fatalerror ) - interface:_close() - interface.eventwrite = false - return -1 - else -- can write :) - if interface._usingssl then -- handle luasec - if interface.eventreadtimeout then -- we have to read first - local ret = interface.readcallback( ) -- call readcallback - --vdebug( "tried to read in writecallback, result:", ret ) - end - if interface.eventwritetimeout then -- luasec only - interface.eventwritetimeout:close( ) -- first we have to close timeout event which where regged after a wantread error - interface.eventwritetimeout = false - end +local function handleclient( client, ip, port, server, pattern, listener, sslctx ) -- creates an client interface + --vdebug("creating client interfacce...") + local interface = { + type = "client"; + conn = client; + currenttime = socket_gettime( ); -- safe the origin + writebuffer = {}; -- writebuffer + writebufferlen = 0; -- length of writebuffer + send = client.send; -- caching table lookups + receive = client.receive; + onconnect = listener.onconnect; -- will be called when client disconnects + ondisconnect = listener.ondisconnect; -- will be called when client disconnects + onincoming = listener.onincoming; -- will be called when client sends data + ontimeout = listener.ontimeout; -- called when fatal socket timeout occurs + onreadtimeout = listener.onreadtimeout; -- called when socket inactivity timeout occurs + ondrain = listener.ondrain; -- called when writebuffer is empty + ondetach = listener.ondetach; -- called when disassociating this listener from this connection + onstatus = listener.onstatus; -- called for status changes (e.g. of SSL/TLS) + eventread = false, eventwrite = false, eventclose = false, + eventhandshake = false, eventstarthandshake = false; -- event handler + eventconnect = false, eventsession = false; -- more event handler... + eventwritetimeout = false; -- even more event handler... + eventreadtimeout = false; + fatalerror = false; -- error message + writecallback = false; -- will be called on write events + readcallback = false; -- will be called on read events + nointerface = true; -- lock/unlock parameter of this interface + noreading = false, nowriting = false; -- locks of the read/writecallback + startsslcallback = false; -- starting handshake callback + position = false; -- position of client in interfacelist + + -- Properties + _ip = ip, _port = port, _server = server, _pattern = pattern, + _serverport = (server and server:port() or nil), + _sslctx = sslctx; -- parameters + _usingssl = false; -- client is using ssl; + } + if not has_luasec then interface.starttls = false; end + interface.id = tostring(interface):match("%x+$"); + interface.writecallback = function( event ) -- called on write events + --vdebug( "new client write event, id/ip/port:", interface, ip, port ) + if interface.nowriting or ( interface.fatalerror and ( "client to close" ~= interface.fatalerror ) ) then -- leave this event + --vdebug( "leaving this event because:", interface.nowriting or interface.fatalerror ) + interface.eventwrite = false + return -1 + end + if EV_TIMEOUT == event then -- took too long to write some data to socket -> disconnect + interface.fatalerror = "timeout during writing" + debug( "writing failed:", interface.fatalerror ) + interface:_close() + interface.eventwrite = false + return -1 + else -- can write :) + if interface._usingssl then -- handle luasec + if interface.eventreadtimeout then -- we have to read first + local ret = interface.readcallback( ) -- call readcallback + --vdebug( "tried to read in writecallback, result:", ret ) end - interface.writebuffer = { t_concat(interface.writebuffer) } - local succ, err, byte = interface.conn:send( interface.writebuffer[1], 1, interface.writebufferlen ) - --vdebug( "write data:", interface.writebuffer, "error:", err, "part:", byte ) - if succ then -- writing succesful - interface.writebuffer[1] = nil - interface.writebufferlen = 0 - interface:ondrain(); - if interface.fatalerror then - debug "closing client after writing" - interface:_close() -- close interface if needed - elseif interface.startsslcallback then -- start ssl connection if needed - debug "starting ssl handshake after writing" - interface.eventstarthandshake = addevent( base, nil, EV_TIMEOUT, interface.startsslcallback, 0 ) - elseif interface.eventreadtimeout then - return EV_WRITE, EV_TIMEOUT - end - interface.eventwrite = nil - return -1 - elseif byte and (err == "timeout" or err == "wantwrite") then -- want write again - --vdebug( "writebuffer is not empty:", err ) - interface.writebuffer[1] = string_sub( interface.writebuffer[1], byte + 1, interface.writebufferlen ) -- new buffer - interface.writebufferlen = interface.writebufferlen - byte - if "wantread" == err then -- happens only with luasec - local callback = function( ) - interface:_close() - interface.eventwritetimeout = nil - return -1; - end - interface.eventwritetimeout = addevent( base, nil, EV_TIMEOUT, callback, cfg.WRITE_TIMEOUT ) -- reg a new timeout event - debug( "wantread during write attempt, reg it in readcallback but dont know what really happens next..." ) - -- hopefully this works with luasec; its simply not possible to use 2 different write events on a socket in luaevent - return -1 - end - return EV_WRITE, cfg.WRITE_TIMEOUT - else -- connection was closed during writing or fatal error - interface.fatalerror = err or "fatal error" - debug( "connection failed in write event:", interface.fatalerror ) - interface:_close() - interface.eventwrite = nil - return -1 + if interface.eventwritetimeout then -- luasec only + interface.eventwritetimeout:close( ) -- first we have to close timeout event which where regged after a wantread error + interface.eventwritetimeout = false end end - end - - interface.readcallback = function( event ) -- called on read events - --vdebug( "new client read event, id/ip/port:", tostring(interface.id), tostring(ip), tostring(port) ) - if interface.noreading or interface.fatalerror then -- leave this event - --vdebug( "leaving this event because:", tostring(interface.noreading or interface.fatalerror) ) - interface.eventread = nil - return -1 - end - if EV_TIMEOUT == event then -- took too long to get some data from client -> disconnect - interface.fatalerror = "timeout during receiving" - debug( "connection failed:", interface.fatalerror ) - interface:_close() - interface.eventread = nil - return -1 - else -- can read - if interface._usingssl then -- handle luasec - if interface.eventwritetimeout then -- ok, in the past writecallback was regged - local ret = interface.writecallback( ) -- call it - --vdebug( "tried to write in readcallback, result:", tostring(ret) ) - end - if interface.eventreadtimeout then - interface.eventreadtimeout:close( ) - interface.eventreadtimeout = nil - end - end - local buffer, err, part = interface.conn:receive( interface._pattern ) -- receive buffer with "pattern" - --vdebug( "read data:", tostring(buffer), "error:", tostring(err), "part:", tostring(part) ) - buffer = buffer or part - if buffer and #buffer > cfg.MAX_READ_LENGTH then -- check buffer length - interface.fatalerror = "receive buffer exceeded" - debug( "fatal error:", interface.fatalerror ) - interface:_close() - interface.eventread = nil - return -1 + interface.writebuffer = { t_concat(interface.writebuffer) } + local succ, err, byte = interface.conn:send( interface.writebuffer[1], 1, interface.writebufferlen ) + --vdebug( "write data:", interface.writebuffer, "error:", err, "part:", byte ) + if succ then -- writing succesful + interface.writebuffer[1] = nil + interface.writebufferlen = 0 + interface:ondrain(); + if interface.fatalerror then + debug "closing client after writing" + interface:_close() -- close interface if needed + elseif interface.startsslcallback then -- start ssl connection if needed + debug "starting ssl handshake after writing" + interface.eventstarthandshake = addevent( base, nil, EV_TIMEOUT, interface.startsslcallback, 0 ) + elseif interface.eventreadtimeout then + return EV_WRITE, EV_TIMEOUT end - if err and ( err ~= "timeout" and err ~= "wantread" ) then - if "wantwrite" == err then -- need to read on write event - if not interface.eventwrite then -- register new write event if needed - interface.eventwrite = addevent( base, interface.conn, EV_WRITE, interface.writecallback, cfg.WRITE_TIMEOUT ) - end - interface.eventreadtimeout = addevent( base, nil, EV_TIMEOUT, - function( ) - interface:_close() - end, cfg.READ_TIMEOUT - ) - debug( "wantwrite during read attempt, reg it in writecallback but dont know what really happens next..." ) - -- to be honest i dont know what happens next, if it is allowed to first read, the write etc... - else -- connection was closed or fatal error - interface.fatalerror = err - debug( "connection failed in read event:", interface.fatalerror ) + interface.eventwrite = nil + return -1 + elseif byte and (err == "timeout" or err == "wantwrite") then -- want write again + --vdebug( "writebuffer is not empty:", err ) + interface.writebuffer[1] = s_sub( interface.writebuffer[1], byte + 1, interface.writebufferlen ) -- new buffer + interface.writebufferlen = interface.writebufferlen - byte + if "wantread" == err then -- happens only with luasec + local callback = function( ) interface:_close() - interface.eventread = nil - return -1 + interface.eventwritetimeout = nil + return -1; end - else - interface.onincoming( interface, buffer, err ) -- send new data to listener - end - if interface.noreading then - interface.eventread = nil; - return -1; + interface.eventwritetimeout = addevent( base, nil, EV_TIMEOUT, callback, cfg.WRITE_TIMEOUT ) -- reg a new timeout event + debug( "wantread during write attempt, reg it in readcallback but dont know what really happens next..." ) + -- hopefully this works with luasec; its simply not possible to use 2 different write events on a socket in luaevent + return -1 end - return EV_READ, cfg.READ_TIMEOUT + return EV_WRITE, cfg.WRITE_TIMEOUT + else -- connection was closed during writing or fatal error + interface.fatalerror = err or "fatal error" + debug( "connection failed in write event:", interface.fatalerror ) + interface:_close() + interface.eventwrite = nil + return -1 end end + end - client:settimeout( 0 ) -- set non blocking - setmetatable(interface, interface_mt) - interfacelist( "add", interface ) -- add to interfacelist - return interface - end -end - -local handleserver -do - function handleserver( server, addr, port, pattern, listener, sslctx ) -- creates an server interface - debug "creating server interface..." - local interface = { - _connections = 0; - - conn = server; - onconnect = listener.onconnect; -- will be called when new client connected - eventread = false; -- read event handler - eventclose = false; -- close event handler - readcallback = false; -- read event callback - fatalerror = false; -- error message - nointerface = true; -- lock/unlock parameter - - _ip = addr, _port = port, _pattern = pattern, - _sslctx = sslctx; - } - interface.id = tostring(interface):match("%x+$"); - interface.readcallback = function( event ) -- server handler, called on incoming connections - --vdebug( "server can accept, id/addr/port:", interface, addr, port ) - if interface.fatalerror then - --vdebug( "leaving this event because:", self.fatalerror ) - interface.eventread = nil - return -1 - end - local delay = cfg.ACCEPT_DELAY - if EV_TIMEOUT == event then - if interface._connections >= cfg.MAX_CONNECTIONS then -- check connection count - debug( "to many connections, seconds to wait for next accept:", delay ) - return EV_TIMEOUT, delay -- timeout... - else - return EV_READ -- accept again - end + interface.readcallback = function( event ) -- called on read events + --vdebug( "new client read event, id/ip/port:", tostring(interface.id), tostring(ip), tostring(port) ) + if interface.noreading or interface.fatalerror then -- leave this event + --vdebug( "leaving this event because:", tostring(interface.noreading or interface.fatalerror) ) + interface.eventread = nil + return -1 + end + if EV_TIMEOUT == event and interface:onreadtimeout() ~= true then + return -1 -- took too long to get some data from client -> disconnect + end + if interface._usingssl then -- handle luasec + if interface.eventwritetimeout then -- ok, in the past writecallback was regged + local ret = interface.writecallback( ) -- call it + --vdebug( "tried to write in readcallback, result:", tostring(ret) ) end - --vdebug("max connection check ok, accepting...") - local client, err = server:accept() -- try to accept; TODO: check err - while client do - if interface._connections >= cfg.MAX_CONNECTIONS then - client:close( ) -- refuse connection - debug( "maximal connections reached, refuse client connection; accept delay:", delay ) - return EV_TIMEOUT, delay -- delay for next accept attempt - end - local client_ip, client_port = client:getpeername( ) - interface._connections = interface._connections + 1 -- increase connection count - local clientinterface = handleclient( client, client_ip, client_port, interface, pattern, listener, sslctx ) - --vdebug( "client id:", clientinterface, "startssl:", startssl ) - if ssl and sslctx then - clientinterface:starttls(sslctx, true) - else - clientinterface:_start_session( true ) - end - debug( "accepted incoming client connection from:", client_ip or "<unknown IP>", client_port or "<unknown port>", "to", port or "<unknown port>"); - - client, err = server:accept() -- try to accept again + if interface.eventreadtimeout then + interface.eventreadtimeout:close( ) + interface.eventreadtimeout = nil end - return EV_READ end - - server:settimeout( 0 ) - setmetatable(interface, interface_mt) - interfacelist( "add", interface ) - interface:_start_session() - return interface - end -end - -local addserver = ( function( ) - return function( addr, port, listener, pattern, sslcfg, startssl ) -- TODO: check arguments - --vdebug( "creating new tcp server with following parameters:", addr or "nil", port or "nil", sslcfg or "nil", startssl or "nil") - local server, err = socket.bind( addr, port, cfg.ACCEPT_QUEUE ) -- create server socket - if not server then - debug( "creating server socket on "..addr.." port "..port.." failed:", err ) - return nil, err + local buffer, err, part = interface.conn:receive( interface._pattern ) -- receive buffer with "pattern" + --vdebug( "read data:", tostring(buffer), "error:", tostring(err), "part:", tostring(part) ) + buffer = buffer or part + if buffer and #buffer > cfg.MAX_READ_LENGTH then -- check buffer length + interface.fatalerror = "receive buffer exceeded" + debug( "fatal error:", interface.fatalerror ) + interface:_close() + interface.eventread = nil + return -1 end - local sslctx - if sslcfg then - if not ssl then - debug "fatal error: luasec not found" - return nil, "luasec not found" - end - sslctx, err = sslcfg - if err then - debug( "error while creating new ssl context for server socket:", err ) - return nil, err + if err and ( err ~= "timeout" and err ~= "wantread" ) then + if "wantwrite" == err then -- need to read on write event + if not interface.eventwrite then -- register new write event if needed + interface.eventwrite = addevent( base, interface.conn, EV_WRITE, interface.writecallback, cfg.WRITE_TIMEOUT ) + end + interface.eventreadtimeout = addevent( base, nil, EV_TIMEOUT, + function( ) + interface:_close() + end, cfg.READ_TIMEOUT + ) + debug( "wantwrite during read attempt, reg it in writecallback but dont know what really happens next..." ) + -- to be honest i dont know what happens next, if it is allowed to first read, the write etc... + else -- connection was closed or fatal error + interface.fatalerror = err + debug( "connection failed in read event:", interface.fatalerror ) + interface:_close() + interface.eventread = nil + return -1 end + else + interface.onincoming( interface, buffer, err ) -- send new data to listener + end + if interface.noreading then + interface.eventread = nil; + return -1; end - local interface = handleserver( server, addr, port, pattern, listener, sslctx, startssl ) -- new server handler - debug( "new server created with id:", tostring(interface)) - return interface + return EV_READ, cfg.READ_TIMEOUT end -end )( ) -local addclient, wrapclient -do - function wrapclient( client, ip, port, listeners, pattern, sslctx ) - local interface = handleclient( client, ip, port, nil, pattern, listeners, sslctx ) - interface:_start_connection(sslctx) - return interface, client - --function handleclient( client, ip, port, server, pattern, listener, _, sslctx ) -- creates an client interface - end - - function addclient( addr, serverport, listener, pattern, localaddr, localport, sslcfg, startssl ) - local client, err = socket.tcp() -- creating new socket - if not client then - debug( "cannot create socket:", err ) - return nil, err + client:settimeout( 0 ) -- set non blocking + setmetatable(interface, interface_mt) + interfacelist[ interface ] = true -- add to interfacelist + return interface +end + +local function handleserver( server, addr, port, pattern, listener, sslctx ) -- creates an server interface + debug "creating server interface..." + local interface = { + _connections = 0; + + type = "server"; + conn = server; + onconnect = listener.onconnect; -- will be called when new client connected + eventread = false; -- read event handler + eventclose = false; -- close event handler + readcallback = false; -- read event callback + fatalerror = false; -- error message + nointerface = true; -- lock/unlock parameter + + _ip = addr, _port = port, _pattern = pattern, + _sslctx = sslctx; + } + interface.id = tostring(interface):match("%x+$"); + interface.readcallback = function( event ) -- server handler, called on incoming connections + --vdebug( "server can accept, id/addr/port:", interface, addr, port ) + if interface.fatalerror then + --vdebug( "leaving this event because:", self.fatalerror ) + interface.eventread = nil + return -1 end - client:settimeout( 0 ) -- set nonblocking - if localaddr then - local res, err = client:bind( localaddr, localport, -1 ) - if not res then - debug( "cannot bind client:", err ) - return nil, err + local delay = cfg.ACCEPT_DELAY + if EV_TIMEOUT == event then + if interface._connections >= cfg.MAX_CONNECTIONS then -- check connection count + debug( "to many connections, seconds to wait for next accept:", delay ) + return EV_TIMEOUT, delay -- timeout... + else + return EV_READ -- accept again end end - local sslctx - if sslcfg then -- handle ssl/new context - if not ssl then - debug "need luasec, but not available" - return nil, "luasec not found" + --vdebug("max connection check ok, accepting...") + local client, err = server:accept() -- try to accept; TODO: check err + while client do + if interface._connections >= cfg.MAX_CONNECTIONS then + client:close( ) -- refuse connection + debug( "maximal connections reached, refuse client connection; accept delay:", delay ) + return EV_TIMEOUT, delay -- delay for next accept attempt end - sslctx, err = sslcfg - if err then - debug( "cannot create new ssl context:", err ) - return nil, err + local client_ip, client_port = client:getpeername( ) + interface._connections = interface._connections + 1 -- increase connection count + local clientinterface = handleclient( client, client_ip, client_port, interface, pattern, listener, sslctx ) + --vdebug( "client id:", clientinterface, "startssl:", startssl ) + if has_luasec and sslctx then + clientinterface:starttls(sslctx, true) + else + clientinterface:_start_session( true ) end + debug( "accepted incoming client connection from:", client_ip or "<unknown IP>", client_port or "<unknown port>", "to", port or "<unknown port>"); + + client, err = server:accept() -- try to accept again end - local res, err = client:connect( addr, serverport ) -- connect - if res or ( err == "timeout" ) then - local ip, port = client:getsockname( ) - local interface = wrapclient( client, ip, serverport, listener, pattern, sslctx, startssl ) - interface:_start_connection( startssl ) - debug( "new connection id:", interface.id ) - return interface, err + return EV_READ + end + + server:settimeout( 0 ) + setmetatable(interface, interface_mt) + interfacelist[ interface ] = true + interface:_start_session() + return interface +end + +local function addserver( addr, port, listener, pattern, sslctx, startssl ) -- TODO: check arguments + --vdebug( "creating new tcp server with following parameters:", addr or "nil", port or "nil", sslctx or "nil", startssl or "nil") + if sslctx and not has_luasec then + debug "fatal error: luasec not found" + return nil, "luasec not found" + end + local server, err = socket.bind( addr, port, cfg.ACCEPT_QUEUE ) -- create server socket + if not server then + debug( "creating server socket on "..addr.." port "..port.." failed:", err ) + return nil, err + end + local interface = handleserver( server, addr, port, pattern, listener, sslctx, startssl ) -- new server handler + debug( "new server created with id:", tostring(interface)) + return interface +end + +local function wrapclient( client, ip, port, listeners, pattern, sslctx ) + local interface = handleclient( client, ip, port, nil, pattern, listeners, sslctx ) + interface:_start_connection(sslctx) + return interface, client + --function handleclient( client, ip, port, server, pattern, listener, _, sslctx ) -- creates an client interface +end + +local function addclient( addr, serverport, listener, pattern, sslctx, typ ) + if sslctx and not has_luasec then + debug "need luasec, but not available" + return nil, "luasec not found" + end + if not typ then + local addrinfo, err = getaddrinfo(addr) + if not addrinfo then return nil, err end + if addrinfo[1] and addrinfo[1].family == "inet6" then + typ = "tcp6" else - debug( "new connection failed:", err ) - return nil, err + typ = "tcp" end end + local create = socket[typ] + if type( create ) ~= "function" then + return nil, "invalid socket type" + end + local client, err = create() -- creating new socket + if not client then + debug( "cannot create socket:", err ) + return nil, err + end + client:settimeout( 0 ) -- set nonblocking + local res, err = client:connect( addr, serverport ) -- connect + if res or ( err == "timeout" ) then + local ip, port = client:getsockname( ) + local interface = wrapclient( client, ip, serverport, listener, pattern, sslctx ) + interface:_start_connection( sslctx ) + debug( "new connection id:", interface.id ) + return interface, err + else + debug( "new connection failed:", err ) + return nil, err + end end - -local loop = function( ) -- starts the event loop +local function loop( ) -- starts the event loop base:loop( ) return "quitting"; end -local newevent = ( function( ) - local add = base.addevent - return function( ... ) - return add( base, ... ) - end -end )( ) +local function newevent( ... ) + return addevent( base, ... ) +end -local closeallservers = function( arg ) - for _, item in ipairs( interfacelist( ) ) do +local function closeallservers ( arg ) + for item in pairs( interfacelist ) do if item.type == "server" then item:close( arg ) end @@ -811,9 +753,9 @@ end local function setquitting(yes) if yes then - -- Quit now - closeallservers(); - base:loopexit(); + -- Quit now + closeallservers(); + base:loopexit(); end end @@ -825,7 +767,7 @@ end -- being garbage-collected local signal_events = {}; -- [signal_num] -> event object local function hook_signal(signal_num, handler) - local function _handler(event) + local function _handler() local ret = handler(); if ret ~= false then -- Continue handling this signal? return EV_SIGNAL; -- Yes @@ -838,14 +780,14 @@ end local function link(sender, receiver, buffersize) local sender_locked; - + function receiver:ondrain() if sender_locked then sender:resume(); sender_locked = nil; end end - + function sender:onincoming(data) receiver:write(data); if receiver.writebufferlen >= buffersize then @@ -857,12 +799,11 @@ local function link(sender, receiver, buffersize) end return { - cfg = cfg, base = base, loop = loop, link = link, - event = event, + event = levent, event_base = base, addevent = newevent, addserver = addserver, diff --git a/net/server_select.lua b/net/server_select.lua index c50a6ce1..52a0d5f1 100644 --- a/net/server_select.lua +++ b/net/server_select.lua @@ -1,7 +1,7 @@ --- +-- -- 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 -- @@ -48,13 +48,14 @@ local coroutine_yield = coroutine.yield --// extern libs //-- -local luasec = use "ssl" +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 = ( luasec and luasec.wrap ) +local ssl_wrap = ( has_luasec and luasec.wrap ) local socket_bind = luasocket.bind local socket_sleep = luasocket.sleep local socket_select = luasocket.select @@ -149,7 +150,7 @@ _accepretry = 10 -- seconds to wait until the next attempt of a full server to a _maxsendlen = 51000 * 1024 -- max len of send buffer _maxreadlen = 25000 * 1024 -- max len of read buffer -_checkinterval = 1200000 -- interval in secs to check idle clients +_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 @@ -295,6 +296,7 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport local status = listeners.onstatus local disconnect = listeners.ondisconnect local drain = listeners.ondrain + local onreadtimeout = listeners.onreadtimeout; local detach = listeners.ondetach local bufferqueue = { } -- buffer array @@ -324,6 +326,8 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport 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 @@ -332,6 +336,7 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport disconnect = listeners.ondisconnect status = listeners.onstatus drain = listeners.ondrain + handler.onreadtimeout = listeners.onreadtimeout detach = listeners.ondetach end handler.getstats = function( ) @@ -404,6 +409,9 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport 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 @@ -575,6 +583,9 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport _ = 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 @@ -592,13 +603,14 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport coroutine_yield( ) -- handshake not finished end end - out_put( "server.lua: ssl handshake error: ", tostring(err or "handshake too long") ) - _ = handler and handler:force_close("ssl handshake failed") + 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 luasec then + if has_luasec then handler.starttls = function( self, _sslctx) if _sslctx then handler:set_sslctx(_sslctx); @@ -624,7 +636,7 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport 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 ) @@ -651,7 +663,7 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport _socketlist[ socket ] = handler _readlistlen = addsocket(_readlist, socket, _readlistlen) - if sslctx and luasec then + if sslctx and has_luasec then out_put "server.lua: auto-starting ssl negotiation..." handler.autostart_ssl = true; local ok, err = handler:starttls(sslctx); @@ -712,7 +724,7 @@ local function link(sender, receiver, buffersize) sender_locked = nil; end end - + local _readbuffer = sender.readbuffer; function sender.readbuffer() _readbuffer(); @@ -727,22 +739,23 @@ 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" - end - if type( port ) ~= "number" or not ( port >= 0 and port <= 65535 ) then + 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 luasec then + 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 - addr = addr or "*" local server, err = socket_bind( addr, port, _tcpbacklog ) if err then out_error( "server.lua, [", addr, "]:", port, ": ", err ) @@ -883,16 +896,18 @@ loop = function(once) -- this is the main loop of the program _starttime = _currenttime for handler, timestamp in pairs( _writetimes ) do if os_difftime( _currenttime - timestamp ) > _sendtimeout then - --_writetimes[ handler ] = nil handler.disconnect( )( handler, "send timeout" ) handler:force_close() -- forced disconnect end end for handler, timestamp in pairs( _readtimes ) do if os_difftime( _currenttime - timestamp ) > _readtimeout then - --_readtimes[ handler ] = nil - handler.disconnect( )( handler, "read timeout" ) - handler:close( ) -- forced disconnect? + 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 @@ -920,6 +935,7 @@ loop = function(once) -- this is the main loop of the program socket_sleep( _sleeptime ) until quitting; if once and quitting == "once" then quitting = nil; return; end + closeall(); return "quitting" end @@ -952,17 +968,46 @@ local wrapclient = function( socket, ip, serverport, listeners, pattern, sslctx return handler, socket end -local addclient = function( address, port, listeners, pattern, sslctx ) - local client, err = luasocket.tcp( ) +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 ) - _, err = client:connect( address, port ) - if err then -- try again - local handler = wrapclient( client, address, port, listeners ) + local ok, err = client:connect( address, port ) + if ok or err == "timeout" then + return wrapclient( client, address, port, listeners, pattern, sslctx ) else - wrapconnection( nil, listeners, client, address, port, "clientport", pattern, sslctx ) + return nil, err end end @@ -992,7 +1037,7 @@ return { addclient = addclient, wrapclient = wrapclient, - + loop = loop, link = link, step = step, diff --git a/net/websocket.lua b/net/websocket.lua new file mode 100644 index 00000000..a4274eec --- /dev/null +++ b/net/websocket.lua @@ -0,0 +1,272 @@ +-- Prosody IM +-- Copyright (C) 2012 Florian Zeitz +-- Copyright (C) 2014 Daurnimator +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local t_concat = table.concat; + +local http = require "net.http"; +local frames = require "net.websocket.frames"; +local base64 = require "util.encodings".base64; +local sha1 = require "util.hashes".sha1; +local random_bytes = require "util.random".bytes; +local timer = require "util.timer"; +local log = require "util.logger".init "websocket"; + +local close_timeout = 3; -- Seconds to wait after sending close frame until closing connection. + +local websockets = {}; + +local websocket_listeners = {}; +function websocket_listeners.ondisconnect(handler, err) + local s = websockets[handler]; + websockets[handler] = nil; + if s.close_timer then + timer.stop(s.close_timer); + s.close_timer = nil; + end + s.readyState = 3; + if s.close_code == nil and s.onerror then s:onerror(err); end + if s.onclose then s:onclose(s.close_code, s.close_message or err); end +end + +function websocket_listeners.ondetach(handler) + websockets[handler] = nil; +end + +local function fail(s, code, reason) + module:log("warn", "WebSocket connection failed, closing. %d %s", code, reason); + s:close(code, reason); + s.handler:close(); + return false +end + +function websocket_listeners.onincoming(handler, buffer, err) + local s = websockets[handler]; + s.readbuffer = s.readbuffer..buffer; + while true do + local frame, len = frames.parse(s.readbuffer); + if frame == nil then break end + s.readbuffer = s.readbuffer:sub(len+1); + + log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data); + + -- Error cases + if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero + return fail(s, 1002, "Reserved bits not zero"); + end + + if frame.opcode < 0x8 then + local databuffer = s.databuffer; + if frame.opcode == 0x0 then -- Continuation frames + if not databuffer then + return fail(s, 1002, "Unexpected continuation frame"); + end + databuffer[#databuffer+1] = frame.data; + elseif frame.opcode == 0x1 or frame.opcode == 0x2 then -- Text or Binary frame + if databuffer then + return fail(s, 1002, "Continuation frame expected"); + end + databuffer = {type=frame.opcode, frame.data}; + s.databuffer = databuffer; + else + return fail(s, 1002, "Reserved opcode"); + end + if frame.FIN then + s.databuffer = nil; + if s.onmessage then + s:onmessage(t_concat(databuffer), databuffer.type); + end + end + else -- Control frame + if frame.length > 125 then -- Control frame with too much payload + return fail(s, 1002, "Payload too large"); + elseif not frame.FIN then -- Fragmented control frame + return fail(s, 1002, "Fragmented control frame"); + end + if frame.opcode == 0x8 then -- Close request + if frame.length == 1 then + return fail(s, 1002, "Close frame with payload, but too short for status code"); + end + local status_code, message = frames.parse_close(frame.data); + if status_code == nil then + --[[ RFC 6455 7.4.1 + 1005 is a reserved value and MUST NOT be set as a status code in a + Close control frame by an endpoint. It is designated for use in + applications expecting a status code to indicate that no status + code was actually present. + ]] + status_code = 1005 + elseif status_code < 1000 then + return fail(s, 1002, "Closed with invalid status code"); + elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then + return fail(s, 1002, "Closed with reserved status code"); + end + s.close_code, s.close_message = status_code, message; + s:close(1000); + return true; + elseif frame.opcode == 0x9 then -- Ping frame + frame.opcode = 0xA; + frame.MASK = true; -- RFC 6455 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked + handler:write(frames.build(frame)); + elseif frame.opcode == 0xA then -- Pong frame + log("debug", "Received unexpected pong frame: " .. tostring(frame.data)); + else + return fail(s, 1002, "Reserved opcode"); + end + end + end + return true; +end + +local websocket_methods = {}; +local function close_timeout_cb(now, timerid, s) + s.close_timer = nil; + log("warn", "Close timeout waiting for server to close, closing manually."); + s.handler:close(); +end +function websocket_methods:close(code, reason) + if self.readyState < 2 then + code = code or 1000; + log("debug", "closing WebSocket with code %i: %s" , code , tostring(reason)); + self.readyState = 2; + local handler = self.handler; + handler:write(frames.build_close(code, reason, true)); + -- Do not close socket straight away, wait for acknowledgement from server. + self.close_timer = timer.add_task(close_timeout, close_timeout_cb, self); + elseif self.readyState == 2 then + log("debug", "tried to close a closing WebSocket, closing the raw socket."); + -- Stop timer + if self.close_timer then + timer.stop(self.close_timer); + self.close_timer = nil; + end + local handler = self.handler; + handler:close(); + else + log("debug", "tried to close a closed WebSocket, ignoring."); + end +end +function websocket_methods:send(data, opcode) + if self.readyState < 1 then + return nil, "WebSocket not open yet, unable to send data."; + elseif self.readyState >= 2 then + return nil, "WebSocket closed, unable to send data."; + end + if opcode == "text" or opcode == nil then + opcode = 0x1; + elseif opcode == "binary" then + opcode = 0x2; + end + local frame = { + FIN = true; + MASK = true; -- RFC 6455 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked + opcode = opcode; + data = tostring(data); + }; + log("debug", "WebSocket sending frame: opcode=%0x, %i bytes", frame.opcode, #frame.data); + return self.handler:write(frames.build(frame)); +end + +local websocket_metatable = { + __index = websocket_methods; +}; + +local function connect(url, ex, listeners) + ex = ex or {}; + + --[[RFC 6455 4.1.7: + The request MUST include a header field with the name + |Sec-WebSocket-Key|. The value of this header field MUST be a + nonce consisting of a randomly selected 16-byte value that has + been base64-encoded (see Section 4 of [RFC4648]). The nonce + MUST be selected randomly for each connection. + ]] + local key = base64.encode(random_bytes(16)); + + -- Either a single protocol string or an array of protocol strings. + local protocol = ex.protocol; + if type(protocol) == "string" then + protocol = { protocol, [protocol] = true }; + elseif type(protocol) == "table" and protocol[1] then + for _, v in ipairs(protocol) do + protocol[v] = true; + end + else + protocol = nil; + end + + local headers = { + ["Upgrade"] = "websocket"; + ["Connection"] = "Upgrade"; + ["Sec-WebSocket-Key"] = key; + ["Sec-WebSocket-Protocol"] = protocol and t_concat(protocol, ", "); + ["Sec-WebSocket-Version"] = "13"; + ["Sec-WebSocket-Extensions"] = ex.extensions; + } + if ex.headers then + for k,v in pairs(ex.headers) do + headers[k] = v; + end + end + + local s = setmetatable({ + readbuffer = ""; + databuffer = nil; + handler = nil; + close_code = nil; + close_message = nil; + close_timer = nil; + readyState = 0; + protocol = nil; + + url = url; + + onopen = listeners.onopen; + onclose = listeners.onclose; + onmessage = listeners.onmessage; + onerror = listeners.onerror; + }, websocket_metatable); + + local http_url = url:gsub("^(ws)", "http"); + local http_req = http.request(http_url, { + method = "GET"; + headers = headers; + sslctx = ex.sslctx; + }, function(b, c, r, http_req) + if c ~= 101 + or r.headers["connection"]:lower() ~= "upgrade" + or r.headers["upgrade"] ~= "websocket" + or r.headers["sec-websocket-accept"] ~= base64.encode(sha1(key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) + or (protocol and not protocol[r.headers["sec-websocket-protocol"]]) + then + s.readyState = 3; + log("warn", "WebSocket connection to %s failed: %s", url, tostring(b)); + if s.onerror then s:onerror("connecting-failed"); end + return; + end + + s.protocol = r.headers["sec-websocket-protocol"]; + + -- Take possession of socket from http + http_req.conn = nil; + local handler = http_req.handler; + s.handler = handler; + websockets[handler] = s; + handler:setlistener(websocket_listeners); + + log("debug", "WebSocket connected successfully to %s", url); + s.readyState = 1; + if s.onopen then s:onopen(); end + websocket_listeners.onincoming(handler, b); + end); + + return s; +end + +return { + connect = connect; +}; diff --git a/net/websocket/frames.lua b/net/websocket/frames.lua new file mode 100644 index 00000000..737f46bb --- /dev/null +++ b/net/websocket/frames.lua @@ -0,0 +1,220 @@ +-- Prosody IM +-- Copyright (C) 2012 Florian Zeitz +-- Copyright (C) 2014 Daurnimator +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local softreq = require "util.dependencies".softreq; +local log = require "util.logger".init "websocket.frames"; +local random_bytes = require "util.random".bytes; + +local bit = assert(softreq"bit" or softreq"bit32", + "No bit module found. See https://prosody.im/doc/depends#bitop"); +local band = bit.band; +local bor = bit.bor; +local bxor = bit.bxor; +local lshift = bit.lshift; +local rshift = bit.rshift; + +local t_concat = table.concat; +local s_byte = string.byte; +local s_char= string.char; +local s_sub = string.sub; +local s_pack = string.pack; +local s_unpack = string.unpack; + +if not s_pack and softreq"struct" then + s_pack = softreq"struct".pack; + s_unpack = softreq"struct".unpack; +end + +local function read_uint16be(str, pos) + local l1, l2 = s_byte(str, pos, pos+1); + return l1*256 + l2; +end +-- FIXME: this may lose precision +local function read_uint64be(str, pos) + local l1, l2, l3, l4, l5, l6, l7, l8 = s_byte(str, pos, pos+7); + local h = lshift(l1, 24) + lshift(l2, 16) + lshift(l3, 8) + l4; + local l = lshift(l5, 24) + lshift(l6, 16) + lshift(l7, 8) + l8; + return h * 2^32 + l; +end +local function pack_uint16be(x) + return s_char(rshift(x, 8), band(x, 0xFF)); +end +local function get_byte(x, n) + return band(rshift(x, n), 0xFF); +end +local function pack_uint64be(x) + local h = band(x / 2^32, 2^32-1); + return s_char(get_byte(h, 24), get_byte(h, 16), get_byte(h, 8), band(h, 0xFF), + get_byte(x, 24), get_byte(x, 16), get_byte(x, 8), band(x, 0xFF)); +end + +if s_pack then + function pack_uint16be(x) + return s_pack(">I2", x); + end + function pack_uint64be(x) + return s_pack(">I8", x); + end +end + +if s_unpack then + function read_uint16be(str, pos) + return s_unpack(">I2", str, pos); + end + function read_uint64be(str, pos) + return s_unpack(">I8", str, pos); + end +end + +local function parse_frame_header(frame) + if #frame < 2 then return; end + + local byte1, byte2 = s_byte(frame, 1, 2); + local result = { + FIN = band(byte1, 0x80) > 0; + RSV1 = band(byte1, 0x40) > 0; + RSV2 = band(byte1, 0x20) > 0; + RSV3 = band(byte1, 0x10) > 0; + opcode = band(byte1, 0x0F); + + MASK = band(byte2, 0x80) > 0; + length = band(byte2, 0x7F); + }; + + local length_bytes = 0; + if result.length == 126 then + length_bytes = 2; + elseif result.length == 127 then + length_bytes = 8; + end + + local header_length = 2 + length_bytes + (result.MASK and 4 or 0); + if #frame < header_length then return; end + + if length_bytes == 2 then + result.length = read_uint16be(frame, 3); + elseif length_bytes == 8 then + result.length = read_uint64be(frame, 3); + end + + if result.MASK then + result.key = { s_byte(frame, length_bytes+3, length_bytes+6) }; + end + + return result, header_length; +end + +-- XORs the string `str` with the array of bytes `key` +-- TODO: optimize +local function apply_mask(str, key, from, to) + from = from or 1 + if from < 0 then from = #str + from + 1 end -- negative indicies + to = to or #str + if to < 0 then to = #str + to + 1 end -- negative indicies + local key_len = #key + local counter = 0; + local data = {}; + for i = from, to do + local key_index = counter%key_len + 1; + counter = counter + 1; + data[counter] = s_char(bxor(key[key_index], s_byte(str, i))); + end + return t_concat(data); +end + +local function parse_frame_body(frame, header, pos) + if header.MASK then + return apply_mask(frame, header.key, pos, pos + header.length - 1); + else + return frame:sub(pos, pos + header.length - 1); + end +end + +local function parse_frame(frame) + local result, pos = parse_frame_header(frame); + if result == nil or #frame < (pos + result.length) then return; end + result.data = parse_frame_body(frame, result, pos+1); + return result, pos + result.length; +end + +local function build_frame(desc) + local data = desc.data or ""; + + assert(desc.opcode and desc.opcode >= 0 and desc.opcode <= 0xF, "Invalid WebSocket opcode"); + if desc.opcode >= 0x8 then + -- RFC 6455 5.5 + assert(#data <= 125, "WebSocket control frames MUST have a payload length of 125 bytes or less."); + end + + local b1 = bor(desc.opcode, + desc.FIN and 0x80 or 0, + desc.RSV1 and 0x40 or 0, + desc.RSV2 and 0x20 or 0, + desc.RSV3 and 0x10 or 0); + + local b2 = #data; + local length_extra; + if b2 <= 125 then -- 7-bit length + length_extra = ""; + elseif b2 <= 0xFFFF then -- 2-byte length + b2 = 126; + length_extra = pack_uint16be(#data); + else -- 8-byte length + b2 = 127; + length_extra = pack_uint64be(#data); + end + + local key = "" + if desc.MASK then + local key_a = desc.key + if key_a then + key = s_char(unpack(key_a, 1, 4)); + else + key = random_bytes(4); + key_a = {key:byte(1,4)}; + end + b2 = bor(b2, 0x80); + data = apply_mask(data, key_a); + end + + return s_char(b1, b2) .. length_extra .. key .. data +end + +local function parse_close(data) + local code, message + if #data >= 2 then + code = read_uint16be(data, 1); + if #data > 2 then + message = s_sub(data, 3); + end + end + return code, message +end + +local function build_close(code, message, mask) + local data = pack_uint16be(code); + if message then + assert(#message<=123, "Close reason must be <=123 bytes"); + data = data .. message; + end + return build_frame({ + opcode = 0x8; + FIN = true; + MASK = mask; + data = data; + }); +end + +return { + parse_header = parse_frame_header; + parse_body = parse_frame_body; + parse = parse_frame; + build = build_frame; + parse_close = parse_close; + build_close = build_close; +}; diff --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua index b544ddc8..5c90c91b 100644 --- a/plugins/adhoc/adhoc.lib.lua +++ b/plugins/adhoc/adhoc.lib.lua @@ -25,12 +25,13 @@ function _M.new(name, node, handler, permission) end function _M.handle_cmd(command, origin, stanza) - local sessionid = stanza.tags[1].attr.sessionid or uuid.generate(); + local cmdtag = stanza.tags[1] + local sessionid = cmdtag.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"); + dataIn.action = cmdtag.attr.action or "execute"; + dataIn.form = cmdtag:get_child("x", "jabber:x:data"); local data, state = command:handler(dataIn, states[sessionid]); states[sessionid] = state; diff --git a/plugins/adhoc/mod_adhoc.lua b/plugins/adhoc/mod_adhoc.lua index 69b2c8da..1c956021 100644 --- a/plugins/adhoc/mod_adhoc.lua +++ b/plugins/adhoc/mod_adhoc.lua @@ -6,86 +6,90 @@ -- local st = require "util.stanza"; +local keys = require "util.iterators".keys; +local array_collect = require "util.array".collect; local is_admin = require "core.usermanager".is_admin; +local jid_split = require "util.jid".split; local adhoc_handle_cmd = module:require "adhoc".handle_cmd; local xmlns_cmd = "http://jabber.org/protocol/commands"; -local xmlns_disco = "http://jabber.org/protocol/disco"; local commands = {}; module:add_feature(xmlns_cmd); -module:hook("iq/host/"..xmlns_disco.."#info:query", function (event) - local origin, stanza = event.origin, event.stanza; - local node = stanza.tags[1].attr.node; - if stanza.attr.type == "get" and node then - if commands[node] then - local privileged = is_admin(stanza.attr.from, stanza.attr.to); - if (commands[node].permission == "admin" and privileged) - or (commands[node].permission == "user") then - reply = st.reply(stanza); - reply:tag("query", { xmlns = xmlns_disco.."#info", - node = node }); - reply:tag("identity", { name = commands[node].name, - category = "automation", type = "command-node" }):up(); - reply:tag("feature", { var = xmlns_cmd }):up(); - reply:tag("feature", { var = "jabber:x:data" }):up(); - else - reply = st.error_reply(stanza, "auth", "forbidden", "This item is not available to you"); - end - origin.send(reply); - return true; - elseif node == xmlns_cmd then - reply = st.reply(stanza); - reply:tag("query", { xmlns = xmlns_disco.."#info", - node = node }); - reply:tag("identity", { name = "Ad-Hoc Commands", - category = "automation", type = "command-list" }):up(); - origin.send(reply); +module:hook("host-disco-info-node", function (event) + local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; + if commands[node] then + local from = stanza.attr.from; + local privileged = is_admin(from, stanza.attr.to); + local global_admin = is_admin(from); + local username, hostname = jid_split(from); + local command = commands[node]; + if (command.permission == "admin" and privileged) + or (command.permission == "global_admin" and global_admin) + or (command.permission == "local_user" and hostname == module.host) + or (command.permission == "user") then + reply:tag("identity", { name = command.name, + category = "automation", type = "command-node" }):up(); + reply:tag("feature", { var = xmlns_cmd }):up(); + reply:tag("feature", { var = "jabber:x:data" }):up(); + event.exists = true; + else + origin.send(st.error_reply(stanza, "auth", "forbidden", "This item is not available to you")); return true; - end + elseif node == xmlns_cmd then + reply:tag("identity", { name = "Ad-Hoc Commands", + category = "automation", type = "command-list" }):up(); + event.exists = true; end end); -module:hook("iq/host/"..xmlns_disco.."#items:query", function (event) - local origin, stanza = event.origin, event.stanza; - if stanza.attr.type == "get" and stanza.tags[1].attr.node - and stanza.tags[1].attr.node == xmlns_cmd then - local admin = is_admin(stanza.attr.from, stanza.attr.to); - local global_admin = is_admin(stanza.attr.from); - reply = st.reply(stanza); - reply:tag("query", { xmlns = xmlns_disco.."#items", - node = xmlns_cmd }); - for node, command in pairs(commands) do - if (command.permission == "admin" and admin) - or (command.permission == "global_admin" and global_admin) - or (command.permission == "user") then - reply:tag("item", { name = command.name, - node = node, jid = module:get_host() }); - reply:up(); - end +module:hook("host-disco-items-node", function (event) + local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; + if node ~= xmlns_cmd then + return; + end + + local from = stanza.attr.from; + local admin = is_admin(from, stanza.attr.to); + local global_admin = is_admin(from); + local username, hostname = jid_split(from); + local nodes = array_collect(keys(commands)):sort(); + for _, node in ipairs(nodes) do + local command = commands[node]; + if (command.permission == "admin" and admin) + or (command.permission == "global_admin" and global_admin) + or (command.permission == "local_user" and hostname == module.host) + or (command.permission == "user") then + reply:tag("item", { name = command.name, + node = node, jid = module:get_host() }); + reply:up(); end - origin.send(reply); - return true; end -end, 500); + event.exists = true; +end); module:hook("iq/host/"..xmlns_cmd..":command", function (event) local origin, stanza = event.origin, event.stanza; if stanza.attr.type == "set" then local node = stanza.tags[1].attr.node - if commands[node] then - local admin = is_admin(stanza.attr.from, stanza.attr.to); - local global_admin = is_admin(stanza.attr.from); - if (commands[node].permission == "admin" and not admin) - or (commands[node].permission == "global_admin" and not global_admin) then + local command = commands[node]; + if command then + local from = stanza.attr.from; + local admin = is_admin(from, stanza.attr.to); + local global_admin = is_admin(from); + local username, hostname = jid_split(from); + if (command.permission == "admin" and not admin) + or (command.permission == "global_admin" and not global_admin) + or (command.permission == "local_user" and hostname ~= module.host) then origin.send(st.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up() :add_child(commands[node]: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(commands[node], origin, stanza); + adhoc_handle_cmd(commands[node], origin, stanza); + return true; end end end, 500); diff --git a/plugins/mod_admin_adhoc.lua b/plugins/mod_admin_adhoc.lua index 232fa5f7..392e715e 100644 --- a/plugins/mod_admin_adhoc.lua +++ b/plugins/mod_admin_adhoc.lua @@ -9,6 +9,7 @@ local _G = _G; local prosody = _G.prosody; local hosts = prosody.hosts; local t_concat = table.concat; +local t_sort = table.sort; local module_host = module:get_host(); @@ -25,10 +26,11 @@ local st, jid = require "util.stanza", require "util.jid"; local timer_add_task = require "util.timer".add_task; local dataforms_new = require "util.dataforms".new; local array = require "util.array"; -local modulemanager = require "modulemanager"; +local modulemanager = require "core.modulemanager"; local core_post_stanza = prosody.core_post_stanza; local adhoc_simple = require "util.adhoc".new_simple_form; local adhoc_initial = require "util.adhoc".new_initial_data_form; +local set = require"util.set"; module:depends("adhoc"); local adhoc_new = module:require "adhoc".new; @@ -245,7 +247,7 @@ local get_user_roster_handler = adhoc_simple(get_user_roster_layout, function(fi local query = st.stanza("query", { xmlns = "jabber:iq:roster" }); for jid in pairs(roster) do - if jid ~= "pending" and jid then + if jid then query:tag("item", { jid = jid, subscription = roster[jid].subscription, @@ -298,7 +300,7 @@ local get_user_stats_handler = adhoc_simple(get_user_stats_layout, function(fiel local IPs = ""; local resources = ""; for jid in pairs(roster) do - if jid ~= "pending" and jid then + if jid then rostersize = rostersize + 1; end end @@ -345,7 +347,7 @@ local get_online_users_command_handler = adhoc_simple(get_online_users_layout, f count = count + 1; if fields.details then for resource, session in pairs(user.sessions or {}) do - local status, priority = "unavailable", tostring(session.priority or "-"); + local status, priority, ip = "unavailable", tostring(session.priority or "-"), session.ip or "<unknown>"; if session.presence then status = session.presence:child_with_name("show"); if status then @@ -354,13 +356,92 @@ local get_online_users_command_handler = adhoc_simple(get_online_users_layout, f status = "available"; end end - users[#users+1] = " - "..resource..": "..status.."("..priority..")"; + users[#users+1] = " - "..resource..": "..status.."("..priority.."), IP: ["..ip.."]"; end end end return { status = "completed", result = {layout = get_online_users_result_layout, values = {onlineuserjids=t_concat(users, "\n")}} }; end); +-- Getting a list of S2S connections (this host) +local list_s2s_this_result = dataforms_new { + title = "List of S2S connections on this host"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/s2s#list" }; + { name = "sessions", type = "text-multi", label = "Connections:" }; + { name = "num_in", type = "text-single", label = "#incomming connections:" }; + { name = "num_out", type = "text-single", label = "#outgoing connections:" }; +}; + +local function session_flags(session, line) + line = line or {}; + + if session.id then + line[#line+1] = "["..session.id.."]" + else + line[#line+1] = "["..session.type..(tostring(session):match("%x*$")).."]" + end + + local flags = {}; + if session.cert_identity_status == "valid" then + flags[#flags+1] = "authenticated"; + end + if session.secure then + flags[#flags+1] = "encrypted"; + end + if session.compressed then + flags[#flags+1] = "compressed"; + end + if session.smacks then + flags[#flags+1] = "sm"; + end + if session.ip and session.ip:match(":") then + flags[#flags+1] = "IPv6"; + end + line[#line+1] = "("..t_concat(flags, ", ")..")"; + + return t_concat(line, " "); +end + +local function list_s2s_this_handler(self, data, state) + local count_in, count_out = 0, 0; + local s2s_list = {}; + + local s2s_sessions = module:shared"/*/s2s/sessions"; + for _, session in pairs(s2s_sessions) do + local remotehost, localhost, direction; + if session.direction == "outgoing" then + direction = "->"; + count_out = count_out + 1; + remotehost, localhost = session.to_host or "?", session.from_host or "?"; + else + direction = "<-"; + count_in = count_in + 1; + remotehost, localhost = session.from_host or "?", session.to_host or "?"; + end + local sess_lines = { r = remotehost, + session_flags(session, { "", direction, remotehost or "?" })}; + + if localhost == module_host then + s2s_list[#s2s_list+1] = sess_lines; + end + end + + t_sort(s2s_list, function(a, b) + return a.r < b.r; + end); + + for i, sess_lines in ipairs(s2s_list) do + s2s_list[i] = sess_lines[1]; + end + + return { status = "completed", result = { layout = list_s2s_this_result; values = { + sessions = t_concat(s2s_list, "\n"), + num_in = tostring(count_in), + num_out = tostring(count_out) + } } }; +end + -- Getting a list of loaded modules local list_modules_result = dataforms_new { title = "List of loaded modules"; @@ -489,7 +570,7 @@ local globally_reload_module_handler = adhoc_initial(globally_reload_module_layo for _, host in pairs(hosts) do loaded_modules:append(array(keys(host.modules))); end - loaded_modules = array(keys(set.new(loaded_modules):items())):sort(); + loaded_modules = array(set.new(loaded_modules):items()):sort(); return { module = loaded_modules }; end, function(fields, err) local is_global = false; @@ -533,6 +614,7 @@ end, function(fields, err) end); local function send_to_online(message, server) + local sessions; if server then sessions = { [server] = hosts[server] }; else @@ -631,7 +713,7 @@ local globally_unload_module_handler = adhoc_initial(globally_unload_module_layo for _, host in pairs(hosts) do loaded_modules:append(array(keys(host.modules))); end - loaded_modules = array(keys(set.new(loaded_modules):items())):sort(); + loaded_modules = array(set.new(loaded_modules):items()):sort(); return { module = loaded_modules }; end, function(fields, err) local is_global = false; @@ -727,6 +809,7 @@ local get_user_password_desc = adhoc_new("Get User Password", "http://jabber.org local get_user_roster_desc = adhoc_new("Get User Roster","http://jabber.org/protocol/admin#get-user-roster", get_user_roster_handler, "admin"); local get_user_stats_desc = adhoc_new("Get User Statistics","http://jabber.org/protocol/admin#user-stats", get_user_stats_handler, "admin"); local get_online_users_desc = adhoc_new("Get List of Online Users", "http://jabber.org/protocol/admin#get-online-users-list", get_online_users_command_handler, "admin"); +local list_s2s_this_desc = adhoc_new("List S2S connections", "http://prosody.im/protocol/s2s#list", list_s2s_this_handler, "admin"); local list_modules_desc = adhoc_new("List loaded modules", "http://prosody.im/protocol/modules#list", list_modules_handler, "admin"); local load_module_desc = adhoc_new("Load module", "http://prosody.im/protocol/modules#load", load_module_handler, "admin"); local globally_load_module_desc = adhoc_new("Globally load module", "http://prosody.im/protocol/modules#global-load", globally_load_module_handler, "global_admin"); @@ -747,6 +830,7 @@ module:provides("adhoc", get_user_password_desc); module:provides("adhoc", get_user_roster_desc); module:provides("adhoc", get_user_stats_desc); module:provides("adhoc", get_online_users_desc); +module:provides("adhoc", list_s2s_this_desc); module:provides("adhoc", list_modules_desc); module:provides("adhoc", load_module_desc); module:provides("adhoc", globally_load_module_desc); diff --git a/plugins/mod_admin_telnet.lua b/plugins/mod_admin_telnet.lua index 86403606..9dfbbc7a 100644 --- a/plugins/mod_admin_telnet.lua +++ b/plugins/mod_admin_telnet.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -17,17 +17,17 @@ local _G = _G; local prosody = _G.prosody; local hosts = prosody.hosts; -local incoming_s2s = prosody.incoming_s2s; local console_listener = { default_port = 5582; default_mode = "*a"; interface = "127.0.0.1" }; local iterators = require "util.iterators"; local keys, values = iterators.keys, iterators.values; -local jid_bare, jid_split = import("util.jid", "bare", "prepped_split"); +local jid_bare, jid_split, jid_join = import("util.jid", "bare", "prepped_split", "join"); local set, array = require "util.set", require "util.array"; local cert_verify_identity = require "util.x509".verify_identity; local envload = require "util.envload".envload; local envloadfile = require "util.envload".envloadfile; +local has_pposix, pposix = pcall(require, "util.pposix"); local commands = module:shared("commands") local def_env = module:shared("env"); @@ -60,20 +60,20 @@ function console:new_session(conn) disconnect = function () conn:close(); end; }; session.env = setmetatable({}, default_env_mt); - + -- Load up environment with helper objects for name, t in pairs(def_env) do if type(t) == "table" then session.env[name] = setmetatable({ session = session }, { __index = t }); end end - + return session; end function console:process_line(session, line) local useglobalenv; - + if line:match("^>") then line = line:gsub("^>", ""); useglobalenv = true; @@ -87,9 +87,9 @@ function console:process_line(session, line) return; end end - + session.env._ = line; - + local chunkname = "=console"; local env = (useglobalenv and redirect_output(_G, session)) or session.env or nil local chunk, err = envload("return "..line, chunkname, env); @@ -103,20 +103,20 @@ function console:process_line(session, line) return; end end - + local ranok, taskok, message = pcall(chunk); - + if not (ranok or message or useglobalenv) and commands[line:lower()] then commands[line:lower()](session, line); return; end - + if not ranok then session.print("Fatal error while running command, it did not complete"); session.print("Error: "..taskok); return; end - + if not message then session.print("Result: "..tostring(taskok)); return; @@ -125,7 +125,7 @@ function console:process_line(session, line) session.print("Message: "..tostring(message)); return; end - + session.print("OK: "..tostring(message)); end @@ -155,6 +155,14 @@ function console_listener.onincoming(conn, data) session.partial_data = data:match("[^\n]+$"); end +function console_listener.onreadtimeout(conn) + local session = sessions[conn]; + if session then + session.send("\0"); + return true; + end +end + function console_listener.ondisconnect(conn, err) local session = sessions[conn]; if session then @@ -217,9 +225,11 @@ function commands.help(session, data) print [[c2s:show(jid) - Show all client sessions with the specified JID (or all if no JID given)]] print [[c2s:show_insecure() - Show all unencrypted client connections]] print [[c2s:show_secure() - Show all encrypted client connections]] + print [[c2s:show_tls() - Show TLS cipher info for encrypted sessions]] print [[c2s:close(jid) - Close all sessions for the specified JID]] elseif section == "s2s" then print [[s2s:show(domain) - Show all s2s connections for the given domain (or all if no domain given)]] + print [[s2s:show_tls(domain) - Show TLS cipher info for encrypted sessions]] print [[s2s:close(from, to) - Close a connection from one domain to another]] print [[s2s:closeall(host) - Close all the incoming/outgoing s2s sessions to specified host]] elseif section == "module" then @@ -272,6 +282,8 @@ end -- Session environment -- -- Anything in def_env will be accessible within the session as a global variable +--luacheck: ignore 212/self + def_env.server = {}; function def_env.server:insane_reload() @@ -313,8 +325,7 @@ local function human(kb) end function def_env.server:memory() - local pposix = require("util.pposix"); - if not pposix.meminfo then + if not has_pposix or not pposix.meminfo then return true, "Lua is using "..collectgarbage("count"); end local mem, lua_mem = pposix.meminfo(), collectgarbage("count"); @@ -337,10 +348,9 @@ local function get_hosts_set(hosts, module) elseif type(hosts) == "string" then return set.new { hosts }; elseif hosts == nil then - local mm = require "modulemanager"; local hosts_set = set.new(array.collect(keys(prosody.hosts))) - / function (host) return (prosody.hosts[host].type == "local" or module and mm.is_loaded(host, module)) and host or nil; end; - if module and mm.get_module("*", module) then + / function (host) return (prosody.hosts[host].type == "local" or module and modulemanager.is_loaded(host, module)) and host or nil; end; + if module and modulemanager.get_module("*", module) then hosts_set:add("*"); end return hosts_set; @@ -348,15 +358,13 @@ local function get_hosts_set(hosts, module) end function def_env.module:load(name, hosts, config) - local mm = require "modulemanager"; - hosts = get_hosts_set(hosts); - + -- Load the module for each host local ok, err, count, mod = true, nil, 0, nil; for host in hosts do - if (not mm.is_loaded(host, name)) then - mod, err = mm.load(host, name, config); + if (not modulemanager.is_loaded(host, name)) then + mod, err = modulemanager.load(host, name, config); if not mod then ok = false; if err == "global-module-already-loaded" then @@ -372,20 +380,18 @@ function def_env.module:load(name, hosts, config) end end end - - return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err)); + + return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err)); end function def_env.module:unload(name, hosts) - local mm = require "modulemanager"; - hosts = get_hosts_set(hosts, name); - + -- Unload the module for each host local ok, err, count = true, nil, 0; for host in hosts do - if mm.is_loaded(host, name) then - ok, err = mm.unload(host, name); + if modulemanager.is_loaded(host, name) then + ok, err = modulemanager.unload(host, name); if not ok then ok = false; self.session.print(err or "Unknown error unloading module"); @@ -399,8 +405,6 @@ function def_env.module:unload(name, hosts) end function def_env.module:reload(name, hosts) - local mm = require "modulemanager"; - hosts = array.collect(get_hosts_set(hosts, name)):sort(function (a, b) if a == "*" then return true elseif b == "*" then return false @@ -410,8 +414,8 @@ function def_env.module:reload(name, hosts) -- Reload the module for each host local ok, err, count = true, nil, 0; for _, host in ipairs(hosts) do - if mm.is_loaded(host, name) then - ok, err = mm.reload(host, name); + if modulemanager.is_loaded(host, name) then + ok, err = modulemanager.reload(host, name); if not ok then ok = false; self.session.print(err or "Unknown error reloading module"); @@ -438,7 +442,7 @@ function def_env.module:list(hosts) if type(hosts) ~= "table" then return false, "Please supply a host or a list of hosts you would like to see"; end - + local print = self.session.print; for _, host in ipairs(hosts) do print((host == "*" and "Global" or host)..":"); @@ -477,61 +481,109 @@ function def_env.config:reload() return ok, (ok and "Config reloaded (you may need to reload modules to take effect)") or tostring(err); end -def_env.hosts = {}; -function def_env.hosts:list() - for host, host_session in pairs(hosts) do - self.session.print(host); +local function common_info(session, line) + if session.id then + line[#line+1] = "["..session.id.."]" + else + line[#line+1] = "["..session.type..(tostring(session):match("%x*$")).."]" end - return true, "Done"; end -function def_env.hosts:add(name) +local function session_flags(session, line) + line = line or {}; + common_info(session, line); + if session.type == "c2s" then + local status, priority = "unavailable", tostring(session.priority or "-"); + if session.presence then + status = session.presence:get_child_text("show") or "available"; + end + line[#line+1] = status.."("..priority..")"; + end + if session.cert_identity_status == "valid" then + line[#line+1] = "(authenticated)"; + end + if session.secure then + line[#line+1] = "(encrypted)"; + end + if session.compressed then + line[#line+1] = "(compressed)"; + end + if session.smacks then + line[#line+1] = "(sm)"; + end + if session.ip and session.ip:match(":") then + line[#line+1] = "(IPv6)"; + end + if session.remote then + line[#line+1] = "(remote)"; + end + return table.concat(line, " "); +end + +local function tls_info(session, line) + line = line or {}; + common_info(session, line); + if session.secure then + local sock = session.conn and session.conn.socket and session.conn:socket(); + if sock and sock.info then + local info = sock:info(); + line[#line+1] = ("(%s with %s)"):format(info.protocol, info.cipher); + else + line[#line+1] = "(cipher info unavailable)"; + end + else + line[#line+1] = "(insecure)"; + end + return table.concat(line, " "); end def_env.c2s = {}; +local function get_jid(session) + if session.username then + return session.full_jid or jid_join(session.username, session.host, session.resource); + end + + local conn = session.conn; + local ip = session.ip or "?"; + local clientport = conn and conn:clientport() or "?"; + local serverip = conn and conn.server and conn:server():ip() or "?"; + local serverport = conn and conn:serverport() or "?" + return jid_join("["..ip.."]:"..clientport, session.host or "["..serverip.."]:"..serverport); +end + local function show_c2s(callback) - for hostname, host in pairs(hosts) do - for username, user in pairs(host.sessions or {}) do - for resource, session in pairs(user.sessions or {}) do - local jid = username.."@"..hostname.."/"..resource; - callback(jid, session); + local c2s = array.collect(values(module:shared"/*/c2s/sessions")); + c2s:sort(function(a, b) + if a.host == b.host then + if a.username == b.username then + return (a.resource or "") > (b.resource or ""); end + return (a.username or "") > (b.username or ""); end - end + return (a.host or "") > (b.host or ""); + end):map(function (session) + callback(get_jid(session), session) + end); end function def_env.c2s:count(match_jid) - local count = 0; - show_c2s(function (jid, session) - if (not match_jid) or jid:match(match_jid) then - count = count + 1; - end - end); - return true, "Total: "..count.." clients"; + return true, "Total: ".. iterators.count(values(module:shared"/*/c2s/sessions")) .." clients"; end -function def_env.c2s:show(match_jid) +function def_env.c2s:show(match_jid, annotate) local print, count = self.session.print, 0; - local curr_host; + annotate = annotate or session_flags; + local curr_host = false; show_c2s(function (jid, session) if curr_host ~= session.host then curr_host = session.host; - print(curr_host); + print(curr_host or "(not connected to any host yet)"); end if (not match_jid) or jid:match(match_jid) then count = count + 1; - local status, priority = "unavailable", tostring(session.priority or "-"); - if session.presence then - status = session.presence:child_with_name("show"); - if status then - status = status:get_text() or "[invalid!]"; - else - status = "available"; - end - end - print(" "..jid.." - "..status.."("..priority..")"); - end + print(annotate(session, { " ", jid })); + end end); return true, "Total: "..count.." clients"; end @@ -542,7 +594,7 @@ function def_env.c2s:show_insecure(match_jid) if ((not match_jid) or jid:match(match_jid)) and not session.secure then count = count + 1; print(jid); - end + end end); return true, "Total: "..count.." insecure client connections"; end @@ -553,11 +605,15 @@ function def_env.c2s:show_secure(match_jid) if ((not match_jid) or jid:match(match_jid)) and session.secure then count = count + 1; print(jid); - end + end end); return true, "Total: "..count.." secure client connections"; end +function def_env.c2s:show_tls(match_jid) + return self:show(match_jid, tls_info); +end + function def_env.c2s:close(match_jid) local count = 0; show_c2s(function (jid, session) @@ -569,99 +625,87 @@ function def_env.c2s:close(match_jid) return true, "Total: "..count.." sessions closed"; end -local function session_flags(session, line) - if session.cert_identity_status == "valid" then - line[#line+1] = "(secure)"; - elseif session.secure then - line[#line+1] = "(encrypted)"; - end - if session.compressed then - line[#line+1] = "(compressed)"; - end - if session.smacks then - line[#line+1] = "(sm)"; - end - if session.conn and session.conn:ip():match(":") then - line[#line+1] = "(IPv6)"; - end - return table.concat(line, " "); -end def_env.s2s = {}; -function def_env.s2s:show(match_jid) - local _print = self.session.print; +function def_env.s2s:show(match_jid, annotate) local print = self.session.print; - + annotate = annotate or session_flags; + local count_in, count_out = 0,0; - - for host, host_session in pairs(hosts) do - print = function (...) _print(host); _print(...); print = _print; end - for remotehost, session in pairs(host_session.s2sout) do - if (not match_jid) or remotehost:match(match_jid) or host:match(match_jid) then - count_out = count_out + 1; - print(session_flags(session, {" ", host, "->", remotehost})); - if session.sendq then - print(" There are "..#session.sendq.." queued outgoing stanzas for this connection"); - end - if session.type == "s2sout_unauthed" then - if session.connecting then - print(" Connection not yet established"); - if not session.srv_hosts then - if not session.conn then - print(" We do not yet have a DNS answer for this host's SRV records"); - else - print(" This host has no SRV records, using A record instead"); - end - elseif session.srv_choice then - print(" We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts); - local srv_choice = session.srv_hosts[session.srv_choice]; - print(" Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269)); + local s2s_list = { }; + + local s2s_sessions = module:shared"/*/s2s/sessions"; + for _, session in pairs(s2s_sessions) do + local remotehost, localhost, direction; + if session.direction == "outgoing" then + direction = "->"; + count_out = count_out + 1; + remotehost, localhost = session.to_host or "?", session.from_host or "?"; + else + direction = "<-"; + count_in = count_in + 1; + remotehost, localhost = session.from_host or "?", session.to_host or "?"; + end + local sess_lines = { l = localhost, r = remotehost, + annotate(session, { "", direction, remotehost or "?" })}; + + if (not match_jid) or remotehost:match(match_jid) or localhost:match(match_jid) then + table.insert(s2s_list, sess_lines); + local print = function (s) table.insert(sess_lines, " "..s); end + if session.sendq then + print("There are "..#session.sendq.." queued outgoing stanzas for this connection"); + end + if session.type == "s2sout_unauthed" then + if session.connecting then + print("Connection not yet established"); + if not session.srv_hosts then + if not session.conn then + print("We do not yet have a DNS answer for this host's SRV records"); + else + print("This host has no SRV records, using A record instead"); end - elseif session.notopen then - print(" The <stream> has not yet been opened"); - elseif not session.dialback_key then - print(" Dialback has not been initiated yet"); - elseif session.dialback_key then - print(" Dialback has been requested, but no result received"); + elseif session.srv_choice then + print("We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts); + local srv_choice = session.srv_hosts[session.srv_choice]; + print("Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269)); end + elseif session.notopen then + print("The <stream> has not yet been opened"); + elseif not session.dialback_key then + print("Dialback has not been initiated yet"); + elseif session.dialback_key then + print("Dialback has been requested, but no result received"); end end - end - local subhost_filter = function (h) - return (match_jid and h:match(match_jid)); - end - for session in pairs(incoming_s2s) do - if session.to_host == host and ((not match_jid) or host:match(match_jid) - or (session.from_host and session.from_host:match(match_jid)) - -- Pft! is what I say to list comprehensions - or (session.hosts and #array.collect(keys(session.hosts)):filter(subhost_filter)>0)) then - count_in = count_in + 1; - print(session_flags(session, {" ", host, "<-", session.from_host or "(unknown)"})); - if session.type == "s2sin_unauthed" then - print(" Connection not yet authenticated"); - end + if session.type == "s2sin_unauthed" then + print("Connection not yet authenticated"); + elseif session.type == "s2sin" then for name in pairs(session.hosts) do if name ~= session.from_host then - print(" also hosts "..tostring(name)); + print("also hosts "..tostring(name)); end end end end - - print = _print; end - - for session in pairs(incoming_s2s) do - if not session.to_host and ((not match_jid) or session.from_host and session.from_host:match(match_jid)) then - count_in = count_in + 1; - print("Other incoming s2s connections"); - print(" (unknown) <- "..(session.from_host or "(unknown)")); - end + + -- Sort by local host, then remote host + table.sort(s2s_list, function(a,b) + if a.l == b.l then return a.r < b.r; end + return a.l < b.l; + end); + local lasthost; + for _, sess_lines in ipairs(s2s_list) do + if sess_lines.l ~= lasthost then print(sess_lines.l); lasthost=sess_lines.l end + for _, line in ipairs(sess_lines) do print(line); end end - return true, "Total: "..count_out.." outgoing, "..count_in.." incoming connections"; end +function def_env.s2s:show_tls(match_jid) + return self:show(match_jid, tls_info); +end + local function print_subject(print, subject) for _, entry in ipairs(subject) do print( @@ -690,14 +734,9 @@ end function def_env.s2s:showcert(domain) local ser = require "util.serialization".serialize; local print = self.session.print; - local domain_sessions = set.new(array.collect(keys(incoming_s2s))) - /function(session) return session.from_host == domain and session or nil; end; - for local_host in values(prosody.hosts) do - local s2sout = local_host.s2sout; - if s2sout and s2sout[domain] then - domain_sessions:add(s2sout[domain]); - end - end + local s2s_sessions = module:shared"/*/s2s/sessions"; + local domain_sessions = set.new(array.collect(values(s2s_sessions))) + /function(session) return (session.to_host == domain or session.from_host == domain) and session or nil; end; local cert_set = {}; for session in domain_sessions do local conn = session.conn; @@ -736,18 +775,18 @@ function def_env.s2s:showcert(domain) local domain_certs = array.collect(values(cert_set)); -- Phew. We now have a array of unique certificates presented by domain. local n_certs = #domain_certs; - + if n_certs == 0 then return "No certificates found for "..domain; end - + local function _capitalize_and_colon(byte) return string.upper(byte)..":"; end local function pretty_fingerprint(hash) return hash:gsub("..", _capitalize_and_colon):sub(1, -2); end - + for cert_info in values(domain_certs) do local certs = cert_info.certs; local cert = certs[1]; @@ -788,76 +827,38 @@ end function def_env.s2s:close(from, to) local print, count = self.session.print, 0; - - if not (from and to) then + local s2s_sessions = module:shared"/*/s2s/sessions"; + + local match_id; + if from and not to then + match_id, from = from; + elseif not to then return false, "Syntax: s2s:close('from', 'to') - Closes all s2s sessions from 'from' to 'to'"; elseif from == to then return false, "Both from and to are the same... you can't do that :)"; end - - if hosts[from] and not hosts[to] then - -- Is an outgoing connection - local session = hosts[from].s2sout[to]; - if not session then - print("No outgoing connection from "..from.." to "..to) - else + + for _, session in pairs(s2s_sessions) do + local id = session.type..tostring(session):match("[a-f0-9]+$"); + if (match_id and match_id == id) + or (session.from_host == from and session.to_host == to) then + print(("Closing connection from %s to %s [%s]"):format(session.from_host, session.to_host, id)); (session.close or s2smanager.destroy_session)(session); - count = count + 1; - print("Closed outgoing session from "..from.." to "..to); - end - elseif hosts[to] and not hosts[from] then - -- Is an incoming connection - for session in pairs(incoming_s2s) do - if session.to_host == to and session.from_host == from then - (session.close or s2smanager.destroy_session)(session); - count = count + 1; - end - end - - if count == 0 then - print("No incoming connections from "..from.." to "..to); - else - print("Closed "..count.." incoming session"..((count == 1 and "") or "s").." from "..from.." to "..to); + count = count + 1 ; end - elseif hosts[to] and hosts[from] then - return false, "Both of the hostnames you specified are local, there are no s2s sessions to close"; - else - return false, "Neither of the hostnames you specified are being used on this server"; end - return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end function def_env.s2s:closeall(host) - local count = 0; - - if not host or type(host) ~= "string" then return false, "wrong syntax: please use s2s:closeall('hostname.tld')"; end - if hosts[host] then - for session in pairs(incoming_s2s) do - if session.to_host == host then - (session.close or s2smanager.destroy_session)(session); - count = count + 1; - end - end - for _, session in pairs(hosts[host].s2sout) do - (session.close or s2smanager.destroy_session)(session); - count = count + 1; - end - else - for session in pairs(incoming_s2s) do - if session.from_host == host then - (session.close or s2smanager.destroy_session)(session); - count = count + 1; - end - end - for _, h in pairs(hosts) do - if h.s2sout[host] then - (h.s2sout[host].close or s2smanager.destroy_session)(h.s2sout[host]); - count = count + 1; - end + local count = 0; + local s2s_sessions = module:shared"/*/s2s/sessions"; + for _,session in pairs(s2s_sessions) do + if not host or session.from_host == host or session.to_host == host then + session:close(); + count = count + 1; end - end - + end if count == 0 then return false, "No sessions to close."; else return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end end @@ -874,9 +875,19 @@ end function def_env.host:list() local print = self.session.print; local i = 0; + local type; for host in values(array.collect(keys(prosody.hosts)):sort()) do i = i + 1; - print(host); + type = hosts[host].type; + if type == "local" then + print(host); + else + type = module:context(host):get_option_string("component_module", type); + if type ~= "component" then + type = type .. " component"; + end + print(("%s (%s)"):format(host, type)); + end end return true, i.." hosts"; end @@ -967,6 +978,20 @@ function def_env.muc:room(room_jid) return setmetatable({ room = room_obj }, console_room_mt); end +function def_env.muc:list(host) + local host_session = hosts[host]; + if not host_session or not host_session.modules.muc then + return nil, "Please supply the address of a local MUC component"; + end + local print = self.session.print; + local c = 0; + for name in keys(host_session.modules.muc.rooms) do + print(name); + c = c + 1; + end + return true, c.." rooms"; +end + local um = require"core.usermanager"; def_env.user = {}; @@ -1111,29 +1136,25 @@ end ------------- function printbanner(session) - local option = module:get_option("console_banner"); - if option == nil or option == "full" or option == "graphic" then + local option = module:get_option_string("console_banner", "full"); + if option == "full" or option == "graphic" then session.print [[ - ____ \ / _ - | _ \ _ __ ___ ___ _-_ __| |_ _ + ____ \ / _ + | _ \ _ __ ___ ___ _-_ __| |_ _ | |_) | '__/ _ \/ __|/ _ \ / _` | | | | | __/| | | (_) \__ \ |_| | (_| | |_| | |_| |_| \___/|___/\___/ \__,_|\__, | - A study in simplicity |___/ + A study in simplicity |___/ ]] end - if option == nil or option == "short" or option == "full" then + if option == "short" or option == "full" then session.print("Welcome to the Prosody administration console. For a list of commands, type: help"); session.print("You may find more help on using this console in our online documentation at "); session.print("http://prosody.im/doc/console\n"); end - if option and option ~= "short" and option ~= "full" and option ~= "graphic" then - if type(option) == "string" then - session.print(option) - elseif type(option) == "function" then - module:log("warn", "Using functions as value for the console_banner option is no longer supported"); - end + if option ~= "short" and option ~= "full" and option ~= "graphic" then + session.print(option); end end diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua index 96976d6f..9327556c 100644 --- a/plugins/mod_announce.lua +++ b/plugins/mod_announce.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -39,22 +39,22 @@ end function handle_announcement(event) local origin, stanza = event.origin, event.stanza; local node, host, resource = jid.split(stanza.attr.to); - + if resource ~= "announce/online" then return; -- Not an announcement end - + if not is_admin(stanza.attr.from) then -- Not an admin? Not allowed! module:log("warn", "Non-admin '%s' tried to send server announcement", stanza.attr.from); return; end - + module:log("info", "Sending server announcement to all online users"); local message = st.clone(stanza); message.attr.type = "headline"; message.attr.from = host; - + local c = send_to_online(message, host); module:log("info", "Announcement sent to %d online users", c); return true; @@ -83,9 +83,9 @@ function announce_handler(self, data, state) module:log("info", "Sending server announcement to all online users"); local message = st.message({type = "headline"}, fields.announcement):up() :tag("subject"):text(fields.subject or "Announcement"); - + local count = send_to_online(message, data.to); - + module:log("info", "Announcement sent to %d online users", count); return { status = "completed", info = ("Announcement sent to %d online users"):format(count) }; else diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua index 2b041e43..78abe50d 100644 --- a/plugins/mod_auth_internal_hashed.lua +++ b/plugins/mod_auth_internal_hashed.lua @@ -7,44 +7,30 @@ -- COPYING file in the source package for more information. -- -local log = require "util.logger".init("auth_internal_hashed"); +local max = math.max; + local getAuthenticationDatabaseSHA1 = require "util.sasl.scram".getAuthenticationDatabaseSHA1; local usermanager = require "core.usermanager"; local generate_uuid = require "util.uuid".generate; local new_sasl = require "util.sasl".new; +local hex = require"util.hex"; +local to_hex, from_hex = hex.to, hex.from; + +local log = module._log; +local host = module.host; local accounts = module:open_store("accounts"); -local to_hex; -do - local function replace_byte_with_hex(byte) - return ("%02x"):format(byte:byte()); - end - function to_hex(binary_string) - return binary_string:gsub(".", replace_byte_with_hex); - end -end - -local from_hex; -do - local function replace_hex_with_byte(hex) - return string.char(tonumber(hex, 16)); - end - function from_hex(hex_string) - return hex_string:gsub("..", replace_hex_with_byte); - end -end -- Default; can be set per-user -local iteration_count = 4096; +local default_iteration_count = 4096; -local host = module.host; -- define auth provider local provider = {}; -log("debug", "initializing internal_hashed authentication provider for host '%s'", host); function provider.test_password(username, password) + log("debug", "test password for user '%s'", username); local credentials = accounts:get(username) or {}; if credentials.password ~= nil and string.len(credentials.password) ~= 0 then @@ -62,12 +48,12 @@ function provider.test_password(username, password) if credentials.iteration_count == nil or credentials.salt == nil or string.len(credentials.salt) == 0 then return nil, "Auth failed. Stored salt and iteration count information is not complete."; end - + local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, credentials.salt, credentials.iteration_count); - + local stored_key_hex = to_hex(stored_key); local server_key_hex = to_hex(server_key); - + if valid and stored_key_hex == credentials.stored_key and server_key_hex == credentials.server_key then return true; else @@ -76,14 +62,15 @@ function provider.test_password(username, password) end function provider.set_password(username, password) + log("debug", "set_password for username '%s'", username); local account = accounts:get(username); if account then - account.salt = account.salt or generate_uuid(); - account.iteration_count = account.iteration_count or iteration_count; + account.salt = generate_uuid(); + account.iteration_count = max(account.iteration_count or 0, default_iteration_count); local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, account.salt, account.iteration_count); local stored_key_hex = to_hex(stored_key); local server_key_hex = to_hex(server_key); - + account.stored_key = stored_key_hex account.server_key = server_key_hex @@ -96,7 +83,7 @@ end function provider.user_exists(username) local account = accounts:get(username); if not account then - log("debug", "account not found for username '%s' at host '%s'", username, host); + log("debug", "account not found for username '%s'", username); return nil, "Auth failed. Invalid username"; end return true; @@ -111,10 +98,10 @@ function provider.create_user(username, password) return accounts:set(username, {}); end local salt = generate_uuid(); - local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, iteration_count); + local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, default_iteration_count); local stored_key_hex = to_hex(stored_key); local server_key_hex = to_hex(server_key); - return accounts:set(username, {stored_key = stored_key_hex, server_key = server_key_hex, salt = salt, iteration_count = iteration_count}); + return accounts:set(username, {stored_key = stored_key_hex, server_key = server_key_hex, salt = salt, iteration_count = default_iteration_count}); end function provider.delete_user(username) @@ -134,7 +121,7 @@ function provider.get_sasl_handler() credentials = accounts:get(username); if not credentials then return; end end - + local stored_key, server_key, iteration_count, salt = credentials.stored_key, credentials.server_key, credentials.iteration_count, credentials.salt; stored_key = stored_key and from_hex(stored_key); server_key = server_key and from_hex(server_key); @@ -143,6 +130,6 @@ function provider.get_sasl_handler() }; return new_sasl(host, testpass_authentication_profile); end - + module:provides("auth", provider); diff --git a/plugins/mod_auth_internal_plain.lua b/plugins/mod_auth_internal_plain.lua index d226fdbe..db528432 100644 --- a/plugins/mod_auth_internal_plain.lua +++ b/plugins/mod_auth_internal_plain.lua @@ -16,10 +16,9 @@ local accounts = module:open_store("accounts"); -- define auth provider local provider = {}; -log("debug", "initializing internal_plain authentication provider for host '%s'", host); function provider.test_password(username, password) - log("debug", "test password for user %s at host %s", username, host); + log("debug", "test password for user '%s'", username); local credentials = accounts:get(username) or {}; if password == credentials.password then @@ -30,11 +29,12 @@ function provider.test_password(username, password) end function provider.get_password(username) - log("debug", "get_password for username '%s' at host '%s'", username, host); + log("debug", "get_password for username '%s'", username); return (accounts:get(username) or {}).password; end function provider.set_password(username, password) + log("debug", "set_password for username '%s'", username); local account = accounts:get(username); if account then account.password = password; @@ -46,7 +46,7 @@ end function provider.user_exists(username) local account = accounts:get(username); if not account then - log("debug", "account not found for username '%s' at host '%s'", username, host); + log("debug", "account not found for username '%s'", username); return nil, "Auth failed. Invalid username"; end return true; @@ -76,6 +76,6 @@ function provider.get_sasl_handler() }; return new_sasl(host, getpass_authentication_profile); end - + module:provides("auth", provider); diff --git a/plugins/mod_blocklist.lua b/plugins/mod_blocklist.lua new file mode 100644 index 00000000..8efbfd96 --- /dev/null +++ b/plugins/mod_blocklist.lua @@ -0,0 +1,325 @@ +-- Prosody IM +-- Copyright (C) 2009-2010 Matthew Wild +-- Copyright (C) 2009-2010 Waqas Hussain +-- Copyright (C) 2014-2015 Kim Alvefur +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- +-- This module implements XEP-0191: Blocking Command +-- + +local user_exists = require"core.usermanager".user_exists; +local rostermanager = require"core.rostermanager"; +local is_contact_subscribed = rostermanager.is_contact_subscribed; +local is_contact_pending_in = rostermanager.is_contact_pending_in; +local load_roster = rostermanager.load_roster; +local save_roster = rostermanager.save_roster; +local st = require"util.stanza"; +local st_error_reply = st.error_reply; +local jid_prep = require"util.jid".prep; +local jid_split = require"util.jid".split; + +local storage = module:open_store(); +local sessions = prosody.hosts[module.host].sessions; + +-- First level cache of blocklists by username. +-- Weak table so may randomly expire at any time. +local cache = setmetatable({}, { __mode = "v" }); + +-- Second level of caching, keeps a fixed number of items, also anchors +-- items in the above cache. +-- +-- The size of this affects how often we will need to load a blocklist from +-- disk, which we want to avoid during routing. On the other hand, we don't +-- want to use too much memory either, so this can be tuned by advanced +-- users. TODO use science to figure out a better default, 64 is just a guess. +local cache_size = module:get_option_number("blocklist_cache_size", 64); +local cache2 = require"util.cache".new(cache_size); + +local null_blocklist = {}; + +module:add_feature("urn:xmpp:blocking"); + +local function set_blocklist(username, blocklist) + local ok, err = storage:set(username, blocklist); + if not ok then + return ok, err; + end + -- Successful save, update the cache + cache2:set(username, blocklist); + cache[username] = blocklist; + return true; +end + +-- Migrates from the old mod_privacy storage +local function migrate_privacy_list(username) + local migrated_data = { [false] = "not empty" }; + local legacy_data = module:open_store("privacy"):get(username); + if legacy_data and legacy_data.lists and legacy_data.default then + legacy_data = legacy_data.lists[legacy_data.default]; + legacy_data = legacy_data and legacy_data.items; + else + return migrated_data; + end + if legacy_data then + module:log("info", "Migrating blocklist from mod_privacy storage for user '%s'", username); + local item, jid; + for i = 1, #legacy_data do + item = legacy_data[i]; + if item.type == "jid" and item.action == "deny" then + jid = jid_prep(item.value); + if not jid then + module:log("warn", "Invalid JID in privacy store for user '%s' not migrated: %s", username, tostring(item.value)); + else + migrated_data[jid] = true; + end + end + end + end + set_blocklist(username, migrated_data); + return migrated_data; +end + +local function get_blocklist(username) + local blocklist = cache[username]; + if not blocklist then + blocklist = cache2:get(username); + end + if not blocklist then + if not user_exists(username, module.host) then + return null_blocklist; + end + blocklist = storage:get(username); + if not blocklist then + blocklist = migrate_privacy_list(username); + end + cache2:set(username, blocklist); + end + cache[username] = blocklist; + return blocklist; +end + +module:hook("iq-get/self/urn:xmpp:blocking:blocklist", function (event) + local origin, stanza = event.origin, event.stanza; + local username = origin.username; + local reply = st.reply(stanza):tag("blocklist", { xmlns = "urn:xmpp:blocking" }); + local blocklist = get_blocklist(username); + for jid in pairs(blocklist) do + if jid then + reply:tag("item", { jid = jid }):up(); + end + end + origin.interested_blocklist = true; -- Gets notified about changes + origin.send(reply); + return true; +end); + +-- Add or remove some jid(s) from the blocklist +-- We want this to be atomic and not do a partial update +local function edit_blocklist(event) + local origin, stanza = event.origin, event.stanza; + local username = origin.username; + local action = stanza.tags[1]; -- "block" or "unblock" + local is_blocking = action.name == "block" or nil; -- nil if unblocking + local new = {}; -- JIDs to block depending or unblock on action + + -- XEP-0191 sayeth: + -- > When the user blocks communications with the contact, the user's + -- > server MUST send unavailable presence information to the contact (but + -- > only if the contact is allowed to receive presence notifications [...] + -- So contacts we need to do that for are added to the set below. + local send_unavailable = is_blocking and {}; + + -- Because blocking someone currently also blocks the ability to reject + -- subscription requests, we'll preemptively reject such + local remove_pending = is_blocking and {}; + + for item in action:childtags("item") do + local jid = jid_prep(item.attr.jid); + if not jid then + origin.send(st_error_reply(stanza, "modify", "jid-malformed")); + return true; + end + item.attr.jid = jid; -- echo back prepped + new[jid] = true; + if is_blocking then + if is_contact_subscribed(username, module.host, jid) then + send_unavailable[jid] = true; + elseif is_contact_pending_in(username, module.host, jid) then + remove_pending[jid] = true; + end + end + end + + if is_blocking and not next(new) then + -- <block/> element does not contain at least one <item/> child element + origin.send(st_error_reply(stanza, "modify", "bad-request")); + return true; + end + + local blocklist = get_blocklist(username); + + local new_blocklist = {}; + + if is_blocking or next(new) then + for jid in pairs(blocklist) do + new_blocklist[jid] = true; + end + for jid in pairs(new) do + new_blocklist[jid] = is_blocking; + end + -- else empty the blocklist + end + new_blocklist[false] = "not empty"; -- In order to avoid doing the migration thing twice + + local ok, err = set_blocklist(username, new_blocklist); + if ok then + origin.send(st.reply(stanza)); + else + origin.send(st_error_reply(stanza, "wait", "internal-server-error", err)); + return true; + end + + if is_blocking then + for jid in pairs(send_unavailable) do + if not blocklist[jid] then + for _, session in pairs(sessions[username].sessions) do + if session.presence then + module:send(st.presence({ type = "unavailable", to = jid, from = session.full_jid })); + end + end + end + end + + if next(remove_pending) then + local roster = load_roster(username, module.host); + for jid in pairs(remove_pending) do + roster[false].pending[jid] = nil; + end + save_roster(username, module.host, roster); + -- Not much we can do about save failing here + end + end + + local blocklist_push = st.iq({ type = "set", id = "blocklist-push" }) + :add_child(action); -- I am lazy + + for _, session in pairs(sessions[username].sessions) do + if session.interested_blocklist then + blocklist_push.attr.to = session.full_jid; + session.send(blocklist_push); + end + end + + return true; +end + +module:hook("iq-set/self/urn:xmpp:blocking:block", edit_blocklist); +module:hook("iq-set/self/urn:xmpp:blocking:unblock", edit_blocklist); + +-- Cache invalidation, solved! +module:hook_global("user-deleted", function (event) + if event.host == module.host then + cache2:set(event.username, nil); + cache[event.username] = nil; + end +end); + +-- Buggy clients +module:hook("iq-error/self/blocklist-push", function (event) + local _, condition, text = event.stanza:get_error(); + (event.origin.log or module._log)("warn", "Client returned an error in response to notification from mod_%s: %s%s%s", module.name, condition, text and ": " or "", text or ""); + return true; +end); + +local function is_blocked(user, jid) + local blocklist = cache[user] or get_blocklist(user); + if blocklist[jid] then return true; end + local node, host = jid_split(jid); + return blocklist[host] or node and blocklist[node..'@'..host]; +end + +-- Event handlers for bouncing or dropping stanzas +local function drop_stanza(event) + local stanza = event.stanza; + local attr = stanza.attr; + local to, from = attr.to, attr.from; + to = to and jid_split(to); + if to and from then + return is_blocked(to, from); + end +end + +local function bounce_stanza(event) + local origin, stanza = event.origin, event.stanza; + if drop_stanza(event) then + origin.send(st_error_reply(stanza, "cancel", "service-unavailable")); + return true; + end +end + +local function bounce_iq(event) + local type = event.stanza.attr.type; + if type == "set" or type == "get" then + return bounce_stanza(event); + end + return drop_stanza(event); -- result or error +end + +local function bounce_message(event) + local type = event.stanza.attr.type; + if type == "chat" or not type or type == "normal" then + return bounce_stanza(event); + end + return drop_stanza(event); -- drop headlines, groupchats etc +end + +local function drop_outgoing(event) + local origin, stanza = event.origin, event.stanza; + local username = origin.username or jid_split(stanza.attr.from); + if not username then return end + local to = stanza.attr.to; + if to then return is_blocked(username, to); end + -- nil 'to' means a self event, don't bock those +end + +local function bounce_outgoing(event) + local origin, stanza = event.origin, event.stanza; + local type = stanza.attr.type; + if type == "error" or stanza.name == "iq" and type == "result" then + return drop_outgoing(event); + end + if drop_outgoing(event) then + origin.send(st_error_reply(stanza, "cancel", "not-acceptable", "You have blocked this JID") + :tag("blocked", { xmlns = "urn:xmpp:blocking:errors" })); + return true; + end +end + +-- Hook all the events! +local prio_in, prio_out = 100, 100; +module:hook("presence/bare", drop_stanza, prio_in); +module:hook("presence/full", drop_stanza, prio_in); + +module:hook("message/bare", bounce_message, prio_in); +module:hook("message/full", bounce_message, prio_in); + +module:hook("iq/bare", bounce_iq, prio_in); +module:hook("iq/full", bounce_iq, prio_in); + +module:hook("pre-message/bare", bounce_outgoing, prio_out); +module:hook("pre-message/full", bounce_outgoing, prio_out); +module:hook("pre-message/host", bounce_outgoing, prio_out); + +-- Note: MUST bounce these, but we don't because this would produce +-- lots of error replies due to server-generated presence. +-- FIXME some day, likely needing changes to mod_presence +module:hook("pre-presence/bare", drop_outgoing, prio_out); +module:hook("pre-presence/full", drop_outgoing, prio_out); +module:hook("pre-presence/host", drop_outgoing, prio_out); + +module:hook("pre-iq/bare", bounce_outgoing, prio_out); +module:hook("pre-iq/full", bounce_outgoing, prio_out); +module:hook("pre-iq/host", bounce_outgoing, prio_out); + diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua index d9c8defd..fd33226c 100644 --- a/plugins/mod_bosh.lua +++ b/plugins/mod_bosh.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -22,6 +22,7 @@ local initialize_filters = require "util.filters".initialize; local math_min = math.min; local xpcall, tostring, type = xpcall, tostring, type; local traceback = debug.traceback; +local runner = require"util.async".runner; local xmlns_streams = "http://etherx.jabber.org/streams"; local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; @@ -37,24 +38,10 @@ local BOSH_DEFAULT_REQUESTS = module:get_option_number("bosh_max_requests", 2); local bosh_max_wait = module:get_option_number("bosh_max_wait", 120); local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure"); - -local default_headers = { ["Content-Type"] = "text/xml; charset=utf-8" }; - local cross_domain = module:get_option("cross_domain_bosh", false); -if cross_domain then - default_headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"; - default_headers["Access-Control-Allow-Headers"] = "Content-Type"; - default_headers["Access-Control-Max-Age"] = "7200"; - - if cross_domain == true then - default_headers["Access-Control-Allow-Origin"] = "*"; - elseif type(cross_domain) == "table" then - cross_domain = table.concat(cross_domain, ", "); - end - if type(cross_domain) == "string" then - default_headers["Access-Control-Allow-Origin"] = cross_domain; - end -end + +if cross_domain == true then cross_domain = "*"; end +if type(cross_domain) == "table" then cross_domain = table.concat(cross_domain, ", "); end local trusted_proxies = module:get_option_set("trusted_proxies", {"127.0.0.1"})._items; @@ -79,7 +66,7 @@ local os_time = os.time; local sessions, inactive_sessions = module:shared("sessions", "inactive_sessions"); -- Used to respond to idle sessions (those with waiting requests) -local waiting_requests = {}; +local waiting_requests = module:shared("waiting_requests"); function on_destroy_request(request) log("debug", "Request destroyed: %s", tostring(request)); waiting_requests[request] = nil; @@ -92,7 +79,7 @@ function on_destroy_request(request) break; end end - + -- If this session now has no requests open, mark it as inactive local max_inactive = session.bosh_max_inactive; if max_inactive and #requests == 0 then @@ -102,11 +89,20 @@ function on_destroy_request(request) end end -function handle_OPTIONS(request) - local headers = {}; - for k,v in pairs(default_headers) do headers[k] = v; end - headers["Content-Type"] = nil; - return { headers = headers, body = "" }; +local function set_cross_domain_headers(response) + local headers = response.headers; + headers.access_control_allow_methods = "GET, POST, OPTIONS"; + headers.access_control_allow_headers = "Content-Type"; + headers.access_control_max_age = "7200"; + headers.access_control_allow_origin = cross_domain; + return response; +end + +function handle_OPTIONS(event) + if cross_domain and event.request.headers.origin then + set_cross_domain_headers(event.response); + end + return ""; end function handle_POST(event) @@ -119,14 +115,24 @@ function handle_POST(event) local context = { request = request, response = response, notopen = true }; local stream = new_xmpp_stream(context, stream_callbacks); response.context = context; - + + local headers = response.headers; + headers.content_type = "text/xml; charset=utf-8"; + + if cross_domain and event.request.headers.origin then + set_cross_domain_headers(response); + end + -- stream:feed() calls the stream_callbacks, so all stanzas in -- the body are processed in this next line before it returns. -- In particular, the streamopened() stream callback is where -- much of the session logic happens, because it's where we first -- get to see the 'sid' of this request. - stream:feed(body); - + if not stream:feed(body) then + module:log("warn", "Error parsing BOSH payload") + return 400; + end + -- Stanzas (if any) in the request have now been processed, and -- we take care of the high-level BOSH logic here, including -- giving a response or putting the request "on hold". @@ -141,9 +147,6 @@ function handle_POST(event) local r = session.requests; log("debug", "Session %s has %d out of %d requests open", context.sid, #r, session.bosh_hold); log("debug", "and there are %d things in the send_buffer:", #session.send_buffer); - for i, thing in ipairs(session.send_buffer) do - log("debug", " %s", tostring(thing)); - end if #r > session.bosh_hold then -- We are holding too many requests, send what's in the buffer, log("debug", "We are holding too many requests, so..."); @@ -162,7 +165,7 @@ function handle_POST(event) session.send_buffer = {}; session.send(resp); end - + if not response.finished then -- We're keeping this request open, to respond later log("debug", "Have nothing to say, so leaving request unanswered for now"); @@ -170,7 +173,7 @@ function handle_POST(event) waiting_requests[response] = os_time() + session.bosh_wait; end end - + if session.bosh_terminate then session.log("debug", "Closing session with %d requests open", #session.requests); session:close(); @@ -179,6 +182,8 @@ function handle_POST(event) return true; -- Inform http server we shall reply later end end + module:log("warn", "Unable to associate request with a session (incomplete request?)"); + return 400; end @@ -188,10 +193,10 @@ local stream_xmlns_attr = { xmlns = "urn:ietf:params:xml:ns:xmpp-streams" }; local function bosh_close_stream(session, reason) (session.log or log)("info", "BOSH client disconnected"); - + local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", ["xmlns:stream"] = xmlns_streams }); - + if reason then close_reply.attr.condition = "remote-stream-error"; @@ -217,14 +222,15 @@ local function bosh_close_stream(session, reason) local response_body = tostring(close_reply); for _, held_request in ipairs(session.requests) do - held_request.headers = default_headers; held_request:send(response_body); end - sessions[session.sid] = nil; + sessions[session.sid] = nil; inactive_sessions[session] = nil; sm_destroy_session(session); end +local runner_callbacks = { }; + -- Handle the <body> tag in the request payload. function stream_callbacks.streamopened(context, attr) local request, response = context.request, context.response; @@ -233,7 +239,7 @@ function stream_callbacks.streamopened(context, attr) if not sid then -- New session request context.notopen = nil; -- Signals that we accept this opening tag - + -- TODO: Sanity checks here (rid, to, known host, etc.) if not hosts[attr.to] then -- Unknown host @@ -243,7 +249,7 @@ function stream_callbacks.streamopened(context, attr) response:send(tostring(close_reply)); return; end - + -- New session sid = new_uuid(); local session = { @@ -256,12 +262,18 @@ function stream_callbacks.streamopened(context, attr) ip = get_ip_from_request(request); }; sessions[sid] = session; - + + session.thread = runner(function (stanza) + session:dispatch_stanza(stanza); + end, runner_callbacks, session); + local filter = initialize_filters(session); - + session.log("debug", "BOSH session created for request from %s", session.ip); log("info", "New BOSH session, assigned it sid '%s'", sid); + hosts[session.host].events.fire_event("bosh-session", { session = session, request = request }); + -- Send creation response local creating_session = true; @@ -279,7 +291,6 @@ function stream_callbacks.streamopened(context, attr) local oldest_request = r[1]; if oldest_request and not session.bosh_processing then log("debug", "We have an open request, so sending on that"); - oldest_request.headers = default_headers; local body_attr = { xmlns = "http://jabber.org/protocol/httpbind", ["xmlns:stream"] = "http://etherx.jabber.org/streams"; type = session.bosh_terminate and "terminate" or nil; @@ -306,17 +317,16 @@ function stream_callbacks.streamopened(context, attr) end request.sid = sid; end - + local session = sessions[sid]; if not session then -- Unknown sid log("info", "Client tried to use sid '%s' which we don't know about", sid); - response.headers = default_headers; response:send(tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" }))); context.notopen = nil; return; end - + if session.rid then local rid = tonumber(attr.rid); local diff = rid - session.rid; @@ -333,7 +343,7 @@ function stream_callbacks.streamopened(context, attr) end session.rid = rid; end - + if attr.type == "terminate" then -- Client wants to end this session, which we'll do -- after processing any stanzas in this request @@ -348,13 +358,17 @@ function stream_callbacks.streamopened(context, attr) if session.notopen then local features = st.stanza("stream:features"); hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); - fire_event("stream-features", session, features); - session.send(tostring(features)); + session.send(features); session.notopen = nil; end end local function handleerr(err) log("error", "Traceback[bosh]: %s", traceback(tostring(err), 2)); end + +function runner_callbacks:error(err) + return handleerr(err); +end + function stream_callbacks.handlestanza(context, stanza) if context.ignore then return; end log("debug", "BOSH stanza received: %s\n", stanza:top_tag()); @@ -364,14 +378,12 @@ function stream_callbacks.handlestanza(context, stanza) stanza.attr.xmlns = nil; end stanza = session.filter("stanzas/in", stanza); - if stanza then - return xpcall(function () return core_process_stanza(session, stanza) end, handleerr); - end + session.thread:run(stanza); end end -function stream_callbacks.streamclosed(request) - local session = sessions[request.sid]; +function stream_callbacks.streamclosed(context) + local session = sessions[context.sid]; if session then session.bosh_processing = false; if #session.send_buffer > 0 then @@ -384,12 +396,11 @@ function stream_callbacks.error(context, error) log("debug", "Error parsing BOSH request payload; %s", error); if not context.sid then local response = context.response; - response.headers = default_headers; response.status_code = 400; response:send(); return; end - + local session = sessions[context.sid]; if error == "stream-error" then -- Remote stream error, we close normally session:close(); @@ -398,7 +409,7 @@ function stream_callbacks.error(context, error) end end -local dead_sessions = {}; +local dead_sessions = module:shared("dead_sessions"); function on_timer() -- log("debug", "Checking for requests soon to timeout..."); -- Identify requests timing out within the next few seconds @@ -413,7 +424,7 @@ function on_timer() end end end - + now = now - 3; local n_dead_sessions = 0; for session, close_after in pairs(inactive_sessions) do diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua index 8524c37e..c638cd03 100644 --- a/plugins/mod_c2s.lua +++ b/plugins/mod_c2s.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -15,9 +15,10 @@ local sessionmanager = require "core.sessionmanager"; local st = require "util.stanza"; local sm_new_session, sm_destroy_session = sessionmanager.new_session, sessionmanager.destroy_session; local uuid_generate = require "util.uuid".generate; +local runner = require "util.async".runner; local xpcall, tostring, type = xpcall, tostring, type; -local traceback = debug.traceback; +local t_insert, t_remove = table.insert, table.remove; local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; @@ -27,16 +28,18 @@ local c2s_timeout = module:get_option_number("c2s_timeout"); local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5); local opt_keepalives = module:get_option_boolean("c2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true)); +local measure_connections = module:measure("connections", "counter"); + local sessions = module:shared("sessions"); local core_process_stanza = prosody.core_process_stanza; local hosts = prosody.hosts; -local stream_callbacks = { default_ns = "jabber:client", handlestanza = core_process_stanza }; +local stream_callbacks = { default_ns = "jabber:client" }; local listener = {}; +local runner_callbacks = {}; --- Stream events handlers local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; -local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; function stream_callbacks.streamopened(session, attr) local send = session.send; @@ -50,15 +53,13 @@ function stream_callbacks.streamopened(session, attr) session.streamid = uuid_generate(); (session.log or session)("debug", "Client sent opening <stream:stream> to %s", session.host); - if not hosts[session.host] or not hosts[session.host].users then + if not hosts[session.host] or not hosts[session.host].modules.c2s then -- We don't serve this host... session:close{ condition = "host-unknown", text = "This server does not serve "..tostring(session.host)}; return; end - send("<?xml version='1.0'?>"..st.stanza("stream:stream", { - xmlns = 'jabber:client', ["xmlns:stream"] = 'http://etherx.jabber.org/streams'; - id = session.streamid, from = session.host, version = '1.0', ["xml:lang"] = 'en' }):top_tag()); + session:open_stream(); (session.log or log)("debug", "Sent reply <stream:stream> to client"); session.notopen = nil; @@ -67,21 +68,27 @@ function stream_callbacks.streamopened(session, attr) -- since we now have a new stream header, session is secured if session.secure == false then session.secure = true; + session.encrypted = true; - -- Check if TLS compression is used local sock = session.conn:socket(); if sock.info then - session.compressed = sock:info"compression"; - elseif sock.compression then - session.compressed = sock:compression(); --COMPAT mw/luasec-hg + local info = sock:info(); + (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher); + session.compressed = info.compression; + else + (session.log or log)("info", "Stream encrypted"); + session.compressed = sock.compression and sock:compression(); --COMPAT mw/luasec-hg end end local features = st.stanza("stream:features"); hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); - module:fire_event("stream-features", session, features); - - send(features); + if features.tags[1] or session.full_jid then + send(features); + else + (session.log or log)("warn", "No features to offer"); + session:close{ condition = "undefined-condition", text = "No features to proceed with" }; + end end function stream_callbacks.streamclosed(session) @@ -116,12 +123,9 @@ function stream_callbacks.error(session, error, data) end end -local function handleerr(err) log("error", "Traceback[c2s]: %s", traceback(tostring(err), 2)); end function stream_callbacks.handlestanza(session, stanza) stanza = session.filter("stanzas/in", stanza); - if stanza then - return xpcall(function () return core_process_stanza(session, stanza) end, handleerr); - end + session.thread:run(stanza); end --- Session methods @@ -129,8 +133,7 @@ local function session_close(session, reason) local log = session.log or log; if session.conn then if session.notopen then - session.send("<?xml version='1.0'?>"); - session.send(st.stanza("stream:stream", default_stream_attr):top_tag()); + session:open_stream(); end if reason then -- nil == no err, initiated by us, false == initiated by client local stream_error = st.stanza("stream:error"); @@ -153,12 +156,12 @@ local function session_close(session, reason) log("debug", "Disconnecting client, <stream:error> is: %s", stream_error); session.send(stream_error); end - + session.send("</stream:stream>"); function session.send() return false; end - + local reason = (reason and (reason.name or reason.text or reason.condition)) or reason; - session.log("info", "c2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason or "session closed"); + session.log("debug", "c2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason or "session closed"); -- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote local conn = session.conn; @@ -188,16 +191,30 @@ module:hook_global("user-deleted", function(event) end end, 200); +function runner_callbacks:ready() + self.data.conn:resume(); +end + +function runner_callbacks:waiting() + self.data.conn:pause(); +end + +function runner_callbacks:error(err) + (self.data.log or log)("error", "Traceback[c2s]: %s", err); +end + --- Port listener function listener.onconnect(conn) + measure_connections(1); local session = sm_new_session(conn); sessions[conn] = session; - + session.log("info", "Client connected"); - + -- Client is using legacy SSL (otherwise mod_tls sets this flag) if conn:ssl() then session.secure = true; + session.encrypted = true; -- Check if TLS compression is used local sock = conn:socket(); @@ -207,34 +224,41 @@ function listener.onconnect(conn) session.compressed = sock:compression(); --COMPAT mw/luasec-hg end end - + if opt_keepalives then conn:setoption("keepalive", opt_keepalives); end - + session.close = session_close; - + local stream = new_xmpp_stream(session, stream_callbacks); session.stream = stream; session.notopen = true; - + function session.reset_stream() session.notopen = true; session.stream:reset(); end - + + session.thread = runner(function (stanza) + core_process_stanza(session, stanza); + end, runner_callbacks, session); + local filter = session.filter; function session.data(data) + -- Parse the data, which will store stanzas in session.pending_stanzas + if data then data = filter("bytes/in", data); if data then local ok, err = stream:feed(data); - if ok then return; end - log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_")); - session:close("not-well-formed"); + if not ok then + log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_")); + session:close("not-well-formed"); + end + end end end - if c2s_timeout then add_task(c2s_timeout, function () if session.type == "c2s_unauthed" then @@ -254,6 +278,7 @@ function listener.onincoming(conn, data) end function listener.ondisconnect(conn, err) + measure_connections(-1); local session = sessions[conn]; if session then (session.log or log)("info", "Client disconnected: %s", err or "connection closed"); @@ -262,14 +287,27 @@ function listener.ondisconnect(conn, err) end end +function listener.onreadtimeout(conn) + local session = sessions[conn]; + if session then + return (hosts[session.host] or prosody).events.fire_event("c2s-read-timeout", { session = session }); + end +end + +local function keepalive(event) + return event.session.send(' '); +end + function listener.associate_session(conn, session) sessions[conn] = session; end -function listener.ondetach(conn) - sessions[conn] = nil; +function module.add_host(module) + module:hook("c2s-read-timeout", keepalive, -1); end +module:hook("c2s-read-timeout", keepalive, -1); + module:hook("server-stopping", function(event) local reason = event.reason; for _, session in pairs(sessions) do diff --git a/plugins/mod_carbons.lua b/plugins/mod_carbons.lua new file mode 100644 index 00000000..9ef14713 --- /dev/null +++ b/plugins/mod_carbons.lua @@ -0,0 +1,110 @@ +-- XEP-0280: Message Carbons implementation for Prosody +-- Copyright (C) 2011 Kim Alvefur +-- +-- This file is MIT/X11 licensed. + +local st = require "util.stanza"; +local jid_bare = require "util.jid".bare; +local xmlns_carbons = "urn:xmpp:carbons:2"; +local xmlns_forward = "urn:xmpp:forward:0"; +local full_sessions, bare_sessions = prosody.full_sessions, prosody.bare_sessions; + +local function toggle_carbons(event) + local origin, stanza = event.origin, event.stanza; + local state = stanza.tags[1].name; + module:log("debug", "%s %sd carbons", origin.full_jid, state); + origin.want_carbons = state == "enable" and stanza.tags[1].attr.xmlns; + origin.send(st.reply(stanza)); + return true; +end +module:hook("iq-set/self/"..xmlns_carbons..":disable", toggle_carbons); +module:hook("iq-set/self/"..xmlns_carbons..":enable", toggle_carbons); + +local function message_handler(event, c2s) + local origin, stanza = event.origin, event.stanza; + local orig_type = stanza.attr.type or "normal"; + local orig_from = stanza.attr.from; + local orig_to = stanza.attr.to; + + if not(orig_type == "chat" or orig_type == "normal" and stanza:get_child("body")) then + return -- Only chat type messages + end + + -- Stanza sent by a local client + local bare_jid = jid_bare(orig_from); + local target_session = origin; + local top_priority = false; + local user_sessions = bare_sessions[bare_jid]; + + -- Stanza about to be delivered to a local client + if not c2s then + bare_jid = jid_bare(orig_to); + target_session = full_sessions[orig_to]; + user_sessions = bare_sessions[bare_jid]; + if not target_session and user_sessions then + -- The top resources will already receive this message per normal routing rules, + -- so we are going to skip them in order to avoid sending duplicated messages. + local top_resources = user_sessions.top_resources; + top_priority = top_resources and top_resources[1].priority + end + end + + if not user_sessions then + module:log("debug", "Skip carbons for offline user"); + return -- No use in sending carbons to an offline user + end + + if stanza:get_child("private", xmlns_carbons) then + if not c2s then + stanza:maptags(function(tag) + if not ( tag.attr.xmlns == xmlns_carbons and tag.name == "private" ) then + return tag; + end + end); + end + module:log("debug", "Message tagged private, ignoring"); + return + elseif stanza:get_child("no-copy", "urn:xmpp:hints") then + module:log("debug", "Message has no-copy hint, ignoring"); + return + elseif stanza:get_child("x", "http://jabber.org/protocol/muc#user") then + module:log("debug", "MUC PM, ignoring"); + return + end + + -- Create the carbon copy and wrap it as per the Stanza Forwarding XEP + local copy = st.clone(stanza); + copy.attr.xmlns = "jabber:client"; + local carbon = st.message{ from = bare_jid, type = orig_type, } + :tag(c2s and "sent" or "received", { xmlns = xmlns_carbons }) + :tag("forwarded", { xmlns = xmlns_forward }) + :add_child(copy):reset(); + + user_sessions = user_sessions and user_sessions.sessions; + for _, session in pairs(user_sessions) do + -- Carbons are sent to resources that have enabled it + if session.want_carbons + -- but not the resource that sent the message, or the one that it's directed to + and session ~= target_session + -- and isn't among the top resources that would receive the message per standard routing rules + and (c2s or session.priority ~= top_priority) then + carbon.attr.to = session.full_jid; + module:log("debug", "Sending carbon to %s", session.full_jid); + session.send(carbon); + end + end +end + +local function c2s_message_handler(event) + return message_handler(event, true) +end + +-- Stanzas sent by local clients +module:hook("pre-message/host", c2s_message_handler, 1); +module:hook("pre-message/bare", c2s_message_handler, 1); +module:hook("pre-message/full", c2s_message_handler, 1); +-- Stanzas to local clients +module:hook("message/bare", message_handler, 1); +module:hook("message/full", message_handler, 1); + +module:add_feature(xmlns_carbons); diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua index 11abab79..a5136f6c 100644 --- a/plugins/mod_component.lua +++ b/plugins/mod_component.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -36,11 +36,13 @@ function module.add_host(module) local env = module.environment; env.connected = false; + env.session = false; local send; local function on_destroy(session, err) env.connected = false; + env.session = false; send = nil; session.on_destroy = nil; end @@ -73,12 +75,18 @@ function module.add_host(module) end if env.connected then - module:log("error", "Second component attempted to connect, denying connection"); - session:close{ condition = "conflict", text = "Component already connected" }; - return true; + local policy = module:get_option_string("component_conflict_resolve", "kick_new"); + if policy == "kick_old" then + env.session:close{ condition = "conflict", text = "Replaced by a new connection" }; + else -- kick_new + module:log("error", "Second component attempted to connect, denying connection"); + session:close{ condition = "conflict", text = "Component already connected" }; + return true; + end end env.connected = true; + env.session = session; send = session.send; session.on_destroy = on_destroy; session.component_validate_from = module:get_option_boolean("validate_from_addresses", true); @@ -178,9 +186,7 @@ function stream_callbacks.streamopened(session, attr) session.streamid = uuid_gen(); session.notopen = nil; -- Return stream header - session.send("<?xml version='1.0'?>"); - session.send(st.stanza("stream:stream", { xmlns=xmlns_component, - ["xmlns:stream"]='http://etherx.jabber.org/streams', id=session.streamid, from=session.host }):top_tag()); + session:open_stream(); end function stream_callbacks.streamclosed(session) diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua index 1ec4c85a..d49e3880 100644 --- a/plugins/mod_compression.lua +++ b/plugins/mod_compression.lua @@ -1,6 +1,6 @@ -- Prosody IM -- Copyright (C) 2009-2012 Tobias Markmann --- +-- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- @@ -26,16 +26,14 @@ end module:hook("stream-features", function(event) local origin, features = event.origin, event.features; - if not origin.compressed and (origin.type == "c2s" or origin.type == "s2sin" or origin.type == "s2sout") then - -- FIXME only advertise compression support when TLS layer has no compression enabled + if not origin.compressed and origin.type == "c2s" then features:add_child(compression_stream_feature); end end); module:hook("s2s-stream-features", function(event) local origin, features = event.origin, event.features; - -- FIXME only advertise compression support when TLS layer has no compression enabled - if not origin.compressed and (origin.type == "c2s" or origin.type == "s2sin" or origin.type == "s2sout") then + if not origin.compressed and origin.type == "s2sin" then features:add_child(compression_stream_feature); end end); @@ -43,13 +41,13 @@ end); -- Hook to activate compression if remote server supports it. module:hook_stanza(xmlns_stream, "features", function (session, stanza) - if not session.compressed and (session.type == "c2s" or session.type == "s2sin" or session.type == "s2sout") then + if not session.compressed and session.type == "s2sout" then -- does remote server support compression? - local comp_st = stanza:child_with_name("compression"); + local comp_st = stanza:get_child("compression", xmlns_compression_feature); if comp_st then -- do we support the mechanism - for a in comp_st:children() do - local algorithm = a[1] + for a in comp_st:childtags("method") do + local algorithm = a:get_text(); if algorithm == "zlib" then session.sends2s(st.stanza("compress", {xmlns=xmlns_compression_protocol}):tag("method"):text("zlib")) session.log("debug", "Enabled compression using zlib.") @@ -103,7 +101,7 @@ local function setup_compression(session, deflate_stream) return; end return compressed; - end); + end); end -- setup decompression for a stream @@ -131,13 +129,13 @@ module:hook("stanza/http://jabber.org/protocol/compress:compressed", function(ev -- create deflate and inflate streams local deflate_stream = get_deflate_stream(session); if not deflate_stream then return true; end - + local inflate_stream = get_inflate_stream(session); if not inflate_stream then return true; end - + -- setup compression for session.w setup_compression(session, deflate_stream); - + -- setup decompression for session.data setup_decompression(session, inflate_stream); session:reset_stream(); @@ -164,29 +162,28 @@ module:hook("stanza/http://jabber.org/protocol/compress:compress", function(even session.log("debug", "Client tried to establish another compression layer."); return true; end - + -- checking if the compression method is supported - local method = stanza:child_with_name("method"); - method = method and (method[1] or ""); + local method = stanza:get_child_text("method"); if method == "zlib" then session.log("debug", "zlib compression enabled."); - + -- create deflate and inflate streams local deflate_stream = get_deflate_stream(session); if not deflate_stream then return true; end - + local inflate_stream = get_inflate_stream(session); if not inflate_stream then return true; end - + (session.sends2s or session.send)(st.stanza("compressed", {xmlns=xmlns_compression_protocol})); session:reset_stream(); - + -- setup compression for session.w setup_compression(session, deflate_stream); - + -- setup decompression for session.data setup_decompression(session, inflate_stream); - + session.compressed = true; elseif method then session.log("debug", "%s compression selected, but we don't support it.", tostring(method)); diff --git a/plugins/mod_debug_sql.lua b/plugins/mod_debug_sql.lua new file mode 100644 index 00000000..7bbbbd88 --- /dev/null +++ b/plugins/mod_debug_sql.lua @@ -0,0 +1,25 @@ +-- Enables SQL query logging +-- +-- luacheck: ignore 213/uri + +local engines = module:shared("/*/sql/connections"); + +for uri, engine in pairs(engines) do + engine:debug(true); +end + +setmetatable(engines, { + __newindex = function (t, uri, engine) + engine:debug(true); + rawset(t, uri, engine); + end +}); + +function module.unload() + setmetatable(engines, nil); + for uri, engine in pairs(engines) do + engine:debug(false); + end +end + + diff --git a/plugins/mod_dialback.lua b/plugins/mod_dialback.lua index dc3c3f10..f0fe949a 100644 --- a/plugins/mod_dialback.lua +++ b/plugins/mod_dialback.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -14,20 +14,33 @@ local st = require "util.stanza"; local sha256_hash = require "util.hashes".sha256; local sha256_hmac = require "util.hashes".hmac_sha256; local nameprep = require "util.encodings".stringprep.nameprep; +local check_cert_status = module:depends"s2s".check_cert_status; +local uuid_gen = require"util.uuid".generate; local xmlns_stream = "http://etherx.jabber.org/streams"; local dialback_requests = setmetatable({}, { __mode = 'v' }); +local dialback_secret = sha256_hash(module:get_option_string("dialback_secret", uuid_gen()), true); +local dwd = module:get_option_boolean("dialback_without_dialback", false); + +function module.save() + return { dialback_secret = dialback_secret }; +end + +function module.restore(state) + dialback_secret = state.dialback_secret; +end + function generate_dialback(id, to, from) - return sha256_hmac(sha256_hash(hosts[from].dialback_secret), to .. ' ' .. from .. ' ' .. id, true); + return sha256_hmac(dialback_secret, to .. ' ' .. from .. ' ' .. id, true); end function initiate_dialback(session) -- generate dialback key session.dialback_key = generate_dialback(session.streamid, session.to_host, session.from_host); session.sends2s(st.stanza("db:result", { from = session.from_host, to = session.to_host }):text(session.dialback_key)); - session.log("info", "sent dialback key on outgoing s2s stream"); + session.log("debug", "sent dialback key on outgoing s2s stream"); end function verify_dialback(id, to, from, key) @@ -36,7 +49,7 @@ end module:hook("stanza/jabber:server:dialback:verify", function(event) local origin, stanza = event.origin, event.stanza; - + if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then -- We are being asked to verify the key, to ensure it was generated by us origin.log("debug", "verifying that dialback key is ours..."); @@ -63,26 +76,36 @@ end); module:hook("stanza/jabber:server:dialback:result", function(event) local origin, stanza = event.origin, event.stanza; - + if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then -- he wants to be identified through dialback -- We need to check the key with the Authoritative server local attr = stanza.attr; local to, from = nameprep(attr.to), nameprep(attr.from); - + if not hosts[to] then -- Not a host that we serve - origin.log("info", "%s tried to connect to %s, which we don't serve", from, to); + origin.log("warn", "%s tried to connect to %s, which we don't serve", from, to); origin:close("host-unknown"); return true; elseif not from then origin:close("improper-addressing"); end - + + if dwd and origin.secure then + if check_cert_status(origin, from) == false then + return + elseif origin.cert_chain_status == "valid" and origin.cert_identity_status == "valid" then + origin.sends2s(st.stanza("db:result", { to = from, from = to, id = attr.id, type = "valid" })); + module:fire_event("s2s-authenticated", { session = origin, host = from }); + return true; + end + end + origin.hosts[from] = { dialback_key = stanza[1] }; - + dialback_requests[from.."/"..origin.streamid] = origin; - + -- COMPAT: ejabberd, gmail and perhaps others do not always set 'to' and 'from' -- on streams. We fill in the session's to/from here instead. if not origin.from_host then @@ -103,7 +126,7 @@ end); module:hook("stanza/jabber:server:dialback:verify", function(event) local origin, stanza = event.origin, event.stanza; - + if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then local attr = stanza.attr; local dialback_verifying = dialback_requests[attr.from.."/"..(attr.id or "")]; @@ -132,10 +155,10 @@ end); module:hook("stanza/jabber:server:dialback:result", function(event) local origin, stanza = event.origin, event.stanza; - + if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then -- Remote server is telling us whether we passed dialback - + local attr = stanza.attr; if not hosts[attr.to] then origin:close("host-unknown"); @@ -154,14 +177,6 @@ module:hook("stanza/jabber:server:dialback:result", function(event) end end); -module:hook_stanza("urn:ietf:params:xml:ns:xmpp-sasl", "failure", function (origin, stanza) - if origin.external_auth == "failed" then - module:log("debug", "SASL EXTERNAL failed, falling back to dialback"); - initiate_dialback(origin); - return true; - end -end, 100); - module:hook_stanza(xmlns_stream, "features", function (origin, stanza) if not origin.external_auth or origin.external_auth == "failed" then module:log("debug", "Initiating dialback..."); diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua index 72c9a34c..61749580 100644 --- a/plugins/mod_disco.lua +++ b/plugins/mod_disco.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -32,7 +32,9 @@ do -- validate disco_items end end -module:add_identity("server", "im", module:get_option_string("name", "Prosody")); -- FIXME should be in the non-existing mod_router +if module:get_host_type() == "local" then + module:add_identity("server", "im", module:get_option_string("name", "Prosody")); -- FIXME should be in the non-existing mod_router +end module:add_feature("http://jabber.org/protocol/disco#info"); module:add_feature("http://jabber.org/protocol/disco#items"); @@ -97,7 +99,18 @@ module:hook("iq/host/http://jabber.org/protocol/disco#info:query", function(even local origin, stanza = event.origin, event.stanza; if stanza.attr.type ~= "get" then return; end local node = stanza.tags[1].attr.node; - if node and node ~= "" and node ~= "http://prosody.im#"..get_server_caps_hash() then return; end -- TODO fire event? + if node and node ~= "" and node ~= "http://prosody.im#"..get_server_caps_hash() then + local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node}); + local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; + local ret = module:fire_event("host-disco-info-node", event); + if ret ~= nil then return ret; end + if event.exists then + origin.send(reply); + else + origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); + end + return true; + end local reply_query = get_server_disco_info(); reply_query.node = node; local reply = st.reply(stanza):add_child(reply_query); @@ -108,9 +121,21 @@ module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(eve local origin, stanza = event.origin, event.stanza; if stanza.attr.type ~= "get" then return; end local node = stanza.tags[1].attr.node; - if node and node ~= "" then return; end -- TODO fire event? - + if node and node ~= "" then + local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items', node=node}); + local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; + local ret = module:fire_event("host-disco-items-node", event); + if ret ~= nil then return ret; end + if event.exists then + origin.send(reply); + else + origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); + end + return true; + end local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); + local ret = module:fire_event("host-disco-items", { origin = origin, stanza = stanza, reply = reply }); + if ret ~= nil then return ret; end for jid, name in pairs(get_children(module.host)) do reply:tag("item", {jid = jid, name = name~=true and name or nil}):up(); end @@ -133,12 +158,24 @@ module:hook("iq/bare/http://jabber.org/protocol/disco#info:query", function(even local origin, stanza = event.origin, event.stanza; if stanza.attr.type ~= "get" then return; end local node = stanza.tags[1].attr.node; - if node and node ~= "" then return; end -- TODO fire event? local username = jid_split(stanza.attr.to) or origin.username; if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then + if node and node ~= "" then + local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node}); + if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account + local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; + local ret = module:fire_event("account-disco-info-node", event); + if ret ~= nil then return ret; end + if event.exists then + origin.send(reply); + else + origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); + end + return true; + end local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info'}); if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account - module:fire_event("account-disco-info", { origin = origin, stanza = reply }); + module:fire_event("account-disco-info", { origin = origin, reply = reply }); origin.send(reply); return true; end @@ -147,12 +184,24 @@ module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", function(eve local origin, stanza = event.origin, event.stanza; if stanza.attr.type ~= "get" then return; end local node = stanza.tags[1].attr.node; - if node and node ~= "" then return; end -- TODO fire event? local username = jid_split(stanza.attr.to) or origin.username; if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then + if node and node ~= "" then + local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items', node=node}); + if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account + local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; + local ret = module:fire_event("account-disco-items-node", event); + if ret ~= nil then return ret; end + if event.exists then + origin.send(reply); + else + origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); + end + return true; + end local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items'}); if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account - module:fire_event("account-disco-items", { origin = origin, stanza = reply }); + module:fire_event("account-disco-items", { origin = origin, stanza = stanza, reply = reply }); origin.send(reply); return true; end diff --git a/plugins/mod_groups.lua b/plugins/mod_groups.lua index f7f632c2..d696d453 100644 --- a/plugins/mod_groups.lua +++ b/plugins/mod_groups.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -10,18 +10,18 @@ local groups; local members; -local groups_file; - local jid, datamanager = require "util.jid", require "util.datamanager"; local jid_prep = jid.prep; local module_host = module:get_host(); -function inject_roster_contacts(username, host, roster) +function inject_roster_contacts(event) + local username, host= event.username, event.host; --module:log("debug", "Injecting group members to roster"); local bare_jid = username.."@"..host; if not members[bare_jid] and not members[false] then return; end -- Not a member of any groups - + + local roster = event.roster; local function import_jids_to_roster(group_name) for jid in pairs(groups[group_name]) do -- Add them to roster @@ -48,7 +48,7 @@ function inject_roster_contacts(username, host, roster) import_jids_to_roster(group_name); end end - + -- Import public groups if members[false] then for _, group_name in ipairs(members[false]) do @@ -56,7 +56,7 @@ function inject_roster_contacts(username, host, roster) import_jids_to_roster(group_name); end end - + if roster[false] then roster[false].version = true; end @@ -80,12 +80,12 @@ function remove_virtual_contacts(username, host, datastore, data) end function module.load() - groups_file = module:get_option_string("groups_file"); + local groups_file = module:get_option_path("groups_file", nil, "config"); if not groups_file then return; end - + module:hook("roster-load", inject_roster_contacts); datamanager.add_callback(remove_virtual_contacts); - + groups = { default = {} }; members = { }; local curr_group = "default"; diff --git a/plugins/mod_http.lua b/plugins/mod_http.lua index 9b574bc8..086887fb 100644 --- a/plugins/mod_http.lua +++ b/plugins/mod_http.lua @@ -1,7 +1,7 @@ -- Prosody IM -- Copyright (C) 2008-2012 Matthew Wild -- Copyright (C) 2008-2012 Waqas Hussain --- +-- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- @@ -45,6 +45,11 @@ local function get_base_path(host_module, app_name, default_app_path) :gsub("%$(%w+)", { host = host_module.host }); end +local function redir_handler(event) + event.response.headers.location = event.request.path.."/"; + return 301; +end + local ports_by_scheme = { http = 80, https = 443, }; -- Helper to deduce a module's external URL @@ -101,6 +106,9 @@ function module.add_host(module) local path = event.request.path:sub(base_path_len); return _handler(event, path); end; + module:hook_object_event(server, event_name:sub(1, -3), redir_handler, -1); + elseif event_name:sub(-1, -1) == "/" then + module:hook_object_event(server, event_name:sub(1, -2), redir_handler, -1); end if not app_handlers[event_name] then app_handlers[event_name] = handler; @@ -119,7 +127,7 @@ function module.add_host(module) module:log("warn", "Not listening on any ports, '%s' will be unreachable", app_name); end end - + local function http_app_removed(event) local app_handlers = apps[event.item.name]; apps[event.item.name] = nil; @@ -127,7 +135,7 @@ function module.add_host(module) module:unhook_object_event(server, event, handler); end end - + module:handle_items("http-provider", http_app_added, http_app_removed); server.add_host(host); @@ -150,7 +158,13 @@ module:provides("net", { listener = server.listener; default_port = 5281; encryption = "ssl"; - ssl_config = { verify = "none" }; + ssl_config = { + verify = { + peer = false, + client_once = false, + "none", + } + }; multiplex = { pattern = "^[A-Z]"; }; diff --git a/plugins/mod_http_errors.lua b/plugins/mod_http_errors.lua index 2568ea80..0c37e104 100644 --- a/plugins/mod_http_errors.lua +++ b/plugins/mod_http_errors.lua @@ -53,7 +53,7 @@ local entities = { local function tohtml(plain) return (plain:gsub("[<>&'\"\n]", entities)); - + end local function get_page(code, extra) diff --git a/plugins/mod_http_files.lua b/plugins/mod_http_files.lua index 6275cca5..0c542714 100644 --- a/plugins/mod_http_files.lua +++ b/plugins/mod_http_files.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua index e7901ab4..c6d62e85 100644 --- a/plugins/mod_iq.lua +++ b/plugins/mod_iq.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/plugins/mod_lastactivity.lua b/plugins/mod_lastactivity.lua index 11053709..2dd61699 100644 --- a/plugins/mod_lastactivity.lua +++ b/plugins/mod_lastactivity.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -19,8 +19,7 @@ module:hook("pre-presence/bare", function(event) local stanza = event.stanza; if not(stanza.attr.to) and stanza.attr.type == "unavailable" then local t = os.time(); - local s = stanza:child_with_name("status"); - s = s and #s.tags == 0 and s[1] or ""; + local s = stanza:get_child_text("status"); map[event.origin.username] = {s = s, t = t}; end end, 10); diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua index 5fb66441..5edc26bb 100644 --- a/plugins/mod_legacyauth.lua +++ b/plugins/mod_legacyauth.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -11,8 +11,8 @@ local st = require "util.stanza"; local t_concat = table.concat; -local secure_auth_only = module:get_option("c2s_require_encryption") - or module:get_option("require_encryption") +local secure_auth_only = module:get_option("c2s_require_encryption", + module:get_option("require_encryption")) or not(module:get_option("allow_unencrypted_plain_auth")); local sessionmanager = require "core.sessionmanager"; @@ -43,10 +43,11 @@ module:hook("stanza/iq/jabber:iq:auth:query", function(event) session.send(st.error_reply(stanza, "modify", "not-acceptable", "Encryption (SSL or TLS) is required to connect to this server")); return true; end - - local username = stanza.tags[1]:child_with_name("username"); - local password = stanza.tags[1]:child_with_name("password"); - local resource = stanza.tags[1]:child_with_name("resource"); + + local query = stanza.tags[1]; + local username = query:get_child("username"); + local password = query:get_child("password"); + local resource = query:get_child("resource"); if not (username and password and resource) then local reply = st.reply(stanza); session.send(reply:query("jabber:iq:auth") diff --git a/plugins/mod_message.lua b/plugins/mod_message.lua index e85da613..fc337db0 100644 --- a/plugins/mod_message.lua +++ b/plugins/mod_message.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -17,7 +17,7 @@ local user_exists = require "core.usermanager".user_exists; local function process_to_bare(bare, origin, stanza) local user = bare_sessions[bare]; - + local t = stanza.attr.type; if t == "error" then -- discard @@ -66,7 +66,7 @@ end module:hook("message/full", function(data) -- message to full JID recieved local origin, stanza = data.origin, data.stanza; - + local session = full_sessions[stanza.attr.to]; if session and session.send(stanza) then return true; diff --git a/plugins/mod_motd.lua b/plugins/mod_motd.lua index 3dd6b816..574a9cf4 100644 --- a/plugins/mod_motd.lua +++ b/plugins/mod_motd.lua @@ -2,7 +2,7 @@ -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2010 Jeff Mitchell --- +-- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- diff --git a/plugins/mod_offline.lua b/plugins/mod_offline.lua index 1ac62f94..08ab8490 100644 --- a/plugins/mod_offline.lua +++ b/plugins/mod_offline.lua @@ -1,7 +1,7 @@ -- Prosody IM -- Copyright (C) 2008-2009 Matthew Wild -- Copyright (C) 2008-2009 Waqas Hussain --- +-- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- @@ -24,13 +24,13 @@ module:hook("message/offline/handle", function(event) else node, host = origin.username, origin.host; end - + stanza.attr.stamp, stanza.attr.stamp_legacy = datetime.datetime(), datetime.legacy(); local result = datamanager.list_append(node, host, "offline", st.preserialize(stanza)); stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil; - + return result; -end); +end, -1); module:hook("message/offline/broadcast", function(event) local origin = event.origin; @@ -48,4 +48,4 @@ module:hook("message/offline/broadcast", function(event) end datamanager.list_store(node, host, "offline", nil); return true; -end); +end, -1); diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua index 22790869..896f3e78 100644 --- a/plugins/mod_pep.lua +++ b/plugins/mod_pep.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -46,7 +46,8 @@ local function subscription_presence(user_bare, recipient) return is_contact_subscribed(username, host, recipient_bare); end -local function publish(session, node, id, item) +module:hook("pep-publish-item", function (event) + local session, node, id, item = event.session, event.node, event.id, event.item; item.attr.xmlns = nil; local disable = #item.tags ~= 1 or #item.tags[1] == 0; if #item.tags == 0 then item.name = "retract"; end @@ -77,7 +78,8 @@ local function publish(session, node, id, item) core_post_stanza(session, stanza); end end -end +end); + local function publish_all(user, recipient, session) local d = data[user]; local notify = recipients[user] and recipients[user][recipient]; @@ -180,7 +182,9 @@ module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event) local id = payload.attr.id or "1"; payload.attr.id = id; session.send(st.reply(stanza)); - publish(session, node, id, st.clone(payload)); + module:fire_event("pep-publish-item", { + node = node, actor = session.jid, id = id, session = session, item = st.clone(payload); + }); return true; end end @@ -271,19 +275,19 @@ module:hook("iq-result/bare/disco", function(event) end); module:hook("account-disco-info", function(event) - local stanza = event.stanza; - stanza:tag('identity', {category='pubsub', type='pep'}):up(); - stanza:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up(); + local reply = event.reply; + reply:tag('identity', {category='pubsub', type='pep'}):up(); + reply:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up(); end); module:hook("account-disco-items", function(event) - local stanza = event.stanza; - local bare = stanza.attr.to; + local reply = event.reply; + local bare = reply.attr.to; local user_data = data[bare]; if user_data then for node, _ in pairs(user_data) do - stanza:tag('item', {jid=bare, node=node}):up(); -- TODO we need to handle queries to these nodes + reply:tag('item', {jid=bare, node=node}):up(); -- TODO we need to handle queries to these nodes end end end); diff --git a/plugins/mod_ping.lua b/plugins/mod_ping.lua index 0bfcac66..1a503409 100644 --- a/plugins/mod_ping.lua +++ b/plugins/mod_ping.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -11,14 +11,11 @@ local st = require "util.stanza"; module:add_feature("urn:xmpp:ping"); local function ping_handler(event) - if event.stanza.attr.type == "get" then - event.origin.send(st.reply(event.stanza)); - return true; - end + return event.origin.send(st.reply(event.stanza)); end -module:hook("iq/bare/urn:xmpp:ping:ping", ping_handler); -module:hook("iq/host/urn:xmpp:ping:ping", ping_handler); +module:hook("iq-get/bare/urn:xmpp:ping:ping", ping_handler); +module:hook("iq-get/host/urn:xmpp:ping:ping", ping_handler); -- Ad-hoc command diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua index b289fa44..7e6d8799 100644 --- a/plugins/mod_posix.lua +++ b/plugins/mod_posix.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -14,8 +14,8 @@ if pposix._VERSION ~= want_pposix_version then module:log("warn", "Unknown version (%s) of binary pposix module, expected %s. Perhaps you need to recompile?", tostring(pposix._VERSION), want_pposix_version); end -local signal = select(2, pcall(require, "util.signal")); -if type(signal) == "string" then +local have_signal, signal = pcall(require, "util.signal"); +if not have_signal then module:log("warn", "Couldn't load signal library, won't respond to SIGTERM"); end @@ -31,27 +31,27 @@ pposix.umask(umask); -- Allow switching away from root, some people like strange ports. module:hook("server-started", function () - local uid = module:get_option("setuid"); - local gid = module:get_option("setgid"); - if gid then - local success, msg = pposix.setgid(gid); - if success then - module:log("debug", "Changed group to %s successfully.", gid); - else - module:log("error", "Failed to change group to %s. Error: %s", gid, msg); - prosody.shutdown("Failed to change group to %s", gid); - end + local uid = module:get_option("setuid"); + local gid = module:get_option("setgid"); + if gid then + local success, msg = pposix.setgid(gid); + if success then + module:log("debug", "Changed group to %s successfully.", gid); + else + module:log("error", "Failed to change group to %s. Error: %s", gid, msg); + prosody.shutdown("Failed to change group to %s", gid); end - if uid then - local success, msg = pposix.setuid(uid); - if success then - module:log("debug", "Changed user to %s successfully.", uid); - else - module:log("error", "Failed to change user to %s. Error: %s", uid, msg); - prosody.shutdown("Failed to change user to %s", uid); - end + end + if uid then + local success, msg = pposix.setuid(uid); + if success then + module:log("debug", "Changed user to %s successfully.", uid); + else + module:log("error", "Failed to change user to %s. Error: %s", uid, msg); + prosody.shutdown("Failed to change user to %s", uid); end - end); + end +end); -- Don't even think about it! if not prosody.start_time then -- server-starting @@ -128,15 +128,7 @@ function syslog_sink_maker(config) end require "core.loggingmanager".register_sink_type("syslog", syslog_sink_maker); -local daemonize = module:get_option("daemonize"); -if daemonize == nil then - local no_daemonize = module:get_option("no_daemonize"); --COMPAT w/ 0.5 - daemonize = not no_daemonize; - if no_daemonize ~= nil then - module:log("warn", "The 'no_daemonize' option is now replaced by 'daemonize'"); - module:log("warn", "Update your config from 'no_daemonize = %s' to 'daemonize = %s'", tostring(no_daemonize), tostring(daemonize)); - end -end +local daemonize = module:get_option("daemonize", prosody.installed); local function remove_log_sinks() local lm = require "core.loggingmanager"; @@ -170,7 +162,7 @@ end module:hook("server-stopped", remove_pidfile); -- Set signal handlers -if signal.signal then +if have_signal then signal.signal("SIGTERM", function () module:log("warn", "Received SIGTERM"); prosody.unlock_globals(); @@ -183,7 +175,7 @@ if signal.signal then prosody.reload_config(); prosody.reopen_logfiles(); end); - + signal.signal("SIGINT", function () module:log("info", "Received SIGINT"); prosody.unlock_globals(); diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua index 8dac2d35..ab57a158 100644 --- a/plugins/mod_presence.lua +++ b/plugins/mod_presence.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -55,14 +55,14 @@ local ignore_presence_priority = module:get_option("ignore_presence_priority"); function handle_normal_presence(origin, stanza) if ignore_presence_priority then - local priority = stanza:child_with_name("priority"); + local priority = stanza:get_child("priority"); if priority and priority[1] ~= "0" then for i=#priority.tags,1,-1 do priority.tags[i] = nil; end for i=#priority,1,-1 do priority[i] = nil; end priority[1] = "0"; end end - local priority = stanza:child_with_name("priority"); + local priority = stanza:get_child("priority"); if priority and #priority > 0 then priority = t_concat(priority); if s_find(priority, "^[+-]?[0-9]+$") then @@ -90,6 +90,7 @@ function handle_normal_presence(origin, stanza) end end if stanza.attr.type == nil and not origin.presence then -- initial presence + module:fire_event("presence/initial", { origin = origin, stanza = stanza } ); origin.presence = stanza; -- FIXME repeated later local probe = st.presence({from = origin.full_jid, type = "probe"}); for jid, item in pairs(roster) do -- probe all contacts we are subscribed to @@ -105,10 +106,8 @@ function handle_normal_presence(origin, stanza) res.presence.attr.to = nil; end end - if roster.pending then -- resend incoming subscription requests - for jid in pairs(roster.pending) do - origin.send(st.presence({type="subscribe", from=jid})); -- TODO add to attribute? Use original? - end + for jid in pairs(roster[false].pending) do -- resend incoming subscription requests + origin.send(st.presence({type="subscribe", from=jid})); -- TODO add to attribute? Use original? end local request = st.presence({type="subscribe", from=origin.username.."@"..origin.host}); for jid, item in pairs(roster) do -- resend outgoing subscription requests @@ -227,7 +226,7 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b local st_from, st_to = stanza.attr.from, stanza.attr.to; stanza.attr.from, stanza.attr.to = from_bare, to_bare; log("debug", "inbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare); - + if stanza.attr.type == "probe" then local result, err = rostermanager.is_contact_subscribed(node, host, from_bare); if result then @@ -312,7 +311,7 @@ module:hook("presence/bare", function(data) if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to)); end - + local user = bare_sessions[to]; if user then for _, session in pairs(user.sessions) do @@ -347,7 +346,7 @@ end); module:hook("presence/host", function(data) -- inbound presence to the host local stanza = data.stanza; - + local from_bare = jid_bare(stanza.attr.from); local t = stanza.attr.type; if t == "probe" then diff --git a/plugins/mod_privacy.lua b/plugins/mod_privacy.lua index 49c9427f..b749b7c7 100644 --- a/plugins/mod_privacy.lua +++ b/plugins/mod_privacy.lua @@ -2,447 +2,12 @@ -- Copyright (C) 2009-2010 Matthew Wild -- Copyright (C) 2009-2010 Waqas Hussain -- Copyright (C) 2009 Thilo Cestonaro --- +-- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -module:add_feature("jabber:iq:privacy"); - -local st = require "util.stanza"; -local bare_sessions, full_sessions = prosody.bare_sessions, prosody.full_sessions; -local util_Jid = require "util.jid"; -local jid_bare = util_Jid.bare; -local jid_split, jid_join = util_Jid.split, util_Jid.join; -local load_roster = require "core.rostermanager".load_roster; -local to_number = tonumber; - -local privacy_storage = module:open_store(); - -function isListUsed(origin, name, privacy_lists) - local user = bare_sessions[origin.username.."@"..origin.host]; - if user then - for resource, session in pairs(user.sessions) do - if resource ~= origin.resource then - if session.activePrivacyList == name then - return true; - elseif session.activePrivacyList == nil and privacy_lists.default == name then - return true; - end - end - end - end -end - -function isAnotherSessionUsingDefaultList(origin) - local user = bare_sessions[origin.username.."@"..origin.host]; - if user then - for resource, session in pairs(user.sessions) do - if resource ~= origin.resource and session.activePrivacyList == nil then - return true; - end - end - end -end - -function declineList(privacy_lists, origin, stanza, which) - if which == "default" then - if isAnotherSessionUsingDefaultList(origin) then - return { "cancel", "conflict", "Another session is online and using the default list."}; - end - privacy_lists.default = nil; - origin.send(st.reply(stanza)); - elseif which == "active" then - origin.activePrivacyList = nil; - origin.send(st.reply(stanza)); - else - return {"modify", "bad-request", "Neither default nor active list specifed to decline."}; - end - return true; -end - -function activateList(privacy_lists, origin, stanza, which, name) - local list = privacy_lists.lists[name]; - - if which == "default" and list then - if isAnotherSessionUsingDefaultList(origin) then - return {"cancel", "conflict", "Another session is online and using the default list."}; - end - privacy_lists.default = name; - origin.send(st.reply(stanza)); - elseif which == "active" and list then - origin.activePrivacyList = name; - origin.send(st.reply(stanza)); - elseif not list then - return {"cancel", "item-not-found", "No such list: "..name}; - else - return {"modify", "bad-request", "No list chosen to be active or default."}; - end - return true; -end - -function deleteList(privacy_lists, origin, stanza, name) - local list = privacy_lists.lists[name]; - - if list then - if isListUsed(origin, name, privacy_lists) then - return {"cancel", "conflict", "Another session is online and using the list which should be deleted."}; - end - if privacy_lists.default == name then - privacy_lists.default = nil; - end - if origin.activePrivacyList == name then - origin.activePrivacyList = nil; - end - privacy_lists.lists[name] = nil; - origin.send(st.reply(stanza)); - return true; - end - return {"modify", "bad-request", "Not existing list specifed to be deleted."}; -end - -function createOrReplaceList (privacy_lists, origin, stanza, name, entries) - local bare_jid = origin.username.."@"..origin.host; - - if privacy_lists.lists == nil then - privacy_lists.lists = {}; - end - - local list = {}; - privacy_lists.lists[name] = list; - - local orderCheck = {}; - list.name = name; - list.items = {}; - - for _,item in ipairs(entries) do - if to_number(item.attr.order) == nil or to_number(item.attr.order) < 0 or orderCheck[item.attr.order] ~= nil then - return {"modify", "bad-request", "Order attribute not valid."}; - end - - if item.attr.type ~= nil and item.attr.type ~= "jid" and item.attr.type ~= "subscription" and item.attr.type ~= "group" then - return {"modify", "bad-request", "Type attribute not valid."}; - end - - local tmp = {}; - orderCheck[item.attr.order] = true; - - tmp["type"] = item.attr.type; - tmp["value"] = item.attr.value; - tmp["action"] = item.attr.action; - tmp["order"] = to_number(item.attr.order); - tmp["presence-in"] = false; - tmp["presence-out"] = false; - tmp["message"] = false; - tmp["iq"] = false; - - if #item.tags > 0 then - for _,tag in ipairs(item.tags) do - tmp[tag.name] = true; - end - end - - if tmp.type == "subscription" then - if tmp.value ~= "both" and - tmp.value ~= "to" and - tmp.value ~= "from" and - tmp.value ~= "none" then - return {"cancel", "bad-request", "Subscription value must be both, to, from or none."}; - end - end - - if tmp.action ~= "deny" and tmp.action ~= "allow" then - return {"cancel", "bad-request", "Action must be either deny or allow."}; - end - list.items[#list.items + 1] = tmp; - end - - table.sort(list.items, function(a, b) return a.order < b.order; end); - - origin.send(st.reply(stanza)); - if bare_sessions[bare_jid] ~= nil then - local iq = st.iq ( { type = "set", id="push1" } ); - iq:tag ("query", { xmlns = "jabber:iq:privacy" } ); - iq:tag ("list", { name = list.name } ):up(); - iq:up(); - for resource, session in pairs(bare_sessions[bare_jid].sessions) do - iq.attr.to = bare_jid.."/"..resource - session.send(iq); - end - else - return {"cancel", "bad-request", "internal error."}; - end - return true; -end - -function getList(privacy_lists, origin, stanza, name) - local reply = st.reply(stanza); - reply:tag("query", {xmlns="jabber:iq:privacy"}); - - if name == nil then - if privacy_lists.lists then - if origin.activePrivacyList then - reply:tag("active", {name=origin.activePrivacyList}):up(); - end - if privacy_lists.default then - reply:tag("default", {name=privacy_lists.default}):up(); - end - for name,list in pairs(privacy_lists.lists) do - reply:tag("list", {name=name}):up(); - end - end - else - local list = privacy_lists.lists[name]; - if list then - reply = reply:tag("list", {name=list.name}); - for _,item in ipairs(list.items) do - reply:tag("item", {type=item.type, value=item.value, action=item.action, order=item.order}); - if item["message"] then reply:tag("message"):up(); end - if item["iq"] then reply:tag("iq"):up(); end - if item["presence-in"] then reply:tag("presence-in"):up(); end - if item["presence-out"] then reply:tag("presence-out"):up(); end - reply:up(); - end - else - return {"cancel", "item-not-found", "Unknown list specified."}; - end - end - - origin.send(reply); - return true; -end - -module:hook("iq/bare/jabber:iq:privacy:query", function(data) - local origin, stanza = data.origin, data.stanza; - - if stanza.attr.to == nil then -- only service requests to own bare JID - local query = stanza.tags[1]; -- the query element - local valid = false; - local privacy_lists = privacy_storage:get(origin.username) or { lists = {} }; - - if privacy_lists.lists[1] then -- Code to migrate from old privacy lists format, remove in 0.8 - module:log("info", "Upgrading format of stored privacy lists for %s@%s", origin.username, origin.host); - local lists = privacy_lists.lists; - for idx, list in ipairs(lists) do - lists[list.name] = list; - lists[idx] = nil; - end - end - - if stanza.attr.type == "set" then - if #query.tags == 1 then -- the <query/> element MUST NOT include more than one child element - for _,tag in ipairs(query.tags) do - if tag.name == "active" or tag.name == "default" then - if tag.attr.name == nil then -- Client declines the use of active / default list - valid = declineList(privacy_lists, origin, stanza, tag.name); - else -- Client requests change of active / default list - valid = activateList(privacy_lists, origin, stanza, tag.name, tag.attr.name); - end - elseif tag.name == "list" and tag.attr.name then -- Client adds / edits a privacy list - if #tag.tags == 0 then -- Client removes a privacy list - valid = deleteList(privacy_lists, origin, stanza, tag.attr.name); - else -- Client edits a privacy list - valid = createOrReplaceList(privacy_lists, origin, stanza, tag.attr.name, tag.tags); - end - end - end - end - elseif stanza.attr.type == "get" then - local name = nil; - local listsToRetrieve = 0; - if #query.tags >= 1 then - for _,tag in ipairs(query.tags) do - if tag.name == "list" then -- Client requests a privacy list from server - name = tag.attr.name; - listsToRetrieve = listsToRetrieve + 1; - end - end - end - if listsToRetrieve == 0 or listsToRetrieve == 1 then - valid = getList(privacy_lists, origin, stanza, name); - end - end - - if valid ~= true then - valid = valid or { "cancel", "bad-request", "Couldn't understand request" }; - if valid[1] == nil then - valid[1] = "cancel"; - end - if valid[2] == nil then - valid[2] = "bad-request"; - end - origin.send(st.error_reply(stanza, valid[1], valid[2], valid[3])); - else - privacy_storage:set(origin.username, privacy_lists); - end - return true; - end -end); - -function checkIfNeedToBeBlocked(e, session) - local origin, stanza = e.origin, e.stanza; - local privacy_lists = privacy_storage:get(session.username) or {}; - local bare_jid = session.username.."@"..session.host; - local to = stanza.attr.to or bare_jid; - local from = stanza.attr.from; - - local is_to_user = bare_jid == jid_bare(to); - local is_from_user = bare_jid == jid_bare(from); - - --module:log("debug", "stanza: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from)); - - if privacy_lists.lists == nil or - not (session.activePrivacyList or privacy_lists.default) - then - return; -- Nothing to block, default is Allow all - end - if is_from_user and is_to_user then - --module:log("debug", "Not blocking communications between user's resources"); - return; -- from one of a user's resource to another => HANDS OFF! - end - - local listname = session.activePrivacyList; - if listname == nil then - listname = privacy_lists.default; -- no active list selected, use default list - end - local list = privacy_lists.lists[listname]; - if not list then -- should never happen - module:log("warn", "given privacy list not found. name: %s for user %s", listname, bare_jid); - return; - end - for _,item in ipairs(list.items) do - local apply = false; - local block = false; - if ( - (stanza.name == "message" and item.message) or - (stanza.name == "iq" and item.iq) or - (stanza.name == "presence" and is_to_user and item["presence-in"]) or - (stanza.name == "presence" and is_from_user and item["presence-out"]) or - (item.message == false and item.iq == false and item["presence-in"] == false and item["presence-out"] == false) - ) then - apply = true; - end - if apply then - local evilJid = {}; - apply = false; - if is_to_user then - --module:log("debug", "evil jid is (from): %s", from); - evilJid.node, evilJid.host, evilJid.resource = jid_split(from); - else - --module:log("debug", "evil jid is (to): %s", to); - evilJid.node, evilJid.host, evilJid.resource = jid_split(to); - end - if item.type == "jid" and - (evilJid.node and evilJid.host and evilJid.resource and item.value == evilJid.node.."@"..evilJid.host.."/"..evilJid.resource) or - (evilJid.node and evilJid.host and item.value == evilJid.node.."@"..evilJid.host) or - (evilJid.host and evilJid.resource and item.value == evilJid.host.."/"..evilJid.resource) or - (evilJid.host and item.value == evilJid.host) then - apply = true; - block = (item.action == "deny"); - elseif item.type == "group" then - local roster = load_roster(session.username, session.host); - local roster_entry = roster[jid_join(evilJid.node, evilJid.host)]; - if roster_entry then - local groups = roster_entry.groups; - for group in pairs(groups) do - if group == item.value then - apply = true; - block = (item.action == "deny"); - break; - end - end - end - elseif item.type == "subscription" then -- we need a valid bare evil jid - local roster = load_roster(session.username, session.host); - local roster_entry = roster[jid_join(evilJid.node, evilJid.host)]; - if (not(roster_entry) and item.value == "none") - or (roster_entry and roster_entry.subscription == item.value) then - apply = true; - block = (item.action == "deny"); - end - elseif item.type == nil then - apply = true; - block = (item.action == "deny"); - end - end - if apply then - if block then - -- drop and not bounce groupchat messages, otherwise users will get kicked - if stanza.attr.type == "groupchat" then - return true; - end - module:log("debug", "stanza blocked: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from)); - if stanza.name == "message" then - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); - elseif stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); - end - return true; -- stanza blocked ! - else - --module:log("debug", "stanza explicitly allowed!") - return; - end - end - end -end - -function preCheckIncoming(e) - local session; - if e.stanza.attr.to ~= nil then - local node, host, resource = jid_split(e.stanza.attr.to); - if node == nil or host == nil then - return; - end - if resource == nil then - local prio = 0; - if bare_sessions[node.."@"..host] ~= nil then - for resource, session_ in pairs(bare_sessions[node.."@"..host].sessions) do - if session_.priority ~= nil and session_.priority > prio then - session = session_; - prio = session_.priority; - end - end - end - else - session = full_sessions[node.."@"..host.."/"..resource]; - end - if session ~= nil then - return checkIfNeedToBeBlocked(e, session); - else - --module:log("debug", "preCheckIncoming: Couldn't get session for jid: %s@%s/%s", tostring(node), tostring(host), tostring(resource)); - end - end -end - -function preCheckOutgoing(e) - local session = e.origin; - if e.stanza.attr.from == nil then - e.stanza.attr.from = session.username .. "@" .. session.host; - if session.resource ~= nil then - e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource; - end - end - if session.username then -- FIXME do properly - return checkIfNeedToBeBlocked(e, session); - end -end - -module:hook("pre-message/full", preCheckOutgoing, 500); -module:hook("pre-message/bare", preCheckOutgoing, 500); -module:hook("pre-message/host", preCheckOutgoing, 500); -module:hook("pre-iq/full", preCheckOutgoing, 500); -module:hook("pre-iq/bare", preCheckOutgoing, 500); -module:hook("pre-iq/host", preCheckOutgoing, 500); -module:hook("pre-presence/full", preCheckOutgoing, 500); -module:hook("pre-presence/bare", preCheckOutgoing, 500); -module:hook("pre-presence/host", preCheckOutgoing, 500); -module:hook("message/full", preCheckIncoming, 500); -module:hook("message/bare", preCheckIncoming, 500); -module:hook("message/host", preCheckIncoming, 500); -module:hook("iq/full", preCheckIncoming, 500); -module:hook("iq/bare", preCheckIncoming, 500); -module:hook("iq/host", preCheckIncoming, 500); -module:hook("presence/full", preCheckIncoming, 500); -module:hook("presence/bare", preCheckIncoming, 500); -module:hook("presence/host", preCheckIncoming, 500); +-- COMPAT w/ pre 0.10 +module:log("error", "The mod_privacy plugin has been replaced by mod_blocklist. Please update your config. For more information see https://prosody.im/doc/modules/mod_privacy"); +module:depends("blocklist"); diff --git a/plugins/mod_private.lua b/plugins/mod_private.lua index 365a997c..c01053d5 100644 --- a/plugins/mod_private.lua +++ b/plugins/mod_private.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -15,38 +15,40 @@ module:add_feature("jabber:iq:private"); module:hook("iq/self/jabber:iq:private:query", function(event) local origin, stanza = event.origin, event.stanza; - local type = stanza.attr.type; local query = stanza.tags[1]; - if #query.tags == 1 then - local tag = query.tags[1]; - local key = tag.name..":"..tag.attr.xmlns; - local data, err = private_storage:get(origin.username); - if err then - origin.send(st.error_reply(stanza, "wait", "internal-server-error")); + if #query.tags ~= 1 then + origin.send(st.error_reply(stanza, "modify", "bad-format")); + return true; + end + local tag = query.tags[1]; + local key = tag.name..":"..tag.attr.xmlns; + local data, err = private_storage:get(origin.username); + if err then + origin.send(st.error_reply(stanza, "wait", "internal-server-error", err)); + return true; + end + if stanza.attr.type == "get" then + if data and data[key] then + origin.send(st.reply(stanza):query("jabber:iq:private"):add_child(st.deserialize(data[key]))); + return true; + else + origin.send(st.reply(stanza):add_child(query)); return true; end - if stanza.attr.type == "get" then - if data and data[key] then - origin.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:private"}):add_child(st.deserialize(data[key]))); - else - origin.send(st.reply(stanza):add_child(stanza.tags[1])); - end - else -- set - if not data then data = {}; end; - if #tag == 0 then - data[key] = nil; - else - data[key] = st.preserialize(tag); - end - -- TODO delete datastore if empty - if private_storage:set(origin.username, data) then - origin.send(st.reply(stanza)); - else - origin.send(st.error_reply(stanza, "wait", "internal-server-error")); - end + else -- type == set + if not data then data = {}; end; + if #tag == 0 then + data[key] = nil; + else + data[key] = st.preserialize(tag); end - else - origin.send(st.error_reply(stanza, "modify", "bad-format")); + -- TODO delete datastore if empty + local ok, err = private_storage:set(origin.username, data); + if not ok then + origin.send(st.error_reply(stanza, "wait", "internal-server-error", err)); + return true; + end + origin.send(st.reply(stanza)); + return true; end - return true; end); diff --git a/plugins/mod_proxy65.lua b/plugins/mod_proxy65.lua index 1fa42bd8..cbbfad12 100644 --- a/plugins/mod_proxy65.lua +++ b/plugins/mod_proxy65.lua @@ -2,7 +2,7 @@ -- Copyright (C) 2008-2011 Matthew Wild -- Copyright (C) 2008-2011 Waqas Hussain -- Copyright (C) 2009 Thilo Cestonaro --- +-- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- @@ -30,7 +30,7 @@ function listener.onincoming(conn, data) (conn == initiator and target or initiator):write(data); return; end -- FIXME server.link should be doing this? - + if not session.greeting_done then local nmethods = data:byte(2) or 0; if data:byte(1) == 0x05 and nmethods > 0 and #data == 2 + nmethods then -- check if we have all the data @@ -90,10 +90,10 @@ end function module.add_host(module) local host, name = module:get_host(), module:get_option_string("name", "SOCKS5 Bytestreams Service"); - - local proxy_address = module:get_option("proxy65_address", host); + + local proxy_address = module:get_option_string("proxy65_address", host); local proxy_port = next(portmanager.get_active_services():search("proxy65", nil)[1] or {}); - local proxy_acl = module:get_option("proxy65_acl"); + local proxy_acl = module:get_option_array("proxy65_acl"); -- COMPAT w/pre-0.9 where proxy65_port was specified in the components section of the config local legacy_config = module:get_option_number("proxy65_port"); @@ -101,30 +101,13 @@ function module.add_host(module) module:log("warn", "proxy65_port is deprecated, please put proxy65_ports = { %d } into the global section instead", legacy_config); end + module:depends("disco"); module:add_identity("proxy", "bytestreams", name); module:add_feature("http://jabber.org/protocol/bytestreams"); - - module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function(event) - local origin, stanza = event.origin, event.stanza; - if not stanza.tags[1].attr.node then - origin.send(st.reply(stanza):query("http://jabber.org/protocol/disco#info") - :tag("identity", {category='proxy', type='bytestreams', name=name}):up() - :tag("feature", {var="http://jabber.org/protocol/bytestreams"}) ); - return true; - end - end, -1); - - module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function(event) - local origin, stanza = event.origin, event.stanza; - if not stanza.tags[1].attr.node then - origin.send(st.reply(stanza):query("http://jabber.org/protocol/disco#items")); - return true; - end - end, -1); - + module:hook("iq-get/host/http://jabber.org/protocol/bytestreams:query", function(event) local origin, stanza = event.origin, event.stanza; - + -- check ACL while proxy_acl and #proxy_acl > 0 do -- using 'while' instead of 'if' so we can break out of it local jid = stanza.attr.from; @@ -137,22 +120,22 @@ function module.add_host(module) origin.send(st.error_reply(stanza, "auth", "forbidden")); return true; end - + local sid = stanza.tags[1].attr.sid; origin.send(st.reply(stanza):tag("query", {xmlns="http://jabber.org/protocol/bytestreams", sid=sid}) :tag("streamhost", {jid=host, host=proxy_address, port=proxy_port})); return true; end); - + module:hook("iq-set/host/http://jabber.org/protocol/bytestreams:query", function(event) local origin, stanza = event.origin, event.stanza; - + local query = stanza.tags[1]; local sid = query.attr.sid; local from = stanza.attr.from; local to = query:get_child_text("activate"); local prepped_to = jid_prep(to); - + local info = "sid: "..tostring(sid)..", initiator: "..tostring(from)..", target: "..tostring(prepped_to or to); if prepped_to and sid then local sha = sha1(sid .. from .. prepped_to, true); diff --git a/plugins/mod_pubsub.lua b/plugins/mod_pubsub.lua deleted file mode 100644 index 04f2b615..00000000 --- a/plugins/mod_pubsub.lua +++ /dev/null @@ -1,463 +0,0 @@ -local pubsub = require "util.pubsub"; -local st = require "util.stanza"; -local jid_bare = require "util.jid".bare; -local uuid_generate = require "util.uuid".generate; -local usermanager = require "core.usermanager"; - -local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; -local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors"; -local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event"; -local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; - -local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false); -local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false); -local pubsub_disco_name = module:get_option("name"); -if type(pubsub_disco_name) ~= "string" then pubsub_disco_name = "Prosody PubSub Service"; end - -local service; - -local handlers = {}; - -function handle_pubsub_iq(event) - local origin, stanza = event.origin, event.stanza; - local pubsub = stanza.tags[1]; - local action = pubsub.tags[1]; - if not action then - return origin.send(st.error_reply(stanza, "cancel", "bad-request")); - end - local handler = handlers[stanza.attr.type.."_"..action.name]; - if handler then - handler(origin, stanza, action); - return true; - end -end - -local pubsub_errors = { - ["conflict"] = { "cancel", "conflict" }; - ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" }; - ["jid-required"] = { "modify", "bad-request", nil, "jid-required" }; - ["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" }; - ["item-not-found"] = { "cancel", "item-not-found" }; - ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" }; - ["forbidden"] = { "auth", "forbidden" }; -}; -function pubsub_error_reply(stanza, error) - local e = pubsub_errors[error]; - local reply = st.error_reply(stanza, unpack(e, 1, 3)); - if e[4] then - reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up(); - end - return reply; -end - -function handlers.get_items(origin, stanza, items) - local node = items.attr.node; - local item = items:get_child("item"); - local id = item and item.attr.id; - - if not node then - return origin.send(pubsub_error_reply(stanza, "nodeid-required")); - end - local ok, results = service:get_items(node, stanza.attr.from, id); - if not ok then - return origin.send(pubsub_error_reply(stanza, results)); - end - - local data = st.stanza("items", { node = node }); - for _, entry in pairs(results) do - data:add_child(entry); - end - local reply; - if data then - reply = st.reply(stanza) - :tag("pubsub", { xmlns = xmlns_pubsub }) - :add_child(data); - else - reply = pubsub_error_reply(stanza, "item-not-found"); - end - return origin.send(reply); -end - -function handlers.get_subscriptions(origin, stanza, subscriptions) - local node = subscriptions.attr.node; - local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from); - if not ok then - return origin.send(pubsub_error_reply(stanza, ret)); - end - local reply = st.reply(stanza) - :tag("pubsub", { xmlns = xmlns_pubsub }) - :tag("subscriptions"); - for _, sub in ipairs(ret) do - reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up(); - end - return origin.send(reply); -end - -function handlers.set_create(origin, stanza, create) - local node = create.attr.node; - local ok, ret, reply; - if node then - ok, ret = service:create(node, stanza.attr.from); - if ok then - reply = st.reply(stanza); - else - reply = pubsub_error_reply(stanza, ret); - end - else - repeat - node = uuid_generate(); - ok, ret = service:create(node, stanza.attr.from); - until ok or ret ~= "conflict"; - if ok then - reply = st.reply(stanza) - :tag("pubsub", { xmlns = xmlns_pubsub }) - :tag("create", { node = node }); - else - reply = pubsub_error_reply(stanza, ret); - end - end - return origin.send(reply); -end - -function handlers.set_delete(origin, stanza, delete) - local node = delete.attr.node; - - local reply, notifier; - if not node then - return origin.send(pubsub_error_reply(stanza, "nodeid-required")); - end - local ok, ret = service:delete(node, stanza.attr.from); - if ok then - reply = st.reply(stanza); - else - reply = pubsub_error_reply(stanza, ret); - end - return origin.send(reply); -end - -function handlers.set_subscribe(origin, stanza, subscribe) - local node, jid = subscribe.attr.node, subscribe.attr.jid; - if not (node and jid) then - return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); - end - --[[ - local options_tag, options = stanza.tags[1]:get_child("options"), nil; - if options_tag then - options = options_form:data(options_tag.tags[1]); - end - --]] - local options_tag, options; -- FIXME - local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options); - local reply; - if ok then - reply = st.reply(stanza) - :tag("pubsub", { xmlns = xmlns_pubsub }) - :tag("subscription", { - node = node, - jid = jid, - subscription = "subscribed" - }):up(); - if options_tag then - reply:add_child(options_tag); - end - else - reply = pubsub_error_reply(stanza, ret); - end - origin.send(reply); -end - -function handlers.set_unsubscribe(origin, stanza, unsubscribe) - local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid; - if not (node and jid) then - return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); - end - local ok, ret = service:remove_subscription(node, stanza.attr.from, jid); - local reply; - if ok then - reply = st.reply(stanza); - else - reply = pubsub_error_reply(stanza, ret); - end - return origin.send(reply); -end - -function handlers.set_publish(origin, stanza, publish) - local node = publish.attr.node; - if not node then - return origin.send(pubsub_error_reply(stanza, "nodeid-required")); - end - local item = publish:get_child("item"); - local id = (item and item.attr.id); - if not id then - id = uuid_generate(); - if item then - item.attr.id = id; - end - end - local ok, ret = service:publish(node, stanza.attr.from, id, item); - local reply; - if ok then - reply = st.reply(stanza) - :tag("pubsub", { xmlns = xmlns_pubsub }) - :tag("publish", { node = node }) - :tag("item", { id = id }); - else - reply = pubsub_error_reply(stanza, ret); - end - return origin.send(reply); -end - -function handlers.set_retract(origin, stanza, retract) - local node, notify = retract.attr.node, retract.attr.notify; - notify = (notify == "1") or (notify == "true"); - local item = retract:get_child("item"); - local id = item and item.attr.id - if not (node and id) then - return origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required")); - end - local reply, notifier; - if notify then - notifier = st.stanza("retract", { id = id }); - end - local ok, ret = service:retract(node, stanza.attr.from, id, notifier); - if ok then - reply = st.reply(stanza); - else - reply = pubsub_error_reply(stanza, ret); - end - return origin.send(reply); -end - -function handlers.set_purge(origin, stanza, purge) - local node, notify = purge.attr.node, purge.attr.notify; - notify = (notify == "1") or (notify == "true"); - local reply; - if not node then - return origin.send(pubsub_error_reply(stanza, "nodeid-required")); - end - local ok, ret = service:purge(node, stanza.attr.from, notify); - if ok then - reply = st.reply(stanza); - else - reply = pubsub_error_reply(stanza, ret); - end - return origin.send(reply); -end - -function simple_broadcast(kind, node, jids, item) - if item then - item = st.clone(item); - item.attr.xmlns = nil; -- Clear the pubsub namespace - end - local message = st.message({ from = module.host, type = "headline" }) - :tag("event", { xmlns = xmlns_pubsub_event }) - :tag(kind, { node = node }) - :add_child(item); - for jid in pairs(jids) do - module:log("debug", "Sending notification to %s", jid); - message.attr.to = jid; - module:send(message); - end -end - -module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq); -module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq); - -local disco_info; - -local feature_map = { - create = { "create-nodes", "instant-nodes", "item-ids" }; - retract = { "delete-items", "retract-items" }; - purge = { "purge-nodes" }; - publish = { "publish", autocreate_on_publish and "auto-create" }; - delete = { "delete-nodes" }; - get_items = { "retrieve-items" }; - add_subscription = { "subscribe" }; - get_subscriptions = { "retrieve-subscriptions" }; -}; - -local function add_disco_features_from_service(disco, service) - for method, features in pairs(feature_map) do - if service[method] then - for _, feature in ipairs(features) do - if feature then - disco:tag("feature", { var = xmlns_pubsub.."#"..feature }):up(); - end - end - end - end - for affiliation in pairs(service.config.capabilities) do - if affiliation ~= "none" and affiliation ~= "owner" then - disco:tag("feature", { var = xmlns_pubsub.."#"..affiliation.."-affiliation" }):up(); - end - end -end - -local function build_disco_info(service) - local disco_info = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" }) - :tag("identity", { category = "pubsub", type = "service", name = pubsub_disco_name }):up() - :tag("feature", { var = "http://jabber.org/protocol/pubsub" }):up(); - add_disco_features_from_service(disco_info, service); - return disco_info; -end - -module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function (event) - local origin, stanza = event.origin, event.stanza; - local node = stanza.tags[1].attr.node; - if not node then - return origin.send(st.reply(stanza):add_child(disco_info)); - else - local ok, ret = service:get_nodes(stanza.attr.from); - if ok and not ret[node] then - ok, ret = false, "item-not-found"; - end - if not ok then - return origin.send(pubsub_error_reply(stanza, ret)); - end - local reply = st.reply(stanza) - :tag("query", { xmlns = "http://jabber.org/protocol/disco#info", node = node }) - :tag("identity", { category = "pubsub", type = "leaf" }); - return origin.send(reply); - end -end); - -local function handle_disco_items_on_node(event) - local stanza, origin = event.stanza, event.origin; - local query = stanza.tags[1]; - local node = query.attr.node; - local ok, ret = service:get_items(node, stanza.attr.from); - if not ok then - return origin.send(pubsub_error_reply(stanza, ret)); - end - - local reply = st.reply(stanza) - :tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = node }); - - for id, item in pairs(ret) do - reply:tag("item", { jid = module.host, name = id }):up(); - end - - return origin.send(reply); -end - - -module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function (event) - if event.stanza.tags[1].attr.node then - return handle_disco_items_on_node(event); - end - local ok, ret = service:get_nodes(event.stanza.attr.from); - if not ok then - event.origin.send(pubsub_error_reply(event.stanza, ret)); - else - local reply = st.reply(event.stanza) - :tag("query", { xmlns = "http://jabber.org/protocol/disco#items" }); - for node, node_obj in pairs(ret) do - reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up(); - end - event.origin.send(reply); - end - return true; -end); - -local admin_aff = module:get_option_string("default_admin_affiliation", "owner"); -local function get_affiliation(jid) - local bare_jid = jid_bare(jid); - if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then - return admin_aff; - end -end - -function set_service(new_service) - service = new_service; - module.environment.service = service; - disco_info = build_disco_info(service); -end - -function module.save() - return { service = service }; -end - -function module.restore(data) - set_service(data.service); -end - -set_service(pubsub.new({ - capabilities = { - none = { - create = false; - publish = false; - retract = false; - get_nodes = true; - - subscribe = true; - unsubscribe = true; - get_subscription = true; - get_subscriptions = true; - get_items = true; - - subscribe_other = false; - unsubscribe_other = false; - get_subscription_other = false; - get_subscriptions_other = false; - - be_subscribed = true; - be_unsubscribed = true; - - set_affiliation = false; - }; - publisher = { - create = false; - publish = true; - retract = true; - get_nodes = true; - - subscribe = true; - unsubscribe = true; - get_subscription = true; - get_subscriptions = true; - get_items = true; - - subscribe_other = false; - unsubscribe_other = false; - get_subscription_other = false; - get_subscriptions_other = false; - - be_subscribed = true; - be_unsubscribed = true; - - set_affiliation = false; - }; - owner = { - create = true; - publish = true; - retract = true; - delete = true; - get_nodes = true; - - subscribe = true; - unsubscribe = true; - get_subscription = true; - get_subscriptions = true; - get_items = true; - - - subscribe_other = true; - unsubscribe_other = true; - get_subscription_other = true; - get_subscriptions_other = true; - - be_subscribed = true; - be_unsubscribed = true; - - set_affiliation = true; - }; - }; - - autocreate_on_publish = autocreate_on_publish; - autocreate_on_subscribe = autocreate_on_subscribe; - - broadcaster = simple_broadcast; - get_affiliation = get_affiliation; - - normalize_jid = jid_bare; -})); diff --git a/plugins/mod_pubsub/mod_pubsub.lua b/plugins/mod_pubsub/mod_pubsub.lua new file mode 100644 index 00000000..e93a5238 --- /dev/null +++ b/plugins/mod_pubsub/mod_pubsub.lua @@ -0,0 +1,238 @@ +local pubsub = require "util.pubsub"; +local st = require "util.stanza"; +local jid_bare = require "util.jid".bare; +local usermanager = require "core.usermanager"; + +local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; +local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event"; +local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; + +local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false); +local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false); +local pubsub_disco_name = module:get_option("name"); +if type(pubsub_disco_name) ~= "string" then pubsub_disco_name = "Prosody PubSub Service"; end +local expose_publisher = module:get_option_boolean("expose_publisher", false) + +local service; + +local lib_pubsub = module:require "pubsub"; +local handlers = lib_pubsub.handlers; +local pubsub_error_reply = lib_pubsub.pubsub_error_reply; + +module:depends("disco"); +module:add_identity("pubsub", "service", pubsub_disco_name); +module:add_feature("http://jabber.org/protocol/pubsub"); + +function handle_pubsub_iq(event) + local origin, stanza = event.origin, event.stanza; + local pubsub = stanza.tags[1]; + local action = pubsub.tags[1]; + if not action then + origin.send(st.error_reply(stanza, "cancel", "bad-request")); + return true; + end + local handler = handlers[stanza.attr.type.."_"..action.name]; + if handler then + handler(origin, stanza, action, service); + return true; + end +end + +function simple_broadcast(kind, node, jids, item, actor) + if item then + item = st.clone(item); + item.attr.xmlns = nil; -- Clear the pubsub namespace + if expose_publisher and actor then + item.attr.publisher = actor + end + end + local message = st.message({ from = module.host, type = "headline" }) + :tag("event", { xmlns = xmlns_pubsub_event }) + :tag(kind, { node = node }) + :add_child(item); + for jid in pairs(jids) do + module:log("debug", "Sending notification to %s", jid); + message.attr.to = jid; + module:send(message); + end +end + +module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq); +module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq); + +local feature_map = { + create = { "create-nodes", "instant-nodes", "item-ids" }; + retract = { "delete-items", "retract-items" }; + purge = { "purge-nodes" }; + publish = { "publish", autocreate_on_publish and "auto-create" }; + delete = { "delete-nodes" }; + get_items = { "retrieve-items" }; + add_subscription = { "subscribe" }; + get_subscriptions = { "retrieve-subscriptions" }; + set_configure = { "config-node" }; + get_default = { "retrieve-default" }; +}; + +local function add_disco_features_from_service(service) + for method, features in pairs(feature_map) do + if service[method] then + for _, feature in ipairs(features) do + if feature then + module:add_feature(xmlns_pubsub.."#"..feature); + end + end + end + end + for affiliation in pairs(service.config.capabilities) do + if affiliation ~= "none" and affiliation ~= "owner" then + module:add_feature(xmlns_pubsub.."#"..affiliation.."-affiliation"); + end + end +end + +module:hook("host-disco-info-node", function (event) + local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; + local ok, ret = service:get_nodes(stanza.attr.from); + if not ok or not ret[node] then + return; + end + event.exists = true; + reply:tag("identity", { category = "pubsub", type = "leaf" }); +end); + +module:hook("host-disco-items-node", function (event) + local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; + local ok, ret = service:get_items(node, stanza.attr.from); + if not ok then + return; + end + + for _, id in ipairs(ret) do + reply:tag("item", { jid = module.host, name = id }):up(); + end + event.exists = true; +end); + + +module:hook("host-disco-items", function (event) + local stanza, origin, reply = event.stanza, event.origin, event.reply; + local ok, ret = service:get_nodes(event.stanza.attr.from); + if not ok then + return; + end + for node, node_obj in pairs(ret) do + reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up(); + end +end); + +local admin_aff = module:get_option_string("default_admin_affiliation", "owner"); +local unowned_aff = module:get_option_string("default_unowned_affiliation"); +local function get_affiliation(jid, node) + local bare_jid = jid_bare(jid); + if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then + return admin_aff; + end + if not node then + return unowned_aff; + end +end + +function set_service(new_service) + service = new_service; + module.environment.service = service; + add_disco_features_from_service(service); +end + +function module.save() + return { service = service }; +end + +function module.restore(data) + set_service(data.service); +end + +function module.load() + if module.reloading then return; end + + set_service(pubsub.new({ + capabilities = { + none = { + create = false; + publish = false; + retract = false; + get_nodes = true; + + subscribe = true; + unsubscribe = true; + get_subscription = true; + get_subscriptions = true; + get_items = true; + + subscribe_other = false; + unsubscribe_other = false; + get_subscription_other = false; + get_subscriptions_other = false; + + be_subscribed = true; + be_unsubscribed = true; + + set_affiliation = false; + }; + publisher = { + create = false; + publish = true; + retract = true; + get_nodes = true; + + subscribe = true; + unsubscribe = true; + get_subscription = true; + get_subscriptions = true; + get_items = true; + + subscribe_other = false; + unsubscribe_other = false; + get_subscription_other = false; + get_subscriptions_other = false; + + be_subscribed = true; + be_unsubscribed = true; + + set_affiliation = false; + }; + owner = { + create = true; + publish = true; + retract = true; + delete = true; + get_nodes = true; + configure = true; + + subscribe = true; + unsubscribe = true; + get_subscription = true; + get_subscriptions = true; + get_items = true; + + + subscribe_other = true; + unsubscribe_other = true; + get_subscription_other = true; + get_subscriptions_other = true; + + be_subscribed = true; + be_unsubscribed = true; + + set_affiliation = true; + }; + }; + + autocreate_on_publish = autocreate_on_publish; + autocreate_on_subscribe = autocreate_on_subscribe; + + broadcaster = simple_broadcast; + get_affiliation = get_affiliation; + + normalize_jid = jid_bare; + })); +end diff --git a/plugins/mod_pubsub/pubsub.lib.lua b/plugins/mod_pubsub/pubsub.lib.lua new file mode 100644 index 00000000..1497c21c --- /dev/null +++ b/plugins/mod_pubsub/pubsub.lib.lua @@ -0,0 +1,317 @@ +local st = require "util.stanza"; +local uuid_generate = require "util.uuid".generate; +local dataform = require"util.dataforms".new; + +local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; +local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors"; +local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; + +local _M = {}; + +local handlers = {}; +_M.handlers = handlers; + +local pubsub_errors = { + ["conflict"] = { "cancel", "conflict" }; + ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" }; + ["jid-required"] = { "modify", "bad-request", nil, "jid-required" }; + ["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" }; + ["item-not-found"] = { "cancel", "item-not-found" }; + ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" }; + ["forbidden"] = { "auth", "forbidden" }; + ["not-allowed"] = { "cancel", "not-allowed" }; +}; +local function pubsub_error_reply(stanza, error) + local e = pubsub_errors[error]; + local reply = st.error_reply(stanza, unpack(e, 1, 3)); + if e[4] then + reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up(); + end + return reply; +end +_M.pubsub_error_reply = pubsub_error_reply; + +local node_config_form = require"util.dataforms".new { + { + type = "hidden"; + name = "FORM_TYPE"; + value = "http://jabber.org/protocol/pubsub#node_config"; + }; + { + type = "text-single"; + name = "pubsub#max_items"; + label = "Max # of items to persist"; + }; +}; + +function handlers.get_items(origin, stanza, items, service) + local node = items.attr.node; + local item = items:get_child("item"); + local id = item and item.attr.id; + + if not node then + origin.send(pubsub_error_reply(stanza, "nodeid-required")); + return true; + end + local ok, results = service:get_items(node, stanza.attr.from, id); + if not ok then + origin.send(pubsub_error_reply(stanza, results)); + return true; + end + + local data = st.stanza("items", { node = node }); + for _, id in ipairs(results) do + data:add_child(results[id]); + end + local reply; + if data then + reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :add_child(data); + else + reply = pubsub_error_reply(stanza, "item-not-found"); + end + origin.send(reply); + return true; +end + +function handlers.get_subscriptions(origin, stanza, subscriptions, service) + local node = subscriptions.attr.node; + local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from); + if not ok then + origin.send(pubsub_error_reply(stanza, ret)); + return true; + end + local reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :tag("subscriptions"); + for _, sub in ipairs(ret) do + reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up(); + end + origin.send(reply); + return true; +end + +function handlers.set_create(origin, stanza, create, service) + local node = create.attr.node; + local ok, ret, reply; + if node then + ok, ret = service:create(node, stanza.attr.from); + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + else + repeat + node = uuid_generate(); + ok, ret = service:create(node, stanza.attr.from); + until ok or ret ~= "conflict"; + if ok then + reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :tag("create", { node = node }); + else + reply = pubsub_error_reply(stanza, ret); + end + end + origin.send(reply); + return true; +end + +function handlers.set_delete(origin, stanza, delete, service) + local node = delete.attr.node; + + local reply, notifier; + if not node then + origin.send(pubsub_error_reply(stanza, "nodeid-required")); + return true; + end + local ok, ret = service:delete(node, stanza.attr.from); + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + origin.send(reply); + return true; +end + +function handlers.set_subscribe(origin, stanza, subscribe, service) + local node, jid = subscribe.attr.node, subscribe.attr.jid; + if not (node and jid) then + origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); + return true; + end + --[[ + local options_tag, options = stanza.tags[1]:get_child("options"), nil; + if options_tag then + options = options_form:data(options_tag.tags[1]); + end + --]] + local options_tag, options; -- FIXME + local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options); + local reply; + if ok then + reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :tag("subscription", { + node = node, + jid = jid, + subscription = "subscribed" + }):up(); + if options_tag then + reply:add_child(options_tag); + end + else + reply = pubsub_error_reply(stanza, ret); + end + origin.send(reply); +end + +function handlers.set_unsubscribe(origin, stanza, unsubscribe, service) + local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid; + if not (node and jid) then + origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); + return true; + end + local ok, ret = service:remove_subscription(node, stanza.attr.from, jid); + local reply; + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + origin.send(reply); + return true; +end + +function handlers.set_publish(origin, stanza, publish, service) + local node = publish.attr.node; + if not node then + origin.send(pubsub_error_reply(stanza, "nodeid-required")); + return true; + end + local item = publish:get_child("item"); + local id = (item and item.attr.id); + if not id then + id = uuid_generate(); + if item then + item.attr.id = id; + end + end + local ok, ret = service:publish(node, stanza.attr.from, id, item); + local reply; + if ok then + reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :tag("publish", { node = node }) + :tag("item", { id = id }); + else + reply = pubsub_error_reply(stanza, ret); + end + origin.send(reply); + return true; +end + +function handlers.set_retract(origin, stanza, retract, service) + local node, notify = retract.attr.node, retract.attr.notify; + notify = (notify == "1") or (notify == "true"); + local item = retract:get_child("item"); + local id = item and item.attr.id + if not (node and id) then + origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required")); + return true; + end + local reply, notifier; + if notify then + notifier = st.stanza("retract", { id = id }); + end + local ok, ret = service:retract(node, stanza.attr.from, id, notifier); + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + origin.send(reply); + return true; +end + +function handlers.set_purge(origin, stanza, purge, service) + local node, notify = purge.attr.node, purge.attr.notify; + notify = (notify == "1") or (notify == "true"); + local reply; + if not node then + origin.send(pubsub_error_reply(stanza, "nodeid-required")); + return true; + end + local ok, ret = service:purge(node, stanza.attr.from, notify); + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + origin.send(reply); + return true; +end + +function handlers.get_configure(origin, stanza, config, service) + local node = config.attr.node; + if not node then + origin.send(pubsub_error_reply(stanza, "nodeid-required")); + return true; + end + + if not service:may(node, stanza.attr.from, "configure") then + origin.send(pubsub_error_reply(stanza, "forbidden")); + return true; + end + + local node_obj = service.nodes[node]; + if not node_obj then + origin.send(pubsub_error_reply(stanza, "item-not-found")); + return true; + end + + local reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub_owner }) + :tag("configure", { node = node }) + :add_child(node_config_form:form(node_obj.config)); + origin.send(reply); + return true; +end + +function handlers.set_configure(origin, stanza, config, service) + local node = config.attr.node; + if not node then + origin.send(pubsub_error_reply(stanza, "nodeid-required")); + return true; + end + if not service:may(node, stanza.attr.from, "configure") then + origin.send(pubsub_error_reply(stanza, "forbidden")); + return true; + end + local new_config, err = node_config_form:data(config.tags[1]); + if not new_config then + origin.send(st.error_reply(stanza, "modify", "bad-request", err)); + return true; + end + local ok, err = service:set_node_config(node, stanza.attr.from, new_config); + if not ok then + origin.send(pubsub_error_reply(stanza, err)); + return true; + end + origin.send(st.reply(stanza)); + return true; +end + +function handlers.get_default(origin, stanza, default, service) + local reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub_owner }) + :tag("default") + :add_child(node_config_form:form(service.node_defaults)); + origin.send(reply); + return true; +end + +return _M; diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua index 3d7a068c..e4b4bd51 100644 --- a/plugins/mod_register.lua +++ b/plugins/mod_register.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -13,9 +13,10 @@ local usermanager_user_exists = require "core.usermanager".user_exists; local usermanager_create_user = require "core.usermanager".create_user; local usermanager_set_password = require "core.usermanager".set_password; local usermanager_delete_user = require "core.usermanager".delete_user; -local os_time = os.time; local nodeprep = require "util.encodings".stringprep.nodeprep; local jid_bare = require "util.jid".bare; +local create_throttle = require "util.throttle".create; +local new_cache = require "util.cache".new; local compat = module:get_option_boolean("registration_compat", true); local allow_registration = module:get_option_boolean("allow_registration", false); @@ -72,7 +73,7 @@ module:add_feature("jabber:iq:register"); local register_stream_feature = st.stanza("register", {xmlns="http://jabber.org/features/iq-register"}):up(); module:hook("stream-features", function(event) - local session, features = event.origin, event.features; + local session, features = event.origin, event.features; -- Advertise registration to unauthorized clients only. if not(allow_registration) or session.type ~= "c2s_unauthed" then @@ -84,6 +85,7 @@ end); local function handle_registration_stanza(event) local session, stanza = event.origin, event.stanza; + local log = session.log or module._log; local query = stanza.tags[1]; if stanza.attr.type == "get" then @@ -97,22 +99,23 @@ local function handle_registration_stanza(event) if query.tags[1] and query.tags[1].name == "remove" then local username, host = session.username, session.host; + -- This one weird trick sends a reply to this stanza before the user is deleted local old_session_close = session.close; session.close = function(session, ...) session.send(st.reply(stanza)); return old_session_close(session, ...); end - + local ok, err = usermanager_delete_user(username, host); - + if not ok then - module:log("debug", "Removing user account %s@%s failed: %s", username, host, err); + log("debug", "Removing user account %s@%s failed: %s", username, host, err); session.close = old_session_close; session.send(st.error_reply(stanza, "cancel", "service-unavailable", err)); return true; end - - module:log("info", "User removed their account: %s@%s", username, host); + + log("info", "User removed their account: %s@%s", username, host); module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session }); else local username = nodeprep(query:get_child_text("username")); @@ -169,17 +172,36 @@ local function parse_response(query) end end -local recent_ips = {}; -local min_seconds_between_registrations = module:get_option("min_seconds_between_registrations"); -local whitelist_only = module:get_option("whitelist_registration_only"); -local whitelisted_ips = module:get_option("registration_whitelist") or { "127.0.0.1" }; -local blacklisted_ips = module:get_option("registration_blacklist") or {}; +local min_seconds_between_registrations = module:get_option_number("min_seconds_between_registrations"); +local whitelist_only = module:get_option_boolean("whitelist_registration_only"); +local whitelisted_ips = module:get_option_set("registration_whitelist", { "127.0.0.1" })._items; +local blacklisted_ips = module:get_option_set("registration_blacklist", {})._items; -for _, ip in ipairs(whitelisted_ips) do whitelisted_ips[ip] = true; end -for _, ip in ipairs(blacklisted_ips) do blacklisted_ips[ip] = true; end +local throttle_max = module:get_option_number("registration_throttle_max", min_seconds_between_registrations and 1); +local throttle_period = module:get_option_number("registration_throttle_period", min_seconds_between_registrations); +local throttle_cache_size = module:get_option_number("registration_throttle_cache_size", 100); +local blacklist_overflow = module:get_option_boolean("blacklist_on_registration_throttle_overload", false); + +local throttle_cache = new_cache(throttle_cache_size, blacklist_overflow and function (ip, throttle) + if not throttle:peek() then + module:log("info", "Adding ip %s to registration blacklist", ip); + blacklisted_ips[ip] = true; + end +end); + +local function check_throttle(ip) + if not throttle_max then return true end + local throttle = throttle_cache:get(ip); + if not throttle then + throttle = create_throttle(throttle_max, throttle_period); + end + throttle_cache:set(ip, throttle); + return throttle:poll(1); +end module:hook("stanza/iq/jabber:iq:register:query", function(event) local session, stanza = event.origin, event.stanza; + local log = session.log or module._log; if not(allow_registration) or session.type ~= "c2s_unauthed" then session.send(st.error_reply(stanza, "cancel", "service-unavailable")); @@ -199,23 +221,14 @@ module:hook("stanza/iq/jabber:iq:register:query", function(event) else -- Check that the user is not blacklisted or registering too often if not session.ip then - module:log("debug", "User's IP not known; can't apply blacklist/whitelist"); + log("debug", "User's IP not known; can't apply blacklist/whitelist"); elseif blacklisted_ips[session.ip] or (whitelist_only and not whitelisted_ips[session.ip]) then session.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not allowed to register an account.")); return true; elseif min_seconds_between_registrations and not whitelisted_ips[session.ip] then - if not recent_ips[session.ip] then - recent_ips[session.ip] = { time = os_time(), count = 1 }; - else - local ip = recent_ips[session.ip]; - ip.count = ip.count + 1; - - if os_time() - ip.time < min_seconds_between_registrations then - ip.time = os_time(); - session.send(st.error_reply(stanza, "wait", "not-acceptable")); - return true; - end - ip.time = os_time(); + if check_throttle(session.ip) then + session.send(st.error_reply(stanza, "wait", "not-acceptable")); + return true; end end local username, password = nodeprep(data.username), data.password; @@ -241,7 +254,7 @@ module:hook("stanza/iq/jabber:iq:register:query", function(event) return true; end session.send(st.reply(stanza)); -- user created! - module:log("info", "User account created: %s@%s", username, host); + log("info", "User account created: %s@%s", username, host); module:fire_event("user-registered", { username = username, host = host, source = "mod_register", session = session }); diff --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua index d530bb45..a674740c 100644 --- a/plugins/mod_roster.lua +++ b/plugins/mod_roster.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -36,15 +36,15 @@ module:hook("iq/self/jabber:iq:roster:query", function(event) if stanza.attr.type == "get" then local roster = st.reply(stanza); - + local client_ver = tonumber(stanza.tags[1].attr.ver); local server_ver = tonumber(session.roster[false].version or 1); - + if not (client_ver and server_ver) or client_ver ~= server_ver then roster:query("jabber:iq:roster"); -- Client does not support versioning, or has stale roster for jid, item in pairs(session.roster) do - if jid ~= "pending" and jid then + if jid then roster:tag("item", { jid = jid, subscription = item.subscription, @@ -64,9 +64,7 @@ module:hook("iq/self/jabber:iq:roster:query", function(event) else -- stanza.attr.type == "set" local query = stanza.tags[1]; if #query.tags == 1 and query.tags[1].name == "item" - and query.tags[1].attr.xmlns == "jabber:iq:roster" and query.tags[1].attr.jid - -- Protection against overwriting roster.pending, until we move it - and query.tags[1].attr.jid ~= "pending" then + and query.tags[1].attr.xmlns == "jabber:iq:roster" and query.tags[1].attr.jid then local item = query.tags[1]; local from_node, from_host = jid_split(stanza.attr.from); local jid = jid_prep(item.attr.jid); @@ -78,7 +76,7 @@ module:hook("iq/self/jabber:iq:roster:query", function(event) local r_item = roster[jid]; if r_item then local to_bare = node and (node.."@"..host) or host; -- bare JID - if r_item.subscription == "both" or r_item.subscription == "from" or (roster.pending and roster.pending[jid]) then + if r_item.subscription == "both" or r_item.subscription == "from" or roster[false].pending[jid] then core_post_stanza(session, st.presence({type="unsubscribed", from=session.full_jid, to=to_bare})); end if r_item.subscription == "both" or r_item.subscription == "to" or r_item.ask then @@ -144,8 +142,8 @@ module:hook_global("user-deleted", function(event) local bare = username .. "@" .. host; local roster = rm_load_roster(username, host); for jid, item in pairs(roster) do - if jid and jid ~= "pending" then - if item.subscription == "both" or item.subscription == "from" or (roster.pending and roster.pending[jid]) then + if jid then + if item.subscription == "both" or item.subscription == "from" or roster[false].pending[jid] then module:send(st.presence({type="unsubscribed", from=bare, to=jid})); end if item.subscription == "both" or item.subscription == "to" or item.ask then diff --git a/plugins/mod_s2s/mod_s2s.lua b/plugins/mod_s2s/mod_s2s.lua index 4173fcfa..3e80c77b 100644 --- a/plugins/mod_s2s/mod_s2s.lua +++ b/plugins/mod_s2s/mod_s2s.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -15,7 +15,6 @@ local core_process_stanza = prosody.core_process_stanza; local tostring, type = tostring, type; local t_insert = table.insert; local xpcall, traceback = xpcall, debug.traceback; -local NULL = {}; local add_task = require "util.timer".add_task; local st = require "util.stanza"; @@ -26,7 +25,6 @@ local s2s_new_incoming = require "core.s2smanager".new_incoming; local s2s_new_outgoing = require "core.s2smanager".new_outgoing; local s2s_destroy_session = require "core.s2smanager".destroy_session; local uuid_gen = require "util.uuid".generate; -local cert_verify_identity = require "util.x509".verify_identity; local fire_global_event = prosody.events.fire_event; local s2sout = module:require("s2sout"); @@ -39,6 +37,8 @@ local secure_domains, insecure_domains = module:get_option_set("s2s_secure_domains", {})._items, module:get_option_set("s2s_insecure_domains", {})._items; local require_encryption = module:get_option_boolean("s2s_require_encryption", false); +local measure_connections = module:measure("connections", "counter"); + local sessions = module:shared("sessions"); local log = module._log; @@ -135,6 +135,12 @@ function route_to_new_session(event) return true; end +local function keepalive(event) + return event.session.sends2s(' '); +end + +module:hook("s2s-read-timeout", keepalive, -1); + function module.add_host(module) if module:get_option_boolean("disallow_s2s", false) then module:log("warn", "The 'disallow_s2s' config option is deprecated, please see http://prosody.im/doc/s2s#disabling"); @@ -143,15 +149,28 @@ function module.add_host(module) module:hook("route/remote", route_to_existing_session, -1); module:hook("route/remote", route_to_new_session, -10); module:hook("s2s-authenticated", make_authenticated, -1); + module:hook("s2s-read-timeout", keepalive, -1); + module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza) + if session.type == "s2sout" then + -- Stream is authenticated and we are seem to be done with feature negotiation, + -- so the stream is ready for stanzas. RFC 6120 Section 4.3 + mark_connected(session); + return true; + elseif not session.dialback_verifying then + session.log("warn", "No SASL EXTERNAL offer and Dialback doesn't seem to be enabled, giving up"); + session:close(); + return false; + end + end, -1); 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; - - session.log("info", "%s s2s connection %s->%s complete", session.direction, from, to); + + session.log("info", "%s s2s connection %s->%s complete", session.direction:gsub("^.", string.upper), from, to); local event_data = { session = session }; if session.type == "s2sout" then @@ -166,7 +185,7 @@ function mark_connected(session) fire_global_event("s2sin-established", event_data); hosts[to].events.fire_event("s2sin-established", event_data); end - + if session.direction == "outgoing" then if sendq then session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", #sendq, session.to_host); @@ -177,7 +196,7 @@ function mark_connected(session) end session.sendq = nil; end - + session.ip_hosts = nil; session.srv_hosts = nil; end @@ -212,14 +231,17 @@ function make_authenticated(event) return false; end session.log("debug", "connection %s->%s is now authenticated for %s", session.from_host, session.to_host, host); - - mark_connected(session); - + + if (session.type == "s2sout" and session.external_auth ~= "succeeded") or session.type == "s2sin" then + -- Stream either used dialback for authentication or is an incoming stream. + mark_connected(session); + end + return true; end --- Helper to check that a session peer's certificate is valid -local function check_cert_status(session) +function check_cert_status(session) local host = session.direction == "outgoing" and session.to_host or session.from_host local conn = session.conn:socket() local cert @@ -227,39 +249,6 @@ local function check_cert_status(session) cert = conn:getpeercertificate() end - if cert then - local chain_valid, errors; - if conn.getpeerverification then - chain_valid, errors = conn:getpeerverification(); - elseif conn.getpeerchainvalid then -- COMPAT mw/luasec-hg - chain_valid, errors = conn:getpeerchainvalid(); - errors = (not chain_valid) and { { errors } } or nil; - else - chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } }; - end - -- Is there any interest in printing out all/the number of errors here? - if not chain_valid then - (session.log or log)("debug", "certificate chain validation result: invalid"); - for depth, t in pairs(errors or NULL) do - (session.log or log)("debug", "certificate error(s) at depth %d: %s", depth-1, table.concat(t, ", ")) - end - session.cert_chain_status = "invalid"; - else - (session.log or log)("debug", "certificate chain validation result: valid"); - session.cert_chain_status = "valid"; - - -- We'll go ahead and verify the asserted identity if the - -- connecting server specified one. - if host then - if cert_verify_identity(host, "xmpp-server", cert) then - session.cert_identity_status = "valid" - else - session.cert_identity_status = "invalid" - end - (session.log or log)("debug", "certificate identity validation result: %s", session.cert_identity_status); - end - end - end return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert }); end @@ -271,23 +260,26 @@ local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; function stream_callbacks.streamopened(session, attr) session.version = tonumber(attr.version) or 0; - + -- TODO: Rename session.secure to session.encrypted if session.secure == false then session.secure = true; + session.encrypted = true; - -- Check if TLS compression is used local sock = session.conn:socket(); if sock.info then - session.compressed = sock:info"compression"; - elseif sock.compression then - session.compressed = sock:compression(); --COMPAT mw/luasec-hg + local info = sock:info(); + (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher); + session.compressed = info.compression; + else + (session.log or log)("info", "Stream encrypted"); + session.compressed = sock.compression and sock:compression(); --COMPAT mw/luasec-hg end end if session.direction == "incoming" then -- Send a reply stream header - + -- Validate to/from local to, from = nameprep(attr.to), nameprep(attr.from); if not to and attr.to then -- COMPAT: Some servers do not reliably set 'to' (especially on stream restarts) @@ -298,7 +290,7 @@ function stream_callbacks.streamopened(session, attr) session:close({ condition = "improper-addressing", text = "Invalid 'from' address" }); return; end - + -- Set session.[from/to]_host if they have not been set already and if -- this session isn't already authenticated if session.type == "s2sin_unauthed" and from and not session.from_host then @@ -313,10 +305,10 @@ function stream_callbacks.streamopened(session, attr) session:close({ condition = "improper-addressing", text = "New stream 'to' attribute does not match original" }); return; end - + -- For convenience we'll put the sanitised values into these variables to, from = session.to_host, session.from_host; - + session.streamid = uuid_gen(); (session.log or log)("debug", "Incoming s2s received %s", st.stanza("stream:stream", attr):top_tag()); if to then @@ -352,15 +344,21 @@ function stream_callbacks.streamopened(session, attr) session.notopen = nil; if session.version >= 1.0 then local features = st.stanza("stream:features"); - + if to then hosts[to].events.fire_event("s2s-stream-features", { origin = session, features = features }); else (session.log or log)("warn", "No 'to' on stream header from %s means we can't offer any features", from or session.ip or "unknown host"); + fire_global_event("s2s-stream-features-legacy", { origin = session, features = features }); + end + + if ( session.type == "s2sin" or session.type == "s2sout" ) or features.tags[1] then + log("debug", "Sending stream features: %s", tostring(features)); + session.sends2s(features); + else + (session.log or log)("warn", "No features to offer, giving up"); + session:close({ condition = "undefined-condition", text = "No features to offer" }); end - - log("debug", "Sending stream features: %s", tostring(features)); - session.sends2s(features); end elseif session.direction == "outgoing" then session.notopen = nil; @@ -390,7 +388,7 @@ function stream_callbacks.streamopened(session, attr) end end session.send_buffer = nil; - + -- If server is pre-1.0, don't wait for features, just do dialback if session.version < 1.0 then if not session.dialback_verifying then @@ -483,10 +481,10 @@ local function session_close(session, reason, remote_reason) session.sends2s("</stream:stream>"); function session.sends2s() return false; end - + local reason = remote_reason or (reason and (reason.text or reason.condition)) or reason; - session.log("info", "%s s2s stream %s->%s closed: %s", session.direction, session.from_host or "(unknown host)", session.to_host or "(unknown host)", reason or "stream closed"); - + session.log("info", "%s s2s stream %s->%s closed: %s", session.direction:gsub("^.", string.upper), session.from_host or "(unknown host)", session.to_host or "(unknown host)", reason or "stream closed"); + -- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote local conn = session.conn; if reason == nil and not session.notopen and session.type == "s2sin" then @@ -504,47 +502,58 @@ local function session_close(session, reason, remote_reason) end end -function session_open_stream(session, from, to) - local attr = { - ["xmlns:stream"] = 'http://etherx.jabber.org/streams', - xmlns = 'jabber:server', - version = session.version and (session.version > 0 and "1.0" or nil), - ["xml:lang"] = 'en', - id = session.streamid, - from = from or "", to = to or "", - } +function session_stream_attrs(session, from, to, attr) if not from or (hosts[from] and hosts[from].modules.dialback) then attr["xmlns:db"] = 'jabber:server:dialback'; end - - session.sends2s("<?xml version='1.0'?>"); - session.sends2s(st.stanza("stream:stream", attr):top_tag()); - return true; + if not from then + attr.from = ''; + end + if not to then + attr.to = ''; + end end -- Session initialization logic shared by incoming and outgoing local function initialize_session(session) local stream = new_xmpp_stream(session, stream_callbacks); + local log = session.log or log; session.stream = stream; - + session.notopen = true; - + function session.reset_stream() session.notopen = true; session.streamid = nil; session.stream:reset(); end - session.open_stream = session_open_stream; - - local filter = session.filter; + session.stream_attrs = session_stream_attrs; + + local filter = initialize_filters(session); + local conn = session.conn; + local w = conn.write; + + function session.sends2s(t) + log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^[^>]*>?")); + if t.name then + t = filter("stanzas/out", t); + end + if t then + t = filter("bytes/out", tostring(t)); + if t then + return w(conn, t); + end + end + end + function session.data(data) data = filter("bytes/in", data); if data then local ok, err = stream:feed(data); if ok then return; end - (session.log or log)("warn", "Received invalid XML: %s", data); - (session.log or log)("warn", "Problem was: %s", err); + log("warn", "Received invalid XML: %s", data); + log("warn", "Problem was: %s", err); session:close("not-well-formed"); end end @@ -556,6 +565,8 @@ local function initialize_session(session) return handlestanza(session, stanza); end + module:fire_event("s2s-created", { session = session }); + add_task(connect_timeout, function () if session.type == "s2sin" or session.type == "s2sout" then return; -- Ok, we're connected @@ -570,32 +581,18 @@ local function initialize_session(session) end function listener.onconnect(conn) + measure_connections(1); conn:setoption("keepalive", opt_keepalives); local session = sessions[conn]; if not session then -- New incoming connection session = s2s_new_incoming(conn); sessions[conn] = session; session.log("debug", "Incoming s2s connection"); - - local filter = initialize_filters(session); - local w = conn.write; - session.sends2s = function (t) - log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^([^>]*>?)")); - if t.name then - t = filter("stanzas/out", t); - end - if t then - t = filter("bytes/out", tostring(t)); - if t then - return w(conn, t); - end - end - end - initialize_session(session); else -- Outgoing session connected session:open_stream(session.from_host, session.to_host); end + session.ip = conn:ip(); end function listener.onincoming(conn, data) @@ -604,7 +601,7 @@ function listener.onincoming(conn, data) session.data(data); end end - + function listener.onstatus(conn, status) if status == "ssl-handshake-complete" then local session = sessions[conn]; @@ -615,14 +612,19 @@ function listener.onstatus(conn, status) end end +function listener.ontimeout(conn) + -- Called instead of onconnect when the connection times out + measure_connections(1); +end + function listener.ondisconnect(conn, err) + measure_connections(-1); local session = sessions[conn]; if session then sessions[conn] = nil; if err and session.direction == "outgoing" and session.notopen then (session.log or log)("debug", "s2s connection attempt failed: %s", err); if s2sout.attempt_connection(session, err) then - (session.log or log)("debug", "...so we're going to try another target"); return; -- Session lives for now end end @@ -631,8 +633,15 @@ function listener.ondisconnect(conn, err) end end +function listener.onreadtimeout(conn) + local session = sessions[conn]; + local host = session.host or session.to_host; + if session then + return (hosts[host] or prosody).events.fire_event("s2s-read-timeout", { session = session }); + end +end + function listener.register_outgoing(conn, session) - session.direction = "outgoing"; sessions[conn] = session; initialize_session(session); end @@ -650,7 +659,7 @@ function check_auth_policy(event) elseif must_secure and insecure_domains[host] then must_secure = false; end - + if must_secure and (session.cert_chain_status ~= "valid" or session.cert_identity_status ~= "valid") then module:log("warn", "Forbidding insecure connection to/from %s", host or session.ip or "(unknown host)"); if session.direction == "incoming" then diff --git a/plugins/mod_s2s/s2sout.lib.lua b/plugins/mod_s2s/s2sout.lib.lua index dc122af7..42413164 100644 --- a/plugins/mod_s2s/s2sout.lib.lua +++ b/plugins/mod_s2s/s2sout.lib.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -46,14 +46,14 @@ end function s2sout.initiate_connection(host_session) initialize_filters(host_session); host_session.version = 1; - + -- Kick the connection attempting machine into life if not s2sout.attempt_connection(host_session) then -- Intentionally not returning here, the -- session is needed, connected or not s2s_destroy_session(host_session); end - + if not host_session.sends2s then -- A sends2s which buffers data (until the stream is opened) -- note that data in this buffer will be sent before the stream is authed @@ -74,22 +74,23 @@ end function s2sout.attempt_connection(host_session, err) local to_host = host_session.to_host; local connect_host, connect_port = to_host and idna_to_ascii(to_host), 5269; - + if not connect_host then return false; end - + 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; + local srv_hosts = { answer = answer }; + host_session.srv_hosts = srv_hosts; + host_session.srv_choice = 0; host_session.connecting = nil; if answer and #answer > 0 then log("debug", "%s has SRV records, handling...", to_host); - local srv_hosts = { answer = answer }; - host_session.srv_hosts = srv_hosts; for _, record in ipairs(answer) do t_insert(srv_hosts, record.srv); end @@ -99,7 +100,7 @@ function s2sout.attempt_connection(host_session, err) return; end t_sort(srv_hosts, compare_srv_priorities); - + local srv_choice = srv_hosts[1]; host_session.srv_choice = 1; if srv_choice then @@ -118,7 +119,7 @@ function s2sout.attempt_connection(host_session, err) end end end, "_xmpp-server._tcp."..connect_host..".", "SRV"); - + return true; -- Attempt in progress elseif host_session.ip_hosts then return s2sout.try_connect(host_session, connect_host, connect_port, err); @@ -128,11 +129,11 @@ function s2sout.attempt_connection(host_session, err) connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port; host_session.log("info", "Connection failed (%s). Attempt #%d: This time to %s:%d", tostring(err), host_session.srv_choice, connect_host, connect_port); else - host_session.log("info", "Out of connection options, can't connect to %s", tostring(host_session.to_host)); + host_session.log("info", "Failed in all attempts to connect to %s", tostring(host_session.to_host)); -- We're out of options return false; end - + if not (connect_host and connect_port) then -- Likely we couldn't resolve DNS log("warn", "Hmm, we're without a host (%s) and port (%s) to connect to for %s, giving up :(", tostring(connect_host), tostring(connect_port), tostring(to_host)); @@ -252,11 +253,12 @@ function s2sout.try_connect(host_session, connect_host, connect_port, err) end function s2sout.make_connect(host_session, connect_host, connect_port) - (host_session.log or log)("info", "Beginning new connection attempt to %s ([%s]:%d)", host_session.to_host, connect_host.addr, connect_port); + (host_session.log or log)("debug", "Beginning new connection attempt to %s ([%s]:%d)", host_session.to_host, connect_host.addr, connect_port); -- Reset secure flag in case this is another -- connection attempt after a failed STARTTLS host_session.secure = nil; + host_session.encrypted = nil; local conn, handler; local proto = connect_host.proto; @@ -267,7 +269,7 @@ function s2sout.make_connect(host_session, connect_host, connect_port) else handler = "Unsupported protocol: "..tostring(proto); end - + if not conn then log("warn", "Failed to create outgoing connection, system error: %s", handler); return false, handler; @@ -279,29 +281,14 @@ function s2sout.make_connect(host_session, connect_host, connect_port) log("warn", "s2s connect() to %s (%s:%d) failed: %s", host_session.to_host, connect_host.addr, connect_port, err); return false, err; end - + conn = wrapclient(conn, connect_host.addr, connect_port, s2s_listener, "*a"); host_session.conn = conn; - - local filter = initialize_filters(host_session); - local w, log = conn.write, host_session.log; - host_session.sends2s = function (t) - log("debug", "sending: %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?")); - if t.name then - t = filter("stanzas/out", t); - end - if t then - t = filter("bytes/out", tostring(t)); - if t then - return w(conn, tostring(t)); - end - end - end - + -- Register this outgoing connection so that xmppserver_listener knows about it -- otherwise it will assume it is a new incoming connection s2s_listener.register_outgoing(conn, host_session); - + log("debug", "Connection attempt in progress..."); return true; end diff --git a/plugins/mod_s2s_auth_certs.lua b/plugins/mod_s2s_auth_certs.lua new file mode 100644 index 00000000..dd0eb3cb --- /dev/null +++ b/plugins/mod_s2s_auth_certs.lua @@ -0,0 +1,49 @@ +module:set_global(); + +local cert_verify_identity = require "util.x509".verify_identity; +local NULL = {}; +local log = module._log; + +module:hook("s2s-check-certificate", function(event) + local session, host, cert = event.session, event.host, event.cert; + local conn = session.conn:socket(); + local log = session.log or log; + + if not cert then + log("warn", "No certificate provided by %s", host or "unknown host"); + return; + end + + local chain_valid, errors; + if conn.getpeerverification then + chain_valid, errors = conn:getpeerverification(); + elseif conn.getpeerchainvalid then -- COMPAT mw/luasec-hg + chain_valid, errors = conn:getpeerchainvalid(); + errors = (not chain_valid) and { { errors } } or nil; + else + chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } }; + end + -- Is there any interest in printing out all/the number of errors here? + if not chain_valid then + log("debug", "certificate chain validation result: invalid"); + for depth, t in pairs(errors or NULL) do + log("debug", "certificate error(s) at depth %d: %s", depth-1, table.concat(t, ", ")) + end + session.cert_chain_status = "invalid"; + else + log("debug", "certificate chain validation result: valid"); + session.cert_chain_status = "valid"; + + -- We'll go ahead and verify the asserted identity if the + -- connecting server specified one. + if host then + if cert_verify_identity(host, "xmpp-server", cert) then + session.cert_identity_status = "valid" + else + session.cert_identity_status = "invalid" + end + log("debug", "certificate identity validation result: %s", session.cert_identity_status); + end + end +end, 509); + diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index c5d3dc91..7e9b0720 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -13,13 +13,13 @@ local sm_bind_resource = require "core.sessionmanager".bind_resource; local sm_make_authenticated = require "core.sessionmanager".make_authenticated; local base64 = require "util.encodings".base64; -local cert_verify_identity = require "util.x509".verify_identity; - local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler; local tostring = tostring; -local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption"); -local allow_unencrypted_plain_auth = module:get_option("allow_unencrypted_plain_auth") +local secure_auth_only = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", false)); +local allow_unencrypted_plain_auth = module:get_option_boolean("allow_unencrypted_plain_auth", false) +local insecure_mechanisms = module:get_option_set("insecure_sasl_mechanisms", allow_unencrypted_plain_auth and {} or {"PLAIN", "LOGIN"}); +local disabled_mechanisms = module:get_option_set("disable_sasl_mechanisms", {}); local log = module._log; @@ -28,15 +28,15 @@ local xmlns_bind ='urn:ietf:params:xml:ns:xmpp-bind'; local function build_reply(status, ret, err_msg) local reply = st.stanza(status, {xmlns = xmlns_sasl}); - if status == "challenge" then - --log("debug", "CHALLENGE: %s", ret or ""); - reply:text(base64.encode(ret or "")); - elseif status == "failure" then + if status == "failure" then reply:tag(ret):up(); if err_msg then reply:tag("text"):text(err_msg); end - elseif status == "success" then - --log("debug", "SUCCESS: %s", ret or ""); - reply:text(base64.encode(ret or "")); + elseif status == "challenge" or status == "success" then + if ret == "" then + reply:text("=") + elseif ret then + reply:text(base64.encode(ret)); + end else module:log("error", "Unknown sasl status: %s", status); end @@ -99,12 +99,10 @@ module:hook_stanza(xmlns_sasl, "failure", function (session, stanza) module:log("info", "SASL EXTERNAL with %s failed", session.to_host) -- TODO: Log the failure reason session.external_auth = "failed" + session:close(); + return true; end, 500) -module:hook_stanza(xmlns_sasl, "failure", function (session, stanza) - -- TODO: Dialback wasn't loaded. Do something useful. -end, 90) - module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza) if session.type ~= "s2sout_unauthed" or not session.secure then return; end @@ -124,71 +122,52 @@ module:hook_stanza("http://etherx.jabber.org/streams", "features", function (ses end, 150); local function s2s_external_auth(session, stanza) + if session.external_auth ~= "offered" then return end -- Unexpected request + local mechanism = stanza.attr.mechanism; - if not session.secure then - if mechanism == "EXTERNAL" then - session.sends2s(build_reply("failure", "encryption-required")) - else - session.sends2s(build_reply("failure", "invalid-mechanism")) - end + if mechanism ~= "EXTERNAL" then + session.sends2s(build_reply("failure", "invalid-mechanism")); return true; end - if mechanism ~= "EXTERNAL" or session.cert_chain_status ~= "valid" then - session.sends2s(build_reply("failure", "invalid-mechanism")) + if not session.secure then + session.sends2s(build_reply("failure", "encryption-required")); return true; end - local text = stanza[1] + local text = stanza[1]; if not text then - session.sends2s(build_reply("failure", "malformed-request")) - return true + session.sends2s(build_reply("failure", "malformed-request")); + return true; end - -- Either the value is "=" and we've already verified the external - -- cert identity, or the value is a string and either matches the - -- from_host ( - - text = base64.decode(text) + text = base64.decode(text); if not text then - session.sends2s(build_reply("failure", "incorrect-encoding")) + session.sends2s(build_reply("failure", "incorrect-encoding")); return true; end - if session.cert_identity_status == "valid" then - if text ~= "" and text ~= session.from_host then - session.sends2s(build_reply("failure", "invalid-authzid")) - return true - end - else - if text == "" then - session.sends2s(build_reply("failure", "invalid-authzid")) - return true - end - - local cert = session.conn:socket():getpeercertificate() - if (cert_verify_identity(text, "xmpp-server", cert)) then - session.cert_identity_status = "valid" - else - session.cert_identity_status = "invalid" - session.sends2s(build_reply("failure", "invalid-authzid")) - return true - end + -- The text value is either "" or equals session.from_host + if not ( text == "" or text == session.from_host ) then + session.sends2s(build_reply("failure", "invalid-authzid")); + return true; end - session.external_auth = "succeeded" - - if not session.from_host then - session.from_host = text; + -- We've already verified the external cert identity before offering EXTERNAL + if session.cert_chain_status ~= "valid" or session.cert_identity_status ~= "valid" then + session.sends2s(build_reply("failure", "not-authorized")); + session:close(); + return true; end - session.sends2s(build_reply("success")) - local domain = text ~= "" and text or session.from_host; - module:log("info", "Accepting SASL EXTERNAL identity from %s", domain); - module:fire_event("s2s-authenticated", { session = session, host = domain }); + -- Success! + session.external_auth = "succeeded"; + session.sends2s(build_reply("success")); + module:log("info", "Accepting SASL EXTERNAL identity from %s", session.from_host); + module:fire_event("s2s-authenticated", { session = session, host = session.from_host }); session:reset_stream(); - return true + return true; end module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event) @@ -206,9 +185,12 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event) session.sasl_handler = usermanager_get_sasl_handler(module.host, session); end local mechanism = stanza.attr.mechanism; - if not session.secure and (secure_auth_only or (mechanism == "PLAIN" and not allow_unencrypted_plain_auth)) then + if not session.secure and (secure_auth_only or insecure_mechanisms:contains(mechanism)) then session.send(build_reply("failure", "encryption-required")); return true; + elseif disabled_mechanisms:contains(mechanism) then + session.send(build_reply("failure", "invalid-mechanism")); + return true; end local valid_mechanism = session.sasl_handler:select(mechanism); if not valid_mechanism then @@ -232,6 +214,10 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event) return true; end); +local function tls_unique(self) + return self.userdata["tls-unique"]:getpeerfinished(); +end + local mechanisms_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' }; local bind_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-bind' }; local xmpp_session_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-session' }; @@ -241,14 +227,32 @@ module:hook("stream-features", function(event) if secure_auth_only and not origin.secure then return; end - origin.sasl_handler = usermanager_get_sasl_handler(module.host, origin); + local sasl_handler = usermanager_get_sasl_handler(module.host, origin) + origin.sasl_handler = sasl_handler; + if origin.encrypted then + -- check wether LuaSec has the nifty binding to the function needed for tls-unique + -- FIXME: would be nice to have this check only once and not for every socket + if sasl_handler.add_cb_handler then + local socket = origin.conn:socket(); + if socket.getpeerfinished then + sasl_handler:add_cb_handler("tls-unique", tls_unique); + end + sasl_handler["userdata"] = { + ["tls-unique"] = socket; + }; + end + end local mechanisms = st.stanza("mechanisms", mechanisms_attr); - for mechanism in pairs(origin.sasl_handler:mechanisms()) do - if mechanism ~= "PLAIN" or origin.secure or allow_unencrypted_plain_auth then + for mechanism in pairs(sasl_handler:mechanisms()) do + if (not disabled_mechanisms:contains(mechanism)) and (origin.secure or not insecure_mechanisms:contains(mechanism)) then mechanisms:tag("mechanism"):text(mechanism):up(); end end - if mechanisms[1] then features:add_child(mechanisms); end + if mechanisms[1] then + features:add_child(mechanisms); + else + (origin.log or log)("warn", "No SASL mechanisms to offer"); + end else features:tag("bind", bind_attr):tag("required"):up():up(); features:tag("session", xmpp_session_attr):tag("optional"):up():up(); @@ -258,10 +262,10 @@ end); module:hook("s2s-stream-features", function(event) local origin, features = event.origin, event.features; if origin.secure and origin.type == "s2sin_unauthed" then - -- Offer EXTERNAL if chain is valid and either we didn't validate - -- the identity or it passed. - if origin.cert_chain_status == "valid" and origin.cert_identity_status ~= "invalid" then --TODO: Configurable - module:log("debug", "Offering SASL EXTERNAL") + -- Offer EXTERNAL only if both chain and identity is valid. + if origin.cert_chain_status == "valid" and origin.cert_identity_status == "valid" then + module:log("debug", "Offering SASL EXTERNAL"); + origin.external_auth = "offered" features:tag("mechanisms", { xmlns = xmlns_sasl }) :tag("mechanism"):text("EXTERNAL") :up():up(); @@ -274,7 +278,7 @@ module:hook("iq/self/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event) local resource; if stanza.attr.type == "set" then local bind = stanza.tags[1]; - resource = bind:child_with_name("resource"); + resource = bind:get_child("resource"); resource = resource and #resource.tags == 0 and resource[1] or nil; end local success, err_type, err, err_msg = sm_bind_resource(origin, resource); diff --git a/plugins/mod_storage_internal.lua b/plugins/mod_storage_internal.lua index 972ecbee..ade4f0a6 100644 --- a/plugins/mod_storage_internal.lua +++ b/plugins/mod_storage_internal.lua @@ -6,6 +6,9 @@ local driver = {}; local driver_mt = { __index = driver }; function driver:open(store, typ) + if typ and typ ~= "keyval" then + return nil, "unsupported-store"; + end return setmetatable({ store = store, type = typ }, driver_mt); end function driver:get(user) diff --git a/plugins/mod_storage_none.lua b/plugins/mod_storage_none.lua index 8f2d2f56..fa925b76 100644 --- a/plugins/mod_storage_none.lua +++ b/plugins/mod_storage_none.lua @@ -1,8 +1,11 @@ local driver = {}; local driver_mt = { __index = driver }; -function driver:open(store) - return setmetatable({ store = store }, driver_mt); +function driver:open(store, typ) + if typ and typ ~= "keyval" then + return nil, "unsupported-store"; + end + return setmetatable({ store = store, type = typ }, driver_mt); end function driver:get(user) return {}; diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua index eed3fec9..bf3c1c7b 100644 --- a/plugins/mod_storage_sql.lua +++ b/plugins/mod_storage_sql.lua @@ -1,184 +1,38 @@ ---[[ - -DB Tables: - Prosody - key-value, map - | host | user | store | key | type | value | - ProsodyArchive - list - | host | user | store | key | time | stanzatype | jsonvalue | - -Mapping: - Roster - Prosody - | host | user | "roster" | "contactjid" | type | value | - | host | user | "roster" | NULL | "json" | roster[false] data | - Account - Prosody - | host | user | "accounts" | "username" | type | value | - - Offline - ProsodyArchive - | host | user | "offline" | "contactjid" | time | "message" | json|XML | - -]] - -local type = type; -local tostring = tostring; -local tonumber = tonumber; -local pairs = pairs; -local next = next; -local setmetatable = setmetatable; -local xpcall = xpcall; -local json = require "util.json"; -local build_url = require"socket.url".build; - -local DBI; -local connection; -local host,user,store = module.host; -local params = module:get_option("sql"); - -local dburi; -local connections = module:shared "/*/sql/connection-cache"; - -local function db2uri(params) - return build_url{ - scheme = params.driver, - user = params.username, - password = params.password, - host = params.host, - port = params.port, - path = params.database, - }; -end - - -local resolve_relative_path = require "core.configmanager".resolve_relative_path; +-- luacheck: ignore 212/self -local function test_connection() - if not connection then return nil; end - if connection:ping() then - return true; - else - module:log("debug", "Database connection closed"); - connection = nil; - connections[dburi] = nil; - end -end -local function connect() - if not test_connection() then - prosody.unlock_globals(); - local dbh, err = DBI.Connect( - params.driver, params.database, - params.username, params.password, - params.host, params.port - ); - prosody.lock_globals(); - if not dbh then - module:log("debug", "Database connection failed: %s", tostring(err)); - return nil, err; - end - module:log("debug", "Successfully connected to database"); - dbh:autocommit(false); -- don't commit automatically - connection = dbh; +local json = require "util.json"; +local sql = require "util.sql"; +local xml_parse = require "util.xml".parse; +local uuid = require "util.uuid"; +local resolve_relative_path = require "util.paths".resolve_relative_path; - connections[dburi] = dbh; - end - return connection; -end +local stanza_mt = require"util.stanza".stanza_mt; +local getmetatable = getmetatable; +local t_concat = table.concat; +local function is_stanza(x) return getmetatable(x) == stanza_mt; end -local function create_table() - if not module:get_option("sql_manage_tables", true) then - return; - end - local create_sql = "CREATE TABLE `prosody` (`host` TEXT, `user` TEXT, `store` TEXT, `key` TEXT, `type` TEXT, `value` TEXT);"; - if params.driver == "PostgreSQL" then - create_sql = create_sql:gsub("`", "\""); - elseif params.driver == "MySQL" then - create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT"); - end - - local stmt, err = connection:prepare(create_sql); - if stmt then - local ok = stmt:execute(); - local commit_ok = connection:commit(); - if ok and commit_ok then - module:log("info", "Initialized new %s database with prosody table", params.driver); - local index_sql = "CREATE INDEX `prosody_index` ON `prosody` (`host`, `user`, `store`, `key`)"; - if params.driver == "PostgreSQL" then - index_sql = index_sql:gsub("`", "\""); - elseif params.driver == "MySQL" then - index_sql = index_sql:gsub("`([,)])", "`(20)%1"); - end - local stmt, err = connection:prepare(index_sql); - local ok, commit_ok, commit_err; - if stmt then - ok, err = stmt:execute(); - commit_ok, commit_err = connection:commit(); - end - if not(ok and commit_ok) then - module:log("warn", "Failed to create index (%s), lookups may not be optimised", err or commit_err); - end - elseif params.driver == "MySQL" then -- COMPAT: Upgrade tables from 0.8.0 - -- Failed to create, but check existing MySQL table here - local stmt = connection:prepare("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'"); - local ok = stmt:execute(); - local commit_ok = connection:commit(); - if ok and commit_ok then - if stmt:rowcount() > 0 then - module:log("info", "Upgrading database schema..."); - local stmt = connection:prepare("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT"); - local ok, err = stmt:execute(); - local commit_ok = connection:commit(); - if ok and commit_ok then - module:log("info", "Database table automatically upgraded"); - else - module:log("error", "Failed to upgrade database schema (%s), please see " - .."http://prosody.im/doc/mysql for help", - err or "unknown error"); - end - end - repeat until not stmt:fetch(); - end +local noop = function() end +local unpack = unpack +local function iterator(result) + return function(result_) + local row = result_(); + if row ~= nil then + return unpack(row); end - elseif params.driver ~= "SQLite3" then -- SQLite normally fails to prepare for existing table - module:log("warn", "Prosody was not able to automatically check/create the database table (%s), " - .."see http://prosody.im/doc/modules/mod_storage_sql#table_management for help.", - err or "unknown error"); - end + end, result, nil; end -do -- process options to get a db connection - local ok; - prosody.unlock_globals(); - ok, DBI = pcall(require, "DBI"); - if not ok then - package.loaded["DBI"] = {}; - module:log("error", "Failed to load the LuaDBI library for accessing SQL databases: %s", DBI); - module:log("error", "More information on installing LuaDBI can be found at http://prosody.im/doc/depends#luadbi"); - end - prosody.lock_globals(); - if not ok or not DBI.Connect then - return; -- Halt loading of this module - end +local default_params = { driver = "SQLite3" }; - params = params or { driver = "SQLite3" }; - - if params.driver == "SQLite3" then - params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite"); - end - - assert(params.driver and params.database, "Both the SQL driver and the database need to be specified"); - - dburi = db2uri(params); - connection = connections[dburi]; - - assert(connect()); - - -- Automatically create table, ignore failure (table probably already exists) - create_table(); -end +local engine; local function serialize(value) local t = type(value); if t == "string" or t == "boolean" or t == "number" then return t, tostring(value); + elseif is_stanza(value) then + return "xml", tostring(value); elseif t == "table" then local value,err = json.encode(value); if value then return "json", value; end @@ -194,55 +48,21 @@ local function deserialize(t, value) elseif t == "number" then return tonumber(value); elseif t == "json" then return json.decode(value); + elseif t == "xml" then + return xml_parse(value); end end -local function dosql(sql, ...) - if params.driver == "PostgreSQL" then - sql = sql:gsub("`", "\""); - end - -- do prepared statement stuff - local stmt, err = connection:prepare(sql); - if not stmt and not test_connection() then error("connection failed"); end - if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end - -- run query - local ok, err = stmt:execute(...); - if not ok and not test_connection() then error("connection failed"); end - if not ok then return nil, err; end - - return stmt; -end -local function getsql(sql, ...) - return dosql(sql, host or "", user or "", store or "", ...); -end -local function setsql(sql, ...) - local stmt, err = getsql(sql, ...); - if not stmt then return stmt, err; end - return stmt:affected(); -end -local function transact(...) - -- ... -end -local function rollback(...) - if connection then connection:rollback(); end -- FIXME check for rollback error? - return ...; -end -local function commit(...) - local success,err = connection:commit(); - if not success then return nil, "SQL commit failed: "..tostring(err); end - return ...; -end +local host = module.host; +local user, store; local function keyval_store_get() - local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?"); - if not stmt then return rollback(nil, err); end - local haveany; local result = {}; - for row in stmt:rows(true) do + for row in engine:select("SELECT `key`,`type`,`value` FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?", host, user or "", store) do haveany = true; - local k = row.key; - local v = deserialize(row.type, row.value); + local k = row[1]; + local v = deserialize(row[2], row[3]); if k and v then if k ~= "" then result[k] = v; elseif type(v) == "table" then for a,b in pairs(v) do @@ -251,164 +71,434 @@ local function keyval_store_get() end end end - return commit(haveany and result or nil); + if haveany then + return result; + end end local function keyval_store_set(data) - local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?"); - if not affected then return rollback(affected, err); end - + engine:delete("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?", host, user or "", store); + if data and next(data) ~= nil then local extradata = {}; for key, value in pairs(data) do if type(key) == "string" and key ~= "" then local t, value = serialize(value); - if not t then return rollback(t, value); end - local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value); - if not ok then return rollback(ok, err); end + assert(t, value); + engine:insert("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", host, user or "", store, key, t, value); else extradata[key] = value; end end if next(extradata) ~= nil then local t, extradata = serialize(extradata); - if not t then return rollback(t, extradata); end - local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", "", t, extradata); - if not ok then return rollback(ok, err); end + assert(t, extradata); + engine:insert("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", host, user or "", store, "", t, extradata); end end - return commit(true); + return true; end +--- Key/value store API (default store type) + local keyval_store = {}; keyval_store.__index = keyval_store; function keyval_store:get(username) - user,store = username,self.store; - if not connection and not connect() then return nil, "Unable to connect to database"; end - local success, ret, err = xpcall(keyval_store_get, debug.traceback); - if not connection and connect() then - success, ret, err = xpcall(keyval_store_get, debug.traceback); + user, store = username, self.store; + local ok, result = engine:transaction(keyval_store_get); + if not ok then + module:log("error", "Unable to read from database %s store for %s: %s", store, username or "<host>", result); + return nil, result; end - if success then return ret, err; else return rollback(nil, ret); end + return result; end function keyval_store:set(username, data) user,store = username,self.store; - if not connection and not connect() then return nil, "Unable to connect to database"; end - local success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback); - if not connection and connect() then - success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback); - end - if success then return ret, err; else return rollback(nil, ret); end + return engine:transaction(function() + return keyval_store_set(data); + end); end function keyval_store:users() - local stmt, err = dosql("SELECT DISTINCT `user` FROM `prosody` WHERE `host`=? AND `store`=?", host, self.store); - if not stmt then - return rollback(nil, err); - end - local next = stmt:rows(); - return commit(function() - local row = next(); - return row and row[1]; + local ok, result = engine:transaction(function() + return engine:select("SELECT DISTINCT `user` FROM `prosody` WHERE `host`=? AND `store`=?", host, self.store); end); + if not ok then return ok, result end + return iterator(result); end -local function map_store_get(key) - local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or ""); - if not stmt then return rollback(nil, err); end - - local haveany; - local result = {}; - for row in stmt:rows(true) do - haveany = true; - local k = row.key; - local v = deserialize(row.type, row.value); - if k and v then - if k ~= "" then result[k] = v; elseif type(v) == "table" then - for a,b in pairs(v) do - result[a] = b; +--- Archive store API + +-- luacheck: ignore 512 431/user 431/store +local map_store = {}; +map_store.__index = map_store; +map_store.remove = {}; +function map_store:get(username, key) + local ok, result = engine:transaction(function() + if type(key) == "string" and key ~= "" then + for row in engine:select("SELECT `type`, `value` FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", host, username or "", self.store, key) do + return deserialize(row[1], row[2]); + end + else + for row in engine:select("SELECT `type`, `value` FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", host, username or "", self.store, "") do + local data = deserialize(row[1], row[2]); + return data and data[key] or nil; + end + end + end); + if not ok then return nil, result; end + return result; +end +function map_store:set(username, key, data) + if data == nil then data = self.remove; end + return self:set_keys(username, { [key] = data }); +end +function map_store:set_keys(username, keydatas) + local ok, result = engine:transaction(function() + for key, data in pairs(keydatas) do + if type(key) == "string" and key ~= "" then + engine:delete("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", + host, username or "", self.store, key); + if data ~= self.remove then + local t, value = assert(serialize(data)); + engine:insert("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", host, username or "", self.store, key, t, value); + end + else + local extradata = {}; + for row in engine:select("SELECT `type`, `value` FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", host, username or "", self.store, "") do + extradata = deserialize(row[1], row[2]); + break; end + engine:delete("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", + host, username or "", self.store, ""); + extradata[key] = data; + local t, value = assert(serialize(extradata)); + engine:insert("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", host, username or "", self.store, "", t, value); end end + return true; + end); + if not ok then return nil, result; end + return result; +end + +local archive_store = {} +archive_store.caps = { + total = true; +}; +archive_store.__index = archive_store +function archive_store:append(username, key, value, when, with) + if type(when) ~= "number" then + when, with, value = value, when, with; end - return commit(haveany and result[key] or nil); + local user,store = username,self.store; + return engine:transaction(function() + if key then + engine:delete("DELETE FROM `prosodyarchive` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", host, user or "", store, key); + else + key = uuid.generate(); + end + local t, value = serialize(value); + engine:insert("INSERT INTO `prosodyarchive` (`host`, `user`, `store`, `when`, `with`, `key`, `type`, `value`) VALUES (?,?,?,?,?,?,?,?)", host, user or "", store, when, with, key, t, value); + return key; + end); end -local function map_store_set(key, data) - local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or ""); - if not affected then return rollback(affected, err); end - - if data and next(data) ~= nil then - if type(key) == "string" and key ~= "" then - local t, value = serialize(data); - if not t then return rollback(t, value); end - local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value); - if not ok then return rollback(ok, err); end + +-- Helpers for building the WHERE clause +local function archive_where(query, args, where) + -- Time range, inclusive + if query.start then + args[#args+1] = query.start + where[#where+1] = "`when` >= ?" + end + + if query["end"] then + args[#args+1] = query["end"]; + if query.start then + where[#where] = "`when` BETWEEN ? AND ?" -- is this inclusive? else - -- TODO non-string keys + where[#where+1] = "`when` <= ?" end end - return commit(true); -end -local map_store = {}; -map_store.__index = map_store; -function map_store:get(username, key) - user,store = username,self.store; - local success, ret, err = xpcall(function() return map_store_get(key); end, debug.traceback); - if success then return ret, err; else return rollback(nil, ret); end + -- Related name + if query.with then + where[#where+1] = "`with` = ?"; + args[#args+1] = query.with + end + + -- Unique id + if query.key then + where[#where+1] = "`key` = ?"; + args[#args+1] = query.key + end end -function map_store:set(username, key, data) - user,store = username,self.store; - local success, ret, err = xpcall(function() return map_store_set(key, data); end, debug.traceback); - if success then return ret, err; else return rollback(nil, ret); end +local function archive_where_id_range(query, args, where) + local args_len = #args + -- Before or after specific item, exclusive + if query.after then -- keys better be unique! + where[#where+1] = "`sort_id` > COALESCE((SELECT `sort_id` FROM `prosodyarchive` WHERE `key` = ? AND `host` = ? AND `user` = ? AND `store` = ? LIMIT 1), 0)" + args[args_len+1], args[args_len+2], args[args_len+3], args[args_len+4] = query.after, args[1], args[2], args[3]; + args_len = args_len + 4 + end + if query.before then + where[#where+1] = "`sort_id` < COALESCE((SELECT `sort_id` FROM `prosodyarchive` WHERE `key` = ? AND `host` = ? AND `user` = ? AND `store` = ? LIMIT 1), (SELECT MAX(`sort_id`)+1 FROM `prosodyarchive`))" + args[args_len+1], args[args_len+2], args[args_len+3], args[args_len+4] = query.before, args[1], args[2], args[3]; + end end -local list_store = {}; -list_store.__index = list_store; -function list_store:scan(username, from, to, jid, typ) - user,store = username,self.store; - - local cols = {"from", "to", "jid", "typ"}; - local vals = { from , to , jid , typ }; - local stmt, err; - local query = "SELECT * FROM `prosodyarchive` WHERE `host`=? AND `user`=? AND `store`=?"; - - query = query.." ORDER BY time"; - --local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or ""); - - return nil, "not-implemented" +function archive_store:find(username, query) + query = query or {}; + local user,store = username,self.store; + local total; + local ok, result = engine:transaction(function() + local sql_query = "SELECT `key`, `type`, `value`, `when`, `with` FROM `prosodyarchive` WHERE %s ORDER BY `sort_id` %s%s;"; + local args = { host, user or "", store, }; + local where = { "`host` = ?", "`user` = ?", "`store` = ?", }; + + archive_where(query, args, where); + + -- Total matching + if query.total then + local stats = engine:select("SELECT COUNT(*) FROM `prosodyarchive` WHERE " .. t_concat(where, " AND "), unpack(args)); + if stats then + local _total = stats() + total = _total and _total[1]; + end + if query.limit == 0 then -- Skip the real query + return noop, total; + end + end + + archive_where_id_range(query, args, where); + + if query.limit then + args[#args+1] = query.limit; + end + + sql_query = sql_query:format(t_concat(where, " AND "), query.reverse and "DESC" or "ASC", query.limit and " LIMIT ?" or ""); + return engine:select(sql_query, unpack(args)); + end); + if not ok then return ok, result end + return function() + local row = result(); + if row ~= nil then + return row[1], deserialize(row[2], row[3]), row[4], row[5]; + end + end, total; end +function archive_store:delete(username, query) + query = query or {}; + local user,store = username,self.store; + return engine:transaction(function() + local sql_query = "DELETE FROM `prosodyarchive` WHERE %s;"; + local args = { host, user or "", store, }; + local where = { "`host` = ?", "`user` = ?", "`store` = ?", }; + if user == true then + table.remove(args, 2); + table.remove(where, 2); + end + archive_where(query, args, where); + archive_where_id_range(query, args, where); + sql_query = sql_query:format(t_concat(where, " AND ")); + return engine:delete(sql_query, unpack(args)); + end); +end + +local stores = { + keyval = keyval_store; + map = map_store; + archive = archive_store; +}; + +--- Implement storage driver API + +-- FIXME: Some of these operations need to operate on the archive store(s) too + local driver = {}; function driver:open(store, typ) - if not typ then -- default key-value store - return setmetatable({ store = store }, keyval_store); + local store_mt = stores[typ or "keyval"]; + if store_mt then + return setmetatable({ store = store }, store_mt); end return nil, "unsupported-store"; end function driver:stores(username) - local sql = "SELECT DISTINCT `store` FROM `prosody` WHERE `host`=? AND `user`" .. + local query = "SELECT DISTINCT `store` FROM `prosody` WHERE `host`=? AND `user`" .. (username == true and "!=?" or "=?"); if username == true or not username then username = ""; end - local stmt, err = dosql(sql, host, username); - if not stmt then - return rollback(nil, err); - end - local next = stmt:rows(); - return commit(function() - local row = next(); - return row and row[1]; + local ok, result = engine:transaction(function() + return engine:select(query, host, username); end); + if not ok then return ok, result end + return iterator(result); end function driver:purge(username) - local stmt, err = dosql("DELETE FROM `prosody` WHERE `host`=? AND `user`=?", host, username); - if not stmt then return rollback(stmt, err); end - local changed, err = stmt:affected(); - if not changed then return rollback(changed, err); end - return commit(true, changed); + return engine:transaction(function() + local stmt,err = engine:delete("DELETE FROM `prosody` WHERE `host`=? AND `user`=?", host, username); + return true, err; + end); +end + +--- Initialization + + +local function create_table(name) + local Table, Column, Index = sql.Table, sql.Column, sql.Index; + + local ProsodyTable = Table { + name= name or "prosody"; + Column { name="host", type="TEXT", nullable=false }; + Column { name="user", type="TEXT", nullable=false }; + Column { name="store", type="TEXT", nullable=false }; + Column { name="key", type="TEXT", nullable=false }; + Column { name="type", type="TEXT", nullable=false }; + Column { name="value", type="MEDIUMTEXT", nullable=false }; + Index { name="prosody_index", "host", "user", "store", "key" }; + }; + engine:transaction(function() + ProsodyTable:create(engine); + end); + + local ProsodyArchiveTable = Table { + name="prosodyarchive"; + Column { name="sort_id", type="INTEGER", primary_key=true, auto_increment=true }; + Column { name="host", type="TEXT", nullable=false }; + Column { name="user", type="TEXT", nullable=false }; + Column { name="store", type="TEXT", nullable=false }; + Column { name="key", type="TEXT", nullable=false }; -- item id + Column { name="when", type="INTEGER", nullable=false }; -- timestamp + Column { name="with", type="TEXT", nullable=false }; -- related id + Column { name="type", type="TEXT", nullable=false }; + Column { name="value", type="MEDIUMTEXT", nullable=false }; + Index { name="prosodyarchive_index", unique = true, "host", "user", "store", "key" }; + }; + engine:transaction(function() + ProsodyArchiveTable:create(engine); + end); +end + +local function upgrade_table(params, apply_changes) + local changes = false; + if params.driver == "MySQL" then + local success,err = engine:transaction(function() + local result = engine:execute("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'"); + if result:rowcount() > 0 then + changes = true; + if apply_changes then + module:log("info", "Upgrading database schema..."); + engine:execute("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT"); + module:log("info", "Database table automatically upgraded"); + end + end + return true; + end); + if not success then + module:log("error", "Failed to check/upgrade database schema (%s), please see " + .."http://prosody.im/doc/mysql for help", + err or "unknown error"); + return false; + end + + -- COMPAT w/pre-0.10: Upgrade table to UTF-8 if not already + local check_encoding_query = "SELECT `COLUMN_NAME`,`COLUMN_TYPE`,`TABLE_NAME` FROM `information_schema`.`columns` WHERE `TABLE_NAME` LIKE 'prosody%%' AND ( `CHARACTER_SET_NAME`!='%s' OR `COLLATION_NAME`!='%s_bin' );"; + check_encoding_query = check_encoding_query:format(engine.charset, engine.charset); + success,err = engine:transaction(function() + local result = engine:execute(check_encoding_query); + local n_bad_columns = result:rowcount(); + if n_bad_columns > 0 then + changes = true; + if apply_changes then + module:log("warn", "Found %d columns in prosody table requiring encoding change, updating now...", n_bad_columns); + local fix_column_query1 = "ALTER TABLE `%s` CHANGE `%s` `%s` BLOB;"; + local fix_column_query2 = "ALTER TABLE `%s` CHANGE `%s` `%s` %s CHARACTER SET '%s' COLLATE '%s_bin';"; + for row in result:rows() do + local column_name, column_type, table_name = unpack(row); + module:log("debug", "Fixing column %s in table %s", column_name, table_name); + engine:execute(fix_column_query1:format(table_name, column_name, column_name)); + engine:execute(fix_column_query2:format(table_name, column_name, column_name, column_type, engine.charset, engine.charset)); + end + module:log("info", "Database encoding upgrade complete!"); + end + end + end); + success,err = engine:transaction(function() return engine:execute(check_encoding_query); end); + if not success then + module:log("error", "Failed to check/upgrade database encoding: %s", err or "unknown error"); + return false; + end + end + return changes; +end + +local function normalize_params(params) + if params.driver == "SQLite3" then + if params.database ~= ":memory:" then + params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite"); + end + end + assert(params.driver and params.database, "Configuration error: Both the SQL driver and the database need to be specified"); + return params; end -module:provides("storage", driver); +function module.load() + if prosody.prosodyctl then return; end + local engines = module:shared("/*/sql/connections"); + local params = normalize_params(module:get_option("sql", default_params)); + engine = engines[sql.db2uri(params)]; + if not engine then + module:log("debug", "Creating new engine"); + engine = sql:create_engine(params, function (engine) + if module:get_option("sql_manage_tables", true) then + -- Automatically create table, ignore failure (table probably already exists) + -- FIXME: we should check in information_schema, etc. + create_table(); + -- Check whether the table needs upgrading + if upgrade_table(params, false) then + module:log("error", "Old database format detected. Please run: prosodyctl mod_%s upgrade", module.name); + return false, "database upgrade needed"; + end + end + end); + engines[sql.db2uri(params)] = engine; + end + + module:provides("storage", driver); +end + +function module.command(arg) + local config = require "core.configmanager"; + local prosodyctl = require "util.prosodyctl"; + local command = table.remove(arg, 1); + if command == "upgrade" then + -- We need to find every unique dburi in the config + local uris = {}; + for host in pairs(prosody.hosts) do + local params = config.get(host, "sql") or default_params; + uris[sql.db2uri(params)] = params; + end + print("We will check and upgrade the following databases:\n"); + for _, params in pairs(uris) do + print("", "["..params.driver.."] "..params.database..(params.host and " on "..params.host or "")); + end + print(""); + print("Ensure you have working backups of the above databases before continuing! "); + if not prosodyctl.show_yesno("Continue with the database upgrade? [yN]") then + print("Ok, no upgrade. But you do have backups, don't you? ...don't you?? :-)"); + return; + end + -- Upgrade each one + for _, params in pairs(uris) do + print("Checking "..params.database.."..."); + engine = sql:create_engine(params); + upgrade_table(params, true); + end + print("All done!"); + else + print("Unknown command: "..command); + end +end diff --git a/plugins/mod_storage_sql1.lua b/plugins/mod_storage_sql1.lua new file mode 100644 index 00000000..a5bb5bfa --- /dev/null +++ b/plugins/mod_storage_sql1.lua @@ -0,0 +1,414 @@ + +--[[ + +DB Tables: + Prosody - key-value, map + | host | user | store | key | type | value | + ProsodyArchive - list + | host | user | store | key | time | stanzatype | jsonvalue | + +Mapping: + Roster - Prosody + | host | user | "roster" | "contactjid" | type | value | + | host | user | "roster" | NULL | "json" | roster[false] data | + Account - Prosody + | host | user | "accounts" | "username" | type | value | + + Offline - ProsodyArchive + | host | user | "offline" | "contactjid" | time | "message" | json|XML | + +]] + +local type = type; +local tostring = tostring; +local tonumber = tonumber; +local pairs = pairs; +local next = next; +local setmetatable = setmetatable; +local xpcall = xpcall; +local json = require "util.json"; +local build_url = require"socket.url".build; + +local DBI; +local connection; +local host,user,store = module.host; +local params = module:get_option("sql"); + +local dburi; +local connections = module:shared "/*/sql/connection-cache"; + +local function db2uri(params) + return build_url{ + scheme = params.driver, + user = params.username, + password = params.password, + host = params.host, + port = params.port, + path = params.database, + }; +end + + +local resolve_relative_path = require "util.paths".resolve_relative_path; + +local function test_connection() + if not connection then return nil; end + if connection:ping() then + return true; + else + module:log("debug", "Database connection closed"); + connection = nil; + connections[dburi] = nil; + end +end +local function connect() + if not test_connection() then + prosody.unlock_globals(); + local dbh, err = DBI.Connect( + params.driver, params.database, + params.username, params.password, + params.host, params.port + ); + prosody.lock_globals(); + if not dbh then + module:log("debug", "Database connection failed: %s", tostring(err)); + return nil, err; + end + module:log("debug", "Successfully connected to database"); + dbh:autocommit(false); -- don't commit automatically + connection = dbh; + + connections[dburi] = dbh; + end + return connection; +end + +local function create_table() + if not module:get_option("sql_manage_tables", true) then + return; + end + local create_sql = "CREATE TABLE `prosody` (`host` TEXT, `user` TEXT, `store` TEXT, `key` TEXT, `type` TEXT, `value` TEXT);"; + if params.driver == "PostgreSQL" then + create_sql = create_sql:gsub("`", "\""); + elseif params.driver == "MySQL" then + create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT"); + end + + local stmt, err = connection:prepare(create_sql); + if stmt then + local ok = stmt:execute(); + local commit_ok = connection:commit(); + if ok and commit_ok then + module:log("info", "Initialized new %s database with prosody table", params.driver); + local index_sql = "CREATE INDEX `prosody_index` ON `prosody` (`host`, `user`, `store`, `key`)"; + if params.driver == "PostgreSQL" then + index_sql = index_sql:gsub("`", "\""); + elseif params.driver == "MySQL" then + index_sql = index_sql:gsub("`([,)])", "`(20)%1"); + end + local stmt, err = connection:prepare(index_sql); + local ok, commit_ok, commit_err; + if stmt then + ok, err = stmt:execute(); + commit_ok, commit_err = connection:commit(); + end + if not(ok and commit_ok) then + module:log("warn", "Failed to create index (%s), lookups may not be optimised", err or commit_err); + end + elseif params.driver == "MySQL" then -- COMPAT: Upgrade tables from 0.8.0 + -- Failed to create, but check existing MySQL table here + local stmt = connection:prepare("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'"); + local ok = stmt:execute(); + local commit_ok = connection:commit(); + if ok and commit_ok then + if stmt:rowcount() > 0 then + module:log("info", "Upgrading database schema..."); + local stmt = connection:prepare("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT"); + local ok, err = stmt:execute(); + local commit_ok = connection:commit(); + if ok and commit_ok then + module:log("info", "Database table automatically upgraded"); + else + module:log("error", "Failed to upgrade database schema (%s), please see " + .."http://prosody.im/doc/mysql for help", + err or "unknown error"); + end + end + repeat until not stmt:fetch(); + end + end + elseif params.driver ~= "SQLite3" then -- SQLite normally fails to prepare for existing table + module:log("warn", "Prosody was not able to automatically check/create the database table (%s), " + .."see http://prosody.im/doc/modules/mod_storage_sql#table_management for help.", + err or "unknown error"); + end +end + +do -- process options to get a db connection + local ok; + prosody.unlock_globals(); + ok, DBI = pcall(require, "DBI"); + if not ok then + package.loaded["DBI"] = {}; + module:log("error", "Failed to load the LuaDBI library for accessing SQL databases: %s", DBI); + module:log("error", "More information on installing LuaDBI can be found at http://prosody.im/doc/depends#luadbi"); + end + prosody.lock_globals(); + if not ok or not DBI.Connect then + return; -- Halt loading of this module + end + + params = params or { driver = "SQLite3" }; + + if params.driver == "SQLite3" then + params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite"); + end + + assert(params.driver and params.database, "Both the SQL driver and the database need to be specified"); + + dburi = db2uri(params); + connection = connections[dburi]; + + assert(connect()); + + -- Automatically create table, ignore failure (table probably already exists) + create_table(); +end + +local function serialize(value) + local t = type(value); + if t == "string" or t == "boolean" or t == "number" then + return t, tostring(value); + elseif t == "table" then + local value,err = json.encode(value); + if value then return "json", value; end + return nil, err; + end + return nil, "Unhandled value type: "..t; +end +local function deserialize(t, value) + if t == "string" then return value; + elseif t == "boolean" then + if value == "true" then return true; + elseif value == "false" then return false; end + elseif t == "number" then return tonumber(value); + elseif t == "json" then + return json.decode(value); + end +end + +local function dosql(sql, ...) + if params.driver == "PostgreSQL" then + sql = sql:gsub("`", "\""); + end + -- do prepared statement stuff + local stmt, err = connection:prepare(sql); + if not stmt and not test_connection() then error("connection failed"); end + if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end + -- run query + local ok, err = stmt:execute(...); + if not ok and not test_connection() then error("connection failed"); end + if not ok then return nil, err; end + + return stmt; +end +local function getsql(sql, ...) + return dosql(sql, host or "", user or "", store or "", ...); +end +local function setsql(sql, ...) + local stmt, err = getsql(sql, ...); + if not stmt then return stmt, err; end + return stmt:affected(); +end +local function transact(...) + -- ... +end +local function rollback(...) + if connection then connection:rollback(); end -- FIXME check for rollback error? + return ...; +end +local function commit(...) + local success,err = connection:commit(); + if not success then return nil, "SQL commit failed: "..tostring(err); end + return ...; +end + +local function keyval_store_get() + local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?"); + if not stmt then return rollback(nil, err); end + + local haveany; + local result = {}; + for row in stmt:rows(true) do + haveany = true; + local k = row.key; + local v = deserialize(row.type, row.value); + if k and v then + if k ~= "" then result[k] = v; elseif type(v) == "table" then + for a,b in pairs(v) do + result[a] = b; + end + end + end + end + return commit(haveany and result or nil); +end +local function keyval_store_set(data) + local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?"); + if not affected then return rollback(affected, err); end + + if data and next(data) ~= nil then + local extradata = {}; + for key, value in pairs(data) do + if type(key) == "string" and key ~= "" then + local t, value = serialize(value); + if not t then return rollback(t, value); end + local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value); + if not ok then return rollback(ok, err); end + else + extradata[key] = value; + end + end + if next(extradata) ~= nil then + local t, extradata = serialize(extradata); + if not t then return rollback(t, extradata); end + local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", "", t, extradata); + if not ok then return rollback(ok, err); end + end + end + return commit(true); +end + +local keyval_store = {}; +keyval_store.__index = keyval_store; +function keyval_store:get(username) + user,store = username,self.store; + if not connection and not connect() then return nil, "Unable to connect to database"; end + local success, ret, err = xpcall(keyval_store_get, debug.traceback); + if not connection and connect() then + success, ret, err = xpcall(keyval_store_get, debug.traceback); + end + if success then return ret, err; else return rollback(nil, ret); end +end +function keyval_store:set(username, data) + user,store = username,self.store; + if not connection and not connect() then return nil, "Unable to connect to database"; end + local success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback); + if not connection and connect() then + success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback); + end + if success then return ret, err; else return rollback(nil, ret); end +end +function keyval_store:users() + local stmt, err = dosql("SELECT DISTINCT `user` FROM `prosody` WHERE `host`=? AND `store`=?", host, self.store); + if not stmt then + return rollback(nil, err); + end + local next = stmt:rows(); + return commit(function() + local row = next(); + return row and row[1]; + end); +end + +local function map_store_get(key) + local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or ""); + if not stmt then return rollback(nil, err); end + + local haveany; + local result = {}; + for row in stmt:rows(true) do + haveany = true; + local k = row.key; + local v = deserialize(row.type, row.value); + if k and v then + if k ~= "" then result[k] = v; elseif type(v) == "table" then + for a,b in pairs(v) do + result[a] = b; + end + end + end + end + return commit(haveany and result[key] or nil); +end +local function map_store_set(key, data) + local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or ""); + if not affected then return rollback(affected, err); end + + if data and next(data) ~= nil then + if type(key) == "string" and key ~= "" then + local t, value = serialize(data); + if not t then return rollback(t, value); end + local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value); + if not ok then return rollback(ok, err); end + else + -- TODO non-string keys + end + end + return commit(true); +end + +local map_store = {}; +map_store.__index = map_store; +function map_store:get(username, key) + user,store = username,self.store; + local success, ret, err = xpcall(function() return map_store_get(key); end, debug.traceback); + if success then return ret, err; else return rollback(nil, ret); end +end +function map_store:set(username, key, data) + user,store = username,self.store; + local success, ret, err = xpcall(function() return map_store_set(key, data); end, debug.traceback); + if success then return ret, err; else return rollback(nil, ret); end +end + +local list_store = {}; +list_store.__index = list_store; +function list_store:scan(username, from, to, jid, typ) + user,store = username,self.store; + + local cols = {"from", "to", "jid", "typ"}; + local vals = { from , to , jid , typ }; + local stmt, err; + local query = "SELECT * FROM `prosodyarchive` WHERE `host`=? AND `user`=? AND `store`=?"; + + query = query.." ORDER BY time"; + --local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or ""); + + return nil, "not-implemented" +end + +local driver = {}; + +function driver:open(store, typ) + if typ and typ ~= "keyval" then + return nil, "unsupported-store"; + end + return setmetatable({ store = store }, keyval_store); +end + +function driver:stores(username) + local sql = "SELECT DISTINCT `store` FROM `prosody` WHERE `host`=? AND `user`" .. + (username == true and "!=?" or "=?"); + if username == true or not username then + username = ""; + end + local stmt, err = dosql(sql, host, username); + if not stmt then + return rollback(nil, err); + end + local next = stmt:rows(); + return commit(function() + local row = next(); + return row and row[1]; + end); +end + +function driver:purge(username) + local stmt, err = dosql("DELETE FROM `prosody` WHERE `host`=? AND `user`=?", host, username); + if not stmt then return rollback(stmt, err); end + local changed, err = stmt:affected(); + if not changed then return rollback(changed, err); end + return commit(true, changed); +end + +module:provides("storage", driver); diff --git a/plugins/storage/mod_xep0227.lua b/plugins/mod_storage_xep0227.lua index 5d07a2ea..ef227ca3 100644 --- a/plugins/storage/mod_xep0227.lua +++ b/plugins/mod_storage_xep0227.lua @@ -7,28 +7,31 @@ local t_remove = table.remove; local os_remove = os.remove; local io_open = io.open; +local paths = require"util.paths"; local st = require "util.stanza"; local parse_xml_real = require "util.xml".parse; local function getXml(user, host) local jid = user.."@"..host; - local path = "data/"..jid..".xml"; + local path = paths.join(prosody.paths.data, jid..".xml"); local f = io_open(path); if not f then return; end local s = f:read("*a"); + f:close(); return parse_xml_real(s); end local function setXml(user, host, xml) local jid = user.."@"..host; - local path = "data/"..jid..".xml"; + local path = paths.join(prosody.paths.data, jid..".xml"); + local f, err = io_open(path, "w"); + if not f then return f, err; end if xml then - local f = io_open(path, "w"); - if not f then return; end local s = tostring(xml); f:write(s); f:close(); return true; else + f:close(); return os_remove(path); end end @@ -44,7 +47,7 @@ local function getUserElement(xml) end end local function createOuterXml(user, host) - return st.stanza("server-data", {xmlns='http://www.xmpp.org/extensions/xep-0227.html#ns'}) + return st.stanza("server-data", {xmlns='urn:xmpp:pie:0'}) :tag("host", {jid=host}) :tag("user", {name = user}); end @@ -63,19 +66,36 @@ end local handlers = {}; +-- In order to support mod_auth_internal_hashed +local extended = "http://prosody.im/protocol/extended-xep0227\1"; + handlers.accounts = { get = function(self, user) - local user = getUserElement(getXml(user, self.host)); + user = getUserElement(getXml(user, self.host)); if user and user.attr.password then return { password = user.attr.password }; + elseif user then + local data = {}; + for k, v in pairs(user.attr) do + if k:sub(1, #extended) == extended then + data[k:sub(#extended+1)] = v; + end + end + return data; end end; set = function(self, user, data) - if data and data.password then + if data then local xml = getXml(user, self.host); if not xml then xml = createOuterXml(user, self.host); end local usere = getUserElement(xml); - usere.attr.password = data.password; + for k, v in pairs(data) do + if k == "password" then + usere.attr.password = v; + else + usere.attr[extended..k] = v; + end + end return setXml(user, self.host, xml); else return setXml(user, self.host, nil); @@ -84,7 +104,7 @@ handlers.accounts = { }; handlers.vcard = { get = function(self, user) - local user = getUserElement(getXml(user, self.host)); + user = getUserElement(getXml(user, self.host)); if user then local vcard = user:get_child("vCard", 'vcard-temp'); if vcard then @@ -113,7 +133,7 @@ handlers.vcard = { }; handlers.private = { get = function(self, user) - local user = getUserElement(getXml(user, self.host)); + user = getUserElement(getXml(user, self.host)); if user then local private = user:get_child("query", "jabber:iq:private"); if private then @@ -147,15 +167,10 @@ handlers.private = { ----------------------------- local driver = {}; -function driver:open(host, datastore, typ) - local instance = setmetatable({}, self); - instance.host = host; - instance.datastore = datastore; +function driver:open(datastore, typ) local handler = handlers[datastore]; - if not handler then return nil; end - for key,val in pairs(handler) do - instance[key] = val; - end + if not handler then return nil, "unsupported-datastore"; end + local instance = setmetatable({ host = module.host; datastore = datastore; }, { __index = handler }); if instance.init then instance:init(); end return instance; end diff --git a/plugins/mod_time.lua b/plugins/mod_time.lua index cb69ebe7..ae7da916 100644 --- a/plugins/mod_time.lua +++ b/plugins/mod_time.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua index 2741b8d4..69aafe82 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -1,16 +1,16 @@ -- 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 config = require "core.configmanager"; local create_context = require "core.certmanager".create_context; +local rawgetopt = require"core.configmanager".rawget; local st = require "util.stanza"; -local c2s_require_encryption = module:get_option("c2s_require_encryption") or module:get_option("require_encryption"); +local c2s_require_encryption = module:get_option("c2s_require_encryption", module:get_option("require_encryption")); local s2s_require_encryption = module:get_option("s2s_require_encryption"); local allow_s2s_tls = module:get_option("s2s_allow_encryption") ~= false; local s2s_secure_auth = module:get_option("s2s_secure_auth"); @@ -22,6 +22,7 @@ end local xmlns_starttls = 'urn:ietf:params:xml:ns:xmpp-tls'; local starttls_attr = { xmlns = xmlns_starttls }; +local starttls_initiate= st.stanza("starttls", starttls_attr); local starttls_proceed = st.stanza("proceed", starttls_attr); local starttls_failure = st.stanza("failure", starttls_attr); local c2s_feature = st.stanza("starttls", starttls_attr); @@ -29,20 +30,56 @@ local s2s_feature = st.stanza("starttls", starttls_attr); if c2s_require_encryption then c2s_feature:tag("required"):up(); end if s2s_require_encryption then s2s_feature:tag("required"):up(); end -local global_ssl_ctx = prosody.global_ssl_ctx; - local hosts = prosody.hosts; local host = hosts[module.host]; +local ssl_ctx_c2s, ssl_ctx_s2sout, ssl_ctx_s2sin; +local ssl_cfg_c2s, ssl_cfg_s2sout, ssl_cfg_s2sin; +do + local NULL, err = {}; + local modhost = module.host; + local parent = modhost:match("%.(.*)$"); + + local parent_ssl = rawgetopt(parent, "ssl") or NULL; + local host_ssl = rawgetopt(modhost, "ssl") or parent_ssl; + + local global_c2s = rawgetopt("*", "c2s_ssl") or NULL; + local parent_c2s = rawgetopt(parent, "c2s_ssl") or NULL; + local host_c2s = rawgetopt(modhost, "c2s_ssl") or parent_c2s; + + local global_s2s = rawgetopt("*", "s2s_ssl") or NULL; + local parent_s2s = rawgetopt(parent, "s2s_ssl") or NULL; + local host_s2s = rawgetopt(modhost, "s2s_ssl") or parent_s2s; + + ssl_ctx_c2s, err, ssl_cfg_c2s = create_context(host.host, "server", host_c2s, host_ssl, global_c2s); -- for incoming client connections + if not ssl_ctx_c2s then module:log("error", "Error creating context for c2s: %s", err); end + + ssl_ctx_s2sout, err, ssl_cfg_s2sout = create_context(host.host, "client", host_s2s, host_ssl, global_s2s); -- for outgoing server connections + if not ssl_ctx_s2sout then module:log("error", "Error creating contexts for s2sout: %s", err); end + + ssl_ctx_s2sin, err, ssl_cfg_s2sin = create_context(host.host, "server", host_s2s, host_ssl, global_s2s); -- for incoming server connections + if not ssl_ctx_s2sin then module:log("error", "Error creating contexts for s2sin: %s", err); end +end + local function can_do_tls(session) + if session.ssl_ctx == false or not session.conn.starttls then + return false; + elseif session.ssl_ctx then + return true; + end if session.type == "c2s_unauthed" then - return session.conn.starttls and host.ssl_ctx_in; + session.ssl_ctx = ssl_ctx_c2s; + session.ssl_cfg = ssl_cfg_c2s; elseif session.type == "s2sin_unauthed" and allow_s2s_tls then - return session.conn.starttls and host.ssl_ctx_in; + session.ssl_ctx = ssl_ctx_s2sin; + session.ssl_cfg = ssl_cfg_s2sin; elseif session.direction == "outgoing" and allow_s2s_tls then - return session.conn.starttls and host.ssl_ctx; + session.ssl_ctx = ssl_ctx_s2sout; + session.ssl_cfg = ssl_cfg_s2sout; + else + return false; end - return false; + return session.ssl_ctx; end -- Hook <starttls/> @@ -51,9 +88,7 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event) if can_do_tls(origin) then (origin.sends2s or origin.send)(starttls_proceed); origin:reset_stream(); - local host = origin.to_host or origin.host; - local ssl_ctx = host and hosts[host].ssl_ctx_in or global_ssl_ctx; - origin.conn:starttls(ssl_ctx); + origin.conn:starttls(origin.ssl_ctx); origin.log("debug", "TLS negotiation started for %s...", origin.type); origin.secure = false; else @@ -81,9 +116,9 @@ end); -- For s2sout connections, start TLS if we can module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza) module:log("debug", "Received features element"); - if can_do_tls(session) and stanza:child_with_ns(xmlns_starttls) then + if can_do_tls(session) and stanza:get_child("starttls", xmlns_starttls) then module:log("debug", "%s is offering TLS, taking up the offer...", session.to_host); - session.sends2s("<starttls xmlns='"..xmlns_starttls.."'/>"); + session.sends2s(starttls_initiate); return true; end end, 500); @@ -91,30 +126,7 @@ end, 500); module:hook_stanza(xmlns_starttls, "proceed", function (session, stanza) module:log("debug", "Proceeding with TLS on s2sout..."); session:reset_stream(); - local ssl_ctx = session.from_host and hosts[session.from_host].ssl_ctx or global_ssl_ctx; - session.conn:starttls(ssl_ctx); + session.conn:starttls(session.ssl_ctx); session.secure = false; return true; end); - -local function assert_log(ret, err) - if not ret then - module:log("error", "Unable to initialize TLS: %s", err); - end - return ret; -end - -function module.load() - local ssl_config = config.rawget(module.host, "ssl"); - if not ssl_config then - local base_host = module.host:match("%.(.*)"); - ssl_config = config.get(base_host, "ssl"); - end - host.ssl_ctx = assert_log(create_context(host.host, "client", ssl_config)); -- for outgoing connections - host.ssl_ctx_in = assert_log(create_context(host.host, "server", ssl_config)); -- for incoming connections -end - -function module.unload() - host.ssl_ctx = nil; - host.ssl_ctx_in = nil; -end diff --git a/plugins/mod_unknown.lua b/plugins/mod_unknown.lua new file mode 100644 index 00000000..4d20b8ad --- /dev/null +++ b/plugins/mod_unknown.lua @@ -0,0 +1,4 @@ +-- Unknown platform stub +module:set_global(); + +-- TODO Do things that make sense if we don't know about the platform diff --git a/plugins/mod_uptime.lua b/plugins/mod_uptime.lua index 3f275b2f..2e369b16 100644 --- a/plugins/mod_uptime.lua +++ b/plugins/mod_uptime.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/plugins/mod_vcard.lua b/plugins/mod_vcard.lua index 26b30e3a..72f92ef7 100644 --- a/plugins/mod_vcard.lua +++ b/plugins/mod_vcard.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/plugins/mod_version.lua b/plugins/mod_version.lua index d35103b6..be244beb 100644 --- a/plugins/mod_version.lua +++ b/plugins/mod_version.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/plugins/mod_watchregistrations.lua b/plugins/mod_watchregistrations.lua index abca90bd..b7be5daf 100644 --- a/plugins/mod_watchregistrations.lua +++ b/plugins/mod_watchregistrations.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/plugins/mod_websocket.lua b/plugins/mod_websocket.lua new file mode 100644 index 00000000..418cd846 --- /dev/null +++ b/plugins/mod_websocket.lua @@ -0,0 +1,304 @@ +-- Prosody IM +-- Copyright (C) 2012-2014 Florian Zeitz +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- +-- luacheck: ignore 431/log + +module:set_global(); + +local add_task = require "util.timer".add_task; +local add_filter = require "util.filters".add_filter; +local sha1 = require "util.hashes".sha1; +local base64 = require "util.encodings".base64.encode; +local st = require "util.stanza"; +local parse_xml = require "util.xml".parse; +local portmanager = require "core.portmanager"; +local sm_destroy_session = require"core.sessionmanager".destroy_session; +local log = module._log; + +local websocket_frames = require"net.websocket.frames"; +local parse_frame = websocket_frames.parse; +local build_frame = websocket_frames.build; +local build_close = websocket_frames.build_close; +local parse_close = websocket_frames.parse_close; + +local t_concat = table.concat; + +local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5); +local consider_websocket_secure = module:get_option_boolean("consider_websocket_secure"); +local cross_domain = module:get_option("cross_domain_websocket"); +if cross_domain then + if cross_domain == true then + cross_domain = "*"; + elseif type(cross_domain) == "table" then + cross_domain = t_concat(cross_domain, ", "); + end + if type(cross_domain) ~= "string" then + cross_domain = nil; + end +end + +local xmlns_framing = "urn:ietf:params:xml:ns:xmpp-framing"; +local xmlns_streams = "http://etherx.jabber.org/streams"; +local xmlns_client = "jabber:client"; +local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; + +module:depends("c2s") +local sessions = module:shared("c2s/sessions"); +local c2s_listener = portmanager.get_service("c2s").listener; + +--- Session methods +local function session_open_stream(session) + local attr = { + xmlns = xmlns_framing, + version = "1.0", + id = session.streamid or "", + from = session.host + }; + session.send(st.stanza("open", attr)); +end + +local function session_close(session, reason) + local log = session.log or log; + if session.conn then + if session.notopen then + session:open_stream(); + end + if reason then -- nil == no err, initiated by us, false == initiated by client + local stream_error = st.stanza("stream:error"); + if type(reason) == "string" then -- assume stream error + stream_error:tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' }); + elseif type(reason) == "table" then + if reason.condition then + stream_error:tag(reason.condition, stream_xmlns_attr):up(); + if reason.text then + stream_error:tag("text", stream_xmlns_attr):text(reason.text):up(); + end + if reason.extra then + stream_error:add_child(reason.extra); + end + elseif reason.name then -- a stanza + stream_error = reason; + end + end + log("debug", "Disconnecting client, <stream:error> is: %s", tostring(stream_error)); + session.send(stream_error); + end + + session.send(st.stanza("close", { xmlns = xmlns_framing })); + function session.send() return false; end + + local reason = (reason and (reason.name or reason.text or reason.condition)) or reason; + session.log("debug", "c2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason or "session closed"); + + -- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote + local conn = session.conn; + if reason == nil and not session.notopen and session.type == "c2s" then + -- Grace time to process data from authenticated cleanly-closed stream + add_task(stream_close_timeout, function () + if not session.destroyed then + session.log("warn", "Failed to receive a stream close response, closing connection anyway..."); + sm_destroy_session(session, reason); + conn:write(build_close(1000, "Stream closed")); + conn:close(); + end + end); + else + sm_destroy_session(session, reason); + conn:write(build_close(1000, "Stream closed")); + conn:close(); + end + end +end + + +--- Filters +local function filter_open_close(data) + if not data:find(xmlns_framing, 1, true) then return data; end + + local oc = parse_xml(data); + if not oc then return data; end + if oc.attr.xmlns ~= xmlns_framing then return data; end + if oc.name == "close" then return "</stream:stream>"; end + if oc.name == "open" then + oc.name = "stream:stream"; + oc.attr.xmlns = nil; + oc.attr["xmlns:stream"] = xmlns_streams; + return oc:top_tag(); + end + + return data; +end +function handle_request(event) + local request, response = event.request, event.response; + local conn = response.conn; + + if not request.headers.sec_websocket_key then + response.headers.content_type = "text/html"; + return [[<!DOCTYPE html><html><head><title>Websocket</title></head><body> + <p>It works! Now point your WebSocket client to this URL to connect to Prosody.</p> + </body></html>]]; + end + + local wants_xmpp = false; + (request.headers.sec_websocket_protocol or ""):gsub("([^,]*),?", function (proto) + if proto == "xmpp" then wants_xmpp = true; end + end); + + if not wants_xmpp then + return 501; + end + + local function websocket_close(code, message) + conn:write(build_close(code, message)); + conn:close(); + end + + local dataBuffer; + local function handle_frame(frame) + local opcode = frame.opcode; + local length = frame.length; + module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data); + + -- Error cases + if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero + websocket_close(1002, "Reserved bits not zero"); + return false; + end + + if opcode == 0x8 then -- close frame + if length == 1 then + websocket_close(1002, "Close frame with payload, but too short for status code"); + return false; + elseif length >= 2 then + local status_code = parse_close(frame.data) + if status_code < 1000 then + websocket_close(1002, "Closed with invalid status code"); + return false; + elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then + websocket_close(1002, "Closed with reserved status code"); + return false; + end + end + end + + if opcode >= 0x8 then + if length > 125 then -- Control frame with too much payload + websocket_close(1002, "Payload too large"); + return false; + end + + if not frame.FIN then -- Fragmented control frame + websocket_close(1002, "Fragmented control frame"); + return false; + end + end + + if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then + websocket_close(1002, "Reserved opcode"); + return false; + end + + if opcode == 0x0 and not dataBuffer then + websocket_close(1002, "Unexpected continuation frame"); + return false; + end + + if (opcode == 0x1 or opcode == 0x2) and dataBuffer then + websocket_close(1002, "Continuation frame expected"); + return false; + end + + -- Valid cases + if opcode == 0x0 then -- Continuation frame + dataBuffer[#dataBuffer+1] = frame.data; + elseif opcode == 0x1 then -- Text frame + dataBuffer = {frame.data}; + elseif opcode == 0x2 then -- Binary frame + websocket_close(1003, "Only text frames are supported"); + return; + elseif opcode == 0x8 then -- Close request + websocket_close(1000, "Goodbye"); + return; + elseif opcode == 0x9 then -- Ping frame + frame.opcode = 0xA; + conn:write(build_frame(frame)); + return ""; + elseif opcode == 0xA then -- Pong frame + module:log("warn", "Received unexpected pong frame: " .. tostring(frame.data)); + return ""; + else + log("warn", "Received frame with unsupported opcode %i", opcode); + return ""; + end + + if frame.FIN then + local data = t_concat(dataBuffer, ""); + dataBuffer = nil; + return data; + end + return ""; + end + + conn:setlistener(c2s_listener); + c2s_listener.onconnect(conn); + + local session = sessions[conn]; + + session.secure = consider_websocket_secure or session.secure; + + session.open_stream = session_open_stream; + session.close = session_close; + + local frameBuffer = ""; + add_filter(session, "bytes/in", function(data) + local cache = {}; + frameBuffer = frameBuffer .. data; + local frame, length = parse_frame(frameBuffer); + + while frame do + frameBuffer = frameBuffer:sub(length + 1); + local result = handle_frame(frame); + if not result then return; end + cache[#cache+1] = filter_open_close(result); + frame, length = parse_frame(frameBuffer); + end + return t_concat(cache, ""); + end); + + add_filter(session, "stanzas/out", function(stanza) + local attr = stanza.attr; + attr.xmlns = attr.xmlns or xmlns_client; + if stanza.name:find("^stream:") then + attr["xmlns:stream"] = attr["xmlns:stream"] or xmlns_streams; + end + return stanza; + end); + + add_filter(session, "bytes/out", function(data) + return build_frame({ FIN = true, opcode = 0x01, data = tostring(data)}); + end); + + response.status_code = 101; + response.headers.upgrade = "websocket"; + response.headers.connection = "Upgrade"; + response.headers.sec_webSocket_accept = base64(sha1(request.headers.sec_websocket_key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")); + response.headers.sec_webSocket_protocol = "xmpp"; + response.headers.access_control_allow_origin = cross_domain; + + return ""; +end + +function module.add_host(module) + module:depends("http"); + module:provides("http", { + name = "websocket"; + default_path = "xmpp-websocket"; + route = { + ["GET"] = handle_request; + ["GET /"] = handle_request; + }; + }); +end diff --git a/plugins/mod_welcome.lua b/plugins/mod_welcome.lua index e498f0b3..9c0c821b 100644 --- a/plugins/mod_welcome.lua +++ b/plugins/mod_welcome.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/plugins/mod_windows.lua b/plugins/mod_windows.lua new file mode 100644 index 00000000..8085fd88 --- /dev/null +++ b/plugins/mod_windows.lua @@ -0,0 +1,4 @@ +-- Windows platform stub +module:set_global(); + +-- TODO Add Windows-specific things here diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua index acc2da0d..69351504 100644 --- a/plugins/muc/mod_muc.lua +++ b/plugins/muc/mod_muc.lua @@ -1,11 +1,12 @@ -- 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 array = require "util.array"; if module:get_host_type() ~= "component" then error("MUC should be loaded as a component, please see http://prosody.im/doc/components", 0); @@ -16,12 +17,15 @@ local muc_name = module:get_option("name"); if type(muc_name) ~= "string" then muc_name = "Prosody Chatrooms"; end local restrict_room_creation = module:get_option("restrict_room_creation"); if restrict_room_creation then - if restrict_room_creation == true then + if restrict_room_creation == true then restrict_room_creation = "admin"; elseif restrict_room_creation ~= "admin" and restrict_room_creation ~= "local" then restrict_room_creation = nil; end end +local lock_rooms = module:get_option_boolean("muc_room_locking", false); +local lock_room_timeout = module:get_option_number("muc_room_lock_timeout", 300); + local muclib = module:require "muc"; local muc_new_room = muclib.new_room; local jid_split = require "util.jid".split; @@ -40,12 +44,17 @@ local room_configs = module:open_store("config"); -- Configurable options muclib.set_max_history_length(module:get_option_number("max_history_messages")); +module:depends("disco"); +module:add_identity("conference", "text", muc_name); +module:add_feature("http://jabber.org/protocol/muc"); + local function is_admin(jid) return um_is_admin(jid, module.host); end -local _set_affiliation = muc_new_room.room_mt.set_affiliation; -local _get_affiliation = muc_new_room.room_mt.get_affiliation; +room_mt = muclib.room_mt; -- Yes, global. +local _set_affiliation = room_mt.set_affiliation; +local _get_affiliation = room_mt.get_affiliation; function muclib.room_mt:get_affiliation(jid) if is_admin(jid) then return "owner"; end return _get_affiliation(self, jid); @@ -83,6 +92,16 @@ function create_room(jid) room.route_stanza = room_route_stanza; room.save = room_save; rooms[jid] = room; + if lock_rooms then + room.locked = true; + if lock_room_timeout and lock_room_timeout > 0 then + module:add_timer(lock_room_timeout, function () + if room.locked then + room:destroy(); -- Not unlocked in time + end + end); + end + end module:fire_event("muc-room-created", { room = room }); return room; end @@ -107,20 +126,15 @@ local host_room = muc_new_room(muc_host); host_room.route_stanza = room_route_stanza; host_room.save = room_save; -local function get_disco_info(stanza) - return st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info") - :tag("identity", {category='conference', type='text', name=muc_name}):up() - :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply -end -local function get_disco_items(stanza) - local reply = st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items"); +module:hook("host-disco-items", function(event) + local reply = event.reply; + module:log("debug", "host-disco-items called"); for jid, room in pairs(rooms) do - if not room:is_hidden() then + if not room:get_hidden() then reply:tag("item", {jid=jid, name=room:get_name()}):up(); end end - return reply; -- TODO cache disco reply -end +end); local function handle_to_domain(event) local origin, stanza = event.origin, event.stanza; @@ -129,11 +143,7 @@ local function handle_to_domain(event) if stanza.name == "iq" and type == "get" then local xmlns = stanza.tags[1].attr.xmlns; local node = stanza.tags[1].attr.node; - if xmlns == "http://jabber.org/protocol/disco#info" and not node then - origin.send(get_disco_info(stanza)); - elseif xmlns == "http://jabber.org/protocol/disco#items" and not node then - origin.send(get_disco_items(stanza)); - elseif xmlns == "http://jabber.org/protocol/muc#unique" then + if xmlns == "http://jabber.org/protocol/muc#unique" then origin.send(st.reply(stanza):tag("unique", {xmlns = xmlns}):text(uuid_gen())); -- FIXME Random UUIDs can theoretically have collisions else origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc @@ -220,7 +230,8 @@ function shutdown_component() if not saved then local stanza = st.presence({type = "unavailable"}) :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) - :tag("item", { affiliation='none', role='none' }):up(); + :tag("item", { affiliation='none', role='none' }):up() + :tag("status", { code = "332"}):up(); for roomjid, room in pairs(rooms) do shutdown_room(room, stanza); end @@ -229,3 +240,39 @@ function shutdown_component() end module.unload = shutdown_component; module:hook_global("server-stopping", shutdown_component); + +-- Ad-hoc commands +module:depends("adhoc") +local t_concat = table.concat; +local keys = require "util.iterators".keys; +local adhoc_new = module:require "adhoc".new; +local adhoc_initial = require "util.adhoc".new_initial_data_form; +local dataforms_new = require "util.dataforms".new; + +local destroy_rooms_layout = dataforms_new { + title = "Destroy rooms"; + instructions = "Select the rooms to destroy"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/muc#destroy" }; + { name = "rooms", type = "list-multi", required = true, label = "Rooms to destroy:"}; +}; + +local destroy_rooms_handler = adhoc_initial(destroy_rooms_layout, function() + return { rooms = array.collect(keys(rooms)):sort() }; +end, function(fields, errors) + if errors then + local errmsg = {}; + for name, err in pairs(errors) do + errmsg[#errmsg + 1] = name .. ": " .. err; + end + return { status = "completed", error = { message = t_concat(errmsg, "\n") } }; + end + for _, room in ipairs(fields.rooms) do + rooms[room]:destroy(); + rooms[room] = nil; + end + return { status = "completed", info = "The following rooms were destroyed:\n"..t_concat(fields.rooms, "\n") }; +end); +local destroy_rooms_desc = adhoc_new("Destroy Rooms", "http://prosody.im/protocol/muc#destroy", destroy_rooms_handler, "admin"); + +module:provides("adhoc", destroy_rooms_desc); diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua index 5879c256..552b9e49 100644 --- a/plugins/muc/muc.lib.lua +++ b/plugins/muc/muc.lib.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -27,28 +27,16 @@ local muc_domain = nil; --module:get_host(); local default_history_length, max_history_length = 20, math.huge; ------------ -local function filter_xmlns_from_array(array, filters) - local count = 0; - for i=#array,1,-1 do - local attr = array[i].attr; - if filters[attr and attr.xmlns] then - t_remove(array, i); - count = count + 1; - end - end - return count; -end -local function filter_xmlns_from_stanza(stanza, filters) - if filters then - if filter_xmlns_from_array(stanza.tags, filters) ~= 0 then - return stanza, filter_xmlns_from_array(stanza, filters); - end +local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true}; +local function presence_filter(tag) + if presence_filters[tag.attr.xmlns] then + return nil; end - return stanza, 0; + return tag; end -local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true}; + local function get_filtered_presence(stanza) - return filter_xmlns_from_stanza(st.clone(stanza):reset(), presence_filters); + return st.clone(stanza):maptags(presence_filter); end local kickable_error_conditions = { ["gone"] = true; @@ -72,17 +60,6 @@ local function is_kickable_error(stanza) local cond = get_error_condition(stanza); return kickable_error_conditions[cond] and cond; end -local function getUsingPath(stanza, path, getText) - local tag = stanza; - for _, name in ipairs(path) do - if type(tag) ~= 'table' then return; end - tag = tag:child_with_name(name); - end - if tag and getText then tag = table.concat(tag); end - return tag; -end -local function getTag(stanza, path) return getUsingPath(stanza, path); end -local function getText(stanza, path) return getUsingPath(stanza, path, true); end ----------- local room_mt = {}; @@ -98,8 +75,8 @@ function room_mt:get_default_role(affiliation) elseif affiliation == "member" then return "participant"; elseif not affiliation then - if not self:is_members_only() then - return self:is_moderated() and "visitor" or "participant"; + if not self:get_members_only() then + return self:get_moderated() and "visitor" or "participant"; end end end @@ -130,18 +107,21 @@ function room_mt:broadcast_message(stanza, historic) end stanza.attr.to = to; if historic then -- add to history - local history = self._data['history']; - if not history then history = {}; self._data['history'] = history; end - stanza = st.clone(stanza); - stanza.attr.to = ""; - local stamp = datetime.datetime(); - stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = stamp}):up(); -- XEP-0203 - stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated) - local entry = { stanza = stanza, stamp = stamp }; - t_insert(history, entry); - while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end + return self:save_to_history(stanza) end end +function room_mt:save_to_history(stanza) + local history = self._data['history']; + if not history then history = {}; self._data['history'] = history; end + stanza = st.clone(stanza); + stanza.attr.to = ""; + local stamp = datetime.datetime(); + stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = stamp}):up(); -- XEP-0203 + stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated) + local entry = { stanza = stanza, stamp = stamp }; + t_insert(history, entry); + while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end +end function room_mt:broadcast_except_nick(stanza, nick) for rnick, occupant in pairs(self._occupants) do if rnick ~= nick then @@ -170,10 +150,10 @@ function room_mt:send_history(to, stanza) if history then local x_tag = stanza and stanza:get_child("x", "http://jabber.org/protocol/muc"); local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc"); - + local maxchars = history_tag and tonumber(history_tag.attr.maxchars); if maxchars then maxchars = math.floor(maxchars); end - + local maxstanzas = math.floor(history_tag and tonumber(history_tag.attr.maxstanzas) or #history); if not history_tag then maxstanzas = 20; end @@ -186,7 +166,7 @@ function room_mt:send_history(to, stanza) local n = 0; local charcount = 0; - + for i=#history,1,-1 do local entry = history[i]; if maxchars then @@ -207,6 +187,8 @@ function room_mt:send_history(to, stanza) self:_route_stanza(msg); end end +end +function room_mt:send_subject(to) if self._data['subject'] then self:_route_stanza(st.message({type='groupchat', from=self._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject'])); end @@ -214,21 +196,28 @@ end function room_mt:get_disco_info(stanza) local count = 0; for _ in pairs(self._occupants) do count = count + 1; end - return st.reply(stanza):query("http://jabber.org/protocol/disco#info") + local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#info") :tag("identity", {category="conference", type="text", name=self:get_name()}):up() :tag("feature", {var="http://jabber.org/protocol/muc"}):up() :tag("feature", {var=self:get_password() and "muc_passwordprotected" or "muc_unsecured"}):up() - :tag("feature", {var=self:is_moderated() and "muc_moderated" or "muc_unmoderated"}):up() - :tag("feature", {var=self:is_members_only() and "muc_membersonly" or "muc_open"}):up() - :tag("feature", {var=self:is_persistent() and "muc_persistent" or "muc_temporary"}):up() - :tag("feature", {var=self:is_hidden() and "muc_hidden" or "muc_public"}):up() + :tag("feature", {var=self:get_moderated() and "muc_moderated" or "muc_unmoderated"}):up() + :tag("feature", {var=self:get_members_only() and "muc_membersonly" or "muc_open"}):up() + :tag("feature", {var=self:get_persistent() and "muc_persistent" or "muc_temporary"}):up() + :tag("feature", {var=self:get_hidden() and "muc_hidden" or "muc_public"}):up() :tag("feature", {var=self._data.whois ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"}):up() - :add_child(dataform.new({ - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" }, - { name = "muc#roominfo_description", label = "Description", value = "" }, - { name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) } - }):form({["muc#roominfo_description"] = self:get_description()}, 'result')) ; + local dataform = dataform.new({ + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" }, + { name = "muc#roominfo_description", label = "Description", value = "" }, + { name = "muc#roominfo_occupants", label = "Number of occupants", value = "" } + }); + local formdata = { + ["muc#roominfo_description"] = self:get_description(), + ["muc#roominfo_occupants"] = tostring(count), + }; + module:fire_event("muc-disco#info", { room = self, reply = reply, form = dataform, formdata = formdata }); + reply:add_child(dataform:form(formdata, 'result')) + return reply; end function room_mt:get_disco_items(stanza) local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); @@ -238,7 +227,6 @@ function room_mt:get_disco_items(stanza) return reply; end function room_mt:set_subject(current_nick, subject) - -- TODO check nick's authority if subject == "" then subject = nil; end self._data['subject'] = subject; self._data['subject_from'] = current_nick; @@ -296,7 +284,7 @@ function room_mt:set_moderated(moderated) if self.save then self:save(true); end end end -function room_mt:is_moderated() +function room_mt:get_moderated() return self._data.moderated; end function room_mt:set_members_only(members_only) @@ -306,7 +294,7 @@ function room_mt:set_members_only(members_only) if self.save then self:save(true); end end end -function room_mt:is_members_only() +function room_mt:get_members_only() return self._data.members_only; end function room_mt:set_persistent(persistent) @@ -316,7 +304,7 @@ function room_mt:set_persistent(persistent) if self.save then self:save(true); end end end -function room_mt:is_persistent() +function room_mt:get_persistent() return self._data.persistent; end function room_mt:set_hidden(hidden) @@ -326,9 +314,15 @@ function room_mt:set_hidden(hidden) if self.save then self:save(true); end end end -function room_mt:is_hidden() +function room_mt:get_hidden() return self._data.hidden; end +function room_mt:get_public() + return not self:get_hidden(); +end +function room_mt:set_public(public) + return self:set_hidden(not public); +end function room_mt:set_changesubject(changesubject) changesubject = changesubject and true or nil; if self._data.changesubject ~= changesubject then @@ -351,12 +345,25 @@ function room_mt:set_historylength(length) end +local valid_whois = { moderators = true, anyone = true }; + +function room_mt:set_whois(whois) + if valid_whois[whois] and self._data.whois ~= whois then + self._data.whois = whois; + if self.save then self:save(true); end + end +end + +function room_mt:get_whois() + return self._data.whois; +end + local function construct_stanza_id(room, stanza) local from_jid, to_nick = stanza.attr.from, stanza.attr.to; local from_nick = room._jid_nick[from_jid]; local occupant = room._occupants[to_nick]; local to_jid = occupant.jid; - + return from_nick, to_jid, base64.encode(to_jid.."\0"..stanza.attr.id.."\0"..md5(from_jid)); end local function deconstruct_stanza_id(room, stanza) @@ -485,6 +492,12 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc log("debug", "%s joining as %s", from, to); if not next(self._affiliations) then -- new room, no owners self._affiliations[jid_bare(from)] = "owner"; + if self.locked and not stanza:get_child("x", "http://jabber.org/protocol/muc") then + self.locked = nil; -- Older groupchat protocol doesn't lock + end + elseif self.locked then -- Deny entry + origin.send(st.error_reply(stanza, "cancel", "item-not-found")); + return; end local affiliation = self:get_affiliation(from); local role = self:get_default_role(affiliation) @@ -506,9 +519,13 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc if self._data.whois == 'anyone' then pr:tag("status", {code='100'}):up(); end + if self.locked then + pr:tag("status", {code='201'}):up(); + end pr.attr.to = from; self:_route_stanza(pr); self:send_history(from, stanza); + self:send_subject(from); elseif not affiliation then -- registration required for entering members-only room local reply = st.error_reply(stanza, "auth", "registration-required"):up(); reply.tags[1].attr.code = "407"; @@ -560,6 +577,7 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc end stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; else -- message + stanza:tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }):up(); stanza.attr.from = current_nick; for jid in pairs(o_data.sessions) do stanza.attr.to = jid; @@ -575,11 +593,11 @@ end function room_mt:send_form(origin, stanza) origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner") - :add_child(self:get_form_layout():form()) + :add_child(self:get_form_layout(stanza.attr.from):form()) ); end -function room_mt:get_form_layout() +function room_mt:get_form_layout(actor) local form = dataform.new({ title = "Configuration for "..self.jid, instructions = "Complete and submit this form to configure the room.", @@ -604,13 +622,13 @@ function room_mt:get_form_layout() name = 'muc#roomconfig_persistentroom', type = 'boolean', label = 'Make Room Persistent?', - value = self:is_persistent() + value = self:get_persistent() }, { name = 'muc#roomconfig_publicroom', type = 'boolean', label = 'Make Room Publicly Searchable?', - value = not self:is_hidden() + value = not self:get_hidden() }, { name = 'muc#roomconfig_changesubject', @@ -637,13 +655,13 @@ function room_mt:get_form_layout() name = 'muc#roomconfig_moderatedroom', type = 'boolean', label = 'Make Room Moderated?', - value = self:is_moderated() + value = self:get_moderated() }, { name = 'muc#roomconfig_membersonly', type = 'boolean', label = 'Make Room Members-Only?', - value = self:is_members_only() + value = self:get_members_only() }, { name = 'muc#roomconfig_historylength', @@ -652,14 +670,9 @@ function room_mt:get_form_layout() value = tostring(self:get_historylength()) } }); - return module:fire_event("muc-config-form", { room = self, form = form }) or form; + return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form; end -local valid_whois = { - moderators = true, - anyone = true, -} - function room_mt:process_form(origin, stanza) local query = stanza.tags[1]; local form; @@ -668,85 +681,52 @@ function room_mt:process_form(origin, stanza) if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form")); return; end - local fields = self:get_form_layout():data(form); - if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration")); return; end - - local dirty = false - - local event = { room = self, fields = fields, changed = dirty }; - module:fire_event("muc-config-submitted", event); - dirty = event.changed or dirty; - - local name = fields['muc#roomconfig_roomname']; - if name ~= self:get_name() then - self:set_name(name); + local fields, errors, present = self:get_form_layout(stanza.attr.from):data(form); + if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then + origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration")); + return; end - local description = fields['muc#roomconfig_roomdesc']; - if description ~= self:get_description() then - self:set_description(description); - end - - local persistent = fields['muc#roomconfig_persistentroom']; - dirty = dirty or (self:is_persistent() ~= persistent) - module:log("debug", "persistent=%s", tostring(persistent)); - - local moderated = fields['muc#roomconfig_moderatedroom']; - dirty = dirty or (self:is_moderated() ~= moderated) - module:log("debug", "moderated=%s", tostring(moderated)); + local changed = {}; - local membersonly = fields['muc#roomconfig_membersonly']; - dirty = dirty or (self:is_members_only() ~= membersonly) - module:log("debug", "membersonly=%s", tostring(membersonly)); - - local public = fields['muc#roomconfig_publicroom']; - dirty = dirty or (self:is_hidden() ~= (not public and true or nil)) - - local changesubject = fields['muc#roomconfig_changesubject']; - dirty = dirty or (self:get_changesubject() ~= (not changesubject and true or nil)) - module:log('debug', 'changesubject=%s', changesubject and "true" or "false") - - local historylength = tonumber(fields['muc#roomconfig_historylength']); - dirty = dirty or (historylength and (self:get_historylength() ~= historylength)); - module:log('debug', 'historylength=%s', historylength) - - - local whois = fields['muc#roomconfig_whois']; - if not valid_whois[whois] then - origin.send(st.error_reply(stanza, 'cancel', 'bad-request', "Invalid value for 'whois'")); - return; + local function handle_option(name, field, allowed) + if not present[field] then return; end + local new = fields[field]; + if allowed and not allowed[new] then return; end + if new == self["get_"..name](self) then return; end + changed[name] = true; + self["set_"..name](self, new); end - local whois_changed = self._data.whois ~= whois - self._data.whois = whois - module:log('debug', 'whois=%s', whois) - local password = fields['muc#roomconfig_roomsecret']; - if self:get_password() ~= password then - self:set_password(password); - end - self:set_moderated(moderated); - self:set_members_only(membersonly); - self:set_persistent(persistent); - self:set_hidden(not public); - self:set_changesubject(changesubject); - self:set_historylength(historylength); + local event = { room = self, fields = fields, changed = changed, stanza = stanza, origin = origin, update_option = handle_option }; + module:fire_event("muc-config-submitted", event); + + handle_option("name", "muc#roomconfig_roomname"); + handle_option("description", "muc#roomconfig_roomdesc"); + handle_option("persistent", "muc#roomconfig_persistentroom"); + handle_option("moderated", "muc#roomconfig_moderatedroom"); + handle_option("members_only", "muc#roomconfig_membersonly"); + handle_option("public", "muc#roomconfig_publicroom"); + handle_option("changesubject", "muc#roomconfig_changesubject"); + handle_option("historylength", "muc#roomconfig_historylength"); + handle_option("whois", "muc#roomconfig_whois", valid_whois); + handle_option("password", "muc#roomconfig_roomsecret"); if self.save then self:save(true); end + if self.locked then + module:fire_event("muc-room-unlocked", { room = self }); + self.locked = nil; + end origin.send(st.reply(stanza)); - if dirty or whois_changed then + if next(changed) then local msg = st.message({type='groupchat', from=self.jid}) - :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}); - - if dirty then - msg.tags[1]:tag('status', {code = '104'}):up(); - end - if whois_changed then - local code = (whois == 'moderators') and "173" or "172"; + :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}) + :tag('status', {code = '104'}):up(); + if changed.whois then + local code = (self:get_whois() == 'moderators') and "173" or "172"; msg.tags[1]:tag('status', {code = code}):up(); end - msg:up(); - self:broadcast_message(msg, false) end end @@ -882,7 +862,7 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end elseif stanza.name == "message" and type == "groupchat" then - local from, to = stanza.attr.from, stanza.attr.to; + local from = stanza.attr.from; local current_nick = self._jid_nick[from]; local occupant = self._occupants[current_nick]; if not occupant then -- not in room @@ -892,11 +872,11 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha else local from = stanza.attr.from; stanza.attr.from = current_nick; - local subject = getText(stanza, {"subject"}); + local subject = stanza:get_child_text("subject"); if subject then if occupant.role == "moderator" or ( self._data.changesubject and occupant.role == "participant" ) then -- and participant - self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza + self:set_subject(current_nick, subject); else stanza.attr.from = from; origin.send(st.error_reply(stanza, "auth", "forbidden")); @@ -944,7 +924,7 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha :tag('body') -- Add a plain message for clients which don't support invites :text(_from..' invited you to the room '.._to..(_reason and (' ('.._reason..')') or "")) :up(); - if self:is_members_only() and not self:get_affiliation(_invitee) then + if self:get_members_only() and not self:get_affiliation(_invitee) then log("debug", "%s invited %s into members only room %s, granting membership", _from, _invitee, _to); self:set_affiliation(_from, _invitee, "member", nil, "Invited by " .. self._jid_nick[_from]) end diff --git a/plugins/sql.lib.lua b/plugins/sql.lib.lua deleted file mode 100644 index 005ee45d..00000000 --- a/plugins/sql.lib.lua +++ /dev/null @@ -1,9 +0,0 @@ -local cache = module:shared("/*/sql.lib/util.sql"); - -if not cache._M then - prosody.unlock_globals(); - cache._M = require "util.sql"; - prosody.lock_globals(); -end - -return cache._M; diff --git a/plugins/storage/sqlbasic.lib.lua b/plugins/storage/sqlbasic.lib.lua deleted file mode 100644 index ab3648f9..00000000 --- a/plugins/storage/sqlbasic.lib.lua +++ /dev/null @@ -1,97 +0,0 @@ - --- Basic SQL driver --- This driver stores data as simple key-values - -local ser = require "util.serialization".serialize; -local envload = require "util.envload".envload; -local deser = function(data) - module:log("debug", "deser: %s", tostring(data)); - if not data then return nil; end - local f = envload("return "..data, nil, {}); - if not f then return nil; end - local s, d = pcall(f); - if not s then return nil; end - return d; -end; - -local driver = {}; -driver.__index = driver; - -driver.item_table = "item"; -driver.list_table = "list"; - -function driver:prepare(sql) - module:log("debug", "query: %s", sql); - local err; - if not self.sqlcache then self.sqlcache = {}; end - local r = self.sqlcache[sql]; - if r then return r; end - r, err = self.connection:prepare(sql); - if not r then error("Unable to prepare SQL statement: "..err); end - self.sqlcache[sql] = r; - return r; -end - -function driver:load(username, host, datastore) - local select = self:prepare("select data from "..self.item_table.." where username=? and host=? and datastore=?"); - select:execute(username, host, datastore); - local row = select:fetch(); - return row and deser(row[1]) or nil; -end - -function driver:store(username, host, datastore, data) - if not data or next(data) == nil then - local delete = self:prepare("delete from "..self.item_table.." where username=? and host=? and datastore=?"); - delete:execute(username, host, datastore); - return true; - else - local d = self:load(username, host, datastore); - if d then -- update - local update = self:prepare("update "..self.item_table.." set data=? where username=? and host=? and datastore=?"); - return update:execute(ser(data), username, host, datastore); - else -- insert - local insert = self:prepare("insert into "..self.item_table.." values (?, ?, ?, ?)"); - return insert:execute(username, host, datastore, ser(data)); - end - end -end - -function driver:list_append(username, host, datastore, data) - if not data then return; end - local insert = self:prepare("insert into "..self.list_table.." values (?, ?, ?, ?)"); - return insert:execute(username, host, datastore, ser(data)); -end - -function driver:list_store(username, host, datastore, data) - -- remove existing data - local delete = self:prepare("delete from "..self.list_table.." where username=? and host=? and datastore=?"); - delete:execute(username, host, datastore); - if data and next(data) ~= nil then - -- add data - for _, d in ipairs(data) do - self:list_append(username, host, datastore, ser(d)); - end - end - return true; -end - -function driver:list_load(username, host, datastore) - local select = self:prepare("select data from "..self.list_table.." where username=? and host=? and datastore=?"); - select:execute(username, host, datastore); - local r = {}; - for row in select:rows() do - table.insert(r, deser(row[1])); - end - return r; -end - -local _M = {}; -function _M.new(dbtype, dbname, ...) - local d = {}; - setmetatable(d, driver); - local dbh = get_database(dbtype, dbname, ...); - --d:set_connection(dbh); - d.connection = dbh; - return d; -end -return _M; diff --git a/plugins/storage/xep227store.lib.lua b/plugins/storage/xep227store.lib.lua deleted file mode 100644 index 5ef8df54..00000000 --- a/plugins/storage/xep227store.lib.lua +++ /dev/null @@ -1,168 +0,0 @@ -
-local st = require "util.stanza";
-
-local function getXml(user, host)
- local jid = user.."@"..host;
- local path = "data/"..jid..".xml";
- local f = io.open(path);
- if not f then return; end
- local s = f:read("*a");
- return parse_xml_real(s);
-end
-local function setXml(user, host, xml)
- local jid = user.."@"..host;
- local path = "data/"..jid..".xml";
- if xml then
- local f = io.open(path, "w");
- if not f then return; end
- local s = tostring(xml);
- f:write(s);
- f:close();
- return true;
- else
- return os.remove(path);
- end
-end
-local function getUserElement(xml)
- if xml and xml.name == "server-data" then
- local host = xml.tags[1];
- if host and host.name == "host" then
- local user = host.tags[1];
- if user and user.name == "user" then
- return user;
- end
- end
- end
-end
-local function createOuterXml(user, host)
- return st.stanza("server-data", {xmlns='http://www.xmpp.org/extensions/xep-0227.html#ns'})
- :tag("host", {jid=host})
- :tag("user", {name = user});
-end
-local function removeFromArray(array, value)
- for i,item in ipairs(array) do
- if item == value then
- table.remove(array, i);
- return;
- end
- end
-end
-local function removeStanzaChild(s, child)
- removeFromArray(s.tags, child);
- removeFromArray(s, child);
-end
-
-local handlers = {};
-
-handlers.accounts = {
- get = function(self, user)
- local user = getUserElement(getXml(user, self.host));
- if user and user.attr.password then
- return { password = user.attr.password };
- end
- end;
- set = function(self, user, data)
- if data and data.password then
- local xml = getXml(user, self.host);
- if not xml then xml = createOuterXml(user, self.host); end
- local usere = getUserElement(xml);
- usere.attr.password = data.password;
- return setXml(user, self.host, xml);
- else
- return setXml(user, self.host, nil);
- end
- end;
-};
-handlers.vcard = {
- get = function(self, user)
- local user = getUserElement(getXml(user, self.host));
- if user then
- local vcard = user:get_child("vCard", 'vcard-temp');
- if vcard then
- return st.preserialize(vcard);
- end
- end
- end;
- set = function(self, user, data)
- local xml = getXml(user, self.host);
- local usere = xml and getUserElement(xml);
- if usere then
- local vcard = usere:get_child("vCard", 'vcard-temp');
- if vcard then
- removeStanzaChild(usere, vcard);
- elseif not data then
- return true;
- end
- if data then
- vcard = st.deserialize(data);
- usere:add_child(vcard);
- end
- return setXml(user, self.host, xml);
- end
- return true;
- end;
-};
-handlers.private = {
- get = function(self, user)
- local user = getUserElement(getXml(user, self.host));
- if user then
- local private = user:get_child("query", "jabber:iq:private");
- if private then
- local r = {};
- for _, tag in ipairs(private.tags) do
- r[tag.name..":"..tag.attr.xmlns] = st.preserialize(tag);
- end
- return r;
- end
- end
- end;
- set = function(self, user, data)
- local xml = getXml(user, self.host);
- local usere = xml and getUserElement(xml);
- if usere then
- local private = usere:get_child("query", 'jabber:iq:private');
- if private then removeStanzaChild(usere, private); end
- if data and next(data) ~= nil then
- private = st.stanza("query", {xmlns='jabber:iq:private'});
- for _,tag in pairs(data) do
- private:add_child(st.deserialize(tag));
- end
- usere:add_child(private);
- end
- return setXml(user, self.host, xml);
- end
- return true;
- end;
-};
-
------------------------------
-local driver = {};
-driver.__index = driver;
-
-function driver:open(host, datastore, typ)
- local cache_key = host.." "..datastore;
- if self.ds_cache[cache_key] then return self.ds_cache[cache_key]; end
- local instance = setmetatable({}, self);
- instance.host = host;
- instance.datastore = datastore;
- local handler = handlers[datastore];
- if not handler then return nil; end
- for key,val in pairs(handler) do
- instance[key] = val;
- end
- if instance.init then instance:init(); end
- self.ds_cache[cache_key] = instance;
- return instance;
-end
-
------------------------------
-local _M = {};
-
-function _M.new()
- local instance = setmetatable({}, driver);
- instance.__index = instance;
- instance.ds_cache = {};
- return instance;
-end
-
-return _M;
@@ -43,6 +43,12 @@ if CFG_DATADIR then end end +if #arg > 0 and arg[1] ~= "--config" then + print("Unknown command-line option: "..tostring(arg[1])); + print("Perhaps you meant to use prosodyctl instead?"); + return 1; +end + -- Global 'prosody' object local prosody = { events = require "util.events".new(); }; _G.prosody = prosody; @@ -121,6 +127,7 @@ end function load_libraries() -- Load socket framework + socket = require "socket"; server = require "net.server" end @@ -151,9 +158,12 @@ function sandbox_require() -- for neat sandboxing of modules local _realG = _G; local _real_require = require; - if not getfenv then + local getfenv = getfenv or function (f) -- FIXME: This is a hack to replace getfenv() in Lua 5.2 - function getfenv(f) return debug.getupvalue(debug.getinfo(f or 1).func, 1); end + local name, env = debug.getupvalue(debug.getinfo(f or 1).func, 1); + if name == "_ENV" then + return env; + end end function require(...) local curr_env = getfenv(2); @@ -262,18 +272,16 @@ function init_global_state() end -- Function to initiate prosody shutdown - function prosody.shutdown(reason) + function prosody.shutdown(reason, code) log("info", "Shutting down: %s", reason or "unknown reason"); prosody.shutdown_reason = reason; - prosody.events.fire_event("server-stopping", {reason = reason}); + prosody.shutdown_code = code; + prosody.events.fire_event("server-stopping", { + reason = reason; + code = code; + }); server.setquitting(true); end - - -- Load SSL settings from config, and create a ctx table - local certmanager = require "core.certmanager"; - local global_ssl_ctx = certmanager.create_context("*", "server"); - prosody.global_ssl_ctx = global_ssl_ctx; - end function read_version() @@ -295,6 +303,7 @@ function load_secondary_libraries() require "util.import" require "util.xmppstream" require "core.stanza_router" + require "core.statsmanager" require "core.hostmanager" require "core.portmanager" require "core.modulemanager" @@ -373,8 +382,10 @@ function loop() prosody.events.fire_event("very-bad-error", {error = err, traceback = traceback}); end + local sleep = require"socket".sleep; + while select(2, xpcall(server.loop, catch_uncaught_error)) ~= "quitting" do - socket.sleep(0.2); + sleep(0.2); end end @@ -411,3 +422,4 @@ cleanup(); prosody.events.fire_event("server-stopped"); log("info", "Shutdown complete"); +os.exit(prosody.shutdown_code) diff --git a/prosody.cfg.lua.dist b/prosody.cfg.lua.dist index 23032932..d2af75a0 100644 --- a/prosody.cfg.lua.dist +++ b/prosody.cfg.lua.dist @@ -4,7 +4,7 @@ -- website at http://prosody.im/doc/configure -- -- Tip: You can check that the syntax of this file is correct --- when you have finished by running: luac -p prosody.cfg.lua +-- when you have finished by running: prosodyctl check config -- If there are any errors, it will let you know what and where -- they are, otherwise it will keep quiet. -- @@ -24,7 +24,7 @@ admins = { } -- Enable use of libevent for better performance under high load -- For more information see: http://prosody.im/doc/libevent ---use_libevent = true; +--use_libevent = true -- 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. @@ -43,7 +43,7 @@ modules_enabled = { "vcard"; -- Allow users to set vCards -- These are commented by default as they have a performance impact - --"privacy"; -- Support privacy lists + --"blocklist"; -- Allow users to block communications with other users --"compression"; -- Stream compression -- Nice to have @@ -63,14 +63,13 @@ modules_enabled = { --"http_files"; -- Serve static files from a directory over HTTP -- Other specific functionality - --"posix"; -- POSIX functionality, sends server to background, enables syslog, etc. --"groups"; -- Shared roster support --"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. -}; +} -- These modules are auto-loaded, but should you want -- to disable them then uncomment them here: @@ -78,11 +77,12 @@ modules_disabled = { -- "offline"; -- Store offline messages -- "c2s"; -- Handle client connections -- "s2s"; -- Handle server-to-server connections -}; + -- "posix"; -- POSIX functionality, sends server to background, enables syslog, etc. +} -- Disable account creation by default, for security -- For more information see http://prosody.im/doc/creating_accounts -allow_registration = false; +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 @@ -94,7 +94,7 @@ ssl = { -- Force clients to use encrypted connections? This option will -- prevent clients from authenticating unless they are using encryption. -c2s_require_encryption = false +c2s_require_encryption = true -- Force certificate authentication for server-to-server connections? -- This provides ideal security, but requires servers you communicate @@ -233,6 +233,7 @@ local function make_host(hostname) type = "local", events = prosody.events, modules = {}, + sessions = {}, users = require "core.usermanager".new_null_provider(hostname) }; end @@ -244,13 +245,14 @@ end local modulemanager = require "core.modulemanager" local prosodyctl = require "util.prosodyctl" -require "socket" +local socket = require "socket" ----------------------- -- FIXME: Duplicate code waiting for util.startup function read_version() -- Try to determine version local version_file = io.open((CFG_SOURCEDIR or ".").."/prosody.version"); + prosody.version = "unknown"; if version_file then prosody.version = version_file:read("*a"):gsub("%s*$", ""); version_file:close(); @@ -258,7 +260,9 @@ function read_version() prosody.version = "hg:"..prosody.version; end else - prosody.version = "unknown"; + local hg = require"util.mercurial"; + local hgid = hg.check_id(CFG_SOURCEDIR or "."); + if hgid then prosody.version = "hg:" .. hgid; end end end @@ -320,7 +324,7 @@ function commands.passwd(arg) show_usage([[passwd JID]], [[Set the password for the specified user account in Prosody]]); return 1; end - local user, host = jid_split(arg[1]) + local user, host = jid_split(arg[1]); if not user and host then show_message [[Failed to understand JID, please supply the JID you want to set the password for]] show_usage [[passwd user@host]] @@ -413,7 +417,11 @@ function commands.start(arg) local ok, ret = prosodyctl.start(); if ok then - if config.get("*", "daemonize") ~= false then + local daemonize = config.get("*", "daemonize"); + if daemonize == nil then + daemonize = prosody.installed; + end + if daemonize then local i=1; while true do local ok, running = prosodyctl.isrunning(); @@ -524,16 +532,32 @@ function commands.about(arg) return 1; end + local pwd = "."; + local lfs = require "lfs"; local array = require "util.array"; local keys = require "util.iterators".keys; + local hg = require"util.mercurial"; + local relpath = config.resolve_relative_path; print("Prosody "..(prosody.version or "(unknown version)")); print(""); print("# Prosody directories"); - print("Data directory: ", CFG_DATADIR or "./"); - print("Plugin directory:", CFG_PLUGINDIR or "./"); - print("Config directory:", CFG_CONFIGDIR or "./"); - print("Source directory:", CFG_SOURCEDIR or "./"); + print("Data directory: "..relpath(pwd, data_path)); + print("Config directory: "..relpath(pwd, CFG_CONFIGDIR or ".")); + print("Source directory: "..relpath(pwd, CFG_SOURCEDIR or ".")); + print("Plugin directories:") + print(" "..(prosody.paths.plugins:gsub("([^;]+);?", function(path) + local opath = path; + path = config.resolve_relative_path(pwd, path); + local hgid, hgrepo = hg.check_id(path); + if not hgid and hgrepo then + return path.." - "..hgrepo .."!\n "; + end + -- 010452cfaf53 is the first commit in the prosody-modules repository + hgrepo = hgrepo == "010452cfaf53" and "prosody-modules"; + return path..(hgid and " - "..(hgrepo or "HG").." rev: "..hgid or "") + .."\n "; + end))); print(""); print("# Lua environment"); print("Lua version: ", _G._VERSION); @@ -555,6 +579,8 @@ function commands.about(arg) print(""); print("# Lua module versions"); local module_versions, longest_name = {}, 8; + local luaevent =dependencies.softreq"luaevent"; + local ssl = dependencies.softreq"ssl"; for name, module in pairs(package.loaded) do if type(module) == "table" and rawget(module, "_VERSION") and name ~= "_G" and not name:match("%.") then @@ -650,40 +676,65 @@ local lfs; local cert_commands = {}; -local function ask_overwrite(filename) - return lfs.attributes(filename) and not show_yesno("Overwrite "..filename .. "?"); +-- If a file already exists, ask if the user wants to use it or replace it +-- Backups the old file if replaced +local function use_existing(filename) + local attrs = lfs.attributes(filename); + if attrs then + if show_yesno(filename .. " exists, do you want to replace it? [y/n]") then + local backup = filename..".bkp~"..os.date("%FT%T", attrs.change); + os.rename(filename, backup); + show_message(filename.." backed up to "..backup); + else + -- Use the existing file + return true; + end + end end function cert_commands.config(arg) if #arg >= 1 and arg[1] ~= "--help" then local conf_filename = (CFG_DATADIR or "./certs") .. "/" .. arg[1] .. ".cnf"; - if ask_overwrite(conf_filename) then + if use_existing(conf_filename) then return nil, conf_filename; end + local distinguished_name; + if arg[#arg]:find("^/") then + distinguished_name = table.remove(arg); + end local conf = openssl.config.new(); conf:from_prosody(hosts, config, arg); - show_message("Please provide details to include in the certificate config file."); - show_message("Leave the field empty to use the default value or '.' to exclude the field.") - for i, k in ipairs(openssl._DN_order) do - local v = conf.distinguished_name[k]; - if v then - local nv; - if k == "commonName" then - v = arg[1] - elseif k == "emailAddress" then - v = "xmpp@" .. arg[1]; - elseif k == "countryName" then - local tld = arg[1]:match"%.([a-z]+)$"; - if tld and #tld == 2 and tld ~= "uk" then - v = tld:upper(); + if distinguished_name then + local dn = {}; + for k, v in distinguished_name:gmatch("/([^=/]+)=([^/]+)") do + table.insert(dn, k); + dn[k] = v; + end + conf.distinguished_name = dn; + else + show_message("Please provide details to include in the certificate config file."); + show_message("Leave the field empty to use the default value or '.' to exclude the field.") + for i, k in ipairs(openssl._DN_order) do + local v = conf.distinguished_name[k]; + if v then + local nv; + if k == "commonName" then + v = arg[1] + elseif k == "emailAddress" then + v = "xmpp@" .. arg[1]; + elseif k == "countryName" then + local tld = arg[1]:match"%.([a-z]+)$"; + if tld and #tld == 2 and tld ~= "uk" then + v = tld:upper(); + end end + nv = show_prompt(("%s (%s):"):format(k, nv or v)); + nv = (not nv or nv == "") and v or nv; + if nv:find"[\192-\252][\128-\191]+" then + conf.req.string_mask = "utf8only" + end + conf.distinguished_name[k] = nv ~= "." and nv or nil; end - nv = show_prompt(("%s (%s):"):format(k, nv or v)); - nv = (not nv or nv == "") and v or nv; - if nv:find"[\192-\252][\128-\191]+" then - conf.req.string_mask = "utf8only" - end - conf.distinguished_name[k] = nv ~= "." and nv or nil; end end local conf_file, err = io.open(conf_filename, "w"); @@ -705,7 +756,7 @@ end function cert_commands.key(arg) if #arg >= 1 and arg[1] ~= "--help" then local key_filename = (CFG_DATADIR or "./certs") .. "/" .. arg[1] .. ".key"; - if ask_overwrite(key_filename) then + if use_existing(key_filename) then return nil, key_filename; end os.remove(key_filename); -- This file, if it exists is unlikely to have write permissions @@ -727,12 +778,12 @@ end function cert_commands.request(arg) if #arg >= 1 and arg[1] ~= "--help" then local req_filename = (CFG_DATADIR or "./certs") .. "/" .. arg[1] .. ".req"; - if ask_overwrite(req_filename) then + if use_existing(req_filename) then return nil, req_filename; end local _, key_filename = cert_commands.key({arg[1]}); local _, conf_filename = cert_commands.config(arg); - if openssl.req{new=true, key=key_filename, utf8=true, config=conf_filename, out=req_filename} then + if openssl.req{new=true, key=key_filename, utf8=true, sha256=true, config=conf_filename, out=req_filename} then show_message("Certificate request written to ".. req_filename); else show_message("There was a problem, see OpenSSL output"); @@ -745,7 +796,7 @@ end function cert_commands.generate(arg) if #arg >= 1 and arg[1] ~= "--help" then local cert_filename = (CFG_DATADIR or "./certs") .. "/" .. arg[1] .. ".crt"; - if ask_overwrite(cert_filename) then + if use_existing(cert_filename) then return nil, cert_filename; end local _, key_filename = cert_commands.key({arg[1]}); @@ -753,8 +804,10 @@ function cert_commands.generate(arg) local ret; if key_filename and conf_filename and cert_filename and openssl.req{new=true, x509=true, nodes=true, key=key_filename, - days=365, sha1=true, utf8=true, config=conf_filename, out=cert_filename} then + days=365, sha256=true, utf8=true, config=conf_filename, out=cert_filename} then show_message("Certificate written to ".. cert_filename); + print(); + show_message(("Example config:\n\nssl = {\n\tcertificate = %q;\n\tkey = %q;\n}"):format(cert_filename, key_filename)); else show_message("There was a problem, see OpenSSL output"); end @@ -783,6 +836,444 @@ function commands.cert(arg) show_usage("cert config|request|generate|key", "Helpers for generating X.509 certificates and keys.") end +function commands.check(arg) + if arg[1] == "--help" then + show_usage([[check]], [[Perform basic checks on your Prosody installation]]); + return 1; + end + local what = table.remove(arg, 1); + local array, set = require "util.array", require "util.set"; + local it = require "util.iterators"; + local ok = true; + local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end + local function enabled_hosts() return it.filter(disabled_hosts, pairs(config.getconfig())); end + if not what or what == "disabled" then + local disabled_hosts = set.new(); + for host, host_options in it.filter("*", pairs(config.getconfig())) do + if host_options.enabled == false then + disabled_hosts:add(host); + end + end + if not disabled_hosts:empty() then + local msg = "Checks will be skipped for these disabled hosts: %s"; + if what then msg = "These hosts are disabled: %s"; end + show_warning(msg, tostring(disabled_hosts)); + if what then return 0; end + print"" + end + end + if not what or what == "config" then + print("Checking config..."); + local deprecated = set.new({ + "bosh_ports", "disallow_s2s", "no_daemonize", "anonymous_login", "require_encryption", + }); + local known_global_options = set.new({ + "pidfile", "log", "plugin_paths", "prosody_user", "prosody_group", "daemonize", + "umask", "prosodyctl_timeout", "use_ipv6", "use_libevent", "network_settings", + "network_backend", "http_default_host", + }); + local config = config.getconfig(); + -- Check that we have any global options (caused by putting a host at the top) + if it.count(it.filter("log", pairs(config["*"]))) == 0 then + ok = false; + print(""); + print(" No global options defined. Perhaps you have put a host definition at the top") + print(" of the config file? They should be at the bottom, see http://prosody.im/doc/configure#overview"); + end + if it.count(enabled_hosts()) == 0 then + ok = false; + print(""); + if it.count(it.filter("*", pairs(config))) == 0 then + print(" No hosts are defined, please add at least one VirtualHost section") + elseif config["*"]["enabled"] == false then + print(" No hosts are enabled. Remove enabled = false from the global section or put enabled = true under at least one VirtualHost section") + else + print(" All hosts are disabled. Remove enabled = false from at least one VirtualHost section") + end + end + if not config["*"].modules_enabled then + print(" No global modules_enabled is set?"); + local suggested_global_modules; + for host, options in enabled_hosts() do + if not options.component_module and options.modules_enabled then + 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 + 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 + print(); + end + -- Check for global options under hosts + local global_options = set.new(it.to_array(it.keys(config["*"]))); + local deprecated_global_options = set.intersection(global_options, deprecated); + if not deprecated_global_options:empty() then + print(""); + print(" You have some deprecated options in the global section:"); + print(" "..tostring(deprecated_global_options)) + ok = false; + end + for host, options in enabled_hosts() do + local host_options = set.new(it.to_array(it.keys(options))); + local misplaced_options = set.intersection(host_options, known_global_options); + for name in pairs(options) do + if name:match("^interfaces?") + or name:match("_ports?$") or name:match("_interfaces?$") + or (name:match("_ssl$") and not name:match("^[cs]2s_ssl$")) then + misplaced_options:add(name); + end + end + if not misplaced_options:empty() then + ok = false; + print(""); + local n = it.count(misplaced_options); + print(" You have "..n.." option"..(n>1 and "s " or " ").."set under "..host.." that should be"); + print(" in the global section of the config file, above any VirtualHost or Component definitions,") + print(" see http://prosody.im/doc/configure#overview for more information.") + print(""); + print(" You need to move the following option"..(n>1 and "s" or "")..": "..table.concat(it.to_array(misplaced_options), ", ")); + end + local subdomain = host:match("^[^.]+"); + if not(host_options:contains("component_module")) and (subdomain == "jabber" or subdomain == "xmpp" + or subdomain == "chat" or subdomain == "im") then + print(""); + print(" Suggestion: If "..host.. " is a new host with no real users yet, consider renaming it now to"); + print(" "..host:gsub("^[^.]+%.", "")..". You can use SRV records to redirect XMPP clients and servers to "..host.."."); + print(" For more information see: http://prosody.im/doc/dns"); + end + end + local all_modules = set.new(config["*"].modules_enabled); + local all_options = set.new(it.to_array(it.keys(config["*"]))); + for host in enabled_hosts() do + all_options:include(set.new(it.to_array(it.keys(config[host])))); + all_modules:include(set.new(config[host].modules_enabled)); + end + for mod in all_modules do + if mod:match("^mod_") then + print(""); + print(" Modules in modules_enabled should not have the 'mod_' prefix included."); + print(" Change '"..mod.."' to '"..mod:match("^mod_(.*)").."'."); + elseif mod:match("^auth_") then + print(""); + print(" Authentication modules should not be added to modules_enabled,"); + print(" but be specified in the 'authentication' option."); + print(" Remove '"..mod.."' from modules_enabled and instead add"); + print(" authentication = '"..mod:match("^auth_(.*)").."'"); + print(" For more information see https://prosody.im/doc/authentication"); + elseif mod:match("^storage_") then + print(""); + print(" storage modules should not be added to modules_enabled,"); + print(" but be specified in the 'storage' option."); + print(" Remove '"..mod.."' from modules_enabled and instead add"); + print(" storage = '"..mod:match("^storage_(.*)").."'"); + print(" For more information see https://prosody.im/doc/storage"); + end + end + local ssl = dependencies.softreq"ssl"; + if not ssl then + if not set.intersection(all_options, set.new({"require_encryption", "c2s_require_encryption", "s2s_require_encryption"})):empty() then + print(""); + print(" You require encryption but LuaSec is not available."); + print(" Connections will fail."); + ok = false; + end + elseif not ssl.loadcertificate then + if all_options:contains("s2s_secure_auth") then + print(""); + print(" You have set s2s_secure_auth but your version of LuaSec does "); + print(" not support certificate validation, so all s2s connections will"); + print(" fail."); + ok = false; + elseif all_options:contains("s2s_secure_domains") then + local secure_domains = set.new(); + for host in enabled_hosts() do + if config[host].s2s_secure_auth == true then + secure_domains:add("*"); + else + secure_domains:include(set.new(config[host].s2s_secure_domains)); + end + end + if not secure_domains:empty() then + print(""); + print(" You have set s2s_secure_domains but your version of LuaSec does "); + print(" not support certificate validation, so s2s connections to/from "); + print(" these domains will fail."); + ok = false; + end + end + end + + print("Done.\n"); + end + if not what or what == "dns" then + local dns = require "net.dns"; + local idna = require "util.encodings".idna; + local ip = require "util.ip"; + local c2s_ports = set.new(config.get("*", "c2s_ports") or {5222}); + local s2s_ports = set.new(config.get("*", "s2s_ports") or {5269}); + + local c2s_srv_required, s2s_srv_required; + if not c2s_ports:contains(5222) then + c2s_srv_required = true; + end + if not s2s_ports:contains(5269) then + s2s_srv_required = true; + end + + local problem_hosts = set.new(); + + local external_addresses, internal_addresses = set.new(), set.new(); + + local fqdn = socket.dns.tohostname(socket.dns.gethostname()); + if fqdn then + local res = dns.lookup(idna.to_ascii(fqdn), "A"); + if res then + for _, record in ipairs(res) do + external_addresses:add(record.a); + end + end + local res = dns.lookup(idna.to_ascii(fqdn), "AAAA"); + if res then + for _, record in ipairs(res) do + external_addresses:add(record.aaaa); + end + end + end + + local local_addresses = require"util.net".local_addresses() or {}; + + for addr in it.values(local_addresses) do + if not ip.new_ip(addr).private then + external_addresses:add(addr); + else + internal_addresses:add(addr); + end + end + + if external_addresses:empty() then + print(""); + print(" Failed to determine the external addresses of this server. Checks may be inaccurate."); + c2s_srv_required, s2s_srv_required = true, true; + end + + local v6_supported = not not socket.tcp6; + + for host, host_options in enabled_hosts() do + local all_targets_ok, some_targets_ok = true, false; + + local is_component = not not host_options.component_module; + print("Checking DNS for "..(is_component and "component" or "host").." "..host.."..."); + local target_hosts = set.new(); + if not is_component then + local res = dns.lookup("_xmpp-client._tcp."..idna.to_ascii(host)..".", "SRV"); + if res then + for _, record in ipairs(res) do + target_hosts:add(record.srv.target); + if not c2s_ports:contains(record.srv.port) then + print(" SRV target "..record.srv.target.." contains unknown client port: "..record.srv.port); + end + end + else + if c2s_srv_required then + print(" No _xmpp-client SRV record found for "..host..", but it looks like you need one."); + all_targst_ok = false; + else + target_hosts:add(host); + end + end + end + local res = dns.lookup("_xmpp-server._tcp."..idna.to_ascii(host)..".", "SRV"); + if res then + for _, record in ipairs(res) do + target_hosts:add(record.srv.target); + if not s2s_ports:contains(record.srv.port) then + print(" SRV target "..record.srv.target.." contains unknown server port: "..record.srv.port); + end + end + else + if s2s_srv_required then + print(" No _xmpp-server SRV record found for "..host..", but it looks like you need one."); + all_targets_ok = false; + else + target_hosts:add(host); + end + end + if target_hosts:empty() then + target_hosts:add(host); + end + + if target_hosts:contains("localhost") then + print(" Target 'localhost' cannot be accessed from other servers"); + target_hosts:remove("localhost"); + end + + local modules = set.new(it.to_array(it.values(host_options.modules_enabled or {}))) + + set.new(it.to_array(it.values(config.get("*", "modules_enabled") or {}))) + + set.new({ config.get(host, "component_module") }); + + if modules:contains("proxy65") then + local proxy65_target = config.get(host, "proxy65_address") or host; + local A, AAAA = dns.lookup(idna.to_ascii(proxy65_target), "A"), dns.lookup(idna.to_ascii(proxy65_target), "AAAA"); + local prob = {}; + if not A then + table.insert(prob, "A"); + end + if v6_supported and not AAAA then + table.insert(prob, "AAAA"); + end + if #prob > 0 then + print(" File transfer proxy "..proxy65_target.." has no "..table.concat(prob, "/").." record. Create one or set 'proxy65_address' to the correct host/IP."); + end + end + + for host in target_hosts do + local host_ok_v4, host_ok_v6; + local res = dns.lookup(idna.to_ascii(host), "A"); + if res then + for _, record in ipairs(res) do + if external_addresses:contains(record.a) then + some_targets_ok = true; + host_ok_v4 = true; + elseif internal_addresses:contains(record.a) then + host_ok_v4 = true; + some_targets_ok = true; + print(" "..host.." A record points to internal address, external connections might fail"); + else + print(" "..host.." A record points to unknown address "..record.a); + all_targets_ok = false; + end + end + end + local res = dns.lookup(idna.to_ascii(host), "AAAA"); + if res then + for _, record in ipairs(res) do + if external_addresses:contains(record.aaaa) then + some_targets_ok = true; + host_ok_v6 = true; + elseif internal_addresses:contains(record.aaaa) then + host_ok_v6 = true; + some_targets_ok = true; + print(" "..host.." AAAA record points to internal address, external connections might fail"); + else + print(" "..host.." AAAA record points to unknown address "..record.aaaa); + all_targets_ok = false; + end + end + end + + local bad_protos = {} + if not host_ok_v4 then + table.insert(bad_protos, "IPv4"); + end + if not host_ok_v6 then + table.insert(bad_protos, "IPv6"); + end + if #bad_protos > 0 then + print(" Host "..host.." does not seem to resolve to this server ("..table.concat(bad_protos, "/")..")"); + end + if host_ok_v6 and not v6_supported then + print(" Host "..host.." has AAAA records, but your version of LuaSocket does not support IPv6."); + print(" Please see http://prosody.im/doc/ipv6 for more information."); + end + end + if not all_targets_ok then + print(" "..(some_targets_ok and "Only some" or "No").." targets for "..host.." appear to resolve to this server."); + if is_component then + print(" DNS records are necessary if you want users on other servers to access this component."); + end + problem_hosts:add(host); + end + print(""); + end + if not problem_hosts:empty() then + print(""); + print("For more information about DNS configuration please see http://prosody.im/doc/dns"); + print(""); + ok = false; + end + end + if not what or what == "certs" then + local cert_ok; + print"Checking certificates..." + local x509_verify_identity = require"util.x509".verify_identity; + local ssl = dependencies.softreq"ssl"; + -- local datetime_parse = require"util.datetime".parse_x509; + local load_cert = ssl and ssl.loadcertificate; + -- or ssl.cert_from_pem + if not ssl then + print("LuaSec not available, can't perform certificate checks") + if what == "certs" then cert_ok = false end + elseif not load_cert then + print("This version of LuaSec (" .. ssl._VERSION .. ") does not support certificate checking"); + cert_ok = false + else + for host in enabled_hosts() do + print("Checking certificate for "..host); + -- First, let's find out what certificate this host uses. + local ssl_config = config.rawget(host, "ssl"); + if not ssl_config then + local base_host = host:match("%.(.*)"); + ssl_config = config.get(base_host, "ssl"); + end + if not ssl_config then + print(" No 'ssl' option defined for "..host) + cert_ok = false + elseif not ssl_config.certificate then + print(" No 'certificate' set in ssl option for "..host) + cert_ok = false + elseif not ssl_config.key then + print(" No 'key' set in ssl option for "..host) + cert_ok = false + else + local key, err = io.open(ssl_config.key); -- Permissions check only + if not key then + print(" Could not open "..ssl_config.key..": "..err); + cert_ok = false + else + key:close(); + end + local cert_fh, err = io.open(ssl_config.certificate); -- Load the file. + if not cert_fh then + print(" Could not open "..ssl_config.certificate..": "..err); + cert_ok = false + else + print(" Certificate: "..ssl_config.certificate) + local cert = load_cert(cert_fh:read"*a"); cert_fh = cert_fh:close(); + if not cert:validat(os.time()) then + print(" Certificate has expired.") + cert_ok = false + end + if config.get(host, "component_module") == nil + and not x509_verify_identity(host, "_xmpp-client", cert) then + print(" Not vaild for client connections to "..host..".") + cert_ok = false + end + if (not (config.get(host, "anonymous_login") + or config.get(host, "authentication") == "anonymous")) + and not x509_verify_identity(host, "_xmpp-server", cert) then + print(" Not vaild for server-to-server connections to "..host..".") + cert_ok = false + end + end + end + end + if cert_ok == false then + print("") + print("For more information about certificates please see http://prosody.im/doc/certificates"); + ok = false + end + end + print("") + end + if not ok then + print("Problems found, see above."); + else + print("All checks passed, congratulations!"); + end + return ok and 0 or 2; +end + --------------------- if command and command:match("^mod_") then -- Is a command in a module diff --git a/tests/modulemanager_option_conversion.lua b/tests/modulemanager_option_conversion.lua index 7dceeaed..100dbe83 100644 --- a/tests/modulemanager_option_conversion.lua +++ b/tests/modulemanager_option_conversion.lua @@ -18,7 +18,7 @@ function test_value(value, returns) assert(module:get_option_number("opt") == returns.number, "number doesn't match"); assert(module:get_option_string("opt") == returns.string, "string doesn't match"); assert(module:get_option_boolean("opt") == returns.boolean, "boolean doesn't match"); - + if type(returns.array) == "table" then local target_array, returned_array = returns.array, module:get_option_array("opt"); assert(#target_array == #returned_array, "array length doesn't match"); @@ -28,7 +28,7 @@ function test_value(value, returns) else assert(module:get_option_array("opt") == returns.array, "array is returned (not nil)"); end - + if type(returns.set) == "table" then local target_items, returned_items = set.new(returns.set), module:get_option_set("opt"); assert(target_items == returned_items, "set doesn't match"); diff --git a/tests/run_tests.sh b/tests/run_tests.sh index d93cd39b..bfb13d00 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -1,3 +1,3 @@ #!/bin/sh rm reports/*.report -lua test.lua $* +exec lua test.lua $* diff --git a/tests/test.lua b/tests/test.lua index bb11ab26..0fcc4907 100644 --- a/tests/test.lua +++ b/tests/test.lua @@ -1,26 +1,30 @@ -- 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 tests_passed = true; function run_all_tests() package.loaded["net.connlisteners"] = { get = function () return {} end }; dotest "util.jid" dotest "util.multitable" - dotest "util.rfc3484" - dotest "net.http" - dotest "core.modulemanager" + dotest "util.rfc6724" + dotest "util.http" dotest "core.stanza_router" dotest "core.s2smanager" dotest "core.configmanager" + dotest "util.ip" dotest "util.stanza" dotest "util.sasl.scram" - + dotest "util.cache" + dotest "util.throttle" + dotest "util.uuid" + dotest "util.random" + dosingletest("test_sasl.lua", "latin1toutf8"); dosingletest("test_utf8.lua", "valid"); end @@ -39,6 +43,8 @@ local _realG = _G; require "util.import" +local envloadfile = require "util.envload".envloadfile; + local env_mt = { __index = function (t,k) return rawget(_realG, k) or print("WARNING: Attempt to access nil global '"..tostring(k).."'"); end }; function testlib_new_env(t) return setmetatable(t or {}, env_mt); @@ -76,29 +82,29 @@ function dosingletest(testname, fname) local tests = setmetatable({}, { __index = _realG }); tests.__unit = testname; tests.__test = fname; - local chunk, err = loadfile(testname); + local chunk, err = envloadfile(testname, tests); if not chunk then print("WARNING: ", "Failed to load tests for "..testname, err); return; end - setfenv(chunk, tests); local success, err = pcall(chunk); if not success then print("WARNING: ", "Failed to initialise tests for "..testname, err); return; end - + if type(tests[fname]) ~= "function" then error(testname.." has no test '"..fname.."'", 0); end - - + + local line_hook, line_info = new_line_coverage_monitor(testname); debug.sethook(line_hook, "l") local success, ret = pcall(tests[fname]); debug.sethook(); if not success then + tests_passed = false; print("TEST FAILED! Unit: ["..testname.."] Function: ["..fname.."]"); print(" Location: "..ret:gsub(":%s*\n", "\n")); line_info(fname, false, report_file); @@ -115,13 +121,12 @@ function dotest(unitname) _fakeG._G = _fakeG; local tests = setmetatable({}, { __index = _fakeG }); tests.__unit = unitname; - local chunk, err = loadfile("test_"..unitname:gsub("%.", "_")..".lua"); + local chunk, err = envloadfile("test_"..unitname:gsub("%.", "_")..".lua", tests); if not chunk then print("WARNING: ", "Failed to load tests for "..unitname, err); return; end - setfenv(chunk, tests); local success, err = pcall(chunk); if not success then print("WARNING: ", "Failed to initialise tests for "..unitname, err); @@ -130,25 +135,30 @@ function dotest(unitname) if tests.env then setmetatable(tests.env, { __index = _realG }); end local unit = setmetatable({}, { __index = setmetatable({ _G = tests.env or _fakeG }, { __index = tests.env or _fakeG }) }); local fn = "../"..unitname:gsub("%.", "/")..".lua"; - local chunk, err = loadfile(fn); + local chunk, err = envloadfile(fn, unit); if not chunk then print("WARNING: ", "Failed to load module: "..unitname, err); return; end - + local oldmodule, old_M = _fakeG.module, _fakeG._M; _fakeG.module = function () setmetatable(unit, nil); unit._M = unit; end - setfenv(chunk, unit); - local success, err = pcall(chunk); + local success, ret = pcall(chunk); _fakeG.module, _fakeG._M = oldmodule, old_M; if not success then - print("WARNING: ", "Failed to initialise module: "..unitname, err); + print("WARNING: ", "Failed to initialise module: "..unitname, ret); return; end - + + if type(ret) == "table" then + for k,v in pairs(ret) do + unit[k] = v; + end + end + for name, f in pairs(unit) do local test = rawget(tests, name); if type(f) ~= "function" then @@ -168,6 +178,7 @@ function dotest(unitname) local success, ret = pcall(test, f, unit); debug.sethook(); if not success then + tests_passed = false; print("TEST FAILED! Unit: ["..unitname.."] Function: ["..name.."]"); print(" Location: "..ret:gsub(":%s*\n", "\n")); line_info(name, false, report_file); @@ -187,6 +198,7 @@ function runtest(f, msg) if success and verbosity >= 2 then print("SUBTEST PASSED: "..(msg or "(no description)")); elseif (not success) and verbosity >= 0 then + tests_passed = false; print("SUBTEST FAILED: "..(msg or "(no description)")); error(ret, 0); end @@ -195,11 +207,11 @@ end function new_line_coverage_monitor(file) local lines_hit, funcs_hit = {}, {}; local total_lines, covered_lines = 0, 0; - + for line in io.lines(file) do total_lines = total_lines + 1; end - + return function (event, line) -- Line hook if not lines_hit[line] then local info = debug.getinfo(2, "fSL") @@ -234,3 +246,5 @@ function new_line_coverage_monitor(file) end run_all_tests() + +os.exit(tests_passed and 0 or 1); diff --git a/tests/test_core_configmanager.lua b/tests/test_core_configmanager.lua index 132dfc74..5bd469c6 100644 --- a/tests/test_core_configmanager.lua +++ b/tests/test_core_configmanager.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -9,27 +9,23 @@ function get(get, config) - config.set("example.com", "test", "testkey", 123); - assert_equal(get("example.com", "test", "testkey"), 123, "Retrieving a set key"); - - config.set("*", "test", "testkey1", 321); - assert_equal(get("*", "test", "testkey1"), 321, "Retrieving a set global key"); - assert_equal(get("example.com", "test", "testkey1"), 321, "Retrieving a set key of undefined host, of which only a globally set one exists"); - - config.set("example.com", "test", ""); -- Creates example.com host in config - assert_equal(get("example.com", "test", "testkey1"), 321, "Retrieving a set key, of which only a globally set one exists"); - + config.set("example.com", "testkey", 123); + assert_equal(get("example.com", "testkey"), 123, "Retrieving a set key"); + + config.set("*", "testkey1", 321); + assert_equal(get("*", "testkey1"), 321, "Retrieving a set global key"); + assert_equal(get("example.com", "testkey1"), 321, "Retrieving a set key of undefined host, of which only a globally set one exists"); + + config.set("example.com", ""); -- Creates example.com host in config + assert_equal(get("example.com", "testkey1"), 321, "Retrieving a set key, of which only a globally set one exists"); + assert_equal(get(), nil, "No parameters to get()"); assert_equal(get("undefined host"), nil, "Getting for undefined host"); - assert_equal(get("undefined host", "undefined section"), nil, "Getting for undefined host & section"); - assert_equal(get("undefined host", "undefined section", "undefined key"), nil, "Getting for undefined host & section & key"); - - assert_equal(get("example.com", "undefined section", "testkey"), nil, "Defined host, undefined section"); + assert_equal(get("undefined host", "undefined key"), nil, "Getting for undefined host & key"); end function set(set, u) - assert_equal(set("*"), false, "Set with no section/key"); - assert_equal(set("*", "set_test"), false, "Set with no key"); + assert_equal(set("*"), false, "Set with no key"); assert_equal(set("*", "set_test", "testkey"), true, "Setting a nil global value"); assert_equal(set("*", "set_test", "testkey", 123), true, "Setting a global value"); diff --git a/tests/test_core_modulemanager.lua b/tests/test_core_modulemanager.lua deleted file mode 100644 index 9498875a..00000000 --- a/tests/test_core_modulemanager.lua +++ /dev/null @@ -1,48 +0,0 @@ --- 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 config = require "core.configmanager"; -local helpers = require "util.helpers"; -local set = require "util.set"; - -function load_modules_for_host(load_modules_for_host, mm) - local test_num = 0; - local function test_load(global_modules_enabled, global_modules_disabled, host_modules_enabled, host_modules_disabled, expected_modules) - test_num = test_num + 1; - -- Prepare - hosts = { ["example.com"] = {} }; - config.set("*", "core", "modules_enabled", global_modules_enabled); - config.set("*", "core", "modules_disabled", global_modules_disabled); - config.set("example.com", "core", "modules_enabled", host_modules_enabled); - config.set("example.com", "core", "modules_disabled", host_modules_disabled); - - expected_modules = set.new(expected_modules); - expected_modules:add_list(helpers.get_upvalue(load_modules_for_host, "autoload_modules")); - - local loaded_modules = set.new(); - function mm.load(host, module) - assert_equal(host, "example.com", test_num..": Host isn't example.com but "..tostring(host)); - assert_equal(expected_modules:contains(module), true, test_num..": Loading unexpected module '"..tostring(module).."'"); - loaded_modules:add(module); - end - load_modules_for_host("example.com"); - assert_equal((expected_modules - loaded_modules):empty(), true, test_num..": Not all modules loaded: "..tostring(expected_modules - loaded_modules)); - end - - test_load({ "one", "two", "three" }, nil, nil, nil, { "one", "two", "three" }); - test_load({ "one", "two", "three" }, {}, nil, nil, { "one", "two", "three" }); - test_load({ "one", "two", "three" }, { "two" }, nil, nil, { "one", "three" }); - test_load({ "one", "two", "three" }, { "three" }, nil, nil, { "one", "two" }); - test_load({ "one", "two", "three" }, nil, nil, { "three" }, { "one", "two" }); - test_load({ "one", "two", "three" }, nil, { "three" }, { "three" }, { "one", "two", "three" }); - - test_load({ "one", "two" }, nil, { "three" }, nil, { "one", "two", "three" }); - test_load({ "one", "two", "three" }, nil, { "three" }, nil, { "one", "two", "three" }); - test_load({ "one", "two", "three" }, { "three" }, { "three" }, nil, { "one", "two", "three" }); - test_load({ "one", "two" }, { "three" }, { "three" }, nil, { "one", "two", "three" }); -end diff --git a/tests/test_core_s2smanager.lua b/tests/test_core_s2smanager.lua index b49c7da6..d2dbf830 100644 --- a/tests/test_core_s2smanager.lua +++ b/tests/test_core_s2smanager.lua @@ -1,11 +1,14 @@ -- 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. -- +env = { + prosody = { events = require "util.events".new() }; +}; function compare_srv_priorities(csp) local r1 = { priority = 10, weight = 0 } @@ -13,7 +16,7 @@ function compare_srv_priorities(csp) local r3 = { priority = 1000, weight = 2 } local r4 = { priority = 1000, weight = 2 } local r5 = { priority = 1000, weight = 5 } - + assert_equal(csp(r1, r1), false); assert_equal(csp(r1, r2), true); assert_equal(csp(r1, r3), true); diff --git a/tests/test_core_stanza_router.lua b/tests/test_core_stanza_router.lua index 0a93694f..ca6b78fc 100644 --- a/tests/test_core_stanza_router.lua +++ b/tests/test_core_stanza_router.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -14,7 +14,7 @@ function core_process_stanza(core_process_stanza, u) local s2sin_session = { from_host = "remotehost", to_host = "localhost", type = "s2sin", hosts = { ["remotehost"] = { authed = true } } } local local_host_session = { host = "localhost", type = "local", s2sout = { ["remotehost"] = s2sout_session } } local local_user_session = { username = "user", host = "localhost", resource = "resource", full_jid = "user@localhost/resource", type = "c2s" } - + _G.prosody.hosts["localhost"] = local_host_session; _G.prosody.full_sessions["user@localhost/resource"] = local_user_session; _G.prosody.bare_sessions["user@localhost"] = { sessions = { resource = local_user_session } }; @@ -23,15 +23,15 @@ function core_process_stanza(core_process_stanza, u) local function test_message_full_jid() local env = testlib_new_env(); local msg = stanza.stanza("message", { to = "user@localhost/resource", type = "chat" }):tag("body"):text("Hello world"); - + local target_routed; - + function env.core_post_stanza(p_origin, p_stanza) assert_equal(p_origin, local_user_session, "origin of routed stanza is not correct"); assert_equal(p_stanza, msg, "routed stanza is not correct one: "..p_stanza:pretty_print()); target_routed = true; end - + env.hosts = hosts; env.prosody = { hosts = hosts }; setfenv(core_process_stanza, env); @@ -42,9 +42,9 @@ function core_process_stanza(core_process_stanza, u) local function test_message_bare_jid() local env = testlib_new_env(); local msg = stanza.stanza("message", { to = "user@localhost", type = "chat" }):tag("body"):text("Hello world"); - + local target_routed; - + function env.core_post_stanza(p_origin, p_stanza) assert_equal(p_origin, local_user_session, "origin of routed stanza is not correct"); assert_equal(p_stanza, msg, "routed stanza is not correct one: "..p_stanza:pretty_print()); @@ -60,9 +60,9 @@ function core_process_stanza(core_process_stanza, u) local function test_message_no_to() local env = testlib_new_env(); local msg = stanza.stanza("message", { type = "chat" }):tag("body"):text("Hello world"); - + local target_handled; - + function env.core_post_stanza(p_origin, p_stanza) assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct"); assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print()); @@ -78,9 +78,9 @@ function core_process_stanza(core_process_stanza, u) local function test_message_to_remote_bare() local env = testlib_new_env(); local msg = stanza.stanza("message", { to = "user@remotehost", type = "chat" }):tag("body"):text("Hello world"); - + local target_routed; - + function env.core_route_stanza(p_origin, p_stanza) assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct"); assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print()); @@ -88,7 +88,7 @@ function core_process_stanza(core_process_stanza, u) end function env.core_post_stanza(...) env.core_route_stanza(...); end - + env.hosts = hosts; setfenv(core_process_stanza, env); assert_equal(core_process_stanza(local_user_session, msg), nil, "core_process_stanza returned incorrect value"); @@ -98,9 +98,9 @@ function core_process_stanza(core_process_stanza, u) local function test_message_to_remote_server() local env = testlib_new_env(); local msg = stanza.stanza("message", { to = "remotehost", type = "chat" }):tag("body"):text("Hello world"); - + local target_routed; - + function env.core_route_stanza(p_origin, p_stanza) assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct"); assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print()); @@ -123,9 +123,9 @@ function core_process_stanza(core_process_stanza, u) local function test_iq_to_remote_server() local env = testlib_new_env(); local msg = stanza.stanza("iq", { to = "remotehost", type = "get", id = "id" }):tag("body"):text("Hello world"); - + local target_routed; - + function env.core_route_stanza(p_origin, p_stanza) assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct"); assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print()); @@ -145,9 +145,9 @@ function core_process_stanza(core_process_stanza, u) local function test_iq_error_to_local_user() local env = testlib_new_env(); local msg = stanza.stanza("iq", { to = "user@localhost/resource", from = "user@remotehost", type = "error", id = "id" }):tag("error", { type = 'cancel' }):tag("item-not-found", { xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' }); - + local target_routed; - + function env.core_route_stanza(p_origin, p_stanza) assert_equal(p_origin, s2sin_session, "origin of handled stanza is not correct"); assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print()); @@ -167,9 +167,9 @@ function core_process_stanza(core_process_stanza, u) local function test_iq_to_local_bare() local env = testlib_new_env(); local msg = stanza.stanza("iq", { to = "user@localhost", from = "user@localhost", type = "get", id = "id" }):tag("ping", { xmlns = "urn:xmpp:ping:0" }); - + local target_handled; - + function env.core_post_stanza(p_origin, p_stanza) assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct"); assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print()); @@ -209,11 +209,11 @@ function core_route_stanza(core_route_stanza) local msg2 = stanza.stanza("iq", { to = "user@localhost/foo", from = "user@localhost", type = "error" }):tag("ping", { xmlns = "urn:xmpp:ping:0" }); --package.loaded["core.usermanager"] = { user_exists = function (user, host) print("RAR!") return true or user == "user" and host == "localhost" and true; end }; local target_handled, target_replied; - + function env.core_post_stanza(p_origin, p_stanza) target_handled = true; end - + function local_user_session.send(data) --print("Replying with: ", tostring(data)); --print(debug.traceback()) diff --git a/tests/test_sasl.lua b/tests/test_sasl.lua index 271fa69a..dd63c5a0 100644 --- a/tests/test_sasl.lua +++ b/tests/test_sasl.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -30,7 +30,7 @@ function latin1toutf8() local function assert_utf8(latin, utf8) assert_equal(_latin1toutf8(latin), utf8, "Incorrect UTF8 from Latin1: "..tostring(latin)); end - + assert_utf8("", "") assert_utf8("test", "test") assert_utf8(nil, nil) diff --git a/tests/test_util_cache.lua b/tests/test_util_cache.lua new file mode 100644 index 00000000..72cb5a85 --- /dev/null +++ b/tests/test_util_cache.lua @@ -0,0 +1,229 @@ + +function new(new) + local c = new(5); + + assert_equal(c:count(), 0); + + c:set("one", 1) + assert_equal(c:count(), 1); + c:set("two", 2) + c:set("three", 3) + c:set("four", 4) + c:set("five", 5); + assert_equal(c:count(), 5); + + c:set("foo", nil); + assert_equal(c:count(), 5); + + assert_equal(c:get("one"), 1); + assert_equal(c:get("two"), 2); + assert_equal(c:get("three"), 3); + assert_equal(c:get("four"), 4); + assert_equal(c:get("five"), 5); + + assert_equal(c:get("foo"), nil); + assert_equal(c:get("bar"), nil); + + c:set("six", 6); + assert_equal(c:count(), 5); + + assert_equal(c:get("one"), nil); + assert_equal(c:get("two"), 2); + assert_equal(c:get("three"), 3); + assert_equal(c:get("four"), 4); + assert_equal(c:get("five"), 5); + assert_equal(c:get("six"), 6); + + c:set("three", nil); + assert_equal(c:count(), 4); + + assert_equal(c:get("one"), nil); + assert_equal(c:get("two"), 2); + assert_equal(c:get("three"), nil); + assert_equal(c:get("four"), 4); + assert_equal(c:get("five"), 5); + assert_equal(c:get("six"), 6); + + c:set("seven", 7); + assert_equal(c:count(), 5); + + assert_equal(c:get("one"), nil); + assert_equal(c:get("two"), 2); + assert_equal(c:get("three"), nil); + assert_equal(c:get("four"), 4); + assert_equal(c:get("five"), 5); + assert_equal(c:get("six"), 6); + assert_equal(c:get("seven"), 7); + + c:set("eight", 8); + assert_equal(c:count(), 5); + + assert_equal(c:get("one"), nil); + assert_equal(c:get("two"), nil); + assert_equal(c:get("three"), nil); + assert_equal(c:get("four"), 4); + assert_equal(c:get("five"), 5); + assert_equal(c:get("six"), 6); + assert_equal(c:get("seven"), 7); + assert_equal(c:get("eight"), 8); + + c:set("four", 4); + assert_equal(c:count(), 5); + + assert_equal(c:get("one"), nil); + assert_equal(c:get("two"), nil); + assert_equal(c:get("three"), nil); + assert_equal(c:get("four"), 4); + assert_equal(c:get("five"), 5); + assert_equal(c:get("six"), 6); + assert_equal(c:get("seven"), 7); + assert_equal(c:get("eight"), 8); + + c:set("nine", 9); + assert_equal(c:count(), 5); + + assert_equal(c:get("one"), nil); + assert_equal(c:get("two"), nil); + assert_equal(c:get("three"), nil); + assert_equal(c:get("four"), 4); + assert_equal(c:get("five"), nil); + assert_equal(c:get("six"), 6); + assert_equal(c:get("seven"), 7); + assert_equal(c:get("eight"), 8); + assert_equal(c:get("nine"), 9); + + local keys = { "nine", "four", "eight", "seven", "six" }; + local values = { 9, 4, 8, 7, 6 }; + local i = 0; + for k, v in c:items() do + i = i + 1; + assert_equal(k, keys[i]); + assert_equal(v, values[i]); + end + assert_equal(i, 5); + + c:set("four", "2+2"); + assert_equal(c:count(), 5); + + assert_equal(c:get("one"), nil); + assert_equal(c:get("two"), nil); + assert_equal(c:get("three"), nil); + assert_equal(c:get("four"), "2+2"); + assert_equal(c:get("five"), nil); + assert_equal(c:get("six"), 6); + assert_equal(c:get("seven"), 7); + assert_equal(c:get("eight"), 8); + assert_equal(c:get("nine"), 9); + + local keys = { "four", "nine", "eight", "seven", "six" }; + local values = { "2+2", 9, 8, 7, 6 }; + local i = 0; + for k, v in c:items() do + i = i + 1; + assert_equal(k, keys[i]); + assert_equal(v, values[i]); + end + assert_equal(i, 5); + + c:set("foo", nil); + assert_equal(c:count(), 5); + + assert_equal(c:get("one"), nil); + assert_equal(c:get("two"), nil); + assert_equal(c:get("three"), nil); + assert_equal(c:get("four"), "2+2"); + assert_equal(c:get("five"), nil); + assert_equal(c:get("six"), 6); + assert_equal(c:get("seven"), 7); + assert_equal(c:get("eight"), 8); + assert_equal(c:get("nine"), 9); + + local keys = { "four", "nine", "eight", "seven", "six" }; + local values = { "2+2", 9, 8, 7, 6 }; + local i = 0; + for k, v in c:items() do + i = i + 1; + assert_equal(k, keys[i]); + assert_equal(v, values[i]); + end + assert_equal(i, 5); + + c:set("four", nil); + + assert_equal(c:get("one"), nil); + assert_equal(c:get("two"), nil); + assert_equal(c:get("three"), nil); + assert_equal(c:get("four"), nil); + assert_equal(c:get("five"), nil); + assert_equal(c:get("six"), 6); + assert_equal(c:get("seven"), 7); + assert_equal(c:get("eight"), 8); + assert_equal(c:get("nine"), 9); + + local keys = { "nine", "eight", "seven", "six" }; + local values = { 9, 8, 7, 6 }; + local i = 0; + for k, v in c:items() do + i = i + 1; + assert_equal(k, keys[i]); + assert_equal(v, values[i]); + end + assert_equal(i, 4); + + local evicted_key, evicted_value; + local c = new(3, function (_key, _value) + evicted_key, evicted_value = _key, _value; + end); + local function set(k, v, should_evict_key, should_evict_value) + evicted_key, evicted_value = nil, nil; + c:set(k, v); + assert_equal(evicted_key, should_evict_key); + assert_equal(evicted_value, should_evict_value); + end + set("a", 1) + set("a", 1) + set("a", 1) + set("a", 1) + set("a", 1) + + set("b", 2) + set("c", 3) + set("b", 2) + set("d", 4, "a", 1) + set("e", 5, "c", 3) + + + local evicted_key, evicted_value; + local c3 = new(1, function (_key, _value, c3) + evicted_key, evicted_value = _key, _value; + if _key == "a" then + -- Put it back in... + -- Check that the newest key/value was set before on_evict was called + assert_equal(c3:get("b"), 2); + -- Sanity check for what we're evicting + assert_equal(_key, "a"); + assert_equal(_value, 1); + -- Re-insert the evicted key (causes this evict function to run again with "b",2) + c3:set(_key, _value) + assert_equal(c3:get(_key), _value) + end + end); + local function set(k, v, should_evict_key, should_evict_value) + evicted_key, evicted_value = nil, nil; + c3:set(k, v); + assert_equal(evicted_key, should_evict_key); + assert_equal(evicted_value, should_evict_value); + end + set("a", 1) + set("a", 1) + set("a", 1) + set("a", 1) + set("a", 1) + + -- The evict handler re-inserts "a"->1, so "b" gets evicted: + set("b", 2, "b", 2) + -- Check the final state is what we expect + assert_equal(c3:get("a"), 1); + assert_equal(c3:get("b"), nil); + assert_equal(c3:count(), 1); +end diff --git a/tests/test_net_http.lua b/tests/test_util_http.lua index e68f96e9..a195df6b 100644 --- a/tests/test_net_http.lua +++ b/tests/test_util_http.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/tests/test_util_ip.lua b/tests/test_util_ip.lua new file mode 100644 index 00000000..0ded1123 --- /dev/null +++ b/tests/test_util_ip.lua @@ -0,0 +1,89 @@ + +function match(match, _M) + local _ = _M.new_ip; + local ip = _"10.20.30.40"; + assert_equal(match(ip, _"10.0.0.0", 8), true); + assert_equal(match(ip, _"10.0.0.0", 16), false); + assert_equal(match(ip, _"10.0.0.0", 24), false); + assert_equal(match(ip, _"10.0.0.0", 32), false); + + assert_equal(match(ip, _"10.20.0.0", 8), true); + assert_equal(match(ip, _"10.20.0.0", 16), true); + assert_equal(match(ip, _"10.20.0.0", 24), false); + assert_equal(match(ip, _"10.20.0.0", 32), false); + + assert_equal(match(ip, _"0.0.0.0", 32), false); + assert_equal(match(ip, _"0.0.0.0", 0), true); + assert_equal(match(ip, _"0.0.0.0"), false); + + assert_equal(match(ip, _"10.0.0.0", 255), false, "excessive number of bits"); + assert_equal(match(ip, _"10.0.0.0", -8), true, "negative number of bits"); + assert_equal(match(ip, _"10.0.0.0", -32), true, "negative number of bits"); + assert_equal(match(ip, _"10.0.0.0", 0), true, "zero bits"); + assert_equal(match(ip, _"10.0.0.0"), false, "no specified number of bits (differing ip)"); + assert_equal(match(ip, _"10.20.30.40"), true, "no specified number of bits (same ip)"); + + assert_equal(match(_"127.0.0.1", _"127.0.0.1"), true, "simple ip"); + + assert_equal(match(_"8.8.8.8", _"8.8.0.0", 16), true); + assert_equal(match(_"8.8.4.4", _"8.8.0.0", 16), true); +end + +function parse_cidr(parse_cidr, _M) + local new_ip = _M.new_ip; + + assert_equal(new_ip"0.0.0.0", new_ip"0.0.0.0") + + local function assert_cidr(cidr, ip, bits) + local parsed_ip, parsed_bits = parse_cidr(cidr); + assert_equal(new_ip(ip), parsed_ip, cidr.." parsed ip is "..ip); + assert_equal(bits, parsed_bits, cidr.." parsed bits is "..tostring(bits)); + end + assert_cidr("0.0.0.0", "0.0.0.0", nil); + assert_cidr("127.0.0.1", "127.0.0.1", nil); + assert_cidr("127.0.0.1/0", "127.0.0.1", 0); + assert_cidr("127.0.0.1/8", "127.0.0.1", 8); + assert_cidr("127.0.0.1/32", "127.0.0.1", 32); + assert_cidr("127.0.0.1/256", "127.0.0.1", 256); + assert_cidr("::/48", "::", 48); +end + +function new_ip(new_ip) + local v4, v6 = "IPv4", "IPv6"; + local function assert_proto(s, proto) + local ip = new_ip(s); + if proto then + assert_equal(ip and ip.proto, proto, "protocol is correct for "..("%q"):format(s)); + else + assert_equal(ip, nil, "address is invalid"); + end + end + assert_proto("127.0.0.1", v4); + assert_proto("::1", v6); + assert_proto("", nil); + assert_proto("abc", nil); + assert_proto(" ", nil); +end + +function commonPrefixLength(cpl, _M) + local new_ip = _M.new_ip; + local function assert_cpl6(a, b, len, v4) + local ipa, ipb = new_ip(a), new_ip(b); + if v4 then len = len+96; end + assert_equal(cpl(ipa, ipb), len, "common prefix length of "..a.." and "..b.." is "..len); + assert_equal(cpl(ipb, ipa), len, "common prefix length of "..b.." and "..a.." is "..len); + end + local function assert_cpl4(a, b, len) + return assert_cpl6(a, b, len, "IPv4"); + end + assert_cpl4("0.0.0.0", "0.0.0.0", 32); + assert_cpl4("255.255.255.255", "0.0.0.0", 0); + assert_cpl4("255.255.255.255", "255.255.0.0", 16); + assert_cpl4("255.255.255.255", "255.255.255.255", 32); + assert_cpl4("255.255.255.255", "255.255.255.255", 32); + + assert_cpl6("::1", "::1", 128); + assert_cpl6("abcd::1", "abcd::1", 128); + assert_cpl6("abcd::abcd", "abcd::", 112); + assert_cpl6("abcd::abcd", "abcd::abcd:abcd", 96); +end diff --git a/tests/test_util_jid.lua b/tests/test_util_jid.lua index a817e644..02a90c3b 100644 --- a/tests/test_util_jid.lua +++ b/tests/test_util_jid.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/tests/test_util_multitable.lua b/tests/test_util_multitable.lua index ed10b128..71a83450 100644 --- a/tests/test_util_multitable.lua +++ b/tests/test_util_multitable.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -41,7 +41,7 @@ function get(get, multitable) end mt = multitable.new(); - + local trigger1, trigger2, trigger3 = {}, {}, {}; local item1, item2, item3 = {}, {}, {}; @@ -51,12 +51,12 @@ function get(get, multitable) mt:add(1, 2, 3, item1); assert_has_all("Has item1 for 1, 2, 3", mt:get(1, 2, 3), item1); - + -- Doesn't support nil --[[ mt:add(nil, item1); mt:add(nil, item2); mt:add(nil, item3); - + assert_has_all("Has all items with (nil)", mt:get(nil), item1, item2, item3); ]] end diff --git a/tests/test_util_queue.lua b/tests/test_util_queue.lua new file mode 100644 index 00000000..b0e1fa3d --- /dev/null +++ b/tests/test_util_queue.lua @@ -0,0 +1,68 @@ +local new = require "util.queue".new; + +local q = new(10); + +assert(q.size == 10); +assert(q:count() == 0); + +assert(q:push("one")); +assert(q:push("two")); +assert(q:push("three")); + +for i = 4, 10 do + print("pushing "..i) + assert(q:push("hello")); + assert(q:count() == i, "count is not "..i.."("..q:count()..")"); +end +assert(q:push("hello") == nil, "queue overfull!"); +assert(q:push("hello") == nil, "queue overfull!"); +assert(q:pop() == "one", "queue item incorrect"); +assert(q:pop() == "two", "queue item incorrect"); +assert(q:push("hello")); +assert(q:push("hello")); +assert(q:pop() == "three", "queue item incorrect"); +assert(q:push("hello")); +assert(q:push("hello") == nil, "queue overfull!"); +assert(q:push("hello") == nil, "queue overfull!"); + +assert(q:count() == 10, "queue count incorrect"); + +for i = 1, 10 do + assert(q:pop() == "hello", "queue item incorrect"); +end + +assert(q:count() == 0, "queue count incorrect"); + +assert(q:push(1)); +for i = 1, 1001 do + assert(q:pop() == i); + assert(q:count() == 0); + assert(q:push(i+1)); + assert(q:count() == 1); +end +assert(q:pop() == 1002); +assert(q:push(1)); +for i = 1, 1000000 do + q:pop(); + q:push(i+1); +end + +-- Test queues that purge old items when pushing to a full queue +local q = new(10, true); + +for i = 1, 10 do + q:push(i); +end + +assert(q:count() == 10); + +assert(q:push(11)); +assert(q:count() == 10); +assert(q:pop() == 2); -- First item should have been purged + +for i = 12, 32 do + assert(q:push(i)); +end + +assert(q:count() == 10); +assert(q:pop() == 23); diff --git a/tests/test_util_random.lua b/tests/test_util_random.lua new file mode 100644 index 00000000..79572ef8 --- /dev/null +++ b/tests/test_util_random.lua @@ -0,0 +1,10 @@ +-- Makes no attempt at testing how random the bytes are, +-- just that it returns the number of bytes requested + +function bytes(bytes) + assert_is(bytes(16)); + + for i = 1, 255 do + assert_equal(i, #bytes(i)); + end +end diff --git a/tests/test_util_rfc3484.lua b/tests/test_util_rfc3484.lua deleted file mode 100644 index 18ae310e..00000000 --- a/tests/test_util_rfc3484.lua +++ /dev/null @@ -1,51 +0,0 @@ --- Prosody IM --- Copyright (C) 2011 Florian Zeitz --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - -function source(source) - local new_ip = require"util.ip".new_ip; - assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("3ffe::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr, "3ffe::1", "prefer appropriate scope"); - assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("fe80::1", "IPv6"), new_ip("fec0::1", "IPv6")}).addr, "fec0::1", "prefer appropriate scope"); - assert_equal(source(new_ip("fec0::1", "IPv6"), {new_ip("fe80::1", "IPv6"), new_ip("2001::1", "IPv6")}).addr, "2001::1", "prefer appropriate scope"); - assert_equal(source(new_ip("ff05::1", "IPv6"), {new_ip("fe80::1", "IPv6"), new_ip("fec0::1", "IPv6"), new_ip("2001::1", "IPv6")}).addr, "fec0::1", "prefer appropriate scope"); - assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("2001::1", "IPv6"), new_ip("2002::1", "IPv6")}).addr, "2001::1", "prefer same address"); - assert_equal(source(new_ip("fec0::1", "IPv6"), {new_ip("fec0::2", "IPv6"), new_ip("2001::1", "IPv6")}).addr, "fec0::2", "prefer appropriate scope"); - assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("2001::2", "IPv6"), new_ip("3ffe::2", "IPv6")}).addr, "2001::2", "longest matching prefix"); - assert_equal(source(new_ip("2002:836b:2179::1", "IPv6"), {new_ip("2002:836b:2179::d5e3:7953:13eb:22e8", "IPv6"), new_ip("2001::2", "IPv6")}).addr, "2002:836b:2179::d5e3:7953:13eb:22e8", "prefer matching label"); -end - -function destination(dest) - local order; - local new_ip = require"util.ip".new_ip; - order = dest({new_ip("2001::1", "IPv6"), new_ip("131.107.65.121", "IPv4")}, {new_ip("2001::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("169.254.13.78", "IPv4")}) - assert_equal(order[1].addr, "2001::1", "prefer matching scope"); - assert_equal(order[2].addr, "131.107.65.121", "prefer matching scope") - - order = dest({new_ip("2001::1", "IPv6"), new_ip("131.107.65.121", "IPv4")}, {new_ip("fe80::1", "IPv6"), new_ip("131.107.65.117", "IPv4")}) - assert_equal(order[1].addr, "131.107.65.121", "prefer matching scope") - assert_equal(order[2].addr, "2001::1", "prefer matching scope") - - order = dest({new_ip("2001::1", "IPv6"), new_ip("10.1.2.3", "IPv4")}, {new_ip("2001::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("10.1.2.4", "IPv4")}) - assert_equal(order[1].addr, "2001::1", "prefer higher precedence"); - assert_equal(order[2].addr, "10.1.2.3", "prefer higher precedence"); - - order = dest({new_ip("2001::1", "IPv6"), new_ip("fec0::1", "IPv6"), new_ip("fe80::1", "IPv6")}, {new_ip("2001::2", "IPv6"), new_ip("fec0::1", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert_equal(order[1].addr, "fe80::1", "prefer smaller scope"); - assert_equal(order[2].addr, "fec0::1", "prefer smaller scope"); - assert_equal(order[3].addr, "2001::1", "prefer smaller scope"); - - order = dest({new_ip("2001::1", "IPv6"), new_ip("3ffe::1", "IPv6")}, {new_ip("2001::2", "IPv6"), new_ip("3f44::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert_equal(order[1].addr, "2001::1", "longest matching prefix"); - assert_equal(order[2].addr, "3ffe::1", "longest matching prefix"); - - order = dest({new_ip("2002:836b:4179::1", "IPv6"), new_ip("2001::1", "IPv6")}, {new_ip("2002:836b:4179::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert_equal(order[1].addr, "2002:836b:4179::1", "prefer matching label"); - assert_equal(order[2].addr, "2001::1", "prefer matching label"); - - order = dest({new_ip("2002:836b:4179::1", "IPv6"), new_ip("2001::1", "IPv6")}, {new_ip("2002:836b:4179::2", "IPv6"), new_ip("2001::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert_equal(order[1].addr, "2001::1", "prefer higher precedence"); - assert_equal(order[2].addr, "2002:836b:4179::1", "prefer higher precedence"); -end diff --git a/tests/test_util_rfc6724.lua b/tests/test_util_rfc6724.lua new file mode 100644 index 00000000..bb73e921 --- /dev/null +++ b/tests/test_util_rfc6724.lua @@ -0,0 +1,97 @@ +-- Prosody IM +-- Copyright (C) 2011-2013 Florian Zeitz +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +function source(source) + local new_ip = require"util.ip".new_ip; + assert_equal(source(new_ip("2001:db8:1::1", "IPv6"), + {new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr, + "2001:db8:3::1", + "prefer appropriate scope"); + assert_equal(source(new_ip("ff05::1", "IPv6"), + {new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr, + "2001:db8:3::1", + "prefer appropriate scope"); + assert_equal(source(new_ip("2001:db8:1::1", "IPv6"), + {new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:2::1", "IPv6")}).addr, + "2001:db8:1::1", + "prefer same address"); -- "2001:db8:1::1" should be marked "deprecated" here, we don't handle that right now + assert_equal(source(new_ip("fe80::1", "IPv6"), + {new_ip("fe80::2", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}).addr, + "fe80::2", + "prefer appropriate scope"); -- "fe80::2" should be marked "deprecated" here, we don't handle that right now + assert_equal(source(new_ip("2001:db8:1::1", "IPv6"), + {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr, + "2001:db8:1::2", + "longest matching prefix"); +--[[ "2001:db8:1::2" should be a care-of address and "2001:db8:3::2" a home address, we can't handle this and would fail + assert_equal(source(new_ip("2001:db8:1::1", "IPv6"), + {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr, + "2001:db8:3::2", + "prefer home address"); +]] + assert_equal(source(new_ip("2002:c633:6401::1", "IPv6"), + {new_ip("2002:c633:6401::d5e3:7953:13eb:22e8", "IPv6"), new_ip("2001:db8:1::2", "IPv6")}).addr, + "2002:c633:6401::d5e3:7953:13eb:22e8", + "prefer matching label"); -- "2002:c633:6401::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now + assert_equal(source(new_ip("2001:db8:1::d5e3:0:0:1", "IPv6"), + {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:1::d5e3:7953:13eb:22e8", "IPv6")}).addr, + "2001:db8:1::d5e3:7953:13eb:22e8", + "prefer temporary address") -- "2001:db8:1::2" should be marked "public" and "2001:db8:1::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now +end + +function destination(dest) + local order; + local new_ip = require"util.ip".new_ip; + order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")}, + {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("169.254.13.78", "IPv4")}) + assert_equal(order[1].addr, "2001:db8:1::1", "prefer matching scope"); + assert_equal(order[2].addr, "198.51.100.121", "prefer matching scope"); + + order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")}, + {new_ip("fe80::1", "IPv6"), new_ip("198.51.100.117", "IPv4")}) + assert_equal(order[1].addr, "198.51.100.121", "prefer matching scope"); + assert_equal(order[2].addr, "2001:db8:1::1", "prefer matching scope"); + + order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("10.1.2.3", "IPv4")}, + {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("10.1.2.4", "IPv4")}) + assert_equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence"); + assert_equal(order[2].addr, "10.1.2.3", "prefer higher precedence"); + + order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")}, + {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")}) + assert_equal(order[1].addr, "fe80::1", "prefer smaller scope"); + assert_equal(order[2].addr, "2001:db8:1::1", "prefer smaller scope"); + +--[[ "2001:db8:1::2" and "fe80::2" should be marked "care-of address", while "2001:db8:3::1" should be marked "home address", we can't currently handle this and would fail the test + order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")}, + {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::2", "IPv6")}) + assert_equal(order[1].addr, "2001:db8:1::1", "prefer home address"); + assert_equal(order[2].addr, "fe80::1", "prefer home address"); +]] + +--[[ "fe80::2" should be marked "deprecated", we can't currently handle this and would fail the test + order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")}, + {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")}) + assert_equal(order[1].addr, "2001:db8:1::1", "avoid deprecated addresses"); + assert_equal(order[2].addr, "fe80::1", "avoid deprecated addresses"); +]] + + order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:3ffe::1", "IPv6")}, + {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3f44::2", "IPv6"), new_ip("fe80::2", "IPv6")}) + assert_equal(order[1].addr, "2001:db8:1::1", "longest matching prefix"); + assert_equal(order[2].addr, "2001:db8:3ffe::1", "longest matching prefix"); + + order = dest({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}, + {new_ip("2002:c633:6401::2", "IPv6"), new_ip("fe80::2", "IPv6")}) + assert_equal(order[1].addr, "2002:c633:6401::1", "prefer matching label"); + assert_equal(order[2].addr, "2001:db8:1::1", "prefer matching label"); + + order = dest({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}, + {new_ip("2002:c633:6401::2", "IPv6"), new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")}) + assert_equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence"); + assert_equal(order[2].addr, "2002:c633:6401::1", "prefer higher precedence"); +end diff --git a/tests/test_util_stanza.lua b/tests/test_util_stanza.lua index fce26f3a..20cc8274 100644 --- a/tests/test_util_stanza.lua +++ b/tests/test_util_stanza.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -18,7 +18,7 @@ end function deserialize(deserialize, st) local stanza = st.stanza("message", { a = "a" }); - + local stanza2 = deserialize(st.preserialize(stanza)); assert_is(stanza2 and stanza.name, "deserialize returns a stanza"); assert_table(stanza2.attr, "Deserialized stanza has attributes"); diff --git a/tests/test_util_throttle.lua b/tests/test_util_throttle.lua new file mode 100644 index 00000000..582f499d --- /dev/null +++ b/tests/test_util_throttle.lua @@ -0,0 +1,34 @@ + +local now = 0; -- wibbly-wobbly... timey-wimey... stuff +local function predictable_gettime() + return now; +end +local function later(n) + now = now + n; -- time passes at a different rate +end + +local function override_gettime(throttle) + local i = 0; + repeat + i = i + 1; + local name = debug.getupvalue(throttle.update, i); + if name then + debug.setupvalue(throttle.update, i, predictable_gettime); + return throttle; + end + until not name; +end + +function create(create) + local a = override_gettime( create(3, 10) ); + + assert_equal(a:poll(1), true); -- 3 -> 2 + assert_equal(a:poll(1), true); -- 2 -> 1 + assert_equal(a:poll(1), true); -- 1 -> 0 + assert_equal(a:poll(1), false); -- MEEP, out of credits! + later(1); -- ... what about + assert_equal(a:poll(1), false); -- now? - Still no! + later(9); -- Later that day + assert_equal(a:poll(1), true); -- Should be back at 3 credits ... 2 +end + diff --git a/tests/test_util_uuid.lua b/tests/test_util_uuid.lua new file mode 100644 index 00000000..d3f72bb4 --- /dev/null +++ b/tests/test_util_uuid.lua @@ -0,0 +1,24 @@ +-- This tests the format, not the randomness + +-- https://tools.ietf.org/html/rfc4122#section-4.4 + +local pattern = "^" .. table.concat({ + string.rep("%x", 8), + string.rep("%x", 4), + "4" .. -- version + string.rep("%x", 3), + "[89ab]" .. -- reserved bits of 1 and 0 + string.rep("%x", 3), + string.rep("%x", 12), +}, "%-") .. "$"; + +function generate(generate) + for i = 1, 100 do + assert_is(generate():match(pattern)); + end +end + +function seed(seed) + assert_equal(seed("random string here"), nil, "seed doesn't return anything"); +end + diff --git a/tests/util/logger.lua b/tests/util/logger.lua index 35facd4e..c133e332 100644 --- a/tests/util/logger.lua +++ b/tests/util/logger.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/tools/ejabberd2prosody.lua b/tools/ejabberd2prosody.lua index 069b5161..46a48f57 100755 --- a/tools/ejabberd2prosody.lua +++ b/tools/ejabberd2prosody.lua @@ -2,7 +2,7 @@ -- 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. -- diff --git a/tools/ejabberdsql2prosody.lua b/tools/ejabberdsql2prosody.lua index 40be8190..69c8cfe8 100644 --- a/tools/ejabberdsql2prosody.lua +++ b/tools/ejabberdsql2prosody.lua @@ -2,7 +2,7 @@ -- 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. -- diff --git a/tools/erlparse.lua b/tools/erlparse.lua index 174585d3..25c38bcf 100644 --- a/tools/erlparse.lua +++ b/tools/erlparse.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/tools/jabberd14sql2prosody.lua b/tools/jabberd14sql2prosody.lua index 386bdcf0..e43dc296 100644 --- a/tools/jabberd14sql2prosody.lua +++ b/tools/jabberd14sql2prosody.lua @@ -5,242 +5,242 @@ do local _parse_sql_actions = { [0] = - 0, 1, 0, 1, 1, 2, 0, 2, 2, 0, 9, 2, 0, 10, 2, 0, 11, 2, 0, 13, - 2, 1, 2, 2, 1, 6, 3, 0, 3, 4, 3, 0, 3, 5, 3, 0, 3, 7, 3, 0, + 0, 1, 0, 1, 1, 2, 0, 2, 2, 0, 9, 2, 0, 10, 2, 0, 11, 2, 0, 13, + 2, 1, 2, 2, 1, 6, 3, 0, 3, 4, 3, 0, 3, 5, 3, 0, 3, 7, 3, 0, 3, 8, 3, 0, 3, 12, 4, 0, 2, 3, 7, 4, 0, 3, 8, 11 }; local _parse_sql_trans_keys = { [0] = - 0, 0, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47, 82, 82, - 69, 69, 65, 65, 84, 84, 69, 69, 32, 32, 68, 84, 65, - 65, 84, 84, 65, 65, 66, 66, 65, 65, 83, 83, 69, 69, - 9, 47, 9, 96, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47, - 10, 96, 10, 96, 9, 47, 9, 59, 45, 45, 10, 10, 42, - 42, 10, 42, 10, 47, 65, 65, 66, 66, 76, 76, 69, 69, - 32, 32, 73, 96, 70, 70, 32, 32, 78, 78, 79, 79, 84, 84, - 32, 32, 69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83, - 83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40, 40, - 10, 10, 32, 41, 32, 32, 75, 96, 69, 69, 89, 89, 32, 32, - 96, 96, 10, 96, 10, 96, 10, 10, 82, 82, 73, 73, 77, - 77, 65, 65, 82, 82, 89, 89, 32, 32, 75, 75, 69, 69, - 89, 89, 32, 32, 78, 78, 73, 73, 81, 81, 85, 85, 69, 69, - 32, 32, 75, 75, 10, 96, 10, 96, 10, 10, 10, 59, 10, - 59, 82, 82, 79, 79, 80, 80, 32, 32, 84, 84, 65, 65, - 66, 66, 76, 76, 69, 69, 32, 32, 73, 73, 70, 70, 32, 32, - 69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83, 83, 32, - 32, 96, 96, 10, 96, 10, 96, 59, 59, 78, 78, 83, 83, - 69, 69, 82, 82, 84, 84, 32, 32, 73, 73, 78, 78, 84, 84, - 79, 79, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40, - 86, 10, 41, 32, 32, 86, 86, 65, 65, 76, 76, 85, 85, - 69, 69, 83, 83, 32, 32, 40, 40, 39, 78, 10, 92, 10, 92, - 41, 44, 44, 59, 32, 78, 48, 57, 41, 57, 48, 57, 41, - 57, 85, 85, 76, 76, 76, 76, 34, 116, 79, 79, 67, 67, - 75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69, 69, - 83, 83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 87, - 87, 82, 82, 73, 73, 84, 84, 69, 69, 69, 69, 84, 84, - 32, 32, 10, 59, 10, 59, 78, 83, 76, 76, 79, 79, 67, 67, - 75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69, + 0, 0, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47, 82, 82, + 69, 69, 65, 65, 84, 84, 69, 69, 32, 32, 68, 84, 65, + 65, 84, 84, 65, 65, 66, 66, 65, 65, 83, 83, 69, 69, + 9, 47, 9, 96, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47, + 10, 96, 10, 96, 9, 47, 9, 59, 45, 45, 10, 10, 42, + 42, 10, 42, 10, 47, 65, 65, 66, 66, 76, 76, 69, 69, + 32, 32, 73, 96, 70, 70, 32, 32, 78, 78, 79, 79, 84, 84, + 32, 32, 69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83, + 83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40, 40, + 10, 10, 32, 41, 32, 32, 75, 96, 69, 69, 89, 89, 32, 32, + 96, 96, 10, 96, 10, 96, 10, 10, 82, 82, 73, 73, 77, + 77, 65, 65, 82, 82, 89, 89, 32, 32, 75, 75, 69, 69, + 89, 89, 32, 32, 78, 78, 73, 73, 81, 81, 85, 85, 69, 69, + 32, 32, 75, 75, 10, 96, 10, 96, 10, 10, 10, 59, 10, + 59, 82, 82, 79, 79, 80, 80, 32, 32, 84, 84, 65, 65, + 66, 66, 76, 76, 69, 69, 32, 32, 73, 73, 70, 70, 32, 32, + 69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83, 83, 32, + 32, 96, 96, 10, 96, 10, 96, 59, 59, 78, 78, 83, 83, + 69, 69, 82, 82, 84, 84, 32, 32, 73, 73, 78, 78, 84, 84, + 79, 79, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40, + 86, 10, 41, 32, 32, 86, 86, 65, 65, 76, 76, 85, 85, + 69, 69, 83, 83, 32, 32, 40, 40, 39, 78, 10, 92, 10, 92, + 41, 44, 44, 59, 32, 78, 48, 57, 41, 57, 48, 57, 41, + 57, 85, 85, 76, 76, 76, 76, 34, 116, 79, 79, 67, 67, + 75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69, 69, + 83, 83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 87, + 87, 82, 82, 73, 73, 84, 84, 69, 69, 69, 69, 84, 84, + 32, 32, 10, 59, 10, 59, 78, 83, 76, 76, 79, 79, 67, 67, + 75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69, 69, 83, 83, 69, 69, 9, 85, 0 }; local _parse_sql_key_spans = { [0] = - 0, 1, 1, 1, 33, 38, 1, 1, 1, 1, 1, 1, 17, 1, 1, 1, 1, 1, 1, 1, - 39, 88, 1, 1, 1, 33, 38, 87, 87, 39, 51, 1, 1, 1, 33, 38, 1, 1, 1, 1, - 1, 24, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, - 1, 10, 1, 22, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 50, 50, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 47, 32, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 40, 83, 83, 4, 16, 47, 10, 17, 10, 17, 1, 1, 1, 83, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1, + 0, 1, 1, 1, 33, 38, 1, 1, 1, 1, 1, 1, 17, 1, 1, 1, 1, 1, 1, 1, + 39, 88, 1, 1, 1, 33, 38, 87, 87, 39, 51, 1, 1, 1, 33, 38, 1, 1, 1, 1, + 1, 24, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, + 1, 10, 1, 22, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 50, 50, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 47, 32, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 40, 83, 83, 4, 16, 47, 10, 17, 10, 17, 1, 1, 1, 83, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1, 1, 50, 50, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 77 }; local _parse_sql_index_offsets = { [0] = - 0, 0, 2, 4, 6, 40, 79, 81, 83, 85, 87, 89, 91, 109, 111, 113, 115, 117, 119, 121, - 123, 163, 252, 254, 256, 258, 292, 331, 419, 507, 547, 599, 601, 603, 605, 639, 678, 680, 682, 684, - 686, 688, 713, 715, 717, 719, 721, 723, 725, 727, 729, 731, 733, 735, 737, 739, 741, 829, 917, 919, - 921, 923, 934, 936, 959, 961, 963, 965, 967, 1055, 1143, 1145, 1147, 1149, 1151, 1153, 1155, 1157, 1159, 1161, - 1163, 1165, 1167, 1169, 1171, 1173, 1175, 1177, 1179, 1181, 1269, 1357, 1359, 1410, 1461, 1463, 1465, 1467, 1469, 1471, - 1473, 1475, 1477, 1479, 1481, 1483, 1485, 1487, 1489, 1491, 1493, 1495, 1497, 1499, 1501, 1503, 1591, 1679, 1681, 1683, - 1685, 1687, 1689, 1691, 1693, 1695, 1697, 1699, 1701, 1703, 1705, 1793, 1881, 1883, 1931, 1964, 1966, 1968, 1970, 1972, - 1974, 1976, 1978, 1980, 1982, 2023, 2107, 2191, 2196, 2213, 2261, 2272, 2290, 2301, 2319, 2321, 2323, 2325, 2409, 2411, - 2413, 2415, 2417, 2419, 2421, 2423, 2425, 2427, 2429, 2431, 2433, 2521, 2609, 2611, 2613, 2615, 2617, 2619, 2621, 2623, + 0, 0, 2, 4, 6, 40, 79, 81, 83, 85, 87, 89, 91, 109, 111, 113, 115, 117, 119, 121, + 123, 163, 252, 254, 256, 258, 292, 331, 419, 507, 547, 599, 601, 603, 605, 639, 678, 680, 682, 684, + 686, 688, 713, 715, 717, 719, 721, 723, 725, 727, 729, 731, 733, 735, 737, 739, 741, 829, 917, 919, + 921, 923, 934, 936, 959, 961, 963, 965, 967, 1055, 1143, 1145, 1147, 1149, 1151, 1153, 1155, 1157, 1159, 1161, + 1163, 1165, 1167, 1169, 1171, 1173, 1175, 1177, 1179, 1181, 1269, 1357, 1359, 1410, 1461, 1463, 1465, 1467, 1469, 1471, + 1473, 1475, 1477, 1479, 1481, 1483, 1485, 1487, 1489, 1491, 1493, 1495, 1497, 1499, 1501, 1503, 1591, 1679, 1681, 1683, + 1685, 1687, 1689, 1691, 1693, 1695, 1697, 1699, 1701, 1703, 1705, 1793, 1881, 1883, 1931, 1964, 1966, 1968, 1970, 1972, + 1974, 1976, 1978, 1980, 1982, 2023, 2107, 2191, 2196, 2213, 2261, 2272, 2290, 2301, 2319, 2321, 2323, 2325, 2409, 2411, + 2413, 2415, 2417, 2419, 2421, 2423, 2425, 2427, 2429, 2431, 2433, 2521, 2609, 2611, 2613, 2615, 2617, 2619, 2621, 2623, 2625, 2627, 2678, 2729, 2736, 2738, 2740, 2742, 2744, 2746, 2748, 2750, 2752, 2754, 2756, 2758, 2760 }; local _parse_sql_indicies = { [0] = - 0, 1, 2, 0, 3, 1, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, - 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 3, 3, 3, 6, 3, 7, - 1, 8, 1, 9, 1, 10, 1, 11, 1, 12, 1, 13, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 14, 1, 15, 1, 16, 1, 17, 1, 18, 1, 19, 1, 20, - 1, 21, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24, - 1, 25, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24, - 1, 25, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 26, 1, 27, 1, 23, 27, 28, 1, 29, 28, - 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, - 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 30, 28, 29, 28, 28, 28, 28, 28, 28, 28, - 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, - 28, 28, 28, 28, 30, 28, 28, 28, 28, 22, 28, 32, 31, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 1, 31, 32, - 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 33, 31, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 36, 1, 37, 1, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 36, 1, 37, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 38, - 1, 35, 38, 39, 1, 40, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, - 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 40, - 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, - 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 39, 39, 39, 34, 39, 42, 1, - 43, 1, 44, 1, 45, 1, 46, 1, 47, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 48, 1, 49, 1, 50, 1, 51, 1, 52, - 1, 53, 1, 54, 1, 55, 1, 56, 1, 57, 1, 58, 1, 59, 1, 60, 1, 61, 1, 48, - 1, 63, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, - 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, - 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, - 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, - 62, 62, 62, 62, 62, 62, 62, 1, 62, 65, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 66, 64, 67, 1, 68, - 1, 69, 1, 70, 1, 1, 1, 1, 1, 1, 1, 1, 71, 1, 72, 1, 73, 1, 1, 1, - 1, 74, 1, 1, 1, 1, 75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 76, 1, 77, - 1, 78, 1, 79, 1, 80, 1, 82, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, - 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, - 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, - 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, - 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 1, 81, 82, 81, 81, 81, 81, - 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, - 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, - 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, - 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, - 81, 83, 81, 69, 83, 84, 1, 85, 1, 86, 1, 87, 1, 88, 1, 89, 1, 90, 1, 91, - 1, 92, 1, 93, 1, 83, 1, 94, 1, 95, 1, 96, 1, 97, 1, 98, 1, 99, 1, 73, - 1, 101, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, - 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, - 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, - 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, - 100, 100, 100, 100, 100, 100, 100, 1, 100, 103, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, - 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, - 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, - 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, - 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 104, 102, 105, 83, 106, - 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, - 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, - 71, 71, 71, 71, 71, 71, 71, 71, 107, 71, 108, 71, 71, 71, 71, 71, 71, 71, 71, 71, - 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, - 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 107, - 71, 109, 1, 110, 1, 111, 1, 112, 1, 113, 1, 114, 1, 115, 1, 116, 1, 117, 1, 118, - 1, 119, 1, 120, 1, 121, 1, 122, 1, 123, 1, 124, 1, 125, 1, 126, 1, 127, 1, 128, - 1, 129, 1, 131, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, - 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, - 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, - 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, - 130, 130, 130, 130, 130, 130, 130, 130, 130, 1, 130, 131, 130, 130, 130, 130, 130, 130, 130, 130, - 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, - 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, - 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, - 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 132, 130, 6, - 1, 133, 1, 134, 1, 135, 1, 136, 1, 137, 1, 138, 1, 139, 1, 140, 1, 141, 1, 142, - 1, 143, 1, 144, 1, 146, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, - 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, - 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, - 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, - 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 1, 145, 148, 147, 147, 147, 147, 147, 147, - 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, - 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, - 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, - 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 149, - 147, 150, 1, 151, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 152, 1, 153, 151, 151, 151, 151, 151, 151, 151, 151, - 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, - 151, 151, 154, 151, 155, 1, 152, 1, 156, 1, 157, 1, 158, 1, 159, 1, 160, 1, 161, 1, - 162, 1, 163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165, - 165, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 166, 1, 168, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, - 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 169, 167, 167, 167, 167, 167, 167, 167, - 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, - 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, - 167, 167, 167, 167, 167, 170, 167, 172, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, - 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 173, 171, 171, 171, - 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, - 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, - 171, 171, 171, 171, 171, 171, 171, 171, 171, 174, 171, 175, 1, 1, 176, 1, 161, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 177, 1, 178, 1, 1, 1, 1, 1, 1, - 163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165, 165, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 166, - 1, 179, 179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 180, 1, 1, 181, 1, 182, 1, 179, - 179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183, - 1, 180, 1, 1, 181, 1, 1, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183, 1, 184, - 1, 185, 1, 186, 1, 171, 1, 1, 171, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 171, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 171, 1, 171, 1, 1, 171, 1, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 171, 1, 1, 1, 171, 1, 171, 1, 187, 1, 188, 1, 189, 1, 190, 1, 191, 1, 192, - 1, 193, 1, 194, 1, 195, 1, 196, 1, 197, 1, 198, 1, 200, 199, 199, 199, 199, 199, 199, - 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, - 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, - 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, - 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 1, - 199, 200, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, - 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, - 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, - 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, - 199, 199, 199, 199, 199, 199, 199, 201, 199, 202, 1, 203, 1, 204, 1, 205, 1, 206, 1, 132, - 1, 207, 1, 208, 1, 209, 1, 210, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, - 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, - 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 211, 209, 2, 209, - 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, - 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, - 209, 209, 209, 209, 209, 209, 209, 211, 209, 212, 1, 1, 1, 1, 213, 1, 214, 1, 215, 1, - 216, 1, 217, 1, 218, 1, 219, 1, 220, 1, 221, 1, 222, 1, 223, 1, 132, 1, 127, 1, - 6, 2, 6, 6, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 224, 1, 225, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 226, 227, + 0, 1, 2, 0, 3, 1, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, + 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 3, 3, 3, 6, 3, 7, + 1, 8, 1, 9, 1, 10, 1, 11, 1, 12, 1, 13, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 14, 1, 15, 1, 16, 1, 17, 1, 18, 1, 19, 1, 20, + 1, 21, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24, + 1, 25, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24, + 1, 25, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 26, 1, 27, 1, 23, 27, 28, 1, 29, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 30, 28, 29, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 30, 28, 28, 28, 28, 22, 28, 32, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 1, 31, 32, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 33, 31, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 36, 1, 37, 1, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 36, 1, 37, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 38, + 1, 35, 38, 39, 1, 40, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, + 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 40, + 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, + 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 39, 39, 39, 34, 39, 42, 1, + 43, 1, 44, 1, 45, 1, 46, 1, 47, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 48, 1, 49, 1, 50, 1, 51, 1, 52, + 1, 53, 1, 54, 1, 55, 1, 56, 1, 57, 1, 58, 1, 59, 1, 60, 1, 61, 1, 48, + 1, 63, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, + 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, + 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, + 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, + 62, 62, 62, 62, 62, 62, 62, 1, 62, 65, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 66, 64, 67, 1, 68, + 1, 69, 1, 70, 1, 1, 1, 1, 1, 1, 1, 1, 71, 1, 72, 1, 73, 1, 1, 1, + 1, 74, 1, 1, 1, 1, 75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 76, 1, 77, + 1, 78, 1, 79, 1, 80, 1, 82, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 1, 81, 82, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 83, 81, 69, 83, 84, 1, 85, 1, 86, 1, 87, 1, 88, 1, 89, 1, 90, 1, 91, + 1, 92, 1, 93, 1, 83, 1, 94, 1, 95, 1, 96, 1, 97, 1, 98, 1, 99, 1, 73, + 1, 101, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 1, 100, 103, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, + 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, + 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, + 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, + 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 104, 102, 105, 83, 106, + 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, + 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, + 71, 71, 71, 71, 71, 71, 71, 71, 107, 71, 108, 71, 71, 71, 71, 71, 71, 71, 71, 71, + 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, + 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 107, + 71, 109, 1, 110, 1, 111, 1, 112, 1, 113, 1, 114, 1, 115, 1, 116, 1, 117, 1, 118, + 1, 119, 1, 120, 1, 121, 1, 122, 1, 123, 1, 124, 1, 125, 1, 126, 1, 127, 1, 128, + 1, 129, 1, 131, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 1, 130, 131, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 132, 130, 6, + 1, 133, 1, 134, 1, 135, 1, 136, 1, 137, 1, 138, 1, 139, 1, 140, 1, 141, 1, 142, + 1, 143, 1, 144, 1, 146, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, + 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, + 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, + 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, + 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 1, 145, 148, 147, 147, 147, 147, 147, 147, + 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, + 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, + 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, + 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 149, + 147, 150, 1, 151, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 152, 1, 153, 151, 151, 151, 151, 151, 151, 151, 151, + 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, + 151, 151, 154, 151, 155, 1, 152, 1, 156, 1, 157, 1, 158, 1, 159, 1, 160, 1, 161, 1, + 162, 1, 163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165, + 165, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 166, 1, 168, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, + 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 169, 167, 167, 167, 167, 167, 167, 167, + 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, + 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, + 167, 167, 167, 167, 167, 170, 167, 172, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, + 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 173, 171, 171, 171, + 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, + 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, + 171, 171, 171, 171, 171, 171, 171, 171, 171, 174, 171, 175, 1, 1, 176, 1, 161, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 177, 1, 178, 1, 1, 1, 1, 1, 1, + 163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165, 165, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 166, + 1, 179, 179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 180, 1, 1, 181, 1, 182, 1, 179, + 179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183, + 1, 180, 1, 1, 181, 1, 1, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183, 1, 184, + 1, 185, 1, 186, 1, 171, 1, 1, 171, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 171, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 171, 1, 171, 1, 1, 171, 1, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 171, 1, 1, 1, 171, 1, 171, 1, 187, 1, 188, 1, 189, 1, 190, 1, 191, 1, 192, + 1, 193, 1, 194, 1, 195, 1, 196, 1, 197, 1, 198, 1, 200, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 1, + 199, 200, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 201, 199, 202, 1, 203, 1, 204, 1, 205, 1, 206, 1, 132, + 1, 207, 1, 208, 1, 209, 1, 210, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, + 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, + 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 211, 209, 2, 209, + 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, + 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, + 209, 209, 209, 209, 209, 209, 209, 211, 209, 212, 1, 1, 1, 1, 213, 1, 214, 1, 215, 1, + 216, 1, 217, 1, 218, 1, 219, 1, 220, 1, 221, 1, 222, 1, 223, 1, 132, 1, 127, 1, + 6, 2, 6, 6, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 224, 1, 225, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 226, 227, 1, 1, 1, 1, 228, 1, 1, 229, 1, 1, 1, 1, 1, 1, 230, 1, 231, 1, 0 }; local _parse_sql_trans_targs = { [0] = - 2, 0, 196, 4, 4, 5, 196, 7, 8, 9, 10, 11, 12, 13, 36, 14, 15, 16, 17, 18, - 19, 20, 21, 21, 22, 24, 27, 23, 25, 25, 26, 28, 28, 29, 30, 30, 31, 33, 32, 34, - 34, 35, 37, 38, 39, 40, 41, 42, 56, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, - 54, 55, 57, 57, 57, 57, 58, 59, 60, 61, 62, 92, 63, 64, 71, 82, 89, 65, 66, 67, - 68, 69, 69, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, - 90, 90, 90, 90, 91, 70, 92, 93, 196, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, - 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 116, 117, 119, 120, 121, 122, 123, 124, 125, - 126, 127, 128, 129, 130, 131, 131, 131, 131, 132, 133, 134, 137, 134, 135, 136, 138, 139, 140, 141, - 142, 143, 144, 145, 150, 151, 154, 146, 146, 147, 157, 146, 146, 147, 157, 148, 149, 196, 144, 151, - 148, 149, 152, 153, 155, 156, 147, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, - 171, 172, 173, 174, 175, 176, 177, 179, 180, 181, 181, 182, 184, 195, 185, 186, 187, 188, 189, 190, + 2, 0, 196, 4, 4, 5, 196, 7, 8, 9, 10, 11, 12, 13, 36, 14, 15, 16, 17, 18, + 19, 20, 21, 21, 22, 24, 27, 23, 25, 25, 26, 28, 28, 29, 30, 30, 31, 33, 32, 34, + 34, 35, 37, 38, 39, 40, 41, 42, 56, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, + 54, 55, 57, 57, 57, 57, 58, 59, 60, 61, 62, 92, 63, 64, 71, 82, 89, 65, 66, 67, + 68, 69, 69, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, + 90, 90, 90, 90, 91, 70, 92, 93, 196, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, + 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 116, 117, 119, 120, 121, 122, 123, 124, 125, + 126, 127, 128, 129, 130, 131, 131, 131, 131, 132, 133, 134, 137, 134, 135, 136, 138, 139, 140, 141, + 142, 143, 144, 145, 150, 151, 154, 146, 146, 147, 157, 146, 146, 147, 157, 148, 149, 196, 144, 151, + 148, 149, 152, 153, 155, 156, 147, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, + 171, 172, 173, 174, 175, 176, 177, 179, 180, 181, 181, 182, 184, 195, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 1, 3, 6, 94, 118, 158, 178, 183 }; local _parse_sql_trans_actions = { [0] = - 1, 0, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 3, 1, 1, 1, 1, 1, 3, 1, 1, 3, 1, 1, 3, 1, 1, 1, 1, - 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 5, 20, 1, 3, 30, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 5, 20, 1, 3, 26, 3, 3, 1, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 5, 20, 1, 3, 42, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, - 1, 1, 11, 1, 5, 5, 1, 5, 20, 46, 5, 1, 3, 34, 1, 14, 1, 17, 1, 1, - 51, 38, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 3, 1, 1, 1, 1, 1, 3, 1, 1, 3, 1, 1, 3, 1, 1, 1, 1, + 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 5, 20, 1, 3, 30, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 5, 20, 1, 3, 26, 3, 3, 1, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 5, 20, 1, 3, 42, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, + 1, 1, 11, 1, 5, 5, 1, 5, 20, 46, 5, 1, 3, 34, 1, 14, 1, 17, 1, 1, + 51, 38, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }; @@ -277,7 +277,7 @@ function parse_sql(data, h) local mark, token; local table_name, columns, value_lists, value_list, value_count; - + cs = parse_sql_start; -- ragel flat exec @@ -322,10 +322,10 @@ function parse_sql(data, h) _inds = _parse_sql_index_offsets[cs]; _slen = _parse_sql_key_spans[cs]; - if _slen > 0 and - _parse_sql_trans_keys[_keys] <= data:byte(p) and - data:byte(p) <= _parse_sql_trans_keys[_keys + 1] then - _trans = _parse_sql_indicies[ _inds + data:byte(p) - _parse_sql_trans_keys[_keys] ]; + if _slen > 0 and + _parse_sql_trans_keys[_keys] <= data:byte(p) and + data:byte(p) <= _parse_sql_trans_keys[_keys + 1] then + _trans = _parse_sql_indicies[ _inds + data:byte(p) - _parse_sql_trans_keys[_keys] ]; else _trans =_parse_sql_indicies[ _inds + _slen ]; end cs = _parse_sql_trans_targs[_trans]; @@ -364,7 +364,7 @@ function parse_sql(data, h) h.create(table_name, columns); -- ACTION elseif _tempval == 7 then --4 FROM_STATE_ACTION_SWITCH -- line 65 "sql.rl" -- end of line directive - + value_count = value_count + 1; value_list[value_count] = token:gsub("\\.", _sql_unescapes); -- ACTION elseif _tempval == 8 then --4 FROM_STATE_ACTION_SWITCH @@ -392,7 +392,7 @@ function parse_sql(data, h) end if _trigger_goto then _continue = true; break; end - end -- endif + end -- endif if _goto_level <= _again then if cs == 0 then diff --git a/tools/migration/migrator/prosody_sql.lua b/tools/migration/migrator/prosody_sql.lua index 27b5835e..180ae910 100644 --- a/tools/migration/migrator/prosody_sql.lua +++ b/tools/migration/migrator/prosody_sql.lua @@ -24,7 +24,7 @@ local function create_table(connection, params) elseif params.driver == "MySQL" then create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT"); end - + local stmt = connection:prepare(create_sql); if stmt then local ok = stmt:execute(); diff --git a/tools/migration/prosody-migrator.lua b/tools/migration/prosody-migrator.lua index 7c933b88..b86e9892 100644 --- a/tools/migration/prosody-migrator.lua +++ b/tools/migration/prosody-migrator.lua @@ -115,7 +115,7 @@ if have_err then print(""); os.exit(1); end - + local itype = config[from_store].type; local otype = config[to_store].type; local reader = require("migrator."..itype).reader(config[from_store]); diff --git a/tools/openfire2prosody.lua b/tools/openfire2prosody.lua index 5ef47602..cd3e62e5 100644 --- a/tools/openfire2prosody.lua +++ b/tools/openfire2prosody.lua @@ -1,7 +1,7 @@ #!/usr/bin/env lua -- Prosody IM -- Copyright (C) 2008-2009 Waqas Hussain --- +-- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- diff --git a/tools/xep227toprosody.lua b/tools/xep227toprosody.lua index 81c9863b..521851e9 100755 --- a/tools/xep227toprosody.lua +++ b/tools/xep227toprosody.lua @@ -3,7 +3,7 @@ -- Copyright (C) 2008-2009 Matthew Wild -- Copyright (C) 2008-2009 Waqas Hussain -- Copyright (C) 2010 Stefan Gehn --- +-- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- diff --git a/util-src/Makefile b/util-src/Makefile index 3a1ba3f2..9c6c377c 100644 --- a/util-src/Makefile +++ b/util-src/Makefile @@ -1,41 +1,34 @@ include ../config.unix -LUA_SUFFIX?=5.1 -LUA_INCDIR?=/usr/include/lua$(LUA_SUFFIX) -LUA_LIB?=lua$(LUA_SUFFIX) -IDN_LIB?=idn -OPENSSL_LIB?=crypto -CC?=gcc -CXX?=g++ -LD?=gcc -CFLAGS+=-ggdb +CFLAGS+=-ggdb -Wall -pedantic -I$(LUA_INCDIR) + +INSTALL_DATA=install -m644 +TARGET?=../util/ + +ALL=encodings.so hashes.so net.so pposix.so signal.so table.so ringbuffer.so + +ifdef RANDOM +ALL+=crand.so +endif .PHONY: all install clean .SUFFIXES: .c .o .so -all: encodings.so hashes.so net.so pposix.so signal.so +all: $(ALL) -install: encodings.so hashes.so net.so pposix.so signal.so - install *.so ../util/ +install: $(ALL) + $(INSTALL_DATA) $^ $(TARGET) clean: - rm -f *.o - rm -f *.so - rm -f ../util/*.so - -encodings.so: encodings.o - MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET; - $(CC) -o $@ $< $(LDFLAGS) $(IDNA_LIBS) + rm -f $(ALL) $(patsubst %.so,%.o,$(ALL)) -hashes.so: hashes.o - MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET; - $(CC) -o $@ $< $(LDFLAGS) -l$(OPENSSL_LIB) +encodings.so: LDLIBS+=$(IDNA_LIBS) -.c.o: - $(CC) $(CFLAGS) -I$(LUA_INCDIR) -c -o $@ $< +hashes.so: LDLIBS+=$(OPENSSL_LIBS) -.o.so: - MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET; - $(LD) -o $@ $< $(LDFLAGS) +crand.o: CFLAGS+=-DWITH_$(RANDOM) +crand.so: LDLIBS+=$(RANDOM_LIBS) +%.so: %.o + $(LD) $(LDFLAGS) -o $@ $^ $(LDLIBS) diff --git a/util-src/crand.c b/util-src/crand.c new file mode 100644 index 00000000..735135fb --- /dev/null +++ b/util-src/crand.c @@ -0,0 +1,156 @@ +/* Prosody IM +-- Copyright (C) 2008-2016 Matthew Wild +-- Copyright (C) 2008-2016 Waqas Hussain +-- Copyright (C) 2016 Kim Alvefur +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- +*/ + +/* +* crand.c +* C PRNG interface +*/ + +#include "lualib.h" +#include "lauxlib.h" + +#include <string.h> +#include <errno.h> + +/* + * TODO: Decide on fixed size or dynamically allocated buffer + */ +#if 1 +#include <stdlib.h> +#else +#define BUFLEN 256 +#endif + +#if defined(WITH_GETRANDOM) +#include <unistd.h> +#include <sys/syscall.h> +#include <linux/random.h> + +#ifndef SYS_getrandom +#error getrandom() requires Linux 3.17 or later +#endif + +/* Was this not supposed to be a function? */ +int getrandom(char *buf, size_t len, int flags) { + return syscall(SYS_getrandom, buf, len, flags); +} + +#elif defined(WITH_ARC4RANDOM) +#include <stdlib.h> +#elif defined(WITH_OPENSSL) +#include <openssl/rand.h> +#else +#error util.crand compiled without a random source +#endif + +int Lrandom(lua_State *L) { +#ifdef BUFLEN + unsigned char buf[BUFLEN]; +#else + unsigned char *buf; +#endif + int ret = 0; + size_t len = (size_t)luaL_checkint(L, 1); +#ifdef BUFLEN + len = len > BUFLEN ? BUFLEN : len; +#else + buf = malloc(len); + + if(buf == NULL) { + lua_pushnil(L); + lua_pushstring(L, "out of memory"); + /* or it migth be better to + * return lua_error(L); + */ + return 2; + } +#endif + +#if defined(WITH_GETRANDOM) + ret = getrandom(buf, len, 0); + + if(ret < 0) { +#ifndef BUFLEN + free(buf); +#endif + lua_pushnil(L); + lua_pushstring(L, strerror(errno)); + lua_pushinteger(L, errno); + return 3; + } + +#elif defined(WITH_ARC4RANDOM) + arc4random_buf(buf, len); + ret = len; +#elif defined(WITH_OPENSSL) + ret = RAND_bytes(buf, len); + + if(ret == 1) { + ret = len; + } else { +#ifndef BUFLEN + free(buf); +#endif + lua_pushnil(L); + lua_pushstring(L, "failed"); + /* lua_pushinteger(L, ERR_get_error()); */ + return 2; + } + +#endif + + lua_pushlstring(L, buf, ret); +#ifndef BUFLEN + free(buf); +#endif + return 1; +} + +#ifdef ENABLE_SEEDING +int Lseed(lua_State *L) { + size_t len; + const char *seed = lua_tolstring(L, 1, &len); + +#if defined(WITH_OPENSSL) + RAND_add(seed, len, len); + return 0; +#else + lua_pushnil(L); + lua_pushliteral(L, "not-supported"); + return 2; +#endif +} +#endif + +int luaopen_util_crand(lua_State *L) { + lua_newtable(L); + lua_pushcfunction(L, Lrandom); + lua_setfield(L, -2, "bytes"); +#ifdef ENABLE_SEEDING + lua_pushcfunction(L, Lseed); + lua_setfield(L, -2, "seed"); +#endif + +#if defined(WITH_GETRANDOM) + lua_pushstring(L, "Linux"); +#elif defined(WITH_ARC4RANDOM) + lua_pushstring(L, "arc4random()"); +#elif defined(WITH_OPENSSL) + lua_pushstring(L, "OpenSSL"); +#endif + lua_setfield(L, -2, "_source"); + +#if defined(WITH_OPENSSL) && defined(_WIN32) + /* Do we need to seed this on Windows? */ +#endif + + return 1; +} + diff --git a/util-src/encodings.c b/util-src/encodings.c index 91826ca4..35677095 100644 --- a/util-src/encodings.c +++ b/util-src/encodings.c @@ -2,7 +2,7 @@ -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 1994-2015 Lua.org, PUC-Rio. --- +-- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- @@ -21,97 +21,131 @@ #include "lua.h" #include "lauxlib.h" +#if (LUA_VERSION_NUM == 501) +#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) +#endif + /***************** BASE64 *****************/ -static const char code[]= -"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static const char code[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; -static void base64_encode(luaL_Buffer *b, unsigned int c1, unsigned int c2, unsigned int c3, int n) -{ - unsigned long tuple=c3+256UL*(c2+256UL*c1); +static void base64_encode(luaL_Buffer* b, unsigned int c1, unsigned int c2, unsigned int c3, int n) { + unsigned long tuple = c3 + 256UL * (c2 + 256UL * c1); int i; char s[4]; - for (i=0; i<4; i++) { - s[3-i] = code[tuple % 64]; + + for(i = 0; i < 4; i++) { + s[3 - i] = code[tuple % 64]; tuple /= 64; } - for (i=n+1; i<4; i++) s[i]='='; - luaL_addlstring(b,s,4); + + for(i = n + 1; i < 4; i++) { + s[i] = '='; + } + + luaL_addlstring(b, s, 4); } -static int Lbase64_encode(lua_State *L) /** encode(s) */ -{ +static int Lbase64_encode(lua_State* L) { /** encode(s) */ size_t l; - const unsigned char *s=(const unsigned char*)luaL_checklstring(L,1,&l); + const unsigned char* s = (const unsigned char*)luaL_checklstring(L, 1, &l); luaL_Buffer b; int n; - luaL_buffinit(L,&b); - for (n=l/3; n--; s+=3) base64_encode(&b,s[0],s[1],s[2],3); - switch (l%3) - { - case 1: base64_encode(&b,s[0],0,0,1); break; - case 2: base64_encode(&b,s[0],s[1],0,2); break; + luaL_buffinit(L, &b); + + for(n = l / 3; n--; s += 3) { + base64_encode(&b, s[0], s[1], s[2], 3); + } + + switch(l % 3) { + case 1: + base64_encode(&b, s[0], 0, 0, 1); + break; + case 2: + base64_encode(&b, s[0], s[1], 0, 2); + break; } + luaL_pushresult(&b); return 1; } -static void base64_decode(luaL_Buffer *b, int c1, int c2, int c3, int c4, int n) -{ - unsigned long tuple=c4+64L*(c3+64L*(c2+64L*c1)); +static void base64_decode(luaL_Buffer* b, int c1, int c2, int c3, int c4, int n) { + unsigned long tuple = c4 + 64L * (c3 + 64L * (c2 + 64L * c1)); char s[3]; - switch (--n) - { - case 3: s[2]=(char) tuple; - case 2: s[1]=(char) (tuple >> 8); - case 1: s[0]=(char) (tuple >> 16); + + switch(--n) { + case 3: + s[2] = (char) tuple; + case 2: + s[1] = (char)(tuple >> 8); + case 1: + s[0] = (char)(tuple >> 16); } - luaL_addlstring(b,s,n); + + luaL_addlstring(b, s, n); } -static int Lbase64_decode(lua_State *L) /** decode(s) */ -{ +static int Lbase64_decode(lua_State* L) { /** decode(s) */ size_t l; - const char *s=luaL_checklstring(L,1,&l); + const char* s = luaL_checklstring(L, 1, &l); luaL_Buffer b; - int n=0; + int n = 0; char t[4]; - luaL_buffinit(L,&b); - for (;;) - { - int c=*s++; - switch (c) - { - const char *p; + luaL_buffinit(L, &b); + + for(;;) { + int c = *s++; + + switch(c) { + const char* p; default: - p=strchr(code,c); if (p==NULL) return 0; - t[n++]= (char) (p-code); - if (n==4) - { - base64_decode(&b,t[0],t[1],t[2],t[3],4); - n=0; + p = strchr(code, c); + + if(p == NULL) { + return 0; } + + t[n++] = (char)(p - code); + + if(n == 4) { + base64_decode(&b, t[0], t[1], t[2], t[3], 4); + n = 0; + } + break; case '=': - switch (n) - { - case 1: base64_decode(&b,t[0],0,0,0,1); break; - case 2: base64_decode(&b,t[0],t[1],0,0,2); break; - case 3: base64_decode(&b,t[0],t[1],t[2],0,3); break; + + switch(n) { + case 1: + base64_decode(&b, t[0], 0, 0, 0, 1); + break; + case 2: + base64_decode(&b, t[0], t[1], 0, 0, 2); + break; + case 3: + base64_decode(&b, t[0], t[1], t[2], 0, 3); + break; } - n=0; + + n = 0; break; case 0: luaL_pushresult(&b); return 1; - case '\n': case '\r': case '\t': case ' ': case '\f': case '\b': + case '\n': + case '\r': + case '\t': + case ' ': + case '\f': + case '\b': break; } } } -static const luaL_Reg Reg_base64[] = -{ +static const luaL_Reg Reg_base64[] = { { "encode", Lbase64_encode }, { "decode", Lbase64_decode }, { NULL, NULL } @@ -129,70 +163,89 @@ static const luaL_Reg Reg_base64[] = /* * Decode one UTF-8 sequence, returning NULL if byte sequence is invalid. */ -static const char *utf8_decode (const char *o, int *val) { +static const char* utf8_decode(const char* o, int* val) { static unsigned int limits[] = {0xFF, 0x7F, 0x7FF, 0xFFFF}; - const unsigned char *s = (const unsigned char *)o; + const unsigned char* s = (const unsigned char*)o; unsigned int c = s[0]; unsigned int res = 0; /* final result */ - if (c < 0x80) /* ascii? */ + + if(c < 0x80) { /* ascii? */ res = c; - else { + } else { int count = 0; /* to count number of continuation bytes */ - while (c & 0x40) { /* still have continuation bytes? */ + + while(c & 0x40) { /* still have continuation bytes? */ int cc = s[++count]; /* read next byte */ - if ((cc & 0xC0) != 0x80) /* not a continuation byte? */ - return NULL; /* invalid byte sequence */ + + if((cc & 0xC0) != 0x80) { /* not a continuation byte? */ + return NULL; /* invalid byte sequence */ + } + res = (res << 6) | (cc & 0x3F); /* add lower 6 bits from cont. byte */ c <<= 1; /* to test next bit */ } + res |= ((c & 0x7F) << (count * 5)); /* add first byte */ - if (count > 3 || res > MAXUNICODE || res <= limits[count] || (0xd800 <= res && res <= 0xdfff) ) - return NULL; /* invalid byte sequence */ + + if(count > 3 || res > MAXUNICODE || res <= limits[count] || (0xd800 <= res && res <= 0xdfff)) { + return NULL; /* invalid byte sequence */ + } + s += count; /* skip continuation bytes read */ } - if (val) *val = res; - return (const char *)s + 1; /* +1 to include first byte */ + + if(val) { + *val = res; + } + + return (const char*)s + 1; /* +1 to include first byte */ } /* * Check that a string is valid UTF-8 * Returns NULL if not */ -const char* check_utf8 (lua_State *L, int idx, size_t *l) { +const char* check_utf8(lua_State* L, int idx, size_t* l) { size_t pos, len; - const char *s = luaL_checklstring(L, 1, &len); + const char* s = luaL_checklstring(L, 1, &len); pos = 0; - while (pos <= len) { - const char *s1 = utf8_decode(s + pos, NULL); - if (s1 == NULL) { /* conversion error? */ + + while(pos <= len) { + const char* s1 = utf8_decode(s + pos, NULL); + + if(s1 == NULL) { /* conversion error? */ return NULL; } + pos = s1 - s; } + if(l != NULL) { *l = len; } + return s; } -static int Lutf8_valid(lua_State *L) { +static int Lutf8_valid(lua_State* L) { lua_pushboolean(L, check_utf8(L, 1, NULL) != NULL); return 1; } -static int Lutf8_length(lua_State *L) { +static int Lutf8_length(lua_State* L) { size_t len; + if(!check_utf8(L, 1, &len)) { lua_pushnil(L); lua_pushliteral(L, "invalid utf8"); return 2; } + lua_pushinteger(L, len); return 1; } -static const luaL_Reg Reg_utf8[] = -{ +static const luaL_Reg Reg_utf8[] = { { "valid", Lutf8_valid }, { "length", Lutf8_length }, { NULL, NULL } @@ -206,61 +259,71 @@ static const luaL_Reg Reg_utf8[] = #include <unicode/ustring.h> #include <unicode/utrace.h> -static int icu_stringprep_prep(lua_State *L, const UStringPrepProfile *profile) -{ +static int icu_stringprep_prep(lua_State* L, const UStringPrepProfile* profile) { size_t input_len; int32_t unprepped_len, prepped_len, output_len; - const char *input; + const char* input; char output[1024]; UChar unprepped[1024]; /* Temporary unicode buffer (1024 characters) */ UChar prepped[1024]; - + UErrorCode err = U_ZERO_ERROR; if(!lua_isstring(L, 1)) { lua_pushnil(L); return 1; } + input = lua_tolstring(L, 1, &input_len); - if (input_len >= 1024) { + + if(input_len >= 1024) { lua_pushnil(L); return 1; } + u_strFromUTF8(unprepped, 1024, &unprepped_len, input, input_len, &err); - if (U_FAILURE(err)) { + + if(U_FAILURE(err)) { lua_pushnil(L); return 1; } + prepped_len = usprep_prepare(profile, unprepped, unprepped_len, prepped, 1024, 0, NULL, &err); - if (U_FAILURE(err)) { + + if(U_FAILURE(err)) { lua_pushnil(L); return 1; } else { u_strToUTF8(output, 1024, &output_len, prepped, prepped_len, &err); - if (U_SUCCESS(err) && output_len < 1024) + + if(U_SUCCESS(err) && output_len < 1024) { lua_pushlstring(L, output, output_len); - else + } else { lua_pushnil(L); + } + return 1; } } -UStringPrepProfile *icu_nameprep; -UStringPrepProfile *icu_nodeprep; -UStringPrepProfile *icu_resourceprep; -UStringPrepProfile *icu_saslprep; +UStringPrepProfile* icu_nameprep; +UStringPrepProfile* icu_nodeprep; +UStringPrepProfile* icu_resourceprep; +UStringPrepProfile* icu_saslprep; /* initialize global ICU stringprep profiles */ -void init_icu() -{ +void init_icu() { UErrorCode err = U_ZERO_ERROR; utrace_setLevel(UTRACE_VERBOSE); icu_nameprep = usprep_openByType(USPREP_RFC3491_NAMEPREP, &err); icu_nodeprep = usprep_openByType(USPREP_RFC3920_NODEPREP, &err); icu_resourceprep = usprep_openByType(USPREP_RFC3920_RESOURCEPREP, &err); icu_saslprep = usprep_openByType(USPREP_RFC4013_SASLPREP, &err); - if (U_FAILURE(err)) fprintf(stderr, "[c] util.encodings: error: %s\n", u_errorName((UErrorCode)err)); + + if(U_FAILURE(err)) { + fprintf(stderr, "[c] util.encodings: error: %s\n", u_errorName((UErrorCode)err)); + } } #define MAKE_PREP_FUNC(myFunc, prep) \ @@ -271,8 +334,7 @@ MAKE_PREP_FUNC(Lstringprep_nodeprep, icu_nodeprep) /** stringprep.nodeprep(s) * MAKE_PREP_FUNC(Lstringprep_resourceprep, icu_resourceprep) /** stringprep.resourceprep(s) */ MAKE_PREP_FUNC(Lstringprep_saslprep, icu_saslprep) /** stringprep.saslprep(s) */ -static const luaL_Reg Reg_stringprep[] = -{ +static const luaL_Reg Reg_stringprep[] = { { "nameprep", Lstringprep_nameprep }, { "nodeprep", Lstringprep_nodeprep }, { "resourceprep", Lstringprep_resourceprep }, @@ -285,24 +347,28 @@ static const luaL_Reg Reg_stringprep[] = #include <stringprep.h> -static int stringprep_prep(lua_State *L, const Stringprep_profile *profile) -{ +static int stringprep_prep(lua_State* L, const Stringprep_profile* profile) { size_t len; - const char *s; + const char* s; char string[1024]; int ret; + if(!lua_isstring(L, 1)) { lua_pushnil(L); return 1; } + s = check_utf8(L, 1, &len); - if (s == NULL || len >= 1024 || len != strlen(s)) { + + if(s == NULL || len >= 1024 || len != strlen(s)) { lua_pushnil(L); return 1; /* TODO return error message */ } + strcpy(string, s); ret = stringprep(string, 1024, (Stringprep_profile_flags)0, profile); - if (ret == STRINGPREP_OK) { + + if(ret == STRINGPREP_OK) { lua_pushstring(L, string); return 1; } else { @@ -319,8 +385,7 @@ MAKE_PREP_FUNC(Lstringprep_nodeprep, stringprep_xmpp_nodeprep) /** stringprep.n MAKE_PREP_FUNC(Lstringprep_resourceprep, stringprep_xmpp_resourceprep) /** stringprep.resourceprep(s) */ MAKE_PREP_FUNC(Lstringprep_saslprep, stringprep_saslprep) /** stringprep.saslprep(s) */ -static const luaL_Reg Reg_stringprep[] = -{ +static const luaL_Reg Reg_stringprep[] = { { "nameprep", Lstringprep_nameprep }, { "nodeprep", Lstringprep_nodeprep }, { "resourceprep", Lstringprep_resourceprep }, @@ -334,62 +399,70 @@ static const luaL_Reg Reg_stringprep[] = #include <unicode/ustdio.h> #include <unicode/uidna.h> /* IDNA2003 or IDNA2008 ? ? ? */ -static int Lidna_to_ascii(lua_State *L) /** idna.to_ascii(s) */ -{ +static int Lidna_to_ascii(lua_State* L) { /** idna.to_ascii(s) */ size_t len; int32_t ulen, dest_len, output_len; - const char *s = luaL_checklstring(L, 1, &len); + const char* s = luaL_checklstring(L, 1, &len); UChar ustr[1024]; UErrorCode err = U_ZERO_ERROR; UChar dest[1024]; char output[1024]; u_strFromUTF8(ustr, 1024, &ulen, s, len, &err); - if (U_FAILURE(err)) { + + if(U_FAILURE(err)) { lua_pushnil(L); return 1; } dest_len = uidna_IDNToASCII(ustr, ulen, dest, 1024, UIDNA_USE_STD3_RULES, NULL, &err); - if (U_FAILURE(err)) { + + if(U_FAILURE(err)) { lua_pushnil(L); return 1; } else { u_strToUTF8(output, 1024, &output_len, dest, dest_len, &err); - if (U_SUCCESS(err) && output_len < 1024) + + if(U_SUCCESS(err) && output_len < 1024) { lua_pushlstring(L, output, output_len); - else + } else { lua_pushnil(L); + } + return 1; } } -static int Lidna_to_unicode(lua_State *L) /** idna.to_unicode(s) */ -{ +static int Lidna_to_unicode(lua_State* L) { /** idna.to_unicode(s) */ size_t len; int32_t ulen, dest_len, output_len; - const char *s = luaL_checklstring(L, 1, &len); + const char* s = luaL_checklstring(L, 1, &len); UChar ustr[1024]; UErrorCode err = U_ZERO_ERROR; UChar dest[1024]; char output[1024]; u_strFromUTF8(ustr, 1024, &ulen, s, len, &err); - if (U_FAILURE(err)) { + + if(U_FAILURE(err)) { lua_pushnil(L); return 1; } dest_len = uidna_IDNToUnicode(ustr, ulen, dest, 1024, UIDNA_USE_STD3_RULES, NULL, &err); - if (U_FAILURE(err)) { + + if(U_FAILURE(err)) { lua_pushnil(L); return 1; } else { u_strToUTF8(output, 1024, &output_len, dest, dest_len, &err); - if (U_SUCCESS(err) && output_len < 1024) + + if(U_SUCCESS(err) && output_len < 1024) { lua_pushlstring(L, output, output_len); - else + } else { lua_pushnil(L); + } + return 1; } } @@ -400,17 +473,20 @@ static int Lidna_to_unicode(lua_State *L) /** idna.to_unicode(s) */ #include <idna.h> #include <idn-free.h> -static int Lidna_to_ascii(lua_State *L) /** idna.to_ascii(s) */ -{ +static int Lidna_to_ascii(lua_State* L) { /** idna.to_ascii(s) */ size_t len; - const char *s = check_utf8(L, 1, &len); - if (s == NULL || len != strlen(s)) { + const char* s = check_utf8(L, 1, &len); + char* output = NULL; + int ret; + + if(s == NULL || len != strlen(s)) { lua_pushnil(L); return 1; /* TODO return error message */ } - char* output = NULL; - int ret = idna_to_ascii_8z(s, &output, IDNA_USE_STD3_ASCII_RULES); - if (ret == IDNA_SUCCESS) { + + ret = idna_to_ascii_8z(s, &output, IDNA_USE_STD3_ASCII_RULES); + + if(ret == IDNA_SUCCESS) { lua_pushstring(L, output); idn_free(output); return 1; @@ -421,13 +497,13 @@ static int Lidna_to_ascii(lua_State *L) /** idna.to_ascii(s) */ } } -static int Lidna_to_unicode(lua_State *L) /** idna.to_unicode(s) */ -{ +static int Lidna_to_unicode(lua_State* L) { /** idna.to_unicode(s) */ size_t len; - const char *s = luaL_checklstring(L, 1, &len); + const char* s = luaL_checklstring(L, 1, &len); char* output = NULL; int ret = idna_to_unicode_8z8z(s, &output, 0); - if (ret == IDNA_SUCCESS) { + + if(ret == IDNA_SUCCESS) { lua_pushstring(L, output); idn_free(output); return 1; @@ -439,8 +515,7 @@ static int Lidna_to_unicode(lua_State *L) /** idna.to_unicode(s) */ } #endif -static const luaL_Reg Reg_idna[] = -{ +static const luaL_Reg Reg_idna[] = { { "to_ascii", Lidna_to_ascii }, { "to_unicode", Lidna_to_unicode }, { NULL, NULL } @@ -448,40 +523,29 @@ static const luaL_Reg Reg_idna[] = /***************** end *****************/ -static const luaL_Reg Reg[] = -{ - { NULL, NULL } -}; - -LUALIB_API int luaopen_util_encodings(lua_State *L) -{ +LUALIB_API int luaopen_util_encodings(lua_State* L) { #ifdef USE_STRINGPREP_ICU init_icu(); #endif - luaL_register(L, "encodings", Reg); + lua_newtable(L); - lua_pushliteral(L, "base64"); lua_newtable(L); - luaL_register(L, NULL, Reg_base64); - lua_settable(L,-3); + luaL_setfuncs(L, Reg_base64, 0); + lua_setfield(L, -2, "base64"); - lua_pushliteral(L, "stringprep"); lua_newtable(L); - luaL_register(L, NULL, Reg_stringprep); - lua_settable(L,-3); + luaL_setfuncs(L, Reg_stringprep, 0); + lua_setfield(L, -2, "stringprep"); - lua_pushliteral(L, "idna"); lua_newtable(L); - luaL_register(L, NULL, Reg_idna); - lua_settable(L,-3); + luaL_setfuncs(L, Reg_idna, 0); + lua_setfield(L, -2, "idna"); - lua_pushliteral(L, "utf8"); lua_newtable(L); - luaL_register(L, NULL, Reg_utf8); - lua_settable(L, -3); + luaL_setfuncs(L, Reg_utf8, 0); + lua_setfield(L, -2, "utf8"); - lua_pushliteral(L, "version"); /** version */ lua_pushliteral(L, "-3.14"); - lua_settable(L,-3); + lua_setfield(L, -2, "version"); return 1; } diff --git a/util-src/hashes.c b/util-src/hashes.c index 33041e83..ecab2e32 100644 --- a/util-src/hashes.c +++ b/util-src/hashes.c @@ -1,7 +1,7 @@ /* Prosody IM -- Copyright (C) 2009-2010 Matthew Wild -- Copyright (C) 2009-2010 Waqas Hussain --- +-- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- @@ -27,15 +27,20 @@ typedef unsigned __int32 uint32_t; #include <openssl/sha.h> #include <openssl/md5.h> +#if (LUA_VERSION_NUM == 501) +#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) +#endif + #define HMAC_IPAD 0x36363636 #define HMAC_OPAD 0x5c5c5c5c -const char *hex_tab = "0123456789abcdef"; -void toHex(const unsigned char *in, int length, unsigned char *out) { +const char* hex_tab = "0123456789abcdef"; +void toHex(const unsigned char* in, int length, unsigned char* out) { int i; - for (i = 0; i < length; i++) { - out[i*2] = hex_tab[(in[i] >> 4) & 0xF]; - out[i*2+1] = hex_tab[(in[i]) & 0xF]; + + for(i = 0; i < length; i++) { + out[i * 2] = hex_tab[(in[i] >> 4) & 0xF]; + out[i * 2 + 1] = hex_tab[(in[i]) & 0xF]; } } @@ -64,15 +69,14 @@ MAKE_HASH_FUNCTION(Lmd5, MD5, MD5_DIGEST_LENGTH) struct hash_desc { int (*Init)(void*); - int (*Update)(void*, const void *, size_t); + int (*Update)(void*, const void*, size_t); int (*Final)(unsigned char*, void*); size_t digestLength; - void *ctx, *ctxo; + void* ctx, *ctxo; }; -static void hmac(struct hash_desc *desc, const char *key, size_t key_len, - const char *msg, size_t msg_len, unsigned char *result) -{ +static void hmac(struct hash_desc* desc, const char* key, size_t key_len, + const char* msg, size_t msg_len, unsigned char* result) { union xory { unsigned char bytes[64]; uint32_t quadbytes[16]; @@ -82,7 +86,7 @@ static void hmac(struct hash_desc *desc, const char *key, size_t key_len, unsigned char hashedKey[64]; /* Maximum used digest length */ union xory k_ipad, k_opad; - if (key_len > 64) { + if(key_len > 64) { desc->Init(desc->ctx); desc->Update(desc->ctx, key, key_len); desc->Final(hashedKey, desc->ctx); @@ -94,7 +98,7 @@ static void hmac(struct hash_desc *desc, const char *key, size_t key_len, memset(k_ipad.bytes + key_len, 0, 64 - key_len); memcpy(k_opad.bytes, k_ipad.bytes, 64); - for (i = 0; i < 16; i++) { + for(i = 0; i < 16; i++) { k_ipad.quadbytes[i] ^= HMAC_IPAD; k_opad.quadbytes[i] ^= HMAC_OPAD; } @@ -139,10 +143,10 @@ MAKE_HMAC_FUNCTION(Lhmac_sha256, SHA256, SHA256_DIGEST_LENGTH, SHA256_CTX) MAKE_HMAC_FUNCTION(Lhmac_sha512, SHA512, SHA512_DIGEST_LENGTH, SHA512_CTX) MAKE_HMAC_FUNCTION(Lhmac_md5, MD5, MD5_DIGEST_LENGTH, MD5_CTX) -static int LscramHi(lua_State *L) { +static int LscramHi(lua_State* L) { union xory { unsigned char bytes[SHA_DIGEST_LENGTH]; - uint32_t quadbytes[SHA_DIGEST_LENGTH/4]; + uint32_t quadbytes[SHA_DIGEST_LENGTH / 4]; }; int i; SHA_CTX ctx, ctxo; @@ -151,32 +155,39 @@ static int LscramHi(lua_State *L) { union xory res; size_t str_len, salt_len; struct hash_desc desc; - const char *str = luaL_checklstring(L, 1, &str_len); - const char *salt = luaL_checklstring(L, 2, &salt_len); - char *salt2; + const char* str = luaL_checklstring(L, 1, &str_len); + const char* salt = luaL_checklstring(L, 2, &salt_len); + char* salt2; const int iter = luaL_checkinteger(L, 3); desc.Init = (int (*)(void*))SHA1_Init; - desc.Update = (int (*)(void*, const void *, size_t))SHA1_Update; + desc.Update = (int (*)(void*, const void*, size_t))SHA1_Update; desc.Final = (int (*)(unsigned char*, void*))SHA1_Final; desc.digestLength = SHA_DIGEST_LENGTH; desc.ctx = &ctx; desc.ctxo = &ctxo; salt2 = malloc(salt_len + 4); - if (salt2 == NULL) - luaL_error(L, "Out of memory in scramHi"); + + if(salt2 == NULL) { + return luaL_error(L, "Out of memory in scramHi"); + } + memcpy(salt2, salt, salt_len); memcpy(salt2 + salt_len, "\0\0\0\1", 4); hmac(&desc, str, str_len, salt2, salt_len + 4, Ust); free(salt2); memcpy(res.bytes, Ust, sizeof(res)); - for (i = 1; i < iter; i++) { + + for(i = 1; i < iter; i++) { int j; hmac(&desc, str, str_len, (char*)Ust, sizeof(Ust), Und.bytes); - for (j = 0; j < SHA_DIGEST_LENGTH/4; j++) + + for(j = 0; j < SHA_DIGEST_LENGTH / 4; j++) { res.quadbytes[j] ^= Und.quadbytes[j]; + } + memcpy(Ust, Und.bytes, sizeof(Ust)); } @@ -185,8 +196,7 @@ static int LscramHi(lua_State *L) { return 1; } -static const luaL_Reg Reg[] = -{ +static const luaL_Reg Reg[] = { { "sha1", Lsha1 }, { "sha224", Lsha224 }, { "sha256", Lsha256 }, @@ -201,11 +211,10 @@ static const luaL_Reg Reg[] = { NULL, NULL } }; -LUALIB_API int luaopen_util_hashes(lua_State *L) -{ - luaL_register(L, "hashes", Reg); - lua_pushliteral(L, "version"); /** version */ +LUALIB_API int luaopen_util_hashes(lua_State* L) { + lua_newtable(L); + luaL_setfuncs(L, Reg, 0);; lua_pushliteral(L, "-3.14"); - lua_settable(L,-3); + lua_setfield(L, -2, "version"); return 1; } diff --git a/util-src/net.c b/util-src/net.c index e307c628..3ccc7618 100644 --- a/util-src/net.c +++ b/util-src/net.c @@ -14,34 +14,37 @@ #include <errno.h> #ifndef _WIN32 - #include <sys/ioctl.h> - #include <sys/types.h> - #include <sys/socket.h> - #include <net/if.h> - #include <ifaddrs.h> - #include <arpa/inet.h> - #include <netinet/in.h> +#include <sys/ioctl.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <net/if.h> +#include <ifaddrs.h> +#include <arpa/inet.h> +#include <netinet/in.h> #endif #include <lua.h> #include <lauxlib.h> +#if (LUA_VERSION_NUM == 501) +#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) +#endif + /* Enumerate all locally configured IP addresses */ -const char * const type_strings[] = { +const char* const type_strings[] = { "both", "ipv4", "ipv6", NULL }; -static int lc_local_addresses(lua_State *L) -{ +static int lc_local_addresses(lua_State* L) { #ifndef _WIN32 /* Link-local IPv4 addresses; see RFC 3927 and RFC 5735 */ const long ip4_linklocal = htonl(0xa9fe0000); /* 169.254.0.0 */ const long ip4_mask = htonl(0xffff0000); - struct ifaddrs *addr = NULL, *a; + struct ifaddrs* addr = NULL, *a; #endif int n = 1; int type = luaL_checkoption(L, 1, "both", type_strings); @@ -50,68 +53,84 @@ static int lc_local_addresses(lua_State *L) const char ipv6 = (type == 0 || type == 2); #ifndef _WIN32 - if (getifaddrs(&addr) < 0) { + + if(getifaddrs(&addr) < 0) { lua_pushnil(L); lua_pushfstring(L, "getifaddrs failed (%d): %s", errno, - strerror(errno)); + strerror(errno)); return 2; } + #endif lua_newtable(L); #ifndef _WIN32 - for (a = addr; a; a = a->ifa_next) { + + for(a = addr; a; a = a->ifa_next) { int family; char ipaddr[INET6_ADDRSTRLEN]; - const char *tmp = NULL; + const char* tmp = NULL; - if (a->ifa_addr == NULL || a->ifa_flags & IFF_LOOPBACK) + if(a->ifa_addr == NULL || a->ifa_flags & IFF_LOOPBACK) { continue; + } family = a->ifa_addr->sa_family; - if (ipv4 && family == AF_INET) { - struct sockaddr_in *sa = (struct sockaddr_in *)a->ifa_addr; - if (!link_local &&((sa->sin_addr.s_addr & ip4_mask) == ip4_linklocal)) + if(ipv4 && family == AF_INET) { + struct sockaddr_in* sa = (struct sockaddr_in*)a->ifa_addr; + + if(!link_local && ((sa->sin_addr.s_addr & ip4_mask) == ip4_linklocal)) { continue; + } + tmp = inet_ntop(family, &sa->sin_addr, ipaddr, sizeof(ipaddr)); - } else if (ipv6 && family == AF_INET6) { - struct sockaddr_in6 *sa = (struct sockaddr_in6 *)a->ifa_addr; - if (!link_local && IN6_IS_ADDR_LINKLOCAL(&sa->sin6_addr)) + } else if(ipv6 && family == AF_INET6) { + struct sockaddr_in6* sa = (struct sockaddr_in6*)a->ifa_addr; + + if(!link_local && IN6_IS_ADDR_LINKLOCAL(&sa->sin6_addr)) { continue; - if (IN6_IS_ADDR_V4MAPPED(&sa->sin6_addr) || IN6_IS_ADDR_V4COMPAT(&sa->sin6_addr)) + } + + if(IN6_IS_ADDR_V4MAPPED(&sa->sin6_addr) || IN6_IS_ADDR_V4COMPAT(&sa->sin6_addr)) { continue; + } + tmp = inet_ntop(family, &sa->sin6_addr, ipaddr, sizeof(ipaddr)); } - if (tmp != NULL) { + if(tmp != NULL) { lua_pushstring(L, tmp); lua_rawseti(L, -2, n++); } + /* TODO: Error reporting? */ } freeifaddrs(addr); #else - if (ipv4) { + + if(ipv4) { lua_pushstring(L, "0.0.0.0"); lua_rawseti(L, -2, n++); } - if (ipv6) { + + if(ipv6) { lua_pushstring(L, "::"); lua_rawseti(L, -2, n++); } + #endif return 1; } -int luaopen_util_net(lua_State* L) -{ +int luaopen_util_net(lua_State* L) { luaL_Reg exports[] = { { "local_addresses", lc_local_addresses }, { NULL, NULL } }; - luaL_register(L, "net", exports); + lua_newtable(L); + luaL_setfuncs(L, exports, 0); return 1; } diff --git a/util-src/pposix.c b/util-src/pposix.c index df814c28..1b69852d 100644 --- a/util-src/pposix.c +++ b/util-src/pposix.c @@ -35,40 +35,39 @@ #include "lualib.h" #include "lauxlib.h" +#if (LUA_VERSION_NUM == 501) +#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) +#endif + #include <fcntl.h> #if defined(__linux__) && defined(_GNU_SOURCE) #include <linux/falloc.h> #endif #if (defined(_SVID_SOURCE) && !defined(WITHOUT_MALLINFO)) - #include <malloc.h> - #define WITH_MALLINFO +#include <malloc.h> +#define WITH_MALLINFO #endif /* Daemonization support */ -static int lc_daemonize(lua_State *L) -{ +static int lc_daemonize(lua_State* L) { pid_t pid; - if ( getppid() == 1 ) - { + if(getppid() == 1) { lua_pushboolean(L, 0); lua_pushstring(L, "already-daemonized"); return 2; } /* Attempt initial fork */ - if((pid = fork()) < 0) - { + if((pid = fork()) < 0) { /* Forking failed */ lua_pushboolean(L, 0); lua_pushstring(L, "fork-failed"); return 2; - } - else if(pid != 0) - { + } else if(pid != 0) { /* We are the parent process */ lua_pushboolean(L, 1); lua_pushnumber(L, pid); @@ -76,8 +75,7 @@ static int lc_daemonize(lua_State *L) } /* and we are the child process */ - if(setsid() == -1) - { + if(setsid() == -1) { /* We failed to become session leader */ /* (we probably already were) */ lua_pushboolean(L, 0); @@ -95,8 +93,9 @@ static int lc_daemonize(lua_State *L) open("/dev/null", O_WRONLY); /* Final fork, use it wisely */ - if(fork()) + if(fork()) { exit(0); + } /* Show's over, let's continue */ lua_pushboolean(L, 1); @@ -106,59 +105,59 @@ static int lc_daemonize(lua_State *L) /* Syslog support */ -const char * const facility_strings[] = { - "auth", +const char* const facility_strings[] = { + "auth", #if !(defined(sun) || defined(__sun)) - "authpriv", + "authpriv", #endif - "cron", - "daemon", + "cron", + "daemon", #if !(defined(sun) || defined(__sun)) - "ftp", + "ftp", #endif - "kern", - "local0", - "local1", - "local2", - "local3", - "local4", - "local5", - "local6", - "local7", - "lpr", - "mail", - "syslog", - "user", - "uucp", - NULL - }; + "kern", + "local0", + "local1", + "local2", + "local3", + "local4", + "local5", + "local6", + "local7", + "lpr", + "mail", + "syslog", + "user", + "uucp", + NULL +}; int facility_constants[] = { - LOG_AUTH, + LOG_AUTH, #if !(defined(sun) || defined(__sun)) - LOG_AUTHPRIV, + LOG_AUTHPRIV, #endif - LOG_CRON, - LOG_DAEMON, + LOG_CRON, + LOG_DAEMON, #if !(defined(sun) || defined(__sun)) - LOG_FTP, + LOG_FTP, #endif - LOG_KERN, - LOG_LOCAL0, - LOG_LOCAL1, - LOG_LOCAL2, - LOG_LOCAL3, - LOG_LOCAL4, - LOG_LOCAL5, - LOG_LOCAL6, - LOG_LOCAL7, - LOG_LPR, - LOG_MAIL, - LOG_NEWS, - LOG_SYSLOG, - LOG_USER, - LOG_UUCP, - -1 - }; + LOG_KERN, + LOG_LOCAL0, + LOG_LOCAL1, + LOG_LOCAL2, + LOG_LOCAL3, + LOG_LOCAL4, + LOG_LOCAL5, + LOG_LOCAL6, + LOG_LOCAL7, + LOG_LPR, + LOG_MAIL, + LOG_NEWS, + LOG_SYSLOG, + LOG_USER, + LOG_UUCP, + -1 +}; /* " The parameter ident in the call of openlog() is probably stored as-is. @@ -170,15 +169,15 @@ int facility_constants[] = { */ char* syslog_ident = NULL; -int lc_syslog_open(lua_State* L) -{ +int lc_syslog_open(lua_State* L) { int facility = luaL_checkoption(L, 2, "daemon", facility_strings); facility = facility_constants[facility]; luaL_checkstring(L, 1); - if(syslog_ident) + if(syslog_ident) { free(syslog_ident); + } syslog_ident = strdup(lua_tostring(L, 1)); @@ -186,53 +185,52 @@ int lc_syslog_open(lua_State* L) return 0; } -const char * const level_strings[] = { - "debug", - "info", - "notice", - "warn", - "error", - NULL - }; +const char* const level_strings[] = { + "debug", + "info", + "notice", + "warn", + "error", + NULL +}; int level_constants[] = { - LOG_DEBUG, - LOG_INFO, - LOG_NOTICE, - LOG_WARNING, - LOG_CRIT, - -1 - }; -int lc_syslog_log(lua_State* L) -{ + LOG_DEBUG, + LOG_INFO, + LOG_NOTICE, + LOG_WARNING, + LOG_CRIT, + -1 +}; +int lc_syslog_log(lua_State* L) { int level = level_constants[luaL_checkoption(L, 1, "notice", level_strings)]; - if(lua_gettop(L) == 3) + if(lua_gettop(L) == 3) { syslog(level, "%s: %s", luaL_checkstring(L, 2), luaL_checkstring(L, 3)); - else + } else { syslog(level, "%s", lua_tostring(L, 2)); + } return 0; } -int lc_syslog_close(lua_State* L) -{ +int lc_syslog_close(lua_State* L) { closelog(); - if(syslog_ident) - { + + if(syslog_ident) { free(syslog_ident); syslog_ident = NULL; } + return 0; } -int lc_syslog_setmask(lua_State* L) -{ +int lc_syslog_setmask(lua_State* L) { int level_idx = luaL_checkoption(L, 1, "notice", level_strings); int mask = 0; - do - { + + do { mask |= LOG_MASK(level_constants[level_idx]); - } while (++level_idx<=4); + } while(++level_idx <= 4); setlogmask(mask); return 0; @@ -240,72 +238,67 @@ int lc_syslog_setmask(lua_State* L) /* getpid */ -int lc_getpid(lua_State* L) -{ +int lc_getpid(lua_State* L) { lua_pushinteger(L, getpid()); return 1; } /* UID/GID functions */ -int lc_getuid(lua_State* L) -{ +int lc_getuid(lua_State* L) { lua_pushinteger(L, getuid()); return 1; } -int lc_getgid(lua_State* L) -{ +int lc_getgid(lua_State* L) { lua_pushinteger(L, getgid()); return 1; } -int lc_setuid(lua_State* L) -{ +int lc_setuid(lua_State* L) { int uid = -1; - if(lua_gettop(L) < 1) + + if(lua_gettop(L) < 1) { return 0; - if(!lua_isnumber(L, 1) && lua_tostring(L, 1)) - { + } + + if(!lua_isnumber(L, 1) && lua_tostring(L, 1)) { /* Passed UID is actually a string, so look up the UID */ - struct passwd *p; + struct passwd* p; p = getpwnam(lua_tostring(L, 1)); - if(!p) - { + + if(!p) { lua_pushboolean(L, 0); lua_pushstring(L, "no-such-user"); return 2; } + uid = p->pw_uid; - } - else - { + } else { uid = lua_tonumber(L, 1); } - if(uid>-1) - { + if(uid > -1) { /* Ok, attempt setuid */ errno = 0; - if(setuid(uid)) - { + + if(setuid(uid)) { /* Fail */ lua_pushboolean(L, 0); - switch(errno) - { - case EINVAL: - lua_pushstring(L, "invalid-uid"); - break; - case EPERM: - lua_pushstring(L, "permission-denied"); - break; - default: - lua_pushstring(L, "unknown-error"); + + switch(errno) { + case EINVAL: + lua_pushstring(L, "invalid-uid"); + break; + case EPERM: + lua_pushstring(L, "permission-denied"); + break; + default: + lua_pushstring(L, "unknown-error"); } + return 2; - } - else - { + } else { /* Success! */ lua_pushboolean(L, 1); return 1; @@ -318,52 +311,50 @@ int lc_setuid(lua_State* L) return 2; } -int lc_setgid(lua_State* L) -{ +int lc_setgid(lua_State* L) { int gid = -1; - if(lua_gettop(L) < 1) + + if(lua_gettop(L) < 1) { return 0; - if(!lua_isnumber(L, 1) && lua_tostring(L, 1)) - { + } + + if(!lua_isnumber(L, 1) && lua_tostring(L, 1)) { /* Passed GID is actually a string, so look up the GID */ - struct group *g; + struct group* g; g = getgrnam(lua_tostring(L, 1)); - if(!g) - { + + if(!g) { lua_pushboolean(L, 0); lua_pushstring(L, "no-such-group"); return 2; } + gid = g->gr_gid; - } - else - { + } else { gid = lua_tonumber(L, 1); } - if(gid>-1) - { + if(gid > -1) { /* Ok, attempt setgid */ errno = 0; - if(setgid(gid)) - { + + if(setgid(gid)) { /* Fail */ lua_pushboolean(L, 0); - switch(errno) - { - case EINVAL: - lua_pushstring(L, "invalid-gid"); - break; - case EPERM: - lua_pushstring(L, "permission-denied"); - break; - default: - lua_pushstring(L, "unknown-error"); + + switch(errno) { + case EINVAL: + lua_pushstring(L, "invalid-gid"); + break; + case EPERM: + lua_pushstring(L, "permission-denied"); + break; + default: + lua_pushstring(L, "unknown-error"); } + return 2; - } - else - { + } else { /* Success! */ lua_pushboolean(L, 1); return 1; @@ -376,90 +367,89 @@ int lc_setgid(lua_State* L) return 2; } -int lc_initgroups(lua_State* L) -{ +int lc_initgroups(lua_State* L) { int ret; gid_t gid; - struct passwd *p; + struct passwd* p; - if(!lua_isstring(L, 1)) - { + if(!lua_isstring(L, 1)) { lua_pushnil(L); lua_pushstring(L, "invalid-username"); return 2; } + p = getpwnam(lua_tostring(L, 1)); - if(!p) - { + + if(!p) { lua_pushnil(L); lua_pushstring(L, "no-such-user"); return 2; } - if(lua_gettop(L) < 2) - lua_pushnil(L); - switch(lua_type(L, 2)) - { - case LUA_TNIL: - gid = p->pw_gid; - break; - case LUA_TNUMBER: - gid = lua_tointeger(L, 2); - break; - default: + + if(lua_gettop(L) < 2) { lua_pushnil(L); - lua_pushstring(L, "invalid-gid"); - return 2; } - ret = initgroups(lua_tostring(L, 1), gid); - if(ret) - { - switch(errno) - { - case ENOMEM: - lua_pushnil(L); - lua_pushstring(L, "no-memory"); + + switch(lua_type(L, 2)) { + case LUA_TNIL: + gid = p->pw_gid; break; - case EPERM: - lua_pushnil(L); - lua_pushstring(L, "permission-denied"); + case LUA_TNUMBER: + gid = lua_tointeger(L, 2); break; default: lua_pushnil(L); - lua_pushstring(L, "unknown-error"); - } + lua_pushstring(L, "invalid-gid"); + return 2; } - else - { + + ret = initgroups(lua_tostring(L, 1), gid); + + if(ret) { + switch(errno) { + case ENOMEM: + lua_pushnil(L); + lua_pushstring(L, "no-memory"); + break; + case EPERM: + lua_pushnil(L); + lua_pushstring(L, "permission-denied"); + break; + default: + lua_pushnil(L); + lua_pushstring(L, "unknown-error"); + } + } else { lua_pushboolean(L, 1); lua_pushnil(L); } + return 2; } -int lc_umask(lua_State* L) -{ +int lc_umask(lua_State* L) { char old_mode_string[7]; mode_t old_mode = umask(strtoul(luaL_checkstring(L, 1), NULL, 8)); snprintf(old_mode_string, sizeof(old_mode_string), "%03o", old_mode); - old_mode_string[sizeof(old_mode_string)-1] = 0; + old_mode_string[sizeof(old_mode_string) - 1] = 0; lua_pushstring(L, old_mode_string); return 1; } -int lc_mkdir(lua_State* L) -{ +int lc_mkdir(lua_State* L) { int ret = mkdir(luaL_checkstring(L, 1), S_IRUSR | S_IWUSR | S_IXUSR - | S_IRGRP | S_IWGRP | S_IXGRP - | S_IROTH | S_IXOTH); /* mode 775 */ + | S_IRGRP | S_IWGRP | S_IXGRP + | S_IROTH | S_IXOTH); /* mode 775 */ - lua_pushboolean(L, ret==0); - if(ret) - { + lua_pushboolean(L, ret == 0); + + if(ret) { lua_pushstring(L, strerror(errno)); return 2; } + return 1; } @@ -473,89 +463,132 @@ int lc_mkdir(lua_State* L) * Example usage: * pposix.setrlimit("NOFILE", 1000, 2000) */ -int string2resource(const char *s) { - if (!strcmp(s, "CORE")) return RLIMIT_CORE; - if (!strcmp(s, "CPU")) return RLIMIT_CPU; - if (!strcmp(s, "DATA")) return RLIMIT_DATA; - if (!strcmp(s, "FSIZE")) return RLIMIT_FSIZE; - if (!strcmp(s, "NOFILE")) return RLIMIT_NOFILE; - if (!strcmp(s, "STACK")) return RLIMIT_STACK; +int string2resource(const char* s) { + if(!strcmp(s, "CORE")) { + return RLIMIT_CORE; + } + + if(!strcmp(s, "CPU")) { + return RLIMIT_CPU; + } + + if(!strcmp(s, "DATA")) { + return RLIMIT_DATA; + } + + if(!strcmp(s, "FSIZE")) { + return RLIMIT_FSIZE; + } + + if(!strcmp(s, "NOFILE")) { + return RLIMIT_NOFILE; + } + + if(!strcmp(s, "STACK")) { + return RLIMIT_STACK; + } + #if !(defined(sun) || defined(__sun)) - if (!strcmp(s, "MEMLOCK")) return RLIMIT_MEMLOCK; - if (!strcmp(s, "NPROC")) return RLIMIT_NPROC; - if (!strcmp(s, "RSS")) return RLIMIT_RSS; + + if(!strcmp(s, "MEMLOCK")) { + return RLIMIT_MEMLOCK; + } + + if(!strcmp(s, "NPROC")) { + return RLIMIT_NPROC; + } + + if(!strcmp(s, "RSS")) { + return RLIMIT_RSS; + } + #endif #ifdef RLIMIT_NICE - if (!strcmp(s, "NICE")) return RLIMIT_NICE; + + if(!strcmp(s, "NICE")) { + return RLIMIT_NICE; + } + #endif return -1; } -int lc_setrlimit(lua_State *L) { +unsigned long int arg_to_rlimit(lua_State* L, int idx, rlim_t current) { + switch(lua_type(L, idx)) { + case LUA_TSTRING: + + if(strcmp(lua_tostring(L, idx), "unlimited") == 0) { + return RLIM_INFINITY; + } + + case LUA_TNUMBER: + return lua_tointeger(L, idx); + case LUA_TNONE: + case LUA_TNIL: + return current; + default: + return luaL_argerror(L, idx, "unexpected type"); + } +} + +int lc_setrlimit(lua_State* L) { + struct rlimit lim; int arguments = lua_gettop(L); - int softlimit = -1; - int hardlimit = -1; - const char *resource = NULL; int rid = -1; + if(arguments < 1 || arguments > 3) { lua_pushboolean(L, 0); lua_pushstring(L, "incorrect-arguments"); return 2; } - resource = luaL_checkstring(L, 1); - softlimit = luaL_checkinteger(L, 2); - hardlimit = luaL_checkinteger(L, 3); + rid = string2resource(luaL_checkstring(L, 1)); - rid = string2resource(resource); - if (rid != -1) { - struct rlimit lim; - struct rlimit lim_current; - - if (softlimit < 0 || hardlimit < 0) { - if (getrlimit(rid, &lim_current)) { - lua_pushboolean(L, 0); - lua_pushstring(L, "getrlimit-failed"); - return 2; - } - } + if(rid == -1) { + lua_pushboolean(L, 0); + lua_pushstring(L, "invalid-resource"); + return 2; + } - if (softlimit < 0) lim.rlim_cur = lim_current.rlim_cur; - else lim.rlim_cur = softlimit; - if (hardlimit < 0) lim.rlim_max = lim_current.rlim_max; - else lim.rlim_max = hardlimit; + /* Fetch current values to use as defaults */ + if(getrlimit(rid, &lim)) { + lua_pushboolean(L, 0); + lua_pushstring(L, "getrlimit-failed"); + return 2; + } - if (setrlimit(rid, &lim)) { - lua_pushboolean(L, 0); - lua_pushstring(L, "setrlimit-failed"); - return 2; - } - } else { - /* Unsupported resoucrce. Sorry I'm pretty limited by POSIX standard. */ + lim.rlim_cur = arg_to_rlimit(L, 2, lim.rlim_cur); + lim.rlim_max = arg_to_rlimit(L, 3, lim.rlim_max); + + if(setrlimit(rid, &lim)) { lua_pushboolean(L, 0); - lua_pushstring(L, "invalid-resource"); + lua_pushstring(L, "setrlimit-failed"); return 2; } + lua_pushboolean(L, 1); return 1; } -int lc_getrlimit(lua_State *L) { +int lc_getrlimit(lua_State* L) { int arguments = lua_gettop(L); - const char *resource = NULL; + const char* resource = NULL; int rid = -1; struct rlimit lim; - if (arguments != 1) { + if(arguments != 1) { lua_pushboolean(L, 0); lua_pushstring(L, "invalid-arguments"); return 2; } + + resource = luaL_checkstring(L, 1); rid = string2resource(resource); - if (rid != -1) { - if (getrlimit(rid, &lim)) { + + if(rid != -1) { + if(getrlimit(rid, &lim)) { lua_pushboolean(L, 0); lua_pushstring(L, "getrlimit-failed."); return 2; @@ -566,27 +599,38 @@ int lc_getrlimit(lua_State *L) { lua_pushstring(L, "invalid-resource"); return 2; } + lua_pushboolean(L, 1); - lua_pushnumber(L, lim.rlim_cur); - lua_pushnumber(L, lim.rlim_max); + + if(lim.rlim_cur == RLIM_INFINITY) { + lua_pushstring(L, "unlimited"); + } else { + lua_pushnumber(L, lim.rlim_cur); + } + + if(lim.rlim_max == RLIM_INFINITY) { + lua_pushstring(L, "unlimited"); + } else { + lua_pushnumber(L, lim.rlim_max); + } + return 3; } -int lc_abort(lua_State* L) -{ +int lc_abort(lua_State* L) { abort(); return 0; } -int lc_uname(lua_State* L) -{ +int lc_uname(lua_State* L) { struct utsname uname_info; - if(uname(&uname_info) != 0) - { + + if(uname(&uname_info) != 0) { lua_pushnil(L); lua_pushstring(L, strerror(errno)); return 2; } + lua_newtable(L); lua_pushstring(L, uname_info.sysname); lua_setfield(L, -2, "sysname"); @@ -598,31 +642,32 @@ int lc_uname(lua_State* L) lua_setfield(L, -2, "version"); lua_pushstring(L, uname_info.machine); lua_setfield(L, -2, "machine"); +#ifdef _GNU_SOURCE + lua_pushstring(L, uname_info.domainname); + lua_setfield(L, -2, "domainname"); +#endif return 1; } -int lc_setenv(lua_State* L) -{ - const char *var = luaL_checkstring(L, 1); - const char *value; +int lc_setenv(lua_State* L) { + const char* var = luaL_checkstring(L, 1); + const char* value; /* If the second argument is nil or nothing, unset the var */ - if(lua_isnoneornil(L, 2)) - { - if(unsetenv(var) != 0) - { + if(lua_isnoneornil(L, 2)) { + if(unsetenv(var) != 0) { lua_pushnil(L); lua_pushstring(L, strerror(errno)); return 2; } + lua_pushboolean(L, 1); return 1; } value = luaL_checkstring(L, 2); - if(setenv(var, value, 1) != 0) - { + if(setenv(var, value, 1) != 0) { lua_pushnil(L); lua_pushstring(L, strerror(errno)); return 2; @@ -633,8 +678,7 @@ int lc_setenv(lua_State* L) } #ifdef WITH_MALLINFO -int lc_meminfo(lua_State* L) -{ +int lc_meminfo(lua_State* L) { struct mallinfo info = mallinfo(); lua_newtable(L); /* This is the total size of memory allocated with sbrk by malloc, in bytes. */ @@ -662,13 +706,14 @@ int lc_meminfo(lua_State* L) * */ #if _XOPEN_SOURCE >= 600 || _POSIX_C_SOURCE >= 200112L || defined(_GNU_SOURCE) -int lc_fallocate(lua_State* L) -{ +int lc_fallocate(lua_State* L) { int ret; off_t offset, len; - FILE *f = *(FILE**) luaL_checkudata(L, 1, LUA_FILEHANDLE); - if (f == NULL) - luaL_error(L, "attempt to use a closed file"); + FILE* f = *(FILE**) luaL_checkudata(L, 1, LUA_FILEHANDLE); + + if(f == NULL) { + return luaL_error(L, "attempt to use a closed file"); + } offset = luaL_checkinteger(L, 2); len = luaL_checkinteger(L, 3); @@ -676,20 +721,23 @@ int lc_fallocate(lua_State* L) #if defined(__linux__) && defined(_GNU_SOURCE) errno = 0; ret = fallocate(fileno(f), FALLOC_FL_KEEP_SIZE, offset, len); - if(ret == 0) - { + + if(ret == 0) { lua_pushboolean(L, 1); return 1; } + /* Some old versions of Linux apparently use the return value instead of errno */ - if(errno == 0) errno = ret; + if(errno == 0) { + errno = ret; + } - if(errno != ENOSYS && errno != EOPNOTSUPP) - { + if(errno != ENOSYS && errno != EOPNOTSUPP) { lua_pushnil(L); lua_pushstring(L, strerror(errno)); return 2; } + #else #warning Only using posix_fallocate() fallback. #warning Linux fallocate() is strongly recommended if available: recompile with -D_GNU_SOURCE @@ -697,18 +745,19 @@ int lc_fallocate(lua_State* L) #endif ret = posix_fallocate(fileno(f), offset, len); - if(ret == 0) - { + + if(ret == 0) { lua_pushboolean(L, 1); return 1; - } - else - { + } else { lua_pushnil(L); lua_pushstring(L, strerror(ret)); /* posix_fallocate() can leave a bunch of NULs at the end, so we cut that * this assumes that offset == length of the file */ - ftruncate(fileno(f), offset); + if(ftruncate(fileno(f), offset) != 0) { + lua_pushstring(L, strerror(errno)); + return 3; + } return 2; } } @@ -716,8 +765,7 @@ int lc_fallocate(lua_State* L) /* Register functions */ -int luaopen_util_pposix(lua_State *L) -{ +int luaopen_util_pposix(lua_State* L) { luaL_Reg exports[] = { { "abort", lc_abort }, @@ -758,7 +806,8 @@ int luaopen_util_pposix(lua_State *L) { NULL, NULL } }; - luaL_register(L, "pposix", exports); + lua_newtable(L); + luaL_setfuncs(L, exports, 0); lua_pushliteral(L, "pposix"); lua_setfield(L, -2, "_NAME"); diff --git a/util-src/ringbuffer.c b/util-src/ringbuffer.c new file mode 100644 index 00000000..f5fa136b --- /dev/null +++ b/util-src/ringbuffer.c @@ -0,0 +1,232 @@ + + +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <stdio.h> + +#include <lua.h> +#include <lauxlib.h> + +#define MIN(a, b) ((a)>(b)?(b):(a)) +#define MAX(a, b) ((a)>(b)?(a):(b)) + +typedef struct { + size_t rpos; /* read position */ + size_t wpos; /* write position */ + size_t alen; /* allocated size */ + size_t blen; /* current content size */ + char* buffer; +} ringbuffer; + +char readchar(ringbuffer* b) { + b->blen--; + return b->buffer[(b->rpos++) % b->alen]; +} + +void writechar(ringbuffer* b, char c) { + b->blen++; + b->buffer[(b->wpos++) % b->alen] = c; +} + +/* make sure position counters stay within the allocation */ +void modpos(ringbuffer* b) { + b->rpos = b->rpos % b->alen; + b->wpos = b->wpos % b->alen; +} + +int find(ringbuffer* b, const char* s, int l) { + size_t i, j; + int m; + + if(b->rpos == b->wpos) { /* empty */ + return 0; + } + + for(i = 0; i <= b->blen - l; i++) { + if(b->buffer[(b->rpos + i) % b->alen] == *s) { + m = 1; + + for(j = 1; j < l; j++) + if(b->buffer[(b->rpos + i + j) % b->alen] != s[j]) { + m = 0; + break; + } + + if(m) { + return i + l; + } + } + } + + return 0; +} + +int rb_find(lua_State* L) { + size_t l, m; + ringbuffer* b = luaL_checkudata(L, 1, "ringbuffer_mt"); + const char* s = luaL_checklstring(L, 2, &l); + m = find(b, s, l); + + if(m > 0) { + lua_pushinteger(L, m); + return 1; + } + + return 0; +} + + +int rb_read(lua_State* L) { + ringbuffer* b = luaL_checkudata(L, 1, "ringbuffer_mt"); + int r = luaL_checkinteger(L, 2); + int peek = lua_toboolean(L, 3); + + if(r > b->blen) { + lua_pushnil(L); + return 1; + } + + if((b->rpos + r) > b->alen) { + lua_pushlstring(L, &b->buffer[b->rpos], b->alen - b->rpos); + lua_pushlstring(L, b->buffer, r - (b->alen - b->rpos)); + lua_concat(L, 2); + } else { + lua_pushlstring(L, &b->buffer[b->rpos], r); + } + + if(!peek) { + b->blen -= r; + b->rpos += r; + modpos(b); + } + + return 1; +} + + +int rb_readuntil(lua_State* L) { + size_t l, m; + ringbuffer* b = luaL_checkudata(L, 1, "ringbuffer_mt"); + const char* s = luaL_checklstring(L, 2, &l); + m = find(b, s, l); + + if(m > 0) { + lua_settop(L, 1); + lua_pushinteger(L, m); + return rb_read(L); + } + + return 0; +} + +int rb_write(lua_State* L) { + size_t l, w = 0; + ringbuffer* b = luaL_checkudata(L, 1, "ringbuffer_mt"); + const char* s = luaL_checklstring(L, 2, &l); + + /* Does `l` bytes fit? */ + if((l + b->blen) > b->alen) { + lua_pushnil(L); + return 1; + } + + while(l-- > 0) { + writechar(b, *s++); + w++; + } + + modpos(b); + + lua_pushinteger(L, w); + + return 1; +} + +int rb_tostring(lua_State* L) { + ringbuffer* b = luaL_checkudata(L, 1, "ringbuffer_mt"); + lua_pushfstring(L, "ringbuffer: %p->%p %d/%d", b, b->buffer, b->blen, b->alen); + return 1; +} + +int rb_length(lua_State* L) { + ringbuffer* b = luaL_checkudata(L, 1, "ringbuffer_mt"); + lua_pushinteger(L, b->blen); + return 1; +} + +int rb_size(lua_State* L) { + ringbuffer* b = luaL_checkudata(L, 1, "ringbuffer_mt"); + lua_pushinteger(L, b->alen); + return 1; +} + +int rb_free(lua_State* L) { + ringbuffer* b = luaL_checkudata(L, 1, "ringbuffer_mt"); + lua_pushinteger(L, b->alen - b->blen); + return 1; +} + +int rb_new(lua_State* L) { + size_t size = luaL_optinteger(L, 1, sysconf(_SC_PAGESIZE)); + ringbuffer* b = lua_newuserdata(L, sizeof(ringbuffer)); + b->rpos = 0; + b->wpos = 0; + b->alen = size; + b->blen = 0; + b->buffer = malloc(size); + + if(b->buffer == NULL) { + return 0; + } + + luaL_getmetatable(L, "ringbuffer_mt"); + lua_setmetatable(L, -2); + + return 1; +} + +int rb_gc(lua_State* L) { + ringbuffer* b = luaL_checkudata(L, 1, "ringbuffer_mt"); + + if(b->buffer != NULL) { + free(b->buffer); + } + + return 0; +} + +int luaopen_util_ringbuffer(lua_State* L) { + if(luaL_newmetatable(L, "ringbuffer_mt")) { + lua_pushcfunction(L, rb_tostring); + lua_setfield(L, -2, "__tostring"); + lua_pushcfunction(L, rb_length); + lua_setfield(L, -2, "__len"); + lua_pushcfunction(L, rb_gc); + lua_setfield(L, -2, "__gc"); + + lua_newtable(L); /* __index */ + { + lua_pushcfunction(L, rb_find); + lua_setfield(L, -2, "find"); + lua_pushcfunction(L, rb_read); + lua_setfield(L, -2, "read"); + lua_pushcfunction(L, rb_readuntil); + lua_setfield(L, -2, "readuntil"); + lua_pushcfunction(L, rb_write); + lua_setfield(L, -2, "write"); + lua_pushcfunction(L, rb_size); + lua_setfield(L, -2, "size"); + lua_pushcfunction(L, rb_length); + lua_setfield(L, -2, "length"); + lua_pushcfunction(L, rb_free); + lua_setfield(L, -2, "free"); + } + lua_setfield(L, -2, "__index"); + } + + lua_newtable(L); + lua_pushcfunction(L, rb_new); + lua_setfield(L, -2, "new"); + return 1; +} diff --git a/util-src/signal.c b/util-src/signal.c index 961d2d3e..725555fa 100644 --- a/util-src/signal.c +++ b/util-src/signal.c @@ -23,7 +23,7 @@ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. + * OTHER DEALINGS IN THE SOFTWARE. */ #include <signal.h> @@ -32,14 +32,17 @@ #include "lua.h" #include "lauxlib.h" +#if (LUA_VERSION_NUM == 501) +#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) +#endif + #ifndef lsig #define lsig -struct lua_signal -{ - char *name; /* name of the signal */ - int sig; /* the signal */ +struct lua_signal { + char* name; /* name of the signal */ + int sig; /* the signal */ }; #endif @@ -47,170 +50,163 @@ struct lua_signal #define LUA_SIGNAL "lua_signal" static const struct lua_signal lua_signals[] = { - /* ANSI C signals */ + /* ANSI C signals */ #ifdef SIGABRT - {"SIGABRT", SIGABRT}, + {"SIGABRT", SIGABRT}, #endif #ifdef SIGFPE - {"SIGFPE", SIGFPE}, + {"SIGFPE", SIGFPE}, #endif #ifdef SIGILL - {"SIGILL", SIGILL}, + {"SIGILL", SIGILL}, #endif #ifdef SIGINT - {"SIGINT", SIGINT}, + {"SIGINT", SIGINT}, #endif #ifdef SIGSEGV - {"SIGSEGV", SIGSEGV}, + {"SIGSEGV", SIGSEGV}, #endif #ifdef SIGTERM - {"SIGTERM", SIGTERM}, + {"SIGTERM", SIGTERM}, #endif - /* posix signals */ + /* posix signals */ #ifdef SIGHUP - {"SIGHUP", SIGHUP}, + {"SIGHUP", SIGHUP}, #endif #ifdef SIGQUIT - {"SIGQUIT", SIGQUIT}, + {"SIGQUIT", SIGQUIT}, #endif #ifdef SIGTRAP - {"SIGTRAP", SIGTRAP}, + {"SIGTRAP", SIGTRAP}, #endif #ifdef SIGKILL - {"SIGKILL", SIGKILL}, + {"SIGKILL", SIGKILL}, #endif #ifdef SIGUSR1 - {"SIGUSR1", SIGUSR1}, + {"SIGUSR1", SIGUSR1}, #endif #ifdef SIGUSR2 - {"SIGUSR2", SIGUSR2}, + {"SIGUSR2", SIGUSR2}, #endif #ifdef SIGPIPE - {"SIGPIPE", SIGPIPE}, + {"SIGPIPE", SIGPIPE}, #endif #ifdef SIGALRM - {"SIGALRM", SIGALRM}, + {"SIGALRM", SIGALRM}, #endif #ifdef SIGCHLD - {"SIGCHLD", SIGCHLD}, + {"SIGCHLD", SIGCHLD}, #endif #ifdef SIGCONT - {"SIGCONT", SIGCONT}, + {"SIGCONT", SIGCONT}, #endif #ifdef SIGSTOP - {"SIGSTOP", SIGSTOP}, + {"SIGSTOP", SIGSTOP}, #endif #ifdef SIGTTIN - {"SIGTTIN", SIGTTIN}, + {"SIGTTIN", SIGTTIN}, #endif #ifdef SIGTTOU - {"SIGTTOU", SIGTTOU}, + {"SIGTTOU", SIGTTOU}, #endif - /* some BSD signals */ + /* some BSD signals */ #ifdef SIGIOT - {"SIGIOT", SIGIOT}, + {"SIGIOT", SIGIOT}, #endif #ifdef SIGBUS - {"SIGBUS", SIGBUS}, + {"SIGBUS", SIGBUS}, #endif #ifdef SIGCLD - {"SIGCLD", SIGCLD}, + {"SIGCLD", SIGCLD}, #endif #ifdef SIGURG - {"SIGURG", SIGURG}, + {"SIGURG", SIGURG}, #endif #ifdef SIGXCPU - {"SIGXCPU", SIGXCPU}, + {"SIGXCPU", SIGXCPU}, #endif #ifdef SIGXFSZ - {"SIGXFSZ", SIGXFSZ}, + {"SIGXFSZ", SIGXFSZ}, #endif #ifdef SIGVTALRM - {"SIGVTALRM", SIGVTALRM}, + {"SIGVTALRM", SIGVTALRM}, #endif #ifdef SIGPROF - {"SIGPROF", SIGPROF}, + {"SIGPROF", SIGPROF}, #endif #ifdef SIGWINCH - {"SIGWINCH", SIGWINCH}, + {"SIGWINCH", SIGWINCH}, #endif #ifdef SIGPOLL - {"SIGPOLL", SIGPOLL}, + {"SIGPOLL", SIGPOLL}, #endif #ifdef SIGIO - {"SIGIO", SIGIO}, + {"SIGIO", SIGIO}, #endif - /* add odd signals */ + /* add odd signals */ #ifdef SIGSTKFLT - {"SIGSTKFLT", SIGSTKFLT}, /* stack fault */ + {"SIGSTKFLT", SIGSTKFLT}, /* stack fault */ #endif #ifdef SIGSYS - {"SIGSYS", SIGSYS}, + {"SIGSYS", SIGSYS}, #endif - {NULL, 0} + {NULL, 0} }; -static lua_State *Lsig = NULL; +static lua_State* Lsig = NULL; static lua_Hook Hsig = NULL; static int Hmask = 0; static int Hcount = 0; -static struct signal_event -{ +static struct signal_event { int Nsig; - struct signal_event *next_event; -} *signal_queue = NULL; + struct signal_event* next_event; +}* signal_queue = NULL; -static struct signal_event *last_event = NULL; +static struct signal_event* last_event = NULL; -static void sighook(lua_State *L, lua_Debug *ar) -{ - struct signal_event *event; - /* restore the old hook */ - lua_sethook(L, Hsig, Hmask, Hcount); +static void sighook(lua_State* L, lua_Debug* ar) { + struct signal_event* event; + /* restore the old hook */ + lua_sethook(L, Hsig, Hmask, Hcount); - lua_pushstring(L, LUA_SIGNAL); - lua_gettable(L, LUA_REGISTRYINDEX); + lua_pushstring(L, LUA_SIGNAL); + lua_gettable(L, LUA_REGISTRYINDEX); - while((event = signal_queue)) - { - lua_pushnumber(L, event->Nsig); - lua_gettable(L, -2); - lua_call(L, 0, 0); - signal_queue = event->next_event; - free(event); - }; + while((event = signal_queue)) { + lua_pushnumber(L, event->Nsig); + lua_gettable(L, -2); + lua_call(L, 0, 0); + signal_queue = event->next_event; + free(event); + }; - lua_pop(L, 1); /* pop lua_signal table */ + lua_pop(L, 1); /* pop lua_signal table */ } -static void handle(int sig) -{ - if(!signal_queue) - { - /* Store the existing debug hook (if any) and its parameters */ - Hsig = lua_gethook(Lsig); - Hmask = lua_gethookmask(Lsig); - Hcount = lua_gethookcount(Lsig); - - signal_queue = malloc(sizeof(struct signal_event)); - signal_queue->Nsig = sig; - signal_queue->next_event = NULL; - - last_event = signal_queue; - - /* Set our new debug hook */ - lua_sethook(Lsig, sighook, LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT, 1); - } - else - { - last_event->next_event = malloc(sizeof(struct signal_event)); - last_event->next_event->Nsig = sig; - last_event->next_event->next_event = NULL; - - last_event = last_event->next_event; - } +static void handle(int sig) { + if(!signal_queue) { + /* Store the existing debug hook (if any) and its parameters */ + Hsig = lua_gethook(Lsig); + Hmask = lua_gethookmask(Lsig); + Hcount = lua_gethookcount(Lsig); + + signal_queue = malloc(sizeof(struct signal_event)); + signal_queue->Nsig = sig; + signal_queue->next_event = NULL; + + last_event = signal_queue; + + /* Set our new debug hook */ + lua_sethook(Lsig, sighook, LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT, 1); + } else { + last_event->next_event = malloc(sizeof(struct signal_event)); + last_event->next_event->Nsig = sig; + last_event->next_event->next_event = NULL; + + last_event = last_event->next_event; + } } /* @@ -222,108 +218,113 @@ static void handle(int sig) * if caught, Lua function _must_ * exit, as the stack is most likely * in an unstable state. -*/ - -static int l_signal(lua_State *L) -{ - int args = lua_gettop(L); - int t, sig; /* type, signal */ - - /* get type of signal */ - luaL_checkany(L, 1); - t = lua_type(L, 1); - if (t == LUA_TNUMBER) - sig = (int) lua_tonumber(L, 1); - else if (t == LUA_TSTRING) - { - lua_pushstring(L, LUA_SIGNAL); - lua_gettable(L, LUA_REGISTRYINDEX); - lua_pushvalue(L, 1); - lua_gettable(L, -2); - if (!lua_isnumber(L, -1)) - luaL_error(L, "invalid signal string"); - sig = (int) lua_tonumber(L, -1); - lua_pop(L, 1); /* get rid of number we pushed */ - } else - luaL_checknumber(L, 1); /* will always error, with good error msg */ - - /* set handler */ - if (args == 1 || lua_isnil(L, 2)) /* clear handler */ - { - lua_pushstring(L, LUA_SIGNAL); - lua_gettable(L, LUA_REGISTRYINDEX); - lua_pushnumber(L, sig); - lua_gettable(L, -2); /* return old handler */ - lua_pushnumber(L, sig); - lua_pushnil(L); - lua_settable(L, -4); - lua_remove(L, -2); /* remove LUA_SIGNAL table */ - signal(sig, SIG_DFL); - } else - { - luaL_checktype(L, 2, LUA_TFUNCTION); - - lua_pushstring(L, LUA_SIGNAL); - lua_gettable(L, LUA_REGISTRYINDEX); - - lua_pushnumber(L, sig); - lua_pushvalue(L, 2); - lua_settable(L, -3); - - /* Set the state for the handler */ - Lsig = L; - - if (lua_toboolean(L, 3)) /* c hook? */ - { - if (signal(sig, handle) == SIG_ERR) - lua_pushboolean(L, 0); - else - lua_pushboolean(L, 1); - } else /* lua_hook */ - { - if (signal(sig, handle) == SIG_ERR) - lua_pushboolean(L, 0); - else - lua_pushboolean(L, 1); - } - } - return 1; +*/ + +static int l_signal(lua_State* L) { + int args = lua_gettop(L); + int t, sig; /* type, signal */ + + /* get type of signal */ + luaL_checkany(L, 1); + t = lua_type(L, 1); + + if(t == LUA_TNUMBER) { + sig = (int) lua_tonumber(L, 1); + } else if(t == LUA_TSTRING) { + lua_pushstring(L, LUA_SIGNAL); + lua_gettable(L, LUA_REGISTRYINDEX); + lua_pushvalue(L, 1); + lua_gettable(L, -2); + + if(!lua_isnumber(L, -1)) { + return luaL_error(L, "invalid signal string"); + } + + sig = (int) lua_tonumber(L, -1); + lua_pop(L, 1); /* get rid of number we pushed */ + } else { + luaL_checknumber(L, 1); /* will always error, with good error msg */ + return luaL_error(L, "unreachable: invalid number was accepted"); + } + + /* set handler */ + if(args == 1 || lua_isnil(L, 2)) { /* clear handler */ + lua_pushstring(L, LUA_SIGNAL); + lua_gettable(L, LUA_REGISTRYINDEX); + lua_pushnumber(L, sig); + lua_gettable(L, -2); /* return old handler */ + lua_pushnumber(L, sig); + lua_pushnil(L); + lua_settable(L, -4); + lua_remove(L, -2); /* remove LUA_SIGNAL table */ + signal(sig, SIG_DFL); + } else { + luaL_checktype(L, 2, LUA_TFUNCTION); + + lua_pushstring(L, LUA_SIGNAL); + lua_gettable(L, LUA_REGISTRYINDEX); + + lua_pushnumber(L, sig); + lua_pushvalue(L, 2); + lua_settable(L, -3); + + /* Set the state for the handler */ + Lsig = L; + + if(lua_toboolean(L, 3)) { /* c hook? */ + if(signal(sig, handle) == SIG_ERR) { + lua_pushboolean(L, 0); + } else { + lua_pushboolean(L, 1); + } + } else { /* lua_hook */ + if(signal(sig, handle) == SIG_ERR) { + lua_pushboolean(L, 0); + } else { + lua_pushboolean(L, 1); + } + } + } + + return 1; } /* * l_raise == raise(signal) * * signal = signal number or string -*/ - -static int l_raise(lua_State *L) -{ - /* int args = lua_gettop(L); */ - int t = 0; /* type */ - lua_Number ret; - - luaL_checkany(L, 1); - - t = lua_type(L, 1); - if (t == LUA_TNUMBER) - { - ret = (lua_Number) raise((int) lua_tonumber(L, 1)); - lua_pushnumber(L, ret); - } else if (t == LUA_TSTRING) - { - lua_pushstring(L, LUA_SIGNAL); - lua_gettable(L, LUA_REGISTRYINDEX); - lua_pushvalue(L, 1); - lua_gettable(L, -2); - if (!lua_isnumber(L, -1)) - luaL_error(L, "invalid signal string"); - ret = (lua_Number) raise((int) lua_tonumber(L, -1)); - lua_pop(L, 1); /* get rid of number we pushed */ - lua_pushnumber(L, ret); - } else - luaL_checknumber(L, 1); /* will always error, with good error msg */ - - return 1; +*/ + +static int l_raise(lua_State* L) { + /* int args = lua_gettop(L); */ + int t = 0; /* type */ + lua_Number ret; + + luaL_checkany(L, 1); + + t = lua_type(L, 1); + + if(t == LUA_TNUMBER) { + ret = (lua_Number) raise((int) lua_tonumber(L, 1)); + lua_pushnumber(L, ret); + } else if(t == LUA_TSTRING) { + lua_pushstring(L, LUA_SIGNAL); + lua_gettable(L, LUA_REGISTRYINDEX); + lua_pushvalue(L, 1); + lua_gettable(L, -2); + + if(!lua_isnumber(L, -1)) { + return luaL_error(L, "invalid signal string"); + } + + ret = (lua_Number) raise((int) lua_tonumber(L, -1)); + lua_pop(L, 1); /* get rid of number we pushed */ + lua_pushnumber(L, ret); + } else { + luaL_checknumber(L, 1); /* will always error, with good error msg */ + } + + return 1; } #if defined(__unix__) || defined(__APPLE__) @@ -335,78 +336,80 @@ static int l_raise(lua_State *L) * * pid = process id * signal = signal number or string -*/ - -static int l_kill(lua_State *L) -{ - int t; /* type */ - lua_Number ret; /* return value */ - - luaL_checknumber(L, 1); /* must be int for pid */ - luaL_checkany(L, 2); /* check for a second arg */ - - t = lua_type(L, 2); - if (t == LUA_TNUMBER) - { - ret = (lua_Number) kill((int) lua_tonumber(L, 1), - (int) lua_tonumber(L, 2)); - lua_pushnumber(L, ret); - } else if (t == LUA_TSTRING) - { - lua_pushstring(L, LUA_SIGNAL); - lua_gettable(L, LUA_REGISTRYINDEX); - lua_pushvalue(L, 2); - lua_gettable(L, -2); - if (!lua_isnumber(L, -1)) - luaL_error(L, "invalid signal string"); - ret = (lua_Number) kill((int) lua_tonumber(L, 1), - (int) lua_tonumber(L, -1)); - lua_pop(L, 1); /* get rid of number we pushed */ - lua_pushnumber(L, ret); - } else - luaL_checknumber(L, 2); /* will always error, with good error msg */ - return 1; +*/ + +static int l_kill(lua_State* L) { + int t; /* type */ + lua_Number ret; /* return value */ + + luaL_checknumber(L, 1); /* must be int for pid */ + luaL_checkany(L, 2); /* check for a second arg */ + + t = lua_type(L, 2); + + if(t == LUA_TNUMBER) { + ret = (lua_Number) kill((int) lua_tonumber(L, 1), + (int) lua_tonumber(L, 2)); + lua_pushnumber(L, ret); + } else if(t == LUA_TSTRING) { + lua_pushstring(L, LUA_SIGNAL); + lua_gettable(L, LUA_REGISTRYINDEX); + lua_pushvalue(L, 2); + lua_gettable(L, -2); + + if(!lua_isnumber(L, -1)) { + return luaL_error(L, "invalid signal string"); + } + + ret = (lua_Number) kill((int) lua_tonumber(L, 1), + (int) lua_tonumber(L, -1)); + lua_pop(L, 1); /* get rid of number we pushed */ + lua_pushnumber(L, ret); + } else { + luaL_checknumber(L, 2); /* will always error, with good error msg */ + } + + return 1; } #endif static const struct luaL_Reg lsignal_lib[] = { - {"signal", l_signal}, - {"raise", l_raise}, + {"signal", l_signal}, + {"raise", l_raise}, #if defined(__unix__) || defined(__APPLE__) - {"kill", l_kill}, + {"kill", l_kill}, #endif - {NULL, NULL} + {NULL, NULL} }; -int luaopen_util_signal(lua_State *L) -{ - int i = 0; - - /* add the library */ - luaL_register(L, "signal", lsignal_lib); - - /* push lua_signals table into the registry */ - /* put the signals inside the library table too, - * they are only a reference */ - lua_pushstring(L, LUA_SIGNAL); - lua_createtable(L, 0, 0); - - while (lua_signals[i].name != NULL) - { - /* registry table */ - lua_pushstring(L, lua_signals[i].name); - lua_pushnumber(L, lua_signals[i].sig); - lua_settable(L, -3); - /* signal table */ - lua_pushstring(L, lua_signals[i].name); - lua_pushnumber(L, lua_signals[i].sig); - lua_settable(L, -5); - i++; - } - - /* add newtable to the registry */ - lua_settable(L, LUA_REGISTRYINDEX); - - return 1; +int luaopen_util_signal(lua_State* L) { + int i = 0; + + /* add the library */ + lua_newtable(L); + luaL_setfuncs(L, lsignal_lib, 0); + + /* push lua_signals table into the registry */ + /* put the signals inside the library table too, + * they are only a reference */ + lua_pushstring(L, LUA_SIGNAL); + lua_newtable(L); + + while(lua_signals[i].name != NULL) { + /* registry table */ + lua_pushstring(L, lua_signals[i].name); + lua_pushnumber(L, lua_signals[i].sig); + lua_settable(L, -3); + /* signal table */ + lua_pushstring(L, lua_signals[i].name); + lua_pushnumber(L, lua_signals[i].sig); + lua_settable(L, -5); + i++; + } + + /* add newtable to the registry */ + lua_settable(L, LUA_REGISTRYINDEX); + + return 1; } diff --git a/util-src/table.c b/util-src/table.c new file mode 100644 index 00000000..6ad45dce --- /dev/null +++ b/util-src/table.c @@ -0,0 +1,14 @@ +#include <lua.h> +#include <lauxlib.h> + +static int Lcreate_table(lua_State* L) { + lua_createtable(L, luaL_checkinteger(L, 1), luaL_checkinteger(L, 2)); + return 1; +} + +int luaopen_util_table(lua_State* L) { + lua_newtable(L); + lua_pushcfunction(L, Lcreate_table); + lua_setfield(L, -2, "create"); + return 1; +} diff --git a/util-src/windows.c b/util-src/windows.c index 3d14ca95..4fcbf21e 100644 --- a/util-src/windows.c +++ b/util-src/windows.c @@ -1,7 +1,7 @@ /* 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. -- @@ -19,23 +19,30 @@ #include "lua.h" #include "lauxlib.h" -static int Lget_nameservers(lua_State *L) { +#if (LUA_VERSION_NUM == 501) +#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) +#endif + +static int Lget_nameservers(lua_State* L) { char stack_buffer[1024]; // stack allocated buffer IP4_ARRAY* ips = (IP4_ARRAY*) stack_buffer; DWORD len = sizeof(stack_buffer); DNS_STATUS status; status = DnsQueryConfig(DnsConfigDnsServerList, FALSE, NULL, NULL, ips, &len); - if (status == 0) { + + if(status == 0) { DWORD i; lua_createtable(L, ips->AddrCount, 0); - for (i = 0; i < ips->AddrCount; i++) { + + for(i = 0; i < ips->AddrCount; i++) { DWORD ip = ips->AddrArray[i]; char ip_str[16] = ""; sprintf_s(ip_str, sizeof(ip_str), "%d.%d.%d.%d", (ip >> 0) & 255, (ip >> 8) & 255, (ip >> 16) & 255, (ip >> 24) & 255); lua_pushstring(L, ip_str); - lua_rawseti(L, -2, i+1); + lua_rawseti(L, -2, i + 1); } + return 1; } else { lua_pushnil(L); @@ -44,46 +51,61 @@ static int Lget_nameservers(lua_State *L) { } } -static int lerror(lua_State *L, char* string) { +static int lerror(lua_State* L, char* string) { lua_pushnil(L); lua_pushfstring(L, "%s: %d", string, GetLastError()); return 2; } -static int Lget_consolecolor(lua_State *L) { +static int Lget_consolecolor(lua_State* L) { HWND console = GetStdHandle(STD_OUTPUT_HANDLE); - WORD color; DWORD read_len; - + WORD color; + DWORD read_len; + CONSOLE_SCREEN_BUFFER_INFO info; - - if (console == INVALID_HANDLE_VALUE) return lerror(L, "GetStdHandle"); - if (!GetConsoleScreenBufferInfo(console, &info)) return lerror(L, "GetConsoleScreenBufferInfo"); - if (!ReadConsoleOutputAttribute(console, &color, 1, info.dwCursorPosition, &read_len)) return lerror(L, "ReadConsoleOutputAttribute"); + + if(console == INVALID_HANDLE_VALUE) { + return lerror(L, "GetStdHandle"); + } + + if(!GetConsoleScreenBufferInfo(console, &info)) { + return lerror(L, "GetConsoleScreenBufferInfo"); + } + + if(!ReadConsoleOutputAttribute(console, &color, 1, info.dwCursorPosition, &read_len)) { + return lerror(L, "ReadConsoleOutputAttribute"); + } lua_pushnumber(L, color); return 1; } -static int Lset_consolecolor(lua_State *L) { +static int Lset_consolecolor(lua_State* L) { int color = luaL_checkint(L, 1); HWND console = GetStdHandle(STD_OUTPUT_HANDLE); - if (console == INVALID_HANDLE_VALUE) return lerror(L, "GetStdHandle"); - if (!SetConsoleTextAttribute(console, color)) return lerror(L, "SetConsoleTextAttribute"); + + if(console == INVALID_HANDLE_VALUE) { + return lerror(L, "GetStdHandle"); + } + + if(!SetConsoleTextAttribute(console, color)) { + return lerror(L, "SetConsoleTextAttribute"); + } + lua_pushboolean(L, 1); return 1; } -static const luaL_Reg Reg[] = -{ +static const luaL_Reg Reg[] = { { "get_nameservers", Lget_nameservers }, { "get_consolecolor", Lget_consolecolor }, { "set_consolecolor", Lset_consolecolor }, { NULL, NULL } }; -LUALIB_API int luaopen_util_windows(lua_State *L) { - luaL_register(L, "windows", Reg); - lua_pushliteral(L, "version"); /** version */ +LUALIB_API int luaopen_util_windows(lua_State* L) { + lua_newtable(L); + luaL_setfuncs(L, Reg, 0); lua_pushliteral(L, "-3.14"); - lua_settable(L,-3); + lua_setfield(L, -2, "version"); return 1; } diff --git a/util/array.lua b/util/array.lua index 2d58e7fb..3ddc97f6 100644 --- a/util/array.lua +++ b/util/array.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -11,8 +11,10 @@ local t_insert, t_sort, t_remove, t_concat local setmetatable = setmetatable; local math_random = math.random; +local math_floor = math.floor; local pairs, ipairs = pairs, ipairs; local tostring = tostring; +local type = type; local array = {}; local array_base = {}; @@ -35,7 +37,7 @@ setmetatable(array, { __call = new_array }); -- Read-only methods function array_methods:random() - return self[math_random(1,#self)]; + return self[math_random(1, #self)]; end -- These methods can be called two ways: @@ -43,7 +45,7 @@ end -- existing_array:method([params, ...]) -- Transform existing array into result -- function array_base.map(outa, ina, func) - for k,v in ipairs(ina) do + for k, v in ipairs(ina) do outa[k] = func(v); end return outa; @@ -52,20 +54,20 @@ end function array_base.filter(outa, ina, func) local inplace, start_length = ina == outa, #ina; local write = 1; - for read=1,start_length do + for read = 1, start_length do local v = ina[read]; if func(v) then outa[write] = v; write = write + 1; end end - + if inplace and write <= start_length then - for i=write,start_length do + for i = write, start_length do outa[i] = nil; end end - + return outa; end @@ -78,34 +80,44 @@ function array_base.sort(outa, ina, ...) end function array_base.pluck(outa, ina, key) - for i=1,#ina do + for i = 1, #ina do outa[i] = ina[i][key]; end return outa; end +function array_base.reverse(outa, ina) + local len = #ina; + if ina == outa then + local middle = math_floor(len/2); + len = len + 1; + local o; -- opposite + for i = 1, middle do + o = len - i; + outa[i], outa[o] = outa[o], outa[i]; + end + else + local off = len + 1; + for i = 1, len do + outa[i] = ina[off - i]; + end + end + return outa; +end + --- These methods only mutate the array function array_methods:shuffle(outa, ina) local len = #self; - for i=1,#self do - local r = math_random(i,len); + for i = 1, #self do + local r = math_random(i, len); self[i], self[r] = self[r], self[i]; end return self; end -function array_methods:reverse() - local len = #self-1; - for i=len,1,-1 do - self:push(self[i]); - self:pop(i); - end - return self; -end - function array_methods:append(array) - local len,len2 = #self, #array; - for i=1,len2 do + local len, len2 = #self, #array; + for i = 1, len2 do self[len+i] = array[i]; end return self; @@ -116,11 +128,7 @@ function array_methods:push(x) return self; end -function array_methods:pop(x) - local v = self[x]; - t_remove(self, x); - return v; -end +array_methods.pop = t_remove; function array_methods:concat(sep) return t_concat(array.map(self, tostring), sep); @@ -135,7 +143,7 @@ function array.collect(f, s, var) local t = {}; while true do var = f(s, var); - if var == nil then break; end + if var == nil then break; end t_insert(t, var); end return setmetatable(t, array_mt); @@ -157,7 +165,4 @@ for method, f in pairs(array_base) do end end -_G.array = array; -module("array"); - return array; diff --git a/util/async.lua b/util/async.lua new file mode 100644 index 00000000..968ec804 --- /dev/null +++ b/util/async.lua @@ -0,0 +1,158 @@ +local log = require "util.logger".init("util.async"); + +local function runner_continue(thread) + -- ASSUMPTION: runner is in 'waiting' state (but we don't have the runner to know for sure) + if coroutine.status(thread) ~= "suspended" then -- This should suffice + return false; + end + local ok, state, runner = coroutine.resume(thread); + if not ok then + local level = 0; + while debug.getinfo(thread, level, "") do level = level + 1; end + ok, runner = debug.getlocal(thread, level-1, 1); + local error_handler = runner.watchers.error; + if error_handler then error_handler(runner, debug.traceback(thread, state)); end + elseif state == "ready" then + -- If state is 'ready', it is our responsibility to update runner.state from 'waiting'. + -- We also have to :run(), because the queue might have further items that will not be + -- processed otherwise. FIXME: It's probably best to do this in a nexttick (0 timer). + runner.state = "ready"; + runner:run(); + end + return true; +end + +local function waiter(num) + local thread = coroutine.running(); + if not thread then + error("Not running in an async context, see http://prosody.im/doc/developers/async"); + end + num = num or 1; + local waiting; + return function () + if num == 0 then return; end -- already done + waiting = true; + coroutine.yield("wait"); + end, function () + num = num - 1; + if num == 0 and waiting then + runner_continue(thread); + elseif num < 0 then + error("done() called too many times"); + end + end; +end + +local function guarder() + local guards = {}; + return function (id, func) + local thread = coroutine.running(); + if not thread then + error("Not running in an async context, see http://prosody.im/doc/developers/async"); + end + local guard = guards[id]; + if not guard then + guard = {}; + guards[id] = guard; + log("debug", "New guard!"); + else + table.insert(guard, thread); + log("debug", "Guarded. %d threads waiting.", #guard) + coroutine.yield("wait"); + end + local function exit() + local next_waiting = table.remove(guard, 1); + if next_waiting then + log("debug", "guard: Executing next waiting thread (%d left)", #guard) + runner_continue(next_waiting); + else + log("debug", "Guard off duty.") + guards[id] = nil; + end + end + if func then + func(); + exit(); + return; + end + return exit; + end; +end + +local runner_mt = {}; +runner_mt.__index = runner_mt; + +local function runner_create_thread(func, self) + local thread = coroutine.create(function (self) + while true do + func(coroutine.yield("ready", self)); + end + end); + assert(coroutine.resume(thread, self)); -- Start it up, it will return instantly to wait for the first input + return thread; +end + +local empty_watchers = {}; +local function runner(func, watchers, data) + return setmetatable({ func = func, thread = false, state = "ready", notified_state = "ready", + queue = {}, watchers = watchers or empty_watchers, data = data } + , runner_mt); +end + +function runner_mt:run(input) + if input ~= nil then + table.insert(self.queue, input); + end + if self.state ~= "ready" then + return true, self.state, #self.queue; + end + + local q, thread = self.queue, self.thread; + if not thread or coroutine.status(thread) == "dead" then + thread = runner_create_thread(self.func, self); + self.thread = thread; + end + + local n, state, err = #q, self.state, nil; + self.state = "running"; + while n > 0 and state == "ready" do + local consumed; + for i = 1,n do + local input = q[i]; + local ok, new_state = coroutine.resume(thread, input); + if not ok then + consumed, state, err = i, "ready", debug.traceback(thread, new_state); + self.thread = nil; + break; + elseif new_state == "wait" then + consumed, state = i, "waiting"; + break; + end + end + if not consumed then consumed = n; end + if q[n+1] ~= nil then + n = #q; + end + for i = 1, n do + q[i] = q[consumed+i]; + end + n = #q; + end + self.state = state; + if err or state ~= self.notified_state then + if err then + state = "error" + else + self.notified_state = state; + end + local handler = self.watchers[state]; + if handler then handler(self, err); end + end + return true, state, n; +end + +function runner_mt:enqueue(input) + table.insert(self.queue, input); +end + +return { waiter = waiter, guarder = guarder, runner = runner }; diff --git a/util/cache.lua b/util/cache.lua new file mode 100644 index 00000000..d3639b3f --- /dev/null +++ b/util/cache.lua @@ -0,0 +1,105 @@ + +local function _remove(list, m) + if m.prev then + m.prev.next = m.next; + end + if m.next then + m.next.prev = m.prev; + end + if list._tail == m then + list._tail = m.prev; + end + if list._head == m then + list._head = m.next; + end + list._count = list._count - 1; +end + +local function _insert(list, m) + if list._head then + list._head.prev = m; + end + m.prev, m.next = nil, list._head; + list._head = m; + if not list._tail then + list._tail = m; + end + list._count = list._count + 1; +end + +local cache_methods = {}; +local cache_mt = { __index = cache_methods }; + +function cache_methods:set(k, v) + local m = self._data[k]; + if m then + -- Key already exists + if v ~= nil then + -- Bump to head of list + _remove(self, m); + _insert(self, m); + m.value = v; + else + -- Remove from list + _remove(self, m); + self._data[k] = nil; + end + return true; + end + -- New key + if v == nil then + return true; + end + -- Check whether we need to remove oldest k/v + local on_evict, evicted_key, evicted_value; + if self._count == self.size then + local tail = self._tail; + on_evict, evicted_key, evicted_value = self._on_evict, tail.key, tail.value; + _remove(self, tail); + self._data[evicted_key] = nil; + end + + m = { key = k, value = v, prev = nil, next = nil }; + self._data[k] = m; + _insert(self, m); + if on_evict and evicted_key then + on_evict(evicted_key, evicted_value, self); + end + return true; +end + +function cache_methods:get(k) + local m = self._data[k]; + if m then + return m.value; + end + return nil; +end + +function cache_methods:items() + local m = self._head; + return function () + if not m then + return; + end + local k, v = m.key, m.value; + m = m.next; + return k, v; + end +end + +function cache_methods:count() + return self._count; +end + +local function new(size, on_evict) + size = assert(tonumber(size), "cache size must be a number"); + size = math.floor(size); + assert(size > 0, "cache size must be greater than zero"); + local data = {}; + return setmetatable({ _data = data, _count = 0, size = size, _head = nil, _tail = nil, _on_evict = on_evict }, cache_mt); +end + +return { + new = new; +} diff --git a/util/caps.lua b/util/caps.lua index a61e7403..cd5ff9c0 100644 --- a/util/caps.lua +++ b/util/caps.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -12,9 +12,9 @@ local sha1 = require "util.hashes".sha1; local t_insert, t_sort, t_concat = table.insert, table.sort, table.concat; local ipairs = ipairs; -module "caps" +local _ENV = nil; -function calculate_hash(disco_info) +local function calculate_hash(disco_info) local identities, features, extensions = {}, {}, {}; for _, tag in ipairs(disco_info) do if tag.name == "identity" then @@ -58,4 +58,6 @@ function calculate_hash(disco_info) return ver, S; end -return _M; +return { + calculate_hash = calculate_hash; +}; diff --git a/util/dataforms.lua b/util/dataforms.lua index ee37157a..79b4d1a4 100644 --- a/util/dataforms.lua +++ b/util/dataforms.lua @@ -1,26 +1,26 @@ -- 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 ipairs = 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 _ENV = nil; local xmlns_forms = 'jabber:x:data'; local form_t = {}; local form_mt = { __index = form_t }; -function new(layout) +local function new(layout) return setmetatable(layout, form_mt); end @@ -32,13 +32,13 @@ function form_t.form(layout, data, formtype) if layout.instructions then form:tag("instructions"):text(layout.instructions):up(); end - for n, field in ipairs(layout) do + for _, 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 @@ -102,11 +102,11 @@ function form_t.form(layout, data, formtype) end form:up(); end - + if field.required then form:tag("required"):up(); end - + -- Jump back up to list of fields form:up(); end @@ -118,6 +118,7 @@ local field_readers = {}; function form_t.data(layout, stanza) local data = {}; local errors = {}; + local present = {}; for _, field in ipairs(layout) do local tag; @@ -133,6 +134,7 @@ function form_t.data(layout, stanza) errors[field.name] = "Required value missing"; end else + present[field.name] = true; local reader = field_readers[field.type]; if reader then data[field.name], errors[field.name] = reader(tag, field.required); @@ -140,35 +142,34 @@ function form_t.data(layout, stanza) end end if next(errors) then - return data, errors; + return data, errors, present; end - return data; + return data, nil, present; 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 +local function simple_text(field_tag, required) + local data = field_tag:get_child_text("value"); + -- XEP-0004 does not say if an empty string is acceptable for a required value + -- so we will follow HTML5 which says that empty string means missing + if required and (data == nil or data == "") then + return nil, "Required value missing"; end + return data; -- Return whatever get_child_text returned, even if empty string +end -field_readers["text-private"] = - field_readers["text-single"]; +field_readers["text-single"] = simple_text; + +field_readers["text-private"] = simple_text; field_readers["jid-single"] = function (field_tag, required) - local raw_data = field_tag:get_child_text("value") + local raw_data, err = simple_text(field_tag, required); + if not raw_data then return raw_data, err; end local data = jid_prep(raw_data); - if data and #data > 0 then - return data - elseif raw_data then + if not data then return nil, "Invalid JID: " .. raw_data; - elseif required then - return nil, "Required value missing"; end + return data; end field_readers["jid-multi"] = @@ -212,8 +213,7 @@ field_readers["text-multi"] = return data, err; end -field_readers["list-single"] = - field_readers["text-single"]; +field_readers["list-single"] = simple_text; local boolean_values = { ["1"] = true, ["true"] = true, @@ -222,15 +222,13 @@ local boolean_values = { 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"; + local raw_value, err = simple_text(field_tag, required); + if not raw_value then return raw_value, err; end + local value = boolean_values[raw_value]; + if value == nil then + return nil, "Invalid boolean representation:" .. raw_value; end + return value; end field_readers["hidden"] = @@ -238,7 +236,9 @@ field_readers["hidden"] = return field_tag:get_child_text("value"); end -return _M; +return { + new = new; +}; --[=[ diff --git a/util/datamanager.lua b/util/datamanager.lua index c69ecd25..07b2fe4a 100644 --- a/util/datamanager.lua +++ b/util/datamanager.lua @@ -43,7 +43,7 @@ pcall(function() fallocate = pposix.fallocate or fallocate; end); -module "datamanager" +local _ENV = nil; ---- utils ----- local encode, decode; @@ -74,7 +74,7 @@ local callbacks = {}; ------- API ------------- -function set_data_path(path) +local function set_data_path(path) log("debug", "Setting data path to: %s", path); data_path = path; end @@ -87,14 +87,14 @@ local function callback(username, host, datastore, data) return username, host, datastore, data; end -function add_callback(func) +local function add_callback(func) if not callbacks[func] then -- Would you really want to set the same callback more than once? callbacks[func] = true; callbacks[#callbacks+1] = func; return true; end end -function remove_callback(func) +local function remove_callback(func) if callbacks[func] then for i, f in ipairs(callbacks) do if f == func then @@ -106,7 +106,7 @@ function remove_callback(func) end end -function getpath(username, host, datastore, ext, create) +local function getpath(username, host, datastore, ext, create) ext = ext or "dat"; host = (host and encode(host)) or "_global"; username = username and encode(username); @@ -119,7 +119,7 @@ function getpath(username, host, datastore, ext, create) end end -function load(username, host, datastore) +local function load(username, host, datastore) local data, ret = envloadfile(getpath(username, host, datastore), {}); if not data then local mode = lfs.attributes(getpath(username, host, datastore), "mode"); @@ -176,7 +176,7 @@ if prosody and prosody.platform ~= "posix" then end end -function store(username, host, datastore, data) +local function store(username, host, datastore, data) if not data then data = {}; end @@ -210,33 +210,62 @@ function store(username, host, datastore, data) return true; end -function list_append(username, host, datastore, data) - if not data then return; end - if callback(username, host, datastore) == false then return true; end - -- save the datastore - local f, msg = io_open(getpath(username, host, datastore, "list", true), "r+"); - if not f then - f, msg = io_open(getpath(username, host, datastore, "list", true), "w"); - end +-- Append a blob of data to a file +local function append(username, host, datastore, ext, data) + if type(data) ~= "string" then return; end + local filename = getpath(username, host, datastore, ext, true); + + local ok; + local f, msg = io_open(filename, "r+"); if not f then - log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil"); - return; + -- File did probably not exist, let's create it + f, msg = io_open(filename, "w"); + if not f then + return nil, msg, "open"; + end end - local data = "item(" .. serialize(data) .. ");\n"; + local pos = f:seek("end"); - local ok, msg = fallocate(f, pos, #data); - f:seek("set", pos); - if ok then - f:write(data); - else + ok, msg = fallocate(f, pos, #data); + if not ok then + log("warn", "fallocate() failed: %s", tostring(msg)); + -- This doesn't work on every file system + end + + if f:seek() ~= pos then + log("debug", "fallocate() changed file position"); + f:seek("set", pos); + end + + ok, msg = f:write(data); + if not ok then + f:close(); + return ok, msg, "write"; + end + + ok, msg = f:close(); + if not ok then + return ok, msg; + end + + return true, pos; +end + +local function list_append(username, host, datastore, data) + if not data then return; end + if callback(username, host, datastore) == false then return true; end + -- save the datastore + + data = "item(" .. serialize(data) .. ");\n"; + local ok, msg = append(username, host, datastore, "list", data); + if not ok then log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil"); return ok, msg; end - f:close(); return true; end -function list_store(username, host, datastore, data) +local function list_store(username, host, datastore, data) if not data then data = {}; end @@ -260,7 +289,7 @@ function list_store(username, host, datastore, data) return true; end -function list_load(username, host, datastore) +local function list_load(username, host, datastore) local items = {}; local data, ret = envloadfile(getpath(username, host, datastore, "list"), {item = function(i) t_insert(items, i); end}); if not data then @@ -288,7 +317,7 @@ local type_map = { list = "list"; } -function users(host, store, typ) +local function users(host, store, typ) typ = type_map[typ or "keyval"]; local store_dir = format("%s/%s/%s", data_path, encode(host), store); @@ -307,7 +336,7 @@ function users(host, store, typ) end, state; end -function stores(username, host, typ) +local function stores(username, host, typ) typ = type_map[typ or "keyval"]; local store_dir = format("%s/%s/", data_path, encode(host)); @@ -347,7 +376,7 @@ local function do_remove(path) return true end -function purge(username, host) +local function purge(username, host) local host_dir = format("%s/%s/", data_path, encode(host)); local ok, iter, state, var = pcall(lfs.dir, host_dir); if not ok then @@ -367,6 +396,20 @@ function purge(username, host) return #errs == 0, t_concat(errs, ", "); end -_M.path_decode = decode; -_M.path_encode = encode; -return _M; +return { + set_data_path = set_data_path; + add_callback = add_callback; + remove_callback = remove_callback; + getpath = getpath; + load = load; + store = store; + append_raw = append; + list_append = list_append; + list_store = list_store; + list_load = list_load; + users = users; + stores = stores; + purge = purge; + path_decode = decode; + path_encode = encode; +}; diff --git a/util/datetime.lua b/util/datetime.lua index a1f62a48..27f28ace 100644 --- a/util/datetime.lua +++ b/util/datetime.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -15,25 +15,25 @@ local os_difftime = os.difftime; local error = error; local tonumber = tonumber; -module "datetime" +local _ENV = nil; -function date(t) +local function date(t) return os_date("!%Y-%m-%d", t); end -function datetime(t) +local function datetime(t) return os_date("!%Y-%m-%dT%H:%M:%SZ", t); end -function time(t) +local function time(t) return os_date("!%H:%M:%S", t); end -function legacy(t) +local function legacy(t) return os_date("!%Y%m%dT%H:%M:%S", t); end -function parse(s) +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+%-]?.*)$"); @@ -54,4 +54,10 @@ function parse(s) end end -return _M; +return { + date = date; + datetime = datetime; + time = time; + legacy = legacy; + parse = parse; +}; diff --git a/util/debug.lua b/util/debug.lua index bff0e347..00f476d0 100644 --- a/util/debug.lua +++ b/util/debug.lua @@ -1,6 +1,9 @@ -- Variables ending with these names will not -- have their values printed ('password' includes -- 'new_password', etc.) +-- +-- luacheck: ignore 122/debug + local censored_names = { password = true; passwd = true; @@ -13,7 +16,7 @@ local termcolours = require "util.termcolours"; local getstring = termcolours.getstring; local styles; do - _ = termcolours.getstyle; + local _ = termcolours.getstyle; styles = { boundary_padding = _("bright"); filename = _("bright", "blue"); @@ -22,20 +25,23 @@ do location = _("yellow"); }; end -module("debugx", package.seeall); -function get_locals_table(level) - level = level + 1; -- Skip this function itself +local function get_locals_table(thread, level) local locals = {}; for local_num = 1, math.huge do - local name, value = debug.getlocal(level, local_num); + local name, value; + if thread then + name, value = debug.getlocal(thread, level, local_num); + else + name, value = debug.getlocal(level+1, local_num); + end if not name then break; end table.insert(locals, { name = name, value = value }); end return locals; end -function get_upvalues_table(func) +local function get_upvalues_table(func) local upvalues = {}; if func then for upvalue_num = 1, math.huge do @@ -47,7 +53,7 @@ function get_upvalues_table(func) return upvalues; end -function string_from_var_table(var_table, max_line_len, indent_str) +local function string_from_var_table(var_table, max_line_len, indent_str) local var_string = {}; local col_pos = 0; max_line_len = max_line_len or math.huge; @@ -83,33 +89,25 @@ function string_from_var_table(var_table, max_line_len, indent_str) end end -function get_traceback_table(thread, start_level) +local function get_traceback_table(thread, start_level) local levels = {}; for level = start_level, math.huge do local info; if thread then - info = debug.getinfo(thread, level+1); + info = debug.getinfo(thread, level); else info = debug.getinfo(level+1); end if not info then break; end - + levels[(level-start_level)+1] = { level = level; info = info; - locals = get_locals_table(level+1); + locals = get_locals_table(thread, level+(thread and 0 or 1)); upvalues = get_upvalues_table(info.func); }; - end - return levels; -end - -function traceback(...) - local ok, ret = pcall(_traceback, ...); - if not ok then - return "Error in error handling: "..ret; end - return ret; + return levels; end local function build_source_boundary_marker(last_source_desc) @@ -117,7 +115,7 @@ local function build_source_boundary_marker(last_source_desc) return getstring(styles.boundary_padding, "v"..padding).." "..getstring(styles.filename, last_source_desc).." "..getstring(styles.boundary_padding, padding..(#last_source_desc%2==0 and "-v" or "v ")); end -function _traceback(thread, message, level) +local function _traceback(thread, message, level) -- Lua manual says: debug.traceback ([thread,] [message [, level]]) -- I fathom this to mean one of: @@ -134,15 +132,15 @@ function _traceback(thread, message, level) return nil; -- debug.traceback() does this end - level = level or 1; + level = level or 0; message = message and (message.."\n") or ""; - - -- +3 counts for this function, and the pcall() and wrapper above us - local levels = get_traceback_table(thread, level+3); - + + -- +3 counts for this function, and the pcall() and wrapper above us, the +1... I don't know. + local levels = get_traceback_table(thread, level+(thread == nil and 4 or 0)); + local last_source_desc; - + local lines = {}; for nlevel, level in ipairs(levels) do local info = level.info; @@ -171,9 +169,11 @@ function _traceback(thread, message, level) nlevel = nlevel-1; table.insert(lines, "\t"..(nlevel==0 and ">" or " ")..getstring(styles.level_num, "("..nlevel..") ")..line); local npadding = (" "):rep(#tostring(nlevel)); - local locals_str = string_from_var_table(level.locals, optimal_line_length, "\t "..npadding); - if locals_str then - table.insert(lines, "\t "..npadding.."Locals: "..locals_str); + if level.locals then + local locals_str = string_from_var_table(level.locals, optimal_line_length, "\t "..npadding); + if locals_str then + table.insert(lines, "\t "..npadding.."Locals: "..locals_str); + end end local upvalues_str = string_from_var_table(level.upvalues, optimal_line_length, "\t "..npadding); if upvalues_str then @@ -186,8 +186,23 @@ function _traceback(thread, message, level) return message.."stack traceback:\n"..table.concat(lines, "\n"); end -function use() +local function traceback(...) + local ok, ret = pcall(_traceback, ...); + if not ok then + return "Error in error handling: "..ret; + end + return ret; +end + +local function use() debug.traceback = traceback; end -return _M; +return { + get_locals_table = get_locals_table; + get_upvalues_table = get_upvalues_table; + string_from_var_table = string_from_var_table; + get_traceback_table = get_traceback_table; + traceback = traceback; + use = use; +}; diff --git a/util/dependencies.lua b/util/dependencies.lua index 4d50cf63..9ab40765 100644 --- a/util/dependencies.lua +++ b/util/dependencies.lua @@ -1,21 +1,19 @@ -- 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. -- -module("dependencies", package.seeall) - -function softreq(...) local ok, lib = pcall(require, ...); if ok then return lib; else return nil, lib; end end +local function softreq(...) local ok, lib = pcall(require, ...); if ok then return lib; else return nil, lib; end end -- Required to be able to find packages installed with luarocks if not softreq "luarocks.loader" then -- LuaRocks 2.x softreq "luarocks.require"; -- LuaRocks <1.x end -function missingdep(name, sources, msg) +local function missingdep(name, sources, msg) print(""); print("**************************"); print("Prosody was unable to find "..tostring(name)); @@ -35,7 +33,7 @@ function missingdep(name, sources, msg) print(""); end --- COMPAT w/pre-0.8 Debian: The Debian config file used to use +-- COMPAT w/pre-0.8 Debian: The Debian config file used to use -- util.ztact, which has been removed from Prosody in 0.8. This -- is to log an error for people who still use it, so they can -- update their configs. @@ -48,19 +46,19 @@ package.preload["util.ztact"] = function () end end; -function check_dependencies() - if _VERSION ~= "Lua 5.1" then +local function check_dependencies() + if _VERSION < "Lua 5.1" then print "***********************************" print("Unsupported Lua version: ".._VERSION); - print("Only Lua 5.1 is supported."); + print("At least Lua 5.1 is required."); print "***********************************" return false; end local fatal; - + local lxp = softreq "lxp" - + if not lxp then missingdep("luaexpat", { ["Debian/Ubuntu"] = "sudo apt-get install liblua5.1-expat0"; @@ -69,9 +67,9 @@ function check_dependencies() }); fatal = true; end - + local socket = softreq "socket" - + if not socket then missingdep("luasocket", { ["Debian/Ubuntu"] = "sudo apt-get install liblua5.1-socket2"; @@ -80,7 +78,7 @@ function check_dependencies() }); fatal = true; end - + local lfs, err = softreq "lfs" if not lfs then missingdep("luafilesystem", { @@ -90,9 +88,9 @@ function check_dependencies() }); fatal = true; end - + local ssl = softreq "ssl" - + if not ssl then missingdep("LuaSec", { ["Debian/Ubuntu"] = "http://prosody.im/download/start#debian_and_ubuntu"; @@ -100,7 +98,7 @@ function check_dependencies() ["Source"] = "http://www.inf.puc-rio.br/~brunoos/luasec/"; }, "SSL/TLS support will not be available"); end - + local encodings, err = softreq "util.encodings" if not encodings then if err:match("not found") then @@ -137,22 +135,27 @@ function check_dependencies() return not fatal; end -function log_warnings() +local function log_warnings() + if _VERSION > "Lua 5.1" then + prosody.log("warn", "Support for %s is experimental, please report any issues", _VERSION); + end + local ssl = softreq"ssl"; if ssl then local major, minor, veryminor, patched = ssl._VERSION:match("(%d+)%.(%d+)%.?(%d*)(M?)"); if not major or ((tonumber(major) == 0 and (tonumber(minor) or 0) <= 3 and (tonumber(veryminor) or 0) <= 2) and patched ~= "M") then - log("error", "This version of LuaSec contains a known bug that causes disconnects, see http://prosody.im/doc/depends"); + prosody.log("error", "This version of LuaSec contains a known bug that causes disconnects, see http://prosody.im/doc/depends"); end end + local lxp = softreq"lxp"; if lxp then if not pcall(lxp.new, { StartDoctypeDecl = false }) then - log("error", "The version of LuaExpat on your system leaves Prosody " + prosody.log("error", "The version of LuaExpat on your system leaves Prosody " .."vulnerable to denial-of-service attacks. You should upgrade to " .."LuaExpat 1.3.0 or higher as soon as possible. See " .."http://prosody.im/doc/depends#luaexpat for more information."); end if not lxp.new({}).getcurrentbytecount then - log("error", "The version of LuaExpat on your system does not support " + prosody.log("error", "The version of LuaExpat on your system does not support " .."stanza size limits, which may leave servers on untrusted " .."networks (e.g. the internet) vulnerable to denial-of-service " .."attacks. You should upgrade to LuaExpat 1.3.0 or higher as " @@ -162,4 +165,9 @@ function log_warnings() end end -return _M; +return { + softreq = softreq; + missingdep = missingdep; + check_dependencies = check_dependencies; + log_warnings = log_warnings; +}; diff --git a/util/events.lua b/util/events.lua index 412acccd..073d2a60 100644 --- a/util/events.lua +++ b/util/events.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -9,15 +9,23 @@ local pairs = pairs; local t_insert = table.insert; +local t_remove = table.remove; local t_sort = table.sort; local setmetatable = setmetatable; local next = next; -module "events" +local _ENV = nil; -function new() +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 @@ -50,6 +58,9 @@ function new() 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); @@ -60,24 +71,91 @@ function new() remove_handler(event, handler); end end; - local function fire_event(event, ...) - local h = handlers[event]; + 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](...); + 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 do + if w[i] == wrapper then + t_remove(w, i); + end + end + if #w == 0 then + if event_name == nil 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 _M; +return { + new = new; +}; diff --git a/util/filters.lua b/util/filters.lua index 6290e53b..f405c0bd 100644 --- a/util/filters.lua +++ b/util/filters.lua @@ -1,22 +1,22 @@ -- 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, t_remove = table.insert, table.remove; -module "filters" +local _ENV = nil; local new_filter_hooks = {}; -function initialize(session) +local function initialize(session) if not session.filters then local filters = {}; session.filters = filters; - + function session.filter(type, data) local filter_list = filters[type]; if filter_list then @@ -28,19 +28,19 @@ function initialize(session) return data; end end - + for i=1,#new_filter_hooks do new_filter_hooks[i](session); end - + return session.filter; end -function add_filter(session, type, callback, priority) +local function add_filter(session, type, callback, priority) if not session.filters then initialize(session); end - + local filter_list = session.filters[type]; if not filter_list then filter_list = {}; @@ -48,19 +48,19 @@ function add_filter(session, type, callback, priority) elseif filter_list[callback] then return; -- Filter already added end - + priority = priority or 0; - + local i = 0; repeat i = i + 1; until not filter_list[i] or filter_list[filter_list[i]] < priority; - + t_insert(filter_list, i, callback); filter_list[callback] = priority; end -function remove_filter(session, type, callback) +local function remove_filter(session, type, callback) if not session.filters then return; end local filter_list = session.filters[type]; if filter_list and filter_list[callback] then @@ -74,11 +74,11 @@ function remove_filter(session, type, callback) end end -function add_filter_hook(callback) +local function add_filter_hook(callback) t_insert(new_filter_hooks, callback); end -function remove_filter_hook(callback) +local function remove_filter_hook(callback) for i=1,#new_filter_hooks do if new_filter_hooks[i] == callback then t_remove(new_filter_hooks, i); @@ -86,4 +86,10 @@ function remove_filter_hook(callback) end end -return _M; +return { + initialize = initialize; + add_filter = add_filter; + remove_filter = remove_filter; + add_filter_hook = add_filter_hook; + remove_filter_hook = remove_filter_hook; +}; diff --git a/util/helpers.lua b/util/helpers.lua index 08b86a7c..bf76d258 100644 --- a/util/helpers.lua +++ b/util/helpers.lua @@ -1,28 +1,18 @@ -- 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 debug = require "util.debug"; -module("helpers", package.seeall); - -- Helper functions for debugging local log = require "util.logger".init("util.debug"); -function log_host_events(host) - return log_events(prosody.hosts[host].events, host); -end - -function revert_log_host_events(host) - return revert_log_events(prosody.hosts[host].events); -end - -function log_events(events, name, logger) +local function log_events(events, name, logger) local f = events.fire_event; if not f then error("Object does not appear to be a util.events object"); @@ -37,11 +27,19 @@ function log_events(events, name, logger) return events; end -function revert_log_events(events) +local function revert_log_events(events) events.fire_event, events[events.fire_event] = events[events.fire_event], nil; -- :)) end -function show_events(events, specific_event) +local function log_host_events(host) + return log_events(prosody.hosts[host].events, host); +end + +local function revert_log_host_events(host) + return revert_log_events(prosody.hosts[host].events); +end + +local function show_events(events, specific_event) local event_handlers = events._handlers; local events_array = {}; local event_handler_arrays = {}; @@ -70,7 +68,7 @@ function show_events(events, specific_event) return table.concat(events_array, "\n"); end -function get_upvalue(f, get_name) +local function get_upvalue(f, get_name) local i, name, value = 0; repeat i = i + 1; @@ -79,4 +77,11 @@ function get_upvalue(f, get_name) return value; end -return _M; +return { + log_host_events = log_host_events; + revert_log_host_events = revert_log_host_events; + log_events = log_events; + revert_log_events = revert_log_events; + show_events = show_events; + get_upvalue = get_upvalue; +}; diff --git a/util/hex.lua b/util/hex.lua new file mode 100644 index 00000000..4cc28d33 --- /dev/null +++ b/util/hex.lua @@ -0,0 +1,26 @@ +local s_char = string.char; +local s_format = string.format; +local s_gsub = string.gsub; +local s_lower = string.lower; + +local char_to_hex = {}; +local hex_to_char = {}; + +do + local char, hex; + for i = 0,255 do + char, hex = s_char(i), s_format("%02x", i); + char_to_hex[char] = hex; + hex_to_char[hex] = char; + end +end + +local function to(s) + return (s_gsub(s, ".", char_to_hex)); +end + +local function from(s) + return (s_gsub(s_lower(s), "%X*(%x%x)%X*", hex_to_char)); +end + +return { to = to, from = from } diff --git a/util/hmac.lua b/util/hmac.lua index 51211c7a..2c4cc6ef 100644 --- a/util/hmac.lua +++ b/util/hmac.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/util/import.lua b/util/import.lua index 81401e8b..c2b9dce1 100644 --- a/util/import.lua +++ b/util/import.lua @@ -1,13 +1,14 @@ -- 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 unpack = table.unpack or unpack; --luacheck: ignore 113 local t_insert = table.insert; function import(module, ...) local m = package.loaded[module] or require(module); diff --git a/util/interpolation.lua b/util/interpolation.lua new file mode 100644 index 00000000..315cc203 --- /dev/null +++ b/util/interpolation.lua @@ -0,0 +1,85 @@ +-- Simple template language +-- +-- The new() function takes a pattern and an escape function and returns +-- a render() function. Both are required. +-- +-- The function render() takes a string template and a table of values. +-- Sequences like {name} in the template string are substituted +-- with values from the table, optionally depending on a modifier +-- symbol. +-- +-- Variants are: +-- {name} is substituted for values["name"] and is escaped using the +-- second argument to new_render(). To disable the escaping, use {name!}. +-- {name.item} can be used to access table items. +-- To renter lists of items: {name# item number {idx} is {item} } +-- Or key-value pairs: {name% t[ {idx} ] = {item} } +-- To show a defaults for missing values {name? sub-template } can be used, +-- which renders a sub-template if values["name"] is false-ish. +-- {name& sub-template } does the opposite, the sub-template is rendered +-- if the selected value is anything but false or nil. + +local type, tostring = type, tostring; +local pairs, ipairs = pairs, ipairs; +local s_sub, s_gsub, s_match = string.sub, string.gsub, string.match; +local t_concat = table.concat; + +local function new_render(pat, escape, funcs) + -- assert(type(pat) == "string", "bad argument #1 to 'new_render' (string expected)"); + -- assert(type(escape) == "function", "bad argument #2 to 'new_render' (function expected)"); + local function render(template, values) + -- assert(type(template) == "string", "bad argument #1 to 'render' (string expected)"); + -- assert(type(values) == "table", "bad argument #2 to 'render' (table expected)"); + return (s_gsub(template, pat, function (block) + block = s_sub(block, 2, -2); + local name, opt, e = s_match(block, "^([%a_][%w_.]*)(%p?)()"); + if not name then return end + local value = values[name]; + if not value and name:find(".", 2, true) then + value = values; + for word in name:gmatch"[^.]+" do + value = value[word]; + if not value then break; end + end + end + if funcs then + while value ~= nil and opt == '|' do + local f; + f, opt, e = s_match(block, "^([%a_][%w_.]*)(%p?)()", e); + f = funcs[f]; + if f then value = f(value); end + end + end + if opt == '#' or opt == '%' then + if type(value) ~= "table" then return ""; end + local iter = opt == '#' and ipairs or pairs; + local out, i, subtpl = {}, 1, s_sub(block, e); + local subvalues = setmetatable({}, { __index = values }); + for idx, item in iter(value) do + subvalues.idx = idx; + subvalues.item = item; + out[i], i = render(subtpl, subvalues), i+1; + end + return t_concat(out); + elseif opt == '&' then + if not value then return ""; end + return render(s_sub(block, e), values); + elseif opt == '?' and not value then + return render(s_sub(block, e), values); + elseif value ~= nil then + if type(value) ~= "string" then + value = tostring(value); + end + if opt ~= '!' then + return escape(value); + end + return value; + end + end)); + end + return render; +end + +return { + new = new_render; +}; diff --git a/util/ip.lua b/util/ip.lua index acfd7f24..ec3b4d7e 100644 --- a/util/ip.lua +++ b/util/ip.lua @@ -96,7 +96,7 @@ local function v6scope(ip) if ip:match("^[0:]*1$") then return 0x2; -- Link-local unicast: - elseif ip:match("^[Ff][Ee][89ABab]") then + elseif ip:match("^[Ff][Ee][89ABab]") then return 0x2; -- Site-local unicast: elseif ip:match("^[Ff][Ee][CcDdEeFf]") then @@ -206,5 +206,40 @@ function ip_methods:scope() 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}; + commonPrefixLength = commonPrefixLength, + parse_cidr = parse_cidr, + match=match}; diff --git a/util/iterators.lua b/util/iterators.lua index 1f6aacb8..868ba786 100644 --- a/util/iterators.lua +++ b/util/iterators.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -10,6 +10,11 @@ local it = {}; +local t_insert = table.insert; +local select, next = select, next; +local unpack = table.unpack or unpack; --luacheck: ignore 113 +local pack = table.pack or function (...) return { n = select("#", ...), ... }; end + -- Reverse an iterator function it.reverse(f, s, var) local results = {}; @@ -19,9 +24,9 @@ function it.reverse(f, s, var) local ret = { f(s, var) }; var = ret[1]; if var == nil then break; end - table.insert(results, 1, ret); + t_insert(results, 1, ret); end - + -- Then return our reverse one local i,max = 0, #results; return function (results) @@ -52,15 +57,15 @@ end -- Given an iterator, iterate only over unique items function it.unique(f, s, var) local set = {}; - + return function () while true do - local ret = { f(s, var) }; + local ret = pack(f(s, var)); var = ret[1]; if var == nil then break; end if not set[var] then set[var] = true; - return var; + return unpack(ret, 1, ret.n); end end end; @@ -69,14 +74,13 @@ end --[[ Return the number of items an iterator returns ]]-- function it.count(f, s, var) local x = 0; - + while true do - local ret = { f(s, var) }; - var = ret[1]; + var = f(s, var); if var == nil then break; end x = x + 1; end - + return x; end @@ -104,7 +108,7 @@ end function it.tail(n, f, s, var) local results, count = {}, 0; while true do - local ret = { f(s, var) }; + local ret = pack(f(s, var)); var = ret[1]; if var == nil then break; end results[(count%n)+1] = ret; @@ -117,9 +121,24 @@ function it.tail(n, f, s, var) return function () pos = pos + 1; if pos > n then return nil; end - return unpack(results[((count-1+pos)%n)+1]); + local ret = results[((count-1+pos)%n)+1]; + return unpack(ret, 1, ret.n); end - --return reverse(head(n, reverse(f, s, var))); + --return reverse(head(n, reverse(f, s, var))); -- ! +end + +function it.filter(filter, f, s, var) + if type(filter) ~= "function" then + local filter_value = filter; + function filter(x) return x ~= filter_value; end + end + return function (s, var) + local ret; + repeat ret = pack(f(s, var)); + var = ret[1]; + until var == nil or filter(unpack(ret, 1, ret.n)); + return unpack(ret, 1, ret.n); + end, s, var; end local function _ripairs_iter(t, key) if key > 1 then return key-1, t[key-1]; end end @@ -139,7 +158,7 @@ function it.to_array(f, s, var) while true do var = f(s, var); if var == nil then break; end - table.insert(t, var); + t_insert(t, var); end return t; end diff --git a/util/jid.lua b/util/jid.lua index 8e0a784c..76155ac7 100644 --- a/util/jid.lua +++ b/util/jid.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -23,9 +23,9 @@ local escapes = { local unescapes = {}; for k,v in pairs(escapes) do unescapes[v] = k; end -module "jid" +local _ENV = nil; -local function _split(jid) +local function split(jid) if not jid then return; end local node, nodepos = match(jid, "^([^@/]+)@()"); local host, hostpos = match(jid, "^([^@/]+)()", nodepos) @@ -34,18 +34,17 @@ local function _split(jid) if (not host) or ((not resource) and #jid >= hostpos) then return nil, nil, nil; end return node, host, resource; end -split = _split; -function bare(jid) - local node, host = _split(jid); +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); +local function prepped_split(jid) + local node, host, resource = split(jid); if host then if sub(host, -1, -1) == "." then -- Strip empty root label host = sub(host, 1, -2); @@ -63,39 +62,29 @@ local function _prepped_split(jid) return node, host, resource; end end -prepped_split = _prepped_split; - -function prep(jid) - local node, host, resource = _prepped_split(jid); - if host then - if node then - host = node .. "@" .. host; - end - if resource then - host = host .. "/" .. resource; - end - end - return host; -end -function join(node, host, resource) - if node and host and resource then +local function join(node, host, resource) + if not host then return end + if node and resource then return node.."@"..host.."/"..resource; - elseif node and host then + elseif node then return node.."@"..host; - elseif host and resource then + elseif resource then return host.."/"..resource; - elseif host then - return host; end - return nil; -- Invalid JID + return host; end -function compare(jid, acl) +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); + 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 @@ -104,7 +93,16 @@ function compare(jid, acl) return false end -function escape(s) return s and (s:gsub(".", escapes)); end -function unescape(s) return s and (s:gsub("\\%x%x", unescapes)); 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 _M; +return { + split = split; + bare = bare; + prepped_split = prepped_split; + join = join; + prep = prep; + compare = compare; + escape = escape; + unescape = unescape; +}; diff --git a/util/json.lua b/util/json.lua index 82ebcc43..becd295d 100644 --- a/util/json.lua +++ b/util/json.lua @@ -13,7 +13,7 @@ local tostring, tonumber = tostring, tonumber; local pairs, ipairs = pairs, ipairs; local next = next; local error = error; -local newproxy, getmetatable, setmetatable = newproxy, getmetatable, setmetatable; +local getmetatable, setmetatable = getmetatable, setmetatable; local print = print; local has_array, array = pcall(require, "util.array"); @@ -22,10 +22,7 @@ local array_mt = has_array and getmetatable(array()) or {}; --module("json") local json = {}; -local null = newproxy and newproxy(true) or {}; -if getmetatable and getmetatable(null) then - getmetatable(null).__tostring = function() return "null"; end; -end +local null = setmetatable({}, { __tostring = function() return "null"; end; }); json.null = null; local escapes = { @@ -348,9 +345,9 @@ local first_escape = { function json.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 diff --git a/util/logger.lua b/util/logger.lua index 26206d4d..e72b29bc 100644 --- a/util/logger.lua +++ b/util/logger.lua @@ -1,23 +1,21 @@ -- 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 pcall = pcall; - -local find = string.find; -local ipairs, pairs, setmetatable = ipairs, pairs, setmetatable; +local pairs = pairs; -module "logger" +local _ENV = nil; local level_sinks = {}; local make_logger; -function init(name) +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"); @@ -52,7 +50,7 @@ function make_logger(source_name, level) return logger; end -function reset() +local function reset() for level, handler_list in pairs(level_sinks) do -- Clear all handlers for this level for i = 1, #handler_list do @@ -61,7 +59,7 @@ function reset() end end -function add_level_sink(level, sink_function) +local function add_level_sink(level, sink_function) if not level_sinks[level] then level_sinks[level] = { sink_function }; else @@ -69,6 +67,10 @@ function add_level_sink(level, sink_function) end end -_M.new = make_logger; - -return _M; +return { + init = init; + make_logger = make_logger; + reset = reset; + add_level_sink = add_level_sink; + new = make_logger; +}; diff --git a/util/mercurial.lua b/util/mercurial.lua new file mode 100644 index 00000000..3f75c4c1 --- /dev/null +++ b/util/mercurial.lua @@ -0,0 +1,34 @@ + +local lfs = require"lfs"; + +local hg = { }; + +function hg.check_id(path) + if lfs.attributes(path, 'mode') ~= "directory" then + return nil, "not a directory"; + end + local hg_dirstate = io.open(path.."/.hg/dirstate"); + local hgid, hgrepo + if hg_dirstate then + hgid = ("%02x%02x%02x%02x%02x%02x"):format(hg_dirstate:read(6):byte(1, 6)); + hg_dirstate:close(); + local hg_changelog = io.open(path.."/.hg/store/00changelog.i"); + if hg_changelog then + hg_changelog:seek("set", 0x20); + hgrepo = ("%02x%02x%02x%02x%02x%02x"):format(hg_changelog:read(6):byte(1, 6)); + hg_changelog:close(); + end + else + local hg_archival,e = io.open(path.."/.hg_archival.txt"); + if hg_archival then + local repo = hg_archival:read("*l"); + local node = hg_archival:read("*l"); + hg_archival:close() + hgid = node and node:match("^node: (%x%x%x%x%x%x%x%x%x%x%x%x)") + hgrepo = repo and repo:match("^repo: (%x%x%x%x%x%x%x%x%x%x%x%x)") + end + end + return hgid, hgrepo; +end + +return hg; diff --git a/util/multitable.lua b/util/multitable.lua index dbf34d28..e4321d3d 100644 --- a/util/multitable.lua +++ b/util/multitable.lua @@ -1,16 +1,17 @@ -- 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 t_insert = table.insert; -local unpack, pairs, next, type = unpack, pairs, next, type; +local pairs, next, type = pairs, next, type; +local unpack = table.unpack or unpack; --luacheck: ignore 113 -module "multitable" +local _ENV = nil; local function get(self, ...) local t = self.data; @@ -126,7 +127,7 @@ local function search_add(self, results, ...) return results; end -function iter(self, ...) +local function iter(self, ...) local query = { ... }; local maxdepth = select("#", ...); local stack = { self.data }; @@ -161,7 +162,7 @@ function iter(self, ...) return it, self; end -function new() +local function new() return { data = {}; get = get; @@ -174,4 +175,7 @@ function new() }; end -return _M; +return { + iter = iter; + new = new; +}; diff --git a/util/openssl.lua b/util/openssl.lua index 39fe99d6..757259f6 100644 --- a/util/openssl.lua +++ b/util/openssl.lua @@ -12,7 +12,7 @@ local config = {}; _M.config = config; local ssl_config = {}; -local ssl_config_mt = {__index=ssl_config}; +local ssl_config_mt = { __index = ssl_config }; function config.new() return setmetatable({ @@ -65,13 +65,12 @@ function ssl_config:serialize() s = s .. ("[%s]\n"):format(k); if k == "subject_alternative_name" then for san, n in pairs(t) do - for i = 1,#n do + for i = 1, #n do s = s .. s_format("%s.%d = %s\n", san, i -1, n[i]); end end elseif k == "distinguished_name" then - for i=1,#DN_order do - local k = DN_order[i] + for i, k in ipairs(t[1] and t or DN_order) do local v = t[k]; if v then s = s .. ("%s = %s\n"):format(k, v); @@ -107,7 +106,7 @@ end function ssl_config:add_sRVName(host, service) t_insert(self.subject_alternative_name.otherName, - s_format("%s;%s", oid_dnssrv, ia5string("_" .. service .."." .. idna_to_ascii(host)))); + s_format("%s;%s", oid_dnssrv, ia5string("_" .. service .. "." .. idna_to_ascii(host)))); end function ssl_config:add_xmppAddr(host) @@ -118,10 +117,10 @@ end function ssl_config:from_prosody(hosts, config, certhosts) -- TODO Decide if this should go elsewhere local found_matching_hosts = false; - for i = 1,#certhosts do + for i = 1, #certhosts do local certhost = certhosts[i]; for name in pairs(hosts) do - if name == certhost or name:sub(-1-#certhost) == "."..certhost then + if name == certhost or name:sub(-1-#certhost) == "." .. certhost then found_matching_hosts = true; self:add_dNSName(name); --print(name .. "#component_module: " .. (config.get(name, "component_module") or "nil")); @@ -144,30 +143,30 @@ end do -- Lua to shell calls. local function shell_escape(s) - return s:gsub("'",[['\'']]); + return "'" .. tostring(s):gsub("'",[['\'']]) .. "'"; end - local function serialize(f,o) - local r = {"openssl", f}; - for k,v in pairs(o) do + local function serialize(command, args) + local commandline = { "openssl", command }; + for k, v in pairs(args) do if type(k) == "string" then - t_insert(r, ("-%s"):format(k)); + t_insert(commandline, ("-%s"):format(k)); if v ~= true then - t_insert(r, ("'%s'"):format(shell_escape(tostring(v)))); + t_insert(commandline, shell_escape(v)); end end end - for _,v in ipairs(o) do - t_insert(r, ("'%s'"):format(shell_escape(tostring(v)))); + for _, v in ipairs(args) do + t_insert(commandline, shell_escape(v)); end - return t_concat(r, " "); + return t_concat(commandline, " "); end local os_execute = os.execute; setmetatable(_M, { - __index=function(_,f) + __index = function(_, command) return function(opts) - return 0 == os_execute(serialize(f, type(opts) == "table" and opts or {})); + return 0 == os_execute(serialize(command, type(opts) == "table" and opts or {})); end; end; }); diff --git a/util/paths.lua b/util/paths.lua new file mode 100644 index 00000000..89f4cad9 --- /dev/null +++ b/util/paths.lua @@ -0,0 +1,44 @@ +local t_concat = table.concat; + +local path_sep = package.config:sub(1,1); + +local path_util = {} + +-- Helper function to resolve relative paths (needed by config) +function path_util.resolve_relative_path(parent_path, path) + if path then + -- Some normalization + parent_path = parent_path:gsub("%"..path_sep.."+$", ""); + path = path:gsub("^%.%"..path_sep.."+", ""); + + local is_relative; + if path_sep == "/" and path:sub(1,1) ~= "/" then + is_relative = true; + elseif path_sep == "\\" and (path:sub(1,1) ~= "/" and (path:sub(2,3) ~= ":\\" and path:sub(2,3) ~= ":/")) then + is_relative = true; + end + if is_relative then + return parent_path..path_sep..path; + end + end + return path; +end + +-- Helper function to convert a glob to a Lua pattern +function path_util.glob_to_pattern(glob) + return "^"..glob:gsub("[%p*?]", function (c) + if c == "*" then + return ".*"; + elseif c == "?" then + return "."; + else + return "%"..c; + end + end).."$"; +end + +function path_util.join(...) + return t_concat({...}, path_sep); +end + +return path_util; diff --git a/util/pluginloader.lua b/util/pluginloader.lua index 112c0d52..004855f0 100644 --- a/util/pluginloader.lua +++ b/util/pluginloader.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -17,9 +17,7 @@ end local io_open = io.open; local envload = require "util.envload".envload; -module "pluginloader" - -function load_file(names) +local function load_file(names) local file, err, path; for i=1,#plugin_dir do for j=1,#names do @@ -35,7 +33,7 @@ function load_file(names) return file, err; end -function load_resource(plugin, resource) +local function load_resource(plugin, resource) resource = resource or "mod_"..plugin..".lua"; local names = { @@ -48,7 +46,7 @@ function load_resource(plugin, resource) return load_file(names); end -function load_code(plugin, resource, env) +local function load_code(plugin, resource, env) local content, err = load_resource(plugin, resource); if not content then return content, err; end local path = err; @@ -57,4 +55,23 @@ function load_code(plugin, resource, env) return f, path; end -return _M; +local function load_code_ext(plugin, resource, extension, env) + local content, err = load_resource(plugin, resource.."."..extension); + if not content then + content, err = load_resource(resource, resource.."."..extension); + if not content then + return content, err; + end + end + local path = err; + local f, err = envload(content, "@"..path, env); + if not f then return f, err; end + return f, path; +end + +return { + load_file = load_file; + load_resource = load_resource; + load_code = load_code; + load_code_ext = load_code_ext; +}; diff --git a/util/prosodyctl.lua b/util/prosodyctl.lua index c6fe1986..f8f28644 100644 --- a/util/prosodyctl.lua +++ b/util/prosodyctl.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -29,25 +29,19 @@ 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, ...) +local function show_message(msg, ...) print(msg:format(...)); end -function show_usage(usage, desc) +local function show_usage(usage, desc) print("Usage: ".._G.arg[0].." "..usage); if desc then print(" "..desc); end end -function getchar(n) +local function getchar(n) local stty_ret = os.execute("stty raw -echo 2>/dev/null"); local ok, char; if stty_ret == 0 then @@ -64,14 +58,14 @@ function getchar(n) end end -function getline() +local function getline() local ok, line = pcall(io.read, "*l"); if ok then return line; end end -function getpass() +local 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 @@ -88,7 +82,7 @@ function getpass() end end -function show_yesno(prompt) +local function show_yesno(prompt) io.write(prompt, " "); local choice = getchar():lower(); io.write("\n"); @@ -99,7 +93,7 @@ function show_yesno(prompt) return (choice == "y"); end -function read_password() +local function read_password() local password; while true do io.write("Enter new password: "); @@ -120,7 +114,7 @@ function read_password() return password; end -function show_prompt(prompt) +local function show_prompt(prompt) io.write(prompt, " "); local line = getline(); line = line and line:gsub("\n$",""); @@ -128,7 +122,7 @@ function show_prompt(prompt) end -- Server control -function adduser(params) +local function adduser(params) local user, host, password = nodeprep(params.user), nameprep(params.host), params.password; if not user then return false, "invalid-username"; @@ -146,15 +140,15 @@ function adduser(params) if not(provider) or provider.name == "null" then usermanager.initialize_host(host); end - + local ok, errmsg = usermanager.create_user(user, password, host); if not ok then - return false, errmsg; + return false, errmsg or "creating-user-failed"; end return true; end -function user_exists(params) +local function user_exists(params) local user, host, password = nodeprep(params.user), nameprep(params.host), params.password; storagemanager.initialize_host(host); @@ -162,28 +156,28 @@ function user_exists(params) if not(provider) or provider.name == "null" then usermanager.initialize_host(host); end - + return usermanager.user_exists(user, host); end -function passwd(params) - if not _M.user_exists(params) then +local function passwd(params) + if not user_exists(params) then return false, "no-such-user"; end - - return _M.adduser(params); + + return adduser(params); end -function deluser(params) - if not _M.user_exists(params) then +local function deluser(params) + if not user_exists(params) then return false, "no-such-user"; end local user, host = nodeprep(params.user), nameprep(params.host); - + return usermanager.delete_user(user, host); end -function getpid() +local function getpid() local pidfile = config.get("*", "pidfile"); if not pidfile then return false, "no-pidfile"; @@ -192,35 +186,35 @@ function getpid() if type(pidfile) ~= "string" then return false, "invalid-pidfile"; end - - local modules_enabled = set.new(config.get("*", "modules_enabled")); - if not modules_enabled:contains("posix") then + + local modules_enabled = set.new(config.get("*", "modules_disabled")); + if prosody.platform ~= "posix" or modules_enabled:contains("posix") then return false, "no-posix"; end - + local file, err = io.open(pidfile, "r+"); if not file then return false, "pidfile-read-failed", err; end - + local locked, err = lfs.lock(file, "w"); if locked then file:close(); return false, "pidfile-not-locked"; end - + local pid = tonumber(file:read("*a")); file:close(); - + if not pid then return false, "invalid-pid"; end - + return true, pid; end -function isrunning() - local ok, pid, err = _M.getpid(); +local function isrunning() + local ok, pid, err = getpid(); if not ok then if pid == "pidfile-read-failed" or pid == "pidfile-not-locked" then -- Report as not running, since we can't open the pidfile @@ -232,8 +226,8 @@ function isrunning() return true, signal.kill(pid, 0) == 0; end -function start() - local ok, ret = _M.isrunning(); +local function start() + local ok, ret = isrunning(); if not ok then return ok, ret; end @@ -248,36 +242,55 @@ function start() return true; end -function stop() - local ok, ret = _M.isrunning(); +local function stop() + local ok, ret = isrunning(); if not ok then return ok, ret; end if not ret then return false, "not-running"; end - - local ok, pid = _M.getpid() + + local ok, pid = getpid() if not ok then return false, pid; end - + signal.kill(pid, signal.SIGTERM); return true; end -function reload() - local ok, ret = _M.isrunning(); +local function reload() + local ok, ret = isrunning(); if not ok then return ok, ret; end if not ret then return false, "not-running"; end - - local ok, pid = _M.getpid() + + local ok, pid = getpid() if not ok then return false, pid; end - + signal.kill(pid, signal.SIGHUP); return true; end -return _M; +return { + show_message = show_message; + show_warning = show_message; + show_usage = show_usage; + getchar = getchar; + getline = getline; + getpass = getpass; + show_yesno = show_yesno; + read_password = read_password; + show_prompt = show_prompt; + adduser = adduser; + user_exists = user_exists; + passwd = passwd; + deluser = deluser; + getpid = getpid; + isrunning = isrunning; + start = start; + stop = stop; + reload = reload; +}; diff --git a/util/pubsub.lua b/util/pubsub.lua index e1418c62..6d12690a 100644 --- a/util/pubsub.lua +++ b/util/pubsub.lua @@ -1,23 +1,27 @@ local events = require "util.events"; - -module("pubsub", package.seeall); +local t_remove = table.remove; local service = {}; local service_mt = { __index = service }; -local default_config = { +local default_config = { __index = { broadcaster = function () end; get_affiliation = function () end; capabilities = {}; -}; +} }; +local default_node_config = { __index = { + ["pubsub#max_items"] = "20"; +} }; -function new(config) +local function new(config) config = config or {}; return setmetatable({ - config = setmetatable(config, { __index = default_config }); + config = setmetatable(config, default_config); + node_defaults = setmetatable(config.node_defaults or {}, default_node_config); affiliations = {}; subscriptions = {}; nodes = {}; + data = {}; events = events.new(); }, service_mt); end @@ -29,13 +33,13 @@ 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 @@ -47,7 +51,7 @@ function service:may(node, actor, action) end end end - + -- Check service-wide capabilities instead local service_capabilities = self.config.capabilities; local caps = service_capabilities[node_aff or service_aff]; @@ -57,7 +61,7 @@ function service:may(node, actor, action) return can; end end - + return false; end @@ -202,7 +206,7 @@ function service:get_subscription(node, actor, jid) return true, node_obj.subscribers[jid]; end -function service:create(node, actor) +function service:create(node, actor, options) -- Access checking if not self:may(node, actor, "create") then return false, "forbidden"; @@ -211,17 +215,20 @@ function service:create(node, actor) if self.nodes[node] then return false, "conflict"; end - + + self.data[node] = {}; self.nodes[node] = { name = node; subscribers = {}; - config = {}; - data = {}; + config = setmetatable(options or {}, {__index=self.node_defaults}); affiliations = {}; }; + setmetatable(self.nodes[node], { __index = { data = self.data[node] } }); -- COMPAT + self.events.fire_event("node-created", { node = node, actor = actor }); local ok, err = self:set_affiliation(node, true, actor, "owner"); if not ok then self.nodes[node] = nil; + self.data[node] = nil; end return ok, err; end @@ -237,10 +244,31 @@ function service:delete(node, actor) return false, "item-not-found"; end self.nodes[node] = nil; + self.data[node] = nil; + self.events.fire_event("node-deleted", { node = node, actor = actor }); self.config.broadcaster("delete", node, node_obj.subscribers); return true; end +local function remove_item_by_id(data, id) + if not data[id] then return end + data[id] = nil; + for i, _id in ipairs(data) do + if id == _id then + t_remove(data, i); + return i; + end + end +end + +local function trim_items(data, max) + max = tonumber(max); + if not max or #data <= max then return end + repeat + data[t_remove(data, 1)] = nil; + until #data <= max +end + function service:publish(node, actor, id, item) -- Access checking if not self:may(node, actor, "publish") then @@ -258,9 +286,13 @@ function service:publish(node, actor, id, item) end node_obj = self.nodes[node]; end - node_obj.data[id] = item; + local node_data = self.data[node]; + remove_item_by_id(node_data, id); + node_data[#node_data + 1] = id; + node_data[id] = item; + trim_items(node_data, node_obj.config["pubsub#max_items"]); self.events.fire_event("item-published", { node = node, actor = actor, id = id, item = item }); - self.config.broadcaster("items", node, node_obj.subscribers, item); + self.config.broadcaster("items", node, node_obj.subscribers, item, actor); return true; end @@ -271,10 +303,11 @@ function service:retract(node, actor, id, retract) end -- local node_obj = self.nodes[node]; - if (not node_obj) or (not node_obj.data[id]) then + if (not node_obj) or (not self.data[node][id]) then return false, "item-not-found"; end - node_obj.data[id] = nil; + self.events.fire_event("item-retracted", { node = node, actor = actor, id = id }); + remove_item_by_id(self.data[node], id); if retract then self.config.broadcaster("items", node, node_obj.subscribers, retract); end @@ -291,7 +324,8 @@ function service:purge(node, actor, notify) if not node_obj then return false, "item-not-found"; end - node_obj.data = {}; -- Purge + self.data[node] = {}; -- Purge + self.events.fire_event("node-purged", { node = node, actor = actor }); if notify then self.config.broadcaster("purge", node, node_obj.subscribers); end @@ -309,9 +343,9 @@ function service:get_items(node, actor, id) return false, "item-not-found"; end if id then -- Restrict results to a single specific item - return true, { [id] = node_obj.data[id] }; + return true, { id, [id] = self.data[node][id] }; else - return true, node_obj.data; + return true, self.data[node]; end end @@ -388,4 +422,24 @@ function service:set_node_capabilities(node, actor, capabilities) return true; end -return _M; +function service:set_node_config(node, actor, new_config) + 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 + + for k,v in pairs(new_config) do + node_obj.config[k] = v; + end + trim_items(self.data[node], node_obj.config["pubsub#max_items"]); + + return true; +end + +return { + new = new; +}; diff --git a/util/queue.lua b/util/queue.lua new file mode 100644 index 00000000..728e905f --- /dev/null +++ b/util/queue.lua @@ -0,0 +1,73 @@ +-- Prosody IM +-- Copyright (C) 2008-2015 Matthew Wild +-- Copyright (C) 2008-2015 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +-- Small ringbuffer library (i.e. an efficient FIFO queue with a size limit) +-- (because unbounded dynamically-growing queues are a bad thing...) + +local have_utable, utable = pcall(require, "util.table"); -- For pre-allocation of table + +local function new(size, allow_wrapping) + -- Head is next insert, tail is next read + local head, tail = 1, 1; + local items = 0; -- Number of stored items + local t = have_utable and utable.create(size, 0) or {}; -- Table to hold items + --luacheck: ignore 212/self + return { + _items = t; + size = size; + count = function (self) return items; end; + push = function (self, item) + if items >= size then + if allow_wrapping then + tail = (tail%size)+1; -- Advance to next oldest item + items = items - 1; + else + return nil, "queue full"; + end + end + t[head] = item; + items = items + 1; + head = (head%size)+1; + return true; + end; + pop = function (self) + if items == 0 then + return nil; + end + local item; + item, t[tail] = t[tail], 0; + tail = (tail%size)+1; + items = items - 1; + return item; + end; + peek = function (self) + if items == 0 then + return nil; + end + return t[tail]; + end; + items = function (self) + --luacheck: ignore 431/t + return function (t, pos) + if pos >= t:count() then + return nil; + end + local read_pos = tail + pos; + if read_pos > t.size then + read_pos = (read_pos%size); + end + return pos+1, t._items[read_pos]; + end, self, 0; + end; + }; +end + +return { + new = new; +}; + diff --git a/util/random.lua b/util/random.lua new file mode 100644 index 00000000..574e2e1c --- /dev/null +++ b/util/random.lua @@ -0,0 +1,30 @@ +-- 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 ok, crand = pcall(require, "util.crand"); +if ok then return crand; end + +local urandom, urandom_err = io.open("/dev/urandom", "r"); + +local function seed() +end + +local function bytes(n) + return urandom:read(n); +end + +if not urandom then + function bytes() + error("Unable to obtain a secure random number generator, please see https://prosody.im/doc/random ("..urandom_err..")"); + end +end + +return { + seed = seed; + bytes = bytes; +}; diff --git a/util/sasl.lua b/util/sasl.lua index afb3861b..5845f34a 100644 --- a/util/sasl.lua +++ b/util/sasl.lua @@ -19,7 +19,7 @@ local setmetatable = setmetatable; local assert = assert; local require = require; -module "sasl" +local _ENV = nil; --[[ Authentication Backend Prototypes: @@ -27,19 +27,38 @@ Authentication Backend Prototypes: state = false : disabled state = true : enabled state = nil : non-existant + +Channel Binding: + +To enable support of channel binding in some mechanisms you need to provide appropriate callbacks in a table +at profile.cb. + +Example: + profile.cb["tls-unique"] = function(self) + return self.user + end + ]] local method = {}; method.__index = method; local mechanisms = {}; local backend_mechanism = {}; +local mechanism_channelbindings = {}; -- register a new SASL mechanims -function registerMechanism(name, backends, f) +local function registerMechanism(name, backends, f, cb_backends) assert(type(name) == "string", "Parameter name MUST be a string."); assert(type(backends) == "string" or type(backends) == "table", "Parameter backends MUST be either a string or a table."); assert(type(f) == "function", "Parameter f MUST be a function."); + if cb_backends then assert(type(cb_backends) == "table"); end mechanisms[name] = f + if cb_backends then + mechanism_channelbindings[name] = {}; + for _, cb_name in ipairs(cb_backends) do + mechanism_channelbindings[name][cb_name] = true; + end + end for _, backend_name in ipairs(backends) do if backend_mechanism[backend_name] == nil then backend_mechanism[backend_name] = {}; end t_insert(backend_mechanism[backend_name], name); @@ -47,7 +66,7 @@ function registerMechanism(name, backends, f) end -- create a new SASL object which can be used to authenticate clients -function new(realm, profile) +local function new(realm, profile) local mechanisms = profile.mechanisms; if not mechanisms then mechanisms = {}; @@ -63,6 +82,15 @@ function new(realm, profile) return setmetatable({ profile = profile, realm = realm, mechs = mechanisms }, method); end +-- add a channel binding handler +function method:add_cb_handler(name, f) + if type(self.profile.cb) ~= "table" then + self.profile.cb = {}; + end + self.profile.cb[name] = f; + return self; +end + -- get a fresh clone with the same realm and profile function method:clean_clone() return new(self.realm, self.profile) @@ -70,7 +98,23 @@ end -- get a list of possible SASL mechanims to use function method:mechanisms() - return self.mechs; + local current_mechs = {}; + for mech, _ in pairs(self.mechs) do + if mechanism_channelbindings[mech] then + if self.profile.cb then + local ok = false; + for cb_name, _ in pairs(self.profile.cb) do + if mechanism_channelbindings[mech][cb_name] then + ok = true; + end + end + if ok == true then current_mechs[mech] = true; end + end + else + current_mechs[mech] = true; + end + end + return current_mechs; end -- select a mechanism to use @@ -92,5 +136,9 @@ require "util.sasl.plain" .init(registerMechanism); require "util.sasl.digest-md5".init(registerMechanism); require "util.sasl.anonymous" .init(registerMechanism); require "util.sasl.scram" .init(registerMechanism); +require "util.sasl.external" .init(registerMechanism); -return _M; +return { + registerMechanism = registerMechanism; + new = new; +}; diff --git a/util/sasl/anonymous.lua b/util/sasl/anonymous.lua index ca5fe404..af05c0e7 100644 --- a/util/sasl/anonymous.lua +++ b/util/sasl/anonymous.lua @@ -16,7 +16,7 @@ local s_match = string.match; local log = require "util.logger".init("sasl"); local generate_uuid = require "util.uuid".generate; -module "sasl.anonymous" +local _ENV = nil; --========================= --SASL ANONYMOUS according to RFC 4505 @@ -39,8 +39,10 @@ local function anonymous(self, message) return "success" end -function init(registerMechanism) +local function init(registerMechanism) registerMechanism("ANONYMOUS", {"anonymous"}, anonymous); end -return _M; +return { + init = init; +} diff --git a/util/sasl/digest-md5.lua b/util/sasl/digest-md5.lua index 591d8537..695dd2a3 100644 --- a/util/sasl/digest-md5.lua +++ b/util/sasl/digest-md5.lua @@ -25,7 +25,7 @@ local log = require "util.logger".init("sasl"); local generate_uuid = require "util.uuid".generate; local nodeprep = require "util.encodings".stringprep.nodeprep; -module "sasl.digest-md5" +local _ENV = nil; --========================= --SASL DIGEST-MD5 according to RFC 2831 @@ -241,8 +241,10 @@ local function digest(self, message) end end -function init(registerMechanism) +local function init(registerMechanism) registerMechanism("DIGEST-MD5", {"plain"}, digest); end -return _M; +return { + init = init; +} diff --git a/util/sasl/external.lua b/util/sasl/external.lua new file mode 100644 index 00000000..5ba90190 --- /dev/null +++ b/util/sasl/external.lua @@ -0,0 +1,27 @@ +local saslprep = require "util.encodings".stringprep.saslprep; + +local _ENV = nil; + +local function external(self, message) + message = saslprep(message); + local state + self.username, state = self.profile.external(message); + + if state == false then + return "failure", "account-disabled"; + elseif state == nil then + return "failure", "not-authorized"; + elseif state == "expired" then + return "false", "credentials-expired"; + end + + return "success"; +end + +local function init(registerMechanism) + registerMechanism("EXTERNAL", {"external"}, external); +end + +return { + init = init; +} diff --git a/util/sasl/plain.lua b/util/sasl/plain.lua index c9ec2911..26e65335 100644 --- a/util/sasl/plain.lua +++ b/util/sasl/plain.lua @@ -16,7 +16,7 @@ local saslprep = require "util.encodings".stringprep.saslprep; local nodeprep = require "util.encodings".stringprep.nodeprep; local log = require "util.logger".init("sasl"); -module "sasl.plain" +local _ENV = nil; -- ================================ -- SASL PLAIN according to RFC 4616 @@ -82,8 +82,10 @@ local function plain(self, message) return "success"; end -function init(registerMechanism) +local function init(registerMechanism) registerMechanism("PLAIN", {"plain", "plain_test"}, plain); end -return _M; +return { + init = init; +} diff --git a/util/sasl/scram.lua b/util/sasl/scram.lua index cf2f0ede..a1b5117a 100644 --- a/util/sasl/scram.lua +++ b/util/sasl/scram.lua @@ -13,7 +13,6 @@ local s_match = string.match; local type = type -local string = string local base64 = require "util.encodings".base64; local hmac_sha1 = require "util.hashes".hmac_sha1; local sha1 = require "util.hashes".sha1; @@ -26,7 +25,7 @@ local t_concat = table.concat; local char = string.char; local byte = string.byte; -module "sasl.scram" +local _ENV = nil; --========================= --SASL SCRAM-SHA-1 according to RFC 5802 @@ -39,18 +38,14 @@ scram_{MECH}: function(username, realm) return stored_key, server_key, iteration_count, salt, state; end + +Supported Channel Binding Backends + +'tls-unique' according to RFC 5929 ]] local default_i = 4096 -local function bp( b ) - local result = "" - for i=1, b:len() do - result = result.."\\"..b:byte(i) - end - return result -end - local xor_map = {0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;1;0;3;2;5;4;7;6;9;8;11;10;13;12;15;14;2;3;0;1;6;7;4;5;10;11;8;9;14;15;12;13;3;2;1;0;7;6;5;4;11;10;9;8;15;14;13;12;4;5;6;7;0;1;2;3;12;13;14;15;8;9;10;11;5;4;7;6;1;0;3;2;13;12;15;14;9;8;11;10;6;7;4;5;2;3;0;1;14;15;12;13;10;11;8;9;7;6;5;4;3;2;1;0;15;14;13;12;11;10;9;8;8;9;10;11;12;13;14;15;0;1;2;3;4;5;6;7;9;8;11;10;13;12;15;14;1;0;3;2;5;4;7;6;10;11;8;9;14;15;12;13;2;3;0;1;6;7;4;5;11;10;9;8;15;14;13;12;3;2;1;0;7;6;5;4;12;13;14;15;8;9;10;11;4;5;6;7;0;1;2;3;13;12;15;14;9;8;11;10;5;4;7;6;1;0;3;2;14;15;12;13;10;11;8;9;6;7;4;5;2;3;0;1;15;14;13;12;11;10;9;8;7;6;5;4;3;2;1;0;}; local result = {}; @@ -73,11 +68,11 @@ local function validate_username(username, _nodeprep) return false end end - + -- replace =2C with , and =3D with = username = username:gsub("=2C", ","); username = username:gsub("=3D", "="); - + -- apply SASLprep username = saslprep(username); @@ -92,7 +87,7 @@ local function hashprep(hashname) return hashname:lower():gsub("-", "_"); end -function getAuthenticationDatabaseSHA1(password, salt, iteration_count) +local function getAuthenticationDatabaseSHA1(password, salt, iteration_count) if type(password) ~= "string" or type(salt) ~= "string" or type(iteration_count) ~= "number" then return false, "inappropriate argument types" end @@ -106,96 +101,131 @@ function getAuthenticationDatabaseSHA1(password, salt, iteration_count) end local function scram_gen(hash_name, H_f, HMAC_f) + local profile_name = "scram_" .. hashprep(hash_name); local function scram_hash(self, message) - if not self.state then self["state"] = {} end - + local support_channel_binding = false; + if self.profile.cb then support_channel_binding = true; end + if type(message) ~= "string" or #message == 0 then return "failure", "malformed-request" end - if not self.state.name then + local state = self.state; + if not state then -- we are processing client_first_message local client_first_message = message; - + -- TODO: fail if authzid is provided, since we don't support them yet - self.state["client_first_message"] = client_first_message; - self.state["gs2_cbind_flag"], self.state["authzid"], self.state["name"], self.state["clientnonce"] - = client_first_message:match("^(%a),(.*),n=(.*),r=([^,]*).*"); + local gs2_header, gs2_cbind_flag, gs2_cbind_name, authzid, client_first_message_bare, username, clientnonce + = s_match(client_first_message, "^(([pny])=?([^,]*),([^,]*),)(m?=?[^,]*,?n=([^,]*),r=([^,]*),?.*)$"); - -- we don't do any channel binding yet - if self.state.gs2_cbind_flag ~= "n" and self.state.gs2_cbind_flag ~= "y" then + if not gs2_cbind_flag then return "failure", "malformed-request"; end - if not self.state.name or not self.state.clientnonce then - return "failure", "malformed-request", "Channel binding isn't support at this time."; + if support_channel_binding and gs2_cbind_flag == "y" then + -- "y" -> client does support channel binding + -- but thinks the server does not. + return "failure", "malformed-request"; + end + + if gs2_cbind_flag == "n" then + -- "n" -> client doesn't support channel binding. + support_channel_binding = false; end - - self.state.name = validate_username(self.state.name, self.profile.nodeprep); - if not self.state.name then + + if support_channel_binding and gs2_cbind_flag == "p" then + -- check whether we support the proposed channel binding type + if not self.profile.cb[gs2_cbind_name] then + return "failure", "malformed-request", "Proposed channel binding type isn't supported."; + end + else + -- no channel binding, + gs2_cbind_name = nil; + end + + username = validate_username(username, self.profile.nodeprep); + if not username then log("debug", "Username violates either SASLprep or contains forbidden character sequences.") return "failure", "malformed-request", "Invalid username."; end - - self.state["servernonce"] = generate_uuid(); - + -- retreive credentials + local stored_key, server_key, salt, iteration_count; if self.profile.plain then - local password, state = self.profile.plain(self, self.state.name, self.realm) + local password, state = self.profile.plain(self, username, self.realm) if state == nil then return "failure", "not-authorized" elseif state == false then return "failure", "account-disabled" end - + password = saslprep(password); if not password then log("debug", "Password violates SASLprep."); return "failure", "not-authorized", "Invalid password." end - self.state.salt = generate_uuid(); - self.state.iteration_count = default_i; + salt = generate_uuid(); + iteration_count = default_i; local succ = false; - succ, self.state.stored_key, self.state.server_key = getAuthenticationDatabaseSHA1(password, self.state.salt, default_i, self.state.iteration_count); + succ, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, iteration_count); if not succ then - log("error", "Generating authentication database failed. Reason: %s", self.state.stored_key); + log("error", "Generating authentication database failed. Reason: %s", stored_key); return "failure", "temporary-auth-failure"; end - elseif self.profile["scram_"..hashprep(hash_name)] then - local stored_key, server_key, iteration_count, salt, state = self.profile["scram_"..hashprep(hash_name)](self, self.state.name, self.realm); + elseif self.profile[profile_name] then + local state; + stored_key, server_key, iteration_count, salt, state = self.profile[profile_name](self, username, self.realm); if state == nil then return "failure", "not-authorized" elseif state == false then return "failure", "account-disabled" end - - self.state.stored_key = stored_key; - self.state.server_key = server_key; - self.state.iteration_count = iteration_count; - self.state.salt = salt end - - local server_first_message = "r="..self.state.clientnonce..self.state.servernonce..",s="..base64.encode(self.state.salt)..",i="..self.state.iteration_count; - self.state["server_first_message"] = server_first_message; + + local nonce = clientnonce .. generate_uuid(); + local server_first_message = "r="..nonce..",s="..base64.encode(salt)..",i="..iteration_count; + self.state = { + gs2_header = gs2_header; + gs2_cbind_name = gs2_cbind_name; + username = username; + nonce = nonce; + + server_key = server_key; + stored_key = stored_key; + client_first_message_bare = client_first_message_bare; + server_first_message = server_first_message; + } return "challenge", server_first_message else -- we are processing client_final_message local client_final_message = message; - - self.state["channelbinding"], self.state["nonce"], self.state["proof"] = client_final_message:match("^c=(.*),r=(.*),.*p=(.*)"); - - if not self.state.proof or not self.state.nonce or not self.state.channelbinding then + + local client_final_message_without_proof, channelbinding, nonce, proof + = s_match(client_final_message, "(c=([^,]*),r=([^,]*),?.-),p=(.*)$"); + + if not proof or not nonce or not channelbinding then return "failure", "malformed-request", "Missing an attribute(p, r or c) in SASL message."; end - if self.state.nonce ~= self.state.clientnonce..self.state.servernonce then + local client_gs2_header = base64.decode(channelbinding) + local our_client_gs2_header = state["gs2_header"] + if state.gs2_cbind_name then + -- we support channelbinding, so check if the value is valid + our_client_gs2_header = our_client_gs2_header .. self.profile.cb[state.gs2_cbind_name](self); + end + if client_gs2_header ~= our_client_gs2_header then + return "failure", "malformed-request", "Invalid channel binding value."; + end + + if nonce ~= state.nonce then return "failure", "malformed-request", "Wrong nonce in client-final-message."; end - - local ServerKey = self.state.server_key; - local StoredKey = self.state.stored_key; - - local AuthMessage = "n=" .. s_match(self.state.client_first_message,"n=(.+)") .. "," .. self.state.server_first_message .. "," .. s_match(client_final_message, "(.+),p=.+") + + local ServerKey = state.server_key; + local StoredKey = state.stored_key; + + local AuthMessage = state.client_first_message_bare .. "," .. state.server_first_message .. "," .. client_final_message_without_proof local ClientSignature = HMAC_f(StoredKey, AuthMessage) - local ClientKey = binaryXOR(ClientSignature, base64.decode(self.state.proof)) + local ClientKey = binaryXOR(ClientSignature, base64.decode(proof)) local ServerSignature = HMAC_f(ServerKey, AuthMessage) if StoredKey == H_f(ClientKey) then local server_final_message = "v="..base64.encode(ServerSignature); - self["username"] = self.state.name; + self["username"] = state.username; return "success", server_final_message; else return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated."; @@ -205,12 +235,18 @@ local function scram_gen(hash_name, H_f, HMAC_f) return scram_hash; end -function init(registerMechanism) +local function init(registerMechanism) local function registerSCRAMMechanism(hash_name, hash, hmac_hash) registerMechanism("SCRAM-"..hash_name, {"plain", "scram_"..(hashprep(hash_name))}, scram_gen(hash_name:lower(), hash, hmac_hash)); + + -- register channel binding equivalent + registerMechanism("SCRAM-"..hash_name.."-PLUS", {"plain", "scram_"..(hashprep(hash_name))}, scram_gen(hash_name:lower(), hash, hmac_hash), {"tls-unique"}); end registerSCRAMMechanism("SHA-1", sha1, hmac_sha1); end -return _M; +return { + getAuthenticationDatabaseSHA1 = getAuthenticationDatabaseSHA1; + init = init; +} diff --git a/util/sasl_cyrus.lua b/util/sasl_cyrus.lua index 19684587..4e9a4af5 100644 --- a/util/sasl_cyrus.lua +++ b/util/sasl_cyrus.lua @@ -60,7 +60,7 @@ local sasl_errstring = { }; setmetatable(sasl_errstring, { __index = function() return "undefined error!" end }); -module "sasl_cyrus" +local _ENV = nil; local method = {}; method.__index = method; @@ -78,11 +78,11 @@ local function init(service_name) end -- create a new SASL object which can be used to authenticate clients --- host_fqdn may be nil in which case gethostname() gives the value. +-- host_fqdn may be nil in which case gethostname() gives the value. -- For GSSAPI, this determines the hostname in the service ticket (after -- reverse DNS canonicalization, only if [libdefaults] rdns = true which --- is the default). -function new(realm, service_name, app_name, host_fqdn) +-- is the default). +local function new(realm, service_name, app_name, host_fqdn) init(app_name or service_name); @@ -163,4 +163,6 @@ function method:process(message) end end -return _M; +return { + new = new; +}; diff --git a/util/serialization.lua b/util/serialization.lua index 8a259184..206f5fbb 100644 --- a/util/serialization.lua +++ b/util/serialization.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -11,18 +11,16 @@ local type = type; local tostring = tostring; local t_insert = table.insert; local t_concat = table.concat; -local error = error; local pairs = pairs; local next = next; -local loadstring = loadstring; local pcall = pcall; local debug_traceback = debug.traceback; local log = require "util.logger".init("serialization"); local envload = require"util.envload".envload; -module "serialization" +local _ENV = nil; local indent = function(i) return string_rep("\t", i); @@ -73,16 +71,16 @@ local function _simplesave(o, ind, t, func) end end -function append(t, o) +local function append(t, o) _simplesave(o, 1, t, t.write or t_insert); return t; end -function serialize(o) +local function serialize(o) return t_concat(append({}, o)); end -function deserialize(str) +local function deserialize(str) if type(str) ~= "string" then return nil; end str = "return "..str; local f, err = envload(str, "@data", {}); @@ -92,4 +90,8 @@ function deserialize(str) return ret; end -return _M; +return { + append = append; + serialize = serialize; + deserialize = deserialize; +}; diff --git a/util/session.lua b/util/session.lua new file mode 100644 index 00000000..b2a726ce --- /dev/null +++ b/util/session.lua @@ -0,0 +1,65 @@ +local initialize_filters = require "util.filters".initialize; +local logger = require "util.logger"; + +local function new_session(typ) + local session = { + type = typ .. "_unauthed"; + }; + return session; +end + +local function set_id(session) + local id = session.type .. tostring(session):match("%x+$"):lower(); + session.id = id; + return session; +end + +local function set_logger(session) + local log = logger.init(session.id); + session.log = log; + return session; +end + +local function set_conn(session, conn) + session.conn = conn; + session.ip = conn:ip(); + return session; +end + +local function set_send(session) + local conn = session.conn; + if not conn then + function session.send(data) + session.log("debug", "Discarding data sent to unconnected session: %s", tostring(data)); + return false; + end + return session; + end + local filter = initialize_filters(session); + local w = conn.write; + session.send = function (t) + if t.name then + t = filter("stanzas/out", t); + end + if t then + t = filter("bytes/out", tostring(t)); + if t then + local ret, err = w(conn, t); + if not ret then + session.log("debug", "Error writing to connection: %s", tostring(err)); + return false, err; + end + end + end + return true; + end + return session; +end + +return { + new = new_session; + set_id = set_id; + set_logger = set_logger; + set_conn = set_conn; + set_send = set_send; +} diff --git a/util/set.lua b/util/set.lua index fa065a9c..c136a522 100644 --- a/util/set.lua +++ b/util/set.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -10,86 +10,49 @@ local ipairs, pairs, setmetatable, next, tostring = ipairs, pairs, setmetatable, next, tostring; local t_concat = table.concat; -module "set" +local _ENV = nil; local set_mt = {}; function set_mt.__call(set, _, k) return next(set._items, k); end -function set_mt.__add(set1, set2) - return _M.union(set1, set2); -end -function set_mt.__sub(set1, set2) - return _M.difference(set1, set2); -end -function set_mt.__div(set, func) - local new_set = _M.new(); - local items, new_items = set._items, new_set._items; - for item in pairs(items) do - local new_item = func(item); - if new_item ~= nil then - new_items[new_item] = true; - end - end - return new_set; -end -function set_mt.__eq(set1, set2) - local set1, set2 = set1._items, set2._items; - for item in pairs(set1) do - if not set2[item] then - return false; - end - end - - for item in pairs(set2) do - if not set1[item] then - return false; - end - end - - return true; -end -function set_mt.__tostring(set) - local s, items = { }, set._items; - for item in pairs(items) do - s[#s+1] = tostring(item); - end - return t_concat(s, ", "); -end local items_mt = {}; function items_mt.__call(items, _, k) return next(items, k); end -function new(list) +local function new(list) local items = setmetatable({}, items_mt); local set = { _items = items }; - + + -- We access the set through an upvalue in these methods, so ignore 'self' being unused + --luacheck: ignore 212/self + function set:add(item) items[item] = true; end - + function set:contains(item) return items[item]; end - + function set:items() - return items; + return next, items; end - + function set:remove(item) items[item] = nil; end - - function set:add_list(list) - if list then - for _, item in ipairs(list) do + + function set:add_list(item_list) + if item_list then + for _, item in ipairs(item_list) do items[item] = true; end end end - + function set:include(otherset) for item in otherset do items[item] = true; @@ -101,22 +64,22 @@ function new(list) items[item] = nil; end end - + function set:empty() return not next(items); end - + if list then set:add_list(list); end - + return setmetatable(set, set_mt); end -function union(set1, set2) +local function union(set1, set2) local set = new(); local items = set._items; - + for item in pairs(set1._items) do items[item] = true; end @@ -124,14 +87,14 @@ function union(set1, set2) for item in pairs(set2._items) do items[item] = true; end - + return set; end -function difference(set1, set2) +local function difference(set1, set2) local set = new(); local items = set._items; - + for item in pairs(set1._items) do items[item] = (not set2._items[item]) or nil; end @@ -139,21 +102,68 @@ function difference(set1, set2) return set; end -function intersection(set1, set2) +local function intersection(set1, set2) local set = new(); local items = set._items; - + set1, set2 = set1._items, set2._items; - + for item in pairs(set1) do items[item] = (not not set2[item]) or nil; end - + return set; end -function xor(set1, set2) +local function xor(set1, set2) return union(set1, set2) - intersection(set1, set2); end -return _M; +function set_mt.__add(set1, set2) + return union(set1, set2); +end +function set_mt.__sub(set1, set2) + return difference(set1, set2); +end +function set_mt.__div(set, func) + local new_set = new(); + local items, new_items = set._items, new_set._items; + for item in pairs(items) do + local new_item = func(item); + if new_item ~= nil then + new_items[new_item] = true; + end + end + return new_set; +end +function set_mt.__eq(set1, set2) + set1, set2 = set1._items, set2._items; + for item in pairs(set1) do + if not set2[item] then + return false; + end + end + + for item in pairs(set2) do + if not set1[item] then + return false; + end + end + + return true; +end +function set_mt.__tostring(set) + local s, items = { }, set._items; + for item in pairs(items) do + s[#s+1] = tostring(item); + end + return t_concat(s, ", "); +end + +return { + new = new; + union = union; + difference = difference; + intersection = intersection; + xor = xor; +}; diff --git a/util/sql.lua b/util/sql.lua index f360d6d0..9981ac3c 100644 --- a/util/sql.lua +++ b/util/sql.lua @@ -1,6 +1,6 @@ local setmetatable, getmetatable = setmetatable, getmetatable; -local ipairs, unpack, select = ipairs, unpack, select; +local ipairs, unpack, select = ipairs, table.unpack or unpack, select; --luacheck: ignore 113 local tonumber, tostring = tonumber, tostring; local assert, xpcall, debug_traceback = assert, xpcall, debug.traceback; local t_concat = table.concat; @@ -13,7 +13,7 @@ local DBI = require "DBI"; DBI.Drivers(); local build_url = require "socket.url".build; -module("sql") +local _ENV = nil; local column_mt = {}; local table_mt = {}; @@ -21,42 +21,17 @@ local query_mt = {}; --local op_mt = {}; local index_mt = {}; -function is_column(x) return getmetatable(x)==column_mt; end -function is_index(x) return getmetatable(x)==index_mt; end -function is_table(x) return getmetatable(x)==table_mt; end -function is_query(x) return getmetatable(x)==query_mt; end ---function is_op(x) return getmetatable(x)==op_mt; end ---function expr(...) return setmetatable({...}, op_mt); end -function Integer(n) return "Integer()" end -function String(n) return "String()" end +local function is_column(x) return getmetatable(x)==column_mt; end +local function is_index(x) return getmetatable(x)==index_mt; end +local function is_table(x) return getmetatable(x)==table_mt; end +local function is_query(x) return getmetatable(x)==query_mt; end +local function Integer(n) return "Integer()" end +local function String(n) return "String()" end ---[[local ops = { - __add = function(a, b) return "("..a.."+"..b..")" end; - __sub = function(a, b) return "("..a.."-"..b..")" end; - __mul = function(a, b) return "("..a.."*"..b..")" end; - __div = function(a, b) return "("..a.."/"..b..")" end; - __mod = function(a, b) return "("..a.."%"..b..")" end; - __pow = function(a, b) return "POW("..a..","..b..")" end; - __unm = function(a) return "NOT("..a..")" end; - __len = function(a) return "COUNT("..a..")" end; - __eq = function(a, b) return "("..a.."=="..b..")" end; - __lt = function(a, b) return "("..a.."<"..b..")" end; - __le = function(a, b) return "("..a.."<="..b..")" end; -}; - -local functions = { - -}; - -local cmap = { - [Integer] = Integer(); - [String] = String(); -};]] - -function Column(definition) +local function Column(definition) return setmetatable(definition, column_mt); end -function Table(definition) +local function Table(definition) local c = {} for i,col in ipairs(definition) do if is_column(col) then @@ -67,7 +42,7 @@ function Table(definition) end return setmetatable({ __table__ = definition, c = c, name = definition.name }, table_mt); end -function Index(definition) +local function Index(definition) return setmetatable(definition, index_mt); end @@ -94,7 +69,6 @@ function index_mt:__tostring() return s..' }'; -- return 'Index{ name="'..self.name..'", type="'..self.type..'" }' end --- local function urldecode(s) return s and (s:gsub("%%(%x%x)", function (c) return s_char(tonumber(c,16)); end)); end local function parse_url(url) @@ -121,32 +95,13 @@ local function parse_url(url) }; end ---[[local session = {}; - -function session.query(...) - local rets = {...}; - local query = setmetatable({ __rets = rets, __filters }, query_mt); - return query; -end --- - -local function db2uri(params) - return build_url{ - scheme = params.driver, - user = params.username, - password = params.password, - host = params.host, - port = params.port, - path = params.database, - }; -end]] - local engine = {}; function engine:connect() if self.conn then return true; end local params = self.params; assert(params.driver, "no driver") + log("debug", "Connecting to [%s] %s...", params.driver, params.database); local dbh, err = DBI.Connect( params.driver, params.database, params.username, params.password, @@ -156,8 +111,19 @@ function engine:connect() dbh:autocommit(false); -- don't commit automatically self.conn = dbh; self.prepared = {}; + local ok, err = self:set_encoding(); + if not ok then + return ok, err; + end + local ok, err = self:onconnect(); + if ok == false then + return ok, err; + end return true; end +function engine:onconnect() + -- Override from create_engine() +end function engine:execute(sql, ...) local success, err = self:connect(); if not success then return success, err; end @@ -177,10 +143,15 @@ function engine:execute(sql, ...) end local result_mt = { __index = { - affected = function(self) return self.__affected; end; - rowcount = function(self) return self.__rowcount; end; + affected = function(self) return self.__stmt:affected(); end; + rowcount = function(self) return self.__stmt:rowcount(); end; } }; +local function debugquery(where, sql, ...) + local i = 0; local a = {...} + log("debug", "[%s] %s", where, sql:gsub("%?", function () i = i + 1; local v = a[i]; if type(v) == "string" then v = ("%q"):format(v); end return tostring(v); end)); +end + function engine:execute_query(sql, ...) if self.params.driver == "PostgreSQL" then sql = sql:gsub("`", "\""); @@ -200,20 +171,41 @@ function engine:execute_update(sql, ...) prepared[sql] = stmt; end assert(stmt:execute(...)); - return setmetatable({ __affected = stmt:affected(), __rowcount = stmt:rowcount() }, result_mt); + return setmetatable({ __stmt = stmt }, result_mt); end engine.insert = engine.execute_update; engine.select = engine.execute_query; engine.delete = engine.execute_update; engine.update = engine.execute_update; +local function debugwrap(name, f) + return function (self, sql, ...) + debugquery(name, sql, ...) + return f(self, sql, ...) + end +end +function engine:debug(enable) + self._debug = enable; + if enable then + engine.insert = debugwrap("insert", engine.execute_update); + engine.select = debugwrap("select", engine.execute_query); + engine.delete = debugwrap("delete", engine.execute_update); + engine.update = debugwrap("update", engine.execute_update); + else + engine.insert = engine.execute_update; + engine.select = engine.execute_query; + engine.delete = engine.execute_update; + engine.update = engine.execute_update; + end +end function engine:_transaction(func, ...) if not self.conn then - local a,b = self:connect(); - if not a then return a,b; end + local ok, err = self:connect(); + if not ok then return ok, err; end end --assert(not self.__transaction, "Recursive transactions not allowed"); local args, n_args = {...}, select("#", ...); local function f() return func(unpack(args, 1, n_args)); end + log("debug", "SQL transaction begin [%s]", tostring(func)); self.__transaction = true; local success, a, b, c = xpcall(f, debug_traceback); self.__transaction = nil; @@ -229,15 +221,15 @@ function engine:_transaction(func, ...) end end function engine:transaction(...) - local a,b = self:_transaction(...); - if not a then + local ok, ret = self:_transaction(...); + if not ok then local conn = self.conn; if not conn or not conn:ping() then self.conn = nil; - a,b = self:_transaction(...); + ok, ret = self:_transaction(...); end end - return a,b; + return ok, ret; end function engine:_create_index(index) local sql = "CREATE INDEX `"..index.name.."` ON `"..index.table.."` ("; @@ -251,19 +243,44 @@ function engine:_create_index(index) elseif self.params.driver == "MySQL" then sql = sql:gsub("`([,)])", "`(20)%1"); end - --print(sql); + if index.unique then + sql = sql:gsub("^CREATE", "CREATE UNIQUE"); + end + if self._debug then + debugquery("create", sql); + end return self:execute(sql); end function engine:_create_table(table) local sql = "CREATE TABLE `"..table.name.."` ("; for i,col in ipairs(table.c) do - sql = sql.."`"..col.name.."` "..col.type; + local col_type = col.type; + if col_type == "MEDIUMTEXT" and self.params.driver ~= "MySQL" then + col_type = "TEXT"; -- MEDIUMTEXT is MySQL-specific + end + if col.auto_increment == true and self.params.driver == "PostgreSQL" then + col_type = "BIGSERIAL"; + end + sql = sql.."`"..col.name.."` "..col_type; if col.nullable == false then sql = sql.." NOT NULL"; end + if col.primary_key == true then sql = sql.." PRIMARY KEY"; end + if col.auto_increment == true then + if self.params.driver == "MySQL" then + sql = sql.." AUTO_INCREMENT"; + elseif self.params.driver == "SQLite3" then + sql = sql.." AUTOINCREMENT"; + end + end if i ~= #table.c then sql = sql..", "; end end sql = sql.. ");" if self.params.driver == "PostgreSQL" then sql = sql:gsub("`", "\""); + elseif self.params.driver == "MySQL" then + sql = sql:gsub(";$", (" CHARACTER SET '%s' COLLATE '%s_bin';"):format(self.charset, self.charset)); + end + if self._debug then + debugquery("create", sql); end local success,err = self:execute(sql); if not success then return success,err; end @@ -274,6 +291,46 @@ function engine:_create_table(table) end return success; end +function engine:set_encoding() -- to UTF-8 + local driver = self.params.driver; + if driver == "SQLite3" then + return self:transaction(function() + if self:select"PRAGMA encoding;"()[1] == "UTF-8" then + self.charset = "utf8"; + end + end); + end + local set_names_query = "SET NAMES '%s';" + local charset = "utf8"; + if driver == "MySQL" then + local ok, charsets = self:transaction(function() + return self:select"SELECT `CHARACTER_SET_NAME` FROM `information_schema`.`CHARACTER_SETS` WHERE `CHARACTER_SET_NAME` LIKE 'utf8%' ORDER BY MAXLEN DESC LIMIT 1;"; + end); + local row = ok and charsets(); + charset = row and row[1] or charset; + set_names_query = set_names_query:gsub(";$", (" COLLATE '%s';"):format(charset.."_bin")); + end + self.charset = charset; + log("debug", "Using encoding '%s' for database connection", charset); + local ok, err = self:transaction(function() return self:execute(set_names_query:format(charset)); end); + if not ok then + return ok, err; + end + + if driver == "MySQL" then + local ok, actual_charset = self:transaction(function () + return self:select"SHOW SESSION VARIABLES LIKE 'character_set_client'"; + end); + for row in actual_charset do + if row[2] ~= charset then + log("error", "MySQL %s is actually %q (expected %q)", row[1], row[2], charset); + return false, "Failed to set connection encoding"; + end + end + end + + return true; +end local engine_mt = { __index = engine }; local function db2uri(params) @@ -286,55 +343,21 @@ local function db2uri(params) path = params.database, }; end -local engine_cache = {}; -- TODO make weak valued -function create_engine(self, params) - local url = db2uri(params); - if not engine_cache[url] then - local engine = setmetatable({ url = url, params = params }, engine_mt); - engine_cache[url] = engine; - end - return engine_cache[url]; -end - ---[[Users = Table { - name="users"; - Column { name="user_id", type=String(), primary_key=true }; -}; -print(Users) -print(Users.c.user_id)]] - ---local engine = create_engine('postgresql://scott:tiger@localhost:5432/mydatabase'); ---[[local engine = create_engine{ driver = "SQLite3", database = "./alchemy.sqlite" }; - -local i = 0; -for row in assert(engine:execute("select * from sqlite_master")):rows(true) do - i = i+1; - print(i); - for k,v in pairs(row) do - print("",k,v); - end +local function create_engine(self, params, onconnect) + return setmetatable({ url = db2uri(params), params = params, onconnect = onconnect }, engine_mt); end -print("---") -Prosody = Table { - name="prosody"; - Column { name="host", type="TEXT", nullable=false }; - Column { name="user", type="TEXT", nullable=false }; - Column { name="store", type="TEXT", nullable=false }; - Column { name="key", type="TEXT", nullable=false }; - Column { name="type", type="TEXT", nullable=false }; - Column { name="value", type="TEXT", nullable=false }; - Index { name="prosody_index", "host", "user", "store", "key" }; +return { + is_column = is_column; + is_index = is_index; + is_table = is_table; + is_query = is_query; + Integer = Integer; + String = String; + Column = Column; + Table = Table; + Index = Index; + create_engine = create_engine; + db2uri = db2uri; }; ---print(Prosody); -assert(engine:transaction(function() - assert(Prosody:create(engine)); -end)); - -for row in assert(engine:execute("select user from prosody")):rows(true) do - print("username:", row['username']) -end ---result.close();]] - -return _M; diff --git a/util/sslconfig.lua b/util/sslconfig.lua new file mode 100644 index 00000000..c849aa28 --- /dev/null +++ b/util/sslconfig.lua @@ -0,0 +1,120 @@ +-- util to easily merge multiple sets of LuaSec context options + +local type = type; +local pairs = pairs; +local rawset = rawset; +local t_concat = table.concat; +local t_insert = table.insert; +local setmetatable = setmetatable; + +local _ENV = nil; + +local handlers = { }; +local finalisers = { }; +local id = function (v) return v end + +-- All "handlers" behave like extended rawset(table, key, value) with extra +-- processing usually merging the new value with the old in some reasonable +-- way +-- If a field does not have a defined handler then a new value simply +-- replaces the old. + + +-- Convert either a list or a set into a special type of set where each +-- item is either positive or negative in order for a later set of options +-- to be able to remove options from this set by filtering out the negative ones +function handlers.options(config, field, new) + local options = config[field] or { }; + if type(new) ~= "table" then new = { new } end + for key, value in pairs(new) do + if value == true or value == false then + options[key] = value; + else -- list item + options[value] = true; + end + end + config[field] = options; +end + +handlers.verify = handlers.options; +handlers.verifyext = handlers.options; + +-- finalisers take something produced by handlers and return what luasec +-- expects it to be + +-- Produce a list of "positive" options from the set +function finalisers.options(options) + local output = {}; + for opt, enable in pairs(options) do + if enable then + output[#output+1] = opt; + end + end + return output; +end + +finalisers.verify = finalisers.options; +finalisers.verifyext = finalisers.options; + +-- We allow ciphers to be a list + +function finalisers.ciphers(cipherlist) + if type(cipherlist) == "table" then + return t_concat(cipherlist, ":"); + end + return cipherlist; +end + +-- protocol = "x" should enable only that protocol +-- protocol = "x+" should enable x and later versions + +local protocols = { "sslv2", "sslv3", "tlsv1", "tlsv1_1", "tlsv1_2" }; +for i = 1, #protocols do protocols[protocols[i] .. "+"] = i - 1; end + +-- this interacts with ssl.options as well to add no_x +local function protocol(config) + local min_protocol = protocols[config.protocol]; + if min_protocol then + config.protocol = "sslv23"; + for i = 1, min_protocol do + t_insert(config.options, "no_"..protocols[i]); + end + end +end + +-- Merge options from 'new' config into 'config' +local function apply(config, new) + if type(new) == "table" then + for field, value in pairs(new) do + (handlers[field] or rawset)(config, field, value); + end + end +end + +-- Finalize the config into the form LuaSec expects +local function final(config) + local output = { }; + for field, value in pairs(config) do + output[field] = (finalisers[field] or id)(value); + end + -- Need to handle protocols last because it adds to the options list + protocol(output); + return output; +end + +local sslopts_mt = { + __index = { + apply = apply; + final = final; + }; +}; + +local function new() + return setmetatable({options={}}, sslopts_mt); +end + +return { + apply = apply; + final = final; + new = new; +}; diff --git a/util/stanza.lua b/util/stanza.lua index 7c214210..90422a06 100644 --- a/util/stanza.lua +++ b/util/stanza.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -35,13 +35,12 @@ end local xmlns_stanzas = "urn:ietf:params:xml:ns:xmpp-stanzas"; -module "stanza" +local _ENV = nil; -stanza_mt = { __type = "stanza" }; +local stanza_mt = { __type = "stanza" }; stanza_mt.__index = stanza_mt; -local stanza_mt = stanza_mt; -function stanza(name, attr) +local function stanza(name, attr) local stanza = { name = name, attr = attr or {}, tags = {} }; return setmetatable(stanza, stanza_mt); end @@ -99,7 +98,7 @@ function stanza_mt:get_child(name, xmlns) 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 @@ -152,7 +151,7 @@ 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 @@ -200,12 +199,8 @@ function stanza_mt:find(path) end -local xml_escape -do - local escape_table = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" }; - function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end - _M.xml_escape = xml_escape; -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; @@ -258,13 +253,13 @@ end function stanza_mt.get_error(stanza) local type, condition, text; - + local error_tag = stanza:get_child("error"); if not error_tag then return nil, nil, nil; end 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 @@ -280,15 +275,13 @@ function stanza_mt.get_error(stanza) return type, condition or "undefined-condition", text; end -do - local id = 0; - function new_id() - id = id + 1; - return "lx"..id; - end +local id = 0; +local function new_id() + id = id + 1; + return "lx"..id; end -function preserialize(stanza) +local function preserialize(stanza) local s = { name = stanza.name, attr = stanza.attr }; for _, child in ipairs(stanza) do if type(child) == "table" then @@ -300,7 +293,7 @@ function preserialize(stanza) return s; end -function deserialize(stanza) +local function deserialize(stanza) -- Set metatable if stanza then local attr = stanza.attr; @@ -333,55 +326,52 @@ function deserialize(stanza) stanza.tags = tags; end end - + return stanza; end -local function _clone(stanza) +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); + child = clone(child); t_insert(tags, child); end t_insert(new, child); end return setmetatable(new, stanza_mt); end -clone = _clone; -function message(attr, body) +local function message(attr, body) if not body then return stanza("message", attr); else return stanza("message", attr):tag("body"):text(body):up(); end end -function iq(attr) +local function iq(attr) if attr and not attr.id then attr.id = new_id(); end return stanza("iq", attr or { id = new_id() }); end -function reply(orig) +local function reply(orig) return 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 -do - local xmpp_stanzas_attr = { xmlns = xmlns_stanzas }; - function error_reply(orig, type, condition, message) - local t = reply(orig); - t.attr.type = "error"; - t:tag("error", {type = type}) --COMPAT: Some day xmlns:stanzas goes here - :tag(condition, xmpp_stanzas_attr):up(); - if (message) then t:tag("text", xmpp_stanzas_attr):text(message):up(); end - return t; -- stanza ready for adding app-specific errors - end +local xmpp_stanzas_attr = { xmlns = xmlns_stanzas }; +local function error_reply(orig, type, condition, message) + local t = reply(orig); + t.attr.type = "error"; + t:tag("error", {type = type}) --COMPAT: Some day xmlns:stanzas goes here + :tag(condition, xmpp_stanzas_attr):up(); + if (message) then t:tag("text", xmpp_stanzas_attr):text(message):up(); end + return t; -- stanza ready for adding app-specific errors end -function presence(attr) +local function presence(attr) return stanza("presence", attr); end @@ -390,7 +380,7 @@ if do_pretty_printing then 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, "</")..getstring(style_tagname, "%s")..getstring(style_punc, ">"); @@ -411,7 +401,7 @@ if do_pretty_printing then 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 @@ -425,4 +415,17 @@ else stanza_mt.pretty_top_tag = stanza_mt.top_tag; end -return _M; +return { + stanza_mt = stanza_mt; + stanza = 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; +}; diff --git a/util/statistics.lua b/util/statistics.lua new file mode 100644 index 00000000..26355026 --- /dev/null +++ b/util/statistics.lua @@ -0,0 +1,160 @@ +local t_sort = table.sort +local m_floor = math.floor; +local time = require "socket".gettime; + +local function nop_function() end + +local function percentile(arr, length, pc) + local n = pc/100 * (length + 1); + local k, d = m_floor(n), n%1; + if k == 0 then + return arr[1] or 0; + elseif k >= length then + return arr[length]; + end + return arr[k] + d*(arr[k+1] - arr[k]); +end + +local function new_registry(config) + config = config or {}; + local duration_sample_interval = config.duration_sample_interval or 5; + local duration_max_samples = config.duration_max_stored_samples or 5000; + + local function get_distribution_stats(events, n_actual_events, since, new_time, units) + local n_stored_events = #events; + t_sort(events); + local sum = 0; + for i = 1, n_stored_events do + sum = sum + events[i]; + end + + return { + samples = events; + sample_count = n_stored_events; + count = n_actual_events, + rate = n_actual_events/(new_time-since); + average = n_stored_events > 0 and sum/n_stored_events or 0, + min = events[1] or 0, + max = events[n_stored_events] or 0, + units = units, + }; + end + + + local registry = {}; + local methods; + methods = { + amount = function (name, initial) + local v = initial or 0; + registry[name..":amount"] = function () return "amount", v; end + return function (new_v) v = new_v; end + end; + counter = function (name, initial) + local v = initial or 0; + registry[name..":amount"] = function () return "amount", v; end + return function (delta) + v = v + delta; + end; + end; + rate = function (name) + local since, n = time(), 0; + registry[name..":rate"] = function () + local t = time(); + local stats = { + rate = n/(t-since); + count = n; + }; + since, n = t, 0; + return "rate", stats.rate, stats; + end; + return function () + n = n + 1; + end; + end; + distribution = function (name, unit, type) + type = type or "distribution"; + local events, last_event = {}, 0; + local n_actual_events = 0; + local since = time(); + + registry[name..":"..type] = function () + local new_time = time(); + local stats = get_distribution_stats(events, n_actual_events, since, new_time, unit); + events, last_event = {}, 0; + n_actual_events = 0; + since = new_time; + return type, stats.average, stats; + end; + + return function (value) + n_actual_events = n_actual_events + 1; + if n_actual_events%duration_sample_interval == 1 then + last_event = (last_event%duration_max_samples) + 1; + events[last_event] = value; + end + end; + end; + sizes = function (name) + return methods.distribution(name, "bytes", "size"); + end; + times = function (name) + local events, last_event = {}, 0; + local n_actual_events = 0; + local since = time(); + + registry[name..":duration"] = function () + local new_time = time(); + local stats = get_distribution_stats(events, n_actual_events, since, new_time, "seconds"); + events, last_event = {}, 0; + n_actual_events = 0; + since = new_time; + return "duration", stats.average, stats; + end; + + return function () + n_actual_events = n_actual_events + 1; + if n_actual_events%duration_sample_interval ~= 1 then + return nop_function; + end + + local start_time = time(); + return function () + local end_time = time(); + local duration = end_time - start_time; + last_event = (last_event%duration_max_samples) + 1; + events[last_event] = duration; + end + end; + end; + + get_stats = function () + return registry; + end; + }; + return methods; +end + +return { + new = new_registry; + get_histogram = function (duration, n_buckets) + n_buckets = n_buckets or 100; + local events, n_events = duration.samples, duration.sample_count; + if not (events and n_events) then + return nil, "not a valid distribution stat"; + end + local histogram = {}; + + for i = 1, 100, 100/n_buckets do + histogram[i] = percentile(events, n_events, i); + end + return histogram; + end; + + get_percentile = function (duration, pc) + local events, n_events = duration.samples, duration.sample_count; + if not (events and n_events) then + return nil, "not a valid distribution stat"; + end + return percentile(events, n_events, pc); + end; +} diff --git a/util/template.lua b/util/template.lua index 66d4fca7..04ebb93d 100644 --- a/util/template.lua +++ b/util/template.lua @@ -1,4 +1,4 @@ - +-- luacheck: ignore 213/i local stanza_mt = require "util.stanza".stanza_mt; local setmetatable = setmetatable; local pairs = pairs; @@ -9,7 +9,7 @@ local debug = debug; local t_remove = table.remove; local parse_xml = require "util.xml".parse; -module("template") +local _ENV = nil; local function trim_xml(stanza) for i=#stanza,1,-1 do @@ -67,12 +67,12 @@ end local function create_cloner(stanza, chunkname) local lookup = {}; local name = create_clone_string(stanza, lookup, ""); - local f = "local setmetatable,stanza_mt=...;return function(data)"; + local src = "local setmetatable,stanza_mt=...;return function(data)"; for i=1,#lookup do - f = f.."local _"..i.."="..lookup[i]..";"; + src = src.."local _"..i.."="..lookup[i]..";"; end - f = f.."return "..name..";end"; - local f,err = loadstring(f, chunkname); + src = src.."return "..name..";end"; + local f,err = loadstring(src, chunkname); if not f then error(err); end return f(setmetatable, stanza_mt); end diff --git a/util/termcolours.lua b/util/termcolours.lua index 6ef3b689..279ff601 100644 --- a/util/termcolours.lua +++ b/util/termcolours.lua @@ -1,10 +1,12 @@ -- 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; @@ -19,7 +21,7 @@ if os.getenv("WINDIR") then end local orig_color = windows and windows.get_consolecolor and windows.get_consolecolor(); -module "termcolours" +local _ENV = nil; local stylemap = { reset = 0; bright = 1, dim = 2, underscore = 4, blink = 5, reverse = 7, hidden = 8; @@ -45,7 +47,7 @@ local cssmap = { }; local fmt_string = char(0x1B).."[%sm%s"..char(0x1B).."[0m"; -function getstring(style, text) +local function getstring(style, text) if style then return format(fmt_string, style, text); else @@ -53,7 +55,7 @@ function getstring(style, text) end end -function getstyle(...) +local function getstyle(...) local styles, result = { ... }, {}; for i, style in ipairs(styles) do style = stylemap[style]; @@ -65,7 +67,7 @@ function getstyle(...) end local last = "0"; -function setstyle(style) +local function setstyle(style) style = style or "0"; if style ~= last then io_write("\27["..style.."m"); @@ -82,7 +84,7 @@ if windows then end end if not orig_color then - function setstyle(style) end + function setstyle() end end end @@ -95,8 +97,13 @@ local function ansi2css(ansi_codes) return "</span><span style='"..t_concat(css, ";").."'>"; end -function tohtml(input) +local function tohtml(input) return input:gsub("\027%[(.-)m", ansi2css); end -return _M; +return { + getstring = getstring; + getstyle = getstyle; + setstyle = setstyle; + tohtml = tohtml; +}; diff --git a/util/throttle.lua b/util/throttle.lua index 55e1d07b..3d3f5d2d 100644 --- a/util/throttle.lua +++ b/util/throttle.lua @@ -3,7 +3,7 @@ local gettime = require "socket".gettime; local setmetatable = setmetatable; local floor = math.floor; -module "throttle" +local _ENV = nil; local throttle = {}; local throttle_mt = { __index = throttle }; @@ -39,8 +39,10 @@ function throttle:poll(cost, split) end end -function create(max, period) +local function create(max, period) return setmetatable({ rate = max / period, max = max, t = 0, balance = max }, throttle_mt); end -return _M; +return { + create = create; +}; diff --git a/util/timer.lua b/util/timer.lua index af1e57b6..3713625d 100644 --- a/util/timer.lua +++ b/util/timer.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -17,7 +17,7 @@ local type = type; local data = {}; local new_data = {}; -module "timer" +local _ENV = nil; local _add_task; if not server.event then @@ -42,7 +42,7 @@ if not server.event then end new_data = {}; end - + local next_time = math_huge; for i, d in pairs(data) do local t, callback = d[1], d[2]; @@ -78,6 +78,6 @@ else end end -add_task = _add_task; - -return _M; +return { + add_task = _add_task; +}; diff --git a/util/uuid.lua b/util/uuid.lua index 3576be8f..f4fd21f6 100644 --- a/util/uuid.lua +++ b/util/uuid.lua @@ -1,36 +1,32 @@ -- 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 error = error; -local round_up = math.ceil; -local urandom, urandom_err = io.open("/dev/urandom", "r"); - -module "uuid" +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) - local binary_random = urandom:read(round_up(n/2)); - local hex_random = binary_random:gsub(".", - function (x) return ("%02x"):format(x:byte()) end); - return hex_random:sub(1, n); + return hex(random_bytes(m_ceil(n/2))):sub(1, n); end + local function get_twobits() - return ("%x"):format(urandom:read(1):byte() % 4 + 8); + return ("%x"):format(random_bytes(1):byte() % 4 + 8); end -function generate() - if not urandom then - error("Unable to obtain a secure random number generator, please see https://prosody.im/doc/random ("..urandom_err..")"); - 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 -function seed() -end - -return _M; +return { + get_nibbles=get_nibbles; + generate = generate ; + -- COMPAT + seed = random.seed; +}; diff --git a/util/watchdog.lua b/util/watchdog.lua index bcb2e274..aa8c6486 100644 --- a/util/watchdog.lua +++ b/util/watchdog.lua @@ -2,12 +2,12 @@ local timer = require "util.timer"; local setmetatable = setmetatable; local os_time = os.time; -module "watchdog" +local _ENV = nil; local watchdog_methods = {}; local watchdog_mt = { __index = watchdog_methods }; -function new(timeout, callback) +local function new(timeout, callback) local watchdog = setmetatable({ timeout = timeout, last_reset = os_time(), callback = callback }, watchdog_mt); timer.add_task(timeout+1, function (current_time) local last_reset = watchdog.last_reset; @@ -31,4 +31,6 @@ function watchdog_methods:cancel() self.last_reset = nil; end -return _M; +return { + new = new; +}; diff --git a/util/x509.lua b/util/x509.lua index 19d4ec6d..f228b201 100644 --- a/util/x509.lua +++ b/util/x509.lua @@ -20,13 +20,11 @@ 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 pairs, ipairs = pairs, ipairs; local s_format = string.format; -local t_insert = table.insert; -local t_concat = table.concat; -module "x509" +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 @@ -149,7 +147,10 @@ local function compare_srvname(host, service, asserted_names) return false end -function verify_identity(host, service, cert) +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]; @@ -161,7 +162,9 @@ function verify_identity(host, service, cert) if sans[oid_xmppaddr] then had_supported_altnames = true - if compare_xmppaddr(host, sans[oid_xmppaddr]) then return true end + 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 @@ -212,4 +215,27 @@ function verify_identity(host, service, cert) return false end -return _M; +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; +}; diff --git a/util/xml.lua b/util/xml.lua index 076490fa..733d821a 100644 --- a/util/xml.lua +++ b/util/xml.lua @@ -2,7 +2,7 @@ local st = require "util.stanza"; local lxp = require "lxp"; -module("xml") +local _ENV = nil; local parse_xml = (function() local ns_prefixes = { @@ -11,6 +11,7 @@ local parse_xml = (function() 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) @@ -26,8 +27,8 @@ local parse_xml = (function() attr[i] = nil; local ns, nm = k:match(ns_pattern); if nm ~= "" then - ns = ns_prefixes[ns]; - if ns then + ns = ns_prefixes[ns]; + if ns then attr[ns..":"..nm] = attr[k]; attr[k] = nil; end @@ -38,7 +39,7 @@ local parse_xml = (function() function handler:CharacterData(data) stanza:text(data); end - function handler:EndElement(tagname) + function handler:EndElement() stanza:up(); end local parser = lxp.new(handler, "\1"); @@ -53,5 +54,6 @@ local parse_xml = (function() end; end)(); -parse = parse_xml; -return _M; +return { + parse = parse_xml; +}; diff --git a/util/xmppstream.lua b/util/xmppstream.lua index 138c86b7..7be63285 100644 --- a/util/xmppstream.lua +++ b/util/xmppstream.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -24,7 +24,7 @@ local lxp_supports_bytecount = not not lxp.new({}).getcurrentbytecount; local default_stanza_size_limit = 1024*1024*10; -- 10MB -module "xmppstream" +local _ENV = nil; local new_parser = lxp.new; @@ -40,29 +40,26 @@ local xmlns_streams = "http://etherx.jabber.org/streams"; local ns_separator = "\1"; local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$"; -_M.ns_separator = ns_separator; -_M.ns_pattern = ns_pattern; - local function dummy_cb() end -function new_sax_handlers(session, stream_callbacks, cb_handleprogress) +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; @@ -82,7 +79,7 @@ function new_sax_handlers(session, stream_callbacks, cb_handleprogress) attr.xmlns = curr_ns; non_streamns_depth = non_streamns_depth + 1; end - + for i=1,#attr do local k = attr[i]; attr[i] = nil; @@ -92,7 +89,7 @@ function new_sax_handlers(session, stream_callbacks, cb_handleprogress) 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(); @@ -116,7 +113,7 @@ function new_sax_handlers(session, stream_callbacks, cb_handleprogress) 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 @@ -205,26 +202,26 @@ function new_sax_handlers(session, stream_callbacks, cb_handleprogress) 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 -function new(session, stream_callbacks, stanza_size_limit) +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; @@ -241,6 +238,25 @@ function new(session, stream_callbacks, stanza_size_limit) 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("<?xml version='1.0'?>"); + send(st.stanza("stream:stream", attr):top_tag()); + return true; + end + return { reset = function () parser = new_parser(handlers, ns_separator, false); @@ -262,4 +278,9 @@ function new(session, stream_callbacks, stanza_size_limit) }; end -return _M; +return { + ns_separator = ns_separator; + ns_pattern = ns_pattern; + new_sax_handlers = new_sax_handlers; + new = new; +}; |