channel:join + channel:leave plugin events
[KiwiIRC.git] / server / weblistener.js
index 3ab24370abd52d5318a0927ad4cfca5b38bfa550..0a61821585829a21eeff35c9eefc73863eac893a 100644 (file)
@@ -1,16 +1,20 @@
-var ws          = require('socket.io'),
-    events      = require('events'),
-    http        = require('http'),
-    https       = require('https'),
-    util        = require('util'),
-    fs          = require('fs'),
-    dns         = require('dns'),
-    url         = require('url'),
-    _           = require('lodash'),
-    Client      = require('./client.js').Client,
-    HttpHandler = require('./httphandler.js').HttpHandler,
-    rehash      = require('./rehash.js'),
-    range_check = require('range_check');
+var engine       = require('engine.io'),
+    WebsocketRpc = require('./websocketrpc.js'),
+    events       = require('events'),
+    http         = require('http'),
+    https        = require('https'),
+    util         = require('util'),
+    fs           = require('fs'),
+    dns          = require('dns'),
+    url          = require('url'),
+    _            = require('lodash'),
+    spdy         = require('spdy'),
+    ipaddr       = require('ipaddr.js'),
+    winston      = require('winston'),
+    Client       = require('./client.js').Client,
+    HttpHandler  = require('./httphandler.js').HttpHandler,
+    rehash       = require('./rehash.js'),
+    Stats        = require('./stats.js');
 
 
 
@@ -24,80 +28,126 @@ rehash.on('rehashed', function (files) {
 var http_handler;
 
 
-var WebListener = function (web_config, transports) {
-    var hs, opts, ws_opts,
+var WebListener = module.exports = function (web_config) {
+    var hs, opts,
         that = this;
 
 
     events.EventEmitter.call(this);
-    
-    http_handler = new HttpHandler(web_config);
-    
-    // Standard options for the socket.io connections
-    ws_opts = {
-        'log level': 0,
-        'log colors': 0
-    };
 
+    http_handler = new HttpHandler(web_config);
 
     if (web_config.ssl) {
         opts = {
-            key: fs.readFileSync(__dirname + '/' + web_config.ssl_key),
-            cert: fs.readFileSync(__dirname + '/' + web_config.ssl_cert)
+            key: fs.readFileSync(web_config.ssl_key),
+            cert: fs.readFileSync(web_config.ssl_cert)
         };
 
         // Do we have an intermediate certificate?
         if (typeof web_config.ssl_ca !== 'undefined') {
-            opts.ca = fs.readFileSync(__dirname + '/' + web_config.ssl_ca);
+            // An array of them?
+            if (typeof web_config.ssl_ca.map !== 'undefined') {
+                opts.ca = web_config.ssl_ca.map(function (f) { return fs.readFileSync(f); });
+
+            } else {
+                opts.ca = fs.readFileSync(web_config.ssl_ca);
+            }
         }
 
+        hs = spdy.createServer(opts);
 
-        hs = https.createServer(opts, handleHttpRequest);
-        
-        // Start socket.io listening on this weblistener
-        this.ws = ws.listen(hs, _.extend({ssl: true}, ws_opts));
         hs.listen(web_config.port, web_config.address, function () {
             that.emit('listening');
         });
-
-        console.log('Listening on ' + web_config.address + ':' + web_config.port.toString() + ' with SSL');
     } else {
 
         // Start some plain-text server up
-        hs = http.createServer(handleHttpRequest);
+        hs = http.createServer();
 
-        // Start socket.io listening on this weblistener
-        this.ws = ws.listen(hs, _.extend({ssl: false}, ws_opts));
         hs.listen(web_config.port, web_config.address, function () {
             that.emit('listening');
         });
-
-        console.log('Listening on ' + web_config.address + ':' + web_config.port.toString() + ' without SSL');
     }
-    
-    this.ws.enable('browser client minification');
-    this.ws.enable('browser client etag');
-    this.ws.set('transports', transports);
-    this.ws.set('resource', (global.config.http_base_path || '') + '/transport');
-
-    this.ws.of('/kiwi').authorization(authoriseConnection)
-        .on('connection', function () {
-            newConnection.apply(that, arguments);
+
+    hs.on('error', function (err) {
+        that.emit('error', err);
+    });
+
+    this.ws = new engine.Server();
+
+    hs.on('upgrade', function(req, socket, head){
+        // engine.io can sometimes "loose" the clients remote address. Keep note of it
+        req.meta = {
+            remote_address: req.connection.remoteAddress
+        };
+
+        that.ws.handleUpgrade(req, socket, head);
+    });
+
+    hs.on('request', function(req, res){
+        var base_path = (global.config.http_base_path || ''),
+            transport_url;
+
+        // Trim off any trailing slashes
+        if (base_path.substr(base_path.length - 1) === '/') {
+            base_path = base_path.substr(0, base_path.length - 1);
+        }
+        transport_url = base_path + '/transport';
+
+        Stats.incr('http.request');
+
+        // engine.io can sometimes "loose" the clients remote address. Keep note of it
+        req.meta = {
+            remote_address: req.connection.remoteAddress
+        };
+
+        // If the request is for our transport, pass it onto engine.io
+        if (req.url.toLowerCase().indexOf(transport_url.toLowerCase()) === 0) {
+            that.ws.handleRequest(req, res);
+        } else {
+            http_handler.serve(req, res);
         }
-    );
-    this.ws.of('/kiwi').on('error', console.log);
+
+
+    });
+
+    this.ws.on('connection', function(socket) {
+        Stats.incr('http.websocket');
+
+        initialiseSocket(socket, function(err, authorised) {
+            var client;
+
+            if (!authorised) {
+                socket.close();
+                return;
+            }
+
+            client = new Client(socket, {server_config: web_config});
+            client.on('dispose', function () {
+                that.emit('client_dispose', this);
+            });
+
+            that.emit('connection', client);
+
+            // Call any modules listening for new clients
+            global.modules.emit('client created', {client: client});
+        });
+    });
 };
 util.inherits(WebListener, events.EventEmitter);
 
 
 
-function handleHttpRequest(request, response) {
-    var uri = url.parse(request.url, true);
-    
-    // If this isn't a socket.io request, pass it onto the http handler
-    if (uri.pathname.substr(0, 10) !== '/socket.io') {
-        http_handler.serve(request, response);
+function rangeCheck(addr, range) {
+    var i, ranges, parts;
+    ranges = (!_.isArray(range)) ? [range] : range;
+    for (i = 0; i < ranges.length; i++) {
+        parts = ranges[i].split('/');
+        if (ipaddr.process(addr).match(ipaddr.process(parts[0]), parts[1])) {
+            return true;
+        }
     }
+    return false;
 }
 
 
@@ -105,54 +155,68 @@ function handleHttpRequest(request, response) {
  * Get the reverse DNS entry for this connection.
  * Used later on for webirc, etc functionality
  */
-function authoriseConnection(handshakeData, callback) {
-    var address = handshakeData.address.address;
+function initialiseSocket(socket, callback) {
+    var request = socket.request,
+        address = request.meta.remote_address,
+        revdns;
+
+    // Key/val data stored to the socket to be read later on
+    // May also be synced to a redis DB to lookup clients
+    socket.meta = socket.request.meta;
 
     // If a forwarded-for header is found, switch the source address
-    if (handshakeData.headers[global.config.http_proxy_ip_header || 'x-forwarded-for']) {
+    if (request.headers[global.config.http_proxy_ip_header || 'x-forwarded-for']) {
         // Check we're connecting from a whitelisted proxy
-        if (!global.config.http_proxies || !range_check.in_range(address, global.config.http_proxies)) {
-            console.log('Unlisted proxy:', address);
+        if (!global.config.http_proxies || !rangeCheck(address, global.config.http_proxies)) {
+            winston.info('Unlisted proxy: %s', address);
             callback(null, false);
             return;
         }
 
         // We're sent from a whitelisted proxy, replace the hosts
-        address = handshakeData.headers['x-forwarded-for'];
+        address = request.headers[global.config.http_proxy_ip_header || 'x-forwarded-for'];
     }
 
-    handshakeData.real_address = address;
-    
+    socket.meta.real_address = address;
+
     // If enabled, don't go over the connection limit
     if (global.config.max_client_conns && global.config.max_client_conns > 0) {
         if (global.clients.numOnAddress(address) + 1 > global.config.max_client_conns) {
             return callback(null, false);
         }
     }
-        
-    dns.reverse(address, function (err, domains) {
-        if (err || domains.length === 0) {
-            handshakeData.revdns = address;
-        } else {
-            handshakeData.revdns = _.first(domains) || address;
-        }
-        
-        // All is well, authorise the connection
-        callback(null, true);
-    });
-}
-
-function newConnection(websocket) {
-    var client, that = this;
-    client = new Client(websocket);
-    client.on('destroy', function () {
-        that.emit('destroy', this);
-    });
-    this.emit('connection', client);
-}
-
-
 
 
+    try {
+        dns.reverse(address, function (err, domains) {
+            if (!err && domains.length > 0) {
+                revdns = _.first(domains);
+            }
+
+            if (!revdns) {
+                // No reverse DNS found, use the IP
+                socket.meta.revdns = address;
+                callback(null, true);
+
+            } else {
+                // Make sure the reverse DNS matches the A record to use the hostname..
+                dns.lookup(revdns, function (err, ip_address, family) {
+                    if (!err && ip_address == address) {
+                        // A record matches PTR, perfectly valid hostname
+                        socket.meta.revdns = revdns;
+                    } else {
+                        // A record does not match the PTR, invalid hostname
+                        socket.meta.revdns = address;
+                    }
+
+                    // We have all the info we need, proceed with the connection
+                    callback(null, true);
+                });
+            }
+        });
 
-module.exports = WebListener;
\ No newline at end of file
+    } catch (err) {
+        socket.meta.revdns = address;
+        callback(null, true);
+    }
+}