├── test ├── fixtures │ ├── foo bar │ ├── nums │ ├── .hidden │ ├── todo.txt │ └── users │ │ ├── tobi.txt │ │ └── index.html ├── mocha.opts ├── app.listen.js ├── responseTime.js ├── shared │ └── index.js ├── query.js ├── limit.js ├── exports.js ├── urlencoded.js ├── server.js ├── utils.js ├── vhost.js ├── cookieParser.js ├── compress.js ├── patch.js ├── timeout.js ├── support │ └── http.js ├── basicAuth.js ├── mounting.js ├── json.js ├── bodyParser.js ├── static.js └── multipart.js ├── .travis.yml ├── lib ├── public │ ├── favicon.ico │ ├── icons │ │ ├── page.png │ │ ├── page_add.png │ │ ├── page_go.png │ │ ├── page_key.png │ │ ├── page_red.png │ │ ├── page_code.png │ │ ├── page_copy.png │ │ ├── page_edit.png │ │ ├── page_error.png │ │ ├── page_excel.png │ │ ├── page_find.png │ │ ├── page_gear.png │ │ ├── page_green.png │ │ ├── page_link.png │ │ ├── page_paste.png │ │ ├── page_save.png │ │ ├── page_white.png │ │ ├── page_word.png │ │ ├── page_world.png │ │ ├── page_attach.png │ │ ├── page_delete.png │ │ ├── page_refresh.png │ │ ├── page_white_c.png │ │ ├── page_white_cd.png │ │ ├── page_white_go.png │ │ ├── page_white_h.png │ │ ├── page_lightning.png │ │ ├── page_paintbrush.png │ │ ├── page_white_add.png │ │ ├── page_white_code.png │ │ ├── page_white_copy.png │ │ ├── page_white_cup.png │ │ ├── page_white_dvd.png │ │ ├── page_white_edit.png │ │ ├── page_white_find.png │ │ ├── page_white_gear.png │ │ ├── page_white_get.png │ │ ├── page_white_key.png │ │ ├── page_white_link.png │ │ ├── page_white_php.png │ │ ├── page_white_put.png │ │ ├── page_white_ruby.png │ │ ├── page_white_star.png │ │ ├── page_white_text.png │ │ ├── page_white_tux.png │ │ ├── page_white_word.png │ │ ├── page_white_zip.png │ │ ├── page_white_acrobat.png │ │ ├── page_white_camera.png │ │ ├── page_white_csharp.png │ │ ├── page_white_delete.png │ │ ├── page_white_error.png │ │ ├── page_white_excel.png │ │ ├── page_white_flash.png │ │ ├── page_white_magnify.png │ │ ├── page_white_medal.png │ │ ├── page_white_office.png │ │ ├── page_white_paint.png │ │ ├── page_white_paste.png │ │ ├── page_white_picture.png │ │ ├── page_white_stack.png │ │ ├── page_white_swoosh.png │ │ ├── page_white_vector.png │ │ ├── page_white_width.png │ │ ├── page_white_world.png │ │ ├── page_white_wrench.png │ │ ├── page_white_code_red.png │ │ ├── page_white_cplusplus.png │ │ ├── page_white_database.png │ │ ├── page_white_freehand.png │ │ ├── page_white_lightning.png │ │ ├── page_white_actionscript.png │ │ ├── page_white_coldfusion.png │ │ ├── page_white_compressed.png │ │ ├── page_white_horizontal.png │ │ ├── page_white_paintbrush.png │ │ ├── page_white_powerpoint.png │ │ ├── page_white_text_width.png │ │ └── page_white_visualstudio.png │ ├── error.html │ ├── directory.html │ └── style.css ├── middleware │ ├── responseTime.js │ ├── query.js │ ├── methodOverride.js │ ├── vhost.js │ ├── limit.js │ ├── timeout.js │ ├── cookieParser.js │ ├── bodyParser.js │ ├── urlencoded.js │ ├── csrf.js │ ├── json.js │ ├── session │ │ ├── store.js │ │ ├── cookie.js │ │ ├── memory.js │ │ └── session.js │ ├── favicon.js │ ├── static.js │ ├── errorHandler.js │ ├── basicAuth.js │ ├── multipart.js │ ├── cookieSession.js │ ├── compress.js │ ├── directory.js │ ├── staticCache.js │ └── logger.js ├── cache.js ├── patch.js ├── connect.js ├── index.js ├── proto.js └── utils.js ├── examples ├── public │ ├── tobi.jpeg │ └── form.html ├── favicon.js ├── logger.js ├── helloworld.js ├── logger.fast.js ├── static.js ├── profiler.js ├── error.js ├── basicAuth.js ├── mounting.js ├── upload.js ├── vhost.js ├── csrf.js ├── bodyParser.js ├── limit.js ├── cookieSession.js ├── logger.format.js └── session.js ├── index.js ├── .npmignore ├── .gitignore ├── support ├── bench ├── app.js ├── docs.js └── docs.jade ├── docs ├── docs.js └── style.css ├── Makefile ├── package.json ├── LICENSE └── Readme.md /test/fixtures/foo bar: -------------------------------------------------------------------------------- 1 | baz -------------------------------------------------------------------------------- /test/fixtures/nums: -------------------------------------------------------------------------------- 1 | 123456789 -------------------------------------------------------------------------------- /test/fixtures/.hidden: -------------------------------------------------------------------------------- 1 | I am hidden -------------------------------------------------------------------------------- /test/fixtures/todo.txt: -------------------------------------------------------------------------------- 1 | - groceries -------------------------------------------------------------------------------- /test/fixtures/users/tobi.txt: -------------------------------------------------------------------------------- 1 | ferret -------------------------------------------------------------------------------- /test/fixtures/users/index.html: -------------------------------------------------------------------------------- 1 |

tobi, loki, jane

-------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.6 4 | - 0.8 5 | - 0.9 -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --require test/support/http 3 | --growl -------------------------------------------------------------------------------- /lib/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/favicon.ico -------------------------------------------------------------------------------- /examples/public/tobi.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/examples/public/tobi.jpeg -------------------------------------------------------------------------------- /lib/public/icons/page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page.png -------------------------------------------------------------------------------- /lib/public/icons/page_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_add.png -------------------------------------------------------------------------------- /lib/public/icons/page_go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_go.png -------------------------------------------------------------------------------- /lib/public/icons/page_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_key.png -------------------------------------------------------------------------------- /lib/public/icons/page_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_red.png -------------------------------------------------------------------------------- /lib/public/icons/page_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_code.png -------------------------------------------------------------------------------- /lib/public/icons/page_copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_copy.png -------------------------------------------------------------------------------- /lib/public/icons/page_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_edit.png -------------------------------------------------------------------------------- /lib/public/icons/page_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_error.png -------------------------------------------------------------------------------- /lib/public/icons/page_excel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_excel.png -------------------------------------------------------------------------------- /lib/public/icons/page_find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_find.png -------------------------------------------------------------------------------- /lib/public/icons/page_gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_gear.png -------------------------------------------------------------------------------- /lib/public/icons/page_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_green.png -------------------------------------------------------------------------------- /lib/public/icons/page_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_link.png -------------------------------------------------------------------------------- /lib/public/icons/page_paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_paste.png -------------------------------------------------------------------------------- /lib/public/icons/page_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_save.png -------------------------------------------------------------------------------- /lib/public/icons/page_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white.png -------------------------------------------------------------------------------- /lib/public/icons/page_word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_word.png -------------------------------------------------------------------------------- /lib/public/icons/page_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_world.png -------------------------------------------------------------------------------- /lib/public/icons/page_attach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_attach.png -------------------------------------------------------------------------------- /lib/public/icons/page_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_delete.png -------------------------------------------------------------------------------- /lib/public/icons/page_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_refresh.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_c.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_cd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_cd.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_go.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_h.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = process.env.CONNECT_COV 3 | ? require('./lib-cov/connect') 4 | : require('./lib/connect'); -------------------------------------------------------------------------------- /lib/public/icons/page_lightning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_lightning.png -------------------------------------------------------------------------------- /lib/public/icons/page_paintbrush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_paintbrush.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_add.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_code.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_copy.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_cup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_cup.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_dvd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_dvd.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_edit.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_find.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_gear.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_get.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_get.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_key.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_link.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_php.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_put.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_put.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_ruby.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_star.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_text.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_tux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_tux.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_word.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_zip.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_acrobat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_acrobat.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_camera.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_csharp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_csharp.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_delete.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_error.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_excel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_excel.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_flash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_flash.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_magnify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_magnify.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_medal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_medal.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_office.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_office.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_paint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_paint.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_paste.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_picture.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_stack.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_swoosh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_swoosh.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_vector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_vector.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_width.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_world.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_wrench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_wrench.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_code_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_code_red.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_cplusplus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_cplusplus.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_database.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_freehand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_freehand.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_lightning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_lightning.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_actionscript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_actionscript.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_coldfusion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_coldfusion.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_compressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_compressed.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_horizontal.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_paintbrush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_paintbrush.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_powerpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_powerpoint.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_text_width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_text_width.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_visualstudio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/connect/master/lib/public/icons/page_white_visualstudio.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.markdown 2 | *.md 3 | .git* 4 | Makefile 5 | benchmarks/ 6 | docs/ 7 | examples/ 8 | install.sh 9 | support/ 10 | test/ 11 | .DS_Store 12 | coverage.html 13 | -------------------------------------------------------------------------------- /examples/favicon.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | // $ curl -i http://localhost:3000/favicon.ico 9 | 10 | connect.createServer( 11 | connect.favicon() 12 | ).listen(3000); -------------------------------------------------------------------------------- /examples/public/form.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
-------------------------------------------------------------------------------- /examples/logger.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | // $ curl http://localhost:3000/favicon.ico 9 | 10 | connect.createServer( 11 | connect.logger() 12 | , connect.favicon() 13 | ).listen(3000); 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.html 2 | .DS_Store 3 | pids 4 | logs 5 | results 6 | *.pid 7 | *.gz 8 | *.log 9 | lib-cov 10 | test/fixtures/foo.bar.baz.css 11 | test/fixtures/style.css 12 | test/fixtures/script.js 13 | test.js 14 | docs/*.html 15 | docs/*.json 16 | node_modules 17 | .idea 18 | -------------------------------------------------------------------------------- /examples/helloworld.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | connect.createServer(function(req, res){ 9 | var body = 'Hello World'; 10 | res.setHeader('Content-Length', body.length); 11 | res.end(body); 12 | }).listen(3000); -------------------------------------------------------------------------------- /examples/logger.fast.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | // $ curl -i http://localhost:3000/favicon.ico 9 | // true defaults to 1000ms 10 | 11 | connect.createServer( 12 | connect.logger({ buffer: 5000 }) 13 | , connect.favicon() 14 | ).listen(3000); -------------------------------------------------------------------------------- /lib/public/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {error} 4 | 5 | 6 | 7 |
8 |

{title}

9 |

{statusCode} {error}

10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/static.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | connect( 9 | connect.static(__dirname + '/public', { maxAge: 0 }) 10 | , function(req, res) { 11 | res.setHeader('Content-Type', 'text/html'); 12 | res.end('') 13 | } 14 | ).listen(3000); -------------------------------------------------------------------------------- /examples/profiler.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | // $ curl -i http://localhost:3000/ 9 | 10 | connect( 11 | connect.profiler() 12 | , connect.favicon() 13 | , connect.static(__dirname) 14 | , function(req, res, next){ 15 | res.end('hello world'); 16 | } 17 | ).listen(3000); 18 | -------------------------------------------------------------------------------- /support/bench: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | node ./support/app & 4 | pid=$! 5 | 6 | bench() { 7 | ab -n 5000 -c 50 -k -q http://127.0.0.1:8000$1 \ 8 | | grep "Requests per" \ 9 | | cut -d ' ' -f 7 \ 10 | | xargs echo "$2:" 11 | } 12 | 13 | sleep .5 14 | bench /hello "Hello World" 15 | bench /10 "10 Middleware" 16 | bench /40 "40 Middleware" 17 | 18 | kill -9 $pid -------------------------------------------------------------------------------- /test/app.listen.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | 4 | describe('app.listen()', function(){ 5 | it('should wrap in an http.Server', function(done){ 6 | var app = connect(); 7 | 8 | app.use(function(req, res){ 9 | res.end(); 10 | }); 11 | 12 | app.listen(5555, function(){ 13 | app 14 | .request('/') 15 | .expect(200, done); 16 | }); 17 | }) 18 | }) -------------------------------------------------------------------------------- /examples/error.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../') 3 | , http = require('http'); 4 | 5 | // try: 6 | // - viewing in a browser 7 | // - curl http://localhost:3000 8 | // - curl -H "Accept: application/json" http://localhost:3000 9 | 10 | var app = connect() 11 | .use(function(req, res, next){ 12 | var err = new Error('oh noes!'); 13 | err.number = 7; 14 | throw err; 15 | }) 16 | .use(connect.errorHandler()); 17 | 18 | http.Server(app).listen(3000); 19 | console.log('Server started on port 3000'); 20 | -------------------------------------------------------------------------------- /test/responseTime.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | 4 | var app = connect(); 5 | 6 | app.use(connect.responseTime()); 7 | 8 | app.use(function(req, res){ 9 | setTimeout(function(){ 10 | res.end(); 11 | }, 30); 12 | }); 13 | 14 | describe('connect.responseTime()', function(){ 15 | it('should set X-Response-Time', function(done){ 16 | app.request() 17 | .get('/') 18 | .end(function(res){ 19 | var n = parseInt(res.headers['x-response-time']); 20 | n.should.be.above(20); 21 | done(); 22 | }); 23 | }) 24 | }) -------------------------------------------------------------------------------- /test/shared/index.js: -------------------------------------------------------------------------------- 1 | 2 | var bytes = require('bytes'); 3 | 4 | exports['default request body'] = function(app){ 5 | it('should default to {}', function(done){ 6 | app.request() 7 | .post('/') 8 | .end(function(res){ 9 | res.body.should.equal('{}'); 10 | done(); 11 | }) 12 | }) 13 | }; 14 | 15 | exports['limit body to'] = function(size, type, app){ 16 | it('should accept a limit option', function(done){ 17 | app.request() 18 | .post('/') 19 | .set('Content-Length', bytes(size) + 1) 20 | .set('Content-Type', type) 21 | .end(function(res){ 22 | res.should.have.status(413); 23 | done(); 24 | }) 25 | }) 26 | } -------------------------------------------------------------------------------- /examples/basicAuth.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | function auth(user, pass) { 9 | return 'tj' == user && 'tobi' == pass; 10 | } 11 | 12 | function authorized(req, res) { 13 | res.end('authorized!'); 14 | } 15 | 16 | function hello(req, res) { 17 | res.end('hello! try /admin'); 18 | } 19 | 20 | // apply globally 21 | 22 | connect( 23 | connect.basicAuth(auth) 24 | , authorized 25 | ).listen(3000); 26 | 27 | // apply to /admin/* only 28 | 29 | var server = connect(); 30 | 31 | server.use('/admin', connect.basicAuth(auth)); 32 | server.use('/admin', authorized); 33 | server.use(hello); 34 | 35 | server.listen(3001); -------------------------------------------------------------------------------- /docs/docs.js: -------------------------------------------------------------------------------- 1 | 2 | $(function(){ 3 | $('code').each(function(){ 4 | $(this).html(highlight($(this).text())); 5 | }); 6 | }); 7 | 8 | function highlight(js) { 9 | return js 10 | .replace(//g, '>') 12 | .replace(/\/\/(.*)/gm, '//$1') 13 | .replace(/('.*?')/gm, '$1') 14 | .replace(/(\d+\.\d+)/gm, '$1') 15 | .replace(/(\d+)/gm, '$1') 16 | .replace(/\bnew *(\w+)/gm, 'new $1') 17 | .replace(/\b(function|new|throw|return|var|if|else)\b/gm, '$1') 18 | } -------------------------------------------------------------------------------- /test/query.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | 4 | var app = connect(); 5 | 6 | app.use(connect.query()); 7 | 8 | app.use(function(req, res){ 9 | res.end(JSON.stringify(req.query)); 10 | }); 11 | 12 | describe('connect.query()', function(){ 13 | it('should parse the query-string', function(done){ 14 | app.request() 15 | .get('/?user[name]=tobi') 16 | .end(function(res){ 17 | res.body.should.equal('{"user":{"name":"tobi"}}'); 18 | done(); 19 | }); 20 | }) 21 | 22 | it('should default to {}', function(done){ 23 | app.request() 24 | .get('/') 25 | .end(function(res){ 26 | res.body.should.equal('{}'); 27 | done(); 28 | }); 29 | }) 30 | }) -------------------------------------------------------------------------------- /lib/middleware/responseTime.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - responseTime 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Reponse time: 10 | * 11 | * Adds the `X-Response-Time` header displaying the response 12 | * duration in milliseconds. 13 | * 14 | * @return {Function} 15 | * @api public 16 | */ 17 | 18 | module.exports = function responseTime(){ 19 | return function(req, res, next){ 20 | var start = new Date; 21 | 22 | if (res._responseTime) return next(); 23 | res._responseTime = true; 24 | 25 | res.on('header', function(){ 26 | var duration = new Date - start; 27 | res.setHeader('X-Response-Time', duration + 'ms'); 28 | }); 29 | 30 | next(); 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /test/limit.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | 4 | var app = connect(); 5 | 6 | app.use(connect.limit('5kb')); 7 | 8 | app.use(function(req, res){ 9 | res.end('stuff'); 10 | }); 11 | 12 | describe('connect.limit()', function(){ 13 | describe('when Content-Length is below', function(){ 14 | it('should bypass limit()', function(done){ 15 | app.request() 16 | .post('/') 17 | .set('Content-Length', 500) 18 | .expect(200, done); 19 | }) 20 | }) 21 | 22 | describe('when Content-Length is too large', function(){ 23 | it('should respond with 413', function(done){ 24 | app.request() 25 | .post('/') 26 | .set('Content-Length', 10 * 1024) 27 | .expect(413, done); 28 | }) 29 | }) 30 | }) -------------------------------------------------------------------------------- /examples/mounting.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | var blog = connect( 9 | connect.router(function(app){ 10 | app.get('/', function(req, res){ 11 | res.end('list blog posts. try /post/0'); 12 | }); 13 | 14 | app.get('/post/:id', function(req, res){ 15 | res.end('got post ' + req.params.id); 16 | }); 17 | }) 18 | ); 19 | 20 | var admin = connect( 21 | connect.basicAuth(function(user, pass){ return 'tj' == user && 'tobi' == pass }) 22 | , function(req, res){ 23 | res.end('admin stuff'); 24 | } 25 | ); 26 | 27 | connect() 28 | .use('/admin', admin) 29 | .use('/blog', blog) 30 | .use(function(req, res){ 31 | res.end('try /blog, /admin, or /blog/post/0'); 32 | }) 33 | .listen(3000); -------------------------------------------------------------------------------- /examples/upload.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | connect() 9 | .use(connect.bodyParser()) 10 | .use(form) 11 | .use(upload) 12 | .listen(3000); 13 | 14 | function form(req, res, next) { 15 | if ('GET' !== req.method) return next(); 16 | res.setHeader('Content-Type', 'text/html'); 17 | res.end('
' 18 | + '' 19 | + '' 20 | + '
'); 21 | } 22 | 23 | function upload(req, res, next) { 24 | if ('POST' !== req.method) return next(); 25 | req.files.images.forEach(function(file){ 26 | console.log(' uploaded : %s %skb', file.name, file.size / 1024 | 0); 27 | }); 28 | } -------------------------------------------------------------------------------- /test/exports.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | 4 | describe('exports', function(){ 5 | describe('.version', function(){ 6 | it('should be a string', function(){ 7 | connect.version.should.be.a('string'); 8 | }) 9 | }) 10 | 11 | describe('.middleware', function(){ 12 | it('should lazy-load middleware', function(){ 13 | connect.middleware.cookieParser.should.be.a('function'); 14 | connect.middleware.bodyParser.should.be.a('function'); 15 | connect.middleware.static.should.be.a('function'); 16 | }) 17 | }) 18 | 19 | describe('.NAME', function(){ 20 | it('should lazy-load middleware', function(){ 21 | connect.cookieParser.should.be.a('function'); 22 | connect.bodyParser.should.be.a('function'); 23 | connect.static.should.be.a('function'); 24 | }) 25 | }) 26 | }) -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | TESTS = test/*.js 3 | REPORTER = dot 4 | DOX = ./node_modules/.bin/dox 5 | 6 | SRC = $(shell find lib/*.js lib/middleware/*.js) 7 | HTML = $(SRC:.js=.html) 8 | 9 | test: 10 | @NODE_ENV=test ./node_modules/.bin/mocha \ 11 | --reporter $(REPORTER) \ 12 | --timeout 600 \ 13 | $(TESTS) 14 | 15 | docs: $(HTML) 16 | @mv $(HTML) docs 17 | 18 | test-cov: lib-cov 19 | @CONNECT_COV=1 $(MAKE) test REPORTER=html-cov > coverage.html 20 | 21 | lib-cov: 22 | @jscoverage lib $@ 23 | 24 | %.html: %.js 25 | $(DOX) < $< | node support/docs > $@ 26 | 27 | docclean: 28 | rm -f $(HTML) 29 | 30 | site: docclean docs 31 | rm -fr /tmp/docs \ 32 | && cp -fr docs /tmp/docs \ 33 | && git checkout gh-pages \ 34 | && cp -fr /tmp/docs/* . \ 35 | && echo "done" 36 | 37 | benchmark: 38 | @./support/bench 39 | 40 | .PHONY: test-cov site docs test docclean benchmark -------------------------------------------------------------------------------- /examples/vhost.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | var account = connect(function(req, res){ 9 | var location = 'http://localhost:3000/account/' + req.subdomains[0]; 10 | res.statusCode = 302; 11 | res.setHeader('Location', location); 12 | res.end('Moved to ' + location); 13 | }); 14 | 15 | var blog = connect(function(req, res){ 16 | res.end('blog app'); 17 | }); 18 | 19 | var main = connect( 20 | connect.router(function(app){ 21 | app.get('/account/:user', function(req, res){ 22 | res.end('viewing user account for ' + req.params.user); 23 | }); 24 | 25 | app.get('/', function(req, res){ 26 | res.end('main app'); 27 | }); 28 | }) 29 | ); 30 | 31 | connect( 32 | connect.logger() 33 | , connect.vhost('blog.localhost', blog) 34 | , connect.vhost('*.localhost', account) 35 | , main 36 | ).listen(3000); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect", 3 | "version": "2.7.0", 4 | "description": "High performance middleware framework", 5 | "keywords": ["framework", "web", "middleware", "connect", "rack"], 6 | "repository": "git://github.com/senchalabs/connect.git", 7 | "author": "TJ Holowaychuk (http://tjholowaychuk.com)", 8 | "dependencies": { 9 | "qs": "0.5.1", 10 | "formidable": "1.0.11", 11 | "cookie-signature": "0.0.1", 12 | "crc": "0.2.0", 13 | "cookie": "0.0.5", 14 | "bytes": "0.0.1", 15 | "send": "0.1.0", 16 | "bytes": "0.1.0", 17 | "fresh": "0.1.0", 18 | "pause": "0.0.1", 19 | "debug": "*" 20 | }, 21 | "devDependencies": { 22 | "should": "*", 23 | "mocha": "*", 24 | "jade": "*", 25 | "dox": "*" 26 | }, 27 | "main": "index", 28 | "engines": { 29 | "node": ">= 0.5.0" 30 | }, 31 | "scripts": { 32 | "test": "make" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /support/app.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('..') 3 | , app = connect(); 4 | 5 | app.use(function(req, res, next){ 6 | if ('/hello' == req.url) { 7 | res.setHeader('Content-Length', 5); 8 | res.end('Hello'); 9 | } else { 10 | next(); 11 | } 12 | }); 13 | 14 | var n = 9; 15 | while (n--) { 16 | app.use(function(req, res, next){ 17 | next(); 18 | }); 19 | } 20 | 21 | app.use(function(req, res, next){ 22 | if ('/10' == req.url) { 23 | res.setHeader('Content-Length', 5); 24 | res.end('Hello'); 25 | } else { 26 | next(); 27 | } 28 | }); 29 | 30 | // NOTE: you would never really have this many 31 | // but it helps show the dispatcher perf 32 | 33 | var n = 29; 34 | while (n--) { 35 | app.use(function(req, res, next){ 36 | next(); 37 | }); 38 | } 39 | 40 | app.use(function(req, res, next){ 41 | if ('/40' == req.url) { 42 | res.setHeader('Content-Length', 5); 43 | res.end('Hello'); 44 | } else { 45 | next(); 46 | } 47 | }); 48 | 49 | app.listen(8000); -------------------------------------------------------------------------------- /lib/middleware/query.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Connect - query 3 | * Copyright(c) 2011 TJ Holowaychuk 4 | * Copyright(c) 2011 Sencha Inc. 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var qs = require('qs') 13 | , parse = require('../utils').parseUrl; 14 | 15 | /** 16 | * Query: 17 | * 18 | * Automatically parse the query-string when available, 19 | * populating the `req.query` object. 20 | * 21 | * Examples: 22 | * 23 | * connect() 24 | * .use(connect.query()) 25 | * .use(function(req, res){ 26 | * res.end(JSON.stringify(req.query)); 27 | * }); 28 | * 29 | * The `options` passed are provided to qs.parse function. 30 | * 31 | * @param {Object} options 32 | * @return {Function} 33 | * @api public 34 | */ 35 | 36 | module.exports = function query(options){ 37 | return function query(req, res, next){ 38 | if (!req.query) { 39 | req.query = ~req.url.indexOf('?') 40 | ? qs.parse(parse(req).query, options) 41 | : {}; 42 | } 43 | 44 | next(); 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /examples/csrf.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../') 7 | , http = require('http'); 8 | 9 | var form = '\n\ 10 |
\n\ 11 | \n\ 12 | \n\ 13 | \n\ 14 |
\n\ 15 | '; 16 | 17 | var app = connect() 18 | .use(connect.cookieParser()) 19 | .use(connect.session({ secret: 'keyboard cat' })) 20 | .use(connect.bodyParser()) 21 | .use(connect.csrf()) 22 | .use(function(req, res, next){ 23 | if ('POST' != req.method) return next(); 24 | req.session.user = req.body.user; 25 | next(); 26 | }) 27 | .use(function(req, res){ 28 | res.setHeader('Content-Type', 'text/html'); 29 | var body = form 30 | .replace('{token}', req.session._csrf) 31 | .replace('{user}', req.session.user && req.session.user.name || ''); 32 | res.end(body); 33 | }); 34 | 35 | http.createServer(app).listen(3000); 36 | console.log('Server listening on port 3000'); 37 | -------------------------------------------------------------------------------- /lib/middleware/methodOverride.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - methodOverride 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Method Override: 11 | * 12 | * Provides faux HTTP method support. 13 | * 14 | * Pass an optional `key` to use when checking for 15 | * a method override, othewise defaults to _\_method_. 16 | * The original method is available via `req.originalMethod`. 17 | * 18 | * @param {String} key 19 | * @return {Function} 20 | * @api public 21 | */ 22 | 23 | module.exports = function methodOverride(key){ 24 | key = key || "_method"; 25 | return function methodOverride(req, res, next) { 26 | req.originalMethod = req.originalMethod || req.method; 27 | 28 | // req.body 29 | if (req.body && key in req.body) { 30 | req.method = req.body[key].toUpperCase(); 31 | delete req.body[key]; 32 | // check X-HTTP-Method-Override 33 | } else if (req.headers['x-http-method-override']) { 34 | req.method = req.headers['x-http-method-override'].toUpperCase(); 35 | } 36 | 37 | next(); 38 | }; 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /examples/bodyParser.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../') 3 | , http = require('http'); 4 | 5 | // visit form.html 6 | 7 | var app = connect() 8 | .use(connect.static(__dirname + '/public')) 9 | .use(connect.bodyParser()) 10 | .use(form) 11 | .use(upload); 12 | 13 | function form(req, res, next){ 14 | if ('GET' != req.method) return next(); 15 | res.statusCode = 302; 16 | res.setHeader('Location', 'form.html'); 17 | res.end(); 18 | } 19 | 20 | function upload(req, res){ 21 | res.setHeader('Content-Type', 'text/html'); 22 | res.write('

thanks ' + req.body.name + '

'); 23 | res.write(''); 37 | } 38 | 39 | http.Server(app).listen(3000); 40 | console.log('Server started on port 3000'); -------------------------------------------------------------------------------- /test/urlencoded.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../') 3 | , should = require('./shared'); 4 | 5 | var app = connect(); 6 | 7 | app.use(connect.urlencoded({ limit: '1mb' })); 8 | 9 | app.use(function(req, res){ 10 | res.end(JSON.stringify(req.body)); 11 | }); 12 | 13 | describe('connect.urlencoded()', function(){ 14 | should['default request body'](app); 15 | should['limit body to']('1mb', 'application/x-www-form-urlencoded', app); 16 | 17 | it('should support all http methods', function(done){ 18 | app.request() 19 | .get('/') 20 | .set('Content-Type', 'application/x-www-form-urlencoded') 21 | .set('Content-Length', 'user=tobi'.length) 22 | .write('user=tobi') 23 | .end(function(res){ 24 | res.body.should.equal('{"user":"tobi"}'); 25 | done(); 26 | }); 27 | }) 28 | 29 | it('should parse x-www-form-urlencoded', function(done){ 30 | app.request() 31 | .post('/') 32 | .set('Content-Type', 'application/x-www-form-urlencoded') 33 | .write('user=tobi') 34 | .end(function(res){ 35 | res.body.should.equal('{"user":"tobi"}'); 36 | done(); 37 | }); 38 | }) 39 | }) -------------------------------------------------------------------------------- /examples/limit.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../') 3 | , http = require('http'); 4 | 5 | // visit form.html 6 | 7 | var app = connect() 8 | .use(connect.static(__dirname + '/public')) 9 | .use(connect.limit('5mb')) 10 | .use(connect.bodyParser()) 11 | .use(form) 12 | .use(upload); 13 | 14 | function form(req, res, next){ 15 | if ('GET' != req.method) return next(); 16 | res.statusCode = 302; 17 | res.setHeader('Location', 'form.html'); 18 | res.end(); 19 | } 20 | 21 | function upload(req, res){ 22 | res.setHeader('Content-Type', 'text/html'); 23 | res.write('

thanks ' + req.body.name + '

'); 24 | res.write(''); 38 | } 39 | 40 | http.Server(app).listen(3000); 41 | console.log('Server started on port 3000'); -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px/1.6 "Helvetica Neue", Helvetica, sans-serif; 4 | } 5 | 6 | h1 { 7 | margin: 0; 8 | font-size: 60px; 9 | } 10 | 11 | h2 { 12 | font-weight: 200; 13 | } 14 | 15 | h3 { 16 | font-weight: bold; 17 | } 18 | 19 | a { 20 | color: #8a6343; 21 | } 22 | 23 | pre { 24 | display: inline-block; 25 | border: 1px solid #eee; 26 | padding: 15px; 27 | border-radius: 5px; 28 | font: 13px Monaco, monospace; 29 | } 30 | 31 | .tags em { 32 | font-weight: bold; 33 | font-style: normal; 34 | } 35 | 36 | #menu { 37 | position: fixed; 38 | top: 10px; 39 | right: 30px; 40 | } 41 | 42 | #menu li { 43 | list-style: none; 44 | } 45 | 46 | #menu li a { 47 | display: block; 48 | text-decoration: none; 49 | padding: 5px 2px; 50 | border-bottom: 1px dotted #eee; 51 | text-align: right; 52 | -webkit-transition: padding 200ms ease-out; 53 | -moz-transition: padding 200ms ease-out; 54 | } 55 | 56 | #menu li a:hover { 57 | padding-right: 15px; 58 | } 59 | 60 | code .comment { color: #a2a2a2 } 61 | code .init { color: #2F6FAD } 62 | code .string { color: #5890AD } 63 | code .keyword { color: #8A6343 } 64 | code .number { color: #2F6FAD } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2010 Sencha Inc. 4 | Copyright (c) 2011 LearnBoost 5 | Copyright (c) 2011 TJ Holowaychuk 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | 'Software'), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /support/docs.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs') 3 | , jade = require('jade'); 4 | 5 | var tmpl = fs.readFileSync('support/docs.jade', 'utf8'); 6 | var fn = jade.compile(tmpl); 7 | 8 | var json = ''; 9 | process.stdin.setEncoding('utf8'); 10 | process.stdin.on('data', function(chunk){ 11 | json += chunk; 12 | }).on('end', function(){ 13 | json = JSON.parse(json); 14 | render(json); 15 | }).resume(); 16 | 17 | function title(comment) { 18 | if (!comment.ctx) return ''; 19 | if (~comment.ctx.string.indexOf('module.exports')) return ''; 20 | if (~comment.ctx.string.indexOf('prototype')) { 21 | return comment.ctx.string.replace('.prototype.', '#'); 22 | } else { 23 | return comment.ctx.string; 24 | } 25 | } 26 | 27 | function id(comment) { 28 | if (!comment.ctx) return ''; 29 | return comment.ctx.string 30 | .replace('()', ''); 31 | } 32 | 33 | function ignore(comment) { 34 | return comment.ignore 35 | || (comment.ctx && ~comment.ctx.string.indexOf('__proto__')) 36 | || ~comment.description.full.indexOf('Module dependencies'); 37 | } 38 | 39 | function render(obj) { 40 | process.stdout.write(fn({ 41 | comments: obj 42 | , ignore: ignore 43 | , title: title 44 | , id: id 45 | })); 46 | } -------------------------------------------------------------------------------- /examples/cookieSession.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../') 3 | , http = require('http'); 4 | 5 | var app = connect() 6 | .use(connect.logger('dev')) 7 | .use(connect.bodyParser()) 8 | .use(connect.cookieParser()) 9 | .use(connect.cookieSession({ secret: 'some secret' })) 10 | .use(post) 11 | .use(clear) 12 | .use(counter); 13 | 14 | function clear(req, res, next) { 15 | if ('/clear' != req.url) return next(); 16 | req.session = null; 17 | res.statusCode = 302; 18 | res.setHeader('Location', '/'); 19 | res.end(); 20 | } 21 | 22 | function post(req, res, next) { 23 | if ('POST' != req.method) return next(); 24 | req.session.name = req.body.name; 25 | next(); 26 | } 27 | 28 | function counter(req, res) { 29 | req.session.count = req.session.count || 0; 30 | var n = req.session.count++; 31 | var name = req.session.name || 'Enter your name'; 32 | res.end('

hits: ' + n + '

' 33 | + '
' 34 | + '

' 35 | + '

' 36 | + '
' 37 | + '

clear session

'); 38 | } 39 | 40 | http.createServer(app).listen(3000); 41 | console.log('Server listening on port 3000'); 42 | -------------------------------------------------------------------------------- /lib/middleware/vhost.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - vhost 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Vhost: 11 | * 12 | * Setup vhost for the given `hostname` and `server`. 13 | * 14 | * connect() 15 | * .use(connect.vhost('foo.com', fooApp)) 16 | * .use(connect.vhost('bar.com', barApp)) 17 | * .use(connect.vhost('*.com', mainApp)) 18 | * 19 | * The `server` may be a Connect server or 20 | * a regular Node `http.Server`. 21 | * 22 | * @param {String} hostname 23 | * @param {Server} server 24 | * @return {Function} 25 | * @api public 26 | */ 27 | 28 | module.exports = function vhost(hostname, server){ 29 | if (!hostname) throw new Error('vhost hostname required'); 30 | if (!server) throw new Error('vhost server required'); 31 | var regexp = new RegExp('^' + hostname.replace(/[*]/g, '(.*?)') + '$', 'i'); 32 | if (server.onvhost) server.onvhost(hostname); 33 | return function vhost(req, res, next){ 34 | if (!req.headers.host) return next(); 35 | var host = req.headers.host.split(':')[0]; 36 | if (!regexp.test(host)) return next(); 37 | if ('function' == typeof server) return server(req, res, next); 38 | server.emit('request', req, res); 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /support/docs.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Connect - High quality middleware for node.js 5 | meta(http-equiv="Content-Type", content="text/html; charset=utf-8") 6 | link(rel='stylesheet', href='style.css') 7 | script(src='jquery.js') 8 | script(src='docs.js') 9 | body 10 | #content 11 | h1 Connect 12 | for comment in comments 13 | unless ignore(comment) 14 | .comment(id=id(comment)) 15 | h2= title(comment) 16 | .description!= comment.description.full 17 | 18 | if comment.tags.length 19 | ul.tags 20 | for tag in comment.tags 21 | if tag.types 22 | if 'param' == tag.type 23 | li #{tag.types.join(' | ')} #{tag.name} #{tag.description} 24 | else 25 | li returns #{tag.types.join(' | ')} #{tag.description} 26 | else if tag.name 27 | li #{tag.name} #{tag.description} 28 | 29 | if comment.code 30 | h3 Source 31 | pre 32 | code!= comment.code 33 | 34 | ul#menu 35 | for comment in comments 36 | unless ignore(comment) 37 | li 38 | a(href='#' + id(comment))= title(comment) 39 | -------------------------------------------------------------------------------- /lib/middleware/limit.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - limit 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var utils = require('../utils'); 13 | 14 | /** 15 | * Limit: 16 | * 17 | * Limit request bodies to the given size in `bytes`. 18 | * 19 | * A string representation of the bytesize may also be passed, 20 | * for example "5mb", "200kb", "1gb", etc. 21 | * 22 | * connect() 23 | * .use(connect.limit('5.5mb')) 24 | * .use(handleImageUpload) 25 | * 26 | * @param {Number|String} bytes 27 | * @return {Function} 28 | * @api public 29 | */ 30 | 31 | module.exports = function limit(bytes){ 32 | if ('string' == typeof bytes) bytes = utils.parseBytes(bytes); 33 | if ('number' != typeof bytes) throw new Error('limit() bytes required'); 34 | return function limit(req, res, next){ 35 | var received = 0 36 | , len = req.headers['content-length'] 37 | ? parseInt(req.headers['content-length'], 10) 38 | : null; 39 | 40 | // self-awareness 41 | if (req._limit) return next(); 42 | req._limit = true; 43 | 44 | // limit by content-length 45 | if (len && len > bytes) return next(utils.error(413)); 46 | 47 | // limit 48 | req.on('data', function(chunk){ 49 | received += chunk.length; 50 | if (received > bytes) req.destroy(); 51 | }); 52 | 53 | next(); 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /lib/middleware/timeout.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - timeout 4 | * Ported from https://github.com/LearnBoost/connect-timeout 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var debug = require('debug')('connect:timeout'); 13 | 14 | /** 15 | * Timeout: 16 | * 17 | * Times out the request in `ms`, defaulting to `5000`. The 18 | * method `req.clearTimeout()` is added to revert this behaviour 19 | * programmatically within your application's middleware, routes, etc. 20 | * 21 | * The timeout error is passed to `next()` so that you may customize 22 | * the response behaviour. This error has the `.timeout` property as 23 | * well as `.status == 408`. 24 | * 25 | * @param {Number} ms 26 | * @return {Function} 27 | * @api public 28 | */ 29 | 30 | module.exports = function timeout(ms) { 31 | ms = ms || 5000; 32 | 33 | return function(req, res, next) { 34 | var id = setTimeout(function(){ 35 | req.emit('timeout', ms); 36 | }, ms); 37 | 38 | req.on('timeout', function(){ 39 | if (req.headerSent) return debug('response started, cannot timeout'); 40 | var err = new Error('Request timeout'); 41 | err.timeout = ms; 42 | err.status = 408; 43 | next(err); 44 | }); 45 | 46 | req.clearTimeout = function(){ 47 | clearTimeout(id); 48 | }; 49 | 50 | res.on('header', function(){ 51 | clearTimeout(id); 52 | }); 53 | 54 | next(); 55 | }; 56 | }; -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - Cache 4 | * Copyright(c) 2011 Sencha Inc. 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Expose `Cache`. 10 | */ 11 | 12 | module.exports = Cache; 13 | 14 | /** 15 | * LRU cache store. 16 | * 17 | * @param {Number} limit 18 | * @api private 19 | */ 20 | 21 | function Cache(limit) { 22 | this.store = {}; 23 | this.keys = []; 24 | this.limit = limit; 25 | } 26 | 27 | /** 28 | * Touch `key`, promoting the object. 29 | * 30 | * @param {String} key 31 | * @param {Number} i 32 | * @api private 33 | */ 34 | 35 | Cache.prototype.touch = function(key, i){ 36 | this.keys.splice(i,1); 37 | this.keys.push(key); 38 | }; 39 | 40 | /** 41 | * Remove `key`. 42 | * 43 | * @param {String} key 44 | * @api private 45 | */ 46 | 47 | Cache.prototype.remove = function(key){ 48 | delete this.store[key]; 49 | }; 50 | 51 | /** 52 | * Get the object stored for `key`. 53 | * 54 | * @param {String} key 55 | * @return {Array} 56 | * @api private 57 | */ 58 | 59 | Cache.prototype.get = function(key){ 60 | return this.store[key]; 61 | }; 62 | 63 | /** 64 | * Add a cache `key`. 65 | * 66 | * @param {String} key 67 | * @return {Array} 68 | * @api private 69 | */ 70 | 71 | Cache.prototype.add = function(key){ 72 | // initialize store 73 | var len = this.keys.push(key); 74 | 75 | // limit reached, invalidate LRU 76 | if (len > this.limit) this.remove(this.keys.shift()); 77 | 78 | var arr = this.store[key] = []; 79 | arr.createdAt = new Date; 80 | return arr; 81 | }; 82 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | 4 | describe('app', function(){ 5 | it('should inherit from event emitter', function(done){ 6 | var app = connect(); 7 | app.on('foo', done); 8 | app.emit('foo'); 9 | }) 10 | 11 | it('should not obscure FQDNs', function(done){ 12 | var app = connect(); 13 | 14 | app.use(function(req, res){ 15 | res.end(req.url); 16 | }); 17 | 18 | app.request() 19 | .get('http://example.com/foo') 20 | .expect('http://example.com/foo', done); 21 | }) 22 | 23 | it('should allow old-style constructor middleware', function(done){ 24 | var app = connect( 25 | connect.json() 26 | , connect.multipart() 27 | , connect.urlencoded() 28 | , function(req, res){ res.end(JSON.stringify(req.body)) }); 29 | 30 | app.stack.should.have.length(4); 31 | 32 | app.request() 33 | .post('/') 34 | .set('Content-Type', 'application/json') 35 | .write('{"foo":"bar"}') 36 | .expect('{"foo":"bar"}', done); 37 | }) 38 | 39 | it('should allow old-style .createServer()', function(){ 40 | var app = connect.createServer( 41 | connect.json() 42 | , connect.multipart() 43 | , connect.urlencoded()); 44 | 45 | app.stack.should.have.length(3); 46 | }) 47 | 48 | it('should escape the 404 response body', function(done){ 49 | var app = connect(); 50 | app.request() 51 | .get('/foo/') 52 | .expect('Cannot GET /foo/<script>stuff</script>', done); 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../') 3 | , utils = connect.utils; 4 | 5 | describe('utils.uid(len)', function(){ 6 | it('should generate a uid of the given length', function(){ 7 | var n = 20; 8 | while (n--) utils.uid(n).should.have.length(n); 9 | utils.uid(10).should.not.equal(utils.uid(10)); 10 | }) 11 | }) 12 | 13 | describe('utils.parseCacheControl(str)', function(){ 14 | it('should parse Cache-Control', function(){ 15 | var parse = utils.parseCacheControl; 16 | parse('no-cache').should.eql({ 'no-cache': true }); 17 | parse('no-store').should.eql({ 'no-store': true }); 18 | parse('no-transform').should.eql({ 'no-transform': true }); 19 | parse('only-if-cached').should.eql({ 'only-if-cached': true }); 20 | parse('max-age=0').should.eql({ 'max-age': 0 }); 21 | parse('max-age=60').should.eql({ 'max-age': 60 }); 22 | parse('max-stale=60').should.eql({ 'max-stale': 60 }); 23 | parse('min-fresh=60').should.eql({ 'min-fresh': 60 }); 24 | parse('public, max-age=60').should.eql({ 'public': true, 'max-age': 60 }); 25 | parse('must-revalidate, max-age=60').should.eql({ 'must-revalidate': true, 'max-age': 60 }); 26 | }) 27 | }) 28 | 29 | describe('utils.mime(req)', function(){ 30 | it('should return the mime-type from Content-Type', function(){ 31 | utils.mime({ headers: { 'content-type': 'text/html; charset=utf8' }}) 32 | .should.equal('text/html'); 33 | 34 | utils.mime({ headers: { 'content-type': 'text/html; charset=utf8' }}) 35 | .should.equal('text/html'); 36 | 37 | utils.mime({ headers: { 'content-type': 'text/html' }}) 38 | .should.equal('text/html'); 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /lib/middleware/cookieParser.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - cookieParser 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var utils = require('./../utils') 14 | , cookie = require('cookie'); 15 | 16 | /** 17 | * Cookie parser: 18 | * 19 | * Parse _Cookie_ header and populate `req.cookies` 20 | * with an object keyed by the cookie names. Optionally 21 | * you may enabled signed cookie support by passing 22 | * a `secret` string, which assigns `req.secret` so 23 | * it may be used by other middleware. 24 | * 25 | * Examples: 26 | * 27 | * connect() 28 | * .use(connect.cookieParser('optional secret string')) 29 | * .use(function(req, res, next){ 30 | * res.end(JSON.stringify(req.cookies)); 31 | * }) 32 | * 33 | * @param {String} secret 34 | * @return {Function} 35 | * @api public 36 | */ 37 | 38 | module.exports = function cookieParser(secret){ 39 | return function cookieParser(req, res, next) { 40 | if (req.cookies) return next(); 41 | var cookies = req.headers.cookie; 42 | 43 | req.secret = secret; 44 | req.cookies = {}; 45 | req.signedCookies = {}; 46 | 47 | if (cookies) { 48 | try { 49 | req.cookies = cookie.parse(cookies); 50 | if (secret) { 51 | req.signedCookies = utils.parseSignedCookies(req.cookies, secret); 52 | req.signedCookies = utils.parseJSONCookies(req.signedCookies); 53 | } 54 | req.cookies = utils.parseJSONCookies(req.cookies); 55 | } catch (err) { 56 | err.status = 400; 57 | return next(err); 58 | } 59 | } 60 | next(); 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /examples/logger.format.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | // $ curl http://localhost:3000/ 9 | 10 | // custom format string 11 | 12 | connect.createServer( 13 | connect.logger(':method :url - :res[content-type]') 14 | , function(req, res){ 15 | res.statusCode = 500; 16 | res.setHeader('Content-Type', 'text/plain'); 17 | res.end('Internal Server Error'); 18 | } 19 | ).listen(3000); 20 | 21 | // $ curl http://localhost:3001/ 22 | // $ curl http://localhost:3001/302 23 | // $ curl http://localhost:3001/404 24 | // $ curl http://localhost:3001/500 25 | 26 | connect() 27 | .use(connect.logger('dev')) 28 | .use('/connect', connect.static(__dirname + '/lib')) 29 | .use('/connect', connect.directory(__dirname + '/lib')) 30 | .use(function(req, res, next){ 31 | switch (req.url) { 32 | case '/500': 33 | var body = 'Internal Server Error'; 34 | res.statusCode = 500; 35 | res.setHeader('Content-Length', body.length); 36 | res.end(body); 37 | break; 38 | case '/404': 39 | var body = 'Not Found'; 40 | res.statusCode = 404; 41 | res.setHeader('Content-Length', body.length); 42 | res.end(body); 43 | break; 44 | case '/302': 45 | var body = 'Found'; 46 | res.statusCode = 302; 47 | res.setHeader('Content-Length', body.length); 48 | res.end(body); 49 | break; 50 | default: 51 | var body = 'OK'; 52 | res.setHeader('Content-Length', body.length); 53 | res.end(body); 54 | } 55 | }) 56 | .listen(3001); 57 | 58 | // pre-defined 59 | 60 | connect() 61 | .use(connect.logger('short')) 62 | .listen(3002); -------------------------------------------------------------------------------- /lib/middleware/bodyParser.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - bodyParser 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var multipart = require('./multipart') 14 | , urlencoded = require('./urlencoded') 15 | , json = require('./json'); 16 | 17 | /** 18 | * Body parser: 19 | * 20 | * Parse request bodies, supports _application/json_, 21 | * _application/x-www-form-urlencoded_, and _multipart/form-data_. 22 | * 23 | * This is equivalent to: 24 | * 25 | * app.use(connect.json()); 26 | * app.use(connect.urlencoded()); 27 | * app.use(connect.multipart()); 28 | * 29 | * Examples: 30 | * 31 | * connect() 32 | * .use(connect.bodyParser()) 33 | * .use(function(req, res) { 34 | * res.end('viewing user ' + req.body.user.name); 35 | * }); 36 | * 37 | * $ curl -d 'user[name]=tj' http://local/ 38 | * $ curl -d '{"user":{"name":"tj"}}' -H "Content-Type: application/json" http://local/ 39 | * 40 | * View [json](json.html), [urlencoded](urlencoded.html), and [multipart](multipart.html) for more info. 41 | * 42 | * @param {Object} options 43 | * @return {Function} 44 | * @api public 45 | */ 46 | 47 | exports = module.exports = function bodyParser(options){ 48 | var _urlencoded = urlencoded(options) 49 | , _multipart = multipart(options) 50 | , _json = json(options); 51 | 52 | return function bodyParser(req, res, next) { 53 | _json(req, res, function(err){ 54 | if (err) return next(err); 55 | _urlencoded(req, res, function(err){ 56 | if (err) return next(err); 57 | _multipart(req, res, next); 58 | }); 59 | }); 60 | } 61 | }; -------------------------------------------------------------------------------- /lib/middleware/urlencoded.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - urlencoded 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var utils = require('../utils') 14 | , _limit = require('./limit') 15 | , qs = require('qs'); 16 | 17 | /** 18 | * noop middleware. 19 | */ 20 | 21 | function noop(req, res, next) { 22 | next(); 23 | } 24 | 25 | /** 26 | * Urlencoded: 27 | * 28 | * Parse x-ww-form-urlencoded request bodies, 29 | * providing the parsed object as `req.body`. 30 | * 31 | * Options: 32 | * 33 | * - `limit` byte limit disabled by default 34 | * 35 | * @param {Object} options 36 | * @return {Function} 37 | * @api public 38 | */ 39 | 40 | exports = module.exports = function(options){ 41 | options = options || {}; 42 | 43 | var limit = options.limit 44 | ? _limit(options.limit) 45 | : noop; 46 | 47 | return function urlencoded(req, res, next) { 48 | if (req._body) return next(); 49 | req.body = req.body || {}; 50 | 51 | if (!utils.hasBody(req)) return next(); 52 | 53 | // check Content-Type 54 | if ('application/x-www-form-urlencoded' != utils.mime(req)) return next(); 55 | 56 | // flag as parsed 57 | req._body = true; 58 | 59 | // parse 60 | limit(req, res, function(err){ 61 | if (err) return next(err); 62 | var buf = ''; 63 | req.setEncoding('utf8'); 64 | req.on('data', function(chunk){ buf += chunk }); 65 | req.on('end', function(){ 66 | try { 67 | req.body = buf.length 68 | ? qs.parse(buf, options) 69 | : {}; 70 | next(); 71 | } catch (err){ 72 | err.body = buf; 73 | next(err); 74 | } 75 | }); 76 | }); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /lib/patch.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var http = require('http') 13 | , res = http.ServerResponse.prototype 14 | , setHeader = res.setHeader 15 | , _renderHeaders = res._renderHeaders 16 | , writeHead = res.writeHead; 17 | 18 | // apply only once 19 | 20 | if (!res._hasConnectPatch) { 21 | 22 | /** 23 | * Provide a public "header sent" flag 24 | * until node does. 25 | * 26 | * @return {Boolean} 27 | * @api public 28 | */ 29 | 30 | res.__defineGetter__('headerSent', function(){ 31 | return this._header; 32 | }); 33 | 34 | /** 35 | * Set header `field` to `val`, special-casing 36 | * the `Set-Cookie` field for multiple support. 37 | * 38 | * @param {String} field 39 | * @param {String} val 40 | * @api public 41 | */ 42 | 43 | res.setHeader = function(field, val){ 44 | var key = field.toLowerCase() 45 | , prev; 46 | 47 | // special-case Set-Cookie 48 | if (this._headers && 'set-cookie' == key) { 49 | if (prev = this.getHeader(field)) { 50 | val = Array.isArray(prev) 51 | ? prev.concat(val) 52 | : [prev, val]; 53 | } 54 | // charset 55 | } else if ('content-type' == key && this.charset) { 56 | val += '; charset=' + this.charset; 57 | } 58 | 59 | return setHeader.call(this, field, val); 60 | }; 61 | 62 | /** 63 | * Proxy to emit "header" event. 64 | */ 65 | 66 | res._renderHeaders = function(){ 67 | if (!this._emittedHeader) this.emit('header'); 68 | this._emittedHeader = true; 69 | return _renderHeaders.call(this); 70 | }; 71 | 72 | res.writeHead = function(){ 73 | if (!this._emittedHeader) this.emit('header'); 74 | this._emittedHeader = true; 75 | return writeHead.apply(this, arguments); 76 | }; 77 | 78 | res._hasConnectPatch = true; 79 | } 80 | -------------------------------------------------------------------------------- /lib/connect.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var EventEmitter = require('events').EventEmitter 14 | , proto = require('./proto') 15 | , utils = require('./utils') 16 | , path = require('path') 17 | , basename = path.basename 18 | , fs = require('fs'); 19 | 20 | // node patches 21 | 22 | require('./patch'); 23 | 24 | // expose createServer() as the module 25 | 26 | exports = module.exports = createServer; 27 | 28 | /** 29 | * Framework version. 30 | */ 31 | 32 | exports.version = '2.6.1'; 33 | 34 | /** 35 | * Expose mime module. 36 | */ 37 | 38 | exports.mime = require('./middleware/static').mime; 39 | 40 | /** 41 | * Expose the prototype. 42 | */ 43 | 44 | exports.proto = proto; 45 | 46 | /** 47 | * Auto-load middleware getters. 48 | */ 49 | 50 | exports.middleware = {}; 51 | 52 | /** 53 | * Expose utilities. 54 | */ 55 | 56 | exports.utils = utils; 57 | 58 | /** 59 | * Create a new connect server. 60 | * 61 | * @return {Function} 62 | * @api public 63 | */ 64 | 65 | function createServer() { 66 | function app(req, res){ app.handle(req, res); } 67 | utils.merge(app, proto); 68 | utils.merge(app, EventEmitter.prototype); 69 | app.route = '/'; 70 | app.stack = []; 71 | for (var i = 0; i < arguments.length; ++i) { 72 | app.use(arguments[i]); 73 | } 74 | return app; 75 | }; 76 | 77 | /** 78 | * Support old `.createServer()` method. 79 | */ 80 | 81 | createServer.createServer = createServer; 82 | 83 | /** 84 | * Auto-load bundled middleware with getters. 85 | */ 86 | 87 | fs.readdirSync(__dirname + '/middleware').forEach(function(filename){ 88 | if (!/\.js$/.test(filename)) return; 89 | var name = basename(filename, '.js'); 90 | function load(){ return require('./middleware/' + name); } 91 | exports.middleware.__defineGetter__(name, load); 92 | exports.__defineGetter__(name, load); 93 | }); 94 | -------------------------------------------------------------------------------- /test/vhost.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../') 3 | , http = require('http'); 4 | 5 | describe('connect.vhost()', function(){ 6 | it('should route by Host', function(done){ 7 | var app = connect() 8 | , tobi = connect() 9 | , loki = connect(); 10 | 11 | app.use(connect.vhost('tobi.com', tobi)); 12 | app.use(connect.vhost('loki.com', loki)); 13 | 14 | tobi.use(function(req, res){ res.end('tobi') }); 15 | loki.use(function(req, res){ res.end('loki') }); 16 | 17 | app.request() 18 | .get('/') 19 | .set('Host', 'tobi.com') 20 | .expect('tobi', done); 21 | }) 22 | 23 | it('should support http.Servers', function(done){ 24 | var app = connect() 25 | , tobi = http.createServer(function(req, res){ res.end('tobi') }) 26 | , loki = http.createServer(function(req, res){ res.end('loki') }) 27 | 28 | app.use(connect.vhost('tobi.com', tobi)); 29 | app.use(connect.vhost('loki.com', loki)); 30 | 31 | app.request() 32 | .get('/') 33 | .set('Host', 'loki.com') 34 | .expect('loki', done); 35 | }) 36 | 37 | it('should support wildcards', function(done){ 38 | var app = connect() 39 | , tobi = http.createServer(function(req, res){ res.end('tobi') }) 40 | , loki = http.createServer(function(req, res){ res.end('loki') }) 41 | 42 | app.use(connect.vhost('*.ferrets.com', loki)); 43 | app.use(connect.vhost('tobi.ferrets.com', tobi)); 44 | 45 | app.request() 46 | .get('/') 47 | .set('Host', 'loki.ferrets.com') 48 | .expect('loki', done); 49 | }) 50 | 51 | it('should 404 unless matched', function(done){ 52 | var app = connect() 53 | , tobi = http.createServer(function(req, res){ res.end('tobi') }) 54 | , loki = http.createServer(function(req, res){ res.end('loki') }) 55 | 56 | app.use(connect.vhost('tobi.com', tobi)); 57 | app.use(connect.vhost('loki.com', loki)); 58 | 59 | app.request() 60 | .get('/') 61 | .set('Host', 'ferrets.com') 62 | .expect(404, done); 63 | }) 64 | }) -------------------------------------------------------------------------------- /lib/middleware/csrf.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Connect - csrf 3 | * Copyright(c) 2011 Sencha Inc. 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var utils = require('../utils'); 12 | 13 | /** 14 | * Anti CSRF: 15 | * 16 | * CRSF protection middleware. 17 | * 18 | * By default this middleware generates a token named "_csrf" 19 | * which should be added to requests which mutate 20 | * state, within a hidden form field, query-string etc. This 21 | * token is validated against the visitor's `req.session._csrf` 22 | * property. 23 | * 24 | * The default `value` function checks `req.body` generated 25 | * by the `bodyParser()` middleware, `req.query` generated 26 | * by `query()`, and the "X-CSRF-Token" header field. 27 | * 28 | * This middleware requires session support, thus should be added 29 | * somewhere _below_ `session()` and `cookieParser()`. 30 | * 31 | * Options: 32 | * 33 | * - `value` a function accepting the request, returning the token 34 | * 35 | * @param {Object} options 36 | * @api public 37 | */ 38 | 39 | module.exports = function csrf(options) { 40 | options = options || {}; 41 | var value = options.value || defaultValue; 42 | 43 | return function(req, res, next){ 44 | // generate CSRF token 45 | var token = req.session._csrf || (req.session._csrf = utils.uid(24)); 46 | 47 | // ignore these methods 48 | if ('GET' == req.method || 'HEAD' == req.method || 'OPTIONS' == req.method) return next(); 49 | 50 | // determine value 51 | var val = value(req); 52 | 53 | // check 54 | if (val != token) return next(utils.error(403)); 55 | 56 | next(); 57 | } 58 | }; 59 | 60 | /** 61 | * Default value function, checking the `req.body` 62 | * and `req.query` for the CSRF token. 63 | * 64 | * @param {IncomingMessage} req 65 | * @return {String} 66 | * @api private 67 | */ 68 | 69 | function defaultValue(req) { 70 | return (req.body && req.body._csrf) 71 | || (req.query && req.query._csrf) 72 | || (req.headers['x-csrf-token']); 73 | } 74 | -------------------------------------------------------------------------------- /lib/middleware/json.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - json 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var utils = require('../utils') 14 | , _limit = require('./limit'); 15 | 16 | /** 17 | * noop middleware. 18 | */ 19 | 20 | function noop(req, res, next) { 21 | next(); 22 | } 23 | 24 | /** 25 | * JSON: 26 | * 27 | * Parse JSON request bodies, providing the 28 | * parsed object as `req.body`. 29 | * 30 | * Options: 31 | * 32 | * - `strict` when `false` anything `JSON.parse()` accepts will be parsed 33 | * - `reviver` used as the second "reviver" argument for JSON.parse 34 | * - `limit` byte limit disabled by default 35 | * 36 | * @param {Object} options 37 | * @return {Function} 38 | * @api public 39 | */ 40 | 41 | exports = module.exports = function(options){ 42 | var options = options || {} 43 | , strict = options.strict !== false; 44 | 45 | var limit = options.limit 46 | ? _limit(options.limit) 47 | : noop; 48 | 49 | return function json(req, res, next) { 50 | if (req._body) return next(); 51 | req.body = req.body || {}; 52 | 53 | if (!utils.hasBody(req)) return next(); 54 | 55 | // check Content-Type 56 | if ('application/json' != utils.mime(req)) return next(); 57 | 58 | // flag as parsed 59 | req._body = true; 60 | 61 | // parse 62 | limit(req, res, function(err){ 63 | if (err) return next(err); 64 | var buf = ''; 65 | req.setEncoding('utf8'); 66 | req.on('data', function(chunk){ buf += chunk }); 67 | req.on('end', function(){ 68 | var first = buf.trim()[0]; 69 | if (strict && '{' != first && '[' != first) return next(utils.error(400, 'invalid json')); 70 | try { 71 | req.body = JSON.parse(buf, options.reviver); 72 | next(); 73 | } catch (err){ 74 | err.body = buf; 75 | err.status = 400; 76 | next(err); 77 | } 78 | }); 79 | }); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /lib/middleware/session/store.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - session - Store 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var EventEmitter = require('events').EventEmitter 14 | , Session = require('./session') 15 | , Cookie = require('./cookie'); 16 | 17 | /** 18 | * Initialize abstract `Store`. 19 | * 20 | * @api private 21 | */ 22 | 23 | var Store = module.exports = function Store(options){}; 24 | 25 | /** 26 | * Inherit from `EventEmitter.prototype`. 27 | */ 28 | 29 | Store.prototype.__proto__ = EventEmitter.prototype; 30 | 31 | /** 32 | * Re-generate the given requests's session. 33 | * 34 | * @param {IncomingRequest} req 35 | * @return {Function} fn 36 | * @api public 37 | */ 38 | 39 | Store.prototype.regenerate = function(req, fn){ 40 | var self = this; 41 | this.destroy(req.sessionID, function(err){ 42 | self.generate(req); 43 | fn(err); 44 | }); 45 | }; 46 | 47 | /** 48 | * Load a `Session` instance via the given `sid` 49 | * and invoke the callback `fn(err, sess)`. 50 | * 51 | * @param {String} sid 52 | * @param {Function} fn 53 | * @api public 54 | */ 55 | 56 | Store.prototype.load = function(sid, fn){ 57 | var self = this; 58 | this.get(sid, function(err, sess){ 59 | if (err) return fn(err); 60 | if (!sess) return fn(); 61 | var req = { sessionID: sid, sessionStore: self }; 62 | sess = self.createSession(req, sess); 63 | fn(null, sess); 64 | }); 65 | }; 66 | 67 | /** 68 | * Create session from JSON `sess` data. 69 | * 70 | * @param {IncomingRequest} req 71 | * @param {Object} sess 72 | * @return {Session} 73 | * @api private 74 | */ 75 | 76 | Store.prototype.createSession = function(req, sess){ 77 | var expires = sess.cookie.expires 78 | , orig = sess.cookie.originalMaxAge; 79 | sess.cookie = new Cookie(sess.cookie); 80 | if ('string' == typeof expires) sess.cookie.expires = new Date(expires); 81 | sess.cookie.originalMaxAge = orig; 82 | req.session = new Session(req, sess); 83 | return req.session; 84 | }; 85 | -------------------------------------------------------------------------------- /lib/middleware/favicon.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - favicon 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var fs = require('fs') 14 | , utils = require('../utils'); 15 | 16 | /** 17 | * Favicon: 18 | * 19 | * By default serves the connect favicon, or the favicon 20 | * located by the given `path`. 21 | * 22 | * Options: 23 | * 24 | * - `maxAge` cache-control max-age directive, defaulting to 1 day 25 | * 26 | * Examples: 27 | * 28 | * Serve default favicon: 29 | * 30 | * connect() 31 | * .use(connect.favicon()) 32 | * 33 | * Serve favicon before logging for brevity: 34 | * 35 | * connect() 36 | * .use(connect.favicon()) 37 | * .use(connect.logger('dev')) 38 | * 39 | * Serve custom favicon: 40 | * 41 | * connect() 42 | * .use(connect.favicon('public/favicon.ico)) 43 | * 44 | * @param {String} path 45 | * @param {Object} options 46 | * @return {Function} 47 | * @api public 48 | */ 49 | 50 | module.exports = function favicon(path, options){ 51 | var options = options || {} 52 | , path = path || __dirname + '/../public/favicon.ico' 53 | , maxAge = options.maxAge || 86400000 54 | , icon; // favicon cache 55 | 56 | return function favicon(req, res, next){ 57 | if ('/favicon.ico' == req.url) { 58 | if (icon) { 59 | res.writeHead(200, icon.headers); 60 | res.end(icon.body); 61 | } else { 62 | fs.readFile(path, function(err, buf){ 63 | if (err) return next(err); 64 | icon = { 65 | headers: { 66 | 'Content-Type': 'image/x-icon' 67 | , 'Content-Length': buf.length 68 | , 'ETag': '"' + utils.md5(buf) + '"' 69 | , 'Cache-Control': 'public, max-age=' + (maxAge / 1000) 70 | }, 71 | body: buf 72 | }; 73 | res.writeHead(200, icon.headers); 74 | res.end(icon.body); 75 | }); 76 | } 77 | } else { 78 | next(); 79 | } 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /test/cookieParser.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('..') 3 | , signature = require('cookie-signature'); 4 | 5 | var app = connect(); 6 | 7 | app.use(connect.cookieParser('keyboard cat')); 8 | 9 | app.use(function(req, res, next){ 10 | if ('/signed' != req.url) return next(); 11 | res.end(JSON.stringify(req.signedCookies)); 12 | }); 13 | 14 | app.use(function(req, res, next){ 15 | res.end(JSON.stringify(req.cookies)); 16 | }); 17 | 18 | describe('connect.cookieParser()', function(){ 19 | describe('when no cookies are sent', function(){ 20 | it('should default req.cookies to {}', function(done){ 21 | app.request() 22 | .get('/') 23 | .expect('{}', done); 24 | }) 25 | 26 | it('should default req.signedCookies to {}', function(done){ 27 | app.request() 28 | .get('/') 29 | .expect('{}', done); 30 | }) 31 | }) 32 | 33 | describe('when cookies are sent', function(){ 34 | it('should populate req.cookies', function(done){ 35 | app.request() 36 | .get('/') 37 | .set('Cookie', 'foo=bar; bar=baz') 38 | .expect('{"foo":"bar","bar":"baz"}', done); 39 | }) 40 | }) 41 | 42 | describe('when a secret is given', function(){ 43 | var val = signature.sign('foobarbaz', 'keyboard cat'); 44 | // TODO: "bar" fails... 45 | 46 | it('should populate req.signedCookies', function(done){ 47 | app.request() 48 | .get('/signed') 49 | .set('Cookie', 'foo=s:' + val) 50 | .expect('{"foo":"foobarbaz"}', done); 51 | }) 52 | 53 | it('should remove the signed value from req.cookies', function(done){ 54 | app.request() 55 | .get('/') 56 | .set('Cookie', 'foo=s:' + val) 57 | .expect('{}', done); 58 | }) 59 | 60 | it('should omit invalid signatures', function(done){ 61 | app.request() 62 | .get('/signed') 63 | .set('Cookie', 'foo=' + val + '3') 64 | .expect('{}', function(){ 65 | app.request() 66 | .get('/') 67 | .set('Cookie', 'foo=' + val + '3') 68 | .expect('{"foo":"foobarbaz.CP7AWaXDfAKIRfH49dQzKJx7sKzzSoPq7/AcBBRVwlI3"}', done); 69 | }); 70 | }) 71 | }) 72 | }) -------------------------------------------------------------------------------- /test/compress.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | 4 | var fixtures = __dirname + '/fixtures'; 5 | 6 | var app = connect(); 7 | app.use(connect.compress()); 8 | app.use(connect.static(fixtures)); 9 | 10 | describe('connect.compress()', function(){ 11 | it('should gzip files', function(done){ 12 | app.request() 13 | .get('/todo.txt') 14 | .set('Accept-Encoding', 'gzip') 15 | .end(function(res){ 16 | res.body.should.not.equal('- groceries'); 17 | done(); 18 | }); 19 | }) 20 | 21 | it('should set Content-Encoding', function(done){ 22 | app.request() 23 | .get('/todo.txt') 24 | .set('Accept-Encoding', 'gzip') 25 | .expect('Content-Encoding', 'gzip', done); 26 | }) 27 | 28 | it('should support HEAD', function(done){ 29 | app.request() 30 | .head('/todo.txt') 31 | .set('Accept-Encoding', 'gzip') 32 | .expect('', done); 33 | }) 34 | 35 | it('should support conditional GETs', function(done){ 36 | app.request() 37 | .get('/todo.txt') 38 | .set('Accept-Encoding', 'gzip') 39 | .end(function(res){ 40 | var date = res.headers['last-modified']; 41 | app.request() 42 | .get('/todo.txt') 43 | .set('Accept-Encoding', 'gzip') 44 | .set('If-Modified-Since', date) 45 | .expect(304, done); 46 | }); 47 | }) 48 | 49 | it('should set Vary', function(done){ 50 | app.request() 51 | .get('/todo.txt') 52 | .set('Accept-Encoding', 'gzip') 53 | .expect('Vary', 'Accept-Encoding', done); 54 | }) 55 | 56 | it('should set Vary at all times', function(done){ 57 | app.request() 58 | .get('/todo.txt') 59 | .expect('Vary', 'Accept-Encoding', done); 60 | }) 61 | 62 | it('should transfer chunked', function(done){ 63 | app.request() 64 | .get('/todo.txt') 65 | .set('Accept-Encoding', 'gzip') 66 | .expect('Transfer-Encoding', 'chunked', done); 67 | }) 68 | 69 | it('should remove Content-Length for chunked', function(done){ 70 | app.request() 71 | .get('/todo.txt') 72 | .set('Accept-Encoding', 'gzip') 73 | .end(function(res){ 74 | res.headers.should.not.have.property('content-length'); 75 | done() 76 | }); 77 | }) 78 | 79 | }) -------------------------------------------------------------------------------- /test/patch.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | 4 | describe('patch', function(){ 5 | describe('"header" event', function(){ 6 | describe('with .setHeader()', function(){ 7 | it('should be emitted', function(done){ 8 | var app = connect(); 9 | 10 | app.use(function(req, res, next){ 11 | res.on('header', function(){ 12 | res.setHeader('bar', 'baz'); 13 | }); 14 | 15 | next(); 16 | }); 17 | 18 | app.use(function(req, res){ 19 | res.setHeader('foo', 'bar'); 20 | res.end(); 21 | }) 22 | 23 | app.request() 24 | .get('/') 25 | .end(function(res){ 26 | res.should.have.header('foo', 'bar'); 27 | res.should.have.header('bar', 'baz'); 28 | done(); 29 | }); 30 | }) 31 | }) 32 | 33 | describe('with .writeHead()', function(){ 34 | it('should be emitted', function(done){ 35 | var app = connect(); 36 | 37 | app.use(function(req, res, next){ 38 | res.on('header', function(){ 39 | res.setHeader('bar', 'baz'); 40 | }); 41 | 42 | next(); 43 | }); 44 | 45 | app.use(function(req, res){ 46 | res.writeHead(200, { foo: 'bar' }); 47 | res.end(); 48 | }) 49 | 50 | app.request() 51 | .get('/') 52 | .end(function(res){ 53 | res.should.have.header('foo', 'bar'); 54 | res.should.have.header('bar', 'baz'); 55 | done(); 56 | }); 57 | }) 58 | }) 59 | 60 | describe('with .end() only', function(){ 61 | it('should be emitted', function(done){ 62 | var app = connect(); 63 | 64 | app.use(function(req, res, next){ 65 | res.on('header', function(){ 66 | res.setHeader('bar', 'baz'); 67 | }); 68 | 69 | next(); 70 | }); 71 | 72 | app.use(function(req, res){ 73 | res.end(); 74 | }) 75 | 76 | app.request() 77 | .get('/') 78 | .end(function(res){ 79 | res.should.have.header('bar', 'baz'); 80 | done(); 81 | }); 82 | }) 83 | }) 84 | 85 | }) 86 | }) -------------------------------------------------------------------------------- /lib/public/directory.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | listing directory {directory} 4 | 5 | 67 | 68 | 69 | 70 |
71 |

{linked-path}

72 | {files} 73 |
74 | 75 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Connect is a middleware framework for node, 4 | * shipping with over 18 bundled middleware and a rich selection of 5 | * 3rd-party middleware. 6 | * 7 | * var app = connect() 8 | * .use(connect.logger('dev')) 9 | * .use(connect.static('public')) 10 | * .use(function(req, res){ 11 | * res.end('hello world\n'); 12 | * }) 13 | * .listen(3000); 14 | * 15 | * Installation: 16 | * 17 | * $ npm install connect 18 | * 19 | * Middleware: 20 | * 21 | * - [logger](logger.html) request logger with custom format support 22 | * - [csrf](csrf.html) Cross-site request forgery protection 23 | * - [compress](compress.html) Gzip compression middleware 24 | * - [basicAuth](basicAuth.html) basic http authentication 25 | * - [bodyParser](bodyParser.html) extensible request body parser 26 | * - [json](json.html) application/json parser 27 | * - [urlencoded](urlencoded.html) application/x-www-form-urlencoded parser 28 | * - [multipart](multipart.html) multipart/form-data parser 29 | * - [timeout](timeout.html) request timeouts 30 | * - [cookieParser](cookieParser.html) cookie parser 31 | * - [session](session.html) session management support with bundled MemoryStore 32 | * - [cookieSession](cookieSession.html) cookie-based session support 33 | * - [methodOverride](methodOverride.html) faux HTTP method support 34 | * - [responseTime](responseTime.html) calculates response-time and exposes via X-Response-Time 35 | * - [staticCache](staticCache.html) memory cache layer for the static() middleware 36 | * - [static](static.html) streaming static file server supporting `Range` and more 37 | * - [directory](directory.html) directory listing middleware 38 | * - [vhost](vhost.html) virtual host sub-domain mapping middleware 39 | * - [favicon](favicon.html) efficient favicon server (with default icon) 40 | * - [limit](limit.html) limit the bytesize of request bodies 41 | * - [query](query.html) automatic querystring parser, populating `req.query` 42 | * - [errorHandler](errorHandler.html) flexible error handler 43 | * 44 | * Links: 45 | * 46 | * - list of [3rd-party](https://github.com/senchalabs/connect/wiki) middleware 47 | * - GitHub [repository](http://github.com/senchalabs/connect) 48 | * - [test documentation](https://github.com/senchalabs/connect/blob/gh-pages/tests.md) 49 | * 50 | */ -------------------------------------------------------------------------------- /lib/middleware/static.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - static 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var send = require('send') 14 | , utils = require('../utils') 15 | , parse = utils.parseUrl 16 | , url = require('url'); 17 | 18 | /** 19 | * Static: 20 | * 21 | * Static file server with the given `root` path. 22 | * 23 | * Examples: 24 | * 25 | * var oneDay = 86400000; 26 | * 27 | * connect() 28 | * .use(connect.static(__dirname + '/public')) 29 | * 30 | * connect() 31 | * .use(connect.static(__dirname + '/public', { maxAge: oneDay })) 32 | * 33 | * Options: 34 | * 35 | * - `maxAge` Browser cache maxAge in milliseconds. defaults to 0 36 | * - `hidden` Allow transfer of hidden files. defaults to false 37 | * - `redirect` Redirect to trailing "/" when the pathname is a dir. defaults to true 38 | * 39 | * @param {String} root 40 | * @param {Object} options 41 | * @return {Function} 42 | * @api public 43 | */ 44 | 45 | exports = module.exports = function static(root, options){ 46 | options = options || {}; 47 | 48 | // root required 49 | if (!root) throw new Error('static() root path required'); 50 | 51 | // default redirect 52 | var redirect = false !== options.redirect; 53 | 54 | return function static(req, res, next) { 55 | if ('GET' != req.method && 'HEAD' != req.method) return next(); 56 | var path = parse(req).pathname; 57 | var pause = utils.pause(req); 58 | 59 | function resume() { 60 | next(); 61 | pause.resume(); 62 | } 63 | 64 | function directory() { 65 | if (!redirect) return resume(); 66 | var pathname = url.parse(req.originalUrl).pathname; 67 | res.statusCode = 301; 68 | res.setHeader('Location', pathname + '/'); 69 | res.end('Redirecting to ' + utils.escape(pathname) + '/'); 70 | } 71 | 72 | function error(err) { 73 | if (404 == err.status) return resume(); 74 | next(err); 75 | } 76 | 77 | send(req, path) 78 | .maxage(options.maxAge || 0) 79 | .root(root) 80 | .hidden(options.hidden) 81 | .on('error', error) 82 | .on('directory', directory) 83 | .pipe(res); 84 | }; 85 | }; 86 | 87 | /** 88 | * Expose mime module. 89 | * 90 | * If you wish to extend the mime table use this 91 | * reference to the "mime" module in the npm registry. 92 | */ 93 | 94 | exports.mime = send.mime; 95 | -------------------------------------------------------------------------------- /test/timeout.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | 4 | var app = connect() 5 | .use(connect.timeout(300)) 6 | .use(function(req, res){ 7 | res.end('Hello'); 8 | }); 9 | 10 | describe('connect.timeout()', function(){ 11 | describe('when below the timeout', function(){ 12 | it('should do nothing', function(done){ 13 | app.request() 14 | .get('/') 15 | .expect('Hello', done); 16 | }) 17 | }) 18 | 19 | describe('when above the timeout', function(){ 20 | describe('with no response made', function(){ 21 | it('should respond with 408 Request timeout', function(done){ 22 | var app = connect() 23 | .use(connect.timeout(300)) 24 | .use(function(req, res){ 25 | setTimeout(function(){ 26 | res.end('Hello'); 27 | }, 400); 28 | }); 29 | 30 | app.request() 31 | .get('/') 32 | .expect(408, done); 33 | }) 34 | 35 | it('should pass the error to next()', function(done){ 36 | var app = connect() 37 | .use(connect.timeout(300)) 38 | .use(function(req, res){ 39 | setTimeout(function(){ 40 | res.end('Hello'); 41 | }, 400); 42 | }) 43 | .use(function(err, req, res, next){ 44 | res.statusCode = err.status; 45 | res.end('timeout of ' + err.timeout + 'ms exceeded'); 46 | }); 47 | 48 | app.request() 49 | .get('/') 50 | .expect('timeout of 300ms exceeded', done); 51 | }) 52 | }) 53 | 54 | describe('with a partial response', function(){ 55 | it('should do nothing', function(done){ 56 | var app = connect() 57 | .use(connect.timeout(300)) 58 | .use(function(req, res){ 59 | res.write('Hello'); 60 | setTimeout(function(){ 61 | res.end(' World'); 62 | }, 400); 63 | }); 64 | 65 | app.request() 66 | .get('/') 67 | .expect('Hello World', done); 68 | }) 69 | }) 70 | }) 71 | 72 | describe('req.clearTimeout()', function(){ 73 | it('should revert this behavior', function(done){ 74 | var app = connect() 75 | .use(connect.timeout(300)) 76 | .use(function(req, res){ 77 | req.clearTimeout(); 78 | setTimeout(function(){ 79 | res.end('Hello'); 80 | }, 400); 81 | }); 82 | 83 | app.request() 84 | .get('/') 85 | .expect('Hello', done); 86 | }) 87 | }) 88 | }) -------------------------------------------------------------------------------- /lib/middleware/errorHandler.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Connect - errorHandler 3 | * Copyright(c) 2010 Sencha Inc. 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var utils = require('../utils') 13 | , fs = require('fs'); 14 | 15 | // environment 16 | 17 | var env = process.env.NODE_ENV || 'development'; 18 | 19 | /** 20 | * Error handler: 21 | * 22 | * Development error handler, providing stack traces 23 | * and error message responses for requests accepting text, html, 24 | * or json. 25 | * 26 | * Text: 27 | * 28 | * By default, and when _text/plain_ is accepted a simple stack trace 29 | * or error message will be returned. 30 | * 31 | * JSON: 32 | * 33 | * When _application/json_ is accepted, connect will respond with 34 | * an object in the form of `{ "error": error }`. 35 | * 36 | * HTML: 37 | * 38 | * When accepted connect will output a nice html stack trace. 39 | * 40 | * @return {Function} 41 | * @api public 42 | */ 43 | 44 | exports = module.exports = function errorHandler(){ 45 | return function errorHandler(err, req, res, next){ 46 | if (err.status) res.statusCode = err.status; 47 | if (res.statusCode < 400) res.statusCode = 500; 48 | if ('test' != env) console.error(err.stack); 49 | var accept = req.headers.accept || ''; 50 | // html 51 | if (~accept.indexOf('html')) { 52 | fs.readFile(__dirname + '/../public/style.css', 'utf8', function(e, style){ 53 | fs.readFile(__dirname + '/../public/error.html', 'utf8', function(e, html){ 54 | var stack = (err.stack || '') 55 | .split('\n').slice(1) 56 | .map(function(v){ return '
  • ' + v + '
  • '; }).join(''); 57 | html = html 58 | .replace('{style}', style) 59 | .replace('{stack}', stack) 60 | .replace('{title}', exports.title) 61 | .replace('{statusCode}', res.statusCode) 62 | .replace(/\{error\}/g, utils.escape(err.toString())); 63 | res.setHeader('Content-Type', 'text/html; charset=utf-8'); 64 | res.end(html); 65 | }); 66 | }); 67 | // json 68 | } else if (~accept.indexOf('json')) { 69 | var error = { message: err.message, stack: err.stack }; 70 | for (var prop in err) error[prop] = err[prop]; 71 | var json = JSON.stringify({ error: error }); 72 | res.setHeader('Content-Type', 'application/json'); 73 | res.end(json); 74 | // plain text 75 | } else { 76 | res.writeHead(res.statusCode, { 'Content-Type': 'text/plain' }); 77 | res.end(err.stack); 78 | } 79 | }; 80 | }; 81 | 82 | /** 83 | * Template title, framework authors may override this value. 84 | */ 85 | 86 | exports.title = 'Connect'; 87 | -------------------------------------------------------------------------------- /lib/middleware/session/cookie.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - session - Cookie 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var utils = require('../../utils') 14 | , cookie = require('cookie'); 15 | 16 | /** 17 | * Initialize a new `Cookie` with the given `options`. 18 | * 19 | * @param {IncomingMessage} req 20 | * @param {Object} options 21 | * @api private 22 | */ 23 | 24 | var Cookie = module.exports = function Cookie(options) { 25 | this.path = '/'; 26 | this.maxAge = null; 27 | this.httpOnly = true; 28 | if (options) utils.merge(this, options); 29 | this.originalMaxAge = undefined == this.originalMaxAge 30 | ? this.maxAge 31 | : this.originalMaxAge; 32 | }; 33 | 34 | /*! 35 | * Prototype. 36 | */ 37 | 38 | Cookie.prototype = { 39 | 40 | /** 41 | * Set expires `date`. 42 | * 43 | * @param {Date} date 44 | * @api public 45 | */ 46 | 47 | set expires(date) { 48 | this._expires = date; 49 | this.originalMaxAge = this.maxAge; 50 | }, 51 | 52 | /** 53 | * Get expires `date`. 54 | * 55 | * @return {Date} 56 | * @api public 57 | */ 58 | 59 | get expires() { 60 | return this._expires; 61 | }, 62 | 63 | /** 64 | * Set expires via max-age in `ms`. 65 | * 66 | * @param {Number} ms 67 | * @api public 68 | */ 69 | 70 | set maxAge(ms) { 71 | this.expires = 'number' == typeof ms 72 | ? new Date(Date.now() + ms) 73 | : ms; 74 | }, 75 | 76 | /** 77 | * Get expires max-age in `ms`. 78 | * 79 | * @return {Number} 80 | * @api public 81 | */ 82 | 83 | get maxAge() { 84 | return this.expires instanceof Date 85 | ? this.expires.valueOf() - Date.now() 86 | : this.expires; 87 | }, 88 | 89 | /** 90 | * Return cookie data object. 91 | * 92 | * @return {Object} 93 | * @api private 94 | */ 95 | 96 | get data() { 97 | return { 98 | originalMaxAge: this.originalMaxAge 99 | , expires: this._expires 100 | , secure: this.secure 101 | , httpOnly: this.httpOnly 102 | , domain: this.domain 103 | , path: this.path 104 | } 105 | }, 106 | 107 | /** 108 | * Return a serialized cookie string. 109 | * 110 | * @return {String} 111 | * @api public 112 | */ 113 | 114 | serialize: function(name, val){ 115 | return cookie.serialize(name, val, this.data); 116 | }, 117 | 118 | /** 119 | * Return JSON representation of this cookie. 120 | * 121 | * @return {Object} 122 | * @api private 123 | */ 124 | 125 | toJSON: function(){ 126 | return this.data; 127 | } 128 | }; 129 | -------------------------------------------------------------------------------- /test/support/http.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var EventEmitter = require('events').EventEmitter 7 | , methods = ['get', 'post', 'put', 'delete', 'head'] 8 | , connect = require('../../') 9 | , http = require('http'); 10 | 11 | module.exports = request; 12 | 13 | connect.proto.request = function(){ 14 | return request(this); 15 | }; 16 | 17 | function request(app) { 18 | return new Request(app); 19 | } 20 | 21 | function Request(app) { 22 | var self = this; 23 | this.data = []; 24 | this.header = {}; 25 | this.app = app; 26 | if (!this.server) { 27 | this.server = http.Server(app); 28 | this.server.listen(0, function(){ 29 | self.addr = self.server.address(); 30 | self.listening = true; 31 | }); 32 | } 33 | } 34 | 35 | /** 36 | * Inherit from `EventEmitter.prototype`. 37 | */ 38 | 39 | Request.prototype.__proto__ = EventEmitter.prototype; 40 | 41 | methods.forEach(function(method){ 42 | Request.prototype[method] = function(path){ 43 | return this.request(method, path); 44 | }; 45 | }); 46 | 47 | Request.prototype.set = function(field, val){ 48 | this.header[field] = val; 49 | return this; 50 | }; 51 | 52 | Request.prototype.write = function(data){ 53 | this.data.push(data); 54 | return this; 55 | }; 56 | 57 | Request.prototype.request = function(method, path){ 58 | this.method = method; 59 | this.path = path; 60 | return this; 61 | }; 62 | 63 | Request.prototype.expect = function(body, fn){ 64 | var args = arguments; 65 | this.end(function(res){ 66 | switch (args.length) { 67 | case 3: 68 | res.headers.should.have.property(body.toLowerCase(), args[1]); 69 | args[2](); 70 | break; 71 | default: 72 | if ('number' == typeof body) { 73 | res.statusCode.should.equal(body); 74 | } else { 75 | res.body.should.equal(body); 76 | } 77 | fn(); 78 | } 79 | }); 80 | }; 81 | 82 | Request.prototype.end = function(fn){ 83 | var self = this; 84 | 85 | if (this.listening) { 86 | var req = http.request({ 87 | method: this.method 88 | , port: this.addr.port 89 | , host: this.addr.address 90 | , path: this.path 91 | , headers: this.header 92 | }); 93 | 94 | this.data.forEach(function(chunk){ 95 | req.write(chunk); 96 | }); 97 | 98 | req.on('response', function(res){ 99 | var buf = ''; 100 | res.setEncoding('utf8'); 101 | res.on('data', function(chunk){ buf += chunk }); 102 | res.on('end', function(){ 103 | res.body = buf; 104 | fn(res); 105 | }); 106 | }); 107 | 108 | req.end(); 109 | } else { 110 | this.server.on('listening', function(){ 111 | self.end(fn); 112 | }); 113 | } 114 | 115 | return this; 116 | }; 117 | -------------------------------------------------------------------------------- /lib/middleware/session/memory.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - session - MemoryStore 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var Store = require('./store'); 14 | 15 | /** 16 | * Initialize a new `MemoryStore`. 17 | * 18 | * @api public 19 | */ 20 | 21 | var MemoryStore = module.exports = function MemoryStore() { 22 | this.sessions = {}; 23 | }; 24 | 25 | /** 26 | * Inherit from `Store.prototype`. 27 | */ 28 | 29 | MemoryStore.prototype.__proto__ = Store.prototype; 30 | 31 | /** 32 | * Attempt to fetch session by the given `sid`. 33 | * 34 | * @param {String} sid 35 | * @param {Function} fn 36 | * @api public 37 | */ 38 | 39 | MemoryStore.prototype.get = function(sid, fn){ 40 | var self = this; 41 | process.nextTick(function(){ 42 | var expires 43 | , sess = self.sessions[sid]; 44 | if (sess) { 45 | sess = JSON.parse(sess); 46 | expires = 'string' == typeof sess.cookie.expires 47 | ? new Date(sess.cookie.expires) 48 | : sess.cookie.expires; 49 | if (!expires || new Date < expires) { 50 | fn(null, sess); 51 | } else { 52 | self.destroy(sid, fn); 53 | } 54 | } else { 55 | fn(); 56 | } 57 | }); 58 | }; 59 | 60 | /** 61 | * Commit the given `sess` object associated with the given `sid`. 62 | * 63 | * @param {String} sid 64 | * @param {Session} sess 65 | * @param {Function} fn 66 | * @api public 67 | */ 68 | 69 | MemoryStore.prototype.set = function(sid, sess, fn){ 70 | var self = this; 71 | process.nextTick(function(){ 72 | self.sessions[sid] = JSON.stringify(sess); 73 | fn && fn(); 74 | }); 75 | }; 76 | 77 | /** 78 | * Destroy the session associated with the given `sid`. 79 | * 80 | * @param {String} sid 81 | * @api public 82 | */ 83 | 84 | MemoryStore.prototype.destroy = function(sid, fn){ 85 | var self = this; 86 | process.nextTick(function(){ 87 | delete self.sessions[sid]; 88 | fn && fn(); 89 | }); 90 | }; 91 | 92 | /** 93 | * Invoke the given callback `fn` with all active sessions. 94 | * 95 | * @param {Function} fn 96 | * @api public 97 | */ 98 | 99 | MemoryStore.prototype.all = function(fn){ 100 | var arr = [] 101 | , keys = Object.keys(this.sessions); 102 | for (var i = 0, len = keys.length; i < len; ++i) { 103 | arr.push(this.sessions[keys[i]]); 104 | } 105 | fn(null, arr); 106 | }; 107 | 108 | /** 109 | * Clear all sessions. 110 | * 111 | * @param {Function} fn 112 | * @api public 113 | */ 114 | 115 | MemoryStore.prototype.clear = function(fn){ 116 | this.sessions = {}; 117 | fn && fn(); 118 | }; 119 | 120 | /** 121 | * Fetch number of sessions. 122 | * 123 | * @param {Function} fn 124 | * @api public 125 | */ 126 | 127 | MemoryStore.prototype.length = function(fn){ 128 | fn(null, Object.keys(this.sessions).length); 129 | }; 130 | -------------------------------------------------------------------------------- /lib/middleware/session/session.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - session - Session 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var utils = require('../../utils'); 14 | 15 | /** 16 | * Create a new `Session` with the given request and `data`. 17 | * 18 | * @param {IncomingRequest} req 19 | * @param {Object} data 20 | * @api private 21 | */ 22 | 23 | var Session = module.exports = function Session(req, data) { 24 | Object.defineProperty(this, 'req', { value: req }); 25 | Object.defineProperty(this, 'id', { value: req.sessionID }); 26 | if ('object' == typeof data) utils.merge(this, data); 27 | }; 28 | 29 | /** 30 | * Update reset `.cookie.maxAge` to prevent 31 | * the cookie from expiring when the 32 | * session is still active. 33 | * 34 | * @return {Session} for chaining 35 | * @api public 36 | */ 37 | 38 | Session.prototype.touch = function(){ 39 | return this.resetMaxAge(); 40 | }; 41 | 42 | /** 43 | * Reset `.maxAge` to `.originalMaxAge`. 44 | * 45 | * @return {Session} for chaining 46 | * @api public 47 | */ 48 | 49 | Session.prototype.resetMaxAge = function(){ 50 | this.cookie.maxAge = this.cookie.originalMaxAge; 51 | return this; 52 | }; 53 | 54 | /** 55 | * Save the session data with optional callback `fn(err)`. 56 | * 57 | * @param {Function} fn 58 | * @return {Session} for chaining 59 | * @api public 60 | */ 61 | 62 | Session.prototype.save = function(fn){ 63 | this.req.sessionStore.set(this.id, this, fn || function(){}); 64 | return this; 65 | }; 66 | 67 | /** 68 | * Re-loads the session data _without_ altering 69 | * the maxAge properties. Invokes the callback `fn(err)`, 70 | * after which time if no exception has occurred the 71 | * `req.session` property will be a new `Session` object, 72 | * although representing the same session. 73 | * 74 | * @param {Function} fn 75 | * @return {Session} for chaining 76 | * @api public 77 | */ 78 | 79 | Session.prototype.reload = function(fn){ 80 | var req = this.req 81 | , store = this.req.sessionStore; 82 | store.get(this.id, function(err, sess){ 83 | if (err) return fn(err); 84 | if (!sess) return fn(new Error('failed to load session')); 85 | store.createSession(req, sess); 86 | fn(); 87 | }); 88 | return this; 89 | }; 90 | 91 | /** 92 | * Destroy `this` session. 93 | * 94 | * @param {Function} fn 95 | * @return {Session} for chaining 96 | * @api public 97 | */ 98 | 99 | Session.prototype.destroy = function(fn){ 100 | delete this.req.session; 101 | this.req.sessionStore.destroy(this.id, fn); 102 | return this; 103 | }; 104 | 105 | /** 106 | * Regenerate this request's session. 107 | * 108 | * @param {Function} fn 109 | * @return {Session} for chaining 110 | * @api public 111 | */ 112 | 113 | Session.prototype.regenerate = function(fn){ 114 | this.req.sessionStore.regenerate(this.req, fn); 115 | return this; 116 | }; 117 | -------------------------------------------------------------------------------- /lib/middleware/basicAuth.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - basicAuth 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var utils = require('../utils') 14 | , unauthorized = utils.unauthorized; 15 | 16 | /** 17 | * Basic Auth: 18 | * 19 | * Enfore basic authentication by providing a `callback(user, pass)`, 20 | * which must return `true` in order to gain access. Alternatively an async 21 | * method is provided as well, invoking `callback(user, pass, callback)`. Populates 22 | * `req.user`. The final alternative is simply passing username / password 23 | * strings. 24 | * 25 | * Simple username and password 26 | * 27 | * connect(connect.basicAuth('username', 'password')); 28 | * 29 | * Callback verification 30 | * 31 | * connect() 32 | * .use(connect.basicAuth(function(user, pass){ 33 | * return 'tj' == user & 'wahoo' == pass; 34 | * })) 35 | * 36 | * Async callback verification, accepting `fn(err, user)`. 37 | * 38 | * connect() 39 | * .use(connect.basicAuth(function(user, pass, fn){ 40 | * User.authenticate({ user: user, pass: pass }, fn); 41 | * })) 42 | * 43 | * @param {Function|String} callback or username 44 | * @param {String} realm 45 | * @api public 46 | */ 47 | 48 | module.exports = function basicAuth(callback, realm) { 49 | var username, password; 50 | 51 | // user / pass strings 52 | if ('string' == typeof callback) { 53 | username = callback; 54 | password = realm; 55 | if ('string' != typeof password) throw new Error('password argument required'); 56 | realm = arguments[2]; 57 | callback = function(user, pass){ 58 | return user == username && pass == password; 59 | } 60 | } 61 | 62 | realm = realm || 'Authorization Required'; 63 | 64 | return function(req, res, next) { 65 | var authorization = req.headers.authorization; 66 | 67 | if (req.user) return next(); 68 | if (!authorization) return unauthorized(res, realm); 69 | 70 | var parts = authorization.split(' '); 71 | 72 | if (parts.length !== 2) return next(utils.error(400)); 73 | 74 | var scheme = parts[0] 75 | , credentials = new Buffer(parts[1], 'base64').toString() 76 | , index = credentials.indexOf(':'); 77 | 78 | if ('Basic' != scheme || index < 0) return next(utils.error(400)); 79 | 80 | var user = credentials.slice(0, index) 81 | , pass = credentials.slice(index + 1); 82 | 83 | // async 84 | if (callback.length >= 3) { 85 | var pause = utils.pause(req); 86 | callback(user, pass, function(err, user){ 87 | if (err || !user) return unauthorized(res, realm); 88 | req.user = req.remoteUser = user; 89 | next(); 90 | pause.resume(); 91 | }); 92 | // sync 93 | } else { 94 | if (callback(user, pass)) { 95 | req.user = req.remoteUser = user; 96 | next(); 97 | } else { 98 | unauthorized(res, realm); 99 | } 100 | } 101 | } 102 | }; 103 | 104 | -------------------------------------------------------------------------------- /test/basicAuth.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | 4 | function test(app, signature) { 5 | describe(signature, function(){ 6 | describe('when missing Authorization', function(){ 7 | it('should respond with 401 and WWW-Authenticate', function(done){ 8 | app.request() 9 | .get('/') 10 | .end(function(res){ 11 | res.statusCode.should.equal(401); 12 | res.headers['www-authenticate'].should.equal('Basic realm="Authorization Required"'); 13 | done(); 14 | }); 15 | }) 16 | }) 17 | 18 | describe('when valid', function(){ 19 | it('should next()', function(done){ 20 | app.request() 21 | .get('/') 22 | .set('Authorization', 'Basic dGo6dG9iaTpsZWFybmJvb3N0') 23 | .end(function(res){ 24 | res.statusCode.should.equal(200); 25 | res.body.should.equal('secret!'); 26 | done(); 27 | }); 28 | }) 29 | }) 30 | 31 | describe('when invalid credentials', function(){ 32 | it('should respond with 401', function(done){ 33 | app.request() 34 | .get('/') 35 | .set('Authorization', 'Basic dGo69iaQ==') 36 | .end(function(res){ 37 | res.statusCode.should.equal(401); 38 | res.headers['www-authenticate'].should.equal('Basic realm="Authorization Required"'); 39 | res.body.should.equal('Unauthorized'); 40 | done(); 41 | }); 42 | }) 43 | }) 44 | 45 | describe('when authorization header is not Basic', function(){ 46 | it('should respond with 400', function(done){ 47 | app.request() 48 | .get('/') 49 | .set('Authorization', 'Digest dGo69iaQ==') 50 | .end(function(res){ 51 | res.statusCode.should.equal(400); 52 | res.body.should.match(/Bad Request/); 53 | done(); 54 | }); 55 | }) 56 | }) 57 | 58 | describe('when authorization header is malformed - contains only one part', function(){ 59 | it('should respond with 400', function(done){ 60 | app.request() 61 | .get('/') 62 | .set('Authorization', 'invalid') 63 | .end(function(res){ 64 | res.statusCode.should.equal(400); 65 | res.body.should.match(/Bad Request/); 66 | done(); 67 | }); 68 | }) 69 | }) 70 | }) 71 | } 72 | 73 | var app = connect(); 74 | 75 | app.use(connect.basicAuth('tj', 'tobi:learnboost')); 76 | 77 | app.use(function(req, res, next){ 78 | req.user.should.equal('tj'); 79 | res.end('secret!'); 80 | }); 81 | 82 | test(app, 'connect.basicAuth(user, pass)'); 83 | 84 | 85 | 86 | var app = connect(); 87 | 88 | app.use(connect.basicAuth(function(user, pass){ 89 | return 'tj' == user && 'tobi:learnboost' == pass; 90 | })); 91 | 92 | app.use(function(req, res, next){ 93 | req.user.should.equal('tj'); 94 | res.end('secret!'); 95 | }); 96 | 97 | test(app, 'connect.basicAuth(callback)'); 98 | 99 | 100 | 101 | var app = connect(); 102 | 103 | app.use(connect.basicAuth(function(user, pass, fn){ 104 | var ok = 'tj' == user && 'tobi:learnboost' == pass; 105 | fn(null, ok 106 | ? { name: 'tj' } 107 | : null); 108 | })); 109 | 110 | app.use(function(req, res, next){ 111 | req.user.name.should.equal('tj'); 112 | res.end('secret!'); 113 | }); 114 | 115 | test(app, 'connect.basicAuth(callback) async'); 116 | -------------------------------------------------------------------------------- /lib/public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 80px 100px; 4 | font: 13px "Helvetica Neue", "Lucida Grande", "Arial"; 5 | background: #ECE9E9 -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#ECE9E9)); 6 | background: #ECE9E9 -moz-linear-gradient(top, #fff, #ECE9E9); 7 | background-repeat: no-repeat; 8 | color: #555; 9 | -webkit-font-smoothing: antialiased; 10 | } 11 | h1, h2, h3 { 12 | margin: 0; 13 | font-size: 22px; 14 | color: #343434; 15 | } 16 | h1 em, h2 em { 17 | padding: 0 5px; 18 | font-weight: normal; 19 | } 20 | h1 { 21 | font-size: 60px; 22 | } 23 | h2 { 24 | margin-top: 10px; 25 | } 26 | h3 { 27 | margin: 5px 0 10px 0; 28 | padding-bottom: 5px; 29 | border-bottom: 1px solid #eee; 30 | font-size: 18px; 31 | } 32 | ul { 33 | margin: 0; 34 | padding: 0; 35 | } 36 | ul li { 37 | margin: 5px 0; 38 | padding: 3px 8px; 39 | list-style: none; 40 | } 41 | ul li:hover { 42 | cursor: pointer; 43 | color: #2e2e2e; 44 | } 45 | ul li .path { 46 | padding-left: 5px; 47 | font-weight: bold; 48 | } 49 | ul li .line { 50 | padding-right: 5px; 51 | font-style: italic; 52 | } 53 | ul li:first-child .path { 54 | padding-left: 0; 55 | } 56 | p { 57 | line-height: 1.5; 58 | } 59 | a { 60 | color: #555; 61 | text-decoration: none; 62 | } 63 | a:hover { 64 | color: #303030; 65 | } 66 | #stacktrace { 67 | margin-top: 15px; 68 | } 69 | .directory h1 { 70 | margin-bottom: 15px; 71 | font-size: 18px; 72 | } 73 | ul#files { 74 | width: 100%; 75 | height: 500px; 76 | } 77 | ul#files li { 78 | padding: 0; 79 | } 80 | ul#files li img { 81 | position: absolute; 82 | top: 5px; 83 | left: 5px; 84 | } 85 | ul#files li a { 86 | position: relative; 87 | display: block; 88 | margin: 1px; 89 | width: 30%; 90 | height: 25px; 91 | line-height: 25px; 92 | text-indent: 8px; 93 | float: left; 94 | border: 1px solid transparent; 95 | -webkit-border-radius: 5px; 96 | -moz-border-radius: 5px; 97 | border-radius: 5px; 98 | overflow: hidden; 99 | text-overflow: ellipsis; 100 | } 101 | ul#files li a.icon { 102 | text-indent: 25px; 103 | } 104 | ul#files li a:focus, 105 | ul#files li a:hover { 106 | outline: none; 107 | background: rgba(255,255,255,0.65); 108 | border: 1px solid #ececec; 109 | } 110 | ul#files li a.highlight { 111 | -webkit-transition: background .4s ease-in-out; 112 | background: #ffff4f; 113 | border-color: #E9DC51; 114 | } 115 | #search { 116 | display: block; 117 | position: fixed; 118 | top: 20px; 119 | right: 20px; 120 | width: 90px; 121 | -webkit-transition: width ease 0.2s, opacity ease 0.4s; 122 | -moz-transition: width ease 0.2s, opacity ease 0.4s; 123 | -webkit-border-radius: 32px; 124 | -moz-border-radius: 32px; 125 | -webkit-box-shadow: inset 0px 0px 3px rgba(0, 0, 0, 0.25), inset 0px 1px 3px rgba(0, 0, 0, 0.7), 0px 1px 0px rgba(255, 255, 255, 0.03); 126 | -moz-box-shadow: inset 0px 0px 3px rgba(0, 0, 0, 0.25), inset 0px 1px 3px rgba(0, 0, 0, 0.7), 0px 1px 0px rgba(255, 255, 255, 0.03); 127 | -webkit-font-smoothing: antialiased; 128 | text-align: left; 129 | font: 13px "Helvetica Neue", Arial, sans-serif; 130 | padding: 4px 10px; 131 | border: none; 132 | background: transparent; 133 | margin-bottom: 0; 134 | outline: none; 135 | opacity: 0.7; 136 | color: #888; 137 | } 138 | #search:focus { 139 | width: 120px; 140 | opacity: 1.0; 141 | } 142 | -------------------------------------------------------------------------------- /lib/middleware/multipart.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Connect - multipart 3 | * Copyright(c) 2010 Sencha Inc. 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var formidable = require('formidable') 13 | , _limit = require('./limit') 14 | , utils = require('../utils') 15 | , qs = require('qs'); 16 | 17 | /** 18 | * noop middleware. 19 | */ 20 | 21 | function noop(req, res, next) { 22 | next(); 23 | } 24 | 25 | /** 26 | * Multipart: 27 | * 28 | * Parse multipart/form-data request bodies, 29 | * providing the parsed object as `req.body` 30 | * and `req.files`. 31 | * 32 | * Configuration: 33 | * 34 | * The options passed are merged with [formidable](https://github.com/felixge/node-formidable)'s 35 | * `IncomingForm` object, allowing you to configure the upload directory, 36 | * size limits, etc. For example if you wish to change the upload dir do the following. 37 | * 38 | * app.use(connect.multipart({ uploadDir: path })); 39 | * 40 | * Options: 41 | * 42 | * - `limit` byte limit defaulting to none 43 | * - `defer` defers processing and exposes the Formidable form object as `req.form`. 44 | * `next()` is called without waiting for the form's "end" event. 45 | * This option is useful if you need to bind to the "progress" event, for example. 46 | * 47 | * @param {Object} options 48 | * @return {Function} 49 | * @api public 50 | */ 51 | 52 | exports = module.exports = function(options){ 53 | options = options || {}; 54 | 55 | var limit = options.limit 56 | ? _limit(options.limit) 57 | : noop; 58 | 59 | return function multipart(req, res, next) { 60 | if (req._body) return next(); 61 | req.body = req.body || {}; 62 | req.files = req.files || {}; 63 | 64 | if (!utils.hasBody(req)) return next(); 65 | 66 | // ignore GET 67 | if ('GET' == req.method || 'HEAD' == req.method) return next(); 68 | 69 | // check Content-Type 70 | if ('multipart/form-data' != utils.mime(req)) return next(); 71 | 72 | // flag as parsed 73 | req._body = true; 74 | 75 | // parse 76 | limit(req, res, function(err){ 77 | if (err) return next(err); 78 | 79 | var form = new formidable.IncomingForm 80 | , data = {} 81 | , files = {} 82 | , done; 83 | 84 | Object.keys(options).forEach(function(key){ 85 | form[key] = options[key]; 86 | }); 87 | 88 | function ondata(name, val, data){ 89 | if (Array.isArray(data[name])) { 90 | data[name].push(val); 91 | } else if (data[name]) { 92 | data[name] = [data[name], val]; 93 | } else { 94 | data[name] = val; 95 | } 96 | } 97 | 98 | form.on('field', function(name, val){ 99 | ondata(name, val, data); 100 | }); 101 | 102 | form.on('file', function(name, val){ 103 | ondata(name, val, files); 104 | }); 105 | 106 | form.on('error', function(err){ 107 | if (!options.defer) { 108 | err.status = 400; 109 | next(err); 110 | } 111 | done = true; 112 | }); 113 | 114 | form.on('end', function(){ 115 | if (done) return; 116 | try { 117 | req.body = qs.parse(data); 118 | req.files = qs.parse(files); 119 | if (!options.defer) next(); 120 | } catch (err) { 121 | form.emit('error', err); 122 | } 123 | }); 124 | 125 | form.parse(req); 126 | 127 | if (options.defer) { 128 | req.form = form; 129 | next(); 130 | } 131 | }); 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /lib/middleware/cookieSession.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - cookieSession 4 | * Copyright(c) 2011 Sencha Inc. 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var utils = require('./../utils') 13 | , Cookie = require('./session/cookie') 14 | , debug = require('debug')('connect:cookieSession') 15 | , signature = require('cookie-signature') 16 | , crc16 = require('crc').crc16; 17 | 18 | /** 19 | * Cookie Session: 20 | * 21 | * Cookie session middleware. 22 | * 23 | * var app = connect(); 24 | * app.use(connect.cookieParser()); 25 | * app.use(connect.cookieSession({ secret: 'tobo!', cookie: { maxAge: 60 * 60 * 1000 }})); 26 | * 27 | * Options: 28 | * 29 | * - `key` cookie name defaulting to `connect.sess` 30 | * - `secret` prevents cookie tampering 31 | * - `cookie` session cookie settings, defaulting to `{ path: '/', httpOnly: true, maxAge: null }` 32 | * - `proxy` trust the reverse proxy when setting secure cookies (via "x-forwarded-proto") 33 | * 34 | * Clearing sessions: 35 | * 36 | * To clear the session simply set its value to `null`, 37 | * `cookieSession()` will then respond with a 1970 Set-Cookie. 38 | * 39 | * req.session = null; 40 | * 41 | * @param {Object} options 42 | * @return {Function} 43 | * @api public 44 | */ 45 | 46 | module.exports = function cookieSession(options){ 47 | // TODO: utilize Session/Cookie to unify API 48 | options = options || {}; 49 | var key = options.key || 'connect.sess' 50 | , trustProxy = options.proxy; 51 | 52 | return function cookieSession(req, res, next) { 53 | 54 | // req.secret is for backwards compatibility 55 | var secret = options.secret || req.secret; 56 | if (!secret) throw new Error('`secret` option required for cookie sessions'); 57 | 58 | // default session 59 | req.session = {}; 60 | var cookie = req.session.cookie = new Cookie(options.cookie); 61 | 62 | // pathname mismatch 63 | if (0 != req.originalUrl.indexOf(cookie.path)) return next(); 64 | 65 | // cookieParser secret 66 | if (!options.secret && req.secret) { 67 | req.session = req.signedCookies[key] || {}; 68 | } else { 69 | // TODO: refactor 70 | var rawCookie = req.cookies[key]; 71 | if (rawCookie) { 72 | var unsigned = utils.parseSignedCookie(rawCookie, secret); 73 | if (unsigned) { 74 | var originalHash = crc16(unsigned); 75 | req.session = utils.parseJSONCookie(unsigned) || {}; 76 | } 77 | } 78 | } 79 | 80 | res.on('header', function(){ 81 | // removed 82 | if (!req.session) { 83 | debug('clear session'); 84 | cookie.expires = new Date(0); 85 | res.setHeader('Set-Cookie', cookie.serialize(key, '')); 86 | return; 87 | } 88 | 89 | delete req.session.cookie; 90 | 91 | // check security 92 | var proto = (req.headers['x-forwarded-proto'] || '').toLowerCase() 93 | , tls = req.connection.encrypted || (trustProxy && 'https' == proto) 94 | , secured = cookie.secure && tls; 95 | 96 | // only send secure cookies via https 97 | if (cookie.secure && !secured) return debug('not secured'); 98 | 99 | // serialize 100 | debug('serializing %j', req.session); 101 | var val = 'j:' + JSON.stringify(req.session); 102 | 103 | // compare hashes, no need to set-cookie if unchanged 104 | if (originalHash == crc16(val)) return debug('unmodified session'); 105 | 106 | // set-cookie 107 | val = 's:' + signature.sign(val, secret); 108 | val = cookie.serialize(key, val); 109 | debug('set-cookie %j', cookie); 110 | res.setHeader('Set-Cookie', val); 111 | }); 112 | 113 | next(); 114 | }; 115 | }; 116 | -------------------------------------------------------------------------------- /test/mounting.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../') 3 | , http = require('http'); 4 | 5 | describe('app.use()', function(){ 6 | var app; 7 | 8 | beforeEach(function(){ 9 | app = connect(); 10 | }) 11 | 12 | describe('with a connect app', function(){ 13 | it('should mount', function(done){ 14 | var blog = connect(); 15 | 16 | blog.use(function(req, res){ 17 | req.url.should.equal('/'); 18 | res.end('blog'); 19 | }); 20 | 21 | app.use('/blog', blog); 22 | 23 | app.request() 24 | .get('/blog') 25 | .expect('blog', done); 26 | }) 27 | 28 | it('should retain req.originalUrl', function(done){ 29 | var app = connect(); 30 | 31 | app.use('/blog', function(req, res){ 32 | res.end(req.originalUrl); 33 | }); 34 | 35 | app.request() 36 | .get('/blog/post/1') 37 | .expect('/blog/post/1', done); 38 | }) 39 | 40 | it('should adjust req.url', function(done){ 41 | var app = connect(); 42 | 43 | app.use('/blog', function(req, res){ 44 | res.end(req.url); 45 | }); 46 | 47 | app.request() 48 | .get('/blog/post/1') 49 | .expect('/post/1', done); 50 | }) 51 | 52 | it('should strip trailing slash', function(done){ 53 | var blog = connect(); 54 | 55 | blog.use(function(req, res){ 56 | req.url.should.equal('/'); 57 | res.end('blog'); 58 | }); 59 | 60 | app.use('/blog/', blog); 61 | 62 | app.request() 63 | .get('/blog') 64 | .expect('blog', done); 65 | }) 66 | 67 | it('should set .route', function(){ 68 | var blog = connect(); 69 | var admin = connect(); 70 | app.use('/blog', blog); 71 | blog.use('/admin', admin); 72 | app.route.should.equal('/'); 73 | blog.route.should.equal('/blog'); 74 | admin.route.should.equal('/admin'); 75 | }) 76 | 77 | it('should not add trailing slash to req.url', function(done) { 78 | var app = connect(); 79 | 80 | app.use('/admin', function(req, res, next) { 81 | next(); 82 | }); 83 | 84 | app.use(function(req, res, next) { 85 | res.end(req.url); 86 | }); 87 | 88 | app.request() 89 | .get('/admin') 90 | .expect('/admin', done); 91 | }) 92 | }) 93 | 94 | describe('with a node app', function(){ 95 | it('should mount', function(done){ 96 | var blog = http.createServer(function(req, res){ 97 | req.url.should.equal('/'); 98 | res.end('blog'); 99 | }); 100 | 101 | app.use('/blog', blog); 102 | 103 | app.request() 104 | .get('/blog') 105 | .expect('blog', done); 106 | }) 107 | }) 108 | 109 | it('should be case insensitive (lower-case route, mixed-case request)', function(done){ 110 | var blog = http.createServer(function(req, res){ 111 | req.url.should.equal('/'); 112 | res.end('blog'); 113 | }); 114 | 115 | app.use('/blog', blog); 116 | 117 | app.request() 118 | .get('/BLog') 119 | .expect('blog', done); 120 | }) 121 | 122 | it('should be case insensitive (mixed-case route, lower-case request)', function(done){ 123 | var blog = http.createServer(function(req, res){ 124 | req.url.should.equal('/'); 125 | res.end('blog'); 126 | }); 127 | 128 | app.use('/BLog', blog); 129 | 130 | app.request() 131 | .get('/blog') 132 | .expect('blog', done); 133 | }) 134 | 135 | it('should be case insensitive (mixed-case route, mixed-case request)', function(done){ 136 | var blog = http.createServer(function(req, res){ 137 | req.url.should.equal('/'); 138 | res.end('blog'); 139 | }); 140 | 141 | app.use('/BLog', blog); 142 | 143 | app.request() 144 | .get('/blOG') 145 | .expect('blog', done); 146 | }) 147 | }) 148 | -------------------------------------------------------------------------------- /lib/middleware/compress.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Connect - compress 3 | * Copyright(c) 2010 Sencha Inc. 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var zlib = require('zlib'); 13 | 14 | /** 15 | * Supported content-encoding methods. 16 | */ 17 | 18 | exports.methods = { 19 | gzip: zlib.createGzip 20 | , deflate: zlib.createDeflate 21 | }; 22 | 23 | /** 24 | * Default filter function. 25 | */ 26 | 27 | exports.filter = function(req, res){ 28 | return /json|text|javascript/.test(res.getHeader('Content-Type')); 29 | }; 30 | 31 | /** 32 | * Compress: 33 | * 34 | * Compress response data with gzip/deflate. 35 | * 36 | * Filter: 37 | * 38 | * A `filter` callback function may be passed to 39 | * replace the default logic of: 40 | * 41 | * exports.filter = function(req, res){ 42 | * return /json|text|javascript/.test(res.getHeader('Content-Type')); 43 | * }; 44 | * 45 | * Options: 46 | * 47 | * All remaining options are passed to the gzip/deflate 48 | * creation functions. Consult node's docs for additional details. 49 | * 50 | * - `chunkSize` (default: 16*1024) 51 | * - `windowBits` 52 | * - `level`: 0-9 where 0 is no compression, and 9 is slow but best compression 53 | * - `memLevel`: 1-9 low is slower but uses less memory, high is fast but uses more 54 | * - `strategy`: compression strategy 55 | * 56 | * @param {Object} options 57 | * @return {Function} 58 | * @api public 59 | */ 60 | 61 | module.exports = function compress(options) { 62 | options = options || {}; 63 | var names = Object.keys(exports.methods) 64 | , filter = options.filter || exports.filter; 65 | 66 | return function(req, res, next){ 67 | var accept = req.headers['accept-encoding'] 68 | , write = res.write 69 | , end = res.end 70 | , stream 71 | , method; 72 | 73 | // vary 74 | res.setHeader('Vary', 'Accept-Encoding'); 75 | 76 | // proxy 77 | 78 | res.write = function(chunk, encoding){ 79 | if (!this.headerSent) this._implicitHeader(); 80 | return stream 81 | ? stream.write(new Buffer(chunk, encoding)) 82 | : write.call(res, chunk, encoding); 83 | }; 84 | 85 | res.end = function(chunk, encoding){ 86 | if (chunk) this.write(chunk, encoding); 87 | return stream 88 | ? stream.end() 89 | : end.call(res); 90 | }; 91 | 92 | res.on('header', function(){ 93 | var encoding = res.getHeader('Content-Encoding') || 'identity'; 94 | 95 | // already encoded 96 | if ('identity' != encoding) return; 97 | 98 | // default request filter 99 | if (!filter(req, res)) return; 100 | 101 | // SHOULD use identity 102 | if (!accept) return; 103 | 104 | // head 105 | if ('HEAD' == req.method) return; 106 | 107 | // default to gzip 108 | if ('*' == accept.trim()) method = 'gzip'; 109 | 110 | // compression method 111 | if (!method) { 112 | for (var i = 0, len = names.length; i < len; ++i) { 113 | if (~accept.indexOf(names[i])) { 114 | method = names[i]; 115 | break; 116 | } 117 | } 118 | } 119 | 120 | // compression method 121 | if (!method) return; 122 | 123 | // compression stream 124 | stream = exports.methods[method](options); 125 | 126 | // header fields 127 | res.setHeader('Content-Encoding', method); 128 | res.removeHeader('Content-Length'); 129 | 130 | // compression 131 | 132 | stream.on('data', function(chunk){ 133 | write.call(res, chunk); 134 | }); 135 | 136 | stream.on('end', function(){ 137 | end.call(res); 138 | }); 139 | 140 | stream.on('drain', function() { 141 | res.emit('drain'); 142 | }); 143 | }); 144 | 145 | next(); 146 | }; 147 | }; 148 | -------------------------------------------------------------------------------- /test/json.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../') 3 | , should = require('./shared'); 4 | 5 | var app = connect(); 6 | 7 | app.use(connect.json({ limit: '1mb' })); 8 | 9 | app.use(function(req, res){ 10 | res.end(JSON.stringify(req.body)); 11 | }); 12 | 13 | app.use(function(err, req, res, next){ 14 | res.statusCode = err.status; 15 | res.end(err.message); 16 | }); 17 | 18 | describe('connect.json()', function(){ 19 | should['default request body'](app); 20 | should['limit body to']('1mb', 'application/json', app); 21 | 22 | it('should parse JSON', function(done){ 23 | app.request() 24 | .post('/') 25 | .set('Content-Type', 'application/json') 26 | .write('{"user":"tobi"}') 27 | .end(function(res){ 28 | res.body.should.equal('{"user":"tobi"}'); 29 | done(); 30 | }); 31 | }) 32 | 33 | it('should fail gracefully', function(done){ 34 | app.request() 35 | .post('/') 36 | .set('Content-Type', 'application/json') 37 | .write('{"user"') 38 | .end(function(res){ 39 | res.body.should.equal('Unexpected end of input'); 40 | done(); 41 | }); 42 | }) 43 | 44 | it('should 400 on malformed JSON', function(done){ 45 | var app = connect(); 46 | app.use(connect.json()); 47 | 48 | app.use(function(req, res){ 49 | res.end(JSON.stringify(req.body)); 50 | }); 51 | 52 | app.request() 53 | .post('/') 54 | .set('Content-Type', 'application/json') 55 | .write('{"foo') 56 | .expect(400, done); 57 | }) 58 | 59 | it('should 400 when no body is given', function(done){ 60 | var app = connect(); 61 | app.use(connect.json()); 62 | 63 | app.use(function(req, res){ 64 | res.end(JSON.stringify(req.body)); 65 | }); 66 | 67 | app.request() 68 | .post('/') 69 | .set('Content-Type', 'application/json') 70 | .expect(400, done); 71 | }) 72 | 73 | it('should support all http methods', function(done){ 74 | var app = connect(); 75 | app.use(connect.json()); 76 | 77 | app.use(function(req, res){ 78 | res.end(JSON.stringify(req.body)); 79 | }); 80 | 81 | app.request() 82 | .get('/') 83 | .set('Content-Type', 'application/json') 84 | .set('Content-Length', '["foo"]'.length) 85 | .write('["foo"]') 86 | .expect('["foo"]', done); 87 | }) 88 | 89 | describe('when strict is false', function(){ 90 | it('should parse primitives', function(done){ 91 | var app = connect(); 92 | app.use(connect.json({ strict: false })); 93 | 94 | app.use(function(req, res){ 95 | res.end(JSON.stringify(req.body)); 96 | }); 97 | 98 | app.request() 99 | .post('/') 100 | .set('Content-Type', 'application/json') 101 | .write('true') 102 | .expect('true', done); 103 | }) 104 | }) 105 | 106 | describe('when strict is true', function(){ 107 | it('should not parse primitives', function(done){ 108 | var app = connect(); 109 | app.use(connect.json({ strict: true })); 110 | 111 | app.use(function(req, res){ 112 | res.end(JSON.stringify(req.body)); 113 | }); 114 | 115 | app.request() 116 | .post('/') 117 | .set('Content-Type', 'application/json') 118 | .write('true') 119 | .end(function(res){ 120 | res.should.have.status(400); 121 | res.body.should.include('invalid json'); 122 | done(); 123 | }); 124 | }) 125 | 126 | it('should allow leading whitespaces in JSON', function(done){ 127 | var app = connect(); 128 | app.use(connect.json({ strict: true })); 129 | 130 | app.use(function(req, res){ 131 | res.end(JSON.stringify(req.body)); 132 | }); 133 | 134 | app.request() 135 | .post('/') 136 | .set('Content-Type', 'application/json') 137 | .write(' { "user": "tobi" }') 138 | .end(function(res){ 139 | res.should.have.status(200); 140 | res.body.should.include('{"user":"tobi"}'); 141 | done(); 142 | }); 143 | }) 144 | }) 145 | 146 | describe('by default', function(){ 147 | it('should 400 on primitives', function(done){ 148 | var app = connect(); 149 | app.use(connect.json()); 150 | 151 | app.use(function(req, res){ 152 | res.end(JSON.stringify(req.body)); 153 | }); 154 | 155 | app.request() 156 | .post('/') 157 | .set('Content-Type', 'application/json') 158 | .write('true') 159 | .expect(400, done); 160 | }) 161 | }) 162 | 163 | it('should support utf-8', function(done){ 164 | var app = connect(); 165 | 166 | app.use(connect.json()); 167 | 168 | app.use(function(req, res, next){ 169 | res.end(req.body.name); 170 | }); 171 | 172 | app.request() 173 | .post('/') 174 | .set('Content-Type', 'application/json; charset=utf-8') 175 | .write('{"name":"论"}') 176 | .expect('论', done); 177 | }) 178 | }) -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | [![build status](https://secure.travis-ci.org/senchalabs/connect.png)](http://travis-ci.org/senchalabs/connect) 2 | # Connect 3 | 4 | Connect is an extensible HTTP server framework for [node](http://nodejs.org), providing high performance "plugins" known as _middleware_. 5 | 6 | Connect is bundled with over _20_ commonly used middleware, including 7 | a logger, session support, cookie parser, and [more](http://senchalabs.github.com/connect). Be sure to view the 2.x [documentation](http://senchalabs.github.com/connect/). 8 | 9 | ```js 10 | var connect = require('connect') 11 | , http = require('http'); 12 | 13 | var app = connect() 14 | .use(connect.favicon()) 15 | .use(connect.logger('dev')) 16 | .use(connect.static('public')) 17 | .use(connect.directory('public')) 18 | .use(connect.cookieParser()) 19 | .use(connect.session({ secret: 'my secret here' })) 20 | .use(function(req, res){ 21 | res.end('Hello from Connect!\n'); 22 | }); 23 | 24 | http.createServer(app).listen(3000); 25 | ``` 26 | 27 | ## Middleware 28 | 29 | - [csrf](http://www.senchalabs.org/connect/csrf.html) 30 | - [basicAuth](http://www.senchalabs.org/connect/basicAuth.html) 31 | - [bodyParser](http://www.senchalabs.org/connect/bodyParser.html) 32 | - [json](http://www.senchalabs.org/connect/json.html) 33 | - [multipart](http://www.senchalabs.org/connect/multipart.html) 34 | - [urlencoded](http://www.senchalabs.org/connect/urlencoded.html) 35 | - [cookieParser](http://www.senchalabs.org/connect/cookieParser.html) 36 | - [directory](http://www.senchalabs.org/connect/directory.html) 37 | - [compress](http://www.senchalabs.org/connect/compress.html) 38 | - [errorHandler](http://www.senchalabs.org/connect/errorHandler.html) 39 | - [favicon](http://www.senchalabs.org/connect/favicon.html) 40 | - [limit](http://www.senchalabs.org/connect/limit.html) 41 | - [logger](http://www.senchalabs.org/connect/logger.html) 42 | - [methodOverride](http://www.senchalabs.org/connect/methodOverride.html) 43 | - [query](http://www.senchalabs.org/connect/query.html) 44 | - [responseTime](http://www.senchalabs.org/connect/responseTime.html) 45 | - [session](http://www.senchalabs.org/connect/session.html) 46 | - [static](http://www.senchalabs.org/connect/static.html) 47 | - [staticCache](http://www.senchalabs.org/connect/staticCache.html) 48 | - [vhost](http://www.senchalabs.org/connect/vhost.html) 49 | - [subdomains](http://www.senchalabs.org/connect/subdomains.html) 50 | - [cookieSession](http://www.senchalabs.org/connect/cookieSession.html) 51 | 52 | ## Running Tests 53 | 54 | first: 55 | 56 | $ npm install -d 57 | 58 | then: 59 | 60 | $ make test 61 | 62 | ## Authors 63 | 64 | Below is the output from [git-summary](http://github.com/visionmedia/git-extras). 65 | 66 | 67 | project: connect 68 | commits: 2033 69 | active : 301 days 70 | files : 171 71 | authors: 72 | 1414 Tj Holowaychuk 69.6% 73 | 298 visionmedia 14.7% 74 | 191 Tim Caswell 9.4% 75 | 51 TJ Holowaychuk 2.5% 76 | 10 Ryan Olds 0.5% 77 | 8 Astro 0.4% 78 | 5 Nathan Rajlich 0.2% 79 | 5 Jakub Nešetřil 0.2% 80 | 3 Daniel Dickison 0.1% 81 | 3 David Rio Deiros 0.1% 82 | 3 Alexander Simmerl 0.1% 83 | 3 Andreas Lind Petersen 0.1% 84 | 2 Aaron Heckmann 0.1% 85 | 2 Jacques Crocker 0.1% 86 | 2 Fabian Jakobs 0.1% 87 | 2 Brian J Brennan 0.1% 88 | 2 Adam Malcontenti-Wilson 0.1% 89 | 2 Glen Mailer 0.1% 90 | 2 James Campos 0.1% 91 | 1 Trent Mick 0.0% 92 | 1 Troy Kruthoff 0.0% 93 | 1 Wei Zhu 0.0% 94 | 1 comerc 0.0% 95 | 1 darobin 0.0% 96 | 1 nateps 0.0% 97 | 1 Marco Sanson 0.0% 98 | 1 Arthur Taylor 0.0% 99 | 1 Aseem Kishore 0.0% 100 | 1 Bart Teeuwisse 0.0% 101 | 1 Cameron Howey 0.0% 102 | 1 Chad Weider 0.0% 103 | 1 Craig Barnes 0.0% 104 | 1 Eran Hammer-Lahav 0.0% 105 | 1 Gregory McWhirter 0.0% 106 | 1 Guillermo Rauch 0.0% 107 | 1 Jae Kwon 0.0% 108 | 1 Jakub Nesetril 0.0% 109 | 1 Joshua Peek 0.0% 110 | 1 Jxck 0.0% 111 | 1 AJ ONeal 0.0% 112 | 1 Michael Hemesath 0.0% 113 | 1 Morten Siebuhr 0.0% 114 | 1 Samori Gorse 0.0% 115 | 1 Tom Jensen 0.0% 116 | 117 | ## Node Compatibility 118 | 119 | Connect `< 1.x` is compatible with node 0.2.x 120 | 121 | 122 | Connect `1.x` is compatible with node 0.4.x 123 | 124 | 125 | Connect (_master_) `2.x` is compatible with node 0.6.x 126 | 127 | ## CLA 128 | 129 | [http://sencha.com/cla](http://sencha.com/cla) 130 | 131 | ## License 132 | 133 | View the [LICENSE](https://github.com/senchalabs/connect/blob/master/LICENSE) file. The [Silk](http://www.famfamfam.com/lab/icons/silk/) icons used by the `directory` middleware created by/copyright of [FAMFAMFAM](http://www.famfamfam.com/). 134 | -------------------------------------------------------------------------------- /examples/session.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../') 3 | , http = require('http'); 4 | 5 | // expire sessions within a minute 6 | // /favicon.ico is ignored, and will not 7 | // receive req.session 8 | 9 | http.createServer(connect() 10 | .use(connect.cookieParser()) 11 | .use(connect.session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }})) 12 | .use(connect.favicon()) 13 | .use(function(req, res, next){ 14 | var sess = req.session; 15 | if (sess.views) { 16 | sess.views++; 17 | res.setHeader('Content-Type', 'text/html'); 18 | res.write('

    views: ' + sess.views + '

    '); 19 | res.write('

    expires in: ' + (sess.cookie.maxAge / 1000) + 's

    '); 20 | res.end(); 21 | } else { 22 | sess.views = 1; 23 | res.end('welcome to the session demo. refresh!'); 24 | } 25 | })).listen(3000); 26 | 27 | console.log('port 3000: 1 minute expiration demo'); 28 | 29 | // $ npm install connect-redis 30 | 31 | try { 32 | var RedisStore = require('connect-redis')(connect); 33 | http.createServer(connect() 34 | .use(connect.cookieParser()) 35 | .use(connect.session({ 36 | secret: 'keyboard cat', 37 | cookie: { maxAge: 60000 * 3 } 38 | , store: new RedisStore 39 | })) 40 | .use(connect.favicon()) 41 | .use(function(req, res, next){ 42 | var sess = req.session; 43 | if (sess.views) { 44 | sess.views++; 45 | res.setHeader('Content-Type', 'text/html'); 46 | res.end('

    views: ' + sess.views + '

    '); 47 | } else { 48 | sess.views = 1; 49 | res.end('welcome to the redis demo. refresh!'); 50 | } 51 | })).listen(3001); 52 | 53 | console.log('port 3001: redis example'); 54 | } catch (err) { 55 | console.log('\033[33m'); 56 | console.log('failed to start the Redis example.'); 57 | console.log('to try it install redis, start redis'); 58 | console.log('install connect-redis, and run this'); 59 | console.log('script again.'); 60 | console.log(' $ redis-server'); 61 | console.log(' $ npm install connect-redis'); 62 | console.log('\033[0m'); 63 | } 64 | 65 | // conditional session support by simply 66 | // wrapping middleware with middleware. 67 | 68 | var sess = connect.session({ secret: 'keyboard cat', cookie: { maxAge: 5000 }}); 69 | 70 | http.createServer(connect() 71 | .use(connect.cookieParser()) 72 | .use(function(req, res, next){ 73 | if ('/foo' == req.url || '/bar' == req.url) { 74 | sess(req, res, next); 75 | } else { 76 | next(); 77 | } 78 | }) 79 | .use(connect.favicon()) 80 | .use(function(req, res, next){ 81 | res.end('has session: ' + (req.session ? 'yes' : 'no')); 82 | })).listen(3002); 83 | 84 | console.log('port 3002: conditional sessions'); 85 | 86 | // Session#reload() will update req.session 87 | // without altering .maxAge 88 | 89 | // view the page several times, and see that the 90 | // setInterval can still gain access to new 91 | // session data 92 | 93 | http.createServer(connect() 94 | .use(connect.cookieParser()) 95 | .use(connect.session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }})) 96 | .use(connect.favicon()) 97 | .use(function(req, res, next){ 98 | var sess = req.session 99 | , prev; 100 | 101 | if (sess.views) { 102 | res.setHeader('Content-Type', 'text/html'); 103 | res.write('

    views: ' + sess.views + '

    '); 104 | res.write('

    expires in: ' + (sess.cookie.maxAge / 1000) + 's

    '); 105 | sess.views++; 106 | res.end(); 107 | } else { 108 | sess.views = 1; 109 | setInterval(function(){ 110 | sess.reload(function(){ 111 | console.log(); 112 | if (prev) console.log('previous views %d, now %d', prev, req.session.views); 113 | console.log('time remaining until expiry: %ds', (req.session.cookie.maxAge / 1000)); 114 | prev = req.session.views; 115 | }); 116 | }, 3000); 117 | res.end('welcome to the session demo. refresh!'); 118 | } 119 | })).listen(3003); 120 | 121 | console.log('port 3003: Session#reload() demo'); 122 | 123 | // by default sessions 124 | // last the duration of 125 | // a user-agent's own session, 126 | // aka while the browser is open. 127 | 128 | http.createServer(connect() 129 | .use(connect.cookieParser()) 130 | .use(connect.session({ secret: 'keyboard cat' })) 131 | .use(connect.favicon()) 132 | .use(function(req, res, next){ 133 | var sess = req.session; 134 | if (sess.views) { 135 | res.setHeader('Content-Type', 'text/html'); 136 | res.write('

    views: ' + sess.views + '

    '); 137 | res.end(); 138 | sess.views++; 139 | } else { 140 | sess.views = 1; 141 | res.end('welcome to the browser session demo. refresh!'); 142 | } 143 | })).listen(3004); 144 | 145 | console.log('port 3004: browser-session length sessions'); 146 | 147 | // persistence example, enter your name! 148 | 149 | http.createServer(connect() 150 | .use(connect.bodyParser()) 151 | .use(connect.cookieParser()) 152 | .use(connect.session({ secret: 'keyboard cat' })) 153 | .use(connect.favicon()) 154 | .use(function(req, res, next){ 155 | if ('POST' != req.method) return next(); 156 | req.session.name = req.body.name; 157 | res.statusCode = 302; 158 | res.setHeader('Location', '/'); 159 | res.end(); 160 | }) 161 | .use(function(req, res, next){ 162 | var sess = req.session; 163 | res.setHeader('Content-Type', 'text/html'); 164 | if (sess.name) res.write('

    Hey ' + sess.name + '!

    '); 165 | else res.write('

    Enter a username:

    '); 166 | res.end('
    ' 167 | + '' 168 | + '' 169 | + '
    '); 170 | })).listen(3005); 171 | 172 | console.log('port 3005: browser-session length sessions persistence example'); 173 | -------------------------------------------------------------------------------- /lib/middleware/directory.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - directory 4 | * Copyright(c) 2011 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | // TODO: icon / style for directories 10 | // TODO: arrow key navigation 11 | // TODO: make icons extensible 12 | 13 | /** 14 | * Module dependencies. 15 | */ 16 | 17 | var fs = require('fs') 18 | , parse = require('url').parse 19 | , utils = require('../utils') 20 | , path = require('path') 21 | , normalize = path.normalize 22 | , extname = path.extname 23 | , join = path.join; 24 | 25 | /*! 26 | * Icon cache. 27 | */ 28 | 29 | var cache = {}; 30 | 31 | /** 32 | * Directory: 33 | * 34 | * Serve directory listings with the given `root` path. 35 | * 36 | * Options: 37 | * 38 | * - `hidden` display hidden (dot) files. Defaults to false. 39 | * - `icons` display icons. Defaults to false. 40 | * - `filter` Apply this filter function to files. Defaults to false. 41 | * 42 | * @param {String} root 43 | * @param {Object} options 44 | * @return {Function} 45 | * @api public 46 | */ 47 | 48 | exports = module.exports = function directory(root, options){ 49 | options = options || {}; 50 | 51 | // root required 52 | if (!root) throw new Error('directory() root path required'); 53 | var hidden = options.hidden 54 | , icons = options.icons 55 | , filter = options.filter 56 | , root = normalize(root); 57 | 58 | return function directory(req, res, next) { 59 | if ('GET' != req.method && 'HEAD' != req.method) return next(); 60 | 61 | var accept = req.headers.accept || 'text/plain' 62 | , url = parse(req.url) 63 | , dir = decodeURIComponent(url.pathname) 64 | , path = normalize(join(root, dir)) 65 | , originalUrl = parse(req.originalUrl) 66 | , originalDir = decodeURIComponent(originalUrl.pathname) 67 | , showUp = path != root && path != root + '/'; 68 | 69 | // null byte(s), bad request 70 | if (~path.indexOf('\0')) return next(utils.error(400)); 71 | 72 | // malicious path, forbidden 73 | if (0 != path.indexOf(root)) return next(utils.error(403)); 74 | 75 | // check if we have a directory 76 | fs.stat(path, function(err, stat){ 77 | if (err) return 'ENOENT' == err.code 78 | ? next() 79 | : next(err); 80 | 81 | if (!stat.isDirectory()) return next(); 82 | 83 | // fetch files 84 | fs.readdir(path, function(err, files){ 85 | if (err) return next(err); 86 | if (!hidden) files = removeHidden(files); 87 | if (filter) files = files.filter(filter); 88 | files.sort(); 89 | 90 | // content-negotiation 91 | for (var key in exports) { 92 | if (~accept.indexOf(key) || ~accept.indexOf('*/*')) { 93 | exports[key](req, res, files, next, originalDir, showUp, icons); 94 | return; 95 | } 96 | } 97 | 98 | // not acceptable 99 | next(utils.error(406)); 100 | }); 101 | }); 102 | }; 103 | }; 104 | 105 | /** 106 | * Respond with text/html. 107 | */ 108 | 109 | exports.html = function(req, res, files, next, dir, showUp, icons){ 110 | fs.readFile(__dirname + '/../public/directory.html', 'utf8', function(err, str){ 111 | if (err) return next(err); 112 | fs.readFile(__dirname + '/../public/style.css', 'utf8', function(err, style){ 113 | if (err) return next(err); 114 | if (showUp) files.unshift('..'); 115 | str = str 116 | .replace('{style}', style) 117 | .replace('{files}', html(files, dir, icons)) 118 | .replace('{directory}', dir) 119 | .replace('{linked-path}', htmlPath(dir)); 120 | res.setHeader('Content-Type', 'text/html'); 121 | res.setHeader('Content-Length', str.length); 122 | res.end(str); 123 | }); 124 | }); 125 | }; 126 | 127 | /** 128 | * Respond with application/json. 129 | */ 130 | 131 | exports.json = function(req, res, files){ 132 | files = JSON.stringify(files); 133 | res.setHeader('Content-Type', 'application/json'); 134 | res.setHeader('Content-Length', files.length); 135 | res.end(files); 136 | }; 137 | 138 | /** 139 | * Respond with text/plain. 140 | */ 141 | 142 | exports.plain = function(req, res, files){ 143 | files = files.join('\n') + '\n'; 144 | res.setHeader('Content-Type', 'text/plain'); 145 | res.setHeader('Content-Length', files.length); 146 | res.end(files); 147 | }; 148 | 149 | /** 150 | * Map html `dir`, returning a linked path. 151 | */ 152 | 153 | function htmlPath(dir) { 154 | var curr = []; 155 | return dir.split('/').map(function(part){ 156 | curr.push(part); 157 | return '' + part + ''; 158 | }).join(' / '); 159 | } 160 | 161 | /** 162 | * Map html `files`, returning an html unordered list. 163 | */ 164 | 165 | function html(files, dir, useIcons) { 166 | return ''; 184 | } 185 | 186 | /** 187 | * Load and cache the given `icon`. 188 | * 189 | * @param {String} icon 190 | * @return {String} 191 | * @api private 192 | */ 193 | 194 | function load(icon) { 195 | if (cache[icon]) return cache[icon]; 196 | return cache[icon] = fs.readFileSync(__dirname + '/../public/icons/' + icon, 'base64'); 197 | } 198 | 199 | /** 200 | * Filter "hidden" `files`, aka files 201 | * beginning with a `.`. 202 | * 203 | * @param {Array} files 204 | * @return {Array} 205 | * @api private 206 | */ 207 | 208 | function removeHidden(files) { 209 | return files.filter(function(file){ 210 | return '.' != file[0]; 211 | }); 212 | } 213 | 214 | /** 215 | * Icon map. 216 | */ 217 | 218 | var icons = { 219 | '.js': 'page_white_code_red.png' 220 | , '.c': 'page_white_c.png' 221 | , '.h': 'page_white_h.png' 222 | , '.cc': 'page_white_cplusplus.png' 223 | , '.php': 'page_white_php.png' 224 | , '.rb': 'page_white_ruby.png' 225 | , '.cpp': 'page_white_cplusplus.png' 226 | , '.swf': 'page_white_flash.png' 227 | , '.pdf': 'page_white_acrobat.png' 228 | , 'default': 'page_white.png' 229 | }; 230 | -------------------------------------------------------------------------------- /lib/proto.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - HTTPServer 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var http = require('http') 14 | , utils = require('./utils') 15 | , debug = require('debug')('connect:dispatcher'); 16 | 17 | // prototype 18 | 19 | var app = module.exports = {}; 20 | 21 | // environment 22 | 23 | var env = process.env.NODE_ENV || 'development'; 24 | 25 | /** 26 | * Utilize the given middleware `handle` to the given `route`, 27 | * defaulting to _/_. This "route" is the mount-point for the 28 | * middleware, when given a value other than _/_ the middleware 29 | * is only effective when that segment is present in the request's 30 | * pathname. 31 | * 32 | * For example if we were to mount a function at _/admin_, it would 33 | * be invoked on _/admin_, and _/admin/settings_, however it would 34 | * not be invoked for _/_, or _/posts_. 35 | * 36 | * Examples: 37 | * 38 | * var app = connect(); 39 | * app.use(connect.favicon()); 40 | * app.use(connect.logger()); 41 | * app.use(connect.static(__dirname + '/public')); 42 | * 43 | * If we wanted to prefix static files with _/public_, we could 44 | * "mount" the `static()` middleware: 45 | * 46 | * app.use('/public', connect.static(__dirname + '/public')); 47 | * 48 | * This api is chainable, so the following is valid: 49 | * 50 | * connect() 51 | * .use(connect.favicon()) 52 | * .use(connect.logger()) 53 | * .use(connect.static(__dirname + '/public')) 54 | * .listen(3000); 55 | * 56 | * @param {String|Function|Server} route, callback or server 57 | * @param {Function|Server} callback or server 58 | * @return {Server} for chaining 59 | * @api public 60 | */ 61 | 62 | app.use = function(route, fn){ 63 | // default route to '/' 64 | if ('string' != typeof route) { 65 | fn = route; 66 | route = '/'; 67 | } 68 | 69 | // wrap sub-apps 70 | if ('function' == typeof fn.handle) { 71 | var server = fn; 72 | fn.route = route; 73 | fn = function(req, res, next){ 74 | server.handle(req, res, next); 75 | }; 76 | } 77 | 78 | // wrap vanilla http.Servers 79 | if (fn instanceof http.Server) { 80 | fn = fn.listeners('request')[0]; 81 | } 82 | 83 | // strip trailing slash 84 | if ('/' == route[route.length - 1]) { 85 | route = route.slice(0, -1); 86 | } 87 | 88 | // add the middleware 89 | debug('use %s %s', route || '/', fn.name || 'anonymous'); 90 | this.stack.push({ route: route, handle: fn }); 91 | 92 | return this; 93 | }; 94 | 95 | /** 96 | * Handle server requests, punting them down 97 | * the middleware stack. 98 | * 99 | * @api private 100 | */ 101 | 102 | app.handle = function(req, res, out) { 103 | var stack = this.stack 104 | , fqdn = ~req.url.indexOf('://') 105 | , removed = '' 106 | , slashAdded = false 107 | , index = 0; 108 | 109 | function next(err) { 110 | var layer, path, status, c; 111 | 112 | if (slashAdded) { 113 | req.url = req.url.substr(1); 114 | slashAdded = false; 115 | } 116 | 117 | req.url = removed + req.url; 118 | req.originalUrl = req.originalUrl || req.url; 119 | removed = ''; 120 | 121 | // next callback 122 | layer = stack[index++]; 123 | 124 | // all done 125 | if (!layer || res.headerSent) { 126 | // delegate to parent 127 | if (out) return out(err); 128 | 129 | // unhandled error 130 | if (err) { 131 | // default to 500 132 | if (res.statusCode < 400) res.statusCode = 500; 133 | debug('default %s', res.statusCode); 134 | 135 | // respect err.status 136 | if (err.status) res.statusCode = err.status; 137 | 138 | // production gets a basic error message 139 | var msg = 'production' == env 140 | ? http.STATUS_CODES[res.statusCode] 141 | : err.stack || err.toString(); 142 | 143 | // log to stderr in a non-test env 144 | if ('test' != env) console.error(err.stack || err.toString()); 145 | if (res.headerSent) return req.socket.destroy(); 146 | res.setHeader('Content-Type', 'text/plain'); 147 | res.setHeader('Content-Length', Buffer.byteLength(msg)); 148 | if ('HEAD' == req.method) return res.end(); 149 | res.end(msg); 150 | } else { 151 | debug('default 404'); 152 | res.statusCode = 404; 153 | res.setHeader('Content-Type', 'text/plain'); 154 | if ('HEAD' == req.method) return res.end(); 155 | res.end('Cannot ' + req.method + ' ' + utils.escape(req.originalUrl)); 156 | } 157 | return; 158 | } 159 | 160 | try { 161 | path = utils.parseUrl(req).pathname; 162 | if (undefined == path) path = '/'; 163 | 164 | // skip this layer if the route doesn't match. 165 | if (0 != path.toLowerCase().indexOf(layer.route.toLowerCase())) return next(err); 166 | 167 | c = path[layer.route.length]; 168 | if (c && '/' != c && '.' != c) return next(err); 169 | 170 | // Call the layer handler 171 | // Trim off the part of the url that matches the route 172 | removed = layer.route; 173 | req.url = req.url.substr(removed.length); 174 | 175 | // Ensure leading slash 176 | if (!fqdn && '/' != req.url[0]) { 177 | req.url = '/' + req.url; 178 | slashAdded = true; 179 | } 180 | 181 | debug('%s', layer.handle.name || 'anonymous'); 182 | var arity = layer.handle.length; 183 | if (err) { 184 | if (arity === 4) { 185 | layer.handle(err, req, res, next); 186 | } else { 187 | next(err); 188 | } 189 | } else if (arity < 4) { 190 | layer.handle(req, res, next); 191 | } else { 192 | next(); 193 | } 194 | } catch (e) { 195 | next(e); 196 | } 197 | } 198 | next(); 199 | }; 200 | 201 | /** 202 | * Listen for connections. 203 | * 204 | * This method takes the same arguments 205 | * as node's `http.Server#listen()`. 206 | * 207 | * HTTP and HTTPS: 208 | * 209 | * If you run your application both as HTTP 210 | * and HTTPS you may wrap them individually, 211 | * since your Connect "server" is really just 212 | * a JavaScript `Function`. 213 | * 214 | * var connect = require('connect') 215 | * , http = require('http') 216 | * , https = require('https'); 217 | * 218 | * var app = connect(); 219 | * 220 | * http.createServer(app).listen(80); 221 | * https.createServer(options, app).listen(443); 222 | * 223 | * @return {http.Server} 224 | * @api public 225 | */ 226 | 227 | app.listen = function(){ 228 | var server = http.createServer(this); 229 | return server.listen.apply(server, arguments); 230 | }; 231 | -------------------------------------------------------------------------------- /lib/middleware/staticCache.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - staticCache 4 | * Copyright(c) 2011 Sencha Inc. 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var utils = require('../utils') 13 | , Cache = require('../cache') 14 | , fresh = require('fresh'); 15 | 16 | /** 17 | * Static cache: 18 | * 19 | * Enables a memory cache layer on top of 20 | * the `static()` middleware, serving popular 21 | * static files. 22 | * 23 | * By default a maximum of 128 objects are 24 | * held in cache, with a max of 256k each, 25 | * totalling ~32mb. 26 | * 27 | * A Least-Recently-Used (LRU) cache algo 28 | * is implemented through the `Cache` object, 29 | * simply rotating cache objects as they are 30 | * hit. This means that increasingly popular 31 | * objects maintain their positions while 32 | * others get shoved out of the stack and 33 | * garbage collected. 34 | * 35 | * Benchmarks: 36 | * 37 | * static(): 2700 rps 38 | * node-static: 5300 rps 39 | * static() + staticCache(): 7500 rps 40 | * 41 | * Options: 42 | * 43 | * - `maxObjects` max cache objects [128] 44 | * - `maxLength` max cache object length 256kb 45 | * 46 | * @param {Object} options 47 | * @return {Function} 48 | * @api public 49 | */ 50 | 51 | module.exports = function staticCache(options){ 52 | var options = options || {} 53 | , cache = new Cache(options.maxObjects || 128) 54 | , maxlen = options.maxLength || 1024 * 256; 55 | 56 | console.warn('connect.staticCache() is deprecated and will be removed in 3.0'); 57 | console.warn('use varnish or similar reverse proxy caches.'); 58 | 59 | return function staticCache(req, res, next){ 60 | var key = cacheKey(req) 61 | , ranges = req.headers.range 62 | , hasCookies = req.headers.cookie 63 | , hit = cache.get(key); 64 | 65 | // cache static 66 | // TODO: change from staticCache() -> cache() 67 | // and make this work for any request 68 | req.on('static', function(stream){ 69 | var headers = res._headers 70 | , cc = utils.parseCacheControl(headers['cache-control'] || '') 71 | , contentLength = headers['content-length'] 72 | , hit; 73 | 74 | // dont cache set-cookie responses 75 | if (headers['set-cookie']) return hasCookies = true; 76 | 77 | // dont cache when cookies are present 78 | if (hasCookies) return; 79 | 80 | // ignore larger files 81 | if (!contentLength || contentLength > maxlen) return; 82 | 83 | // don't cache partial files 84 | if (headers['content-range']) return; 85 | 86 | // dont cache items we shouldn't be 87 | // TODO: real support for must-revalidate / no-cache 88 | if ( cc['no-cache'] 89 | || cc['no-store'] 90 | || cc['private'] 91 | || cc['must-revalidate']) return; 92 | 93 | // if already in cache then validate 94 | if (hit = cache.get(key)){ 95 | if (headers.etag == hit[0].etag) { 96 | hit[0].date = new Date; 97 | return; 98 | } else { 99 | cache.remove(key); 100 | } 101 | } 102 | 103 | // validation notifiactions don't contain a steam 104 | if (null == stream) return; 105 | 106 | // add the cache object 107 | var arr = []; 108 | 109 | // store the chunks 110 | stream.on('data', function(chunk){ 111 | arr.push(chunk); 112 | }); 113 | 114 | // flag it as complete 115 | stream.on('end', function(){ 116 | var cacheEntry = cache.add(key); 117 | delete headers['x-cache']; // Clean up (TODO: others) 118 | cacheEntry.push(200); 119 | cacheEntry.push(headers); 120 | cacheEntry.push.apply(cacheEntry, arr); 121 | }); 122 | }); 123 | 124 | if (req.method == 'GET' || req.method == 'HEAD') { 125 | if (ranges) { 126 | next(); 127 | } else if (!hasCookies && hit && !mustRevalidate(req, hit)) { 128 | res.setHeader('X-Cache', 'HIT'); 129 | respondFromCache(req, res, hit); 130 | } else { 131 | res.setHeader('X-Cache', 'MISS'); 132 | next(); 133 | } 134 | } else { 135 | next(); 136 | } 137 | } 138 | }; 139 | 140 | /** 141 | * Respond with the provided cached value. 142 | * TODO: Assume 200 code, that's iffy. 143 | * 144 | * @param {Object} req 145 | * @param {Object} res 146 | * @param {Object} cacheEntry 147 | * @return {String} 148 | * @api private 149 | */ 150 | 151 | function respondFromCache(req, res, cacheEntry) { 152 | var status = cacheEntry[0] 153 | , headers = utils.merge({}, cacheEntry[1]) 154 | , content = cacheEntry.slice(2); 155 | 156 | headers.age = (new Date - new Date(headers.date)) / 1000 || 0; 157 | 158 | switch (req.method) { 159 | case 'HEAD': 160 | res.writeHead(status, headers); 161 | res.end(); 162 | break; 163 | case 'GET': 164 | if (utils.conditionalGET(req) && fresh(req.headers, headers)) { 165 | headers['content-length'] = 0; 166 | res.writeHead(304, headers); 167 | res.end(); 168 | } else { 169 | res.writeHead(status, headers); 170 | 171 | function write() { 172 | while (content.length) { 173 | if (false === res.write(content.shift())) { 174 | res.once('drain', write); 175 | return; 176 | } 177 | } 178 | res.end(); 179 | } 180 | 181 | write(); 182 | } 183 | break; 184 | default: 185 | // This should never happen. 186 | res.writeHead(500, ''); 187 | res.end(); 188 | } 189 | } 190 | 191 | /** 192 | * Determine whether or not a cached value must be revalidated. 193 | * 194 | * @param {Object} req 195 | * @param {Object} cacheEntry 196 | * @return {String} 197 | * @api private 198 | */ 199 | 200 | function mustRevalidate(req, cacheEntry) { 201 | var cacheHeaders = cacheEntry[1] 202 | , reqCC = utils.parseCacheControl(req.headers['cache-control'] || '') 203 | , cacheCC = utils.parseCacheControl(cacheHeaders['cache-control'] || '') 204 | , cacheAge = (new Date - new Date(cacheHeaders.date)) / 1000 || 0; 205 | 206 | if ( cacheCC['no-cache'] 207 | || cacheCC['must-revalidate'] 208 | || cacheCC['proxy-revalidate']) return true; 209 | 210 | if (reqCC['no-cache']) return true; 211 | 212 | if (null != reqCC['max-age']) return reqCC['max-age'] < cacheAge; 213 | 214 | if (null != cacheCC['max-age']) return cacheCC['max-age'] < cacheAge; 215 | 216 | return false; 217 | } 218 | 219 | /** 220 | * The key to use in the cache. For now, this is the URL path and query. 221 | * 222 | * 'http://example.com?key=value' -> '/?key=value' 223 | * 224 | * @param {Object} req 225 | * @return {String} 226 | * @api private 227 | */ 228 | 229 | function cacheKey(req) { 230 | return utils.parseUrl(req).path; 231 | } 232 | -------------------------------------------------------------------------------- /test/bodyParser.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | 4 | var app = connect(); 5 | 6 | app.use(connect.bodyParser()); 7 | 8 | app.use(function(req, res){ 9 | res.end(JSON.stringify(req.body)); 10 | }); 11 | 12 | describe('connect.bodyParser()', function(){ 13 | it('should default to {}', function(done){ 14 | app.request() 15 | .post('/') 16 | .end(function(res){ 17 | res.body.should.equal('{}'); 18 | done(); 19 | }) 20 | }) 21 | 22 | it('should parse JSON', function(done){ 23 | app.request() 24 | .post('/') 25 | .set('Content-Type', 'application/json') 26 | .write('{"user":"tobi"}') 27 | .end(function(res){ 28 | res.body.should.equal('{"user":"tobi"}'); 29 | done(); 30 | }); 31 | }) 32 | 33 | it('should parse x-www-form-urlencoded', function(done){ 34 | app.request() 35 | .post('/') 36 | .set('Content-Type', 'application/x-www-form-urlencoded') 37 | .write('user=tobi') 38 | .end(function(res){ 39 | res.body.should.equal('{"user":"tobi"}'); 40 | done(); 41 | }); 42 | }) 43 | 44 | describe('with multipart/form-data', function(){ 45 | it('should populate req.body', function(done){ 46 | app.request() 47 | .post('/') 48 | .set('Content-Type', 'multipart/form-data; boundary=foo') 49 | .write('--foo\r\n') 50 | .write('Content-Disposition: form-data; name="user"\r\n') 51 | .write('\r\n') 52 | .write('Tobi') 53 | .write('\r\n--foo--') 54 | .end(function(res){ 55 | res.body.should.equal('{"user":"Tobi"}'); 56 | done(); 57 | }); 58 | }) 59 | 60 | it('should support files', function(done){ 61 | var app = connect(); 62 | 63 | app.use(connect.bodyParser()); 64 | 65 | app.use(function(req, res){ 66 | req.body.user.should.eql({ name: 'Tobi' }); 67 | req.files.text.path.should.not.include('.txt'); 68 | req.files.text.constructor.name.should.equal('File'); 69 | res.end(req.files.text.name); 70 | }); 71 | 72 | app.request() 73 | .post('/') 74 | .set('Content-Type', 'multipart/form-data; boundary=foo') 75 | .write('--foo\r\n') 76 | .write('Content-Disposition: form-data; name="user[name]"\r\n') 77 | .write('\r\n') 78 | .write('Tobi') 79 | .write('\r\n--foo\r\n') 80 | .write('Content-Disposition: form-data; name="text"; filename="foo.txt"\r\n') 81 | .write('\r\n') 82 | .write('some text here') 83 | .write('\r\n--foo--') 84 | .end(function(res){ 85 | res.body.should.equal('foo.txt'); 86 | done(); 87 | }); 88 | }) 89 | 90 | it('should expose options to formidable', function(done){ 91 | var app = connect(); 92 | 93 | app.use(connect.bodyParser({ 94 | keepExtensions: true 95 | })); 96 | 97 | app.use(function(req, res){ 98 | req.body.user.should.eql({ name: 'Tobi' }); 99 | req.files.text.path.should.include('.txt'); 100 | req.files.text.constructor.name.should.equal('File'); 101 | res.end(req.files.text.name); 102 | }); 103 | 104 | app.request() 105 | .post('/') 106 | .set('Content-Type', 'multipart/form-data; boundary=foo') 107 | .write('--foo\r\n') 108 | .write('Content-Disposition: form-data; name="user[name]"\r\n') 109 | .write('\r\n') 110 | .write('Tobi') 111 | .write('\r\n--foo\r\n') 112 | .write('Content-Disposition: form-data; name="text"; filename="foo.txt"\r\n') 113 | .write('\r\n') 114 | .write('some text here') 115 | .write('\r\n--foo--') 116 | .end(function(res){ 117 | res.body.should.equal('foo.txt'); 118 | done(); 119 | }); 120 | }) 121 | 122 | it('should work with multiple fields', function(done){ 123 | app.request() 124 | .post('/') 125 | .set('Content-Type', 'multipart/form-data; boundary=foo') 126 | .write('--foo\r\n') 127 | .write('Content-Disposition: form-data; name="user"\r\n') 128 | .write('\r\n') 129 | .write('Tobi') 130 | .write('\r\n--foo\r\n') 131 | .write('Content-Disposition: form-data; name="age"\r\n') 132 | .write('\r\n') 133 | .write('1') 134 | .write('\r\n--foo--') 135 | .end(function(res){ 136 | res.body.should.equal('{"user":"Tobi","age":"1"}'); 137 | done(); 138 | }); 139 | }) 140 | 141 | it('should support nesting', function(done){ 142 | app.request() 143 | .post('/') 144 | .set('Content-Type', 'multipart/form-data; boundary=foo') 145 | .write('--foo\r\n') 146 | .write('Content-Disposition: form-data; name="user[name][first]"\r\n') 147 | .write('\r\n') 148 | .write('tobi') 149 | .write('\r\n--foo\r\n') 150 | .write('Content-Disposition: form-data; name="user[name][last]"\r\n') 151 | .write('\r\n') 152 | .write('holowaychuk') 153 | .write('\r\n--foo\r\n') 154 | .write('Content-Disposition: form-data; name="user[age]"\r\n') 155 | .write('\r\n') 156 | .write('1') 157 | .write('\r\n--foo\r\n') 158 | .write('Content-Disposition: form-data; name="species"\r\n') 159 | .write('\r\n') 160 | .write('ferret') 161 | .write('\r\n--foo--') 162 | .end(function(res){ 163 | var obj = JSON.parse(res.body); 164 | obj.user.age.should.equal('1'); 165 | obj.user.name.should.eql({ first: 'tobi', last: 'holowaychuk' }); 166 | obj.species.should.equal('ferret'); 167 | done(); 168 | }); 169 | }) 170 | 171 | it('should support multiple files of the same name', function(done){ 172 | var app = connect(); 173 | 174 | app.use(connect.bodyParser()); 175 | 176 | app.use(function(req, res){ 177 | req.files.text.should.have.length(2); 178 | req.files.text[0].constructor.name.should.equal('File'); 179 | req.files.text[1].constructor.name.should.equal('File'); 180 | res.end(); 181 | }); 182 | 183 | app.request() 184 | .post('/') 185 | .set('Content-Type', 'multipart/form-data; boundary=foo') 186 | .write('--foo\r\n') 187 | .write('Content-Disposition: form-data; name="text"; filename="foo.txt"\r\n') 188 | .write('\r\n') 189 | .write('some text here') 190 | .write('\r\n--foo\r\n') 191 | .write('Content-Disposition: form-data; name="text"; filename="bar.txt"\r\n') 192 | .write('\r\n') 193 | .write('some more text stuff') 194 | .write('\r\n--foo--') 195 | .end(function(res){ 196 | res.statusCode.should.equal(200); 197 | done(); 198 | }); 199 | }) 200 | 201 | it('should support nested files', function(done){ 202 | var app = connect(); 203 | 204 | app.use(connect.bodyParser()); 205 | 206 | app.use(function(req, res){ 207 | Object.keys(req.files.docs).should.have.length(2); 208 | req.files.docs.foo.name.should.equal('foo.txt'); 209 | req.files.docs.bar.name.should.equal('bar.txt'); 210 | res.end(); 211 | }); 212 | 213 | app.request() 214 | .post('/') 215 | .set('Content-Type', 'multipart/form-data; boundary=foo') 216 | .write('--foo\r\n') 217 | .write('Content-Disposition: form-data; name="docs[foo]"; filename="foo.txt"\r\n') 218 | .write('\r\n') 219 | .write('some text here') 220 | .write('\r\n--foo\r\n') 221 | .write('Content-Disposition: form-data; name="docs[bar]"; filename="bar.txt"\r\n') 222 | .write('\r\n') 223 | .write('some more text stuff') 224 | .write('\r\n--foo--') 225 | .end(function(res){ 226 | res.statusCode.should.equal(200); 227 | done(); 228 | }); 229 | }) 230 | 231 | it('should next(err) on multipart failure', function(done){ 232 | var app = connect(); 233 | 234 | app.use(connect.bodyParser()); 235 | 236 | app.use(function(req, res){ 237 | res.end('whoop'); 238 | }); 239 | 240 | app.use(function(err, req, res, next){ 241 | err.message.should.equal('parser error, 16 of 28 bytes parsed'); 242 | res.statusCode = 500; 243 | res.end(); 244 | }); 245 | 246 | app.request() 247 | .post('/') 248 | .set('Content-Type', 'multipart/form-data; boundary=foo') 249 | .write('--foo\r\n') 250 | .write('Content-filename="foo.txt"\r\n') 251 | .write('\r\n') 252 | .write('some text here') 253 | .write('Content-Disposition: form-data; name="text"; filename="bar.txt"\r\n') 254 | .write('\r\n') 255 | .write('some more text stuff') 256 | .write('\r\n--foo--') 257 | .end(function(res){ 258 | res.statusCode.should.equal(500); 259 | done(); 260 | }); 261 | }) 262 | 263 | }) 264 | }) -------------------------------------------------------------------------------- /lib/middleware/logger.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Connect - logger 3 | * Copyright(c) 2010 Sencha Inc. 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var bytes = require('bytes'); 13 | 14 | /*! 15 | * Log buffer. 16 | */ 17 | 18 | var buf = []; 19 | 20 | /*! 21 | * Default log buffer duration. 22 | */ 23 | 24 | var defaultBufferDuration = 1000; 25 | 26 | /** 27 | * Logger: 28 | * 29 | * Log requests with the given `options` or a `format` string. 30 | * 31 | * Options: 32 | * 33 | * - `format` Format string, see below for tokens 34 | * - `stream` Output stream, defaults to _stdout_ 35 | * - `buffer` Buffer duration, defaults to 1000ms when _true_ 36 | * - `immediate` Write log line on request instead of response (for response times) 37 | * 38 | * Tokens: 39 | * 40 | * - `:req[header]` ex: `:req[Accept]` 41 | * - `:res[header]` ex: `:res[Content-Length]` 42 | * - `:http-version` 43 | * - `:response-time` 44 | * - `:remote-addr` 45 | * - `:date` 46 | * - `:method` 47 | * - `:url` 48 | * - `:referrer` 49 | * - `:user-agent` 50 | * - `:status` 51 | * 52 | * Formats: 53 | * 54 | * Pre-defined formats that ship with connect: 55 | * 56 | * - `default` ':remote-addr - - [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"' 57 | * - `short` ':remote-addr - :method :url HTTP/:http-version :status :res[content-length] - :response-time ms' 58 | * - `tiny` ':method :url :status :res[content-length] - :response-time ms' 59 | * - `dev` concise output colored by response status for development use 60 | * 61 | * Examples: 62 | * 63 | * connect.logger() // default 64 | * connect.logger('short') 65 | * connect.logger('tiny') 66 | * connect.logger({ immediate: true, format: 'dev' }) 67 | * connect.logger(':method :url - :referrer') 68 | * connect.logger(':req[content-type] -> :res[content-type]') 69 | * connect.logger(function(tokens, req, res){ return 'some format string' }) 70 | * 71 | * Defining Tokens: 72 | * 73 | * To define a token, simply invoke `connect.logger.token()` with the 74 | * name and a callback function. The value returned is then available 75 | * as ":type" in this case. 76 | * 77 | * connect.logger.token('type', function(req, res){ return req.headers['content-type']; }) 78 | * 79 | * Defining Formats: 80 | * 81 | * All default formats are defined this way, however it's public API as well: 82 | * 83 | * connect.logger.format('name', 'string or function') 84 | * 85 | * @param {String|Function|Object} format or options 86 | * @return {Function} 87 | * @api public 88 | */ 89 | 90 | exports = module.exports = function logger(options) { 91 | if ('object' == typeof options) { 92 | options = options || {}; 93 | } else if (options) { 94 | options = { format: options }; 95 | } else { 96 | options = {}; 97 | } 98 | 99 | // output on request instead of response 100 | var immediate = options.immediate; 101 | 102 | // format name 103 | var fmt = exports[options.format] || options.format || exports.default; 104 | 105 | // compile format 106 | if ('function' != typeof fmt) fmt = compile(fmt); 107 | 108 | // options 109 | var stream = options.stream || process.stdout 110 | , buffer = options.buffer; 111 | 112 | // buffering support 113 | if (buffer) { 114 | var realStream = stream 115 | , interval = 'number' == typeof buffer 116 | ? buffer 117 | : defaultBufferDuration; 118 | 119 | // flush interval 120 | setInterval(function(){ 121 | if (buf.length) { 122 | realStream.write(buf.join('')); 123 | buf.length = 0; 124 | } 125 | }, interval); 126 | 127 | // swap the stream 128 | stream = { 129 | write: function(str){ 130 | buf.push(str); 131 | } 132 | }; 133 | } 134 | 135 | return function logger(req, res, next) { 136 | req._startTime = new Date; 137 | 138 | // immediate 139 | if (immediate) { 140 | var line = fmt(exports, req, res); 141 | if (null == line) return; 142 | stream.write(line + '\n'); 143 | // proxy end to output logging 144 | } else { 145 | var end = res.end; 146 | res.end = function(chunk, encoding){ 147 | res.end = end; 148 | res.end(chunk, encoding); 149 | var line = fmt(exports, req, res); 150 | if (null == line) return; 151 | stream.write(line + '\n'); 152 | }; 153 | } 154 | 155 | 156 | next(); 157 | }; 158 | }; 159 | 160 | /** 161 | * Compile `fmt` into a function. 162 | * 163 | * @param {String} fmt 164 | * @return {Function} 165 | * @api private 166 | */ 167 | 168 | function compile(fmt) { 169 | fmt = fmt.replace(/"/g, '\\"'); 170 | var js = ' return "' + fmt.replace(/:([-\w]{2,})(?:\[([^\]]+)\])?/g, function(_, name, arg){ 171 | return '"\n + (tokens["' + name + '"](req, res, "' + arg + '") || "-") + "'; 172 | }) + '";' 173 | return new Function('tokens, req, res', js); 174 | }; 175 | 176 | /** 177 | * Define a token function with the given `name`, 178 | * and callback `fn(req, res)`. 179 | * 180 | * @param {String} name 181 | * @param {Function} fn 182 | * @return {Object} exports for chaining 183 | * @api public 184 | */ 185 | 186 | exports.token = function(name, fn) { 187 | exports[name] = fn; 188 | return this; 189 | }; 190 | 191 | /** 192 | * Define a `fmt` with the given `name`. 193 | * 194 | * @param {String} name 195 | * @param {String|Function} fmt 196 | * @return {Object} exports for chaining 197 | * @api public 198 | */ 199 | 200 | exports.format = function(name, str){ 201 | exports[name] = str; 202 | return this; 203 | }; 204 | 205 | /** 206 | * Default format. 207 | */ 208 | 209 | exports.format('default', ':remote-addr - - [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"'); 210 | 211 | /** 212 | * Short format. 213 | */ 214 | 215 | exports.format('short', ':remote-addr - :method :url HTTP/:http-version :status :res[content-length] - :response-time ms'); 216 | 217 | /** 218 | * Tiny format. 219 | */ 220 | 221 | exports.format('tiny', ':method :url :status :res[content-length] - :response-time ms'); 222 | 223 | /** 224 | * dev (colored) 225 | */ 226 | 227 | exports.format('dev', function(tokens, req, res){ 228 | var status = res.statusCode 229 | , len = parseInt(res.getHeader('Content-Length'), 10) 230 | , color = 32; 231 | 232 | if (status >= 500) color = 31 233 | else if (status >= 400) color = 33 234 | else if (status >= 300) color = 36; 235 | 236 | len = isNaN(len) 237 | ? '' 238 | : len = ' - ' + bytes(len); 239 | 240 | return '\033[90m' + req.method 241 | + ' ' + req.originalUrl + ' ' 242 | + '\033[' + color + 'm' + res.statusCode 243 | + ' \033[90m' 244 | + (new Date - req._startTime) 245 | + 'ms' + len 246 | + '\033[0m'; 247 | }); 248 | 249 | /** 250 | * request url 251 | */ 252 | 253 | exports.token('url', function(req){ 254 | return req.originalUrl || req.url; 255 | }); 256 | 257 | /** 258 | * request method 259 | */ 260 | 261 | exports.token('method', function(req){ 262 | return req.method; 263 | }); 264 | 265 | /** 266 | * response time in milliseconds 267 | */ 268 | 269 | exports.token('response-time', function(req){ 270 | return new Date - req._startTime; 271 | }); 272 | 273 | /** 274 | * UTC date 275 | */ 276 | 277 | exports.token('date', function(){ 278 | return new Date().toUTCString(); 279 | }); 280 | 281 | /** 282 | * response status code 283 | */ 284 | 285 | exports.token('status', function(req, res){ 286 | return res.statusCode; 287 | }); 288 | 289 | /** 290 | * normalized referrer 291 | */ 292 | 293 | exports.token('referrer', function(req){ 294 | return req.headers['referer'] || req.headers['referrer']; 295 | }); 296 | 297 | /** 298 | * remote address 299 | */ 300 | 301 | exports.token('remote-addr', function(req){ 302 | if (req.ip) return req.ip; 303 | var sock = req.socket; 304 | if (sock.socket) return sock.socket.remoteAddress; 305 | return sock.remoteAddress; 306 | }); 307 | 308 | /** 309 | * HTTP version 310 | */ 311 | 312 | exports.token('http-version', function(req){ 313 | return req.httpVersionMajor + '.' + req.httpVersionMinor; 314 | }); 315 | 316 | /** 317 | * UA string 318 | */ 319 | 320 | exports.token('user-agent', function(req){ 321 | return req.headers['user-agent']; 322 | }); 323 | 324 | /** 325 | * request header 326 | */ 327 | 328 | exports.token('req', function(req, res, field){ 329 | return req.headers[field.toLowerCase()]; 330 | }); 331 | 332 | /** 333 | * response header 334 | */ 335 | 336 | exports.token('res', function(req, res, field){ 337 | return (res._headers || {})[field.toLowerCase()]; 338 | }); 339 | 340 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - utils 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var http = require('http') 14 | , crypto = require('crypto') 15 | , parse = require('url').parse 16 | , signature = require('cookie-signature'); 17 | 18 | /** 19 | * Return `true` if the request has a body, otherwise return `false`. 20 | * 21 | * @param {IncomingMessage} req 22 | * @return {Boolean} 23 | * @api private 24 | */ 25 | 26 | exports.hasBody = function(req) { 27 | return 'transfer-encoding' in req.headers || 'content-length' in req.headers; 28 | }; 29 | 30 | /** 31 | * Extract the mime type from the given request's 32 | * _Content-Type_ header. 33 | * 34 | * @param {IncomingMessage} req 35 | * @return {String} 36 | * @api private 37 | */ 38 | 39 | exports.mime = function(req) { 40 | var str = req.headers['content-type'] || ''; 41 | return str.split(';')[0]; 42 | }; 43 | 44 | /** 45 | * Generate an `Error` from the given status `code` 46 | * and optional `msg`. 47 | * 48 | * @param {Number} code 49 | * @param {String} msg 50 | * @return {Error} 51 | * @api private 52 | */ 53 | 54 | exports.error = function(code, msg){ 55 | var err = new Error(msg || http.STATUS_CODES[code]); 56 | err.status = code; 57 | return err; 58 | }; 59 | 60 | /** 61 | * Return md5 hash of the given string and optional encoding, 62 | * defaulting to hex. 63 | * 64 | * utils.md5('wahoo'); 65 | * // => "e493298061761236c96b02ea6aa8a2ad" 66 | * 67 | * @param {String} str 68 | * @param {String} encoding 69 | * @return {String} 70 | * @api private 71 | */ 72 | 73 | exports.md5 = function(str, encoding){ 74 | return crypto 75 | .createHash('md5') 76 | .update(str) 77 | .digest(encoding || 'hex'); 78 | }; 79 | 80 | /** 81 | * Merge object b with object a. 82 | * 83 | * var a = { foo: 'bar' } 84 | * , b = { bar: 'baz' }; 85 | * 86 | * utils.merge(a, b); 87 | * // => { foo: 'bar', bar: 'baz' } 88 | * 89 | * @param {Object} a 90 | * @param {Object} b 91 | * @return {Object} 92 | * @api private 93 | */ 94 | 95 | exports.merge = function(a, b){ 96 | if (a && b) { 97 | for (var key in b) { 98 | a[key] = b[key]; 99 | } 100 | } 101 | return a; 102 | }; 103 | 104 | /** 105 | * Escape the given string of `html`. 106 | * 107 | * @param {String} html 108 | * @return {String} 109 | * @api private 110 | */ 111 | 112 | exports.escape = function(html){ 113 | return String(html) 114 | .replace(/&(?!\w+;)/g, '&') 115 | .replace(//g, '>') 117 | .replace(/"/g, '"'); 118 | }; 119 | 120 | 121 | /** 122 | * Return a unique identifier with the given `len`. 123 | * 124 | * utils.uid(10); 125 | * // => "FDaS435D2z" 126 | * 127 | * @param {Number} len 128 | * @return {String} 129 | * @api private 130 | */ 131 | 132 | exports.uid = function(len) { 133 | return crypto.randomBytes(Math.ceil(len * 3 / 4)) 134 | .toString('base64') 135 | .slice(0, len); 136 | }; 137 | 138 | /** 139 | * Sign the given `val` with `secret`. 140 | * 141 | * @param {String} val 142 | * @param {String} secret 143 | * @return {String} 144 | * @api private 145 | */ 146 | 147 | exports.sign = function(val, secret){ 148 | console.warn('do not use utils.sign(), use https://github.com/visionmedia/node-cookie-signature') 149 | return val + '.' + crypto 150 | .createHmac('sha256', secret) 151 | .update(val) 152 | .digest('base64') 153 | .replace(/=+$/, ''); 154 | }; 155 | 156 | /** 157 | * Unsign and decode the given `val` with `secret`, 158 | * returning `false` if the signature is invalid. 159 | * 160 | * @param {String} val 161 | * @param {String} secret 162 | * @return {String|Boolean} 163 | * @api private 164 | */ 165 | 166 | exports.unsign = function(val, secret){ 167 | console.warn('do not use utils.unsign(), use https://github.com/visionmedia/node-cookie-signature') 168 | var str = val.slice(0, val.lastIndexOf('.')); 169 | return exports.sign(str, secret) == val 170 | ? str 171 | : false; 172 | }; 173 | 174 | /** 175 | * Parse signed cookies, returning an object 176 | * containing the decoded key/value pairs, 177 | * while removing the signed key from `obj`. 178 | * 179 | * @param {Object} obj 180 | * @return {Object} 181 | * @api private 182 | */ 183 | 184 | exports.parseSignedCookies = function(obj, secret){ 185 | var ret = {}; 186 | Object.keys(obj).forEach(function(key){ 187 | var val = obj[key]; 188 | if (0 == val.indexOf('s:')) { 189 | val = signature.unsign(val.slice(2), secret); 190 | if (val) { 191 | ret[key] = val; 192 | delete obj[key]; 193 | } 194 | } 195 | }); 196 | return ret; 197 | }; 198 | 199 | /** 200 | * Parse a signed cookie string, return the decoded value 201 | * 202 | * @param {String} str signed cookie string 203 | * @param {String} secret 204 | * @return {String} decoded value 205 | * @api private 206 | */ 207 | 208 | exports.parseSignedCookie = function(str, secret){ 209 | return 0 == str.indexOf('s:') 210 | ? signature.unsign(str.slice(2), secret) 211 | : str; 212 | }; 213 | 214 | /** 215 | * Parse JSON cookies. 216 | * 217 | * @param {Object} obj 218 | * @return {Object} 219 | * @api private 220 | */ 221 | 222 | exports.parseJSONCookies = function(obj){ 223 | Object.keys(obj).forEach(function(key){ 224 | var val = obj[key]; 225 | var res = exports.parseJSONCookie(val); 226 | if (res) obj[key] = res; 227 | }); 228 | return obj; 229 | }; 230 | 231 | /** 232 | * Parse JSON cookie string 233 | * 234 | * @param {String} str 235 | * @return {Object} Parsed object or null if not json cookie 236 | * @api private 237 | */ 238 | 239 | exports.parseJSONCookie = function(str) { 240 | if (0 == str.indexOf('j:')) { 241 | try { 242 | return JSON.parse(str.slice(2)); 243 | } catch (err) { 244 | // no op 245 | } 246 | } 247 | }; 248 | 249 | /** 250 | * Pause `data` and `end` events on the given `obj`. 251 | * Middleware performing async tasks _should_ utilize 252 | * this utility (or similar), to re-emit data once 253 | * the async operation has completed, otherwise these 254 | * events may be lost. 255 | * 256 | * var pause = utils.pause(req); 257 | * fs.readFile(path, function(){ 258 | * next(); 259 | * pause.resume(); 260 | * }); 261 | * 262 | * @param {Object} obj 263 | * @return {Object} 264 | * @api private 265 | */ 266 | 267 | exports.pause = require('pause'); 268 | 269 | /** 270 | * Strip `Content-*` headers from `res`. 271 | * 272 | * @param {ServerResponse} res 273 | * @api private 274 | */ 275 | 276 | exports.removeContentHeaders = function(res){ 277 | Object.keys(res._headers).forEach(function(field){ 278 | if (0 == field.indexOf('content')) { 279 | res.removeHeader(field); 280 | } 281 | }); 282 | }; 283 | 284 | /** 285 | * Check if `req` is a conditional GET request. 286 | * 287 | * @param {IncomingMessage} req 288 | * @return {Boolean} 289 | * @api private 290 | */ 291 | 292 | exports.conditionalGET = function(req) { 293 | return req.headers['if-modified-since'] 294 | || req.headers['if-none-match']; 295 | }; 296 | 297 | /** 298 | * Respond with 401 "Unauthorized". 299 | * 300 | * @param {ServerResponse} res 301 | * @param {String} realm 302 | * @api private 303 | */ 304 | 305 | exports.unauthorized = function(res, realm) { 306 | res.statusCode = 401; 307 | res.setHeader('WWW-Authenticate', 'Basic realm="' + realm + '"'); 308 | res.end('Unauthorized'); 309 | }; 310 | 311 | /** 312 | * Respond with 304 "Not Modified". 313 | * 314 | * @param {ServerResponse} res 315 | * @param {Object} headers 316 | * @api private 317 | */ 318 | 319 | exports.notModified = function(res) { 320 | exports.removeContentHeaders(res); 321 | res.statusCode = 304; 322 | res.end(); 323 | }; 324 | 325 | /** 326 | * Return an ETag in the form of `"-"` 327 | * from the given `stat`. 328 | * 329 | * @param {Object} stat 330 | * @return {String} 331 | * @api private 332 | */ 333 | 334 | exports.etag = function(stat) { 335 | return '"' + stat.size + '-' + Number(stat.mtime) + '"'; 336 | }; 337 | 338 | /** 339 | * Parse the given Cache-Control `str`. 340 | * 341 | * @param {String} str 342 | * @return {Object} 343 | * @api private 344 | */ 345 | 346 | exports.parseCacheControl = function(str){ 347 | var directives = str.split(',') 348 | , obj = {}; 349 | 350 | for(var i = 0, len = directives.length; i < len; i++) { 351 | var parts = directives[i].split('=') 352 | , key = parts.shift().trim() 353 | , val = parseInt(parts.shift(), 10); 354 | 355 | obj[key] = isNaN(val) ? true : val; 356 | } 357 | 358 | return obj; 359 | }; 360 | 361 | /** 362 | * Parse the `req` url with memoization. 363 | * 364 | * @param {ServerRequest} req 365 | * @return {Object} 366 | * @api private 367 | */ 368 | 369 | exports.parseUrl = function(req){ 370 | var parsed = req._parsedUrl; 371 | if (parsed && parsed.href == req.url) { 372 | return parsed; 373 | } else { 374 | return req._parsedUrl = parse(req.url); 375 | } 376 | }; 377 | 378 | /** 379 | * Parse byte `size` string. 380 | * 381 | * @param {String} size 382 | * @return {Number} 383 | * @api private 384 | */ 385 | 386 | exports.parseBytes = require('bytes'); 387 | -------------------------------------------------------------------------------- /test/static.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | 4 | var fixtures = __dirname + '/fixtures'; 5 | 6 | var app = connect(); 7 | app.use(connect.static(fixtures)); 8 | 9 | app.use(function(req, res){ 10 | res.statusCode = 404; 11 | res.end('sorry!'); 12 | }); 13 | 14 | describe('connect.static()', function(){ 15 | it('should serve static files', function(done){ 16 | app.request() 17 | .get('/todo.txt') 18 | .expect('- groceries', done); 19 | }) 20 | 21 | it('should support nesting', function(done){ 22 | app.request() 23 | .get('/users/tobi.txt') 24 | .expect('ferret', done); 25 | }) 26 | 27 | it('should set Content-Type', function(done){ 28 | app.request() 29 | .get('/todo.txt') 30 | .expect('Content-Type', 'text/plain; charset=UTF-8', done); 31 | }) 32 | 33 | it('should default max-age=0', function(done){ 34 | app.request() 35 | .get('/todo.txt') 36 | .expect('Cache-Control', 'public, max-age=0', done); 37 | }) 38 | 39 | it('should support urlencoded pathnames', function(done){ 40 | app.request() 41 | .get('/foo%20bar') 42 | .expect('baz', done); 43 | }) 44 | 45 | it('should redirect directories', function(done){ 46 | app.request() 47 | .get('/users') 48 | .expect(301, done); 49 | }) 50 | 51 | it('should support index.html', function(done){ 52 | app.request() 53 | .get('/users/') 54 | .end(function(res){ 55 | res.body.should.equal('

    tobi, loki, jane

    '); 56 | res.headers.should.have.property('content-type', 'text/html; charset=UTF-8'); 57 | done(); 58 | }) 59 | }) 60 | 61 | it('should support ../', function(done){ 62 | app.request() 63 | .get('/users/../todo.txt') 64 | .expect('- groceries', done); 65 | }) 66 | 67 | it('should support HEAD', function(done){ 68 | app.request() 69 | .head('/todo.txt') 70 | .expect('', done); 71 | }) 72 | 73 | it('should support conditional requests', function(done){ 74 | app.request() 75 | .get('/todo.txt') 76 | .end(function(res){ 77 | app.request() 78 | .get('/todo.txt') 79 | .set('If-None-Match', res.headers.etag) 80 | .expect(304, done); 81 | }); 82 | }) 83 | 84 | describe('hidden files', function(){ 85 | it('should be ignored by default', function(done){ 86 | app.request() 87 | .get('/.hidden') 88 | .expect(404, done); 89 | }) 90 | 91 | it('should be served when hidden: true is given', function(done){ 92 | var app = connect(); 93 | 94 | app.use(connect.static(fixtures, { hidden: true })); 95 | 96 | app.request() 97 | .get('/.hidden') 98 | .expect('I am hidden', done); 99 | }) 100 | }) 101 | 102 | describe('maxAge', function(){ 103 | it('should be 0 by default', function(done){ 104 | app.request() 105 | .get('/todo.txt') 106 | .end(function(res){ 107 | res.should.have.header('cache-control', 'public, max-age=0'); 108 | done(); 109 | }); 110 | }) 111 | 112 | it('should be reasonable when infinite', function(done){ 113 | var app = connect(); 114 | 115 | app.use(connect.static(fixtures, { maxAge: Infinity })); 116 | 117 | app.request() 118 | .get('/todo.txt') 119 | .end(function(res){ 120 | res.should.have.header('cache-control', 'public, max-age=' + 60*60*24*365); 121 | done(); 122 | }); 123 | }) 124 | }) 125 | 126 | describe('when traversing passed root', function(){ 127 | it('should respond with 403 Forbidden', function(done){ 128 | app.request() 129 | .get('/users/../../todo.txt') 130 | .expect(403, done); 131 | }) 132 | 133 | it('should catch urlencoded ../', function(done){ 134 | app.request() 135 | .get('/users/%2e%2e/%2e%2e/todo.txt') 136 | .expect(403, done); 137 | }) 138 | }) 139 | 140 | describe('on ENOENT', function(){ 141 | it('should next()', function(done){ 142 | app.request() 143 | .get('/does-not-exist') 144 | .end(function(res){ 145 | res.should.have.status(404); 146 | res.body.should.equal('sorry!'); 147 | done(); 148 | }); 149 | }) 150 | }) 151 | 152 | describe('Range', function(){ 153 | it('should support byte ranges', function(done){ 154 | app.request() 155 | .get('/nums') 156 | .set('Range', 'bytes=0-4') 157 | .expect('12345', done); 158 | }) 159 | 160 | it('should be inclusive', function(done){ 161 | app.request() 162 | .get('/nums') 163 | .set('Range', 'bytes=0-0') 164 | .expect('1', done); 165 | }) 166 | 167 | it('should set Content-Range', function(done){ 168 | app.request() 169 | .get('/nums') 170 | .set('Range', 'bytes=2-5') 171 | .expect('Content-Range', 'bytes 2-5/9', done); 172 | }) 173 | 174 | it('should support -n', function(done){ 175 | app.request() 176 | .get('/nums') 177 | .set('Range', 'bytes=-3') 178 | .expect('789', done); 179 | }) 180 | 181 | it('should support n-', function(done){ 182 | app.request() 183 | .get('/nums') 184 | .set('Range', 'bytes=3-') 185 | .expect('456789', done); 186 | }) 187 | 188 | it('should respond with 206 "Partial Content"', function(done){ 189 | app.request() 190 | .get('/nums') 191 | .set('Range', 'bytes=0-4') 192 | .expect(206, done); 193 | }) 194 | 195 | it('should set Content-Length to the # of octets transferred', function(done){ 196 | app.request() 197 | .get('/nums') 198 | .set('Range', 'bytes=2-3') 199 | .end(function(res){ 200 | res.body.should.equal('34'); 201 | res.headers['content-length'].should.equal('2'); 202 | done(); 203 | }); 204 | }) 205 | 206 | describe('when last-byte-pos of the range is greater than current length', function(){ 207 | it('is taken to be equal to one less than the current length', function(done){ 208 | app.request() 209 | .get('/nums') 210 | .set('Range', 'bytes=2-50') 211 | .expect('Content-Range', 'bytes 2-8/9', done) 212 | }) 213 | 214 | it('should adapt the Content-Length accordingly', function(done){ 215 | app.request() 216 | .get('/nums') 217 | .set('Range', 'bytes=2-50') 218 | .end(function(res){ 219 | res.headers['content-length'].should.equal('7'); 220 | done(); 221 | }); 222 | }) 223 | }) 224 | 225 | describe('when the first- byte-pos of the range is greater than the current length', function(){ 226 | it('should respond with 416', function(done){ 227 | app.request() 228 | .get('/nums') 229 | .set('Range', 'bytes=9-50') 230 | .expect(416, done); 231 | }) 232 | 233 | it('should include a Content-Range field with a byte-range- resp-spec of "*" and an instance-length specifying the current length', function(done){ 234 | app.request() 235 | .get('/nums') 236 | .set('Range', 'bytes=9-50') 237 | .expect('Content-Range', 'bytes */9', done) 238 | }) 239 | }) 240 | 241 | describe('when syntactically invalid', function(){ 242 | it('should respond with 200 and the entire contents', function(done){ 243 | app.request() 244 | .get('/nums') 245 | .set('Range', 'asdf') 246 | .expect('123456789', done); 247 | }) 248 | }) 249 | }) 250 | 251 | describe('when a trailing backslash is given', function(){ 252 | it('should 500', function(done){ 253 | app.request() 254 | .get('/todo.txt\\') 255 | .expect(500, done); 256 | }) 257 | }) 258 | 259 | describe('with a malformed URL', function(){ 260 | it('should respond with 400', function(done){ 261 | app.request() 262 | .get('/%') 263 | .expect(400, done) 264 | }); 265 | }) 266 | 267 | describe('on ENAMETOOLONG', function(){ 268 | it('should next()', function(done){ 269 | var path = Array(100).join('foobar'); 270 | 271 | app.request() 272 | .get('/' + path) 273 | .expect(404, done); 274 | }) 275 | }) 276 | 277 | describe('on ENOTDIR', function(){ 278 | it('should next()', function(done) { 279 | app.request() 280 | .get('/todo.txt/a.php') 281 | .expect(404, done); 282 | }) 283 | }) 284 | 285 | describe('when mounted', function(){ 286 | it('should redirect relative to the originalUrl', function(done){ 287 | var app = connect(); 288 | 289 | app.use('/static', connect.static('test/fixtures')); 290 | 291 | app.request() 292 | .get('/static/users') 293 | .end(function(res){ 294 | res.headers.location.should.equal('/static/users/'); 295 | res.should.have.status(301); 296 | done(); 297 | }); 298 | }) 299 | }) 300 | 301 | describe('when responding non-2xx or 304', function(){ 302 | it('should respond as-is', function(done){ 303 | var app = connect(); 304 | var n = 0; 305 | 306 | app.use(function(req, res, next){ 307 | switch (n++) { 308 | case 0: return next(); 309 | case 1: res.statusCode = 500; return next(); 310 | } 311 | }); 312 | 313 | app.use(connect.static(fixtures)); 314 | 315 | app.request() 316 | .get('/todo.txt') 317 | .end(function(res){ 318 | app.request() 319 | .get('/todo.txt') 320 | .set('If-None-Match', res.headers.etag) 321 | .end(function(res){ 322 | res.should.have.status(500); 323 | res.body.should.equal('- groceries'); 324 | done(); 325 | }) 326 | }) 327 | }) 328 | }) 329 | }) 330 | -------------------------------------------------------------------------------- /test/multipart.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../') 3 | , should = require('./shared'); 4 | 5 | var app = connect(); 6 | 7 | app.use(connect.multipart({ limit: '20mb' })); 8 | 9 | app.use(function(req, res){ 10 | res.end(JSON.stringify(req.body)); 11 | }); 12 | 13 | describe('connect.multipart()', function(){ 14 | should['default request body'](app); 15 | should['limit body to']('20mb', 'multipart/form-data', app); 16 | 17 | it('should ignore GET', function(done){ 18 | app.request() 19 | .get('/') 20 | .set('Content-Type', 'multipart/form-data; boundary=foo') 21 | .write('--foo\r\n') 22 | .write('Content-Disposition: form-data; name="user"\r\n') 23 | .write('\r\n') 24 | .write('Tobi') 25 | .write('\r\n--foo--') 26 | .end(function(res){ 27 | res.body.should.equal('{}'); 28 | done(); 29 | }); 30 | }) 31 | 32 | describe('with multipart/form-data', function(){ 33 | it('should populate req.body', function(done){ 34 | app.request() 35 | .post('/') 36 | .set('Content-Type', 'multipart/form-data; boundary=foo') 37 | .write('--foo\r\n') 38 | .write('Content-Disposition: form-data; name="user"\r\n') 39 | .write('\r\n') 40 | .write('Tobi') 41 | .write('\r\n--foo--') 42 | .end(function(res){ 43 | res.body.should.equal('{"user":"Tobi"}'); 44 | done(); 45 | }); 46 | }) 47 | 48 | it('should support files', function(done){ 49 | var app = connect(); 50 | 51 | app.use(connect.multipart()); 52 | 53 | app.use(function(req, res){ 54 | req.body.user.should.eql({ name: 'Tobi' }); 55 | req.files.text.path.should.not.include('.txt'); 56 | req.files.text.constructor.name.should.equal('File'); 57 | res.end(req.files.text.name); 58 | }); 59 | 60 | app.request() 61 | .post('/') 62 | .set('Content-Type', 'multipart/form-data; boundary=foo') 63 | .write('--foo\r\n') 64 | .write('Content-Disposition: form-data; name="user[name]"\r\n') 65 | .write('\r\n') 66 | .write('Tobi') 67 | .write('\r\n--foo\r\n') 68 | .write('Content-Disposition: form-data; name="text"; filename="foo.txt"\r\n') 69 | .write('\r\n') 70 | .write('some text here') 71 | .write('\r\n--foo--') 72 | .end(function(res){ 73 | res.body.should.equal('foo.txt'); 74 | done(); 75 | }); 76 | }) 77 | 78 | it('should expose options to formidable', function(done){ 79 | var app = connect(); 80 | 81 | app.use(connect.multipart({ 82 | keepExtensions: true 83 | })); 84 | 85 | app.use(function(req, res){ 86 | req.body.user.should.eql({ name: 'Tobi' }); 87 | req.files.text.path.should.include('.txt'); 88 | req.files.text.constructor.name.should.equal('File'); 89 | res.end(req.files.text.name); 90 | }); 91 | 92 | app.request() 93 | .post('/') 94 | .set('Content-Type', 'multipart/form-data; boundary=foo') 95 | .write('--foo\r\n') 96 | .write('Content-Disposition: form-data; name="user[name]"\r\n') 97 | .write('\r\n') 98 | .write('Tobi') 99 | .write('\r\n--foo\r\n') 100 | .write('Content-Disposition: form-data; name="text"; filename="foo.txt"\r\n') 101 | .write('\r\n') 102 | .write('some text here') 103 | .write('\r\n--foo--') 104 | .end(function(res){ 105 | res.body.should.equal('foo.txt'); 106 | done(); 107 | }); 108 | }) 109 | 110 | it('should work with multiple fields', function(done){ 111 | app.request() 112 | .post('/') 113 | .set('Content-Type', 'multipart/form-data; boundary=foo') 114 | .write('--foo\r\n') 115 | .write('Content-Disposition: form-data; name="user"\r\n') 116 | .write('\r\n') 117 | .write('Tobi') 118 | .write('\r\n--foo\r\n') 119 | .write('Content-Disposition: form-data; name="age"\r\n') 120 | .write('\r\n') 121 | .write('1') 122 | .write('\r\n--foo--') 123 | .end(function(res){ 124 | res.body.should.equal('{"user":"Tobi","age":"1"}'); 125 | done(); 126 | }); 127 | }) 128 | 129 | it('should support nesting', function(done){ 130 | app.request() 131 | .post('/') 132 | .set('Content-Type', 'multipart/form-data; boundary=foo') 133 | .write('--foo\r\n') 134 | .write('Content-Disposition: form-data; name="user[name][first]"\r\n') 135 | .write('\r\n') 136 | .write('tobi') 137 | .write('\r\n--foo\r\n') 138 | .write('Content-Disposition: form-data; name="user[name][last]"\r\n') 139 | .write('\r\n') 140 | .write('holowaychuk') 141 | .write('\r\n--foo\r\n') 142 | .write('Content-Disposition: form-data; name="user[age]"\r\n') 143 | .write('\r\n') 144 | .write('1') 145 | .write('\r\n--foo\r\n') 146 | .write('Content-Disposition: form-data; name="species"\r\n') 147 | .write('\r\n') 148 | .write('ferret') 149 | .write('\r\n--foo--') 150 | .end(function(res){ 151 | var obj = JSON.parse(res.body); 152 | obj.user.age.should.equal('1'); 153 | obj.user.name.should.eql({ first: 'tobi', last: 'holowaychuk' }); 154 | obj.species.should.equal('ferret'); 155 | done(); 156 | }); 157 | }) 158 | 159 | it('should support multiple files of the same name', function(done){ 160 | var app = connect(); 161 | 162 | app.use(connect.multipart()); 163 | 164 | app.use(function(req, res){ 165 | req.files.text.should.have.length(2); 166 | req.files.text[0].constructor.name.should.equal('File'); 167 | req.files.text[1].constructor.name.should.equal('File'); 168 | res.end(); 169 | }); 170 | 171 | app.request() 172 | .post('/') 173 | .set('Content-Type', 'multipart/form-data; boundary=foo') 174 | .write('--foo\r\n') 175 | .write('Content-Disposition: form-data; name="text"; filename="foo.txt"\r\n') 176 | .write('\r\n') 177 | .write('some text here') 178 | .write('\r\n--foo\r\n') 179 | .write('Content-Disposition: form-data; name="text"; filename="bar.txt"\r\n') 180 | .write('\r\n') 181 | .write('some more text stuff') 182 | .write('\r\n--foo--') 183 | .end(function(res){ 184 | res.statusCode.should.equal(200); 185 | done(); 186 | }); 187 | }) 188 | 189 | it('should support nested files', function(done){ 190 | var app = connect(); 191 | 192 | app.use(connect.multipart()); 193 | 194 | app.use(function(req, res){ 195 | Object.keys(req.files.docs).should.have.length(2); 196 | req.files.docs.foo.name.should.equal('foo.txt'); 197 | req.files.docs.bar.name.should.equal('bar.txt'); 198 | res.end(); 199 | }); 200 | 201 | app.request() 202 | .post('/') 203 | .set('Content-Type', 'multipart/form-data; boundary=foo') 204 | .write('--foo\r\n') 205 | .write('Content-Disposition: form-data; name="docs[foo]"; filename="foo.txt"\r\n') 206 | .write('\r\n') 207 | .write('some text here') 208 | .write('\r\n--foo\r\n') 209 | .write('Content-Disposition: form-data; name="docs[bar]"; filename="bar.txt"\r\n') 210 | .write('\r\n') 211 | .write('some more text stuff') 212 | .write('\r\n--foo--') 213 | .end(function(res){ 214 | res.statusCode.should.equal(200); 215 | done(); 216 | }); 217 | }) 218 | 219 | it('should next(err) on multipart failure', function(done){ 220 | var app = connect(); 221 | 222 | app.use(connect.multipart()); 223 | 224 | app.use(function(req, res){ 225 | res.end('whoop'); 226 | }); 227 | 228 | app.use(function(err, req, res, next){ 229 | err.message.should.equal('parser error, 16 of 28 bytes parsed'); 230 | res.statusCode = err.status; 231 | res.end('bad request'); 232 | }); 233 | 234 | app.request() 235 | .post('/') 236 | .set('Content-Type', 'multipart/form-data; boundary=foo') 237 | .write('--foo\r\n') 238 | .write('Content-filename="foo.txt"\r\n') 239 | .write('\r\n') 240 | .write('some text here') 241 | .write('Content-Disposition: form-data; name="text"; filename="bar.txt"\r\n') 242 | .write('\r\n') 243 | .write('some more text stuff') 244 | .write('\r\n--foo--') 245 | .end(function(res){ 246 | res.statusCode.should.equal(400); 247 | res.body.should.equal('bad request'); 248 | done(); 249 | }); 250 | }) 251 | 252 | it('should default req.files to {}', function(done){ 253 | var app = connect(); 254 | 255 | app.use(connect.multipart()); 256 | 257 | app.use(function(req, res){ 258 | res.end(JSON.stringify(req.files)); 259 | }); 260 | 261 | app.request() 262 | .post('/') 263 | .end(function(res){ 264 | res.body.should.equal('{}'); 265 | done(); 266 | }); 267 | }) 268 | 269 | it('should defer processing if `defer` is set', function(done){ 270 | var app = connect(); 271 | 272 | app.use(connect.multipart({ defer: true })); 273 | 274 | app.use(function(req, res){ 275 | JSON.stringify(req.body).should.equal("{}"); 276 | req.form.on("end", function() { 277 | res.end(JSON.stringify(req.body)); 278 | }); 279 | }); 280 | 281 | app.request() 282 | .post('/') 283 | .set('Content-Type', 'multipart/form-data; boundary=foo') 284 | .write('--foo\r\n') 285 | .write('Content-Disposition: form-data; name="user"\r\n') 286 | .write('\r\n') 287 | .write('Tobi') 288 | .write('\r\n--foo\r\n') 289 | .write('Content-Disposition: form-data; name="age"\r\n') 290 | .write('\r\n') 291 | .write('1') 292 | .write('\r\n--foo--') 293 | .end(function(res){ 294 | res.body.should.equal('{"user":"Tobi","age":"1"}'); 295 | done(); 296 | }); 297 | }) 298 | 299 | }) 300 | }) 301 | --------------------------------------------------------------------------------