2 -- Copyright (C) 2008-2009 Matthew Wild
3 -- Copyright (C) 2008-2009 Waqas Hussain
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
11 local st = require "util.stanza";
12 local sm_bind_resource = require "core.sessionmanager".bind_resource;
13 local sm_make_authenticated = require "core.sessionmanager".make_authenticated;
14 local base64 = require "util.encodings".base64;
15 local to_unicode = require "util.encodings".idna.to_unicode;
17 local datamanager_load = require "util.datamanager".load;
18 local usermanager_validate_credentials = require "core.usermanager".validate_credentials;
19 local t_concat, t_insert = table.concat, table.insert;
20 local tostring = tostring;
21 local jid_split = require "util.jid".split
22 local md5 = require "util.hashes".md5;
23 local config = require "core.configmanager";
25 local secure_auth_only = config.get(module:get_host(), "core", "require_encryption");
27 local log = module._log;
29 local xmlns_sasl ='urn:ietf:params:xml:ns:xmpp-sasl';
30 local xmlns_bind ='urn:ietf:params:xml:ns:xmpp-bind';
31 local xmlns_stanzas ='urn:ietf:params:xml:ns:xmpp-stanzas';
33 local new_sasl = require "util.sasl".new;
35 local function build_reply(status, ret, err_msg)
36 local reply = st.stanza(status, {xmlns = xmlns_sasl});
37 if status == "challenge" then
38 log("debug", "%s", ret or "");
39 reply:text(base64.encode(ret or ""));
40 elseif status == "failure" then
42 if err_msg then reply:tag("text"):text(err_msg); end
43 elseif status == "success" then
44 log("debug", "%s", ret or "");
45 reply:text(base64.encode(ret or ""));
47 module:log("error", "Unknown sasl status: %s", status);
52 local function handle_status(session, status)
53 if status == "failure" then
54 session.sasl_handler = nil;
55 elseif status == "success" then
56 if not session.sasl_handler.username then -- TODO move this to sessionmanager
57 module:log("warn", "SASL succeeded but we didn't get a username!");
58 session.sasl_handler = nil;
59 session:reset_stream();
62 sm_make_authenticated(session, session.sasl_handler.username);
63 session.sasl_handler = nil;
64 session:reset_stream();
68 local function password_callback(node, hostname, realm, mechanism, decoder)
69 local password = (datamanager_load(node, to_unicode(hostname), "accounts") or {}).password; -- FIXME handle hashed passwords
70 local func = function(x) return x; end;
72 if mechanism == "PLAIN" then
73 return func, password;
74 elseif mechanism == "DIGEST-MD5" then
75 if decoder then node, hostname, password = decoder(node), decoder(hostname), decoder(password); end
76 return func, md5(node..":"..realm..":"..password);
82 local function sasl_handler(session, stanza)
83 if stanza.name == "auth" then
84 -- FIXME ignoring duplicates because ejabberd does
85 if config.get(session.host or "*", "core", "anonymous_login") then
86 if stanza.attr.mechanism ~= "ANONYMOUS" then
87 return session.send(build_reply("failure", "invalid-mechanism"));
89 elseif stanza.attr.mechanism == "ANONYMOUS" then
90 return session.send(build_reply("failure", "mechanism-too-weak"));
92 session.sasl_handler = new_sasl(stanza.attr.mechanism, session.host, password_callback);
93 if not session.sasl_handler then
94 return session.send(build_reply("failure", "invalid-mechanism"));
96 elseif not session.sasl_handler then
97 return; -- FIXME ignoring out of order stanzas because ejabberd does
99 local text = stanza[1];
101 text = base64.decode(text);
102 log("debug", "%s", text);
104 session.sasl_handler = nil;
105 session.send(build_reply("failure", "incorrect-encoding"));
109 local status, ret, err_msg = session.sasl_handler:feed(text);
110 handle_status(session, status);
111 local s = build_reply(status, ret, err_msg);
112 log("debug", "sasl reply: %s", tostring(s));
116 module:add_handler("c2s_unauthed", "auth", xmlns_sasl, sasl_handler);
117 module:add_handler("c2s_unauthed", "abort", xmlns_sasl, sasl_handler);
118 module:add_handler("c2s_unauthed", "response", xmlns_sasl, sasl_handler);
120 local mechanisms_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' };
121 local bind_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-bind' };
122 local xmpp_session_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-session' };
123 module:add_event_hook("stream-features",
124 function (session, features)
125 if not session.username then
126 if secure_auth_only and not session.secure then
129 features:tag("mechanisms", mechanisms_attr);
130 -- TODO: Provide PLAIN only if TLS is active, this is a SHOULD from the introduction of RFC 4616. This behavior could be overridden via configuration but will issuing a warning or so.
131 if config.get(session.host or "*", "core", "anonymous_login") then
132 features:tag("mechanism"):text("ANONYMOUS"):up();
134 features:tag("mechanism"):text("DIGEST-MD5"):up();
135 features:tag("mechanism"):text("PLAIN"):up();
139 features:tag("bind", bind_attr):tag("required"):up():up();
140 features:tag("session", xmpp_session_attr):up();
144 module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-bind",
145 function (session, stanza)
146 log("debug", "Client requesting a resource bind");
148 if stanza.attr.type == "set" then
149 local bind = stanza.tags[1];
150 if bind and bind.attr.xmlns == xmlns_bind then
151 resource = bind:child_with_name("resource");
153 resource = resource[1];
157 local success, err_type, err, err_msg = sm_bind_resource(session, resource);
159 session.send(st.error_reply(stanza, err_type, err, err_msg));
161 session.send(st.reply(stanza)
162 :tag("bind", { xmlns = xmlns_bind})
163 :tag("jid"):text(session.full_jid));
167 module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-session",
168 function (session, stanza)
169 log("debug", "Client requesting a session");
170 session.send(st.reply(stanza));