Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return redirection for symlinks #87

Closed
wants to merge 16 commits into from
Closed
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -229,7 +234,7 @@ var app = http.createServer(function onRequest (req, res) {
}).listen(3000)
```

## License
## License

[MIT](LICENSE)

Expand Down
128 changes: 84 additions & 44 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ var createError = require('http-errors')
var debug = require('debug')('send')
var deprecate = require('depd')('send')
var destroy = require('destroy')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please undo all these changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just put the requires sorted alphabetically and grouped by build-ins, third-parties and local modules, as it's usually accepted in the Node.js community. Also I've made the requires style uniform, instead of some of them using its own var statement and others a common one, and some requires that were hidden between the variables declarations. Do you still want these clean-up changes to be undone?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you still want these clean-up changes to be undone?

Yes. It is extremely important to us that git blame stays intact, thus why there is a mixed style.

var encodeUrl = require('encodeurl')
var escapeHtml = require('escape-html')
var etag = require('etag')
var EventEmitter = require('events').EventEmitter
Expand All @@ -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
Expand Down Expand Up @@ -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 <a href="' + escLoc + '">' + escLoc + '</a>\n'

responseStatus(res, 301, msg)
}

/**
* Return a `SendStream` for `req` and `path`.
*
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -278,7 +308,6 @@ SendStream.prototype.error = function error (status, error) {
}

var res = this.res
var msg = statuses[status]

// clear existing headers
clearHeaders(res)
Expand All @@ -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)
}

/**
Expand Down Expand Up @@ -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
Expand All @@ -445,17 +470,38 @@ SendStream.prototype.redirect = function redirect (path) {
return
}

var loc = encodeUrl(collapseLeadingSlashes(path + '/'))
var msg = 'Redirecting to <a href="' + escapeHtml(loc) + '">' + escapeHtml(loc) + '</a>\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)
})
}

/**
Expand Down Expand Up @@ -666,30 +712,40 @@ 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++]

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)
})
Expand All @@ -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)
})
Expand Down Expand Up @@ -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.
*
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
48 changes: 48 additions & 0 deletions test/send.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="/pets/">/pets/</a>\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 <a href="' + destination + '">' + destination + '</a>\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')
Expand Down