Software / code / prosody-modules
File
mod_captcha_registration/modules/mod_register.lua @ 6110:1a6cd0bbb7ab
mod_compliance_2023: Add 2023 Version of the compliance module, basis is the 2021 Version.
diff --git a/mod_compliance_2023/README.md b/mod_compliance_2023/README.md
new file mode 100644
--- /dev/null
+++ b/mod_compliance_2023/README.md
@@ -0,0 +1,22 @@
+---
+summary: XMPP Compliance Suites 2023 self-test
+labels:
+- Stage-Beta
+rockspec:
+ dependencies:
+ - mod_cloud_notify
+
+...
+
+Compare the list of enabled modules with
+[XEP-0479: XMPP Compliance Suites 2023] and produce basic report to the
+Prosody log file.
+
+If installed with the Prosody plugin installer then all modules needed for a green checkmark should be included. (With prosody 0.12 only [mod_cloud_notify] is not included with prosody and we need the community module)
+
+# Compatibility
+
+ Prosody-Version Status
+ --------------- ----------------------
+ trunk Works as of 2024-12-21
+ 0.12 Works
diff --git a/mod_compliance_2023/mod_compliance_2023.lua b/mod_compliance_2023/mod_compliance_2023.lua
new file mode 100644
--- /dev/null
+++ b/mod_compliance_2023/mod_compliance_2023.lua
@@ -0,0 +1,79 @@
+-- Copyright (c) 2021 Kim Alvefur
+--
+-- This module is MIT licensed.
+
+local hostmanager = require "core.hostmanager";
+
+local array = require "util.array";
+local set = require "util.set";
+
+local modules_enabled = module:get_option_inherited_set("modules_enabled");
+
+for host in pairs(hostmanager.get_children(module.host)) do
+ local component = module:context(host):get_option_string("component_module");
+ if component then
+ modules_enabled:add(component);
+ modules_enabled:include(module:context(host):get_option_set("modules_enabled", {}));
+ end
+end
+
+local function check(suggested, alternate, ...)
+ if set.intersection(modules_enabled, set.new({suggested; alternate; ...})):empty() then return suggested; end
+ return false;
+end
+
+local compliance = {
+ array {"Server"; check("tls"); check("disco")};
+
+ array {"Advanced Server"; check("pep", "pep_simple")};
+
+ array {"Web"; check("bosh"); check("websocket")};
+
+ -- No Server requirements for Advanced Web
+
+ array {"IM"; check("vcard_legacy", "vcard"); check("carbons"); check("http_file_share", "http_upload")};
+
+ array {
+ "Advanced IM";
+ check("vcard_legacy", "vcard");
+ check("blocklist");
+ check("muc");
+ check("private");
+ check("smacks");
+ check("mam");
+ check("bookmarks");
+ };
+
+ array {"Mobile"; check("smacks"); check("csi_simple", "csi_battery_saver")};
+
+ array {"Advanced Mobile"; check("cloud_notify")};
+
+ array {"A/V Calling"; check("turn_external", "external_services", "turncredentials", "extdisco")};
+
+};
+
+function check_compliance()
+ local compliant = true;
+ for _, suite in ipairs(compliance) do
+ local section = suite:pop(1);
+ if module:get_option_boolean("compliance_" .. section:lower():gsub("%A", "_"), true) then
+ local missing = set.new(suite:filter(function(m) return type(m) == "string" end):map(function(m) return "mod_" .. m end));
+ if suite[1] then
+ if compliant then
+ compliant = false;
+ module:log("warn", "Missing some modules for XMPP Compliance 2023");
+ end
+ module:log("info", "%s Compliance: %s", section, missing);
+ end
+ end
+ end
+
+ if compliant then module:log("info", "XMPP Compliance 2023: Compliant ✔️"); end
+end
+
+if prosody.start_time then
+ check_compliance()
+else
+ module:hook_global("server-started", check_compliance);
+end
+
| author | Menel <menel@snikket.de> |
|---|---|
| date | Sun, 22 Dec 2024 16:06:28 +0100 |
| parent | 2728:11fdfd73a527 |
line wrap: on
line source
-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Modifications copyright (C) 2014 mrDoctorWho -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st = require "util.stanza"; local dataform_new = require "util.dataforms".new; 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 timer = require "util.timer"; local math = require "math"; local captcha = require "captcha"; local base64 = require "util.encodings".base64.encode; local sha1 = require "util.hashes".sha1; local captcha_ids = {}; local config = module:get_option("captcha_config") or {}; local compat = module:get_option_boolean("registration_compat", true); local allow_registration = module:get_option_boolean("allow_registration", false); local additional_fields = module:get_option("additional_registration_fields", {}); local account_details = module:open_store("account_details"); local field_map = { username = { name = "username", type = "text-single", label = "Username", required = true }; password = { name = "password", type = "text-private", label = "Password", required = true }; nick = { name = "nick", type = "text-single", label = "Nickname" }; name = { name = "name", type = "text-single", label = "Full Name" }; first = { name = "first", type = "text-single", label = "Given Name" }; last = { name = "last", type = "text-single", label = "Family Name" }; email = { name = "email", type = "text-single", label = "Email" }; address = { name = "address", type = "text-single", label = "Street" }; city = { name = "city", type = "text-single", label = "City" }; state = { name = "state", type = "text-single", label = "State" }; zip = { name = "zip", type = "text-single", label = "Postal code" }; phone = { name = "phone", type = "text-single", label = "Telephone number" }; url = { name = "url", type = "text-single", label = "Webpage" }; date = { name = "date", type = "text-single", label = "Birth date" }; -- something new formtype = { name = "FORM_TYPE", type = "hidden"}; captcha_text = { name = "captcha_text", type = "fixed", label = "Warning: "}; captcha_psi = { name = "captchahidden", type = "hidden" }; -- Don't know exactly why, but it exists in ejabberd register form captcha_url = { name = "url", type = "text-single", label = "Captcha url"}; from = { name = "from", type = "hidden" }; captcha_challenge = { name = "challenge", type = "hidden" }; sid = { name = "sid", type = "hidden" }; ocr = { name = "ocr", label = "Enter shown text", required = true, type = "media" } }; local registration_form = dataform_new{ field_map.formtype; field_map.username; field_map.password; field_map.captcha_text; -- field_map.captcha_psi; -- Maybe later, i really have no idea why it used in ejabberd reg form field_map.captcha_url; field_map.from; field_map.captcha_challenge; field_map.sid; field_map.ocr; }; function delete_captcha(cid) os.remove(string.format("%s/%s.png", config.dir, cid)) captcha_ids[cid] = nil; end for _, field in ipairs(additional_fields) do if type(field) == "table" then registration_form[#registration_form + 1] = field; else if field:match("%+$") then field = field:sub(1, #field - 1); field_map[field].required = true; end registration_form[#registration_form + 1] = field_map[field]; registration_query:tag(field):up(); end end 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; -- Advertise registration to unauthorized clients only. if not(allow_registration) or session.type ~= "c2s_unauthed" then return end features:add_child(register_stream_feature); end); local function handle_registration_stanza(event) local session, stanza = event.origin, event.stanza; local query = stanza.tags[1]; if stanza.attr.type == "get" then local reply = st.reply(stanza); reply:tag("query", {xmlns = "jabber:iq:register"}) :tag("registered"):up() :tag("username"):text(session.username):up() :tag("password"):up(); session.send(reply); else -- stanza.attr.type == "set" if query.tags[1] and query.tags[1].name == "remove" then local username, host = session.username, session.host; 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); 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); module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session }); else local username = nodeprep(query:get_child("username"):get_text()); local password = query:get_child("password"):get_text(); if username and password then if username == session.username then if usermanager_set_password(username, password, session.host) then session.send(st.reply(stanza)); else -- TODO unable to write file, file may be locked, etc, what's the correct error? session.send(st.error_reply(stanza, "wait", "internal-server-error")); end else session.send(st.error_reply(stanza, "modify", "bad-request")); end else session.send(st.error_reply(stanza, "modify", "bad-request")); end end end return true; end module:hook("iq/self/jabber:iq:register:query", handle_registration_stanza); if compat then module:hook("iq/host/jabber:iq:register:query", function (event) local session, stanza = event.origin, event.stanza; if session.type == "c2s" and jid_bare(stanza.attr.to) == session.host then return handle_registration_stanza(event); end end); end local function parse_response(query) local form = query:get_child("x", "jabber:x:data"); if form then return registration_form:data(form); else local data = {}; local errors = {}; for _, field in ipairs(registration_form) do local name, required = field.name, field.required; if field_map[name] then data[name] = query:get_child_text(name); if (not data[name] or #data[name] == 0) and required then errors[name] = "Required value missing"; end end end if next(errors) then return data, errors; end return data; 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", "::1" }; local blacklisted_ips = module:get_option("registration_blacklist") or {}; 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 function get_file(name) local file = io.open(name, "rb") local data = file:read("*all") file:close() return data end local function get_captcha() local cap = captcha.new(); math.randomseed(os_time()); local cid = tostring(math.random(1000, 90000)); -- random cid used for cap name cap:font(config.font); cap:scribble(); captcha_ids[cid] = cap:write(string.format("%s/%s.png", config.dir, cid)):lower(); timer.add_task(config.timeout, function() delete_captcha(cid) end); -- Add new function to use arguments. Is there any other way in lua? Or it even works? return cid end module:hook("stanza/iq/jabber:iq:register:query", function(event) local session, stanza = event.origin, event.stanza; if not(allow_registration) or session.type ~= "c2s_unauthed" then session.send(st.error_reply(stanza, "cancel", "service-unavailable")); else local query = stanza.tags[1]; if stanza.attr.type == "get" then local reply = st.reply(stanza):query("jabber:iq:register"); -- TODO: Move this in standalone function local challenge = get_captcha() local captcha_data = get_file(config.dir.."/"..challenge..".png") local captcha_sha = sha1(captcha_data, true) -- omg local captcha_base64 = base64(captcha_data) -- lol wut xml = registration_form:form(({FORM_TYPE = "urn:xmpp:captcha", from = session.host, ocr = {{ type = "image/png", uri = string.format("cid:sha1+%s@bob.xmpp.org", captcha_sha) }}; url = string.format("http://%s:5280/%s/%s", session.host, config.web_path, challenge); captcha_text = "If you can't see an image, follow link below"; challenge = challenge; sid = "1"; })); data = st.stanza("data", {xmlns = "urn:xmpp:bob", cid = string.format("sha1+%s@bob.xmpp.org", captcha_sha), type = "image/png", ["max-age"] = config.timeout}) :text(captcha_base64); reply = reply:add_child(xml); reply = reply:add_child(data); session.send(reply); elseif stanza.attr.type == "set" then if query.tags[1] and query.tags[1].name == "remove" then session.send(st.error_reply(stanza, "auth", "registration-required")); else local data, errors = parse_response(query); if errors then session.send(st.error_reply(stanza, "modify", "not-acceptable")); 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"); 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(); end end local host = module.host; local ocr = data.ocr:lower(); local challenge = data.challenge; local username, password = nodeprep(data.username), data.password; data.username, data.password = nil, nil; if challenge == nil or captcha_ids[challenge] == nil then session.send(st.error_reply(stanza, "modify", "not-acceptable", "Captcha id is invalid or it has expired")); delete_captcha(challenge); return true; elseif ocr ~= captcha_ids[challenge] then session.send(st.error_reply(stanza, "modify", "not-acceptable", "Invalid captcha text")); delete_captcha(challenge); return true; end if not username or username == "" then session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is invalid.")); delete_captcha(challenge); return true; end local user = { username = username , host = host, allowed = true } module:fire_event("user-registering", user); if not user.allowed then delete_captcha(challenge); session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is forbidden.")); elseif usermanager_user_exists(username, host) then delete_captcha(challenge) session.send(st.error_reply(stanza, "cancel", "conflict", "The requested username already exists.")); else -- TODO unable to write file, file may be locked, etc, what's the correct error? local error_reply = st.error_reply(stanza, "wait", "internal-server-error", "Failed to write data to disk."); if usermanager_create_user(username, password, host) then if next(data) and not account_details:set(username, data) then delete_captcha(challenge); usermanager_delete_user(username, host); session.send(error_reply); return true; end session.send(st.reply(stanza)); -- user created! module:log("info", "User account created: %s@%s", username, host); module:fire_event("user-registered", { username = username, host = host, source = "mod_register", session = session }); else delete_captcha(challenge); session.send(error_reply); end end end end end end return true; end); function string:split(sep) local sep, fields = sep or ":", {} local pattern = string.format("([^%s]+)", sep) self:gsub(pattern, function(c) fields[#fields+1] = c end) return fields end function handle_http_request(event) local request = event.request; local path = request.path; local cid = path:split("/")[2]; if cid == nil or captcha_ids[cid] == nil then return nil; end request.response = { status_code = 200; headers = { content_type = "image/png" }; body = get_file(string.format("%s/%s.png", config.dir, cid)); }; return request.response; end; module:provides("http", { default_path = "/"..config.web_path; route = { ["GET /*"] = handle_http_request; }; });