├── test ├── fixtures │ ├── foo bar │ ├── nums │ ├── .hidden │ ├── todo.txt │ └── users │ │ ├── tobi.txt │ │ └── index.html ├── mocha.opts ├── app.listen.js ├── responseTime.js ├── shared │ └── index.js ├── query.js ├── limit.js ├── exports.js ├── urlencoded.js ├── directory.js ├── utils.js ├── cookieParser.js ├── compress.js ├── csrf.js ├── patch.js ├── vhost.js ├── timeout.js ├── server.js ├── support │ └── http.js ├── basicAuth.js ├── mounting.js ├── json.js ├── bodyParser.js └── multipart.js ├── .travis.yml ├── lib ├── public │ ├── favicon.ico │ ├── icons │ │ ├── page.png │ │ ├── page_add.png │ │ ├── page_code.png │ │ ├── page_copy.png │ │ ├── page_edit.png │ │ ├── page_find.png │ │ ├── page_gear.png │ │ ├── page_go.png │ │ ├── page_key.png │ │ ├── page_link.png │ │ ├── page_red.png │ │ ├── page_save.png │ │ ├── page_word.png │ │ ├── page_attach.png │ │ ├── page_delete.png │ │ ├── page_error.png │ │ ├── page_excel.png │ │ ├── page_green.png │ │ ├── page_paste.png │ │ ├── page_white.png │ │ ├── page_world.png │ │ ├── page_lightning.png │ │ ├── page_refresh.png │ │ ├── page_white_add.png │ │ ├── page_white_c.png │ │ ├── page_white_cd.png │ │ ├── page_white_cup.png │ │ ├── page_white_dvd.png │ │ ├── page_white_get.png │ │ ├── page_white_go.png │ │ ├── page_white_h.png │ │ ├── page_white_key.png │ │ ├── page_white_php.png │ │ ├── page_white_put.png │ │ ├── page_white_tux.png │ │ ├── page_white_zip.png │ │ ├── page_paintbrush.png │ │ ├── page_white_code.png │ │ ├── page_white_copy.png │ │ ├── page_white_edit.png │ │ ├── page_white_error.png │ │ ├── page_white_excel.png │ │ ├── page_white_find.png │ │ ├── page_white_flash.png │ │ ├── page_white_gear.png │ │ ├── page_white_link.png │ │ ├── page_white_medal.png │ │ ├── page_white_paint.png │ │ ├── page_white_paste.png │ │ ├── page_white_ruby.png │ │ ├── page_white_stack.png │ │ ├── page_white_star.png │ │ ├── page_white_text.png │ │ ├── page_white_width.png │ │ ├── page_white_word.png │ │ ├── page_white_world.png │ │ ├── page_white_acrobat.png │ │ ├── page_white_camera.png │ │ ├── page_white_code_red.png │ │ ├── page_white_csharp.png │ │ ├── page_white_database.png │ │ ├── page_white_delete.png │ │ ├── page_white_freehand.png │ │ ├── page_white_magnify.png │ │ ├── page_white_office.png │ │ ├── page_white_picture.png │ │ ├── page_white_swoosh.png │ │ ├── page_white_vector.png │ │ ├── page_white_wrench.png │ │ ├── page_white_coldfusion.png │ │ ├── page_white_compressed.png │ │ ├── page_white_cplusplus.png │ │ ├── page_white_horizontal.png │ │ ├── page_white_lightning.png │ │ ├── page_white_paintbrush.png │ │ ├── page_white_powerpoint.png │ │ ├── page_white_text_width.png │ │ ├── page_white_actionscript.png │ │ └── page_white_visualstudio.png │ ├── error.html │ ├── directory.html │ └── style.css ├── middleware │ ├── responseTime.js │ ├── query.js │ ├── methodOverride.js │ ├── vhost.js │ ├── timeout.js │ ├── cookieParser.js │ ├── bodyParser.js │ ├── urlencoded.js │ ├── csrf.js │ ├── favicon.js │ ├── session │ │ ├── store.js │ │ ├── memory.js │ │ ├── cookie.js │ │ └── session.js │ ├── json.js │ ├── limit.js │ ├── static.js │ ├── errorHandler.js │ ├── basicAuth.js │ ├── multipart.js │ ├── cookieSession.js │ ├── compress.js │ ├── directory.js │ ├── staticCache.js │ └── logger.js ├── cache.js ├── patch.js ├── connect.js ├── index.js ├── proto.js └── utils.js ├── examples ├── public │ ├── tobi.jpeg │ └── form.html ├── favicon.js ├── logger.js ├── helloworld.js ├── logger.fast.js ├── static.js ├── profiler.js ├── error.js ├── basicAuth.js ├── mounting.js ├── upload.js ├── vhost.js ├── csrf.js ├── bodyParser.js ├── limit.js ├── cookieSession.js ├── logger.format.js └── session.js ├── index.js ├── .npmignore ├── .gitignore ├── support ├── bench ├── app.js ├── docs.js └── docs.jade ├── docs ├── docs.js └── style.css ├── Makefile ├── package.json ├── LICENSE └── Readme.md /test/fixtures/foo bar: -------------------------------------------------------------------------------- 1 | baz -------------------------------------------------------------------------------- /test/fixtures/nums: -------------------------------------------------------------------------------- 1 | 123456789 -------------------------------------------------------------------------------- /test/fixtures/.hidden: -------------------------------------------------------------------------------- 1 | I am hidden -------------------------------------------------------------------------------- /test/fixtures/todo.txt: -------------------------------------------------------------------------------- 1 | - groceries -------------------------------------------------------------------------------- /test/fixtures/users/tobi.txt: -------------------------------------------------------------------------------- 1 | ferret -------------------------------------------------------------------------------- /test/fixtures/users/index.html: -------------------------------------------------------------------------------- 1 |
tobi, loki, jane
-------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.8" 4 | - "0.10" -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --require test/support/http 3 | --growl -------------------------------------------------------------------------------- /lib/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/favicon.ico -------------------------------------------------------------------------------- /examples/public/tobi.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/examples/public/tobi.jpeg -------------------------------------------------------------------------------- /lib/public/icons/page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page.png -------------------------------------------------------------------------------- /lib/public/icons/page_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_add.png -------------------------------------------------------------------------------- /lib/public/icons/page_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_code.png -------------------------------------------------------------------------------- /lib/public/icons/page_copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_copy.png -------------------------------------------------------------------------------- /lib/public/icons/page_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_edit.png -------------------------------------------------------------------------------- /lib/public/icons/page_find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_find.png -------------------------------------------------------------------------------- /lib/public/icons/page_gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_gear.png -------------------------------------------------------------------------------- /lib/public/icons/page_go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_go.png -------------------------------------------------------------------------------- /lib/public/icons/page_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_key.png -------------------------------------------------------------------------------- /lib/public/icons/page_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_link.png -------------------------------------------------------------------------------- /lib/public/icons/page_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_red.png -------------------------------------------------------------------------------- /lib/public/icons/page_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_save.png -------------------------------------------------------------------------------- /lib/public/icons/page_word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_word.png -------------------------------------------------------------------------------- /lib/public/icons/page_attach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_attach.png -------------------------------------------------------------------------------- /lib/public/icons/page_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_delete.png -------------------------------------------------------------------------------- /lib/public/icons/page_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_error.png -------------------------------------------------------------------------------- /lib/public/icons/page_excel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_excel.png -------------------------------------------------------------------------------- /lib/public/icons/page_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_green.png -------------------------------------------------------------------------------- /lib/public/icons/page_paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_paste.png -------------------------------------------------------------------------------- /lib/public/icons/page_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white.png -------------------------------------------------------------------------------- /lib/public/icons/page_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_world.png -------------------------------------------------------------------------------- /lib/public/icons/page_lightning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_lightning.png -------------------------------------------------------------------------------- /lib/public/icons/page_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_refresh.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_add.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_c.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_cd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_cd.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_cup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_cup.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_dvd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_dvd.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_get.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_get.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_go.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_h.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_key.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_php.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_put.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_put.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_tux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_tux.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_zip.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = process.env.CONNECT_COV 3 | ? require('./lib-cov/connect') 4 | : require('./lib/connect'); -------------------------------------------------------------------------------- /lib/public/icons/page_paintbrush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_paintbrush.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_code.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_copy.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_edit.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_error.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_excel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_excel.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_find.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_flash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_flash.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_gear.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_link.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_medal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_medal.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_paint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_paint.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_paste.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_ruby.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_stack.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_star.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_text.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_width.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_word.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_world.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_acrobat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_acrobat.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_camera.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_code_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_code_red.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_csharp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_csharp.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_database.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_delete.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_freehand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_freehand.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_magnify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_magnify.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_office.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_office.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_picture.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_swoosh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_swoosh.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_vector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_vector.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_wrench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_wrench.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_coldfusion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_coldfusion.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_compressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_compressed.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_cplusplus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_cplusplus.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_horizontal.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_lightning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_lightning.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_paintbrush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_paintbrush.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_powerpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_powerpoint.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_text_width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_text_width.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_actionscript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_actionscript.png -------------------------------------------------------------------------------- /lib/public/icons/page_white_visualstudio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darobin/connect/master/lib/public/icons/page_white_visualstudio.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.markdown 2 | *.md 3 | .git* 4 | Makefile 5 | benchmarks/ 6 | docs/ 7 | examples/ 8 | install.sh 9 | support/ 10 | test/ 11 | .DS_Store 12 | coverage.html 13 | -------------------------------------------------------------------------------- /examples/favicon.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | // $ curl -i http://localhost:3000/favicon.ico 9 | 10 | connect.createServer( 11 | connect.favicon() 12 | ).listen(3000); -------------------------------------------------------------------------------- /examples/public/form.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/logger.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | // $ curl http://localhost:3000/favicon.ico 9 | 10 | connect.createServer( 11 | connect.logger() 12 | , connect.favicon() 13 | ).listen(3000); 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.html 2 | .DS_Store 3 | pids 4 | logs 5 | results 6 | *.pid 7 | *.gz 8 | *.log 9 | lib-cov 10 | test/fixtures/foo.bar.baz.css 11 | test/fixtures/style.css 12 | test/fixtures/script.js 13 | test.js 14 | docs/*.html 15 | docs/*.json 16 | node_modules 17 | .idea 18 | -------------------------------------------------------------------------------- /examples/helloworld.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | connect.createServer(function(req, res){ 9 | var body = 'Hello World'; 10 | res.setHeader('Content-Length', body.length); 11 | res.end(body); 12 | }).listen(3000); -------------------------------------------------------------------------------- /examples/logger.fast.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | // $ curl -i http://localhost:3000/favicon.ico 9 | // true defaults to 1000ms 10 | 11 | connect.createServer( 12 | connect.logger({ buffer: 5000 }) 13 | , connect.favicon() 14 | ).listen(3000); -------------------------------------------------------------------------------- /examples/static.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | connect( 9 | connect.static(__dirname + '/public', { maxAge: 0 }) 10 | , function(req, res) { 11 | res.setHeader('Content-Type', 'text/html'); 12 | res.end('
')
13 | }
14 | ).listen(3000);
--------------------------------------------------------------------------------
/lib/public/error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | thanks ' + req.body.name + '
'); 23 | res.write('thanks ' + req.body.name + '
'); 24 | res.write('hits: ' + n + '
' 33 | + '' 37 | + ''); 38 | } 39 | 40 | http.createServer(app).listen(3000); 41 | console.log('Server listening on port 3000'); 42 | -------------------------------------------------------------------------------- /lib/middleware/vhost.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - vhost 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Vhost: 11 | * 12 | * Setup vhost for the given `hostname` and `server`. 13 | * 14 | * connect() 15 | * .use(connect.vhost('foo.com', fooApp)) 16 | * .use(connect.vhost('bar.com', barApp)) 17 | * .use(connect.vhost('*.com', mainApp)) 18 | * 19 | * The `server` may be a Connect server or 20 | * a regular Node `http.Server`. 21 | * 22 | * @param {String} hostname 23 | * @param {Server} server 24 | * @return {Function} 25 | * @api public 26 | */ 27 | 28 | module.exports = function vhost(hostname, server){ 29 | if (!hostname) throw new Error('vhost hostname required'); 30 | if (!server) throw new Error('vhost server required'); 31 | var regexp = new RegExp('^' + hostname.replace(/[^*\w]/g, '\\$&').replace(/[*]/g, '(?:.*?)') + '$', 'i'); 32 | if (server.onvhost) server.onvhost(hostname); 33 | return function vhost(req, res, next){ 34 | if (!req.headers.host) return next(); 35 | var host = req.headers.host.split(':')[0]; 36 | if (!regexp.test(host)) return next(); 37 | if ('function' == typeof server) return server(req, res, next); 38 | server.emit('request', req, res); 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /support/docs.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Connect - High quality middleware for node.js 5 | meta(http-equiv="Content-Type", content="text/html; charset=utf-8") 6 | link(rel='stylesheet', href='style.css') 7 | script(src='jquery.js') 8 | script(src='docs.js') 9 | body 10 | #content 11 | h1 Connect 12 | for comment in comments 13 | unless ignore(comment) 14 | .comment(id=id(comment)) 15 | h2= title(comment) 16 | .description!= comment.description.full 17 | 18 | if comment.tags.length 19 | ul.tags 20 | for tag in comment.tags 21 | if tag.types 22 | if 'param' == tag.type 23 | li #{tag.types.join(' | ')} #{tag.name} #{tag.description} 24 | else 25 | li returns #{tag.types.join(' | ')} #{tag.description} 26 | else if tag.name 27 | li #{tag.name} #{tag.description} 28 | 29 | if comment.code 30 | h3 Source 31 | pre 32 | code!= comment.code 33 | 34 | ul#menu 35 | for comment in comments 36 | unless ignore(comment) 37 | li 38 | a(href='#' + id(comment))= title(comment) 39 | -------------------------------------------------------------------------------- /lib/middleware/timeout.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Connect - timeout 3 | * Ported from https://github.com/LearnBoost/connect-timeout 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var debug = require('debug')('connect:timeout'); 12 | 13 | /** 14 | * Timeout: 15 | * 16 | * Times out the request in `ms`, defaulting to `5000`. The 17 | * method `req.clearTimeout()` is added to revert this behaviour 18 | * programmatically within your application's middleware, routes, etc. 19 | * 20 | * The timeout error is passed to `next()` so that you may customize 21 | * the response behaviour. This error has the `.timeout` property as 22 | * well as `.status == 503`. 23 | * 24 | * @param {Number} ms 25 | * @return {Function} 26 | * @api public 27 | */ 28 | 29 | module.exports = function timeout(ms) { 30 | ms = ms || 5000; 31 | 32 | return function(req, res, next) { 33 | var id = setTimeout(function(){ 34 | req.emit('timeout', ms); 35 | }, ms); 36 | 37 | req.on('timeout', function(){ 38 | if (res.headerSent) return debug('response started, cannot timeout'); 39 | var err = new Error('Response timeout'); 40 | err.timeout = ms; 41 | err.status = 503; 42 | next(err); 43 | }); 44 | 45 | req.clearTimeout = function(){ 46 | clearTimeout(id); 47 | }; 48 | 49 | res.on('header', function(){ 50 | clearTimeout(id); 51 | }); 52 | 53 | next(); 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /test/directory.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('..'); 3 | 4 | var app = connect(); 5 | app.use(connect.directory('.')); 6 | 7 | describe('directory()', function(){ 8 | describe('when Accept: application/json is given', function(){ 9 | it('should respond with json', function(done){ 10 | app.request() 11 | .get('/') 12 | .set('Accept', 'application/json') 13 | .end(function(res){ 14 | var arr = JSON.parse(res.body); 15 | arr.should.include('lib'); 16 | arr.should.include('node_modules'); 17 | arr.should.include('docs'); 18 | arr.should.include('Readme.md'); 19 | done(); 20 | }); 21 | }) 22 | }) 23 | 24 | describe('when Accept: text/html is given', function(){ 25 | it('should respond with json', function(done){ 26 | app.request() 27 | .get('/') 28 | .set('Accept', 'text/html') 29 | .end(function(res){ 30 | res.body.should.include(' this.limit) this.remove(this.keys.shift()); 77 | 78 | var arr = this.store[key] = []; 79 | arr.createdAt = new Date; 80 | return arr; 81 | }; 82 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../') 3 | , utils = connect.utils; 4 | 5 | describe('utils.uid(len)', function(){ 6 | it('should generate a uid of the given length', function(){ 7 | var n = 20; 8 | while (n--) utils.uid(n).should.have.length(n); 9 | utils.uid(10).should.not.equal(utils.uid(10)); 10 | }) 11 | }) 12 | 13 | describe('utils.parseCacheControl(str)', function(){ 14 | it('should parse Cache-Control', function(){ 15 | var parse = utils.parseCacheControl; 16 | parse('no-cache').should.eql({ 'no-cache': true }); 17 | parse('no-store').should.eql({ 'no-store': true }); 18 | parse('no-transform').should.eql({ 'no-transform': true }); 19 | parse('only-if-cached').should.eql({ 'only-if-cached': true }); 20 | parse('max-age=0').should.eql({ 'max-age': 0 }); 21 | parse('max-age=60').should.eql({ 'max-age': 60 }); 22 | parse('max-stale=60').should.eql({ 'max-stale': 60 }); 23 | parse('min-fresh=60').should.eql({ 'min-fresh': 60 }); 24 | parse('public, max-age=60').should.eql({ 'public': true, 'max-age': 60 }); 25 | parse('must-revalidate, max-age=60').should.eql({ 'must-revalidate': true, 'max-age': 60 }); 26 | }) 27 | }) 28 | 29 | describe('utils.mime(req)', function(){ 30 | it('should return the mime-type from Content-Type', function(){ 31 | utils.mime({ headers: { 'content-type': 'text/html; charset=utf8' }}) 32 | .should.equal('text/html'); 33 | 34 | utils.mime({ headers: { 'content-type': 'text/html; charset=utf8' }}) 35 | .should.equal('text/html'); 36 | 37 | utils.mime({ headers: { 'content-type': 'text/html' }}) 38 | .should.equal('text/html'); 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /lib/middleware/cookieParser.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - cookieParser 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var utils = require('./../utils') 14 | , cookie = require('cookie'); 15 | 16 | /** 17 | * Cookie parser: 18 | * 19 | * Parse _Cookie_ header and populate `req.cookies` 20 | * with an object keyed by the cookie names. Optionally 21 | * you may enabled signed cookie support by passing 22 | * a `secret` string, which assigns `req.secret` so 23 | * it may be used by other middleware. 24 | * 25 | * Examples: 26 | * 27 | * connect() 28 | * .use(connect.cookieParser('optional secret string')) 29 | * .use(function(req, res, next){ 30 | * res.end(JSON.stringify(req.cookies)); 31 | * }) 32 | * 33 | * @param {String} secret 34 | * @return {Function} 35 | * @api public 36 | */ 37 | 38 | module.exports = function cookieParser(secret){ 39 | return function cookieParser(req, res, next) { 40 | if (req.cookies) return next(); 41 | var cookies = req.headers.cookie; 42 | 43 | req.secret = secret; 44 | req.cookies = {}; 45 | req.signedCookies = {}; 46 | 47 | if (cookies) { 48 | try { 49 | req.cookies = cookie.parse(cookies); 50 | if (secret) { 51 | req.signedCookies = utils.parseSignedCookies(req.cookies, secret); 52 | req.signedCookies = utils.parseJSONCookies(req.signedCookies); 53 | } 54 | req.cookies = utils.parseJSONCookies(req.cookies); 55 | } catch (err) { 56 | err.status = 400; 57 | return next(err); 58 | } 59 | } 60 | next(); 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /examples/logger.format.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var connect = require('../'); 7 | 8 | // $ curl http://localhost:3000/ 9 | 10 | // custom format string 11 | 12 | connect.createServer( 13 | connect.logger(':method :url - :res[content-type]') 14 | , function(req, res){ 15 | res.statusCode = 500; 16 | res.setHeader('Content-Type', 'text/plain'); 17 | res.end('Internal Server Error'); 18 | } 19 | ).listen(3000); 20 | 21 | // $ curl http://localhost:3001/ 22 | // $ curl http://localhost:3001/302 23 | // $ curl http://localhost:3001/404 24 | // $ curl http://localhost:3001/500 25 | 26 | connect() 27 | .use(connect.logger('dev')) 28 | .use('/connect', connect.static(__dirname + '/lib')) 29 | .use('/connect', connect.directory(__dirname + '/lib')) 30 | .use(function(req, res, next){ 31 | switch (req.url) { 32 | case '/500': 33 | var body = 'Internal Server Error'; 34 | res.statusCode = 500; 35 | res.setHeader('Content-Length', body.length); 36 | res.end(body); 37 | break; 38 | case '/404': 39 | var body = 'Not Found'; 40 | res.statusCode = 404; 41 | res.setHeader('Content-Length', body.length); 42 | res.end(body); 43 | break; 44 | case '/302': 45 | var body = 'Found'; 46 | res.statusCode = 302; 47 | res.setHeader('Content-Length', body.length); 48 | res.end(body); 49 | break; 50 | default: 51 | var body = 'OK'; 52 | res.setHeader('Content-Length', body.length); 53 | res.end(body); 54 | } 55 | }) 56 | .listen(3001); 57 | 58 | // pre-defined 59 | 60 | connect() 61 | .use(connect.logger('short')) 62 | .listen(3002); -------------------------------------------------------------------------------- /lib/middleware/bodyParser.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - bodyParser 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var multipart = require('./multipart') 14 | , urlencoded = require('./urlencoded') 15 | , json = require('./json'); 16 | 17 | /** 18 | * Body parser: 19 | * 20 | * Parse request bodies, supports _application/json_, 21 | * _application/x-www-form-urlencoded_, and _multipart/form-data_. 22 | * 23 | * This is equivalent to: 24 | * 25 | * app.use(connect.json()); 26 | * app.use(connect.urlencoded()); 27 | * app.use(connect.multipart()); 28 | * 29 | * Examples: 30 | * 31 | * connect() 32 | * .use(connect.bodyParser()) 33 | * .use(function(req, res) { 34 | * res.end('viewing user ' + req.body.user.name); 35 | * }); 36 | * 37 | * $ curl -d 'user[name]=tj' http://local/ 38 | * $ curl -d '{"user":{"name":"tj"}}' -H "Content-Type: application/json" http://local/ 39 | * 40 | * View [json](json.html), [urlencoded](urlencoded.html), and [multipart](multipart.html) for more info. 41 | * 42 | * @param {Object} options 43 | * @return {Function} 44 | * @api public 45 | */ 46 | 47 | exports = module.exports = function bodyParser(options){ 48 | var _urlencoded = urlencoded(options) 49 | , _multipart = multipart(options) 50 | , _json = json(options); 51 | 52 | return function bodyParser(req, res, next) { 53 | _json(req, res, function(err){ 54 | if (err) return next(err); 55 | _urlencoded(req, res, function(err){ 56 | if (err) return next(err); 57 | _multipart(req, res, next); 58 | }); 59 | }); 60 | } 61 | }; -------------------------------------------------------------------------------- /lib/middleware/urlencoded.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - urlencoded 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var utils = require('../utils') 14 | , _limit = require('./limit') 15 | , qs = require('qs'); 16 | 17 | /** 18 | * noop middleware. 19 | */ 20 | 21 | function noop(req, res, next) { 22 | next(); 23 | } 24 | 25 | /** 26 | * Urlencoded: 27 | * 28 | * Parse x-ww-form-urlencoded request bodies, 29 | * providing the parsed object as `req.body`. 30 | * 31 | * Options: 32 | * 33 | * - `limit` byte limit disabled by default 34 | * 35 | * @param {Object} options 36 | * @return {Function} 37 | * @api public 38 | */ 39 | 40 | exports = module.exports = function(options){ 41 | options = options || {}; 42 | 43 | var limit = options.limit 44 | ? _limit(options.limit) 45 | : noop; 46 | 47 | return function urlencoded(req, res, next) { 48 | if (req._body) return next(); 49 | req.body = req.body || {}; 50 | 51 | if (!utils.hasBody(req)) return next(); 52 | 53 | // check Content-Type 54 | if ('application/x-www-form-urlencoded' != utils.mime(req)) return next(); 55 | 56 | // flag as parsed 57 | req._body = true; 58 | 59 | // parse 60 | limit(req, res, function(err){ 61 | if (err) return next(err); 62 | var buf = ''; 63 | req.setEncoding('utf8'); 64 | req.on('data', function(chunk){ buf += chunk }); 65 | req.on('end', function(){ 66 | try { 67 | req.body = buf.length 68 | ? qs.parse(buf, options) 69 | : {}; 70 | next(); 71 | } catch (err){ 72 | err.body = buf; 73 | next(err); 74 | } 75 | }); 76 | }); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /lib/patch.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var http = require('http') 13 | , res = http.ServerResponse.prototype 14 | , setHeader = res.setHeader 15 | , _renderHeaders = res._renderHeaders 16 | , writeHead = res.writeHead; 17 | 18 | // apply only once 19 | 20 | if (!res._hasConnectPatch) { 21 | 22 | /** 23 | * Provide a public "header sent" flag 24 | * until node does. 25 | * 26 | * @return {Boolean} 27 | * @api public 28 | */ 29 | 30 | res.__defineGetter__('headerSent', function(){ 31 | return this._header; 32 | }); 33 | 34 | /** 35 | * Set header `field` to `val`, special-casing 36 | * the `Set-Cookie` field for multiple support. 37 | * 38 | * @param {String} field 39 | * @param {String} val 40 | * @api public 41 | */ 42 | 43 | res.setHeader = function(field, val){ 44 | var key = field.toLowerCase() 45 | , prev; 46 | 47 | // special-case Set-Cookie 48 | if (this._headers && 'set-cookie' == key) { 49 | if (prev = this.getHeader(field)) { 50 | val = Array.isArray(prev) 51 | ? prev.concat(val) 52 | : [prev, val]; 53 | } 54 | // charset 55 | } else if ('content-type' == key && this.charset) { 56 | val += '; charset=' + this.charset; 57 | } 58 | 59 | return setHeader.call(this, field, val); 60 | }; 61 | 62 | /** 63 | * Proxy to emit "header" event. 64 | */ 65 | 66 | res._renderHeaders = function(){ 67 | if (!this._emittedHeader) this.emit('header'); 68 | this._emittedHeader = true; 69 | return _renderHeaders.call(this); 70 | }; 71 | 72 | res.writeHead = function(){ 73 | if (!this._emittedHeader) this.emit('header'); 74 | this._emittedHeader = true; 75 | return writeHead.apply(this, arguments); 76 | }; 77 | 78 | res._hasConnectPatch = true; 79 | } 80 | -------------------------------------------------------------------------------- /lib/connect.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Connect 3 | * Copyright(c) 2010 Sencha Inc. 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var EventEmitter = require('events').EventEmitter 13 | , proto = require('./proto') 14 | , utils = require('./utils') 15 | , path = require('path') 16 | , basename = path.basename 17 | , fs = require('fs'); 18 | 19 | // node patches 20 | 21 | require('./patch'); 22 | 23 | // expose createServer() as the module 24 | 25 | exports = module.exports = createServer; 26 | 27 | /** 28 | * Framework version. 29 | */ 30 | 31 | exports.version = '2.7.11'; 32 | 33 | /** 34 | * Expose mime module. 35 | */ 36 | 37 | exports.mime = require('./middleware/static').mime; 38 | 39 | /** 40 | * Expose the prototype. 41 | */ 42 | 43 | exports.proto = proto; 44 | 45 | /** 46 | * Auto-load middleware getters. 47 | */ 48 | 49 | exports.middleware = {}; 50 | 51 | /** 52 | * Expose utilities. 53 | */ 54 | 55 | exports.utils = utils; 56 | 57 | /** 58 | * Create a new connect server. 59 | * 60 | * @return {Function} 61 | * @api public 62 | */ 63 | 64 | function createServer() { 65 | function app(req, res, next){ app.handle(req, res, next); } 66 | utils.merge(app, proto); 67 | utils.merge(app, EventEmitter.prototype); 68 | app.route = '/'; 69 | app.stack = []; 70 | for (var i = 0; i < arguments.length; ++i) { 71 | app.use(arguments[i]); 72 | } 73 | return app; 74 | }; 75 | 76 | /** 77 | * Support old `.createServer()` method. 78 | */ 79 | 80 | createServer.createServer = createServer; 81 | 82 | /** 83 | * Auto-load bundled middleware with getters. 84 | */ 85 | 86 | fs.readdirSync(__dirname + '/middleware').forEach(function(filename){ 87 | if (!/\.js$/.test(filename)) return; 88 | var name = basename(filename, '.js'); 89 | function load(){ return require('./middleware/' + name); } 90 | exports.middleware.__defineGetter__(name, load); 91 | exports.__defineGetter__(name, load); 92 | }); 93 | -------------------------------------------------------------------------------- /lib/middleware/csrf.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Connect - csrf 3 | * Copyright(c) 2011 Sencha Inc. 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var utils = require('../utils'); 12 | 13 | /** 14 | * Anti CSRF: 15 | * 16 | * CRSF protection middleware. 17 | * 18 | * By default this middleware generates a token named "_csrf" 19 | * which should be added to requests which mutate 20 | * state, within a hidden form field, query-string etc. This 21 | * token is validated against the visitor's `req.session._csrf` 22 | * property. 23 | * 24 | * The default `value` function checks `req.body` generated 25 | * by the `bodyParser()` middleware, `req.query` generated 26 | * by `query()`, and the "X-CSRF-Token" header field. 27 | * 28 | * This middleware requires session support, thus should be added 29 | * somewhere _below_ `session()` and `cookieParser()`. 30 | * 31 | * Options: 32 | * 33 | * - `value` a function accepting the request, returning the token 34 | * 35 | * @param {Object} options 36 | * @api public 37 | */ 38 | 39 | module.exports = function csrf(options) { 40 | options = options || {}; 41 | var value = options.value || defaultValue; 42 | 43 | return function(req, res, next){ 44 | // generate CSRF token 45 | var token = req.session._csrf || (req.session._csrf = utils.uid(24)); 46 | 47 | // ignore these methods 48 | if ('GET' == req.method || 'HEAD' == req.method || 'OPTIONS' == req.method) return next(); 49 | 50 | // determine value 51 | var val = value(req); 52 | 53 | // check 54 | if (val != token) return next(utils.error(403)); 55 | 56 | next(); 57 | } 58 | }; 59 | 60 | /** 61 | * Default value function, checking the `req.body` 62 | * and `req.query` for the CSRF token. 63 | * 64 | * @param {IncomingMessage} req 65 | * @return {String} 66 | * @api private 67 | */ 68 | 69 | function defaultValue(req) { 70 | return (req.body && req.body._csrf) 71 | || (req.query && req.query._csrf) 72 | || (req.headers['x-csrf-token']); 73 | } 74 | -------------------------------------------------------------------------------- /lib/middleware/favicon.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Connect - favicon 3 | * Copyright(c) 2010 Sencha Inc. 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var fs = require('fs') 13 | , utils = require('../utils'); 14 | 15 | /** 16 | * Favicon: 17 | * 18 | * By default serves the connect favicon, or the favicon 19 | * located by the given `path`. 20 | * 21 | * Options: 22 | * 23 | * - `maxAge` cache-control max-age directive, defaulting to 1 day 24 | * 25 | * Examples: 26 | * 27 | * Serve default favicon: 28 | * 29 | * connect() 30 | * .use(connect.favicon()) 31 | * 32 | * Serve favicon before logging for brevity: 33 | * 34 | * connect() 35 | * .use(connect.favicon()) 36 | * .use(connect.logger('dev')) 37 | * 38 | * Serve custom favicon: 39 | * 40 | * connect() 41 | * .use(connect.favicon('public/favicon.ico')) 42 | * 43 | * @param {String} path 44 | * @param {Object} options 45 | * @return {Function} 46 | * @api public 47 | */ 48 | 49 | module.exports = function favicon(path, options){ 50 | var options = options || {} 51 | , path = path || __dirname + '/../public/favicon.ico' 52 | , maxAge = options.maxAge || 86400000 53 | , icon; // favicon cache 54 | 55 | return function favicon(req, res, next){ 56 | if ('/favicon.ico' == req.url) { 57 | if (icon) { 58 | res.writeHead(200, icon.headers); 59 | res.end(icon.body); 60 | } else { 61 | fs.readFile(path, function(err, buf){ 62 | if (err) return next(err); 63 | icon = { 64 | headers: { 65 | 'Content-Type': 'image/x-icon' 66 | , 'Content-Length': buf.length 67 | , 'ETag': '"' + utils.md5(buf) + '"' 68 | , 'Cache-Control': 'public, max-age=' + (maxAge / 1000) 69 | }, 70 | body: buf 71 | }; 72 | res.writeHead(200, icon.headers); 73 | res.end(icon.body); 74 | }); 75 | } 76 | } else { 77 | next(); 78 | } 79 | }; 80 | }; 81 | -------------------------------------------------------------------------------- /lib/middleware/session/store.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - session - Store 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var EventEmitter = require('events').EventEmitter 14 | , Session = require('./session') 15 | , Cookie = require('./cookie'); 16 | 17 | /** 18 | * Initialize abstract `Store`. 19 | * 20 | * @api private 21 | */ 22 | 23 | var Store = module.exports = function Store(options){}; 24 | 25 | /** 26 | * Inherit from `EventEmitter.prototype`. 27 | */ 28 | 29 | Store.prototype.__proto__ = EventEmitter.prototype; 30 | 31 | /** 32 | * Re-generate the given requests's session. 33 | * 34 | * @param {IncomingRequest} req 35 | * @return {Function} fn 36 | * @api public 37 | */ 38 | 39 | Store.prototype.regenerate = function(req, fn){ 40 | var self = this; 41 | this.destroy(req.sessionID, function(err){ 42 | self.generate(req); 43 | fn(err); 44 | }); 45 | }; 46 | 47 | /** 48 | * Load a `Session` instance via the given `sid` 49 | * and invoke the callback `fn(err, sess)`. 50 | * 51 | * @param {String} sid 52 | * @param {Function} fn 53 | * @api public 54 | */ 55 | 56 | Store.prototype.load = function(sid, fn){ 57 | var self = this; 58 | this.get(sid, function(err, sess){ 59 | if (err) return fn(err); 60 | if (!sess) return fn(); 61 | var req = { sessionID: sid, sessionStore: self }; 62 | sess = self.createSession(req, sess); 63 | fn(null, sess); 64 | }); 65 | }; 66 | 67 | /** 68 | * Create session from JSON `sess` data. 69 | * 70 | * @param {IncomingRequest} req 71 | * @param {Object} sess 72 | * @return {Session} 73 | * @api private 74 | */ 75 | 76 | Store.prototype.createSession = function(req, sess){ 77 | var expires = sess.cookie.expires 78 | , orig = sess.cookie.originalMaxAge; 79 | sess.cookie = new Cookie(sess.cookie); 80 | if ('string' == typeof expires) sess.cookie.expires = new Date(expires); 81 | sess.cookie.originalMaxAge = orig; 82 | req.session = new Session(req, sess); 83 | return req.session; 84 | }; 85 | -------------------------------------------------------------------------------- /test/cookieParser.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('..') 3 | , signature = require('cookie-signature'); 4 | 5 | var app = connect(); 6 | 7 | app.use(connect.cookieParser('keyboard cat')); 8 | 9 | app.use(function(req, res, next){ 10 | if ('/signed' != req.url) return next(); 11 | res.end(JSON.stringify(req.signedCookies)); 12 | }); 13 | 14 | app.use(function(req, res, next){ 15 | res.end(JSON.stringify(req.cookies)); 16 | }); 17 | 18 | describe('connect.cookieParser()', function(){ 19 | describe('when no cookies are sent', function(){ 20 | it('should default req.cookies to {}', function(done){ 21 | app.request() 22 | .get('/') 23 | .expect('{}', done); 24 | }) 25 | 26 | it('should default req.signedCookies to {}', function(done){ 27 | app.request() 28 | .get('/') 29 | .expect('{}', done); 30 | }) 31 | }) 32 | 33 | describe('when cookies are sent', function(){ 34 | it('should populate req.cookies', function(done){ 35 | app.request() 36 | .get('/') 37 | .set('Cookie', 'foo=bar; bar=baz') 38 | .expect('{"foo":"bar","bar":"baz"}', done); 39 | }) 40 | }) 41 | 42 | describe('when a secret is given', function(){ 43 | var val = signature.sign('foobarbaz', 'keyboard cat'); 44 | // TODO: "bar" fails... 45 | 46 | it('should populate req.signedCookies', function(done){ 47 | app.request() 48 | .get('/signed') 49 | .set('Cookie', 'foo=s:' + val) 50 | .expect('{"foo":"foobarbaz"}', done); 51 | }) 52 | 53 | it('should remove the signed value from req.cookies', function(done){ 54 | app.request() 55 | .get('/') 56 | .set('Cookie', 'foo=s:' + val) 57 | .expect('{}', done); 58 | }) 59 | 60 | it('should omit invalid signatures', function(done){ 61 | app.request() 62 | .get('/signed') 63 | .set('Cookie', 'foo=' + val + '3') 64 | .expect('{}', function(){ 65 | app.request() 66 | .get('/') 67 | .set('Cookie', 'foo=' + val + '3') 68 | .expect('{"foo":"foobarbaz.CP7AWaXDfAKIRfH49dQzKJx7sKzzSoPq7/AcBBRVwlI3"}', done); 69 | }); 70 | }) 71 | }) 72 | }) -------------------------------------------------------------------------------- /test/compress.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | 4 | var fixtures = __dirname + '/fixtures'; 5 | 6 | var app = connect(); 7 | app.use(connect.compress()); 8 | app.use(connect.static(fixtures)); 9 | 10 | describe('connect.compress()', function(){ 11 | it('should gzip files', function(done){ 12 | app.request() 13 | .get('/todo.txt') 14 | .set('Accept-Encoding', 'gzip') 15 | .end(function(res){ 16 | res.body.should.not.equal('- groceries'); 17 | done(); 18 | }); 19 | }) 20 | 21 | it('should set Content-Encoding', function(done){ 22 | app.request() 23 | .get('/todo.txt') 24 | .set('Accept-Encoding', 'gzip') 25 | .expect('Content-Encoding', 'gzip', done); 26 | }) 27 | 28 | it('should support HEAD', function(done){ 29 | app.request() 30 | .head('/todo.txt') 31 | .set('Accept-Encoding', 'gzip') 32 | .expect('', done); 33 | }) 34 | 35 | it('should support conditional GETs', function(done){ 36 | app.request() 37 | .get('/todo.txt') 38 | .set('Accept-Encoding', 'gzip') 39 | .end(function(res){ 40 | var date = res.headers['last-modified']; 41 | app.request() 42 | .get('/todo.txt') 43 | .set('Accept-Encoding', 'gzip') 44 | .set('If-Modified-Since', date) 45 | .expect(304, done); 46 | }); 47 | }) 48 | 49 | it('should set Vary', function(done){ 50 | app.request() 51 | .get('/todo.txt') 52 | .set('Accept-Encoding', 'gzip') 53 | .expect('Vary', 'Accept-Encoding', done); 54 | }) 55 | 56 | it('should set Vary at all times', function(done){ 57 | app.request() 58 | .get('/todo.txt') 59 | .expect('Vary', 'Accept-Encoding', done); 60 | }) 61 | 62 | it('should transfer chunked', function(done){ 63 | app.request() 64 | .get('/todo.txt') 65 | .set('Accept-Encoding', 'gzip') 66 | .expect('Transfer-Encoding', 'chunked', done); 67 | }) 68 | 69 | it('should remove Content-Length for chunked', function(done){ 70 | app.request() 71 | .get('/todo.txt') 72 | .set('Accept-Encoding', 'gzip') 73 | .end(function(res){ 74 | res.headers.should.not.have.property('content-length'); 75 | done() 76 | }); 77 | }) 78 | 79 | }) -------------------------------------------------------------------------------- /test/csrf.js: -------------------------------------------------------------------------------- 1 | var connect = require('../'); 2 | 3 | describe('csrf', function(){ 4 | it('should work with a valid token', function(done){ 5 | var app = connect(); 6 | 7 | app.use(connect.cookieParser()) 8 | app.use(connect.session({ secret: 'greg' })); 9 | app.use(connect.bodyParser()); 10 | app.use(connect.csrf()); 11 | app.use(function(req, res){ 12 | res.end(req.session._csrf || 'none'); 13 | }); 14 | 15 | app.request() 16 | .get('/') 17 | .end(function(res){ 18 | var token = res.body; 19 | 20 | app.request() 21 | .post('/') 22 | .set('Cookie', res.headers['set-cookie'][0]) 23 | .set('X-CSRF-Token', token) 24 | .end(function(res){ 25 | res.statusCode.should.equal(200) 26 | done(); 27 | }); 28 | }); 29 | }); 30 | 31 | it('should fail with an invalid token', function(done){ 32 | var app = connect(); 33 | 34 | app.use(connect.cookieParser()); 35 | app.use(connect.session({ secret: 'greg' })); 36 | app.use(connect.bodyParser()); 37 | app.use(connect.csrf()); 38 | app.use(function(req, res){ 39 | res.end(req.session._csrf || 'none'); 40 | }); 41 | 42 | app.request() 43 | .get('/') 44 | .end(function(res){ 45 | app.request() 46 | .post('/') 47 | .set('Cookie', res.headers['set-cookie'][0]) 48 | .set('X-CSRF-Token', '42') 49 | .end(function(res){ 50 | res.statusCode.should.equal(403) 51 | done(); 52 | }); 53 | }); 54 | }); 55 | 56 | it('should fail with no token', function(done){ 57 | var app = connect(); 58 | 59 | app.use(connect.cookieParser()); 60 | app.use(connect.session({ secret: 'greg' })); 61 | app.use(connect.bodyParser()); 62 | app.use(connect.csrf()); 63 | app.use(function(req, res){ 64 | res.end(req.session._csrf || 'none'); 65 | }); 66 | 67 | app.request() 68 | .get('/') 69 | .end(function(res){ 70 | app.request() 71 | .set('Cookie', res.headers['set-cookie'][0]) 72 | .post('/') 73 | .end(function(res){ 74 | res.statusCode.should.equal(403); 75 | done(); 76 | }); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /lib/middleware/json.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - json 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var utils = require('../utils') 14 | , _limit = require('./limit'); 15 | 16 | /** 17 | * noop middleware. 18 | */ 19 | 20 | function noop(req, res, next) { 21 | next(); 22 | } 23 | 24 | /** 25 | * JSON: 26 | * 27 | * Parse JSON request bodies, providing the 28 | * parsed object as `req.body`. 29 | * 30 | * Options: 31 | * 32 | * - `strict` when `false` anything `JSON.parse()` accepts will be parsed 33 | * - `reviver` used as the second "reviver" argument for JSON.parse 34 | * - `limit` byte limit disabled by default 35 | * 36 | * @param {Object} options 37 | * @return {Function} 38 | * @api public 39 | */ 40 | 41 | exports = module.exports = function(options){ 42 | var options = options || {} 43 | , strict = options.strict !== false; 44 | 45 | var limit = options.limit 46 | ? _limit(options.limit) 47 | : noop; 48 | 49 | return function json(req, res, next) { 50 | if (req._body) return next(); 51 | req.body = req.body || {}; 52 | 53 | if (!utils.hasBody(req)) return next(); 54 | 55 | // check Content-Type 56 | if ('application/json' != utils.mime(req)) return next(); 57 | 58 | // flag as parsed 59 | req._body = true; 60 | 61 | // parse 62 | limit(req, res, function(err){ 63 | if (err) return next(err); 64 | var buf = ''; 65 | req.setEncoding('utf8'); 66 | req.on('data', function(chunk){ buf += chunk }); 67 | req.on('end', function(){ 68 | var first = buf.trim()[0]; 69 | 70 | if (0 == buf.length) { 71 | return next(utils.error(400, 'invalid json, empty body')); 72 | } 73 | 74 | if (strict && '{' != first && '[' != first) return next(utils.error(400, 'invalid json')); 75 | try { 76 | req.body = JSON.parse(buf, options.reviver); 77 | } catch (err){ 78 | err.body = buf; 79 | err.status = 400; 80 | return next(err); 81 | } 82 | next(); 83 | }); 84 | }); 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /test/patch.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | 4 | describe('patch', function(){ 5 | describe('"header" event', function(){ 6 | describe('with .setHeader()', function(){ 7 | it('should be emitted', function(done){ 8 | var app = connect(); 9 | 10 | app.use(function(req, res, next){ 11 | res.on('header', function(){ 12 | res.setHeader('bar', 'baz'); 13 | }); 14 | 15 | next(); 16 | }); 17 | 18 | app.use(function(req, res){ 19 | res.setHeader('foo', 'bar'); 20 | res.end(); 21 | }) 22 | 23 | app.request() 24 | .get('/') 25 | .end(function(res){ 26 | res.should.have.header('foo', 'bar'); 27 | res.should.have.header('bar', 'baz'); 28 | done(); 29 | }); 30 | }) 31 | }) 32 | 33 | describe('with .writeHead()', function(){ 34 | it('should be emitted', function(done){ 35 | var app = connect(); 36 | 37 | app.use(function(req, res, next){ 38 | res.on('header', function(){ 39 | res.setHeader('bar', 'baz'); 40 | }); 41 | 42 | next(); 43 | }); 44 | 45 | app.use(function(req, res){ 46 | res.writeHead(200, { foo: 'bar' }); 47 | res.end(); 48 | }) 49 | 50 | app.request() 51 | .get('/') 52 | .end(function(res){ 53 | res.should.have.header('foo', 'bar'); 54 | res.should.have.header('bar', 'baz'); 55 | done(); 56 | }); 57 | }) 58 | }) 59 | 60 | describe('with .end() only', function(){ 61 | it('should be emitted', function(done){ 62 | var app = connect(); 63 | 64 | app.use(function(req, res, next){ 65 | res.on('header', function(){ 66 | res.setHeader('bar', 'baz'); 67 | }); 68 | 69 | next(); 70 | }); 71 | 72 | app.use(function(req, res){ 73 | res.end(); 74 | }) 75 | 76 | app.request() 77 | .get('/') 78 | .end(function(res){ 79 | res.should.have.header('bar', 'baz'); 80 | done(); 81 | }); 82 | }) 83 | }) 84 | 85 | }) 86 | }) -------------------------------------------------------------------------------- /lib/middleware/limit.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - limit 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var utils = require('../utils'), 13 | brokenPause = utils.brokenPause; 14 | 15 | /** 16 | * Limit: 17 | * 18 | * Limit request bodies to the given size in `bytes`. 19 | * 20 | * A string representation of the bytesize may also be passed, 21 | * for example "5mb", "200kb", "1gb", etc. 22 | * 23 | * connect() 24 | * .use(connect.limit('5.5mb')) 25 | * .use(handleImageUpload) 26 | * 27 | * @param {Number|String} bytes 28 | * @return {Function} 29 | * @api public 30 | */ 31 | 32 | module.exports = function limit(bytes){ 33 | if ('string' == typeof bytes) bytes = utils.parseBytes(bytes); 34 | if ('number' != typeof bytes) throw new Error('limit() bytes required'); 35 | return function limit(req, res, next){ 36 | var received = 0 37 | , len = req.headers['content-length'] 38 | ? parseInt(req.headers['content-length'], 10) 39 | : null; 40 | 41 | // self-awareness 42 | if (req._limit) return next(); 43 | req._limit = true; 44 | 45 | // limit by content-length 46 | if (len && len > bytes) return next(utils.error(413)); 47 | 48 | // limit 49 | if (brokenPause) { 50 | listen(); 51 | } else { 52 | req.on('newListener', function handler(event) { 53 | if (event !== 'data') return; 54 | 55 | req.removeListener('newListener', handler); 56 | // Start listening at the end of the current loop 57 | // otherwise the request will be consumed too early. 58 | // Sideaffect is `limit` will miss the first chunk, 59 | // but that's not a big deal. 60 | // Unfortunately, the tests don't have large enough 61 | // request bodies to test this. 62 | process.nextTick(listen); 63 | }); 64 | }; 65 | 66 | next(); 67 | 68 | function listen() { 69 | req.on('data', function(chunk) { 70 | received += Buffer.isBuffer(chunk) 71 | ? chunk.length : 72 | Buffer.byteLength(chunk); 73 | 74 | if (received > bytes) req.destroy(); 75 | }); 76 | }; 77 | }; 78 | }; -------------------------------------------------------------------------------- /test/vhost.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../') 3 | , http = require('http'); 4 | 5 | describe('connect.vhost()', function(){ 6 | it('should route by Host', function(done){ 7 | var app = connect() 8 | , tobi = connect() 9 | , loki = connect(); 10 | 11 | app.use(connect.vhost('tobi.com', tobi)); 12 | app.use(connect.vhost('loki.com', loki)); 13 | 14 | tobi.use(function(req, res){ res.end('tobi') }); 15 | loki.use(function(req, res){ res.end('loki') }); 16 | 17 | app.request() 18 | .get('/') 19 | .set('Host', 'tobi.com') 20 | .expect('tobi', done); 21 | }) 22 | 23 | it('should support http.Servers', function(done){ 24 | var app = connect() 25 | , tobi = http.createServer(function(req, res){ res.end('tobi') }) 26 | , loki = http.createServer(function(req, res){ res.end('loki') }) 27 | 28 | app.use(connect.vhost('tobi.com', tobi)); 29 | app.use(connect.vhost('loki.com', loki)); 30 | 31 | app.request() 32 | .get('/') 33 | .set('Host', 'loki.com') 34 | .expect('loki', done); 35 | }) 36 | 37 | it('should support wildcards', function(done){ 38 | var app = connect() 39 | , tobi = http.createServer(function(req, res){ res.end('tobi') }) 40 | , loki = http.createServer(function(req, res){ res.end('loki') }) 41 | 42 | app.use(connect.vhost('*.ferrets.com', loki)); 43 | app.use(connect.vhost('tobi.ferrets.com', tobi)); 44 | 45 | app.request() 46 | .get('/') 47 | .set('Host', 'loki.ferrets.com') 48 | .expect('loki', done); 49 | }) 50 | 51 | it('should 404 unless matched', function(done){ 52 | var app = connect() 53 | , tobi = http.createServer(function(req, res){ res.end('tobi') }) 54 | , loki = http.createServer(function(req, res){ res.end('loki') }) 55 | 56 | app.use(connect.vhost('tobi.com', tobi)); 57 | app.use(connect.vhost('loki.com', loki)); 58 | 59 | app.request() 60 | .get('/') 61 | .set('Host', 'ferrets.com') 62 | .expect(404, done); 63 | }) 64 | 65 | it('should treat dot as a dot', function(done){ 66 | var app = connect() 67 | , tobi = http.createServer(function(req, res){ res.end('tobi') }) 68 | 69 | app.use(connect.vhost('a.b.com', tobi)); 70 | 71 | app.request() 72 | .get('/') 73 | .set('Host', 'aXb.com') 74 | .expect(404, done); 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Connect is a middleware framework for node, 4 | * shipping with over 18 bundled middleware and a rich selection of 5 | * 3rd-party middleware. 6 | * 7 | * var app = connect() 8 | * .use(connect.logger('dev')) 9 | * .use(connect.static('public')) 10 | * .use(function(req, res){ 11 | * res.end('hello world\n'); 12 | * }) 13 | * .listen(3000); 14 | * 15 | * Installation: 16 | * 17 | * $ npm install connect 18 | * 19 | * Middleware: 20 | * 21 | * - [logger](logger.html) request logger with custom format support 22 | * - [csrf](csrf.html) Cross-site request forgery protection 23 | * - [compress](compress.html) Gzip compression middleware 24 | * - [basicAuth](basicAuth.html) basic http authentication 25 | * - [bodyParser](bodyParser.html) extensible request body parser 26 | * - [json](json.html) application/json parser 27 | * - [urlencoded](urlencoded.html) application/x-www-form-urlencoded parser 28 | * - [multipart](multipart.html) multipart/form-data parser 29 | * - [timeout](timeout.html) request timeouts 30 | * - [cookieParser](cookieParser.html) cookie parser 31 | * - [session](session.html) session management support with bundled MemoryStore 32 | * - [cookieSession](cookieSession.html) cookie-based session support 33 | * - [methodOverride](methodOverride.html) faux HTTP method support 34 | * - [responseTime](responseTime.html) calculates response-time and exposes via X-Response-Time 35 | * - [staticCache](staticCache.html) memory cache layer for the static() middleware 36 | * - [static](static.html) streaming static file server supporting `Range` and more 37 | * - [directory](directory.html) directory listing middleware 38 | * - [vhost](vhost.html) virtual host sub-domain mapping middleware 39 | * - [favicon](favicon.html) efficient favicon server (with default icon) 40 | * - [limit](limit.html) limit the bytesize of request bodies 41 | * - [query](query.html) automatic querystring parser, populating `req.query` 42 | * - [errorHandler](errorHandler.html) flexible error handler 43 | * 44 | * Links: 45 | * 46 | * - list of [3rd-party](https://github.com/senchalabs/connect/wiki) middleware 47 | * - GitHub [repository](http://github.com/senchalabs/connect) 48 | * - [test documentation](https://github.com/senchalabs/connect/blob/gh-pages/tests.md) 49 | * 50 | */ -------------------------------------------------------------------------------- /lib/public/directory.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |views: ' + sess.views + '
'); 20 | res.write('expires in: ' + (sess.cookie.maxAge / 1000) + 's
'); 21 | res.end(); 22 | } else { 23 | sess.views = 1; 24 | res.end('welcome to the session demo. refresh!'); 25 | } 26 | })).listen(3007); 27 | 28 | console.log('port 3007: 1 minute expiration demo'); 29 | 30 | 31 | // expire sessions within a minute 32 | // /favicon.ico is ignored, and will not 33 | // receive req.session 34 | 35 | http.createServer(connect() 36 | .use(connect.cookieParser()) 37 | .use(connect.session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }})) 38 | .use(connect.favicon()) 39 | .use(function(req, res, next){ 40 | var sess = req.session; 41 | if (sess.views) { 42 | sess.views++; 43 | res.setHeader('Content-Type', 'text/html'); 44 | res.write('views: ' + sess.views + '
'); 45 | res.write('expires in: ' + (sess.cookie.maxAge / 1000) + 's
'); 46 | res.end(); 47 | } else { 48 | sess.views = 1; 49 | res.end('welcome to the session demo. refresh!'); 50 | } 51 | })).listen(3006); 52 | 53 | console.log('port 3006: 1 minute expiration demo'); 54 | 55 | // $ npm install connect-redis 56 | 57 | try { 58 | var RedisStore = require('connect-redis')(connect); 59 | http.createServer(connect() 60 | .use(connect.cookieParser()) 61 | .use(connect.session({ 62 | secret: 'keyboard cat', 63 | cookie: { maxAge: 60000 * 3 } 64 | , store: new RedisStore 65 | })) 66 | .use(connect.favicon()) 67 | .use(function(req, res, next){ 68 | var sess = req.session; 69 | if (sess.views) { 70 | sess.views++; 71 | res.setHeader('Content-Type', 'text/html'); 72 | res.end('views: ' + sess.views + '
'); 73 | } else { 74 | sess.views = 1; 75 | res.end('welcome to the redis demo. refresh!'); 76 | } 77 | })).listen(3001); 78 | 79 | console.log('port 3001: redis example'); 80 | } catch (err) { 81 | console.log('\033[33m'); 82 | console.log('failed to start the Redis example.'); 83 | console.log('to try it install redis, start redis'); 84 | console.log('install connect-redis, and run this'); 85 | console.log('script again.'); 86 | console.log(' $ redis-server'); 87 | console.log(' $ npm install connect-redis'); 88 | console.log('\033[0m'); 89 | } 90 | 91 | // conditional session support by simply 92 | // wrapping middleware with middleware. 93 | 94 | var sess = connect.session({ secret: 'keyboard cat', cookie: { maxAge: 5000 }}); 95 | 96 | http.createServer(connect() 97 | .use(connect.cookieParser()) 98 | .use(function(req, res, next){ 99 | if ('/foo' == req.url || '/bar' == req.url) { 100 | sess(req, res, next); 101 | } else { 102 | next(); 103 | } 104 | }) 105 | .use(connect.favicon()) 106 | .use(function(req, res, next){ 107 | res.end('has session: ' + (req.session ? 'yes' : 'no')); 108 | })).listen(3002); 109 | 110 | console.log('port 3002: conditional sessions'); 111 | 112 | // Session#reload() will update req.session 113 | // without altering .maxAge 114 | 115 | // view the page several times, and see that the 116 | // setInterval can still gain access to new 117 | // session data 118 | 119 | http.createServer(connect() 120 | .use(connect.cookieParser()) 121 | .use(connect.session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }})) 122 | .use(connect.favicon()) 123 | .use(function(req, res, next){ 124 | var sess = req.session 125 | , prev; 126 | 127 | if (sess.views) { 128 | res.setHeader('Content-Type', 'text/html'); 129 | res.write('views: ' + sess.views + '
'); 130 | res.write('expires in: ' + (sess.cookie.maxAge / 1000) + 's
'); 131 | sess.views++; 132 | res.end(); 133 | } else { 134 | sess.views = 1; 135 | setInterval(function(){ 136 | sess.reload(function(){ 137 | console.log(); 138 | if (prev) console.log('previous views %d, now %d', prev, req.session.views); 139 | console.log('time remaining until expiry: %ds', (req.session.cookie.maxAge / 1000)); 140 | prev = req.session.views; 141 | }); 142 | }, 3000); 143 | res.end('welcome to the session demo. refresh!'); 144 | } 145 | })).listen(3003); 146 | 147 | console.log('port 3003: Session#reload() demo'); 148 | 149 | // by default sessions 150 | // last the duration of 151 | // a user-agent's own session, 152 | // aka while the browser is open. 153 | 154 | http.createServer(connect() 155 | .use(connect.cookieParser()) 156 | .use(connect.session({ secret: 'keyboard cat' })) 157 | .use(connect.favicon()) 158 | .use(function(req, res, next){ 159 | var sess = req.session; 160 | if (sess.views) { 161 | res.setHeader('Content-Type', 'text/html'); 162 | res.write('views: ' + sess.views + '
'); 163 | res.end(); 164 | sess.views++; 165 | } else { 166 | sess.views = 1; 167 | res.end('welcome to the browser session demo. refresh!'); 168 | } 169 | })).listen(3004); 170 | 171 | console.log('port 3004: browser-session length sessions'); 172 | 173 | // persistence example, enter your name! 174 | 175 | http.createServer(connect() 176 | .use(connect.bodyParser()) 177 | .use(connect.cookieParser()) 178 | .use(connect.session({ secret: 'keyboard cat' })) 179 | .use(connect.favicon()) 180 | .use(function(req, res, next){ 181 | if ('POST' != req.method) return next(); 182 | req.session.name = req.body.name; 183 | res.statusCode = 302; 184 | res.setHeader('Location', '/'); 185 | res.end(); 186 | }) 187 | .use(function(req, res, next){ 188 | var sess = req.session; 189 | res.setHeader('Content-Type', 'text/html'); 190 | if (sess.name) res.write('Hey ' + sess.name + '!
'); 191 | else res.write('Enter a username:
'); 192 | res.end(''); 196 | })).listen(3005); 197 | 198 | console.log('port 3005: browser-session length sessions persistence example'); 199 | -------------------------------------------------------------------------------- /lib/proto.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - HTTPServer 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var http = require('http') 14 | , utils = require('./utils') 15 | , debug = require('debug')('connect:dispatcher'); 16 | 17 | // prototype 18 | 19 | var app = module.exports = {}; 20 | 21 | // environment 22 | 23 | var env = process.env.NODE_ENV || 'development'; 24 | 25 | /** 26 | * Utilize the given middleware `handle` to the given `route`, 27 | * defaulting to _/_. This "route" is the mount-point for the 28 | * middleware, when given a value other than _/_ the middleware 29 | * is only effective when that segment is present in the request's 30 | * pathname. 31 | * 32 | * For example if we were to mount a function at _/admin_, it would 33 | * be invoked on _/admin_, and _/admin/settings_, however it would 34 | * not be invoked for _/_, or _/posts_. 35 | * 36 | * Examples: 37 | * 38 | * var app = connect(); 39 | * app.use(connect.favicon()); 40 | * app.use(connect.logger()); 41 | * app.use(connect.static(__dirname + '/public')); 42 | * 43 | * If we wanted to prefix static files with _/public_, we could 44 | * "mount" the `static()` middleware: 45 | * 46 | * app.use('/public', connect.static(__dirname + '/public')); 47 | * 48 | * This api is chainable, so the following is valid: 49 | * 50 | * connect() 51 | * .use(connect.favicon()) 52 | * .use(connect.logger()) 53 | * .use(connect.static(__dirname + '/public')) 54 | * .listen(3000); 55 | * 56 | * @param {String|Function|Server} route, callback or server 57 | * @param {Function|Server} callback or server 58 | * @return {Server} for chaining 59 | * @api public 60 | */ 61 | 62 | app.use = function(route, fn){ 63 | // default route to '/' 64 | if ('string' != typeof route) { 65 | fn = route; 66 | route = '/'; 67 | } 68 | 69 | // wrap sub-apps 70 | if ('function' == typeof fn.handle) { 71 | var server = fn; 72 | fn.route = route; 73 | fn = function(req, res, next){ 74 | server.handle(req, res, next); 75 | }; 76 | } 77 | 78 | // wrap vanilla http.Servers 79 | if (fn instanceof http.Server) { 80 | fn = fn.listeners('request')[0]; 81 | } 82 | 83 | // strip trailing slash 84 | if ('/' == route[route.length - 1]) { 85 | route = route.slice(0, -1); 86 | } 87 | 88 | // add the middleware 89 | debug('use %s %s', route || '/', fn.name || 'anonymous'); 90 | this.stack.push({ route: route, handle: fn }); 91 | 92 | return this; 93 | }; 94 | 95 | /** 96 | * Handle server requests, punting them down 97 | * the middleware stack. 98 | * 99 | * @api private 100 | */ 101 | 102 | app.handle = function(req, res, out) { 103 | var stack = this.stack 104 | , fqdn = ~req.url.indexOf('://') 105 | , removed = '' 106 | , slashAdded = false 107 | , index = 0; 108 | 109 | function next(err) { 110 | var layer, path, status, c; 111 | 112 | if (slashAdded) { 113 | req.url = req.url.substr(1); 114 | slashAdded = false; 115 | } 116 | 117 | req.url = removed + req.url; 118 | req.originalUrl = req.originalUrl || req.url; 119 | removed = ''; 120 | 121 | // next callback 122 | layer = stack[index++]; 123 | 124 | // all done 125 | if (!layer || res.headerSent) { 126 | // delegate to parent 127 | if (out) return out(err); 128 | 129 | // unhandled error 130 | if (err) { 131 | // default to 500 132 | if (res.statusCode < 400) res.statusCode = 500; 133 | debug('default %s', res.statusCode); 134 | 135 | // respect err.status 136 | if (err.status) res.statusCode = err.status; 137 | 138 | // production gets a basic error message 139 | var msg = 'production' == env 140 | ? http.STATUS_CODES[res.statusCode] 141 | : err.stack || err.toString(); 142 | 143 | // log to stderr in a non-test env 144 | if ('test' != env) console.error(err.stack || err.toString()); 145 | if (res.headerSent) return req.socket.destroy(); 146 | res.setHeader('Content-Type', 'text/plain'); 147 | res.setHeader('Content-Length', Buffer.byteLength(msg)); 148 | if ('HEAD' == req.method) return res.end(); 149 | res.end(msg); 150 | } else { 151 | debug('default 404'); 152 | res.statusCode = 404; 153 | res.setHeader('Content-Type', 'text/plain'); 154 | if ('HEAD' == req.method) return res.end(); 155 | res.end('Cannot ' + req.method + ' ' + utils.escape(req.originalUrl)); 156 | } 157 | return; 158 | } 159 | 160 | try { 161 | path = utils.parseUrl(req).pathname; 162 | if (undefined == path) path = '/'; 163 | 164 | // skip this layer if the route doesn't match. 165 | if (0 != path.toLowerCase().indexOf(layer.route.toLowerCase())) return next(err); 166 | 167 | c = path[layer.route.length]; 168 | if (c && '/' != c && '.' != c) return next(err); 169 | 170 | // Call the layer handler 171 | // Trim off the part of the url that matches the route 172 | removed = layer.route; 173 | req.url = req.url.substr(removed.length); 174 | 175 | // Ensure leading slash 176 | if (!fqdn && '/' != req.url[0]) { 177 | req.url = '/' + req.url; 178 | slashAdded = true; 179 | } 180 | 181 | debug('%s', layer.handle.name || 'anonymous'); 182 | var arity = layer.handle.length; 183 | if (err) { 184 | if (arity === 4) { 185 | layer.handle(err, req, res, next); 186 | } else { 187 | next(err); 188 | } 189 | } else if (arity < 4) { 190 | layer.handle(req, res, next); 191 | } else { 192 | next(); 193 | } 194 | } catch (e) { 195 | next(e); 196 | } 197 | } 198 | next(); 199 | }; 200 | 201 | /** 202 | * Listen for connections. 203 | * 204 | * This method takes the same arguments 205 | * as node's `http.Server#listen()`. 206 | * 207 | * HTTP and HTTPS: 208 | * 209 | * If you run your application both as HTTP 210 | * and HTTPS you may wrap them individually, 211 | * since your Connect "server" is really just 212 | * a JavaScript `Function`. 213 | * 214 | * var connect = require('connect') 215 | * , http = require('http') 216 | * , https = require('https'); 217 | * 218 | * var app = connect(); 219 | * 220 | * http.createServer(app).listen(80); 221 | * https.createServer(options, app).listen(443); 222 | * 223 | * @return {http.Server} 224 | * @api public 225 | */ 226 | 227 | app.listen = function(){ 228 | var server = http.createServer(this); 229 | return server.listen.apply(server, arguments); 230 | }; 231 | -------------------------------------------------------------------------------- /lib/middleware/staticCache.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - staticCache 4 | * Copyright(c) 2011 Sencha Inc. 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var utils = require('../utils') 13 | , Cache = require('../cache') 14 | , fresh = require('fresh'); 15 | 16 | /** 17 | * Static cache: 18 | * 19 | * Enables a memory cache layer on top of 20 | * the `static()` middleware, serving popular 21 | * static files. 22 | * 23 | * By default a maximum of 128 objects are 24 | * held in cache, with a max of 256k each, 25 | * totalling ~32mb. 26 | * 27 | * A Least-Recently-Used (LRU) cache algo 28 | * is implemented through the `Cache` object, 29 | * simply rotating cache objects as they are 30 | * hit. This means that increasingly popular 31 | * objects maintain their positions while 32 | * others get shoved out of the stack and 33 | * garbage collected. 34 | * 35 | * Benchmarks: 36 | * 37 | * static(): 2700 rps 38 | * node-static: 5300 rps 39 | * static() + staticCache(): 7500 rps 40 | * 41 | * Options: 42 | * 43 | * - `maxObjects` max cache objects [128] 44 | * - `maxLength` max cache object length 256kb 45 | * 46 | * @param {Object} options 47 | * @return {Function} 48 | * @api public 49 | */ 50 | 51 | module.exports = function staticCache(options){ 52 | var options = options || {} 53 | , cache = new Cache(options.maxObjects || 128) 54 | , maxlen = options.maxLength || 1024 * 256; 55 | 56 | console.warn('connect.staticCache() is deprecated and will be removed in 3.0'); 57 | console.warn('use varnish or similar reverse proxy caches.'); 58 | 59 | return function staticCache(req, res, next){ 60 | var key = cacheKey(req) 61 | , ranges = req.headers.range 62 | , hasCookies = req.headers.cookie 63 | , hit = cache.get(key); 64 | 65 | // cache static 66 | // TODO: change from staticCache() -> cache() 67 | // and make this work for any request 68 | req.on('static', function(stream){ 69 | var headers = res._headers 70 | , cc = utils.parseCacheControl(headers['cache-control'] || '') 71 | , contentLength = headers['content-length'] 72 | , hit; 73 | 74 | // dont cache set-cookie responses 75 | if (headers['set-cookie']) return hasCookies = true; 76 | 77 | // dont cache when cookies are present 78 | if (hasCookies) return; 79 | 80 | // ignore larger files 81 | if (!contentLength || contentLength > maxlen) return; 82 | 83 | // don't cache partial files 84 | if (headers['content-range']) return; 85 | 86 | // dont cache items we shouldn't be 87 | // TODO: real support for must-revalidate / no-cache 88 | if ( cc['no-cache'] 89 | || cc['no-store'] 90 | || cc['private'] 91 | || cc['must-revalidate']) return; 92 | 93 | // if already in cache then validate 94 | if (hit = cache.get(key)){ 95 | if (headers.etag == hit[0].etag) { 96 | hit[0].date = new Date; 97 | return; 98 | } else { 99 | cache.remove(key); 100 | } 101 | } 102 | 103 | // validation notifiactions don't contain a steam 104 | if (null == stream) return; 105 | 106 | // add the cache object 107 | var arr = []; 108 | 109 | // store the chunks 110 | stream.on('data', function(chunk){ 111 | arr.push(chunk); 112 | }); 113 | 114 | // flag it as complete 115 | stream.on('end', function(){ 116 | var cacheEntry = cache.add(key); 117 | delete headers['x-cache']; // Clean up (TODO: others) 118 | cacheEntry.push(200); 119 | cacheEntry.push(headers); 120 | cacheEntry.push.apply(cacheEntry, arr); 121 | }); 122 | }); 123 | 124 | if (req.method == 'GET' || req.method == 'HEAD') { 125 | if (ranges) { 126 | next(); 127 | } else if (!hasCookies && hit && !mustRevalidate(req, hit)) { 128 | res.setHeader('X-Cache', 'HIT'); 129 | respondFromCache(req, res, hit); 130 | } else { 131 | res.setHeader('X-Cache', 'MISS'); 132 | next(); 133 | } 134 | } else { 135 | next(); 136 | } 137 | } 138 | }; 139 | 140 | /** 141 | * Respond with the provided cached value. 142 | * TODO: Assume 200 code, that's iffy. 143 | * 144 | * @param {Object} req 145 | * @param {Object} res 146 | * @param {Object} cacheEntry 147 | * @return {String} 148 | * @api private 149 | */ 150 | 151 | function respondFromCache(req, res, cacheEntry) { 152 | var status = cacheEntry[0] 153 | , headers = utils.merge({}, cacheEntry[1]) 154 | , content = cacheEntry.slice(2); 155 | 156 | headers.age = (new Date - new Date(headers.date)) / 1000 || 0; 157 | 158 | switch (req.method) { 159 | case 'HEAD': 160 | res.writeHead(status, headers); 161 | res.end(); 162 | break; 163 | case 'GET': 164 | if (utils.conditionalGET(req) && fresh(req.headers, headers)) { 165 | headers['content-length'] = 0; 166 | res.writeHead(304, headers); 167 | res.end(); 168 | } else { 169 | res.writeHead(status, headers); 170 | 171 | function write() { 172 | while (content.length) { 173 | if (false === res.write(content.shift())) { 174 | res.once('drain', write); 175 | return; 176 | } 177 | } 178 | res.end(); 179 | } 180 | 181 | write(); 182 | } 183 | break; 184 | default: 185 | // This should never happen. 186 | res.writeHead(500, ''); 187 | res.end(); 188 | } 189 | } 190 | 191 | /** 192 | * Determine whether or not a cached value must be revalidated. 193 | * 194 | * @param {Object} req 195 | * @param {Object} cacheEntry 196 | * @return {String} 197 | * @api private 198 | */ 199 | 200 | function mustRevalidate(req, cacheEntry) { 201 | var cacheHeaders = cacheEntry[1] 202 | , reqCC = utils.parseCacheControl(req.headers['cache-control'] || '') 203 | , cacheCC = utils.parseCacheControl(cacheHeaders['cache-control'] || '') 204 | , cacheAge = (new Date - new Date(cacheHeaders.date)) / 1000 || 0; 205 | 206 | if ( cacheCC['no-cache'] 207 | || cacheCC['must-revalidate'] 208 | || cacheCC['proxy-revalidate']) return true; 209 | 210 | if (reqCC['no-cache']) return true; 211 | 212 | if (null != reqCC['max-age']) return reqCC['max-age'] < cacheAge; 213 | 214 | if (null != cacheCC['max-age']) return cacheCC['max-age'] < cacheAge; 215 | 216 | return false; 217 | } 218 | 219 | /** 220 | * The key to use in the cache. For now, this is the URL path and query. 221 | * 222 | * 'http://example.com?key=value' -> '/?key=value' 223 | * 224 | * @param {Object} req 225 | * @return {String} 226 | * @api private 227 | */ 228 | 229 | function cacheKey(req) { 230 | return utils.parseUrl(req).path; 231 | } 232 | -------------------------------------------------------------------------------- /test/bodyParser.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('..'); 3 | var assert = require('assert'); 4 | 5 | var app = connect(); 6 | 7 | app.use(connect.bodyParser()); 8 | 9 | app.use(function(req, res){ 10 | res.end(JSON.stringify(req.body)); 11 | }); 12 | 13 | describe('connect.bodyParser()', function(){ 14 | it('should default to {}', function(done){ 15 | app.request() 16 | .post('/') 17 | .end(function(res){ 18 | res.body.should.equal('{}'); 19 | done(); 20 | }) 21 | }) 22 | 23 | it('should parse JSON', function(done){ 24 | app.request() 25 | .post('/') 26 | .set('Content-Type', 'application/json') 27 | .write('{"user":"tobi"}') 28 | .end(function(res){ 29 | res.body.should.equal('{"user":"tobi"}'); 30 | done(); 31 | }); 32 | }) 33 | 34 | it('should parse x-www-form-urlencoded', function(done){ 35 | app.request() 36 | .post('/') 37 | .set('Content-Type', 'application/x-www-form-urlencoded') 38 | .write('user=tobi') 39 | .end(function(res){ 40 | res.body.should.equal('{"user":"tobi"}'); 41 | done(); 42 | }); 43 | }) 44 | 45 | describe('with multipart/form-data', function(){ 46 | it('should populate req.body', function(done){ 47 | app.request() 48 | .post('/') 49 | .set('Content-Type', 'multipart/form-data; boundary=foo') 50 | .write('--foo\r\n') 51 | .write('Content-Disposition: form-data; name="user"\r\n') 52 | .write('\r\n') 53 | .write('Tobi') 54 | .write('\r\n--foo--') 55 | .end(function(res){ 56 | res.body.should.equal('{"user":"Tobi"}'); 57 | done(); 58 | }); 59 | }) 60 | 61 | it('should support files', function(done){ 62 | var app = connect(); 63 | 64 | app.use(connect.bodyParser()); 65 | 66 | app.use(function(req, res){ 67 | assert('Tobi' == req.body.user.name); 68 | req.files.text.path.should.not.include('.txt'); 69 | req.files.text.constructor.name.should.equal('File'); 70 | res.end(req.files.text.name); 71 | }); 72 | 73 | app.request() 74 | .post('/') 75 | .set('Content-Type', 'multipart/form-data; boundary=foo') 76 | .write('--foo\r\n') 77 | .write('Content-Disposition: form-data; name="user[name]"\r\n') 78 | .write('\r\n') 79 | .write('Tobi') 80 | .write('\r\n--foo\r\n') 81 | .write('Content-Disposition: form-data; name="text"; filename="foo.txt"\r\n') 82 | .write('\r\n') 83 | .write('some text here') 84 | .write('\r\n--foo--') 85 | .end(function(res){ 86 | res.body.should.equal('foo.txt'); 87 | done(); 88 | }); 89 | }) 90 | 91 | it('should expose options to formidable', function(done){ 92 | var app = connect(); 93 | 94 | app.use(connect.bodyParser({ 95 | keepExtensions: true 96 | })); 97 | 98 | app.use(function(req, res){ 99 | assert('Tobi' == req.body.user.name); 100 | assert(~req.files.text.path.indexOf('.txt')); 101 | res.end(req.files.text.name); 102 | }); 103 | 104 | app.request() 105 | .post('/') 106 | .set('Content-Type', 'multipart/form-data; boundary=foo') 107 | .write('--foo\r\n') 108 | .write('Content-Disposition: form-data; name="user[name]"\r\n') 109 | .write('\r\n') 110 | .write('Tobi') 111 | .write('\r\n--foo\r\n') 112 | .write('Content-Disposition: form-data; name="text"; filename="foo.txt"\r\n') 113 | .write('\r\n') 114 | .write('some text here') 115 | .write('\r\n--foo--') 116 | .end(function(res){ 117 | res.body.should.equal('foo.txt'); 118 | done(); 119 | }); 120 | }) 121 | 122 | it('should work with multiple fields', function(done){ 123 | app.request() 124 | .post('/') 125 | .set('Content-Type', 'multipart/form-data; boundary=foo') 126 | .write('--foo\r\n') 127 | .write('Content-Disposition: form-data; name="user"\r\n') 128 | .write('\r\n') 129 | .write('Tobi') 130 | .write('\r\n--foo\r\n') 131 | .write('Content-Disposition: form-data; name="age"\r\n') 132 | .write('\r\n') 133 | .write('1') 134 | .write('\r\n--foo--') 135 | .end(function(res){ 136 | res.body.should.equal('{"user":"Tobi","age":"1"}'); 137 | done(); 138 | }); 139 | }) 140 | 141 | it('should support nesting', function(done){ 142 | app.request() 143 | .post('/') 144 | .set('Content-Type', 'multipart/form-data; boundary=foo') 145 | .write('--foo\r\n') 146 | .write('Content-Disposition: form-data; name="user[name][first]"\r\n') 147 | .write('\r\n') 148 | .write('tobi') 149 | .write('\r\n--foo\r\n') 150 | .write('Content-Disposition: form-data; name="user[name][last]"\r\n') 151 | .write('\r\n') 152 | .write('holowaychuk') 153 | .write('\r\n--foo\r\n') 154 | .write('Content-Disposition: form-data; name="user[age]"\r\n') 155 | .write('\r\n') 156 | .write('1') 157 | .write('\r\n--foo\r\n') 158 | .write('Content-Disposition: form-data; name="species"\r\n') 159 | .write('\r\n') 160 | .write('ferret') 161 | .write('\r\n--foo--') 162 | .end(function(res){ 163 | var obj = JSON.parse(res.body); 164 | obj.user.age.should.equal('1'); 165 | obj.user.name.should.eql({ first: 'tobi', last: 'holowaychuk' }); 166 | obj.species.should.equal('ferret'); 167 | done(); 168 | }); 169 | }) 170 | 171 | it('should support multiple files of the same name', function(done){ 172 | var app = connect(); 173 | 174 | app.use(connect.bodyParser()); 175 | 176 | app.use(function(req, res){ 177 | req.files.text.should.have.length(2); 178 | req.files.text[0].constructor.name.should.equal('File'); 179 | req.files.text[1].constructor.name.should.equal('File'); 180 | res.end(); 181 | }); 182 | 183 | app.request() 184 | .post('/') 185 | .set('Content-Type', 'multipart/form-data; boundary=foo') 186 | .write('--foo\r\n') 187 | .write('Content-Disposition: form-data; name="text"; filename="foo.txt"\r\n') 188 | .write('\r\n') 189 | .write('some text here') 190 | .write('\r\n--foo\r\n') 191 | .write('Content-Disposition: form-data; name="text"; filename="bar.txt"\r\n') 192 | .write('\r\n') 193 | .write('some more text stuff') 194 | .write('\r\n--foo--') 195 | .end(function(res){ 196 | res.statusCode.should.equal(200); 197 | done(); 198 | }); 199 | }) 200 | 201 | it('should support nested files', function(done){ 202 | var app = connect(); 203 | 204 | app.use(connect.bodyParser()); 205 | 206 | app.use(function(req, res){ 207 | Object.keys(req.files.docs).should.have.length(2); 208 | req.files.docs.foo.name.should.equal('foo.txt'); 209 | req.files.docs.bar.name.should.equal('bar.txt'); 210 | res.end(); 211 | }); 212 | 213 | app.request() 214 | .post('/') 215 | .set('Content-Type', 'multipart/form-data; boundary=foo') 216 | .write('--foo\r\n') 217 | .write('Content-Disposition: form-data; name="docs[foo]"; filename="foo.txt"\r\n') 218 | .write('\r\n') 219 | .write('some text here') 220 | .write('\r\n--foo\r\n') 221 | .write('Content-Disposition: form-data; name="docs[bar]"; filename="bar.txt"\r\n') 222 | .write('\r\n') 223 | .write('some more text stuff') 224 | .write('\r\n--foo--') 225 | .end(function(res){ 226 | res.statusCode.should.equal(200); 227 | done(); 228 | }); 229 | }) 230 | 231 | it('should next(err) on multipart failure', function(done){ 232 | var app = connect(); 233 | 234 | app.use(connect.bodyParser()); 235 | 236 | app.use(function(req, res){ 237 | res.end('whoop'); 238 | }); 239 | 240 | app.use(function(err, req, res, next){ 241 | err.message.should.equal('parser error, 16 of 28 bytes parsed'); 242 | res.statusCode = 500; 243 | res.end(); 244 | }); 245 | 246 | app.request() 247 | .post('/') 248 | .set('Content-Type', 'multipart/form-data; boundary=foo') 249 | .write('--foo\r\n') 250 | .write('Content-filename="foo.txt"\r\n') 251 | .write('\r\n') 252 | .write('some text here') 253 | .write('Content-Disposition: form-data; name="text"; filename="bar.txt"\r\n') 254 | .write('\r\n') 255 | .write('some more text stuff') 256 | .write('\r\n--foo--') 257 | .end(function(res){ 258 | res.statusCode.should.equal(500); 259 | done(); 260 | }); 261 | }) 262 | 263 | }) 264 | }) 265 | -------------------------------------------------------------------------------- /lib/middleware/logger.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Connect - logger 3 | * Copyright(c) 2010 Sencha Inc. 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var bytes = require('bytes'); 13 | 14 | /*! 15 | * Log buffer. 16 | */ 17 | 18 | var buf = []; 19 | 20 | /*! 21 | * Default log buffer duration. 22 | */ 23 | 24 | var defaultBufferDuration = 1000; 25 | 26 | /** 27 | * Logger: 28 | * 29 | * Log requests with the given `options` or a `format` string. 30 | * 31 | * Options: 32 | * 33 | * - `format` Format string, see below for tokens 34 | * - `stream` Output stream, defaults to _stdout_ 35 | * - `buffer` Buffer duration, defaults to 1000ms when _true_ 36 | * - `immediate` Write log line on request instead of response (for response times) 37 | * 38 | * Tokens: 39 | * 40 | * - `:req[header]` ex: `:req[Accept]` 41 | * - `:res[header]` ex: `:res[Content-Length]` 42 | * - `:http-version` 43 | * - `:response-time` 44 | * - `:remote-addr` 45 | * - `:date` 46 | * - `:method` 47 | * - `:url` 48 | * - `:referrer` 49 | * - `:user-agent` 50 | * - `:status` 51 | * 52 | * Formats: 53 | * 54 | * Pre-defined formats that ship with connect: 55 | * 56 | * - `default` ':remote-addr - - [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"' 57 | * - `short` ':remote-addr - :method :url HTTP/:http-version :status :res[content-length] - :response-time ms' 58 | * - `tiny` ':method :url :status :res[content-length] - :response-time ms' 59 | * - `dev` concise output colored by response status for development use 60 | * 61 | * Examples: 62 | * 63 | * connect.logger() // default 64 | * connect.logger('short') 65 | * connect.logger('tiny') 66 | * connect.logger({ immediate: true, format: 'dev' }) 67 | * connect.logger(':method :url - :referrer') 68 | * connect.logger(':req[content-type] -> :res[content-type]') 69 | * connect.logger(function(tokens, req, res){ return 'some format string' }) 70 | * 71 | * Defining Tokens: 72 | * 73 | * To define a token, simply invoke `connect.logger.token()` with the 74 | * name and a callback function. The value returned is then available 75 | * as ":type" in this case. 76 | * 77 | * connect.logger.token('type', function(req, res){ return req.headers['content-type']; }) 78 | * 79 | * Defining Formats: 80 | * 81 | * All default formats are defined this way, however it's public API as well: 82 | * 83 | * connect.logger.format('name', 'string or function') 84 | * 85 | * @param {String|Function|Object} format or options 86 | * @return {Function} 87 | * @api public 88 | */ 89 | 90 | exports = module.exports = function logger(options) { 91 | if ('object' == typeof options) { 92 | options = options || {}; 93 | } else if (options) { 94 | options = { format: options }; 95 | } else { 96 | options = {}; 97 | } 98 | 99 | // output on request instead of response 100 | var immediate = options.immediate; 101 | 102 | // format name 103 | var fmt = exports[options.format] || options.format || exports.default; 104 | 105 | // compile format 106 | if ('function' != typeof fmt) fmt = compile(fmt); 107 | 108 | // options 109 | var stream = options.stream || process.stdout 110 | , buffer = options.buffer; 111 | 112 | // buffering support 113 | if (buffer) { 114 | var realStream = stream 115 | , interval = 'number' == typeof buffer 116 | ? buffer 117 | : defaultBufferDuration; 118 | 119 | // flush interval 120 | setInterval(function(){ 121 | if (buf.length) { 122 | realStream.write(buf.join('')); 123 | buf.length = 0; 124 | } 125 | }, interval); 126 | 127 | // swap the stream 128 | stream = { 129 | write: function(str){ 130 | buf.push(str); 131 | } 132 | }; 133 | } 134 | 135 | return function logger(req, res, next) { 136 | req._startTime = new Date; 137 | 138 | // immediate 139 | if (immediate) { 140 | var line = fmt(exports, req, res); 141 | if (null == line) return; 142 | stream.write(line + '\n'); 143 | // proxy end to output logging 144 | } else { 145 | var end = res.end; 146 | res.end = function(chunk, encoding){ 147 | res.end = end; 148 | res.end(chunk, encoding); 149 | var line = fmt(exports, req, res); 150 | if (null == line) return; 151 | stream.write(line + '\n'); 152 | }; 153 | } 154 | 155 | 156 | next(); 157 | }; 158 | }; 159 | 160 | /** 161 | * Compile `fmt` into a function. 162 | * 163 | * @param {String} fmt 164 | * @return {Function} 165 | * @api private 166 | */ 167 | 168 | function compile(fmt) { 169 | fmt = fmt.replace(/"/g, '\\"'); 170 | var js = ' return "' + fmt.replace(/:([-\w]{2,})(?:\[([^\]]+)\])?/g, function(_, name, arg){ 171 | return '"\n + (tokens["' + name + '"](req, res, "' + arg + '") || "-") + "'; 172 | }) + '";' 173 | return new Function('tokens, req, res', js); 174 | }; 175 | 176 | /** 177 | * Define a token function with the given `name`, 178 | * and callback `fn(req, res)`. 179 | * 180 | * @param {String} name 181 | * @param {Function} fn 182 | * @return {Object} exports for chaining 183 | * @api public 184 | */ 185 | 186 | exports.token = function(name, fn) { 187 | exports[name] = fn; 188 | return this; 189 | }; 190 | 191 | /** 192 | * Define a `fmt` with the given `name`. 193 | * 194 | * @param {String} name 195 | * @param {String|Function} fmt 196 | * @return {Object} exports for chaining 197 | * @api public 198 | */ 199 | 200 | exports.format = function(name, str){ 201 | exports[name] = str; 202 | return this; 203 | }; 204 | 205 | /** 206 | * Default format. 207 | */ 208 | 209 | exports.format('default', ':remote-addr - - [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"'); 210 | 211 | /** 212 | * Short format. 213 | */ 214 | 215 | exports.format('short', ':remote-addr - :method :url HTTP/:http-version :status :res[content-length] - :response-time ms'); 216 | 217 | /** 218 | * Tiny format. 219 | */ 220 | 221 | exports.format('tiny', ':method :url :status :res[content-length] - :response-time ms'); 222 | 223 | /** 224 | * dev (colored) 225 | */ 226 | 227 | exports.format('dev', function(tokens, req, res){ 228 | var status = res.statusCode 229 | , len = parseInt(res.getHeader('Content-Length'), 10) 230 | , color = 32; 231 | 232 | if (status >= 500) color = 31 233 | else if (status >= 400) color = 33 234 | else if (status >= 300) color = 36; 235 | 236 | len = isNaN(len) 237 | ? '' 238 | : len = ' - ' + bytes(len); 239 | 240 | return '\033[90m' + req.method 241 | + ' ' + req.originalUrl + ' ' 242 | + '\033[' + color + 'm' + res.statusCode 243 | + ' \033[90m' 244 | + (new Date - req._startTime) 245 | + 'ms' + len 246 | + '\033[0m'; 247 | }); 248 | 249 | /** 250 | * request url 251 | */ 252 | 253 | exports.token('url', function(req){ 254 | return req.originalUrl || req.url; 255 | }); 256 | 257 | /** 258 | * request method 259 | */ 260 | 261 | exports.token('method', function(req){ 262 | return req.method; 263 | }); 264 | 265 | /** 266 | * response time in milliseconds 267 | */ 268 | 269 | exports.token('response-time', function(req){ 270 | return new Date - req._startTime; 271 | }); 272 | 273 | /** 274 | * UTC date 275 | */ 276 | 277 | exports.token('date', function(){ 278 | return new Date().toUTCString(); 279 | }); 280 | 281 | /** 282 | * response status code 283 | */ 284 | 285 | exports.token('status', function(req, res){ 286 | return res.statusCode; 287 | }); 288 | 289 | /** 290 | * normalized referrer 291 | */ 292 | 293 | exports.token('referrer', function(req){ 294 | return req.headers['referer'] || req.headers['referrer']; 295 | }); 296 | 297 | /** 298 | * remote address 299 | */ 300 | 301 | exports.token('remote-addr', function(req){ 302 | if (req.ip) return req.ip; 303 | var sock = req.socket; 304 | if (sock.socket) return sock.socket.remoteAddress; 305 | return sock.remoteAddress; 306 | }); 307 | 308 | /** 309 | * HTTP version 310 | */ 311 | 312 | exports.token('http-version', function(req){ 313 | return req.httpVersionMajor + '.' + req.httpVersionMinor; 314 | }); 315 | 316 | /** 317 | * UA string 318 | */ 319 | 320 | exports.token('user-agent', function(req){ 321 | return req.headers['user-agent']; 322 | }); 323 | 324 | /** 325 | * request header 326 | */ 327 | 328 | exports.token('req', function(req, res, field){ 329 | return req.headers[field.toLowerCase()]; 330 | }); 331 | 332 | /** 333 | * response header 334 | */ 335 | 336 | exports.token('res', function(req, res, field){ 337 | return (res._headers || {})[field.toLowerCase()]; 338 | }); 339 | 340 | -------------------------------------------------------------------------------- /test/multipart.js: -------------------------------------------------------------------------------- 1 | 2 | var connect = require('../'); 3 | var assert = require('assert'); 4 | var should = require('./shared'); 5 | 6 | var app = connect(); 7 | 8 | app.use(connect.multipart({ limit: '20mb' })); 9 | 10 | app.use(function(req, res){ 11 | res.end(JSON.stringify(req.body)); 12 | }); 13 | 14 | describe('connect.multipart()', function(){ 15 | should['default request body'](app); 16 | should['limit body to']('20mb', 'multipart/form-data', app); 17 | 18 | it('should ignore GET', function(done){ 19 | app.request() 20 | .get('/') 21 | .set('Content-Type', 'multipart/form-data; boundary=foo') 22 | .write('--foo\r\n') 23 | .write('Content-Disposition: form-data; name="user"\r\n') 24 | .write('\r\n') 25 | .write('Tobi') 26 | .write('\r\n--foo--') 27 | .end(function(res){ 28 | res.body.should.equal('{}'); 29 | done(); 30 | }); 31 | }) 32 | 33 | describe('with multipart/form-data', function(){ 34 | it('should populate req.body', function(done){ 35 | app.request() 36 | .post('/') 37 | .set('Content-Type', 'multipart/form-data; boundary=foo') 38 | .write('--foo\r\n') 39 | .write('Content-Disposition: form-data; name="user"\r\n') 40 | .write('\r\n') 41 | .write('Tobi') 42 | .write('\r\n--foo--') 43 | .end(function(res){ 44 | res.body.should.equal('{"user":"Tobi"}'); 45 | done(); 46 | }); 47 | }) 48 | 49 | it('should support files', function(done){ 50 | var app = connect(); 51 | 52 | app.use(connect.multipart()); 53 | 54 | app.use(function(req, res){ 55 | assert('Tobi' == req.body.user.name); 56 | res.end(req.files.text.name); 57 | }); 58 | 59 | app.request() 60 | .post('/') 61 | .set('Content-Type', 'multipart/form-data; boundary=foo') 62 | .write('--foo\r\n') 63 | .write('Content-Disposition: form-data; name="user[name]"\r\n') 64 | .write('\r\n') 65 | .write('Tobi') 66 | .write('\r\n--foo\r\n') 67 | .write('Content-Disposition: form-data; name="text"; filename="foo.txt"\r\n') 68 | .write('\r\n') 69 | .write('some text here') 70 | .write('\r\n--foo--') 71 | .end(function(res){ 72 | res.body.should.equal('foo.txt'); 73 | done(); 74 | }); 75 | }) 76 | 77 | it('should expose options to formidable', function(done){ 78 | var app = connect(); 79 | 80 | app.use(connect.multipart({ 81 | keepExtensions: true 82 | })); 83 | 84 | app.use(function(req, res){ 85 | assert('Tobi' == req.body.user.name); 86 | assert(~req.files.text.path.indexOf('.txt')); 87 | res.end(req.files.text.name); 88 | }); 89 | 90 | app.request() 91 | .post('/') 92 | .set('Content-Type', 'multipart/form-data; boundary=foo') 93 | .write('--foo\r\n') 94 | .write('Content-Disposition: form-data; name="user[name]"\r\n') 95 | .write('\r\n') 96 | .write('Tobi') 97 | .write('\r\n--foo\r\n') 98 | .write('Content-Disposition: form-data; name="text"; filename="foo.txt"\r\n') 99 | .write('\r\n') 100 | .write('some text here') 101 | .write('\r\n--foo--') 102 | .end(function(res){ 103 | res.body.should.equal('foo.txt'); 104 | done(); 105 | }); 106 | }) 107 | 108 | it('should work with multiple fields', function(done){ 109 | app.request() 110 | .post('/') 111 | .set('Content-Type', 'multipart/form-data; boundary=foo') 112 | .write('--foo\r\n') 113 | .write('Content-Disposition: form-data; name="user"\r\n') 114 | .write('\r\n') 115 | .write('Tobi') 116 | .write('\r\n--foo\r\n') 117 | .write('Content-Disposition: form-data; name="age"\r\n') 118 | .write('\r\n') 119 | .write('1') 120 | .write('\r\n--foo--') 121 | .end(function(res){ 122 | res.body.should.equal('{"user":"Tobi","age":"1"}'); 123 | done(); 124 | }); 125 | }) 126 | 127 | it('should support nesting', function(done){ 128 | app.request() 129 | .post('/') 130 | .set('Content-Type', 'multipart/form-data; boundary=foo') 131 | .write('--foo\r\n') 132 | .write('Content-Disposition: form-data; name="user[name][first]"\r\n') 133 | .write('\r\n') 134 | .write('tobi') 135 | .write('\r\n--foo\r\n') 136 | .write('Content-Disposition: form-data; name="user[name][last]"\r\n') 137 | .write('\r\n') 138 | .write('holowaychuk') 139 | .write('\r\n--foo\r\n') 140 | .write('Content-Disposition: form-data; name="user[age]"\r\n') 141 | .write('\r\n') 142 | .write('1') 143 | .write('\r\n--foo\r\n') 144 | .write('Content-Disposition: form-data; name="species"\r\n') 145 | .write('\r\n') 146 | .write('ferret') 147 | .write('\r\n--foo--') 148 | .end(function(res){ 149 | var obj = JSON.parse(res.body); 150 | obj.user.age.should.equal('1'); 151 | obj.user.name.should.eql({ first: 'tobi', last: 'holowaychuk' }); 152 | obj.species.should.equal('ferret'); 153 | done(); 154 | }); 155 | }) 156 | 157 | it('should support multiple files of the same name', function(done){ 158 | var app = connect(); 159 | 160 | app.use(connect.multipart()); 161 | 162 | app.use(function(req, res){ 163 | req.files.text.should.have.length(2); 164 | req.files.text[0].constructor.name.should.equal('File'); 165 | req.files.text[1].constructor.name.should.equal('File'); 166 | res.end(); 167 | }); 168 | 169 | app.request() 170 | .post('/') 171 | .set('Content-Type', 'multipart/form-data; boundary=foo') 172 | .write('--foo\r\n') 173 | .write('Content-Disposition: form-data; name="text"; filename="foo.txt"\r\n') 174 | .write('\r\n') 175 | .write('some text here') 176 | .write('\r\n--foo\r\n') 177 | .write('Content-Disposition: form-data; name="text"; filename="bar.txt"\r\n') 178 | .write('\r\n') 179 | .write('some more text stuff') 180 | .write('\r\n--foo--') 181 | .end(function(res){ 182 | res.statusCode.should.equal(200); 183 | done(); 184 | }); 185 | }) 186 | 187 | it('should support nested files', function(done){ 188 | var app = connect(); 189 | 190 | app.use(connect.multipart()); 191 | 192 | app.use(function(req, res){ 193 | Object.keys(req.files.docs).should.have.length(2); 194 | req.files.docs.foo.name.should.equal('foo.txt'); 195 | req.files.docs.bar.name.should.equal('bar.txt'); 196 | res.end(); 197 | }); 198 | 199 | app.request() 200 | .post('/') 201 | .set('Content-Type', 'multipart/form-data; boundary=foo') 202 | .write('--foo\r\n') 203 | .write('Content-Disposition: form-data; name="docs[foo]"; filename="foo.txt"\r\n') 204 | .write('\r\n') 205 | .write('some text here') 206 | .write('\r\n--foo\r\n') 207 | .write('Content-Disposition: form-data; name="docs[bar]"; filename="bar.txt"\r\n') 208 | .write('\r\n') 209 | .write('some more text stuff') 210 | .write('\r\n--foo--') 211 | .end(function(res){ 212 | res.statusCode.should.equal(200); 213 | done(); 214 | }); 215 | }) 216 | 217 | it('should next(err) on multipart failure', function(done){ 218 | var app = connect(); 219 | 220 | app.use(connect.multipart()); 221 | 222 | app.use(function(req, res){ 223 | res.end('whoop'); 224 | }); 225 | 226 | app.use(function(err, req, res, next){ 227 | err.message.should.equal('parser error, 16 of 28 bytes parsed'); 228 | res.statusCode = err.status; 229 | res.end('bad request'); 230 | }); 231 | 232 | app.request() 233 | .post('/') 234 | .set('Content-Type', 'multipart/form-data; boundary=foo') 235 | .write('--foo\r\n') 236 | .write('Content-filename="foo.txt"\r\n') 237 | .write('\r\n') 238 | .write('some text here') 239 | .write('Content-Disposition: form-data; name="text"; filename="bar.txt"\r\n') 240 | .write('\r\n') 241 | .write('some more text stuff') 242 | .write('\r\n--foo--') 243 | .end(function(res){ 244 | res.statusCode.should.equal(400); 245 | res.body.should.equal('bad request'); 246 | done(); 247 | }); 248 | }) 249 | 250 | it('should default req.files to {}', function(done){ 251 | var app = connect(); 252 | 253 | app.use(connect.multipart()); 254 | 255 | app.use(function(req, res){ 256 | res.end(JSON.stringify(req.files)); 257 | }); 258 | 259 | app.request() 260 | .post('/') 261 | .end(function(res){ 262 | res.body.should.equal('{}'); 263 | done(); 264 | }); 265 | }) 266 | 267 | it('should defer processing if `defer` is set', function(done){ 268 | var app = connect(); 269 | 270 | app.use(connect.multipart({ defer: true })); 271 | 272 | app.use(function(req, res){ 273 | JSON.stringify(req.body).should.equal("{}"); 274 | req.form.on("end", function() { 275 | res.end(JSON.stringify(req.body)); 276 | }); 277 | }); 278 | 279 | app.request() 280 | .post('/') 281 | .set('Content-Type', 'multipart/form-data; boundary=foo') 282 | .write('--foo\r\n') 283 | .write('Content-Disposition: form-data; name="user"\r\n') 284 | .write('\r\n') 285 | .write('Tobi') 286 | .write('\r\n--foo\r\n') 287 | .write('Content-Disposition: form-data; name="age"\r\n') 288 | .write('\r\n') 289 | .write('1') 290 | .write('\r\n--foo--') 291 | .end(function(res){ 292 | res.body.should.equal('{"user":"Tobi","age":"1"}'); 293 | done(); 294 | }); 295 | }) 296 | 297 | }) 298 | }) 299 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Connect - utils 3 | * Copyright(c) 2010 Sencha Inc. 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var http = require('http') 13 | , crypto = require('crypto') 14 | , parse = require('url').parse 15 | , signature = require('cookie-signature') 16 | , nodeVersion = process.versions.node.split('.'); 17 | 18 | // pause is broken in node < 0.10 19 | exports.brokenPause = parseInt(nodeVersion[0], 10) === 0 20 | && parseInt(nodeVersion[1], 10) < 10; 21 | 22 | /** 23 | * Return `true` if the request has a body, otherwise return `false`. 24 | * 25 | * @param {IncomingMessage} req 26 | * @return {Boolean} 27 | * @api private 28 | */ 29 | 30 | exports.hasBody = function(req) { 31 | return 'transfer-encoding' in req.headers 32 | || req.headers["content-length"] !== '0'; 33 | }; 34 | 35 | /** 36 | * Extract the mime type from the given request's 37 | * _Content-Type_ header. 38 | * 39 | * @param {IncomingMessage} req 40 | * @return {String} 41 | * @api private 42 | */ 43 | 44 | exports.mime = function(req) { 45 | var str = req.headers['content-type'] || ''; 46 | return str.split(';')[0]; 47 | }; 48 | 49 | /** 50 | * Generate an `Error` from the given status `code` 51 | * and optional `msg`. 52 | * 53 | * @param {Number} code 54 | * @param {String} msg 55 | * @return {Error} 56 | * @api private 57 | */ 58 | 59 | exports.error = function(code, msg){ 60 | var err = new Error(msg || http.STATUS_CODES[code]); 61 | err.status = code; 62 | return err; 63 | }; 64 | 65 | /** 66 | * Return md5 hash of the given string and optional encoding, 67 | * defaulting to hex. 68 | * 69 | * utils.md5('wahoo'); 70 | * // => "e493298061761236c96b02ea6aa8a2ad" 71 | * 72 | * @param {String} str 73 | * @param {String} encoding 74 | * @return {String} 75 | * @api private 76 | */ 77 | 78 | exports.md5 = function(str, encoding){ 79 | return crypto 80 | .createHash('md5') 81 | .update(str) 82 | .digest(encoding || 'hex'); 83 | }; 84 | 85 | /** 86 | * Merge object b with object a. 87 | * 88 | * var a = { foo: 'bar' } 89 | * , b = { bar: 'baz' }; 90 | * 91 | * utils.merge(a, b); 92 | * // => { foo: 'bar', bar: 'baz' } 93 | * 94 | * @param {Object} a 95 | * @param {Object} b 96 | * @return {Object} 97 | * @api private 98 | */ 99 | 100 | exports.merge = function(a, b){ 101 | if (a && b) { 102 | for (var key in b) { 103 | a[key] = b[key]; 104 | } 105 | } 106 | return a; 107 | }; 108 | 109 | /** 110 | * Escape the given string of `html`. 111 | * 112 | * @param {String} html 113 | * @return {String} 114 | * @api private 115 | */ 116 | 117 | exports.escape = function(html){ 118 | return String(html) 119 | .replace(/&(?!\w+;)/g, '&') 120 | .replace(//g, '>') 122 | .replace(/"/g, '"'); 123 | }; 124 | 125 | 126 | /** 127 | * Return a unique identifier with the given `len`. 128 | * 129 | * utils.uid(10); 130 | * // => "FDaS435D2z" 131 | * 132 | * @param {Number} len 133 | * @return {String} 134 | * @api private 135 | */ 136 | 137 | exports.uid = function(len) { 138 | return crypto.randomBytes(Math.ceil(len * 3 / 4)) 139 | .toString('base64') 140 | .slice(0, len) 141 | .replace(/\//g, '-') 142 | .replace(/\+/g, '_'); 143 | }; 144 | 145 | /** 146 | * Sign the given `val` with `secret`. 147 | * 148 | * @param {String} val 149 | * @param {String} secret 150 | * @return {String} 151 | * @api private 152 | */ 153 | 154 | exports.sign = function(val, secret){ 155 | console.warn('do not use utils.sign(), use https://github.com/visionmedia/node-cookie-signature') 156 | return val + '.' + crypto 157 | .createHmac('sha256', secret) 158 | .update(val) 159 | .digest('base64') 160 | .replace(/=+$/, ''); 161 | }; 162 | 163 | /** 164 | * Unsign and decode the given `val` with `secret`, 165 | * returning `false` if the signature is invalid. 166 | * 167 | * @param {String} val 168 | * @param {String} secret 169 | * @return {String|Boolean} 170 | * @api private 171 | */ 172 | 173 | exports.unsign = function(val, secret){ 174 | console.warn('do not use utils.unsign(), use https://github.com/visionmedia/node-cookie-signature') 175 | var str = val.slice(0, val.lastIndexOf('.')); 176 | return exports.sign(str, secret) == val 177 | ? str 178 | : false; 179 | }; 180 | 181 | /** 182 | * Parse signed cookies, returning an object 183 | * containing the decoded key/value pairs, 184 | * while removing the signed key from `obj`. 185 | * 186 | * @param {Object} obj 187 | * @return {Object} 188 | * @api private 189 | */ 190 | 191 | exports.parseSignedCookies = function(obj, secret){ 192 | var ret = {}; 193 | Object.keys(obj).forEach(function(key){ 194 | var val = obj[key]; 195 | if (0 == val.indexOf('s:')) { 196 | val = signature.unsign(val.slice(2), secret); 197 | if (val) { 198 | ret[key] = val; 199 | delete obj[key]; 200 | } 201 | } 202 | }); 203 | return ret; 204 | }; 205 | 206 | /** 207 | * Parse a signed cookie string, return the decoded value 208 | * 209 | * @param {String} str signed cookie string 210 | * @param {String} secret 211 | * @return {String} decoded value 212 | * @api private 213 | */ 214 | 215 | exports.parseSignedCookie = function(str, secret){ 216 | return 0 == str.indexOf('s:') 217 | ? signature.unsign(str.slice(2), secret) 218 | : str; 219 | }; 220 | 221 | /** 222 | * Parse JSON cookies. 223 | * 224 | * @param {Object} obj 225 | * @return {Object} 226 | * @api private 227 | */ 228 | 229 | exports.parseJSONCookies = function(obj){ 230 | Object.keys(obj).forEach(function(key){ 231 | var val = obj[key]; 232 | var res = exports.parseJSONCookie(val); 233 | if (res) obj[key] = res; 234 | }); 235 | return obj; 236 | }; 237 | 238 | /** 239 | * Parse JSON cookie string 240 | * 241 | * @param {String} str 242 | * @return {Object} Parsed object or null if not json cookie 243 | * @api private 244 | */ 245 | 246 | exports.parseJSONCookie = function(str) { 247 | if (0 == str.indexOf('j:')) { 248 | try { 249 | return JSON.parse(str.slice(2)); 250 | } catch (err) { 251 | // no op 252 | } 253 | } 254 | }; 255 | 256 | /** 257 | * Pause `data` and `end` events on the given `obj`. 258 | * Middleware performing async tasks _should_ utilize 259 | * this utility (or similar), to re-emit data once 260 | * the async operation has completed, otherwise these 261 | * events may be lost. Pause is only required for 262 | * node versions less than 10, and is replaced with 263 | * noop's otherwise. 264 | * 265 | * var pause = utils.pause(req); 266 | * fs.readFile(path, function(){ 267 | * next(); 268 | * pause.resume(); 269 | * }); 270 | * 271 | * @param {Object} obj 272 | * @return {Object} 273 | * @api private 274 | */ 275 | 276 | exports.pause = exports.brokenPause 277 | ? require('pause') 278 | : function () { 279 | return { 280 | end: noop, 281 | resume: noop 282 | } 283 | } 284 | 285 | /** 286 | * Strip `Content-*` headers from `res`. 287 | * 288 | * @param {ServerResponse} res 289 | * @api private 290 | */ 291 | 292 | exports.removeContentHeaders = function(res){ 293 | Object.keys(res._headers).forEach(function(field){ 294 | if (0 == field.indexOf('content')) { 295 | res.removeHeader(field); 296 | } 297 | }); 298 | }; 299 | 300 | /** 301 | * Check if `req` is a conditional GET request. 302 | * 303 | * @param {IncomingMessage} req 304 | * @return {Boolean} 305 | * @api private 306 | */ 307 | 308 | exports.conditionalGET = function(req) { 309 | return req.headers['if-modified-since'] 310 | || req.headers['if-none-match']; 311 | }; 312 | 313 | /** 314 | * Respond with 401 "Unauthorized". 315 | * 316 | * @param {ServerResponse} res 317 | * @param {String} realm 318 | * @api private 319 | */ 320 | 321 | exports.unauthorized = function(res, realm) { 322 | res.statusCode = 401; 323 | res.setHeader('WWW-Authenticate', 'Basic realm="' + realm + '"'); 324 | res.end('Unauthorized'); 325 | }; 326 | 327 | /** 328 | * Respond with 304 "Not Modified". 329 | * 330 | * @param {ServerResponse} res 331 | * @param {Object} headers 332 | * @api private 333 | */ 334 | 335 | exports.notModified = function(res) { 336 | exports.removeContentHeaders(res); 337 | res.statusCode = 304; 338 | res.end(); 339 | }; 340 | 341 | /** 342 | * Return an ETag in the form of `"