From 5ec3ed30b7dd10d49af701025012321a01d604c8 Mon Sep 17 00:00:00 2001 From: Daniel Stockman Date: Tue, 22 Oct 2013 18:51:49 -0700 Subject: [PATCH] Opt-in to the realpathSync/statSync change via --no-resolve-symlinks flag. --- lib/args.js | 5 + lib/combohandler.js | 2 +- lib/middleware/dynamicPath.js | 9 +- lib/utils.js | 12 +- test/server.js | 237 ++++++++++++++++++++-------------- 5 files changed, 160 insertions(+), 105 deletions(-) diff --git a/lib/args.js b/lib/args.js index 6cfebe4..200f494 100644 --- a/lib/args.js +++ b/lib/args.js @@ -14,6 +14,7 @@ var knownOpts = { "pids": path, "port": Number, "restart": Boolean, + "resolveSymlinks": Boolean, "root": [String, Array], "rootsFile": path, "server": path, @@ -26,6 +27,7 @@ var knownOpts = { }; var shortHands = { + "--no-resolve-symlinks": ["--no-resolveSymlinks"], "h": ["--help"], "v": ["--version"], "a": ["--server"], @@ -66,6 +68,9 @@ exports = module.exports = { msg.push(" Set this to `0` to expire immediately, `null` to omit these"); msg.push(" headers entirely."); msg.push(""); + msg.push(" --no-resolve-symlinks"); + msg.push(" If passed, rootPaths that are symlinks will not be resolved (the default)."); + msg.push(""); msg.push("Cluster Options:"); msg.push(" --cluster Enable clustering of server across multiple processes."); msg.push(" -d, --pids Directory where pidfiles are stored. [$PREFIX/var/run]"); diff --git a/lib/combohandler.js b/lib/combohandler.js index 4730e37..12bf30a 100644 --- a/lib/combohandler.js +++ b/lib/combohandler.js @@ -44,7 +44,7 @@ exports.combine = function (config) { // Intentionally using the sync method because this only runs when the // middleware is initialized, and we want it to throw if there's an // error. - rootPathResolved = resolvePathSync(config.rootPath); + rootPathResolved = resolvePathSync(config.rootPath, config.resolveSymlinks); } function combineMiddleware(req, res, next) { diff --git a/lib/middleware/dynamicPath.js b/lib/middleware/dynamicPath.js index 6388cf2..3ee16f9 100644 --- a/lib/middleware/dynamicPath.js +++ b/lib/middleware/dynamicPath.js @@ -43,7 +43,7 @@ exports = module.exports = function (options) { } // cache config object used in middleware - dynamicPathMiddleware.CONFIG = parseConfig(options.rootPath); + dynamicPathMiddleware.CONFIG = parseConfig(options.rootPath, options.resolveSymlinks); return dynamicPathMiddleware; }; @@ -102,7 +102,8 @@ function getDynamicKeys(rootPath) { Create a config object for use in dynamicPathMiddleware. @method parseConfig -@param {String} rootPatha +@param {String} rootPath +@param {Boolean} resolveSymlinks @return {Object} config @property {String} config.rootPath @property {String} config.dynamicParam @@ -110,7 +111,7 @@ Create a config object for use in dynamicPathMiddleware. @property {String} config.statCache @private **/ -function parseConfig(rootPath) { +function parseConfig(rootPath, resolveSymlinks) { rootPath = path.normalize(rootPath); var dynamicKeys = getDynamicKeys(rootPath); @@ -144,7 +145,7 @@ function parseConfig(rootPath) { // Intentionally using the sync method because this only runs when the // middleware is initialized, and we want it to throw if there's an error. - rootPath = resolvePathSync(rootPath); + rootPath = resolvePathSync(rootPath, resolveSymlinks); return { "rootPath" : rootPath, diff --git a/lib/utils.js b/lib/utils.js index 34136ee..926390e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -43,12 +43,18 @@ function memoize(source) { }; } -function resolvePathSync(rootPath) { - // Turns out fs.realpathSync was always defaulting empty strings to cwd(). +function resolvePathSync(rootPath, resolveSymlinks) { + // Turns out fs.realpathSync always defaults empty strings to cwd(). rootPath = rootPath || process.cwd(); + // Intentionally using the sync method because this only runs when the // middleware is initialized, and we want it to throw if there's an error. - fs.statSync(rootPath); + if (resolveSymlinks !== false) { + rootPath = fs.realpathSync(rootPath); + } else { + fs.statSync(rootPath); + } + // The resulting rootPath should always have a trailing slash. return path.normalize(rootPath + path.sep); } diff --git a/test/server.js b/test/server.js index d54cf05..69d5360 100644 --- a/test/server.js +++ b/test/server.js @@ -674,47 +674,64 @@ describe('combohandler', function () { ].join('\n'); var SIMPLE_RAW = SIMPLE_IMPORTS_RAW + SIMPLE_URLS_RAW; - describe("route with fully-qualified dynamic path", function () { - before(function () { - var combined = combo.combine({ - webRoot : COMPLEX_ROOT, - rootPath: COMPLEX_ROOT + '/versioned/:version/base/' - }); + function dynamicFiletree(opts) { + var expectedRelativePath = opts.relativePath || "js/a.js"; + var expectedResolvedPath = path.join(COMPLEX_ROOT, opts.realPath, expectedRelativePath); + var expectedRootPath = path.join(COMPLEX_ROOT, opts.rootPath); - app.get("/c/:version/fs-fq", combined, function (req, res, next) { - var rootPath = res.locals.rootPath; - rootPath.should.equal(path.join(COMPLEX_ROOT, '/versioned/deeper/base/')); - next(); - }, combo.respond); + return function (req, res, next) { + var rootPath = res.locals.rootPath; + rootPath.should.equal(expectedRootPath); - app.get("/c/:version/ln-fq", combined, function (req, res, next) { - var rootPath = res.locals.rootPath; - rootPath.should.equal(path.join(COMPLEX_ROOT, '/versioned/shallower/base/')); + var relativePath = res.locals.relativePaths[0]; + relativePath.should.equal(expectedRelativePath); - var relativePath = res.locals.relativePaths[0]; - relativePath.should.equal('js/a.js'); + fs.realpath(path.join(rootPath, relativePath), function (err, resolved) { + assert.ifError(err); + resolved.should.equal(expectedResolvedPath); + next(); + }); + }; + } - fs.realpath(path.join(rootPath, relativePath), function (err, resolved) { - assert.ifError(err); - resolved.should.equal(path.join(COMPLEX_ROOT, '/base/', relativePath)); - next(); - }); - }, combo.respond); + function dynamicSymlinks(opts) { + var expectedTemplateFile = opts.template || TEMPLATE_SIMPLE; + var expectedRelativePath = opts.relativePath || "css/urls/simple.css"; + var expectedResolvedBody = expectedTemplateFile.replace(/__ROOT__/g, opts.rootPath); - app.get("/c/:version/fq-noimports", combined, function (req, res, next) { - var rootPath = res.locals.rootPath; - rootPath.should.equal(path.join(COMPLEX_ROOT, '/versioned/shallower/base/')); + return function (req, res, next) { + var relativePath = res.locals.relativePaths[0]; + relativePath.should.equal(expectedRelativePath); - var relativePath = res.locals.relativePaths[0]; - relativePath.should.equal('css/urls/simple.css'); + // console.error(res.body); + res.body.should.equal(expectedResolvedBody); - // console.error(res.body); - var expected = (SIMPLE_IMPORTS_RAW + TEMPLATE_URLS_SIMPLE) - .replace(/__ROOT__/g, '/versioned/shallower/base/'); - res.body.should.equal(expected); + next(); + }; + } - next(); - }, combo.respond); + describe("route with fully-qualified dynamic path", function () { + before(function () { + var combined = combo.combine({ + webRoot : COMPLEX_ROOT, + rootPath: COMPLEX_ROOT + '/versioned/:version/base/' + }); + + app.get("/c/:version/fs-fq", combined, dynamicFiletree({ + realPath: "/versioned/deeper/base/", + rootPath: "/versioned/deeper/base/" + }), combo.respond); + + app.get("/c/:version/ln-fq", combined, dynamicFiletree({ + realPath: "/base/", + rootPath: "/versioned/shallower/base/" + }), combo.respond); + + app.get("/c/:version/fq-noimports", combined, dynamicSymlinks({ + template: SIMPLE_IMPORTS_RAW + TEMPLATE_URLS_SIMPLE, + realPath: "/versioned/shallower/base/", + rootPath: "/versioned/shallower/base/" + }), combo.respond); }); it("should read rootPath from filesystem directly", assertResponds({ @@ -732,91 +749,117 @@ describe('combohandler', function () { describe("route with one-sided dynamic path", function () { describe("and rootPath symlinked shallower", function () { - before(function () { - var combined = combo.combine({ - rewriteImports: true, - webRoot : COMPLEX_ROOT, - rootPath: COMPLEX_ROOT + '/versioned/shallower/base/' + describe("when resolveSymlinks is true", function () { + before(function () { + var resolved = combo.combine({ + rewriteImports: true, + webRoot : COMPLEX_ROOT, + rootPath: COMPLEX_ROOT + '/versioned/shallower/base/' + }); + + app.get("/r/:version/fs-shallow", resolved, dynamicFiletree({ + realPath: "/base/", + rootPath: "/base/" + }), combo.respond); + + app.get("/r/:version/ln-shallow", resolved, dynamicSymlinks({ + rootPath: "/base/" + }), combo.respond); }); - app.get("/c/:version/fs-shallow", combined, function (req, res, next) { - var rootPath = res.locals.rootPath; - rootPath.should.equal(path.join(COMPLEX_ROOT, '/versioned/shallower/base/')); + it("should resolve files from realpath in filesystem", assertResponds({ + path: "/r/cafebabe/fs-shallow?js/a.js&js/b.js" + })); - var relativePath = res.locals.relativePaths[0]; - relativePath.should.equal('js/a.js'); + it("should rewrite url() through symlink", assertResponds({ + path: "/r/cafebabe/ln-shallow?css/urls/simple.css" + })); + }); - fs.realpath(path.join(rootPath, relativePath), function (err, resolved) { - assert.ifError(err); - resolved.should.equal(path.join(COMPLEX_ROOT, '/base/', relativePath)); - next(); + describe("when resolveSymlinks is false", function () { + before(function () { + var symlinkd = combo.combine({ + rewriteImports: true, + resolveSymlinks: false, + webRoot : COMPLEX_ROOT, + rootPath: COMPLEX_ROOT + '/versioned/shallower/base/' }); - }, combo.respond); - app.get("/c/:version/ln-shallow", combined, function (req, res, next) { - var relativePath = res.locals.relativePaths[0]; - relativePath.should.equal('css/urls/simple.css'); + app.get("/s/:version/fs-shallow", symlinkd, dynamicFiletree({ + realPath: "/base/", + rootPath: "/versioned/shallower/base/" + }), combo.respond); - // console.error(res.body); - var expected = TEMPLATE_SIMPLE - .replace(/__ROOT__/g, '/versioned/shallower/base/'); - res.body.should.equal(expected); - - next(); - }, combo.respond); - }); + app.get("/s/:version/ln-shallow", symlinkd, dynamicSymlinks({ + rootPath: "/versioned/shallower/base/" + }), combo.respond); + }); - it("should resolve files from symlink in filesystem", assertResponds({ - path: "/c/cafebabe/fs-shallow?js/a.js&js/b.js" - })); + it("should resolve files from symlink in filesystem", assertResponds({ + path: "/s/cafebabe/fs-shallow?js/a.js&js/b.js" + })); - it("should rewrite url() through symlink", assertResponds({ - path: "/c/cafebabe/ln-shallow?css/urls/simple.css" - })); + it("should rewrite url() using symlink", assertResponds({ + path: "/s/cafebabe/ln-shallow?css/urls/simple.css" + })); + }); }); describe("and rootPath symlinked deeper", function () { - before(function () { - var combined = combo.combine({ - rewriteImports: true, - webRoot : COMPLEX_ROOT, - rootPath: COMPLEX_ROOT + '/deep-link/' + describe("when resolveSymlinks is true", function () { + before(function () { + var resolved = combo.combine({ + rewriteImports: true, + webRoot : COMPLEX_ROOT, + rootPath: COMPLEX_ROOT + '/deep-link/' + }); + + app.get("/r/:version/fs-deeper", resolved, dynamicFiletree({ + realPath: "/versioned/deeper/base/", + rootPath: "/versioned/deeper/base/" + }), combo.respond); + + app.get("/r/:version/ln-deeper", resolved, dynamicSymlinks({ + rootPath: "/versioned/deeper/base/" + }), combo.respond); }); - app.get("/c/:version/fs-deeper", combined, function (req, res, next) { - var rootPath = res.locals.rootPath; - rootPath.should.equal(path.join(COMPLEX_ROOT, '/deep-link/')); + it("should read rootPath from filesystem directly", assertResponds({ + path: "/r/cafebabe/fs-deeper?js/a.js&js/b.js" + })); - var relativePath = res.locals.relativePaths[0]; - relativePath.should.equal('js/a.js'); + it("should *still* rewrite url() through symlink", assertResponds({ + path: "/r/cafebabe/ln-deeper?css/urls/simple.css" + })); + }); - fs.realpath(path.join(rootPath, relativePath), function (err, resolved) { - assert.ifError(err); - resolved.should.equal(path.join(COMPLEX_ROOT, '/versioned/deeper/base/', relativePath)); - next(); + describe("when resolveSymlinks is false", function () { + before(function () { + var symlinkd = combo.combine({ + rewriteImports: true, + resolveSymlinks: false, + webRoot : COMPLEX_ROOT, + rootPath: COMPLEX_ROOT + '/deep-link/' }); - }, combo.respond); - app.get("/c/:version/ln-deeper", combined, function (req, res, next) { - var relativePath = res.locals.relativePaths[0]; - relativePath.should.equal('css/urls/simple.css'); + app.get("/s/:version/fs-deeper", symlinkd, dynamicFiletree({ + realPath: "/versioned/deeper/base/", + rootPath: "/deep-link/" + }), combo.respond); - // console.error(res.body); - var expected = TEMPLATE_SIMPLE - .replace(/__ROOT__/g, '/deep-link/'); - res.body.should.equal(expected); - - next(); - }, combo.respond); - }); + app.get("/s/:version/ln-deeper", symlinkd, dynamicSymlinks({ + rootPath: "/deep-link/" + }), combo.respond); + }); - it("should read rootPath from filesystem directly", assertResponds({ - path: "/c/cafebabe/fs-deeper?js/a.js&js/b.js" - })); + it("should read rootPath from symlink in filesystem", assertResponds({ + path: "/s/cafebabe/fs-deeper?js/a.js&js/b.js" + })); - it("should *still* rewrite url() through symlink", assertResponds({ - path: "/c/cafebabe/ln-deeper?css/urls/simple.css" - })); + it("should *still* rewrite url() using symlink", assertResponds({ + path: "/s/cafebabe/ln-deeper?css/urls/simple.css" + })); + }); }); }); });