static.js 9.8 KB

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