/*! * 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\-]+)*(?:\.v\d+\.\d+\.\d+)?(?:\.js)$/ , versioning = /\.v\d+\.\d+\.\d+(?:\.js)$/; /** * 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); }); this.add('/socket.io.v', { mime: mime.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 = path.match(bundle) , 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('close', 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); // only write content if it's not a HEAD request and we actually have // some content to write (304's doesn't have content). res.end( req.method !== 'HEAD' && content ? 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 , versioned = reply.versioned , headers = { 'Content-Type': mime.type }; // check if we can add a etag if (self.manager.enabled('browser client etag') && reply.etag && !versioned) { headers['Etag'] = reply.etag; } // see if we need to set Expire headers because the path is versioned if (versioned) { var expires = self.manager.get('browser client expires'); headers['Cache-Control'] = 'private, x-gzip-ok="", max-age=' + expires; headers['Date'] = new Date().toUTCString(); headers['Expires'] = new Date(Date.now() + (expires * 1000)).toUTCString(); } if (gzip && reply.gzip) { headers['Content-Length'] = reply.gzip.length; headers['Content-Encoding'] = 'gzip'; headers['Vary'] = 'Accept-Encoding'; 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 , versioned: versioning.test(path) }; // 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'); } };