├── test ├── fixtures │ ├── directory │ │ ├── bar │ │ ├── foo │ │ ├── .hidden │ │ └── baz.js │ ├── list │ ├── .hidden │ ├── some text.txt │ ├── index.html │ ├── foo.bar.baz.sass │ ├── style.less │ ├── style.sass │ ├── user.json │ ├── script.coffee │ ├── ssl.crt │ └── ssl.key ├── query.test.js ├── responseTime.test.js ├── favicon.test.js ├── methodOverride.test.js ├── directory.test.js ├── cookieParser.test.js ├── compiler.test.js ├── vhost.test.js ├── bodyParser.test.js ├── basicAuth.test.js ├── utils.test.js ├── errorHandler.test.js ├── logger.test.js ├── connect.test.js ├── static.test.js └── router.test.js ├── index.js ├── 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 │ ├── cookieParser.js │ ├── methodOverride.js │ ├── vhost.js │ ├── session │ │ ├── store.js │ │ ├── cookie.js │ │ ├── memory.js │ │ └── session.js │ ├── limit.js │ ├── favicon.js │ ├── bodyParser.js │ ├── profiler.js │ ├── basicAuth.js │ ├── errorHandler.js │ ├── compiler.js │ ├── logger.js │ ├── static.js │ ├── directory.js │ ├── router.js │ └── session.js ├── patch.js ├── https.js ├── index.js ├── connect.js ├── http.js └── utils.js ├── examples ├── public │ └── tobi.jpeg ├── favicon.js ├── logger.js ├── helloworld.js ├── logger.fast.js ├── static.js ├── profiler.js ├── error.js ├── basicAuth.js ├── mounting.js ├── vhost.js ├── logger.format.js └── session.js ├── .npmignore ├── .gitignore ├── .gitmodules ├── Makefile ├── meta.json ├── package.json ├── LICENSE ├── Readme.md ├── support ├── page.html └── docs.js └── docs └── main.css /test/fixtures/directory/bar: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/directory/foo: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/list: -------------------------------------------------------------------------------- 1 | 123456789 -------------------------------------------------------------------------------- /test/fixtures/.hidden: -------------------------------------------------------------------------------- 1 | hidden 2 | -------------------------------------------------------------------------------- /test/fixtures/directory/.hidden: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/directory/baz.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/some text.txt: -------------------------------------------------------------------------------- 1 | whoop -------------------------------------------------------------------------------- /test/fixtures/index.html: -------------------------------------------------------------------------------- 1 |

Wahoo!

-------------------------------------------------------------------------------- /test/fixtures/foo.bar.baz.sass: -------------------------------------------------------------------------------- 1 | foo 2 | :color #000 -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/connect'); -------------------------------------------------------------------------------- /test/fixtures/style.less: -------------------------------------------------------------------------------- 1 | @red: #cc0000; 2 | body { color: @red } -------------------------------------------------------------------------------- /test/fixtures/style.sass: -------------------------------------------------------------------------------- 1 | body 2 | :font-size 12px 3 | :color #000 -------------------------------------------------------------------------------- /test/fixtures/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tj", 3 | "email": "tj@vision-media.ca" 4 | } -------------------------------------------------------------------------------- /lib/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/favicon.ico -------------------------------------------------------------------------------- /examples/public/tobi.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/examples/public/tobi.jpeg -------------------------------------------------------------------------------- /lib/public/icons/page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page.png -------------------------------------------------------------------------------- /test/fixtures/script.coffee: -------------------------------------------------------------------------------- 1 | $ = jQuery 2 | $(document).ready -> 3 | console.log "Say Hello to CoffeeScript" 4 | -------------------------------------------------------------------------------- /lib/public/icons/page_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_add.png -------------------------------------------------------------------------------- /lib/public/icons/page_go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_go.png -------------------------------------------------------------------------------- /lib/public/icons/page_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_key.png -------------------------------------------------------------------------------- /lib/public/icons/page_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_red.png -------------------------------------------------------------------------------- /lib/public/icons/page_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_code.png -------------------------------------------------------------------------------- /lib/public/icons/page_copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_copy.png -------------------------------------------------------------------------------- /lib/public/icons/page_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_edit.png -------------------------------------------------------------------------------- /lib/public/icons/page_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_error.png -------------------------------------------------------------------------------- /lib/public/icons/page_excel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_excel.png -------------------------------------------------------------------------------- /lib/public/icons/page_find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_find.png -------------------------------------------------------------------------------- /lib/public/icons/page_gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_gear.png -------------------------------------------------------------------------------- /lib/public/icons/page_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_green.png -------------------------------------------------------------------------------- /lib/public/icons/page_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_link.png -------------------------------------------------------------------------------- /lib/public/icons/page_paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_paste.png -------------------------------------------------------------------------------- /lib/public/icons/page_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_save.png -------------------------------------------------------------------------------- /lib/public/icons/page_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white.png -------------------------------------------------------------------------------- /lib/public/icons/page_word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_word.png -------------------------------------------------------------------------------- /lib/public/icons/page_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_world.png -------------------------------------------------------------------------------- /lib/public/icons/page_attach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_attach.png -------------------------------------------------------------------------------- /lib/public/icons/page_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_delete.png -------------------------------------------------------------------------------- /lib/public/icons/page_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_refresh.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_c.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_cd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_cd.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_go.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_h.png -------------------------------------------------------------------------------- /lib/public/icons/page_lightning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_lightning.png -------------------------------------------------------------------------------- /lib/public/icons/page_paintbrush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_paintbrush.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_add.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_code.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_copy.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_cup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_cup.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_dvd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_dvd.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_edit.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_find.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_gear.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_get.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_get.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_key.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_link.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_php.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_put.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_put.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_ruby.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_star.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_text.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_tux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_tux.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_word.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_zip.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_acrobat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_acrobat.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_camera.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_csharp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_csharp.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_delete.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_error.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_excel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_excel.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_flash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_flash.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_magnify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_magnify.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_medal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_medal.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_office.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_office.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_paint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_paint.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_paste.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_picture.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_stack.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_swoosh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_swoosh.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_vector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_vector.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_width.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_world.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_wrench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_wrench.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_code_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_code_red.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_cplusplus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_cplusplus.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_database.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_freehand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_freehand.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_lightning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_lightning.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_actionscript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_actionscript.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_coldfusion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_coldfusion.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_compressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_compressed.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_horizontal.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_paintbrush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_paintbrush.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_powerpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_powerpoint.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_text_width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/connect/master/lib/public/icons/page_white_text_width.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_visualstudio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subtleGradient/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 | -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | pids 3 | logs 4 | results 5 | *.pid 6 | *.gz 7 | *.log 8 | lib-cov 9 | test/fixtures/foo.bar.baz.css 10 | test/fixtures/style.css 11 | test/fixtures/script.js 12 | test.js 13 | docs/*.html 14 | docs/*.json 15 | node_modules 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /lib/public/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {error} 4 | 5 | 6 | 7 |
8 |

{title}

9 |

500 {error}

10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "examples/sousaball/support/microtemplates"] 2 | path = examples/sousaball/support/microtemplates 3 | url = git://github.com/creationix/microtemplates.git 4 | [submodule "examples/sousaball/support/postgres-js"] 5 | path = examples/sousaball/support/postgres-js 6 | url = git://github.com/creationix/postgres-js.git 7 | [submodule "support/coffee-script"] 8 | path = support/coffee-script 9 | url = http://github.com/jashkenas/coffee-script.git 10 | -------------------------------------------------------------------------------- /examples/error.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | // try: 9 | // - viewing in a browser 10 | // - curl http://localhost:3000 11 | // - curl -H "Accept: application/json" http://localhost:3000 12 | 13 | var server = connect.createServer( 14 | function(req, res, next){ 15 | throw new Error('oh noes!'); 16 | }, 17 | connect.errorHandler({ stack: true, dump: true }) 18 | ); 19 | 20 | server.listen(3000); 21 | console.log('Connect server listening on port 3000'); -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | TEST = node_modules/.bin/expresso 3 | TESTS ?= test/*.test.js 4 | SRC = $(shell find lib -type f -name "*.js") 5 | 6 | test: 7 | @NODE_ENV=test ./$(TEST) \ 8 | -I lib \ 9 | $(TEST_FLAGS) $(TESTS) 10 | 11 | test-cov: 12 | @$(MAKE) test TEST_FLAGS="--cov" 13 | 14 | docs: 15 | @mkdir -p docs 16 | @node support/docs.js $(SRC) 17 | 18 | docclean: 19 | rm -f docs/*.{html,json} 20 | 21 | site: docclean docs 22 | rm -fr /tmp/docs \ 23 | && cp -fr docs /tmp/docs \ 24 | && git checkout gh-pages \ 25 | && cp -fr /tmp/docs/* . \ 26 | && echo "done" 27 | 28 | .PHONY: site docs test test-cov docclean -------------------------------------------------------------------------------- /test/query.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect') 7 | , assert = require('assert') 8 | , http = require('http'); 9 | 10 | var app = connect( 11 | connect.query() 12 | , function(req, res){ 13 | res.end(JSON.stringify(req.query)); 14 | } 15 | ); 16 | 17 | module.exports = { 18 | 'test empty query string object': function(){ 19 | assert.response(app, 20 | { url: '/' }, 21 | { body: '{}' }); 22 | }, 23 | 24 | 'test query string': function(){ 25 | assert.response(app, 26 | { url: '/?foo=bar' }, 27 | { body: '{"foo":"bar"}' }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /test/responseTime.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect') 7 | , assert = require('assert') 8 | , should = require('should') 9 | , http = require('http'); 10 | 11 | var app = connect.createServer( 12 | connect.responseTime(), 13 | function(req, res){ 14 | setTimeout(function(){ 15 | res.end('Hello'); 16 | }, 500); 17 | } 18 | ); 19 | 20 | module.exports = { 21 | 'test X-Response-Time': function(){ 22 | assert.response(app, 23 | { url: '/' }, 24 | { body: 'Hello' 25 | , headers: { 26 | 'X-Response-Time': /^\d+ms$/ 27 | }}); 28 | } 29 | }; -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /meta.json: -------------------------------------------------------------------------------- 1 | {"files":["lib/connect.js","lib/http.js","lib/https.js","lib/index.js","lib/middleware/basicAuth.js","lib/middleware/bodyParser.js","lib/middleware/compiler.js","lib/middleware/cookieParser.js","lib/middleware/directory.js","lib/middleware/errorHandler.js","lib/middleware/favicon.js","lib/middleware/limit.js","lib/middleware/logger.js","lib/middleware/methodOverride.js","lib/middleware/profiler.js","lib/middleware/responseTime.js","lib/middleware/router.js","lib/middleware/session/cookie.js","lib/middleware/session/memory.js","lib/middleware/session/session.js","lib/middleware/session/store.js","lib/middleware/session.js","lib/middleware/static.js","lib/middleware/vhost.js","lib/patch.js","lib/utils.js"]} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect", 3 | "version": "1.5.1", 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 | "repository": "git://github.com/senchalabs/connect", 9 | "dependencies": { 10 | "qs": ">= 0.0.6", 11 | "mime": ">= 0.0.1" 12 | }, 13 | "devDependencies": { 14 | "expresso": "0.7.6", 15 | "koala": "0.1.2", 16 | "less": "1.1.1", 17 | "sass": "0.5.0", 18 | "markdown": "0.2.1", 19 | "ejs": "0.4.3", 20 | "should": "0.2.1" 21 | }, 22 | "main": "index", 23 | "engines": { "node": ">= 0.4.1 < 0.5.0" } 24 | } -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /test/fixtures/ssl.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICATCCAWoCCQCqn8Q1CZmBhDANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJB 3 | VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 4 | cyBQdHkgTHRkMB4XDTExMDMzMDE3NDcxNloXDTEyMDMyOTE3NDcxNlowRTELMAkG 5 | A1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0 6 | IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAz3tY 7 | yeOP2bO18sA0BL+IRq21YL5VVFxxHmbHgnx2ZglLtul4AHpAwq0aUk+26UqvkCZe 8 | tvhlzWCbvQECuFF4lVXs7/6QSw0YheFhmjf8V0LK4FDJC1ueu+AvBsT6UBof/7GG 9 | EvriOXHgDcoRMJJCY7Y5l/EUBpp29Fyy9KVdtJUCAwEAATANBgkqhkiG9w0BAQUF 10 | AAOBgQCXm+Qr/BdigkObPgriHyDhBx88jyQL27SRUQbInI2nRuo2t9bgx2bEkK8p 11 | UtVc6KBe9QxpRbZPH7XpHh8jtZyT1YsX3LqNvVOpDDIO9wb4L9SyXrx4LUckDd6S 12 | 8qa0O8QH6nWt1l6K1tihMsjnN1f66WWnUyzQ/o+bVbJFSjgSgg== 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /test/favicon.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect') 7 | , assert = require('assert') 8 | , http = require('http'); 9 | 10 | module.exports = { 11 | 'test headers': function(){ 12 | var app = connect.createServer(connect.favicon()); 13 | 14 | assert.response(app, 15 | { url: '/favicon.ico' }, 16 | { status: 200 17 | , headers: { 18 | 'Content-Type': 'image/x-icon' 19 | , 'Cache-Control': 'public, max-age=86400' 20 | }}); 21 | }, 22 | 23 | 'test custom favicon': function(){ 24 | var app = connect.createServer(); 25 | app.use(connect.favicon(__dirname + '/../lib/public/favicon.ico')); 26 | assert.response(app, 27 | { url: '/favicon.ico' }, 28 | { status: 200, headers: { 'Content-Type': 'image/x-icon' }}); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /lib/middleware/responseTime.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - responseTime 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Adds the `X-Response-Time` header displaying the response 10 | * duration in milliseconds. 11 | * 12 | * @return {Function} 13 | * @api public 14 | */ 15 | 16 | module.exports = function responseTime(){ 17 | return function(req, res, next){ 18 | var writeHead = res.writeHead 19 | , start = new Date; 20 | 21 | if (res._responseTime) return next(); 22 | res._responseTime = true; 23 | 24 | // proxy writeHead to calculate duration 25 | res.writeHead = function(status, headers){ 26 | var duration = new Date - start; 27 | res.setHeader('X-Response-Time', duration + 'ms'); 28 | res.writeHead = writeHead; 29 | res.writeHead(status, headers); 30 | }; 31 | 32 | next(); 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /lib/middleware/query.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - query 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * Copyright(c) 2011 Sencha Inc. 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var qs = require('qs') 14 | , parse = require('url').parse; 15 | 16 | /** 17 | * Automatically parse the query-string when available, 18 | * populating the `req.query` object. 19 | * 20 | * Examples: 21 | * 22 | * connect( 23 | * connect.query() 24 | * , function(req, res){ 25 | * res.end(JSON.stringify(req.query)); 26 | * } 27 | * ).listen(3000); 28 | * 29 | * @return {Function} 30 | * @api public 31 | */ 32 | 33 | module.exports = function query(){ 34 | return function query(req, res, next){ 35 | req.query = ~req.url.indexOf('?') 36 | ? qs.parse(parse(req.url).query) 37 | : {}; 38 | next(); 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /test/fixtures/ssl.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQDPe1jJ44/Zs7XywDQEv4hGrbVgvlVUXHEeZseCfHZmCUu26XgA 3 | ekDCrRpST7bpSq+QJl62+GXNYJu9AQK4UXiVVezv/pBLDRiF4WGaN/xXQsrgUMkL 4 | W5674C8GxPpQGh//sYYS+uI5ceANyhEwkkJjtjmX8RQGmnb0XLL0pV20lQIDAQAB 5 | AoGBAL+jyZXod7T4deV7PFDqbEAEMJTkGMKsA9u1yS+wMFf83A9dw/aE9Q4bf0Vp 6 | 1aPT1SdLGY7dDoLNaewAY/fFYJ69F//K2dkh3p0e5sWHn8WpL3dxuymfi4wB3ye/ 7 | 1UCfdrVH98RqxFljt7f8dEn2bnAgA4gWKcMvl+YULHpDbfXhAkEA5zS+A7SUOrFJ 8 | P10803+4DSFnxmilQifR0gE5qBLjUqJKyrRpOttCOzMBNFYAZIB2bsvtppHEXMOB 9 | XZw0jxML7QJBAOW7Tu7WIb4+hfrtA56qTJgoOy8WwqEPtZIMyl0x2ukQf+3oIXK/ 10 | +Jz14kn4UFaGOxWEnKL3HIqU1g3mqvnOxkkCQEtVihxW+H1vSriUvr8DPIs6uT+S 11 | 1VYK93j/4TN8hAlmzAvkYO1Gh/wWEGxnIVWd7fkIBXVixaKcKUjBHvcHc7kCQD5r 12 | kXvlpM97T44pfjVLUnp5W/NkfMekbBJd9VIzLKbs+8WZsBTswlFroeu1U6be3Ajx 13 | ulmxSQkCfdLTHRu5KjkCQAbbBJMgc1yZ1e5wGVbPwFQiAssQ72EGF5s7jdWLFzvP 14 | julPtSt0Bek4LtEqpOBfvVTH9zDhnVhBFSGEl/JEFvM= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/methodOverride.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect') 7 | , assert = require('assert') 8 | , should = require('should') 9 | , http = require('http'); 10 | 11 | var app = connect.createServer( 12 | connect.bodyParser(), 13 | connect.methodOverride(), 14 | function(req, res){ 15 | res.end(req.method); 16 | } 17 | ); 18 | 19 | module.exports = { 20 | 'test request body': function(){ 21 | assert.response(app, 22 | { url: '/' 23 | , method: 'POST' 24 | , data: '_method=put' 25 | , headers: { 'Content-Type': 'application/x-www-form-urlencoded' }}, 26 | { body: 'PUT' }); 27 | }, 28 | 29 | 'test x-http-method-override': function(){ 30 | assert.response(app, 31 | { url: '/' 32 | , method: 'POST' 33 | , data: '_method=put' 34 | , headers: { 'X-HTTP-Method-Override': 'DELETE' }}, 35 | { body: 'DELETE' }); 36 | } 37 | }; -------------------------------------------------------------------------------- /test/directory.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect') 7 | , assert = require('assert'); 8 | 9 | var app = connect( 10 | connect.directory(__dirname + '/fixtures/directory') 11 | ); 12 | 13 | module.exports = { 14 | 'test default': function(){ 15 | assert.response(app, 16 | { url: '/' }, 17 | { body: 'bar\nbaz.js\nfoo\n' }); 18 | }, 19 | 20 | 'test Accept: text/plain': function(){ 21 | assert.response(app, 22 | { url: '/', headers: { Accept: 'text/plain' }}, 23 | { body: 'bar\nbaz.js\nfoo\n' }); 24 | }, 25 | 26 | 'test Accept: application/json': function(){ 27 | assert.response(app, 28 | { url: '/', headers: { Accept: 'application/json' }}, 29 | { body: '["bar","baz.js","foo"]' }); 30 | }, 31 | 32 | 'test forbidden': function(){ 33 | assert.response(app, 34 | { url: '/../../../' }, 35 | { status: 403 }); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /test/cookieParser.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect') 7 | , assert = require('assert') 8 | , http = require('http'); 9 | 10 | var app = connect.createServer( 11 | connect.cookieParser(), 12 | function(req, res, next){ 13 | res.end(JSON.stringify(req.cookies)); 14 | } 15 | ); 16 | 17 | module.exports = { 18 | 'test without cookies': function(){ 19 | assert.response(app, 20 | { url: '/' }, 21 | { body: '{}' }); 22 | }, 23 | 24 | 'test single cookie': function(){ 25 | assert.response(app, 26 | { url: '/', headers: { Cookie: ['sid=123'] }}, 27 | { body: '{"sid":"123"}' }); 28 | }, 29 | 30 | 'test several cookies': function(){ 31 | assert.response(app, 32 | { url: '/', headers: { Cookie: ['sid=123', 'name=tj'] }}, 33 | { body: '{"sid":"123","name":"tj"}' }); 34 | }, 35 | 36 | 'test malformed cookie': function() { 37 | assert.response(app, 38 | { url: '/', headers: { Cookie: ['sid=%g23'] }}, 39 | { body: '{"sid":"%g23"}' }); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /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 | 15 | /** 16 | * Parse _Cookie_ header and populate `req.cookies` 17 | * with an object keyed by the cookie names. 18 | * 19 | * Examples: 20 | * 21 | * connect.createServer( 22 | * connect.cookieParser() 23 | * , function(req, res, next){ 24 | * res.end(JSON.stringify(req.cookies)); 25 | * } 26 | * ); 27 | * 28 | * @return {Function} 29 | * @api public 30 | */ 31 | 32 | module.exports = function cookieParser(){ 33 | return function cookieParser(req, res, next) { 34 | var cookie = req.headers.cookie; 35 | if (req.cookies) return next(); 36 | req.cookies = {}; 37 | if (cookie) { 38 | try { 39 | req.cookies = utils.parseCookie(cookie); 40 | } catch (err) { 41 | return next(err); 42 | } 43 | } 44 | next(); 45 | }; 46 | }; -------------------------------------------------------------------------------- /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 | * Provides faux HTTP method support. 11 | * 12 | * Pass an optional `key` to use when checking for 13 | * a method override, othewise defaults to _\_method_. 14 | * The original method is available via `req.originalMethod`. 15 | * 16 | * @param {String} key 17 | * @return {Function} 18 | * @api public 19 | */ 20 | 21 | module.exports = function methodOverride(key){ 22 | key = key || "_method"; 23 | return function methodOverride(req, res, next) { 24 | req.originalMethod = req.originalMethod || req.method; 25 | 26 | // req.body 27 | if (req.body && key in req.body) { 28 | req.method = req.body[key].toUpperCase(); 29 | delete req.body[key]; 30 | // check X-HTTP-Method-Override 31 | } else if (req.headers['x-http-method-override']) { 32 | req.method = req.headers['x-http-method-override'].toUpperCase(); 33 | } 34 | 35 | next(); 36 | }; 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /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.OutgoingMessage.prototype; 14 | 15 | // original setHeader() 16 | 17 | var setHeader = res.setHeader; 18 | 19 | 20 | if (!res._hasConnectPatch) { 21 | 22 | /** 23 | * Set header `field` to `val`, special-casing 24 | * the `Set-Cookie` field for multiple support. 25 | * 26 | * @param {String} field 27 | * @param {String} val 28 | * @api public 29 | */ 30 | 31 | res.setHeader = function(field, val){ 32 | var key = field.toLowerCase() 33 | , prev; 34 | 35 | // special-case Set-Cookie 36 | if (this._headers && 'set-cookie' == key) { 37 | if (prev = this.getHeader(field)) { 38 | val = Array.isArray(prev) 39 | ? prev.concat(val) 40 | : [prev, val]; 41 | } 42 | // charset 43 | } else if ('content-type' == key && this.charset) { 44 | val += '; charset=' + this.charset; 45 | } 46 | 47 | return setHeader.call(this, field, val); 48 | }; 49 | 50 | res._hasConnectPatch = true; 51 | } 52 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /lib/https.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 HTTPServer = require('./http').Server 14 | , https = require('https'); 15 | 16 | /** 17 | * Initialize a new `Server` with the given 18 | *`options` and `middleware`. The HTTPS api 19 | * is identical to the [HTTP](http.html) server, 20 | * however TLS `options` must be provided before 21 | * passing in the optional middleware. 22 | * 23 | * @params {Object} options 24 | * @params {Array} middleawre 25 | * @return {Server} 26 | * @api public 27 | */ 28 | 29 | var Server = exports.Server = function HTTPSServer(options, middleware) { 30 | this.stack = []; 31 | middleware.forEach(function(fn){ 32 | this.use(fn); 33 | }, this); 34 | https.Server.call(this, options, this.handle); 35 | }; 36 | 37 | /** 38 | * Inherit from `http.Server.prototype`. 39 | */ 40 | 41 | Server.prototype.__proto__ = https.Server.prototype; 42 | 43 | // mixin HTTPServer methods 44 | 45 | Object.keys(HTTPServer.prototype).forEach(function(method){ 46 | Server.prototype[method] = HTTPServer.prototype[method]; 47 | }); -------------------------------------------------------------------------------- /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 with ansi-escape sequence colors 11 | // format: ms 12 | 13 | connect.createServer( 14 | connect.logger('\033[90m:method\033[0m \033[36m:url\033[0m \033[90m:status :response-timems -> :res[Content-Type]\033[0m') 15 | , function(req, res){ 16 | res.statusCode = 500; 17 | res.setHeader('Content-Type', 'text/plain'); 18 | res.end('Internal Server Error'); 19 | } 20 | ).listen(3000); 21 | 22 | // $ curl http://localhost:3001/ 23 | // $ curl http://localhost:3001/404 24 | // $ curl http://localhost:3001/500 25 | 26 | connect.createServer( 27 | connect.logger(function(req, res, format){ 28 | var colors = { 404: 33, 500: 31 } 29 | , color = colors[res.statusCode] || 32; 30 | return format('\033[90m:method :url \033[0m\033[' + color + 'm:status\033[0m'); 31 | }) 32 | , function(req, res){ 33 | switch (req.url) { 34 | case '/404': res.statusCode = 404; break; 35 | case '/500': res.statusCode = 500; break; 36 | } 37 | res.end('weee'); 38 | } 39 | ).listen(3001); -------------------------------------------------------------------------------- /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 | * Setup vhost for the given `hostname` and `server`. 11 | * 12 | * Examples: 13 | * 14 | * connect( 15 | * connect.vhost('foo.com', 16 | * connect.createServer(...middleware...) 17 | * ), 18 | * connect.vhost('bar.com', 19 | * connect.createServer(...middleware...) 20 | * ) 21 | * ); 22 | * 23 | * @param {String} hostname 24 | * @param {Server} server 25 | * @return {Function} 26 | * @api public 27 | */ 28 | 29 | module.exports = function vhost(hostname, server){ 30 | if (!hostname) throw new Error('vhost hostname required'); 31 | if (!server) throw new Error('vhost server required'); 32 | var regexp = new RegExp('^' + hostname.replace(/[*]/g, '(.*?)') + '$'); 33 | if (server.onvhost) server.onvhost(hostname); 34 | return function vhost(req, res, next){ 35 | if (!req.headers.host) return next(); 36 | var host = req.headers.host.split(':')[0]; 37 | if (req.subdomains = regexp.exec(host)) { 38 | req.subdomains = req.subdomains[0].split('.').slice(0, -1); 39 | server.emit("request", req, res, next); 40 | } else { 41 | next(); 42 | } 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /test/compiler.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect') 7 | , assert = require('assert') 8 | , should = require('should') 9 | , http = require('http') 10 | , fs = require('fs'); 11 | 12 | var fixtures = __dirname + '/fixtures'; 13 | 14 | try { 15 | fs.unlinkSync(fixtures + '/style.css'); 16 | } catch (err) { 17 | // ignore 18 | } 19 | 20 | module.exports = { 21 | test: function(){ 22 | var app = connect.createServer( 23 | connect.compiler({ 24 | src: fixtures 25 | , enable: ['sass', 'coffeescript'] 26 | }), 27 | connect.static(fixtures) 28 | ); 29 | 30 | assert.response(app, 31 | { url: '/doesnotexist.css' }, 32 | { body: 'Cannot GET /doesnotexist.css', status: 404 }); 33 | 34 | assert.response(app, 35 | { url: '/style.css' }, 36 | { body: 'body {\n font-size: 12px;\n color: #000;}\n' }); 37 | 38 | assert.response(app, 39 | { url: '/style.css' }, 40 | { body: 'body {\n font-size: 12px;\n color: #000;}\n' }); 41 | 42 | assert.response(app, 43 | { url: '/foo.bar.baz.css' }, 44 | { body: 'foo {\n color: #000;}\n' }); 45 | }, 46 | 47 | 'test .compilers': function(){ 48 | connect.compiler.compilers.should.be.a('object'); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /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 Session = require('./session') 14 | , Cookie = require('./cookie') 15 | , utils = require('../../utils'); 16 | 17 | /** 18 | * Initialize abstract `Store`. 19 | * 20 | * @api private 21 | */ 22 | 23 | var Store = module.exports = function Store(options){}; 24 | 25 | /** 26 | * Re-generate the given requests's session. 27 | * 28 | * @param {IncomingRequest} req 29 | * @return {Function} fn 30 | * @api public 31 | */ 32 | 33 | Store.prototype.regenerate = function(req, fn){ 34 | var self = this; 35 | this.destroy(req.sessionID, function(err){ 36 | self.generate(req); 37 | fn(err); 38 | }); 39 | }; 40 | 41 | /** 42 | * Create session from JSON `sess` data. 43 | * 44 | * @param {IncomingRequest} req 45 | * @param {Object} sess 46 | * @return {Session} 47 | * @api private 48 | */ 49 | 50 | Store.prototype.createSession = function(req, sess){ 51 | var expires = sess.cookie.expires 52 | , orig = sess.cookie.originalMaxAge; 53 | sess.cookie = new Cookie(sess.cookie); 54 | if ('string' == typeof expires) sess.cookie.expires = new Date(expires); 55 | sess.cookie.originalMaxAge = orig; 56 | req.session = new Session(req, sess); 57 | req.session.resetLastAccess(); 58 | return req.session; 59 | }; -------------------------------------------------------------------------------- /test/vhost.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect') 7 | , assert = require('assert') 8 | , should = require('should') 9 | , http = require('http'); 10 | 11 | module.exports = { 12 | 'test with host': function(){ 13 | var a = connect.createServer(function(req, res){ 14 | res.end('from foo'); 15 | }); 16 | 17 | var b = connect.createServer(function(req, res){ 18 | res.end('from bar'); 19 | }); 20 | 21 | var app = connect.createServer( 22 | connect.vhost('foo.com', a) 23 | , connect.vhost('bar.com', b) 24 | ); 25 | 26 | assert.response(app, 27 | { url: '/', headers: { Host: 'foo.com' }}, 28 | { body: 'from foo' }); 29 | 30 | assert.response(app, 31 | { url: '/', headers: { Host: 'bar.com' }}, 32 | { body: 'from bar' }); 33 | 34 | assert.response(app, 35 | { url: '/', headers: { Host: 'other.com' }}, 36 | { body: 'Cannot GET /', status: 404 }); 37 | }, 38 | 39 | 'test with wildcard': function(){ 40 | var server = connect.createServer(function(req, res){ 41 | res.end('from ' + req.subdomains.join(', ')); 42 | }); 43 | 44 | var app = connect.createServer( 45 | connect.vhost('*.foo.com', server) 46 | , connect.vhost('foo.com', server) 47 | ); 48 | 49 | assert.response(app, 50 | { url: '/', headers: { Host: 'foo.com' }}, 51 | { body: 'from foo' }); 52 | 53 | assert.response(app, 54 | { url: '/', headers: { Host: 'tj.foo.com' }}, 55 | { body: 'from tj, foo' }); 56 | 57 | assert.response(app, 58 | { url: '/', headers: { Host: 'holowaychuk.tj.foo.com' }}, 59 | { body: 'from holowaychuk, tj, foo' }); 60 | } 61 | }; -------------------------------------------------------------------------------- /lib/middleware/limit.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - limit 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Limit request bodies to the given size in `bytes`. 10 | * 11 | * A string representation of the bytesize may also be passed, 12 | * for example "5mb", "200kb", "1gb", etc. 13 | * 14 | * Examples: 15 | * 16 | * var server = connect( 17 | * connect.limit('5.5mb') 18 | * ).listen(3000); 19 | * 20 | * TODO: pause EV_READ 21 | * 22 | * @param {Number|String} bytes 23 | * @return {Function} 24 | * @api public 25 | */ 26 | 27 | module.exports = function limit(bytes){ 28 | if ('string' == typeof bytes) bytes = parse(bytes); 29 | if ('number' != typeof bytes) throw new Error('limit() bytes required'); 30 | return function limit(req, res, next){ 31 | var received = 0 32 | , len = req.headers['content-length'] 33 | ? parseInt(req.headers['content-length'], 10) 34 | : null; 35 | 36 | // deny the request 37 | function deny() { 38 | req.destroy(); 39 | } 40 | 41 | // self-awareness 42 | if (req._limit) return next(); 43 | req._limit = true; 44 | 45 | // limit by content-length 46 | if (len && len > bytes) deny(); 47 | 48 | // limit 49 | req.on('data', function(chunk){ 50 | received += chunk.length; 51 | if (received > bytes) deny(); 52 | }); 53 | 54 | next(); 55 | }; 56 | }; 57 | 58 | /** 59 | * Parse byte `size` string. 60 | * 61 | * @param {String} size 62 | * @return {Number} 63 | * @api private 64 | */ 65 | 66 | function parse(size) { 67 | var parts = size.match(/^(\d+(?:\.\d+)?) *(kb|mb|gb)$/) 68 | , n = parseFloat(parts[1]) 69 | , type = parts[2]; 70 | 71 | var map = { 72 | kb: 1024 73 | , mb: 1024 * 1024 74 | , gb: 1024 * 1024 * 1024 75 | }; 76 | 77 | return map[type] * n; 78 | } -------------------------------------------------------------------------------- /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 cache. 18 | */ 19 | 20 | var icon; 21 | 22 | /** 23 | * By default serves the connect favicon, or the favicon 24 | * located by the given `path`. 25 | * 26 | * Options: 27 | * 28 | * - `maxAge` cache-control max-age directive, defaulting to 1 day 29 | * 30 | * Examples: 31 | * 32 | * connect.createServer( 33 | * connect.favicon() 34 | * ); 35 | * 36 | * connect.createServer( 37 | * connect.favicon(__dirname + '/public/favicon.ico') 38 | * ); 39 | * 40 | * @param {String} path 41 | * @param {Object} options 42 | * @return {Function} 43 | * @api public 44 | */ 45 | 46 | module.exports = function favicon(path, options){ 47 | var options = options || {} 48 | , path = path || __dirname + '/../public/favicon.ico' 49 | , maxAge = options.maxAge || 86400000; 50 | 51 | return function favicon(req, res, next){ 52 | if ('/favicon.ico' == req.url) { 53 | if (icon) { 54 | res.writeHead(200, icon.headers); 55 | res.end(icon.body); 56 | } else { 57 | fs.readFile(path, function(err, buf){ 58 | if (err) return next(err); 59 | icon = { 60 | headers: { 61 | 'Content-Type': 'image/x-icon' 62 | , 'Content-Length': buf.length 63 | , 'ETag': '"' + utils.md5(buf) + '"' 64 | , 'Cache-Control': 'public, max-age=' + (maxAge / 1000) 65 | }, 66 | body: buf 67 | }; 68 | res.writeHead(200, icon.headers); 69 | res.end(icon.body); 70 | }); 71 | } 72 | } else { 73 | next(); 74 | } 75 | }; 76 | }; -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Connect 2 | 3 | Connect is an extensible HTTP server framework for [node](http://nodejs.org), providing high performance "plugins" known as _middleware_. 4 | 5 | Connect is bundled with over _14_ commonly used middleware, including 6 | a logger, session support, cookie parser, and [more](http://senchalabs.github.com/connect). Be sure to view the 1.0 [documentation](http://senchalabs.github.com/connect/). 7 | 8 | ## Running Tests 9 | 10 | first: 11 | 12 | $ npm install -d 13 | 14 | then: 15 | 16 | $ make test 17 | 18 | ## Authors 19 | 20 | Below is the output from [git-summary](http://github.com/visionmedia/git-extras). 21 | 22 | project: connect 23 | commits: 1616 24 | files : 168 25 | authors: 26 | 1086 Tj Holowaychuk 27 | 298 visionmedia 28 | 191 Tim Caswell 29 | 8 Astro 30 | 5 Nathan Rajlich 31 | 4 Jakub Nešetřil 32 | 3 Alexander Simmerl 33 | 2 Jacques Crocker 34 | 2 Andreas Lind Petersen 35 | 2 Fabian Jakobs 36 | 2 Aaron Heckmann 37 | 2 James Campos 38 | 1 nateps 39 | 1 Gregory McWhirter 40 | 1 Adam Malcontenti-Wilson 41 | 1 Joshua Peek 42 | 1 Jxck 43 | 1 Eran Hammer-Lahav 44 | 1 TJ Holowaychuk 45 | 1 Bart Teeuwisse 46 | 1 Aseem Kishore 47 | 1 Guillermo Rauch 48 | 1 Jakub Nesetril 49 | 50 | 51 | ## Node Compatibility 52 | 53 | Connect `< 1.0.0` is compatible with node 0.2.x 54 | 55 | 56 | Connect `>= 1.0.0` is compatible with node 0.4.x 57 | 58 | ## CLA 59 | 60 | [http://code.google.com/legal/individual-cla-v1.0.html](http://code.google.com/legal/individual-cla-v1.0.html) 61 | 62 | ## License 63 | 64 | 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/). -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * # Connect 4 | * 5 | * Connect is a middleware framework for node, 6 | * shipping with over 11 bundled middleware and a rich choice of 7 | * [3rd-party middleware](https://github.com/senchalabs/connect/wiki). 8 | * 9 | * Installation: 10 | * 11 | * $ npm install connect 12 | * 13 | * API: 14 | * 15 | * - [connect](connect.html) general 16 | * - [http](http.html) http server 17 | * - [https](https.html) https server 18 | * 19 | * Middleware: 20 | * 21 | * - [logger](middleware-logger.html) request logger with custom format support 22 | * - [basicAuth](middleware-basicAuth.html) basic http authentication 23 | * - [bodyParser](middleware-bodyParser.html) extensible request body parser 24 | * - [cookieParser](middleware-cookieParser.html) cookie parser 25 | * - [session](middleware-session.html) session management support with bundled [MemoryStore](middleware-session-memory.html) 26 | * - [compiler](middleware-compiler.html) static asset compiler (sass, less, coffee-script, etc) 27 | * - [methodOverride](middleware-methodOverride.html) faux HTTP method support 28 | * - [responseTime](middleware-responseTime.html) calculates response-time and exposes via X-Response-Time 29 | * - [router](middleware-router.html) provides rich Sinatra / Express-like routing 30 | * - [static](middleware-static.html) streaming static file server supporting `Range` and more 31 | * - [directory](middleware-directory.html) directory listing middleware 32 | * - [vhost](middleware-vhost.html) virtual host sub-domain mapping middleware 33 | * - [favicon](middleware-favicon.html) efficient favicon server (with default icon) 34 | * - [limit](middleware-limit.html) limit the bytesize of request bodies 35 | * - [profiler](middleware-profiler.html) request profiler reporting response-time, memory usage, etc 36 | * - [query](middleware-query.html) automatic querystring parser, populating `req.query` 37 | * - [errorHandler](middleware-errorHandler.html) flexible error handler 38 | * 39 | * Internals: 40 | * 41 | * - connect [utilities](utils.html) 42 | * - node monkey [patches](patch.html) 43 | * 44 | */ -------------------------------------------------------------------------------- /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/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 qs = require('qs'); 14 | 15 | /** 16 | * Extract the mime type from the given request's 17 | * _Content-Type_ header. 18 | * 19 | * @param {IncomingMessage} req 20 | * @return {String} 21 | * @api private 22 | */ 23 | 24 | function mime(req) { 25 | var str = req.headers['content-type'] || ''; 26 | return str.split(';')[0]; 27 | } 28 | 29 | /** 30 | * Parse request bodies. 31 | * 32 | * By default _application/json_ and _application/x-www-form-urlencoded_ 33 | * are supported, however you may map `connect.bodyParser.parse[contentType]` 34 | * to a function of your choice to replace existing parsers, or implement 35 | * one for other content-types. 36 | * 37 | * Examples: 38 | * 39 | * connect.createServer( 40 | * connect.bodyParser() 41 | * , function(req, res) { 42 | * res.end('viewing user ' + req.body.user.name); 43 | * } 44 | * ); 45 | * 46 | * Since both _json_ and _x-www-form-urlencoded_ are supported by 47 | * default, either of the following requests would result in the response 48 | * of "viewing user tj". 49 | * 50 | * $ curl -d 'user[name]=tj' http://localhost/ 51 | * $ curl -d '{"user":{"name":"tj"}}' -H "Content-Type: application/json" http://localhost/ 52 | * 53 | * @return {Function} 54 | * @api public 55 | */ 56 | 57 | exports = module.exports = function bodyParser(){ 58 | return function bodyParser(req, res, next) { 59 | if ('GET' == req.method || 'HEAD' == req.method) return next(); 60 | var parser = exports.parse[mime(req)]; 61 | if (parser && !req.body) { 62 | var data = ''; 63 | req.setEncoding('utf8'); 64 | req.on('data', function(chunk) { data += chunk; }); 65 | req.on('end', function(){ 66 | req.rawBody = data; 67 | try { 68 | req.body = data 69 | ? parser(data) 70 | : {}; 71 | } catch (err) { 72 | return next(err); 73 | } 74 | next(); 75 | }); 76 | } else { 77 | next(); 78 | } 79 | } 80 | }; 81 | 82 | /** 83 | * Supported decoders. 84 | * 85 | * - application/x-www-form-urlencoded 86 | * - application/json 87 | */ 88 | 89 | exports.parse = { 90 | 'application/x-www-form-urlencoded': qs.parse 91 | , 'application/json': JSON.parse 92 | }; -------------------------------------------------------------------------------- /test/bodyParser.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect') 7 | , assert = require('assert') 8 | , http = require('http'); 9 | 10 | var app = connect.createServer( 11 | connect.bodyParser(), 12 | function(req, res){ 13 | res.writeHead(200); 14 | res.end(JSON.stringify(req.body)); 15 | }); 16 | 17 | module.exports = { 18 | 'test x-www-form-urlencoded body': function(){ 19 | assert.response(app, 20 | { url: '/' 21 | , data: 'foo=bar' 22 | , method: 'POST' 23 | , headers: { 'Content-Type': 'application/x-www-form-urlencoded' }}, 24 | { body: '{"foo":"bar"}' }); 25 | }, 26 | 27 | 'test PUT x-www-form-urlencoded body': function(){ 28 | assert.response(app, 29 | { url: '/' 30 | , data: 'foo=bar' 31 | , method: 'PUT' 32 | , headers: { 'Content-Type': 'application/x-www-form-urlencoded' }}, 33 | { body: '{"foo":"bar"}' }); 34 | }, 35 | 36 | 'test json body': function(){ 37 | assert.response(app, 38 | { url: '/' 39 | , data: '{"foo":"bar"}' 40 | , method: 'PUT' 41 | , headers: { 'Content-Type': 'application/json' }}, 42 | { body: '{"foo":"bar"}' }); 43 | }, 44 | 45 | 'test POST with no data': function(){ 46 | assert.response(app, 47 | { url: '/', method: 'POST' }, 48 | { body: '' }); 49 | }, 50 | 51 | 'test GET with content-type': function(){ 52 | assert.response(app, 53 | { url: '/', headers: { 'Content-Type': 'application/json' }}, 54 | { body: '' }); 55 | }, 56 | 57 | 'test custom parser': function(){ 58 | connect.bodyParser.parse['application/x-awesome'] = function(str){ 59 | var obj = {} 60 | , parts = str.split('.'); 61 | obj[parts.shift()] = parts.shift(); 62 | return obj; 63 | }; 64 | 65 | assert.response(app, 66 | { url: '/' 67 | , method: 'POST' 68 | , data: 'foo.bar' 69 | , headers: { 'Content-Type': 'application/x-awesome' }}, 70 | { body: '{"foo":"bar"}' }); 71 | }, 72 | 73 | 'test mount-safety': function(){ 74 | var app = connect( 75 | connect.bodyParser() 76 | , function(req, res){ 77 | res.end(req.body.name); 78 | } 79 | ); 80 | 81 | var app2 = connect(connect.bodyParser()); 82 | app2.use('/test', app); 83 | 84 | assert.response(app2, 85 | { url: '/test' 86 | , method: 'POST' 87 | , data: '{"name":"tj"}' 88 | , headers: { 'Content-Type': 'application/json' }}, 89 | { body: 'tj' }); 90 | } 91 | }; -------------------------------------------------------------------------------- /support/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% if ('index' == name) { %> 5 | Connect - middleware framework for nodejs 6 | <% } else { %> 7 | Connect - <%= name %> 8 | <% } %> 9 | 10 | 11 | 12 | 13 |
14 | <% if ('index' != name ){ %> 15 |

<%= name %>

16 | <%= filename %> 17 | <% } %> 18 | 19 | <% var n = 0; %> 20 | <% comments.forEach(function(comment){ %> 21 | <% if ('index' != name && (!comment.tags.length || comment.isPrivate)) return %> 22 | <% if (!comment.ignore) { %> 23 |
24 | <% if (n++ != 0) { %> 25 |

.<%= comment.method %>()

26 | <% } %> 27 |

<%- comment.description %>

28 | 29 | <% if (comment.body) { %> 30 |
31 | <%- comment.body %> 32 |
33 | <% } %> 34 | 35 | <% if (comment.tags.length) { %> 36 |
    37 | <% comment.tags.forEach(function(tag){ %> 38 | <% if ('return' == tag.type) { %> 39 |
  • returns <%= tag.types.join(', ') %>
  • 40 | <% } %> 41 | 42 | <% if ('api' == tag.type) { %> 43 |
  • api <%= tag.visibility %>
  • 44 | <% } %> 45 | 46 | <% if ('type' == tag.type) { %> 47 |
  • type <%= tag.value %>
  • 48 | <% } %> 49 | 50 | <% if ('param' == tag.type) { %> 51 |
  • param <%= tag.types.join(', ') %> <%= tag.name %> <%= tag.description %>
  • 52 | <% } %> 53 | <% }) %> 54 |
55 | 56 | 63 | <% }%> 64 |
65 | <% } %> 66 | <% }) %> 67 | <% if ('index' == name) { %> 68 | View Source 69 | <% } %> 70 |
71 | 72 | -------------------------------------------------------------------------------- /lib/middleware/profiler.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - profiler 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Profile the duration of a request. 10 | * 11 | * Typically this middleware should be utilized 12 | * _above_ all others, as it proxies the `res.end()` 13 | * method, being first allows it to encapsulate all 14 | * other middleware. 15 | * 16 | * Example Output: 17 | * 18 | * GET / 19 | * response time 2ms 20 | * memory rss 52.00kb 21 | * memory vsize 2.07mb 22 | * heap before 3.76mb / 8.15mb 23 | * heap after 3.80mb / 8.15mb 24 | * 25 | * @api public 26 | */ 27 | 28 | module.exports = function profiler(){ 29 | return function(req, res, next){ 30 | var end = res.end 31 | , start = snapshot(); 32 | 33 | // state snapshot 34 | function snapshot() { 35 | return { 36 | mem: process.memoryUsage() 37 | , time: new Date 38 | }; 39 | } 40 | 41 | // proxy res.end() 42 | res.end = function(data, encoding){ 43 | res.end = end; 44 | res.end(data, encoding); 45 | compare(req, start, snapshot()) 46 | }; 47 | 48 | next(); 49 | } 50 | }; 51 | 52 | /** 53 | * Compare `start` / `end` snapshots. 54 | * 55 | * @param {IncomingRequest} req 56 | * @param {Object} start 57 | * @param {Object} end 58 | * @api private 59 | */ 60 | 61 | function compare(req, start, end) { 62 | console.log(); 63 | row(req.method, req.url); 64 | row('response time:', (end.time - start.time) + 'ms'); 65 | row('memory rss:', formatBytes(end.mem.rss - start.mem.rss)); 66 | row('memory vsize:', formatBytes(end.mem.vsize - start.mem.vsize)); 67 | row('heap before:', formatBytes(start.mem.heapUsed) + ' / ' + formatBytes(start.mem.heapTotal)); 68 | row('heap after:', formatBytes(end.mem.heapUsed) + ' / ' + formatBytes(end.mem.heapTotal)); 69 | console.log(); 70 | } 71 | 72 | /** 73 | * Row helper 74 | * 75 | * @param {String} key 76 | * @param {String} val 77 | * @api private 78 | */ 79 | 80 | function row(key, val) { 81 | console.log(' \033[90m%s\033[0m \033[36m%s\033[0m', key, val); 82 | } 83 | 84 | /** 85 | * Format byte-size. 86 | * 87 | * @param {Number} bytes 88 | * @return {String} 89 | * @api private 90 | */ 91 | 92 | function formatBytes(bytes) { 93 | var kb = 1024 94 | , mb = 1024 * kb 95 | , gb = 1024 * mb; 96 | if (bytes < kb) return bytes + 'b'; 97 | if (bytes < mb) return (bytes / kb).toFixed(2) + 'kb'; 98 | if (bytes < gb) return (bytes / mb).toFixed(2) + 'mb'; 99 | return (bytes / gb).toFixed(2) + 'gb'; 100 | }; 101 | -------------------------------------------------------------------------------- /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 | 15 | /** 16 | * Initialize a new `Cookie` with the given `options`. 17 | * 18 | * @param {Object} options 19 | * @api private 20 | */ 21 | 22 | var Cookie = module.exports = function Cookie(options) { 23 | this.path = '/'; 24 | this.httpOnly = true; 25 | this.maxAge = 14400000; 26 | if (options) utils.merge(this, options); 27 | this.originalMaxAge = undefined == this.originalMaxAge 28 | ? this.maxAge 29 | : this.originalMaxAge; 30 | }; 31 | 32 | /** 33 | * Prototype. 34 | */ 35 | 36 | Cookie.prototype = { 37 | 38 | /** 39 | * Set expires `date`. 40 | * 41 | * @param {Date} date 42 | * @api public 43 | */ 44 | 45 | set expires(date) { 46 | this._expires = date; 47 | this.originalMaxAge = this.maxAge; 48 | }, 49 | 50 | /** 51 | * Get expires `date`. 52 | * 53 | * @return {Date} 54 | * @api public 55 | */ 56 | 57 | get expires() { 58 | return this._expires; 59 | }, 60 | 61 | /** 62 | * Set expires via max-age in `ms`. 63 | * 64 | * @param {Number} ms 65 | * @api public 66 | */ 67 | 68 | set maxAge(ms) { 69 | this.expires = 'number' == typeof ms 70 | ? new Date(Date.now() + ms) 71 | : ms; 72 | }, 73 | 74 | /** 75 | * Get expires max-age in `ms`. 76 | * 77 | * @return {Number} 78 | * @api public 79 | */ 80 | 81 | get maxAge() { 82 | return this.expires instanceof Date 83 | ? this.expires.valueOf() - Date.now() 84 | : this.expires; 85 | }, 86 | 87 | /** 88 | * Return cookie data object. 89 | * 90 | * @return {Object} 91 | * @api private 92 | */ 93 | 94 | get data() { 95 | return { 96 | originalMaxAge: this.originalMaxAge 97 | , expires: this._expires 98 | , secure: this.secure 99 | , httpOnly: this.httpOnly 100 | , domain: this.domain 101 | , path: this.path 102 | } 103 | }, 104 | 105 | /** 106 | * Return a serialized cookie string. 107 | * 108 | * @return {String} 109 | * @api public 110 | */ 111 | 112 | serialize: function(name, val){ 113 | return utils.serializeCookie(name, val, this.data); 114 | }, 115 | 116 | /** 117 | * Return JSON representation of this cookie. 118 | * 119 | * @return {Object} 120 | * @api private 121 | */ 122 | 123 | toJSON: function(){ 124 | return this.data; 125 | } 126 | }; 127 | -------------------------------------------------------------------------------- /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 | , badRequest = utils.badRequest; 16 | 17 | /** 18 | * Enfore basic authentication by providing a `callback(user, pass)`, 19 | * which must return `true` in order to gain access. Alternatively an async 20 | * method is provided as well, invoking `callback(user, pass, callback)`. Populates 21 | * `req.remoteUser`. The final alternative is simply passing username / password 22 | * strings. 23 | * 24 | * Examples: 25 | * 26 | * connect(connect.basicAuth('username', 'password')); 27 | * 28 | * connect( 29 | * connect.basicAuth(function(user, pass){ 30 | * return 'tj' == user & 'wahoo' == pass; 31 | * }) 32 | * ); 33 | * 34 | * connect( 35 | * connect.basicAuth(function(user, pass, fn){ 36 | * User.authenticate({ user: user, pass: pass }, fn); 37 | * }) 38 | * ); 39 | * 40 | * @param {Function|String} callback or username 41 | * @param {String} realm 42 | * @api public 43 | */ 44 | 45 | module.exports = function basicAuth(callback, realm) { 46 | var username, password; 47 | 48 | // user / pass strings 49 | if ('string' == typeof callback) { 50 | username = callback; 51 | password = realm; 52 | if ('string' != typeof password) throw new Error('password argument required'); 53 | realm = arguments[2]; 54 | callback = function(user, pass){ 55 | return user == username && pass == password; 56 | } 57 | } 58 | 59 | realm = realm || 'Authorization Required'; 60 | 61 | return function(req, res, next) { 62 | var authorization = req.headers.authorization; 63 | 64 | if (req.remoteUser) return next(); 65 | if (!authorization) return unauthorized(res, realm); 66 | 67 | var parts = authorization.split(' ') 68 | , scheme = parts[0] 69 | , credentials = new Buffer(parts[1], 'base64').toString().split(':'); 70 | 71 | if ('Basic' != scheme) return badRequest(res); 72 | 73 | // async 74 | if (callback.length >= 3) { 75 | var pause = utils.pause(req); 76 | callback(credentials[0], credentials[1], function(err, user){ 77 | if (err || !user) return unauthorized(res, realm); 78 | req.remoteUser = user; 79 | next(); 80 | pause.resume(); 81 | }); 82 | // sync 83 | } else { 84 | if (callback(credentials[0], credentials[1])) { 85 | req.remoteUser = credentials[0]; 86 | next(); 87 | } else { 88 | unauthorized(res, realm); 89 | } 90 | } 91 | } 92 | }; 93 | 94 | -------------------------------------------------------------------------------- /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 HTTPServer = require('./http').Server 14 | , HTTPSServer = require('./https').Server 15 | , fs = require('fs'); 16 | 17 | // node patches 18 | 19 | require('./patch'); 20 | 21 | // expose createServer() as the module 22 | 23 | exports = module.exports = createServer; 24 | 25 | /** 26 | * Framework version. 27 | */ 28 | 29 | exports.version = '1.5.1'; 30 | 31 | /** 32 | * Initialize a new `connect.HTTPServer` with the middleware 33 | * passed to this function. When an object is passed _first_, 34 | * we assume these are the tls options, and return a `connect.HTTPSServer`. 35 | * 36 | * Examples: 37 | * 38 | * An example HTTP server, accepting several middleware. 39 | * 40 | * var server = connect.createServer( 41 | * connect.logger() 42 | * , connect.static(__dirname + '/public') 43 | * ); 44 | * 45 | * An HTTPS server, utilizing the same middleware as above. 46 | * 47 | * var server = connect.createServer( 48 | * { key: key, cert: cert } 49 | * , connect.logger() 50 | * , connect.static(__dirname + '/public') 51 | * ); 52 | * 53 | * Alternatively with connect 1.0 we may omit `createServer()`. 54 | * 55 | * connect( 56 | * connect.logger() 57 | * , connect.static(__dirname + '/public') 58 | * ).listen(3000); 59 | * 60 | * @param {Object|Function} ... 61 | * @return {Server} 62 | * @api public 63 | */ 64 | 65 | function createServer() { 66 | if ('object' == typeof arguments[0]) { 67 | return new HTTPSServer(arguments[0], Array.prototype.slice.call(arguments, 1)); 68 | } else { 69 | return new HTTPServer(Array.prototype.slice.call(arguments)); 70 | } 71 | }; 72 | 73 | // support connect.createServer() 74 | 75 | exports.createServer = createServer; 76 | 77 | // auto-load getters 78 | 79 | exports.middleware = {}; 80 | 81 | /** 82 | * Auto-load bundled middleware with getters. 83 | */ 84 | 85 | fs.readdirSync(__dirname + '/middleware').forEach(function(filename){ 86 | if (/\.js$/.test(filename)) { 87 | var name = filename.substr(0, filename.lastIndexOf('.')); 88 | exports.middleware.__defineGetter__(name, function(){ 89 | return require('./middleware/' + name); 90 | }); 91 | } 92 | }); 93 | 94 | // expose utils 95 | 96 | exports.utils = require('./utils'); 97 | 98 | // expose getters as first-class exports 99 | 100 | exports.utils.merge(exports, exports.middleware); 101 | 102 | // expose constructors 103 | 104 | exports.HTTPServer = HTTPServer; 105 | exports.HTTPSServer = HTTPSServer; 106 | 107 | -------------------------------------------------------------------------------- /test/basicAuth.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect') 7 | , assert = require('assert') 8 | , http = require('http'); 9 | 10 | // sync 11 | 12 | var app = connect( 13 | connect.basicAuth(function(user, pass){ 14 | return 'tj' == user && 'tobi' == pass; 15 | }), 16 | function(req, res){ 17 | res.end('wahoo'); 18 | } 19 | ); 20 | 21 | // async 22 | 23 | var User = { 24 | authenticate: function(query, fn){ 25 | if (query.name == 'tj' && query.pass == 'tobi') { 26 | fn(null, { name: 'tj' }); 27 | } else { 28 | fn(new Error('user not found')); 29 | } 30 | } 31 | } 32 | 33 | var async = connect( 34 | connect.basicAuth(function(user, pass, fn){ 35 | User.authenticate({ name: user, pass: pass }, fn); 36 | }), 37 | function(req, res){ 38 | res.end('wahoo'); 39 | } 40 | ); 41 | 42 | module.exports = { 43 | 'test user / pass options': function(){ 44 | var app = connect( 45 | connect.basicAuth('tj', 'tobi'), 46 | function(req, res){ 47 | res.send('wahoo'); 48 | } 49 | ); 50 | 51 | assert.response(app, 52 | { url: '/' }, 53 | { body: 'Unauthorized' }); 54 | 55 | assert.response(app, 56 | { url: '/', headers: { Authorization: 'Basic dGo6dG9iaQo=' }}, 57 | { body: 'Unauthorized' }); 58 | }, 59 | 60 | 'test missing Authorization field': function(){ 61 | assert.response(app, 62 | { url: '/' }, 63 | { body: 'Unauthorized' 64 | , status: 401 65 | , headers: { 66 | 'WWW-Authenticate': 'Basic realm="Authorization Required"' 67 | }}); 68 | }, 69 | 70 | 'test authorized': function(){ 71 | assert.response(app, 72 | { url: '/', headers: { Authorization: 'Basic dGo6dG9iaQ==' }}, 73 | { body: 'wahoo', status: 200 }); 74 | }, 75 | 76 | 'test unauthorized': function(){ 77 | assert.response(app, 78 | { url: '/', headers: { Authorization: 'Basic dasdfasdfas' }}, 79 | { body: 'Unauthorized', status: 401 }); 80 | }, 81 | 82 | 'test bad request': function(){ 83 | assert.response(app, 84 | { url: '/', headers: { Authorization: 'Foo asdfasdf' }}, 85 | { body: 'Bad Request', status: 400 }); 86 | }, 87 | 88 | 'test async authorized': function(){ 89 | assert.response(async, 90 | { url: '/', headers: { Authorization: 'Basic dGo6dG9iaQ==' }}, 91 | { body: 'wahoo', status: 200 }); 92 | }, 93 | 94 | 'test async unauthorized': function(){ 95 | assert.response(async, 96 | { url: '/', headers: { Authorization: 'Basic dasdfasdfas' }}, 97 | { body: 'Unauthorized', status: 401 }); 98 | }, 99 | 100 | 'test async bad request': function(){ 101 | assert.response(async, 102 | { url: '/', headers: { Authorization: 'Foo asdfasdf' }}, 103 | { body: 'Bad Request', status: 400 }); 104 | }, 105 | }; -------------------------------------------------------------------------------- /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 | , utils = require('../../utils') 15 | , Session = require('./session'); 16 | 17 | /** 18 | * Initialize a new `MemoryStore`. 19 | * 20 | * @api public 21 | */ 22 | 23 | var MemoryStore = module.exports = function MemoryStore() { 24 | this.sessions = {}; 25 | }; 26 | 27 | /** 28 | * Inherit from `Store.prototype`. 29 | */ 30 | 31 | MemoryStore.prototype.__proto__ = Store.prototype; 32 | 33 | /** 34 | * Attempt to fetch session by the given `sid`. 35 | * 36 | * @param {String} sid 37 | * @param {Function} fn 38 | * @api public 39 | */ 40 | 41 | MemoryStore.prototype.get = function(sid, fn){ 42 | var self = this; 43 | process.nextTick(function(){ 44 | var expires 45 | , sess = self.sessions[sid]; 46 | if (sess) { 47 | sess = JSON.parse(sess); 48 | expires = 'string' == typeof sess.cookie.expires 49 | ? new Date(sess.cookie.expires) 50 | : sess.cookie.expires; 51 | if (!expires || new Date < expires) { 52 | fn(null, sess); 53 | } else { 54 | self.destroy(sid, fn); 55 | } 56 | } else { 57 | fn(); 58 | } 59 | }); 60 | }; 61 | 62 | /** 63 | * Commit the given `sess` object associated with the given `sid`. 64 | * 65 | * @param {String} sid 66 | * @param {Session} sess 67 | * @param {Function} fn 68 | * @api public 69 | */ 70 | 71 | MemoryStore.prototype.set = function(sid, sess, fn){ 72 | var self = this; 73 | process.nextTick(function(){ 74 | self.sessions[sid] = JSON.stringify(sess); 75 | fn && fn(); 76 | }); 77 | }; 78 | 79 | /** 80 | * Destroy the session associated with the given `sid`. 81 | * 82 | * @param {String} sid 83 | * @api public 84 | */ 85 | 86 | MemoryStore.prototype.destroy = function(sid, fn){ 87 | var self = this; 88 | process.nextTick(function(){ 89 | delete self.sessions[sid]; 90 | fn && fn(); 91 | }); 92 | }; 93 | 94 | /** 95 | * Invoke the given callback `fn` with all active sessions. 96 | * 97 | * @param {Function} fn 98 | * @api public 99 | */ 100 | 101 | MemoryStore.prototype.all = function(fn){ 102 | var arr = [] 103 | , keys = Object.keys(this.sessions); 104 | for (var i = 0, len = keys.length; i < len; ++i) { 105 | arr.push(this.sessions[keys[i]]); 106 | } 107 | fn(null, arr); 108 | }; 109 | 110 | /** 111 | * Clear all sessions. 112 | * 113 | * @param {Function} fn 114 | * @api public 115 | */ 116 | 117 | MemoryStore.prototype.clear = function(fn){ 118 | this.sessions = {}; 119 | fn && fn(); 120 | }; 121 | 122 | /** 123 | * Fetch number of sessions. 124 | * 125 | * @param {Function} fn 126 | * @api public 127 | */ 128 | 129 | MemoryStore.prototype.length = function(fn){ 130 | fn(null, Object.keys(this.sessions).length); 131 | }; 132 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var utils = require('connect').utils 7 | , should = require('should') 8 | , Stream = require('stream').Stream; 9 | 10 | module.exports = { 11 | 'test md5()': function(){ 12 | utils.md5('wahoo', 'base64').should.equal('5JMpgGF2EjbJawLqaqiirQ=='); 13 | utils.md5('wahoo').should.equal('e493298061761236c96b02ea6aa8a2ad'); 14 | }, 15 | 16 | 'test uid()': function(){ 17 | for(var i = 0; i < 100; ++i){ 18 | utils.uid(i).should.have.length(i); 19 | } 20 | }, 21 | 22 | 'test parseCookie()': function(){ 23 | utils.parseCookie('foo=bar').should.eql({ foo: 'bar' }); 24 | utils.parseCookie('SID=123').should.eql({ sid: '123' }); 25 | 26 | utils.parseCookie('foo = bar; baz = raz') 27 | .should.eql({ foo: 'bar', baz: 'raz' }); 28 | 29 | utils.parseCookie('fbs="uid=0987654321&name=Test+User"') 30 | .should.eql({ fbs: 'uid=0987654321&name=Test User' }); 31 | 32 | utils.parseCookie('email=tobi%2Bferret@foo.com') 33 | .should.eql({ email: 'tobi+ferret@foo.com' }); 34 | }, 35 | 36 | 'test serializeCookie()': function(){ 37 | utils 38 | .serializeCookie('foo', 'bar', { path: '/' }) 39 | .should.equal('foo=bar; path=/'); 40 | 41 | utils 42 | .serializeCookie('foo', 'bar', { secure: true }) 43 | .should.equal('foo=bar; secure'); 44 | 45 | utils 46 | .serializeCookie('foo', 'bar', { secure: false }) 47 | .should.equal('foo=bar'); 48 | 49 | utils 50 | .serializeCookie('foo', 'foo bar') 51 | .should.equal('foo=foo%20bar'); 52 | 53 | utils.parseCookie(utils.serializeCookie('fbs', 'uid=123&name=Test User')) 54 | .should.eql({ fbs: 'uid=123&name=Test User' }); 55 | }, 56 | 57 | 'test pause()': function(defer){ 58 | var calls = 0 59 | , data = [] 60 | , req = new Stream; 61 | 62 | req.write = function(data){ 63 | this.emit('data', data); 64 | }; 65 | req.end = function(){ 66 | this.emit('end'); 67 | }; 68 | 69 | var pause = utils.pause(req); 70 | 71 | req.write('one'); 72 | req.write('two'); 73 | req.end(); 74 | 75 | req.on('data', function(chunk){ 76 | ++calls; 77 | data.push(chunk); 78 | }); 79 | req.on('end', function(){ 80 | ++calls; 81 | data.should.have.length(2); 82 | }); 83 | 84 | pause.resume(); 85 | 86 | defer(function(){ 87 | calls.should.equal(3); 88 | }); 89 | }, 90 | 91 | 'test .parseRange()': function(){ 92 | utils.parseRange(1000, 'bytes=0-499').should.eql([{ start: 0, end: 499 }]); 93 | utils.parseRange(1000, 'bytes=40-80').should.eql([{ start: 40, end: 80 }]); 94 | utils.parseRange(1000, 'bytes=-500').should.eql([{ start: 500, end: 999 }]); 95 | utils.parseRange(1000, 'bytes=-400').should.eql([{ start: 600, end: 999 }]); 96 | utils.parseRange(1000, 'bytes=500-').should.eql([{ start: 500, end: 999 }]); 97 | utils.parseRange(1000, 'bytes=400-').should.eql([{ start: 400, end: 999 }]); 98 | utils.parseRange(1000, 'bytes=0-0').should.eql([{ start: 0, end: 0 }]); 99 | utils.parseRange(1000, 'bytes=-1').should.eql([{ start: 999, end: 999 }]); 100 | } 101 | }; -------------------------------------------------------------------------------- /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 | , url = require('url') 14 | , fs = require('fs'); 15 | 16 | /** 17 | * Flexible error handler, providing (_optional_) stack traces 18 | * and error message responses for requests accepting text, html, 19 | * or json. 20 | * 21 | * Options: 22 | * 23 | * - `showStack`, `stack` respond with both the error message and stack trace. Defaults to `false` 24 | * - `showMessage`, `message`, respond with the exception message only. Defaults to `false` 25 | * - `dumpExceptions`, `dump`, dump exceptions to stderr (without terminating the process). Defaults to `false` 26 | * 27 | * Text: 28 | * 29 | * By default, and when _text/plain_ is accepted a simple stack trace 30 | * or error message will be returned. 31 | * 32 | * JSON: 33 | * 34 | * When _application/json_ is accepted, connect will respond with 35 | * an object in the form of `{ "error": error }`. 36 | * 37 | * HTML: 38 | * 39 | * When accepted connect will output a nice html stack trace. 40 | * 41 | * @param {Object} options 42 | * @return {Function} 43 | * @api public 44 | */ 45 | 46 | exports = module.exports = function errorHandler(options){ 47 | options = options || {}; 48 | 49 | // defaults 50 | var showStack = options.showStack || options.stack 51 | , showMessage = options.showMessage || options.message 52 | , dumpExceptions = options.dumpExceptions || options.dump 53 | , formatUrl = options.formatUrl; 54 | 55 | return function errorHandler(err, req, res, next){ 56 | res.statusCode = 500; 57 | if (dumpExceptions) console.error(err.stack); 58 | if (showStack) { 59 | var accept = req.headers.accept || ''; 60 | // html 61 | if (~accept.indexOf('html')) { 62 | fs.readFile(__dirname + '/../public/style.css', 'utf8', function(e, style){ 63 | fs.readFile(__dirname + '/../public/error.html', 'utf8', function(e, html){ 64 | var stack = (err.stack || '') 65 | .split('\n').slice(1) 66 | .map(function(v){ return '
  • ' + v + '
  • '; }).join(''); 67 | html = html 68 | .replace('{style}', style) 69 | .replace('{stack}', stack) 70 | .replace('{title}', exports.title) 71 | .replace(/\{error\}/g, utils.escape(err.toString())); 72 | res.setHeader('Content-Type', 'text/html'); 73 | res.end(html); 74 | }); 75 | }); 76 | // json 77 | } else if (~accept.indexOf('json')) { 78 | var json = JSON.stringify({ error: err }); 79 | res.setHeader('Content-Type', 'application/json'); 80 | res.end(json); 81 | // plain text 82 | } else { 83 | res.writeHead(500, { 'Content-Type': 'text/plain' }); 84 | res.end(err.stack); 85 | } 86 | } else { 87 | var body = showMessage 88 | ? err.toString() 89 | : 'Internal Server Error'; 90 | res.setHeader('Content-Type', 'text/plain'); 91 | res.end(body); 92 | } 93 | }; 94 | }; 95 | 96 | /** 97 | * Template title. 98 | */ 99 | 100 | exports.title = 'Connect'; -------------------------------------------------------------------------------- /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/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 | , Cookie = require('./cookie'); 15 | 16 | /** 17 | * Create a new `Session` with the given request and `data`. 18 | * 19 | * @param {IncomingRequest} req 20 | * @param {Object} data 21 | * @api private 22 | */ 23 | 24 | var Session = module.exports = function Session(req, data) { 25 | Object.defineProperty(this, 'req', { value: req }); 26 | Object.defineProperty(this, 'id', { value: req.sessionID }); 27 | if ('object' == typeof data) { 28 | utils.merge(this, data); 29 | } else { 30 | this.lastAccess = Date.now(); 31 | } 32 | }; 33 | 34 | /** 35 | * Update `.lastAccess` timestamp, 36 | * and reset `.cookie.maxAge` to prevent 37 | * the cookie from expiring when the 38 | * session is still active. 39 | * 40 | * @return {Session} for chaining 41 | * @api public 42 | */ 43 | 44 | Session.prototype.touch = function(){ 45 | return this 46 | .resetLastAccess() 47 | .resetMaxAge(); 48 | }; 49 | 50 | /** 51 | * Update `.lastAccess` timestamp. 52 | * 53 | * @return {Session} for chaining 54 | * @api public 55 | */ 56 | 57 | Session.prototype.resetLastAccess = function(){ 58 | this.lastAccess = Date.now(); 59 | return this; 60 | }; 61 | 62 | /** 63 | * Reset `.maxAge` to `.originalMaxAge`. 64 | * 65 | * @return {Session} for chaining 66 | * @api public 67 | */ 68 | 69 | Session.prototype.resetMaxAge = function(){ 70 | this.cookie.maxAge = this.cookie.originalMaxAge; 71 | return this; 72 | }; 73 | 74 | /** 75 | * Save the session data with optional callback `fn(err)`. 76 | * 77 | * @param {Function} fn 78 | * @return {Session} for chaining 79 | * @api public 80 | */ 81 | 82 | Session.prototype.save = function(fn){ 83 | this.req.sessionStore.set(this.id, this, fn || function(){}); 84 | return this; 85 | }; 86 | 87 | /** 88 | * Re-loads the session data _without_ altering 89 | * the maxAge or lastAccess properties. Invokes the 90 | * callback `fn(err)`, after which time if no exception 91 | * has occurred the `req.session` property will be 92 | * a new `Session` object, although representing the 93 | * same session. 94 | * 95 | * @param {Function} fn 96 | * @return {Session} for chaining 97 | * @api public 98 | */ 99 | 100 | Session.prototype.reload = function(fn){ 101 | var req = this.req 102 | , store = this.req.sessionStore; 103 | store.get(this.id, function(err, sess){ 104 | if (err) return fn(err); 105 | if (!sess) return fn(new Error('failed to load session')); 106 | store.createSession(req, sess); 107 | fn(); 108 | }); 109 | return this; 110 | }; 111 | 112 | /** 113 | * Destroy `this` session. 114 | * 115 | * @param {Function} fn 116 | * @return {Session} for chaining 117 | * @api public 118 | */ 119 | 120 | Session.prototype.destroy = function(fn){ 121 | delete this.req.session; 122 | this.req.sessionStore.destroy(this.id, fn); 123 | return this; 124 | }; 125 | 126 | /** 127 | * Regenerate this request's session. 128 | * 129 | * @param {Function} fn 130 | * @return {Session} for chaining 131 | * @api public 132 | */ 133 | 134 | Session.prototype.regenerate = function(fn){ 135 | this.req.sessionStore.regenerate(this.req, fn); 136 | return this; 137 | }; 138 | -------------------------------------------------------------------------------- /docs/main.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | padding: 100px; 4 | font: 14px/1.4 'helvetica neue', helvetica, arial, sans-serif; 5 | } 6 | 7 | body.subpage { 8 | text-align: center; 9 | } 10 | 11 | a { 12 | text-decoration: none; 13 | color: #3b88d8; 14 | } 15 | 16 | a:hover { 17 | text-decoration: underline; 18 | } 19 | 20 | p.description { 21 | display: block; 22 | width: 100%; 23 | } 24 | 25 | h1 { font-size: 28px; } 26 | h2 { font-size: 20px; } 27 | 28 | pre { 29 | margin: 20px 0 10px 20px; 30 | background: #fefefe; 31 | padding: 20px 10px; 32 | -webkit-box-shadow: inset 0 0 2px rgba(0,0,0,0.2); 33 | } 34 | 35 | ul { 36 | padding: 0 15px; 37 | } 38 | 39 | ul li { 40 | margin: 0 20px; 41 | padding: 3px 0; 42 | } 43 | 44 | ul li code, 45 | p code { 46 | color: #3b88d8; 47 | } 48 | 49 | ul.tags { 50 | list-style: none; 51 | padding: 0; 52 | } 53 | 54 | ul.tags li { 55 | margin: 0; 56 | } 57 | 58 | ul.tags li em { 59 | font-weight: bold; 60 | } 61 | 62 | ul.tags li .types { 63 | color: #3b88d8; 64 | } 65 | 66 | ul.tags li.api { 67 | display: none; 68 | } 69 | 70 | .filename { 71 | margin-top: -40px; 72 | display: block; 73 | float: right; 74 | font-style: italic; 75 | font-size: 12px; 76 | color: #ccc; 77 | } 78 | 79 | .button { 80 | -webkit-user-select: none; 81 | display: block; 82 | background: #3b88d8; 83 | background: -moz-linear-gradient(0% 100% 90deg, #377ad0, #52a8e8); 84 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#52a8e8), to(#377ad0)); 85 | border-top: 1px solid #4081af; 86 | border-right: 1px solid #2e69a3; 87 | border-bottom: 1px solid #20559a; 88 | border-left: 1px solid #2e69a3; 89 | -moz-border-radius: 16px; 90 | -webkit-border-radius: 16px; 91 | border-radius: 16px; 92 | color: #fff; 93 | font-family: "lucida grande", sans-serif; 94 | font-size: 11px; 95 | font-weight: normal; 96 | line-height: 1; 97 | padding: 3px 0 5px 0; 98 | text-align: center; 99 | text-shadow: 0 -1px 1px #3275bc; 100 | width: 112px; 101 | -webkit-background-clip: padding-box; 102 | } 103 | 104 | .button:hover { 105 | background: #2a81d7; 106 | background: -moz-linear-gradient(0% 100% 90deg, #206bcb, #3e9ee5); 107 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#3e9ee5), to(#206bcb)); 108 | border-top: 1px solid #2a73a6; 109 | border-right: 1px solid #165899; 110 | border-bottom: 1px solid #07428f; 111 | border-left: 1px solid #165899; 112 | cursor: pointer; 113 | text-shadow: 0 -1px 1px #1d62ab; 114 | -webkit-background-clip: padding-box; 115 | text-decoration: none; 116 | } 117 | 118 | .button:active { 119 | background: #3282d3; 120 | border: 1px solid #154c8c; 121 | border-bottom: 1px solid #0e408e; 122 | text-shadow: 0 -1px 1px #2361a4; 123 | -webkit-background-clip: padding-box; 124 | } 125 | 126 | button[disabled].download-itunes, 127 | button[disabled].download-itunes:hover, 128 | button[disabled].download-itunes:active { 129 | background: #999; 130 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#dadada), to(#f3f3f3)); 131 | border-top: 1px solid #c5c5c5; 132 | border-right: 1px solid #cecece; 133 | border-bottom: 1px solid #d9d9d9; 134 | border-left: 1px solid #cecece; 135 | color: #8f8f8f; 136 | cursor: not-allowed; 137 | text-shadow: 0 -1px 1px #ebebeb; 138 | } 139 | 140 | .button::-moz-focus-inner { 141 | border: 0; 142 | padding: 0; 143 | } 144 | 145 | .comment { 146 | margin-bottom: 80px; 147 | } 148 | 149 | #content { 150 | width: 600px; 151 | } 152 | 153 | pre { 154 | overflow-x: auto; 155 | } 156 | 157 | code .keyword { font-weight: bold; } 158 | code .comment { color: #999; } 159 | code .string { color: #3b88d8; } 160 | code .class { color: #3b88d8; font-weight: bold; } 161 | code .variable { color: #424242; } -------------------------------------------------------------------------------- /test/errorHandler.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect') 7 | , assert = require('assert') 8 | , should = require('should') 9 | , http = require('http'); 10 | 11 | module.exports = { 12 | 'test defaults with next(err)': function(){ 13 | var app = connect.createServer( 14 | function(req, res, next){ 15 | next(new Error('keyboard cat!')); 16 | }, 17 | connect.errorHandler() 18 | ); 19 | 20 | assert.response(app, 21 | { url: '/' }, 22 | { body: 'Internal Server Error' 23 | , status: 500 }); 24 | }, 25 | 26 | 'test defaults with caught exception': function(){ 27 | var app = connect.createServer( 28 | function(req, res, next){ 29 | throw new Error('keyboard cat!'); 30 | }, 31 | connect.errorHandler() 32 | ); 33 | 34 | assert.response(app, 35 | { url: '/' }, 36 | { body: 'Internal Server Error' 37 | , status: 500 }); 38 | }, 39 | 40 | 'test showMessage': function(){ 41 | var app = connect.createServer( 42 | function(req, res, next){ 43 | next(new Error('keyboard cat!')); 44 | }, 45 | connect.errorHandler({ showMessage: true }) 46 | ); 47 | 48 | assert.response(app, 49 | { url: '/' }, 50 | { body: 'Error: keyboard cat!' 51 | , status: 500 }); 52 | }, 53 | 54 | 'test message': function(){ 55 | var app = connect.createServer( 56 | function(req, res, next){ 57 | next(new Error('keyboard cat!')); 58 | }, 59 | connect.errorHandler({ message: true }) 60 | ); 61 | 62 | assert.response(app, 63 | { url: '/' }, 64 | { body: 'Error: keyboard cat!' 65 | , status: 500 }); 66 | }, 67 | 68 | 'test showStack': function(){ 69 | var app = connect.createServer( 70 | function(req, res, next){ 71 | next(new Error('keyboard cat!')); 72 | }, 73 | connect.errorHandler({ showStack: true }) 74 | ); 75 | 76 | assert.response(app, 77 | { url: '/' }, 78 | function(res){ 79 | var buf = ''; 80 | res.body.should.include.string('Error: keyboard cat!'); 81 | res.body.should.include.string('test/errorHandler.test.js'); 82 | }); 83 | }, 84 | 85 | 'test stack': function(){ 86 | var app = connect.createServer( 87 | function(req, res, next){ 88 | next(new Error('keyboard cat!')); 89 | }, 90 | connect.errorHandler({ stack: true }) 91 | ); 92 | 93 | assert.response(app, 94 | { url: '/' }, 95 | function(res){ 96 | var buf = ''; 97 | res.body.should.include.string('Error: keyboard cat!'); 98 | res.body.should.include.string('test/errorHandler.test.js'); 99 | }); 100 | }, 101 | 102 | 'test showStack html': function(){ 103 | var app = connect.createServer( 104 | function(req, res, next){ 105 | next(new Error('keyboard cat!')); 106 | }, 107 | connect.errorHandler({ stack: true }) 108 | ); 109 | 110 | assert.response(app, 111 | { url: '/', headers: { Accept: 'text/html, application/json' }}, 112 | { status: 500 113 | , headers: { 'Content-Type': 'text/html' }}); 114 | }, 115 | 116 | 'test showStack json': function(){ 117 | var app = connect.createServer( 118 | function(req, res, next){ 119 | next(new Error('keyboard cat!')); 120 | }, 121 | connect.errorHandler({ stack: true }) 122 | ); 123 | 124 | assert.response(app, 125 | { url: '/', headers: { Accept: 'application/json' }}, 126 | { status: 500 127 | , headers: { 'Content-Type': 'application/json' }}); 128 | }, 129 | 130 | 'test showStack text': function(){ 131 | var app = connect.createServer( 132 | function(req, res, next){ 133 | next(new Error('keyboard cat!')); 134 | }, 135 | connect.errorHandler({ stack: true }) 136 | ); 137 | 138 | assert.response(app, 139 | { url: '/', headers: { Accept: 'text/plain' }}, 140 | { status: 500 141 | , headers: { 'Content-Type': 'text/plain' }}); 142 | } 143 | } -------------------------------------------------------------------------------- /lib/middleware/compiler.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - compiler 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 | , path = require('path') 15 | , parse = require('url').parse; 16 | 17 | /** 18 | * Require cache. 19 | */ 20 | 21 | var cache = {}; 22 | 23 | /** 24 | * Setup compiler. 25 | * 26 | * Options: 27 | * 28 | * - `src` Source directory, defaults to **CWD**. 29 | * - `dest` Destination directory, defaults `src`. 30 | * - `enable` Array of enabled compilers. 31 | * 32 | * Compilers: 33 | * 34 | * - `sass` Compiles cass to css 35 | * - `less` Compiles less to css 36 | * - `coffeescript` Compiles coffee to js 37 | * 38 | * @param {Object} options 39 | * @api public 40 | */ 41 | 42 | exports = module.exports = function compiler(options){ 43 | options = options || {}; 44 | 45 | var srcDir = options.src || process.cwd() 46 | , destDir = options.dest || srcDir 47 | , enable = options.enable; 48 | 49 | if (!enable || enable.length === 0) { 50 | throw new Error('compiler\'s "enable" option is not set, nothing will be compiled.'); 51 | } 52 | 53 | return function compiler(req, res, next){ 54 | if ('GET' != req.method) return next(); 55 | var pathname = parse(req.url).pathname; 56 | for (var i = 0, len = enable.length; i < len; ++i) { 57 | var name = enable[i] 58 | , compiler = compilers[name]; 59 | if (compiler.match.test(pathname)) { 60 | var src = (srcDir + pathname).replace(compiler.match, compiler.ext) 61 | , dest = destDir + pathname; 62 | 63 | // Compare mtimes 64 | fs.stat(src, function(err, srcStats){ 65 | if (err) { 66 | if ('ENOENT' == err.code) { 67 | next(); 68 | } else { 69 | next(err); 70 | } 71 | } else { 72 | fs.stat(dest, function(err, destStats){ 73 | if (err) { 74 | // Oh snap! it does not exist, compile it 75 | if ('ENOENT' == err.code) { 76 | compile(); 77 | } else { 78 | next(err); 79 | } 80 | } else { 81 | // Source has changed, compile it 82 | if (srcStats.mtime > destStats.mtime) { 83 | compile(); 84 | } else { 85 | // Defer file serving 86 | next(); 87 | } 88 | } 89 | }); 90 | } 91 | }); 92 | 93 | // Compile to the destination 94 | function compile() { 95 | fs.readFile(src, 'utf8', function(err, str){ 96 | if (err) { 97 | next(err); 98 | } else { 99 | compiler.compile(str, function(err, str){ 100 | if (err) { 101 | next(err); 102 | } else { 103 | fs.writeFile(dest, str, 'utf8', function(err){ 104 | next(err); 105 | }); 106 | } 107 | }); 108 | } 109 | }); 110 | } 111 | return; 112 | } 113 | } 114 | next(); 115 | }; 116 | }; 117 | 118 | /** 119 | * Bundled compilers: 120 | * 121 | * - [sass](http://github.com/visionmedia/sass.js) to _css_ 122 | * - [less](http://github.com/cloudhead/less.js) to _css_ 123 | * - [coffee](http://github.com/jashkenas/coffee-script) to _js_ 124 | */ 125 | 126 | var compilers = exports.compilers = { 127 | sass: { 128 | match: /\.css$/, 129 | ext: '.sass', 130 | compile: function(str, fn){ 131 | var sass = cache.sass || (cache.sass = require('sass')); 132 | try { 133 | fn(null, sass.render(str)); 134 | } catch (err) { 135 | fn(err); 136 | } 137 | } 138 | }, 139 | less: { 140 | match: /\.css$/, 141 | ext: '.less', 142 | compile: function(str, fn){ 143 | var less = cache.less || (cache.less = require('less')); 144 | try { 145 | less.render(str, fn); 146 | } catch (err) { 147 | fn(err); 148 | } 149 | } 150 | }, 151 | coffeescript: { 152 | match: /\.js$/, 153 | ext: '.coffee', 154 | compile: function(str, fn){ 155 | var coffee = cache.coffee || (cache.coffee = require('coffee-script')); 156 | try { 157 | fn(null, coffee.compile(str)); 158 | } catch (err) { 159 | fn(err); 160 | } 161 | } 162 | } 163 | }; -------------------------------------------------------------------------------- /examples/session.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | 4 | // expire sessions within a minute 5 | // /favicon.ico is ignored, and will not 6 | // receive req.session 7 | 8 | connect( 9 | connect.cookieParser() 10 | , connect.session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }}) 11 | , connect.favicon() 12 | , function(req, res, next){ 13 | var sess = req.session; 14 | if (sess.views) { 15 | res.setHeader('Content-Type', 'text/html'); 16 | res.write('

    views: ' + sess.views + '

    '); 17 | res.write('

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

    '); 18 | res.end(); 19 | sess.views++; 20 | } else { 21 | sess.views = 1; 22 | res.end('welcome to the session demo. refresh!'); 23 | } 24 | } 25 | ).listen(3000); 26 | console.log('port 3000: 1 minute expiration demo'); 27 | 28 | // session cookie example 29 | // existing as long as the browser 30 | // session is active 31 | 32 | connect( 33 | connect.cookieParser() 34 | , connect.session({ secret: 'keyboard cat', cookie: { maxAge: 5000 }}) 35 | , connect.favicon() 36 | , function(req, res, next){ 37 | var sess = req.session; 38 | if (sess.views) { 39 | sess.views++; 40 | res.setHeader('Content-Type', 'text/html'); 41 | res.end('

    views: ' + sess.views + '

    '); 42 | } else { 43 | sess.views = 1; 44 | sess.cookie.expires = false; 45 | res.end('welcome to the session demo. refresh!'); 46 | } 47 | } 48 | ).listen(3001); 49 | console.log('port 3001: session cookies'); 50 | 51 | // $ npm install connect-redis 52 | 53 | try { 54 | var RedisStore = require('connect-redis')(connect); 55 | connect( 56 | connect.cookieParser() 57 | , connect.session({ 58 | secret: 'keyboard cat' 59 | , cookie: { maxAge: 60000 * 3 } 60 | , store: new RedisStore 61 | }) 62 | , connect.favicon() 63 | , function(req, res, next){ 64 | var sess = req.session; 65 | if (sess.views) { 66 | sess.views++; 67 | res.setHeader('Content-Type', 'text/html'); 68 | res.end('

    views: ' + sess.views + '

    '); 69 | } else { 70 | sess.views = 1; 71 | res.end('welcome to the redis demo. refresh!'); 72 | } 73 | } 74 | ).listen(3002); 75 | console.log('port 3002: redis example'); 76 | } catch (err) { 77 | console.log('\033[33m'); 78 | console.log('failed to start the Redis example.'); 79 | console.log('to try it install redis, start redis'); 80 | console.log('install connect-redis, and run this'); 81 | console.log('script again.'); 82 | console.log(' $ redis-server'); 83 | console.log(' $ npm install connect-redis'); 84 | console.log('\033[0m'); 85 | } 86 | 87 | // conditional session support by simply 88 | // wrapping middleware with middleware. 89 | 90 | var sess = connect.session({ secret: 'keyboard cat', cookie: { maxAge: 5000 }}); 91 | 92 | connect( 93 | connect.cookieParser() 94 | , function(req, res, next){ 95 | if ('/foo' == req.url || '/bar' == req.url) { 96 | sess(req, res, next); 97 | } else { 98 | next(); 99 | } 100 | } 101 | , connect.favicon() 102 | , function(req, res, next){ 103 | res.end('has session: ' + (req.session ? 'yes' : 'no')); 104 | } 105 | ).listen(3003); 106 | console.log('port 3003: conditional sessions'); 107 | 108 | // Session#reload() will update req.session 109 | // without altering .maxAge or .lastAccess 110 | 111 | // view the page several times, and see that the 112 | // setInterval can still gain access to new 113 | // session data 114 | 115 | connect( 116 | connect.cookieParser() 117 | , connect.session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }}) 118 | , connect.favicon() 119 | , function(req, res, next){ 120 | var sess = req.session 121 | , prev; 122 | 123 | if (sess.views) { 124 | res.setHeader('Content-Type', 'text/html'); 125 | res.write('

    views: ' + sess.views + '

    '); 126 | res.write('

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

    '); 127 | sess.views++; 128 | res.end(); 129 | } else { 130 | sess.views = 1; 131 | setInterval(function(){ 132 | sess.reload(function(){ 133 | console.log(); 134 | if (prev) console.log('previous views %d, now %d', prev, req.session.views); 135 | console.log('time remaining until expiry: %ds', (req.session.cookie.maxAge / 1000)); 136 | prev = req.session.views; 137 | }); 138 | }, 3000); 139 | res.end('welcome to the session demo. refresh!'); 140 | } 141 | } 142 | ).listen(3004); 143 | console.log('port 3004: Session#reload() demo'); -------------------------------------------------------------------------------- /lib/middleware/logger.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - logger 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Log buffer. 11 | */ 12 | 13 | var buf = []; 14 | 15 | /** 16 | * Default log buffer duration. 17 | */ 18 | 19 | var defaultBufferDuration = 1000; 20 | 21 | /** 22 | * Log requests with the given `options` or a `format` string. 23 | * 24 | * Options: 25 | * 26 | * - `format` Format string, see below for tokens 27 | * - `stream` Output stream, defaults to _stdout_ 28 | * - `buffer` Buffer duration, defaults to 1000ms when _true_ 29 | * 30 | * Tokens: 31 | * 32 | * - `:req[header]` ex: `:req[Accept]` 33 | * - `:res[header]` ex: `:res[Content-Length]` 34 | * - `:http-version` 35 | * - `:response-time` 36 | * - `:remote-addr` 37 | * - `:date` 38 | * - `:method` 39 | * - `:url` 40 | * - `:referrer` 41 | * - `:user-agent` 42 | * - `:status` 43 | * 44 | * @param {String|Function|Object} format or options 45 | * @return {Function} 46 | * @api public 47 | */ 48 | 49 | module.exports = function logger(options) { 50 | if ('object' == typeof options) { 51 | options = options || {}; 52 | } else if (options) { 53 | options = { format: options }; 54 | } else { 55 | options = {}; 56 | } 57 | 58 | var fmt = options.format 59 | , stream = options.stream || process.stdout 60 | , buffer = options.buffer; 61 | 62 | // buffering support 63 | if (buffer) { 64 | var realStream = stream 65 | , interval = 'number' == typeof buffer 66 | ? buffer 67 | : defaultBufferDuration; 68 | 69 | // flush interval 70 | setInterval(function(){ 71 | if (buf.length) { 72 | realStream.write(buf.join(''), 'ascii'); 73 | buf.length = 0; 74 | } 75 | }, interval); 76 | 77 | // swap the stream 78 | stream = { 79 | write: function(str){ 80 | buf.push(str); 81 | } 82 | }; 83 | } 84 | 85 | return function logger(req, res, next) { 86 | var start = +new Date 87 | , statusCode 88 | , writeHead = res.writeHead 89 | , end = res.end 90 | , url = req.originalUrl; 91 | 92 | // mount safety 93 | if (req._logging) return next(); 94 | 95 | // flag as logging 96 | req._logging = true; 97 | 98 | // proxy for statusCode. 99 | res.writeHead = function(code, headers){ 100 | res.writeHead = writeHead; 101 | res.writeHead(code, headers); 102 | res.__statusCode = statusCode = code; 103 | res.__headers = headers || {}; 104 | }; 105 | 106 | // proxy end to output a line to the provided logger. 107 | if (fmt) { 108 | res.end = function(chunk, encoding) { 109 | res.end = end; 110 | res.end(chunk, encoding); 111 | res.responseTime = +new Date - start; 112 | if ('function' == typeof fmt) { 113 | var line = fmt(req, res, function(str){ return format(str, req, res); }); 114 | if (line) stream.write(line + '\n', 'ascii'); 115 | } else { 116 | stream.write(format(fmt, req, res) + '\n', 'ascii'); 117 | } 118 | }; 119 | } else { 120 | res.end = function(chunk, encoding) { 121 | var contentLength = (res._headers && res._headers['content-length']) 122 | || (res.__headers && res.__headers['Content-Length']) 123 | || '-'; 124 | 125 | res.end = end; 126 | res.end(chunk, encoding); 127 | 128 | stream.write((req.socket && (req.socket.remoteAddress || (req.socket.socket && req.socket.socket.remoteAddress))) 129 | + ' - - [' + (new Date).toUTCString() + ']' 130 | + ' "' + req.method + ' ' + url 131 | + ' HTTP/' + req.httpVersionMajor + '.' + req.httpVersionMinor + '" ' 132 | + (statusCode || res.statusCode) + ' ' + contentLength 133 | + ' "' + (req.headers['referer'] || req.headers['referrer'] || '') 134 | + '" "' + (req.headers['user-agent'] || '') + '"\n', 'ascii'); 135 | }; 136 | } 137 | 138 | next(); 139 | }; 140 | }; 141 | 142 | /** 143 | * Return formatted log line. 144 | * 145 | * @param {String} str 146 | * @param {IncomingMessage} req 147 | * @param {ServerResponse} res 148 | * @return {String} 149 | * @api private 150 | */ 151 | 152 | function format(str, req, res) { 153 | return str 154 | .replace(':url', req.originalUrl) 155 | .replace(':method', req.method) 156 | .replace(':status', res.__statusCode || res.statusCode) 157 | .replace(':response-time', res.responseTime) 158 | .replace(':date', new Date().toUTCString()) 159 | .replace(':referrer', req.headers['referer'] || req.headers['referrer'] || '-') 160 | .replace(':http-version', req.httpVersionMajor + '.' + req.httpVersionMinor) 161 | .replace(':remote-addr', req.socket && (req.socket.remoteAddress || (req.socket.socket && req.socket.socket.remoteAddress))) 162 | .replace(':user-agent', req.headers['user-agent'] || '') 163 | .replace(/:req\[([^\]]+)\]/g, function(_, field){ return req.headers[field.toLowerCase()] || '-'; }) 164 | .replace(/:res\[([^\]]+)\]/g, function(_, field){ 165 | return (res._headers 166 | ? (res._headers[field.toLowerCase()] || res.__headers[field]) 167 | : (res.__headers && res.__headers[field])) || '-'; 168 | }); 169 | } 170 | -------------------------------------------------------------------------------- /support/docs.js: -------------------------------------------------------------------------------- 1 | 2 | // TODO: fix koala... super outdated 3 | require.paths.unshift(__dirname + '/koala/lib'); 4 | 5 | /** 6 | * Module dependencies. 7 | */ 8 | 9 | var markdown = require('markdown').markdown.toHTML 10 | , koala = require('koala') 11 | , path = require('path') 12 | , ejs = require('ejs') 13 | , fs = require('fs'); 14 | 15 | // meta-data 16 | 17 | var meta = {}; 18 | 19 | // doc data 20 | 21 | var docs = []; 22 | 23 | // files 24 | 25 | var files = meta.files = process.argv.slice(2) 26 | , pending = files.length; 27 | 28 | // templates 29 | 30 | var page = ejs.compile(fs.readFileSync(__dirname + '/page.html', 'utf8')); 31 | 32 | // destination 33 | 34 | var dest = __dirname + '/../docs'; 35 | 36 | // parse files 37 | 38 | files.forEach(function(file){ 39 | fs.readFile(file, 'utf8', function(err, js){ 40 | if (err) throw err; 41 | 42 | // store data 43 | docs.push({ 44 | comments: comments(js) 45 | , filename: file 46 | , basename: path.basename(file) 47 | , dirname: path.dirname(file) 48 | , name: path.basename(file, '.js') 49 | }); 50 | 51 | // done 52 | --pending || render(docs); 53 | }); 54 | }); 55 | 56 | /** 57 | * Parse comments in `js`. 58 | * 59 | * @param {String} js 60 | * @return {Array} 61 | * @api public 62 | */ 63 | 64 | function comments(js) { 65 | var comments = [] 66 | , comment 67 | , buf = '' 68 | , ignore 69 | , within 70 | , code; 71 | 72 | for (var i = 0, len = js.length; i < len; ++i) { 73 | // start comment 74 | if ('/' == js[i] && '*' == js[i+1]) { 75 | // code following previous comment 76 | if (buf.trim().length) { 77 | comment = comments[comments.length - 1]; 78 | comment.method = parseMethod(code = buf.trim()); 79 | code = koala.render('.js', code); 80 | comment.code = code; 81 | buf = ''; 82 | } 83 | i += 2; 84 | within = true; 85 | ignore = '!' == js[i]; 86 | // end comment 87 | } else if ('*' == js[i] && '/' == js[i+1]) { 88 | i += 2; 89 | buf = buf.replace(/^ *\* ?/gm, ''); 90 | var comment = parseComment(buf); 91 | comment.ignore = ignore; 92 | comments.push(comment); 93 | within = ignore = false; 94 | buf = ''; 95 | // buffer comment or code 96 | } else { 97 | buf += js[i]; 98 | } 99 | } 100 | 101 | return comments; 102 | } 103 | 104 | /** 105 | * Parse comment string. 106 | * 107 | * @param {String} str 108 | * @return {Object} 109 | * @api public 110 | */ 111 | 112 | function parseComment(str) { 113 | str = str.trim(); 114 | var comment = { tags: [] }; 115 | 116 | // parse comment body 117 | comment.content = str.split('@')[0].replace(/^([\w ]+):/gm, '## $1'); 118 | comment.description = comment.content.split('\n\n')[0]; 119 | comment.body = comment.content.split('\n\n').slice(1).join('\n\n'); 120 | 121 | // parse tags 122 | if (~str.indexOf('@')) { 123 | var tags = '@' + str.split('@').slice(1).join('@'); 124 | comment.tags = tags.split('\n').map(parseTag); 125 | comment.isPrivate = comment.tags.some(function(tag){ 126 | return 'api' == tag.type && 'private' == tag.visibility; 127 | }) 128 | } 129 | 130 | // syntax highlighting 131 | function highlight(str) { 132 | return str.replace(/
    ([^]+?)<\/code>/g, function(_, js){
    133 |       return '
    ' + koala.render('.js', js) + '';
    134 |     });
    135 |   }
    136 | 
    137 |   // markdown
    138 |   comment.content = highlight(markdown(comment.content));
    139 |   comment.description = highlight(markdown(comment.description));
    140 |   comment.body = highlight(markdown(comment.body));
    141 | 
    142 | 
    143 |   return comment;
    144 | }
    145 | 
    146 | /**
    147 |  * Parse tag string "@param {Array} name description" etc.
    148 |  *
    149 |  * @param {String}
    150 |  * @return {Object}
    151 |  * @api public
    152 |  */
    153 | 
    154 | function parseTag(str) {
    155 |   var tag = {} 
    156 |     , parts = str.split(/ +/)
    157 |     , type = tag.type = parts.shift().replace('@', '');
    158 | 
    159 |   switch (type) {
    160 |     case 'param':
    161 |       tag.types = parseTagTypes(parts.shift());
    162 |       tag.name = parts.shift();
    163 |       tag.description = parts.join(' ');
    164 |       break;
    165 |     case 'return':
    166 |       tag.types = parseTagTypes(parts.shift());
    167 |       break;
    168 |     case 'api':
    169 |       tag.visibility = parts.shift();
    170 |       break;
    171 |     case 'type':
    172 |       tag.value = parts.shift();
    173 |       break;
    174 |   }
    175 | 
    176 |   return tag;
    177 | }
    178 | 
    179 | /**
    180 |  * Parse tag type string "{Array|Object}" etc.
    181 |  *
    182 |  * @param {String} str
    183 |  * @return {Array}
    184 |  * @api public
    185 |  */
    186 | 
    187 | function parseTagTypes(str) {
    188 |   return str
    189 |     .replace(/[{}]/g, '')
    190 |     .split(/ *[|,\/] */);
    191 | }
    192 | 
    193 | function parseMethod(str) {
    194 |   var captures;
    195 |   if (captures = /(?:exports\.|function |prototype\.)(\w+)/.exec(str)) {
    196 |     return captures[1];
    197 |   }
    198 | }
    199 | 
    200 | /**
    201 |  * Render the given array of `docs`.
    202 |  *
    203 |  * @param {Array} docs
    204 |  * @api public
    205 |  */
    206 | 
    207 | function render(docs) {
    208 |   fs.writeFile(dest + '/meta.json', JSON.stringify(meta));
    209 |   docs.forEach(renderFile);
    210 | }
    211 | 
    212 | /**
    213 |  * Render single documentation file.
    214 |  *
    215 |  * @param {Object} doc
    216 |  * @api public
    217 |  */
    218 | 
    219 | function renderFile(doc) {
    220 |   // output destination
    221 |   var out = dest + '/' + doc.filename
    222 |     .replace(/\//g, '-')
    223 |     .replace('.js', '.html')
    224 |     .replace('lib-', '');
    225 | 
    226 |   // template
    227 |   var html = page(doc);
    228 | 
    229 |   // save template
    230 |   fs.writeFile(out, html, function(err){
    231 |     console.log('  \033[90mcompiled\033[0m \033[36m%s\033[0m', doc.filename);
    232 |     if (err) throw err;
    233 |   });
    234 | }
    
    
    --------------------------------------------------------------------------------
    /lib/middleware/static.js:
    --------------------------------------------------------------------------------
      1 | 
      2 | /*!
      3 |  * Connect - staticProvider
      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 |   , path = require('path')
     15 |   , join = path.join
     16 |   , basename = path.basename
     17 |   , normalize = path.normalize
     18 |   , utils = require('../utils')
     19 |   , Buffer = require('buffer').Buffer
     20 |   , parse = require('url').parse
     21 |   , mime = require('mime');
     22 | 
     23 | /**
     24 |  * Static file server with the given `root` path.
     25 |  *
     26 |  * Examples:
     27 |  *
     28 |  *     var oneDay = 86400000;
     29 |  *
     30 |  *     connect(
     31 |  *       connect.static(__dirname + '/public')
     32 |  *     ).listen(3000);
     33 |  *
     34 |  *     connect(
     35 |  *       connect.static(__dirname + '/public', { maxAge: oneDay })
     36 |  *     ).listen(3000);
     37 |  *
     38 |  * Options:
     39 |  *
     40 |  *    - `maxAge`   Browser cache maxAge in milliseconds. defaults to 0
     41 |  *    - `hidden`   Allow transfer of hidden files. defaults to false
     42 |  *
     43 |  * @param {String} root
     44 |  * @param {Object} options
     45 |  * @return {Function}
     46 |  * @api public
     47 |  */
     48 | 
     49 | exports = module.exports = function static(root, options){
     50 |   options = options || {};
     51 | 
     52 |   // root required
     53 |   if (!root) throw new Error('static() root path required');
     54 |   options.root = root;
     55 | 
     56 |   return function static(req, res, next) {
     57 |     options.path = req.url;
     58 |     send(req, res, next, options);
     59 |   };
     60 | };
     61 | 
     62 | /**
     63 |  * Expose mime module.
     64 |  */
     65 | 
     66 | exports.mime = mime;
     67 | 
     68 | /**
     69 |  * Respond with 416  "Requested Range Not Satisfiable"
     70 |  *
     71 |  * @param {ServerResponse} res
     72 |  * @api private
     73 |  */
     74 | 
     75 | function invalidRange(res) {
     76 |   var body = 'Requested Range Not Satisfiable';
     77 |   res.setHeader('Content-Type', 'text/plain');
     78 |   res.setHeader('Content-Length', body.length);
     79 |   res.statusCode = 416;
     80 |   res.end(body);
     81 | }
     82 | 
     83 | /**
     84 |  * Attempt to tranfer the requseted file to `res`.
     85 |  *
     86 |  * @param {ServerRequest}
     87 |  * @param {ServerResponse}
     88 |  * @param {Function} next
     89 |  * @param {Object} options
     90 |  * @api private
     91 |  */
     92 | 
     93 | var send = exports.send = function(req, res, next, options){
     94 |   options = options || {};
     95 |   if (!options.path) throw new Error('path required');
     96 | 
     97 |   // setup
     98 |   var maxAge = options.maxAge || 0
     99 |     , ranges = req.headers.range
    100 |     , head = 'HEAD' == req.method
    101 |     , root = options.root ? normalize(options.root) : null
    102 |     , fn = options.callback
    103 |     , hidden = options.hidden
    104 |     , done;
    105 | 
    106 |   // replace next() with callback when available
    107 |   if (fn) next = fn;
    108 | 
    109 |   // ignore non-GET requests
    110 |   if ('GET' != req.method && !head) return next();
    111 | 
    112 |   // parse url
    113 |   var url = parse(options.path)
    114 |     , path = decodeURIComponent(url.pathname)
    115 |     , type;
    116 | 
    117 |   // when root is not given, consider .. malicious
    118 |   if (!root && ~path.indexOf('..')) return utils.forbidden(res);
    119 | 
    120 |   // join / normalize from optional root dir
    121 |   path = normalize(join(root, path));
    122 | 
    123 |   // malicious path
    124 |   if (root && 0 != path.indexOf(root)) return fn
    125 |     ? fn(new Error('Forbidden'))
    126 |     : utils.forbidden(res);
    127 |     
    128 |   // index.html support
    129 |   if ('/' == path[path.length - 1]) path += 'index.html';
    130 | 
    131 |   // "hidden" file
    132 |   if (!hidden && '.' == basename(path)[0]) return next();
    133 | 
    134 |   // mime type
    135 |   type = mime.lookup(path);
    136 | 
    137 |   fs.stat(path, function(err, stat){
    138 |     // ignore ENOENT
    139 |     if (err) {
    140 |       if (fn) return fn(err);
    141 |       return 'ENOENT' == err.code
    142 |         ? next()
    143 |         : next(err);
    144 |     // ignore directories
    145 |     } else if (stat.isDirectory()) {
    146 |       return fn
    147 |         ? fn(new Error('Cannot Transfer Directory'))
    148 |         : next();
    149 |     }
    150 | 
    151 |     var opts = {};
    152 | 
    153 |     // we have a Range request
    154 |     if (ranges) {
    155 |       ranges = utils.parseRange(stat.size, ranges);
    156 |       // valid
    157 |       if (ranges) {
    158 |         // TODO: stream options
    159 |         // TODO: multiple support
    160 |         opts.start = ranges[0].start;
    161 |         opts.end = ranges[0].end;
    162 |         res.statusCode = 206;
    163 |         res.setHeader('Content-Range', 'bytes '
    164 |           + opts.start
    165 |           + '-'
    166 |           + opts.end
    167 |           + '/'
    168 |           + stat.size);
    169 |       // invalid
    170 |       } else {
    171 |         return fn
    172 |           ? fn(new Error('Requested Range Not Satisfiable'))
    173 |           : invalidRange(res);
    174 |       }
    175 |     // stream the entire file
    176 |     } else {
    177 |       res.setHeader('Content-Length', stat.size);
    178 |       res.setHeader('Cache-Control', 'public, max-age=' + (maxAge / 1000));
    179 |       res.setHeader('Last-Modified', stat.mtime.toUTCString());
    180 |       res.setHeader('ETag', utils.etag(stat));
    181 | 
    182 |       // conditional GET support
    183 |       if (utils.conditionalGET(req)) {
    184 |         if (!utils.modified(req, res)) {
    185 |           return utils.notModified(res);
    186 |         }
    187 |       }
    188 |     }
    189 | 
    190 |     // header fields
    191 |     if (!res.getHeader('content-type')) {
    192 |       var charset = mime.charsets.lookup(type);
    193 |       res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
    194 |     }
    195 |     res.setHeader('Accept-Ranges', 'bytes');
    196 | 
    197 |     // transfer
    198 |     if (head) return res.end();
    199 | 
    200 |     // stream
    201 |     var stream = fs.createReadStream(path, opts);
    202 |     stream.pipe(res);
    203 | 
    204 |     // callback
    205 |     if (fn) {
    206 |       function callback(err) { done || fn(err); done = true }
    207 |       req.on('close', callback);
    208 |       req.socket.on('error', callback);
    209 |       stream.on('end', callback);
    210 |     }
    211 |   });
    212 | };
    213 | 
    
    
    --------------------------------------------------------------------------------
    /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 |  * Serve directory listings with the given `root` path.
     33 |  *
     34 |  * Options:
     35 |  *
     36 |  *  - `hidden` display hidden (dot) files. Defaults to false
     37 |  *  - `icons` display icons. Defaults to false
     38 |  *
     39 |  * @param {String} root
     40 |  * @param {Object} options
     41 |  * @return {Function}
     42 |  * @api public
     43 |  */
     44 | 
     45 | exports = module.exports = function directory(root, options){
     46 |   options = options || {};
     47 | 
     48 |   // root required
     49 |   if (!root) throw new Error('directory() root path required');
     50 |   var hidden = options.hidden
     51 |     , icons = options.icons
     52 |     , root = normalize(root);
     53 | 
     54 |   return function directory(req, res, next) {
     55 |     var accept = req.headers.accept || 'text/plain'
     56 |       , url = parse(req.url)
     57 |       , dir = decodeURIComponent(url.pathname)
     58 |       , path = normalize(join(root, dir))
     59 |       , originalUrl = parse(req.originalUrl)
     60 |       , originalDir = decodeURIComponent(originalUrl.pathname)
     61 |       , showUp = path != root && path != root + '/';
     62 | 
     63 |     // malicious path
     64 |     if (0 != path.indexOf(root)) return utils.forbidden(res);
     65 | 
     66 |     // check if we have a directory
     67 |     fs.stat(path, function(err, stat){
     68 |       if (err) return 'ENOENT' == err.code
     69 |         ? next()
     70 |         : next(err);
     71 | 
     72 |       if (!stat.isDirectory()) return next();
     73 | 
     74 |       // fetch files
     75 |       fs.readdir(path, function(err, files){
     76 |         if (err) return next(err);
     77 |         if (!hidden) files = removeHidden(files);
     78 |         // content-negotiation
     79 |         for (var key in exports) {
     80 |           if (~accept.indexOf(key) || ~accept.indexOf('*/*')) {
     81 |             exports[key](req, res, files, next, originalDir, showUp, icons);
     82 |             return;
     83 |           }
     84 |         }
     85 |         utils.notAcceptable(res);
     86 |       });
     87 |     });
     88 |   };
     89 | };
     90 | 
     91 | /**
     92 |  * Respond with text/html.
     93 |  */
     94 | 
     95 | exports.html = function(req, res, files, next, dir, showUp, icons){
     96 |   fs.readFile(__dirname + '/../public/directory.html', 'utf8', function(err, str){
     97 |     if (err) return next(err);
     98 |     fs.readFile(__dirname + '/../public/style.css', 'utf8', function(err, style){
     99 |       if (err) return next(err);
    100 |       if (showUp) files.unshift('..');
    101 |       str = str
    102 |         .replace('{style}', style)
    103 |         .replace('{files}', html(files, dir, icons))
    104 |         .replace('{directory}', dir)
    105 | 		.replace('{linked-path}', htmlPath(dir));
    106 |       res.setHeader('Content-Type', 'text/html');
    107 |       res.setHeader('Content-Length', str.length);
    108 |       res.end(str);
    109 |     });
    110 |   });
    111 | };
    112 | 
    113 | /**
    114 |  * Respond with application/json.
    115 |  */
    116 | 
    117 | exports.json = function(req, res, files){
    118 |   files = JSON.stringify(files);
    119 |   res.setHeader('Content-Type', 'application/json');
    120 |   res.setHeader('Content-Length', files.length);
    121 |   res.end(files);
    122 | };
    123 | 
    124 | /**
    125 |  * Respond with text/plain.
    126 |  */
    127 | 
    128 | exports.plain = function(req, res, files){
    129 |   files = files.join('\n') + '\n';
    130 |   res.setHeader('Content-Type', 'text/plain');
    131 |   res.setHeader('Content-Length', files.length);
    132 |   res.end(files);
    133 | };
    134 | 
    135 | /**
    136 |  * Map html `dir`, returning a linked path.
    137 |  */
    138 | 
    139 | function htmlPath(dir) {
    140 |   var curr = [];
    141 |   return dir.split('/').map(function(part){
    142 |     curr.push(part);
    143 |     return '' + part + '';
    144 |   }).join(' / ');
    145 | }
    146 | 
    147 | /**
    148 |  * Map html `files`, returning an html unordered list.
    149 |  */
    150 | 
    151 | function html(files, dir, useIcons) {
    152 |   return '
      ' + files.map(function(file){ 153 | var icon = '' 154 | , classes = []; 155 | 156 | if (useIcons && '..' != file) { 157 | icon = icons[extname(file)] || icons.default; 158 | icon = ''; 159 | classes.push('icon'); 160 | } 161 | 162 | return '
    • ' 167 | + icon + file + '
    • '; 168 | 169 | }).join('\n') + '
    '; 170 | } 171 | 172 | /** 173 | * Load and cache the given `icon`. 174 | * 175 | * @param {String} icon 176 | * @return {String} 177 | * @api private 178 | */ 179 | 180 | function load(icon) { 181 | if (cache[icon]) return cache[icon]; 182 | return cache[icon] = fs.readFileSync(__dirname + '/../public/icons/' + icon, 'base64'); 183 | } 184 | 185 | /** 186 | * Filter "hidden" `files`, aka files 187 | * beginning with a `.`. 188 | * 189 | * @param {Array} files 190 | * @return {Array} 191 | * @api private 192 | */ 193 | 194 | function removeHidden(files) { 195 | return files.filter(function(file){ 196 | return '.' != file[0]; 197 | }); 198 | } 199 | 200 | /** 201 | * Icon map. 202 | */ 203 | 204 | var icons = { 205 | '.js': 'page_white_code_red.png' 206 | , '.c': 'page_white_c.png' 207 | , '.h': 'page_white_h.png' 208 | , '.cc': 'page_white_cplusplus.png' 209 | , '.php': 'page_white_php.png' 210 | , '.rb': 'page_white_ruby.png' 211 | , '.cpp': 'page_white_cplusplus.png' 212 | , '.swf': 'page_white_flash.png' 213 | , '.pdf': 'page_white_acrobat.png' 214 | , 'default': 'page_white.png' 215 | }; 216 | -------------------------------------------------------------------------------- /lib/http.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 | , parse = require('url').parse 15 | , assert = require('assert'); 16 | 17 | // environment 18 | 19 | var env = process.env.NODE_ENV || 'development'; 20 | 21 | /** 22 | * Initialize a new `Server` with the given `middleware`. 23 | * 24 | * Examples: 25 | * 26 | * var server = connect.createServer( 27 | * connect.favicon() 28 | * , connect.logger() 29 | * , connect.static(__dirname + '/public') 30 | * ); 31 | * 32 | * @params {Array} middleware 33 | * @return {Server} 34 | * @api public 35 | */ 36 | 37 | var Server = exports.Server = function HTTPServer(middleware) { 38 | this.stack = []; 39 | middleware.forEach(function(fn){ 40 | this.use(fn); 41 | }, this); 42 | http.Server.call(this, this.handle); 43 | }; 44 | 45 | /** 46 | * Inherit from `http.Server.prototype`. 47 | */ 48 | 49 | Server.prototype.__proto__ = http.Server.prototype; 50 | 51 | /** 52 | * Utilize the given middleware `handle` to the given `route`, 53 | * defaulting to _/_. This "route" is the mount-point for the 54 | * middleware, when given a value other than _/_ the middleware 55 | * is only effective when that segment is present in the request's 56 | * pathname. 57 | * 58 | * For example if we were to mount a function at _/admin_, it would 59 | * be invoked on _/admin_, and _/admin/settings_, however it would 60 | * not be invoked for _/_, or _/posts_. 61 | * 62 | * This is effectively the same as passing middleware to `connect.createServer()`, 63 | * however provides a progressive api. 64 | * 65 | * Examples: 66 | * 67 | * var server = connect.createServer(); 68 | * server.use(connect.favicon()); 69 | * server.use(connect.logger()); 70 | * server.use(connect.static(__dirname + '/public')); 71 | * 72 | * If we wanted to prefix static files with _/public_, we could 73 | * "mount" the `static()` middleware: 74 | * 75 | * server.use('/public', connect.static(__dirname + '/public')); 76 | * 77 | * This api is chainable, meaning the following is valid: 78 | * 79 | * connect.createServer() 80 | * .use(connect.favicon()) 81 | * .use(connect.logger()) 82 | * .use(connect.static(__dirname + '/public')) 83 | * .listen(3000); 84 | * 85 | * @param {String|Function} route or handle 86 | * @param {Function} handle 87 | * @return {Server} 88 | * @api public 89 | */ 90 | 91 | Server.prototype.use = function(route, handle){ 92 | this.route = '/'; 93 | 94 | // default route to '/' 95 | if ('string' != typeof route) { 96 | handle = route; 97 | route = '/'; 98 | } 99 | 100 | // multiples 101 | if (arguments.length > 2) { 102 | return Array.prototype.slice.call(arguments, 1).forEach(function(fn){ 103 | this.use(route, fn); 104 | }, this); 105 | } 106 | 107 | // wrap sub-apps 108 | if ('function' == typeof handle.handle) { 109 | var server = handle; 110 | server.route = route; 111 | handle = function(req, res, next) { 112 | server.handle(req, res, next); 113 | }; 114 | } 115 | 116 | // wrap vanilla http.Servers 117 | if (handle instanceof http.Server) { 118 | handle = handle.listeners('request')[0]; 119 | } 120 | 121 | // normalize route to not trail with slash 122 | if ('/' == route[route.length - 1]) { 123 | route = route.substr(0, route.length - 1); 124 | } 125 | 126 | // add the middleware 127 | this.stack.push({ route: route, handle: handle }); 128 | 129 | // allow chaining 130 | return this; 131 | }; 132 | 133 | /** 134 | * Handle server requests, punting them down 135 | * the middleware stack. 136 | * 137 | * @api private 138 | */ 139 | 140 | Server.prototype.handle = function(req, res, out) { 141 | var writeHead = res.writeHead 142 | , stack = this.stack 143 | , removed = '' 144 | , index = 0; 145 | 146 | function next(err) { 147 | var layer, path, c; 148 | req.url = removed + req.url; 149 | req.originalUrl = req.originalUrl || req.url; 150 | removed = ''; 151 | 152 | layer = stack[index++]; 153 | 154 | // all done 155 | if (!layer) { 156 | // but wait! we have a parent 157 | if (out) return out(err); 158 | 159 | // otherwise send a proper error message to the browser. 160 | if (err) { 161 | var msg = 'production' == env 162 | ? 'Internal Server Error' 163 | : err.stack || err.toString(); 164 | 165 | // output to stderr in a non-test env 166 | if ('test' != env) console.error(err.stack || err.toString()); 167 | 168 | res.statusCode = 500; 169 | res.setHeader('Content-Type', 'text/plain'); 170 | res.end(msg); 171 | } else { 172 | res.statusCode = 404; 173 | res.setHeader('Content-Type', 'text/plain'); 174 | res.end('Cannot ' + req.method + ' ' + req.url); 175 | } 176 | return; 177 | } 178 | 179 | try { 180 | path = parse(req.url).pathname; 181 | if (undefined == path) path = '/'; 182 | 183 | // skip this layer if the route doesn't match. 184 | if (0 != path.indexOf(layer.route)) return next(err); 185 | 186 | c = path[layer.route.length]; 187 | if (c && '/' != c && '.' != c) return next(err); 188 | 189 | // Call the layer handler 190 | // Trim off the part of the url that matches the route 191 | removed = layer.route; 192 | req.url = req.url.substr(removed.length); 193 | 194 | // Ensure leading slash 195 | if ('/' != req.url[0]) req.url = '/' + req.url; 196 | 197 | var arity = layer.handle.length; 198 | if (err) { 199 | if (arity === 4) { 200 | layer.handle(err, req, res, next); 201 | } else { 202 | next(err); 203 | } 204 | } else if (arity < 4) { 205 | layer.handle(req, res, next); 206 | } else { 207 | next(); 208 | } 209 | } catch (e) { 210 | if (e instanceof assert.AssertionError) { 211 | console.error(e.stack + '\n'); 212 | next(e); 213 | } else { 214 | next(e); 215 | } 216 | } 217 | } 218 | next(); 219 | }; -------------------------------------------------------------------------------- /test/logger.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect') 7 | , assert = require('assert') 8 | , should = require('should') 9 | , https = require('https') 10 | , fs = require('fs'); 11 | 12 | module.exports = { 13 | 'test http :req[header]': function(){ 14 | var logLine = ''; 15 | var app = connect( 16 | connect.logger({ 17 | format: ':req[foo]', 18 | stream: { write: function(line){ logLine = line; } } 19 | }) 20 | ); 21 | 22 | assert.response(app, 23 | { url: '/', headers: { Foo: 'Bar' } }, 24 | function(){ 25 | assert.equal(logLine, 'Bar\n'); 26 | }); 27 | }, 28 | 29 | 'test http :res[header]': function(){ 30 | var logLine = '' 31 | var app = connect( 32 | connect.logger({ 33 | format: ':res[content-type]', 34 | stream: { write: function(line){ logLine = line; } } 35 | }) 36 | ); 37 | 38 | assert.response(app, 39 | { url: '/', headers: { Foo: 'Bar' } }, 40 | function(){ 41 | assert.equal(logLine, 'text/plain\n'); 42 | }); 43 | }, 44 | 45 | 'test http :res[header] default': function(){ 46 | var logLine = '' 47 | var app = connect( 48 | connect.logger({ 49 | format: ':method :url :res[content-length]', 50 | stream: { write: function(line){ logLine = line; } } 51 | }) 52 | ); 53 | 54 | assert.response(app, 55 | { url: '/' }, 56 | function(){ 57 | assert.equal(logLine, 'GET / -\n'); 58 | }); 59 | }, 60 | 61 | 'test http :http-version': function(){ 62 | var logLine = ''; 63 | var app = connect( 64 | connect.logger({ 65 | format: ':http-version', 66 | stream: { write: function(line){ logLine = line; } } 67 | }) 68 | ); 69 | 70 | assert.response(app, 71 | { url: '/' }, 72 | function(){ 73 | assert.equal(logLine, '1.1\n'); 74 | }); 75 | }, 76 | 77 | 'test http :response-time': function(){ 78 | var logLine = ''; 79 | var app = connect( 80 | connect.logger({ 81 | format: ':response-time', 82 | stream: { write: function(line){ logLine = line; } } 83 | }) 84 | ); 85 | 86 | assert.response(app, 87 | { url: '/' }, 88 | function(){ 89 | assert.type(parseInt(logLine, 10), 'number'); 90 | }); 91 | }, 92 | 93 | 'test http :remote-addr': function(){ 94 | var logLine = ''; 95 | var app = connect( 96 | connect.logger({ 97 | format: ':remote-addr', 98 | stream: { write: function(line){ logLine = line; } } 99 | }) 100 | ); 101 | 102 | assert.response(app, 103 | { url: '/'}, 104 | function(){ 105 | assert.equal(logLine, '127.0.0.1\n'); 106 | }); 107 | }, 108 | 109 | 'test http :date': function(){ 110 | var logLine = ''; 111 | var app = connect( 112 | connect.logger({ 113 | format: ':date', 114 | stream: { write: function(line){ logLine = line; } } 115 | }) 116 | ); 117 | 118 | assert.response(app, 119 | { url: '/'}, 120 | function(){ 121 | assert.doesNotThrow(function(){ 122 | new Date(logLine); 123 | }); 124 | assert.type(Date.parse(logLine.replace('\n', '')), 'number'); 125 | }); 126 | }, 127 | 128 | 'test http :method': function(){ 129 | var logLine = ''; 130 | var app = connect( 131 | connect.logger({ 132 | format: ':method', 133 | stream: { write: function(line){ logLine = line; } } 134 | }) 135 | ); 136 | 137 | assert.response(app, 138 | { method: 'post', url: '/' }, 139 | function(){ 140 | assert.equal(logLine, 'POST\n'); 141 | }); 142 | }, 143 | 144 | 'test http :url': function(){ 145 | var logLine = ''; 146 | var app = connect( 147 | connect.logger({ 148 | format: ':url', 149 | stream: { write: function(line){ logLine = line; } } 150 | }) 151 | ); 152 | 153 | assert.response(app, 154 | { url: '/foo/bar?baz=equals&the#ossom' }, 155 | function(){ 156 | assert.equal(logLine, '/foo/bar?baz=equals&the#ossom\n'); 157 | }); 158 | }, 159 | 160 | 'test http :referrer': function(){ 161 | var logLine = ''; 162 | var app = connect( 163 | connect.logger({ 164 | format: ':referrer', 165 | stream: { write: function(line){ logLine = line; } } 166 | }) 167 | ); 168 | 169 | assert.response(app, 170 | { url: '/', headers: { referrer: 'http://google.com' } }, 171 | function(){ 172 | assert.equal(logLine, 'http://google.com\n'); 173 | }); 174 | }, 175 | 176 | 'test http :user-agent': function(){ 177 | var logLine = ''; 178 | var app = connect( 179 | connect.logger({ 180 | format: ':user-agent', 181 | stream: { write: function(line){ logLine = line; } } 182 | }) 183 | ); 184 | 185 | assert.response(app, 186 | { url: '/', headers: { 'user-agent': 'FooBarClient 1.1.0' } }, 187 | function(){ 188 | assert.equal(logLine, 'FooBarClient 1.1.0\n'); 189 | }); 190 | }, 191 | 192 | 'test http :status': function(){ 193 | var logLine = ''; 194 | var app = connect( 195 | connect.logger({ 196 | format: ':status', 197 | stream: { write: function(line){ logLine = line; } } 198 | }) 199 | ); 200 | 201 | assert.response(app, 202 | { url: '/' }, 203 | function(){ 204 | assert.equal(logLine, '404\n'); 205 | }); 206 | }, 207 | 208 | 'test https :remote-addr': function(){ 209 | var logLine = ''; 210 | var app = connect({ 211 | key: fs.readFileSync(__dirname + '/fixtures/ssl.key'), 212 | cert: fs.readFileSync(__dirname + '/fixtures/ssl.crt') 213 | }, 214 | connect.logger({ 215 | format: ':remote-addr', 216 | stream: { write: function(line){ logLine = line; } } 217 | }) 218 | ); 219 | 220 | app.listen(7777, '127.0.0.1', function() { 221 | var request = https.request({ 222 | host: '127.0.0.1' 223 | , port: 7777 224 | , method: 'GET' 225 | , path: '/' 226 | }); 227 | 228 | request.on('response', function(response){ 229 | response.body = ''; 230 | 231 | response.setEncoding('utf8'); 232 | response.on('data', function(data) { response.body += data; }); 233 | response.on('end', function(){ 234 | var status = 404; 235 | 236 | assert.equal(response.statusCode, status); 237 | assert.equal(logLine, '127.0.0.1\n'); 238 | 239 | app.close(); 240 | }); 241 | }); 242 | 243 | request.end(); 244 | }); 245 | } 246 | }; 247 | -------------------------------------------------------------------------------- /test/connect.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect') 7 | , exec = require('child_process').exec 8 | , should = require('should') 9 | , assert = require('assert') 10 | , https = require('https') 11 | , http = require('http') 12 | , fs = require('fs'); 13 | 14 | module.exports = { 15 | 'test version': function(){ 16 | connect.version.should.match(/^\d+\.\d+\.\d+$/); 17 | }, 18 | 19 | 'test connect()': function(){ 20 | var app = connect( 21 | function(req, res){ 22 | res.end('wahoo'); 23 | } 24 | ); 25 | 26 | assert.response(app, 27 | { url: '/' }, 28 | { body: 'wahoo' }); 29 | }, 30 | 31 | 'test use()': function(){ 32 | var app = connect.createServer(); 33 | 34 | app.use('/blog', function(req, res){ 35 | res.end('blog'); 36 | }); 37 | 38 | var ret = app.use( 39 | function(req, res){ 40 | res.end('default'); 41 | } 42 | ); 43 | 44 | ret.should.equal(app); 45 | 46 | assert.response(app, 47 | { url: '/' }, 48 | { body: 'default', status: 200 }); 49 | 50 | assert.response(app, 51 | { url: '/blog' }, 52 | { body: 'blog', status: 200 }); 53 | }, 54 | 55 | 'test use() several': function(beforeExit){ 56 | var app = connect.createServer() 57 | , calls = 0; 58 | 59 | function a(req, res, next){ 60 | ++calls; 61 | next(); 62 | } 63 | 64 | function b(req, res, next){ 65 | ++calls; 66 | res.end('admin'); 67 | } 68 | 69 | app.use('/admin', a, b); 70 | 71 | assert.response(app, 72 | { url: '/admin' }, 73 | { body: 'admin' }); 74 | 75 | beforeExit(function(){ 76 | calls.should.equal(2); 77 | }); 78 | }, 79 | 80 | 'test path matching': function(){ 81 | var n = 0 82 | , app = connect.createServer(); 83 | 84 | app.use('/hello/world', function(req, res, next){ 85 | switch (++n) { 86 | case 1: 87 | case 2: 88 | req.url.should.equal('/'); 89 | break; 90 | case 3: 91 | req.originalUrl.should.equal('/hello/world/and/more/segments'); 92 | req.url.should.equal('/and/more/segments'); 93 | break; 94 | case 4: 95 | req.url.should.equal('/images/foo.png?with=query&string'); 96 | break; 97 | } 98 | 99 | res.end('hello world'); 100 | }); 101 | 102 | app.use('/hello', function(req, res, next){ 103 | res.end('hello'); 104 | }); 105 | 106 | var foo = connect(function(req, res, next){ 107 | res.end(foo.route); 108 | }); 109 | 110 | app.use('/foo', function(req, res, next){ next(); }, foo); 111 | 112 | assert.response(app, 113 | { url: '/foo' }, 114 | { body: '/foo' }); 115 | 116 | assert.response(app, 117 | { url: '/hello' }, 118 | { body: 'hello' }); 119 | 120 | assert.response(app, 121 | { url: '/hello/' }, 122 | { body: 'hello' }); 123 | 124 | assert.response(app, 125 | { url: '/hello/world' }, 126 | { body: 'hello world' }); 127 | 128 | assert.response(app, 129 | { url: '/hello/world/' }, 130 | { body: 'hello world' }); 131 | 132 | assert.response(app, 133 | { url: '/hello/world/and/more/segments' }, 134 | { body: 'hello world' }); 135 | 136 | assert.response(app, 137 | { url: '/hello/world/images/foo.png?with=query&string' }, 138 | { body: 'hello world' }); 139 | }, 140 | 141 | 'test unmatched path': function(){ 142 | var app = connect.createServer(); 143 | 144 | assert.response(app, 145 | { url: '/' }, 146 | { body: 'Cannot GET /', status: 404 }); 147 | 148 | assert.response(app, 149 | { url: '/foo', method: 'POST' }, 150 | { body: 'Cannot POST /foo', status: 404 }); 151 | }, 152 | 153 | 'test error handling': function(){ 154 | var calls = 0; 155 | var app = connect.createServer( 156 | function(req, res, next){ 157 | // Pass error 158 | next(new Error('lame')); 159 | }, 160 | function(err, req, res, next){ 161 | ++calls; 162 | err.should.be.an.instanceof(Error); 163 | req.should.be.a('object'); 164 | res.should.be.a('object'); 165 | next.should.be.a('function'); 166 | req.body = err.message; 167 | next(err); 168 | }, 169 | function(err, req, res, next){ 170 | ++calls; 171 | err.should.be.an.instanceof(Error); 172 | req.should.be.a('object'); 173 | res.should.be.a('object'); 174 | next.should.be.a('function'); 175 | // Recover exceptional state 176 | next(); 177 | }, 178 | function(req, res, next){ 179 | res.end(req.body); 180 | }, 181 | connect.errorHandler() 182 | ); 183 | 184 | assert.response(app, 185 | { url: '/' }, 186 | { body: 'lame', status: 200 }, 187 | function(){ 188 | calls.should.equal(2); 189 | }); 190 | }, 191 | 192 | 'test catch error': function(){ 193 | var app = connect.createServer( 194 | function(req, res, next){ 195 | doesNotExist(); 196 | } 197 | ); 198 | 199 | assert.response(app, 200 | { url: '/' }, 201 | { status: 500 }); 202 | }, 203 | 204 | 'test mounting': function(){ 205 | var app = connect.createServer(); 206 | 207 | app.use('/', function(req, res){ 208 | // TODO: should inherit parent's /hello 209 | // to become /hello/world/view 210 | app.route.should.equal('/world/view'); 211 | res.end('viewing hello world'); 212 | }); 213 | 214 | var app1 = connect.createServer(); 215 | app1.use('/world/view', app); 216 | app1.use('/world', function(req, res){ 217 | app1.route.should.equal('/hello'); 218 | res.end('hello world'); 219 | }); 220 | 221 | var app2 = connect.createServer(); 222 | app2.use('/hello', app1); 223 | app2.use('/hello', function(req, res){ 224 | app2.route.should.equal('/'); 225 | res.end('hello'); 226 | }); 227 | 228 | assert.response(app2, 229 | { url: '/hello/world/view' }, 230 | { body: 'viewing hello world' }); 231 | 232 | assert.response(app2, 233 | { url: '/hello/world' }, 234 | { body: 'hello world' }); 235 | 236 | assert.response(app2, 237 | { url: '/hello' }, 238 | { body: 'hello' }); 239 | }, 240 | 241 | 'test mounting http.Server': function(){ 242 | var app = connect.createServer() 243 | , world = http.createServer(function(req, res){ 244 | res.end('world'); 245 | }); 246 | 247 | app.use('/hello/', world); 248 | 249 | assert.response(app, 250 | { url: '/hello' }, 251 | { body: 'world' }); 252 | }, 253 | 254 | 'test .charset': function(){ 255 | var app = connect(function(req, res){ 256 | res.charset = 'utf8'; 257 | res.setHeader('Content-Type', 'text/html'); 258 | res.end('test'); 259 | }); 260 | 261 | assert.response(app, 262 | { url: '/' }, 263 | { headers: { 'Content-Type': 'text/html; charset=utf8' }}); 264 | } 265 | }; -------------------------------------------------------------------------------- /test/static.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect') 7 | , assert = require('assert') 8 | , should = require('should') 9 | , http = require('http'); 10 | 11 | /** 12 | * Path to ./test/fixtures/ 13 | */ 14 | 15 | var fixturesPath = __dirname + '/fixtures'; 16 | 17 | var app = connect.createServer( 18 | connect.static(fixturesPath) 19 | ); 20 | 21 | var app2 = connect.createServer( 22 | connect.static(fixturesPath + '/foo/bar/../../') 23 | ); 24 | 25 | module.exports = { 26 | 'test valid file': function(){ 27 | assert.response(app, 28 | { url: '/user.json' }, 29 | function(res){ 30 | res.statusCode.should.equal(200); 31 | JSON.parse(res.body).should.eql({ name: 'tj', email: 'tj@vision-media.ca' }); 32 | res.headers.should.have.property('content-length', '55'); 33 | res.headers.should.have.property('cache-control', 'public, max-age=0'); 34 | res.headers.should.have.property('last-modified'); 35 | res.headers.should.have.property('etag'); 36 | }); 37 | }, 38 | 39 | 'test maxAge': function(){ 40 | var app = connect.createServer( 41 | connect.static(fixturesPath, { maxAge: 60000 }) 42 | ); 43 | 44 | assert.response(app, 45 | { url: '/user.json' }, 46 | function(res){ 47 | res.statusCode.should.equal(200); 48 | res.headers.should.have.property('cache-control', 'public, max-age=60'); 49 | }); 50 | }, 51 | 52 | 'test url encoding': function(){ 53 | assert.response(app, 54 | { url: '/some%20text.txt' }, 55 | { body: 'whoop', status: 200 }); 56 | }, 57 | 58 | 'test index.html support': function(){ 59 | assert.response(app, 60 | { url: '/' }, 61 | { body: '

    Wahoo!

    ' 62 | , status: 200 63 | , headers: { 64 | 'Content-Type': 'text/html; charset=UTF-8' 65 | }}); 66 | }, 67 | 68 | 'test index.html support when missing': function(){ 69 | var app = connect.createServer( 70 | connect.static(__dirname) 71 | ); 72 | 73 | assert.response(app, 74 | { url: '/' }, 75 | { body: 'Cannot GET /', status: 404 }); 76 | }, 77 | 78 | 'test invalid file': function(){ 79 | assert.response(app, 80 | { url: '/foo.json' }, 81 | { body: 'Cannot GET /foo.json', status: 404 }); 82 | }, 83 | 84 | 'test directory': function(){ 85 | assert.response(app, 86 | { url: '/fixtures' }, 87 | { body: 'Cannot GET /fixtures', status: 404 }); 88 | }, 89 | 90 | 'test forbidden': function(){ 91 | assert.response(app, 92 | { url: '/../favicon.test.js' }, 93 | { body: 'Forbidden', status: 403 }); 94 | }, 95 | 96 | 'test forbidden urlencoded': function(){ 97 | assert.response(app, 98 | { url: '/%2e%2e/favicon.test.js' }, 99 | { body: 'Forbidden', status: 403 }); 100 | }, 101 | 102 | 'test relative': function(){ 103 | assert.response(app, 104 | { url: '/foo/../some%20text.txt' }, 105 | { body: 'whoop' }); 106 | }, 107 | 108 | 'test relative root': function(){ 109 | assert.response(app2, 110 | { url: '/foo/../some%20text.txt' }, 111 | { body: 'whoop' }); 112 | }, 113 | 114 | 'test 404 on hidden file': function(){ 115 | assert.response(app, 116 | { url: '/.hidden' }, 117 | { status: 404 }); 118 | }, 119 | 120 | 'test "hidden" option': function(){ 121 | var app = connect.createServer( 122 | connect.static(fixturesPath, { hidden: true }) 123 | ); 124 | 125 | assert.response(app, 126 | { url: '/.hidden' }, 127 | { body: 'hidden\n' }); 128 | }, 129 | 130 | 'test HEAD': function(){ 131 | assert.response(app, 132 | { url: '/user.json', method: 'HEAD' }, 133 | { status: 200, headers: { 'Content-Length': '55' }}); 134 | }, 135 | 136 | 'test Range': function(){ 137 | assert.response(app, 138 | { url: '/list', headers: { Range: 'bytes=0-4' }}, 139 | { body: '12345', status: 206 }); 140 | }, 141 | 142 | 'test Range 2': function(){ 143 | assert.response(app, 144 | { url: '/list', headers: { Range: 'bytes=5-9' }}, 145 | { body: '6789', status: 206 }); 146 | }, 147 | 148 | 'test invalid Range': function(){ 149 | assert.response(app, 150 | { url: '/list', headers: { Range: 'bytes=RAWR' }}, 151 | { body: 'Requested Range Not Satisfiable', status: 416 }); 152 | }, 153 | 154 | 'test callback': function(){ 155 | var app = connect.createServer( 156 | function(req, res, next){ 157 | connect.static.send(req, res, function(err){ 158 | res.end('done'); 159 | }, { path: req.url }); 160 | } 161 | ); 162 | 163 | assert.response(app, 164 | { url: '/list' }, 165 | { body: 'done' }); 166 | }, 167 | 168 | 'test If-Modified-Since modified': function(){ 169 | assert.response(app, 170 | { url: '/list', headers: { 'If-Modified-Since': new Date('2000').toUTCString() }}, 171 | { body: '123456789', status: 200 }); 172 | }, 173 | 174 | 'test If-Modified-Since unmodified': function(){ 175 | assert.response(app, 176 | { url: '/list', headers: { 'If-Modified-Since': new Date('2050').toUTCString() }}, 177 | { body: '', status: 304 }, 178 | function(res){ 179 | Object.keys(res.headers).forEach(function(field){ 180 | if (0 == field.indexOf('content')) { 181 | should.fail('failed to strip ' + field); 182 | } 183 | }); 184 | }); 185 | }, 186 | 187 | 'test ETag unmodified': function(){ 188 | var app = connect.createServer( 189 | connect.static(fixturesPath) 190 | ); 191 | 192 | app.listen(9898, function(){ 193 | var options = { path: '/list', port: 9898, host: '127.0.0.1' }; 194 | http.get(options, function(res){ 195 | res.statusCode.should.equal(200); 196 | res.headers.should.have.property('etag'); 197 | var etag = res.headers.etag; 198 | options.headers = { 'If-None-Match': etag }; 199 | http.get(options, function(res){ 200 | etag.should.equal(res.headers.etag); 201 | res.statusCode.should.equal(304); 202 | res.headers.should.not.have.property('content-length'); 203 | res.headers.should.not.have.property('content-type'); 204 | app.close(); 205 | }); 206 | }); 207 | }); 208 | }, 209 | 210 | 'test ETag multiple given, unmodified': function(){ 211 | var app = connect.createServer( 212 | connect.static(fixturesPath) 213 | ); 214 | 215 | app.listen(9899, function(){ 216 | var options = { path: '/list', port: 9899, host: '127.0.0.1' }; 217 | http.get(options, function(res){ 218 | res.statusCode.should.equal(200); 219 | res.headers.should.have.property('etag'); 220 | var etag = res.headers.etag; 221 | options.headers = { 'If-None-Match': 'foo, bar, ' + etag }; 222 | http.get(options, function(res){ 223 | etag.should.equal(res.headers.etag); 224 | res.statusCode.should.equal(304); 225 | res.headers.should.not.have.property('content-length'); 226 | res.headers.should.not.have.property('content-type'); 227 | app.close(); 228 | }); 229 | }); 230 | }); 231 | }, 232 | 233 | 'test custom mime type definition': function(){ 234 | require('mime').define({ 'application/coffee-script': ['coffee'] }); 235 | assert.response(app, 236 | { url: '/script.coffee' }, 237 | { headers: { 'Content-Type': 'application/coffee-script' }}); 238 | }, 239 | 240 | 'test do not override Content-Type header': function(){ 241 | var app = connect.createServer( 242 | function(req, res, next){ 243 | res.setHeader('Content-Type', 'text/bozo; charset=ISO-8859-1'); 244 | next(); 245 | }, 246 | connect.static(fixturesPath) 247 | ); 248 | 249 | assert.response(app, 250 | { url: '/' }, 251 | { body: '

    Wahoo!

    ' 252 | , status: 200 253 | , headers: { 254 | 'Content-Type': 'text/bozo; charset=ISO-8859-1' 255 | }}); 256 | }, 257 | 258 | 'test mime export': function(){ 259 | connect.static.mime.define.should.be.a('function'); 260 | } 261 | }; -------------------------------------------------------------------------------- /lib/middleware/router.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - router 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 | , parse = require('url').parse; 15 | 16 | /** 17 | * Expose router. 18 | */ 19 | 20 | exports = module.exports = router; 21 | 22 | /** 23 | * Supported HTTP / WebDAV methods. 24 | */ 25 | 26 | var _methods = exports.methods = [ 27 | 'get' 28 | , 'post' 29 | , 'put' 30 | , 'delete' 31 | , 'connect' 32 | , 'options' 33 | , 'trace' 34 | , 'copy' 35 | , 'lock' 36 | , 'mkcol' 37 | , 'move' 38 | , 'propfind' 39 | , 'proppatch' 40 | , 'unlock' 41 | , 'report' 42 | , 'mkactivity' 43 | , 'checkout' 44 | , 'merge' 45 | ]; 46 | 47 | /** 48 | * Provides Sinatra and Express-like routing capabilities. 49 | * 50 | * Examples: 51 | * 52 | * connect.router(function(app){ 53 | * app.get('/user/:id', function(req, res, next){ 54 | * // populates req.params.id 55 | * }); 56 | * app.put('/user/:id', function(req, res, next){ 57 | * // populates req.params.id 58 | * }); 59 | * }) 60 | * 61 | * @param {Function} fn 62 | * @return {Function} 63 | * @api public 64 | */ 65 | 66 | function router(fn){ 67 | var self = this 68 | , methods = {} 69 | , routes = {} 70 | , params = {}; 71 | 72 | if (!fn) throw new Error('router provider requires a callback function'); 73 | 74 | // Generate method functions 75 | _methods.forEach(function(method){ 76 | methods[method] = generateMethodFunction(method.toUpperCase()); 77 | }); 78 | 79 | // Alias del -> delete 80 | methods.del = methods.delete; 81 | 82 | // Apply callback to all methods 83 | methods.all = function(){ 84 | var args = arguments; 85 | _methods.forEach(function(name){ 86 | methods[name].apply(this, args); 87 | }); 88 | return self; 89 | }; 90 | 91 | // Register param callback 92 | methods.param = function(name, fn){ 93 | params[name] = fn; 94 | }; 95 | 96 | fn.call(this, methods); 97 | 98 | function generateMethodFunction(name) { 99 | var localRoutes = routes[name] = routes[name] || []; 100 | return function(path, fn){ 101 | var keys = [] 102 | , middleware = []; 103 | 104 | // slice middleware 105 | if (arguments.length > 2) { 106 | middleware = Array.prototype.slice.call(arguments, 1, arguments.length); 107 | fn = middleware.pop(); 108 | middleware = utils.flatten(middleware); 109 | } 110 | 111 | fn.middleware = middleware; 112 | 113 | if (!path) throw new Error(name + ' route requires a path'); 114 | if (!fn) throw new Error(name + ' route ' + path + ' requires a callback'); 115 | var regexp = path instanceof RegExp 116 | ? path 117 | : normalizePath(path, keys); 118 | localRoutes.push({ 119 | fn: fn 120 | , path: regexp 121 | , keys: keys 122 | , orig: path 123 | , method: name 124 | }); 125 | return self; 126 | }; 127 | } 128 | 129 | function router(req, res, next){ 130 | var route 131 | , self = this; 132 | 133 | (function pass(i){ 134 | if (route = match(req, routes, i)) { 135 | var i = 0 136 | , keys = route.keys; 137 | 138 | req.params = route.params; 139 | 140 | // Param preconditions 141 | (function param(err) { 142 | try { 143 | var key = keys[i++] 144 | , val = req.params[key] 145 | , fn = params[key]; 146 | 147 | if ('route' == err) { 148 | pass(req._route_index + 1); 149 | // Error 150 | } else if (err) { 151 | next(err); 152 | // Param has callback 153 | } else if (fn) { 154 | // Return style 155 | if (1 == fn.length) { 156 | req.params[key] = fn(val); 157 | param(); 158 | // Middleware style 159 | } else { 160 | fn(req, res, param, val); 161 | } 162 | // Finished processing params 163 | } else if (!key) { 164 | // route middleware 165 | i = 0; 166 | (function nextMiddleware(err){ 167 | var fn = route.middleware[i++]; 168 | if ('route' == err) { 169 | pass(req._route_index + 1); 170 | } else if (err) { 171 | next(err); 172 | } else if (fn) { 173 | fn(req, res, nextMiddleware); 174 | } else { 175 | route.call(self, req, res, function(err){ 176 | if (err) { 177 | next(err); 178 | } else { 179 | pass(req._route_index + 1); 180 | } 181 | }); 182 | } 183 | })(); 184 | // More params 185 | } else { 186 | param(); 187 | } 188 | } catch (err) { 189 | next(err); 190 | } 191 | })(); 192 | } else if ('OPTIONS' == req.method) { 193 | options(req, res, routes); 194 | } else { 195 | next(); 196 | } 197 | })(); 198 | }; 199 | 200 | router.remove = function(path, method){ 201 | var fns = router.lookup(path, method); 202 | fns.forEach(function(fn){ 203 | routes[fn.method].splice(fn.index, 1); 204 | }); 205 | }; 206 | 207 | router.lookup = function(path, method, ret){ 208 | ret = ret || []; 209 | 210 | // method specific lookup 211 | if (method) { 212 | method = method.toUpperCase(); 213 | if (routes[method]) { 214 | routes[method].forEach(function(route, i){ 215 | if (path == route.orig) { 216 | var fn = route.fn; 217 | fn.regexp = route.path; 218 | fn.keys = route.keys; 219 | fn.path = route.orig; 220 | fn.method = route.method; 221 | fn.index = i; 222 | ret.push(fn); 223 | } 224 | }); 225 | } 226 | // global lookup 227 | } else { 228 | _methods.forEach(function(method){ 229 | router.lookup(path, method, ret); 230 | }); 231 | } 232 | 233 | return ret; 234 | }; 235 | 236 | router.match = function(url, method, ret){ 237 | var ret = ret || [] 238 | , i = 0 239 | , fn 240 | , req; 241 | 242 | // method specific matches 243 | if (method) { 244 | method = method.toUpperCase(); 245 | req = { url: url, method: method }; 246 | while (fn = match(req, routes, i)) { 247 | i = req._route_index + 1; 248 | ret.push(fn); 249 | } 250 | // global matches 251 | } else { 252 | _methods.forEach(function(method){ 253 | router.match(url, method, ret); 254 | }); 255 | } 256 | 257 | return ret; 258 | }; 259 | 260 | return router; 261 | } 262 | 263 | /** 264 | * Respond to OPTIONS. 265 | * 266 | * @param {ServerRequest} req 267 | * @param {ServerResponse} req 268 | * @param {Array} routes 269 | * @api private 270 | */ 271 | 272 | function options(req, res, routes) { 273 | var pathname = parse(req.url).pathname 274 | , body = optionsFor(pathname, routes).join(','); 275 | res.writeHead(200, { 276 | 'Content-Length': body.length 277 | , 'Allow': body 278 | }); 279 | res.end(body); 280 | } 281 | 282 | /** 283 | * Return OPTIONS array for the given `path`, matching `routes`. 284 | * 285 | * @param {String} path 286 | * @param {Array} routes 287 | * @return {Array} 288 | * @api private 289 | */ 290 | 291 | function optionsFor(path, routes) { 292 | return _methods.filter(function(method){ 293 | var arr = routes[method.toUpperCase()]; 294 | for (var i = 0, len = arr.length; i < len; ++i) { 295 | if (arr[i].path.test(path)) return true; 296 | } 297 | }).map(function(method){ 298 | return method.toUpperCase(); 299 | }); 300 | } 301 | 302 | /** 303 | * Normalize the given path string, 304 | * returning a regular expression. 305 | * 306 | * An empty array should be passed, 307 | * which will contain the placeholder 308 | * key names. For example "/user/:id" will 309 | * then contain ["id"]. 310 | * 311 | * @param {String} path 312 | * @param {Array} keys 313 | * @return {RegExp} 314 | * @api private 315 | */ 316 | 317 | function normalizePath(path, keys) { 318 | path = path 319 | .concat('/?') 320 | .replace(/\/\(/g, '(?:/') 321 | .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional){ 322 | keys.push(key); 323 | slash = slash || ''; 324 | return '' 325 | + (optional ? '' : slash) 326 | + '(?:' 327 | + (optional ? slash : '') 328 | + (format || '') + (capture || '([^/]+?)') + ')' 329 | + (optional || ''); 330 | }) 331 | .replace(/([\/.])/g, '\\$1') 332 | .replace(/\*/g, '(.+)'); 333 | return new RegExp('^' + path + '$', 'i'); 334 | } 335 | 336 | /** 337 | * Attempt to match the given request to 338 | * one of the routes. When successful 339 | * a route function is returned. 340 | * 341 | * @param {ServerRequest} req 342 | * @param {Object} routes 343 | * @return {Function} 344 | * @api private 345 | */ 346 | 347 | function match(req, routes, i) { 348 | var captures 349 | , method = req.method 350 | , i = i || 0; 351 | if ('HEAD' == method) method = 'GET'; 352 | if (routes = routes[method]) { 353 | var url = parse(req.url) 354 | , pathname = url.pathname; 355 | for (var len = routes.length; i < len; ++i) { 356 | var route = routes[i] 357 | , fn = route.fn 358 | , path = route.path 359 | , keys = fn.keys = route.keys; 360 | if (captures = path.exec(pathname)) { 361 | fn.method = method; 362 | fn.params = []; 363 | for (var j = 1, len = captures.length; j < len; ++j) { 364 | var key = keys[j-1], 365 | val = typeof captures[j] === 'string' 366 | ? decodeURIComponent(captures[j]) 367 | : captures[j]; 368 | if (key) { 369 | fn.params[key] = val; 370 | } else { 371 | fn.params.push(val); 372 | } 373 | } 374 | req._route_index = i; 375 | return fn; 376 | } 377 | } 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /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 crypto = require('crypto') 14 | , Path = require('path') 15 | , fs = require('fs'); 16 | 17 | /** 18 | * Flatten the given `arr`. 19 | * 20 | * @param {Array} arr 21 | * @return {Array} 22 | * @api private 23 | */ 24 | 25 | exports.flatten = function(arr, ret){ 26 | var ret = ret || [] 27 | , len = arr.length; 28 | for (var i = 0; i < len; ++i) { 29 | if (Array.isArray(arr[i])) { 30 | exports.flatten(arr[i], ret); 31 | } else { 32 | ret.push(arr[i]); 33 | } 34 | } 35 | return ret; 36 | }; 37 | 38 | /** 39 | * Return md5 hash of the given string and optional encoding, 40 | * defaulting to hex. 41 | * 42 | * utils.md5('wahoo'); 43 | * // => "e493298061761236c96b02ea6aa8a2ad" 44 | * 45 | * @param {String} str 46 | * @param {String} encoding 47 | * @return {String} 48 | * @api public 49 | */ 50 | 51 | exports.md5 = function(str, encoding){ 52 | return crypto 53 | .createHash('md5') 54 | .update(str) 55 | .digest(encoding || 'hex'); 56 | }; 57 | 58 | /** 59 | * Merge object b with object a. 60 | * 61 | * var a = { foo: 'bar' } 62 | * , b = { bar: 'baz' }; 63 | * 64 | * utils.merge(a, b); 65 | * // => { foo: 'bar', bar: 'baz' } 66 | * 67 | * @param {Object} a 68 | * @param {Object} b 69 | * @return {Object} 70 | * @api public 71 | */ 72 | 73 | exports.merge = function(a, b){ 74 | if (a && b) { 75 | for (var key in b) { 76 | a[key] = b[key]; 77 | } 78 | } 79 | return a; 80 | }; 81 | 82 | /** 83 | * Escape the given string of `html`. 84 | * 85 | * @param {String} html 86 | * @return {String} 87 | * @api public 88 | */ 89 | 90 | exports.escape = function(html){ 91 | return String(html) 92 | .replace(/&(?!\w+;)/g, '&') 93 | .replace(//g, '>') 95 | .replace(/"/g, '"'); 96 | }; 97 | 98 | 99 | /** 100 | * Return a unique identifier with the given `len`. 101 | * 102 | * utils.uid(10); 103 | * // => "FDaS435D2z" 104 | * 105 | * @param {Number} len 106 | * @return {String} 107 | * @api public 108 | */ 109 | 110 | exports.uid = function(len) { 111 | var buf = [] 112 | , chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 113 | , charlen = chars.length; 114 | 115 | for (var i = 0; i < len; ++i) { 116 | buf.push(chars[getRandomInt(0, charlen - 1)]); 117 | } 118 | 119 | return buf.join(''); 120 | }; 121 | 122 | /** 123 | * Parse the given cookie string into an object. 124 | * 125 | * @param {String} str 126 | * @return {Object} 127 | * @api public 128 | */ 129 | 130 | exports.parseCookie = function(str){ 131 | var obj = {} 132 | , pairs = str.split(/[;,] */); 133 | for (var i = 0, len = pairs.length; i < len; ++i) { 134 | var pair = pairs[i] 135 | , eqlIndex = pair.indexOf('=') 136 | , key = pair.substr(0, eqlIndex).trim().toLowerCase() 137 | , val = pair.substr(++eqlIndex, pair.length).trim(); 138 | 139 | // quoted values 140 | if ('"' == val[0]) val = val.slice(1, -1); 141 | 142 | // only assign once 143 | if (undefined == obj[key]) { 144 | val = val.replace(/\+/g, ' '); 145 | try { 146 | obj[key] = decodeURIComponent(val); 147 | } catch (err) { 148 | if (err instanceof URIError) { 149 | obj[key] = val; 150 | } else { 151 | throw err; 152 | } 153 | } 154 | } 155 | } 156 | return obj; 157 | }; 158 | 159 | /** 160 | * Serialize the given object into a cookie string. 161 | * 162 | * utils.serializeCookie('name', 'tj', { httpOnly: true }) 163 | * // => "name=tj; httpOnly" 164 | * 165 | * @param {String} name 166 | * @param {String} val 167 | * @param {Object} obj 168 | * @return {String} 169 | * @api public 170 | */ 171 | 172 | exports.serializeCookie = function(name, val, obj){ 173 | var pairs = [name + '=' + encodeURIComponent(val)] 174 | , obj = obj || {}; 175 | 176 | if (obj.domain) pairs.push('domain=' + obj.domain); 177 | if (obj.path) pairs.push('path=' + obj.path); 178 | if (obj.expires) pairs.push('expires=' + obj.expires.toUTCString()); 179 | if (obj.httpOnly) pairs.push('httpOnly'); 180 | if (obj.secure) pairs.push('secure'); 181 | 182 | return pairs.join('; '); 183 | }; 184 | 185 | /** 186 | * Pause `data` and `end` events on the given `obj`. 187 | * Middleware performing async tasks _should_ utilize 188 | * this utility (or similar), to re-emit data once 189 | * the async operation has completed, otherwise these 190 | * events may be lost. 191 | * 192 | * var pause = utils.pause(req); 193 | * fs.readFile(path, function(){ 194 | * next(); 195 | * pause.resume(); 196 | * }); 197 | * 198 | * @param {Object} obj 199 | * @return {Object} 200 | * @api public 201 | */ 202 | 203 | exports.pause = function(obj){ 204 | var onData 205 | , onEnd 206 | , events = []; 207 | 208 | // buffer data 209 | obj.on('data', onData = function(data, encoding){ 210 | events.push(['data', data, encoding]); 211 | }); 212 | 213 | // buffer end 214 | obj.on('end', onEnd = function(data, encoding){ 215 | events.push(['end', data, encoding]); 216 | }); 217 | 218 | return { 219 | end: function(){ 220 | obj.removeListener('data', onData); 221 | obj.removeListener('end', onEnd); 222 | }, 223 | resume: function(){ 224 | this.end(); 225 | for (var i = 0, len = events.length; i < len; ++i) { 226 | obj.emit.apply(obj, events[i]); 227 | } 228 | } 229 | }; 230 | }; 231 | 232 | /** 233 | * Check `req` and `res` to see if it has been modified. 234 | * 235 | * @param {IncomingMessage} req 236 | * @param {ServerResponse} res 237 | * @return {Boolean} 238 | * @api public 239 | */ 240 | 241 | exports.modified = function(req, res, headers) { 242 | var headers = headers || res._headers || {} 243 | , modifiedSince = req.headers['if-modified-since'] 244 | , lastModified = headers['last-modified'] 245 | , noneMatch = req.headers['if-none-match'] 246 | , etag = headers['etag']; 247 | 248 | if (noneMatch) noneMatch = noneMatch.split(/ *, */); 249 | 250 | // check If-None-Match 251 | if (noneMatch && etag && ~noneMatch.indexOf(etag)) { 252 | return false; 253 | } 254 | 255 | // check If-Modified-Since 256 | if (modifiedSince && lastModified) { 257 | modifiedSince = new Date(modifiedSince); 258 | lastModified = new Date(lastModified); 259 | // Ignore invalid dates 260 | if (!isNaN(modifiedSince.getTime())) { 261 | if (lastModified <= modifiedSince) return false; 262 | } 263 | } 264 | 265 | return true; 266 | }; 267 | 268 | /** 269 | * Strip `Content-*` headers from `res`. 270 | * 271 | * @param {ServerResponse} res 272 | * @api public 273 | */ 274 | 275 | exports.removeContentHeaders = function(res){ 276 | Object.keys(res._headers).forEach(function(field){ 277 | if (0 == field.indexOf('content')) { 278 | res.removeHeader(field); 279 | } 280 | }); 281 | }; 282 | 283 | /** 284 | * Check if `req` is a conditional GET request. 285 | * 286 | * @param {IncomingMessage} req 287 | * @return {Boolean} 288 | * @api public 289 | */ 290 | 291 | exports.conditionalGET = function(req) { 292 | return req.headers['if-modified-since'] 293 | || req.headers['if-none-match']; 294 | }; 295 | 296 | /** 297 | * Respond with 403 "Forbidden". 298 | * 299 | * @param {ServerResponse} res 300 | * @api public 301 | */ 302 | 303 | exports.forbidden = function(res) { 304 | var body = 'Forbidden'; 305 | res.setHeader('Content-Type', 'text/plain'); 306 | res.setHeader('Content-Length', body.length); 307 | res.statusCode = 403; 308 | res.end(body); 309 | }; 310 | 311 | /** 312 | * Respond with 401 "Unauthorized". 313 | * 314 | * @param {ServerResponse} res 315 | * @param {String} realm 316 | * @api public 317 | */ 318 | 319 | exports.unauthorized = function(res, realm) { 320 | res.statusCode = 401; 321 | res.setHeader('WWW-Authenticate', 'Basic realm="' + realm + '"'); 322 | res.end('Unauthorized'); 323 | }; 324 | 325 | /** 326 | * Respond with 400 "Bad Request". 327 | * 328 | * @param {ServerResponse} res 329 | * @api public 330 | */ 331 | 332 | exports.badRequest = function(res) { 333 | res.statusCode = 400; 334 | res.end('Bad Request'); 335 | }; 336 | 337 | /** 338 | * Respond with 304 "Not Modified". 339 | * 340 | * @param {ServerResponse} res 341 | * @param {Object} headers 342 | * @api public 343 | */ 344 | 345 | exports.notModified = function(res) { 346 | exports.removeContentHeaders(res); 347 | res.statusCode = 304; 348 | res.end(); 349 | }; 350 | 351 | /** 352 | * Return an ETag in the form of `"-"` 353 | * from the given `stat`. 354 | * 355 | * @param {Object} stat 356 | * @return {String} 357 | * @api public 358 | */ 359 | 360 | exports.etag = function(stat) { 361 | return '"' + stat.size + '-' + Number(stat.mtime) + '"'; 362 | }; 363 | 364 | /** 365 | * Parse "Range" header `str` relative to the given file `size`. 366 | * 367 | * @param {Number} size 368 | * @param {String} str 369 | * @return {Array} 370 | * @api public 371 | */ 372 | 373 | exports.parseRange = function(size, str){ 374 | var valid = true; 375 | var arr = str.substr(6).split(',').map(function(range){ 376 | var range = range.split('-') 377 | , start = parseInt(range[0], 10) 378 | , end = parseInt(range[1], 10); 379 | 380 | // -500 381 | if (isNaN(start)) { 382 | start = size - end; 383 | end = size - 1; 384 | // 500- 385 | } else if (isNaN(end)) { 386 | end = size - 1; 387 | } 388 | 389 | // Invalid 390 | if (isNaN(start) || isNaN(end) || start > end) valid = false; 391 | 392 | return { start: start, end: end }; 393 | }); 394 | return valid ? arr : undefined; 395 | }; 396 | 397 | /** 398 | * Convert array-like object to an `Array`. 399 | * 400 | * node-bench measured "16.5 times faster than Array.prototype.slice.call()" 401 | * 402 | * @param {Object} obj 403 | * @return {Array} 404 | * @api public 405 | */ 406 | 407 | var toArray = exports.toArray = function(obj){ 408 | var len = obj.length 409 | , arr = new Array(len); 410 | for (var i = 0; i < len; ++i) { 411 | arr[i] = obj[i]; 412 | } 413 | return arr; 414 | }; 415 | 416 | /** 417 | * Retrun a random int, used by `utils.uid()` 418 | * 419 | * @param {Number} min 420 | * @param {Number} max 421 | * @return {Number} 422 | * @api private 423 | */ 424 | 425 | function getRandomInt(min, max) { 426 | return Math.floor(Math.random() * (max - min + 1)) + min; 427 | } 428 | -------------------------------------------------------------------------------- /lib/middleware/session.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - 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 Session = require('./session/session') 14 | , MemoryStore = require('./session/memory') 15 | , Cookie = require('./session/cookie') 16 | , Store = require('./session/store') 17 | , utils = require('./../utils') 18 | , parse = require('url').parse 19 | , crypto = require('crypto'); 20 | 21 | // environment 22 | 23 | var env = process.env.NODE_ENV; 24 | 25 | /** 26 | * Expose the middleware. 27 | */ 28 | 29 | exports = module.exports = session; 30 | 31 | /** 32 | * Expose constructors. 33 | */ 34 | 35 | exports.Store = Store; 36 | exports.Cookie = Cookie; 37 | exports.Session = Session; 38 | exports.MemoryStore = MemoryStore; 39 | 40 | /** 41 | * Warning message for `MemoryStore` usage in production. 42 | */ 43 | 44 | var warning = 'Warning: connection.session() MemoryStore is not\n' 45 | + 'designed for a production environment, as it will leak\n' 46 | + 'memory, and obviously only work within a single process.'; 47 | 48 | /** 49 | * Default finger-printing function. 50 | */ 51 | 52 | function defaultFingerprint(req) { 53 | var ua = req.headers['user-agent'] || ''; 54 | return ua.replace(/;?\schromeframe\/[\d\.]+/, ''); 55 | }; 56 | 57 | /** 58 | * Paths to ignore, defaulting to `/favicon.ico`. 59 | */ 60 | 61 | exports.ignore = ['/favicon.ico']; 62 | 63 | /** 64 | * Setup session store with the given `options`. 65 | * 66 | * Session data is _not_ saved in the cookie itself, however 67 | * cookies are used, so we must use the [cookieParser()](middleware-cookieParser.html) 68 | * middleware _before_ `session()`. 69 | * 70 | * Examples: 71 | * 72 | * connect.createServer( 73 | * connect.cookieParser() 74 | * , connect.session({ secret: 'keyboard cat' }) 75 | * ); 76 | * 77 | * Options: 78 | * 79 | * - `key` cookie name defaulting to `connect.sid` 80 | * - `store` Session store instance 81 | * - `fingerprint` Custom fingerprint generating function 82 | * - `cookie` Session cookie settings, defaulting to `{ path: '/', httpOnly: true, maxAge: 14400000 }` 83 | * - `secret` Secret string used to compute hash 84 | * 85 | * Ignore Paths: 86 | * 87 | * By default `/favicon.ico` is the only ignored path, all others 88 | * will utilize sessions, to manipulate the paths ignored, use 89 | * `connect.session.ignore.push('/my/path')`. This works for _full_ 90 | * pathnames only, not segments nor substrings. 91 | * 92 | * connect.session.ignore.push('/robots.txt'); 93 | * 94 | * ## req.session 95 | * 96 | * To store or access session data, simply use the request property `req.session`, 97 | * which is (generally) serialized as JSON by the store, so nested objects 98 | * are typically fine. For example below is a user-specific view counter: 99 | * 100 | * connect( 101 | * connect.cookieParser() 102 | * , connect.session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }}) 103 | * , connect.favicon() 104 | * , function(req, res, next){ 105 | * var sess = req.session; 106 | * if (sess.views) { 107 | * res.setHeader('Content-Type', 'text/html'); 108 | * res.write('

    views: ' + sess.views + '

    '); 109 | * res.write('

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

    '); 110 | * res.end(); 111 | * sess.views++; 112 | * } else { 113 | * sess.views = 1; 114 | * res.end('welcome to the session demo. refresh!'); 115 | * } 116 | * } 117 | * ).listen(3000); 118 | * 119 | * ## Session#regenerate() 120 | * 121 | * To regenerate the session simply invoke the method, once complete 122 | * a new SID and `Session` instance will be initialized at `req.session`. 123 | * 124 | * req.session.regenerate(function(err){ 125 | * // will have a new session here 126 | * }); 127 | * 128 | * ## Session#destroy() 129 | * 130 | * Destroys the session, removing `req.session`, will be re-generated next request. 131 | * 132 | * req.session.destroy(function(err){ 133 | * // cannot access session here 134 | * }); 135 | * 136 | * ## Session#reload() 137 | * 138 | * Reloads the session data. 139 | * 140 | * req.session.reload(function(err){ 141 | * // session updated 142 | * }); 143 | * 144 | * ## Session#save() 145 | * 146 | * Save the session. 147 | * 148 | * req.session.save(function(err){ 149 | * // session saved 150 | * }); 151 | * 152 | * ## Session#touch() 153 | * 154 | * Updates the `.maxAge`, and `.lastAccess` properties. Typically this is 155 | * not necessary to call, as the session middleware does this for you. 156 | * 157 | * ## Session#cookie 158 | * 159 | * Each session has a unique cookie object accompany it. This allows 160 | * you to alter the session cookie per visitor. For example we can 161 | * set `req.session.cookie.expires` to `false` to enable the cookie 162 | * to remain for only the duration of the user-agent. 163 | * 164 | * ## Session#maxAge 165 | * 166 | * Alternatively `req.session.cookie.maxAge` will return the time 167 | * remaining in milliseconds, which we may also re-assign a new value 168 | * to adjust the `.expires` property appropriately. The following 169 | * are essentially equivalent 170 | * 171 | * var hour = 3600000; 172 | * req.session.cookie.expires = new Date(Date.now() + hour); 173 | * req.session.cookie.maxAge = hour; 174 | * 175 | * For example when `maxAge` is set to `60000` (one minute), and 30 seconds 176 | * has elapsed it will return `30000` until the current request has completed, 177 | * at which time `req.session.touch()` is called to update `req.session.lastAccess`, 178 | * and reset `req.session.maxAge` to its original value. 179 | * 180 | * req.session.cookie.maxAge; 181 | * // => 30000 182 | * 183 | * Session Store Implementation: 184 | * 185 | * Every session store _must_ implement the following methods 186 | * 187 | * - `.get(sid, callback)` 188 | * - `.set(sid, session, callback)` 189 | * - `.destroy(sid, callback)` 190 | * 191 | * Recommended methods include, but are not limited to: 192 | * 193 | * - `.length(callback)` 194 | * - `.clear(callback)` 195 | * 196 | * For an example implementation view the [connect-redis](http://github.com/visionmedia/connect-redis) repo. 197 | * 198 | * @param {Object} options 199 | * @return {Function} 200 | * @api public 201 | */ 202 | 203 | function session(options){ 204 | var options = options || {} 205 | , key = options.key || 'connect.sid' 206 | , secret = options.secret 207 | , store = options.store || new MemoryStore 208 | , fingerprint = options.fingerprint || defaultFingerprint 209 | , cookie = options.cookie; 210 | 211 | // notify user that this store is not 212 | // meant for a production environment 213 | if ('production' == env && store instanceof MemoryStore) { 214 | console.warn(warning); 215 | } 216 | 217 | // ensure secret is present 218 | if (!secret) { 219 | throw new Error('connect.session({ secret: "string" }) required for security'); 220 | } 221 | 222 | // session hashing function 223 | store.hash = function(req, base) { 224 | return crypto 225 | .createHmac('sha256', secret) 226 | .update(base + fingerprint(req)) 227 | .digest('base64') 228 | .replace(/=*$/, ''); 229 | }; 230 | 231 | // generates the new session 232 | store.generate = function(req){ 233 | var base = utils.uid(24); 234 | var sessionID = base + '.' + store.hash(req, base); 235 | req.sessionID = sessionID; 236 | req.session = new Session(req); 237 | req.session.cookie = new Cookie(cookie); 238 | }; 239 | 240 | return function session(req, res, next) { 241 | // self-awareness 242 | if (req.session) return next(); 243 | 244 | // parse url 245 | var url = parse(req.url) 246 | , path = url.pathname; 247 | 248 | // ignorable paths 249 | if (~exports.ignore.indexOf(path)) return next(); 250 | 251 | // expose store 252 | req.sessionStore = store; 253 | 254 | // proxy writeHead() to Set-Cookie 255 | var writeHead = res.writeHead; 256 | res.writeHead = function(status, headers){ 257 | if (req.session) { 258 | var cookie = req.session.cookie; 259 | // only send secure session cookies when there is a secure connection. 260 | // proxySecure is a custom attribute to allow for a reverse proxy 261 | // to handle SSL connections and to communicate to connect over HTTP that 262 | // the incoming connection is secure. 263 | var secured = cookie.secure && (req.connection.encrypted || req.connection.proxySecure); 264 | if (secured || !cookie.secure) { 265 | res.setHeader('Set-Cookie', cookie.serialize(key, req.sessionID)); 266 | } 267 | } 268 | 269 | res.writeHead = writeHead; 270 | return res.writeHead(status, headers); 271 | }; 272 | 273 | // proxy end() to commit the session 274 | var end = res.end; 275 | res.end = function(data, encoding){ 276 | res.end = end; 277 | if (req.session) { 278 | // HACK: ensure Set-Cookie for implicit writeHead() 279 | if (!res._header) res._implicitHeader(); 280 | req.session.resetMaxAge(); 281 | req.session.save(function(){ 282 | res.end(data, encoding); 283 | }); 284 | } else { 285 | res.end(data, encoding); 286 | } 287 | }; 288 | 289 | // session hashing 290 | function hash(base) { 291 | return store.hash(req, base); 292 | } 293 | 294 | // generate the session 295 | function generate() { 296 | store.generate(req); 297 | } 298 | 299 | // get the sessionID from the cookie 300 | req.sessionID = req.cookies[key]; 301 | 302 | // make a new session if the browser doesn't send a sessionID 303 | if (!req.sessionID) { 304 | generate(); 305 | next(); 306 | return; 307 | } 308 | 309 | // check the fingerprint 310 | var parts = req.sessionID.split('.'); 311 | if (parts[1] != hash(parts[0])) { 312 | generate(); 313 | next(); 314 | return; 315 | } 316 | 317 | // generate the session object 318 | var pause = utils.pause(req); 319 | store.get(req.sessionID, function(err, sess){ 320 | // proxy to resume() events 321 | var _next = next; 322 | next = function(err){ 323 | _next(err); 324 | pause.resume(); 325 | } 326 | 327 | // error handling 328 | if (err) { 329 | if ('ENOENT' == err.code) { 330 | generate(); 331 | next(); 332 | } else { 333 | next(err); 334 | } 335 | // no session 336 | } else if (!sess) { 337 | generate(); 338 | next(); 339 | // populate req.session 340 | } else { 341 | store.createSession(req, sess); 342 | next(); 343 | } 344 | }); 345 | }; 346 | }; 347 | -------------------------------------------------------------------------------- /test/router.test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('connect') 7 | , assert = require('assert') 8 | , should = require('should') 9 | , http = require('http'); 10 | 11 | module.exports = { 12 | 'test methods': function(){ 13 | var app = connect.createServer( 14 | connect.router(function(app){ 15 | app.get('/', function(req, res){ 16 | res.end('GET /'); 17 | }); 18 | 19 | app.post('/', function(req, res){ 20 | res.end('POST /'); 21 | }); 22 | 23 | app.put('/', function(req, res){ 24 | res.end('PUT /'); 25 | }); 26 | }) 27 | ); 28 | 29 | assert.response(app, 30 | { url: '/' }, 31 | { body: 'GET /' }); 32 | 33 | assert.response(app, 34 | { url: '/', method: 'POST' }, 35 | { body: 'POST /' }); 36 | 37 | assert.response(app, 38 | { url: '/', method: 'PUT' }, 39 | { body: 'PUT /' }); 40 | }, 41 | 42 | 'test params': function(){ 43 | var app = connect.createServer( 44 | connect.router(function(app){ 45 | app.get('/user/:id', function(req, res){ 46 | res.end('user ' + req.params.id); 47 | }); 48 | 49 | app.get('/user/:id/:op', function(req, res){ 50 | res.end(req.params.op + 'ing user ' + req.params.id); 51 | }); 52 | }) 53 | ); 54 | 55 | assert.response(app, 56 | { url: '/user/12/' }, 57 | { body: 'user 12' }); 58 | 59 | assert.response(app, 60 | { url: '/user/12' }, 61 | { body: 'user 12' }); 62 | 63 | assert.response(app, 64 | { url: '/user/tj.holowaychuk' }, 65 | { body: 'user tj.holowaychuk' }); 66 | 67 | assert.response(app, 68 | { url: '/user/12/edit' }, 69 | { body: 'editing user 12' }); 70 | }, 71 | 72 | 'test optional params': function(){ 73 | var app = connect.createServer( 74 | connect.router(function(app){ 75 | app.get('/user/:id?', function(req, res){ 76 | res.end('user ' + (req.params.id || 'account')); 77 | }); 78 | 79 | app.get('/account/:id?/:op', function(req, res){ 80 | res.end(req.params.op + 'ing user ' + (req.params.id || 'account')); 81 | }); 82 | }) 83 | ); 84 | 85 | assert.response(app, 86 | { url: '/user/12' }, 87 | { body: 'user 12' }); 88 | 89 | assert.response(app, 90 | { url: '/account/edit' }, 91 | { body: 'editing user account' }); 92 | 93 | assert.response(app, 94 | { url: '/account/12/edit' }, 95 | { body: 'editing user 12' }); 96 | }, 97 | 98 | 'test splat': function(){ 99 | var app = connect.createServer( 100 | connect.router(function(app){ 101 | app.get('/file/*', function(req, res){ 102 | res.end('file ' + req.params[0]); 103 | }); 104 | }) 105 | ); 106 | 107 | assert.response(app, 108 | { url: '/file' }, 109 | { status: 404 }); 110 | 111 | assert.response(app, 112 | { url: '/file/jquery.js' }, 113 | { body: 'file jquery.js' }); 114 | 115 | assert.response(app, 116 | { url: '/file/public/javascripts/jquery.js' }, 117 | { body: 'file public/javascripts/jquery.js' }); 118 | }, 119 | 120 | 'test several splats': function(){ 121 | var app = connect.createServer( 122 | connect.router(function(app){ 123 | app.get('/file/*.*', function(req, res){ 124 | res.end('file ' + req.params[0] + ' ext ' + req.params[1]); 125 | }); 126 | 127 | app.get('/move/*/to/*', function(req, res){ 128 | res.end('moved ' + req.params[0] + ' to ' + req.params[1]); 129 | }); 130 | }) 131 | ); 132 | 133 | assert.response(app, 134 | { url: '/file/jquery.js' }, 135 | { body: 'file jquery ext js' }); 136 | 137 | assert.response(app, 138 | { url: '/file/public/javascripts/jquery.js' }, 139 | { body: 'file public/javascripts/jquery ext js' }); 140 | 141 | assert.response(app, 142 | { url: '/move/jquery/to/jquery.js' }, 143 | { body: 'moved jquery to jquery.js' }); 144 | }, 145 | 146 | 'test named captures': function(){ 147 | var app = connect.createServer( 148 | connect.router(function(app){ 149 | app.get('/page/:from(\\d+)-:to(\\d+)', function(req, res){ 150 | res.end('viewing ' + req.params.from + ' to ' + req.params.to); 151 | }); 152 | }) 153 | ); 154 | 155 | assert.response(app, 156 | { url: '/page/1-9' }, 157 | { body: 'viewing 1 to 9' }); 158 | 159 | assert.response(app, 160 | { url: '/page/3-b' }, 161 | { status: 404 }); 162 | }, 163 | 164 | 'test format': function(){ 165 | var app = connect.createServer( 166 | connect.router(function(app){ 167 | app.get('/users.:format', function(req, res){ 168 | res.end('format ' + req.params.format); 169 | }); 170 | 171 | app.get('/users', function(req, res){ 172 | res.end('users'); 173 | }); 174 | }) 175 | ); 176 | 177 | assert.response(app, 178 | { url: '/users' }, 179 | { body: 'users' }); 180 | 181 | assert.response(app, 182 | { url: '/users.json' }, 183 | { body: 'format json' }); 184 | }, 185 | 186 | 'test optional format': function(){ 187 | var app = connect.createServer( 188 | connect.router(function(app){ 189 | app.get('/users.:format?', function(req, res){ 190 | res.end('format ' + (req.params.format || 'html')); 191 | }); 192 | }) 193 | ); 194 | 195 | assert.response(app, 196 | { url: '/users' }, 197 | { body: 'format html' }); 198 | 199 | assert.response(app, 200 | { url: '/users.json' }, 201 | { body: 'format json' }); 202 | }, 203 | 204 | 'test regular expressions': function(){ 205 | var app = connect.createServer( 206 | connect.router(function(app){ 207 | app.get(/\/commits\/(\w+)\.\.(\w+)\/?/i, function(req, res){ 208 | res.end( 209 | 'from ' + req.params[0] 210 | + ' to ' + req.params[1]); 211 | }); 212 | }) 213 | ); 214 | 215 | assert.response(app, 216 | { url: '/commits/abc..def' }, 217 | { body: 'from abc to def' }); 218 | }, 219 | 220 | 'test next()': function(){ 221 | var hits = []; 222 | 223 | var app = connect.createServer( 224 | connect.router(function(app){ 225 | app.get('/:user', function(req, res, next){ 226 | hits.push('a'); 227 | next(); 228 | }); 229 | 230 | app.get('/:user', function(req, res, next){ 231 | hits.push('b'); 232 | next(); 233 | }); 234 | 235 | app.get('/:user', function(req, res, next){ 236 | hits.push('c'); 237 | res.end(req.params.user); 238 | }); 239 | }) 240 | ); 241 | 242 | assert.response(app, 243 | { url: '/tj' }, 244 | { body: 'tj' }, 245 | function(){ 246 | hits.should.eql(['a', 'b', 'c']); 247 | }); 248 | }, 249 | 250 | 'test next(err)': function(){ 251 | var hits = []; 252 | 253 | var app = connect.createServer( 254 | connect.router(function(app){ 255 | app.get('/:user', function(req, res, next){ 256 | hits.push('a'); 257 | next(); 258 | }); 259 | 260 | app.get('/:user', function(req, res, next){ 261 | hits.push('b'); 262 | next(new Error('keyboard cat')); 263 | }); 264 | 265 | app.get('/:user', function(req, res, next){ 266 | hits.push('c'); 267 | res.end(req.params.user); 268 | }); 269 | }), 270 | function(err, req, res, next) { 271 | res.end(err.toString()); 272 | } 273 | ); 274 | 275 | assert.response(app, 276 | { url: '/tj' }, 277 | { body: 'Error: keyboard cat' }, 278 | function(){ 279 | hits.should.eql(['a', 'b']); 280 | }); 281 | }, 282 | 283 | 'test HEAD': function(){ 284 | var app = connect.createServer( 285 | connect.router(function(app){ 286 | app.get('/items', function(req, res){ 287 | res.end('HEAD' == req.method 288 | ? '' 289 | : 'foo, bar, baz'); 290 | }); 291 | }) 292 | ); 293 | 294 | assert.response(app, 295 | { url: '/items', method: 'HEAD' }, 296 | { body: '' }); 297 | }, 298 | 299 | 'test OPTIONS': function(){ 300 | var app = connect.createServer( 301 | connect.router(function(app){ 302 | app.get('/items', function(){}); 303 | app.post('/items', function(){}); 304 | app.get('/users', function(){}); 305 | app.options('/accounts', function(req, res){ 306 | res.writeHead(204, { Allow: 'GET' }); 307 | res.end(); 308 | }); 309 | }) 310 | ); 311 | 312 | assert.response(app, 313 | { url: '/items', method: 'OPTIONS' }, 314 | { body: 'GET,POST', headers: { Allow: 'GET,POST' }}); 315 | 316 | assert.response(app, 317 | { url: '/users', method: 'OPTIONS' }, 318 | { body: 'GET', headers: { Allow: 'GET' }}); 319 | 320 | assert.response(app, 321 | { url: '/accounts', method: 'OPTIONS' }, 322 | { headers: { Allow: 'GET' }}); 323 | }, 324 | 325 | 'test immutable params': function(){ 326 | var app = connect.createServer( 327 | connect.router(function(app){ 328 | app.get('/user/:id', function(req, res, next){ 329 | req.params.id = parseInt(req.params.id, 10); 330 | next(); 331 | }); 332 | 333 | app.get('/user/:id', function(req, res){ 334 | res.end(typeof req.params.id); 335 | }); 336 | }) 337 | ); 338 | 339 | assert.response(app, 340 | { url: '/user/12' }, 341 | { body: 'string' }); 342 | }, 343 | 344 | 'test .lookup()': function(){ 345 | var router = connect.router(function(app){ 346 | app.get('/user/:id', function(req, res, next){ 347 | req.params.id = parseInt(req.params.id, 10); 348 | next(); 349 | }); 350 | 351 | app.get('/user/:id', function(req, res){ 352 | res.end(typeof req.params.id); 353 | }); 354 | 355 | app.put('/user/:id', function(){}); 356 | app.get('/user/:id/edit', function(){}); 357 | app.post('/user', function(){}); 358 | }); 359 | 360 | var fn = router.lookup('/user/:id')[0]; 361 | fn.regexp.should.be.an.instanceof(RegExp); 362 | fn.path.should.equal('/user/:id'); 363 | fn.method.should.equal('GET'); 364 | fn.keys.should.eql(['id']); 365 | 366 | router.lookup('/user/:id')[0].should.be.a('function'); 367 | router.lookup('/user/:id').should.have.length(3); 368 | router.lookup('/user/:id', 'GET').should.have.length(2); 369 | router.lookup('/user/:id', 'get').should.have.length(2); 370 | router.lookup('/user/:id/edit', 'GET').should.have.length(1); 371 | router.lookup('/user/:id', 'PUT').should.have.length(1); 372 | router.lookup('/user/:id', 'FOO').should.be.empty; 373 | }, 374 | 375 | 'test .match()': function(){ 376 | var router = connect.router(function(app){ 377 | app.get('/user/:id', function(req, res, next){ 378 | req.params.id = parseInt(req.params.id, 10); 379 | next(); 380 | }); 381 | 382 | app.get('/user/:id', function(req, res){ 383 | res.end(typeof req.params.id); 384 | }); 385 | 386 | app.put('/user/:id', function(){}); 387 | app.get('/user/:id/edit', function(){}); 388 | app.post('/user', function(){}); 389 | }); 390 | 391 | var fn = router.match('/user/12')[0]; 392 | fn.keys.should.eql(['id']); 393 | fn.params.id.should.equal('12'); 394 | 395 | router.match('/').should.be.empty; 396 | router.match('/', 'GET').should.be.empty; 397 | router.match('/user/12', 'GET')[0].should.be.a('function'); 398 | router.match('/user/12/edit', 'GET').should.have.length(1); 399 | router.match('/user/12', 'GET').should.have.length(2); 400 | router.match('/user/12', 'PUT').should.have.length(1); 401 | router.match('/user', 'POST').should.have.length(1); 402 | router.match('/user').should.have.length(1); 403 | }, 404 | 405 | 'test .remove()': function(){ 406 | var router = connect.router(function(app){ 407 | app.get('/', function(req, res){}); 408 | app.get('/foo/bar', function(req, res){}); 409 | app.get('/bar', function(req, res){}); 410 | app.put('/bar', function(req, res){}); 411 | app.post('/bar', function(req, res){}); 412 | }); 413 | 414 | router.remove('/', 'GET'); 415 | router.remove('/bar'); 416 | router.lookup('/', 'GET').should.be.empty; 417 | router.lookup('/bar', 'GET').should.be.empty; 418 | router.lookup('/bar', 'PUT').should.be.empty; 419 | router.lookup('/bar', 'POST').should.be.empty; 420 | router.lookup('/foo/bar', 'GET').should.not.be.empty; 421 | } 422 | }; 423 | --------------------------------------------------------------------------------