rc-web@69: rc-web@69: /*! rc-web@69: * socket.io-node rc-web@69: * Copyright(c) 2011 LearnBoost rc-web@69: * MIT Licensed rc-web@69: */ rc-web@69: rc-web@69: /** rc-web@69: * Module dependencies. rc-web@69: */ rc-web@69: rc-web@69: var client = require('socket.io-client') rc-web@69: , cp = require('child_process') rc-web@69: , fs = require('fs') rc-web@69: , util = require('./util'); rc-web@69: rc-web@69: /** rc-web@69: * File type details. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: var mime = { rc-web@69: js: { rc-web@69: type: 'application/javascript' rc-web@69: , encoding: 'utf8' rc-web@69: , gzip: true rc-web@69: } rc-web@69: , swf: { rc-web@69: type: 'application/x-shockwave-flash' rc-web@69: , encoding: 'binary' rc-web@69: , gzip: false rc-web@69: } rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Regexp for matching custom transport patterns. Users can configure their own rc-web@69: * socket.io bundle based on the url structure. Different transport names are rc-web@69: * concatinated using the `+` char. /socket.io/socket.io+websocket.js should rc-web@69: * create a bundle that only contains support for the websocket. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: var bundle = /\+((?:\+)?[\w\-]+)*(?:\.v\d+\.\d+\.\d+)?(?:\.js)$/ rc-web@69: , versioning = /\.v\d+\.\d+\.\d+(?:\.js)$/; rc-web@69: rc-web@69: /** rc-web@69: * Export the constructor rc-web@69: */ rc-web@69: rc-web@69: exports = module.exports = Static; rc-web@69: rc-web@69: /** rc-web@69: * Static constructor rc-web@69: * rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: function Static (manager) { rc-web@69: this.manager = manager; rc-web@69: this.cache = {}; rc-web@69: this.paths = {}; rc-web@69: rc-web@69: this.init(); rc-web@69: } rc-web@69: rc-web@69: /** rc-web@69: * Initialize the Static by adding default file paths. rc-web@69: * rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Static.prototype.init = function () { rc-web@69: /** rc-web@69: * Generates a unique id based the supplied transports array rc-web@69: * rc-web@69: * @param {Array} transports The array with transport types rc-web@69: * @api private rc-web@69: */ rc-web@69: function id (transports) { rc-web@69: var id = transports.join('').split('').map(function (char) { rc-web@69: return ('' + char.charCodeAt(0)).split('').pop(); rc-web@69: }).reduce(function (char, id) { rc-web@69: return char +id; rc-web@69: }); rc-web@69: rc-web@69: return client.version + ':' + id; rc-web@69: } rc-web@69: rc-web@69: /** rc-web@69: * Generates a socket.io-client file based on the supplied transports. rc-web@69: * rc-web@69: * @param {Array} transports The array with transport types rc-web@69: * @param {Function} callback Callback for the static.write rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: function build (transports, callback) { rc-web@69: client.builder(transports, { rc-web@69: minify: self.manager.enabled('browser client minification') rc-web@69: }, function (err, content) { rc-web@69: callback(err, content ? new Buffer(content) : null, id(transports)); rc-web@69: } rc-web@69: ); rc-web@69: } rc-web@69: rc-web@69: var self = this; rc-web@69: rc-web@69: // add our default static files rc-web@69: this.add('/static/flashsocket/WebSocketMain.swf', { rc-web@69: file: client.dist + '/WebSocketMain.swf' rc-web@69: }); rc-web@69: rc-web@69: this.add('/static/flashsocket/WebSocketMainInsecure.swf', { rc-web@69: file: client.dist + '/WebSocketMainInsecure.swf' rc-web@69: }); rc-web@69: rc-web@69: // generates dedicated build based on the available transports rc-web@69: this.add('/socket.io.js', function (path, callback) { rc-web@69: build(self.manager.get('transports'), callback); rc-web@69: }); rc-web@69: rc-web@69: this.add('/socket.io.v', { mime: mime.js }, function (path, callback) { rc-web@69: build(self.manager.get('transports'), callback); rc-web@69: }); rc-web@69: rc-web@69: // allow custom builds based on url paths rc-web@69: this.add('/socket.io+', { mime: mime.js }, function (path, callback) { rc-web@69: var available = self.manager.get('transports') rc-web@69: , matches = path.match(bundle) rc-web@69: , transports = []; rc-web@69: rc-web@69: if (!matches) return callback('No valid transports'); rc-web@69: rc-web@69: // make sure they valid transports rc-web@69: matches[0].split('.')[0].split('+').slice(1).forEach(function (transport) { rc-web@69: if (!!~available.indexOf(transport)) { rc-web@69: transports.push(transport); rc-web@69: } rc-web@69: }); rc-web@69: rc-web@69: if (!transports.length) return callback('No valid transports'); rc-web@69: build(transports, callback); rc-web@69: }); rc-web@69: rc-web@69: // clear cache when transports change rc-web@69: this.manager.on('set:transports', function (key, value) { rc-web@69: delete self.cache['/socket.io.js']; rc-web@69: Object.keys(self.cache).forEach(function (key) { rc-web@69: if (bundle.test(key)) { rc-web@69: delete self.cache[key]; rc-web@69: } rc-web@69: }); rc-web@69: }); rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Gzip compress buffers. rc-web@69: * rc-web@69: * @param {Buffer} data The buffer that needs gzip compression rc-web@69: * @param {Function} callback rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Static.prototype.gzip = function (data, callback) { rc-web@69: var gzip = cp.spawn('gzip', ['-9', '-c', '-f', '-n']) rc-web@69: , encoding = Buffer.isBuffer(data) ? 'binary' : 'utf8' rc-web@69: , buffer = [] rc-web@69: , err; rc-web@69: rc-web@69: gzip.stdout.on('data', function (data) { rc-web@69: buffer.push(data); rc-web@69: }); rc-web@69: rc-web@69: gzip.stderr.on('data', function (data) { rc-web@69: err = data +''; rc-web@69: buffer.length = 0; rc-web@69: }); rc-web@69: rc-web@69: gzip.on('close', function () { rc-web@69: if (err) return callback(err); rc-web@69: rc-web@69: var size = 0 rc-web@69: , index = 0 rc-web@69: , i = buffer.length rc-web@69: , content; rc-web@69: rc-web@69: while (i--) { rc-web@69: size += buffer[i].length; rc-web@69: } rc-web@69: rc-web@69: content = new Buffer(size); rc-web@69: i = buffer.length; rc-web@69: rc-web@69: buffer.forEach(function (buffer) { rc-web@69: var length = buffer.length; rc-web@69: rc-web@69: buffer.copy(content, index, 0, length); rc-web@69: index += length; rc-web@69: }); rc-web@69: rc-web@69: buffer.length = 0; rc-web@69: callback(null, content); rc-web@69: }); rc-web@69: rc-web@69: gzip.stdin.end(data, encoding); rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Is the path a static file? rc-web@69: * rc-web@69: * @param {String} path The path that needs to be checked rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Static.prototype.has = function (path) { rc-web@69: // fast case rc-web@69: if (this.paths[path]) return this.paths[path]; rc-web@69: rc-web@69: var keys = Object.keys(this.paths) rc-web@69: , i = keys.length; rc-web@69: rc-web@69: while (i--) { rc-web@69: if (-~path.indexOf(keys[i])) return this.paths[keys[i]]; rc-web@69: } rc-web@69: rc-web@69: return false; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Add new paths new paths that can be served using the static provider. rc-web@69: * rc-web@69: * @param {String} path The path to respond to rc-web@69: * @param {Options} options Options for writing out the response rc-web@69: * @param {Function} [callback] Optional callback if no options.file is rc-web@69: * supplied this would be called instead. rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Static.prototype.add = function (path, options, callback) { rc-web@69: var extension = /(?:\.(\w{1,4}))$/.exec(path); rc-web@69: rc-web@69: if (!callback && typeof options == 'function') { rc-web@69: callback = options; rc-web@69: options = {}; rc-web@69: } rc-web@69: rc-web@69: options.mime = options.mime || (extension ? mime[extension[1]] : false); rc-web@69: rc-web@69: if (callback) options.callback = callback; rc-web@69: if (!(options.file || options.callback) || !options.mime) return false; rc-web@69: rc-web@69: this.paths[path] = options; rc-web@69: rc-web@69: return true; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Writes a static response. rc-web@69: * rc-web@69: * @param {String} path The path for the static content rc-web@69: * @param {HTTPRequest} req The request object rc-web@69: * @param {HTTPResponse} res The response object rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Static.prototype.write = function (path, req, res) { rc-web@69: /** rc-web@69: * Write a response without throwing errors because can throw error if the rc-web@69: * response is no longer writable etc. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: function write (status, headers, content, encoding) { rc-web@69: try { rc-web@69: res.writeHead(status, headers || undefined); rc-web@69: rc-web@69: // only write content if it's not a HEAD request and we actually have rc-web@69: // some content to write (304's doesn't have content). rc-web@69: res.end( rc-web@69: req.method !== 'HEAD' && content ? content : '' rc-web@69: , encoding || undefined rc-web@69: ); rc-web@69: } catch (e) {} rc-web@69: } rc-web@69: rc-web@69: /** rc-web@69: * Answers requests depending on the request properties and the reply object. rc-web@69: * rc-web@69: * @param {Object} reply The details and content to reply the response with rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: function answer (reply) { rc-web@69: var cached = req.headers['if-none-match'] === reply.etag; rc-web@69: if (cached && self.manager.enabled('browser client etag')) { rc-web@69: return write(304); rc-web@69: } rc-web@69: rc-web@69: var accept = req.headers['accept-encoding'] || '' rc-web@69: , gzip = !!~accept.toLowerCase().indexOf('gzip') rc-web@69: , mime = reply.mime rc-web@69: , versioned = reply.versioned rc-web@69: , headers = { rc-web@69: 'Content-Type': mime.type rc-web@69: }; rc-web@69: rc-web@69: // check if we can add a etag rc-web@69: if (self.manager.enabled('browser client etag') && reply.etag && !versioned) { rc-web@69: headers['Etag'] = reply.etag; rc-web@69: } rc-web@69: rc-web@69: // see if we need to set Expire headers because the path is versioned rc-web@69: if (versioned) { rc-web@69: var expires = self.manager.get('browser client expires'); rc-web@69: headers['Cache-Control'] = 'private, x-gzip-ok="", max-age=' + expires; rc-web@69: headers['Date'] = new Date().toUTCString(); rc-web@69: headers['Expires'] = new Date(Date.now() + (expires * 1000)).toUTCString(); rc-web@69: } rc-web@69: rc-web@69: if (gzip && reply.gzip) { rc-web@69: headers['Content-Length'] = reply.gzip.length; rc-web@69: headers['Content-Encoding'] = 'gzip'; rc-web@69: headers['Vary'] = 'Accept-Encoding'; rc-web@69: write(200, headers, reply.gzip.content, mime.encoding); rc-web@69: } else { rc-web@69: headers['Content-Length'] = reply.length; rc-web@69: write(200, headers, reply.content, mime.encoding); rc-web@69: } rc-web@69: rc-web@69: self.manager.log.debug('served static content ' + path); rc-web@69: } rc-web@69: rc-web@69: var self = this rc-web@69: , details; rc-web@69: rc-web@69: // most common case first rc-web@69: if (this.manager.enabled('browser client cache') && this.cache[path]) { rc-web@69: return answer(this.cache[path]); rc-web@69: } else if (this.manager.get('browser client handler')) { rc-web@69: return this.manager.get('browser client handler').call(this, req, res); rc-web@69: } else if ((details = this.has(path))) { rc-web@69: /** rc-web@69: * A small helper function that will let us deal with fs and dynamic files rc-web@69: * rc-web@69: * @param {Object} err Optional error rc-web@69: * @param {Buffer} content The data rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: function ready (err, content, etag) { rc-web@69: if (err) { rc-web@69: self.manager.log.warn('Unable to serve file. ' + (err.message || err)); rc-web@69: return write(500, null, 'Error serving static ' + path); rc-web@69: } rc-web@69: rc-web@69: // store the result in the cache rc-web@69: var reply = self.cache[path] = { rc-web@69: content: content rc-web@69: , length: content.length rc-web@69: , mime: details.mime rc-web@69: , etag: etag || client.version rc-web@69: , versioned: versioning.test(path) rc-web@69: }; rc-web@69: rc-web@69: // check if gzip is enabled rc-web@69: if (details.mime.gzip && self.manager.enabled('browser client gzip')) { rc-web@69: self.gzip(content, function (err, content) { rc-web@69: if (!err) { rc-web@69: reply.gzip = { rc-web@69: content: content rc-web@69: , length: content.length rc-web@69: } rc-web@69: } rc-web@69: rc-web@69: answer(reply); rc-web@69: }); rc-web@69: } else { rc-web@69: answer(reply); rc-web@69: } rc-web@69: } rc-web@69: rc-web@69: if (details.file) { rc-web@69: fs.readFile(details.file, ready); rc-web@69: } else if(details.callback) { rc-web@69: details.callback.call(this, path, ready); rc-web@69: } else { rc-web@69: write(404, null, 'File handle not found'); rc-web@69: } rc-web@69: } else { rc-web@69: write(404, null, 'File not found'); rc-web@69: } rc-web@69: };