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