From f6ebb7b8d679b263cb01b30b870be280b180e212 Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Thu, 14 Jul 2011 20:54:40 +0200 Subject: [PATCH 01/15] API structure --- lib/static.js | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 lib/static.js diff --git a/lib/static.js b/lib/static.js new file mode 100644 index 0000000000..a6be0dbb62 --- /dev/null +++ b/lib/static.js @@ -0,0 +1,81 @@ + +/*! +* socket.io-node +* Copyright(c) 2011 LearnBoost +* MIT Licensed +*/ + +/** + * Module dependencies. + */ + +var client = require('socket.io-client') + , cp = require('child_process'); + +/** + * Export the constructor + */ + +exports = module.exports = Static; + +/** + * Static constructor + * + * @api public + */ + +function Static () { + this.cache = {}; + this.paths = {}; + this.etag = client.version; +} + +/** + * Gzip compress buffers + * + * @param {Buffer} buffer The buffer that needs gzip compression + * @param {Function} callback + * @api public + */ + +Static.prototype.gzip = function (buffer, callback) { + +}; + +/** + * Is the path a staic file + * + * @param {String} path The path that needs to be checked + * @api public + */ + +Static.prototype.has = function (path) { + +}; + +/** + * 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) { + +}; + +/** + * 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) { + +}; From 168b207c6dc0b561246f01a1de627d526d894d47 Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Thu, 14 Jul 2011 21:24:26 +0200 Subject: [PATCH 02/15] Inital draft of gzip support --- lib/static.js | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/static.js b/lib/static.js index a6be0dbb62..5e23d925ba 100644 --- a/lib/static.js +++ b/lib/static.js @@ -10,7 +10,8 @@ */ var client = require('socket.io-client') - , cp = require('child_process'); + , cp = require('child_process') + , fs = require('fs'); /** * Export the constructor @@ -33,13 +34,53 @@ function Static () { /** * Gzip compress buffers * - * @param {Buffer} buffer The buffer that needs gzip compression + * @param {Buffer} data The buffer that needs gzip compression * @param {Function} callback * @api public */ -Static.prototype.gzip = function (buffer, callback) { +Static.prototype.gzip = function (data, callback) { + var gzip = cp.spawn('gzip', ['-9']) + , 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.write(data, 'utf8'); + gzip.stdin.end(); }; /** From d043c33351bc2c53a0629a7066b523ed244e8694 Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Fri, 15 Jul 2011 00:13:54 +0200 Subject: [PATCH 03/15] Added the write and answer fn --- lib/static.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/lib/static.js b/lib/static.js index 5e23d925ba..ea0ed2e5de 100644 --- a/lib/static.js +++ b/lib/static.js @@ -118,5 +118,49 @@ Static.prototype.add = function (path, options, callback) { */ Static.prototype.write = function (path, req, res) { + /** + * Writes a response, safely + * + * @api private + */ + + function write (status, headers, content, encoding) { + try { + res.writeHead(status, headers || undefined); + res.end(content || '', encoding || undefined); + } catch (e) {} + } + + function answer (reply) { + var cached = req.headers['if-none-match'] === self.etag; + if (cached && self.manager.enabled('browser client cache') { + 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') && self.etag) { + headers['Etag'] = self.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.debug('served static content ' + path); + } + var self = this; }; From afdfdb815e2b010f3a8a788ceabde4bff73a5186 Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Fri, 15 Jul 2011 00:24:57 +0200 Subject: [PATCH 04/15] Finished the write function setup --- lib/static.js | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/lib/static.js b/lib/static.js index ea0ed2e5de..21f87c1c37 100644 --- a/lib/static.js +++ b/lib/static.js @@ -159,8 +159,52 @@ Static.prototype.write = function (path, req, res) { write(200, headers, reply.content, mime.encoding); } - self.manager.debug('served static content ' + path); + self.manager.log.debug('served static content ' + path); } var self = this; + + 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 { + function ready (err, content) { + 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 + }; + + if (details.gzip){ + self.gzip(content, function (err, content) { + if (!err) { + reply.gzip = { + content: content + , length: content.length; + } + } + + answer(reply); + }); + } else { + answer(reply); + } + }; + + var details = this.paths[path]; + 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'); + } + } }; From 9e97e6c69195f7679cf2aca965521fd7f187a85a Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Fri, 15 Jul 2011 00:30:25 +0200 Subject: [PATCH 05/15] Added more comments --- lib/static.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/static.js b/lib/static.js index 21f87c1c37..58fff8ed9f 100644 --- a/lib/static.js +++ b/lib/static.js @@ -119,7 +119,8 @@ Static.prototype.add = function (path, options, callback) { Static.prototype.write = function (path, req, res) { /** - * Writes a response, safely + * Write a response without throwing errors because can throw error if the + * response is no longer writable etc. * * @api private */ @@ -131,6 +132,13 @@ Static.prototype.write = function (path, req, res) { } 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'] === self.etag; if (cached && self.manager.enabled('browser client cache') { @@ -164,11 +172,20 @@ Static.prototype.write = function (path, req, res) { var self = this; + // 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 { + /** + * 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) { if (err) { self.manager.log.warn('Unable to serve file. ' + (err.message || err)); @@ -182,6 +199,7 @@ Static.prototype.write = function (path, req, res) { , mime: details.mime }; + // check if we need to gzip it if (details.gzip){ self.gzip(content, function (err, content) { if (!err) { From c2d98bde72e0f4a959829805002062d2e9530a47 Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Fri, 15 Jul 2011 22:10:51 +0200 Subject: [PATCH 06/15] Add now stores the request details and has returns the details for static content --- lib/static.js | 47 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/lib/static.js b/lib/static.js index 58fff8ed9f..3c9bd3838b 100644 --- a/lib/static.js +++ b/lib/static.js @@ -11,7 +11,27 @@ var client = require('socket.io-client') , cp = require('child_process') - , fs = require('fs'); + , fs = require('fs') + , util = require('./util'); + + +/** + * File type details + */ + +var mime = { + js: { + type: 'application/javascript' + , encoding: 'utf8' + , gzip: true + } + , swf: { + type: 'application/x-shockwave-flash' + , encoding: 'binary' + , gzip: false + } +}; + /** * Export the constructor @@ -25,7 +45,8 @@ exports = module.exports = Static; * @api public */ -function Static () { +function Static (manager) { + this.manager = manager; this.cache = {}; this.paths = {}; this.etag = client.version; @@ -49,7 +70,7 @@ Static.prototype.gzip = function (data, callback) { }); gzip.stderr.on('data', function (data) { - err = data +''; + err = data +'';Ma buffer.length = 0; }); @@ -91,7 +112,17 @@ Static.prototype.gzip = function (data, callback) { */ 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; }; /** @@ -105,7 +136,15 @@ Static.prototype.has = function (path) { */ Static.prototype.add = function (path, options, callback) { + var extension = /(?:\.(\w{1,4}))$/.exec(path) + , mime = options.mime || (extension ? mime[extension[1]] : false); + + if (callback) options.callback = callback; + if (!options.file || !options.callback) return false; + + this.paths[path] = options; + return true; }; /** @@ -158,7 +197,7 @@ Static.prototype.write = function (path, req, res) { } // check if we can send gzip data - if (gzip && reply.gzip) { + if (gzip && mime.gzip) { headers['Content-Length'] = reply.gzip.length; headers['Content-Encoding'] = 'gzip'; write(200, headers, reply.gzip.content, mime.encoding); From 4743744efc3726c3679354b25d62eac8b26cc1ea Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Fri, 15 Jul 2011 22:31:03 +0200 Subject: [PATCH 07/15] Expose the constructor in socket.io and have the Manager use the new API --- lib/manager.js | 115 +++++++---------------------------------------- lib/socket.io.js | 8 ++++ lib/static.js | 16 ++++--- 3 files changed, 32 insertions(+), 107 deletions(-) diff --git a/lib/manager.js b/lib/manager.js index 8c030912cd..793342249c 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,6 +79,7 @@ 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 handler': false @@ -137,6 +140,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 +509,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 +639,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 +859,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 index 3c9bd3838b..f01f5a0b26 100644 --- a/lib/static.js +++ b/lib/static.js @@ -70,7 +70,7 @@ Static.prototype.gzip = function (data, callback) { }); gzip.stderr.on('data', function (data) { - err = data +'';Ma + err = data +''; buffer.length = 0; }); @@ -197,7 +197,7 @@ Static.prototype.write = function (path, req, res) { } // check if we can send gzip data - if (gzip && mime.gzip) { + if (gzip && reply.gzip) { headers['Content-Length'] = reply.gzip.length; headers['Content-Encoding'] = 'gzip'; write(200, headers, reply.gzip.content, mime.encoding); @@ -209,14 +209,15 @@ Static.prototype.write = function (path, req, res) { self.manager.log.debug('served static content ' + path); } - var self = this; + 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 { + } else if((details = this.has(path))) { /** * A small helper function that will let us deal with fs and dynamic files * @@ -238,8 +239,8 @@ Static.prototype.write = function (path, req, res) { , mime: details.mime }; - // check if we need to gzip it - if (details.gzip){ + // 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 = { @@ -255,7 +256,6 @@ Static.prototype.write = function (path, req, res) { } }; - var details = this.paths[path]; if (details.file) { fs.readFile(details.file, ready); } else if(details.callback) { @@ -263,5 +263,7 @@ Static.prototype.write = function (path, req, res) { } else { write(404, null, 'File handle not found'); } + } else { + write(404, null, 'File not found'); } }; From 4bdc30734c7bdb8134f249c0861886dab69f6985 Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Fri, 15 Jul 2011 23:59:10 +0200 Subject: [PATCH 08/15] Fixed small typos --- lib/manager.js | 2 +- lib/static.js | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/manager.js b/lib/manager.js index 793342249c..c0cc576e9f 100644 --- a/lib/manager.js +++ b/lib/manager.js @@ -146,7 +146,7 @@ Manager.prototype.__defineGetter__('log', function () { * @api public */ -Manager.prototype.__defineGetter_('static', function () { +Manager.prototype.__defineGetter__('static', function () { return this.get('static'); }); diff --git a/lib/static.js b/lib/static.js index f01f5a0b26..17e74fc20c 100644 --- a/lib/static.js +++ b/lib/static.js @@ -32,7 +32,6 @@ var mime = { } }; - /** * Export the constructor */ @@ -50,8 +49,22 @@ function Static (manager) { this.cache = {}; this.paths = {}; this.etag = client.version; + + this.init(manager); } +/** + * Initialize the Static by adding default file paths + * + * @api public + */ + +Static.prototype.init = function (manager) { + manager.on('set:transports', function (value, key){ + + }); +}; + /** * Gzip compress buffers * @@ -180,7 +193,7 @@ Static.prototype.write = function (path, req, res) { function answer (reply) { var cached = req.headers['if-none-match'] === self.etag; - if (cached && self.manager.enabled('browser client cache') { + if (cached && self.manager.enabled('browser client etag')) { return write(304); } @@ -217,7 +230,7 @@ Static.prototype.write = function (path, req, res) { 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))) { + } else if ((details = this.has(path))) { /** * A small helper function that will let us deal with fs and dynamic files * @@ -245,7 +258,7 @@ Static.prototype.write = function (path, req, res) { if (!err) { reply.gzip = { content: content - , length: content.length; + , length: content.length } } @@ -254,7 +267,7 @@ Static.prototype.write = function (path, req, res) { } else { answer(reply); } - }; + } if (details.file) { fs.readFile(details.file, ready); From 2f8eb63557403a81ee63258bc0a6daafd2768ff8 Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Sat, 16 Jul 2011 21:55:06 +0200 Subject: [PATCH 09/15] Added support for automatic generation of socket.io files Added support for custom socket.io files based on path structure For example socket.io+websocket+xhr-polling.js will create a dedicated build with only support for websockets and xhr-polling. --- lib/static.js | 82 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/lib/static.js b/lib/static.js index 17e74fc20c..7cf596ae8d 100644 --- a/lib/static.js +++ b/lib/static.js @@ -48,9 +48,8 @@ function Static (manager) { this.manager = manager; this.cache = {}; this.paths = {}; - this.etag = client.version; - this.init(manager); + this.init(); } /** @@ -59,9 +58,71 @@ function Static (manager) { * @api public */ -Static.prototype.init = function (manager) { - manager.on('set:transports', function (value, key){ - +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) { + return transports.join('').split('').map(function (char) { + return ('' + char.charCodeAt(0)).split('').pop(); + }).reduce(function (char, id) { + return char +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; + + // 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+', {}, function (path, callback) { + var available = self.manager.get('transports') + , matches = /+((?:\+)?[\w\-]+)*(?:\.js)$/g.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.lengt) return callback('No valid transports'); + build(transports, callback); }); }; @@ -97,7 +158,7 @@ Static.prototype.gzip = function (data, callback) { while (i--) { size += buffer[i].length; - } + }) content = new Buffer(size); i = buffer.length; @@ -153,7 +214,7 @@ Static.prototype.add = function (path, options, callback) { , mime = options.mime || (extension ? mime[extension[1]] : false); if (callback) options.callback = callback; - if (!options.file || !options.callback) return false; + if (!options.file || !options.callback || !options.mime) return false; this.paths[path] = options; @@ -205,8 +266,8 @@ Static.prototype.write = function (path, req, res) { }; // check if we can add a etag - if (self.manager.enabled('browser client etag') && self.etag) { - headers['Etag'] = self.etag; + if (self.manager.enabled('browser client etag') && reply.etag) { + headers['Etag'] = reply.etag; } // check if we can send gzip data @@ -239,7 +300,7 @@ Static.prototype.write = function (path, req, res) { * @api private */ - function ready (err, content) { + 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); @@ -250,6 +311,7 @@ Static.prototype.write = function (path, req, res) { content: content , length: content.length , mime: details.mime + , etag: etag || client.version }; // check if gzip is enabled From 9cd51b1f6b3f58611e10a71044d49dd591d2556b Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Sun, 17 Jul 2011 00:10:43 +0200 Subject: [PATCH 10/15] Passes test suite --- test/static.test.js | 301 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 test/static.test.js diff --git a/test/static.test.js b/test/static.test.js new file mode 100644 index 0000000000..ac2c515d60 --- /dev/null +++ b/test/static.test.js @@ -0,0 +1,301 @@ + +/*! +* socket.io-node +* Copyright(c) 2011 LearnBoost +* MIT Licensed +*/ + +/** + * Test dependencies. + */ + +var sio = require('socket.io') + , cp = require('child_process') + , should = require('./common') + , ports = 15000; + +/** + * Unzip gzip + */ + +function gunzip (data, callback) { + var gunzip = cp.spawn('gunzip') + , buffer = [] + , err; + + guzip.stdout.on('data', function (data) { + buffer.push(data); + }); + + guzip.stderr.on('data', function (data) { + err = data +''; + buffer.length = 0; + }); + + guzip.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); + }); + + guzip.stdin.write(data, 'utf8'); + guzip.stdin.end(); +} + +/** + * Test. + */ + +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 = 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 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 = 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.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 = 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 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(); + }); + } + +}; From 5c24bf8c1df0b66eb7c24404e4c1ec1dd37ba0d9 Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Sun, 17 Jul 2011 01:24:21 +0200 Subject: [PATCH 11/15] Removed old testcases from manager and tiny fix for Static --- lib/static.js | 34 ++++--- test/manager.test.js | 230 ------------------------------------------- 2 files changed, 18 insertions(+), 246 deletions(-) diff --git a/lib/static.js b/lib/static.js index 7cf596ae8d..2e6ab96c33 100644 --- a/lib/static.js +++ b/lib/static.js @@ -66,11 +66,13 @@ Static.prototype.init = function () { * @api private */ function id (transports) { - return transports.join('').split('').map(function (char) { + 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; } /** @@ -85,12 +87,12 @@ Static.prototype.init = function () { client.builder(transports, { minify: self.manager.enabled('browser client minification') }, function (err, content) { - callback(err, content ? new Buffer(content) || null, id(transports)); + callback(err, content ? new Buffer(content) : null, id(transports)); } ); } - var self; + var self = this; // add our default static files this.add('/static/flashsocket/WebSocketMain.swf', { @@ -107,9 +109,9 @@ Static.prototype.init = function () { }); // allow custom builds based on url paths - this.add('/socket.io+', {}, function (path, callback) { + this.add('/socket.io+', { mime: mime.js }, function (path, callback) { var available = self.manager.get('transports') - , matches = /+((?:\+)?[\w\-]+)*(?:\.js)$/g.exec(path) + , matches = /\+((?:\+)?[\w\-]+)*(?:\.js)$/g.exec(path) , transports = []; if (!matches) return callback('No valid transports'); @@ -121,7 +123,7 @@ Static.prototype.init = function () { } }); - if (!transports.lengt) return callback('No valid transports'); + if (!transports.length) return callback('No valid transports'); build(transports, callback); }); }; @@ -135,7 +137,8 @@ Static.prototype.init = function () { */ Static.prototype.gzip = function (data, callback) { - var gzip = cp.spawn('gzip', ['-9']) + var gzip = cp.spawn('gzip', ['-9', '-c']) + , encoding = Buffer.isBuffer(data) ? 'binary' : 'utf8' , buffer = [] , err; @@ -158,7 +161,7 @@ Static.prototype.gzip = function (data, callback) { while (i--) { size += buffer[i].length; - }) + } content = new Buffer(size); i = buffer.length; @@ -174,8 +177,7 @@ Static.prototype.gzip = function (data, callback) { callback(null, content); }); - gzip.stdin.write(data, 'utf8'); - gzip.stdin.end(); + gzip.stdin.end(data, encoding); }; /** @@ -191,7 +193,7 @@ Static.prototype.has = function (path) { var keys = Object.keys(this.paths) , i = keys.length; - + while (i--) { if (!!~path.indexOf(keys[i])) return this.paths[keys[i]]; } @@ -210,11 +212,11 @@ Static.prototype.has = function (path) { */ Static.prototype.add = function (path, options, callback) { - var extension = /(?:\.(\w{1,4}))$/.exec(path) - , mime = options.mime || (extension ? mime[extension[1]] : false); + var extension = /(?:\.(\w{1,4}))$/.exec(path); + options.mime = options.mime || (extension ? mime[extension[1]] : false); if (callback) options.callback = callback; - if (!options.file || !options.callback || !options.mime) return false; + if (!(options.file || options.callback) || !options.mime) return false; this.paths[path] = options; @@ -253,7 +255,7 @@ Static.prototype.write = function (path, req, res) { */ function answer (reply) { - var cached = req.headers['if-none-match'] === self.etag; + var cached = req.headers['if-none-match'] === reply.etag; if (cached && self.manager.enabled('browser client etag')) { return write(304); } @@ -315,7 +317,7 @@ Static.prototype.write = function (path, req, res) { }; // check if gzip is enabled - if (details.mime.gzip && self.manager.enabled('browser client gzip')){ + if (details.mime.gzip && self.manager.enabled('browser client gzip')) { self.gzip(content, function (err, content) { if (!err) { reply.gzip = { 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) From f88eedc3c8c024d778ab89a238c5f650be39143b Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Sun, 17 Jul 2011 11:13:12 +0200 Subject: [PATCH 12/15] More tests --- test/static.test.js | 104 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 96 insertions(+), 8 deletions(-) diff --git a/test/static.test.js b/test/static.test.js index ac2c515d60..ae0a354b10 100644 --- a/test/static.test.js +++ b/test/static.test.js @@ -19,20 +19,20 @@ var sio = require('socket.io') */ function gunzip (data, callback) { - var gunzip = cp.spawn('gunzip') + var gunzip = cp.spawn('gunzip', ['-c']) , buffer = [] , err; - guzip.stdout.on('data', function (data) { + gunzip.stdout.on('data', function (data) { buffer.push(data); }); - guzip.stderr.on('data', function (data) { + gunzip.stderr.on('data', function (data) { err = data +''; buffer.length = 0; }); - guzip.on('exit', function () { + gunzip.on('exit', function () { if (err) return callback(err); var size = 0 @@ -58,8 +58,8 @@ function gunzip (data, callback) { callback(null, content); }); - guzip.stdin.write(data, 'utf8'); - guzip.stdin.end(); + gunzip.stdin.write(data, 'utf8'); + gunzip.stdin.end(); } /** @@ -68,7 +68,7 @@ function gunzip (data, callback) { module.exports = { - 'test that the client is served': function (done) { + 'test that the client is served': function (done) { var port = ++ports , io = sio.listen(port) , cl = client(port); @@ -86,6 +86,59 @@ module.exports = { }); }, + 'test that the custom build client 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+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.configure(function () { + 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 etag is served': function (done) { var port = ++ports , io = sio.listen(port) @@ -108,6 +161,37 @@ module.exports = { }); }, + 'test that the client is served with gzip': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.configure(function () { + 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]+)/); + + gunzip(data, function (err, data){ + console.log(err); + 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) @@ -181,7 +265,11 @@ module.exports = { }); 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) { + cl.get('/socket.io/socket.io.js', { + headers: { + 'if-none-match': res.headers.etag + } + }, function (res, data) { res.statusCode.should.eql(304); cl.end(); From 4b94f2b8bf025a474711a7dc8e3d2eb4391f5c95 Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Sun, 17 Jul 2011 20:21:08 +0200 Subject: [PATCH 13/15] Passes all 14 tests --- lib/static.js | 29 +++++++- test/static.test.js | 176 ++++++++++++++++++++++---------------------- 2 files changed, 115 insertions(+), 90 deletions(-) diff --git a/lib/static.js b/lib/static.js index 2e6ab96c33..eaa0565d92 100644 --- a/lib/static.js +++ b/lib/static.js @@ -16,7 +16,9 @@ var client = require('socket.io-client') /** - * File type details + * File type details. + * + * @api private */ var mime = { @@ -32,6 +34,17 @@ var mime = { } }; +/** + * 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 */ @@ -111,7 +124,7 @@ Static.prototype.init = function () { // allow custom builds based on url paths this.add('/socket.io+', { mime: mime.js }, function (path, callback) { var available = self.manager.get('transports') - , matches = /\+((?:\+)?[\w\-]+)*(?:\.js)$/g.exec(path) + , matches = bundle.exec(path) , transports = []; if (!matches) return callback('No valid transports'); @@ -126,6 +139,16 @@ Static.prototype.init = function () { 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]; + } + }); + }); }; /** @@ -137,7 +160,7 @@ Static.prototype.init = function () { */ Static.prototype.gzip = function (data, callback) { - var gzip = cp.spawn('gzip', ['-9', '-c']) + var gzip = cp.spawn('gzip', ['-9', '-c', '-f', '-n']) , encoding = Buffer.isBuffer(data) ? 'binary' : 'utf8' , buffer = [] , err; diff --git a/test/static.test.js b/test/static.test.js index ae0a354b10..a40cce7bef 100644 --- a/test/static.test.js +++ b/test/static.test.js @@ -12,55 +12,7 @@ var sio = require('socket.io') , cp = require('child_process') , should = require('./common') - , ports = 15000; - -/** - * Unzip gzip - */ - -function gunzip (data, callback) { - var gunzip = cp.spawn('gunzip', ['-c']) - , buffer = [] - , err; - - gunzip.stdout.on('data', function (data) { - buffer.push(data); - }); - - gunzip.stderr.on('data', function (data) { - err = data +''; - buffer.length = 0; - }); - - gunzip.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); - }); - - gunzip.stdin.write(data, 'utf8'); - gunzip.stdin.end(); -} + , ports = 15400; /** * Test. @@ -91,9 +43,7 @@ module.exports = { , io = sio.listen(port) , cl = client(port); - io.configure(function () { - io.enable('browser client etag'); - }); + io.enable('browser client etag'); cl.get('/socket.io/socket.io+websocket.js', function (res, data) { res.headers['content-type'].should.eql('application/javascript'); @@ -115,12 +65,10 @@ module.exports = { 'test that the client is build with the enabled transports': function (done) { var port = ++ports - , io = sio.listen(port) + , io = sio.listen(port) , cl = client(port); - io.configure(function () { - io.set('transports', ['websocket']); - }); + io.set('transports', ['websocket']); cl.get('/socket.io/socket.io.js', function (res, data) { res.headers['content-type'].should.eql('application/javascript'); @@ -139,14 +87,52 @@ module.exports = { }); }, - 'test that the client etag is served': function (done) { + 'test that the client cache is cleared when transports change': function (done) { var port = ++ports , io = sio.listen(port) , cl = client(port); - io.configure(function () { - io.enable('browser client etag'); + 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'); @@ -166,9 +152,7 @@ module.exports = { , io = sio.listen(port) , cl = client(port); - io.configure(function () { - io.enable('browser client gzip'); - }); + io.enable('browser client gzip'); cl.get('/socket.io/socket.io.js', { headers: { @@ -180,14 +164,9 @@ module.exports = { res.headers['content-encoding'].should.eql('gzip'); res.headers['content-length'].should.match(/([0-9]+)/); - gunzip(data, function (err, data){ - console.log(err); - data.should.match(/XMLHttpRequest/); - - cl.end(); - io.server.close(); - done(); - }); + cl.end(); + io.server.close(); + done(); } ); }, @@ -220,14 +199,43 @@ module.exports = { }); }, - 'test that the cached client etag is served': function (done) { + 'test that the client is not cached': function (done) { var port = ++ports , io = sio.listen(port) , cl = client(port); - io.configure(function () { - io.enable('browser client etag'); + 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'); @@ -260,9 +268,7 @@ module.exports = { , io = sio.listen(port) , cl = client(port); - io.configure(function () { - io.enable('browser client etag'); - }); + io.enable('browser client etag'); cl.get('/socket.io/socket.io.js', function (res, data) { cl.get('/socket.io/socket.io.js', { @@ -290,9 +296,7 @@ module.exports = { , io2 = sio.listen(port) , cl2 = client(port); - io.configure(function () { - io.enable('browser client minification'); - }); + io.enable('browser client minification'); cl.get('/socket.io/socket.io.js', function (res, data) { var length = data.length; @@ -362,15 +366,13 @@ module.exports = { , 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'); + 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) { From 23e14223bd002d8f53e6c4dd554994fad02909ef Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Sun, 17 Jul 2011 23:50:06 +0200 Subject: [PATCH 14/15] Added more test cases and allow a more flexible constructor for adding content --- lib/static.js | 8 ++++- test/static.test.js | 87 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/lib/static.js b/lib/static.js index eaa0565d92..da48b0af41 100644 --- a/lib/static.js +++ b/lib/static.js @@ -117,7 +117,7 @@ Static.prototype.init = function () { }); // generates dedicated build based on the available transports - this.add('/socket.io.js', {}, function (path, callback) { + this.add('/socket.io.js', function (path, callback) { build(self.manager.get('transports'), callback); }); @@ -236,6 +236,12 @@ Static.prototype.has = function (path) { 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; diff --git a/test/static.test.js b/test/static.test.js index a40cce7bef..06ca98e05b 100644 --- a/test/static.test.js +++ b/test/static.test.js @@ -10,7 +10,6 @@ */ var sio = require('socket.io') - , cp = require('child_process') , should = require('./common') , ports = 15400; @@ -20,6 +19,30 @@ var sio = require('socket.io') 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) @@ -124,7 +147,6 @@ module.exports = { done(); }); }); - }, 'test that the client etag is served': function (done) { @@ -147,6 +169,28 @@ module.exports = { }); }, + '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) @@ -204,7 +248,7 @@ module.exports = { , io = sio.listen(port) , cl = client(port); - io.static.add('/random.js', {}, function (path, callback) { + io.static.add('/random.js', function (path, callback) { var random = Math.floor(Date.now() * Math.random()).toString(); callback(null, new Buffer(random)); }); @@ -275,13 +319,14 @@ module.exports = { headers: { 'if-none-match': res.headers.etag } - }, function (res, data) { - res.statusCode.should.eql(304); + }, function (res, data) { + res.statusCode.should.eql(304); - cl.end(); - io.server.close(); - done(); - }); + cl.end(); + io.server.close(); + done(); + } + ); }); }, @@ -361,6 +406,30 @@ module.exports = { }); }, + '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) From 0d3441d8b3b385806c7e2f74ab5095ea4f9fc5e1 Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Mon, 18 Jul 2011 00:20:05 +0200 Subject: [PATCH 15/15] Added gzip to the options list and fixed a typo --- lib/manager.js | 1 + lib/static.js | 21 ++++++++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/manager.js b/lib/manager.js index c0cc576e9f..2defc5aa2a 100644 --- a/lib/manager.js +++ b/lib/manager.js @@ -82,6 +82,7 @@ function Manager (server) { , 'browser client cache': true , 'browser client minification': false , 'browser client etag': false + , 'browser client gzip': false , 'browser client handler': false , 'client store expiration': 15 }; diff --git a/lib/static.js b/lib/static.js index da48b0af41..03505ddd40 100644 --- a/lib/static.js +++ b/lib/static.js @@ -14,7 +14,6 @@ var client = require('socket.io-client') , fs = require('fs') , util = require('./util'); - /** * File type details. * @@ -28,10 +27,10 @@ var mime = { , gzip: true } , swf: { - type: 'application/x-shockwave-flash' - , encoding: 'binary' - , gzip: false - } + type: 'application/x-shockwave-flash' + , encoding: 'binary' + , gzip: false + } }; /** @@ -66,7 +65,7 @@ function Static (manager) { } /** - * Initialize the Static by adding default file paths + * Initialize the Static by adding default file paths. * * @api public */ @@ -89,7 +88,7 @@ Static.prototype.init = function () { } /** - * Generates a socket.io-client file based on the supplied transports + * 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 @@ -152,7 +151,7 @@ Static.prototype.init = function () { }; /** - * Gzip compress buffers + * Gzip compress buffers. * * @param {Buffer} data The buffer that needs gzip compression * @param {Function} callback @@ -204,7 +203,7 @@ Static.prototype.gzip = function (data, callback) { }; /** - * Is the path a staic file + * Is the path a static file? * * @param {String} path The path that needs to be checked * @api public @@ -225,7 +224,7 @@ Static.prototype.has = function (path) { }; /** - * Add new paths new paths that can be served using the static provider + * 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 @@ -253,7 +252,7 @@ Static.prototype.add = function (path, options, callback) { }; /** - * Writes a static response + * Writes a static response. * * @param {String} path The path for the static content * @param {HTTPRequest} req The request object