Add .jshintrc file for server code
[KiwiIRC.git] / server / proxy.js
CommitLineData
b5574d3b 1var 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
9module.exports = {
10 ProxyServer: ProxyServer,
11 ProxySocket: ProxySocket
12};
13
14function debug() {
6d52928c 15 //console.log.apply(console, arguments);
b5574d3b
D
16}
17
18// Socket connection responses
643f5ea9
D
19var RESPONSE_ERROR = '0';
20var RESPONSE_OK = '1';
21var RESPONSE_ECONNRESET = '2';
22var RESPONSE_ECONNREFUSED = '3';
23var RESPONSE_ENOTFOUND = '4';
24var 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
33function ProxyServer() {
34 events.EventEmitter.call(this);
35}
36util.inherits(ProxyServer, events.EventEmitter);
b5574d3b
D
37
38
585d3189
D
39ProxyServer.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
85ProxyServer.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 105function 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
120ProxyPipe.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
138ProxyPipe.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
172ProxyPipe.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
208ProxyPipe.prototype._onRawSocketConnect = function() {
209 this.proxy_server.emit('socket_connected', this);
b5574d3b
D
210};
211
212
213ProxyPipe.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
224ProxyPipe.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
236ProxyPipe.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
244ProxyPipe.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
251ProxyPipe.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 274function 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
287util.inherits(ProxySocket, stream.Duplex);
288
289
290ProxySocket.prototype.setMeta = function(meta) {
291 this.meta = meta;
292};
293
294
96ecb5e7
D
295ProxySocket.prototype.connectTls = function() {
296 this.meta.ssl = true;
297 return this.connect.apply(this, arguments);
298};
299
300
b5574d3b
D
301ProxySocket.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
332ProxySocket.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
344ProxySocket.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
359ProxySocket.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
369ProxySocket.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
379ProxySocket.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
416ProxySocket.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
428ProxySocket.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};