Permalink
Browse files

Closed issue 82 : handling cookies - added tests too

  • Loading branch information...
1 parent 8869b2e commit 8323eed4915bb73b33544bc276f3840c13969134 alessioalex committed Nov 12, 2011
Showing with 309 additions and 200 deletions.
  1. +19 โˆ’0 README.md
  2. +34 โˆ’48 main.js
  3. +0 โˆ’23 oauth.js
  4. +29 โˆ’0 tests/test-cookie.js
  5. +90 โˆ’0 tests/test-cookiejar.js
  6. +8 โˆ’1 tests/test-errors.js
  7. +0 โˆ’109 tests/test-oauth.js
  8. +0 โˆ’19 uuid.js
  9. +57 โˆ’0 vendor/cookie/index.js
  10. +72 โˆ’0 vendor/cookie/jar.js
View
19 README.md
@@ -112,6 +112,7 @@ The first argument can be either a url or an options object. The only required o
* `timeout` - Integer containing the number of milliseconds to wait for a request to respond before aborting the request
* `proxy` - An HTTP proxy to be used. Support proxy Auth with Basic Auth the same way it's supported with the `url` parameter by embedding the auth info in the uri.
* `strictSSL` - Set to `true` to require that SSL certificates be valid. Note: to use your own certificate authority, you need to specify an agent that was created with that ca as an option.
+* `jar` - Set to `request.jar()` if you don't want cookies to be remembered for future use
The callback argument gets 3 arguments. The first is an error when applicable (usually from the http.Client option not the http.ClientRequest object). The second in an http.ClientResponse object. The third is the response body buffer.
@@ -191,3 +192,21 @@ request.get(url)
}
)
```
+The jar holds the state of the cookies being set, each request sends the proper cookies for that domain. The boiler is easy to get rid of with `defaults` or per each request if you set `options.jar`.
+
+```javascript
+var j = request.jar()
+var request = request.defaults({jar:j})
+request('http://www.google.com', function () {
+ request('http://images.google.com')
+})
+```
+OR
+```javascript
+var j = request.jar()
+// you can also set a cookie like so:
+j.add('sid=LsoJ7tV9wfwz2VkE9lxC4EqU.%2BXxV0F9i1hfCMb4uNKZTpAUJLTwlf57FewvHne%2BAGVI; path=/; expires=Sun, 23 Nov 2011 16:50:21 GMT; httpOnly')
+request(url: 'http://www.google.com', jar: j, function () {
+ request('http://images.google.com')
+})
+```
View
82 main.js
@@ -20,8 +20,9 @@ var http = require('http')
, stream = require('stream')
, qs = require('querystring')
, mimetypes = require('./mimetypes')
- , oauth = require('./oauth')
- , uuid = require('./uuid')
+ , Cookie = require('./vendor/cookie')
+ , CookieJar = require('./vendor/cookie/jar')
+ , cookieJar = new CookieJar
;
try {
@@ -137,6 +138,18 @@ Request.prototype.request = function () {
setHost = true
}
+ // if user tries to define a bad cookie jar
+ if (self.jar && !(self.jar instanceof CookieJar)) {
+ throw new Error("Argument error, options.jar.")
+ } else if (self.jar) {
+ // fetch cookie from the user defined cookie jar
+ var cookies = self.jar.get({ url: self.uri.href })
+ } else {
+ // fetch cookie from the global cookie jar
+ var cookies = cookieJar.get({ url: self.uri.href })
+ }
+ if (cookies) {self.headers.Cookie = cookies}
+
if (!self.uri.pathname) {self.uri.pathname = '/'}
if (!self.uri.port) {
if (self.uri.protocol == 'http:') {self.uri.port = 80}
@@ -164,52 +177,6 @@ Request.prototype.request = function () {
if (self.onResponse) self.on('error', function (e) {self.onResponse(e)})
if (self.callback) self.on('error', function (e) {self.callback(e)})
- if (self.form) {
- self.headers['content-type'] = 'application/x-www-form-urlencoded; charset=utf-8'
- self.body = qs.stringify(self.form).toString('utf8')
- }
-
- if (self.oauth) {
- var form
- if (self.headers['content-type'] &&
- self.headers['content-type'].slice(0, 'application/x-www-form-urlencoded'.length) ===
- 'application/x-www-form-urlencoded'
- ) {
- form = qs.parse(self.body)
- }
- if (self.uri.query) {
- form = qs.parse(self.uri.query)
- }
- if (!form) form = {}
- var oa = {}
- for (i in form) oa[i] = form[i]
- for (i in self.oauth) oa['oauth_'+i] = self.oauth[i]
- if (!oa.oauth_version) oa.oauth_version = '1.0'
- if (!oa.oauth_timestamp) oa.oauth_timestamp = Math.floor( (new Date()).getTime() / 1000 ).toString()
- if (!oa.oauth_nonce) oa.oauth_nonce = uuid().replace(/-/g, '')
-
- oa.oauth_signature_method = 'HMAC-SHA1'
-
- var consumer_secret = oa.oauth_consumer_secret
- delete oa.oauth_consumer_secret
- var token_secret = oa.oauth_token_secret
- delete oa.oauth_token_secret
-
- var baseurl = self.uri.protocol + '//' + self.uri.host + self.uri.pathname
- var signature = oauth.hmacsign(self.method, baseurl, oa, consumer_secret, token_secret)
-
- // oa.oauth_signature = signature
- for (i in form) {
- if ( i.slice(0, 'oauth_') in self.oauth) {
- // skip
- } else {
- delete oa['oauth_'+i]
- }
- }
- self.headers.authorization =
- 'OAuth '+Object.keys(oa).sort().map(function (i) {return i+'="'+encodeURIComponent(oa[i])+'"'}).join(',')
- self.headers.authorization += ',oauth_signature="'+encodeURIComponent(signature)+'"'
- }
if (self.uri.auth && !self.headers.authorization) {
self.headers.authorization = "Basic " + toBase64(self.uri.auth.split(':').map(function(item){ return qs.unescape(item)}).join(':'))
@@ -406,6 +373,15 @@ Request.prototype.request = function () {
response.body = JSON.parse(response.body)
} catch (e) {}
}
+ if (response.statusCode == 200) {
+ // only add cookies if no custom jar is defined (by the user)
+ if (response.headers['set-cookie'] && !self.jar) {
+ response.headers['set-cookie'].forEach(function(cookie) {
+ cookieJar.add(new Cookie(cookie));
+ });
+ }
+ }
+
self.callback(null, response, response.body)
})
}
@@ -525,6 +501,8 @@ request.defaults = function (options) {
de.put = def(request.put)
de.head = def(request.head)
de.del = def(request.del)
+ de.cookie = def(request.cookie)
+ de.jar = def(request.jar)
return de
}
@@ -552,3 +530,11 @@ request.del = function (options, callback) {
options.method = 'DELETE'
return request(options, callback)
}
+request.cookie = function (str) {
+ if (typeof str !== 'string') throw new Error("The cookie function only accepts STRING as param")
+ return new Cookie(str)
+}
+request.jar = function () {
+ return new CookieJar
+}
+
View
23 oauth.js
@@ -1,23 +0,0 @@
-var crypto = require('crypto')
- , qs = require('querystring')
- ;
-
-function sha1 (key, body) {
- return crypto.createHmac('sha1', key).update(body).digest('base64')
-}
-
-function hmacsign (httpMethod, base_uri, params, consumer_secret, token_secret, body) {
- // adapted from https://dev.twitter.com/docs/auth/oauth
- var base =
- httpMethod + "&" +
- encodeURIComponent( base_uri ) + "&" +
- Object.keys(params).sort().map(function (i) {
- // big WTF here with the escape + encoding but it's what twitter wants
- return encodeURIComponent(qs.escape(i)) + "%3D" + encodeURIComponent(qs.escape(params[i]))
- }).join("%26")
- var key = consumer_secret + '&'
- if (token_secret) key += token_secret
- return sha1(key, base)
-}
-
-exports.hmacsign = hmacsign
View
29 tests/test-cookie.js
@@ -0,0 +1,29 @@
+var Cookie = require('../vendor/cookie')
+ , assert = require('assert');
+
+var str = 'sid=s543qactge.wKE61E01Bs%2BKhzmxrwrnug; path=/; httpOnly; expires=Sat, 04 Dec 2010 23:27:28 GMT';
+var cookie = new Cookie(str);
+
+// test .toString()
+assert.equal(cookie.toString(), str);
+
+// test .path
+assert.equal(cookie.path, '/');
+
+// test .httpOnly
+assert.equal(cookie.httpOnly, true);
+
+// test .name
+assert.equal(cookie.name, 'sid');
+
+// test .value
+assert.equal(cookie.value, 's543qactge.wKE61E01Bs%2BKhzmxrwrnug');
+
+// test .expires
+assert.equal(cookie.expires instanceof Date, true);
+
+// test .path default
+var cookie = new Cookie('foo=bar', { url: 'http://foo.com/bar' });
+assert.equal(cookie.path, '/bar');
+
+console.log('All tests passed');
View
90 tests/test-cookiejar.js
@@ -0,0 +1,90 @@
+var Cookie = require('../vendor/cookie')
+ , Jar = require('../vendor/cookie/jar')
+ , assert = require('assert');
+
+function expires(ms) {
+ return new Date(Date.now() + ms).toUTCString();
+}
+
+// test .get() expiration
+(function() {
+ var jar = new Jar;
+ var cookie = new Cookie('sid=1234; path=/; expires=' + expires(1000));
+ jar.add(cookie);
+ setTimeout(function(){
+ var cookies = jar.get({ url: 'http://foo.com/foo' });
+ assert.equal(cookies.length, 1);
+ assert.equal(cookies[0], cookie);
+ setTimeout(function(){
+ var cookies = jar.get({ url: 'http://foo.com/foo' });
+ assert.equal(cookies.length, 0);
+ }, 1000);
+ }, 5);
+})();
+
+// test .get() path support
+(function() {
+ var jar = new Jar;
+ var a = new Cookie('sid=1234; path=/');
+ var b = new Cookie('sid=1111; path=/foo/bar');
+ var c = new Cookie('sid=2222; path=/');
+ jar.add(a);
+ jar.add(b);
+ jar.add(c);
+
+ // should remove the duplicates
+ assert.equal(jar.cookies.length, 2);
+
+ // same name, same path, latter prevails
+ var cookies = jar.get({ url: 'http://foo.com/' });
+ assert.equal(cookies.length, 1);
+ assert.equal(cookies[0], c);
+
+ // same name, diff path, path specifity prevails, latter prevails
+ var cookies = jar.get({ url: 'http://foo.com/foo/bar' });
+ assert.equal(cookies.length, 1);
+ assert.equal(cookies[0], b);
+
+ var jar = new Jar;
+ var a = new Cookie('sid=1111; path=/foo/bar');
+ var b = new Cookie('sid=1234; path=/');
+ jar.add(a);
+ jar.add(b);
+
+ var cookies = jar.get({ url: 'http://foo.com/foo/bar' });
+ assert.equal(cookies.length, 1);
+ assert.equal(cookies[0], a);
+
+ var cookies = jar.get({ url: 'http://foo.com/' });
+ assert.equal(cookies.length, 1);
+ assert.equal(cookies[0], b);
+
+ var jar = new Jar;
+ var a = new Cookie('sid=1111; path=/foo/bar');
+ var b = new Cookie('sid=3333; path=/foo/bar');
+ var c = new Cookie('pid=3333; path=/foo/bar');
+ var d = new Cookie('sid=2222; path=/foo/');
+ var e = new Cookie('sid=1234; path=/');
+ jar.add(a);
+ jar.add(b);
+ jar.add(c);
+ jar.add(d);
+ jar.add(e);
+
+ var cookies = jar.get({ url: 'http://foo.com/foo/bar' });
+ assert.equal(cookies.length, 2);
+ assert.equal(cookies[0], b);
+ assert.equal(cookies[1], c);
+
+ var cookies = jar.get({ url: 'http://foo.com/foo/' });
+ assert.equal(cookies.length, 1);
+ assert.equal(cookies[0], d);
+
+ var cookies = jar.get({ url: 'http://foo.com/' });
+ assert.equal(cookies.length, 1);
+ assert.equal(cookies[0], e);
+})();
+
+setTimeout(function() {
+ console.log('All tests passed');
+}, 1200);
View
9 tests/test-errors.js
@@ -21,10 +21,17 @@ try {
}
try {
+ request({uri:local, jar: 'foo'})
+ assert.fail("Should have throw")
+} catch(e) {
+ assert.equal(e.message, 'Argument error, options.jar.')
+}
+
+try {
request({uri:local, multipart: [{}]})
assert.fail("Should have throw")
} catch(e) {
assert.equal(e.message, 'Body attribute missing in multipart.')
}
-console.log("All tests passed.")
+console.log("All tests passed.")
View
109 tests/test-oauth.js
@@ -1,109 +0,0 @@
-var hmacsign = require('../oauth').hmacsign
- , assert = require('assert')
- , qs = require('querystring')
- , request = require('../main')
- ;
-
-function getsignature (r) {
- var sign
- r.headers.authorization.slice('OAuth '.length).replace(/,\ /g, ',').split(',').forEach(function (v) {
- if (v.slice(0, 'oauth_signature="'.length) === 'oauth_signature="') sign = v.slice('oauth_signature="'.length, -1)
- })
- return decodeURIComponent(sign)
-}
-
-// Tests from Twitter documentation https://dev.twitter.com/docs/auth/oauth
-
-var reqsign = hmacsign('POST', 'https://api.twitter.com/oauth/request_token',
- { oauth_callback: 'http://localhost:3005/the_dance/process_callback?service_provider_id=11'
- , oauth_consumer_key: 'GDdmIQH6jhtmLUypg82g'
- , oauth_nonce: 'QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk'
- , oauth_signature_method: 'HMAC-SHA1'
- , oauth_timestamp: '1272323042'
- , oauth_version: '1.0'
- }, "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98")
-
-console.log(reqsign)
-console.log('8wUi7m5HFQy76nowoCThusfgB+Q=')
-assert.equal(reqsign, '8wUi7m5HFQy76nowoCThusfgB+Q=')
-
-var accsign = hmacsign('POST', 'https://api.twitter.com/oauth/access_token',
- { oauth_consumer_key: 'GDdmIQH6jhtmLUypg82g'
- , oauth_nonce: '9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8'
- , oauth_signature_method: 'HMAC-SHA1'
- , oauth_token: '8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc'
- , oauth_timestamp: '1272323047'
- , oauth_verifier: 'pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY'
- , oauth_version: '1.0'
- }, "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98", "x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA")
-
-console.log(accsign)
-console.log('PUw/dHA4fnlJYM6RhXk5IU/0fCc=')
-assert.equal(accsign, 'PUw/dHA4fnlJYM6RhXk5IU/0fCc=')
-
-var upsign = hmacsign('POST', 'http://api.twitter.com/1/statuses/update.json',
- { oauth_consumer_key: "GDdmIQH6jhtmLUypg82g"
- , oauth_nonce: "oElnnMTQIZvqvlfXM56aBLAf5noGD0AQR3Fmi7Q6Y"
- , oauth_signature_method: "HMAC-SHA1"
- , oauth_token: "819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw"
- , oauth_timestamp: "1272325550"
- , oauth_version: "1.0"
- , status: 'setting up my twitter ็งใฎใ•ใˆใšใ‚Šใ‚’่จญๅฎšใ™ใ‚‹'
- }, "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98", "J6zix3FfA9LofH0awS24M3HcBYXO5nI1iYe8EfBA")
-
-console.log(upsign)
-console.log('yOahq5m0YjDDjfjxHaXEsW9D+X0=')
-assert.equal(upsign, 'yOahq5m0YjDDjfjxHaXEsW9D+X0=')
-
-
-var r = request.post(
- { url: 'https://api.twitter.com/oauth/request_token'
- , oauth:
- { callback: 'http://localhost:3005/the_dance/process_callback?service_provider_id=11'
- , consumer_key: 'GDdmIQH6jhtmLUypg82g'
- , nonce: 'QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk'
- , timestamp: '1272323042'
- , version: '1.0'
- , consumer_secret: "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98"
- }
- })
-
-console.log(getsignature(r))
-assert.equal(reqsign, getsignature(r))
-
-var r = request.post(
- { url: 'https://api.twitter.com/oauth/access_token'
- , oauth:
- { consumer_key: 'GDdmIQH6jhtmLUypg82g'
- , nonce: '9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8'
- , signature_method: 'HMAC-SHA1'
- , token: '8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc'
- , timestamp: '1272323047'
- , verifier: 'pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY'
- , version: '1.0'
- , consumer_secret: "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98"
- , token_secret: "x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA"
- }
- })
-
-console.log(getsignature(r))
-assert.equal(accsign, getsignature(r))
-
-var r = request.post(
- { url: 'http://api.twitter.com/1/statuses/update.json'
- , oauth:
- { consumer_key: "GDdmIQH6jhtmLUypg82g"
- , nonce: "oElnnMTQIZvqvlfXM56aBLAf5noGD0AQR3Fmi7Q6Y"
- , signature_method: "HMAC-SHA1"
- , token: "819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw"
- , timestamp: "1272325550"
- , version: "1.0"
- , consumer_secret: "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98"
- , token_secret: "J6zix3FfA9LofH0awS24M3HcBYXO5nI1iYe8EfBA"
- }
- , form: {status: 'setting up my twitter ็งใฎใ•ใˆใšใ‚Šใ‚’่จญๅฎšใ™ใ‚‹'}
- })
-
-console.log(getsignature(r))
-assert.equal(upsign, getsignature(r))
-
View
19 uuid.js
@@ -1,19 +0,0 @@
-module.exports = function () {
- var s = [], itoh = '0123456789ABCDEF';
-
- // Make array of random hex digits. The UUID only has 32 digits in it, but we
- // allocate an extra items to make room for the '-'s we'll be inserting.
- for (var i = 0; i <36; i++) s[i] = Math.floor(Math.random()*0x10);
-
- // Conform to RFC-4122, section 4.4
- s[14] = 4; // Set 4 high bits of time_high field to version
- s[19] = (s[19] & 0x3) | 0x8; // Specify 2 high bits of clock sequence
-
- // Convert to hex chars
- for (var i = 0; i <36; i++) s[i] = itoh[s[i]];
-
- // Insert '-'s
- s[8] = s[13] = s[18] = s[23] = '-';
-
- return s.join('');
-}
View
57 vendor/cookie/index.js
@@ -0,0 +1,57 @@
+/*!
+ * Tobi - Cookie
+ * Copyright(c) 2010 LearnBoost <dev@learnboost.com>
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+var url = require('url');
+
+/**
+ * Initialize a new `Cookie` with the given cookie `str` and `req`.
+ *
+ * @param {String} str
+ * @param {IncomingRequest} req
+ * @api private
+ */
+
+var Cookie = exports = module.exports = function Cookie(str, req) {
+ this.str = str;
+
+ // First key is the name
+ this.name = str.substr(0, str.indexOf('='));
+
+ // Map the key/val pairs
+ str.split(/ *; */).reduce(function(obj, pair){
+ pair = pair.split(/ *= */);
+ obj[pair[0]] = pair[1] || true;
+ return obj;
+ }, this);
+
+ // Assign value
+ this.value = this[this.name];
+
+ // Expires
+ this.expires = this.expires
+ ? new Date(this.expires)
+ : Infinity;
+
+ // Default or trim path
+ this.path = this.path
+ ? this.path.trim()
+ : url.parse(req.url).pathname;
+};
+
+/**
+ * Return the original cookie string.
+ *
+ * @return {String}
+ * @api public
+ */
+
+Cookie.prototype.toString = function(){
+ return this.str;
+};
View
72 vendor/cookie/jar.js
@@ -0,0 +1,72 @@
+/*!
+* Tobi - CookieJar
+* Copyright(c) 2010 LearnBoost <dev@learnboost.com>
+* MIT Licensed
+*/
+
+/**
+* Module dependencies.
+*/
+
+var url = require('url');
+
+/**
+* Initialize a new `CookieJar`.
+*
+* @api private
+*/
+
+var CookieJar = exports = module.exports = function CookieJar() {
+ this.cookies = [];
+};
+
+/**
+* Add the given `cookie` to the jar.
+*
+* @param {Cookie} cookie
+* @api private
+*/
+
+CookieJar.prototype.add = function(cookie){
+ this.cookies = this.cookies.filter(function(c){
+ // Avoid duplication (same path, same name)
+ return !(c.name == cookie.name && c.path == cookie.path);
+ });
+ this.cookies.push(cookie);
+};
+
+/**
+* Get cookies for the given `req`.
+*
+* @param {IncomingRequest} req
+* @return {Array}
+* @api private
+*/
+
+CookieJar.prototype.get = function(req){
+ var path = url.parse(req.url).pathname
+ , now = new Date
+ , specificity = {};
+ return this.cookies.filter(function(cookie){
+ if (0 == path.indexOf(cookie.path) && now < cookie.expires
+ && cookie.path.length > (specificity[cookie.name] || 0))
+ return specificity[cookie.name] = cookie.path.length;
+ });
+};
+
+/**
+* Return Cookie string for the given `req`.
+*
+* @param {IncomingRequest} req
+* @return {String}
+* @api private
+*/
+
+CookieJar.prototype.cookieString = function(req){
+ var cookies = this.get(req);
+ if (cookies.length) {
+ return cookies.map(function(cookie){
+ return cookie.name + '=' + cookie.value;
+ }).join('; ');
+ }
+};

0 comments on commit 8323eed

Please sign in to comment.