123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395 |
- /*!
- * socket.io-node
- * Copyright(c) 2011 LearnBoost <dev@learnboost.com>
- * 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');
- }
- };
|