diff --git a/README.md b/README.md
index 4167926..f4e5be0 100644
--- a/README.md
+++ b/README.md
@@ -96,6 +96,11 @@ Provide a max-age in milliseconds for http caching, defaults to 0.
This can also be a string accepted by the
[ms](https://www.npmjs.org/package/ms#readme) module.
+##### redirectSymlinks
+
+When `true`, return symlinks as `301 Redirect` instead of the file they are
+pointing. Default is `false`.
+
##### root
Serve files relative to `path`.
@@ -229,7 +234,7 @@ var app = http.createServer(function onRequest (req, res) {
}).listen(3000)
```
-## License
+## License
[MIT](LICENSE)
diff --git a/index.js b/index.js
index 81ec0b3..c7a007e 100644
--- a/index.js
+++ b/index.js
@@ -16,7 +16,6 @@ var createError = require('http-errors')
var debug = require('debug')('send')
var deprecate = require('depd')('send')
var destroy = require('destroy')
-var encodeUrl = require('encodeurl')
var escapeHtml = require('escape-html')
var etag = require('etag')
var EventEmitter = require('events').EventEmitter
@@ -31,6 +30,10 @@ var statuses = require('statuses')
var Stream = require('stream')
var util = require('util')
+var dirname = path.dirname
+var relative = path.relative
+var url = require('url')
+
/**
* Path function references.
* @private
@@ -79,6 +82,31 @@ module.exports.mime = mime
var listenerCount = EventEmitter.listenerCount ||
function (emitter, type) { return emitter.listeners(type).length }
+function responseStatus (res, code, msg) {
+ var errMsg = msg
+ if (errMsg == null) {
+ errMsg = statuses[code]
+
+ res.setHeader('Content-Type', 'text/plain; charset=UTF-8')
+ }
+
+ res.statusCode = code
+ res.setHeader('Content-Length', Buffer.byteLength(msg))
+ res.setHeader('X-Content-Type-Options', 'nosniff')
+ res.end(errMsg)
+}
+
+function redirect (res, loc) {
+ // redirect
+ res.setHeader('Content-Type', 'text/html; charset=UTF-8')
+ res.setHeader('Location', loc)
+
+ var escLoc = escapeHtml(loc)
+ var msg = 'Redirecting to ' + escLoc + '\n'
+
+ responseStatus(res, 301, msg)
+}
+
/**
* Return a `SendStream` for `req` and `path`.
*
@@ -166,6 +194,8 @@ function SendStream (req, path, options) {
? resolve(opts.root)
: null
+ this._redirectSymlinks = opts.redirectSymlinks
+
if (!this._root && opts.from) {
this.from(opts.from)
}
@@ -278,7 +308,6 @@ SendStream.prototype.error = function error (status, error) {
}
var res = this.res
- var msg = statuses[status]
// clear existing headers
clearHeaders(res)
@@ -289,11 +318,7 @@ SendStream.prototype.error = function error (status, error) {
}
// send basic response
- res.statusCode = status
- res.setHeader('Content-Type', 'text/plain; charset=UTF-8')
- res.setHeader('Content-Length', Buffer.byteLength(msg))
- res.setHeader('X-Content-Type-Options', 'nosniff')
- res.end(msg)
+ responseStatus(res, status)
}
/**
@@ -434,7 +459,7 @@ SendStream.prototype.isRangeFresh = function isRangeFresh () {
* @private
*/
-SendStream.prototype.redirect = function redirect (path) {
+SendStream.prototype.redirect = function redirectDirectory (path) {
if (listenerCount(this, 'directory') !== 0) {
this.emit('directory')
return
@@ -445,17 +470,38 @@ SendStream.prototype.redirect = function redirect (path) {
return
}
- var loc = encodeUrl(collapseLeadingSlashes(path + '/'))
- var msg = 'Redirecting to ' + escapeHtml(loc) + '\n'
- var res = this.res
+ redirect(this.res, path + '/')
+}
- // redirect
- res.statusCode = 301
- res.setHeader('Content-Type', 'text/html; charset=UTF-8')
- res.setHeader('Content-Length', Buffer.byteLength(msg))
- res.setHeader('X-Content-Type-Options', 'nosniff')
- res.setHeader('Location', loc)
- res.end(msg)
+/**
+ * Redirect a symbolic link.
+ *
+ * @param {string} path
+ * @private
+ */
+
+SendStream.prototype.redirectSymbolicLink = function redirectSymbolicLink (path) {
+ var self = this
+
+ fs.readlink(path, function (err, linkString) {
+ if (err) return self.onStatError(err)
+
+ // Get absolute path on the real filesystem of the destination
+ path = dirname(path)
+ var to = resolve(path, linkString)
+
+ // Check destination is not out of files root
+ if (to.indexOf(self._root) !== 0) return this.error(403)
+
+ // Get relative paths for all symlinks, also for absolute ones
+ linkString = relative(path, to)
+
+ // Resolve the URL, and make it relative (is this necessary?)
+ linkString = url.resolve(self.path, linkString)
+ linkString = relative(dirname(self.path), linkString)
+
+ redirect(self.res, linkString)
+ })
}
/**
@@ -666,22 +712,30 @@ SendStream.prototype.sendFile = function sendFile (path) {
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) {
+ fs.lstat(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)
+
+ if (stat.isSymbolicLink() && self._redirectSymlinks) {
+ return self.redirectSymbolicLink(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)
+ if (err) return self.onStatError(err)
+ return self.error(404)
}
var p = path + '.' + self._extensions[i++]
@@ -689,7 +743,9 @@ SendStream.prototype.sendFile = function sendFile (path) {
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)
})
@@ -703,21 +759,23 @@ SendStream.prototype.sendFile = function sendFile (path) {
* @api private
*/
SendStream.prototype.sendIndex = function sendIndex (path) {
- var i = -1
+ var i = 0
var self = this
function next (err) {
- if (++i >= self._index.length) {
+ if (self._index.length <= i) {
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)
})
@@ -846,24 +904,6 @@ function clearHeaders (res) {
res._headerNames = {}
}
-/**
- * Collapse all leading slashes into a single slash
- *
- * @param {string} str
- * @private
- */
-function collapseLeadingSlashes (str) {
- for (var i = 0; i < str.length; i++) {
- if (str[i] !== '/') {
- break
- }
- }
-
- return i > 1
- ? '/' + str.substr(i)
- : str
-}
-
/**
* Determine if path parts contain a dotfile.
*
diff --git a/package.json b/package.json
index 3a93e89..9d932f7 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,6 @@
"debug": "~2.2.0",
"depd": "~1.1.0",
"destroy": "~1.0.4",
- "encodeurl": "~1.0.1",
"escape-html": "~1.0.3",
"etag": "~1.7.0",
"fresh": "0.3.0",
diff --git a/test/send.js b/test/send.js
index da101aa..bf34024 100644
--- a/test/send.js
+++ b/test/send.js
@@ -132,6 +132,54 @@ describe('send(file).pipe(res)', function () {
.expect(200, '404 ENOENT', done)
})
+ it('should 301 if the directory exists', function (done) {
+ request(app)
+ .get('/pets')
+ .expect('Location', '/pets/')
+ .expect(301, 'Redirecting to /pets/\n', done)
+ })
+
+ it('should not redirect on symbolic links', function (done) {
+ var destination = 'name.txt'
+ var path2 = path.join(fixtures, 'symlink')
+
+ fs.symlink(destination, path2, function (error) {
+ if (error && error.code !== 'EEXIST') return done(error)
+
+ request(app)
+ .get('/symlink')
+ .expect(200, 'tobi', function (error) {
+ fs.unlink(path2, function (error2) {
+ done(error || error2)
+ })
+ })
+ })
+ })
+
+ it("should 301 if it's a symbolic link and we want to redirect", function (done) {
+ var app = http.createServer(function (req, res) {
+ send(req, req.url, {root: fixtures, redirectSymlinks: true})
+ .on('error', function (err) { res.end(err.statusCode + ' ' + err.code) })
+ .pipe(res)
+ })
+
+ var destination = 'name.txt'
+ var path2 = path.join(fixtures, 'symlink')
+
+ fs.symlink(destination, path2, function (error) {
+ if (error && error.code !== 'EEXIST') return done(error)
+
+ request(app)
+ .get('/symlink')
+ .expect('Location', destination)
+ .expect(301, 'Redirecting to ' + destination + '\n', function (error) {
+ fs.unlink(path2, function (error2) {
+ done(error || error2)
+ })
+ })
+ })
+ })
+
it('should not override content-type', function (done) {
var app = http.createServer(function (req, res) {
res.setHeader('Content-Type', 'application/x-custom')