send
Advanced tools
Comparing version 0.2.0 to 0.3.0
0.3.0 / 2014-04-24 | ||
================== | ||
* Fix sending files with dots without root set | ||
* Coerce option types | ||
* Accept API options in options object | ||
* Set etags to "weak" | ||
* Include file path in etag | ||
* Make "Can't set headers after they are sent." catchable | ||
* Send full entity-body for multi range requests | ||
* Default directory access to 403 when index disabled | ||
* Support multiple index paths | ||
* Support "If-Range" header | ||
* Control whether to generate etags | ||
* deps: mime@1.2.11 | ||
0.2.0 / 2014-01-29 | ||
@@ -3,0 +19,0 @@ ================== |
144
lib/send.js
@@ -19,2 +19,4 @@ | ||
var upPathRegexp = /(?:^|[\\\/])\.\.(?:[\\\/]|$)/; | ||
/** | ||
@@ -64,8 +66,11 @@ * Expose `send`. | ||
var self = this; | ||
options = options || {}; | ||
this.req = req; | ||
this.path = path; | ||
this.options = options || {}; | ||
this.maxage(0); | ||
this.hidden(false); | ||
this.index('index.html'); | ||
this.options = options; | ||
this.etag(('etag' in options) ? options.etag : true); | ||
this.maxage(options.maxage); | ||
this.hidden(options.hidden); | ||
this.index(('index' in options) ? options.index : 'index.html'); | ||
if (options.root || options.from) this.root(options.root || options.from); | ||
} | ||
@@ -80,2 +85,17 @@ | ||
/** | ||
* Enable or disable etag generation. | ||
* | ||
* @param {Boolean} val | ||
* @return {SendStream} | ||
* @api public | ||
*/ | ||
SendStream.prototype.etag = function(val){ | ||
val = Boolean(val); | ||
debug('etag %s', val); | ||
this._etag = val; | ||
return this; | ||
}; | ||
/** | ||
* Enable or disable "hidden" (dot) files. | ||
@@ -89,2 +109,3 @@ * | ||
SendStream.prototype.hidden = function(val){ | ||
val = Boolean(val); | ||
debug('hidden %s', val); | ||
@@ -96,6 +117,6 @@ this._hidden = val; | ||
/** | ||
* Set index `path`, set to a falsy | ||
* Set index `paths`, set to a falsy | ||
* value to disable index support. | ||
* | ||
* @param {String|Boolean} path | ||
* @param {String|Boolean|Array} paths | ||
* @return {SendStream} | ||
@@ -105,5 +126,6 @@ * @api public | ||
SendStream.prototype.index = function(path){ | ||
debug('index %s', path); | ||
this._index = path; | ||
SendStream.prototype.index = function index(paths){ | ||
var index = !paths ? [] : Array.isArray(paths) ? paths : [paths]; | ||
debug('index %j', index); | ||
this._index = index; | ||
return this; | ||
@@ -122,2 +144,3 @@ }; | ||
SendStream.prototype.from = function(path){ | ||
path = String(path); | ||
this._root = normalize(path); | ||
@@ -136,2 +159,4 @@ return this; | ||
SendStream.prototype.maxage = function(ms){ | ||
ms = Number(ms); | ||
if (isNaN(ms)) ms = 0; | ||
if (Infinity == ms) ms = 60 * 60 * 24 * 365 * 1000; | ||
@@ -168,3 +193,3 @@ debug('max-age %d', ms); | ||
SendStream.prototype.isMalicious = function(){ | ||
return !this._root && ~this.path.indexOf('..'); | ||
return !this._root && ~this.path.indexOf('..') && upPathRegexp.test(this.path); | ||
}; | ||
@@ -236,2 +261,14 @@ | ||
/** | ||
* 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 | ||
@@ -274,2 +311,19 @@ * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}). | ||
/** | ||
* 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`. | ||
@@ -283,2 +337,3 @@ * | ||
if (this.listeners('directory').length) return this.emit('directory'); | ||
if (this.hasTrailingSlash()) return this.error(403); | ||
var res = this.res; | ||
@@ -328,3 +383,6 @@ path += '/'; | ||
// index file support | ||
if (this._index && this.hasTrailingSlash()) path += this._index; | ||
if (this._index.length && this.hasTrailingSlash()) { | ||
this.sendIndex(path); | ||
return res; | ||
} | ||
@@ -357,4 +415,9 @@ debug('stat "%s"', path); | ||
if (res._header) { | ||
// impossible to send now | ||
return this.headersAlreadySent(); | ||
} | ||
// set header fields | ||
this.setHeader(stat); | ||
this.setHeader(path, stat); | ||
@@ -382,4 +445,11 @@ // set content-type | ||
// 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); | ||
@@ -389,4 +459,6 @@ return this.error(416); | ||
// valid (syntactically invalid ranges are treated as a regular response) | ||
if (-2 != ranges) { | ||
// 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; | ||
@@ -417,2 +489,34 @@ options.end = offset + ranges[0].end; | ||
/** | ||
* 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.redirect(self.path); | ||
} | ||
var p = path + self._index[i]; | ||
debug('stat "%s"', p); | ||
fs.stat(p, function(err, stat){ | ||
if (err) return next(err); | ||
if (stat.isDirectory()) return self.redirect(self.path); | ||
self.emit('file', p, stat); | ||
self.send(p, stat); | ||
}); | ||
} | ||
if (!this.hasTrailingSlash()) path += '/'; | ||
next(); | ||
}; | ||
/** | ||
* Stream `path` to the response. | ||
@@ -477,5 +581,6 @@ * | ||
/** | ||
* Set reaponse header fields, most | ||
* Set response header fields, most | ||
* fields may be pre-defined. | ||
* | ||
* @param {String} path | ||
* @param {Object} stat | ||
@@ -485,9 +590,14 @@ * @api private | ||
SendStream.prototype.setHeader = function(stat){ | ||
SendStream.prototype.setHeader = function setHeader(path, stat){ | ||
var res = this.res; | ||
if (!res.getHeader('Accept-Ranges')) res.setHeader('Accept-Ranges', 'bytes'); | ||
if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat)); | ||
if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString()); | ||
if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (this._maxage / 1000)); | ||
if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString()); | ||
if (this._etag && !res.getHeader('ETag')) { | ||
var etag = utils.etag(path, stat); | ||
debug('etag %s', etag); | ||
res.setHeader('ETag', etag); | ||
} | ||
}; |
/** | ||
* Return an ETag in the form of `"<size>-<mtime>"` | ||
* from the given `stat`. | ||
* Module dependencies. | ||
*/ | ||
var crc32 = require('buffer-crc32').unsigned; | ||
/** | ||
* Return a weak ETag from the given `path` and `stat`. | ||
* | ||
* @param {String} path | ||
* @param {Object} stat | ||
@@ -11,4 +17,5 @@ * @return {String} | ||
exports.etag = function(stat) { | ||
return '"' + stat.size + '-' + Number(stat.mtime) + '"'; | ||
exports.etag = function etag(path, stat) { | ||
var tag = String(stat.mtime.getTime()) + ':' + String(stat.size) + ':' + path; | ||
return 'W/"' + crc32(tag) + '"'; | ||
}; | ||
@@ -15,0 +22,0 @@ |
{ | ||
"name": "send", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "Better streaming static file server with Range and conditional-GET support", | ||
@@ -12,5 +12,6 @@ "keywords": [ | ||
"dependencies": { | ||
"debug": "*", | ||
"mime": "~1.2.9", | ||
"buffer-crc32": "0.2.1", | ||
"debug": "0.8.0", | ||
"fresh": "~0.2.1", | ||
"mime": "1.2.11", | ||
"range-parser": "~1.0.0" | ||
@@ -21,7 +22,7 @@ }, | ||
"should": "*", | ||
"supertest": "0.0.1", | ||
"supertest": "0.10.0", | ||
"connect": "2.x" | ||
}, | ||
"scripts": { | ||
"test": "make test" | ||
"test": "mocha --require should --reporter spec --bail" | ||
}, | ||
@@ -33,2 +34,2 @@ "repository": { | ||
"main": "index" | ||
} | ||
} |
@@ -46,4 +46,3 @@ # send | ||
// /www/example.com/public/* | ||
send(req, url.parse(req.url).pathname) | ||
.root('/www/example.com/public') | ||
send(req, url.parse(req.url).pathname, {root: '/www/example.com/public'}) | ||
.on('error', error) | ||
@@ -57,2 +56,26 @@ .on('directory', redirect) | ||
### Options | ||
#### etag | ||
Enable or disable etag generation, defaults to true. | ||
#### hidden | ||
Enable or disable transfer of hidden files, defaults to false. | ||
#### index | ||
By default send supports "index.html" files, to disable this | ||
set `false` or to supply a new index pass a string or an array | ||
in preferred order. | ||
#### maxage | ||
Provide a max-age in milliseconds for http caching, defaults to 0. | ||
#### root | ||
Serve files relative to `path`. | ||
### Events | ||
@@ -66,2 +89,6 @@ | ||
### .etag(bool) | ||
Enable or disable etag generation, defaults to true. | ||
### .root(dir) | ||
@@ -71,6 +98,7 @@ | ||
### .index(path) | ||
### .index(paths) | ||
By default send supports "index.html" files, to disable this | ||
invoke `.index(false)` or to supply a new index pass a string. | ||
invoke `.index(false)` or to supply a new index pass a string | ||
or an array in preferred order. | ||
@@ -77,0 +105,0 @@ ### .maxage(ms) |
Sorry, the diff of this file is not supported yet
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Floating dependency
QualityPackage has a dependency with a floating version range. This can cause issues if the dependency publishes a new major version.
Found 1 instance in 1 package
19439
532
0
157
5
7
2
+ Addedbuffer-crc32@0.2.1
+ Addedbuffer-crc32@0.2.1(transitive)
+ Addeddebug@0.8.0(transitive)
- Removeddebug@4.3.5(transitive)
- Removedms@2.1.2(transitive)
Updateddebug@0.8.0
Updatedmime@1.2.11