# HG changeset patch # User Kim Alvefur # Date 1372376315 -7200 # Node ID 6f8e7f65f704208cf9f25ef2d1c6afd1dc0eec60 # Parent 447af80a16ad839d9423c1d13c6cdfb1f9427f29 mod_net_dovecotauth: Initial commit of server implementation of the Dovecot authentication protocol diff -r 447af80a16ad -r 6f8e7f65f704 mod_net_dovecotauth/mod_net_dovecotauth.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_net_dovecotauth/mod_net_dovecotauth.lua Fri Jun 28 01:38:35 2013 +0200 @@ -0,0 +1,177 @@ +-- mod_net_dovecotauth.lua +-- +-- Protocol spec: +-- http://dovecot.org/doc/auth-protocol.txt +-- +-- Example postfix config: +-- sudo postconf smtpd_sasl_path=inet:127.0.0.1:28484 +-- sudo postconf smtpd_sasl_type=dovecot +-- sudo postconf smtpd_sasl_auth_enable=yes + +module:set_global(); + +-- Imports +local new_sasl = require "core.usermanager".get_sasl_handler; +local user_exists = require "core.usermanager".user_exists; +local base64 = require"util.encodings".base64; +local new_buffer = module:require"buffer".new; +local dump = require"util.serialization".serialize; + +-- Config +local vhost = module:get_option_string("dovecotauth_host", (next(hosts))); -- TODO Is there a better solution? +local allow_master = module:get_option_boolean("adovecotauth_allow_master", false); + +-- Active sessions +local sessions = {}; + +-- Session methods +local new_session; +do +local sess = { }; +local sess_mt = { __index = sess }; + +function new_session(conn) + local sess = { type = "?", conn = conn, buf = assert(new_buffer()), sasl = {} } + function sess:log(l, m, ...) + return module:log(l, self.type..tonumber(tostring(self):match("%x+$"), 16)..": "..m, ...); + end + return setmetatable(sess, sess_mt); +end + +function sess:send(...) + local data = table.concat({...}, "\t") .. "\n" + -- self:log("debug", "SEND: %s", dump(ret)); + return self.conn:write(data); +end + +local mech_params = { + ANONYMOUS = "anonymous"; + PLAIN = "plaintext"; + ["DIGEST-MD5"] = "mutual-auth"; + ["SCRAM-SHA-1"] = "mutual-auth"; + ["SCRAM-SHA-1-PLUS"] = "mutual-auth"; +} + +function sess:handshake() + self:send("VERSION", 1, 1); + self:send("SPID", pposix.getpid()); + self:send("CUID", tonumber(tostring(self):match"%x+$", 16)); + for mech in pairs(self.g_sasl:mechanisms()) do + self:send("MECH", mech, mech_params[mech]); + end + self:send("DONE"); +end + +function sess:feed(data) + -- TODO break this up a bit + -- module:log("debug", "sess = %s", dump(self)); + local buf = self.buf; + buf:write(data); + local line = buf:read("*l") + while line and line ~= "" do + local part = line:gmatch("[^\t]+"); + local command = part(); + if command == "VERSION" then + local major = tonumber(part()); + local minor = tonumber(part()); + if major ~= 1 then + self:log("warn", "Wrong version, expected 1.1, got %s.%s", tostring(major), tostring(minor)); + self.conn:close(); + break; + end + elseif command == "CPID" then + self.type = "C"; + self.pid = part(); + elseif command == "SPID" and allow_master then + self.type = "M"; + self.pid = part(); + elseif command == "AUTH" and self.type ~= "?" then + -- C: "AUTH" TAB TAB TAB service= [TAB ] + local id = part() -- + local sasl = self.sasl[id]; + local mech = part(); + if not sasl then + -- TODO Should maybe initialize SASL handler after parsing the line? + sasl = self.g_sasl:clean_clone(); + self.sasl[id] = sasl; + if not sasl:select(mech) then + self:send("FAIL", id, "reason=invalid-mechanism"); + self.sasl[id] = nil; + sasl = false + end + end + if sasl then + local params = {}; -- Not used for anything yet + for p in part do + local k,v = p:match("^([^=]*)=(.*)$"); + if k == "resp" then + self:log("debug", "params = %s", dump(params)); + v = base64.decode(v); + local status, ret, err = sasl:process(v); + self:log("debug", status); + if status == "challenge" then + self:send("CONT", id, base64.encode(ret)); + elseif status == "failure" then + self.sasl[id] = nil; + self:send("FAIL", id, "reason="..tostring(err)); + elseif status == "success" then + self.sasl[id] = nil; + self:send("OK", id, "user="..sasl.username, ret and "resp="..base64.encode(ret)); + end + break; -- resp MUST be the last param + else + params[k or p] = v or true; + end + end + end + elseif command == "USER" and self.type == "M" then + -- FIXME Should this be on a separate listener? + local id = part(); + local user = part(); + if user and user_exists(user, vhost) then + self:send("USER", id); + else + self:send("NOTFOUND", id); + end + else + self:log("warn", "Unhandled command %s", tostring(command)); + self.conn:close(); + break; + end + line = buf:read("*l"); + end +end + +end + +local listener = {} + +function listener.onconnect(conn) + s = new_session(conn); + sessions[conn] = s; + local g_sasl = new_sasl(vhost, s); + s.g_sasl = g_sasl; + s:handshake(); +end + +function listener.onincoming(conn, data) + local s = sessions[conn]; + -- s:log("debug", "RECV %s", dump(data)); + return s:feed(data); +end + +function listener.ondisconnect(conn) + sessions[conn] = nil; +end + +function module.unload() + for c in pairs(sessions) do + c:close(); + end +end + +module:provides("net", { + default_port = 28484; + listener = listener; +}); +