4 * Copyright(c) 2011 LearnBoost <dev@learnboost.com>
12 var client = require('socket.io-client')
13 , cp = require('child_process')
15 , util = require('./util');
25 type: 'application/javascript'
30 type: 'application/x-shockwave-flash'
37 * Regexp for matching custom transport patterns. Users can configure their own
38 * socket.io bundle based on the url structure. Different transport names are
39 * concatinated using the `+` char. /socket.io/socket.io+websocket.js should
40 * create a bundle that only contains support for the websocket.
45 var bundle = /\+((?:\+)?[\w\-]+)*(?:\.v\d+\.\d+\.\d+)?(?:\.js)$/
46 , versioning = /\.v\d+\.\d+\.\d+(?:\.js)$/;
49 * Export the constructor
52 exports = module.exports = Static;
60 function Static (manager) {
61 this.manager = manager;
69 * Initialize the Static by adding default file paths.
74 Static.prototype.init = function () {
76 * Generates a unique id based the supplied transports array
78 * @param {Array} transports The array with transport types
81 function id (transports) {
82 var id = transports.join('').split('').map(function (char) {
83 return ('' + char.charCodeAt(0)).split('').pop();
84 }).reduce(function (char, id) {
88 return client.version + ':' + id;
92 * Generates a socket.io-client file based on the supplied transports.
94 * @param {Array} transports The array with transport types
95 * @param {Function} callback Callback for the static.write
99 function build (transports, callback) {
100 client.builder(transports, {
101 minify: self.manager.enabled('browser client minification')
102 }, function (err, content) {
103 callback(err, content ? new Buffer(content) : null, id(transports));
110 // add our default static files
111 this.add('/static/flashsocket/WebSocketMain.swf', {
112 file: client.dist + '/WebSocketMain.swf'
115 this.add('/static/flashsocket/WebSocketMainInsecure.swf', {
116 file: client.dist + '/WebSocketMainInsecure.swf'
119 // generates dedicated build based on the available transports
120 this.add('/socket.io.js', function (path, callback) {
121 build(self.manager.get('transports'), callback);
124 this.add('/socket.io.v', { mime: mime.js }, function (path, callback) {
125 build(self.manager.get('transports'), callback);
128 // allow custom builds based on url paths
129 this.add('/socket.io+', { mime: mime.js }, function (path, callback) {
130 var available = self.manager.get('transports')
131 , matches = path.match(bundle)
134 if (!matches) return callback('No valid transports');
136 // make sure they valid transports
137 matches[0].split('.')[0].split('+').slice(1).forEach(function (transport) {
138 if (!!~available.indexOf(transport)) {
139 transports.push(transport);
143 if (!transports.length) return callback('No valid transports');
144 build(transports, callback);
147 // clear cache when transports change
148 this.manager.on('set:transports', function (key, value) {
149 delete self.cache['/socket.io.js'];
150 Object.keys(self.cache).forEach(function (key) {
151 if (bundle.test(key)) {
152 delete self.cache[key];
159 * Gzip compress buffers.
161 * @param {Buffer} data The buffer that needs gzip compression
162 * @param {Function} callback
166 Static.prototype.gzip = function (data, callback) {
167 var gzip = cp.spawn('gzip', ['-9', '-c', '-f', '-n'])
168 , encoding = Buffer.isBuffer(data) ? 'binary' : 'utf8'
172 gzip.stdout.on('data', function (data) {
176 gzip.stderr.on('data', function (data) {
181 gzip.on('close', function () {
182 if (err) return callback(err);
190 size += buffer[i].length;
193 content = new Buffer(size);
196 buffer.forEach(function (buffer) {
197 var length = buffer.length;
199 buffer.copy(content, index, 0, length);
204 callback(null, content);
207 gzip.stdin.end(data, encoding);
211 * Is the path a static file?
213 * @param {String} path The path that needs to be checked
217 Static.prototype.has = function (path) {
219 if (this.paths[path]) return this.paths[path];
221 var keys = Object.keys(this.paths)
225 if (-~path.indexOf(keys[i])) return this.paths[keys[i]];
232 * Add new paths new paths that can be served using the static provider.
234 * @param {String} path The path to respond to
235 * @param {Options} options Options for writing out the response
236 * @param {Function} [callback] Optional callback if no options.file is
237 * supplied this would be called instead.
241 Static.prototype.add = function (path, options, callback) {
242 var extension = /(?:\.(\w{1,4}))$/.exec(path);
244 if (!callback && typeof options == 'function') {
249 options.mime = options.mime || (extension ? mime[extension[1]] : false);
251 if (callback) options.callback = callback;
252 if (!(options.file || options.callback) || !options.mime) return false;
254 this.paths[path] = options;
260 * Writes a static response.
262 * @param {String} path The path for the static content
263 * @param {HTTPRequest} req The request object
264 * @param {HTTPResponse} res The response object
268 Static.prototype.write = function (path, req, res) {
270 * Write a response without throwing errors because can throw error if the
271 * response is no longer writable etc.
276 function write (status, headers, content, encoding) {
278 res.writeHead(status, headers || undefined);
280 // only write content if it's not a HEAD request and we actually have
281 // some content to write (304's doesn't have content).
283 req.method !== 'HEAD' && content ? content : ''
284 , encoding || undefined
290 * Answers requests depending on the request properties and the reply object.
292 * @param {Object} reply The details and content to reply the response with
296 function answer (reply) {
297 var cached = req.headers['if-none-match'] === reply.etag;
298 if (cached && self.manager.enabled('browser client etag')) {
302 var accept = req.headers['accept-encoding'] || ''
303 , gzip = !!~accept.toLowerCase().indexOf('gzip')
305 , versioned = reply.versioned
307 'Content-Type': mime.type
310 // check if we can add a etag
311 if (self.manager.enabled('browser client etag') && reply.etag && !versioned) {
312 headers['Etag'] = reply.etag;
315 // see if we need to set Expire headers because the path is versioned
317 var expires = self.manager.get('browser client expires');
318 headers['Cache-Control'] = 'private, x-gzip-ok="", max-age=' + expires;
319 headers['Date'] = new Date().toUTCString();
320 headers['Expires'] = new Date(Date.now() + (expires * 1000)).toUTCString();
323 if (gzip && reply.gzip) {
324 headers['Content-Length'] = reply.gzip.length;
325 headers['Content-Encoding'] = 'gzip';
326 headers['Vary'] = 'Accept-Encoding';
327 write(200, headers, reply.gzip.content, mime.encoding);
329 headers['Content-Length'] = reply.length;
330 write(200, headers, reply.content, mime.encoding);
333 self.manager.log.debug('served static content ' + path);
339 // most common case first
340 if (this.manager.enabled('browser client cache') && this.cache[path]) {
341 return answer(this.cache[path]);
342 } else if (this.manager.get('browser client handler')) {
343 return this.manager.get('browser client handler').call(this, req, res);
344 } else if ((details = this.has(path))) {
346 * A small helper function that will let us deal with fs and dynamic files
348 * @param {Object} err Optional error
349 * @param {Buffer} content The data
353 function ready (err, content, etag) {
355 self.manager.log.warn('Unable to serve file. ' + (err.message || err));
356 return write(500, null, 'Error serving static ' + path);
359 // store the result in the cache
360 var reply = self.cache[path] = {
362 , length: content.length
364 , etag: etag || client.version
365 , versioned: versioning.test(path)
368 // check if gzip is enabled
369 if (details.mime.gzip && self.manager.enabled('browser client gzip')) {
370 self.gzip(content, function (err, content) {
374 , length: content.length
386 fs.readFile(details.file, ready);
387 } else if(details.callback) {
388 details.callback.call(this, path, ready);
390 write(404, null, 'File handle not found');
393 write(404, null, 'File not found');