-- Dovecot authentication backend for Prosody -- -- Copyright (C) 2010 Javier Torres -- Copyright (C) 2010-2011 Matthew Wild -- Copyright (C) 2010-2011 Waqas Hussain -- Copyright (C) 2011 Kim Alvefur -- pcall(require, "socket.unix"); local datamanager = require "util.datamanager"; local usermanager = require "core.usermanager"; 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 sha1 = require "util.hashes".sha1; local prosody = prosody; local socket_path = module:get_option_string("dovecot_auth_socket", "/var/run/dovecot/auth-login"); local socket_host = module:get_option_string("dovecot_auth_host", "127.0.0.1"); local socket_port = module:get_option_string("dovecot_auth_port"); local append_host = module:get_option_boolean("auth_append_host", false); if not socket_port and not socket.unix then error("LuaSocket was not compiled with UNIX socket support. Try using Dovecot 2.x with inet_listener support, or recompile LuaSocket with UNIX socket support."); end function new_provider(host) local provider = { name = "dovecot", request_id = 0 }; log("debug", "initializing dovecot authentication provider for host '%s'", host); local conn; -- Generate an id for this connection (must be a 31-bit number, unique per process) local pid = tonumber(sha1(host, true):sub(1, 6), 16); -- Closes the socket function provider.close(self) if conn then conn:close(); conn = nil; end end -- The following connects to a new socket and send the handshake function provider.connect(self) -- Destroy old socket provider:close(); local ok, err; if socket_port then log("debug", "connecting to dovecot TCP socket at '%s':'%s'", socket_host, socket_port); conn = socket.tcp(); ok, err = conn:connect(socket_host, socket_port); elseif socket.unix then log("debug", "connecting to dovecot UNIX socket at '%s'", socket_path); conn = socket.unix(); ok, err = conn:connect(socket_path); else err = "luasocket was not compiled with UNIX sockets support"; end if not ok then if socket_port then log("error", "error connecting to dovecot TCP socket at '%s':'%s'. error was '%s'. check permissions", socket_host, socket_port, err); else log("error", "error connecting to dovecot UNIX socket at '%s'. error was '%s'. check permissions", socket_path, err); end provider:close(); return false; end -- Send our handshake log("debug", "sending handshake to dovecot. version 1.1, cpid '%d'", pid); 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; local supported_mechs = {}; while (not done) do local line = provider:receive(); if not line then return false; end log("debug", "dovecot handshake: '%s'", line); local parts = line:gmatch("[^\t]+"); local first = parts(); if first == "VERSION" then -- Version should be 1.1 local major_version = parts(); if major_version ~= "1" then log("error", "dovecot server version is not 1.x. it is %s.x", major_version); provider:close(); return false; end elseif first == "MECH" then local mech = parts(); supported_mechs[mech] = true; elseif first == "DONE" then -- We need PLAIN if not supported_mechs.PLAIN then log("warn", "server doesn't support PLAIN mechanism."); provider:close(); return false; end done = true; end end return true; end -- Wrapper for send(). Handles errors function provider.send(self, data) local ok, err = conn:send(data); if not ok then log("error", "error sending '%s' to dovecot. error was '%s'", data, err); provider:close(); return false; end return true; end -- Wrapper for receive(). Handles errors function provider.receive(self) local line, err = conn:receive(); if not line then log("error", "error receiving data from dovecot. error was '%s'", err); provider:close(); return false; end return line; end function provider.send_auth_request(self, username, password) if not conn then if not provider:connect() then return nil, "Auth failed. Dovecot communications error"; end end -- Send auth data if append_host then username = username .. "@" .. module.host; end local b64 = base64.encode(username .. "\0" .. username .. "\0" .. password); provider.request_id = provider.request_id + 1 % 4294967296 local msg = "AUTH\t" .. provider.request_id .. "\tPLAIN\tservice=XMPP\tresp=" .. b64; log("debug", "sending auth request for '%s' with password '%s': '%s'", username, password, msg); if not provider:send(msg .. "\n") then return nil, "Auth failed. Dovecot communications error"; end -- Get response local line = provider:receive(); log("debug", "got auth response: '%s'", line); if not line then return nil, "Auth failed. Dovecot communications error"; end local parts = line:gmatch("[^\t]+"); -- Check response local status = parts(); local resp_id = tonumber(parts()); if resp_id ~= provider.request_id then log("warn", "dovecot response_id(%s) doesn't match request_id(%s)", resp_id, provider.request_id); provider:close(); return nil, "Auth failed. Dovecot communications error"; end return status, parts; end function provider.test_password(username, password) log("debug", "test password '%s' for user %s at host %s", password, username, module.host); local status, extra = provider:send_auth_request(username, password); if status == "OK" then log("info", "login ok for '%s'", username); return true; else log("info", "login failed for '%s'", username); 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) log("debug", "user_exists for user %s at host %s", username, module.host); -- Send a request. If the response (FAIL) contains an extra -- parameter like user= then it exists. local status, extra = provider:send_auth_request(username, ""); local param = extra(); while param do local parts = param:gmatch("[^=]+"); local name = parts(); local value = parts(); if name == "user" then log("debug", "user '%s' exists", username); return true; end param = extra(); end log("debug", "user '%s' does not exists (or dovecot didn't send user= parameter)", username); return false; end function provider.create_user(username, password) return nil, "Cannot create_user in dovecot backend."; end function provider.get_sasl_handler() local getpass_authentication_profile = { plain_test = function(sasl, 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(module.host, getpass_authentication_profile); end return provider; end module:add_item("auth-provider", new_provider(module.host));