From 0158a78c4c78a498aaeb9d1cfdcf24c3ea91a53c Mon Sep 17 00:00:00 2001 From: meltingice Date: Thu, 5 May 2011 10:56:23 -0400 Subject: [PATCH] Added support for OAuth Echo --- index.js | 3 +- lib/oauthecho.js | 322 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 lib/oauthecho.js diff --git a/index.js b/index.js index dbd938a..1649f74 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,3 @@ -exports.OAuth = require("./lib/oauth").OAuth; +exports.OAuth = require("./lib/oauth").OAuth; +exports.OAuthEcho = require("./lib/oauthecho").OAuthEcho; exports.OAuth2 = require("./lib/oauth2").OAuth2; \ No newline at end of file diff --git a/lib/oauthecho.js b/lib/oauthecho.js new file mode 100644 index 0000000..141b8fc --- /dev/null +++ b/lib/oauthecho.js @@ -0,0 +1,322 @@ +var crypto= require('crypto'), + sha1= require('./sha1'), + http= require('http'), + https= require('https'), + URL= require('url'), + querystring= require('querystring'); + +exports.OAuthEcho= function(realm, verify_credentials, consumerKey, consumerSecret, version, signatureMethod, nonceSize, customHeaders) { + 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.post = function(url, oauth_token, oauth_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_secret, "POST", url, extra_params, post_body, post_content_type, callback ); +} + +exports.OAuthEcho.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.OAuthEcho.prototype._getSignature= function(method, url, parameters, tokenSecret) { + var signatureBase= this._createSignatureBase(method, url, parameters); + return this._createSignature( signatureBase, tokenSecret ); +} + +exports.OAuthEcho.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; +} + +exports.OAuthEcho.prototype._getTimestamp= function() { + return Math.floor( (new Date()).getTime() / 1000 ); +} + +// Is the parameter considered an OAuth parameter +exports.OAuthEcho.prototype._isParameterNameAnOAuthParameter= function(parameter) { + var m = parameter.match('^oauth_'); + if( m && ( m[0] === "oauth_" ) ) { + return true; + } + else { + return false; + } +}; + +exports.OAuthEcho.prototype._performSecureRequest= function( oauth_token, oauth_token_secret, method, url, extra_params, post_body, post_content_type, callback ) { + + // Here we use verify_credentials instead of url + var orderedParameters= this._prepareParameters(url, oauth_token, oauth_token_secret, method, extra_params); + + if( !post_content_type ) { + post_content_type= "application/x-www-form-urlencoded"; + } + var parsedUrl= URL.parse( url, false ); + if( parsedUrl.protocol == "http:" && !parsedUrl.port ) parsedUrl.port= 80; + if( parsedUrl.protocol == "https:" && !parsedUrl.port ) parsedUrl.port= 443; + + var headers= {}; + + // OAuth Echo header + headers["X-Verify-Credentials-Authorization"]= this._buildAuthorizationHeaders(orderedParameters); + headers["Host"] = parsedUrl.host + + for( var key in this._headers ) { + if (this._headers.hasOwnProperty(key)) { + headers[key]= this._headers[key]; + } + } + + // Filter out any passed extra_params that are really to do with OAuth + for(var key in extra_params) { + if( this._isParameterNameAnOAuthParameter( key ) ) { + delete extra_params[key]; + } + } + + if( (method == "POST" || method == "PUT") && ( post_body == null && extra_params != null) ) { + post_body= querystring.stringify(extra_params); + } + + headers["Content-length"]= post_body ? post_body.length : 0; //Probably going to fail if not posting ascii + headers["Content-Type"]= post_content_type; + + var path; + if( !parsedUrl.pathname || parsedUrl.pathname == "" ) parsedUrl.pathname ="/"; + if( parsedUrl.query ) path= parsedUrl.pathname + "?"+ parsedUrl.query ; + else path= parsedUrl.pathname; + + var request; + if( parsedUrl.protocol == "https:" ) { + request= this._createClient(parsedUrl.port, parsedUrl.hostname, method, path, headers, true); + } + else { + request= this._createClient(parsedUrl.port, parsedUrl.hostname, method, path, headers); + } + + if( callback ) { + var data=""; + var self= this; + request.on('response', function (response) { + response.setEncoding('utf8'); + response.on('data', function (chunk) { + data+=chunk; + }); + response.on('end', function () { + if( response.statusCode != 200 ) { + // Follow 302 redirects with Location HTTP header + if(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); + } + } else { + callback(null, data, response); + } + }); + }); + + request.on("error", callback); + + 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.OAuthEcho.prototype._createClient= function( port, hostname, method, path, headers, sslEnabled ) { + var options = { + host: hostname, + port: port, + path: path, + method: method, + headers: headers + }; + var httpModel; + if( sslEnabled ) { + httpModel= https; + } else { + httpModel= http; + } + return httpModel.request(options); +} + +exports.OAuthEcho.prototype._prepareParameters= function( url, oauth_token, oauth_token_secret, method, extra_params ) { + var oauthParameters= { + "oauth_timestamp": this._getTimestamp(), + "oauth_nonce": this._getNonce(this._nonceSize), + "oauth_version": this._version, + "oauth_signature_method": this._signatureMethod, + "oauth_consumer_key": this._consumerKey, + "oauth_token": oauth_token + }; + + var sig= this._getSignature( "GET", this._verifyCredentials, this._normaliseRequestParams(oauthParameters), oauth_token_secret); + var orderedParameters= this._sortRequestParams( this._makeArrayOfArgumentsHash(oauthParameters) ); + orderedParameters[orderedParameters.length]= ["oauth_signature", sig]; + + return orderedParameters; +} + +// build the OAuth request authorization header +exports.OAuthEcho.prototype._buildAuthorizationHeaders= function(orderedParameters) { + var authHeader='OAuth 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.OAuthEcho.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