rc-web@69
|
1
|
rc-web@69
|
2 /*!
|
rc-web@69
|
3 * socket.io-node
|
rc-web@69
|
4 * Copyright(c) 2011 LearnBoost <dev@learnboost.com>
|
rc-web@69
|
5 * MIT Licensed
|
rc-web@69
|
6 */
|
rc-web@69
|
7
|
rc-web@69
|
8 /**
|
rc-web@69
|
9 * Module dependencies.
|
rc-web@69
|
10 */
|
rc-web@69
|
11
|
rc-web@69
|
12 var client = require('socket.io-client')
|
rc-web@69
|
13 , cp = require('child_process')
|
rc-web@69
|
14 , fs = require('fs')
|
rc-web@69
|
15 , util = require('./util');
|
rc-web@69
|
16
|
rc-web@69
|
17 /**
|
rc-web@69
|
18 * File type details.
|
rc-web@69
|
19 *
|
rc-web@69
|
20 * @api private
|
rc-web@69
|
21 */
|
rc-web@69
|
22
|
rc-web@69
|
23 var mime = {
|
rc-web@69
|
24 js: {
|
rc-web@69
|
25 type: 'application/javascript'
|
rc-web@69
|
26 , encoding: 'utf8'
|
rc-web@69
|
27 , gzip: true
|
rc-web@69
|
28 }
|
rc-web@69
|
29 , swf: {
|
rc-web@69
|
30 type: 'application/x-shockwave-flash'
|
rc-web@69
|
31 , encoding: 'binary'
|
rc-web@69
|
32 , gzip: false
|
rc-web@69
|
33 }
|
rc-web@69
|
34 };
|
rc-web@69
|
35
|
rc-web@69
|
36 /**
|
rc-web@69
|
37 * Regexp for matching custom transport patterns. Users can configure their own
|
rc-web@69
|
38 * socket.io bundle based on the url structure. Different transport names are
|
rc-web@69
|
39 * concatinated using the `+` char. /socket.io/socket.io+websocket.js should
|
rc-web@69
|
40 * create a bundle that only contains support for the websocket.
|
rc-web@69
|
41 *
|
rc-web@69
|
42 * @api private
|
rc-web@69
|
43 */
|
rc-web@69
|
44
|
rc-web@69
|
45 var bundle = /\+((?:\+)?[\w\-]+)*(?:\.v\d+\.\d+\.\d+)?(?:\.js)$/
|
rc-web@69
|
46 , versioning = /\.v\d+\.\d+\.\d+(?:\.js)$/;
|
rc-web@69
|
47
|
rc-web@69
|
48 /**
|
rc-web@69
|
49 * Export the constructor
|
rc-web@69
|
50 */
|
rc-web@69
|
51
|
rc-web@69
|
52 exports = module.exports = Static;
|
rc-web@69
|
53
|
rc-web@69
|
54 /**
|
rc-web@69
|
55 * Static constructor
|
rc-web@69
|
56 *
|
rc-web@69
|
57 * @api public
|
rc-web@69
|
58 */
|
rc-web@69
|
59
|
rc-web@69
|
60 function Static (manager) {
|
rc-web@69
|
61 this.manager = manager;
|
rc-web@69
|
62 this.cache = {};
|
rc-web@69
|
63 this.paths = {};
|
rc-web@69
|
64
|
rc-web@69
|
65 this.init();
|
rc-web@69
|
66 }
|
rc-web@69
|
67
|
rc-web@69
|
68 /**
|
rc-web@69
|
69 * Initialize the Static by adding default file paths.
|
rc-web@69
|
70 *
|
rc-web@69
|
71 * @api public
|
rc-web@69
|
72 */
|
rc-web@69
|
73
|
rc-web@69
|
74 Static.prototype.init = function () {
|
rc-web@69
|
75 /**
|
rc-web@69
|
76 * Generates a unique id based the supplied transports array
|
rc-web@69
|
77 *
|
rc-web@69
|
78 * @param {Array} transports The array with transport types
|
rc-web@69
|
79 * @api private
|
rc-web@69
|
80 */
|
rc-web@69
|
81 function id (transports) {
|
rc-web@69
|
82 var id = transports.join('').split('').map(function (char) {
|
rc-web@69
|
83 return ('' + char.charCodeAt(0)).split('').pop();
|
rc-web@69
|
84 }).reduce(function (char, id) {
|
rc-web@69
|
85 return char +id;
|
rc-web@69
|
86 });
|
rc-web@69
|
87
|
rc-web@69
|
88 return client.version + ':' + id;
|
rc-web@69
|
89 }
|
rc-web@69
|
90
|
rc-web@69
|
91 /**
|
rc-web@69
|
92 * Generates a socket.io-client file based on the supplied transports.
|
rc-web@69
|
93 *
|
rc-web@69
|
94 * @param {Array} transports The array with transport types
|
rc-web@69
|
95 * @param {Function} callback Callback for the static.write
|
rc-web@69
|
96 * @api private
|
rc-web@69
|
97 */
|
rc-web@69
|
98
|
rc-web@69
|
99 function build (transports, callback) {
|
rc-web@69
|
100 client.builder(transports, {
|
rc-web@69
|
101 minify: self.manager.enabled('browser client minification')
|
rc-web@69
|
102 }, function (err, content) {
|
rc-web@69
|
103 callback(err, content ? new Buffer(content) : null, id(transports));
|
rc-web@69
|
104 }
|
rc-web@69
|
105 );
|
rc-web@69
|
106 }
|
rc-web@69
|
107
|
rc-web@69
|
108 var self = this;
|
rc-web@69
|
109
|
rc-web@69
|
110 // add our default static files
|
rc-web@69
|
111 this.add('/static/flashsocket/WebSocketMain.swf', {
|
rc-web@69
|
112 file: client.dist + '/WebSocketMain.swf'
|
rc-web@69
|
113 });
|
rc-web@69
|
114
|
rc-web@69
|
115 this.add('/static/flashsocket/WebSocketMainInsecure.swf', {
|
rc-web@69
|
116 file: client.dist + '/WebSocketMainInsecure.swf'
|
rc-web@69
|
117 });
|
rc-web@69
|
118
|
rc-web@69
|
119 // generates dedicated build based on the available transports
|
rc-web@69
|
120 this.add('/socket.io.js', function (path, callback) {
|
rc-web@69
|
121 build(self.manager.get('transports'), callback);
|
rc-web@69
|
122 });
|
rc-web@69
|
123
|
rc-web@69
|
124 this.add('/socket.io.v', { mime: mime.js }, function (path, callback) {
|
rc-web@69
|
125 build(self.manager.get('transports'), callback);
|
rc-web@69
|
126 });
|
rc-web@69
|
127
|
rc-web@69
|
128 // allow custom builds based on url paths
|
rc-web@69
|
129 this.add('/socket.io+', { mime: mime.js }, function (path, callback) {
|
rc-web@69
|
130 var available = self.manager.get('transports')
|
rc-web@69
|
131 , matches = path.match(bundle)
|
rc-web@69
|
132 , transports = [];
|
rc-web@69
|
133
|
rc-web@69
|
134 if (!matches) return callback('No valid transports');
|
rc-web@69
|
135
|
rc-web@69
|
136 // make sure they valid transports
|
rc-web@69
|
137 matches[0].split('.')[0].split('+').slice(1).forEach(function (transport) {
|
rc-web@69
|
138 if (!!~available.indexOf(transport)) {
|
rc-web@69
|
139 transports.push(transport);
|
rc-web@69
|
140 }
|
rc-web@69
|
141 });
|
rc-web@69
|
142
|
rc-web@69
|
143 if (!transports.length) return callback('No valid transports');
|
rc-web@69
|
144 build(transports, callback);
|
rc-web@69
|
145 });
|
rc-web@69
|
146
|
rc-web@69
|
147 // clear cache when transports change
|
rc-web@69
|
148 this.manager.on('set:transports', function (key, value) {
|
rc-web@69
|
149 delete self.cache['/socket.io.js'];
|
rc-web@69
|
150 Object.keys(self.cache).forEach(function (key) {
|
rc-web@69
|
151 if (bundle.test(key)) {
|
rc-web@69
|
152 delete self.cache[key];
|
rc-web@69
|
153 }
|
rc-web@69
|
154 });
|
rc-web@69
|
155 });
|
rc-web@69
|
156 };
|
rc-web@69
|
157
|
rc-web@69
|
158 /**
|
rc-web@69
|
159 * Gzip compress buffers.
|
rc-web@69
|
160 *
|
rc-web@69
|
161 * @param {Buffer} data The buffer that needs gzip compression
|
rc-web@69
|
162 * @param {Function} callback
|
rc-web@69
|
163 * @api public
|
rc-web@69
|
164 */
|
rc-web@69
|
165
|
rc-web@69
|
166 Static.prototype.gzip = function (data, callback) {
|
rc-web@69
|
167 var gzip = cp.spawn('gzip', ['-9', '-c', '-f', '-n'])
|
rc-web@69
|
168 , encoding = Buffer.isBuffer(data) ? 'binary' : 'utf8'
|
rc-web@69
|
169 , buffer = []
|
rc-web@69
|
170 , err;
|
rc-web@69
|
171
|
rc-web@69
|
172 gzip.stdout.on('data', function (data) {
|
rc-web@69
|
173 buffer.push(data);
|
rc-web@69
|
174 });
|
rc-web@69
|
175
|
rc-web@69
|
176 gzip.stderr.on('data', function (data) {
|
rc-web@69
|
177 err = data +'';
|
rc-web@69
|
178 buffer.length = 0;
|
rc-web@69
|
179 });
|
rc-web@69
|
180
|
rc-web@69
|
181 gzip.on('close', function () {
|
rc-web@69
|
182 if (err) return callback(err);
|
rc-web@69
|
183
|
rc-web@69
|
184 var size = 0
|
rc-web@69
|
185 , index = 0
|
rc-web@69
|
186 , i = buffer.length
|
rc-web@69
|
187 , content;
|
rc-web@69
|
188
|
rc-web@69
|
189 while (i--) {
|
rc-web@69
|
190 size += buffer[i].length;
|
rc-web@69
|
191 }
|
rc-web@69
|
192
|
rc-web@69
|
193 content = new Buffer(size);
|
rc-web@69
|
194 i = buffer.length;
|
rc-web@69
|
195
|
rc-web@69
|
196 buffer.forEach(function (buffer) {
|
rc-web@69
|
197 var length = buffer.length;
|
rc-web@69
|
198
|
rc-web@69
|
199 buffer.copy(content, index, 0, length);
|
rc-web@69
|
200 index += length;
|
rc-web@69
|
201 });
|
rc-web@69
|
202
|
rc-web@69
|
203 buffer.length = 0;
|
rc-web@69
|
204 callback(null, content);
|
rc-web@69
|
205 });
|
rc-web@69
|
206
|
rc-web@69
|
207 gzip.stdin.end(data, encoding);
|
rc-web@69
|
208 };
|
rc-web@69
|
209
|
rc-web@69
|
210 /**
|
rc-web@69
|
211 * Is the path a static file?
|
rc-web@69
|
212 *
|
rc-web@69
|
213 * @param {String} path The path that needs to be checked
|
rc-web@69
|
214 * @api public
|
rc-web@69
|
215 */
|
rc-web@69
|
216
|
rc-web@69
|
217 Static.prototype.has = function (path) {
|
rc-web@69
|
218 // fast case
|
rc-web@69
|
219 if (this.paths[path]) return this.paths[path];
|
rc-web@69
|
220
|
rc-web@69
|
221 var keys = Object.keys(this.paths)
|
rc-web@69
|
222 , i = keys.length;
|
rc-web@69
|
223
|
rc-web@69
|
224 while (i--) {
|
rc-web@69
|
225 if (-~path.indexOf(keys[i])) return this.paths[keys[i]];
|
rc-web@69
|
226 }
|
rc-web@69
|
227
|
rc-web@69
|
228 return false;
|
rc-web@69
|
229 };
|
rc-web@69
|
230
|
rc-web@69
|
231 /**
|
rc-web@69
|
232 * Add new paths new paths that can be served using the static provider.
|
rc-web@69
|
233 *
|
rc-web@69
|
234 * @param {String} path The path to respond to
|
rc-web@69
|
235 * @param {Options} options Options for writing out the response
|
rc-web@69
|
236 * @param {Function} [callback] Optional callback if no options.file is
|
rc-web@69
|
237 * supplied this would be called instead.
|
rc-web@69
|
238 * @api public
|
rc-web@69
|
239 */
|
rc-web@69
|
240
|
rc-web@69
|
241 Static.prototype.add = function (path, options, callback) {
|
rc-web@69
|
242 var extension = /(?:\.(\w{1,4}))$/.exec(path);
|
rc-web@69
|
243
|
rc-web@69
|
244 if (!callback && typeof options == 'function') {
|
rc-web@69
|
245 callback = options;
|
rc-web@69
|
246 options = {};
|
rc-web@69
|
247 }
|
rc-web@69
|
248
|
rc-web@69
|
249 options.mime = options.mime || (extension ? mime[extension[1]] : false);
|
rc-web@69
|
250
|
rc-web@69
|
251 if (callback) options.callback = callback;
|
rc-web@69
|
252 if (!(options.file || options.callback) || !options.mime) return false;
|
rc-web@69
|
253
|
rc-web@69
|
254 this.paths[path] = options;
|
rc-web@69
|
255
|
rc-web@69
|
256 return true;
|
rc-web@69
|
257 };
|
rc-web@69
|
258
|
rc-web@69
|
259 /**
|
rc-web@69
|
260 * Writes a static response.
|
rc-web@69
|
261 *
|
rc-web@69
|
262 * @param {String} path The path for the static content
|
rc-web@69
|
263 * @param {HTTPRequest} req The request object
|
rc-web@69
|
264 * @param {HTTPResponse} res The response object
|
rc-web@69
|
265 * @api public
|
rc-web@69
|
266 */
|
rc-web@69
|
267
|
rc-web@69
|
268 Static.prototype.write = function (path, req, res) {
|
rc-web@69
|
269 /**
|
rc-web@69
|
270 * Write a response without throwing errors because can throw error if the
|
rc-web@69
|
271 * response is no longer writable etc.
|
rc-web@69
|
272 *
|
rc-web@69
|
273 * @api private
|
rc-web@69
|
274 */
|
rc-web@69
|
275
|
rc-web@69
|
276 function write (status, headers, content, encoding) {
|
rc-web@69
|
277 try {
|
rc-web@69
|
278 res.writeHead(status, headers || undefined);
|
rc-web@69
|
279
|
rc-web@69
|
280 // only write content if it's not a HEAD request and we actually have
|
rc-web@69
|
281 // some content to write (304's doesn't have content).
|
rc-web@69
|
282 res.end(
|
rc-web@69
|
283 req.method !== 'HEAD' && content ? content : ''
|
rc-web@69
|
284 , encoding || undefined
|
rc-web@69
|
285 );
|
rc-web@69
|
286 } catch (e) {}
|
rc-web@69
|
287 }
|
rc-web@69
|
288
|
rc-web@69
|
289 /**
|
rc-web@69
|
290 * Answers requests depending on the request properties and the reply object.
|
rc-web@69
|
291 *
|
rc-web@69
|
292 * @param {Object} reply The details and content to reply the response with
|
rc-web@69
|
293 * @api private
|
rc-web@69
|
294 */
|
rc-web@69
|
295
|
rc-web@69
|
296 function answer (reply) {
|
rc-web@69
|
297 var cached = req.headers['if-none-match'] === reply.etag;
|
rc-web@69
|
298 if (cached && self.manager.enabled('browser client etag')) {
|
rc-web@69
|
299 return write(304);
|
rc-web@69
|
300 }
|
rc-web@69
|
301
|
rc-web@69
|
302 var accept = req.headers['accept-encoding'] || ''
|
rc-web@69
|
303 , gzip = !!~accept.toLowerCase().indexOf('gzip')
|
rc-web@69
|
304 , mime = reply.mime
|
rc-web@69
|
305 , versioned = reply.versioned
|
rc-web@69
|
306 , headers = {
|
rc-web@69
|
307 'Content-Type': mime.type
|
rc-web@69
|
308 };
|
rc-web@69
|
309
|
rc-web@69
|
310 // check if we can add a etag
|
rc-web@69
|
311 if (self.manager.enabled('browser client etag') && reply.etag && !versioned) {
|
rc-web@69
|
312 headers['Etag'] = reply.etag;
|
rc-web@69
|
313 }
|
rc-web@69
|
314
|
rc-web@69
|
315 // see if we need to set Expire headers because the path is versioned
|
rc-web@69
|
316 if (versioned) {
|
rc-web@69
|
317 var expires = self.manager.get('browser client expires');
|
rc-web@69
|
318 headers['Cache-Control'] = 'private, x-gzip-ok="", max-age=' + expires;
|
rc-web@69
|
319 headers['Date'] = new Date().toUTCString();
|
rc-web@69
|
320 headers['Expires'] = new Date(Date.now() + (expires * 1000)).toUTCString();
|
rc-web@69
|
321 }
|
rc-web@69
|
322
|
rc-web@69
|
323 if (gzip && reply.gzip) {
|
rc-web@69
|
324 headers['Content-Length'] = reply.gzip.length;
|
rc-web@69
|
325 headers['Content-Encoding'] = 'gzip';
|
rc-web@69
|
326 headers['Vary'] = 'Accept-Encoding';
|
rc-web@69
|
327 write(200, headers, reply.gzip.content, mime.encoding);
|
rc-web@69
|
328 } else {
|
rc-web@69
|
329 headers['Content-Length'] = reply.length;
|
rc-web@69
|
330 write(200, headers, reply.content, mime.encoding);
|
rc-web@69
|
331 }
|
rc-web@69
|
332
|
rc-web@69
|
333 self.manager.log.debug('served static content ' + path);
|
rc-web@69
|
334 }
|
rc-web@69
|
335
|
rc-web@69
|
336 var self = this
|
rc-web@69
|
337 , details;
|
rc-web@69
|
338
|
rc-web@69
|
339 // most common case first
|
rc-web@69
|
340 if (this.manager.enabled('browser client cache') && this.cache[path]) {
|
rc-web@69
|
341 return answer(this.cache[path]);
|
rc-web@69
|
342 } else if (this.manager.get('browser client handler')) {
|
rc-web@69
|
343 return this.manager.get('browser client handler').call(this, req, res);
|
rc-web@69
|
344 } else if ((details = this.has(path))) {
|
rc-web@69
|
345 /**
|
rc-web@69
|
346 * A small helper function that will let us deal with fs and dynamic files
|
rc-web@69
|
347 *
|
rc-web@69
|
348 * @param {Object} err Optional error
|
rc-web@69
|
349 * @param {Buffer} content The data
|
rc-web@69
|
350 * @api private
|
rc-web@69
|
351 */
|
rc-web@69
|
352
|
rc-web@69
|
353 function ready (err, content, etag) {
|
rc-web@69
|
354 if (err) {
|
rc-web@69
|
355 self.manager.log.warn('Unable to serve file. ' + (err.message || err));
|
rc-web@69
|
356 return write(500, null, 'Error serving static ' + path);
|
rc-web@69
|
357 }
|
rc-web@69
|
358
|
rc-web@69
|
359 // store the result in the cache
|
rc-web@69
|
360 var reply = self.cache[path] = {
|
rc-web@69
|
361 content: content
|
rc-web@69
|
362 , length: content.length
|
rc-web@69
|
363 , mime: details.mime
|
rc-web@69
|
364 , etag: etag || client.version
|
rc-web@69
|
365 , versioned: versioning.test(path)
|
rc-web@69
|
366 };
|
rc-web@69
|
367
|
rc-web@69
|
368 // check if gzip is enabled
|
rc-web@69
|
369 if (details.mime.gzip && self.manager.enabled('browser client gzip')) {
|
rc-web@69
|
370 self.gzip(content, function (err, content) {
|
rc-web@69
|
371 if (!err) {
|
rc-web@69
|
372 reply.gzip = {
|
rc-web@69
|
373 content: content
|
rc-web@69
|
374 , length: content.length
|
rc-web@69
|
375 }
|
rc-web@69
|
376 }
|
rc-web@69
|
377
|
rc-web@69
|
378 answer(reply);
|
rc-web@69
|
379 });
|
rc-web@69
|
380 } else {
|
rc-web@69
|
381 answer(reply);
|
rc-web@69
|
382 }
|
rc-web@69
|
383 }
|
rc-web@69
|
384
|
rc-web@69
|
385 if (details.file) {
|
rc-web@69
|
386 fs.readFile(details.file, ready);
|
rc-web@69
|
387 } else if(details.callback) {
|
rc-web@69
|
388 details.callback.call(this, path, ready);
|
rc-web@69
|
389 } else {
|
rc-web@69
|
390 write(404, null, 'File handle not found');
|
rc-web@69
|
391 }
|
rc-web@69
|
392 } else {
|
rc-web@69
|
393 write(404, null, 'File not found');
|
rc-web@69
|
394 }
|
rc-web@69
|
395 };
|