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