Socket
Socket
Sign inDemoInstall

send

Package Overview
Dependencies
15
Maintainers
1
Versions
62
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.13.2 to 0.14.0

25

HISTORY.md

@@ -0,1 +1,26 @@

0.14.0 / 2016-06-06
===================
* Add `acceptRanges` option
* Add `cacheControl` option
* Attempt to combine multiple ranges into single range
* Correctly inherit from `Stream` class
* Fix `Content-Range` header in 416 responses when using `start`/`end` options
* Fix `Content-Range` header missing from default 416 responses
* Ignore non-byte `Range` headers
* deps: http-errors@~1.5.0
- Add `HttpError` export, for `err instanceof createError.HttpError`
- Support new code `421 Misdirected Request`
- Use `setprototypeof` module to replace `__proto__` setting
- deps: inherits@2.0.1
- deps: statuses@'>= 1.3.0 < 2'
- perf: enable strict mode
* deps: range-parser@~1.2.0
- Fix incorrectly returning -1 when there is at least one valid range
- perf: remove internal function
* deps: statuses@~1.3.0
- Add `421 Misdirected Request`
- perf: enable strict mode
* perf: remove argument reassignment
0.13.2 / 2016-03-05

@@ -2,0 +27,0 @@ ===================

528

index.js
/*!
* send
* Copyright(c) 2012 TJ Holowaychuk
* Copyright(c) 2014-2015 Douglas Christopher Wilson
* Copyright(c) 2014-2016 Douglas Christopher Wilson
* MIT Licensed

@@ -20,27 +20,48 @@ */

var escapeHtml = require('escape-html')
, parseRange = require('range-parser')
, Stream = require('stream')
, mime = require('mime')
, fresh = require('fresh')
, path = require('path')
, fs = require('fs')
, normalize = path.normalize
, join = path.join
var etag = require('etag')
var EventEmitter = require('events').EventEmitter;
var ms = require('ms');
var EventEmitter = require('events').EventEmitter
var fresh = require('fresh')
var fs = require('fs')
var mime = require('mime')
var ms = require('ms')
var onFinished = require('on-finished')
var parseRange = require('range-parser')
var path = require('path')
var statuses = require('statuses')
var Stream = require('stream')
var util = require('util')
/**
* Variables.
* Path function references.
* @private
*/
var extname = path.extname
var maxMaxAge = 60 * 60 * 24 * 365 * 1000; // 1 year
var join = path.join
var normalize = path.normalize
var resolve = path.resolve
var sep = path.sep
var toString = Object.prototype.toString
var upPathRegexp = /(?:^|[\\\/])\.\.(?:[\\\/]|$)/
/**
* Regular expression for identifying a bytes Range header.
* @private
*/
var BYTES_RANGE_REGEXP = /^ *bytes=/
/**
* Maximum value allowed for the max age.
* @private
*/
var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000 // 1 year
/**
* Regular expression to match a path with a directory up component.
* @private
*/
var UP_PATH_REGEXP = /(?:^|[\\\/])\.\.(?:[\\\/]|$)/
/**
* Module exports.

@@ -58,4 +79,4 @@ * @public

/* istanbul ignore next */
var listenerCount = EventEmitter.listenerCount
|| function(emitter, type){ return emitter.listeners(type).length; };
var listenerCount = EventEmitter.listenerCount ||
function (emitter, type) { return emitter.listeners(type).length }

@@ -72,4 +93,4 @@ /**

function send(req, path, options) {
return new SendStream(req, path, options);
function send (req, path, options) {
return new SendStream(req, path, options)
}

@@ -86,3 +107,5 @@

function SendStream(req, path, options) {
function SendStream (req, path, options) {
Stream.call(this)
var opts = options || {}

@@ -94,2 +117,10 @@

this._acceptRanges = opts.acceptRanges !== undefined
? Boolean(opts.acceptRanges)
: true
this._cacheControl = opts.cacheControl !== undefined
? Boolean(opts.cacheControl)
: true
this._etag = opts.etag !== undefined

@@ -135,3 +166,3 @@ ? Boolean(opts.etag)

this._maxage = !isNaN(this._maxage)
? Math.min(Math.max(0, this._maxage), maxMaxAge)
? Math.min(Math.max(0, this._maxage), MAX_MAXAGE)
: 0

@@ -149,6 +180,6 @@

/**
* Inherits from `Stream.prototype`.
* Inherits from `Stream`.
*/
SendStream.prototype.__proto__ = Stream.prototype;
util.inherits(SendStream, Stream)

@@ -163,8 +194,7 @@ /**

SendStream.prototype.etag = deprecate.function(function etag(val) {
val = Boolean(val);
debug('etag %s', val);
this._etag = val;
return this;
}, 'send.etag: pass etag as option');
SendStream.prototype.etag = deprecate.function(function etag (val) {
this._etag = Boolean(val)
debug('etag %s', this._etag)
return this
}, 'send.etag: pass etag as option')

@@ -179,9 +209,8 @@ /**

SendStream.prototype.hidden = deprecate.function(function hidden(val) {
val = Boolean(val);
debug('hidden %s', val);
this._hidden = val;
SendStream.prototype.hidden = deprecate.function(function hidden (val) {
this._hidden = Boolean(val)
this._dotfiles = undefined
return this;
}, 'send.hidden: use dotfiles option');
debug('hidden %s', this._hidden)
return this
}, 'send.hidden: use dotfiles option')

@@ -197,8 +226,8 @@ /**

SendStream.prototype.index = deprecate.function(function index(paths) {
var index = !paths ? [] : normalizeList(paths, 'paths argument');
debug('index %o', paths);
this._index = index;
return this;
}, 'send.index: pass index as option');
SendStream.prototype.index = deprecate.function(function index (paths) {
var index = !paths ? [] : normalizeList(paths, 'paths argument')
debug('index %o', paths)
this._index = index
return this
}, 'send.index: pass index as option')

@@ -213,13 +242,13 @@ /**

SendStream.prototype.root = function(path){
path = String(path);
this._root = resolve(path)
return this;
};
SendStream.prototype.root = function root (path) {
this._root = resolve(String(path))
debug('root %s', this._root)
return this
}
SendStream.prototype.from = deprecate.function(SendStream.prototype.root,
'send.from: pass root as option');
'send.from: pass root as option')
SendStream.prototype.root = deprecate.function(SendStream.prototype.root,
'send.root: pass root as option');
'send.root: pass root as option')

@@ -234,12 +263,12 @@ /**

SendStream.prototype.maxage = deprecate.function(function maxage(maxAge) {
maxAge = typeof maxAge === 'string'
SendStream.prototype.maxage = deprecate.function(function maxage (maxAge) {
this._maxage = typeof maxAge === 'string'
? ms(maxAge)
: Number(maxAge);
if (isNaN(maxAge)) maxAge = 0;
if (Infinity == maxAge) maxAge = 60 * 60 * 24 * 365 * 1000;
debug('max-age %d', maxAge);
this._maxage = maxAge;
return this;
}, 'send.maxage: pass maxAge as option');
: Number(maxAge)
this._maxage = !isNaN(this._maxage)
? Math.min(Math.max(0, this._maxage), MAX_MAXAGE)
: 0
debug('max-age %d', this._maxage)
return this
}, 'send.maxage: pass maxAge as option')

@@ -254,3 +283,3 @@ /**

SendStream.prototype.error = function error(status, error) {
SendStream.prototype.error = function error (status, error) {
// emit if listeners instead of responding

@@ -266,5 +295,10 @@ if (listenerCount(this, 'error') !== 0) {

// wipe all existing headers
res._headers = null
// clear existing headers
clearHeaders(res)
// add error headers
if (error && error.headers) {
setHeaders(res, error.headers)
}
// send basic response

@@ -285,5 +319,5 @@ res.statusCode = status

SendStream.prototype.hasTrailingSlash = function(){
return '/' == this.path[this.path.length - 1];
};
SendStream.prototype.hasTrailingSlash = function hasTrailingSlash () {
return this.path[this.path.length - 1] === '/'
}

@@ -297,6 +331,6 @@ /**

SendStream.prototype.isConditionalGET = function(){
return this.req.headers['if-none-match']
|| this.req.headers['if-modified-since'];
};
SendStream.prototype.isConditionalGET = function isConditionalGET () {
return this.req.headers['if-none-match'] ||
this.req.headers['if-modified-since']
}

@@ -309,3 +343,3 @@ /**

SendStream.prototype.removeContentHeaderFields = function removeContentHeaderFields() {
SendStream.prototype.removeContentHeaderFields = function removeContentHeaderFields () {
var res = this.res

@@ -328,9 +362,9 @@ var headers = Object.keys(res._headers || {})

SendStream.prototype.notModified = function(){
var res = this.res;
debug('not modified');
this.removeContentHeaderFields();
res.statusCode = 304;
res.end();
};
SendStream.prototype.notModified = function notModified () {
var res = this.res
debug('not modified')
this.removeContentHeaderFields()
res.statusCode = 304
res.end()
}

@@ -343,7 +377,7 @@ /**

SendStream.prototype.headersAlreadySent = function headersAlreadySent(){
var err = new Error('Can\'t set headers after they are sent.');
debug('headers already sent');
this.error(500, err);
};
SendStream.prototype.headersAlreadySent = function headersAlreadySent () {
var err = new Error('Can\'t set headers after they are sent.')
debug('headers already sent')
this.error(500, err)
}

@@ -358,6 +392,7 @@ /**

SendStream.prototype.isCachable = function(){
var res = this.res;
return (res.statusCode >= 200 && res.statusCode < 300) || 304 == res.statusCode;
};
SendStream.prototype.isCachable = function isCachable () {
var statusCode = this.res.statusCode
return (statusCode >= 200 && statusCode < 300) ||
statusCode === 304
}

@@ -371,3 +406,3 @@ /**

SendStream.prototype.onStatError = function onStatError(error) {
SendStream.prototype.onStatError = function onStatError (error) {
switch (error.code) {

@@ -392,5 +427,5 @@ case 'ENAMETOOLONG':

SendStream.prototype.isFresh = function(){
return fresh(this.req.headers, this.res._headers);
};
SendStream.prototype.isFresh = function isFresh () {
return fresh(this.req.headers, this.res._headers)
}

@@ -404,11 +439,13 @@ /**

SendStream.prototype.isRangeFresh = function isRangeFresh(){
var ifRange = this.req.headers['if-range'];
SendStream.prototype.isRangeFresh = function isRangeFresh () {
var ifRange = this.req.headers['if-range']
if (!ifRange) return true;
if (!ifRange) {
return true
}
return ~ifRange.indexOf('"')
? ~ifRange.indexOf(this.res._headers['etag'])
: Date.parse(this.res._headers['last-modified']) <= Date.parse(ifRange);
};
: Date.parse(this.res._headers['last-modified']) <= Date.parse(ifRange)
}

@@ -422,3 +459,3 @@ /**

SendStream.prototype.redirect = function redirect(path) {
SendStream.prototype.redirect = function redirect (path) {
if (listenerCount(this, 'directory') !== 0) {

@@ -455,16 +492,21 @@ this.emit('directory')

SendStream.prototype.pipe = function(res){
var self = this
, args = arguments
, root = this._root;
SendStream.prototype.pipe = function pipe (res) {
// root path
var root = this._root
// references
this.res = res;
this.res = res
// decode the path
var path = decode(this.path)
if (path === -1) return this.error(400)
if (path === -1) {
this.error(400)
return res
}
// null byte(s)
if (~path.indexOf('\0')) return this.error(400);
if (~path.indexOf('\0')) {
this.error(400)
return res
}

@@ -474,5 +516,6 @@ var parts

// malicious path
if (upPathRegexp.test(normalize('.' + sep + path))) {
if (UP_PATH_REGEXP.test(normalize('.' + sep + path))) {
debug('malicious path "%s"', path)
return this.error(403)
this.error(403)
return res
}

@@ -488,5 +531,6 @@

// ".." is malicious without "root"
if (upPathRegexp.test(path)) {
if (UP_PATH_REGEXP.test(path)) {
debug('malicious path "%s"', path)
return this.error(403)
this.error(403)
return res
}

@@ -517,6 +561,8 @@

case 'deny':
return this.error(403)
this.error(403)
return res
case 'ignore':
default:
return this.error(404)
this.error(404)
return res
}

@@ -527,9 +573,9 @@ }

if (this._index.length && this.path[this.path.length - 1] === '/') {
this.sendIndex(path);
return res;
this.sendIndex(path)
return res
}
this.sendFile(path);
return res;
};
this.sendFile(path)
return res
}

@@ -543,14 +589,15 @@ /**

SendStream.prototype.send = function(path, stat){
var len = stat.size;
SendStream.prototype.send = function send (path, stat) {
var len = stat.size
var options = this.options
var opts = {}
var res = this.res;
var req = this.req;
var ranges = req.headers.range;
var offset = options.start || 0;
var res = this.res
var req = this.req
var ranges = req.headers.range
var offset = options.start || 0
if (res._header) {
// impossible to send now
return this.headersAlreadySent();
this.headersAlreadySent()
return
}

@@ -561,53 +608,57 @@

// set header fields
this.setHeader(path, stat);
this.setHeader(path, stat)
// set content-type
this.type(path);
this.type(path)
// conditional GET support
if (this.isConditionalGET()
&& this.isCachable()
&& this.isFresh()) {
return this.notModified();
if (this.isConditionalGET() && this.isCachable() && this.isFresh()) {
this.notModified()
return
}
// adjust len to start/end options
len = Math.max(0, len - offset);
len = Math.max(0, len - offset)
if (options.end !== undefined) {
var bytes = options.end - offset + 1;
if (len > bytes) len = bytes;
var bytes = options.end - offset + 1
if (len > bytes) len = bytes
}
// Range support
if (ranges) {
ranges = parseRange(len, ranges);
if (this._acceptRanges && BYTES_RANGE_REGEXP.test(ranges)) {
// parse
ranges = parseRange(len, ranges, {
combine: true
})
// If-Range support
if (!this.isRangeFresh()) {
debug('range stale');
ranges = -2;
debug('range stale')
ranges = -2
}
// unsatisfiable
if (-1 == ranges) {
debug('range unsatisfiable');
res.setHeader('Content-Range', 'bytes */' + stat.size);
return this.error(416);
if (ranges === -1) {
debug('range unsatisfiable')
// Content-Range
res.setHeader('Content-Range', contentRange('bytes', len))
// 416 Requested Range Not Satisfiable
return this.error(416, {
headers: {'Content-Range': res.getHeader('Content-Range')}
})
}
// valid (syntactically invalid/multiple ranges are treated as a regular response)
if (-2 != ranges && ranges.length === 1) {
debug('range %j', ranges);
if (ranges !== -2 && ranges.length === 1) {
debug('range %j', ranges)
// Content-Range
res.statusCode = 206;
res.setHeader('Content-Range', 'bytes '
+ ranges[0].start
+ '-'
+ ranges[0].end
+ '/'
+ len);
res.statusCode = 206
res.setHeader('Content-Range', contentRange('bytes', len, ranges[0]))
offset += ranges[0].start;
len = ranges[0].end - ranges[0].start + 1;
// adjust for requested range
offset += ranges[0].start
len = ranges[0].end - ranges[0].start + 1
}

@@ -626,9 +677,12 @@ }

// content-length
res.setHeader('Content-Length', len);
res.setHeader('Content-Length', len)
// HEAD support
if ('HEAD' == req.method) return res.end();
if (req.method === 'HEAD') {
res.end()
return
}
this.stream(path, opts)
};
}

@@ -641,11 +695,9 @@ /**

*/
SendStream.prototype.sendFile = function sendFile(path) {
SendStream.prototype.sendFile = function sendFile (path) {
var i = 0
var self = this
debug('stat "%s"', path);
fs.stat(path, function onstat(err, stat) {
if (err && err.code === 'ENOENT'
&& !extname(path)
&& path[path.length - 1] !== sep) {
debug('stat "%s"', path)
fs.stat(path, function onstat (err, stat) {
if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) {
// not found, check extensions

@@ -660,3 +712,3 @@ return next(err)

function next(err) {
function next (err) {
if (self._extensions.length <= i) {

@@ -686,25 +738,25 @@ return err

*/
SendStream.prototype.sendIndex = function sendIndex(path){
var i = -1;
var self = this;
SendStream.prototype.sendIndex = function sendIndex (path) {
var i = -1
var self = this
function next(err){
function next (err) {
if (++i >= self._index.length) {
if (err) return self.onStatError(err);
return self.error(404);
if (err) return self.onStatError(err)
return self.error(404)
}
var p = join(path, self._index[i]);
var p = join(path, self._index[i])
debug('stat "%s"', p);
fs.stat(p, function(err, stat){
if (err) return next(err);
if (stat.isDirectory()) return next();
self.emit('file', p, stat);
self.send(p, stat);
});
debug('stat "%s"', p)
fs.stat(p, function (err, stat) {
if (err) return next(err)
if (stat.isDirectory()) return next()
self.emit('file', p, stat)
self.send(p, stat)
})
}
next();
};
next()
}

@@ -719,38 +771,37 @@ /**

SendStream.prototype.stream = function(path, options){
SendStream.prototype.stream = function stream (path, options) {
// TODO: this is all lame, refactor meeee
var finished = false;
var self = this;
var res = this.res;
var req = this.req;
var finished = false
var self = this
var res = this.res
// pipe
var stream = fs.createReadStream(path, options);
this.emit('stream', stream);
stream.pipe(res);
var stream = fs.createReadStream(path, options)
this.emit('stream', stream)
stream.pipe(res)
// response finished, done with the fd
onFinished(res, function onfinished(){
finished = true;
destroy(stream);
});
onFinished(res, function onfinished () {
finished = true
destroy(stream)
})
// error handling code-smell
stream.on('error', function onerror(err){
stream.on('error', function onerror (err) {
// request already finished
if (finished) return;
if (finished) return
// clean up stream
finished = true;
destroy(stream);
finished = true
destroy(stream)
// error
self.onStatError(err);
});
self.onStatError(err)
})
// end
stream.on('end', function onend(){
self.emit('end');
});
};
stream.on('end', function onend () {
self.emit('end')
})
}

@@ -765,19 +816,19 @@ /**

SendStream.prototype.type = function type(path) {
var res = this.res;
SendStream.prototype.type = function type (path) {
var res = this.res
if (res.getHeader('Content-Type')) return;
if (res.getHeader('Content-Type')) return
var type = mime.lookup(path);
var type = mime.lookup(path)
if (!type) {
debug('no content-type');
return;
debug('no content-type')
return
}
var charset = mime.charsets.lookup(type);
var charset = mime.charsets.lookup(type)
debug('content-type %s', type);
res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
};
debug('content-type %s', type)
res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''))
}

@@ -793,10 +844,18 @@ /**

SendStream.prototype.setHeader = function setHeader(path, stat){
var res = this.res;
SendStream.prototype.setHeader = function setHeader (path, stat) {
var res = this.res
this.emit('headers', res, path, stat);
this.emit('headers', res, path, stat)
if (!res.getHeader('Accept-Ranges')) res.setHeader('Accept-Ranges', 'bytes');
if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + Math.floor(this._maxage / 1000));
if (this._acceptRanges && !res.getHeader('Accept-Ranges')) {
debug('accept ranges')
res.setHeader('Accept-Ranges', 'bytes')
}
if (this._cacheControl && !res.getHeader('Cache-Control')) {
var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000)
debug('cache-control %s', cacheControl)
res.setHeader('Cache-Control', cacheControl)
}
if (this._lastModified && !res.getHeader('Last-Modified')) {

@@ -813,5 +872,17 @@ var modified = stat.mtime.toUTCString()

}
};
}
/**
* Clear all headers from a response.
*
* @param {object} res
* @private
*/
function clearHeaders (res) {
res._headers = {}
res._headerNames = {}
}
/**
* Determine if path parts contain a dotfile.

@@ -822,3 +893,3 @@ *

function containsDotFile(parts) {
function containsDotFile (parts) {
for (var i = 0; i < parts.length; i++) {

@@ -834,2 +905,14 @@ if (parts[i][0] === '.') {

/**
* Create a Content-Range header.
*
* @param {string} type
* @param {number} size
* @param {array} [range]
*/
function contentRange (type, size, range) {
return type + ' ' + (range ? range.start + '-' + range.end : '*') + '/' + size
}
/**
* decodeURIComponent.

@@ -844,3 +927,3 @@ *

function decode(path) {
function decode (path) {
try {

@@ -861,3 +944,3 @@ return decodeURIComponent(path)

function normalizeList(val, name) {
function normalizeList (val, name) {
var list = [].concat(val || [])

@@ -873,1 +956,18 @@

}
/**
* Set an object of headers on a response.
*
* @param {object} res
* @param {object} headers
* @private
*/
function setHeaders (res, headers) {
var keys = Object.keys(headers)
for (var i = 0; i < keys.length; i++) {
var key = keys[i]
res.setHeader(key, headers[key])
}
}
{
"name": "send",
"description": "Better streaming static file server with Range and conditional-GET support",
"version": "0.13.2",
"version": "0.14.0",
"author": "TJ Holowaychuk <tj@vision-media.ca>",

@@ -23,13 +23,17 @@ "contributors": [

"fresh": "0.3.0",
"http-errors": "~1.3.1",
"http-errors": "~1.5.0",
"mime": "1.3.4",
"ms": "0.7.1",
"on-finished": "~2.3.0",
"range-parser": "~1.0.3",
"statuses": "~1.2.1"
"range-parser": "~1.2.0",
"statuses": "~1.3.0"
},
"devDependencies": {
"after": "0.8.1",
"istanbul": "0.4.2",
"mocha": "2.4.5",
"eslint": "2.11.1",
"eslint-config-standard": "5.3.1",
"eslint-plugin-promise": "1.3.1",
"eslint-plugin-standard": "1.3.2",
"istanbul": "0.4.3",
"mocha": "2.5.3",
"supertest": "1.1.0"

@@ -47,2 +51,3 @@ },

"scripts": {
"lint": "eslint **/*.js",
"test": "mocha --check-leaks --reporter spec --bail",

@@ -49,0 +54,0 @@ "test-ci": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --check-leaks --reporter spec",

@@ -37,2 +37,13 @@ # send

##### acceptRanges
Enable or disable accepting ranged requests, defaults to true.
Disabling this will not send `Accept-Ranges` and ignore the contents
of the `Range` request header.
##### cacheControl
Enable or disable setting `Cache-Control` response header, defaults to
true. Disabling this will ignore the `maxAge` option.
##### dotfiles

@@ -39,0 +50,0 @@

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc