comparison mod_auth_imap/auth_imap/sasl_imap.lib.lua @ 1200:34216cdffda6

mod_auth_imap: unfortunately large commit which adds support for SSL (including cert verification), appending the realm to usernames, and various IMAP protocol fixes
author Matthew Wild <mwild1@gmail.com>
date Thu, 26 Sep 2013 18:14:45 +0100
parents 5d46281a5d23
children 7dbde05b48a9
comparison
equal deleted inserted replaced
1199:5d46281a5d23 1200:34216cdffda6
10 local s_match = string.match; 10 local s_match = string.match;
11 local t_concat = table.concat; 11 local t_concat = table.concat;
12 local tostring, tonumber = tostring, tonumber; 12 local tostring, tonumber = tostring, tonumber;
13 13
14 local socket = require "socket" 14 local socket = require "socket"
15 -- TODO -- local ssl = require "ssl" 15 local ssl = require "ssl"
16 local x509 = require "util.x509";
16 local base64 = require "util.encodings".base64; 17 local base64 = require "util.encodings".base64;
17 local b64, unb64 = base64.encode, base64.decode; 18 local b64, unb64 = base64.encode, base64.decode;
18 19
19 local _M = {}; 20 local _M = {};
20 21
32 ["DIGEST-MD5"] = function(message) 33 ["DIGEST-MD5"] = function(message)
33 return s_match(message, "username=\"([^\"]*)\""); 34 return s_match(message, "username=\"([^\"]*)\"");
34 end, 35 end,
35 } 36 }
36 37
37 local function connect(host, port, ssl) 38 local function connect(host, port, ssl_params)
38 port = tonumber(port) or (ssl and 993 or 143); 39 port = tonumber(port) or (ssl_params and 993 or 143);
39 log("debug", "connect() to %s:%s:%d", ssl and "ssl" or "tcp", host, tonumber(port)); 40 log("debug", "connect() to %s:%s:%d", ssl_params and "ssl" or "tcp", host, tonumber(port));
40 local conn = socket.tcp(); 41 local conn = socket.tcp();
41 42
42 -- Create a connection to imap socket 43 -- Create a connection to imap socket
43 log("debug", "connecting to imap at '%s:%d'", host, port); 44 log("debug", "connecting to imap at '%s:%d'", host, port);
44 local ok, err = conn:connect(host, port); 45 local ok, err = conn:connect(host, port);
45 conn:settimeout(10); 46 conn:settimeout(10);
46 if not ok then 47 if not ok then
47 log("error", "error connecting to imap at '%s:%d'. error was '%s'. check permissions", host, port, err); 48 log("error", "error connecting to imap at '%s:%d': %s", host, port, err);
48 return false; 49 return false;
49 end 50 end
50 51
52 if ssl_params then
53 -- Perform SSL handshake
54 local ok, err = ssl.wrap(conn, ssl_params);
55 if ok then
56 conn = ok;
57 ok, err = conn:dohandshake();
58 end
59 if not ok then
60 log("error", "error initializing ssl connection to imap at '%s:%d': %s", host, port, err);
61 conn:close();
62 return false;
63 end
64
65 -- Verify certificate
66 if ssl_params.verify then
67 if not conn.getpeercertificate then
68 log("error", "unable to verify certificate, newer LuaSec required: https://prosody.im/doc/depends#luasec");
69 conn:close();
70 return false;
71 end
72 if not x509.verify_identity(host, nil, conn:getpeercertificate()) then
73 log("warn", "invalid certificate for imap service %s:%d, denying connection", host, port);
74 return false;
75 end
76 end
77 end
78
51 -- Parse IMAP handshake 79 -- Parse IMAP handshake
52 local done = false;
53 local supported_mechs = {}; 80 local supported_mechs = {};
54 local line = conn:receive("*l"); 81 local line = conn:receive("*l");
55 log("debug", "imap handshake: '%s'", line);
56 if not line then 82 if not line then
57 return false; 83 return false;
58 end 84 end
85 log("debug", "imap greeting: '%s'", line);
59 local caps = line:match("^%*%s+OK%s+(%b[])"); 86 local caps = line:match("^%*%s+OK%s+(%b[])");
87 if not caps or not caps:match("^%[CAPABILITY ") then
88 conn:send("A CAPABILITY\n");
89 line = conn:receive("*l");
90 log("debug", "imap capabilities response: '%s'", line);
91 caps = line:match("^%*%s+CAPABILITY%s+(.*)$");
92 if not conn:receive("*l"):match("^A OK") then
93 log("debug", "imap capabilities command failed")
94 conn:close();
95 return false;
96 end
97 elseif caps then
98 caps = caps:sub(2,-2); -- Strip surrounding []
99 end
60 if caps then 100 if caps then
61 caps = caps:sub(2,-2);
62 for cap in caps:gmatch("%S+") do 101 for cap in caps:gmatch("%S+") do
63 log("debug", "Capability: %s", cap); 102 log("debug", "Capability: %s", cap);
64 local mech = cap:match("AUTH=(.*)"); 103 local mech = cap:match("AUTH=(.*)");
65 if mech then 104 if mech then
66 log("debug", "Supported SASL mechanism: %s", mech); 105 log("debug", "Supported SASL mechanism: %s", mech);
71 110
72 return conn, supported_mechs; 111 return conn, supported_mechs;
73 end 112 end
74 113
75 -- create a new SASL object which can be used to authenticate clients 114 -- create a new SASL object which can be used to authenticate clients
76 function _M.new(realm, service_name, host, port, ssl) 115 function _M.new(realm, service_name, host, port, ssl_params, append_host)
77 log("debug", "new(%q, %q, %q, %d)", realm or "", service_name or "", host or "", port or 0); 116 log("debug", "new(%q, %q, %q, %d)", realm or "", service_name or "", host or "", port or 0);
78 local sasl_i = { 117 local sasl_i = {
79 realm = realm, 118 realm = realm;
80 service_name = service_name, 119 service_name = service_name;
81 _host = host, 120 _host = host;
82 _port = port, 121 _port = port;
83 _ssl = ssl 122 _ssl_params = ssl_params;
123 _append_host = append_host;
84 }; 124 };
85 125
86 local conn, mechs = connect(host, port, ssl); 126 local conn, mechs = connect(host, port, ssl_params);
87 if not conn then 127 if not conn then
88 return nil, "Socket connection failure"; 128 return nil, "Socket connection failure";
129 end
130 if append_host then
131 mechs = { PLAIN = mechs.PLAIN };
89 end 132 end
90 sasl_i.conn, sasl_i.mechs = conn, mechs; 133 sasl_i.conn, sasl_i.mechs = conn, mechs;
91 return setmetatable(sasl_i, method); 134 return setmetatable(sasl_i, method);
92 end 135 end
93 136
96 if self.conn then 139 if self.conn then
97 self.conn:close(); 140 self.conn:close();
98 self.conn = nil; 141 self.conn = nil;
99 end 142 end
100 log("debug", "method:clean_clone()"); 143 log("debug", "method:clean_clone()");
101 return _M.new(self.realm, self.service_name, self._host, self._port) 144 return _M.new(self.realm, self.service_name, self._host, self._port, self._ssl_params, self._append_host)
102 end 145 end
103 146
104 -- get a list of possible SASL mechanisms to use 147 -- get a list of possible SASL mechanisms to use
105 function method:mechanisms() 148 function method:mechanisms()
106 log("debug", "method:mechanisms()"); 149 log("debug", "method:mechanisms()");
132 175
133 -- feed new messages to process into the library 176 -- feed new messages to process into the library
134 function method:process(message) 177 function method:process(message)
135 local username = mitm[self.selected](message); 178 local username = mitm[self.selected](message);
136 if username then self.username = username; end 179 if username then self.username = username; end
137 log("debug", "method:process(%d bytes)", #message); 180 if self._append_host and self.selected == "PLAIN" then
181 message = message:gsub("^([^%z]*%z[^%z]+)(%z[^%z]+)$", "%1@"..self.realm.."%2");
182 end
183 log("debug", "method:process(%d bytes): %q", #message, message:gsub("%z", "."));
138 local ok, err = self.conn:send(b64(message).."\n"); 184 local ok, err = self.conn:send(b64(message).."\n");
139 if not ok then 185 if not ok then
140 log("error", "Could not write to socket: %s", err); 186 log("error", "Could not write to socket: %s", err);
141 return "failure", "internal-server-error", err 187 return "failure", "internal-server-error", err
142 end 188 end
145 if not line then 191 if not line then
146 log("error", "Could not read from socket: %s", err); 192 log("error", "Could not read from socket: %s", err);
147 return "failure", "internal-server-error", err 193 return "failure", "internal-server-error", err
148 end 194 end
149 log("debug", "Received %d bytes from socket: %s", #line, line); 195 log("debug", "Received %d bytes from socket: %s", #line, line);
196
197 while line and line:match("^%* ") do
198 line, err = self.conn:receive("*l");
199 end
150 200
151 if line:match("^%+") and #line > 2 then 201 if line:match("^%+") and #line > 2 then
152 local data = line:sub(3); 202 local data = line:sub(3);
153 data = data and unb64(data); 203 data = data and unb64(data);
154 return "challenge", unb64(data); 204 return "challenge", unb64(data);