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