diff --git a/lib/manager.js b/lib/manager.js index 8c030912cd..2defc5aa2a 100644 --- a/lib/manager.js +++ b/lib/manager.js @@ -21,6 +21,7 @@ var http = require('http') , Socket = require('./socket') , MemoryStore = require('./stores/memory') , SocketNamespace = require('./namespace') + , Static = require('./static') , EventEmitter = process.EventEmitter; /** @@ -64,6 +65,7 @@ function Manager (server) { , log: true , store: new MemoryStore , logger: new Logger + , static: new Static(this) , heartbeats: true , resource: '/socket.io' , transports: defaultTransports @@ -77,8 +79,10 @@ function Manager (server) { , 'flash policy port': 843 , 'destroy upgrade': true , 'browser client': true + , 'browser client cache': true , 'browser client minification': false , 'browser client etag': false + , 'browser client gzip': false , 'browser client handler': false , 'client store expiration': 15 }; @@ -137,6 +141,16 @@ Manager.prototype.__defineGetter__('log', function () { return logger; }); +/** + * Static accessor. + * + * @api public + */ + +Manager.prototype.__defineGetter__('static', function () { + return this.get('static'); +}); + /** * Get settings. * @@ -496,7 +510,7 @@ Manager.prototype.handleRequest = function (req, res) { if (data.static || !data.transport && !data.protocol) { if (data.static && this.enabled('browser client')) { - this.handleClientRequest(req, res, data); + this.static.write(data.path, req, res); } else { res.writeHead(200); res.end('Welcome to socket.io.'); @@ -626,104 +640,6 @@ Manager.prototype.handleClient = function (data, req) { } }; -/** - * Dictionary for static file serving - * - * @api public - */ - -Manager.static = { - cache: {} - , paths: { - '/static/flashsocket/WebSocketMain.swf': client.dist + '/WebSocketMain.swf' - , '/static/flashsocket/WebSocketMainInsecure.swf': - client.dist + '/WebSocketMainInsecure.swf' - , '/socket.io.js': client.dist + '/socket.io.js' - , '/socket.io.js.min': client.dist + '/socket.io.min.js' - } - , mime: { - 'js': { - contentType: 'application/javascript' - , encoding: 'utf8' - } - , 'swf': { - contentType: 'application/x-shockwave-flash' - , encoding: 'binary' - } - } -}; - -/** - * Serves the client. - * - * @api private - */ - -Manager.prototype.handleClientRequest = function (req, res, data) { - var static = Manager.static - , extension = data.path.split('.').pop() - , file = data.path + (this.enabled('browser client minification') - && extension == 'js' ? '.min' : '') - , location = static.paths[file] - , cache = static.cache[file]; - - var self = this; - - /** - * Writes a response, safely - * - * @api private - */ - - function write (status, headers, content, encoding) { - try { - res.writeHead(status, headers || null); - res.end(content || '', encoding || null); - } catch (e) {} - } - - function serve () { - if (req.headers['if-none-match'] === cache.Etag) { - return write(304); - } - - var mime = static.mime[extension] - , headers = { - 'Content-Type': mime.contentType - , 'Content-Length': cache.length - }; - - if (self.enabled('browser client etag') && cache.Etag) { - headers.Etag = cache.Etag; - } - - write(200, headers, cache.content, mime.encoding); - self.log.debug('served static ' + data.path); - } - - if (this.get('browser client handler')) { - this.get('browser client handler').call(this, req, res); - } else if (!cache) { - fs.readFile(location, function (err, data) { - if (err) { - write(500, null, 'Error serving static ' + data.path); - self.log.warn('Can\'t cache '+ data.path +', ' + err.message); - return; - } - - cache = Manager.static.cache[file] = { - content: data - , length: data.length - , Etag: client.version - }; - - serve(); - }); - } else { - serve(); - } -}; - /** * Generates a session id. * @@ -944,7 +860,7 @@ Manager.prototype.checkRequest = function (req) { data.protocol = Number(pieces[1]); data.transport = pieces[2]; data.id = pieces[3]; - data.static = !!Manager.static.paths[path]; + data.static = !!this.static.has(path); }; return data; diff --git a/lib/socket.io.js b/lib/socket.io.js index ce07dca5eb..9393fc3d88 100644 --- a/lib/socket.io.js +++ b/lib/socket.io.js @@ -92,6 +92,14 @@ exports.Transport = require('./transport'); exports.Socket = require('./socket'); +/** + * Static constructor. + * + * @api public + */ + +exports.Static = require('./static'); + /** * Store constructor. * diff --git a/lib/static.js b/lib/static.js new file mode 100644 index 0000000000..03505ddd40 --- /dev/null +++ b/lib/static.js @@ -0,0 +1,374 @@ + +/*! +* socket.io-node +* Copyright(c) 2011 LearnBoost +* MIT Licensed +*/ + +/** + * Module dependencies. + */ + +var client = require('socket.io-client') + , cp = require('child_process') + , fs = require('fs') + , util = require('./util'); + +/** + * File type details. + * + * @api private + */ + +var mime = { + js: { + type: 'application/javascript' + , encoding: 'utf8' + , gzip: true + } + , swf: { + type: 'application/x-shockwave-flash' + , encoding: 'binary' + , gzip: false + } +}; + +/** + * Regexp for matching custom transport patterns. Users can configure their own + * socket.io bundle based on the url structure. Different transport names are + * concatinated using the `+` char. /socket.io/socket.io+websocket.js should + * create a bundle that only contains support for the websocket. + * + * @api private + */ + +var bundle = /\+((?:\+)?[\w\-]+)*(?:\.js)$/g; + +/** + * Export the constructor + */ + +exports = module.exports = Static; + +/** + * Static constructor + * + * @api public + */ + +function Static (manager) { + this.manager = manager; + this.cache = {}; + this.paths = {}; + + this.init(); +} + +/** + * Initialize the Static by adding default file paths. + * + * @api public + */ + +Static.prototype.init = function () { + /** + * Generates a unique id based the supplied transports array + * + * @param {Array} transports The array with transport types + * @api private + */ + function id (transports) { + var id = transports.join('').split('').map(function (char) { + return ('' + char.charCodeAt(0)).split('').pop(); + }).reduce(function (char, id) { + return char +id; + }); + + return client.version + ':' + id; + } + + /** + * Generates a socket.io-client file based on the supplied transports. + * + * @param {Array} transports The array with transport types + * @param {Function} callback Callback for the static.write + * @api private + */ + + function build (transports, callback) { + client.builder(transports, { + minify: self.manager.enabled('browser client minification') + }, function (err, content) { + callback(err, content ? new Buffer(content) : null, id(transports)); + } + ); + } + + var self = this; + + // add our default static files + this.add('/static/flashsocket/WebSocketMain.swf', { + file: client.dist + '/WebSocketMain.swf' + }); + + this.add('/static/flashsocket/WebSocketMainInsecure.swf', { + file: client.dist + '/WebSocketMainInsecure.swf' + }); + + // generates dedicated build based on the available transports + this.add('/socket.io.js', function (path, callback) { + build(self.manager.get('transports'), callback); + }); + + // allow custom builds based on url paths + this.add('/socket.io+', { mime: mime.js }, function (path, callback) { + var available = self.manager.get('transports') + , matches = bundle.exec(path) + , transports = []; + + if (!matches) return callback('No valid transports'); + + // make sure they valid transports + matches[0].split('.')[0].split('+').slice(1).forEach(function (transport) { + if (!!~available.indexOf(transport)) { + transports.push(transport); + } + }); + + if (!transports.length) return callback('No valid transports'); + build(transports, callback); + }); + + // clear cache when transports change + this.manager.on('set:transports', function (key, value) { + delete self.cache['/socket.io.js']; + Object.keys(self.cache).forEach(function (key) { + if (bundle.test(key)) { + delete self.cache[key]; + } + }); + }); +}; + +/** + * Gzip compress buffers. + * + * @param {Buffer} data The buffer that needs gzip compression + * @param {Function} callback + * @api public + */ + +Static.prototype.gzip = function (data, callback) { + var gzip = cp.spawn('gzip', ['-9', '-c', '-f', '-n']) + , encoding = Buffer.isBuffer(data) ? 'binary' : 'utf8' + , buffer = [] + , err; + + gzip.stdout.on('data', function (data) { + buffer.push(data); + }); + + gzip.stderr.on('data', function (data) { + err = data +''; + buffer.length = 0; + }); + + gzip.on('exit', function () { + if (err) return callback(err); + + var size = 0 + , index = 0 + , i = buffer.length + , content; + + while (i--) { + size += buffer[i].length; + } + + content = new Buffer(size); + i = buffer.length; + + buffer.forEach(function (buffer) { + var length = buffer.length; + + buffer.copy(content, index, 0, length); + index += length; + }); + + buffer.length = 0; + callback(null, content); + }); + + gzip.stdin.end(data, encoding); +}; + +/** + * Is the path a static file? + * + * @param {String} path The path that needs to be checked + * @api public + */ + +Static.prototype.has = function (path) { + // fast case + if (this.paths[path]) return this.paths[path]; + + var keys = Object.keys(this.paths) + , i = keys.length; + + while (i--) { + if (!!~path.indexOf(keys[i])) return this.paths[keys[i]]; + } + + return false; +}; + +/** + * Add new paths new paths that can be served using the static provider. + * + * @param {String} path The path to respond to + * @param {Options} options Options for writing out the response + * @param {Function} [callback] Optional callback if no options.file is + * supplied this would be called instead. + * @api public + */ + +Static.prototype.add = function (path, options, callback) { + var extension = /(?:\.(\w{1,4}))$/.exec(path); + + if (!callback && typeof options == 'function') { + callback = options; + options = {}; + } + + options.mime = options.mime || (extension ? mime[extension[1]] : false); + + if (callback) options.callback = callback; + if (!(options.file || options.callback) || !options.mime) return false; + + this.paths[path] = options; + + return true; +}; + +/** + * Writes a static response. + * + * @param {String} path The path for the static content + * @param {HTTPRequest} req The request object + * @param {HTTPResponse} res The response object + * @api public + */ + +Static.prototype.write = function (path, req, res) { + /** + * Write a response without throwing errors because can throw error if the + * response is no longer writable etc. + * + * @api private + */ + + function write (status, headers, content, encoding) { + try { + res.writeHead(status, headers || undefined); + res.end(content || '', encoding || undefined); + } catch (e) {} + } + + /** + * Answers requests depending on the request properties and the reply object. + * + * @param {Object} reply The details and content to reply the response with + * @api private + */ + + function answer (reply) { + var cached = req.headers['if-none-match'] === reply.etag; + if (cached && self.manager.enabled('browser client etag')) { + return write(304); + } + + var accept = req.headers['accept-encoding'] || '' + , gzip = !!~accept.toLowerCase().indexOf('gzip') + , mime = reply.mime + , headers = { + 'Content-Type': mime.type + }; + + // check if we can add a etag + if (self.manager.enabled('browser client etag') && reply.etag) { + headers['Etag'] = reply.etag; + } + + // check if we can send gzip data + if (gzip && reply.gzip) { + headers['Content-Length'] = reply.gzip.length; + headers['Content-Encoding'] = 'gzip'; + write(200, headers, reply.gzip.content, mime.encoding); + } else { + headers['Content-Length'] = reply.length; + write(200, headers, reply.content, mime.encoding); + } + + self.manager.log.debug('served static content ' + path); + } + + var self = this + , details; + + // most common case first + if (this.manager.enabled('browser client cache') && this.cache[path]) { + return answer(this.cache[path]); + } else if (this.manager.get('browser client handler')) { + return this.manager.get('browser client handler').call(this, req, res); + } else if ((details = this.has(path))) { + /** + * A small helper function that will let us deal with fs and dynamic files + * + * @param {Object} err Optional error + * @param {Buffer} content The data + * @api private + */ + + function ready (err, content, etag) { + if (err) { + self.manager.log.warn('Unable to serve file. ' + (err.message || err)); + return write(500, null, 'Error serving static ' + path); + } + + // store the result in the cache + var reply = self.cache[path] = { + content: content + , length: content.length + , mime: details.mime + , etag: etag || client.version + }; + + // check if gzip is enabled + if (details.mime.gzip && self.manager.enabled('browser client gzip')) { + self.gzip(content, function (err, content) { + if (!err) { + reply.gzip = { + content: content + , length: content.length + } + } + + answer(reply); + }); + } else { + answer(reply); + } + } + + if (details.file) { + fs.readFile(details.file, ready); + } else if(details.callback) { + details.callback.call(this, path, ready); + } else { + write(404, null, 'File handle not found'); + } + } else { + write(404, null, 'File not found'); + } +}; diff --git a/test/manager.test.js b/test/manager.test.js index 4a1dfd9af3..03f30447cd 100644 --- a/test/manager.test.js +++ b/test/manager.test.js @@ -134,236 +134,6 @@ module.exports = { }); }, - 'test that the client is served': function (done) { - var port = ++ports - , io = sio.listen(port) - , cl = client(port); - - cl.get('/socket.io/socket.io.js', function (res, data) { - res.headers['content-type'].should.eql('application/javascript'); - res.headers['content-length'].should.match(/([0-9]+)/); - should.strictEqual(res.headers.etag, undefined); - - data.should.match(/XMLHttpRequest/); - - cl.end(); - io.server.close(); - done(); - }); - }, - - 'test that the client etag is served': function (done) { - var port = ++ports - , io = sio.listen(port) - , cl = client(port); - - io.configure(function () { - io.enable('browser client etag'); - }); - - cl.get('/socket.io/socket.io.js', function (res, data) { - res.headers['content-type'].should.eql('application/javascript'); - res.headers['content-length'].should.match(/([0-9]+)/); - res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/); - - data.should.match(/XMLHttpRequest/); - - cl.end(); - io.server.close(); - done(); - }); - }, - - 'test that the cached client is served': function (done) { - var port = ++ports - , io = sio.listen(port) - , cl = client(port); - - cl.get('/socket.io/socket.io.js', function (res, data) { - res.headers['content-type'].should.eql('application/javascript'); - res.headers['content-length'].should.match(/([0-9]+)/); - should.strictEqual(res.headers.etag, undefined); - - data.should.match(/XMLHttpRequest/); - var static = sio.Manager.static; - static.cache['/socket.io.js'].content.should.match(/XMLHttpRequest/); - - cl.get('/socket.io/socket.io.js', function (res, data) { - res.headers['content-type'].should.eql('application/javascript'); - res.headers['content-length'].should.match(/([0-9]+)/); - should.strictEqual(res.headers.etag, undefined); - - data.should.match(/XMLHttpRequest/); - - cl.end(); - io.server.close(); - done(); - }); - }); - }, - - 'test that the cached client etag is served': function (done) { - var port = ++ports - , io = sio.listen(port) - , cl = client(port); - - io.configure(function () { - io.enable('browser client etag'); - }); - - cl.get('/socket.io/socket.io.js', function (res, data) { - res.headers['content-type'].should.eql('application/javascript'); - res.headers['content-length'].should.match(/([0-9]+)/); - res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/); - - data.should.match(/XMLHttpRequest/); - var static = sio.Manager.static - , cache = static.cache['/socket.io.js']; - - cache.content.toString().should.match(/XMLHttpRequest/); - Buffer.isBuffer(cache.content).should.be.true; - - cl.get('/socket.io/socket.io.js', function (res, data) { - res.headers['content-type'].should.eql('application/javascript'); - res.headers['content-length'].should.match(/([0-9]+)/); - res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/); - - data.should.match(/XMLHttpRequest/); - - cl.end(); - io.server.close(); - done(); - }); - }); - }, - - 'test that the cached client sends a 304 header': function (done) { - var port = ++ports - , io = sio.listen(port) - , cl = client(port); - - io.configure(function () { - io.enable('browser client etag'); - }); - - cl.get('/socket.io/socket.io.js', function (res, data) { - cl.get('/socket.io/socket.io.js', {headers:{'if-none-match':res.headers.etag}}, function (res, data) { - res.statusCode.should.eql(304); - - cl.end(); - io.server.close(); - done(); - }); - }); - }, - - 'test that client minification works': function (done) { - // server 1 - var port = ++ports - , io = sio.listen(port) - , cl = client(port); - - // server 2 - var port = ++ports - , io2 = sio.listen(port) - , cl2 = client(port); - - io.configure(function () { - io.enable('browser client minification'); - }); - - cl.get('/socket.io/socket.io.js', function (res, data) { - var length = data.length; - - cl.end(); - io.server.close(); - - cl2.get('/socket.io/socket.io.js', function (res, data) { - res.headers['content-type'].should.eql('application/javascript'); - res.headers['content-length'].should.match(/([0-9]+)/); - should.strictEqual(res.headers.etag, undefined); - - data.should.match(/XMLHttpRequest/); - data.length.should.be.greaterThan(length); - - cl2.end(); - io2.server.close(); - done(); - }); - }); - }, - - 'test that the WebSocketMain.swf is served': function (done) { - var port = ++ports - , io = sio.listen(port) - , cl = client(port); - - cl.get('/socket.io/static/flashsocket/WebSocketMain.swf', function (res, data) { - res.headers['content-type'].should.eql('application/x-shockwave-flash'); - res.headers['content-length'].should.match(/([0-9]+)/); - should.strictEqual(res.headers.etag, undefined); - - var static = sio.Manager.static - , cache = static.cache['/static/flashsocket/WebSocketMain.swf']; - - Buffer.isBuffer(cache.content).should.be.true; - - cl.end(); - io.server.close(); - done(); - }); - }, - - 'test that the WebSocketMainInsecure.swf is served': function (done) { - var port = ++ports - , io = sio.listen(port) - , cl = client(port); - - cl.get('/socket.io/static/flashsocket/WebSocketMainInsecure.swf', function (res, data) { - res.headers['content-type'].should.eql('application/x-shockwave-flash'); - res.headers['content-length'].should.match(/([0-9]+)/); - should.strictEqual(res.headers.etag, undefined); - - var static = sio.Manager.static - , cache = static.cache['/static/flashsocket/WebSocketMain.swf']; - - Buffer.isBuffer(cache.content).should.be.true; - - cl.end(); - io.server.close(); - done(); - }); - }, - - 'test that you can serve custom clients': function (done) { - var port = ++ports - , io = sio.listen(port) - , cl = client(port); - - io.configure(function () { - io.set('browser client handler', function (req, res) { - res.writeHead(200, { - 'Content-Type': 'application/javascript' - , 'Content-Length': 13 - , 'ETag': '1.0' - }); - res.end('custom_client'); - }); - }); - - cl.get('/socket.io/socket.io.js', function (res, data) { - res.headers['content-type'].should.eql('application/javascript'); - res.headers['content-length'].should.eql(13); - res.headers.etag.should.eql('1.0'); - - data.should.eql('custom_client'); - - cl.end(); - io.server.close(); - done(); - }); - }, - 'test that you can disable clients': function (done) { var port = ++ports , io = sio.listen(port) diff --git a/test/static.test.js b/test/static.test.js new file mode 100644 index 0000000000..06ca98e05b --- /dev/null +++ b/test/static.test.js @@ -0,0 +1,460 @@ + +/*! +* socket.io-node +* Copyright(c) 2011 LearnBoost +* MIT Licensed +*/ + +/** + * Test dependencies. + */ + +var sio = require('socket.io') + , should = require('./common') + , ports = 15400; + +/** + * Test. + */ + +module.exports = { + + 'test that the default static files are available': function (done) { + var port = ++ports + , io = sio.listen(port); + + (!!io.static.has('/socket.io.js')).should.be.true; + (!!io.static.has('/socket.io+')).should.be.true; + (!!io.static.has('/static/flashsocket/WebSocketMain.swf')).should.be.true; + (!!io.static.has('/static/flashsocket/WebSocketMainInsecure.swf')).should.be.true; + + io.server.close(); + done(); + }, + + 'test that static files are correctly looked up': function (done) { + var port = ++ports + , io = sio.listen(port); + + (!!io.static.has('/socket.io.js')).should.be.true; + (!!io.static.has('/invalidfilehereplease.js')).should.be.false; + + io.server.close(); + done(); + }, + + 'test that the client is served': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + cl.get('/socket.io/socket.io.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + should.strictEqual(res.headers.etag, undefined); + + data.should.match(/XMLHttpRequest/); + + cl.end(); + io.server.close(); + done(); + }); + }, + + 'test that the custom build client is served': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.enable('browser client etag'); + + cl.get('/socket.io/socket.io+websocket.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/); + + data.should.match(/XMLHttpRequest/); + data.should.match(/WS\.prototype\.name/); + data.should.not.match(/Flashsocket\.prototype\.name/); + data.should.not.match(/HTMLFile\.prototype\.name/); + data.should.not.match(/JSONPPolling\.prototype\.name/); + data.should.not.match(/XHRPolling\.prototype\.name/); + + cl.end(); + io.server.close(); + done(); + }); + }, + + 'test that the client is build with the enabled transports': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.set('transports', ['websocket']); + + cl.get('/socket.io/socket.io.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + + data.should.match(/XMLHttpRequest/); + data.should.match(/WS\.prototype\.name/); + data.should.not.match(/Flashsocket\.prototype\.name/); + data.should.not.match(/HTMLFile\.prototype\.name/); + data.should.not.match(/JSONPPolling\.prototype\.name/); + data.should.not.match(/XHRPolling\.prototype\.name/); + + cl.end(); + io.server.close(); + done(); + }); + }, + + 'test that the client cache is cleared when transports change': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.set('transports', ['websocket']); + + cl.get('/socket.io/socket.io.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + + data.should.match(/XMLHttpRequest/); + data.should.match(/WS\.prototype\.name/); + data.should.not.match(/Flashsocket\.prototype\.name/); + data.should.not.match(/HTMLFile\.prototype\.name/); + data.should.not.match(/JSONPPolling\.prototype\.name/); + data.should.not.match(/XHRPolling\.prototype\.name/); + + io.set('transports', ['xhr-polling']); + should.strictEqual(io.static.cache['/socket.io.js'], undefined); + + cl.get('/socket.io/socket.io.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + + data.should.match(/XMLHttpRequest/); + data.should.match(/XHRPolling\.prototype\.name/); + data.should.not.match(/Flashsocket\.prototype\.name/); + data.should.not.match(/HTMLFile\.prototype\.name/); + data.should.not.match(/JSONPPolling\.prototype\.name/); + data.should.not.match(/WS\.prototype\.name/); + + cl.end(); + io.server.close(); + done(); + }); + }); + }, + + 'test that the client etag is served': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.enable('browser client etag'); + + cl.get('/socket.io/socket.io.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/); + + data.should.match(/XMLHttpRequest/); + + cl.end(); + io.server.close(); + done(); + }); + }, + + 'test that the client etag is changed for new transports': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.set('transports', ['websocket']); + io.enable('browser client etag'); + + cl.get('/socket.io/socket.io.js', function (res, data) { + var wsEtag = res.headers.etag; + + io.set('transports', ['xhr-polling']); + cl.get('/socket.io/socket.io.js', function (res, data) { + res.headers.etag.should.not.equal(wsEtag); + + cl.end(); + io.server.close(); + done(); + }); + }); + }, + + 'test that the client is served with gzip': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.enable('browser client gzip'); + + cl.get('/socket.io/socket.io.js', { + headers: { + 'accept-encoding': 'deflate, gzip' + } + } + , function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-encoding'].should.eql('gzip'); + res.headers['content-length'].should.match(/([0-9]+)/); + + cl.end(); + io.server.close(); + done(); + } + ); + }, + + 'test that the cached client is served': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + cl.get('/socket.io/socket.io.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + should.strictEqual(res.headers.etag, undefined); + + data.should.match(/XMLHttpRequest/); + var static = io.static; + static.cache['/socket.io.js'].content.should.match(/XMLHttpRequest/); + + cl.get('/socket.io/socket.io.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + should.strictEqual(res.headers.etag, undefined); + + data.should.match(/XMLHttpRequest/); + + cl.end(); + io.server.close(); + done(); + }); + }); + }, + + 'test that the client is not cached': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.static.add('/random.js', function (path, callback) { + var random = Math.floor(Date.now() * Math.random()).toString(); + callback(null, new Buffer(random)); + }); + + io.disable('browser client cache'); + + cl.get('/socket.io/random.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + should.strictEqual(res.headers.etag, undefined); + + cl.get('/socket.io/random.js', function (res, random) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + should.strictEqual(res.headers.etag, undefined); + + data.should.not.equal(random); + + cl.end(); + io.server.close(); + done(); + }); + }); + }, + + 'test that the cached client etag is served': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.enable('browser client etag'); + + cl.get('/socket.io/socket.io.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/); + + data.should.match(/XMLHttpRequest/); + var static = io.static + , cache = static.cache['/socket.io.js']; + + cache.content.toString().should.match(/XMLHttpRequest/); + Buffer.isBuffer(cache.content).should.be.true; + + cl.get('/socket.io/socket.io.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/); + + data.should.match(/XMLHttpRequest/); + + cl.end(); + io.server.close(); + done(); + }); + }); + }, + + 'test that the cached client sends a 304 header': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.enable('browser client etag'); + + cl.get('/socket.io/socket.io.js', function (res, data) { + cl.get('/socket.io/socket.io.js', { + headers: { + 'if-none-match': res.headers.etag + } + }, function (res, data) { + res.statusCode.should.eql(304); + + cl.end(); + io.server.close(); + done(); + } + ); + }); + }, + + 'test that client minification works': function (done) { + // server 1 + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + // server 2 + var port = ++ports + , io2 = sio.listen(port) + , cl2 = client(port); + + io.enable('browser client minification'); + + cl.get('/socket.io/socket.io.js', function (res, data) { + var length = data.length; + + cl.end(); + io.server.close(); + + cl2.get('/socket.io/socket.io.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + should.strictEqual(res.headers.etag, undefined); + + data.should.match(/XMLHttpRequest/); + data.length.should.be.greaterThan(length); + + cl2.end(); + io2.server.close(); + done(); + }); + }); + }, + + 'test that the WebSocketMain.swf is served': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + cl.get('/socket.io/static/flashsocket/WebSocketMain.swf', function (res, data) { + res.headers['content-type'].should.eql('application/x-shockwave-flash'); + res.headers['content-length'].should.match(/([0-9]+)/); + should.strictEqual(res.headers.etag, undefined); + + var static = io.static + , cache = static.cache['/static/flashsocket/WebSocketMain.swf']; + + Buffer.isBuffer(cache.content).should.be.true; + + cl.end(); + io.server.close(); + done(); + }); + }, + + 'test that the WebSocketMainInsecure.swf is served': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + cl.get('/socket.io/static/flashsocket/WebSocketMainInsecure.swf', function (res, data) { + res.headers['content-type'].should.eql('application/x-shockwave-flash'); + res.headers['content-length'].should.match(/([0-9]+)/); + should.strictEqual(res.headers.etag, undefined); + + var static = io.static + , cache = static.cache['/static/flashsocket/WebSocketMainInsecure.swf']; + + Buffer.isBuffer(cache.content).should.be.true; + + cl.end(); + io.server.close(); + done(); + }); + }, + + 'test that swf files are not served with gzip': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.enable('browser client gzip'); + + cl.get('/socket.io/static/flashsocket/WebSocketMain.swf', { + headers: { + 'accept-encoding': 'deflate, gzip' + } + } + , function (res, data) { + res.headers['content-type'].should.eql('application/x-shockwave-flash'); + res.headers['content-length'].should.match(/([0-9]+)/); + should.strictEqual(res.headers['content-encoding'], undefined); + + cl.end(); + io.server.close(); + done(); + } + ); + }, + + 'test that you can serve custom clients': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.set('browser client handler', function (req, res) { + res.writeHead(200, { + 'Content-Type': 'application/javascript' + , 'Content-Length': 13 + , 'ETag': '1.0' + }); + res.end('custom_client'); + }); + + cl.get('/socket.io/socket.io.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.eql(13); + res.headers.etag.should.eql('1.0'); + + data.should.eql('custom_client'); + + cl.end(); + io.server.close(); + done(); + }); + } + +};