UNPKG

acs-apiclient

Version:

Node helper library for connecting to the Answers Cloud Services (ForeSee in particular) web API in a headless manner.

733 lines (676 loc) 24.3 kB
var OAuth = require('./lib/oauth.js').OAuth, quickhttp = require('./lib/quickhttp.js'), extend = require('extend'), acsutils = require('./lib/acsutils.js'), datecriteria = require('./lib/datecriteria.js'); /** * Base options * @type {{root: string, request_token: string, access_token: string, authorization: string, login: string, consumer_key: string, consumer_secret: string, consumer_type: string, username: string, password: string, oauth_token: string, oauth_token_secret: string, oauth_verifier: string, oauth_access_token: string, oauth_access_token_secret: string, jsession_id: null}} * @private */ var _baseOpts = { service_root: 'https://portal2.foreseeresults.com', request_token: '/services/oauth/request_token', access_token: '/services/oauth/access_token', authorization: '/services/oauth/user_authorization', login: '/services/login', consumer_key: acsutils._provideSymbol, consumer_secret: acsutils._provideSymbol, consumer_type: acsutils._provideSymbol, username: acsutils._provideSymbol, password: acsutils._provideSymbol, oauth_token: acsutils._provideSymbol, oauth_token_secret: acsutils._provideSymbol, oauth_verifier: acsutils._provideSymbol, oauth_access_token: acsutils._provideSymbol, oauth_access_token_secret: acsutils._provideSymbol, jsession_id: null, debug: false }; /** * Headlessly handles API authentication and API requests * @param options Object Configuration options. Must contain consumer_key, consumer_secret, consumer_type at a minimum, and either username and password OR oauth_access_token and oauth_access_token_secret. * @param errorcallback Function Will be called whenever there is an error. * @constructor */ var ACSClient = function (options, errorcallback) { /** * Holds all the options */ this.opts = extend({}, _baseOpts, options || {}); /** * The error handler * @private */ this._errorcallback = errorcallback || function () { // no-op }; // Do some validation var def = acsutils.isDefined, info; if (!def(this.opts.service_root)) { info = {msg: "Invalid service_root.", code: "MISSINGINFO"}; if (this.opts.debug) { console.log(info) } this._errorcallback(info); } if (!def(this.opts.consumer_key)) { info = {msg: "Provide your ACS consumer key.", code: "MISSINGINFO"}; if (this.opts.debug) { console.log(info) } this._errorcallback(info); } if (!def(this.opts.consumer_secret)) { info = {msg: "Provide your ACS consumer secret.", code: "MISSINGINFO"}; if (this.opts.debug) { console.log(info) } this._errorcallback(info); } if (!def(this.opts.consumer_type)) { info = {msg: "Provide your ACS consumer type.", code: "MISSINGINFO"}; if (this.opts.debug) { console.log(info) } this._errorcallback(info); } if (!(def(this.opts.username) && def(this.opts.password)) && !(def(this.opts.oauth_access_token) && def(this.opts.oauth_access_token_secret))) { info = { msg: "You need to either provide username and password or oauth_access_token and oauth_access_token_secret in order to connect.", code: "MISSINGINFO" }; if (this.opts.debug) { console.log(info) } this._errorcallback(info); } }; /** * Re-log in * @param callback * @private */ ACSClient.prototype._resetRetryAuthenticationState = function (callback) { // Try logging in again ONCE this.opts.oauth_token_secret = acsutils._provideSymbol; this.opts.oauth_verifier = acsutils._provideSymbol; this.opts.oauth_access_token = acsutils._provideSymbol; this.opts.oauth_access_token_secret = acsutils._provideSymbol; this.opts.jsession_id = null; this._oa = null; this._verifyAuthenticationState(callback); }; /** * Overwrite any old cookies with the new ones */ ACSClient.prototype._reconcileCookies = function (oldcookies, newcookies) { oldcookies = oldcookies || []; newcookies = newcookies || []; function getCookieName(ck) { ck = (ck || "").toString(); return ck.split('=')[0]; } var finalcookies = []; for (var i = 0; i < oldcookies.length; i++) { var finalcookie = oldcookies[i]; var cname = getCookieName(oldcookies[i]); for (var j = 0; j < newcookies.length; j++) { var nname = getCookieName(newcookies[j]); if (cname == nname) { finalcookie = newcookies.splice(j, 1).toString(); } } finalcookies.push(finalcookie); } for (var k = 0; k < newcookies.length; k++) { finalcookies.push(newcookies[k]); } return finalcookies; }; /** * Remove a cookie value * @param cookieset * @param cookiename * @private */ ACSClient.prototype._removeCookieFromSet = function (cookieset, cookiename) { function getCookieName(ck) { ck = (ck || "").toString(); return ck.split('=')[0]; } for (var i = 0; i < cookieset.length; i++) { var cn = getCookieName(cookieset[i]); if (cn.toLowerCase() == cookiename.toLowerCase()) { cookieset.splice(i, 1); } } return cookieset; }; /** * Add a cookie * @param cookieset * @param cookiename * @private */ ACSClient.prototype._addCookieToSet = function (cookieset, cookiename, value) { this._removeCookieFromSet(cookieset, cookiename); cookieset.push(cookiename + "=" + value); return cookieset; }; /** * Get the cookie value * @param cookieset * @param cookiename * @private */ ACSClient.prototype._getCookieValue = function (cookieset, cookiename) { function getCookieName(ck) { ck = (ck || "").toString(); return ck.split('=')[0]; } var finalval = ""; for (var i = 0; i < cookieset.length; i++) { var cn = getCookieName(cookieset[i]); if (cn.toLowerCase() == cookiename.toLowerCase()) { finalval = cookieset[i].toString().substr(cookieset[i].toString().indexOf('=') + 1); finalval = finalval.substr(0, finalval.indexOf(';')); break; } } return finalval; }; /** * Ensure we are authenticated * @param callback Function The success / failure callback * @private */ ACSClient.prototype._verifyAuthenticationState = function (callback) { callback = callback || function () { // no-op }; // Quickreference isDefined() var def = acsutils.isDefined, opts = this.opts, ctx = this; // We need to log in if (!this.oa) { /** * Our main oAuth instance * @type {OAuth} * @private */ this._oa = new OAuth(opts.service_root + opts.request_token, opts.service_root + opts.access_token, opts.consumer_key, opts.consumer_secret, "1.0", "http://localhost", "HMAC-SHA1"); } if (!(def(this.opts.oauth_access_token) && def(this.opts.oauth_access_token_secret))) { // Get the request token this._oa.getOAuthRequestToken(function (error, oauth_token, oauth_token_secret, results, serverCookies) { if (error) { var errorinfo = { msg: "There was an issue getting the request token: " + JSON.stringify(error), code: "INVALIDREQUESTTOKEN" }; if (opts.debug) { console.log(errorinfo); } ctx._errorcallback(errorinfo); callback(errorinfo); } else { // All good opts.oauth_token = oauth_token; opts.oauth_token_secret = oauth_token_secret; quickhttp.post(opts.service_root, opts.login, 443, "j_username=" + encodeURIComponent(opts.username) + "&j_password=" + encodeURIComponent(opts.password), function (code, response, body) { serverCookies = ctx._reconcileCookies(serverCookies, response.headers['set-cookie']); var jsessionid = ctx._getCookieValue(serverCookies, "jsessionid"); var redirectLocation = response.caseless.dict.location.toString(); if (code != 302 || !jsessionid || jsessionid.length < 2) { var errorinfo2 = { msg: "There was an issue logging in the user. Did not get the anticipated response from the server. Response code " + code, code: "COULDNOTLOGIN" }; if (opts.debug) { console.log(errorinfo2); } ctx._errorcallback(errorinfo2); callback(errorinfo2); } else if (redirectLocation.indexOf('#loginfailed') > -1) { var errorinfo3 = {msg: "Credentials were invalid.", code: "INVALIDCREDENTIALS"}; if (opts.debug) { console.log(errorinfo3); } ctx._errorcallback(errorinfo3); callback(errorinfo3); } else { opts.jsession_id = jsessionid; // Add the consumer type cookie ctx._addCookieToSet(serverCookies, "CONSUMER_TYPE", opts.consumer_type); // Authorize quickhttp.get(opts.service_root, opts.authorization + "?oauth_token=" + encodeURIComponent(opts.oauth_token), 443, function (code, response, body) { // Integrate new cookies serverCookies = ctx._reconcileCookies(serverCookies, response.headers['set-cookie']); if (code != 302) { var errorinfo4 = { msg: "Could not authorize the oauth_token. Server responded with " + code, code: "COULDNOTAUTHTOKEN" }; if (opts.debug) { console.log(errorinfo4); } ctx._errorcallback(errorinfo4); callback(errorinfo4); } else { try { var path = response.headers.location.toString().split('?')[1], parts = path.split('&'), verifier = ""; for (var p = 0; p < parts.length; p++) { var pbits = parts[p].split('='); if (pbits[0] == 'oauth_verifier') { verifier = pbits[1]; break; } } } catch (e) { var errorinfo92 = { msg: "REST API header format incorrect. Could not authenticate user. Location was " + response.headers.location.toString(), code: "RESTAPIAUTHHEADER" }; if (opts.debug) { console.log(errorinfo92); } ctx._errorcallback(errorinfo92); callback(errorinfo92); return; } if (verifier.length < 2) { var errorinfo5 = { msg: "Could not find oAuth verifier. Server response location was " + path, code: "COULDNOTFINDVERIFIER" }; if (opts.debug) { console.log(errorinfo5); } ctx._errorcallback(errorinfo5); callback(errorinfo5); } else { opts.verifier = verifier; // Get the access token ctx._oa.getOAuthAccessToken(opts.oauth_token, opts.oauth_token_secret, opts.verifier, function (error, oauth_access_token, oauth_access_token_secret, results2) { if (error) { var errorinfo6 = { msg: "Error getting the access token: " + JSON.stringify(error), code: "COULDNOTGETACCESSTOKEN" }; if (opts.debug) { console.log(errorinfo6); } ctx._errorcallback(errorinfo6); callback(errorinfo6); } else { if (!def(oauth_access_token) || !def(oauth_access_token_secret)) { var errorinfo7 = { msg: "Error getting the access token since they were null.", code: "COULDNOTGETACCESSTOKENNULL" }; if (opts.debug) { console.log(errorinfo7); } ctx._errorcallback(errorinfo7); callback(errorinfo7); } else { // Assign the all importent access and token secrets opts.oauth_access_token = oauth_access_token; opts.oauth_access_token_secret = oauth_access_token_secret; // Make the callback to user code callback(); } } }, serverCookies); } } }, serverCookies, function (error) { var errorinfo62 = { msg: "Error performing HTTP Get: " + JSON.stringify(error), code: "Error" }; if (opts.debug) { console.log(errorinfo62); } ctx._errorcallback(errorinfo62); callback(errorinfo62); }); } }, serverCookies, function (error) { var errorinfo623 = { msg: "Error performing HTTP Post: " + JSON.stringify(error), code: "Error" }; if (opts.debug) { console.log(errorinfo623); } ctx._errorcallback(errorinfo623); callback(errorinfo623); }); } }); } else { callback(); } }; /** * Perform an oAuth request, taking into consideration the differences in PUT, GET, POST, DELETE * @param path * @param method * @param data * @param callback * @param queued * @private */ ACSClient.prototype._performReasonedRequest = function (path, method, data, callback, queued) { method = method.toUpperCase().trim(); callback = callback || function () { // no-op }; data = data || {}; if (queued || (method !== "POST" && method !== "PUT")) { var qstrver = "?", cnt = 0; for (var item in data) { if (cnt > 0) { qstrver += "&"; } if (item == 'criteria' || item == 'dateRange') { qstrver += encodeURIComponent(item) + "=" + encodeURIComponent(JSON.stringify(data[item])); } else { qstrver += encodeURIComponent(item) + "=" + encodeURIComponent(data[item]); } cnt++; } if (cnt == 0) { qstrver = ""; } } var startTime = (new Date()).getTime(); var callback2 = callback; switch (method) { case "GET": if (this.opts.debug) { callback2 = function (st, met, url) { return function() { var duration = (new Date()).getTime() - st; console.log("Complete: ", duration, met, url); callback.apply(this, arguments); }; }(startTime, "GET", this.opts.service_root + "/services/" + path + qstrver); } this._oa.get(this.opts.service_root + "/services/" + path + qstrver, this.opts.oauth_access_token, this.opts.oauth_access_token_secret, callback2); break; case "DELETE": if (this.opts.debug) { callback2 = function (st, met, url) { return function() { var duration = (new Date()).getTime() - st; console.log("Complete: ", duration, met, url); callback.apply(this, arguments); }; }(startTime, "DELETE", this.opts.service_root + "/services/" + path + qstrver); } this._oa.delete(this.opts.service_root + "/services/" + path + qstrver, this.opts.oauth_access_token, this.opts.oauth_access_token_secret, callback2); break; case "PUT": if (this.opts.debug) { callback2 = function (st, met, url) { return function() { var duration = (new Date()).getTime() - st; console.log("Complete: ", duration, met, url); callback.apply(this, arguments); }; }(startTime, "PUT", this.opts.service_root + "/services/" + path); } this._oa.put(this.opts.service_root + "/services/" + path, this.opts.oauth_access_token, this.opts.oauth_access_token_secret, data, "application/json", callback2); break; case "POST": if (queued) { if (this.opts.debug) { callback2 = function (st, met, url) { return function() { var duration = (new Date()).getTime() - st; console.log("Complete: ", duration, met, url); callback.apply(this, arguments); }; }(startTime, "POST", this.opts.service_root + "/services/" + path + qstrver); } this._oa.post(this.opts.service_root + "/services/" + path + qstrver, this.opts.oauth_access_token, this.opts.oauth_access_token_secret, {}, "application/json", callback2); } else { if (this.opts.debug) { callback2 = function (st, met, url) { return function() { var duration = (new Date()).getTime() - st; console.log("Complete: ", duration, met, url); callback.apply(this, arguments); }; }(startTime, "POST", this.opts.service_root + "/services/" + path); } this._oa.post(this.opts.service_root + "/services/" + path, this.opts.oauth_access_token, this.opts.oauth_access_token_secret, data, "application/json", callback2); } break; } }; /** * Ensure that you are authenticated. Use this sparingly. * @param callback */ ACSClient.prototype.authenticate = function (callback) { callback = callback || function () { // no-op }; var ctx = this; this._verifyAuthenticationState(function (error) { if (error) { // call the callback callback(error, (!error)); } else { // Call the current user endpoint ctx.callResource("currentUser", "GET", {}, function (error) { // call the callback callback(error, (!error)); }); } }); }; /** * Call a protected resource * @param path String The resource. Eg: "currentUser" * @param method String "GET", "PUT", "POST", "DELETE" * @param data Object * @param callback Function Will be called on success or failure */ ACSClient.prototype.callResource = function (path, method, data, callback) { if (typeof method == 'function') { callback = method; method = "GET"; data = {}; } if (typeof data == 'function') { callback = data; data = {}; } method = (method || "GET").toUpperCase().trim(); data = data || {}; callback = callback || function () { // no-op }; var ctx = this; path = path.replace('\\', '/'); // Strip leading slashes if (path.charAt(0) == '/') { path = path.substr(1); } this._verifyAuthenticationState(function (error) { // Great! Was there an error? if (error) { callback(error); } else { ctx._performReasonedRequest(path, method, data, function (error, response, body, serverCookies) { if (response && response.statusCode == 401) { // Try logging in again ctx._resetRetryAuthenticationState(function (error) { if (error) { callback(error); } else { ctx._performReasonedRequest(path, method, data, function (error, response, body, serverCookies) { if (error) { var errorinfo = { msg: "There was an error retrieving protected resource \"" + path + "\" using \"" + method + "\"", code: "ERRORCONTACTINGRESOURCE" }; ctx._errorcallback(errorinfo); callback(errorinfo); } else { var dtaobj = JSON.parse(body); if (dtaobj.errorCode) { callback({ msg: dtaobj.message, code: dtaobj.errorCode.toString() }, null); } else { callback(null, dtaobj); } } }); } }); } else if (response && response.statusCode == 404) { var errorinfo = { msg: "There was an error retrieving protected resource \"" + path + "\" using \"" + method + "\"", code: "ERRORCONTACTINGRESOURCE", body: body }; if (ctx.opts.debug) { console.log(errorinfo); } ctx._errorcallback(errorinfo); callback(errorinfo); } else if (error) { var errorinfo = { msg: "There was an error retrieving protected resource \"" + path + "\" using \"" + method + "\"", code: "ERRORCONTACTINGRESOURCE", body: body }; if (ctx.opts.debug) { console.log(errorinfo); } ctx._errorcallback(errorinfo); callback(errorinfo); } else { try { var dtaobj = JSON.parse(body); if (dtaobj.errorCode) { callback({ msg: dtaobj.message, code: dtaobj.errorCode.toString(), body: body }, null); } else { callback(null, dtaobj); } } catch (e) { if (ctx.opts.debug) { console.log({ msg: "Could not parse the response: " + response.statusCode, code: 100, body: body }); } callback({ msg: "Could not parse the response: " + response.statusCode, code: 100, body: body }, null); } } }); } }); }; /** * Call a protected, queued resource. These resources start with a POST to initiate the request, and then GETs to check * the status of the request and eventually get the results. * @param path String The resource. Eg: "currentUser" * @param data Object to be used for request options * @param callback Function Will be called on success or failure once queued request has completed * @param id Number If present this is a nested call to see if previously requested report has completed */ ACSClient.prototype.callQueuedResource = function (path, data, callback, id) { var method = id ? "GET" : "POST"; var queued = id ? false : true; var ctx = this; if (typeof data == 'function') { callback = data; data = {}; } data = data || {}; callback = callback || function () { // no-op }; path = path.replace('\\', '/'); // Strip leading slashes if (path.charAt(0) == '/') { path = path.substr(1); } this._verifyAuthenticationState(function (error) { // Great! Was there an error? if (error) { callback(error); } else { ctx._performReasonedRequest(path, method, data, function (error, response, body, serverCookies) { var dtaobj = body ? JSON.parse(body) : {}; var id = dtaobj.reportId; var respStatus = response && response.statusCode; var reportStatus = dtaobj.reportStatus; var wait = reportStatus === "GENERATED" ? 0 : 1000; if (error) { var errorinfo = { msg: "There was an error retrieving protected resource \"" + path + "\" using \"" + method + "\"", code: "ERRORCONTACTINGRESOURCE" }; ctx._errorcallback(errorinfo); callback(errorinfo); } else if (respStatus === 201 || respStatus === 202) { setTimeout((function () { this.callQueuedResource(path, {reportId: id}, callback, id); }).bind(ctx), wait); } else { if (dtaobj.errorCode) { callback({ msg: dtaobj.message, code: dtaobj.errorCode.toString() }, null); } else { callback(null, dtaobj); } } }, queued); } }); }; /** * Constructs a properly formatted date object * @param clientId Number The client ID. Needed for some dates. * @returns {*|exports} */ ACSClient.prototype.constructDateObject = function (clientId) { return datecriteria.apply(this, arguments); }; /** * Expose the API to the world * @type {Function} */ module.exports = ACSClient;