From 3816cd75edb6f3d13478eefd2134b37ddb1b62b8 Mon Sep 17 00:00:00 2001 From: Nick Jacob Date: Sun, 6 May 2012 06:03:03 -0400 Subject: [PATCH] initial commit --- README.md | 65 ++ node_modules/oauth/.npmignore | 1 + node_modules/oauth/LICENSE | 8 + node_modules/oauth/Makefile | 7 + node_modules/oauth/Readme.md | 78 ++ .../oauth/examples/express-gdata/server.js | 168 ++++ .../express-gdata/views/google_calendars.ejs | 21 + .../express-gdata/views/google_contacts.ejs | 24 + .../examples/express-gdata/views/layout.ejs | 9 + .../oauth/examples/term.ie.oauth-HMAC-SHA1.js | 31 + .../oauth/examples/term.ie.oauth-PLAINTEXT.js | 34 + node_modules/oauth/index.js | 3 + node_modules/oauth/lib/_utils.js | 4 + node_modules/oauth/lib/oauth.js | 548 ++++++++++++ node_modules/oauth/lib/oauth2.js | 161 ++++ node_modules/oauth/lib/sha1.js | 334 ++++++++ node_modules/oauth/package.json | 40 + node_modules/oauth/tests/oauth.js | 779 ++++++++++++++++++ node_modules/oauth/tests/oauth2.js | 67 ++ node_modules/oauth/tests/sha1.js | 13 + ohauth-strategies.js | 38 + ohauth.js | 162 ++++ package.json | 32 + 23 files changed, 2627 insertions(+) create mode 100644 README.md create mode 100644 node_modules/oauth/.npmignore create mode 100644 node_modules/oauth/LICENSE create mode 100644 node_modules/oauth/Makefile create mode 100644 node_modules/oauth/Readme.md create mode 100644 node_modules/oauth/examples/express-gdata/server.js create mode 100644 node_modules/oauth/examples/express-gdata/views/google_calendars.ejs create mode 100644 node_modules/oauth/examples/express-gdata/views/google_contacts.ejs create mode 100644 node_modules/oauth/examples/express-gdata/views/layout.ejs create mode 100644 node_modules/oauth/examples/term.ie.oauth-HMAC-SHA1.js create mode 100644 node_modules/oauth/examples/term.ie.oauth-PLAINTEXT.js create mode 100644 node_modules/oauth/index.js create mode 100644 node_modules/oauth/lib/_utils.js create mode 100644 node_modules/oauth/lib/oauth.js create mode 100644 node_modules/oauth/lib/oauth2.js create mode 100644 node_modules/oauth/lib/sha1.js create mode 100644 node_modules/oauth/package.json create mode 100644 node_modules/oauth/tests/oauth.js create mode 100644 node_modules/oauth/tests/oauth2.js create mode 100644 node_modules/oauth/tests/sha1.js create mode 100644 ohauth-strategies.js create mode 100644 ohauth.js create mode 100644 package.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..45cced7 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# OhAuth! +## Lazy OAuth for Express + +### 1. How Lazy? +How about this -- here's your server js: + + var Express = require('express') + , app = Express.createServer(Express.favicon(),Express.static(__dirname)) + , OhAuth = require('./ohauth') + , strategies = require('./ohauth-strategies'); + + + var cK = 'consumerKey', cS = 'consumerSecret' + , ohAuth = new OhAuth(strategies.twitter(cK,cS,'http://127.0.0.1:1337',app); + + app.configure(function(){ + app.use(app.router); + }); + + app.listen(1337); + app.get('/', function (req, res) { + res.sendfile(__dirname + '/index.html'); + }); + + // when you're done, you'll get an access key and an access secret printing out here + app.get('/oauth/success',function(req,res){ + res.send(req.session.credentials); + }); + +Now, after a little `node app.js`, just point your browser over to `localhost:1337/oauth/login` + + +### 2. Wait, so it does everything for me? +Pretty much. It has some defaults that can be changed by passing new values (either in the strategy, or the dict if you're using your own). + +* loginURL: `/oauth/login` +* callbackURL: `/oauth/callback` +* sucessURL: `/oauth/succecss` +* errorURL: `/oauth/error` + +It sets up all of them except *success*, because I figured you'd probably want to implement it yourself. `/oauth/login` goes straight to twitter. + +### 3. I want to make API calls..what do? +There are methods for GET and POST that just wrap the node-oauth library (what this whole things is wrapping). Check it out: + +#### Get request: + + ohAuth.get({ accessToken: '', accessSecret: '' },url,params,function(status,data,response){ + console.log(response); // there's the return of the call + }); + +#### Post request: + + ohAuth.post({ accessToken: '', accessSecret: '' },url,params,function(status,data,response){ + console.log(response); // there's the return of the call + },contentType); // contentType is an optional parameter..oh and params is optional too + +### 3. Giant Disclaimer +This is not fully tested, and probably has a lot of issue in the smaller details. It's really just a wrapper around oauth.js, and etc. and use at own risk etc. But if you want really quick and easy oauth and don't care much about configuration, here she is! + +### 4. TODO + +1. I'm adding the ability to bind to the event of getting the tokens, so you can store them in your database (they're still floating around on the req.session though, so you can always look there in the meantime) + +2. Testing testing testing \ No newline at end of file diff --git a/node_modules/oauth/.npmignore b/node_modules/oauth/.npmignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/node_modules/oauth/.npmignore @@ -0,0 +1 @@ +node_modules diff --git a/node_modules/oauth/LICENSE b/node_modules/oauth/LICENSE new file mode 100644 index 0000000..f8049f8 --- /dev/null +++ b/node_modules/oauth/LICENSE @@ -0,0 +1,8 @@ +The MIT License (MIT) +Copyright (c) <2010-2012> + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/oauth/Makefile b/node_modules/oauth/Makefile new file mode 100644 index 0000000..be7c339 --- /dev/null +++ b/node_modules/oauth/Makefile @@ -0,0 +1,7 @@ +# +# Run all tests +# +test: + @@node_modules/.bin/vows tests/* --spec + +.PHONY: test install diff --git a/node_modules/oauth/Readme.md b/node_modules/oauth/Readme.md new file mode 100644 index 0000000..bfdb6b8 --- /dev/null +++ b/node_modules/oauth/Readme.md @@ -0,0 +1,78 @@ +node-oauth +=========== +A simple oauth API for node.js . This API allows users to authenticate against OAUTH providers, and thus act as OAuth consumers. It also has support for OAuth Echo, which is used for communicating with 3rd party media providers such as TwitPic and yFrog. + +Tested against Twitter (http://twitter.com), term.ie (http://term.ie/oauth/example/), TwitPic, and Yahoo! + +Also provides rudimentary OAuth2 support, tested against facebook, github, foursquare, google and Janrain. For more complete usage examples please take a look at connect-auth (http://github.com/ciaranj/connect-auth) + + +Change History +============== + +* 0.9.7 + - OAuth2: Pass back any extra response data for calls to getOAuthAccessToken (Thanks to Tang Bo Hao) + - OAuth2: Don't force a https request if given a http url (Thanks to Damien Mathieu) + - OAuth2: Supports specifying a grant-type of 'refresh-token' (Thanks to Luke Baker) +* 0.9.6 + - OAuth2: Support for 302 redirects (Thanks Patrick Negri). + - OAuth1/2: Some code tidying. ( Thanks to Raoul Millais ) +* 0.9.5 + - OAuth1: Allow usage of HTTP verbs other than GET for retrieving the access and request tokens (Thanks to Raoul Millais) +* 0.9.4 + - OAuth1/2: Support for OAuth providers that drop connections (don't send response lengths? [Google]) + - OAuth2: Change getOAuthAccessToken to POST rather than GET ( Possible Breaking change!!! ... re-tested against Google, Github, Facebook, FourSquare and Janrain and seems ok .. is closer to the spec (v20) ) +* 0.9.3 + - OAuth1: Adds support for following 301 redirects (Thanks bdickason) +* 0.9.2 + - OAuth1: Correct content length calculated for non-ascii post bodies (Thanks selead) + - OAuth1: Allowed for configuration of the 'access token' name used when requesting protected resources (OAuth2) +* 0.9.1 + - OAuth1: Added support for automatically following 302 redirects (Thanks neyric) + - OAuth1: Added support for OAuth Echo (Thanks Ryan LeFevre). + - OAuth1: Improved handling of 2xx responses (Thanks Neil Mansilla). +* 0.9.0 + - OAuth1/2: Compatibility fixes to bring node-oauth up to speed with node.js 0.4x [thanks to Rasmus Andersson for starting the work ] +* 0.8.4 + - OAuth1: Fixed issue #14 (Parameter ordering ignored encodings). + - OAuth1: Added support for repeated parameter names. + - OAuth1/2: Implements issue #15 (Use native SHA1 if available, 10x speed improvement!). + - OAuth2: Fixed issue #16 (Should use POST when requesting access tokens.). + - OAuth2: Fixed Issue #17 (OAuth2 spec compliance). + - OAuth1: Implemented enhancement #13 (Adds support for PUT & DELETE http verbs). + - OAuth1: Fixes issue #18 (Complex/Composite url arguments [thanks novemberborn]) +* 0.8.3 + - OAuth1: Fixed an issue where the auth header code depended on the Array's toString method (Yohei Sasaki) Updated the getOAuthRequestToken method so we can access google's OAuth secured methods. Also re-implemented and fleshed out the test suite. +* 0.8.2 + - OAuth1: The request returning methods will now write the POST body if provided (Chris Anderson), the code responsible for manipulating the headers is a bit safe now when working with other code (Paul McKellar) + - Package: Tweaked the package.json to use index.js instead of main.js +* 0.8.1 + - OAuth1: Added mechanism to get hold of a signed Node Request object, ready for attaching response listeners etc. (Perfect for streaming APIs) +* 0.8.0 + - OAuth1: Standardised method capitalisation, the old getOauthAccessToken is now getOAuthAccessToken (Breaking change to existing code) +* 0.7.7 + - OAuth1: Looks like non oauth_ parameters where appearing within the Authorization headers, which I believe to be incorrect. +* 0.7.6 + - OAuth1: Added in oauth_verifier property to getAccessToken required for 1.0A +* 0.7.5 + - Package: Added in a main.js to simplify the require'ing of OAuth +* 0.7.4 + - OAuth1: Minor change to add an error listener to the OAuth client (thanks troyk) +* 0.7.3 + - OAuth2: Now sends a Content-Length Http header to keep nginx happy :) +* 0.7.2 + - OAuth1: Fixes some broken unit tests! +* 0.7.0 + - OAuth1/2: Introduces support for HTTPS end points and callback URLS for OAuth 1.0A and Oauth 2 (Please be aware that this was a breaking change to the constructor arguments order) + +Contributors +============ + +* Ciaran Jessup - ciaranj@gmail.com +* Mark Wubben - http://equalmedia.com/ +* Ryan LeFevre - http://meltingice.net +* Raoul Millais +* Patrick Negri - http://github.com/pnegri +* Tang Bo Hao - http://github.com/btspoony +* Damien Mathieu - http://42.dmathieu.com +* Luke Baker - http://github.com/lukebaker diff --git a/node_modules/oauth/examples/express-gdata/server.js b/node_modules/oauth/examples/express-gdata/server.js new file mode 100644 index 0000000..3c7bf7f --- /dev/null +++ b/node_modules/oauth/examples/express-gdata/server.js @@ -0,0 +1,168 @@ +var express = require('express'), + OAuth = require('oauth').OAuth, + querystring = require('querystring'); + +// Setup the Express.js server +var app = express.createServer(); +app.use(express.logger()); +app.use(express.bodyParser()); +app.use(express.cookieParser()); +app.use(express.session({ + secret: "skjghskdjfhbqigohqdiouk" +})); + +// Home Page +app.get('/', function(req, res){ + if(!req.session.oauth_access_token) { + res.redirect("/google_login"); + } + else { + res.redirect("/google_contacts"); + } +}); + +// Request an OAuth Request Token, and redirects the user to authorize it +app.get('/google_login', function(req, res) { + + var getRequestTokenUrl = "https://www.google.com/accounts/OAuthGetRequestToken"; + + // GData specifid: scopes that wa want access to + var gdataScopes = [ + querystring.escape("https://www.google.com/m8/feeds/"), + querystring.escape("https://www.google.com/calendar/feeds/") + ]; + + var oa = new OAuth(getRequestTokenUrl+"?scope="+gdataScopes.join('+'), + "https://www.google.com/accounts/OAuthGetAccessToken", + "anonymous", + "anonymous", + "1.0", + "http://localhost:3000/google_cb"+( req.param('action') && req.param('action') != "" ? "?action="+querystring.escape(req.param('action')) : "" ), + "HMAC-SHA1"); + + oa.getOAuthRequestToken(function(error, oauth_token, oauth_token_secret, results){ + if(error) { + console.log('error'); + console.log(error); + } + else { + // store the tokens in the session + req.session.oa = oa; + req.session.oauth_token = oauth_token; + req.session.oauth_token_secret = oauth_token_secret; + + // redirect the user to authorize the token + res.redirect("https://www.google.com/accounts/OAuthAuthorizeToken?oauth_token="+oauth_token); + } + }) + +}); + +// Callback for the authorization page +app.get('/google_cb', function(req, res) { + + // get the OAuth access token with the 'oauth_verifier' that we received + + var oa = new OAuth(req.session.oa._requestUrl, + req.session.oa._accessUrl, + req.session.oa._consumerKey, + req.session.oa._consumerSecret, + req.session.oa._version, + req.session.oa._authorize_callback, + req.session.oa._signatureMethod); + + console.log(oa); + + oa.getOAuthAccessToken( + req.session.oauth_token, + req.session.oauth_token_secret, + req.param('oauth_verifier'), + function(error, oauth_access_token, oauth_access_token_secret, results2) { + + if(error) { + console.log('error'); + console.log(error); + } + else { + + // store the access token in the session + req.session.oauth_access_token = oauth_access_token; + req.session.oauth_access_token_secret = oauth_access_token_secret; + + res.redirect((req.param('action') && req.param('action') != "") ? req.param('action') : "/google_contacts"); + } + + }); + +}); + + +function require_google_login(req, res, next) { + if(!req.session.oauth_access_token) { + res.redirect("/google_login?action="+querystring.escape(req.originalUrl)); + return; + } + next(); +}; + +app.get('/google_contacts', require_google_login, function(req, res) { + var oa = new OAuth(req.session.oa._requestUrl, + req.session.oa._accessUrl, + req.session.oa._consumerKey, + req.session.oa._consumerSecret, + req.session.oa._version, + req.session.oa._authorize_callback, + req.session.oa._signatureMethod); + + console.log(oa); + + // Example using GData API v3 + // GData Specific Header + oa._headers['GData-Version'] = '3.0'; + + oa.getProtectedResource( + "https://www.google.com/m8/feeds/contacts/default/full?alt=json", + "GET", + req.session.oauth_access_token, + req.session.oauth_access_token_secret, + function (error, data, response) { + + var feed = JSON.parse(data); + + res.render('google_contacts.ejs', { + locals: { feed: feed } + }); + }); + +}); + +app.get('/google_calendars', require_google_login, function(req, res) { + var oa = new OAuth(req.session.oa._requestUrl, + req.session.oa._accessUrl, + req.session.oa._consumerKey, + req.session.oa._consumerSecret, + req.session.oa._version, + req.session.oa._authorize_callback, + req.session.oa._signatureMethod); + // Example using GData API v2 + // GData Specific Header + oa._headers['GData-Version'] = '2'; + + oa.getProtectedResource( + "https://www.google.com/calendar/feeds/default/allcalendars/full?alt=jsonc", + "GET", + req.session.oauth_access_token, + req.session.oauth_access_token_secret, + function (error, data, response) { + + var feed = JSON.parse(data); + + res.render('google_calendars.ejs', { + locals: { feed: feed } + }); + }); + +}); + +app.listen(3000); +console.log("listening on http://localhost:3000"); diff --git a/node_modules/oauth/examples/express-gdata/views/google_calendars.ejs b/node_modules/oauth/examples/express-gdata/views/google_calendars.ejs new file mode 100644 index 0000000..15b826f --- /dev/null +++ b/node_modules/oauth/examples/express-gdata/views/google_calendars.ejs @@ -0,0 +1,21 @@ + +

Check google_contacts

+ +

Google Calendars

+ +<% for(var i = 0 ; i < feed.data.items.length ; i++ ) { + + var calendar = feed.data.items[i]; %> +
+ +

"><%= calendar["title"] %>

+ +

canEdit: <%= calendar["canEdit"] %>

+

accessLevel: <%= calendar["accessLevel"] %>

+

timeZone: <%= calendar["timeZone"] %>

+

kind: <%= calendar["kind"] %>

+

updated: <%= calendar["updated"] %>

+

created: <%= calendar["created"] %>

+ +
+<% } %> \ No newline at end of file diff --git a/node_modules/oauth/examples/express-gdata/views/google_contacts.ejs b/node_modules/oauth/examples/express-gdata/views/google_contacts.ejs new file mode 100644 index 0000000..a2050b2 --- /dev/null +++ b/node_modules/oauth/examples/express-gdata/views/google_contacts.ejs @@ -0,0 +1,24 @@ + +

Check google_calendars

+ +

Google Contacts

+ +<% for(var i = 0 ; i < feed.feed.entry.length ; i++ ) { + + var contact = feed.feed.entry[i]; %> + +
+ + <%= contact["title"]["$t"] %> + <% emails = contact["gd$email"] %> + +
    + <% for(var j = 0 ; j < emails.length ; j++) { %> +
  • <%= emails[j]["address" ]%>
  • + <% } %> +
+ +
+ + +<% } %> diff --git a/node_modules/oauth/examples/express-gdata/views/layout.ejs b/node_modules/oauth/examples/express-gdata/views/layout.ejs new file mode 100644 index 0000000..8d1ac6d --- /dev/null +++ b/node_modules/oauth/examples/express-gdata/views/layout.ejs @@ -0,0 +1,9 @@ + + + + + +<%- body %> + + + \ No newline at end of file diff --git a/node_modules/oauth/examples/term.ie.oauth-HMAC-SHA1.js b/node_modules/oauth/examples/term.ie.oauth-HMAC-SHA1.js new file mode 100644 index 0000000..91af05d --- /dev/null +++ b/node_modules/oauth/examples/term.ie.oauth-HMAC-SHA1.js @@ -0,0 +1,31 @@ +var util= require('util') + +var OAuth= require('../lib/oauth').OAuth; + +var oa= new OAuth("http://term.ie/oauth/example/request_token.php", + "http://term.ie/oauth/example/access_token.php", + "key", + "secret", + "1.0", + null, + "HMAC-SHA1") + +oa.getOAuthRequestToken(function(error, oauth_token, oauth_token_secret, results){ + if(error) util.puts('error :' + error) + else { + util.puts('oauth_token :' + oauth_token) + util.puts('oauth_token_secret :' + oauth_token_secret) + util.puts('requestoken results :' + util.inspect(results)) + util.puts("Requesting access token") + oa.getOAuthAccessToken(oauth_token, oauth_token_secret, function(error, oauth_access_token, oauth_access_token_secret, results2) { + util.puts('oauth_access_token :' + oauth_access_token) + util.puts('oauth_token_secret :' + oauth_access_token_secret) + util.puts('accesstoken results :' + util.inspect(results2)) + util.puts("Requesting access token") + var data= ""; + oa.getProtectedResource("http://term.ie/oauth/example/echo_api.php?foo=bar&too=roo", "GET", oauth_access_token, oauth_access_token_secret, function (error, data, response) { + util.puts(data); + }); + }); + } +}) diff --git a/node_modules/oauth/examples/term.ie.oauth-PLAINTEXT.js b/node_modules/oauth/examples/term.ie.oauth-PLAINTEXT.js new file mode 100644 index 0000000..45c52e4 --- /dev/null +++ b/node_modules/oauth/examples/term.ie.oauth-PLAINTEXT.js @@ -0,0 +1,34 @@ +var util= require('util') + +var OAuth= require('../lib/oauth').OAuth; + +var oa = new OAuth("http://term.ie/oauth/example/request_token.php?foo=bar", + "http://term.ie/oauth/example/access_token.php", + "key", + "secret", + "1.0", + null, + "PLAINTEXT") + +oa.getOAuthRequestToken(function(error, oauth_token, oauth_token_secret, results){ + if (error) return console.log('error :' + error) + console.log('oauth_token :' + oauth_token) + console.log('oauth_token_secret :' + oauth_token_secret) + console.log('requestoken results :', results) + console.log("Requesting access token") + oa.getOAuthAccessToken(oauth_token, oauth_token_secret, + function(error, oauth_access_token, + oauth_access_token_secret, results2) { + console.log('oauth_access_token :' + oauth_access_token) + console.log('oauth_token_secret :' + oauth_access_token_secret) + console.log('accesstoken results :', results2) + console.log("Requesting access token") + var data= ""; + oa.getProtectedResource( + "http://term.ie/oauth/example/echo_api.php?foo=bar&too=roo", "GET", + oauth_access_token, oauth_access_token_secret, + function (error, data, response) { + console.log(data); + }); + }); +}) diff --git a/node_modules/oauth/index.js b/node_modules/oauth/index.js new file mode 100644 index 0000000..e20716d --- /dev/null +++ b/node_modules/oauth/index.js @@ -0,0 +1,3 @@ +exports.OAuth = require("./lib/oauth").OAuth; +exports.OAuthEcho = require("./lib/oauth").OAuthEcho; +exports.OAuth2 = require("./lib/oauth2").OAuth2; \ No newline at end of file diff --git a/node_modules/oauth/lib/_utils.js b/node_modules/oauth/lib/_utils.js new file mode 100644 index 0000000..d0feed9 --- /dev/null +++ b/node_modules/oauth/lib/_utils.js @@ -0,0 +1,4 @@ +// Returns true if this is a host that closes *before* it ends?!?! +module.exports.isAnEarlyCloseHost= function( hostName ) { + return hostName.match(".*google(apis)?.com$") +} \ No newline at end of file diff --git a/node_modules/oauth/lib/oauth.js b/node_modules/oauth/lib/oauth.js new file mode 100644 index 0000000..6283b1e --- /dev/null +++ b/node_modules/oauth/lib/oauth.js @@ -0,0 +1,548 @@ +var crypto= require('crypto'), + sha1= require('./sha1'), + http= require('http'), + https= require('https'), + URL= require('url'), + querystring= require('querystring'), + OAuthUtils= require('./_utils'); + +exports.OAuth= function(requestUrl, accessUrl, consumerKey, consumerSecret, version, authorize_callback, signatureMethod, nonceSize, customHeaders) { + this._isEcho = false; + + this._requestUrl= requestUrl; + this._accessUrl= accessUrl; + this._consumerKey= consumerKey; + this._consumerSecret= this._encodeData( consumerSecret ); + this._version= version; + if( authorize_callback === undefined ) { + this._authorize_callback= "oob"; + } + else { + this._authorize_callback= authorize_callback; + } + + if( signatureMethod != "PLAINTEXT" && signatureMethod != "HMAC-SHA1") + throw new Error("Un-supported signature method: " + signatureMethod ) + this._signatureMethod= signatureMethod; + this._nonceSize= nonceSize || 32; + this._headers= customHeaders || {"Accept" : "*/*", + "Connection" : "close", + "User-Agent" : "Node authentication"} + this._clientOptions= this._defaultClientOptions= {"requestTokenHttpMethod": "POST", + "accessTokenHttpMethod": "POST"}; +}; + +exports.OAuthEcho= function(realm, verify_credentials, consumerKey, consumerSecret, version, signatureMethod, nonceSize, customHeaders) { + this._isEcho = true; + + this._realm= realm; + this._verifyCredentials = verify_credentials; + this._consumerKey= consumerKey; + this._consumerSecret= this._encodeData( consumerSecret ); + this._version= version; + + if( signatureMethod != "PLAINTEXT" && signatureMethod != "HMAC-SHA1") + throw new Error("Un-supported signature method: " + signatureMethod ); + this._signatureMethod= signatureMethod; + this._nonceSize= nonceSize || 32; + this._headers= customHeaders || {"Accept" : "*/*", + "Connection" : "close", + "User-Agent" : "Node authentication"}; +} + +exports.OAuthEcho.prototype = exports.OAuth.prototype; + +exports.OAuth.prototype._getTimestamp= function() { + return Math.floor( (new Date()).getTime() / 1000 ); +} + +exports.OAuth.prototype._encodeData= function(toEncode){ + if( toEncode == null || toEncode == "" ) return "" + else { + var result= encodeURIComponent(toEncode); + // Fix the mismatch between OAuth's RFC3986's and Javascript's beliefs in what is right and wrong ;) + return result.replace(/\!/g, "%21") + .replace(/\'/g, "%27") + .replace(/\(/g, "%28") + .replace(/\)/g, "%29") + .replace(/\*/g, "%2A"); + } +} + +exports.OAuth.prototype._decodeData= function(toDecode) { + if( toDecode != null ) { + toDecode = toDecode.replace(/\+/g, " "); + } + return decodeURIComponent( toDecode); +} + +exports.OAuth.prototype._getSignature= function(method, url, parameters, tokenSecret) { + var signatureBase= this._createSignatureBase(method, url, parameters); + return this._createSignature( signatureBase, tokenSecret ); +} + +exports.OAuth.prototype._normalizeUrl= function(url) { + var parsedUrl= URL.parse(url, true) + var port =""; + if( parsedUrl.port ) { + if( (parsedUrl.protocol == "http:" && parsedUrl.port != "80" ) || + (parsedUrl.protocol == "https:" && parsedUrl.port != "443") ) { + port= ":" + parsedUrl.port; + } + } + + if( !parsedUrl.pathname || parsedUrl.pathname == "" ) parsedUrl.pathname ="/"; + + return parsedUrl.protocol + "//" + parsedUrl.hostname + port + parsedUrl.pathname; +} + +// Is the parameter considered an OAuth parameter +exports.OAuth.prototype._isParameterNameAnOAuthParameter= function(parameter) { + var m = parameter.match('^oauth_'); + if( m && ( m[0] === "oauth_" ) ) { + return true; + } + else { + return false; + } +}; + +// build the OAuth request authorization header +exports.OAuth.prototype._buildAuthorizationHeaders= function(orderedParameters) { + var authHeader="OAuth "; + if( this._isEcho ) { + authHeader += 'realm="' + this._realm + '",'; + } + + for( var i= 0 ; i < orderedParameters.length; i++) { + // Whilst the all the parameters should be included within the signature, only the oauth_ arguments + // should appear within the authorization header. + if( this._isParameterNameAnOAuthParameter(orderedParameters[i][0]) ) { + authHeader+= "" + this._encodeData(orderedParameters[i][0])+"=\""+ this._encodeData(orderedParameters[i][1])+"\","; + } + } + + authHeader= authHeader.substring(0, authHeader.length-1); + return authHeader; +} + +// Takes an object literal that represents the arguments, and returns an array +// of argument/value pairs. +exports.OAuth.prototype._makeArrayOfArgumentsHash= function(argumentsHash) { + var argument_pairs= []; + for(var key in argumentsHash ) { + var value= argumentsHash[key]; + if( Array.isArray(value) ) { + for(var i=0;i= 200 && response.statusCode <= 299 ) { + callback(null, data, response); + } else { + // Follow 301 or 302 redirects with Location HTTP header + if((response.statusCode == 301 || response.statusCode == 302) && response.headers && response.headers.location) { + self._performSecureRequest( oauth_token, oauth_token_secret, method, response.headers.location, extra_params, post_body, post_content_type, callback); + } + else { + callback({ statusCode: response.statusCode, data: data }, data, response); + } + } + } + } + + request.on('response', function (response) { + response.setEncoding('utf8'); + response.on('data', function (chunk) { + data+=chunk; + }); + response.on('end', function () { + passBackControl( response ); + }); + response.on('close', function () { + if( allowEarlyClose ) { + passBackControl( response ); + } + }); + }); + + request.on("error", function(err) { + callbackCalled= true; + callback( err ) + }); + + if( (method == "POST" || method =="PUT") && post_body != null && post_body != "" ) { + request.write(post_body); + } + request.end(); + } + else { + if( (method == "POST" || method =="PUT") && post_body != null && post_body != "" ) { + request.write(post_body); + } + return request; + } + + return; +} + +exports.OAuth.prototype.setClientOptions= function(options) { + var key, + mergedOptions= {}, + hasOwnProperty= Object.prototype.hasOwnProperty; + + for( key in this._defaultClientOptions ) { + if( !hasOwnProperty.call(options, key) ) { + mergedOptions[key]= this._defaultClientOptions[key]; + } else { + mergedOptions[key]= options[key]; + } + } + + this._clientOptions= mergedOptions; +}; + +exports.OAuth.prototype.getOAuthAccessToken= function(oauth_token, oauth_token_secret, oauth_verifier, callback) { + var extraParams= {}; + if( typeof oauth_verifier == "function" ) { + callback= oauth_verifier; + } else { + extraParams.oauth_verifier= oauth_verifier; + } + + this._performSecureRequest( oauth_token, oauth_token_secret, this._clientOptions.accessTokenHttpMethod, this._accessUrl, extraParams, null, null, function(error, data, response) { + if( error ) callback(error); + else { + var results= querystring.parse( data ); + var oauth_access_token= results["oauth_token"]; + delete results["oauth_token"]; + var oauth_access_token_secret= results["oauth_token_secret"]; + delete results["oauth_token_secret"]; + callback(null, oauth_access_token, oauth_access_token_secret, results ); + } + }) +} + +// Deprecated +exports.OAuth.prototype.getProtectedResource= function(url, method, oauth_token, oauth_token_secret, callback) { + this._performSecureRequest( oauth_token, oauth_token_secret, method, url, null, "", null, callback ); +} + +exports.OAuth.prototype.delete= function(url, oauth_token, oauth_token_secret, callback) { + return this._performSecureRequest( oauth_token, oauth_token_secret, "DELETE", url, null, "", null, callback ); +} + +exports.OAuth.prototype.get= function(url, oauth_token, oauth_token_secret, callback) { + return this._performSecureRequest( oauth_token, oauth_token_secret, "GET", url, null, "", null, callback ); +} + +exports.OAuth.prototype._putOrPost= function(method, url, oauth_token, oauth_token_secret, post_body, post_content_type, callback) { + var extra_params= null; + if( typeof post_content_type == "function" ) { + callback= post_content_type; + post_content_type= null; + } + if( typeof post_body != "string" ) { + post_content_type= "application/x-www-form-urlencoded" + extra_params= post_body; + post_body= null; + } + return this._performSecureRequest( oauth_token, oauth_token_secret, method, url, extra_params, post_body, post_content_type, callback ); +} + + +exports.OAuth.prototype.put= function(url, oauth_token, oauth_token_secret, post_body, post_content_type, callback) { + return this._putOrPost("PUT", url, oauth_token, oauth_token_secret, post_body, post_content_type, callback); +} + +exports.OAuth.prototype.post= function(url, oauth_token, oauth_token_secret, post_body, post_content_type, callback) { + return this._putOrPost("POST", url, oauth_token, oauth_token_secret, post_body, post_content_type, callback); +} + +/** + * Gets a request token from the OAuth provider and passes that information back + * to the calling code. + * + * The callback should expect a function of the following form: + * + * function(err, token, token_secret, parsedQueryString) {} + * + * This method has optional parameters so can be called in the following 2 ways: + * + * 1) Primary use case: Does a basic request with no extra parameters + * getOAuthRequestToken( callbackFunction ) + * + * 2) As above but allows for provision of extra parameters to be sent as part of the query to the server. + * getOAuthRequestToken( extraParams, callbackFunction ) + * + * N.B. This method will HTTP POST verbs by default, if you wish to override this behaviour you will + * need to provide a requestTokenHttpMethod option when creating the client. + * + **/ +exports.OAuth.prototype.getOAuthRequestToken= function( extraParams, callback ) { + if( typeof extraParams == "function" ){ + callback = extraParams; + extraParams = {}; + } + // Callbacks are 1.0A related + if( this._authorize_callback ) { + extraParams["oauth_callback"]= this._authorize_callback; + } + this._performSecureRequest( null, null, this._clientOptions.requestTokenHttpMethod, this._requestUrl, extraParams, null, null, function(error, data, response) { + if( error ) callback(error); + else { + var results= querystring.parse(data); + + var oauth_token= results["oauth_token"]; + var oauth_token_secret= results["oauth_token_secret"]; + delete results["oauth_token"]; + delete results["oauth_token_secret"]; + callback(null, oauth_token, oauth_token_secret, results ); + } + }); +} + +exports.OAuth.prototype.signUrl= function(url, oauth_token, oauth_token_secret, method) { + + if( method === undefined ) { + var method= "GET"; + } + + var orderedParameters= this._prepareParameters(oauth_token, oauth_token_secret, method, url, {}); + var parsedUrl= URL.parse( url, false ); + + var query=""; + for( var i= 0 ; i < orderedParameters.length; i++) { + query+= orderedParameters[i][0]+"="+ this._encodeData(orderedParameters[i][1]) + "&"; + } + query= query.substring(0, query.length-1); + + return parsedUrl.protocol + "//"+ parsedUrl.host + parsedUrl.pathname + "?" + query; +}; + +exports.OAuth.prototype.authHeader= function(url, oauth_token, oauth_token_secret, method) { + if( method === undefined ) { + var method= "GET"; + } + + var orderedParameters= this._prepareParameters(oauth_token, oauth_token_secret, method, url, {}); + return this._buildAuthorizationHeaders(orderedParameters); +}; diff --git a/node_modules/oauth/lib/oauth2.js b/node_modules/oauth/lib/oauth2.js new file mode 100644 index 0000000..191d3ef --- /dev/null +++ b/node_modules/oauth/lib/oauth2.js @@ -0,0 +1,161 @@ +var querystring= require('querystring'), + crypto= require('crypto'), + https= require('https'), + http= require('http'), + URL= require('url'), + OAuthUtils= require('./_utils'); + +exports.OAuth2= function(clientId, clientSecret, baseSite, authorizePath, accessTokenPath) { + this._clientId= clientId; + this._clientSecret= clientSecret; + this._baseSite= baseSite; + this._authorizeUrl= authorizePath || "/oauth/authorize"; + this._accessTokenUrl= accessTokenPath || "/oauth/access_token"; + this._accessTokenName= "access_token"; +} + +// This 'hack' method is required for sites that don't use +// 'access_token' as the name of the access token (for requests). +// ( http://tools.ietf.org/html/draft-ietf-oauth-v2-16#section-7 ) +// it isn't clear what the correct value should be atm, so allowing +// for specific (temporary?) override for now. +exports.OAuth2.prototype.setAccessTokenName= function ( name ) { + this._accessTokenName= name; +} + +exports.OAuth2.prototype._getAccessTokenUrl= function() { + return this._baseSite + this._accessTokenUrl; /* + "?" + querystring.stringify(params); */ +} + +exports.OAuth2.prototype._request= function(method, url, headers, post_body, access_token, callback) { + + var http_library= https; + var creds = crypto.createCredentials({ }); + var parsedUrl= URL.parse( url, true ); + if( parsedUrl.protocol == "https:" && !parsedUrl.port ) { + parsedUrl.port= 443; + } + + // As this is OAUth2, we *assume* https unless told explicitly otherwise. + if( parsedUrl.protocol != "https:" ) { + http_library= http; + } + + var realHeaders= {}; + if( headers ) { + for(var key in headers) { + realHeaders[key] = headers[key]; + } + } + realHeaders['Host']= parsedUrl.host; + + realHeaders['Content-Length']= post_body ? Buffer.byteLength(post_body) : 0; + if( access_token ) { + if( ! parsedUrl.query ) parsedUrl.query= {}; + parsedUrl.query[this._accessTokenName]= access_token; + } + + var result= ""; + var queryStr= querystring.stringify(parsedUrl.query); + if( queryStr ) queryStr= "?" + queryStr; + var options = { + host:parsedUrl.hostname, + port: parsedUrl.port, + path: parsedUrl.pathname + queryStr, + method: method, + headers: realHeaders + }; + + // Some hosts *cough* google appear to close the connection early / send no content-length header + // allow this behaviour. + var allowEarlyClose= OAuthUtils.isAnEarlyCloseHost(options.host); + var callbackCalled= false; + function passBackControl( response, result ) { + if(!callbackCalled) { + callbackCalled=true; + if( response.statusCode != 200 && (response.statusCode != 301) && (response.statusCode != 302) ) { + callback({ statusCode: response.statusCode, data: result }); + } else { + callback(null, result, response); + } + } + } + + var request = http_library.request(options, function (response) { + response.on("data", function (chunk) { + result+= chunk + }); + response.on("close", function (err) { + if( allowEarlyClose ) { + passBackControl( response, result ); + } + }); + response.addListener("end", function () { + passBackControl( response, result ); + }); + }); + request.on('error', function(e) { + callbackCalled= true; + callback(e); + }); + + if( method == 'POST' && post_body ) { + request.write(post_body); + } + request.end(); +} + + +exports.OAuth2.prototype.getAuthorizeUrl= function( params ) { + var params= params || {}; + params['client_id'] = this._clientId; + params['type'] = 'web_server'; + return this._baseSite + this._authorizeUrl + "?" + querystring.stringify(params); +} + +exports.OAuth2.prototype.getOAuthAccessToken= function(code, params, callback) { + var params= params || {}; + params['client_id'] = this._clientId; + params['client_secret'] = this._clientSecret; + params['type']= 'web_server'; + var codeParam = (params.grant_type === 'refresh_token') ? 'refresh_token' : 'code'; + params[codeParam]= code; + + var post_data= querystring.stringify( params ); + var post_headers= { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + + this._request("POST", this._getAccessTokenUrl(), post_headers, post_data, null, function(error, data, response) { + if( error ) callback(error); + else { + var results; + try { + // As of http://tools.ietf.org/html/draft-ietf-oauth-v2-07 + // responses should be in JSON + results= JSON.parse( data ); + } + catch(e) { + // .... However both Facebook + Github currently use rev05 of the spec + // and neither seem to specify a content-type correctly in their response headers :( + // clients of these services will suffer a *minor* performance cost of the exception + // being thrown + results= querystring.parse( data ); + } + var access_token= results["access_token"]; + var refresh_token= results["refresh_token"]; + delete results["refresh_token"]; + callback(null, access_token, refresh_token, results); // callback results =-= + } + }); +} + +// Deprecated +exports.OAuth2.prototype.getProtectedResource= function(url, access_token, callback) { + this._request("GET", url, {}, "", access_token, callback ); +} + +exports.OAuth2.prototype.get= function(url, access_token, callback) { + this._request("GET", url, {}, "", access_token, callback ); +} diff --git a/node_modules/oauth/lib/sha1.js b/node_modules/oauth/lib/sha1.js new file mode 100644 index 0000000..d73277a --- /dev/null +++ b/node_modules/oauth/lib/sha1.js @@ -0,0 +1,334 @@ +/* + * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined + * in FIPS 180-1 + * Version 2.2 Copyright Paul Johnston 2000 - 2009. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for details. + */ + +/* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ +var hexcase = 1; /* hex output format. 0 - lowercase; 1 - uppercase */ +var b64pad = "="; /* base-64 pad character. "=" for strict RFC compliance */ + +/* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ +function hex_sha1(s) { return rstr2hex(rstr_sha1(str2rstr_utf8(s))); } +function b64_sha1(s) { return rstr2b64(rstr_sha1(str2rstr_utf8(s))); } +function any_sha1(s, e) { return rstr2any(rstr_sha1(str2rstr_utf8(s)), e); } +function hex_hmac_sha1(k, d) + { return rstr2hex(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d))); } +function b64_hmac_sha1(k, d) + { return rstr2b64(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d))); } +function any_hmac_sha1(k, d, e) + { return rstr2any(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d)), e); } + +/* + * Perform a simple self-test to see if the VM is working + */ +function sha1_vm_test() +{ + return hex_sha1("abc").toLowerCase() == "a9993e364706816aba3e25717850c26c9cd0d89d"; +} + +/* + * Calculate the SHA1 of a raw string + */ +function rstr_sha1(s) +{ + return binb2rstr(binb_sha1(rstr2binb(s), s.length * 8)); +} + +/* + * Calculate the HMAC-SHA1 of a key and some data (raw strings) + */ +function rstr_hmac_sha1(key, data) +{ + var bkey = rstr2binb(key); + if(bkey.length > 16) bkey = binb_sha1(bkey, key.length * 8); + + var ipad = Array(16), opad = Array(16); + for(var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = binb_sha1(ipad.concat(rstr2binb(data)), 512 + data.length * 8); + return binb2rstr(binb_sha1(opad.concat(hash), 512 + 160)); +} + +/* + * Convert a raw string to a hex string + */ +function rstr2hex(input) +{ + try { hexcase } catch(e) { hexcase=0; } + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var output = ""; + var x; + for(var i = 0; i < input.length; i++) + { + x = input.charCodeAt(i); + output += hex_tab.charAt((x >>> 4) & 0x0F) + + hex_tab.charAt( x & 0x0F); + } + return output; +} + +/* + * Convert a raw string to a base-64 string + */ +function rstr2b64(input) +{ + try { b64pad } catch(e) { b64pad=''; } + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var output = ""; + var len = input.length; + for(var i = 0; i < len; i += 3) + { + var triplet = (input.charCodeAt(i) << 16) + | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0) + | (i + 2 < len ? input.charCodeAt(i+2) : 0); + for(var j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > input.length * 8) output += b64pad; + else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F); + } + } + return output; +} + +/* + * Convert a raw string to an arbitrary string encoding + */ +function rstr2any(input, encoding) +{ + var divisor = encoding.length; + var remainders = Array(); + var i, q, x, quotient; + + /* Convert to an array of 16-bit big-endian values, forming the dividend */ + var dividend = Array(Math.ceil(input.length / 2)); + for(i = 0; i < dividend.length; i++) + { + dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1); + } + + /* + * Repeatedly perform a long division. The binary array forms the dividend, + * the length of the encoding is the divisor. Once computed, the quotient + * forms the dividend for the next step. We stop when the dividend is zero. + * All remainders are stored for later use. + */ + while(dividend.length > 0) + { + quotient = Array(); + x = 0; + for(i = 0; i < dividend.length; i++) + { + x = (x << 16) + dividend[i]; + q = Math.floor(x / divisor); + x -= q * divisor; + if(quotient.length > 0 || q > 0) + quotient[quotient.length] = q; + } + remainders[remainders.length] = x; + dividend = quotient; + } + + /* Convert the remainders to the output string */ + var output = ""; + for(i = remainders.length - 1; i >= 0; i--) + output += encoding.charAt(remainders[i]); + + /* Append leading zero equivalents */ + var full_length = Math.ceil(input.length * 8 / + (Math.log(encoding.length) / Math.log(2))) + for(i = output.length; i < full_length; i++) + output = encoding[0] + output; + + return output; +} + +/* + * Encode a string as utf-8. + * For efficiency, this assumes the input is valid utf-16. + */ +function str2rstr_utf8(input) +{ + var output = ""; + var i = -1; + var x, y; + + while(++i < input.length) + { + /* Decode utf-16 surrogate pairs */ + x = input.charCodeAt(i); + y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0; + if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF) + { + x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF); + i++; + } + + /* Encode output as utf-8 */ + if(x <= 0x7F) + output += String.fromCharCode(x); + else if(x <= 0x7FF) + output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F), + 0x80 | ( x & 0x3F)); + else if(x <= 0xFFFF) + output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F), + 0x80 | ((x >>> 6 ) & 0x3F), + 0x80 | ( x & 0x3F)); + else if(x <= 0x1FFFFF) + output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07), + 0x80 | ((x >>> 12) & 0x3F), + 0x80 | ((x >>> 6 ) & 0x3F), + 0x80 | ( x & 0x3F)); + } + return output; +} + +/* + * Encode a string as utf-16 + */ +function str2rstr_utf16le(input) +{ + var output = ""; + for(var i = 0; i < input.length; i++) + output += String.fromCharCode( input.charCodeAt(i) & 0xFF, + (input.charCodeAt(i) >>> 8) & 0xFF); + return output; +} + +function str2rstr_utf16be(input) +{ + var output = ""; + for(var i = 0; i < input.length; i++) + output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF, + input.charCodeAt(i) & 0xFF); + return output; +} + +/* + * Convert a raw string to an array of big-endian words + * Characters >255 have their high-byte silently ignored. + */ +function rstr2binb(input) +{ + var output = Array(input.length >> 2); + for(var i = 0; i < output.length; i++) + output[i] = 0; + for(var i = 0; i < input.length * 8; i += 8) + output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (24 - i % 32); + return output; +} + +/* + * Convert an array of big-endian words to a string + */ +function binb2rstr(input) +{ + var output = ""; + for(var i = 0; i < input.length * 32; i += 8) + output += String.fromCharCode((input[i>>5] >>> (24 - i % 32)) & 0xFF); + return output; +} + +/* + * Calculate the SHA-1 of an array of big-endian words, and a bit length + */ +function binb_sha1(x, len) +{ + /* append padding */ + x[len >> 5] |= 0x80 << (24 - len % 32); + x[((len + 64 >> 9) << 4) + 15] = len; + + var w = Array(80); + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + var e = -1009589776; + + for(var i = 0; i < x.length; i += 16) + { + var olda = a; + var oldb = b; + var oldc = c; + var oldd = d; + var olde = e; + + for(var j = 0; j < 80; j++) + { + if(j < 16) w[j] = x[i + j]; + else w[j] = bit_rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); + var t = safe_add(safe_add(bit_rol(a, 5), sha1_ft(j, b, c, d)), + safe_add(safe_add(e, w[j]), sha1_kt(j))); + e = d; + d = c; + c = bit_rol(b, 30); + b = a; + a = t; + } + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + e = safe_add(e, olde); + } + return Array(a, b, c, d, e); + +} + +/* + * Perform the appropriate triplet combination function for the current + * iteration + */ +function sha1_ft(t, b, c, d) +{ + if(t < 20) return (b & c) | ((~b) & d); + if(t < 40) return b ^ c ^ d; + if(t < 60) return (b & c) | (b & d) | (c & d); + return b ^ c ^ d; +} + +/* + * Determine the appropriate additive constant for the current iteration + */ +function sha1_kt(t) +{ + return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : + (t < 60) ? -1894007588 : -899497514; +} + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) +{ + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function bit_rol(num, cnt) +{ + return (num << cnt) | (num >>> (32 - cnt)); +} + +exports.HMACSHA1= function(key, data) { + return b64_hmac_sha1(key, data); +} \ No newline at end of file diff --git a/node_modules/oauth/package.json b/node_modules/oauth/package.json new file mode 100644 index 0000000..76a029d --- /dev/null +++ b/node_modules/oauth/package.json @@ -0,0 +1,40 @@ +{ + "name": "oauth", + "description": "Library for interacting with OAuth 1.0, 1.0A, 2 and Echo. Provides simplified client access and allows for construction of more complex apis and OAuth providers.", + "version": "0.9.7", + "directories": { + "lib": "./lib" + }, + "main": "index.js", + "author": { + "name": "Ciaran Jessup", + "email": "ciaranj@gmail.com" + }, + "repository": { + "type": "git", + "url": "git://github.com/ciaranj/node-oauth.git" + }, + "devDependencies": { + "vows": "0.5.x" + }, + "scripts": { + "test": "make test" + }, + "licenses": [ + { + "type": "MIT", + "url": "http://github.com/ciaranj/node-oauth/raw/master/LICENSE" + } + ], + "_id": "oauth@0.9.7", + "dependencies": {}, + "optionalDependencies": {}, + "engines": { + "node": "*" + }, + "_engineSupported": true, + "_npmVersion": "1.1.16", + "_nodeVersion": "v0.6.15", + "_defaultsLoaded": true, + "_from": "oauth@0.X.X" +} diff --git a/node_modules/oauth/tests/oauth.js b/node_modules/oauth/tests/oauth.js new file mode 100644 index 0000000..b52f1e5 --- /dev/null +++ b/node_modules/oauth/tests/oauth.js @@ -0,0 +1,779 @@ +var vows = require('vows'), + assert = require('assert'), + events = require('events'), + OAuth= require('../lib/oauth').OAuth, + OAuthEcho= require('../lib/oauth').OAuthEcho; + +var DummyResponse =function( statusCode ) { + this.statusCode= statusCode; + this.headers= {}; +} +DummyResponse.prototype= events.EventEmitter.prototype; +DummyResponse.prototype.setEncoding= function() {} + +var DummyRequest =function( response ) { + this.response= response; +} +DummyRequest.prototype= events.EventEmitter.prototype; +DummyRequest.prototype.write= function(post_body){} +DummyRequest.prototype.write= function(post_body){ + this.emit('response',this.response); +} +DummyRequest.prototype.end= function(){ + this.response.emit('end'); +} + +vows.describe('OAuth').addBatch({ + 'When generating the signature base string described in http://oauth.net/core/1.0/#sig_base_example': { + topic: new OAuth(null, null, null, null, null, null, "HMAC-SHA1"), + 'we get the expected result string': function (oa) { + var result= oa._createSignatureBase("GET", "http://photos.example.net/photos", + "file=vacation.jpg&oauth_consumer_key=dpf43f3p2l4k3l03&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk&oauth_version=1.0&size=original") + assert.equal( result, "GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal"); + } + }, + 'When normalising a url': { + topic: new OAuth(null, null, null, null, null, null, "HMAC-SHA1"), + 'default ports should be stripped': function(oa) { + assert.equal( oa._normalizeUrl("https://somehost.com:443/foo/bar"), "https://somehost.com/foo/bar" ); + }, + 'should leave in non-default ports from urls for use in signature generation': function(oa) { + assert.equal( oa._normalizeUrl("https://somehost.com:446/foo/bar"), "https://somehost.com:446/foo/bar" ); + assert.equal( oa._normalizeUrl("http://somehost.com:81/foo/bar"), "http://somehost.com:81/foo/bar" ); + }, + 'should add a trailing slash when no path at all is present': function(oa) { + assert.equal( oa._normalizeUrl("http://somehost.com"), "http://somehost.com/") + } + }, + 'When making an array out of the arguments hash' : { + topic: new OAuth(null, null, null, null, null, null, "HMAC-SHA1"), + 'flatten out arguments that are arrays' : function(oa) { + var parameters= {"z": "a", + "a": ["1", "2"], + "1": "c" }; + var parameterResults= oa._makeArrayOfArgumentsHash(parameters); + assert.equal(parameterResults.length, 4); + assert.equal(parameterResults[0][0], "1"); + assert.equal(parameterResults[1][0], "z"); + assert.equal(parameterResults[2][0], "a"); + assert.equal(parameterResults[3][0], "a"); + } + }, + 'When ordering the request parameters' : { + topic: new OAuth(null, null, null, null, null, null, "HMAC-SHA1"), + 'Order them by name' : function(oa) { + var parameters= {"z": "a", + "a": "b", + "1": "c" }; + var parameterResults= oa._sortRequestParams(oa._makeArrayOfArgumentsHash(parameters)) + assert.equal(parameterResults[0][0], "1"); + assert.equal(parameterResults[1][0], "a"); + assert.equal(parameterResults[2][0], "z"); + }, + 'If two parameter names are the same then order by the value': function(oa) { + var parameters= {"z": "a", + "a": ["z", "b", "b", "a", "y"], + "1": "c" }; + var parameterResults= oa._sortRequestParams(oa._makeArrayOfArgumentsHash(parameters)) + assert.equal(parameterResults[0][0], "1"); + assert.equal(parameterResults[1][0], "a"); + assert.equal(parameterResults[1][1], "a"); + assert.equal(parameterResults[2][0], "a"); + assert.equal(parameterResults[2][1], "b"); + assert.equal(parameterResults[3][0], "a"); + assert.equal(parameterResults[3][1], "b"); + assert.equal(parameterResults[4][0], "a"); + assert.equal(parameterResults[4][1], "y"); + assert.equal(parameterResults[5][0], "a"); + assert.equal(parameterResults[5][1], "z"); + assert.equal(parameterResults[6][0], "z"); + } + }, + 'When normalising the request parameters': { + topic: new OAuth(null, null, null, null, null, null, "HMAC-SHA1"), + 'the resulting parameters should be encoded and ordered as per http://tools.ietf.org/html/rfc5849#section-3.1 (3.4.1.3.2)' : function(oa) { + var parameters= {"b5" : "=%3D", + "a3": ["a", "2 q"], + "c@": "", + "a2": "r b", + "oauth_consumer_key": "9djdj82h48djs9d2", + "oauth_token":"kkk9d7dh3k39sjv7", + "oauth_signature_method": "HMAC-SHA1", + "oauth_timestamp": "137131201", + "oauth_nonce": "7d8f3e4a", + "c2" : ""}; + var normalisedParameterString= oa._normaliseRequestParams(parameters); + assert.equal(normalisedParameterString, "a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9djdj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1&oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7"); + } + }, + 'When preparing the parameters for use in signing': { + topic: new OAuth(null, null, null, null, null, null, "HMAC-SHA1"), + 'We need to be wary of node\'s auto object creation from foo[bar] style url parameters' : function(oa) { + var result= oa._prepareParameters( "", "", "", "http://foo.com?foo[bar]=xxx&bar[foo]=yyy", {} ); + assert.equal( result[0][0], "bar[foo]") + assert.equal( result[0][1], "yyy") + assert.equal( result[1][0], "foo[bar]") + assert.equal( result[1][1], "xxx") + } + }, + 'When signing a url': { + topic: function() { + var oa= new OAuth(null, null, "consumerkey", "consumersecret", "1.0", null, "HMAC-SHA1"); + oa._getTimestamp= function(){ return "1272399856"; } + oa._getNonce= function(){ return "ybHPeOEkAUJ3k2wJT9Xb43MjtSgTvKqp"; } + return oa; + }, + 'Provide a valid signature when no token present': function(oa) { + assert.equal( oa.signUrl("http://somehost.com:3323/foo/poop?bar=foo"), "http://somehost.com:3323/foo/poop?bar=foo&oauth_consumer_key=consumerkey&oauth_nonce=ybHPeOEkAUJ3k2wJT9Xb43MjtSgTvKqp&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1272399856&oauth_version=1.0&oauth_signature=7ytO8vPSLut2GzHjU9pn1SV9xjc%3D"); + }, + 'Provide a valid signature when a token is present': function(oa) { + assert.equal( oa.signUrl("http://somehost.com:3323/foo/poop?bar=foo", "token"), "http://somehost.com:3323/foo/poop?bar=foo&oauth_consumer_key=consumerkey&oauth_nonce=ybHPeOEkAUJ3k2wJT9Xb43MjtSgTvKqp&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1272399856&oauth_token=token&oauth_version=1.0&oauth_signature=9LwCuCWw5sURtpMroIolU3YwsdI%3D"); + }, + 'Provide a valid signature when a token and a token secret is present': function(oa) { + assert.equal( oa.signUrl("http://somehost.com:3323/foo/poop?bar=foo", "token", "tokensecret"), "http://somehost.com:3323/foo/poop?bar=foo&oauth_consumer_key=consumerkey&oauth_nonce=ybHPeOEkAUJ3k2wJT9Xb43MjtSgTvKqp&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1272399856&oauth_token=token&oauth_version=1.0&oauth_signature=zeOR0Wsm6EG6XSg0Vw%2FsbpoSib8%3D"); + } + }, + 'When getting a request token': { + topic: function() { + var oa= new OAuth(null, null, "consumerkey", "consumersecret", "1.0", null, "HMAC-SHA1"); + oa._getTimestamp= function(){ return "1272399856"; } + oa._getNonce= function(){ return "ybHPeOEkAUJ3k2wJT9Xb43MjtSgTvKqp"; } + oa._performSecureRequest= function(){ return this.requestArguments = arguments; } + return oa; + }, + 'Use the HTTP method in the client options': function(oa) { + oa.setClientOptions({ requestTokenHttpMethod: "GET" }); + oa.getOAuthRequestToken(function() {}); + assert.equal(oa.requestArguments[2], "GET"); + }, + 'Use a POST by default': function(oa) { + oa.setClientOptions({}); + oa.getOAuthRequestToken(function() {}); + assert.equal(oa.requestArguments[2], "POST"); + } + }, + 'When getting an access token': { + topic: function() { + var oa= new OAuth(null, null, "consumerkey", "consumersecret", "1.0", null, "HMAC-SHA1"); + oa._getTimestamp= function(){ return "1272399856"; } + oa._getNonce= function(){ return "ybHPeOEkAUJ3k2wJT9Xb43MjtSgTvKqp"; } + oa._performSecureRequest= function(){ return this.requestArguments = arguments; } + return oa; + }, + 'Use the HTTP method in the client options': function(oa) { + oa.setClientOptions({ accessTokenHttpMethod: "GET" }); + oa.getOAuthAccessToken(function() {}); + assert.equal(oa.requestArguments[2], "GET"); + }, + 'Use a POST by default': function(oa) { + oa.setClientOptions({}); + oa.getOAuthAccessToken(function() {}); + assert.equal(oa.requestArguments[2], "POST"); + } + }, + 'When get authorization header' : { + topic: function() { + var oa= new OAuth(null, null, "consumerkey", "consumersecret", "1.0", null, "HMAC-SHA1"); + oa._getTimestamp= function(){ return "1272399856"; } + oa._getNonce= function(){ return "ybHPeOEkAUJ3k2wJT9Xb43MjtSgTvKqp"; } + return oa; + }, + 'Provide a valid signature when a token and a token secret is present': function(oa) { + assert.equal( oa.authHeader("http://somehost.com:3323/foo/poop?bar=foo", "token", "tokensecret"), 'OAuth oauth_consumer_key="consumerkey",oauth_nonce="ybHPeOEkAUJ3k2wJT9Xb43MjtSgTvKqp",oauth_signature_method="HMAC-SHA1",oauth_timestamp="1272399856",oauth_token="token",oauth_version="1.0",oauth_signature="zeOR0Wsm6EG6XSg0Vw%2FsbpoSib8%3D"'); + } + }, + 'When get the OAuth Echo authorization header': { + topic: function () { + var realm = "http://foobar.com/"; + var verifyCredentials = "http://api.foobar.com/verify.json"; + var oa = new OAuthEcho(realm, verifyCredentials, "consumerkey", "consumersecret", "1.0A", "HMAC-SHA1"); + oa._getTimestamp= function(){ return "1272399856"; } + oa._getNonce= function(){ return "ybHPeOEkAUJ3k2wJT9Xb43MjtSgTvKqp"; } + return oa; + }, + 'Provide a valid signature when a token and token secret is present': function (oa) { + assert.equal( oa.authHeader("http://somehost.com:3323/foo/poop?bar=foo", "token", "tokensecret"), 'OAuth realm="http://foobar.com/",oauth_consumer_key="consumerkey",oauth_nonce="ybHPeOEkAUJ3k2wJT9Xb43MjtSgTvKqp",oauth_signature_method="HMAC-SHA1",oauth_timestamp="1272399856",oauth_token="token",oauth_version="1.0A",oauth_signature="0rr1LhSxACX2IEWRq3uCb4IwtOs%3D"'); + } + }, + 'When non standard ports are used': { + topic: function() { + var oa= new OAuth(null, null, null, null, null, null, "HMAC-SHA1"), + mockProvider= {}; + + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + assert.equal(headers.Host, "somehost.com:8080"); + assert.equal(hostname, "somehost.com"); + assert.equal(port, "8080"); + return { + on: function() {}, + end: function() {} + }; + } + return oa; + }, + 'getProtectedResource should correctly define the host headers': function(oa) { + oa.getProtectedResource("http://somehost.com:8080", "GET", "oauth_token", null, function(){}) + } + }, + 'When building the OAuth Authorization header': { + topic: new OAuth(null, null, null, null, null, null, "HMAC-SHA1"), + 'All provided oauth arguments should be concatentated correctly' : function(oa) { + var parameters= [ + ["oauth_timestamp", "1234567"], + ["oauth_nonce", "ABCDEF"], + ["oauth_version", "1.0"], + ["oauth_signature_method", "HMAC-SHA1"], + ["oauth_consumer_key", "asdasdnm2321b3"]]; + assert.equal(oa._buildAuthorizationHeaders(parameters), 'OAuth oauth_timestamp="1234567",oauth_nonce="ABCDEF",oauth_version="1.0",oauth_signature_method="HMAC-SHA1",oauth_consumer_key="asdasdnm2321b3"'); + }, + '*Only* Oauth arguments should be concatentated, others should be disregarded' : function(oa) { + var parameters= [ + ["foo", "2343"], + ["oauth_timestamp", "1234567"], + ["oauth_nonce", "ABCDEF"], + ["bar", "dfsdfd"], + ["oauth_version", "1.0"], + ["oauth_signature_method", "HMAC-SHA1"], + ["oauth_consumer_key", "asdasdnm2321b3"], + ["foobar", "asdasdnm2321b3"]]; + assert.equal(oa._buildAuthorizationHeaders(parameters), 'OAuth oauth_timestamp="1234567",oauth_nonce="ABCDEF",oauth_version="1.0",oauth_signature_method="HMAC-SHA1",oauth_consumer_key="asdasdnm2321b3"'); + }, + '_buildAuthorizationHeaders should not depends on Array.prototype.toString' : function(oa) { + var _toString = Array.prototype.toString; + Array.prototype.toString = function(){ return '[Array] ' + this.length; }; // toString overwrite example used in jsdom. + var parameters= [ + ["foo", "2343"], + ["oauth_timestamp", "1234567"], + ["oauth_nonce", "ABCDEF"], + ["bar", "dfsdfd"], + ["oauth_version", "1.0"], + ["oauth_signature_method", "HMAC-SHA1"], + ["oauth_consumer_key", "asdasdnm2321b3"], + ["foobar", "asdasdnm2321b3"]]; + assert.equal(oa._buildAuthorizationHeaders(parameters), 'OAuth oauth_timestamp="1234567",oauth_nonce="ABCDEF",oauth_version="1.0",oauth_signature_method="HMAC-SHA1",oauth_consumer_key="asdasdnm2321b3"'); + Array.prototype.toString = _toString; + } + }, + 'When performing the Secure Request' : { + topic: new OAuth("http://foo.com/RequestToken", + "http://foo.com/AccessToken", + "anonymous", "anonymous", + "1.0A", "http://foo.com/callback", "HMAC-SHA1"), + 'using the POST method' : { + 'Any passed extra_params should form part of the POST body': function(oa) { + var post_body_written= false; + var op= oa._createClient; + try { + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + return { + write: function(post_body){ + post_body_written= true; + assert.equal(post_body,"scope=foobar%2C1%2C2"); + } + }; + } + oa._performSecureRequest("token", "token_secret", 'POST', 'http://foo.com/protected_resource', {"scope": "foobar,1,2"}); + assert.equal(post_body_written, true); + } + finally { + oa._createClient= op; + } + } + } + }, + 'When performing a secure' : { + topic: new OAuth("http://foo.com/RequestToken", + "http://foo.com/AccessToken", + "anonymous", "anonymous", + "1.0A", "http://foo.com/callback", "HMAC-SHA1"), + 'POST' : { + 'if no callback is passed' : { + 'it should return a request object': function(oa) { + var request= oa.post("http://foo.com/blah", "token", "token_secret", "BLAH", "text/plain") + assert.isObject(request); + assert.equal(request.method, "POST"); + request.end(); + } + }, + 'if a callback is passed' : { + "it should call the internal request's end method and return nothing": function(oa) { + var callbackCalled= false; + var op= oa._createClient; + try { + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + return { + write: function(){}, + on: function() {}, + end: function() { + callbackCalled= true; + } + }; + } + var request= oa.post("http://foo.com/blah", "token", "token_secret", "BLAH", "text/plain", function(e,d){}) + assert.equal(callbackCalled, true); + assert.isUndefined(request); + } + finally { + oa._createClient= op; + } + } + }, + 'if the post_body is not a string' : { + "It should be url encoded and the content type set to be x-www-form-urlencoded" : function(oa) { + var op= oa._createClient; + try { + var callbackCalled= false; + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + assert.equal(headers["Content-Type"], "application/x-www-form-urlencoded") + return { + write: function(data){ + callbackCalled= true; + assert.equal(data, "foo=1%2C2%2C3&bar=1%2B2"); + }, + on: function() {}, + end: function() { + } + }; + } + var request= oa.post("http://foo.com/blah", "token", "token_secret", {"foo":"1,2,3", "bar":"1+2"}) + assert.equal(callbackCalled, true); + } + finally { + oa._createClient= op; + } + } + }, + 'if the post_body is a string' : { + "and it contains non ascii (7/8bit) characters" : { + "the content length should be the byte count, and not the string length" : function(oa) { + var testString= "Tôi yêu node"; + var testStringLength= testString.length; + var testStringBytesLength= Buffer.byteLength(testString); + assert.notEqual(testStringLength, testStringBytesLength); // Make sure we're testing a string that differs between byte-length and char-length! + + var op= oa._createClient; + try { + var callbackCalled= false; + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + assert.equal(headers["Content-length"], testStringBytesLength); + return { + write: function(data){ + callbackCalled= true; + assert.equal(data, testString); + }, + on: function() {}, + end: function() { + } + }; + } + var request= oa.post("http://foo.com/blah", "token", "token_secret", "Tôi yêu node") + assert.equal(callbackCalled, true); + } + finally { + oa._createClient= op; + } + } + }, + "and no post_content_type is specified" : { + "It should be written as is, with a content length specified, and the encoding should be set to be x-www-form-urlencoded" : function(oa) { + var op= oa._createClient; + try { + var callbackCalled= false; + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + assert.equal(headers["Content-Type"], "application/x-www-form-urlencoded"); + assert.equal(headers["Content-length"], 23); + return { + write: function(data){ + callbackCalled= true; + assert.equal(data, "foo=1%2C2%2C3&bar=1%2B2"); + }, + on: function() {}, + end: function() { + } + }; + } + var request= oa.post("http://foo.com/blah", "token", "token_secret", "foo=1%2C2%2C3&bar=1%2B2") + assert.equal(callbackCalled, true); + } + finally { + oa._createClient= op; + } + } + }, + "and a post_content_type is specified" : { + "It should be written as is, with a content length specified, and the encoding should be set to be as specified" : function(oa) { + var op= oa._createClient; + try { + var callbackCalled= false; + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + assert.equal(headers["Content-Type"], "unicorn/encoded"); + assert.equal(headers["Content-length"], 23); + return { + write: function(data){ + callbackCalled= true; + assert.equal(data, "foo=1%2C2%2C3&bar=1%2B2"); + }, + on: function() {}, + end: function() { + } + }; + } + var request= oa.post("http://foo.com/blah", "token", "token_secret", "foo=1%2C2%2C3&bar=1%2B2", "unicorn/encoded") + assert.equal(callbackCalled, true); + } + finally { + oa._createClient= op; + } + } + } + } + }, + 'GET' : { + 'if no callback is passed' : { + 'it should return a request object': function(oa) { + var request= oa.get("http://foo.com/blah", "token", "token_secret") + assert.isObject(request); + assert.equal(request.method, "GET"); + request.end(); + } + }, + 'if a callback is passed' : { + "it should call the internal request's end method and return nothing": function(oa) { + var callbackCalled= false; + var op= oa._createClient; + try { + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + return { + on: function() {}, + end: function() { + callbackCalled= true; + } + }; + } + var request= oa.get("http://foo.com/blah", "token", "token_secret", function(e,d) {}) + assert.equal(callbackCalled, true); + assert.isUndefined(request); + } + finally { + oa._createClient= op; + } + } + } + }, + 'PUT' : { + 'if no callback is passed' : { + 'it should return a request object': function(oa) { + var request= oa.put("http://foo.com/blah", "token", "token_secret", "BLAH", "text/plain") + assert.isObject(request); + assert.equal(request.method, "PUT"); + request.end(); + } + }, + 'if a callback is passed' : { + "it should call the internal request's end method and return nothing": function(oa) { + var callbackCalled= 0; + var op= oa._createClient; + try { + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + return { + on: function() {}, + write: function(data) { + callbackCalled++; + }, + end: function() { + callbackCalled++; + } + }; + } + var request= oa.put("http://foo.com/blah", "token", "token_secret", "BLAH", "text/plain", function(e,d){}) + assert.equal(callbackCalled, 2); + assert.isUndefined(request); + } + finally { + oa._createClient= op; + } + } + }, + 'if the post_body is not a string' : { + "It should be url encoded and the content type set to be x-www-form-urlencoded" : function(oa) { + var op= oa._createClient; + try { + var callbackCalled= false; + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + assert.equal(headers["Content-Type"], "application/x-www-form-urlencoded") + return { + write: function(data) { + callbackCalled= true; + assert.equal(data, "foo=1%2C2%2C3&bar=1%2B2"); + } + }; + } + var request= oa.put("http://foo.com/blah", "token", "token_secret", {"foo":"1,2,3", "bar":"1+2"}) + assert.equal(callbackCalled, true); + } + finally { + oa._createClient= op; + } + } + }, + 'if the post_body is a string' : { + "and no post_content_type is specified" : { + "It should be written as is, with a content length specified, and the encoding should be set to be x-www-form-urlencoded" : function(oa) { + var op= oa._createClient; + try { + var callbackCalled= false; + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + assert.equal(headers["Content-Type"], "application/x-www-form-urlencoded"); + assert.equal(headers["Content-length"], 23); + return { + write: function(data) { + callbackCalled= true; + assert.equal(data, "foo=1%2C2%2C3&bar=1%2B2"); + } + }; + } + var request= oa.put("http://foo.com/blah", "token", "token_secret", "foo=1%2C2%2C3&bar=1%2B2") + assert.equal(callbackCalled, true); + } + finally { + oa._createClient= op; + } + } + }, + "and a post_content_type is specified" : { + "It should be written as is, with a content length specified, and the encoding should be set to be as specified" : function(oa) { + var op= oa._createClient; + try { + var callbackCalled= false; + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + assert.equal(headers["Content-Type"], "unicorn/encoded"); + assert.equal(headers["Content-length"], 23); + return { + write: function(data) { + callbackCalled= true; + assert.equal(data, "foo=1%2C2%2C3&bar=1%2B2"); + } + }; + } + var request= oa.put("http://foo.com/blah", "token", "token_secret", "foo=1%2C2%2C3&bar=1%2B2", "unicorn/encoded") + assert.equal(callbackCalled, true); + } + finally { + oa._createClient= op; + } + } + } + } + }, + 'DELETE' : { + 'if no callback is passed' : { + 'it should return a request object': function(oa) { + var request= oa.delete("http://foo.com/blah", "token", "token_secret") + assert.isObject(request); + assert.equal(request.method, "DELETE"); + request.end(); + } + }, + 'if a callback is passed' : { + "it should call the internal request's end method and return nothing": function(oa) { + var callbackCalled= false; + var op= oa._createClient; + try { + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + return { + on: function() {}, + end: function() { + callbackCalled= true; + } + }; + } + var request= oa.delete("http://foo.com/blah", "token", "token_secret", function(e,d) {}) + assert.equal(callbackCalled, true); + assert.isUndefined(request); + } + finally { + oa._createClient= op; + } + } + } + }, + 'Request With a Callback' : { + 'and a 200 response code is received' : { + 'it should callback successfully' : function(oa) { + var op= oa._createClient; + var callbackCalled = false; + try { + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + return new DummyRequest( new DummyResponse(200) ); + } + oa._performSecureRequest("token", "token_secret", 'POST', 'http://originalurl.com', {"scope": "foobar,1,2"}, null, null, function(error) { + // callback + callbackCalled= true; + assert.equal(error, undefined); + }); + assert.equal(callbackCalled, true) + } + finally { + oa._createClient= op; + } + } + }, + 'and a 210 response code is received' : { + 'it should callback successfully' : function(oa) { + var op= oa._createClient; + var callbackCalled = false; + try { + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + return new DummyRequest( new DummyResponse(210) ); + } + oa._performSecureRequest("token", "token_secret", 'POST', 'http://originalurl.com', {"scope": "foobar,1,2"}, null, null, function(error) { + // callback + callbackCalled= true; + assert.equal(error, undefined); + }); + assert.equal(callbackCalled, true) + } + finally { + oa._createClient= op; + } + } + }, + 'And A 301 redirect is received' : { + 'and there is a location header' : { + 'it should (re)perform the secure request but with the new location' : function(oa) { + var op= oa._createClient; + var psr= oa._performSecureRequest; + var responseCounter = 1; + var callbackCalled = false; + var DummyResponse =function() { + if( responseCounter == 1 ){ + this.statusCode= 301; + this.headers= {location:"http://redirectto.com"}; + responseCounter++; + } + else { + this.statusCode= 200; + } + } + DummyResponse.prototype= events.EventEmitter.prototype; + DummyResponse.prototype.setEncoding= function() {} + + try { + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + return new DummyRequest( new DummyResponse() ); + } + oa._performSecureRequest= function( oauth_token, oauth_token_secret, method, url, extra_params, post_body, post_content_type, callback ) { + if( responseCounter == 1 ) { + assert.equal(url, "http://originalurl.com"); + } + else { + assert.equal(url, "http://redirectto.com"); + } + return psr.call(oa, oauth_token, oauth_token_secret, method, url, extra_params, post_body, post_content_type, callback ) + } + + oa._performSecureRequest("token", "token_secret", 'POST', 'http://originalurl.com', {"scope": "foobar,1,2"}, null, null, function() { + // callback + assert.equal(responseCounter, 2); + callbackCalled= true; + }); + assert.equal(callbackCalled, true) + } + finally { + oa._createClient= op; + oa._performSecureRequest= psr; + } + } + }, + 'but there is no location header' : { + 'it should execute the callback, passing the HTTP Response code' : function(oa) { + var op= oa._createClient; + var callbackCalled = false; + try { + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + return new DummyRequest( new DummyResponse(301) ); + } + oa._performSecureRequest("token", "token_secret", 'POST', 'http://originalurl.com', {"scope": "foobar,1,2"}, null, null, function(error) { + // callback + assert.equal(error.statusCode, 301); + callbackCalled= true; + }); + assert.equal(callbackCalled, true) + } + finally { + oa._createClient= op; + } + } + } + }, + 'And A 302 redirect is received' : { + 'and there is a location header' : { + 'it should (re)perform the secure request but with the new location' : function(oa) { + var op= oa._createClient; + var psr= oa._performSecureRequest; + var responseCounter = 1; + var callbackCalled = false; + var DummyResponse =function() { + if( responseCounter == 1 ){ + this.statusCode= 302; + this.headers= {location:"http://redirectto.com"}; + responseCounter++; + } + else { + this.statusCode= 200; + } + } + DummyResponse.prototype= events.EventEmitter.prototype; + DummyResponse.prototype.setEncoding= function() {} + + try { + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + return new DummyRequest( new DummyResponse() ); + } + oa._performSecureRequest= function( oauth_token, oauth_token_secret, method, url, extra_params, post_body, post_content_type, callback ) { + if( responseCounter == 1 ) { + assert.equal(url, "http://originalurl.com"); + } + else { + assert.equal(url, "http://redirectto.com"); + } + return psr.call(oa, oauth_token, oauth_token_secret, method, url, extra_params, post_body, post_content_type, callback ) + } + + oa._performSecureRequest("token", "token_secret", 'POST', 'http://originalurl.com', {"scope": "foobar,1,2"}, null, null, function() { + // callback + assert.equal(responseCounter, 2); + callbackCalled= true; + }); + assert.equal(callbackCalled, true) + } + finally { + oa._createClient= op; + oa._performSecureRequest= psr; + } + } + }, + 'but there is no location header' : { + 'it should execute the callback, passing the HTTP Response code' : function(oa) { + var op= oa._createClient; + var callbackCalled = false; + try { + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + return new DummyRequest( new DummyResponse(302) ); + } + oa._performSecureRequest("token", "token_secret", 'POST', 'http://originalurl.com', {"scope": "foobar,1,2"}, null, null, function(error) { + // callback + assert.equal(error.statusCode, 302); + callbackCalled= true; + }); + assert.equal(callbackCalled, true) + } + finally { + oa._createClient= op; + } + } + } + } + } + } +}).export(module); diff --git a/node_modules/oauth/tests/oauth2.js b/node_modules/oauth/tests/oauth2.js new file mode 100644 index 0000000..4eb47cd --- /dev/null +++ b/node_modules/oauth/tests/oauth2.js @@ -0,0 +1,67 @@ +var vows = require('vows'), + assert = require('assert'), + OAuth2= require('../lib/oauth2').OAuth2; + +vows.describe('OAuth2').addBatch({ + 'Given an OAuth2 instance, ': { + topic: new OAuth2(), + 'When handling the access token response': { + 'we should correctly extract the token if received as form-data': function (oa) { + oa._request= function( method, url, fo, bar, bleh, callback) { + callback(null, "access_token=access&refresh_token=refresh"); + }; + oa.getOAuthAccessToken("", {}, function(error, access_token, refresh_token) { + assert.equal( access_token, "access"); + assert.equal( refresh_token, "refresh"); + }); + }, + 'we should correctly extract the token if received as a JSON literal': function (oa) { + oa._request= function(method, url, headers, post_body, access_token, callback) { + callback(null, '{"access_token":"access","refresh_token":"refresh"}'); + }; + oa.getOAuthAccessToken("", {}, function(error, access_token, refresh_token) { + assert.equal( access_token, "access"); + assert.equal( refresh_token, "refresh"); + }); + }, + 'we should return the received data to the calling method': function (oa) { + oa._request= function(method, url, headers, post_body, access_token, callback) { + callback(null, '{"access_token":"access","refresh_token":"refresh","extra_1":1, "extra_2":"foo"}'); + }; + oa.getOAuthAccessToken("", {}, function(error, access_token, refresh_token, results) { + assert.equal( access_token, "access"); + assert.equal( refresh_token, "refresh"); + assert.isNotNull( results ); + assert.equal( results.extra_1, 1); + assert.equal( results.extra_2, "foo"); + }); + } + }, + 'When no grant_type parameter is specified': { + 'we should pass the value of the code argument as the code parameter': function(oa) { + oa._request= function(method, url, headers, post_body, access_token, callback) { + assert.isTrue( post_body.indexOf("code=xsds23") != -1 ) + } + oa.getOAuthAccessToken("xsds23", {} ); + } + }, + 'When an invalid grant_type parameter is specified': { + 'we should pass the value of the code argument as the code parameter': function(oa) { + oa._request= function(method, url, headers, post_body, access_token, callback) { + assert.isTrue( post_body.indexOf("code=xsds23") != -1 ) + } + oa.getOAuthAccessToken("xsds23", {grant_type:"refresh_toucan"} ); + } + }, + 'When a grant_type parameter of value "refresh_token" is specified': { + 'we should pass the value of the code argument as the refresh_token parameter, should pass a grant_type parameter, but shouldn\'t pass a code parameter' : function(oa) { + oa._request= function(method, url, headers, post_body, access_token, callback) { + assert.isTrue( post_body.indexOf("refresh_token=sdsds2") != -1 ) + assert.isTrue( post_body.indexOf("grant_type=refresh_token") != -1 ) + assert.isTrue( post_body.indexOf("code=") == -1 ) + } + oa.getOAuthAccessToken("sdsds2", {grant_type:"refresh_token"} ); + } + } + } +}).export(module); \ No newline at end of file diff --git a/node_modules/oauth/tests/sha1.js b/node_modules/oauth/tests/sha1.js new file mode 100644 index 0000000..18ba0ae --- /dev/null +++ b/node_modules/oauth/tests/sha1.js @@ -0,0 +1,13 @@ +var vows = require('vows'), + assert = require('assert'); + +vows.describe('SHA1 Hashing').addBatch({ + 'When using the SHA1 Hashing function': { + topic: require('../lib/sha1'), + 'we get the specified digest as described in http://oauth.net/core/1.0/#sig_base_example (A.5.2)': function (sha1) { + assert.equal (sha1.HMACSHA1( "kd94hf93k423kf44&pfkkdhi9sl3r4s00", + "GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal"), + "tR3+Ty81lMeYAr/Fid0kMTYa/WM="); + } + } +}).export(module); \ No newline at end of file diff --git a/ohauth-strategies.js b/ohauth-strategies.js new file mode 100644 index 0000000..4d7fcc9 --- /dev/null +++ b/ohauth-strategies.js @@ -0,0 +1,38 @@ +var Strategies = { + twitter: function(consumerKey,consumerSecret,urls){ + var config = { + requestURL: 'https://api.twitter.com/oauth/request_token', + accessURL: 'https://api.twitter.com/oauth/access_token', + authorizeURL: 'https://api.twitter.com/oauth/authorize', + consumerKey: consumerKey, + consumerSecret: consumerSecret + }; + if(typeof urls ==='object'){ + config.loginURL = urls.loginURL; + config.callbackURL = urls.callBackURL; + config.successURL = urls.successURL; + config.errorURL = urls.errorURL; + }else config.rootURL = urls; //it's just the necessary one + // return it for use in the constructor + return config; + }, + socialflow: function(consumerKey,consumerSecret){ + var config = { + requestURL: 'https://www.socialflow.com/oauth/request_token', + accessURL: 'https://www.socialflow.com/oauth/access_token', + authorizeURL: 'https://www.socialflow.com/oauth/authorize', + consumerKey: consumerKey, + consumerSecret: consumerSecret + }; + if(typeof urls ==='object'){ + config.loginURL = urls.loginURL; + config.callBackURL = urls.callBackURL; + config.successURL = urls.successURL; + config.errorURL = urls.errorURL; + }else config.callBackURL = urls; //it's just the necessary one + // return it for use in the constructor + return config; + } +}; +// doesn't really exist as anything +module.exports = Strategies; \ No newline at end of file diff --git a/ohauth.js b/ohauth.js new file mode 100644 index 0000000..9a49225 --- /dev/null +++ b/ohauth.js @@ -0,0 +1,162 @@ +// OhAuth - lazy oauth drop-in for express +// it uses oauth.js (node-oauth) and make it so you can +// just pass in your express app and expect it to work +var OAuth = require('oauth').OAuth; + + +var OhAuth = function(apiData,app,Express){ + if(!apiData || !app) throw new Error('OhAuth requires data & app to work'); + this.apiData = apiData; + this.logging = apiData.logging || false; + this.__setURLS(); + this.__configureApp(app); +}; +OhAuth.strategies = { + twitter: function(consumerKey,consumerSecret,urls){ + var config = { + requestURL: 'https://api.twitter.com/oauth/request_token', + accessURL: 'https://api.twitter.com/oauth/access_token', + authorizeURL: 'https://api.twitter.com/oauth/authorize', + consumerKey: consumerKey, + consumerSecret: consumerSecret + }; + if(typeof urls ==='object'){ + config.loginURL = urls.loginURL; + config.callbackURL = urls.callBackURL; + config.successURL = urls.successURL; + config.errorURL = urls.errorURL; + }else config.rootURL = urls; //it's just the necessary one + // return it for use in the constructor + return config; + }, + socialflow: function(consumerKey,consumerSecret){ + var config = { + requestURL: 'https://www.socialflow.com/oauth/request_token', + accessURL: 'https://www.socialflow.com/oauth/access_token', + authorizeURL: 'https://www.socialflow.com/oauth/authorize', + consumerKey: consumerKey, + consumerSecret: consumerSecret + }; + if(typeof urls ==='object'){ + config.loginURL = urls.loginURL; + config.callBackURL = urls.callBackURL; + config.successURL = urls.successURL; + config.errorURL = urls.errorURL; + }else config.callBackURL = urls; //it's just the necessary one + // return it for use in the constructor + return config; + } +}; + +OhAuth.prototype = { + + __configureApp: function(app,Express){ + this.app = app; + Express = Express || require('express'); + // set up the app + app.configure(function(){ + app.use(Express.cookieParser()); + app.use(Express.bodyParser()); + app.use(Express.methodOverride()); + app.use(Express.session({secret: 'thuglife'})); + }); + // configure routes in/for express -- remember scope/closures! + var self = this; + this.app.get(this.urls.login,function(req,res){ self.__login(req,res); }); + this.app.get(this.urls.callback,function(req,res){ self.__callback(req,res); }); + // this.app.get(this.urls.success,this.__success); who wants this? + this.app.get(this.urls.error,function(req,res){ self.__error(req,res);}); + }, + __setURLS: function(){ + this.urls = { + login : this.apiData.loginURL || '/oauth/login', + callback : this.apiData.callbackURL || '/oauth/callback', + success : this.apiData.successURL || '/oauth/success', + error : this.apiData.erorrURL || '/oauth/login' // I prefer this + }; + }, + __configureOAuth: function(){ + if(!(this.apiData.requestURL || this.apiData.accessURL || this.apiData.authorizeURL + || this.apiData.consumerKey || this.apiData.consumerSecret)){ + // missing something + throw new Error('Missing API configuration data'); + } + this.oa = new OAuth(this.apiData.requestURL,this.apiData.accessURL, + this.apiData.consumerKey,this.apiData.consumerSecret, + '1.0A',(this.apiData.rootURL+this.urls.callback),'HMAC-SHA1'); + // set up callbacks if you want them + this.requestTokens = function(cb,data){ + if(this.apiData.requestTokens) this.apiData.requestTokens(data); + cb(); + }; + this.accessTokens = function(cb,data){ + if(this.apiData.accessTokens) this.apiData.accessTokens(data); + cb(); + } + }, + __login: function(req,res){ + var self = this; + self.__configureOAuth(); + self.oa.getOAuthRequestToken(function(error,oauth_token,oauth_token_secret,results){ + if(error){ + if(self.logging) new Error(error); + console.log('error'); + // res.redirect(self.urls.error); + } + else { + req.session.oa = self.oa; + req.session.oauth_token = oauth_token; + req.session.oauth_token_secret = oauth_token_secret; + // send them back if you want them + auth_url = self.apiData.authorizeURL; + self.requestTokens(function(){ res.redirect(auth_url+'?oauth_token='+oauth_token); }, + oauth_token,oauth_token_secret); + } + }); + }, + __callback: function(req,res){ + var self = this; + self.oa.getOAuthAccessToken(req.session.oauth_token, req.session.oauth_token_secret, + req.query.oauth_verifier, function(err,accessToken,accessSecret,results){ + + if(err){ + if(self.logging) new Error(err); + console.log(err); + // res.redirect(self.urls.error); + } + else{ + req.session.credentials = req.session.credentials || {}; + req.session.credentials.accessToken = accessToken; + req.session.credentials.accessSecret = accessSecret; + //send them on if he wants them + success = self.urls.success; + self.accessTokens(function(){ res.redirect(success);},accessToken,accessSecret); + } + }); + }, + // methods for calling the API + get: function(cred,url,params,done){ + if(typeof params ==='function') done = params; // params is optional + else url += __formatGet(params); + this.oa.get(url,cred.accessToken,cred.accessSecret,done); + }, + post: function(cred,url,data,done,contentype){ + if(typeof data==='function'){ + contentype = done; + done = data; + } + if(!contentype) var contentype = 'application/x-www-form-urlencoded'; + this.oa.post(url,cred.accessToken,cred.accessSecret,data,contentype,done); + }, + __formatGet: function(params){ + var output = '?'; + for(var k in params){ + if(output!=='?')output+='&'; + output+=k+'='+params[k]; + } + return output; + } +}; + +// commonjs +module.exports = OhAuth; diff --git a/package.json b/package.json new file mode 100644 index 0000000..43a43a3 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "ohauth", + "version": "0.1.0", + "description": "Really lazy OAuth 1.0 for express", + "keywords": [ + "OAuth", + "Express", + "easy" + ], + "repository": { + "type": "git", + "url": "" + }, + "author": { + "name": "Nick Jacob", + "email": "me@nickjacob.com", + "url": "http://nickjacob.com" + }, + "dependencies": { + "oauth": "0.X.X" + }, + "main": "ohauth.js", + "engines": { + "node": ">= 0.5.0 < 0.7.X" + }, + "_id": "ohauth@0.1.0", + "optionalDependencies": {}, + "_engineSupported": "true", + "_npmVersion": "1.1.16", + "_nodeVersion": "v0.6.15" + +}