Commit | Line | Data |
---|---|---|
b5574d3b | 1 | var stream = require('stream'), |
643f5ea9 | 2 | util = require('util'), |
445f3e2a | 3 | events = require('events'), |
585d3189 D |
4 | net = require('net'), |
5 | tls = require('tls'), | |
6 | fs = require('fs'); | |
b5574d3b D |
7 | |
8 | ||
9 | module.exports = { | |
10 | ProxyServer: ProxyServer, | |
11 | ProxySocket: ProxySocket | |
12 | }; | |
13 | ||
14 | function debug() { | |
6d52928c | 15 | //console.log.apply(console, arguments); |
b5574d3b D |
16 | } |
17 | ||
18 | // Socket connection responses | |
643f5ea9 D |
19 | var RESPONSE_ERROR = '0'; |
20 | var RESPONSE_OK = '1'; | |
21 | var RESPONSE_ECONNRESET = '2'; | |
22 | var RESPONSE_ECONNREFUSED = '3'; | |
23 | var RESPONSE_ENOTFOUND = '4'; | |
24 | var RESPONSE_ETIMEDOUT = '5'; | |
b5574d3b D |
25 | |
26 | ||
27 | ||
28 | /** | |
29 | * ProxyServer | |
30 | * Listens for connections from a kiwi server, dispatching ProxyPipe | |
31 | * instances for each connection | |
32 | */ | |
445f3e2a D |
33 | function ProxyServer() { |
34 | events.EventEmitter.call(this); | |
35 | } | |
36 | util.inherits(ProxyServer, events.EventEmitter); | |
b5574d3b D |
37 | |
38 | ||
585d3189 D |
39 | ProxyServer.prototype.listen = function(listen_port, listen_addr, opts) { |
40 | var that = this, | |
e0bdbc31 D |
41 | serv_opts = {}, |
42 | connection_event = 'connection'; | |
585d3189 D |
43 | |
44 | opts = opts || {}; | |
45 | ||
46 | // Listen using SSL? | |
47 | if (opts.ssl) { | |
48 | serv_opts = { | |
49 | key: fs.readFileSync(opts.ssl_key), | |
50 | cert: fs.readFileSync(opts.ssl_cert) | |
51 | }; | |
52 | ||
53 | // Do we have an intermediate certificate? | |
54 | if (typeof opts.ssl_ca !== 'undefined') { | |
55 | // An array of them? | |
56 | if (typeof opts.ssl_ca.map !== 'undefined') { | |
57 | serv_opts.ca = opts.ssl_ca.map(function (f) { return fs.readFileSync(f); }); | |
58 | ||
59 | } else { | |
60 | serv_opts.ca = fs.readFileSync(opts.ssl_ca); | |
61 | } | |
62 | } | |
63 | ||
64 | this.server = tls.createServer(serv_opts); | |
65 | ||
e0bdbc31 D |
66 | connection_event = 'secureConnection'; |
67 | ||
585d3189 D |
68 | } |
69 | ||
70 | // No SSL, start a simple clear text server | |
71 | else { | |
72 | this.server = new net.Server(); | |
73 | } | |
b5574d3b | 74 | |
9556a028 D |
75 | this.server.listen(listen_port, listen_addr, function() { |
76 | that.emit('listening'); | |
77 | }); | |
b5574d3b | 78 | |
e0bdbc31 | 79 | this.server.on(connection_event, function(socket) { |
445f3e2a | 80 | new ProxyPipe(socket, that); |
b5574d3b D |
81 | }); |
82 | }; | |
83 | ||
84 | ||
85 | ProxyServer.prototype.close = function(callback) { | |
86 | if (this.server) { | |
87 | return this.server.close(callback); | |
88 | } | |
89 | ||
90 | if (typeof callback === 'function') | |
91 | callback(); | |
92 | }; | |
93 | ||
94 | ||
95 | ||
96 | ||
97 | /** | |
98 | * ProxyPipe | |
99 | * Takes connections from a kiwi server, then: | |
100 | * 1. Reads its meta data such as username for identd lookups | |
101 | * 2. Make the connection to the IRC server | |
102 | * 3. Reply to the kiwi server with connection status | |
103 | * 4. If all ok, pipe data between the 2 sockets as a proxy | |
104 | */ | |
445f3e2a | 105 | function ProxyPipe(kiwi_socket, proxy_server) { |
e0bdbc31 D |
106 | debug('[KiwiProxy] New Kiwi connection'); |
107 | ||
445f3e2a D |
108 | this.kiwi_socket = kiwi_socket; |
109 | this.proxy_server = proxy_server; | |
110 | this.irc_socket = null; | |
111 | this.buffer = ''; | |
112 | this.meta = null; | |
b5574d3b | 113 | |
e0bdbc31 | 114 | debug('[KiwiProxy] Setting encoding to utf8'); |
643f5ea9 D |
115 | kiwi_socket.setEncoding('utf8'); |
116 | kiwi_socket.on('readable', this.kiwiSocketOnReadable.bind(this)); | |
b5574d3b D |
117 | } |
118 | ||
119 | ||
120 | ProxyPipe.prototype.destroy = function() { | |
121 | this.buffer = null; | |
122 | this.meta = null; | |
123 | ||
124 | if (this.irc_socket) { | |
125 | this.irc_socket.destroy(); | |
126 | this.irc_socket.removeAllListeners(); | |
127 | this.irc_socket = null; | |
128 | } | |
129 | ||
130 | if (this.kiwi_socket) { | |
131 | this.kiwi_socket.destroy(); | |
132 | this.kiwi_socket.removeAllListeners(); | |
133 | this.kiwi_socket = null; | |
134 | } | |
135 | }; | |
136 | ||
137 | ||
643f5ea9 D |
138 | ProxyPipe.prototype.kiwiSocketOnReadable = function() { |
139 | var chunk, meta; | |
b5574d3b | 140 | |
643f5ea9 D |
141 | while ((chunk = this.kiwi_socket.read()) !== null) { |
142 | this.buffer += chunk; | |
143 | } | |
b5574d3b D |
144 | |
145 | // Not got a complete line yet? Wait some more | |
643f5ea9 | 146 | if (this.buffer.indexOf('\n') === -1) |
b5574d3b D |
147 | return; |
148 | ||
149 | try { | |
e0bdbc31 | 150 | debug('[KiwiProxy] Found a complete line in the buffer'); |
643f5ea9 | 151 | meta = JSON.parse(this.buffer.substr(0, this.buffer.indexOf('\n'))); |
b5574d3b | 152 | } catch (err) { |
e0bdbc31 | 153 | debug('[KiwiProxy] Error parsing meta'); |
b5574d3b D |
154 | this.destroy(); |
155 | return; | |
156 | } | |
157 | ||
158 | if (!meta.username) { | |
e0bdbc31 | 159 | debug('[KiwiProxy] Meta does not contain a username'); |
b5574d3b D |
160 | this.destroy(); |
161 | return; | |
162 | } | |
163 | ||
164 | this.buffer = ''; | |
165 | this.meta = meta; | |
643f5ea9 | 166 | this.kiwi_socket.removeAllListeners('readable'); |
b5574d3b D |
167 | |
168 | this.makeIrcConnection(); | |
169 | }; | |
170 | ||
171 | ||
172 | ProxyPipe.prototype.makeIrcConnection = function() { | |
e0bdbc31 | 173 | debug('[KiwiProxy] Opening proxied connection to: ' + this.meta.host + ':' + this.meta.port.toString()); |
5bba0980 D |
174 | |
175 | var local_address = this.meta.interface ? | |
176 | this.meta.interface : | |
177 | '0.0.0.0'; | |
178 | ||
e0bdbc31 D |
179 | if (this.meta.ssl) { |
180 | this.irc_socket = tls.connect({ | |
181 | port: parseInt(this.meta.port, 10), | |
182 | host: this.meta.host, | |
183 | rejectUnauthorized: global.config.reject_unauthorised_certificates, | |
5bba0980 | 184 | localAddress: local_address |
e0bdbc31 D |
185 | }, this._onSocketConnect.bind(this)); |
186 | ||
187 | } else { | |
5bba0980 D |
188 | this.irc_socket = net.connect({ |
189 | port: parseInt(this.meta.port, 10), | |
190 | host: this.meta.host, | |
191 | localAddress: local_address | |
192 | }, this._onSocketConnect.bind(this)); | |
e0bdbc31 | 193 | } |
b5574d3b D |
194 | |
195 | this.irc_socket.setTimeout(10000); | |
196 | this.irc_socket.on('error', this._onSocketError.bind(this)); | |
197 | this.irc_socket.on('timeout', this._onSocketTimeout.bind(this)); | |
96ecb5e7 D |
198 | |
199 | // We need the raw socket connect event, not after any SSL handshakes or anything | |
200 | if (this.irc_socket.socket) { | |
201 | this.irc_socket.socket.on('connect', this._onRawSocketConnect.bind(this)); | |
202 | } else { | |
203 | this.irc_socket.on('connect', this._onRawSocketConnect.bind(this)); | |
204 | } | |
205 | }; | |
206 | ||
207 | ||
208 | ProxyPipe.prototype._onRawSocketConnect = function() { | |
209 | this.proxy_server.emit('socket_connected', this); | |
b5574d3b D |
210 | }; |
211 | ||
212 | ||
213 | ProxyPipe.prototype._onSocketConnect = function() { | |
b5574d3b | 214 | debug('[KiwiProxy] ProxyPipe::_onSocketConnect()'); |
643f5ea9 | 215 | |
445f3e2a D |
216 | this.proxy_server.emit('connection_open', this); |
217 | ||
643f5ea9 D |
218 | // Now that we're connected to the detination, return no |
219 | // error back to the kiwi server and start piping | |
b5574d3b D |
220 | this.kiwi_socket.write(new Buffer(RESPONSE_OK.toString()), this.startPiping.bind(this)); |
221 | }; | |
222 | ||
223 | ||
224 | ProxyPipe.prototype._onSocketError = function(err) { | |
225 | var replies = { | |
226 | ECONNRESET: RESPONSE_ECONNRESET, | |
227 | ECONNREFUSED: RESPONSE_ECONNREFUSED, | |
228 | ENOTFOUND: RESPONSE_ENOTFOUND, | |
229 | ETIMEDOUT: RESPONSE_ETIMEDOUT | |
230 | }; | |
231 | debug('[KiwiProxy] IRC Error ' + err.code); | |
232 | this.kiwi_socket.write(new Buffer((replies[err.code] || RESPONSE_ERROR).toString()), 'UTF8', this.destroy.bind(this)); | |
233 | }; | |
234 | ||
235 | ||
236 | ProxyPipe.prototype._onSocketTimeout = function() { | |
237 | this.has_timed_out = true; | |
238 | debug('[KiwiProxy] IRC Timeout'); | |
239 | this.irc_socket.destroy(); | |
240 | this.kiwi_socket.write(new Buffer(RESPONSE_ETIMEDOUT.toString()), 'UTF8', this.destroy.bind(this)); | |
241 | }; | |
242 | ||
243 | ||
643f5ea9 D |
244 | ProxyPipe.prototype._onSocketClose = function() { |
245 | debug('[KiwiProxy] IRC Socket closed'); | |
445f3e2a | 246 | this.proxy_server.emit('connection_close', this); |
643f5ea9 D |
247 | this.destroy(); |
248 | }; | |
249 | ||
250 | ||
b5574d3b D |
251 | ProxyPipe.prototype.startPiping = function() { |
252 | debug('[KiwiProxy] ProxyPipe::startPiping()'); | |
643f5ea9 D |
253 | |
254 | // Let the piping handle socket closures | |
255 | this.irc_socket.removeAllListeners('error'); | |
256 | this.irc_socket.removeAllListeners('timeout'); | |
257 | ||
258 | this.irc_socket.on('close', this._onSocketClose.bind(this)); | |
259 | ||
260 | this.kiwi_socket.setEncoding('binary'); | |
261 | ||
b5574d3b D |
262 | this.kiwi_socket.pipe(this.irc_socket); |
263 | this.irc_socket.pipe(this.kiwi_socket); | |
264 | }; | |
265 | ||
266 | ||
267 | ||
268 | ||
269 | ||
270 | /** | |
271 | * ProxySocket | |
272 | * Transparent socket interface to a kiwi proxy | |
273 | */ | |
29bc2066 | 274 | function ProxySocket(proxy_port, proxy_addr, meta, proxy_opts) { |
b5574d3b D |
275 | stream.Duplex.call(this); |
276 | ||
277 | this.connected_fn = null; | |
643f5ea9 D |
278 | this.proxy_addr = proxy_addr; |
279 | this.proxy_port = proxy_port; | |
29bc2066 | 280 | this.proxy_opts = proxy_opts || {}; |
5bba0980 D |
281 | |
282 | this.setMeta(meta || {}); | |
b5574d3b D |
283 | |
284 | this.state = 'disconnected'; | |
285 | } | |
286 | ||
287 | util.inherits(ProxySocket, stream.Duplex); | |
288 | ||
289 | ||
290 | ProxySocket.prototype.setMeta = function(meta) { | |
291 | this.meta = meta; | |
292 | }; | |
293 | ||
294 | ||
96ecb5e7 D |
295 | ProxySocket.prototype.connectTls = function() { |
296 | this.meta.ssl = true; | |
297 | return this.connect.apply(this, arguments); | |
298 | }; | |
299 | ||
300 | ||
b5574d3b D |
301 | ProxySocket.prototype.connect = function(dest_port, dest_addr, connected_fn) { |
302 | this.meta.host = dest_addr; | |
303 | this.meta.port = dest_port; | |
304 | this.connected_fn = connected_fn; | |
305 | ||
306 | if (!this.meta.host || !this.meta.port) { | |
307 | debug('[KiwiProxy] Invalid destination addr/port', this.meta); | |
308 | return false; | |
309 | } | |
310 | ||
e0bdbc31 D |
311 | debug('[KiwiProxy] Connecting to proxy ' + this.proxy_addr + ':' + this.proxy_port.toString() + ' SSL: ' + (!!this.proxy_opts.ssl).toString()); |
312 | if (this.proxy_opts.ssl) { | |
313 | this.socket = tls.connect({ | |
314 | port: this.proxy_port, | |
315 | host: this.proxy_addr, | |
316 | rejectUnauthorized: !!global.config.reject_unauthorised_certificates, | |
317 | }, this._onSocketConnect.bind(this)); | |
318 | ||
319 | } else { | |
320 | this.socket = net.connect(this.proxy_port, this.proxy_addr, this._onSocketConnect.bind(this)); | |
321 | } | |
b5574d3b | 322 | |
96ecb5e7 | 323 | this.socket.setTimeout(10000); |
b5574d3b D |
324 | this.socket.on('data', this._onSocketData.bind(this)); |
325 | this.socket.on('close', this._onSocketClose.bind(this)); | |
326 | this.socket.on('error', this._onSocketError.bind(this)); | |
327 | ||
328 | return this; | |
329 | }; | |
330 | ||
331 | ||
332 | ProxySocket.prototype.destroySocket = function() { | |
333 | if (!this.socket) | |
334 | return; | |
335 | ||
336 | this.socket.removeAllListeners(); | |
337 | this.socket.destroy(); | |
338 | delete this.socket; | |
643f5ea9 D |
339 | |
340 | debug('[KiwiProxy] Destroying socket'); | |
b5574d3b D |
341 | }; |
342 | ||
343 | ||
344 | ProxySocket.prototype._read = function() { | |
345 | var data; | |
346 | ||
347 | if (this.state === 'connected' && this.socket) { | |
348 | while ((data = this.socket.read()) !== null) { | |
643f5ea9 | 349 | if (this.push(data) === false) { |
b5574d3b D |
350 | break; |
351 | } | |
352 | } | |
353 | } else { | |
354 | this.push(''); | |
355 | } | |
356 | }; | |
357 | ||
358 | ||
359 | ProxySocket.prototype._write = function(chunk, encoding, callback) { | |
360 | if (this.state === 'connected' && this.socket) { | |
361 | return this.socket.write(chunk, encoding, callback); | |
362 | } else { | |
e0bdbc31 | 363 | debug('[KiwiProxy] Trying to write to an unfinished socket. State=' + this.state); |
643f5ea9 | 364 | callback('Not connected'); |
b5574d3b D |
365 | } |
366 | }; | |
367 | ||
368 | ||
369 | ProxySocket.prototype._onSocketConnect = function() { | |
370 | var meta = this.meta || {}; | |
371 | ||
372 | this.state = 'handshaking'; | |
373 | ||
374 | debug('[KiwiProxy] Connected to proxy, sending meta'); | |
375 | this.socket.write(JSON.stringify(meta) + '\n'); | |
376 | }; | |
377 | ||
378 | ||
379 | ProxySocket.prototype._onSocketData = function(data) { | |
380 | if (this.state === 'connected') { | |
381 | this.emit('data', data); | |
382 | return; | |
383 | } | |
384 | ||
385 | var buffer_str = data.toString(), | |
386 | status = buffer_str[0], | |
387 | error_code, | |
388 | error_codes = {}; | |
389 | ||
643f5ea9 D |
390 | error_codes[RESPONSE_ERROR] = 'ERROR'; |
391 | error_codes[RESPONSE_ECONNRESET] = 'ECONNRESET'; | |
b5574d3b | 392 | error_codes[RESPONSE_ECONNREFUSED] = 'ECONNREFUSED'; |
643f5ea9 D |
393 | error_codes[RESPONSE_ENOTFOUND] = 'ENOTFOUND'; |
394 | error_codes[RESPONSE_ETIMEDOUT] = 'ETIMEDOUT'; | |
b5574d3b D |
395 | |
396 | debug('[KiwiProxy] Recieved socket status: ' + data.toString()); | |
397 | if (status === RESPONSE_OK) { | |
398 | debug('[KiwiProxy] Remote socket connected OK'); | |
399 | this.state = 'connected'; | |
400 | ||
401 | if (typeof this.connected_fn === 'function') | |
402 | connected_fn(); | |
403 | ||
404 | this.emit('connect'); | |
405 | ||
406 | } else { | |
407 | this.destroySocket(); | |
408 | ||
409 | error_code = error_codes[status] || error_codes[RESPONSE_ERROR]; | |
410 | debug('[KiwiProxy] Error: ' + error_code); | |
411 | this.emit('error', new Error(error_code)); | |
412 | } | |
413 | }; | |
414 | ||
415 | ||
416 | ProxySocket.prototype._onSocketClose = function(had_error) { | |
e0bdbc31 | 417 | debug('[KiwiProxy] _onSocketClose() had_error=' + had_error.toString()); |
b5574d3b D |
418 | if (this.state === 'connected') { |
419 | this.emit('close', had_error); | |
420 | return; | |
421 | } | |
422 | ||
423 | if (!this.ignore_close) | |
424 | this.emit('error', new Error(RESPONSE_ERROR)); | |
425 | }; | |
426 | ||
427 | ||
428 | ProxySocket.prototype._onSocketError = function(err) { | |
e0bdbc31 | 429 | debug('[KiwiProxy] _onSocketError() err=' + err.toString()); |
643f5ea9 | 430 | this.ignore_close = true; |
b5574d3b D |
431 | this.emit('error', err); |
432 | }; |