postal.js/example/node/node_modules/socket.io/lib/static.js

400 lines
9.5 KiB
JavaScript

/*!
* 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( 'exit', function () {
if ( err ) {
return callback( err );
}
var size = 0
, index = 0
, i = buffer.length
, content;
while ( i-- ) {
size += buffer[i].length;
}
content = new Buffer( size );
i = buffer.length;
buffer.forEach( function ( buffer ) {
var length = buffer.length;
buffer.copy( content, index, 0, length );
index += length;
} );
buffer.length = 0;
callback( null, content );
} );
gzip.stdin.end( data, encoding );
};
/**
* Is the path a static file?
*
* @param {String} path The path that needs to be checked
* @api public
*/
Static.prototype.has = function ( path ) {
// fast case
if ( this.paths[path] ) {
return this.paths[path];
}
var keys = Object.keys( this.paths )
, i = keys.length;
while ( i-- ) {
if ( -~path.indexOf( keys[i] ) ) {
return this.paths[keys[i]];
}
}
return false;
};
/**
* Add new paths new paths that can be served using the static provider.
*
* @param {String} path The path to respond to
* @param {Options} options Options for writing out the response
* @param {Function} [callback] Optional callback if no options.file is
* supplied this would be called instead.
* @api public
*/
Static.prototype.add = function ( path, options, callback ) {
var extension = /(?:\.(\w{1,4}))$/.exec( path );
if ( !callback && typeof options == 'function' ) {
callback = options;
options = {};
}
options.mime = options.mime || (extension ? mime[extension[1]] : false);
if ( callback ) {
options.callback = callback;
}
if ( !(options.file || options.callback) || !options.mime ) {
return false;
}
this.paths[path] = options;
return true;
};
/**
* Writes a static response.
*
* @param {String} path The path for the static content
* @param {HTTPRequest} req The request object
* @param {HTTPResponse} res The response object
* @api public
*/
Static.prototype.write = function ( path, req, res ) {
/**
* Write a response without throwing errors because can throw error if the
* response is no longer writable etc.
*
* @api private
*/
function write( status, headers, content, encoding ) {
try {
res.writeHead( status, headers || undefined );
// 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' );
}
};