channel:join + channel:leave plugin events
[KiwiIRC.git] / server / proxy.js
index ca55da19a99c49b563244928fa3b034ea92a70ef..0782fdd97d974a1b6f6f55c5c76cb2575f6a5887 100644 (file)
@@ -1,8 +1,9 @@
 var stream = require('stream'),
     util   = require('util'),
-    net    = require("net"),
-    tls    = require("tls"),
-    Identd = require('./identd');
+    events = require('events'),
+    net    = require('net'),
+    tls    = require('tls'),
+    fs     = require('fs');
 
 
 module.exports = {
@@ -11,7 +12,7 @@ module.exports = {
 };
 
 function debug() {
-    console.log.apply(console, arguments);
+    //console.log.apply(console, arguments);
 }
 
 // Socket connection responses
@@ -29,35 +30,54 @@ var RESPONSE_ETIMEDOUT     = '5';
  * Listens for connections from a kiwi server, dispatching ProxyPipe
  * instances for each connection
  */
-function ProxyServer() {}
+function ProxyServer() {
+    events.EventEmitter.call(this);
+}
+util.inherits(ProxyServer, events.EventEmitter);
 
 
-ProxyServer.prototype.listen = function(listen_port, listen_addr) {
-    var that = this;
+ProxyServer.prototype.listen = function(listen_port, listen_addr, opts) {
+    var that = this,
+        serv_opts = {},
+        connection_event = 'connection';
 
-    // Username lookup function for the identd
-    var identdResolveUser = function(port_here, port_there, callback) {
-        var key = port_here.toString() + '_' + port_there.toString();
+    opts = opts || {};
 
-        global.data.get(key, function(err, val) {
-            callback(val);
-        });
-    };
-    /*
-    this.identd_server = new Identd({
-        bind_addr: global.config.identd.address,
-        bind_port: global.config.identd.port,
-        user_id: identdResolveUser
-    });
+    // Listen using SSL?
+    if (opts.ssl) {
+        serv_opts = {
+            key: fs.readFileSync(opts.ssl_key),
+            cert: fs.readFileSync(opts.ssl_cert)
+        };
+
+        // Do we have an intermediate certificate?
+        if (typeof opts.ssl_ca !== 'undefined') {
+            // An array of them?
+            if (typeof opts.ssl_ca.map !== 'undefined') {
+                serv_opts.ca = opts.ssl_ca.map(function (f) { return fs.readFileSync(f); });
+
+            } else {
+                serv_opts.ca = fs.readFileSync(opts.ssl_ca);
+            }
+        }
+
+        this.server = tls.createServer(serv_opts);
 
-    this.identd_server.start();
-    */
-    // Start listening for proxy connections connections
-    this.server = new net.Server();
-    this.server.listen(listen_port, listen_addr);
+        connection_event = 'secureConnection';
+
+    }
 
-    this.server.on('connection', function(socket) {
-        new ProxyPipe(socket);
+    // No SSL, start a simple clear text server
+    else {
+        this.server = new net.Server();
+    }
+
+    this.server.listen(listen_port, listen_addr, function() {
+        that.emit('listening');
+    });
+
+    this.server.on(connection_event, function(socket) {
+        new ProxyPipe(socket, that);
     });
 };
 
@@ -82,19 +102,21 @@ ProxyServer.prototype.close = function(callback) {
  * 3. Reply to the kiwi server with connection status
  * 4. If all ok, pipe data between the 2 sockets as a proxy
  */
-function ProxyPipe(kiwi_socket) {
-    this.kiwi_socket = kiwi_socket;
-    this.irc_socket  = null;
-    this.buffer      = '';
-    this.meta        = null;
+function ProxyPipe(kiwi_socket, proxy_server) {
+    debug('[KiwiProxy] New Kiwi connection');
+
+    this.kiwi_socket  = kiwi_socket;
+    this.proxy_server = proxy_server;
+    this.irc_socket   = null;
+    this.buffers      = [];
+    this.meta         = null;
 
-    kiwi_socket.setEncoding('utf8');
     kiwi_socket.on('readable', this.kiwiSocketOnReadable.bind(this));
 }
 
 
 ProxyPipe.prototype.destroy = function() {
-    this.buffer = null;
+    this.buffers = null;
     this.meta = null;
 
     if (this.irc_socket) {
@@ -112,29 +134,35 @@ ProxyPipe.prototype.destroy = function() {
 
 
 ProxyPipe.prototype.kiwiSocketOnReadable = function() {
-    var chunk, meta;
+    var chunk, buffer, meta;
 
     while ((chunk = this.kiwi_socket.read()) !== null) {
-        this.buffer += chunk;
+        this.buffers.push(chunk);
     }
 
     // Not got a complete line yet? Wait some more
-    if (this.buffer.indexOf('\n') === -1)
+    chunk = this.buffers[this.buffers.length-1];
+    if (!chunk || chunk[chunk.length-1] !== 0x0A)
         return;
 
+    buffer = new Buffer.concat(this.buffers);
+    this.buffers = null;
+
     try {
-        meta = JSON.parse(this.buffer.substr(0, this.buffer.indexOf('\n')));
+        debug('[KiwiProxy] Found a complete line in the buffer');
+        meta = JSON.parse(buffer.toString('utf8'));
     } catch (err) {
+        debug('[KiwiProxy] Error parsing meta');
         this.destroy();
         return;
     }
 
     if (!meta.username) {
+        debug('[KiwiProxy] Meta does not contain a username');
         this.destroy();
         return;
     }
 
-    this.buffer = '';
     this.meta = meta;
     this.kiwi_socket.removeAllListeners('readable');
 
@@ -143,20 +171,51 @@ ProxyPipe.prototype.kiwiSocketOnReadable = function() {
 
 
 ProxyPipe.prototype.makeIrcConnection = function() {
-    debug('[KiwiProxy] Proxied connection to: ' + this.meta.host + ':' + this.meta.port.toString());
-    this.irc_socket = this.meta.ssl ?
-        tls.connect(parseInt(this.meta.port, 10), this.meta.host, this._onSocketConnect.bind(this)) :
-        net.connect(parseInt(this.meta.port, 10), this.meta.host, this._onSocketConnect.bind(this));
+    debug('[KiwiProxy] Opening proxied connection to: ' + this.meta.host + ':' + this.meta.port.toString());
+
+    var local_address = this.meta.interface ?
+        this.meta.interface :
+        '0.0.0.0';
+
+    if (this.meta.ssl) {
+        this.irc_socket = tls.connect({
+            port: parseInt(this.meta.port, 10),
+            host: this.meta.host,
+            rejectUnauthorized: global.config.reject_unauthorised_certificates,
+            localAddress: local_address
+        }, this._onSocketConnect.bind(this));
+
+    } else {
+        this.irc_socket = net.connect({
+            port: parseInt(this.meta.port, 10),
+            host: this.meta.host,
+            localAddress: local_address
+        }, this._onSocketConnect.bind(this));
+    }
 
     this.irc_socket.setTimeout(10000);
     this.irc_socket.on('error', this._onSocketError.bind(this));
     this.irc_socket.on('timeout', this._onSocketTimeout.bind(this));
+
+    // We need the raw socket connect event, not after any SSL handshakes or anything
+    if (this.irc_socket.socket) {
+        this.irc_socket.socket.on('connect', this._onRawSocketConnect.bind(this));
+    } else {
+        this.irc_socket.on('connect', this._onRawSocketConnect.bind(this));
+    }
+};
+
+
+ProxyPipe.prototype._onRawSocketConnect = function() {
+    this.proxy_server.emit('socket_connected', this);
 };
 
 
 ProxyPipe.prototype._onSocketConnect = function() {
     debug('[KiwiProxy] ProxyPipe::_onSocketConnect()');
 
+    this.proxy_server.emit('connection_open', this);
+
     // Now that we're connected to the detination, return no
     // error back to the kiwi server and start piping
     this.kiwi_socket.write(new Buffer(RESPONSE_OK.toString()), this.startPiping.bind(this));
@@ -185,6 +244,7 @@ ProxyPipe.prototype._onSocketTimeout = function() {
 
 ProxyPipe.prototype._onSocketClose = function() {
     debug('[KiwiProxy] IRC Socket closed');
+    this.proxy_server.emit('connection_close', this);
     this.destroy();
 };
 
@@ -198,8 +258,6 @@ ProxyPipe.prototype.startPiping = function() {
 
     this.irc_socket.on('close', this._onSocketClose.bind(this));
 
-    this.kiwi_socket.setEncoding('binary');
-
     this.kiwi_socket.pipe(this.irc_socket);
     this.irc_socket.pipe(this.kiwi_socket);
 };
@@ -212,13 +270,15 @@ ProxyPipe.prototype.startPiping = function() {
  * ProxySocket
  * Transparent socket interface to a kiwi proxy
  */
-function ProxySocket(proxy_port, proxy_addr, meta) {
+function ProxySocket(proxy_port, proxy_addr, meta, proxy_opts) {
     stream.Duplex.call(this);
 
     this.connected_fn = null;
     this.proxy_addr   = proxy_addr;
     this.proxy_port   = proxy_port;
-    this.meta         = meta || {};
+    this.proxy_opts   = proxy_opts || {};
+
+    this.setMeta(meta || {});
 
     this.state = 'disconnected';
 }
@@ -231,6 +291,12 @@ ProxySocket.prototype.setMeta = function(meta) {
 };
 
 
+ProxySocket.prototype.connectTls = function() {
+    this.meta.ssl = true;
+    return this.connect.apply(this, arguments);
+};
+
+
 ProxySocket.prototype.connect = function(dest_port, dest_addr, connected_fn) {
     this.meta.host = dest_addr;
     this.meta.port = dest_port;
@@ -241,10 +307,19 @@ ProxySocket.prototype.connect = function(dest_port, dest_addr, connected_fn) {
         return false;
     }
 
-    debug('[KiwiProxy] Connecting to proxy ' + this.proxy_addr + ':' + this.proxy_port.toString());
-    this.socket = net.connect(this.proxy_port, this.proxy_addr, this._onSocketConnect.bind(this));
-    this.socket.setTimeout(10000);
+    debug('[KiwiProxy] Connecting to proxy ' + this.proxy_addr + ':' + this.proxy_port.toString() + ' SSL: ' + (!!this.proxy_opts.ssl).toString());
+    if (this.proxy_opts.ssl) {
+        this.socket = tls.connect({
+            port: this.proxy_port,
+            host: this.proxy_addr,
+            rejectUnauthorized: !!global.config.reject_unauthorised_certificates,
+        }, this._onSocketConnect.bind(this));
 
+    } else {
+        this.socket = net.connect(this.proxy_port, this.proxy_addr, this._onSocketConnect.bind(this));
+    }
+
+    this.socket.setTimeout(10000);
     this.socket.on('data', this._onSocketData.bind(this));
     this.socket.on('close', this._onSocketClose.bind(this));
     this.socket.on('error', this._onSocketError.bind(this));
@@ -284,6 +359,7 @@ ProxySocket.prototype._write = function(chunk, encoding, callback) {
     if (this.state === 'connected' && this.socket) {
         return this.socket.write(chunk, encoding, callback);
     } else {
+        debug('[KiwiProxy] Trying to write to an unfinished socket. State=' + this.state);
         callback('Not connected');
     }
 };
@@ -337,6 +413,7 @@ ProxySocket.prototype._onSocketData = function(data) {
 
 
 ProxySocket.prototype._onSocketClose = function(had_error) {
+    debug('[KiwiProxy] _onSocketClose() had_error=' + had_error.toString());
     if (this.state === 'connected') {
         this.emit('close', had_error);
         return;
@@ -348,6 +425,7 @@ ProxySocket.prototype._onSocketClose = function(had_error) {
 
 
 ProxySocket.prototype._onSocketError = function(err) {
+    debug('[KiwiProxy] _onSocketError() err=' + err.toString());
     this.ignore_close = true;
     this.emit('error', err);
 };
\ No newline at end of file