comparison 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
parents
children e7294423512f
comparison
equal deleted inserted replaced
1087:447af80a16ad 1088:6f8e7f65f704
1 -- mod_net_dovecotauth.lua
2 --
3 -- Protocol spec:
4 -- http://dovecot.org/doc/auth-protocol.txt
5 --
6 -- Example postfix config:
7 -- sudo postconf smtpd_sasl_path=inet:127.0.0.1:28484
8 -- sudo postconf smtpd_sasl_type=dovecot
9 -- sudo postconf smtpd_sasl_auth_enable=yes
10
11 module:set_global();
12
13 -- Imports
14 local new_sasl = require "core.usermanager".get_sasl_handler;
15 local user_exists = require "core.usermanager".user_exists;
16 local base64 = require"util.encodings".base64;
17 local new_buffer = module:require"buffer".new;
18 local dump = require"util.serialization".serialize;
19
20 -- Config
21 local vhost = module:get_option_string("dovecotauth_host", (next(hosts))); -- TODO Is there a better solution?
22 local allow_master = module:get_option_boolean("adovecotauth_allow_master", false);
23
24 -- Active sessions
25 local sessions = {};
26
27 -- Session methods
28 local new_session;
29 do
30 local sess = { };
31 local sess_mt = { __index = sess };
32
33 function new_session(conn)
34 local sess = { type = "?", conn = conn, buf = assert(new_buffer()), sasl = {} }
35 function sess:log(l, m, ...)
36 return module:log(l, self.type..tonumber(tostring(self):match("%x+$"), 16)..": "..m, ...);
37 end
38 return setmetatable(sess, sess_mt);
39 end
40
41 function sess:send(...)
42 local data = table.concat({...}, "\t") .. "\n"
43 -- self:log("debug", "SEND: %s", dump(ret));
44 return self.conn:write(data);
45 end
46
47 local mech_params = {
48 ANONYMOUS = "anonymous";
49 PLAIN = "plaintext";
50 ["DIGEST-MD5"] = "mutual-auth";
51 ["SCRAM-SHA-1"] = "mutual-auth";
52 ["SCRAM-SHA-1-PLUS"] = "mutual-auth";
53 }
54
55 function sess:handshake()
56 self:send("VERSION", 1, 1);
57 self:send("SPID", pposix.getpid());
58 self:send("CUID", tonumber(tostring(self):match"%x+$", 16));
59 for mech in pairs(self.g_sasl:mechanisms()) do
60 self:send("MECH", mech, mech_params[mech]);
61 end
62 self:send("DONE");
63 end
64
65 function sess:feed(data)
66 -- TODO break this up a bit
67 -- module:log("debug", "sess = %s", dump(self));
68 local buf = self.buf;
69 buf:write(data);
70 local line = buf:read("*l")
71 while line and line ~= "" do
72 local part = line:gmatch("[^\t]+");
73 local command = part();
74 if command == "VERSION" then
75 local major = tonumber(part());
76 local minor = tonumber(part());
77 if major ~= 1 then
78 self:log("warn", "Wrong version, expected 1.1, got %s.%s", tostring(major), tostring(minor));
79 self.conn:close();
80 break;
81 end
82 elseif command == "CPID" then
83 self.type = "C";
84 self.pid = part();
85 elseif command == "SPID" and allow_master then
86 self.type = "M";
87 self.pid = part();
88 elseif command == "AUTH" and self.type ~= "?" then
89 -- C: "AUTH" TAB <id> TAB <mechanism> TAB service=<service> [TAB <parameters>]
90 local id = part() -- <id>
91 local sasl = self.sasl[id];
92 local mech = part();
93 if not sasl then
94 -- TODO Should maybe initialize SASL handler after parsing the line?
95 sasl = self.g_sasl:clean_clone();
96 self.sasl[id] = sasl;
97 if not sasl:select(mech) then
98 self:send("FAIL", id, "reason=invalid-mechanism");
99 self.sasl[id] = nil;
100 sasl = false
101 end
102 end
103 if sasl then
104 local params = {}; -- Not used for anything yet
105 for p in part do
106 local k,v = p:match("^([^=]*)=(.*)$");
107 if k == "resp" then
108 self:log("debug", "params = %s", dump(params));
109 v = base64.decode(v);
110 local status, ret, err = sasl:process(v);
111 self:log("debug", status);
112 if status == "challenge" then
113 self:send("CONT", id, base64.encode(ret));
114 elseif status == "failure" then
115 self.sasl[id] = nil;
116 self:send("FAIL", id, "reason="..tostring(err));
117 elseif status == "success" then
118 self.sasl[id] = nil;
119 self:send("OK", id, "user="..sasl.username, ret and "resp="..base64.encode(ret));
120 end
121 break; -- resp MUST be the last param
122 else
123 params[k or p] = v or true;
124 end
125 end
126 end
127 elseif command == "USER" and self.type == "M" then
128 -- FIXME Should this be on a separate listener?
129 local id = part();
130 local user = part();
131 if user and user_exists(user, vhost) then
132 self:send("USER", id);
133 else
134 self:send("NOTFOUND", id);
135 end
136 else
137 self:log("warn", "Unhandled command %s", tostring(command));
138 self.conn:close();
139 break;
140 end
141 line = buf:read("*l");
142 end
143 end
144
145 end
146
147 local listener = {}
148
149 function listener.onconnect(conn)
150 s = new_session(conn);
151 sessions[conn] = s;
152 local g_sasl = new_sasl(vhost, s);
153 s.g_sasl = g_sasl;
154 s:handshake();
155 end
156
157 function listener.onincoming(conn, data)
158 local s = sessions[conn];
159 -- s:log("debug", "RECV %s", dump(data));
160 return s:feed(data);
161 end
162
163 function listener.ondisconnect(conn)
164 sessions[conn] = nil;
165 end
166
167 function module.unload()
168 for c in pairs(sessions) do
169 c:close();
170 end
171 end
172
173 module:provides("net", {
174 default_port = 28484;
175 listener = listener;
176 });
177