+ return true;
+end
+
+module:hook_stanza(xmlns_sasl, "success", function (session, stanza)
+ if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end
+ module:log("debug", "SASL EXTERNAL with %s succeeded", session.to_host);
+ session.external_auth = "succeeded"
+ session:reset_stream();
+
+ local default_stream_attr = {xmlns = "jabber:server", ["xmlns:stream"] = "http://etherx.jabber.org/streams",
+ ["xmlns:db"] = 'jabber:server:dialback', version = "1.0", to = session.to_host, from = session.from_host};
+ session.sends2s("<?xml version='1.0'?>");
+ session.sends2s(st.stanza("stream:stream", default_stream_attr):top_tag());
+
+ s2s_make_authenticated(session, session.to_host);
+ return true;
+end)
+
+module:hook_stanza(xmlns_sasl, "failure", function (session, stanza)
+ if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end
+
+ module:log("info", "SASL EXTERNAL with %s failed", session.to_host)
+ -- TODO: Log the failure reason
+ session.external_auth = "failed"
+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
+
+ local mechanisms = stanza:get_child("mechanisms", xmlns_sasl)
+ if mechanisms then
+ for mech in mechanisms:childtags() do
+ if mech[1] == "EXTERNAL" then
+ module:log("debug", "Initiating SASL EXTERNAL with %s", session.to_host);
+ local reply = st.stanza("auth", {xmlns = xmlns_sasl, mechanism = "EXTERNAL"});
+ reply:text(base64.encode(session.from_host))
+ session.sends2s(reply)
+ session.external_auth = "attempting"
+ return true
+ end
+ end
+ end
+end, 150);
+
+local function s2s_external_auth(session, stanza)
+ 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
+ return true;
+ end
+
+ if mechanism ~= "EXTERNAL" or session.cert_chain_status ~= "valid" then
+ session.sends2s(build_reply("failure", "invalid-mechanism"))
+ return true;
+ end
+
+ local text = stanza[1]
+ if not text then
+ 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)
+ if not text then
+ 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
+ end
+
+ session.external_auth = "succeeded"
+
+ if not session.from_host then
+ session.from_host = text;
+ 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);
+ s2s_make_authenticated(session, domain);
+ session:reset_stream();
+ return true