# HG changeset patch # User Kim Alvefur # Date 1569622573 -7200 # Node ID 5ae2e865eea05e5e860340dacedc74e291363ea7 # Parent d14fc974efbc5c7391d787946042a2cdf9a17db5 mod_sasl2: Experimental implementation of XEP-0388 diff -r d14fc974efbc -r 5ae2e865eea0 mod_sasl2/mod_sasl2.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_sasl2/mod_sasl2.lua Sat Sep 28 00:16:13 2019 +0200 @@ -0,0 +1,147 @@ +-- Prosody IM +-- Copyright (C) 2019 Kim Alvefur +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- +-- XEP-0388: Extensible SASL Profile +-- + +local st = require "util.stanza"; +local errors = require "util.error"; +local base64 = require "util.encodings".base64; +local jid_join = require "util.jid".join; + +local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler; +local sm_make_authenticated = require "core.sessionmanager".make_authenticated; + +local xmlns_sasl2 = "urn:xmpp:sasl:1"; + +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", { "DIGEST-MD5" }); + +local host = module.host; + +local function tls_unique(self) + return self.userdata["tls-unique"]:getpeerfinished(); +end + +module:hook("stream-features", function(event) + local origin, features = event.origin, event.features; + local log = origin.log or module._log; + + if origin.type ~= "c2s_unauthed" then + log("debug", "Already authenticated"); + return + end + + local sasl_handler = usermanager_get_sasl_handler(host, origin) + origin.sasl_handler = sasl_handler; + + 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 + + local mechanisms = st.stanza("mechanisms", { xmlns = xmlns_sasl2 }); + + local available_mechanisms = sasl_handler:mechanisms() + for mechanism in pairs(available_mechanisms) do + if disabled_mechanisms:contains(mechanism) then + log("debug", "Not offering disabled mechanism %s", mechanism); + elseif not origin.secure and insecure_mechanisms:contains(mechanism) then + log("debug", "Not offering mechanism %s on insecure connection", mechanism); + else + log("debug", "Offering mechanism %s", mechanism); + mechanisms:text_tag("mechanism", mechanism); + end + end + + features:add_direct_child(mechanisms); +end, 1); + +local function handle_status(session, status, ret, err_msg) + local err = nil; + if status == "error" then + ret, err = nil, ret; + if not errors.is_err(err) then + err = errors.new({ condition = err, text = err_msg }, { session = session }); + end + end + + module:fire_event("sasl2/"..session.base_type.."/"..status, { + session = session, + message = ret; + error = err; + }); +end + +module:hook("sasl2/c2s/failure", function (event) + local session = event.session + session.send(st.stanza("failure", { xmlns = xmlns_sasl2 }) + :tag(event.error.condition)); + return true; +end); + +module:hook("sasl2/c2s/challenge", function (event) + local session = event.session; + session.send(st.stanza("challenge", { xmlns = xmlns_sasl2 }) + :text_tag(event.message)); +end); + +module:hook("sasl2/c2s/success", function (event) + local session = event.session + local ok, err = sm_make_authenticated(session, session.sasl_handler.username); + if not ok then + handle_status(session, "failure", err); + return true; + end + event.success = st.stanza("success", { xmlns = xmlns_sasl2 }); +end, 1000); + +module:hook("sasl2/c2s/success", function (event) + local session = event.session + event.success:text_tag("authorization-identifier", jid_join(session.username, session.host, session.resource)); + session.send(event.success); + local features = st.stanza("stream:features"); + module:fire_event("stream-features", { origin = session, features = features }); + session.send(features); +end, -1000); + +local function process_cdata(session, cdata) + if cdata then + cdata = base64.decode(cdata); + if not cdata then + return handle_status(session, "failure"); + end + end + return handle_status(session, session.sasl_handler:process(cdata)); +end + +module:hook_tag(xmlns_sasl2, "authenticate", function (session, auth) + local sasl_handler = session.sasl_handler; + if not sasl_handler then + sasl_handler = usermanager_get_sasl_handler(host, session); + session.sasl_handler = sasl_handler; + end + local mechanism = assert(auth.attr.mechanism); + if not sasl_handler:select(mechanism) then + return handle_status(session, "failure"); + end + local initial = auth:get_child_text("initial-response"); + return process_cdata(session, initial); +end); + +module:hook_tag(xmlns_sasl2, "response", function (session, response) + local sasl_handler = session.sasl_handler; + if not sasl_handler or not sasl_handler.selected then + return handle_status(session, "failure"); + end + return process_cdata(session, response:get_text()); +end);