Mercurial > prosody-modules
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; |
