comparison mod_auth_imap/auth_imap/sasl_imap.lib.lua @ 1196:f45ca6edc159

mod_auth_imap: Authentication module that works by passing through SASL to a IMAP connection
author Kim Alvefur <zash@zash.se>
date Thu, 26 Sep 2013 13:43:27 +0200
parents
children 5d46281a5d23
comparison
equal deleted inserted replaced
1195:f502cbffbdd4 1196:f45ca6edc159
1 -- Dovecot authentication backend for Prosody
2 --
3 -- Copyright (C) 2011 Kim Alvefur
4 --
5
6 local log = require "util.logger".init("sasl_imap");
7
8 local setmetatable = setmetatable;
9
10 local s_match, s_gmatch = string.match, string.gmatch
11 local t_concat = table.concat;
12 local m_random = math.random;
13 local tostring, tonumber = tostring, tonumber;
14
15 local socket = require "socket"
16 -- TODO -- local ssl = require "ssl"
17 local base64 = require "util.encodings".base64;
18 local b64, unb64 = base64.encode, base64.decode;
19
20 local _M = {};
21
22 local method = {};
23 method.__index = method;
24
25 -- For extracting the username.
26 local mitm = {
27 PLAIN = function(message)
28 return s_match(message, "^[^%z]*%z([^%z]+)%z[^%z]+");
29 end,
30 ["SCRAM-SHA-1"] = function(message)
31 return s_match(message, "^[^,]+,[^,]*,n=([^,]*)");
32 end,
33 ["DIGEST-MD5"] = function(message)
34 return s_match(message, "username=\"([^\"]*)\"");
35 end,
36 }
37
38 local function connect(host, port, ssl)
39 port = tonumber(port) or (ssl and 993 or 143);
40 log("debug", "connect() to %s:%s:%d", ssl and "ssl" or "tcp", host, tonumber(port));
41 local conn = socket.tcp();
42
43 -- Create a connection to imap socket
44 log("debug", "connecting to imap at '%s:%d'", host, port);
45 local ok, err = conn:connect(host, port);
46 conn:settimeout(10);
47 if not ok then
48 log("error", "error connecting to imap at '%s:%d'. error was '%s'. check permissions", host, port, err);
49 return false;
50 end
51
52 -- Parse IMAP handshake
53 local done = false;
54 local supported_mechs = {};
55 local line = conn:receive("*l");
56 log("debug", "imap handshake: '%s'", line);
57 if not line then
58 return false;
59 end
60 local caps = line:match("^%*%s+OK%s+(%b[])");
61 if caps then
62 caps = caps:sub(2,-2);
63 for cap in caps:gmatch("%S+") do
64 log("debug", "Capability: %s", cap);
65 local mech = cap:match("AUTH=(.*)");
66 if mech then
67 log("debug", "Supported SASL mechanism: %s", mech);
68 supported_mechs[mech] = mitm[mech] and true or nil;
69 end
70 end
71 end
72
73 return conn, supported_mechs;
74 end
75
76 -- create a new SASL object which can be used to authenticate clients
77 function _M.new(realm, service_name, host, port, ssl)
78 log("debug", "new(%q, %q, %q, %d)", realm or "", service_name or "", host or "", port or 0);
79 local sasl_i = {
80 realm = realm,
81 service_name = service_name,
82 _host = host,
83 _port = port,
84 _ssl = ssl
85 };
86
87 local conn, mechs = connect(host, port, ssl);
88 if not conn then
89 return nil, "Socket connection failure";
90 end
91 sasl_i.conn, sasl_i.mechs = conn, mechs;
92 return setmetatable(sasl_i, method);
93 end
94
95 -- get a fresh clone with the same realm and service name
96 function method:clean_clone()
97 if self.conn then
98 self.conn:close();
99 self.conn = nil;
100 end
101 log("debug", "method:clean_clone()");
102 return _M.new(self.realm, self.service_name, self._host, self._port)
103 end
104
105 -- get a list of possible SASL mechanisms to use
106 function method:mechanisms()
107 log("debug", "method:mechanisms()");
108 return self.mechs;
109 end
110
111 -- select a mechanism to use
112 function method:select(mechanism)
113 log("debug", "method:select(%q)", mechanism);
114 if not self.selected and self.mechs[mechanism] then
115 self.tag = tostring({}):match("0x(%x*)$");
116 self.selected = mechanism;
117 local selectmsg = t_concat({ self.tag, "AUTHENTICATE", mechanism }, " ");
118 log("debug", "Sending %d bytes: %q", #selectmsg, selectmsg);
119 local ok, err = self.conn:send(selectmsg.."\n");
120 if not ok then
121 log("error", "Could not write to socket: %s", err);
122 return "failure", "internal-server-error", err
123 end
124 local line, err = self.conn:receive("*l");
125 if not line then
126 log("error", "Could not read from socket: %s", err);
127 return "failure", "internal-server-error", err
128 end
129 log("debug", "Received %d bytes: %q", #line, line);
130 return line:match("^+")
131 end
132 end
133
134 -- feed new messages to process into the library
135 function method:process(message)
136 local username = mitm[self.selected](message);
137 if username then self.username = username; end
138 log("debug", "method:process(%d bytes)", #message);
139 local ok, err = self.conn:send(b64(message).."\n");
140 if not ok then
141 log("error", "Could not write to socket: %s", err);
142 return "failure", "internal-server-error", err
143 end
144 log("debug", "Sent %d bytes to socket", ok);
145 local line, err = self.conn:receive("*l");
146 if not line then
147 log("error", "Could not read from socket: %s", err);
148 return "failure", "internal-server-error", err
149 end
150 log("debug", "Received %d bytes from socket: %s", #line, line);
151
152 if line:match("^%+") and #line > 2 then
153 local data = line:sub(3);
154 data = data and unb64(data);
155 return "challenge", unb64(data);
156 elseif line:sub(1, #self.tag) == self.tag then
157 local ok, rest = line:sub(#self.tag+1):match("(%w+)%s+(.*)");
158 ok = ok:lower();
159 log("debug", "%s: %s", ok, rest);
160 if ok == "ok" then
161 return "success"
162 elseif ok == "no" then
163 return "failure", "not-authorized", rest;
164 end
165 elseif line:match("^%* BYE") then
166 local err = line:match("BYE%s*(.*)");
167 return "failure", "not-authorized", err;
168 end
169 end
170
171 return _M;