Diff

mod_net_dovecotauth/mod_net_dovecotauth.lua @ 1088:6f8e7f65f704

mod_net_dovecotauth: Initial commit of server implementation of the Dovecot authentication protocol
author Kim Alvefur <zash@zash.se>
date Fri, 28 Jun 2013 01:38:35 +0200
child 1491:e7294423512f
line wrap: on
line diff
--- /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 <id> TAB <mechanism> TAB service=<service> [TAB <parameters>]
+			local id = part() -- <id>
+			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;
+});
+