view mod_auth_dovecot/mod_auth_dovecot.lua @ 271:05ea4abb664d

mod_auth_dovecot: Load dovecot socket path form config
author Javier Torres <javitonino@gmail.com>
date Sat, 30 Oct 2010 23:39:22 +0200
parents 853ae6ae87bf
children 6b35c23664db
line wrap: on
line source

-- Dovecot authentication backend for Prosody
--
-- Copyright (C) 2010 Javier Torres
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
--

local socket_unix = require "socket.unix";
local datamanager = require "util.datamanager";
local log = require "util.logger".init("auth_dovecot");
local new_sasl = require "util.sasl".new;
local nodeprep = require "util.encodings".stringprep.nodeprep;
local base64 = require "util.encodings".base64;
local pposix = require "util.pposix";

local prosody = _G.prosody;
local socket_path = module:get_option_string("dovecot_auth_socket", "/var/run/dovecot/auth-login");

function new_default_provider(host)
	local provider = { name = "dovecot", c = nil };
	log("debug", "initializing dovecot authentication provider for host '%s'", host);
	
	-- Closes the socket
	function provider.close(self)
		if (provider.c ~= nil) then
			provider.c:close();
		end
		provider.c = nil;
	end
	
	-- The following connects to a new socket and send the handshake
	function provider.connect(self)
		-- Destroy old socket
		provider:close();
		
		provider.c = socket.unix();
		
		-- Create a connection to dovecot socket
		local r, e = provider.c:connect(socket_path);
		if (not r) then
			log("warn", "error connecting to dovecot socket at '%s'. error was '%s'. check permissions", socket_path, e);
			provider:close();
			return false;
		end
		
		-- Send our handshake
		local pid = pposix.getpid();
		if not provider:send("VERSION\t1\t1\n") then
			return false
		end
		if (not provider:send("CPID\t" .. pid .. "\n")) then
			return false
		end
		
		-- Parse Dovecot's handshake
		local done = false;
		while (not done) do
			local l = provider:receive();
			if (not l) then
				return false;
			end
			
			parts = string.gmatch(l, "[^\t]+");
			first = parts();
			if (first == "VERSION") then
				-- Version should be 1.1
				local v1 = parts();
				local v2 = parts();
				
				if (not (v1 == "1" and v2 == "1")) then
					log("warn", "server version is not 1.1. it is %s.%s", v1, v2);
					provider:close();
					return false;
				end
			elseif (first == "MECH") then
				-- Mechanisms should include PLAIN
				local ok = false;
				for p in parts do
					if p == "PLAIN" then
						ok = true;
					end
				end
				if (not ok) then
					log("warn", "server doesn't support PLAIN mechanism. It supports '%s'", l);
					provider:close();
					return false;
				end
			elseif (first == "DONE") then
				done = true;
			end
		end
		return true;
	end
	
	-- Wrapper for send(). Handles errors
	function provider.send(self, data)
		local r, e = provider.c:send(data);
		if (not r) then
			log("warn", "error sending '%s' to dovecot. error was '%s'", data, e);
			provider:close();
			return false;
		end
		return true;
	end
	
	-- Wrapper for receive(). Handles errors
	function provider.receive(self)
		local r, e = provider.c:receive();
		if (not r) then
			log("warn", "error receiving data from dovecot. error was '%s'", socket, e);
			provider:close();
			return false;
		end
		return r;
	end
	
	function provider.test_password(username, password)
		log("debug", "test password '%s' for user %s at host %s", password, username, module.host);
		
		local tries = 0;
		
		if (provider.c == nil or tries > 0) then
			if (not provider:connect()) then
				return nil, "Auth failed. Dovecot communications error";
			end
		end
		
		-- Send auth data
		username = username .. "@" .. module.host; -- FIXME: this is actually a hack for my server
		local b64 = base64.encode(username .. "\0" .. username .. "\0" .. password);
		local id = "54321"; -- FIXME: probably can just be a fixed value if making one request per connection
		if (not provider:send("AUTH\t" .. id .. "\tPLAIN\tservice=XMPP\tresp=" .. b64 .. "\n")) then
			return nil, "Auth failed. Dovecot communications error";
		end
		
		-- Get response
		local l = provider:receive();
		if (not l) then
			return nil, "Auth failed. Dovecot communications error";
		end
		local parts = string.gmatch(l, "[^\t]+");
		
		-- Check response
		if (parts() == "OK") then
			return true;
		else
			return nil, "Auth failed. Invalid username or password.";
		end
	end

	function provider.get_password(username)
		return nil, "Cannot get_password in dovecot backend.";
	end
	
	function provider.set_password(username, password)
		return nil, "Cannot set_password in dovecot backend.";
	end

	function provider.user_exists(username)
		--TODO: Send an auth request. If it returns FAIL <id> user=<user> then user exists.
		return nil, "user_exists not yet implemented in dovecot backend.";
	end

	function provider.create_user(username, password)
		return nil, "Cannot create_user in dovecot backend.";
	end

	function provider.get_sasl_handler()
		local realm = module:get_option("sasl_realm") or module.host;
		local getpass_authentication_profile = {
			plain_test = function(username, password, realm)
			local prepped_username = nodeprep(username);
			if not prepped_username then
				log("debug", "NODEprep failed on username: %s", username);
				return "", nil;
			end
			return usermanager.test_password(prepped_username, realm, password), true;
		end
		};
		return new_sasl(realm, getpass_authentication_profile);
	end
	
	return provider;
end

module:add_item("auth-provider", new_default_provider(module.host));