PageRenderTime 30ms CodeModel.GetById 15ms app.highlight 11ms RepoModel.GetById 0ms app.codeStats 1ms

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