acs-apiclient
Version:
Node helper library for connecting to the Answers Cloud Services (ForeSee in particular) web API in a headless manner.
698 lines (615 loc) • 20.1 kB
JavaScript
/**
* An oAuth 1.0a library that conforms to ACS's use of oAuth
*/
var crypto = require('crypto'),
sha1 = require('./sha1'),
request = require('request'),
URL = require('url'),
querystring = require('querystring'),
acsUtils = require('../lib/acsutils.js');
/**
* Set up the export object
*/
var Utils = {};
/**
* The class that will handle the oAuth handshake
*/
Utils.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);
if (signatureMethod == "RSA-SHA1") {
this._privateKey = consumerSecret;
}
this._version = version;
if (authorize_callback === undefined) {
this._authorize_callback = "oob";
}
else {
this._authorize_callback = authorize_callback;
}
if (signatureMethod != "PLAINTEXT" && signatureMethod != "HMAC-SHA1" && signatureMethod != "RSA-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",
"followRedirects": true
};
this._oauthParameterSeperator = ",";
};
/**
* Perform the echo
*/
Utils.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);
if (signatureMethod == "RSA-SHA1") {
this._privateKey = consumerSecret;
}
this._version = version;
if (signatureMethod != "PLAINTEXT" && signatureMethod != "HMAC-SHA1" && signatureMethod != "RSA-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._oauthParameterSeperator = ",";
};
Utils.OAuthEcho.prototype = Utils.OAuth.prototype;
Utils.OAuth.prototype._getTimestamp = function () {
return Math.floor((new Date()).getTime() / 1000);
};
Utils.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");
}
};
/**
* Decode some data
* @param toDecode
* @returns {string}
* @private
*/
Utils.OAuth.prototype._decodeData = function (toDecode) {
if (toDecode !== null) {
toDecode = toDecode.replace(/\+/g, " ");
}
return decodeURIComponent(toDecode);
};
/**
* Get the signature
* @param method
* @param url
* @param parameters
* @param tokenSecret
* @returns {*}
* @private
*/
Utils.OAuth.prototype._getSignature = function (method, url, parameters, tokenSecret) {
var signatureBase = this._createSignatureBase(method, url, parameters);
return this._createSignature(signatureBase, tokenSecret);
};
/**
* Normalize a URL
* @param url
* @returns {string}
* @private
*/
Utils.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?
* @param parameter
* @returns {boolean}
* @private
*/
Utils.OAuth.prototype._isParameterNameAnOAuthParameter = function (parameter) {
var m = parameter.match('^oauth_');
if (m && ( m[0] === "oauth_" )) {
return true;
}
else {
return false;
}
};
/**
* Builds the OAuth request authorization header
*/
Utils.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]) + "\"" + this._oauthParameterSeperator;
}
}
authHeader = authHeader.substring(0, authHeader.length - this._oauthParameterSeperator.length);
return authHeader;
};
/**
* Takes an object literal that represents the arguments, and returns an array
* of the argument / value pairs
*/
Utils.OAuth.prototype._makeArrayOfArgumentsHash = function (argumentsHash) {
var argument_pairs = [];
for (var key in argumentsHash) {
if (argumentsHash.hasOwnProperty(key)) {
var value = argumentsHash[key];
if (Array.isArray(value)) {
for (var i = 0; i < value.length; i++) {
argument_pairs[argument_pairs.length] = [key, value[i]];
}
}
else {
argument_pairs[argument_pairs.length] = [key, value];
}
}
}
return argument_pairs;
};
/**
* Sorts the encoded key value pairs by encoded name, then encoded value
*/
Utils.OAuth.prototype._sortRequestParams = function (argument_pairs) {
// Sort by name, then value.
argument_pairs.sort(function (a, b) {
if (a[0] == b[0]) {
return a[1] < b[1] ? -1 : 1;
}
else return a[0] < b[0] ? -1 : 1;
});
return argument_pairs;
};
/**
* Perform normalization on the request parameters themselves
* @param args
* @returns {string}
* @private
*/
Utils.OAuth.prototype._normaliseRequestParams = function (args) {
var argument_pairs = this._makeArrayOfArgumentsHash(args),
i = 0;
// First encode them #3.4.1.3.2 .1
for (i = 0; i < argument_pairs.length; i++) {
argument_pairs[i][0] = this._encodeData(argument_pairs[i][0]);
argument_pairs[i][1] = this._encodeData(argument_pairs[i][1]);
}
// Then sort them #3.4.1.3.2 .2
argument_pairs = this._sortRequestParams(argument_pairs);
// Then concatenate together #3.4.1.3.2 .3 & .4
args = "";
for (i = 0; i < argument_pairs.length; i++) {
args += argument_pairs[i][0];
args += "=";
args += argument_pairs[i][1];
if (i < argument_pairs.length - 1) args += "&";
}
return args;
};
/**
* Create the signature base
*/
Utils.OAuth.prototype._createSignatureBase = function (method, url, parameters) {
url = this._encodeData(this._normalizeUrl(url));
parameters = this._encodeData(parameters);
return method.toUpperCase() + "&" + url + "&" + parameters;
};
/**
* Create the signature
*/
Utils.OAuth.prototype._createSignature = function (signatureBase, tokenSecret) {
if (tokenSecret === undefined) {
tokenSecret = "";
}
else {
tokenSecret = this._encodeData(tokenSecret);
}
// consumerSecret is already encoded
var key = this._consumerSecret + "&" + tokenSecret,
hash = "";
if (this._signatureMethod == "PLAINTEXT") {
hash = key;
}
else if (this._signatureMethod == "RSA-SHA1") {
key = this._privateKey || "";
hash = crypto.createSign("RSA-SHA1").update(signatureBase).sign(key, 'base64');
}
else {
if (crypto.Hmac) {
hash = crypto.createHmac("sha1", key).update(signatureBase).digest("base64");
}
else {
hash = sha1.HMACSHA1(key, signatureBase);
}
}
return hash;
};
/**
* The valid nonce characters
*/
Utils.OAuth.prototype.NONCE_CHARS = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9'];
/**
* Generate the nonce
*/
Utils.OAuth.prototype._getNonce = function (nonceSize) {
var result = [];
var chars = this.NONCE_CHARS;
var char_pos;
var nonce_chars_length = chars.length;
for (var i = 0; i < nonceSize; i++) {
char_pos = Math.floor(Math.random() * nonce_chars_length);
result[i] = chars[char_pos];
}
return result.join('');
};
/**
* Create an HTTP client
*/
Utils.OAuth.prototype._createClient = function (url, method, headers, cookies, post_body, callback) {
var jar,
parsedUrl = URL.parse(url, false),
baseurl = parsedUrl.href.toString().replace(parsedUrl.path.toString(), '');
if (cookies) {
jar = request.jar();
if (cookies instanceof Array) {
for (var i = 0; i < cookies.length; i++) {
var cookiearr = request.cookie(cookies[i]);
jar.setCookie(cookiearr, baseurl);
}
} else {
for (var ck in cookies) {
var cookie = request.cookie(ck + '=' + cookies[ck]);
jar.setCookie(cookie, baseurl);
}
}
}
var options = {
url: url,
method: method,
headers: headers,
jar: typeof jar != 'undefined' ? jar : null,
pool: false
};
if(post_body != null && typeof post_body != "string"){
options.body = JSON.stringify(post_body);
}
request(options, function(error, response, body) {
if(response && response.headers){
var serverCookies = typeof(response.headers['set-cookie']) == 'undefined' ? [] : response.headers['set-cookie'];
}
callback(error, response, body, serverCookies);
});
};
/**
* Set up the standard request parameters
*/
Utils.OAuth.prototype._prepareParameters = function (oauth_token, oauth_token_secret, method, url, 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
},
sig;
if (oauth_token) {
oauthParameters.oauth_token = oauth_token;
}
if (this._isEcho) {
sig = this._getSignature("GET", this._verifyCredentials, this._normaliseRequestParams(oauthParameters), oauth_token_secret);
}
else {
if (extra_params) {
for (var key1 in extra_params) {
if (extra_params.hasOwnProperty(key1)) {
oauthParameters[key1] = extra_params[key1];
}
}
}
var parsedUrl = URL.parse(url, false);
if (parsedUrl.query) {
var key2,
extraParameters = querystring.parse(parsedUrl.query);
for (var key in extraParameters) {
var value = extraParameters[key];
if (typeof value == "object") {
// TODO: This probably should be recursive
for (key2 in value) {
oauthParameters[key + "[" + key2 + "]"] = value[key2];
}
} else {
oauthParameters[key] = value;
}
}
}
sig = this._getSignature(method, url, this._normaliseRequestParams(oauthParameters), oauth_token_secret);
}
var orderedParameters = this._sortRequestParams(this._makeArrayOfArgumentsHash(oauthParameters));
orderedParameters[orderedParameters.length] = ["oauth_signature", sig];
return orderedParameters;
};
/**
* Run a secure request
*/
Utils.OAuth.prototype._performSecureRequest = function (oauth_token, oauth_token_secret, method, url, extra_params, post_body, post_content_type, callback, cookies) {
var orderedParameters = this._prepareParameters(oauth_token, oauth_token_secret, method, url, extra_params),
headers = {},
authorization = this._buildAuthorizationHeaders(orderedParameters),
parsedUrl = URL.parse(url, false);
if (this._isEcho) {
headers["X-Verify-Credentials-Authorization"] = authorization;
}
else {
headers.Authorization = authorization;
}
headers.Host = parsedUrl.host;
for (var key3 in this._headers) {
if (this._headers.hasOwnProperty(key3)) {
headers[key3] = this._headers[key3];
}
}
// Filter out any passed extra_params that are really to do with OAuth
for (var key4 in extra_params) {
if (this._isParameterNameAnOAuthParameter(key4)) {
delete extra_params[key4];
}
}
// Make the request
this._createClient(url, method, headers, cookies, post_body, callback);
};
/**
* Specify the client options
* @param options
*/
Utils.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;
};
/**
* Get the access token
* @param oauth_token
* @param oauth_token_secret
* @param oauth_verifier
* @param callback
* @param cookies
*/
Utils.OAuth.prototype.getOAuthAccessToken = function (oauth_token, oauth_token_secret, oauth_verifier, callback, cookies) {
var extraParams = {};
if (typeof oauth_verifier == "function") {
callback = oauth_verifier;
} else {
extraParams.oauth_verifier = oauth_verifier;
}
this._clientOptions.accessTokenHttpMethod = "GET";
this._performSecureRequest(oauth_token, oauth_token_secret, this._clientOptions.accessTokenHttpMethod, this._accessUrl, extraParams, null, null, function (error, response, body, serverCookies) {
if (error) {
callback(error);
}
else {
var results = querystring.parse(body),
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, serverCookies);
}
}, cookies);
};
/**
* Deprecated (Get a protected resource)
* @param url
* @param method
* @param oauth_token
* @param oauth_token_secret
* @param callback
*/
Utils.OAuth.prototype.getProtectedResource = function (url, method, oauth_token, oauth_token_secret, callback) {
this._performSecureRequest(oauth_token, oauth_token_secret, method, url, null, "", null, callback);
};
/**
* Run a delete
* @param url
* @param oauth_token
* @param oauth_token_secret
* @param callback
* @returns {*}
*/
Utils.OAuth.prototype.delete = function (url, oauth_token, oauth_token_secret, callback) {
return this._performSecureRequest(oauth_token, oauth_token_secret, "DELETE", url, null, "", null, callback);
};
/**
* Run a get
* @param url
* @param oauth_token
* @param oauth_token_secret
* @param callback
* @returns {*}
*/
Utils.OAuth.prototype.get = function (url, oauth_token, oauth_token_secret, callback) {
return this._performSecureRequest(oauth_token, oauth_token_secret, "GET", url, null, "", null, callback);
};
/**
* Put or post
* @param method
* @param url
* @param oauth_token
* @param oauth_token_secret
* @param post_body
* @param post_content_type
* @param callback
* @returns {*}
* @private
*/
Utils.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;
}
return this._performSecureRequest(oauth_token, oauth_token_secret, method, url, extra_params, post_body, post_content_type, callback);
};
/**
* Put
* @param url
* @param oauth_token
* @param oauth_token_secret
* @param post_body
* @param post_content_type
* @param callback
* @returns {*}
*/
Utils.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);
};
/**
* Post
* @param url
* @param oauth_token
* @param oauth_token_secret
* @param post_body
* @param post_content_type
* @param callback
* @returns {*}
*/
Utils.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.
*
**/
Utils.OAuth.prototype.getOAuthRequestToken = function (extraParams, callback) {
// Validate the callback
if (typeof extraParams == "function") {
callback = extraParams;
extraParams = {};
}
// Callbacks are 1.0A related
if (this._authorize_callback) {
extraParams.oauth_callback = this._authorize_callback;
}
// Perform the request
this._performSecureRequest(null, null, this._clientOptions.requestTokenHttpMethod, this._requestUrl, extraParams, null, null, function (error, response, body, serverCookies) {
if (error) {
callback(error);
} else {
var results = querystring.parse(body);
var oauth_token = results.oauth_token,
oauth_token_secret = results.oauth_token_secret;;
delete results.oauth_token;
delete results.oauth_token_secret;
callback(null, oauth_token, oauth_token_secret, results, serverCookies);
}
});
};
/**
* Properly sign a URL request
* @param url
* @param oauth_token
* @param oauth_token_secret
* @param method
* @returns {string}
*/
Utils.OAuth.prototype.signUrl = function (url, oauth_token, oauth_token_secret, method) {
if (method === undefined) {
method = "GET";
}
var orderedParameters = this._prepareParameters(oauth_token, oauth_token_secret, method, url, {}),
parsedUrl = URL.parse(url, false),
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;
};
/**
* Generate the auth header
* @param url
* @param oauth_token
* @param oauth_token_secret
* @param method
* @returns {*}
*/
Utils.OAuth.prototype.authHeader = function (url, oauth_token, oauth_token_secret, method) {
if (method === undefined) {
method = "GET";
}
var orderedParameters = this._prepareParameters(oauth_token, oauth_token_secret, method, url, {});
return this._buildAuthorizationHeaders(orderedParameters);
};
/**
* Expose the utils to the module exports
* @type {{}}
*/
module.exports = Utils;