Software / code / prosody-modules
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 |
| child | 1491: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 |