├── 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 |
')
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 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 |
--------------------------------------------------------------------------------
.<%= comment.method %>()
26 | <% } %> 27 |<%- comment.description %>
28 | 29 | <% if (comment.body) { %> 30 |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 |