/node_modules/express/node_modules/send/lib/send.js

https://bitbucket.org/gagginaspinnata/todo-app-with-angularjs · JavaScript · 462 lines · 199 code · 77 blank · 186 comment · 46 complexity · bc567ca2e6c58fa42147326cd7a3e762 MD5 · raw file

  1. /**
  2. * Module dependencies.
  3. */
  4. var debug = require('debug')('send')
  5. , parseRange = require('range-parser')
  6. , Stream = require('stream')
  7. , mime = require('mime')
  8. , fresh = require('fresh')
  9. , path = require('path')
  10. , http = require('http')
  11. , fs = require('fs')
  12. , basename = path.basename
  13. , normalize = path.normalize
  14. , join = path.join
  15. , utils = require('./utils');
  16. /**
  17. * Expose `send`.
  18. */
  19. exports = module.exports = send;
  20. /**
  21. * Expose mime module.
  22. */
  23. exports.mime = mime;
  24. /**
  25. * Return a `SendStream` for `req` and `path`.
  26. *
  27. * @param {Request} req
  28. * @param {String} path
  29. * @return {SendStream}
  30. * @api public
  31. */
  32. function send(req, path) {
  33. return new SendStream(req, path);
  34. }
  35. /**
  36. * Initialize a `SendStream` with the given `path`.
  37. *
  38. * Events:
  39. *
  40. * - `error` an error occurred
  41. * - `stream` file streaming has started
  42. * - `end` streaming has completed
  43. * - `directory` a directory was requested
  44. *
  45. * @param {Request} req
  46. * @param {String} path
  47. * @api private
  48. */
  49. function SendStream(req, path) {
  50. var self = this;
  51. this.req = req;
  52. this.path = path;
  53. this.maxage(0);
  54. this.hidden(false);
  55. this.index('index.html');
  56. }
  57. /**
  58. * Inherits from `Stream.prototype`.
  59. */
  60. SendStream.prototype.__proto__ = Stream.prototype;
  61. /**
  62. * Enable or disable "hidden" (dot) files.
  63. *
  64. * @param {Boolean} path
  65. * @return {SendStream}
  66. * @api public
  67. */
  68. SendStream.prototype.hidden = function(val){
  69. debug('hidden %s', val);
  70. this._hidden = val;
  71. return this;
  72. };
  73. /**
  74. * Set index `path`, set to a falsy
  75. * value to disable index support.
  76. *
  77. * @param {String|Boolean} path
  78. * @return {SendStream}
  79. * @api public
  80. */
  81. SendStream.prototype.index = function(path){
  82. debug('index %s', path);
  83. this._index = path;
  84. return this;
  85. };
  86. /**
  87. * Set root `path`.
  88. *
  89. * @param {String} path
  90. * @return {SendStream}
  91. * @api public
  92. */
  93. SendStream.prototype.root =
  94. SendStream.prototype.from = function(path){
  95. this._root = normalize(path);
  96. return this;
  97. };
  98. /**
  99. * Set max-age to `ms`.
  100. *
  101. * @param {Number} ms
  102. * @return {SendStream}
  103. * @api public
  104. */
  105. SendStream.prototype.maxage = function(ms){
  106. if (Infinity == ms) ms = 60 * 60 * 24 * 365 * 1000;
  107. debug('max-age %d', ms);
  108. this._maxage = ms;
  109. return this;
  110. };
  111. /**
  112. * Emit error with `status`.
  113. *
  114. * @param {Number} status
  115. * @api private
  116. */
  117. SendStream.prototype.error = function(status, err){
  118. var res = this.res;
  119. var msg = http.STATUS_CODES[status];
  120. err = err || new Error(msg);
  121. err.status = status;
  122. if (this.listeners('error').length) return this.emit('error', err);
  123. res.statusCode = err.status;
  124. res.end(msg);
  125. };
  126. /**
  127. * Check if the pathname is potentially malicious.
  128. *
  129. * @return {Boolean}
  130. * @api private
  131. */
  132. SendStream.prototype.isMalicious = function(){
  133. return !this._root && ~this.path.indexOf('..');
  134. };
  135. /**
  136. * Check if the pathname ends with "/".
  137. *
  138. * @return {Boolean}
  139. * @api private
  140. */
  141. SendStream.prototype.hasTrailingSlash = function(){
  142. return '/' == this.path[this.path.length - 1];
  143. };
  144. /**
  145. * Check if the basename leads with ".".
  146. *
  147. * @return {Boolean}
  148. * @api private
  149. */
  150. SendStream.prototype.hasLeadingDot = function(){
  151. return '.' == basename(this.path)[0];
  152. };
  153. /**
  154. * Check if this is a conditional GET request.
  155. *
  156. * @return {Boolean}
  157. * @api private
  158. */
  159. SendStream.prototype.isConditionalGET = function(){
  160. return this.req.headers['if-none-match']
  161. || this.req.headers['if-modified-since'];
  162. };
  163. /**
  164. * Strip content-* header fields.
  165. *
  166. * @api private
  167. */
  168. SendStream.prototype.removeContentHeaderFields = function(){
  169. var res = this.res;
  170. Object.keys(res._headers).forEach(function(field){
  171. if (0 == field.indexOf('content')) {
  172. res.removeHeader(field);
  173. }
  174. });
  175. };
  176. /**
  177. * Respond with 304 not modified.
  178. *
  179. * @api private
  180. */
  181. SendStream.prototype.notModified = function(){
  182. var res = this.res;
  183. debug('not modified');
  184. this.removeContentHeaderFields();
  185. res.statusCode = 304;
  186. res.end();
  187. };
  188. /**
  189. * Check if the request is cacheable, aka
  190. * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}).
  191. *
  192. * @return {Boolean}
  193. * @api private
  194. */
  195. SendStream.prototype.isCachable = function(){
  196. var res = this.res;
  197. return (res.statusCode >= 200 && res.statusCode < 300) || 304 == res.statusCode;
  198. };
  199. /**
  200. * Handle stat() error.
  201. *
  202. * @param {Error} err
  203. * @api private
  204. */
  205. SendStream.prototype.onStatError = function(err){
  206. var notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'];
  207. if (~notfound.indexOf(err.code)) return this.error(404, err);
  208. this.error(500, err);
  209. };
  210. /**
  211. * Check if the cache is fresh.
  212. *
  213. * @return {Boolean}
  214. * @api private
  215. */
  216. SendStream.prototype.isFresh = function(){
  217. return fresh(this.req.headers, this.res._headers);
  218. };
  219. /**
  220. * Redirect to `path`.
  221. *
  222. * @param {String} path
  223. * @api private
  224. */
  225. SendStream.prototype.redirect = function(path){
  226. if (this.listeners('directory').length) return this.emit('directory');
  227. var res = this.res;
  228. path += '/';
  229. res.statusCode = 301;
  230. res.setHeader('Location', path);
  231. res.end('Redirecting to ' + utils.escape(path));
  232. };
  233. /**
  234. * Pipe to `res.
  235. *
  236. * @param {Stream} res
  237. * @return {Stream} res
  238. * @api public
  239. */
  240. SendStream.prototype.pipe = function(res){
  241. var self = this
  242. , args = arguments
  243. , path = this.path
  244. , root = this._root;
  245. // references
  246. this.res = res;
  247. // invalid request uri
  248. path = utils.decode(path);
  249. if (-1 == path) return this.error(400);
  250. // null byte(s)
  251. if (~path.indexOf('\0')) return this.error(400);
  252. // join / normalize from optional root dir
  253. if (root) path = normalize(join(this._root, path));
  254. // ".." is malicious without "root"
  255. if (this.isMalicious()) return this.error(403);
  256. // malicious path
  257. if (root && 0 != path.indexOf(root)) return this.error(403);
  258. // hidden file support
  259. if (!this._hidden && this.hasLeadingDot()) return this.error(404);
  260. // index file support
  261. if (this._index && this.hasTrailingSlash()) path += this._index;
  262. debug('stat "%s"', path);
  263. fs.stat(path, function(err, stat){
  264. if (err) return self.onStatError(err);
  265. if (stat.isDirectory()) return self.redirect(self.path);
  266. self.send(path, stat);
  267. });
  268. return res;
  269. };
  270. /**
  271. * Transfer `path`.
  272. *
  273. * @param {String} path
  274. * @api public
  275. */
  276. SendStream.prototype.send = function(path, stat){
  277. var options = {};
  278. var len = stat.size;
  279. var res = this.res;
  280. var req = this.req;
  281. var ranges = req.headers.range;
  282. // set header fields
  283. this.setHeader(stat);
  284. // set content-type
  285. this.type(path);
  286. // conditional GET support
  287. if (this.isConditionalGET()
  288. && this.isCachable()
  289. && this.isFresh()) {
  290. return this.notModified();
  291. }
  292. // Range support
  293. if (ranges) {
  294. ranges = parseRange(len, ranges);
  295. // unsatisfiable
  296. if (-1 == ranges) {
  297. res.setHeader('Content-Range', 'bytes */' + stat.size);
  298. return this.error(416);
  299. }
  300. // valid (syntactically invalid ranges are treated as a regular response)
  301. if (-2 != ranges) {
  302. options.start = ranges[0].start;
  303. options.end = ranges[0].end;
  304. // Content-Range
  305. len = options.end - options.start + 1;
  306. res.statusCode = 206;
  307. res.setHeader('Content-Range', 'bytes '
  308. + options.start
  309. + '-'
  310. + options.end
  311. + '/'
  312. + stat.size);
  313. }
  314. }
  315. // content-length
  316. res.setHeader('Content-Length', len);
  317. // HEAD support
  318. if ('HEAD' == req.method) return res.end();
  319. this.stream(path, options);
  320. };
  321. /**
  322. * Stream `path` to the response.
  323. *
  324. * @param {String} path
  325. * @param {Object} options
  326. * @api private
  327. */
  328. SendStream.prototype.stream = function(path, options){
  329. // TODO: this is all lame, refactor meeee
  330. var self = this;
  331. var res = this.res;
  332. var req = this.req;
  333. // pipe
  334. var stream = fs.createReadStream(path, options);
  335. this.emit('stream', stream);
  336. stream.pipe(res);
  337. // socket closed, done with the fd
  338. req.on('close', stream.destroy.bind(stream));
  339. // error handling code-smell
  340. stream.on('error', function(err){
  341. // no hope in responding
  342. if (res._header) {
  343. console.error(err.stack);
  344. req.destroy();
  345. return;
  346. }
  347. // 500
  348. err.status = 500;
  349. self.emit('error', err);
  350. });
  351. // end
  352. stream.on('end', function(){
  353. self.emit('end');
  354. });
  355. };
  356. /**
  357. * Set content-type based on `path`
  358. * if it hasn't been explicitly set.
  359. *
  360. * @param {String} path
  361. * @api private
  362. */
  363. SendStream.prototype.type = function(path){
  364. var res = this.res;
  365. if (res.getHeader('Content-Type')) return;
  366. var type = mime.lookup(path);
  367. var charset = mime.charsets.lookup(type);
  368. debug('content-type %s', type);
  369. res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
  370. };
  371. /**
  372. * Set reaponse header fields, most
  373. * fields may be pre-defined.
  374. *
  375. * @param {Object} stat
  376. * @api private
  377. */
  378. SendStream.prototype.setHeader = function(stat){
  379. var res = this.res;
  380. if (!res.getHeader('Accept-Ranges')) res.setHeader('Accept-Ranges', 'bytes');
  381. if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat));
  382. if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString());
  383. if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (this._maxage / 1000));
  384. if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString());
  385. };