Permalink
Browse files

first commit

  • Loading branch information...
0 parents commit 9ea9f3fab11fb964a35633bdac5717b07bd19090 @eldargab committed May 12, 2012
Showing with 249 additions and 0 deletions.
  1. +1 −0 .gitignore
  2. +106 −0 lib/middleware.js
  3. +9 −0 package.json
  4. +106 −0 test/middleware.js
  5. +27 −0 test/support/fake-stat.js
@@ -0,0 +1 @@
+node_modules/
@@ -0,0 +1,106 @@
+var parseUrl = require('url').parse
+var PATH = require('path')
+var STATUS_CODES = require('http').STATUS_CODES
+var fs = require('fs')
+
+module.exports = function extensions (opts) {
+ opts = opts || {}
+ opts.root = opts.root || process.cwd()
+ opts.exts = opts.exts || {}
+
+ function Options (req, res, next) {
+ this.req = req
+ this.res = res
+ this.next = next
+ }
+
+ Options.prototype = opts
+
+ return function (req, res, next) {
+ var path = decodeUri(parseUrl(req.url).pathname)
+
+ if (path instanceof URIError) return next(error(400))
+
+ if (~path.indexOf('\0')) return next(error(400))
+
+ path = PATH.normalize(PATH.join(opts.root, path))
+
+ if (0 != path.indexOf(opts.root)) return next(error(403))
+
+ handle(path, new Options(req, res, next))
+ }
+}
+
+function handle (path, opts) {
+ debugger
+ fs.stat(path, function (error, stat) {
+ if (error && error.code != 'ENOENT') return opts.next(error)
+
+ if (error && error.code == 'ENOENT') {
+ lookupExts(path, Object.keys(opts.exts), function (err, file) {
+ if (err) return opts.next(err)
+ if (!file) return opts.next()
+ handleFile(file, opts)
+ })
+ return
+ }
+
+ if (stat.isDirectory()) {
+ handle(PATH.join(path, 'index'), opts)
+ return
+ }
+
+ handleFile(path, opts)
+ })
+}
+
+function handleFile (file, opts) {
+ var ext = PATH.extname(file)
+ var handler = opts.exts[ext]
+ if (!handler) return opts.next()
+ handler(file, opts.req, opts.res, opts)
+}
+
+function lookupExts (path, exts, cb) {
+ var done = false
+ var checked = 0
+
+ function onstat (p) {
+ return function statCb (error, stat) {
+ if (done) return
+ if (error && error.code != 'ENOENT') {
+ done = true
+ cb(error)
+ return
+ }
+ if (error) {
+ checked++
+ if (checked == exts.length) return cb()
+ return
+ }
+ done = true
+ cb(null, p)
+ }
+ }
+
+ exts.forEach(function (ext) {
+ var p = path + ext
+ fs.stat(p, onstat(p))
+ })
+}
+
+
+function decodeUri (uri) {
+ try {
+ return decodeURIComponent(uri)
+ }
+ catch (e) {
+ return e
+ }
+}
+
+function error (code) {
+ var err = new Error(STATUS_CODES[code])
+ err.status = code
+ return err
+}
@@ -0,0 +1,9 @@
+{
+ "author": "Eldar Gabdullin <eldargab@gmail.com>",
+ "name": "connect-extensions",
+ "description": "Serve files with specified handlers with possibility to omit extensions",
+ "version": "0.0.0",
+ "main": "lib/middleware",
+ "dependencies": {},
+ "devDependencies": {}
+}
@@ -0,0 +1,106 @@
+var chai = require('chai')
+chai.use(require('sinon-chai'))
+chai.should()
+var sinon = require('sinon')
+var fs = require('fs')
+var statNative = fs.stat
+var FakeStat = require('./support/fake-stat')
+var Middleware = require('../lib/middleware')
+
+function Next () {
+ var spy = sinon.spy()
+
+ spy.nextMiddleware = function () {
+ this.should.have.been.calledWithExactly()
+ }
+
+ spy.error = function (code) {
+ var arg = this.firstCall.args[0]
+ arg.should.be.an.instanceof(Error)
+ arg.status.should.equal(code)
+ }
+
+ spy.notCalled = function () {
+ this.should.not.have.been.called
+ }
+
+ return spy
+}
+
+
+describe('Middleware', function () {
+ var m, next, exts, req, res, options
+
+ afterEach(function () {
+ fs.stat = statNative
+ })
+
+ beforeEach(function () {
+ exts = {
+ '.jade': sinon.spy(),
+ '.md': sinon.spy()
+ }
+ m = Middleware({
+ root: 'root',
+ exts: exts,
+ option: 'option'
+ })
+ fs.stat = FakeStat([
+ 'root/',
+ 'root/index.jade',
+ 'root/article.md',
+ 'root/path with spaces.md',
+ 'root/subdir/doc.jade'
+ ])
+ next = Next()
+ req = {}
+ res = {}
+ })
+
+ function test (url) {
+ req.url = url
+ m(req, res, next)
+ }
+
+ it('Should pass control to next middleware on non-existent path', function () {
+ test('/non-existent/path')
+ next.nextMiddleware()
+ })
+
+ it('Should be able to determine file extension and handler', function () {
+ test('/article')
+ next.notCalled()
+ exts['.md'].should.have.been.calledWith('root/article.md')
+ })
+
+ it('Should pass filename, req, res, and passed options to handler', function () {
+ test('/article')
+ exts['.md'].should.have.been.calledWith('root/article.md', req, res)
+ var options = exts['.md'].firstCall.args[3]
+ options.option.should.equal('option')
+ })
+
+ it('Should support index files in dirs', function () {
+ test('/')
+ next.notCalled()
+ exts['.jade'].should.have.been.calledWith('root/index.jade')
+ })
+
+ it('Should support urlencoded paths', function () {
+ test('/path%20with%20spaces.md')
+ next.notCalled()
+ exts['.md'].should.have.been.calledWith('root/path with spaces.md')
+ })
+
+ it('Should respond with 403 Forbidden when traversing root', function () {
+ test('/malicious/%2e%2e/%2e%2e/path')
+ next.error(403)
+ })
+
+ it('Should pass fs.stat errors to next middleware', function () {
+ var error = new Error
+ fs.stat.withArgs('root/runtime/error').yields(error)
+ test('/runtime/error')
+ next.should.have.been.calledWithExactly(error)
+ })
+})
@@ -0,0 +1,27 @@
+var sinon = require('sinon')
+var extname = require('path').extname
+var normalize = require('path').normalize
+
+module.exports = function (paths) {
+ var stub = sinon.stub()
+ stub.yields(FsError('ENOENT'))
+ paths.forEach(function (p) {
+ var stats = new Stat(!extname(p))
+ stub.withArgs(normalize(p)).yields(null, stats)
+ })
+ return stub
+}
+
+function Stat (isDir) {
+ this._isDir = isDir
+}
+
+Stat.prototype.isDirectory = function () {
+ return !!this._isDir
+}
+
+function FsError (code) {
+ var err = new Error
+ err.code = code
+ return err
+}

0 comments on commit 9ea9f3f

Please sign in to comment.