SHOW:
|
|
- or go back to the newest paste.
1 | // Proxy AUTH requests selectively by domain | |
2 | var sock = require('./line_socket'); | |
3 | var utils = require('./utils'); | |
4 | var smtp_regexp = /^([0-9]{3})([ -])(.*)/; | |
5 | ||
6 | exports.register = function () { | |
7 | this.inherits('auth/auth_base'); | |
8 | }; | |
9 | ||
10 | exports.hook_capabilities = function (next, connection) { | |
11 | if (connection.using_tls) { | |
12 | var methods = [ 'PLAIN', 'LOGIN' ]; | |
13 | connection.capabilities.push('AUTH ' + methods.join(' ')); | |
14 | connection.notes.allowed_auth_methods = methods; | |
15 | } | |
16 | next(); | |
17 | }; | |
18 | ||
19 | exports.check_plain_passwd = function (connection, user, passwd, cb) { | |
20 | var domain; | |
21 | if ((domain = /@([^@]+)$/.exec(user))) { | |
22 | domain = domain[1].toLowerCase(); | |
23 | } | |
24 | else { | |
25 | // AUTH user not in user@domain.com format | |
26 | connection.logerror(this, 'AUTH user="' + user + '" error="not in required format"'); | |
27 | return cb(false); | |
28 | } | |
29 | ||
30 | // Check if domain exists in configuration file | |
31 | var config = this.config.get('auth_proxy.ini'); | |
32 | if (!config.domains[domain]) { | |
33 | connection.logerror(this, 'AUTH user="' + user + '" error="domain \'' + domain + '\' is not defined"'); | |
34 | return cb(false); | |
35 | } | |
36 | ||
37 | this.try_auth_proxy(connection, config.domains[domain].split(/[,; ]/), user, passwd, cb); | |
38 | }; | |
39 | ||
40 | exports.try_auth_proxy = function (connection, hosts, user, passwd, cb) { | |
41 | if (!hosts || (hosts && !hosts.length)) return cb(false); | |
42 | if (typeof hosts !== 'object') { | |
43 | hosts = [ hosts ]; | |
44 | } | |
45 | ||
46 | var self = this; | |
47 | var host = hosts.shift(); | |
48 | var methods = []; | |
49 | var auth_complete = false; | |
50 | var auth_success = false; | |
51 | var command = 'connect'; | |
52 | var response = []; | |
53 | var secure = false; | |
54 | ||
55 | var hostport = host.split(/:/); | |
56 | var socket = sock.connect(((hostport[1]) ? hostport[1] : 25), hostport[0]); | |
57 | connection.logdebug(self, 'attempting connection to host=' + hostport[0] + ' port=' + ((hostport[1]) ? hostport[1] : 25) + 'with password: ' + passwd + ', and username: ' + user); | |
58 | socket.setTimeout(30 * 1000); | |
59 | socket.on('connect', function () { | |
60 | }); | |
61 | socket.on('close', function () { | |
62 | if (!auth_complete) { | |
63 | // Try next host | |
64 | return self.try_auth_proxy(connection, hosts, user, passwd, cb); | |
65 | } | |
66 | connection.loginfo(self, 'AUTH user="' + user + '" host="' + host + '" success=' + auth_success); | |
67 | return cb(auth_success); | |
68 | }); | |
69 | socket.on('timeout', function () { | |
70 | connection.logerror(self, "connection timed out"); | |
71 | socket.end(); | |
72 | // Try next host | |
73 | return self.try_auth_proxy(connection, hosts, user, passwd, cb); | |
74 | }); | |
75 | socket.on('error', function (err) { | |
76 | connection.logerror(self, "connection failed to host " + host + ": " + err); | |
77 | return self.try_auth_proxy(connection, hosts, user, passwd, cb); | |
78 | }); | |
79 | socket.send_command = function (cmd, data) { | |
80 | var line = cmd + (data ? (' ' + data) : ''); | |
81 | if (cmd === 'dot') { | |
82 | line = '.'; | |
83 | } | |
84 | connection.logprotocol(self, "C: " + line); | |
85 | command = cmd.toLowerCase(); | |
86 | this.write(line + "\r\n"); | |
87 | // Clear response buffer from previous command | |
88 | response = []; | |
89 | }; | |
90 | socket.on('line', function (line) { | |
91 | connection.logprotocol(self, "S: " + line); | |
92 | var matches = smtp_regexp.exec(line); | |
93 | if (!matches) { | |
94 | connection.logdebug(self, "Unmatched line: " + line); | |
95 | return; | |
96 | } | |
97 | ||
98 | var code = matches[1]; | |
99 | var cont = matches[2]; | |
100 | var rest = matches[3]; | |
101 | response.push(rest); | |
102 | ||
103 | connection.logdebug(self, 'code=' + code + ' cont="' + cont + '" rest="' + rest + '"' + 'state=' + command); | |
104 | ||
105 | if (cont !== ' ') { | |
106 | // We have more lines to receive, so we wait until the last one. | |
107 | return; | |
108 | } | |
109 | ||
110 | connection.logdebug(self, 'command state: ' + command); | |
111 | if (command === 'ehlo') { | |
112 | if (code[0] === '5') { | |
113 | // EHLO command rejected; abort | |
114 | socket.send_command('QUIT'); | |
115 | return; | |
116 | } | |
117 | // Parse CAPABILITIES | |
118 | var i; | |
119 | for (i in response) { | |
120 | if (/^STARTTLS/.test(response[i])) { | |
121 | continue; | |
122 | if (secure) continue; // silly remote, we've already upgraded | |
123 | var key = self.config.get('tls_key.pem', 'binary'); | |
124 | var cert = self.config.get('tls_cert.pem', 'binary'); | |
125 | // Use TLS opportunistically if we found the key and certificate | |
126 | if (key && cert) { | |
127 | this.on('secure', function () { | |
128 | secure = true; | |
129 | socket.send_command('EHLO', self.config.get('me')); | |
130 | }); | |
131 | socket.send_command('STARTTLS'); | |
132 | return; | |
133 | } | |
134 | } | |
135 | else if (/^AUTH /.test(response[i])) { | |
136 | // Parse supported AUTH methods | |
137 | var parse = /^AUTH (.+)$/.exec(response[i]); | |
138 | methods = parse[1].split(/\s+/); | |
139 | connection.logdebug(self, 'found supported AUTH methods: ' + methods); | |
140 | // Prefer PLAIN as it's easiest | |
141 | if (methods.indexOf('PLAIN') !== -1) { | |
142 | socket.send_command('AUTH','PLAIN ' + utils.base64("\0" + user + "\0" + passwd)); | |
143 | return; | |
144 | } | |
145 | else if (methods.indexOf('LOGIN') !== -1) { | |
146 | socket.send_command('AUTH','LOGIN'); | |
147 | return; | |
148 | } | |
149 | else { | |
150 | // No compatible methods; abort... | |
151 | connection.logdebug(self, 'no compatible AUTH methods'); | |
152 | socket.send_command('QUIT'); | |
153 | return; | |
154 | } | |
155 | } | |
156 | } | |
157 | } | |
158 | if (command === 'auth') { | |
159 | // Handle LOGIN | |
160 | if (code[0] === '3' && response[0] === 'VXNlcm5hbWU6') { | |
161 | // Write to the socket directly to keep the state at 'auth' | |
162 | this.write(utils.base64(user) + "\r\n"); | |
163 | response = []; | |
164 | return; | |
165 | } | |
166 | else if (code[0] === '3' && response[0] === 'UGFzc3dvcmQ6') { | |
167 | this.write(utils.base64(passwd) + "\r\n"); | |
168 | response = []; | |
169 | return; | |
170 | } | |
171 | if (code[0] === '5') { | |
172 | // Initial attempt failed; strip domain and retry. | |
173 | var u; | |
174 | if ((u = /^([^@]+)@.+$/.exec(user))) { | |
175 | user = u[1]; | |
176 | if (methods.indexOf('PLAIN') !== -1) { | |
177 | socket.send_command('AUTH', 'PLAIN ' + utils.base64("\0" + user + "\0" + passwd)); | |
178 | } | |
179 | else if (methods.indexOf('LOGIN') !== -1) { | |
180 | socket.send_command('AUTH', 'LOGIN'); | |
181 | } | |
182 | return; | |
183 | } | |
184 | else { | |
185 | // Don't attempt any other hosts | |
186 | auth_complete = true; | |
187 | } | |
188 | } | |
189 | } | |
190 | if (/^[345]/.test(code)) { | |
191 | // Got an unhandled error | |
192 | connection.logdebug(self, 'error: ' + line); | |
193 | socket.send_command('QUIT'); | |
194 | return; | |
195 | } | |
196 | switch (command) { | |
197 | case 'starttls': | |
198 | var tls_options = { key: key, cert: cert }; | |
199 | this.upgrade(tls_options); | |
200 | break; | |
201 | case 'connect': | |
202 | socket.send_command('EHLO', self.config.get('me')); | |
203 | break; | |
204 | case 'auth': | |
205 | // AUTH was successful | |
206 | auth_complete = true; | |
207 | auth_success = true; | |
208 | socket.send_command('QUIT'); | |
209 | break; | |
210 | case 'ehlo': | |
211 | case 'helo': | |
212 | case 'quit': | |
213 | socket.end(); | |
214 | break; | |
215 | default: | |
216 | throw new Error("[auth/auth_proxy] unknown command: " + command); | |
217 | } | |
218 | }); | |
219 | }; |