diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..4b5665da7d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index fbb64d6ad6..90dbb6975a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ -.DS_Store +_site node_modules -*.sock -testing diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000000..4a9d77fc56 --- /dev/null +++ b/Gemfile @@ -0,0 +1 @@ +gem 'github-pages' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000000..22d13c3ee6 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,127 @@ +GEM + specs: + RedCloth (4.2.9) + activesupport (4.2.1) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + blankslate (2.1.2.4) + celluloid (0.16.0) + timers (~> 4.0.0) + classifier-reborn (2.0.3) + fast-stemmer (~> 1.0) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.9.1.1) + colorator (0.1) + execjs (2.5.2) + fast-stemmer (1.0.2) + ffi (1.9.8) + gemoji (2.1.0) + github-pages (37) + RedCloth (= 4.2.9) + github-pages-health-check (~> 0.2) + jekyll (= 2.4.0) + jekyll-coffeescript (= 1.0.1) + jekyll-feed (= 0.3.0) + jekyll-mentions (= 0.2.1) + jekyll-redirect-from (= 0.8.0) + jekyll-sass-converter (= 1.2.0) + jekyll-sitemap (= 0.8.1) + jemoji (= 0.4.0) + kramdown (= 1.5.0) + liquid (= 2.6.2) + maruku (= 0.7.0) + mercenary (~> 0.3) + pygments.rb (= 0.6.1) + rdiscount (= 2.1.7) + redcarpet (= 3.1.2) + terminal-table (~> 1.4) + github-pages-health-check (0.2.2) + net-dns (~> 0.6) + public_suffix (~> 1.4) + hitimes (1.2.2) + html-pipeline (1.9.0) + activesupport (>= 2) + nokogiri (~> 1.4) + i18n (0.7.0) + jekyll (2.4.0) + classifier-reborn (~> 2.0) + colorator (~> 0.1) + jekyll-coffeescript (~> 1.0) + jekyll-gist (~> 1.0) + jekyll-paginate (~> 1.0) + jekyll-sass-converter (~> 1.0) + jekyll-watch (~> 1.1) + kramdown (~> 1.3) + liquid (~> 2.6.1) + mercenary (~> 0.3.3) + pygments.rb (~> 0.6.0) + redcarpet (~> 3.1) + safe_yaml (~> 1.0) + toml (~> 0.1.0) + jekyll-coffeescript (1.0.1) + coffee-script (~> 2.2) + jekyll-feed (0.3.0) + jekyll-gist (1.2.1) + jekyll-mentions (0.2.1) + html-pipeline (~> 1.9.0) + jekyll (~> 2.0) + jekyll-paginate (1.1.0) + jekyll-redirect-from (0.8.0) + jekyll (>= 2.0) + jekyll-sass-converter (1.2.0) + sass (~> 3.2) + jekyll-sitemap (0.8.1) + jekyll-watch (1.2.1) + listen (~> 2.7) + jemoji (0.4.0) + gemoji (~> 2.0) + html-pipeline (~> 1.9) + jekyll (~> 2.0) + json (1.8.2) + kramdown (1.5.0) + liquid (2.6.2) + listen (2.10.0) + celluloid (~> 0.16.0) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + maruku (0.7.0) + mercenary (0.3.5) + mini_portile (0.6.2) + minitest (5.6.0) + net-dns (0.8.0) + nokogiri (1.6.6.2) + mini_portile (~> 0.6.0) + parslet (1.5.0) + blankslate (~> 2.0) + posix-spawn (0.3.11) + public_suffix (1.5.1) + pygments.rb (0.6.1) + posix-spawn (~> 0.3.6) + yajl-ruby (~> 1.2.0) + rb-fsevent (0.9.4) + rb-inotify (0.9.5) + ffi (>= 0.5.0) + rdiscount (2.1.7) + redcarpet (3.1.2) + safe_yaml (1.0.4) + sass (3.4.13) + terminal-table (1.4.5) + thread_safe (0.3.5) + timers (4.0.1) + hitimes + toml (0.1.2) + parslet (~> 1.5.0) + tzinfo (1.2.2) + thread_safe (~> 0.1) + yajl-ruby (1.2.1) + +PLATFORMS + ruby + +DEPENDENCIES + github-pages diff --git a/Makefile b/Makefile deleted file mode 100644 index 51fbe9831a..0000000000 --- a/Makefile +++ /dev/null @@ -1,10 +0,0 @@ - -index.html: head.html foot.html index.md - @markdown < index.md \ - | cat head.html - foot.html \ - > $@ - -clean: - rm -f index.html - -.PHONY: clean \ No newline at end of file diff --git a/README.md b/README.md index a89c99485d..38de2ad63a 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,27 @@ -# mocha: How to Build the Site +# [mochajs.org](http://mochajs.org): How to Build the Site -So you wanna build the site? +*So you wanna build the site?* -## Requirements +mochajs.org is now built using [Jekyll](http://jekyllrb.com), the popular static site generator. -There's two: +## Prerequisites -1. [make](http://www.gnu.org/software/make/) -2. [markdown](http://daringfireball.net/projects/markdown/) +1. Some recent version of Ruby +2. A recent version of Node.JS +3. [Bundler](http://bundler.io) -### Mac OS X + To install Bundler, after installing Ruby, execute `gem install bundler`. + +4. Now, execute `npm install`. This will install Jekyll and a bunch of other + crap we need. + +5. To build, execute `npm run-script build`. -Install `markdown` via Homebrew: +For more information, refer to the [Jekyll Docs](http://jekyllrb.com/docs/home/) and [GitHub's Tutorial](https://help.github.com/articles/using-jekyll-with-pages/) on the subject. -```sh -brew install markdown -``` +## Notes -Or download from [here](http://daringfireball.net/projects/markdown/). - -Celebrate with tequila! Or try to build first. Probably want to build first. - -### Linux - -#### Ubuntu 14.04 - -1. `sudo apt-get install build-essential` to install make. -2. `sudo apt-get install markdown` to install markdown. -3. That seems to do it. It's just a Perl script, so you can [get it from here](http://daringfireball.net/projects/markdown/) otherwise. - -### Windows - -*To be filled in by somebody using Windows* - -## Building - -Execute: - -``` -make clean && make -``` - -You should now have an updated `index.html`. Open it in your browser and proceed to tweak it until it's correct, because the compiler seem a little wonky. +- The TOC is generated with [marked-toc](https://www.npmjs.com/package/marked-toc). +- The `id` attributes for all of the headers are generated with JavaScript. +- The `_site` directory is where the generated site lives. It is *not* under version control, because GitHub Pages generates it for us. diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000000..c46cbc71d9 --- /dev/null +++ b/_config.yml @@ -0,0 +1,10 @@ +url: http://mochajs.org +markdown: redcarpet +highlighter: pygments +exclude: + - node_modules + - README.md + - Gemfile* + - package.json + - CNAME + diff --git a/_includes/footer.html b/_includes/footer.html new file mode 100644 index 0000000000..0837114608 --- /dev/null +++ b/_includes/footer.html @@ -0,0 +1,28 @@ + diff --git a/_includes/head.html b/_includes/head.html new file mode 100644 index 0000000000..097bf8343a --- /dev/null +++ b/_includes/head.html @@ -0,0 +1,12 @@ +
+ +simple, flexible, fun
+%s
%e', str)); - } - - // toggle code - var h2 = el.getElementsByTagName('h2')[0]; - - on(h2, 'click', function(){ - pre.style.display = 'none' == pre.style.display - ? 'block' - : 'none'; - }); - - // code - // TODO: defer - if (!test.pending) { - var pre = fragment('
%e
', utils.clean(test.fn.toString()));
- el.appendChild(pre);
- pre.style.display = 'none';
- }
-
- stack[0].appendChild(el);
- });
-}
-
-/**
- * Display error `msg`.
- */
-
-function error(msg) {
- document.body.appendChild(fragment('Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect | |
4 | * Copyright(c) 2010 Sencha Inc. | |
5 | * Copyright(c) 2011 TJ Holowaychuk | |
6 | * MIT Licensed | |
7 | */ | |
8 | ||
9 | /** | |
10 | * Module dependencies. | |
11 | */ | |
12 | ||
13 | 1 | var EventEmitter = require('events').EventEmitter |
14 | , proto = require('./proto') | |
15 | , utils = require('./utils') | |
16 | , path = require('path') | |
17 | , basename = path.basename | |
18 | , fs = require('fs'); | |
19 | ||
20 | // node patches | |
21 | ||
22 | 1 | require('./patch'); |
23 | ||
24 | // expose createServer() as the module | |
25 | ||
26 | 1 | exports = module.exports = createServer; |
27 | ||
28 | /** | |
29 | * Framework version. | |
30 | */ | |
31 | ||
32 | 1 | exports.version = '2.0.0alpha1'; |
33 | ||
34 | /** | |
35 | * Expose the prototype. | |
36 | */ | |
37 | ||
38 | 1 | exports.proto = proto; |
39 | ||
40 | /** | |
41 | * Auto-load middleware getters. | |
42 | */ | |
43 | ||
44 | 1 | exports.middleware = {}; |
45 | ||
46 | /** | |
47 | * Expose utilities. | |
48 | */ | |
49 | ||
50 | 1 | exports.utils = utils; |
51 | ||
52 | /** | |
53 | * Create a new connect server. | |
54 | * | |
55 | * @return {Function} | |
56 | * @api public | |
57 | */ | |
58 | ||
59 | 1 | function createServer() { |
60 | 199 | function app(req, res){ app.handle(req, res); } |
61 | 71 | utils.merge(app, proto); |
62 | 71 | utils.merge(app, EventEmitter.prototype); |
63 | 71 | app.route = '/'; |
64 | 71 | app.stack = []; |
65 | 71 | return app; |
66 | 1 | }; |
67 | ||
68 | /** | |
69 | * Auto-load bundled middleware with getters. | |
70 | */ | |
71 | ||
72 | 1 | fs.readdirSync(__dirname + '/middleware').forEach(function(filename){ |
73 | 23 | if (!/\.js$/.test(filename)) return; |
74 | 21 | var name = basename(filename, '.js'); |
75 | 21 | function load(){ |
76 | 99 | return require('./middleware/' + name); |
77 | } | |
78 | 21 | exports.middleware.__defineGetter__(name, load); |
79 | 21 | exports.__defineGetter__(name, load); |
80 | }); |
Line | Hits | Source |
---|---|---|
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 | 1 | var http = require('http') |
14 | , parse = require('url').parse | |
15 | , utils = require('./utils'); | |
16 | ||
17 | // prototype | |
18 | ||
19 | 1 | var app = module.exports = {}; |
20 | ||
21 | // environment | |
22 | ||
23 | 1 | 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 | 1 | app.use = function(route, fn){ |
63 | // default route to '/' | |
64 | 155 | if ('string' != typeof route) { |
65 | 150 | fn = route; |
66 | 150 | route = '/'; |
67 | } | |
68 | ||
69 | // wrap sub-apps | |
70 | 155 | if ('function' == typeof fn.handle) { |
71 | 4 | var server = fn; |
72 | 4 | fn.route = route; |
73 | 4 | fn = function(req, res, next){ |
74 | 2 | server.handle(req, res, next); |
75 | }; | |
76 | } | |
77 | ||
78 | // wrap vanilla http.Servers | |
79 | 155 | if (fn instanceof http.Server) { |
80 | 1 | fn = fn.listeners('request')[0]; |
81 | } | |
82 | ||
83 | // strip trailing slash | |
84 | 155 | if ('/' == route[route.length - 1]) { |
85 | 151 | route = route.slice(0, -1); |
86 | } | |
87 | ||
88 | // add the middleware | |
89 | 155 | this.stack.push({ route: route, handle: fn }); |
90 | ||
91 | 155 | return this; |
92 | }; | |
93 | ||
94 | /** | |
95 | * Handle server requests, punting them down | |
96 | * the middleware stack. | |
97 | * | |
98 | * @api private | |
99 | */ | |
100 | ||
101 | 1 | app.handle = function(req, res, out) { |
102 | 130 | var stack = this.stack |
103 | , fqdn = ~req.url.indexOf('://') | |
104 | , removed = '' | |
105 | , index = 0; | |
106 | ||
107 | 130 | function next(err) { |
108 | 268 | var layer, path, status, c; |
109 | 268 | req.url = removed + req.url; |
110 | 268 | req.originalUrl = req.originalUrl || req.url; |
111 | 268 | removed = ''; |
112 | ||
113 | // next callback | |
114 | 268 | layer = stack[index++]; |
115 | ||
116 | // all done | |
117 | 268 | if (!layer || res.headerSent) { |
118 | // delegate to parent | |
119 | 11 | if (out) return out(err); |
120 | ||
121 | // unhandled error | |
122 | 11 | if (err) { |
123 | // default to 500 | |
124 | 16 | if (res.statusCode < 400) res.statusCode = 500; |
125 | ||
126 | // respect err.status | |
127 | 14 | if (err.status) res.statusCode = err.status; |
128 | ||
129 | // production gets a basic error message | |
130 | 8 | var msg = 'production' == env |
131 | ? http.STATUS_CODES[res.statusCode] | |
132 | : err.stack || err.toString(); | |
133 | ||
134 | // log to stderr in a non-test env | |
135 | 8 | if ('test' != env) console.error(err.stack || err.toString()); |
136 | 8 | if (res.headerSent) return req.socket.destroy(); |
137 | 8 | res.setHeader('Content-Type', 'text/plain'); |
138 | 8 | res.setHeader('Content-Length', Buffer.byteLength(msg)); |
139 | 8 | if ('HEAD' == req.method) return res.end(); |
140 | 8 | res.end(msg); |
141 | } else { | |
142 | 3 | res.statusCode = 404; |
143 | 3 | res.setHeader('Content-Type', 'text/plain'); |
144 | 3 | if ('HEAD' == req.method) return res.end(); |
145 | 3 | res.end('Cannot ' + req.method + ' ' + utils.escape(req.url)); |
146 | } | |
147 | 11 | return; |
148 | } | |
149 | ||
150 | 257 | try { |
151 | 257 | path = parse(req.url).pathname; |
152 | 257 | if (undefined == path) path = '/'; |
153 | ||
154 | // skip this layer if the route doesn't match. | |
155 | 257 | if (0 != path.indexOf(layer.route)) return next(err); |
156 | ||
157 | 257 | c = path[layer.route.length]; |
158 | 257 | if (c && '/' != c && '.' != c) return next(err); |
159 | ||
160 | // Call the layer handler | |
161 | // Trim off the part of the url that matches the route | |
162 | 257 | removed = layer.route; |
163 | 257 | req.url = req.url.substr(removed.length); |
164 | ||
165 | // Ensure leading slash | |
166 | 260 | if (!fqdn && '/' != req.url[0]) req.url = '/' + req.url; |
167 | ||
168 | 257 | var arity = layer.handle.length; |
169 | 257 | if (err) { |
170 | 8 | if (arity === 4) { |
171 | 3 | layer.handle(err, req, res, next); |
172 | } else { | |
173 | 5 | next(err); |
174 | } | |
175 | 249 | } else if (arity < 4) { |
176 | 249 | layer.handle(req, res, next); |
177 | } else { | |
178 | 0 | next(); |
179 | } | |
180 | } catch (e) { | |
181 | 1 | next(e); |
182 | } | |
183 | } | |
184 | 130 | next(); |
185 | }; |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect - utils | |
4 | * Copyright(c) 2010 Sencha Inc. | |
5 | * Copyright(c) 2011 TJ Holowaychuk | |
6 | * MIT Licensed | |
7 | */ | |
8 | ||
9 | /** | |
10 | * Module dependencies. | |
11 | */ | |
12 | ||
13 | 1 | var http = require('http') |
14 | , crypto = require('crypto') | |
15 | , Path = require('path') | |
16 | , fs = require('fs'); | |
17 | ||
18 | /** | |
19 | * Extract the mime type from the given request's | |
20 | * _Content-Type_ header. | |
21 | * | |
22 | * @param {IncomingMessage} req | |
23 | * @return {String} | |
24 | * @api private | |
25 | */ | |
26 | ||
27 | 1 | exports.mime = function(req) { |
28 | 49 | var str = req.headers['content-type'] || ''; |
29 | 49 | return str.split(';')[0]; |
30 | }; | |
31 | ||
32 | /** | |
33 | * Generate an `Error` from the given status `code`. | |
34 | * | |
35 | * @param {Number} code | |
36 | * @return {Error} | |
37 | * @api private | |
38 | */ | |
39 | ||
40 | 1 | exports.error = function(code){ |
41 | 5 | var err = new Error(http.STATUS_CODES[code]); |
42 | 5 | err.status = code; |
43 | 5 | return err; |
44 | }; | |
45 | ||
46 | /** | |
47 | * Flatten the given `arr`. | |
48 | * | |
49 | * @param {Array} arr | |
50 | * @return {Array} | |
51 | * @api private | |
52 | */ | |
53 | ||
54 | 1 | exports.flatten = function(arr, ret){ |
55 | 0 | var ret = ret || [] |
56 | , len = arr.length; | |
57 | 0 | for (var i = 0; i < len; ++i) { |
58 | 0 | if (Array.isArray(arr[i])) { |
59 | 0 | exports.flatten(arr[i], ret); |
60 | } else { | |
61 | 0 | ret.push(arr[i]); |
62 | } | |
63 | } | |
64 | 0 | return ret; |
65 | }; | |
66 | ||
67 | /** | |
68 | * Return md5 hash of the given string and optional encoding, | |
69 | * defaulting to hex. | |
70 | * | |
71 | * utils.md5('wahoo'); | |
72 | * // => "e493298061761236c96b02ea6aa8a2ad" | |
73 | * | |
74 | * @param {String} str | |
75 | * @param {String} encoding | |
76 | * @return {String} | |
77 | * @api public | |
78 | */ | |
79 | ||
80 | 1 | exports.md5 = function(str, encoding){ |
81 | 0 | return crypto |
82 | .createHash('md5') | |
83 | .update(str) | |
84 | .digest(encoding || 'hex'); | |
85 | }; | |
86 | ||
87 | /** | |
88 | * Merge object b with object a. | |
89 | * | |
90 | * var a = { foo: 'bar' } | |
91 | * , b = { bar: 'baz' }; | |
92 | * | |
93 | * utils.merge(a, b); | |
94 | * // => { foo: 'bar', bar: 'baz' } | |
95 | * | |
96 | * @param {Object} a | |
97 | * @param {Object} b | |
98 | * @return {Object} | |
99 | * @api private | |
100 | */ | |
101 | ||
102 | 1 | exports.merge = function(a, b){ |
103 | 178 | if (a && b) { |
104 | 178 | for (var key in b) { |
105 | 885 | a[key] = b[key]; |
106 | } | |
107 | } | |
108 | 178 | return a; |
109 | }; | |
110 | ||
111 | /** | |
112 | * Escape the given string of `html`. | |
113 | * | |
114 | * @param {String} html | |
115 | * @return {String} | |
116 | * @api private | |
117 | */ | |
118 | ||
119 | 1 | exports.escape = function(html){ |
120 | 3 | return String(html) |
121 | .replace(/&(?!\w+;)/g, '&') | |
122 | .replace(/</g, '<') | |
123 | .replace(/>/g, '>') | |
124 | .replace(/"/g, '"'); | |
125 | }; | |
126 | ||
127 | ||
128 | /** | |
129 | * Return a unique identifier with the given `len`. | |
130 | * | |
131 | * utils.uid(10); | |
132 | * // => "FDaS435D2z" | |
133 | * | |
134 | * @param {Number} len | |
135 | * @return {String} | |
136 | * @api private | |
137 | */ | |
138 | ||
139 | 1 | exports.uid = function(len) { |
140 | 43 | return crypto.randomBytes(Math.ceil(len * 3 / 4)) |
141 | .toString('base64') | |
142 | .slice(0, len); | |
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 | 1 | exports.sign = function(val, secret){ |
155 | 47 | return val + '.' + crypto |
156 | .createHmac('sha1', secret) | |
157 | .update(val) | |
158 | .digest('base64') | |
159 | .replace(/=+$/, ''); | |
160 | }; | |
161 | ||
162 | /** | |
163 | * Unsign and decode the given `val` with `secret`, | |
164 | * returning `false` if the signature is invalid. | |
165 | * | |
166 | * @param {String} val | |
167 | * @param {String} secret | |
168 | * @return {String|Boolean} | |
169 | * @api private | |
170 | */ | |
171 | ||
172 | 1 | exports.unsign = function(val, secret){ |
173 | 17 | var str = val.slice(0,val.lastIndexOf('.')); |
174 | 17 | return exports.sign(str, secret) == val |
175 | ? str | |
176 | : false; | |
177 | }; | |
178 | ||
179 | /** | |
180 | * Parse signed cookies, returning an object | |
181 | * containing the decoded key/value pairs, | |
182 | * while removing the signed key from `obj`. | |
183 | * | |
184 | * @param {Object} obj | |
185 | * @return {Object} | |
186 | * @api private | |
187 | */ | |
188 | ||
189 | 1 | exports.parseSignedCookies = function(obj, secret){ |
190 | 8 | var ret = {}; |
191 | 8 | Object.keys(obj).forEach(function(key){ |
192 | 12 | var val = obj[key] |
193 | , signed = exports.unsign(val, secret); | |
194 | ||
195 | 12 | if (signed) { |
196 | 7 | ret[key] = signed; |
197 | 7 | delete obj[key]; |
198 | } | |
199 | }); | |
200 | 8 | return ret; |
201 | }; | |
202 | ||
203 | /** | |
204 | * Parse JSON cookies. | |
205 | * | |
206 | * @param {Object} obj | |
207 | * @return {Object} | |
208 | * @api private | |
209 | */ | |
210 | ||
211 | 1 | exports.parseJSONCookies = function(obj){ |
212 | 16 | Object.keys(obj).forEach(function(key){ |
213 | 12 | var val = obj[key]; |
214 | 12 | if (0 == val.indexOf('j:')) { |
215 | 2 | try { |
216 | 2 | obj[key] = JSON.parse(val.slice(2)); |
217 | } catch (err) { | |
218 | // nothing | |
219 | } | |
220 | } | |
221 | }); | |
222 | 16 | return obj; |
223 | }; | |
224 | ||
225 | /** | |
226 | * Parse the given cookie string into an object. | |
227 | * | |
228 | * @param {String} str | |
229 | * @return {Object} | |
230 | * @api private | |
231 | */ | |
232 | ||
233 | 1 | exports.parseCookie = function(str){ |
234 | 14 | var obj = {} |
235 | , pairs = str.split(/[;,] */); | |
236 | 14 | for (var i = 0, len = pairs.length; i < len; ++i) { |
237 | 19 | var pair = pairs[i] |
238 | , eqlIndex = pair.indexOf('=') | |
239 | , key = pair.substr(0, eqlIndex).trim() | |
240 | , val = pair.substr(++eqlIndex, pair.length).trim(); | |
241 | ||
242 | // quoted values | |
243 | 20 | if ('"' == val[0]) val = val.slice(1, -1); |
244 | ||
245 | // only assign once | |
246 | 19 | if (undefined == obj[key]) { |
247 | 19 | val = val.replace(/\+/g, ' '); |
248 | 19 | try { |
249 | 19 | obj[key] = decodeURIComponent(val); |
250 | } catch (err) { | |
251 | 0 | if (err instanceof URIError) { |
252 | 0 | obj[key] = val; |
253 | } else { | |
254 | 0 | throw err; |
255 | } | |
256 | } | |
257 | } | |
258 | } | |
259 | 14 | return obj; |
260 | }; | |
261 | ||
262 | /** | |
263 | * Serialize the given object into a cookie string. | |
264 | * | |
265 | * utils.serializeCookie('name', 'tj', { httpOnly: true }) | |
266 | * // => "name=tj; httpOnly" | |
267 | * | |
268 | * @param {String} name | |
269 | * @param {String} val | |
270 | * @param {Object} obj | |
271 | * @return {String} | |
272 | * @api private | |
273 | */ | |
274 | ||
275 | 1 | exports.serializeCookie = function(name, val, obj){ |
276 | 33 | var pairs = [name + '=' + encodeURIComponent(val)] |
277 | , obj = obj || {}; | |
278 | ||
279 | 33 | if (obj.domain) pairs.push('domain=' + obj.domain); |
280 | 62 | if (obj.path) pairs.push('path=' + obj.path); |
281 | 50 | if (obj.expires) pairs.push('expires=' + obj.expires.toUTCString()); |
282 | 57 | if (obj.httpOnly) pairs.push('httpOnly'); |
283 | 37 | if (obj.secure) pairs.push('secure'); |
284 | ||
285 | 33 | return pairs.join('; '); |
286 | }; | |
287 | ||
288 | /** | |
289 | * Pause `data` and `end` events on the given `obj`. | |
290 | * Middleware performing async tasks _should_ utilize | |
291 | * this utility (or similar), to re-emit data once | |
292 | * the async operation has completed, otherwise these | |
293 | * events may be lost. | |
294 | * | |
295 | * var pause = utils.pause(req); | |
296 | * fs.readFile(path, function(){ | |
297 | * next(); | |
298 | * pause.resume(); | |
299 | * }); | |
300 | * | |
301 | * @param {Object} obj | |
302 | * @return {Object} | |
303 | * @api private | |
304 | */ | |
305 | ||
306 | 1 | exports.pause = function(obj){ |
307 | 7 | var onData |
308 | , onEnd | |
309 | , events = []; | |
310 | ||
311 | // buffer data | |
312 | 7 | obj.on('data', onData = function(data, encoding){ |
313 | 0 | events.push(['data', data, encoding]); |
314 | }); | |
315 | ||
316 | // buffer end | |
317 | 7 | obj.on('end', onEnd = function(data, encoding){ |
318 | 6 | events.push(['end', data, encoding]); |
319 | }); | |
320 | ||
321 | 7 | return { |
322 | end: function(){ | |
323 | 6 | obj.removeListener('data', onData); |
324 | 6 | obj.removeListener('end', onEnd); |
325 | }, | |
326 | resume: function(){ | |
327 | 6 | this.end(); |
328 | 6 | for (var i = 0, len = events.length; i < len; ++i) { |
329 | 5 | obj.emit.apply(obj, events[i]); |
330 | } | |
331 | } | |
332 | }; | |
333 | }; | |
334 | ||
335 | /** | |
336 | * Check `req` and `res` to see if it has been modified. | |
337 | * | |
338 | * @param {IncomingMessage} req | |
339 | * @param {ServerResponse} res | |
340 | * @return {Boolean} | |
341 | * @api private | |
342 | */ | |
343 | ||
344 | 1 | exports.modified = function(req, res, headers) { |
345 | 1 | var headers = headers || res._headers || {} |
346 | , modifiedSince = req.headers['if-modified-since'] | |
347 | , lastModified = headers['last-modified'] | |
348 | , noneMatch = req.headers['if-none-match'] | |
349 | , etag = headers['etag']; | |
350 | ||
351 | 1 | if (noneMatch) noneMatch = noneMatch.split(/ *, */); |
352 | ||
353 | // check If-None-Match | |
354 | 1 | if (noneMatch && etag && ~noneMatch.indexOf(etag)) { |
355 | 0 | return false; |
356 | } | |
357 | ||
358 | // check If-Modified-Since | |
359 | 1 | if (modifiedSince && lastModified) { |
360 | 1 | modifiedSince = new Date(modifiedSince); |
361 | 1 | lastModified = new Date(lastModified); |
362 | // Ignore invalid dates | |
363 | 1 | if (!isNaN(modifiedSince.getTime())) { |
364 | 2 | if (lastModified <= modifiedSince) return false; |
365 | } | |
366 | } | |
367 | ||
368 | 0 | return true; |
369 | }; | |
370 | ||
371 | /** | |
372 | * Strip `Content-*` headers from `res`. | |
373 | * | |
374 | * @param {ServerResponse} res | |
375 | * @api private | |
376 | */ | |
377 | ||
378 | 1 | exports.removeContentHeaders = function(res){ |
379 | 1 | Object.keys(res._headers).forEach(function(field){ |
380 | 6 | if (0 == field.indexOf('content')) { |
381 | 1 | res.removeHeader(field); |
382 | } | |
383 | }); | |
384 | }; | |
385 | ||
386 | /** | |
387 | * Check if `req` is a conditional GET request. | |
388 | * | |
389 | * @param {IncomingMessage} req | |
390 | * @return {Boolean} | |
391 | * @api private | |
392 | */ | |
393 | ||
394 | 1 | exports.conditionalGET = function(req) { |
395 | 36 | return req.headers['if-modified-since'] |
396 | || req.headers['if-none-match']; | |
397 | }; | |
398 | ||
399 | /** | |
400 | * Respond with 401 "Unauthorized". | |
401 | * | |
402 | * @param {ServerResponse} res | |
403 | * @param {String} realm | |
404 | * @api private | |
405 | */ | |
406 | ||
407 | 1 | exports.unauthorized = function(res, realm) { |
408 | 6 | res.statusCode = 401; |
409 | 6 | res.setHeader('WWW-Authenticate', 'Basic realm="' + realm + '"'); |
410 | 6 | res.end('Unauthorized'); |
411 | }; | |
412 | ||
413 | /** | |
414 | * Respond with 304 "Not Modified". | |
415 | * | |
416 | * @param {ServerResponse} res | |
417 | * @param {Object} headers | |
418 | * @api private | |
419 | */ | |
420 | ||
421 | 1 | exports.notModified = function(res) { |
422 | 1 | exports.removeContentHeaders(res); |
423 | 1 | res.statusCode = 304; |
424 | 1 | res.end(); |
425 | }; | |
426 | ||
427 | /** | |
428 | * Return an ETag in the form of `"<size>-<mtime>"` | |
429 | * from the given `stat`. | |
430 | * | |
431 | * @param {Object} stat | |
432 | * @return {String} | |
433 | * @api private | |
434 | */ | |
435 | ||
436 | 1 | exports.etag = function(stat) { |
437 | 0 | return '"' + stat.size + '-' + Number(stat.mtime) + '"'; |
438 | }; | |
439 | ||
440 | /** | |
441 | * Parse "Range" header `str` relative to the given file `size`. | |
442 | * | |
443 | * @param {Number} size | |
444 | * @param {String} str | |
445 | * @return {Array} | |
446 | * @api private | |
447 | */ | |
448 | ||
449 | 1 | exports.parseRange = function(size, str){ |
450 | 15 | var valid = true; |
451 | 15 | var arr = str.substr(6).split(',').map(function(range){ |
452 | 15 | var range = range.split('-') |
453 | , start = parseInt(range[0], 10) | |
454 | , end = parseInt(range[1], 10); | |
455 | ||
456 | // -500 | |
457 | 15 | if (isNaN(start)) { |
458 | 4 | start = size - end; |
459 | 4 | end = size - 1; |
460 | // 500- | |
461 | 11 | } else if (isNaN(end)) { |
462 | 3 | end = size - 1; |
463 | } | |
464 | ||
465 | // Invalid | |
466 | 16 | if (isNaN(start) || isNaN(end) || start > end) valid = false; |
467 | ||
468 | 15 | return { start: start, end: end }; |
469 | }); | |
470 | 15 | return valid ? arr : undefined; |
471 | }; | |
472 | ||
473 | /** | |
474 | * Parse the given Cache-Control `str`. | |
475 | * | |
476 | * @param {String} str | |
477 | * @return {Object} | |
478 | * @api private | |
479 | */ | |
480 | ||
481 | 1 | exports.parseCacheControl = function(str){ |
482 | 26 | var directives = str.split(',') |
483 | , obj = {}; | |
484 | ||
485 | 26 | for(var i = 0, len = directives.length; i < len; i++) { |
486 | 37 | var parts = directives[i].split('=') |
487 | , key = parts.shift().trim() | |
488 | , val = parseInt(parts.shift(), 10); | |
489 | ||
490 | 37 | obj[key] = isNaN(val) ? true : val; |
491 | } | |
492 | ||
493 | 26 | return obj; |
494 | }; | |
495 | ||
496 | /** | |
497 | * Convert array-like object to an `Array`. | |
498 | * | |
499 | * node-bench measured "16.5 times faster than Array.prototype.slice.call()" | |
500 | * | |
501 | * @param {Object} obj | |
502 | * @return {Array} | |
503 | * @api private | |
504 | */ | |
505 | ||
506 | 1 | var toArray = exports.toArray = function(obj){ |
507 | 0 | var len = obj.length |
508 | , arr = new Array(len); | |
509 | 0 | for (var i = 0; i < len; ++i) { |
510 | 0 | arr[i] = obj[i]; |
511 | } | |
512 | 0 | return arr; |
513 | }; |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect | |
4 | * Copyright(c) 2011 TJ Holowaychuk | |
5 | * MIT Licensed | |
6 | */ | |
7 | ||
8 | /** | |
9 | * Module dependencies. | |
10 | */ | |
11 | ||
12 | 1 | 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 | 1 | 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 | 1 | res.__defineGetter__('headerSent', function(){ |
31 | 273 | 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 | 1 | res.setHeader = function(field, val){ |
44 | 265 | var key = field.toLowerCase() |
45 | , prev; | |
46 | ||
47 | // special-case Set-Cookie | |
48 | 265 | if (this._headers && 'set-cookie' == key) { |
49 | 3 | if (prev = this.getHeader(field)) { |
50 | 0 | val = Array.isArray(prev) |
51 | ? prev.concat(val) | |
52 | : [prev, val]; | |
53 | } | |
54 | // charset | |
55 | 262 | } else if ('content-type' == key && this.charset) { |
56 | 0 | val += '; charset=' + this.charset; |
57 | } | |
58 | ||
59 | 265 | return setHeader.call(this, field, val); |
60 | }; | |
61 | ||
62 | /** | |
63 | * Proxy to emit "header" event. | |
64 | */ | |
65 | ||
66 | 1 | res._renderHeaders = function(){ |
67 | 78 | if (!this._emittedHeader) this.emit('header'); |
68 | 78 | this._emittedHeader = true; |
69 | 78 | return _renderHeaders.call(this); |
70 | }; | |
71 | ||
72 | 1 | res.writeHead = function(){ |
73 | 256 | if (!this._emittedHeader) this.emit('header'); |
74 | 128 | this._emittedHeader = true; |
75 | 128 | return writeHead.apply(this, arguments); |
76 | }; | |
77 | ||
78 | 1 | res._hasConnectPatch = true; |
79 | } |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect - basicAuth | |
4 | * Copyright(c) 2010 Sencha Inc. | |
5 | * Copyright(c) 2011 TJ Holowaychuk | |
6 | * MIT Licensed | |
7 | */ | |
8 | ||
9 | /** | |
10 | * Module dependencies. | |
11 | */ | |
12 | ||
13 | 1 | var utils = require('../utils') |
14 | , unauthorized = utils.unauthorized; | |
15 | ||
16 | /** | |
17 | * Basic Auth: | |
18 | * | |
19 | * Enfore basic authentication by providing a `callback(user, pass)`, | |
20 | * which must return `true` in order to gain access. Alternatively an async | |
21 | * method is provided as well, invoking `callback(user, pass, callback)`. Populates | |
22 | * `req.user`. The final alternative is simply passing username / password | |
23 | * strings. | |
24 | * | |
25 | * Simple username and password | |
26 | * | |
27 | * connect(connect.basicAuth('username', 'password')); | |
28 | * | |
29 | * Callback verification | |
30 | * | |
31 | * connect() | |
32 | * .use(connect.basicAuth(function(user, pass){ | |
33 | * return 'tj' == user & 'wahoo' == pass; | |
34 | * })) | |
35 | * | |
36 | * Async callback verification, accepting `fn(err, user)`. | |
37 | * | |
38 | * connect() | |
39 | * .use(connect.basicAuth(function(user, pass, fn){ | |
40 | * User.authenticate({ user: user, pass: pass }, fn); | |
41 | * })) | |
42 | * | |
43 | * @param {Function|String} callback or username | |
44 | * @param {String} realm | |
45 | * @api public | |
46 | */ | |
47 | ||
48 | 1 | module.exports = function basicAuth(callback, realm) { |
49 | 3 | var username, password; |
50 | ||
51 | // user / pass strings | |
52 | 3 | if ('string' == typeof callback) { |
53 | 1 | username = callback; |
54 | 1 | password = realm; |
55 | 1 | if ('string' != typeof password) throw new Error('password argument required'); |
56 | 1 | realm = arguments[2]; |
57 | 1 | callback = function(user, pass){ |
58 | 2 | return user == username && pass == password; |
59 | } | |
60 | } | |
61 | ||
62 | 3 | realm = realm || 'Authorization Required'; |
63 | ||
64 | 3 | return function(req, res, next) { |
65 | 9 | var authorization = req.headers.authorization; |
66 | ||
67 | 9 | if (req.user) return next(); |
68 | 12 | if (!authorization) return unauthorized(res, realm); |
69 | ||
70 | 6 | var parts = authorization.split(' ') |
71 | , scheme = parts[0] | |
72 | , credentials = new Buffer(parts[1], 'base64').toString().split(':') | |
73 | , user = credentials[0] | |
74 | , pass = credentials[1]; | |
75 | ||
76 | 6 | if ('Basic' != scheme) return next(utils.error(400)); |
77 | ||
78 | // async | |
79 | 6 | if (callback.length >= 3) { |
80 | 2 | var pause = utils.pause(req); |
81 | 2 | callback(user, pass, function(err, user){ |
82 | 3 | if (err || !user) return unauthorized(res, realm); |
83 | 1 | req.user = user; |
84 | 1 | next(); |
85 | 1 | pause.resume(); |
86 | }); | |
87 | // sync | |
88 | } else { | |
89 | 4 | if (callback(user, pass)) { |
90 | 2 | req.user = user; |
91 | 2 | next(); |
92 | } else { | |
93 | 2 | unauthorized(res, realm); |
94 | } | |
95 | } | |
96 | } | |
97 | }; | |
98 |
Line | Hits | Source |
---|---|---|
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 | 1 | 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 | 1 | exports = module.exports = function bodyParser(options){ |
48 | 6 | var _urlencoded = urlencoded(options) |
49 | , _multipart = multipart(options) | |
50 | , _json = json(options); | |
51 | ||
52 | 6 | return function bodyParser(req, res, next) { |
53 | 11 | _json(req, res, function(err){ |
54 | 11 | if (err) return next(err); |
55 | 11 | _urlencoded(req, res, function(err){ |
56 | 11 | if (err) return next(err); |
57 | 11 | _multipart(req, res, next); |
58 | }); | |
59 | }); | |
60 | } | |
61 | }; |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect - multipart | |
4 | * Copyright(c) 2010 Sencha Inc. | |
5 | * Copyright(c) 2011 TJ Holowaychuk | |
6 | * MIT Licensed | |
7 | */ | |
8 | ||
9 | /** | |
10 | * Module dependencies. | |
11 | */ | |
12 | ||
13 | 1 | var formidable = require('formidable') |
14 | , utils = require('../utils') | |
15 | , qs = require('qs'); | |
16 | ||
17 | /** | |
18 | * Multipart: | |
19 | * | |
20 | * Parse multipart/form-data request bodies, | |
21 | * providing the parsed object as `req.body` | |
22 | * and `req.files`. | |
23 | * | |
24 | * Configuration: | |
25 | * | |
26 | * The options passed are merged with [formidable](https://github.com/felixge/node-formidable)'s | |
27 | * `IncomingForm` object, allowing you to configure the upload directory, | |
28 | * size limits, etc. For example if you wish to change the upload dir do the following. | |
29 | * | |
30 | * app.use(connect.multipart({ uploadDir: path })); | |
31 | * | |
32 | * @param {Object} options | |
33 | * @return {Function} | |
34 | * @api public | |
35 | */ | |
36 | ||
37 | 1 | exports = module.exports = function(options){ |
38 | 13 | options = options || {}; |
39 | 13 | return function multipart(req, res, next) { |
40 | 24 | if (req._body) return next(); |
41 | 20 | req.body = req.body || {}; |
42 | 20 | req.files = req.files || {}; |
43 | ||
44 | // ignore GET | |
45 | 21 | if ('GET' == req.method || 'HEAD' == req.method) return next(); |
46 | ||
47 | // check Content-Type | |
48 | 22 | if ('multipart/form-data' != utils.mime(req)) return next(); |
49 | ||
50 | // flag as parsed | |
51 | 16 | req._body = true; |
52 | ||
53 | // parse | |
54 | 16 | var form = new formidable.IncomingForm |
55 | , data = {} | |
56 | , files = {} | |
57 | , done; | |
58 | ||
59 | 16 | Object.keys(options).forEach(function(key){ |
60 | 2 | form[key] = options[key]; |
61 | }); | |
62 | ||
63 | 16 | function ondata(name, val, data){ |
64 | 32 | if (Array.isArray(data[name])) { |
65 | 0 | data[name].push(val); |
66 | 32 | } else if (data[name]) { |
67 | 2 | data[name] = [data[name], val]; |
68 | } else { | |
69 | 30 | data[name] = val; |
70 | } | |
71 | } | |
72 | ||
73 | 16 | form.on('field', function(name, val){ |
74 | 18 | ondata(name, val, data); |
75 | }); | |
76 | ||
77 | 16 | form.on('file', function(name, val){ |
78 | 14 | ondata(name, val, files); |
79 | }); | |
80 | ||
81 | 16 | form.on('error', function(err){ |
82 | 2 | next(err); |
83 | 2 | done = true; |
84 | }); | |
85 | ||
86 | 16 | form.on('end', function(){ |
87 | 18 | if (done) return; |
88 | 14 | try { |
89 | 14 | req.body = qs.parse(data); |
90 | 14 | req.files = qs.parse(files); |
91 | 14 | next(); |
92 | } catch (err) { | |
93 | 0 | next(err); |
94 | } | |
95 | }); | |
96 | ||
97 | 16 | form.parse(req); |
98 | } | |
99 | }; |
Line | Hits | Source |
---|---|---|
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 | 1 | var utils = require('../utils') |
14 | , qs = require('qs'); | |
15 | ||
16 | /** | |
17 | * Urlencoded: | |
18 | * | |
19 | * Parse x-ww-form-urlencoded request bodies, | |
20 | * providing the parsed object as `req.body`. | |
21 | * | |
22 | * @param {Object} options | |
23 | * @return {Function} | |
24 | * @api public | |
25 | */ | |
26 | ||
27 | 1 | exports = module.exports = function(options){ |
28 | 7 | options = options || {}; |
29 | 7 | return function urlencoded(req, res, next) { |
30 | 14 | if (req._body) return next(); |
31 | 12 | req.body = req.body || {}; |
32 | ||
33 | // ignore GET | |
34 | 12 | if ('GET' == req.method || 'HEAD' == req.method) return next(); |
35 | ||
36 | // check Content-Type | |
37 | 22 | if ('application/x-www-form-urlencoded' != utils.mime(req)) return next(); |
38 | ||
39 | // flag as parsed | |
40 | 2 | req._body = true; |
41 | ||
42 | // parse | |
43 | 2 | var buf = ''; |
44 | 2 | req.setEncoding('utf8'); |
45 | 4 | req.on('data', function(chunk){ buf += chunk }); |
46 | 2 | req.on('end', function(){ |
47 | 2 | try { |
48 | 2 | req.body = buf.length |
49 | ? qs.parse(buf) | |
50 | : {}; | |
51 | 2 | next(); |
52 | } catch (err){ | |
53 | 0 | next(err); |
54 | } | |
55 | }); | |
56 | } | |
57 | }; |
Line | Hits | Source |
---|---|---|
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 | 1 | var utils = require('../utils'); |
14 | ||
15 | /** | |
16 | * JSON: | |
17 | * | |
18 | * Parse JSON request bodies, providing the | |
19 | * parsed object as `req.body`. | |
20 | * | |
21 | * @param {Object} options | |
22 | * @return {Function} | |
23 | * @api public | |
24 | */ | |
25 | ||
26 | 1 | exports = module.exports = function(options){ |
27 | 8 | options = options || {}; |
28 | 8 | return function json(req, res, next) { |
29 | 16 | if (req._body) return next(); |
30 | 16 | req.body = req.body || {}; |
31 | ||
32 | // ignore GET | |
33 | 17 | if ('GET' == req.method || 'HEAD' == req.method) return next(); |
34 | ||
35 | // check Content-Type | |
36 | 26 | if ('application/json' != utils.mime(req)) return next(); |
37 | ||
38 | // flag as parsed | |
39 | 4 | req._body = true; |
40 | ||
41 | // parse | |
42 | 4 | var buf = ''; |
43 | 4 | req.setEncoding('utf8'); |
44 | 8 | req.on('data', function(chunk){ buf += chunk }); |
45 | 4 | req.on('end', function(){ |
46 | 4 | try { |
47 | 4 | req.body = buf.length |
48 | ? JSON.parse(buf) | |
49 | : {}; | |
50 | 2 | next(); |
51 | } catch (err){ | |
52 | 2 | err.status = 400; |
53 | 2 | next(err); |
54 | } | |
55 | }); | |
56 | } | |
57 | }; |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect - compress | |
4 | * Copyright(c) 2010 Sencha Inc. | |
5 | * Copyright(c) 2011 TJ Holowaychuk | |
6 | * MIT Licensed | |
7 | */ | |
8 | ||
9 | /** | |
10 | * Module dependencies. | |
11 | */ | |
12 | ||
13 | 1 | var zlib = require('zlib'); |
14 | ||
15 | /** | |
16 | * Supported content-encoding methods. | |
17 | */ | |
18 | ||
19 | 1 | exports.methods = { |
20 | gzip: zlib.createGzip | |
21 | , deflate: zlib.createDeflate | |
22 | }; | |
23 | ||
24 | /** | |
25 | * Default filter function. | |
26 | */ | |
27 | ||
28 | 1 | exports.filter = function(req, res){ |
29 | 9 | var type = res.getHeader('Content-Type') || ''; |
30 | 9 | return type.match(/json|text|javascript/); |
31 | }; | |
32 | ||
33 | /** | |
34 | * Compress: | |
35 | * | |
36 | * Compress response data with gzip/deflate. | |
37 | * | |
38 | * Filter: | |
39 | * | |
40 | * A `filter` callback function may be passed to | |
41 | * replace the default logic of: | |
42 | * | |
43 | * exports.filter = function(req, res){ | |
44 | * var type = res.getHeader('Content-Type') || ''; | |
45 | * return type.match(/json|text|javascript/); | |
46 | * }; | |
47 | * | |
48 | * Options: | |
49 | * | |
50 | * All remaining options are passed to the gzip/deflate | |
51 | * creation functions. Consult node's docs for additional details. | |
52 | * | |
53 | * - `chunkSize` (default: 16*1024) | |
54 | * - `windowBits` | |
55 | * - `level`: 0-9 where 0 is no compression, and 9 is slow but best compression | |
56 | * - `memLevel`: 1-9 low is slower but uses less memory, high is fast but uses more | |
57 | * - `strategy`: compression strategy | |
58 | * | |
59 | * @param {Object} options | |
60 | * @return {Function} | |
61 | * @api public | |
62 | */ | |
63 | ||
64 | 1 | module.exports = function compress(options) { |
65 | 1 | var options = options || {} |
66 | , names = Object.keys(exports.methods) | |
67 | , filter = options.filter || exports.filter; | |
68 | ||
69 | 1 | return function(req, res, next){ |
70 | 9 | var accept = req.headers['accept-encoding'] |
71 | , write = res.write | |
72 | , end = res.end | |
73 | , stream | |
74 | , method; | |
75 | ||
76 | // vary | |
77 | 9 | res.setHeader('Vary', 'Accept-Encoding'); |
78 | ||
79 | // proxy | |
80 | ||
81 | 9 | res.write = function(chunk, encoding){ |
82 | 14 | if (!this.headerSent) this._implicitHeader(); |
83 | 7 | return stream |
84 | ? stream.write(chunk, encoding) | |
85 | : write.call(res, chunk, encoding); | |
86 | }; | |
87 | ||
88 | 9 | res.end = function(chunk, encoding){ |
89 | 9 | if (chunk) this.write(chunk, encoding); |
90 | 9 | return stream |
91 | ? stream.end() | |
92 | : end.call(res); | |
93 | }; | |
94 | ||
95 | 9 | res.on('header', function(){ |
96 | // default request filter | |
97 | 10 | if (!filter(req, res)) return; |
98 | ||
99 | // SHOULD use identity | |
100 | 9 | if (!accept) return; |
101 | ||
102 | // head | |
103 | 8 | if ('HEAD' == req.method) return; |
104 | ||
105 | // default to gzip | |
106 | 6 | if ('*' == accept.trim()) method = 'gzip'; |
107 | ||
108 | // compression method | |
109 | 6 | if (!method) { |
110 | 6 | for (var i = 0, len = names.length; i < len; ++i) { |
111 | 6 | if (~accept.indexOf(names[i])) { |
112 | 6 | method = names[i]; |
113 | 6 | break; |
114 | } | |
115 | } | |
116 | } | |
117 | ||
118 | // compression method | |
119 | 6 | if (!method) return; |
120 | ||
121 | // compression stream | |
122 | 6 | stream = exports.methods[method](options); |
123 | ||
124 | // header fields | |
125 | 6 | res.setHeader('Content-Encoding', method); |
126 | 6 | res.removeHeader('Content-Length'); |
127 | ||
128 | // compression | |
129 | ||
130 | 6 | stream.on('data', function(chunk){ |
131 | 12 | write.call(res, chunk); |
132 | }); | |
133 | ||
134 | 6 | stream.on('end', function(){ |
135 | 6 | end.call(res); |
136 | }); | |
137 | ||
138 | }); | |
139 | ||
140 | 9 | next(); |
141 | }; | |
142 | } |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * Connect - staticProvider | |
3 | * Copyright(c) 2010 Sencha Inc. | |
4 | * Copyright(c) 2011 TJ Holowaychuk | |
5 | * MIT Licensed | |
6 | */ | |
7 | ||
8 | /** | |
9 | * Module dependencies. | |
10 | */ | |
11 | ||
12 | 1 | var fs = require('fs') |
13 | , path = require('path') | |
14 | , join = path.join | |
15 | , basename = path.basename | |
16 | , normalize = path.normalize | |
17 | , utils = require('../utils') | |
18 | , Buffer = require('buffer').Buffer | |
19 | , parse = require('url').parse | |
20 | , mime = require('mime'); | |
21 | ||
22 | /** | |
23 | * Static: | |
24 | * | |
25 | * Static file server with the given `root` path. | |
26 | * | |
27 | * Examples: | |
28 | * | |
29 | * var oneDay = 86400000; | |
30 | * | |
31 | * connect() | |
32 | * .use(connect.static(__dirname + '/public')) | |
33 | * | |
34 | * connect() | |
35 | * .use(connect.static(__dirname + '/public', { maxAge: oneDay })) | |
36 | * | |
37 | * Options: | |
38 | * | |
39 | * - `maxAge` Browser cache maxAge in milliseconds. defaults to 0 | |
40 | * - `hidden` Allow transfer of hidden files. defaults to false | |
41 | * - `redirect` Redirect to trailing "/" when the pathname is a dir | |
42 | * | |
43 | * @param {String} root | |
44 | * @param {Object} options | |
45 | * @return {Function} | |
46 | * @api public | |
47 | */ | |
48 | ||
49 | 1 | exports = module.exports = function static(root, options){ |
50 | 6 | options = options || {}; |
51 | ||
52 | // root required | |
53 | 6 | if (!root) throw new Error('static() root path required'); |
54 | 6 | options.root = root; |
55 | ||
56 | 6 | return function static(req, res, next) { |
57 | 36 | options.path = req.url; |
58 | 36 | options.getOnly = true; |
59 | 36 | send(req, res, next, options); |
60 | }; | |
61 | }; | |
62 | ||
63 | /** | |
64 | * Expose mime module. | |
65 | * | |
66 | * If you wish to extend the mime table use this | |
67 | * reference to the "mime" module in the npm registry. | |
68 | */ | |
69 | ||
70 | 1 | exports.mime = mime; |
71 | ||
72 | /** | |
73 | * decodeURIComponent. | |
74 | * | |
75 | * Allows V8 to only deoptimize this fn instead of all | |
76 | * of send(). | |
77 | * | |
78 | * @param {String} path | |
79 | * @api private | |
80 | */ | |
81 | ||
82 | 1 | function decode(path){ |
83 | 36 | try { |
84 | 36 | return decodeURIComponent(path); |
85 | } catch (err) { | |
86 | 1 | return err; |
87 | } | |
88 | } | |
89 | ||
90 | /** | |
91 | * Attempt to tranfer the requested file to `res`. | |
92 | * | |
93 | * @param {ServerRequest} | |
94 | * @param {ServerResponse} | |
95 | * @param {Function} next | |
96 | * @param {Object} options | |
97 | * @api private | |
98 | */ | |
99 | ||
100 | 1 | var send = exports.send = function(req, res, next, options){ |
101 | 36 | options = options || {}; |
102 | 36 | if (!options.path) throw new Error('path required'); |
103 | ||
104 | // setup | |
105 | 36 | var maxAge = options.maxAge || 0 |
106 | , ranges = req.headers.range | |
107 | , head = 'HEAD' == req.method | |
108 | , get = 'GET' == req.method | |
109 | , root = options.root ? normalize(options.root) : null | |
110 | , redirect = false === options.redirect ? false : true | |
111 | , getOnly = options.getOnly | |
112 | , fn = options.callback | |
113 | , hidden = options.hidden | |
114 | , done; | |
115 | ||
116 | // replace next() with callback when available | |
117 | 36 | if (fn) next = fn; |
118 | ||
119 | // ignore non-GET requests | |
120 | 36 | if (getOnly && !get && !head) return next(); |
121 | ||
122 | // parse url | |
123 | 36 | var url = parse(options.path) |
124 | , path = decode(url.pathname) | |
125 | , type; | |
126 | ||
127 | 37 | if ('URIError: URI malformed' == path) return next(utils.error(400)); |
128 | ||
129 | // null byte(s) | |
130 | 35 | if (~path.indexOf('\0')) return next(utils.error(400)); |
131 | ||
132 | // when root is not given, consider .. malicious | |
133 | 35 | if (!root && ~path.indexOf('..')) return next(utils.error(403)); |
134 | ||
135 | // index.html support | |
136 | 36 | if (normalize('/') == path[path.length - 1]) path += 'index.html'; |
137 | ||
138 | // join / normalize from optional root dir | |
139 | 35 | path = normalize(join(root, path)); |
140 | ||
141 | // malicious path | |
142 | 37 | if (root && 0 != path.indexOf(root)) return next(utils.error(403)); |
143 | ||
144 | // "hidden" file | |
145 | 34 | if (!hidden && '.' == basename(path)[0]) return next(); |
146 | ||
147 | 32 | fs.stat(path, function(err, stat){ |
148 | // mime type | |
149 | 32 | type = mime.lookup(path); |
150 | ||
151 | // ignore ENOENT | |
152 | 32 | if (err) { |
153 | 1 | if (fn) return fn(err); |
154 | 1 | return ('ENOENT' == err.code || 'ENAMETOOLONG' == err.code) |
155 | ? next() | |
156 | : next(err); | |
157 | // redirect directory in case index.html is present | |
158 | 31 | } else if (stat.isDirectory()) { |
159 | 1 | if (!redirect) return next(); |
160 | 1 | res.statusCode = 301; |
161 | 1 | res.setHeader('Location', url.pathname + '/'); |
162 | 1 | res.end('Redirecting to ' + url.pathname + '/'); |
163 | 1 | return; |
164 | } | |
165 | ||
166 | // header fields | |
167 | 60 | if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString()); |
168 | 58 | if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (maxAge / 1000)); |
169 | 60 | if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString()); |
170 | 30 | if (!res.getHeader('Content-Type')) { |
171 | 30 | var charset = mime.charsets.lookup(type); |
172 | 30 | res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : '')); |
173 | } | |
174 | 30 | res.setHeader('Accept-Ranges', 'bytes'); |
175 | ||
176 | // conditional GET support | |
177 | 30 | if (utils.conditionalGET(req)) { |
178 | 1 | if (!utils.modified(req, res)) { |
179 | 1 | req.emit('static'); |
180 | 1 | return utils.notModified(res); |
181 | } | |
182 | } | |
183 | ||
184 | 29 | var opts = {} |
185 | , len = stat.size; | |
186 | ||
187 | // we have a Range request | |
188 | 29 | if (ranges) { |
189 | 7 | ranges = utils.parseRange(stat.size, ranges); |
190 | // valid | |
191 | 7 | if (ranges) { |
192 | // TODO: stream options | |
193 | // TODO: multiple support | |
194 | 6 | opts.start = ranges[0].start; |
195 | 6 | opts.end = ranges[0].end; |
196 | 6 | len = Math.min(len, opts.end - opts.start + 1); |
197 | 6 | res.statusCode = 206; |
198 | 6 | res.setHeader('Content-Range', 'bytes ' |
199 | + opts.start | |
200 | + '-' | |
201 | + opts.end | |
202 | + '/' | |
203 | + stat.size); | |
204 | // invalid range | |
205 | } else { | |
206 | 1 | return next(utils.error(416)); |
207 | } | |
208 | } | |
209 | ||
210 | 28 | res.setHeader('Content-Length', len); |
211 | ||
212 | // transfer | |
213 | 32 | if (head) return res.end(); |
214 | ||
215 | // stream | |
216 | 24 | var stream = fs.createReadStream(path, opts); |
217 | 24 | req.emit('static', stream); |
218 | 24 | req.on('close', stream.destroy.bind(stream)); |
219 | 24 | stream.pipe(res); |
220 | ||
221 | // callback | |
222 | 24 | if (fn) { |
223 | 0 | function callback(err) { done || fn(err); done = true } |
224 | 0 | req.on('close', callback); |
225 | 0 | req.socket.on('error', callback); |
226 | 0 | stream.on('error', callback); |
227 | 0 | stream.on('end', callback); |
228 | } else { | |
229 | 24 | stream.on('error', function(err){ |
230 | 1 | if (res.headerSent) { |
231 | 0 | console.error(err.stack); |
232 | 0 | req.destroy(); |
233 | } else { | |
234 | 1 | next(err); |
235 | } | |
236 | }); | |
237 | } | |
238 | }); | |
239 | }; |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect - limit | |
4 | * Copyright(c) 2011 TJ Holowaychuk | |
5 | * MIT Licensed | |
6 | */ | |
7 | ||
8 | /** | |
9 | * Module dependencies. | |
10 | */ | |
11 | ||
12 | 1 | var utils = require('../utils'); |
13 | ||
14 | /** | |
15 | * Limit: | |
16 | * | |
17 | * Limit request bodies to the given size in `bytes`. | |
18 | * | |
19 | * A string representation of the bytesize may also be passed, | |
20 | * for example "5mb", "200kb", "1gb", etc. | |
21 | * | |
22 | * connect() | |
23 | * .use(connect.limit('5.5mb')) | |
24 | * .use(handleImageUpload) | |
25 | * | |
26 | * @param {Number|String} bytes | |
27 | * @return {Function} | |
28 | * @api public | |
29 | */ | |
30 | ||
31 | 1 | module.exports = function limit(bytes){ |
32 | 2 | if ('string' == typeof bytes) bytes = parse(bytes); |
33 | 1 | if ('number' != typeof bytes) throw new Error('limit() bytes required'); |
34 | 1 | return function limit(req, res, next){ |
35 | 2 | var received = 0 |
36 | , len = req.headers['content-length'] | |
37 | ? parseInt(req.headers['content-length'], 10) | |
38 | : null; | |
39 | ||
40 | // self-awareness | |
41 | 2 | if (req._limit) return next(); |
42 | 2 | req._limit = true; |
43 | ||
44 | // limit by content-length | |
45 | 3 | if (len && len > bytes) return next(utils.error(413)); |
46 | ||
47 | // limit | |
48 | 1 | req.on('data', function(chunk){ |
49 | 0 | received += chunk.length; |
50 | 0 | if (received > bytes) req.destroy(); |
51 | }); | |
52 | ||
53 | 1 | next(); |
54 | }; | |
55 | }; | |
56 | ||
57 | /** | |
58 | * Parse byte `size` string. | |
59 | * | |
60 | * @param {String} size | |
61 | * @return {Number} | |
62 | * @api private | |
63 | */ | |
64 | ||
65 | 1 | function parse(size) { |
66 | 1 | var parts = size.match(/^(\d+(?:\.\d+)?) *(kb|mb|gb)$/) |
67 | , n = parseFloat(parts[1]) | |
68 | , type = parts[2]; | |
69 | ||
70 | 1 | var map = { |
71 | kb: 1024 | |
72 | , mb: 1024 * 1024 | |
73 | , gb: 1024 * 1024 * 1024 | |
74 | }; | |
75 | ||
76 | 1 | return map[type] * n; |
77 | } |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect - query | |
4 | * Copyright(c) 2011 TJ Holowaychuk | |
5 | * Copyright(c) 2011 Sencha Inc. | |
6 | * MIT Licensed | |
7 | */ | |
8 | ||
9 | /** | |
10 | * Module dependencies. | |
11 | */ | |
12 | ||
13 | 1 | var qs = require('qs') |
14 | , parse = require('url').parse; | |
15 | ||
16 | /** | |
17 | * Query: | |
18 | * | |
19 | * Automatically parse the query-string when available, | |
20 | * populating the `req.query` object. | |
21 | * | |
22 | * Examples: | |
23 | * | |
24 | * connect() | |
25 | * .use(connect.query()) | |
26 | * .use(function(req, res){ | |
27 | * res.end(JSON.stringify(req.query)); | |
28 | * }); | |
29 | * | |
30 | * @return {Function} | |
31 | * @api public | |
32 | */ | |
33 | ||
34 | 1 | module.exports = function query(){ |
35 | 1 | return function query(req, res, next){ |
36 | 2 | req.query = ~req.url.indexOf('?') |
37 | ? qs.parse(parse(req.url).query) | |
38 | : {}; | |
39 | 2 | next(); |
40 | }; | |
41 | }; |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect - responseTime | |
4 | * Copyright(c) 2011 TJ Holowaychuk | |
5 | * MIT Licensed | |
6 | */ | |
7 | ||
8 | /** | |
9 | * Reponse time: | |
10 | * | |
11 | * Adds the `X-Response-Time` header displaying the response | |
12 | * duration in milliseconds. | |
13 | * | |
14 | * @return {Function} | |
15 | * @api public | |
16 | */ | |
17 | ||
18 | 1 | module.exports = function responseTime(){ |
19 | 1 | return function(req, res, next){ |
20 | 1 | var start = new Date; |
21 | ||
22 | 1 | if (res._responseTime) return next(); |
23 | 1 | res._responseTime = true; |
24 | ||
25 | 1 | res.on('header', function(header){ |
26 | 1 | var duration = new Date - start; |
27 | 1 | res.setHeader('X-Response-time', duration + 'ms'); |
28 | }); | |
29 | ||
30 | 1 | next(); |
31 | }; | |
32 | }; |
Line | Hits | Source |
---|---|---|
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 | 1 | var utils = require('./../utils'); |
14 | ||
15 | /** | |
16 | * Cookie parser: | |
17 | * | |
18 | * Parse _Cookie_ header and populate `req.cookies` | |
19 | * with an object keyed by the cookie names. Optionally | |
20 | * you may enabled signed cookie support by passing | |
21 | * a `secret` string, which assigns `req.secret` so | |
22 | * it may be used by other middleware such as `session()`. | |
23 | * | |
24 | * Examples: | |
25 | * | |
26 | * connect() | |
27 | * .use(connect.cookieParser('keyboard cat')) | |
28 | * .use(function(req, res, next){ | |
29 | * res.end(JSON.stringify(req.cookies)); | |
30 | * }) | |
31 | * | |
32 | * @param {String} secret | |
33 | * @return {Function} | |
34 | * @api public | |
35 | */ | |
36 | ||
37 | 1 | module.exports = function cookieParser(secret){ |
38 | 29 | return function cookieParser(req, res, next) { |
39 | 35 | var cookie = req.headers.cookie; |
40 | 35 | if (req.cookies) return next(); |
41 | ||
42 | 35 | req.secret = secret; |
43 | 35 | req.cookies = {}; |
44 | 35 | req.signedCookies = {}; |
45 | ||
46 | 35 | if (cookie) { |
47 | 8 | try { |
48 | 8 | req.cookies = utils.parseCookie(cookie); |
49 | 8 | if (secret) { |
50 | 8 | req.signedCookies = utils.parseSignedCookies(req.cookies, secret); |
51 | 8 | req.signedCookies = utils.parseJSONCookies(req.signedCookies); |
52 | } | |
53 | 8 | req.cookies = utils.parseJSONCookies(req.cookies); |
54 | } catch (err) { | |
55 | 0 | return next(err); |
56 | } | |
57 | } | |
58 | 35 | next(); |
59 | }; | |
60 | }; |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect - session | |
4 | * Copyright(c) 2010 Sencha Inc. | |
5 | * Copyright(c) 2011 TJ Holowaychuk | |
6 | * MIT Licensed | |
7 | */ | |
8 | ||
9 | /** | |
10 | * Module dependencies. | |
11 | */ | |
12 | ||
13 | 1 | var Session = require('./session/session') |
14 | , debug = require('debug')('connect:session') | |
15 | , MemoryStore = require('./session/memory') | |
16 | , Cookie = require('./session/cookie') | |
17 | , Store = require('./session/store') | |
18 | , utils = require('./../utils') | |
19 | , parse = require('url').parse | |
20 | , crypto = require('crypto'); | |
21 | ||
22 | // environment | |
23 | ||
24 | 1 | var env = process.env.NODE_ENV; |
25 | ||
26 | /** | |
27 | * Expose the middleware. | |
28 | */ | |
29 | ||
30 | 1 | exports = module.exports = session; |
31 | ||
32 | /** | |
33 | * Expose constructors. | |
34 | */ | |
35 | ||
36 | 1 | exports.Store = Store; |
37 | 1 | exports.Cookie = Cookie; |
38 | 1 | exports.Session = Session; |
39 | 1 | exports.MemoryStore = MemoryStore; |
40 | ||
41 | /** | |
42 | * Warning message for `MemoryStore` usage in production. | |
43 | */ | |
44 | ||
45 | 1 | var warning = 'Warning: connection.session() MemoryStore is not\n' |
46 | + 'designed for a production environment, as it will leak\n' | |
47 | + 'memory, and obviously only work within a single process.'; | |
48 | ||
49 | /** | |
50 | * Session: | |
51 | * | |
52 | * Setup session store with the given `options`. | |
53 | * | |
54 | * Session data is _not_ saved in the cookie itself, however | |
55 | * cookies are used, so we must use the [cookieParser()](cookieParser.html) | |
56 | * middleware _before_ `session()`. | |
57 | * | |
58 | * Examples: | |
59 | * | |
60 | * connect() | |
61 | * .use(connect.cookieParser('keyboard cat')) | |
62 | * .use(connect.session({ key: 'sid', cookie: { secure: true }})) | |
63 | * | |
64 | * Options: | |
65 | * | |
66 | * - `key` cookie name defaulting to `connect.sid` | |
67 | * - `store` session store instance | |
68 | * - `cookie` session cookie settings, defaulting to `{ path: '/', httpOnly: true, maxAge: null }` | |
69 | * - `proxy` trust the reverse proxy when setting secure cookies (via "x-forwarded-proto") | |
70 | * | |
71 | * Cookie option: | |
72 | * | |
73 | * By default `cookie.maxAge` is `null`, meaning no "expires" parameter is set | |
74 | * so the cookie becomes a browser-session cookie. When the user closes the | |
75 | * browser the cookie (and session) will be removed. | |
76 | * | |
77 | * ## req.session | |
78 | * | |
79 | * To store or access session data, simply use the request property `req.session`, | |
80 | * which is (generally) serialized as JSON by the store, so nested objects | |
81 | * are typically fine. For example below is a user-specific view counter: | |
82 | * | |
83 | * connect() | |
84 | * .use(connect.favicon()) | |
85 | * .use(connect.cookieParser('keyboard cat')) | |
86 | * .use(connect.session({ cookie: { maxAge: 60000 }})) | |
87 | * .use(function(req, res, next){ | |
88 | * var sess = req.session; | |
89 | * if (sess.views) { | |
90 | * res.setHeader('Content-Type', 'text/html'); | |
91 | * res.write('<p>views: ' + sess.views + '</p>'); | |
92 | * res.write('<p>expires in: ' + (sess.cookie.maxAge / 1000) + 's</p>'); | |
93 | * res.end(); | |
94 | * sess.views++; | |
95 | * } else { | |
96 | * sess.views = 1; | |
97 | * res.end('welcome to the session demo. refresh!'); | |
98 | * } | |
99 | * } | |
100 | * )).listen(3000); | |
101 | * | |
102 | * ## Session#regenerate() | |
103 | * | |
104 | * To regenerate the session simply invoke the method, once complete | |
105 | * a new SID and `Session` instance will be initialized at `req.session`. | |
106 | * | |
107 | * req.session.regenerate(function(err){ | |
108 | * // will have a new session here | |
109 | * }); | |
110 | * | |
111 | * ## Session#destroy() | |
112 | * | |
113 | * Destroys the session, removing `req.session`, will be re-generated next request. | |
114 | * | |
115 | * req.session.destroy(function(err){ | |
116 | * // cannot access session here | |
117 | * }); | |
118 | * | |
119 | * ## Session#reload() | |
120 | * | |
121 | * Reloads the session data. | |
122 | * | |
123 | * req.session.reload(function(err){ | |
124 | * // session updated | |
125 | * }); | |
126 | * | |
127 | * ## Session#save() | |
128 | * | |
129 | * Save the session. | |
130 | * | |
131 | * req.session.save(function(err){ | |
132 | * // session saved | |
133 | * }); | |
134 | * | |
135 | * ## Session#touch() | |
136 | * | |
137 | * Updates the `.maxAge`, and `.lastAccess` properties. Typically this is | |
138 | * not necessary to call, as the session middleware does this for you. | |
139 | * | |
140 | * ## Session#cookie | |
141 | * | |
142 | * Each session has a unique cookie object accompany it. This allows | |
143 | * you to alter the session cookie per visitor. For example we can | |
144 | * set `req.session.cookie.expires` to `false` to enable the cookie | |
145 | * to remain for only the duration of the user-agent. | |
146 | * | |
147 | * ## Session#maxAge | |
148 | * | |
149 | * Alternatively `req.session.cookie.maxAge` will return the time | |
150 | * remaining in milliseconds, which we may also re-assign a new value | |
151 | * to adjust the `.expires` property appropriately. The following | |
152 | * are essentially equivalent | |
153 | * | |
154 | * var hour = 3600000; | |
155 | * req.session.cookie.expires = new Date(Date.now() + hour); | |
156 | * req.session.cookie.maxAge = hour; | |
157 | * | |
158 | * For example when `maxAge` is set to `60000` (one minute), and 30 seconds | |
159 | * has elapsed it will return `30000` until the current request has completed, | |
160 | * at which time `req.session.touch()` is called to update `req.session.lastAccess`, | |
161 | * and reset `req.session.maxAge` to its original value. | |
162 | * | |
163 | * req.session.cookie.maxAge; | |
164 | * // => 30000 | |
165 | * | |
166 | * Session Store Implementation: | |
167 | * | |
168 | * Every session store _must_ implement the following methods | |
169 | * | |
170 | * - `.get(sid, callback)` | |
171 | * - `.set(sid, session, callback)` | |
172 | * - `.destroy(sid, callback)` | |
173 | * | |
174 | * Recommended methods include, but are not limited to: | |
175 | * | |
176 | * - `.length(callback)` | |
177 | * - `.clear(callback)` | |
178 | * | |
179 | * For an example implementation view the [connect-redis](http://github.com/visionmedia/connect-redis) repo. | |
180 | * | |
181 | * @param {Object} options | |
182 | * @return {Function} | |
183 | * @api public | |
184 | */ | |
185 | ||
186 | 1 | function session(options){ |
187 | 15 | var options = options || {} |
188 | , key = options.key || 'connect.sid' | |
189 | , store = options.store || new MemoryStore | |
190 | , cookie = options.cookie | |
191 | , trustProxy = options.proxy; | |
192 | ||
193 | // notify user that this store is not | |
194 | // meant for a production environment | |
195 | 15 | if ('production' == env && store instanceof MemoryStore) { |
196 | 0 | console.warn(warning); |
197 | } | |
198 | ||
199 | // generates the new session | |
200 | 15 | store.generate = function(req){ |
201 | 21 | req.sessionID = utils.uid(24); |
202 | 21 | req.session = new Session(req); |
203 | 21 | req.session.cookie = new Cookie(req, cookie); |
204 | }; | |
205 | ||
206 | 15 | return function session(req, res, next) { |
207 | // self-awareness | |
208 | 24 | if (req.session) return next(); |
209 | ||
210 | // ensure secret is available or bail | |
211 | 24 | if (!req.secret) throw new Error('connect.cookieParser("secret") required for security when using sessions'); |
212 | ||
213 | // parse url | |
214 | 24 | var url = parse(req.url) |
215 | , path = url.pathname | |
216 | , sessionIsNew; | |
217 | ||
218 | // expose store | |
219 | 24 | req.sessionStore = store; |
220 | ||
221 | // set-cookie | |
222 | 24 | res.on('header', function(){ |
223 | 25 | if (!req.session) return; |
224 | 23 | var cookie = req.session.cookie |
225 | , proto = (req.headers['x-forwarded-proto'] || '').toLowerCase() | |
226 | , tls = req.connection.encrypted || (trustProxy && 'https' == proto) | |
227 | , secured = cookie.secure && tls; | |
228 | ||
229 | // browser-session cookies only set-cookie once | |
230 | 24 | if (null == cookie.expires && !sessionIsNew) return; |
231 | ||
232 | // only send secure cookies via https | |
233 | 24 | if (cookie.secure && !secured) return debug('not secured'); |
234 | ||
235 | 20 | debug('set %s to %s', key, req.sessionID); |
236 | 20 | res.setHeader('Set-Cookie', cookie.serialize(key, req.sessionID)); |
237 | }); | |
238 | ||
239 | // proxy end() to commit the session | |
240 | 24 | var end = res.end; |
241 | 24 | res.end = function(data, encoding){ |
242 | 24 | res.end = end; |
243 | 25 | if (!req.session) return res.end(data, encoding); |
244 | 23 | debug('saving'); |
245 | 23 | req.session.resetMaxAge(); |
246 | 23 | req.session.save(function(){ |
247 | 23 | debug('saved'); |
248 | 23 | res.end(data, encoding); |
249 | }); | |
250 | }; | |
251 | ||
252 | // generate the session | |
253 | 24 | function generate() { |
254 | 19 | sessionIsNew = true; |
255 | 19 | store.generate(req); |
256 | } | |
257 | ||
258 | // get the sessionID from the cookie | |
259 | 24 | req.sessionID = req.signedCookies[key]; |
260 | ||
261 | // generate a session if the browser doesn't send a sessionID | |
262 | 24 | if (!req.sessionID) { |
263 | 19 | debug('no SID sent, generating session'); |
264 | 19 | generate(); |
265 | 19 | next(); |
266 | 19 | return; |
267 | } | |
268 | ||
269 | // generate the session object | |
270 | 5 | var pause = utils.pause(req); |
271 | 5 | debug('fetching %s', req.sessionID); |
272 | 5 | store.get(req.sessionID, function(err, sess){ |
273 | // proxy to resume() events | |
274 | 5 | var _next = next; |
275 | 5 | next = function(err){ |
276 | 5 | _next(err); |
277 | 5 | pause.resume(); |
278 | } | |
279 | ||
280 | // error handling | |
281 | 5 | if (err) { |
282 | 0 | debug('error'); |
283 | 0 | if ('ENOENT' == err.code) { |
284 | 0 | generate(); |
285 | 0 | next(); |
286 | } else { | |
287 | 0 | next(err); |
288 | } | |
289 | // no session | |
290 | 5 | } else if (!sess) { |
291 | 0 | debug('no session found'); |
292 | 0 | generate(); |
293 | 0 | next(); |
294 | // populate req.session | |
295 | } else { | |
296 | 5 | debug('session found'); |
297 | 5 | store.createSession(req, sess); |
298 | 5 | next(); |
299 | } | |
300 | }); | |
301 | }; | |
302 | 1 | }; |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect - session - Session | |
4 | * Copyright(c) 2010 Sencha Inc. | |
5 | * Copyright(c) 2011 TJ Holowaychuk | |
6 | * MIT Licensed | |
7 | */ | |
8 | ||
9 | /** | |
10 | * Module dependencies. | |
11 | */ | |
12 | ||
13 | 1 | var utils = require('../../utils') |
14 | , Cookie = require('./cookie'); | |
15 | ||
16 | /** | |
17 | * Create a new `Session` with the given request and `data`. | |
18 | * | |
19 | * @param {IncomingRequest} req | |
20 | * @param {Object} data | |
21 | * @api private | |
22 | */ | |
23 | ||
24 | 1 | var Session = module.exports = function Session(req, data) { |
25 | 26 | Object.defineProperty(this, 'req', { value: req }); |
26 | 26 | Object.defineProperty(this, 'id', { value: req.sessionID }); |
27 | 26 | if ('object' == typeof data) { |
28 | 5 | utils.merge(this, data); |
29 | } else { | |
30 | 21 | this.lastAccess = Date.now(); |
31 | } | |
32 | }; | |
33 | ||
34 | /** | |
35 | * Update `.lastAccess` timestamp, | |
36 | * and reset `.cookie.maxAge` to prevent | |
37 | * the cookie from expiring when the | |
38 | * session is still active. | |
39 | * | |
40 | * @return {Session} for chaining | |
41 | * @api public | |
42 | */ | |
43 | ||
44 | 1 | Session.prototype.touch = function(){ |
45 | 0 | return this |
46 | .resetLastAccess() | |
47 | .resetMaxAge(); | |
48 | }; | |
49 | ||
50 | /** | |
51 | * Update `.lastAccess` timestamp. | |
52 | * | |
53 | * @return {Session} for chaining | |
54 | * @api public | |
55 | */ | |
56 | ||
57 | 1 | Session.prototype.resetLastAccess = function(){ |
58 | 5 | this.lastAccess = Date.now(); |
59 | 5 | return this; |
60 | }; | |
61 | ||
62 | /** | |
63 | * Reset `.maxAge` to `.originalMaxAge`. | |
64 | * | |
65 | * @return {Session} for chaining | |
66 | * @api public | |
67 | */ | |
68 | ||
69 | 1 | Session.prototype.resetMaxAge = function(){ |
70 | 23 | this.cookie.maxAge = this.cookie.originalMaxAge; |
71 | 23 | return this; |
72 | }; | |
73 | ||
74 | /** | |
75 | * Save the session data with optional callback `fn(err)`. | |
76 | * | |
77 | * @param {Function} fn | |
78 | * @return {Session} for chaining | |
79 | * @api public | |
80 | */ | |
81 | ||
82 | 1 | Session.prototype.save = function(fn){ |
83 | 23 | this.req.sessionStore.set(this.id, this, fn || function(){}); |
84 | 23 | return this; |
85 | }; | |
86 | ||
87 | /** | |
88 | * Re-loads the session data _without_ altering | |
89 | * the maxAge or lastAccess properties. Invokes the | |
90 | * callback `fn(err)`, after which time if no exception | |
91 | * has occurred the `req.session` property will be | |
92 | * a new `Session` object, although representing the | |
93 | * same session. | |
94 | * | |
95 | * @param {Function} fn | |
96 | * @return {Session} for chaining | |
97 | * @api public | |
98 | */ | |
99 | ||
100 | 1 | Session.prototype.reload = function(fn){ |
101 | 0 | var req = this.req |
102 | , store = this.req.sessionStore; | |
103 | 0 | store.get(this.id, function(err, sess){ |
104 | 0 | if (err) return fn(err); |
105 | 0 | if (!sess) return fn(new Error('failed to load session')); |
106 | 0 | store.createSession(req, sess); |
107 | 0 | fn(); |
108 | }); | |
109 | 0 | return this; |
110 | }; | |
111 | ||
112 | /** | |
113 | * Destroy `this` session. | |
114 | * | |
115 | * @param {Function} fn | |
116 | * @return {Session} for chaining | |
117 | * @api public | |
118 | */ | |
119 | ||
120 | 1 | Session.prototype.destroy = function(fn){ |
121 | 1 | delete this.req.session; |
122 | 1 | this.req.sessionStore.destroy(this.id, fn); |
123 | 1 | return this; |
124 | }; | |
125 | ||
126 | /** | |
127 | * Regenerate this request's session. | |
128 | * | |
129 | * @param {Function} fn | |
130 | * @return {Session} for chaining | |
131 | * @api public | |
132 | */ | |
133 | ||
134 | 1 | Session.prototype.regenerate = function(fn){ |
135 | 2 | this.req.sessionStore.regenerate(this.req, fn); |
136 | 2 | return this; |
137 | }; |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect - session - Cookie | |
4 | * Copyright(c) 2010 Sencha Inc. | |
5 | * Copyright(c) 2011 TJ Holowaychuk | |
6 | * MIT Licensed | |
7 | */ | |
8 | ||
9 | /** | |
10 | * Module dependencies. | |
11 | */ | |
12 | ||
13 | 1 | var utils = require('../../utils'); |
14 | ||
15 | /** | |
16 | * Initialize a new `Cookie` with the given `options`. | |
17 | * | |
18 | * @param {IncomingMessage} req | |
19 | * @param {Object} options | |
20 | * @api private | |
21 | */ | |
22 | ||
23 | 1 | var Cookie = module.exports = function Cookie(req, options) { |
24 | 37 | this.path = '/'; |
25 | 37 | this.maxAge = null; |
26 | 37 | this.httpOnly = true; |
27 | 61 | if (options) utils.merge(this, options); |
28 | 37 | Object.defineProperty(this, 'req', { value: req }); |
29 | 37 | this.originalMaxAge = undefined == this.originalMaxAge |
30 | ? this.maxAge | |
31 | : this.originalMaxAge; | |
32 | }; | |
33 | ||
34 | /*! | |
35 | * Prototype. | |
36 | */ | |
37 | ||
38 | 1 | Cookie.prototype = { |
39 | ||
40 | /** | |
41 | * Set expires `date`. | |
42 | * | |
43 | * @param {Date} date | |
44 | * @api public | |
45 | */ | |
46 | ||
47 | set expires(date) { | |
48 | 86 | this._expires = date; |
49 | 86 | this.originalMaxAge = this.maxAge; |
50 | }, | |
51 | ||
52 | /** | |
53 | * Get expires `date`. | |
54 | * | |
55 | * @return {Date} | |
56 | * @api public | |
57 | */ | |
58 | ||
59 | get expires() { | |
60 | 241 | return this._expires; |
61 | }, | |
62 | ||
63 | /** | |
64 | * Set expires via max-age in `ms`. | |
65 | * | |
66 | * @param {Number} ms | |
67 | * @api public | |
68 | */ | |
69 | ||
70 | set maxAge(ms) { | |
71 | 75 | this.expires = 'number' == typeof ms |
72 | ? new Date(Date.now() + ms) | |
73 | : ms; | |
74 | }, | |
75 | ||
76 | /** | |
77 | * Get expires max-age in `ms`. | |
78 | * | |
79 | * @return {Number} | |
80 | * @api public | |
81 | */ | |
82 | ||
83 | get maxAge() { | |
84 | 105 | return this.expires instanceof Date |
85 | ? this.expires.valueOf() - Date.now() | |
86 | : this.expires; | |
87 | }, | |
88 | ||
89 | /** | |
90 | * Return cookie data object. | |
91 | * | |
92 | * @return {Object} | |
93 | * @api private | |
94 | */ | |
95 | ||
96 | get data() { | |
97 | 43 | return { |
98 | originalMaxAge: this.originalMaxAge | |
99 | , expires: this._expires | |
100 | , secure: this.secure | |
101 | , httpOnly: this.httpOnly | |
102 | , domain: this.domain | |
103 | , path: this.path | |
104 | } | |
105 | }, | |
106 | ||
107 | /** | |
108 | * Return a serialized cookie string. | |
109 | * | |
110 | * @return {String} | |
111 | * @api public | |
112 | */ | |
113 | ||
114 | serialize: function(name, val){ | |
115 | 20 | val = utils.sign(val, this.req.secret); |
116 | 20 | return utils.serializeCookie(name, val, this.data); |
117 | }, | |
118 | ||
119 | /** | |
120 | * Return JSON representation of this cookie. | |
121 | * | |
122 | * @return {Object} | |
123 | * @api private | |
124 | */ | |
125 | ||
126 | toJSON: function(){ | |
127 | 23 | return this.data; |
128 | } | |
129 | }; |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect - session - MemoryStore | |
4 | * Copyright(c) 2010 Sencha Inc. | |
5 | * Copyright(c) 2011 TJ Holowaychuk | |
6 | * MIT Licensed | |
7 | */ | |
8 | ||
9 | /** | |
10 | * Module dependencies. | |
11 | */ | |
12 | ||
13 | 1 | var Store = require('./store') |
14 | , utils = require('../../utils') | |
15 | , Session = require('./session'); | |
16 | ||
17 | /** | |
18 | * Initialize a new `MemoryStore`. | |
19 | * | |
20 | * @api public | |
21 | */ | |
22 | ||
23 | 1 | var MemoryStore = module.exports = function MemoryStore() { |
24 | 15 | this.sessions = {}; |
25 | }; | |
26 | ||
27 | /** | |
28 | * Inherit from `Store.prototype`. | |
29 | */ | |
30 | ||
31 | 1 | MemoryStore.prototype.__proto__ = Store.prototype; |
32 | ||
33 | /** | |
34 | * Attempt to fetch session by the given `sid`. | |
35 | * | |
36 | * @param {String} sid | |
37 | * @param {Function} fn | |
38 | * @api public | |
39 | */ | |
40 | ||
41 | 1 | MemoryStore.prototype.get = function(sid, fn){ |
42 | 5 | var self = this; |
43 | 5 | process.nextTick(function(){ |
44 | 5 | var expires |
45 | , sess = self.sessions[sid]; | |
46 | 5 | if (sess) { |
47 | 5 | sess = JSON.parse(sess); |
48 | 5 | expires = 'string' == typeof sess.cookie.expires |
49 | ? new Date(sess.cookie.expires) | |
50 | : sess.cookie.expires; | |
51 | 5 | if (!expires || new Date < expires) { |
52 | 5 | fn(null, sess); |
53 | } else { | |
54 | 0 | self.destroy(sid, fn); |
55 | } | |
56 | } else { | |
57 | 0 | fn(); |
58 | } | |
59 | }); | |
60 | }; | |
61 | ||
62 | /** | |
63 | * Commit the given `sess` object associated with the given `sid`. | |
64 | * | |
65 | * @param {String} sid | |
66 | * @param {Session} sess | |
67 | * @param {Function} fn | |
68 | * @api public | |
69 | */ | |
70 | ||
71 | 1 | MemoryStore.prototype.set = function(sid, sess, fn){ |
72 | 23 | var self = this; |
73 | 23 | process.nextTick(function(){ |
74 | 23 | self.sessions[sid] = JSON.stringify(sess); |
75 | 23 | fn && fn(); |
76 | }); | |
77 | }; | |
78 | ||
79 | /** | |
80 | * Destroy the session associated with the given `sid`. | |
81 | * | |
82 | * @param {String} sid | |
83 | * @api public | |
84 | */ | |
85 | ||
86 | 1 | MemoryStore.prototype.destroy = function(sid, fn){ |
87 | 3 | var self = this; |
88 | 3 | process.nextTick(function(){ |
89 | 3 | delete self.sessions[sid]; |
90 | 3 | fn && fn(); |
91 | }); | |
92 | }; | |
93 | ||
94 | /** | |
95 | * Invoke the given callback `fn` with all active sessions. | |
96 | * | |
97 | * @param {Function} fn | |
98 | * @api public | |
99 | */ | |
100 | ||
101 | 1 | MemoryStore.prototype.all = function(fn){ |
102 | 0 | var arr = [] |
103 | , keys = Object.keys(this.sessions); | |
104 | 0 | for (var i = 0, len = keys.length; i < len; ++i) { |
105 | 0 | arr.push(this.sessions[keys[i]]); |
106 | } | |
107 | 0 | fn(null, arr); |
108 | }; | |
109 | ||
110 | /** | |
111 | * Clear all sessions. | |
112 | * | |
113 | * @param {Function} fn | |
114 | * @api public | |
115 | */ | |
116 | ||
117 | 1 | MemoryStore.prototype.clear = function(fn){ |
118 | 0 | this.sessions = {}; |
119 | 0 | fn && fn(); |
120 | }; | |
121 | ||
122 | /** | |
123 | * Fetch number of sessions. | |
124 | * | |
125 | * @param {Function} fn | |
126 | * @api public | |
127 | */ | |
128 | ||
129 | 1 | MemoryStore.prototype.length = function(fn){ |
130 | 0 | fn(null, Object.keys(this.sessions).length); |
131 | }; |
Line | Hits | Source |
---|---|---|
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 | 1 | var EventEmitter = require('events').EventEmitter |
14 | , Session = require('./session') | |
15 | , Cookie = require('./cookie') | |
16 | , utils = require('../../utils'); | |
17 | ||
18 | /** | |
19 | * Initialize abstract `Store`. | |
20 | * | |
21 | * @api private | |
22 | */ | |
23 | ||
24 | 1 | var Store = module.exports = function Store(options){}; |
25 | ||
26 | /** | |
27 | * Inherit from `EventEmitter.prototype`. | |
28 | */ | |
29 | ||
30 | 1 | Store.prototype.__proto__ = EventEmitter.prototype; |
31 | ||
32 | /** | |
33 | * Re-generate the given requests's session. | |
34 | * | |
35 | * @param {IncomingRequest} req | |
36 | * @return {Function} fn | |
37 | * @api public | |
38 | */ | |
39 | ||
40 | 1 | Store.prototype.regenerate = function(req, fn){ |
41 | 2 | var self = this; |
42 | 2 | this.destroy(req.sessionID, function(err){ |
43 | 2 | self.generate(req); |
44 | 2 | fn(err); |
45 | }); | |
46 | }; | |
47 | ||
48 | /** | |
49 | * Load a `Session` instance via the given `sid` | |
50 | * and invoke the callback `fn(err, sess)`. | |
51 | * | |
52 | * @param {String} sid | |
53 | * @param {Function} fn | |
54 | * @api public | |
55 | */ | |
56 | ||
57 | 1 | Store.prototype.load = function(sid, fn){ |
58 | 0 | var self = this; |
59 | 0 | this.get(sid, function(err, sess){ |
60 | 0 | if (err) return fn(err); |
61 | 0 | if (!sess) return fn(); |
62 | 0 | var req = { sessionID: sid, sessionStore: self }; |
63 | 0 | sess = self.createSession(req, sess, false); |
64 | 0 | fn(null, sess); |
65 | }); | |
66 | }; | |
67 | ||
68 | /** | |
69 | * Create session from JSON `sess` data. | |
70 | * | |
71 | * @param {IncomingRequest} req | |
72 | * @param {Object} sess | |
73 | * @return {Session} | |
74 | * @api private | |
75 | */ | |
76 | ||
77 | 1 | Store.prototype.createSession = function(req, sess, update){ |
78 | 5 | var expires = sess.cookie.expires |
79 | , orig = sess.cookie.originalMaxAge | |
80 | , update = null == update ? true : false; | |
81 | 5 | sess.cookie = new Cookie(req, sess.cookie); |
82 | 9 | if ('string' == typeof expires) sess.cookie.expires = new Date(expires); |
83 | 5 | sess.cookie.originalMaxAge = orig; |
84 | 5 | req.session = new Session(req, sess); |
85 | 10 | if (update) req.session.resetLastAccess(); |
86 | 5 | return req.session; |
87 | }; |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect - staticCache | |
4 | * Copyright(c) 2011 Sencha Inc. | |
5 | * MIT Licensed | |
6 | */ | |
7 | ||
8 | /** | |
9 | * Module dependencies. | |
10 | */ | |
11 | ||
12 | 1 | var http = require('http') |
13 | , utils = require('../utils') | |
14 | , Cache = require('../cache') | |
15 | , url = require('url') | |
16 | , fs = require('fs'); | |
17 | ||
18 | /** | |
19 | * Static cache: | |
20 | * | |
21 | * Enables a memory cache layer on top of | |
22 | * the `static()` middleware, serving popular | |
23 | * static files. | |
24 | * | |
25 | * By default a maximum of 128 objects are | |
26 | * held in cache, with a max of 256k each, | |
27 | * totalling ~32mb. | |
28 | * | |
29 | * A Least-Recently-Used (LRU) cache algo | |
30 | * is implemented through the `Cache` object, | |
31 | * simply rotating cache objects as they are | |
32 | * hit. This means that increasingly popular | |
33 | * objects maintain their positions while | |
34 | * others get shoved out of the stack and | |
35 | * garbage collected. | |
36 | * | |
37 | * Benchmarks: | |
38 | * | |
39 | * static(): 2700 rps | |
40 | * node-static: 5300 rps | |
41 | * static() + staticCache(): 7500 rps | |
42 | * | |
43 | * Options: | |
44 | * | |
45 | * - `maxObjects` max cache objects [128] | |
46 | * - `maxLength` max cache object length 256kb | |
47 | * | |
48 | * @param {Type} name | |
49 | * @return {Type} | |
50 | * @api public | |
51 | */ | |
52 | ||
53 | 1 | module.exports = function staticCache(options){ |
54 | 3 | var options = options || {} |
55 | , cache = new Cache(options.maxObjects || 128) | |
56 | , maxlen = options.maxLength || 1024 * 256; | |
57 | ||
58 | 3 | return function staticCache(req, res, next){ |
59 | 10 | var path = url.parse(req.url).pathname |
60 | , ranges = req.headers.range | |
61 | , hit = cache.get(path) | |
62 | , hitCC | |
63 | , uaCC | |
64 | , header | |
65 | , age; | |
66 | ||
67 | 10 | function miss() { |
68 | 4 | res.setHeader('X-Cache', 'MISS'); |
69 | 4 | next(); |
70 | } | |
71 | ||
72 | // cache static | |
73 | // TODO: change from staticCache() -> static() | |
74 | // and make this work for any request | |
75 | 10 | req.on('static', function(stream){ |
76 | 2 | var headers = res._headers |
77 | , cc = utils.parseCacheControl(headers['cache-control'] || '') | |
78 | , contentLength = headers['content-length'] | |
79 | , hit; | |
80 | ||
81 | // ignore larger files | |
82 | 2 | if (!contentLength || contentLength > maxlen) return; |
83 | ||
84 | // dont cache items we shouldn't be | |
85 | // TODO: real support for must-revalidate / no-cache | |
86 | 2 | if ( cc['no-cache'] |
87 | || cc['no-store'] | |
88 | || cc['private'] | |
89 | 0 | || cc['must-revalidate']) return; |
90 | ||
91 | // if already in cache then validate | |
92 | 2 | if (hit = cache.get(path)){ |
93 | 1 | if (headers.etag == hit[0].etag) { |
94 | 1 | hit[0].date = new Date; |
95 | 1 | return; |
96 | } else { | |
97 | 0 | cache.remove(path); |
98 | } | |
99 | } | |
100 | ||
101 | // validation notifiactions don't contain a steam | |
102 | 1 | if (null == stream) return; |
103 | ||
104 | // add the cache object | |
105 | 1 | var arr = cache.add(path); |
106 | 1 | arr.push(headers); |
107 | ||
108 | // store the chunks | |
109 | 1 | stream.on('data', function(chunk){ |
110 | 1 | arr.push(chunk); |
111 | }); | |
112 | ||
113 | // flag it as complete | |
114 | 1 | stream.on('end', function(){ |
115 | 1 | arr.complete = true; |
116 | }); | |
117 | }); | |
118 | ||
119 | // cache hit, doesnt support range requests | |
120 | 10 | if (hit && hit.complete && !ranges) { |
121 | 7 | header = utils.merge({}, hit[0]); |
122 | 7 | header.Age = age = (new Date - new Date(header.date)) / 1000 | 0; |
123 | 7 | header.date = new Date().toUTCString(); |
124 | ||
125 | // parse cache-controls | |
126 | 7 | hitCC = utils.parseCacheControl(header['cache-control'] || ''); |
127 | 7 | uaCC = utils.parseCacheControl(req.headers['cache-control'] || ''); |
128 | ||
129 | // check if we must revalidate(bypass) | |
130 | 8 | if (hitCC['no-cache'] || uaCC['no-cache']) return miss(); |
131 | ||
132 | // check freshness of entity | |
133 | 6 | if (isStale(hitCC, age) || isStale(uaCC, age)) return miss(); |
134 | ||
135 | // conditional GET support | |
136 | 6 | if (utils.conditionalGET(req)) { |
137 | 0 | if (!utils.modified(req, res, header)) { |
138 | 0 | header['content-length'] = 0; |
139 | 0 | res.writeHead(304, header); |
140 | 0 | return res.end(); |
141 | } | |
142 | } | |
143 | ||
144 | // HEAD support | |
145 | 6 | if ('HEAD' == req.method) { |
146 | 2 | res.writeHead(200, header); |
147 | 2 | return res.end(); |
148 | } | |
149 | ||
150 | // respond with cache | |
151 | 4 | header['x-cache'] = 'HIT'; |
152 | 4 | res.writeHead(200, header); |
153 | ||
154 | // backpressure | |
155 | 4 | function write(i) { |
156 | 8 | var buf = hit[i]; |
157 | 12 | if (!buf) return res.end(); |
158 | 4 | if (false === res.write(buf)) { |
159 | 0 | res.once('drain', function(){ |
160 | 0 | write(++i); |
161 | }); | |
162 | } else { | |
163 | 4 | write(++i); |
164 | } | |
165 | } | |
166 | ||
167 | 4 | return write(1); |
168 | } | |
169 | ||
170 | 3 | miss(); |
171 | } | |
172 | }; | |
173 | ||
174 | /** | |
175 | * Check if cache item is stale | |
176 | * | |
177 | * @param {Object} cc | |
178 | * @param {Number} age | |
179 | * @return {Boolean} | |
180 | * @api private | |
181 | */ | |
182 | ||
183 | 1 | function isStale(cc, age) { |
184 | 12 | return cc['max-age'] && cc['max-age'] <= age; |
185 | } |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect - Cache | |
4 | * Copyright(c) 2011 Sencha Inc. | |
5 | * MIT Licensed | |
6 | */ | |
7 | ||
8 | /** | |
9 | * Expose `Cache`. | |
10 | */ | |
11 | ||
12 | 1 | module.exports = Cache; |
13 | ||
14 | /** | |
15 | * LRU cache store. | |
16 | * | |
17 | * @param {Number} limit | |
18 | * @api private | |
19 | */ | |
20 | ||
21 | 1 | function Cache(limit) { |
22 | 3 | this.store = {}; |
23 | 3 | this.keys = []; |
24 | 3 | this.limit = limit; |
25 | } | |
26 | ||
27 | /** | |
28 | * Touch `key`, promoting the object. | |
29 | * | |
30 | * @param {String} key | |
31 | * @param {Number} i | |
32 | * @api private | |
33 | */ | |
34 | ||
35 | 1 | Cache.prototype.touch = function(key, i){ |
36 | 0 | this.keys.splice(i,1); |
37 | 0 | this.keys.push(key); |
38 | }; | |
39 | ||
40 | /** | |
41 | * Remove `key`. | |
42 | * | |
43 | * @param {String} key | |
44 | * @api private | |
45 | */ | |
46 | ||
47 | 1 | Cache.prototype.remove = function(key){ |
48 | 0 | delete this.store[key]; |
49 | }; | |
50 | ||
51 | /** | |
52 | * Get the object stored for `key`. | |
53 | * | |
54 | * @param {String} key | |
55 | * @return {Array} | |
56 | * @api private | |
57 | */ | |
58 | ||
59 | 1 | Cache.prototype.get = function(key){ |
60 | 12 | return this.store[key]; |
61 | }; | |
62 | ||
63 | /** | |
64 | * Add a cache `key`. | |
65 | * | |
66 | * @param {String} key | |
67 | * @return {Array} | |
68 | * @api private | |
69 | */ | |
70 | ||
71 | 1 | Cache.prototype.add = function(key){ |
72 | // initialize store | |
73 | 1 | var len = this.keys.push(key); |
74 | ||
75 | // limit reached, invalidate LRU | |
76 | 1 | if (len > this.limit) this.remove(this.keys.shift()); |
77 | ||
78 | 1 | var arr = this.store[key] = []; |
79 | 1 | arr.createdAt = new Date; |
80 | 1 | return arr; |
81 | }; |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Connect - cookieSession | |
4 | * Copyright(c) 2011 Sencha Inc. | |
5 | * MIT Licensed | |
6 | */ | |
7 | ||
8 | /** | |
9 | * Module dependencies. | |
10 | */ | |
11 | ||
12 | 1 | var utils = require('./../utils') |
13 | , Cookie = require('./session/cookie') | |
14 | , debug = require('debug')('connect:cookieSession'); | |
15 | ||
16 | // environment | |
17 | ||
18 | 1 | var env = process.env.NODE_ENV; |
19 | ||
20 | /** | |
21 | * Cookie Session: | |
22 | * | |
23 | * Cookie session middleware. | |
24 | * | |
25 | * var app = connect(); | |
26 | * app.use(connect.cookieParser('tobo!')); | |
27 | * app.use(connect.cookieSession({ cookie: { maxAge: 60 * 60 * 1000 }})); | |
28 | * | |
29 | * Options: | |
30 | * | |
31 | * - `key` cookie name defaulting to `connect.sess` | |
32 | * - `cookie` session cookie settings, defaulting to `{ path: '/', httpOnly: true, maxAge: null }` | |
33 | * - `proxy` trust the reverse proxy when setting secure cookies (via "x-forwarded-proto") | |
34 | * | |
35 | * @param {Object} options | |
36 | * @return {Function} | |
37 | * @api public | |
38 | */ | |
39 | ||
40 | 1 | module.exports = function cookieSession(options){ |
41 | // TODO: utilize Session/Cookie to unify API | |
42 | // TODO: only set-cookie on changes to the session data | |
43 | ||
44 | 14 | var options = options || {} |
45 | , key = options.key || 'connect.sess' | |
46 | , cookie = options.cookie | |
47 | , trustProxy = options.proxy; | |
48 | ||
49 | 14 | return function cookieSession(req, res, next) { |
50 | 11 | req.session = req.signedCookies[key] || {}; |
51 | 11 | req.session.cookie = new Cookie(req, cookie); |
52 | ||
53 | 11 | res.on('header', function(){ |
54 | // removed | |
55 | 11 | if (!req.session) { |
56 | 1 | debug('clear session'); |
57 | 1 | res.setHeader('Set-Cookie', key + '=; expires=' + new Date(0).toUTCString()); |
58 | 1 | return; |
59 | } | |
60 | ||
61 | 10 | var cookie = req.session.cookie; |
62 | 10 | delete req.session.cookie; |
63 | ||
64 | // check security | |
65 | 10 | var proto = (req.headers['x-forwarded-proto'] || '').toLowerCase() |
66 | , tls = req.connection.encrypted || (trustProxy && 'https' == proto) | |
67 | , secured = cookie.secure && tls; | |
68 | ||
69 | // only send secure cookies via https | |
70 | 12 | if (cookie.secure && !secured) return debug('not secured'); |
71 | ||
72 | // set cookie | |
73 | 8 | debug('serializing %j', req.session); |
74 | 8 | var val = 'j:' + JSON.stringify(req.session); |
75 | 8 | val = utils.sign(val, req.secret); |
76 | 8 | val = utils.serializeCookie(key, val, cookie); |
77 | 8 | debug('cookie %j', cookie); |
78 | 8 | res.setHeader('Set-Cookie', val); |
79 | }); | |
80 | ||
81 | 11 | next(); |
82 | }; | |
83 | }; |