mod_auth_dovecot/auth_dovecot/sasl_dovecot.lib.lua
changeset 474 942738953ff3
child 700 0c130c45b7c1
equal deleted inserted replaced
473:99b246b37809 474:942738953ff3
       
     1 -- Dovecot authentication backend for Prosody
       
     2 --
       
     3 -- Copyright (C) 2008-2009 Tobias Markmann
       
     4 -- Copyright (C) 2010 Javier Torres
       
     5 -- Copyright (C) 2010-2011 Matthew Wild
       
     6 -- Copyright (C) 2010-2011 Waqas Hussain
       
     7 -- Copyright (C) 2011 Kim Alvefur
       
     8 --
       
     9 --    Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
       
    10 --
       
    11 --        * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
       
    12 --        * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
       
    13 --        * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
       
    14 --
       
    15 --    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
       
    16 
       
    17 -- This code is based on util.sasl_cyrus and the old mod_auth_dovecot
       
    18 
       
    19 local log = require "util.logger".init("sasl_dovecot");
       
    20 
       
    21 local setmetatable = setmetatable;
       
    22 
       
    23 local s_match, s_gmatch = string.match, string.gmatch
       
    24 local t_concat = table.concat;
       
    25 local m_random = math.random;
       
    26 local tostring, tonumber = tostring, tonumber;
       
    27 
       
    28 local socket = require "socket"
       
    29 pcall(require, "socket.unix");
       
    30 local base64 = require "util.encodings".base64;
       
    31 local b64, unb64 = base64.encode, base64.decode;
       
    32 local jid_escape = require "util.jid".escape;
       
    33 local prepped_split = require "util.jid".prepped_split;
       
    34 local nodeprep = require "util.encodings".stringprep.nodeprep;
       
    35 
       
    36 --module "sasl_dovecot"
       
    37 local _M = {};
       
    38 
       
    39 local request_id = 0;
       
    40 local method = {};
       
    41 method.__index = method;
       
    42 local conn, supported_mechs, pid;
       
    43 
       
    44 local function connect(socket_info)
       
    45 	--log("debug", "connect(%q)", socket_path);
       
    46 	if conn then conn:close(); pid = nil; end
       
    47 	if not pid then pid = tonumber(tostring(conn):match("0x%x*$")) end
       
    48 
       
    49 	local socket_type = (type(socket_info) == "string") and "UNIX" or "TCP";
       
    50 
       
    51 	local ok, err;
       
    52 	if socket_type == "TCP" then
       
    53 		local socket_host, socket_port = unpack(socket_info);
       
    54 		conn = socket.tcp();
       
    55 		ok, err = conn:connect(socket_host, socket_port);
       
    56 		socket_path = ("%s:%d"):format(socket_host, socket_port);
       
    57 	elseif socket.unix then
       
    58 		conn = socket.unix();
       
    59 		ok, err = conn:connect(socket_path);
       
    60 	else
       
    61 		err = "luasocket was not compiled with UNIX sockets support";
       
    62 	end
       
    63 
       
    64 	if not ok then
       
    65 		log("error", "error connecting to dovecot %s socket at '%s'. error was '%s'", socket_type, socket_path, err);
       
    66 		return false;
       
    67 	end
       
    68 
       
    69 	-- Send our handshake
       
    70 	log("debug", "sending handshake to dovecot. version 1.1, cpid '%d'", pid);
       
    71 	if not conn:send("VERSION\t1\t1\n") then
       
    72 		return false
       
    73 	end
       
    74 	if not conn:send("CPID\t" .. pid .. "\n") then
       
    75 		return false
       
    76 	end
       
    77 
       
    78 	-- Parse Dovecot's handshake
       
    79 	local done = false;
       
    80 	supported_mechs = {};
       
    81 	while (not done) do
       
    82 		local line = conn:receive();
       
    83 		if not line then
       
    84 			return false;
       
    85 		end
       
    86 
       
    87 		--log("debug", "dovecot handshake: '%s'", line);
       
    88 		local parts = line:gmatch("[^\t]+");
       
    89 		local first = parts();
       
    90 		if first == "VERSION" then
       
    91 			-- Version should be 1.1
       
    92 			local major_version = parts();
       
    93 
       
    94 			if major_version ~= "1" then
       
    95 				log("error", "dovecot server version is not 1.x. it is %s.x", major_version);
       
    96 				conn:close();
       
    97 				return false;
       
    98 			end
       
    99 		elseif first == "MECH" then
       
   100 			local mech = parts();
       
   101 			supported_mechs[mech] = true;
       
   102 		elseif first == "DONE" then
       
   103 			done = true;
       
   104 		end
       
   105 	end
       
   106 	return conn, supported_mechs;
       
   107 end
       
   108 
       
   109 -- create a new SASL object which can be used to authenticate clients
       
   110 function _M.new(realm, service_name, socket_info, config)
       
   111 	--log("debug", "new(%q, %q, %q)", realm or "", service_name or "", socket_info or "");
       
   112 	local sasl_i = { realm = realm, service_name = service_name, socket_info = socket_info, config = config or {} };
       
   113 
       
   114 	request_id = request_id + 1;
       
   115 	sasl_i.request_id = request_id;
       
   116 	local conn, mechs = conn, supported_mechs;
       
   117 	if not conn then
       
   118 		conn, mechs = connect(socket_info);
       
   119 		if not conn then
       
   120 			return nil, "Socket connection failure";
       
   121 		end
       
   122 	end
       
   123 	sasl_i.conn, sasl_i.mechs = conn, mechs;
       
   124 	return setmetatable(sasl_i, method);
       
   125 end
       
   126 
       
   127 -- [[
       
   128 function method:send(...)
       
   129 	local msg = t_concat({...}, "\t");
       
   130 	local ok, err = self.conn:send(authmsg.."\n");
       
   131 	if not ok then
       
   132 		log("error", "Could not write to socket: %s", err);
       
   133 		return nil, err;
       
   134 	end
       
   135 	return true;
       
   136 end
       
   137 
       
   138 function method:recv()
       
   139 	local line, err = self.conn:receive();
       
   140 	--log("debug", "Sent %d bytes to socket", ok);
       
   141 	local line, err = self.conn:receive();
       
   142 	if not line then
       
   143 		log("error", "Could not read from socket: %s", err);
       
   144 		return nil, err;
       
   145 	end
       
   146 	return line;
       
   147 end
       
   148 -- ]]
       
   149 
       
   150 function method:plain_test(username, password, realm)
       
   151 	if self:select("PLAIN") then
       
   152 		return self:process(("\0%s\0%s"):format(username, password));
       
   153 	end
       
   154 end
       
   155 
       
   156 -- get a fresh clone with the same realm and service name
       
   157 function method:clean_clone()
       
   158 	--log("debug", "method:clean_clone()");
       
   159 	return _M.new(self.realm, self.service_name, self.socket_info, self.config)
       
   160 end
       
   161 
       
   162 -- get a list of possible SASL mechanims to use
       
   163 function method:mechanisms()
       
   164 	--log("debug", "method:mechanisms()");
       
   165 	return self.mechs;
       
   166 end
       
   167 
       
   168 -- select a mechanism to use
       
   169 function method:select(mechanism)
       
   170 	--log("debug", "method:select(%q)", mechanism);
       
   171 	if not self.selected and self.mechs[mechanism] then
       
   172 		self.selected = mechanism;
       
   173 		return true;
       
   174 	end
       
   175 end
       
   176 
       
   177 -- feed new messages to process into the library
       
   178 function method:process(message)
       
   179 	--log("debug", "method:process"..(message and "(%q)" or "()"), message);
       
   180 	--if not message then
       
   181 		--return "challenge";
       
   182 		--return "failure", "malformed-request";
       
   183 	--end
       
   184 	local request_id = self.request_id;
       
   185 	local authmsg;
       
   186 	if not self.started then
       
   187 		self.started = true;
       
   188 		authmsg = t_concat({
       
   189 			"AUTH",
       
   190 			request_id,
       
   191 			self.selected,
       
   192 			"service="..self.service_name,
       
   193 			"resp="..(message and b64(message) or "=")
       
   194 		}, "\t");
       
   195 	else
       
   196 		authmsg = t_concat({
       
   197 			"CONT",
       
   198 			request_id,
       
   199 			(message and b64(message) or "=")
       
   200 		}, "\t");
       
   201 	end
       
   202 	--log("debug", "Sending %d bytes: %q", #authmsg, authmsg);
       
   203 	local ok, err = self.conn:send(authmsg.."\n");
       
   204 	if not ok then
       
   205 		log("error", "Could not write to socket: %s", err);
       
   206 		return "failure", "internal-server-error", err
       
   207 	end
       
   208 	--log("debug", "Sent %d bytes to socket", ok);
       
   209 	local line, err = self.conn:receive();
       
   210 	if not line then
       
   211 		log("error", "Could not read from socket: %s", err);
       
   212 		return "failure", "internal-server-error", err
       
   213 	end
       
   214 	--log("debug", "Received %d bytes from socket: %s", #line, line);
       
   215 
       
   216 	local parts = line:gmatch("[^\t]+");
       
   217 	local resp = parts();
       
   218 	local id = tonumber(parts());
       
   219 
       
   220 	if id ~= request_id then
       
   221 		return "failure", "internal-server-error", "Unexpected request id"
       
   222 	end
       
   223 
       
   224 	local data = {};
       
   225 	for param in parts do
       
   226 		data[#data+1]=param;
       
   227 		local k,v = param:match("^([^=]*)=?(.*)$");
       
   228 		if k and #k>0 then
       
   229 			data[k]=v or true;
       
   230 		end
       
   231 	end
       
   232 
       
   233 	if data.user then
       
   234 		local handle_domain = self.config.handle_domain;
       
   235 		local validate_domain = self.config.validate_domain;
       
   236 		if handle_domain == "split" then
       
   237 			local domain;
       
   238 			self.username, domain = prepped_split(data.user);
       
   239 			if validate_domain and domain ~= self.realm then
       
   240 				return "failure", "not-authorized", "Domain mismatch";
       
   241 			end
       
   242 		elseif handle_domain == "escape" then
       
   243 			self.username = nodeprep(jid_escape(data.user));
       
   244 		else
       
   245 			self.username = nodeprep(data.user);
       
   246 		end
       
   247 		if not self.username then 
       
   248 			return "failure", "not-authorized", "Username failed NODEprep"
       
   249 		end
       
   250 	end
       
   251 
       
   252 	if resp == "FAIL" then
       
   253 		if data.temp then
       
   254 			return "failure", "temporary-auth-failure", data.reason;
       
   255 		elseif data.authz then
       
   256 			return "failure", "invalid-authzid", data.reason;
       
   257 		else
       
   258 			return "failure", "not-authorized", data.reason;
       
   259 		end
       
   260 	elseif resp == "CONT" then
       
   261 		return "challenge", unb64(data[1]);
       
   262 	elseif resp == "OK" then
       
   263 		return "success", data.resp and unb64(data.resp) or nil; 
       
   264 	end
       
   265 end
       
   266 
       
   267 return _M;