Proxy server type in config file
[KiwiIRC.git] / server / proxy.js
CommitLineData
b5574d3b 1var stream = require('stream'),
643f5ea9 2 util = require('util'),
445f3e2a 3 events = require('events'),
643f5ea9 4 net = require("net"),
445f3e2a 5 tls = require("tls");
b5574d3b
D
6
7
8module.exports = {
9 ProxyServer: ProxyServer,
10 ProxySocket: ProxySocket
11};
12
13function debug() {
9556a028 14 //console.log.apply(console, arguments);
b5574d3b
D
15}
16
17// Socket connection responses
643f5ea9
D
18var RESPONSE_ERROR = '0';
19var RESPONSE_OK = '1';
20var RESPONSE_ECONNRESET = '2';
21var RESPONSE_ECONNREFUSED = '3';
22var RESPONSE_ENOTFOUND = '4';
23var RESPONSE_ETIMEDOUT = '5';
b5574d3b
D
24
25
26
27/**
28 * ProxyServer
29 * Listens for connections from a kiwi server, dispatching ProxyPipe
30 * instances for each connection
31 */
445f3e2a
D
32function ProxyServer() {
33 events.EventEmitter.call(this);
34}
35util.inherits(ProxyServer, events.EventEmitter);
b5574d3b
D
36
37
38ProxyServer.prototype.listen = function(listen_port, listen_addr) {
39 var that = this;
40
b5574d3b
D
41 // Start listening for proxy connections connections
42 this.server = new net.Server();
9556a028
D
43 this.server.listen(listen_port, listen_addr, function() {
44 that.emit('listening');
45 });
b5574d3b
D
46
47 this.server.on('connection', function(socket) {
445f3e2a 48 new ProxyPipe(socket, that);
b5574d3b
D
49 });
50};
51
52
53ProxyServer.prototype.close = function(callback) {
54 if (this.server) {
55 return this.server.close(callback);
56 }
57
58 if (typeof callback === 'function')
59 callback();
60};
61
62
63
64
65/**
66 * ProxyPipe
67 * Takes connections from a kiwi server, then:
68 * 1. Reads its meta data such as username for identd lookups
69 * 2. Make the connection to the IRC server
70 * 3. Reply to the kiwi server with connection status
71 * 4. If all ok, pipe data between the 2 sockets as a proxy
72 */
445f3e2a
D
73function ProxyPipe(kiwi_socket, proxy_server) {
74 this.kiwi_socket = kiwi_socket;
75 this.proxy_server = proxy_server;
76 this.irc_socket = null;
77 this.buffer = '';
78 this.meta = null;
b5574d3b 79
643f5ea9
D
80 kiwi_socket.setEncoding('utf8');
81 kiwi_socket.on('readable', this.kiwiSocketOnReadable.bind(this));
b5574d3b
D
82}
83
84
85ProxyPipe.prototype.destroy = function() {
86 this.buffer = null;
87 this.meta = null;
88
89 if (this.irc_socket) {
90 this.irc_socket.destroy();
91 this.irc_socket.removeAllListeners();
92 this.irc_socket = null;
93 }
94
95 if (this.kiwi_socket) {
96 this.kiwi_socket.destroy();
97 this.kiwi_socket.removeAllListeners();
98 this.kiwi_socket = null;
99 }
100};
101
102
643f5ea9
D
103ProxyPipe.prototype.kiwiSocketOnReadable = function() {
104 var chunk, meta;
b5574d3b 105
643f5ea9
D
106 while ((chunk = this.kiwi_socket.read()) !== null) {
107 this.buffer += chunk;
108 }
b5574d3b
D
109
110 // Not got a complete line yet? Wait some more
643f5ea9 111 if (this.buffer.indexOf('\n') === -1)
b5574d3b
D
112 return;
113
114 try {
643f5ea9 115 meta = JSON.parse(this.buffer.substr(0, this.buffer.indexOf('\n')));
b5574d3b
D
116 } catch (err) {
117 this.destroy();
118 return;
119 }
120
121 if (!meta.username) {
122 this.destroy();
123 return;
124 }
125
126 this.buffer = '';
127 this.meta = meta;
643f5ea9 128 this.kiwi_socket.removeAllListeners('readable');
b5574d3b
D
129
130 this.makeIrcConnection();
131};
132
133
134ProxyPipe.prototype.makeIrcConnection = function() {
135 debug('[KiwiProxy] Proxied connection to: ' + this.meta.host + ':' + this.meta.port.toString());
136 this.irc_socket = this.meta.ssl ?
137 tls.connect(parseInt(this.meta.port, 10), this.meta.host, this._onSocketConnect.bind(this)) :
138 net.connect(parseInt(this.meta.port, 10), this.meta.host, this._onSocketConnect.bind(this));
139
140 this.irc_socket.setTimeout(10000);
141 this.irc_socket.on('error', this._onSocketError.bind(this));
142 this.irc_socket.on('timeout', this._onSocketTimeout.bind(this));
143};
144
145
146ProxyPipe.prototype._onSocketConnect = function() {
b5574d3b 147 debug('[KiwiProxy] ProxyPipe::_onSocketConnect()');
643f5ea9 148
445f3e2a
D
149 this.proxy_server.emit('connection_open', this);
150
643f5ea9
D
151 // Now that we're connected to the detination, return no
152 // error back to the kiwi server and start piping
b5574d3b
D
153 this.kiwi_socket.write(new Buffer(RESPONSE_OK.toString()), this.startPiping.bind(this));
154};
155
156
157ProxyPipe.prototype._onSocketError = function(err) {
158 var replies = {
159 ECONNRESET: RESPONSE_ECONNRESET,
160 ECONNREFUSED: RESPONSE_ECONNREFUSED,
161 ENOTFOUND: RESPONSE_ENOTFOUND,
162 ETIMEDOUT: RESPONSE_ETIMEDOUT
163 };
164 debug('[KiwiProxy] IRC Error ' + err.code);
165 this.kiwi_socket.write(new Buffer((replies[err.code] || RESPONSE_ERROR).toString()), 'UTF8', this.destroy.bind(this));
166};
167
168
169ProxyPipe.prototype._onSocketTimeout = function() {
170 this.has_timed_out = true;
171 debug('[KiwiProxy] IRC Timeout');
172 this.irc_socket.destroy();
173 this.kiwi_socket.write(new Buffer(RESPONSE_ETIMEDOUT.toString()), 'UTF8', this.destroy.bind(this));
174};
175
176
643f5ea9
D
177ProxyPipe.prototype._onSocketClose = function() {
178 debug('[KiwiProxy] IRC Socket closed');
445f3e2a 179 this.proxy_server.emit('connection_close', this);
643f5ea9
D
180 this.destroy();
181};
182
183
b5574d3b
D
184ProxyPipe.prototype.startPiping = function() {
185 debug('[KiwiProxy] ProxyPipe::startPiping()');
643f5ea9
D
186
187 // Let the piping handle socket closures
188 this.irc_socket.removeAllListeners('error');
189 this.irc_socket.removeAllListeners('timeout');
190
191 this.irc_socket.on('close', this._onSocketClose.bind(this));
192
193 this.kiwi_socket.setEncoding('binary');
194
b5574d3b
D
195 this.kiwi_socket.pipe(this.irc_socket);
196 this.irc_socket.pipe(this.kiwi_socket);
197};
198
199
200
201
202
203/**
204 * ProxySocket
205 * Transparent socket interface to a kiwi proxy
206 */
207function ProxySocket(proxy_port, proxy_addr, meta) {
208 stream.Duplex.call(this);
209
210 this.connected_fn = null;
643f5ea9
D
211 this.proxy_addr = proxy_addr;
212 this.proxy_port = proxy_port;
213 this.meta = meta || {};
b5574d3b
D
214
215 this.state = 'disconnected';
216}
217
218util.inherits(ProxySocket, stream.Duplex);
219
220
221ProxySocket.prototype.setMeta = function(meta) {
222 this.meta = meta;
223};
224
225
226ProxySocket.prototype.connect = function(dest_port, dest_addr, connected_fn) {
227 this.meta.host = dest_addr;
228 this.meta.port = dest_port;
229 this.connected_fn = connected_fn;
230
231 if (!this.meta.host || !this.meta.port) {
232 debug('[KiwiProxy] Invalid destination addr/port', this.meta);
233 return false;
234 }
235
236 debug('[KiwiProxy] Connecting to proxy ' + this.proxy_addr + ':' + this.proxy_port.toString());
237 this.socket = net.connect(this.proxy_port, this.proxy_addr, this._onSocketConnect.bind(this));
238 this.socket.setTimeout(10000);
239
240 this.socket.on('data', this._onSocketData.bind(this));
241 this.socket.on('close', this._onSocketClose.bind(this));
242 this.socket.on('error', this._onSocketError.bind(this));
243
244 return this;
245};
246
247
248ProxySocket.prototype.destroySocket = function() {
249 if (!this.socket)
250 return;
251
252 this.socket.removeAllListeners();
253 this.socket.destroy();
254 delete this.socket;
643f5ea9
D
255
256 debug('[KiwiProxy] Destroying socket');
b5574d3b
D
257};
258
259
260ProxySocket.prototype._read = function() {
261 var data;
262
263 if (this.state === 'connected' && this.socket) {
264 while ((data = this.socket.read()) !== null) {
643f5ea9 265 if (this.push(data) === false) {
b5574d3b
D
266 break;
267 }
268 }
269 } else {
270 this.push('');
271 }
272};
273
274
275ProxySocket.prototype._write = function(chunk, encoding, callback) {
276 if (this.state === 'connected' && this.socket) {
277 return this.socket.write(chunk, encoding, callback);
278 } else {
643f5ea9 279 callback('Not connected');
b5574d3b
D
280 }
281};
282
283
284ProxySocket.prototype._onSocketConnect = function() {
285 var meta = this.meta || {};
286
287 this.state = 'handshaking';
288
289 debug('[KiwiProxy] Connected to proxy, sending meta');
290 this.socket.write(JSON.stringify(meta) + '\n');
291};
292
293
294ProxySocket.prototype._onSocketData = function(data) {
295 if (this.state === 'connected') {
296 this.emit('data', data);
297 return;
298 }
299
300 var buffer_str = data.toString(),
301 status = buffer_str[0],
302 error_code,
303 error_codes = {};
304
643f5ea9
D
305 error_codes[RESPONSE_ERROR] = 'ERROR';
306 error_codes[RESPONSE_ECONNRESET] = 'ECONNRESET';
b5574d3b 307 error_codes[RESPONSE_ECONNREFUSED] = 'ECONNREFUSED';
643f5ea9
D
308 error_codes[RESPONSE_ENOTFOUND] = 'ENOTFOUND';
309 error_codes[RESPONSE_ETIMEDOUT] = 'ETIMEDOUT';
b5574d3b
D
310
311 debug('[KiwiProxy] Recieved socket status: ' + data.toString());
312 if (status === RESPONSE_OK) {
313 debug('[KiwiProxy] Remote socket connected OK');
314 this.state = 'connected';
315
316 if (typeof this.connected_fn === 'function')
317 connected_fn();
318
319 this.emit('connect');
320
321 } else {
322 this.destroySocket();
323
324 error_code = error_codes[status] || error_codes[RESPONSE_ERROR];
325 debug('[KiwiProxy] Error: ' + error_code);
326 this.emit('error', new Error(error_code));
327 }
328};
329
330
331ProxySocket.prototype._onSocketClose = function(had_error) {
332 if (this.state === 'connected') {
333 this.emit('close', had_error);
334 return;
335 }
336
337 if (!this.ignore_close)
338 this.emit('error', new Error(RESPONSE_ERROR));
339};
340
341
342ProxySocket.prototype._onSocketError = function(err) {
643f5ea9 343 this.ignore_close = true;
b5574d3b
D
344 this.emit('error', err);
345};