You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

773 lines
16 KiB

7 years ago
  1. /**
  2. * Module dependencies.
  3. */
  4. var debug = require('debug')('send')
  5. var deprecate = require('depd')('send')
  6. var destroy = require('destroy')
  7. var escapeHtml = require('escape-html')
  8. , parseRange = require('range-parser')
  9. , Stream = require('stream')
  10. , mime = require('mime')
  11. , fresh = require('fresh')
  12. , path = require('path')
  13. , http = require('http')
  14. , fs = require('fs')
  15. , normalize = path.normalize
  16. , join = path.join
  17. var etag = require('etag')
  18. var EventEmitter = require('events').EventEmitter;
  19. var ms = require('ms');
  20. var onFinished = require('on-finished')
  21. /**
  22. * Variables.
  23. */
  24. var extname = path.extname
  25. var maxMaxAge = 60 * 60 * 24 * 365 * 1000; // 1 year
  26. var resolve = path.resolve
  27. var sep = path.sep
  28. var toString = Object.prototype.toString
  29. var upPathRegexp = /(?:^|[\\\/])\.\.(?:[\\\/]|$)/
  30. /**
  31. * Expose `send`.
  32. */
  33. exports = module.exports = send;
  34. /**
  35. * Expose mime module.
  36. */
  37. exports.mime = mime;
  38. /**
  39. * Shim EventEmitter.listenerCount for node.js < 0.10
  40. */
  41. /* istanbul ignore next */
  42. var listenerCount = EventEmitter.listenerCount
  43. || function(emitter, type){ return emitter.listeners(type).length; };
  44. /**
  45. * Return a `SendStream` for `req` and `path`.
  46. *
  47. * @param {Request} req
  48. * @param {String} path
  49. * @param {Object} options
  50. * @return {SendStream}
  51. * @api public
  52. */
  53. function send(req, path, options) {
  54. return new SendStream(req, path, options);
  55. }
  56. /**
  57. * Initialize a `SendStream` with the given `path`.
  58. *
  59. * @param {Request} req
  60. * @param {String} path
  61. * @param {Object} options
  62. * @api private
  63. */
  64. function SendStream(req, path, options) {
  65. var self = this;
  66. options = options || {};
  67. this.req = req;
  68. this.path = path;
  69. this.options = options;
  70. this._etag = options.etag !== undefined
  71. ? Boolean(options.etag)
  72. : true
  73. this._dotfiles = options.dotfiles !== undefined
  74. ? options.dotfiles
  75. : 'ignore'
  76. if (['allow', 'deny', 'ignore'].indexOf(this._dotfiles) === -1) {
  77. throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"')
  78. }
  79. this._hidden = Boolean(options.hidden)
  80. if ('hidden' in options) {
  81. deprecate('hidden: use dotfiles: \'' + (this._hidden ? 'allow' : 'ignore') + '\' instead')
  82. }
  83. // legacy support
  84. if (!('dotfiles' in options)) {
  85. this._dotfiles = undefined
  86. }
  87. this._extensions = options.extensions !== undefined
  88. ? normalizeList(options.extensions)
  89. : []
  90. this._index = options.index !== undefined
  91. ? normalizeList(options.index)
  92. : ['index.html']
  93. this._lastModified = options.lastModified !== undefined
  94. ? Boolean(options.lastModified)
  95. : true
  96. this._maxage = options.maxAge || options.maxage
  97. this._maxage = typeof this._maxage === 'string'
  98. ? ms(this._maxage)
  99. : Number(this._maxage)
  100. this._maxage = !isNaN(this._maxage)
  101. ? Math.min(Math.max(0, this._maxage), maxMaxAge)
  102. : 0
  103. this._root = options.root
  104. ? resolve(options.root)
  105. : null
  106. if (!this._root && options.from) {
  107. this.from(options.from);
  108. }
  109. }
  110. /**
  111. * Inherits from `Stream.prototype`.
  112. */
  113. SendStream.prototype.__proto__ = Stream.prototype;
  114. /**
  115. * Enable or disable etag generation.
  116. *
  117. * @param {Boolean} val
  118. * @return {SendStream}
  119. * @api public
  120. */
  121. SendStream.prototype.etag = deprecate.function(function etag(val) {
  122. val = Boolean(val);
  123. debug('etag %s', val);
  124. this._etag = val;
  125. return this;
  126. }, 'send.etag: pass etag as option');
  127. /**
  128. * Enable or disable "hidden" (dot) files.
  129. *
  130. * @param {Boolean} path
  131. * @return {SendStream}
  132. * @api public
  133. */
  134. SendStream.prototype.hidden = deprecate.function(function hidden(val) {
  135. val = Boolean(val);
  136. debug('hidden %s', val);
  137. this._hidden = val;
  138. this._dotfiles = undefined
  139. return this;
  140. }, 'send.hidden: use dotfiles option');
  141. /**
  142. * Set index `paths`, set to a falsy
  143. * value to disable index support.
  144. *
  145. * @param {String|Boolean|Array} paths
  146. * @return {SendStream}
  147. * @api public
  148. */
  149. SendStream.prototype.index = deprecate.function(function index(paths) {
  150. var index = !paths ? [] : normalizeList(paths);
  151. debug('index %o', paths);
  152. this._index = index;
  153. return this;
  154. }, 'send.index: pass index as option');
  155. /**
  156. * Set root `path`.
  157. *
  158. * @param {String} path
  159. * @return {SendStream}
  160. * @api public
  161. */
  162. SendStream.prototype.root = function(path){
  163. path = String(path);
  164. this._root = resolve(path)
  165. return this;
  166. };
  167. SendStream.prototype.from = deprecate.function(SendStream.prototype.root,
  168. 'send.from: pass root as option');
  169. SendStream.prototype.root = deprecate.function(SendStream.prototype.root,
  170. 'send.root: pass root as option');
  171. /**
  172. * Set max-age to `maxAge`.
  173. *
  174. * @param {Number} maxAge
  175. * @return {SendStream}
  176. * @api public
  177. */
  178. SendStream.prototype.maxage = deprecate.function(function maxage(maxAge) {
  179. maxAge = typeof maxAge === 'string'
  180. ? ms(maxAge)
  181. : Number(maxAge);
  182. if (isNaN(maxAge)) maxAge = 0;
  183. if (Infinity == maxAge) maxAge = 60 * 60 * 24 * 365 * 1000;
  184. debug('max-age %d', maxAge);
  185. this._maxage = maxAge;
  186. return this;
  187. }, 'send.maxage: pass maxAge as option');
  188. /**
  189. * Emit error with `status`.
  190. *
  191. * @param {Number} status
  192. * @api private
  193. */
  194. SendStream.prototype.error = function(status, err){
  195. var res = this.res;
  196. var msg = http.STATUS_CODES[status];
  197. err = err || new Error(msg);
  198. err.status = status;
  199. // emit if listeners instead of responding
  200. if (listenerCount(this, 'error') !== 0) {
  201. return this.emit('error', err);
  202. }
  203. // wipe all existing headers
  204. res._headers = undefined;
  205. res.statusCode = err.status;
  206. res.end(msg);
  207. };
  208. /**
  209. * Check if the pathname ends with "/".
  210. *
  211. * @return {Boolean}
  212. * @api private
  213. */
  214. SendStream.prototype.hasTrailingSlash = function(){
  215. return '/' == this.path[this.path.length - 1];
  216. };
  217. /**
  218. * Check if this is a conditional GET request.
  219. *
  220. * @return {Boolean}
  221. * @api private
  222. */
  223. SendStream.prototype.isConditionalGET = function(){
  224. return this.req.headers['if-none-match']
  225. || this.req.headers['if-modified-since'];
  226. };
  227. /**
  228. * Strip content-* header fields.
  229. *
  230. * @api private
  231. */
  232. SendStream.prototype.removeContentHeaderFields = function(){
  233. var res = this.res;
  234. Object.keys(res._headers).forEach(function(field){
  235. if (0 == field.indexOf('content')) {
  236. res.removeHeader(field);
  237. }
  238. });
  239. };
  240. /**
  241. * Respond with 304 not modified.
  242. *
  243. * @api private
  244. */
  245. SendStream.prototype.notModified = function(){
  246. var res = this.res;
  247. debug('not modified');
  248. this.removeContentHeaderFields();
  249. res.statusCode = 304;
  250. res.end();
  251. };
  252. /**
  253. * Raise error that headers already sent.
  254. *
  255. * @api private
  256. */
  257. SendStream.prototype.headersAlreadySent = function headersAlreadySent(){
  258. var err = new Error('Can\'t set headers after they are sent.');
  259. debug('headers already sent');
  260. this.error(500, err);
  261. };
  262. /**
  263. * Check if the request is cacheable, aka
  264. * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}).
  265. *
  266. * @return {Boolean}
  267. * @api private
  268. */
  269. SendStream.prototype.isCachable = function(){
  270. var res = this.res;
  271. return (res.statusCode >= 200 && res.statusCode < 300) || 304 == res.statusCode;
  272. };
  273. /**
  274. * Handle stat() error.
  275. *
  276. * @param {Error} err
  277. * @api private
  278. */
  279. SendStream.prototype.onStatError = function(err){
  280. var notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'];
  281. if (~notfound.indexOf(err.code)) return this.error(404, err);
  282. this.error(500, err);
  283. };
  284. /**
  285. * Check if the cache is fresh.
  286. *
  287. * @return {Boolean}
  288. * @api private
  289. */
  290. SendStream.prototype.isFresh = function(){
  291. return fresh(this.req.headers, this.res._headers);
  292. };
  293. /**
  294. * Check if the range is fresh.
  295. *
  296. * @return {Boolean}
  297. * @api private
  298. */
  299. SendStream.prototype.isRangeFresh = function isRangeFresh(){
  300. var ifRange = this.req.headers['if-range'];
  301. if (!ifRange) return true;
  302. return ~ifRange.indexOf('"')
  303. ? ~ifRange.indexOf(this.res._headers['etag'])
  304. : Date.parse(this.res._headers['last-modified']) <= Date.parse(ifRange);
  305. };
  306. /**
  307. * Redirect to `path`.
  308. *
  309. * @param {String} path
  310. * @api private
  311. */
  312. SendStream.prototype.redirect = function(path){
  313. if (listenerCount(this, 'directory') !== 0) {
  314. return this.emit('directory');
  315. }
  316. if (this.hasTrailingSlash()) return this.error(403);
  317. var res = this.res;
  318. path += '/';
  319. res.statusCode = 301;
  320. res.setHeader('Content-Type', 'text/html; charset=utf-8');
  321. res.setHeader('Location', path);
  322. res.end('Redirecting to <a href="' + escapeHtml(path) + '">' + escapeHtml(path) + '</a>\n');
  323. };
  324. /**
  325. * Pipe to `res.
  326. *
  327. * @param {Stream} res
  328. * @return {Stream} res
  329. * @api public
  330. */
  331. SendStream.prototype.pipe = function(res){
  332. var self = this
  333. , args = arguments
  334. , root = this._root;
  335. // references
  336. this.res = res;
  337. // decode the path
  338. var path = decode(this.path)
  339. if (path === -1) return this.error(400)
  340. // null byte(s)
  341. if (~path.indexOf('\0')) return this.error(400);
  342. var parts
  343. if (root !== null) {
  344. // join / normalize from optional root dir
  345. path = normalize(join(root, path))
  346. root = normalize(root + sep)
  347. // malicious path
  348. if ((path + sep).substr(0, root.length) !== root) {
  349. debug('malicious path "%s"', path)
  350. return this.error(403)
  351. }
  352. // explode path parts
  353. parts = path.substr(root.length).split(sep)
  354. } else {
  355. // ".." is malicious without "root"
  356. if (upPathRegexp.test(path)) {
  357. debug('malicious path "%s"', path)
  358. return this.error(403)
  359. }
  360. // explode path parts
  361. parts = normalize(path).split(sep)
  362. // resolve the path
  363. path = resolve(path)
  364. }
  365. // dotfile handling
  366. if (containsDotFile(parts)) {
  367. var access = this._dotfiles
  368. // legacy support
  369. if (access === undefined) {
  370. access = parts[parts.length - 1][0] === '.'
  371. ? (this._hidden ? 'allow' : 'ignore')
  372. : 'allow'
  373. }
  374. debug('%s dotfile "%s"', access, path)
  375. switch (access) {
  376. case 'allow':
  377. break
  378. case 'deny':
  379. return this.error(403)
  380. case 'ignore':
  381. default:
  382. return this.error(404)
  383. }
  384. }
  385. // index file support
  386. if (this._index.length && this.path[this.path.length - 1] === '/') {
  387. this.sendIndex(path);
  388. return res;
  389. }
  390. this.sendFile(path);
  391. return res;
  392. };
  393. /**
  394. * Transfer `path`.
  395. *
  396. * @param {String} path
  397. * @api public
  398. */
  399. SendStream.prototype.send = function(path, stat){
  400. var options = this.options;
  401. var len = stat.size;
  402. var res = this.res;
  403. var req = this.req;
  404. var ranges = req.headers.range;
  405. var offset = options.start || 0;
  406. if (res._header) {
  407. // impossible to send now
  408. return this.headersAlreadySent();
  409. }
  410. debug('pipe "%s"', path)
  411. // set header fields
  412. this.setHeader(path, stat);
  413. // set content-type
  414. this.type(path);
  415. // conditional GET support
  416. if (this.isConditionalGET()
  417. && this.isCachable()
  418. && this.isFresh()) {
  419. return this.notModified();
  420. }
  421. // adjust len to start/end options
  422. len = Math.max(0, len - offset);
  423. if (options.end !== undefined) {
  424. var bytes = options.end - offset + 1;
  425. if (len > bytes) len = bytes;
  426. }
  427. // Range support
  428. if (ranges) {
  429. ranges = parseRange(len, ranges);
  430. // If-Range support
  431. if (!this.isRangeFresh()) {
  432. debug('range stale');
  433. ranges = -2;
  434. }
  435. // unsatisfiable
  436. if (-1 == ranges) {
  437. debug('range unsatisfiable');
  438. res.setHeader('Content-Range', 'bytes */' + stat.size);
  439. return this.error(416);
  440. }
  441. // valid (syntactically invalid/multiple ranges are treated as a regular response)
  442. if (-2 != ranges && ranges.length === 1) {
  443. debug('range %j', ranges);
  444. options.start = offset + ranges[0].start;
  445. options.end = offset + ranges[0].end;
  446. // Content-Range
  447. res.statusCode = 206;
  448. res.setHeader('Content-Range', 'bytes '
  449. + ranges[0].start
  450. + '-'
  451. + ranges[0].end
  452. + '/'
  453. + len);
  454. len = options.end - options.start + 1;
  455. }
  456. }
  457. // content-length
  458. res.setHeader('Content-Length', len);
  459. // HEAD support
  460. if ('HEAD' == req.method) return res.end();
  461. this.stream(path, options);
  462. };
  463. /**
  464. * Transfer file for `path`.
  465. *
  466. * @param {String} path
  467. * @api private
  468. */
  469. SendStream.prototype.sendFile = function sendFile(path) {
  470. var i = 0
  471. var self = this
  472. debug('stat "%s"', path);
  473. fs.stat(path, function onstat(err, stat) {
  474. if (err && err.code === 'ENOENT'
  475. && !extname(path)
  476. && path[path.length - 1] !== sep) {
  477. // not found, check extensions
  478. return next(err)
  479. }
  480. if (err) return self.onStatError(err)
  481. if (stat.isDirectory()) return self.redirect(self.path)
  482. self.emit('file', path, stat)
  483. self.send(path, stat)
  484. })
  485. function next(err) {
  486. if (self._extensions.length <= i) {
  487. return err
  488. ? self.onStatError(err)
  489. : self.error(404)
  490. }
  491. var p = path + '.' + self._extensions[i++]
  492. debug('stat "%s"', p)
  493. fs.stat(p, function (err, stat) {
  494. if (err) return next(err)
  495. if (stat.isDirectory()) return next()
  496. self.emit('file', p, stat)
  497. self.send(p, stat)
  498. })
  499. }
  500. }
  501. /**
  502. * Transfer index for `path`.
  503. *
  504. * @param {String} path
  505. * @api private
  506. */
  507. SendStream.prototype.sendIndex = function sendIndex(path){
  508. var i = -1;
  509. var self = this;
  510. function next(err){
  511. if (++i >= self._index.length) {
  512. if (err) return self.onStatError(err);
  513. return self.error(404);
  514. }
  515. var p = join(path, self._index[i]);
  516. debug('stat "%s"', p);
  517. fs.stat(p, function(err, stat){
  518. if (err) return next(err);
  519. if (stat.isDirectory()) return next();
  520. self.emit('file', p, stat);
  521. self.send(p, stat);
  522. });
  523. }
  524. next();
  525. };
  526. /**
  527. * Stream `path` to the response.
  528. *
  529. * @param {String} path
  530. * @param {Object} options
  531. * @api private
  532. */
  533. SendStream.prototype.stream = function(path, options){
  534. // TODO: this is all lame, refactor meeee
  535. var finished = false;
  536. var self = this;
  537. var res = this.res;
  538. var req = this.req;
  539. // pipe
  540. var stream = fs.createReadStream(path, options);
  541. this.emit('stream', stream);
  542. stream.pipe(res);
  543. // response finished, done with the fd
  544. onFinished(res, function onfinished(){
  545. finished = true;
  546. destroy(stream);
  547. });
  548. // error handling code-smell
  549. stream.on('error', function onerror(err){
  550. // request already finished
  551. if (finished) return;
  552. // clean up stream
  553. finished = true;
  554. destroy(stream);
  555. // error
  556. self.onStatError(err);
  557. });
  558. // end
  559. stream.on('end', function onend(){
  560. self.emit('end');
  561. });
  562. };
  563. /**
  564. * Set content-type based on `path`
  565. * if it hasn't been explicitly set.
  566. *
  567. * @param {String} path
  568. * @api private
  569. */
  570. SendStream.prototype.type = function(path){
  571. var res = this.res;
  572. if (res.getHeader('Content-Type')) return;
  573. var type = mime.lookup(path);
  574. var charset = mime.charsets.lookup(type);
  575. debug('content-type %s', type);
  576. res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
  577. };
  578. /**
  579. * Set response header fields, most
  580. * fields may be pre-defined.
  581. *
  582. * @param {String} path
  583. * @param {Object} stat
  584. * @api private
  585. */
  586. SendStream.prototype.setHeader = function setHeader(path, stat){
  587. var res = this.res;
  588. this.emit('headers', res, path, stat);
  589. if (!res.getHeader('Accept-Ranges')) res.setHeader('Accept-Ranges', 'bytes');
  590. if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString());
  591. if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + Math.floor(this._maxage / 1000));
  592. if (this._lastModified && !res.getHeader('Last-Modified')) {
  593. var modified = stat.mtime.toUTCString()
  594. debug('modified %s', modified)
  595. res.setHeader('Last-Modified', modified)
  596. }
  597. if (this._etag && !res.getHeader('ETag')) {
  598. var val = etag(stat)
  599. debug('etag %s', val)
  600. res.setHeader('ETag', val)
  601. }
  602. };
  603. /**
  604. * Determine if path parts contain a dotfile.
  605. *
  606. * @api private
  607. */
  608. function containsDotFile(parts) {
  609. for (var i = 0; i < parts.length; i++) {
  610. if (parts[i][0] === '.') {
  611. return true
  612. }
  613. }
  614. return false
  615. }
  616. /**
  617. * decodeURIComponent.
  618. *
  619. * Allows V8 to only deoptimize this fn instead of all
  620. * of send().
  621. *
  622. * @param {String} path
  623. * @api private
  624. */
  625. function decode(path) {
  626. try {
  627. return decodeURIComponent(path)
  628. } catch (err) {
  629. return -1
  630. }
  631. }
  632. /**
  633. * Normalize the index option into an array.
  634. *
  635. * @param {boolean|string|array} val
  636. * @api private
  637. */
  638. function normalizeList(val){
  639. return [].concat(val || [])
  640. }