Socket
Socket
Sign inDemoInstall

send

Package Overview
Dependencies
12
Maintainers
2
Versions
62
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.8.5 to 0.9.0

LICENSE

7

History.md

@@ -0,1 +1,8 @@

0.9.0 / 2014-09-07
==================
* Add `lastMofified` option
* Use `etag` to generate `ETag` header
* deps: debug@~2.0.0
0.8.5 / 2014-09-04

@@ -2,0 +9,0 @@ ==================

773

index.js
module.exports = require('./lib/send');
/**
* Module dependencies.
*/
var debug = require('debug')('send')
var deprecate = require('depd')('send')
var destroy = require('destroy')
var escapeHtml = require('escape-html')
, parseRange = require('range-parser')
, Stream = require('stream')
, mime = require('mime')
, fresh = require('fresh')
, path = require('path')
, http = require('http')
, fs = require('fs')
, normalize = path.normalize
, join = path.join
var etag = require('etag')
var EventEmitter = require('events').EventEmitter;
var ms = require('ms');
var onFinished = require('on-finished')
/**
* Variables.
*/
var extname = path.extname
var maxMaxAge = 60 * 60 * 24 * 365 * 1000; // 1 year
var resolve = path.resolve
var sep = path.sep
var toString = Object.prototype.toString
var upPathRegexp = /(?:^|[\\\/])\.\.(?:[\\\/]|$)/
/**
* Expose `send`.
*/
exports = module.exports = send;
/**
* Expose mime module.
*/
exports.mime = mime;
/**
* Shim EventEmitter.listenerCount for node.js < 0.10
*/
/* istanbul ignore next */
var listenerCount = EventEmitter.listenerCount
|| function(emitter, type){ return emitter.listeners(type).length; };
/**
* Return a `SendStream` for `req` and `path`.
*
* @param {Request} req
* @param {String} path
* @param {Object} options
* @return {SendStream}
* @api public
*/
function send(req, path, options) {
return new SendStream(req, path, options);
}
/**
* Initialize a `SendStream` with the given `path`.
*
* @param {Request} req
* @param {String} path
* @param {Object} options
* @api private
*/
function SendStream(req, path, options) {
var self = this;
options = options || {};
this.req = req;
this.path = path;
this.options = options;
this._etag = options.etag !== undefined
? Boolean(options.etag)
: true
this._dotfiles = options.dotfiles !== undefined
? options.dotfiles
: 'ignore'
if (['allow', 'deny', 'ignore'].indexOf(this._dotfiles) === -1) {
throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"')
}
this._hidden = Boolean(options.hidden)
if ('hidden' in options) {
deprecate('hidden: use dotfiles: \'' + (this._hidden ? 'allow' : 'ignore') + '\' instead')
}
// legacy support
if (!('dotfiles' in options)) {
this._dotfiles = undefined
}
this._extensions = options.extensions !== undefined
? normalizeList(options.extensions)
: []
this._index = options.index !== undefined
? normalizeList(options.index)
: ['index.html']
this._lastModified = options.lastModified !== undefined
? Boolean(options.lastModified)
: true
this._maxage = options.maxAge || options.maxage
this._maxage = typeof this._maxage === 'string'
? ms(this._maxage)
: Number(this._maxage)
this._maxage = !isNaN(this._maxage)
? Math.min(Math.max(0, this._maxage), maxMaxAge)
: 0
this._root = options.root
? resolve(options.root)
: null
if (!this._root && options.from) {
this.from(options.from);
}
}
/**
* Inherits from `Stream.prototype`.
*/
SendStream.prototype.__proto__ = Stream.prototype;
/**
* Enable or disable etag generation.
*
* @param {Boolean} val
* @return {SendStream}
* @api public
*/
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');
/**
* Enable or disable "hidden" (dot) files.
*
* @param {Boolean} path
* @return {SendStream}
* @api public
*/
SendStream.prototype.hidden = deprecate.function(function hidden(val) {
val = Boolean(val);
debug('hidden %s', val);
this._hidden = val;
this._dotfiles = undefined
return this;
}, 'send.hidden: use dotfiles option');
/**
* Set index `paths`, set to a falsy
* value to disable index support.
*
* @param {String|Boolean|Array} paths
* @return {SendStream}
* @api public
*/
SendStream.prototype.index = deprecate.function(function index(paths) {
var index = !paths ? [] : normalizeList(paths);
debug('index %o', paths);
this._index = index;
return this;
}, 'send.index: pass index as option');
/**
* Set root `path`.
*
* @param {String} path
* @return {SendStream}
* @api public
*/
SendStream.prototype.root = function(path){
path = String(path);
this._root = resolve(path)
return this;
};
SendStream.prototype.from = deprecate.function(SendStream.prototype.root,
'send.from: pass root as option');
SendStream.prototype.root = deprecate.function(SendStream.prototype.root,
'send.root: pass root as option');
/**
* Set max-age to `maxAge`.
*
* @param {Number} maxAge
* @return {SendStream}
* @api public
*/
SendStream.prototype.maxage = deprecate.function(function maxage(maxAge) {
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');
/**
* Emit error with `status`.
*
* @param {Number} status
* @api private
*/
SendStream.prototype.error = function(status, err){
var res = this.res;
var msg = http.STATUS_CODES[status];
err = err || new Error(msg);
err.status = status;
// emit if listeners instead of responding
if (listenerCount(this, 'error') !== 0) {
return this.emit('error', err);
}
// wipe all existing headers
res._headers = undefined;
res.statusCode = err.status;
res.end(msg);
};
/**
* Check if the pathname ends with "/".
*
* @return {Boolean}
* @api private
*/
SendStream.prototype.hasTrailingSlash = function(){
return '/' == this.path[this.path.length - 1];
};
/**
* Check if this is a conditional GET request.
*
* @return {Boolean}
* @api private
*/
SendStream.prototype.isConditionalGET = function(){
return this.req.headers['if-none-match']
|| this.req.headers['if-modified-since'];
};
/**
* Strip content-* header fields.
*
* @api private
*/
SendStream.prototype.removeContentHeaderFields = function(){
var res = this.res;
Object.keys(res._headers).forEach(function(field){
if (0 == field.indexOf('content')) {
res.removeHeader(field);
}
});
};
/**
* Respond with 304 not modified.
*
* @api private
*/
SendStream.prototype.notModified = function(){
var res = this.res;
debug('not modified');
this.removeContentHeaderFields();
res.statusCode = 304;
res.end();
};
/**
* Raise error that headers already sent.
*
* @api private
*/
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);
};
/**
* Check if the request is cacheable, aka
* responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}).
*
* @return {Boolean}
* @api private
*/
SendStream.prototype.isCachable = function(){
var res = this.res;
return (res.statusCode >= 200 && res.statusCode < 300) || 304 == res.statusCode;
};
/**
* Handle stat() error.
*
* @param {Error} err
* @api private
*/
SendStream.prototype.onStatError = function(err){
var notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'];
if (~notfound.indexOf(err.code)) return this.error(404, err);
this.error(500, err);
};
/**
* Check if the cache is fresh.
*
* @return {Boolean}
* @api private
*/
SendStream.prototype.isFresh = function(){
return fresh(this.req.headers, this.res._headers);
};
/**
* Check if the range is fresh.
*
* @return {Boolean}
* @api private
*/
SendStream.prototype.isRangeFresh = function isRangeFresh(){
var ifRange = this.req.headers['if-range'];
if (!ifRange) return true;
return ~ifRange.indexOf('"')
? ~ifRange.indexOf(this.res._headers['etag'])
: Date.parse(this.res._headers['last-modified']) <= Date.parse(ifRange);
};
/**
* Redirect to `path`.
*
* @param {String} path
* @api private
*/
SendStream.prototype.redirect = function(path){
if (listenerCount(this, 'directory') !== 0) {
return this.emit('directory');
}
if (this.hasTrailingSlash()) return this.error(403);
var res = this.res;
path += '/';
res.statusCode = 301;
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.setHeader('Location', path);
res.end('Redirecting to <a href="' + escapeHtml(path) + '">' + escapeHtml(path) + '</a>\n');
};
/**
* Pipe to `res.
*
* @param {Stream} res
* @return {Stream} res
* @api public
*/
SendStream.prototype.pipe = function(res){
var self = this
, args = arguments
, root = this._root;
// references
this.res = res;
// decode the path
var path = decode(this.path)
if (path === -1) return this.error(400)
// null byte(s)
if (~path.indexOf('\0')) return this.error(400);
var parts
if (root !== null) {
// join / normalize from optional root dir
path = normalize(join(root, path))
root = normalize(root + sep)
// malicious path
if ((path + sep).substr(0, root.length) !== root) {
debug('malicious path "%s"', path)
return this.error(403)
}
// explode path parts
parts = path.substr(root.length).split(sep)
} else {
// ".." is malicious without "root"
if (upPathRegexp.test(path)) {
debug('malicious path "%s"', path)
return this.error(403)
}
// explode path parts
parts = normalize(path).split(sep)
// resolve the path
path = resolve(path)
}
// dotfile handling
if (containsDotFile(parts)) {
var access = this._dotfiles
// legacy support
if (access === undefined) {
access = parts[parts.length - 1][0] === '.'
? (this._hidden ? 'allow' : 'ignore')
: 'allow'
}
debug('%s dotfile "%s"', access, path)
switch (access) {
case 'allow':
break
case 'deny':
return this.error(403)
case 'ignore':
default:
return this.error(404)
}
}
// index file support
if (this._index.length && this.path[this.path.length - 1] === '/') {
this.sendIndex(path);
return res;
}
this.sendFile(path);
return res;
};
/**
* Transfer `path`.
*
* @param {String} path
* @api public
*/
SendStream.prototype.send = function(path, stat){
var options = this.options;
var len = stat.size;
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();
}
debug('pipe "%s"', path)
// set header fields
this.setHeader(path, stat);
// set content-type
this.type(path);
// conditional GET support
if (this.isConditionalGET()
&& this.isCachable()
&& this.isFresh()) {
return this.notModified();
}
// adjust len to start/end options
len = Math.max(0, len - offset);
if (options.end !== undefined) {
var bytes = options.end - offset + 1;
if (len > bytes) len = bytes;
}
// Range support
if (ranges) {
ranges = parseRange(len, ranges);
// If-Range support
if (!this.isRangeFresh()) {
debug('range stale');
ranges = -2;
}
// unsatisfiable
if (-1 == ranges) {
debug('range unsatisfiable');
res.setHeader('Content-Range', 'bytes */' + stat.size);
return this.error(416);
}
// valid (syntactically invalid/multiple ranges are treated as a regular response)
if (-2 != ranges && ranges.length === 1) {
debug('range %j', ranges);
options.start = offset + ranges[0].start;
options.end = offset + ranges[0].end;
// Content-Range
res.statusCode = 206;
res.setHeader('Content-Range', 'bytes '
+ ranges[0].start
+ '-'
+ ranges[0].end
+ '/'
+ len);
len = options.end - options.start + 1;
}
}
// content-length
res.setHeader('Content-Length', len);
// HEAD support
if ('HEAD' == req.method) return res.end();
this.stream(path, options);
};
/**
* Transfer file for `path`.
*
* @param {String} path
* @api private
*/
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) {
// not found, check extensions
return next(err)
}
if (err) return self.onStatError(err)
if (stat.isDirectory()) return self.redirect(self.path)
self.emit('file', path, stat)
self.send(path, stat)
})
function next(err) {
if (self._extensions.length <= i) {
return err
? self.onStatError(err)
: self.error(404)
}
var p = path + '.' + self._extensions[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)
})
}
}
/**
* Transfer index for `path`.
*
* @param {String} path
* @api private
*/
SendStream.prototype.sendIndex = function sendIndex(path){
var i = -1;
var self = this;
function next(err){
if (++i >= self._index.length) {
if (err) return self.onStatError(err);
return self.error(404);
}
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);
});
}
next();
};
/**
* Stream `path` to the response.
*
* @param {String} path
* @param {Object} options
* @api private
*/
SendStream.prototype.stream = function(path, options){
// TODO: this is all lame, refactor meeee
var finished = false;
var self = this;
var res = this.res;
var req = this.req;
// pipe
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);
});
// error handling code-smell
stream.on('error', function onerror(err){
// request already finished
if (finished) return;
// clean up stream
finished = true;
destroy(stream);
// error
self.onStatError(err);
});
// end
stream.on('end', function onend(){
self.emit('end');
});
};
/**
* Set content-type based on `path`
* if it hasn't been explicitly set.
*
* @param {String} path
* @api private
*/
SendStream.prototype.type = function(path){
var res = this.res;
if (res.getHeader('Content-Type')) return;
var type = mime.lookup(path);
var charset = mime.charsets.lookup(type);
debug('content-type %s', type);
res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
};
/**
* Set response header fields, most
* fields may be pre-defined.
*
* @param {String} path
* @param {Object} stat
* @api private
*/
SendStream.prototype.setHeader = function setHeader(path, stat){
var res = this.res;
this.emit('headers', res, path, stat);
if (!res.getHeader('Accept-Ranges')) res.setHeader('Accept-Ranges', 'bytes');
if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString());
if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + Math.floor(this._maxage / 1000));
if (this._lastModified && !res.getHeader('Last-Modified')) {
var modified = stat.mtime.toUTCString()
debug('modified %s', modified)
res.setHeader('Last-Modified', modified)
}
if (this._etag && !res.getHeader('ETag')) {
var val = etag(stat)
debug('etag %s', val)
res.setHeader('ETag', val)
}
};
/**
* Determine if path parts contain a dotfile.
*
* @api private
*/
function containsDotFile(parts) {
for (var i = 0; i < parts.length; i++) {
if (parts[i][0] === '.') {
return true
}
}
return false
}
/**
* decodeURIComponent.
*
* Allows V8 to only deoptimize this fn instead of all
* of send().
*
* @param {String} path
* @api private
*/
function decode(path) {
try {
return decodeURIComponent(path)
} catch (err) {
return -1
}
}
/**
* Normalize the index option into an array.
*
* @param {boolean|string|array} val
* @api private
*/
function normalizeList(val){
return [].concat(val || [])
}

10

package.json
{
"name": "send",
"description": "Better streaming static file server with Range and conditional-GET support",
"version": "0.8.5",
"version": "0.9.0",
"author": "TJ Holowaychuk <tj@vision-media.ca>",

@@ -17,6 +17,7 @@ "contributors": [

"dependencies": {
"debug": "1.0.4",
"debug": "~2.0.0",
"depd": "0.4.4",
"destroy": "1.0.3",
"escape-html": "1.0.1",
"etag": "~1.3.0",
"fresh": "0.2.2",

@@ -34,2 +35,7 @@ "mime": "1.2.11",

},
"files": [
"History.md",
"LICENSE",
"index.js"
],
"engines": {

@@ -36,0 +42,0 @@ "node": ">= 0.8.0"

# send
[![NPM version](https://badge.fury.io/js/send.svg)](https://badge.fury.io/js/send)
[![Build Status](https://travis-ci.org/visionmedia/send.svg?branch=master)](https://travis-ci.org/visionmedia/send)
[![Coverage Status](https://img.shields.io/coveralls/visionmedia/send.svg?branch=master)](https://coveralls.io/r/visionmedia/send)
[![Gittip](http://img.shields.io/gittip/dougwilson.svg)](https://www.gittip.com/dougwilson/)
[![NPM Version][npm-image]][npm-url]
[![NPM Downloads][downloads-image]][downloads-url]
[![Build Status][travis-image]][travis-url]
[![Test Coverage][coveralls-image]][coveralls-url]
[![Gittip][gittip-image]][gittip-url]

@@ -13,64 +14,22 @@ Send is Connect's `static()` extracted for generalized use, a streaming static file

$ npm install send
## Examples
Small:
```js
var http = require('http');
var send = require('send');
var app = http.createServer(function(req, res){
send(req, req.url).pipe(res);
}).listen(3000);
```bash
$ npm install send
```
Serving from a root directory with custom error-handling:
## API
```js
var http = require('http');
var send = require('send');
var url = require('url');
var app = http.createServer(function(req, res){
// your custom error-handling logic:
function error(err) {
res.statusCode = err.status || 500;
res.end(err.message);
}
// your custom headers
function headers(res, path, stat) {
// serve all files for download
res.setHeader('Content-Disposition', 'attachment');
}
// your custom directory handling logic:
function redirect() {
res.statusCode = 301;
res.setHeader('Location', req.url + '/');
res.end('Redirecting to ' + req.url + '/');
}
// transfer arbitrary files from within
// /www/example.com/public/*
send(req, url.parse(req.url).pathname, {root: '/www/example.com/public'})
.on('error', error)
.on('directory', redirect)
.on('headers', headers)
.pipe(res);
}).listen(3000);
var send = require('send')
```
## API
### send(req, path, [options])
### Options
Create a new `SendStream` for the given path to send to a `res`. The `req` is
the Node.js HTTP request and the `path` is a urlencoded path to send (urlencoded,
not the actual file-system path).
#### etag
#### Options
Enable or disable etag generation, defaults to true.
##### dotfiles
#### dotfiles
Set how "dotfiles" are treated when encountered. A dotfile is a file

@@ -89,4 +48,8 @@ or directory that begins with a dot ("."). Note this check is done on

#### extensions
##### etag
Enable or disable etag generation, defaults to true.
##### extensions
If a given file doesn't exist, try appending one of the given extensions,

@@ -97,3 +60,3 @@ in the given order. By default, this is disabled (set to `false`). An

#### index
##### index

@@ -104,4 +67,9 @@ By default send supports "index.html" files, to disable this

#### maxAge
##### lastModified
Enable or disable `Last-Modified` header, defaults to true. Uses the file
system's last modified value.
##### maxAge
Provide a max-age in milliseconds for http caching, defaults to 0.

@@ -111,3 +79,3 @@ This can also be a string accepted by the

#### root
##### root

@@ -118,2 +86,4 @@ Serve files relative to `path`.

The `SendStream` is an event emitter and will emeit the following events:
- `error` an error occurred `(err)`

@@ -126,2 +96,7 @@ - `directory` a directory was requested

### .pipe
The `pipe` method is used to pipe the response into the Node.js HTTP response
object, typically `send(req, path, options).pipe(res)`.
## Error-handling

@@ -151,25 +126,65 @@

## License
## Examples
(The MIT License)
Small:
Copyright (c) 2012 TJ Holowaychuk &lt;tj@vision-media.ca&gt;
```js
var http = require('http');
var send = require('send');
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
var app = http.createServer(function(req, res){
send(req, req.url).pipe(res);
}).listen(3000);
```
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
Serving from a root directory with custom error-handling:
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```js
var http = require('http');
var send = require('send');
var url = require('url');
var app = http.createServer(function(req, res){
// your custom error-handling logic:
function error(err) {
res.statusCode = err.status || 500;
res.end(err.message);
}
// your custom headers
function headers(res, path, stat) {
// serve all files for download
res.setHeader('Content-Disposition', 'attachment');
}
// your custom directory handling logic:
function redirect() {
res.statusCode = 301;
res.setHeader('Location', req.url + '/');
res.end('Redirecting to ' + req.url + '/');
}
// transfer arbitrary files from within
// /www/example.com/public/*
send(req, url.parse(req.url).pathname, {root: '/www/example.com/public'})
.on('error', error)
.on('directory', redirect)
.on('headers', headers)
.pipe(res);
}).listen(3000);
```
## License
[MIT](LICENSE)
[npm-image]: https://img.shields.io/npm/v/send.svg?style=flat
[npm-url]: https://npmjs.org/package/send
[travis-image]: https://img.shields.io/travis/visionmedia/send.svg?style=flat
[travis-url]: https://travis-ci.org/visionmedia/send
[coveralls-image]: https://img.shields.io/coveralls/visionmedia/send.svg?style=flat
[coveralls-url]: https://coveralls.io/r/visionmedia/send?branch=master
[downloads-image]: https://img.shields.io/npm/dm/send.svg?style=flat
[downloads-url]: https://npmjs.org/package/send
[gittip-image]: https://img.shields.io/gittip/dougwilson.svg?style=flat
[gittip-url]: https://www.gittip.com/dougwilson/
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