Comparing version 1.2.2 to 2.0.0
#!/usr/bin/env node | ||
var st = require('../st.js') | ||
var http = require('http') | ||
var port = +(process.env.PORT || 1337) | ||
var host = undefined | ||
var dir = '' | ||
var url = '/' | ||
var cacheSize = 0 | ||
var dot = false | ||
var index = true | ||
var cache = true | ||
var age = null | ||
var cors = false | ||
const st = require('../st.js') | ||
const http = require('http') | ||
let port = +(process.env.PORT || 1337) | ||
let host | ||
let dir = '' | ||
let url = '/' | ||
let dot = false | ||
let index = true | ||
let cache = true | ||
let age = null | ||
let cors = false | ||
for (var i = 2; i < process.argv.length; i++) { | ||
for (let i = 2; i < process.argv.length; i++) { | ||
switch (process.argv[i]) { | ||
@@ -48,5 +47,7 @@ case '-p': | ||
dot = process.argv[++i] | ||
if (dot === undefined || dot === 'true') dot = true | ||
else if (dot === 'false') dot = false | ||
else if (dot.charAt(0) === '-') { | ||
if (dot === undefined || dot === 'true') { | ||
dot = true | ||
} else if (dot === 'false') { | ||
dot = false | ||
} else if (dot.charAt(0) === '-') { | ||
--i | ||
@@ -65,5 +66,7 @@ dot = true | ||
index = process.argv[++i] | ||
if (index === undefined || index === 'true') index = true | ||
if (index === 'false') index = false | ||
if (index.charAt(0) === '-') { | ||
if (index === undefined || index === 'true') { | ||
index = true | ||
} else if (index === 'false') { | ||
index = false | ||
} else if (index.charAt(0) === '-') { | ||
--i | ||
@@ -100,3 +103,3 @@ index = true | ||
case '--cors': | ||
cors = true; | ||
cors = true | ||
break | ||
@@ -108,43 +111,45 @@ } | ||
console.log( | ||
['st' | ||
,'Static file server in node' | ||
,'' | ||
,'Options:' | ||
,'' | ||
,'-h --help Show this help' | ||
,'' | ||
,'-p --port PORT Listen on PORT (default=1337)' | ||
,'' | ||
,'-H --host HOST Bind address HOST (default=*)' | ||
,'' | ||
,'-l --localhost Same as "--host localhost"' | ||
,'' | ||
,'-d --dir DIRECTORY Serve the contents of DIRECTORY (default=cwd)' | ||
,'' | ||
,'-u --url /url Serve at this mount url (default=/)' | ||
,'' | ||
,'-i --index [INDEX] Use the specified INDEX filename as the result' | ||
,' when a directory is requested. Set to "true"' | ||
,' to turn autoindexing on, or "false" to turn it' | ||
,' off. If no INDEX is provided, then it will turn' | ||
,' autoindexing on. (default=true)' | ||
,'' | ||
,'-ni --no-index Same as "--index false"' | ||
,'' | ||
,'-. --dot [DOT] Allow .files to be served. Set to "false" to' | ||
,' disable.' | ||
,'' | ||
,'-n. --no-dot Same as "--dot false"' | ||
,'' | ||
,'-co --cors Enable CORS to serve files to any domain.' | ||
,'' | ||
,'-nc --no-cache Turn off all caching.' | ||
,'' | ||
,'-a --age AGE Max age (in ms) of cache entries.' | ||
].join('\n')) | ||
['st', | ||
'Static file server in node', | ||
'', | ||
'Options:', | ||
'', | ||
'-h --help Show this help', | ||
'', | ||
'-p --port PORT Listen on PORT (default=1337)', | ||
'', | ||
'-H --host HOST Bind address HOST (default=*)', | ||
'', | ||
'-l --localhost Same as "--host localhost"', | ||
'', | ||
'-d --dir DIRECTORY Serve the contents of DIRECTORY (default=cwd)', | ||
'', | ||
'-u --url /url Serve at this mount url (default=/)', | ||
'', | ||
'-i --index [INDEX] Use the specified INDEX filename as the result', | ||
' when a directory is requested. Set to "true"', | ||
' to turn autoindexing on, or "false" to turn it', | ||
' off. If no INDEX is provided, then it will turn', | ||
' autoindexing on. (default=true)', | ||
'', | ||
'-ni --no-index Same as "--index false"', | ||
'', | ||
'-. --dot [DOT] Allow .files to be served. Set to "false" to', | ||
' disable.', | ||
'', | ||
'-n. --no-dot Same as "--dot false"', | ||
'', | ||
'-co --cors Enable CORS to serve files to any domain.', | ||
'', | ||
'-nc --no-cache Turn off all caching.', | ||
'', | ||
'-a --age AGE Max age (in ms) of cache entries.' | ||
].join('\n')) | ||
} | ||
if (isNaN(port)) throw new Error('invalid port: '+port) | ||
if (isNaN(port)) { | ||
throw new Error('invalid port: ' + port) | ||
} | ||
var opt = { | ||
const opt = { | ||
path: dir, | ||
@@ -168,5 +173,5 @@ url: url, | ||
if (age) { | ||
Object.keys(opt.cache).forEach(function (k) { | ||
for (const k in opt.cache) { | ||
opt.cache[k].maxAge = age | ||
}) | ||
} | ||
} | ||
@@ -176,11 +181,14 @@ // maybe other cache-manipulating CLI flags? | ||
var mount = st(opt) | ||
const mount = st(opt) | ||
http.createServer(function (q, s) { | ||
if (mount(q, s)) return | ||
if (mount(q, s)) { | ||
return | ||
} | ||
s.statusCode = 404 | ||
s.end('not found') | ||
}).listen(port, host, function() { | ||
var addr = this.address() | ||
var port = addr.port | ||
}).listen(port, host, function () { | ||
const addr = this.address() | ||
const port = addr.port | ||
if (!host) { | ||
@@ -192,3 +200,4 @@ host = addr.address | ||
} | ||
console.log('listening at http://' + host + ':' + port) | ||
}) |
{ | ||
"name": "st", | ||
"version": "1.2.2", | ||
"version": "2.0.0", | ||
"description": "A module for serving static files. Does etags, caching, etc.", | ||
@@ -8,18 +8,20 @@ "main": "st.js", | ||
"dependencies": { | ||
"async-cache": "~1.1.0", | ||
"bl": "~1.2.1", | ||
"async-cache": "^1.1.0", | ||
"bl": "^4.0.0", | ||
"fd": "~0.0.2", | ||
"mime": "~1.4.1", | ||
"negotiator": "~0.6.1" | ||
"mime": "^2.4.4", | ||
"negotiator": "~0.6.2" | ||
}, | ||
"optionalDependencies": { | ||
"graceful-fs": "~4.1.11" | ||
"graceful-fs": "^4.2.3" | ||
}, | ||
"devDependencies": { | ||
"request": "~2.83.0", | ||
"rimraf": "~2.6.2", | ||
"tap": "~10.7.2" | ||
"request": "^2.88.0", | ||
"rimraf": "^3.0.0", | ||
"standard": "^14.3.1", | ||
"tap": "^14.9.2" | ||
}, | ||
"scripts": { | ||
"test": "tap test/*.js test/cli/*-test.js" | ||
"lint": "standard", | ||
"test": "npm run lint && tap test/*.js test/cli/*-test.js" | ||
}, | ||
@@ -26,0 +28,0 @@ "repository": { |
# st | ||
[![Travis Status](https://api.travis-ci.org/isaacs/st.svg?branch=master)](https://travis-ci.org/isaacs/st) | ||
A module for serving static files. Does etags, caching, etc. | ||
@@ -12,4 +14,4 @@ | ||
```javascript | ||
var st = require('st') | ||
var http = require('http') | ||
const st = require('st') | ||
const http = require('http') | ||
@@ -26,5 +28,7 @@ http.createServer( | ||
```javascript | ||
var mount = st({ path: __dirname + '/static', url: '/static' }) | ||
http.createServer(function(req, res) { | ||
var stHandled = mount(req, res); | ||
const path = require('path') | ||
const mount = st({ path: path.join(__dirname, '/static'), url: '/static' }) | ||
http.createServer((req, res) => { | ||
const stHandled = mount(req, res) | ||
if (stHandled) | ||
@@ -40,7 +44,7 @@ return | ||
```javascript | ||
var mount = st({ path: __dirname + '/static', url: '/static' }) | ||
http.createServer(function(req, res) { | ||
mount(req, res, function() { | ||
res.end('this is not a static file') | ||
}) | ||
const path = require('path') | ||
const mount = st({ path: path.join(__dirname, '/static'), url: '/static' }) | ||
http.createServer((req, res) => { | ||
mount(req, res, () => res.end('this is not a static file')) | ||
}).listen(1339) | ||
@@ -54,4 +58,6 @@ ``` | ||
```javascript | ||
var mount = st({ path: __dirname + '/static', url: '/' }) | ||
http.createServer(function(req, res) { | ||
const path = require('path') | ||
const mount = st({ path: path.join(__dirname, '/static'), url: '/' }) | ||
http.createServer((req, res) => { | ||
if (shouldDoThing(req)) { | ||
@@ -69,8 +75,8 @@ doTheThing(req, res) | ||
```javascript | ||
var mount = st({ path: __dirname + '/static', url: '/', passthrough: true }) | ||
http.createServer(function(req, res) { | ||
mount(req, res, function() { | ||
res.end('this is not a static file'); | ||
}); | ||
}).listen(1341); | ||
const path = require('path') | ||
const mount = st({ path: path.join(__dirname, '/static'), url: '/', passthrough: true}) | ||
http.createServer((req, res) => { | ||
mount(req, res, () => res.end('this is not a static file')) | ||
}).listen(1341) | ||
``` | ||
@@ -114,4 +120,4 @@ | ||
```javascript | ||
var st = require('st') | ||
var mount = st({ | ||
const st = require('st') | ||
const mount = st({ | ||
path: 'resources/static/', // resolved against the process cwd | ||
@@ -169,3 +175,3 @@ url: 'static/', // defaults to '/' | ||
// with bare node.js | ||
http.createServer(function (req, res) { | ||
http.createServer((req, res) => { | ||
if (mount(req, res)) return // serving a static file | ||
@@ -178,3 +184,3 @@ myCustomLogic(req, res) | ||
// or | ||
app.route('/static/:fooblz', function (req, res, next) { | ||
app.route('/static/:fooblz', (req, res, next) => { | ||
mount(req, res, next) // will call next() if it doesn't do anything | ||
@@ -181,0 +187,0 @@ }) |
940
st.js
@@ -1,8 +0,5 @@ | ||
module.exports = st | ||
st.Mount = Mount | ||
var mime = require('mime') | ||
var path = require('path') | ||
var fs | ||
const mime = require('mime') | ||
const path = require('path') | ||
const url = require('url') | ||
let fs | ||
try { | ||
@@ -13,16 +10,13 @@ fs = require('graceful-fs') | ||
} | ||
var url = require('url') | ||
var zlib = require('zlib') | ||
var Neg = require('negotiator') | ||
var http = require('http') | ||
var AC = require('async-cache') | ||
var util = require('util') | ||
var FD = require('fd') | ||
var bl = require('bl') | ||
const zlib = require('zlib') | ||
const Neg = require('negotiator') | ||
const http = require('http') | ||
const AC = require('async-cache') | ||
const FD = require('fd') | ||
const bl = require('bl') | ||
// default caching options | ||
var defaultCacheOptions = { | ||
const defaultCacheOptions = { | ||
fd: { | ||
max: 1000, | ||
maxAge: 1000 * 60 * 60, | ||
maxAge: 1000 * 60 * 60 | ||
}, | ||
@@ -35,5 +29,3 @@ stat: { | ||
max: 1024 * 1024 * 64, | ||
length: function (n) { | ||
return n.length | ||
}, | ||
length: (n) => n.length, | ||
maxAge: 1000 * 60 * 10 | ||
@@ -43,5 +35,3 @@ }, | ||
max: 1024 * 8, | ||
length: function (n) { | ||
return n.length | ||
}, | ||
length: (n) => n.length, | ||
maxAge: 1000 * 60 * 10 | ||
@@ -51,5 +41,3 @@ }, | ||
max: 1000, | ||
length: function (n) { | ||
return n.length | ||
}, | ||
length: (n) => n.length, | ||
maxAge: 1000 * 60 * 10 | ||
@@ -59,4 +47,20 @@ } | ||
// lru-cache doesn't like when max=0, so we just pretend | ||
// everything is really big. kind of a kludge, but easiest way | ||
// to get it done | ||
const none = { | ||
max: 1, | ||
length: () => Infinity | ||
} | ||
const noCaching = { | ||
fd: none, | ||
stat: none, | ||
index: none, | ||
readdir: none, | ||
content: none | ||
} | ||
function st (opt) { | ||
var p, u | ||
let p, u | ||
if (typeof opt === 'string') { | ||
@@ -71,11 +75,24 @@ p = opt | ||
if (!opt) opt = {} | ||
else opt = util._extend({}, opt) | ||
if (!opt) { | ||
opt = {} | ||
} else { | ||
opt = Object.assign({}, opt) | ||
} | ||
if (!p) p = opt.path | ||
if (typeof p !== 'string') throw new Error('no path specified') | ||
if (!p) { | ||
p = opt.path | ||
} | ||
if (typeof p !== 'string') { | ||
throw new Error('no path specified') | ||
} | ||
p = path.resolve(p) | ||
if (!u) u = opt.url | ||
if (!u) u = '' | ||
if (u.charAt(0) !== '/') u = '/' + u | ||
if (!u) { | ||
u = opt.url | ||
} | ||
if (!u) { | ||
u = '' | ||
} | ||
if (u.charAt(0) !== '/') { | ||
u = '/' + u | ||
} | ||
@@ -85,4 +102,4 @@ opt.url = u | ||
var m = new Mount(opt) | ||
var fn = m.serve.bind(m) | ||
const m = new Mount(opt) | ||
const fn = m.serve.bind(m) | ||
fn._this = m | ||
@@ -92,400 +109,418 @@ return fn | ||
function Mount (opt) { | ||
if (!opt) throw new Error('no options provided') | ||
if (typeof opt !== 'object') throw new Error('invalid options') | ||
if (!(this instanceof Mount)) return new Mount(opt) | ||
class Mount { | ||
constructor (opt) { | ||
if (!opt) { | ||
throw new Error('no options provided') | ||
} | ||
if (typeof opt !== 'object') { | ||
throw new Error('invalid options') | ||
} | ||
if (!(this instanceof Mount)) { | ||
return new Mount(opt) | ||
} | ||
this.opt = opt | ||
this.url = opt.url | ||
this.path = opt.path | ||
this._index = opt.index === false ? false | ||
: typeof opt.index === 'string' ? opt.index | ||
: true | ||
this.fdman = FD() | ||
this.opt = opt | ||
this.url = opt.url | ||
this.path = opt.path | ||
this._index = opt.index === false ? false | ||
: typeof opt.index === 'string' ? opt.index | ||
: true | ||
this.fdman = FD() | ||
// cache basically everything | ||
var c = this.getCacheOptions(opt) | ||
this.cache = { | ||
fd: AC(c.fd), | ||
stat: AC(c.stat), | ||
index: AC(c.index), | ||
readdir: AC(c.readdir), | ||
content: AC(c.content) | ||
// cache basically everything | ||
const c = this.getCacheOptions(opt) | ||
this.cache = { | ||
fd: AC(c.fd), | ||
stat: AC(c.stat), | ||
index: AC(c.index), | ||
readdir: AC(c.readdir), | ||
content: AC(c.content) | ||
} | ||
this._cacheControl = | ||
c.content.maxAge === false | ||
? undefined | ||
: typeof c.content.cacheControl === 'string' | ||
? c.content.cacheControl | ||
: opt.cache === false | ||
? 'no-cache' | ||
: 'public, max-age=' + (c.content.maxAge / 1000) | ||
} | ||
this._cacheControl = | ||
c.content.maxAge === false | ||
? undefined | ||
: typeof c.content.cacheControl == 'string' | ||
? c.content.cacheControl | ||
: opt.cache === false | ||
? 'no-cache' | ||
: 'public, max-age=' + (c.content.maxAge / 1000) | ||
} | ||
getCacheOptions (opt) { | ||
let o = opt.cache | ||
const set = (key) => { | ||
return o[key] === false | ||
? Object.assign({}, none) | ||
: Object.assign(Object.assign({}, d[key]), o[key]) | ||
} | ||
// lru-cache doesn't like when max=0, so we just pretend | ||
// everything is really big. kind of a kludge, but easiest way | ||
// to get it done | ||
var none = { max: 1, length: function() { | ||
return Infinity | ||
}} | ||
var noCaching = { | ||
fd: none, | ||
stat: none, | ||
index: none, | ||
readdir: none, | ||
content: none | ||
} | ||
if (o === false) { | ||
o = noCaching | ||
} else if (!o) { | ||
o = {} | ||
} | ||
Mount.prototype.getCacheOptions = function (opt) { | ||
var o = opt.cache | ||
, set = function (key) { | ||
return o[key] === false | ||
? util._extend({}, none) | ||
: util._extend(util._extend({}, d[key]), o[key]) | ||
} | ||
const d = defaultCacheOptions | ||
if (o === false) | ||
o = noCaching | ||
else if (!o) | ||
o = {} | ||
// should really only ever set max and maxAge here. | ||
// load and fd disposal is important to control. | ||
const c = { | ||
fd: set('fd'), | ||
stat: set('stat'), | ||
index: set('index'), | ||
readdir: set('readdir'), | ||
content: set('content') | ||
} | ||
var d = defaultCacheOptions | ||
c.fd.dispose = this.fdman.close.bind(this.fdman) | ||
c.fd.load = this.fdman.open.bind(this.fdman) | ||
// should really only ever set max and maxAge here. | ||
// load and fd disposal is important to control. | ||
var c = { | ||
fd: set('fd'), | ||
stat: set('stat'), | ||
index: set('index'), | ||
readdir: set('readdir'), | ||
content: set('content'), | ||
c.stat.load = this._loadStat.bind(this) | ||
c.index.load = this._loadIndex.bind(this) | ||
c.readdir.load = this._loadReaddir.bind(this) | ||
c.content.load = this._loadContent.bind(this) | ||
return c | ||
} | ||
c.fd.dispose = this.fdman.close.bind(this.fdman) | ||
c.fd.load = this.fdman.open.bind(this.fdman) | ||
// get the path component from a URI | ||
getUriPath (u) { | ||
let p = url.parse(u).pathname // eslint-disable-line | ||
c.stat.load = this._loadStat.bind(this) | ||
c.index.load = this._loadIndex.bind(this) | ||
c.readdir.load = this._loadReaddir.bind(this) | ||
c.content.load = this._loadContent.bind(this) | ||
return c | ||
} | ||
// Encoded dots are dots | ||
p = p.replace(/%2e/ig, '.') | ||
// get the path component from a URI | ||
Mount.prototype.getUriPath = function (u) { | ||
var p = url.parse(u).pathname | ||
// encoded slashes are / | ||
p = p.replace(/%2f|%5c/ig, '/') | ||
// Encoded dots are dots | ||
p = p.replace(/%2e/ig, '.') | ||
// back slashes are slashes | ||
p = p.replace(/[/\\]/g, '/') | ||
// encoded slashes are / | ||
p = p.replace(/%2f|%5c/ig, '/') | ||
// Make sure it starts with a slash | ||
p = p.replace(/^\//, '/') | ||
if ((/[/\\]\.\.([/\\]|$)/).test(p)) { | ||
// traversal urls not ever even slightly allowed. clearly shenanigans | ||
// send a 403 on that noise, do not pass go, do not collect $200 | ||
return 403 | ||
} | ||
// back slashes are slashes | ||
p = p.replace(/[\/\\]/g, '/') | ||
u = path.normalize(p).replace(/\\/g, '/') | ||
if (u.indexOf(this.url) !== 0) { | ||
return false | ||
} | ||
// Make sure it starts with a slash | ||
p = p.replace(/^\//, '/') | ||
if ((/[\/\\]\.\.([\/\\]|$)/).test(p)) { | ||
// traversal urls not ever even slightly allowed. clearly shenanigans | ||
// send a 403 on that noise, do not pass go, do not collect $200 | ||
return 403 | ||
} | ||
try { | ||
u = decodeURIComponent(u) | ||
} catch (e) { | ||
// if decodeURIComponent failed, we weren't given a valid URL to begin with. | ||
return false | ||
} | ||
u = path.normalize(p).replace(/\\/g, '/') | ||
if (u.indexOf(this.url) !== 0) return false | ||
// /a/b/c mounted on /path/to/z/d/x | ||
// /a/b/c/d --> /path/to/z/d/x/d | ||
u = u.substr(this.url.length) | ||
if (u.charAt(0) !== '/') { | ||
u = '/' + u | ||
} | ||
try { | ||
u = decodeURIComponent(u) | ||
return u | ||
} | ||
catch (e) { | ||
// if decodeURIComponent failed, we weren't given a valid URL to begin with. | ||
return false | ||
} | ||
// /a/b/c mounted on /path/to/z/d/x | ||
// /a/b/c/d --> /path/to/z/d/x/d | ||
u = u.substr(this.url.length) | ||
if (u.charAt(0) !== '/') u = '/' + u | ||
return u | ||
} | ||
// get a path from a url | ||
Mount.prototype.getPath = function (u) { | ||
return path.join(this.path, u) | ||
} | ||
// get a url from a path | ||
Mount.prototype.getUrl = function (p) { | ||
p = path.resolve(p) | ||
if (p.indexOf(this.path) !== 0) return false | ||
p = path.join('/', p.substr(this.path.length)) | ||
var u = path.join(this.url, p).replace(/\\/g, '/') | ||
return u | ||
} | ||
Mount.prototype.serve = function (req, res, next) { | ||
if (req.method !== 'HEAD' && req.method !== 'GET') { | ||
if (typeof next === 'function') next() | ||
return false | ||
// get a path from a url | ||
getPath (u) { | ||
return path.join(this.path, u) | ||
} | ||
// querystrings are of no concern to us | ||
if (!req.sturl) | ||
req.sturl = this.getUriPath(req.url) | ||
// don't allow dot-urls by default, unless explicitly allowed. | ||
// If we got a 403, then it's explicitly forbidden. | ||
if (req.sturl === 403 || (!this.opt.dot && (/(^|\/)\./).test(req.sturl))) { | ||
res.statusCode = 403 | ||
res.end('Forbidden') | ||
return true | ||
// get a url from a path | ||
getUrl (p) { | ||
p = path.resolve(p) | ||
if (p.indexOf(this.path) !== 0) { | ||
return false | ||
} | ||
p = path.join('/', p.substr(this.path.length)) | ||
const u = path.join(this.url, p).replace(/\\/g, '/') | ||
return u | ||
} | ||
// Falsey here means we got some kind of invalid path. | ||
// Probably urlencoding we couldn't understand, or some | ||
// other "not compatible with st, but maybe ok" thing. | ||
if (typeof req.sturl !== 'string' || req.sturl == '') { | ||
if (typeof next === 'function') next() | ||
return false | ||
} | ||
serve (req, res, next) { | ||
if (req.method !== 'HEAD' && req.method !== 'GET') { | ||
if (typeof next === 'function') { | ||
next() | ||
} | ||
return false | ||
} | ||
var p = this.getPath(req.sturl) | ||
// querystrings are of no concern to us | ||
if (!req.sturl) { | ||
req.sturl = this.getUriPath(req.url) | ||
} | ||
// now we have a path. check for the fd. | ||
this.cache.fd.get(p, function (er, fd) { | ||
// inability to open is some kind of error, probably 404 | ||
// if we're in passthrough, AND got a next function, we can | ||
// fall through to that. otherwise, we already returned true, | ||
// send an error. | ||
if (er) { | ||
if (this.opt.passthrough === true && er.code === 'ENOENT' && next) | ||
return next() | ||
return this.error(er, res) | ||
// don't allow dot-urls by default, unless explicitly allowed. | ||
// If we got a 403, then it's explicitly forbidden. | ||
if (req.sturl === 403 || (!this.opt.dot && (/(^|\/)\./).test(req.sturl))) { | ||
res.statusCode = 403 | ||
res.end('Forbidden') | ||
return true | ||
} | ||
// we may be about to use this, so don't let it be closed by cache purge | ||
this.fdman.checkout(p, fd) | ||
// a safe end() function that can be called multiple times but | ||
// only perform a single checkin | ||
var end = this.fdman.checkinfn(p, fd) | ||
// Falsey here means we got some kind of invalid path. | ||
// Probably urlencoding we couldn't understand, or some | ||
// other "not compatible with st, but maybe ok" thing. | ||
if (typeof req.sturl !== 'string' || req.sturl === '') { | ||
if (typeof next === 'function') { | ||
next() | ||
} | ||
return false | ||
} | ||
this.cache.stat.get(fd+':'+p, function (er, stat) { | ||
const p = this.getPath(req.sturl) | ||
// now we have a path. check for the fd. | ||
this.cache.fd.get(p, (er, fd) => { | ||
// inability to open is some kind of error, probably 404 | ||
// if we're in passthrough, AND got a next function, we can | ||
// fall through to that. otherwise, we already returned true, | ||
// send an error. | ||
if (er) { | ||
if (next && this.opt.passthrough === true && this._index === false) { | ||
if (this.opt.passthrough === true && er.code === 'ENOENT' && next) { | ||
return next() | ||
} | ||
end() | ||
return this.error(er, res) | ||
} | ||
var isDirectory = stat.isDirectory() | ||
// we may be about to use this, so don't let it be closed by cache purge | ||
this.fdman.checkout(p, fd) | ||
// a safe end() function that can be called multiple times but | ||
// only perform a single checkin | ||
const end = this.fdman.checkinfn(p, fd) | ||
if (isDirectory) { | ||
end() // we won't need this fd for a directory in any case | ||
if (next && this.opt.passthrough === true && this._index === false) { | ||
// this is done before if-modified-since and if-non-match checks so | ||
// cached modified and etag values won't return 304's if we've since | ||
// switched to !index. See Issue #51. | ||
return next() | ||
this.cache.stat.get(fd + ':' + p, (er, stat) => { | ||
if (er) { | ||
if (next && this.opt.passthrough === true && this._index === false) { | ||
return next() | ||
} | ||
end() | ||
return this.error(er, res) | ||
} | ||
} | ||
var ims = req.headers['if-modified-since'] | ||
if (ims) ims = new Date(ims).getTime() | ||
if (ims && ims >= stat.mtime.getTime()) { | ||
res.statusCode = 304 | ||
res.end() | ||
return end() | ||
} | ||
const isDirectory = stat.isDirectory() | ||
var etag = getEtag(stat) | ||
if (req.headers['if-none-match'] === etag) { | ||
res.statusCode = 304 | ||
res.end() | ||
return end() | ||
} | ||
if (isDirectory) { | ||
end() // we won't need this fd for a directory in any case | ||
if (next && this.opt.passthrough === true && this._index === false) { | ||
// this is done before if-modified-since and if-non-match checks so | ||
// cached modified and etag values won't return 304's if we've since | ||
// switched to !index. See Issue #51. | ||
return next() | ||
} | ||
} | ||
// only set headers once we're sure we'll be serving this request | ||
if (!res.getHeader('cache-control') && this._cacheControl) | ||
res.setHeader('cache-control', this._cacheControl) | ||
res.setHeader('last-modified', stat.mtime.toUTCString()) | ||
res.setHeader('etag', etag) | ||
let ims = req.headers['if-modified-since'] | ||
if (ims) { | ||
ims = new Date(ims).getTime() | ||
} | ||
if (ims && ims >= stat.mtime.getTime()) { | ||
res.statusCode = 304 | ||
res.end() | ||
return end() | ||
} | ||
if (this.opt.cors) { | ||
res.setHeader('Access-Control-Allow-Origin', '*') | ||
res.setHeader('Access-Control-Allow-Headers', | ||
'Origin, X-Requested-With, Content-Type, Accept, Range') | ||
} | ||
const etag = getEtag(stat) | ||
if (req.headers['if-none-match'] === etag) { | ||
res.statusCode = 304 | ||
res.end() | ||
return end() | ||
} | ||
return isDirectory | ||
? this.index(p, req, res) | ||
: this.file(p, fd, stat, etag, req, res, end) | ||
}.bind(this)) | ||
}.bind(this)) | ||
// only set headers once we're sure we'll be serving this request | ||
if (!res.getHeader('cache-control') && this._cacheControl) { | ||
res.setHeader('cache-control', this._cacheControl) | ||
} | ||
res.setHeader('last-modified', stat.mtime.toUTCString()) | ||
res.setHeader('etag', etag) | ||
return true | ||
} | ||
if (this.opt.cors) { | ||
res.setHeader('Access-Control-Allow-Origin', '*') | ||
res.setHeader('Access-Control-Allow-Headers', | ||
'Origin, X-Requested-With, Content-Type, Accept, Range') | ||
} | ||
Mount.prototype.error = function (er, res) { | ||
res.statusCode = typeof er === 'number' ? er | ||
: er.code === 'ENOENT' || er.code === 'EISDIR' ? 404 | ||
: er.code === 'EPERM' || er.code === 'EACCES' ? 403 | ||
: 500 | ||
return isDirectory | ||
? this.index(p, req, res) | ||
: this.file(p, fd, stat, etag, req, res, end) | ||
}) | ||
}) | ||
if (typeof res.error === 'function') { | ||
// pattern of express and ErrorPage | ||
return res.error(res.statusCode, er) | ||
return true | ||
} | ||
res.setHeader('content-type', 'text/plain') | ||
res.end(http.STATUS_CODES[res.statusCode] + '\n') | ||
} | ||
error (er, res) { | ||
res.statusCode = typeof er === 'number' ? er | ||
: er.code === 'ENOENT' || er.code === 'EISDIR' ? 404 | ||
: er.code === 'EPERM' || er.code === 'EACCES' ? 403 | ||
: 500 | ||
Mount.prototype.index = function (p, req, res) { | ||
if (this._index === true) { | ||
return this.autoindex(p, req, res) | ||
if (typeof res.error === 'function') { | ||
// pattern of express and ErrorPage | ||
return res.error(res.statusCode, er) | ||
} | ||
res.setHeader('content-type', 'text/plain') | ||
res.end(http.STATUS_CODES[res.statusCode] + '\n') | ||
} | ||
if (typeof this._index === 'string') { | ||
if (!/\/$/.test(req.sturl)) req.sturl += '/' | ||
req.sturl += this._index | ||
return this.serve(req, res) | ||
} | ||
return this.error(404, res) | ||
} | ||
Mount.prototype.autoindex = function (p, req, res) { | ||
if (!/\/$/.exec(req.sturl)) { | ||
res.statusCode = 301 | ||
res.setHeader('location', req.sturl + '/') | ||
res.end('Moved: ' + req.sturl + '/') | ||
return | ||
index (p, req, res) { | ||
if (this._index === true) { | ||
return this.autoindex(p, req, res) | ||
} | ||
if (typeof this._index === 'string') { | ||
if (!/\/$/.test(req.sturl)) { | ||
req.sturl += '/' | ||
} | ||
req.sturl += this._index | ||
return this.serve(req, res) | ||
} | ||
return this.error(404, res) | ||
} | ||
this.cache.index.get(p, function (er, html) { | ||
if (er) return this.error(er, res) | ||
autoindex (p, req, res) { | ||
if (!/\/$/.exec(req.sturl)) { | ||
res.statusCode = 301 | ||
res.setHeader('location', req.sturl + '/') | ||
res.end('Moved: ' + req.sturl + '/') | ||
return | ||
} | ||
res.statusCode = 200 | ||
res.setHeader('content-type', 'text/html') | ||
res.setHeader('content-length', html.length) | ||
res.end(html) | ||
}.bind(this)) | ||
} | ||
this.cache.index.get(p, (er, html) => { | ||
if (er) { | ||
return this.error(er, res) | ||
} | ||
Mount.prototype.file = function (p, fd, stat, etag, req, res, end) { | ||
var key = stat.size + ':' + etag | ||
var mt = mime.lookup(path.extname(p)) | ||
if (mt !== 'application/octet-stream') { | ||
res.setHeader('content-type', mt) | ||
res.statusCode = 200 | ||
res.setHeader('content-type', 'text/html') | ||
res.setHeader('content-length', html.length) | ||
res.end(html) | ||
}) | ||
} | ||
// only use the content cache if it will actually fit there. | ||
if (this.cache.content.has(key)) { | ||
end() | ||
this.cachedFile(p, stat, etag, req, res) | ||
} else { | ||
this.streamFile(p, fd, stat, etag, req, res, end) | ||
} | ||
} | ||
file (p, fd, stat, etag, req, res, end) { | ||
const key = stat.size + ':' + etag | ||
Mount.prototype.cachedFile = function (p, stat, etag, req, res) { | ||
var key = stat.size + ':' + etag | ||
var gz = this.opt.gzip !== false && getGz(p, req) | ||
const mt = mime.getType(path.extname(p)) | ||
if (mt !== 'application/octet-stream') { | ||
res.setHeader('content-type', mt) | ||
} | ||
this.cache.content.get(key, function (er, content) { | ||
if (er) return this.error(er, res) | ||
res.statusCode = 200 | ||
if (this.opt.cachedHeader) | ||
res.setHeader('x-from-cache', 'true') | ||
if (gz && content.gz) { | ||
res.setHeader('content-encoding', 'gzip') | ||
res.setHeader('content-length', content.gz.length) | ||
res.end(content.gz) | ||
// only use the content cache if it will actually fit there. | ||
if (this.cache.content.has(key)) { | ||
end() | ||
this.cachedFile(p, stat, etag, req, res) | ||
} else { | ||
res.setHeader('content-length', content.length) | ||
res.end(content) | ||
this.streamFile(p, fd, stat, etag, req, res, end) | ||
} | ||
}.bind(this)) | ||
} | ||
} | ||
Mount.prototype.streamFile = function (p, fd, stat, etag, req, res, end) { | ||
var streamOpt = { fd: fd, start: 0, end: stat.size } | ||
var stream = fs.createReadStream(p, streamOpt) | ||
stream.destroy = function () {} | ||
cachedFile (p, stat, etag, req, res) { | ||
const key = stat.size + ':' + etag | ||
const gz = this.opt.gzip !== false && getGz(p, req) | ||
// gzip only if not explicitly turned off or client doesn't accept it | ||
var gzOpt = this.opt.gzip !== false | ||
var gz = gzOpt && getGz(p, req) | ||
var cachable = this.cache.content._cache.max > stat.size | ||
var gzstr | ||
this.cache.content.get(key, (er, content) => { | ||
if (er) { | ||
return this.error(er, res) | ||
} | ||
res.statusCode = 200 | ||
if (this.opt.cachedHeader) { | ||
res.setHeader('x-from-cache', 'true') | ||
} | ||
if (gz && content.gz) { | ||
res.setHeader('content-encoding', 'gzip') | ||
res.setHeader('content-length', content.gz.length) | ||
res.end(content.gz) | ||
} else { | ||
res.setHeader('content-length', content.length) | ||
res.end(content) | ||
} | ||
}) | ||
} | ||
// need a gzipped version for the cache, so do it regardless of what the client wants | ||
if (gz || (gzOpt && cachable)) gzstr = zlib.Gzip() | ||
streamFile (p, fd, stat, etag, req, res, end) { | ||
const streamOpt = { fd: fd, start: 0, end: stat.size } | ||
let stream = fs.createReadStream(p, streamOpt) | ||
stream.destroy = () => {} | ||
// too late to effectively handle any errors. | ||
// just kill the connection if that happens. | ||
stream.on('error', function(e) { | ||
console.error('Error serving %s fd=%d\n%s', p, fd, e.stack || e.message) | ||
res.socket.destroy() | ||
end() | ||
}) | ||
// gzip only if not explicitly turned off or client doesn't accept it | ||
const gzOpt = this.opt.gzip !== false | ||
const gz = gzOpt && getGz(p, req) | ||
const cachable = this.cache.content._cache.max > stat.size | ||
let gzstr | ||
if (res.filter) stream = stream.pipe(res.filter) | ||
// need a gzipped version for the cache, so do it regardless of what the client wants | ||
if (gz || (gzOpt && cachable)) { | ||
gzstr = zlib.Gzip() | ||
} | ||
res.statusCode = 200 | ||
// too late to effectively handle any errors. | ||
// just kill the connection if that happens. | ||
stream.on('error', (e) => { | ||
console.error('Error serving %s fd=%d\n%s', p, fd, e.stack || e.message) | ||
res.socket.destroy() | ||
end() | ||
}) | ||
if (gz) { | ||
// we don't know how long it'll be, since it will be compressed. | ||
res.setHeader('content-encoding', 'gzip') | ||
stream.pipe(gzstr).pipe(res) | ||
} else { | ||
if (!res.filter) res.setHeader('content-length', stat.size) | ||
stream.pipe(res) | ||
if (gzstr) | ||
stream.pipe(gzstr) // for cache | ||
} | ||
if (res.filter) { | ||
stream = stream.pipe(res.filter) | ||
} | ||
stream.on('end', function () { | ||
process.nextTick(end) | ||
}) | ||
res.statusCode = 200 | ||
if (cachable) { | ||
// collect it, and put it in the cache | ||
if (gz) { | ||
// we don't know how long it'll be, since it will be compressed. | ||
res.setHeader('content-encoding', 'gzip') | ||
stream.pipe(gzstr).pipe(res) | ||
} else { | ||
if (!res.filter) { | ||
res.setHeader('content-length', stat.size) | ||
} | ||
stream.pipe(res) | ||
if (gzstr) { | ||
stream.pipe(gzstr) | ||
} // for cache | ||
} | ||
var calls = 0 | ||
stream.on('end', () => process.nextTick(end)) | ||
// called by bl() for both the raw stream and gzipped stream if we're | ||
// caching gzipped data | ||
var collectEnd = function () { | ||
if (++calls == (gzOpt ? 2 : 1)) { | ||
var content = bufs.slice() | ||
content.gz = gzbufs && gzbufs.slice() | ||
this.cache.content.set(key, content) | ||
if (cachable) { | ||
// collect it, and put it in the cache | ||
let calls = 0 | ||
// called by bl() for both the raw stream and gzipped stream if we're | ||
// caching gzipped data | ||
const collectEnd = () => { | ||
if (++calls === (gzOpt ? 2 : 1)) { | ||
const content = bufs.slice() | ||
content.gz = gzbufs && gzbufs.slice() | ||
this.cache.content.set(key, content) | ||
} | ||
} | ||
}.bind(this) | ||
var key = stat.size + ':' + etag | ||
var bufs = bl(collectEnd) | ||
var gzbufs | ||
const key = stat.size + ':' + etag | ||
const bufs = bl(collectEnd) | ||
let gzbufs | ||
stream.pipe(bufs) | ||
stream.pipe(bufs) | ||
if (gzstr) { | ||
gzbufs = bl(collectEnd) | ||
gzstr.pipe(gzbufs) | ||
if (gzstr) { | ||
gzbufs = bl(collectEnd) | ||
gzstr.pipe(gzbufs) | ||
} | ||
} | ||
} | ||
} | ||
// cache-fillers | ||
// cache-fillers | ||
Mount.prototype._loadIndex = function (p, cb) { | ||
// truncate off the first bits | ||
var url = p.substr(this.path.length).replace(/\\/g, '/') | ||
var t = url | ||
_loadIndex (p, cb) { | ||
// truncate off the first bits | ||
const url = p.substr(this.path.length).replace(/\\/g, '/') | ||
const t = url | ||
.replace(/"/g, '"') | ||
@@ -496,20 +531,22 @@ .replace(/</g, '<') | ||
var str = | ||
'<!doctype html>' + | ||
'<html>' + | ||
'<head><title>Index of ' + t + '</title></head>' + | ||
'<body>' + | ||
'<h1>Index of ' + t + '</h1>' + | ||
'<hr><pre><a href="../">../</a>\n' | ||
let str = | ||
'<!doctype html>' + | ||
'<html>' + | ||
'<head><title>Index of ' + t + '</title></head>' + | ||
'<body>' + | ||
'<h1>Index of ' + t + '</h1>' + | ||
'<hr><pre><a href="../">../</a>\n' | ||
this.cache.readdir.get(p, function (er, data) { | ||
if (er) return cb(er) | ||
this.cache.readdir.get(p, (er, data) => { | ||
if (er) { | ||
return cb(er) | ||
} | ||
var nameLen = 0 | ||
var sizeLen = 0 | ||
let nameLen = 0 | ||
let sizeLen = 0 | ||
Object.keys(data).map(function (f) { | ||
var d = data[f] | ||
Object.keys(data).map((f) => { | ||
const d = data[f] | ||
var name = f | ||
let name = f | ||
.replace(/"/g, '"') | ||
@@ -520,82 +557,98 @@ .replace(/</g, '<') | ||
if (d.size === '-') name += '/' | ||
var showName = name.replace(/^(.{40}).{3,}$/, '$1..>') | ||
var linkName = encodeURIComponent(name) | ||
.replace(/%2e/ig, '.') // Encoded dots are dots | ||
.replace(/%2f|%5c/ig, '/') // encoded slashes are / | ||
.replace(/[\/\\]/g, '/') // back slashes are slashes | ||
if (d.size === '-') { | ||
name += '/' | ||
} | ||
const showName = name.replace(/^(.{40}).{3,}$/, '$1..>') | ||
const linkName = encodeURIComponent(name) | ||
.replace(/%2e/ig, '.') // Encoded dots are dots | ||
.replace(/%2f|%5c/ig, '/') // encoded slashes are / | ||
.replace(/[/\\]/g, '/') // back slashes are slashes | ||
nameLen = Math.max(nameLen, showName.length) | ||
sizeLen = Math.max(sizeLen, ('' + d.size).length) | ||
return [ '<a href="' + linkName + '">' + showName + '</a>', | ||
d.mtime, d.size, showName ] | ||
}).sort(function (a, b) { | ||
return a[2] === '-' && b[2] !== '-' ? -1 // dirs first | ||
: a[2] !== '-' && b[2] === '-' ? 1 | ||
: a[0].toLowerCase() < b[0].toLowerCase() ? -1 // then alpha | ||
: a[0].toLowerCase() > b[0].toLowerCase() ? 1 | ||
: 0 | ||
}).forEach(function (line) { | ||
var namePad = new Array(8 + nameLen - line[3].length).join(' ') | ||
var sizePad = new Array(8 + sizeLen - ('' + line[2]).length).join(' ') | ||
str += line[0] + namePad + | ||
line[1].toISOString() + | ||
sizePad + line[2] + '\n' | ||
nameLen = Math.max(nameLen, showName.length) | ||
sizeLen = Math.max(sizeLen, ('' + d.size).length) | ||
return ['<a href="' + linkName + '">' + showName + '</a>', | ||
d.mtime, d.size, showName] | ||
}).sort((a, b) => { | ||
return a[2] === '-' && b[2] !== '-' ? -1 // dirs first | ||
: a[2] !== '-' && b[2] === '-' ? 1 | ||
: a[0].toLowerCase() < b[0].toLowerCase() ? -1 // then alpha | ||
: a[0].toLowerCase() > b[0].toLowerCase() ? 1 | ||
: 0 | ||
}).forEach((line) => { | ||
const namePad = new Array(8 + nameLen - line[3].length).join(' ') | ||
const sizePad = new Array(8 + sizeLen - ('' + line[2]).length).join(' ') | ||
str += line[0] + namePad + | ||
line[1].toISOString() + | ||
sizePad + line[2] + '\n' | ||
}) | ||
str += '</pre><hr></body></html>' | ||
cb(null, Buffer.from(str)) | ||
}) | ||
} | ||
str += '</pre><hr></body></html>' | ||
cb(null, new Buffer(str)) | ||
}) | ||
} | ||
_loadReaddir (p, cb) { | ||
let len | ||
let data | ||
fs.readdir(p, (er, files) => { | ||
if (er) { | ||
return cb(er) | ||
} | ||
files = files.filter((f) => { | ||
if (!this.opt.dot) { | ||
return !/^\./.test(f) | ||
} else { | ||
return f !== '.' && f !== '..' | ||
} | ||
}) | ||
len = files.length | ||
data = {} | ||
files.forEach((file) => { | ||
const pf = path.join(p, file) | ||
this.cache.stat.get(pf, (er, stat) => { | ||
if (er) { | ||
return cb(er) | ||
} | ||
if (stat.isDirectory()) { | ||
stat.size = '-' | ||
} | ||
data[file] = stat | ||
next() | ||
}) | ||
}) | ||
}) | ||
Mount.prototype._loadReaddir = function (p, cb) { | ||
var len | ||
var data | ||
fs.readdir(p, function (er, files) { | ||
if (er) return cb(er) | ||
files = files.filter(function (f) { | ||
if (!this.opt.dot) return !/^\./.test(f) | ||
else return f !== '.' && f !== '..' | ||
}.bind(this)) | ||
len = files.length | ||
data = {} | ||
files.forEach(function (file) { | ||
var pf = path.join(p, file) | ||
this.cache.stat.get(pf, function (er, stat) { | ||
if (er) return cb(er) | ||
if (stat.isDirectory()) stat.size = '-' | ||
data[file] = stat | ||
next() | ||
}.bind(this)) | ||
}.bind(this)) | ||
}.bind(this)) | ||
const next = () => { | ||
if (--len === 0) { | ||
cb(null, data) | ||
} | ||
} | ||
} | ||
function next () { | ||
if (--len === 0) cb(null, data) | ||
_loadStat (key, cb) { | ||
// key is either fd:path or just a path | ||
const fdp = key.match(/^(\d+):(.*)/) | ||
if (fdp) { | ||
const fd = +fdp[1] | ||
const p = fdp[2] | ||
fs.fstat(fd, (er, stat) => { | ||
if (er) { | ||
return cb(er) | ||
} | ||
this.cache.stat.set(p, stat) | ||
cb(null, stat) | ||
}) | ||
} else { | ||
fs.stat(key, cb) | ||
} | ||
} | ||
} | ||
Mount.prototype._loadStat = function (key, cb) { | ||
// key is either fd:path or just a path | ||
var fdp = key.match(/^(\d+):(.*)/) | ||
if (fdp) { | ||
var fd = +fdp[1] | ||
var p = fdp[2] | ||
fs.fstat(fd, function (er, stat) { | ||
if (er) return cb(er) | ||
this.cache.stat.set(p, stat) | ||
cb(null, stat) | ||
}.bind(this)) | ||
} else { | ||
fs.stat(key, cb) | ||
_loadContent () { | ||
// this function should never be called. | ||
// we check if the thing is in the cache, and if not, stream it in | ||
// manually. this.cache.content.get() should not ever happen. | ||
throw new Error('This should not ever happen') | ||
} | ||
} | ||
Mount.prototype._loadContent = function () { | ||
// this function should never be called. | ||
// we check if the thing is in the cache, and if not, stream it in | ||
// manually. this.cache.content.get() should not ever happen. | ||
throw new Error('This should not ever happen') | ||
} | ||
function getEtag (s) { | ||
@@ -605,6 +658,6 @@ return '"' + s.dev + '-' + s.ino + '-' + s.mtime.getTime() + '"' | ||
function getGz (p,req) { | ||
var gz = false | ||
function getGz (p, req) { | ||
let gz = false | ||
if (!/\.t?gz$/.exec(p)) { | ||
var neg = req.negotiator || new Neg(req) | ||
const neg = req.negotiator || new Neg(req) | ||
gz = neg.preferredEncoding(['gzip', 'identity']) === 'gzip' | ||
@@ -614,1 +667,4 @@ } | ||
} | ||
module.exports = st | ||
module.exports.Mount = Mount |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
66380
35
1752
295
4
10
8
+ Addedbase64-js@1.5.1(transitive)
+ Addedbl@4.1.0(transitive)
+ Addedbuffer@5.7.1(transitive)
+ Addedgraceful-fs@4.2.11(transitive)
+ Addedieee754@1.2.1(transitive)
+ Addedmime@2.6.0(transitive)
+ Addedreadable-stream@3.6.2(transitive)
+ Addedstring_decoder@1.3.0(transitive)
- Removedbl@1.2.3(transitive)
- Removedcore-util-is@1.0.3(transitive)
- Removedgraceful-fs@4.1.15(transitive)
- Removedisarray@1.0.0(transitive)
- Removedmime@1.4.1(transitive)
- Removedprocess-nextick-args@2.0.1(transitive)
- Removedreadable-stream@2.3.8(transitive)
- Removedsafe-buffer@5.1.2(transitive)
- Removedstring_decoder@1.1.1(transitive)
Updatedasync-cache@^1.1.0
Updatedbl@^4.0.0
Updatedmime@^2.4.4
Updatednegotiator@~0.6.2