Commit | Line | Data |
---|---|---|
b5574d3b | 1 | var 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 | ||
8 | module.exports = { | |
9 | ProxyServer: ProxyServer, | |
10 | ProxySocket: ProxySocket | |
11 | }; | |
12 | ||
13 | function debug() { | |
9556a028 | 14 | //console.log.apply(console, arguments); |
b5574d3b D |
15 | } |
16 | ||
17 | // Socket connection responses | |
643f5ea9 D |
18 | var RESPONSE_ERROR = '0'; |
19 | var RESPONSE_OK = '1'; | |
20 | var RESPONSE_ECONNRESET = '2'; | |
21 | var RESPONSE_ECONNREFUSED = '3'; | |
22 | var RESPONSE_ENOTFOUND = '4'; | |
23 | var 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 |
32 | function ProxyServer() { |
33 | events.EventEmitter.call(this); | |
34 | } | |
35 | util.inherits(ProxyServer, events.EventEmitter); | |
b5574d3b D |
36 | |
37 | ||
38 | ProxyServer.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 | ||
53 | ProxyServer.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 |
73 | function 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 | ||
85 | ProxyPipe.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 |
103 | ProxyPipe.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 | ||
134 | ProxyPipe.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 | ||
146 | ProxyPipe.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 | ||
157 | ProxyPipe.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 | ||
169 | ProxyPipe.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 |
177 | ProxyPipe.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 |
184 | ProxyPipe.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 | */ | |
207 | function 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 | ||
218 | util.inherits(ProxySocket, stream.Duplex); | |
219 | ||
220 | ||
221 | ProxySocket.prototype.setMeta = function(meta) { | |
222 | this.meta = meta; | |
223 | }; | |
224 | ||
225 | ||
226 | ProxySocket.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 | ||
248 | ProxySocket.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 | ||
260 | ProxySocket.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 | ||
275 | ProxySocket.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 | ||
284 | ProxySocket.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 | ||
294 | ProxySocket.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 | ||
331 | ProxySocket.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 | ||
342 | ProxySocket.prototype._onSocketError = function(err) { | |
643f5ea9 | 343 | this.ignore_close = true; |
b5574d3b D |
344 | this.emit('error', err); |
345 | }; |