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.

473 lines
9.5 KiB

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