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() { | |
9556a028 | 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, | |
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 | ||
82 | ProxyServer.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 |
102 | function 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 | ||
114 | ProxyPipe.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 |
132 | ProxyPipe.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 | ||
163 | ProxyPipe.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 | ||
182 | ProxyPipe.prototype._onRawSocketConnect = function() { | |
183 | this.proxy_server.emit('socket_connected', this); | |
b5574d3b D |
184 | }; |
185 | ||
186 | ||
187 | ProxyPipe.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 | ||
198 | ProxyPipe.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 | ||
210 | ProxyPipe.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 |
218 | ProxyPipe.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 |
225 | ProxyPipe.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 | 248 | function 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 | ||
260 | util.inherits(ProxySocket, stream.Duplex); | |
261 | ||
262 | ||
263 | ProxySocket.prototype.setMeta = function(meta) { | |
264 | this.meta = meta; | |
265 | }; | |
266 | ||
267 | ||
96ecb5e7 D |
268 | ProxySocket.prototype.connectTls = function() { |
269 | this.meta.ssl = true; | |
270 | return this.connect.apply(this, arguments); | |
271 | }; | |
272 | ||
273 | ||
b5574d3b D |
274 | ProxySocket.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 | ||
298 | ProxySocket.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 | ||
310 | ProxySocket.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 | ||
325 | ProxySocket.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 | ||
334 | ProxySocket.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 | ||
344 | ProxySocket.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 | ||
381 | ProxySocket.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 | ||
392 | ProxySocket.prototype._onSocketError = function(err) { | |
643f5ea9 | 393 | this.ignore_close = true; |
b5574d3b D |
394 | this.emit('error', err); |
395 | }; |