/** UnityBase client. Internally contain instance of {@link http} and provide useful methods for work with UnityBase server.
*
*
var UBConnection = require('UBConnection');
var conn = new UBConnection({host: 'localhost', port: '888', path: 'autotest', keepAlive: true});
// alternative way is:
// var conn = new UBConnection('http://localhost:888/orm');
// but in this case keepAlive is false
conn.onRequestAuthParams = function(){ return {authSchema: 'UB', login: 'admin', password: 'admin'} }
conn.get('getDomainInfo');
* @module UBConnection
*
*/
/*global nsha256*/
var
http = require('http'),
/*jshint -W079 */
//MPV-already in global _ = require('lodash'),
UBSession = require('UBSession'),
CryptoJS = require('CryptoJS/core');
/*jshint +W079 */
CryptoJS.MD5 = require('CryptoJS/md5');
//regular expression for URLs server not require authorization.
var NON_AUTH_URLS_RE = /(\/|^)(models|auth|getAppInfo|downloads)(\/|\?|$)/;
/**
* Create HTTP(s) connection to UnityBase server
* @constructor
* @param {Object} options Connection parameters. See {@link module:http http.request} for details
*/
function UBConnection(options){
var
me = this,
client = http.request(options),
appName,
servicePath,
ubSession = null,
lookupCache = {},
userDataDefault = {lang: 'en'},
appInfo = {};
/**
* Internal instance of HTTP client
* @type {ClientRequest}
* @protected
* @readonly
*/
this.clientRequest = client;
appName = client.options.path;
servicePath = client.options.path;
if (servicePath.charAt(servicePath.length-1) !== '/') servicePath = servicePath + '/'; //normalize path
if (appName.length !== 1){
if (appName.charAt(0) === '/') appName = appName.slice(1, 100); // remove leading / from app name
if (appName.charAt(appName.length-1) === '/') appName = appName.slice(0, appName.length-1); // remove / from end of app name
}
/**
* Root path to all application-level method
* @type {String}
* @readonly
*/
this.servicePath = servicePath;
/**
* Name of UnityBase application
* @type {String}
* @readonly
*/
this.appName = appName;
/**
* Callback for resolving user credential.
* Take a {UBConnection} as a parameter, must return authorization parameters object:
*
* {authSchema: authType, login: login, password: password, [apiKey: ]}
*
* For a internal usage (requests from a locahost or other systems, etc.) and in case authShema == 'UB' it is possible to pass a
* `apiKey` instead of a password. apiKey is actually a `uba_user.uPasswordHashHexa` content
*
* @type {function}
*/
this.onRequestAuthParams = null;
/**
* @deprecated Do not use this property doe to memory overuse - see http://forum.ub.softline.kiev.ua/viewtopic.php?f=12&t=85
*/
appInfo = this.get('getAppInfo'); //non-auth request
/**
* Endpoint name for query (`runList` before 1.12, `ubql` after 1.12
* @type {string}
*/
this.queryMethod = appInfo.serverVersion.startsWith('1.9.') || appInfo.serverVersion.startsWith('1.10.') ? 'runList' : 'ubql';
/**
* Retrieve application information.
* @method
* @returns {Object} result of getAppInfo method
*/
this.getAppInfo = function() {
return appInfo;
};
/** Is server require content encryption
* @type {Boolean}
* @readonly
*/
this.encryptContent = appInfo.encryptContent || false;
/** `base64` encoded server certificate used for cryptographic operation
* @type {Boolean}
* @readonly
*/
this.serverCertificate = appInfo.serverCertificate || '';
/** Lifetime (in second) of session encryption
* @type {Number}
* @readonly
*/
this.sessionKeyLifeTime = appInfo.sessionKeyLifeTime || 0;
/**
* Possible server authentication method
* @type {Array.<string>}
* @readonly
*/
this.authMethods = appInfo.authMethods;
/**
* Is UnityBase server require authorization
* @type {Boolean}
* @readonly
*/
this.authNeed = me.authMethods && (me.authMethods.length > 0);
//noinspection JSUnusedGlobalSymbols
/**
* AdminUI settings
* @type {Object}
*/
this.appConfig = appInfo['adminUI'];
/**
*
* @param {Boolean} isRepeat
* @private
* @return {UBSession}
*/
this.authorize = function(isRepeat){
var
me = this,
authParams, resp, serverNonce, clientNonce, secretWord, pwdHash,
request2 = {};
if (!ubSession || isRepeat){
ubSession = null;
if (!me.onRequestAuthParams){
throw new Error ('set UBConnection.onRequestAuthParams function to perform authorized requests');
}
authParams = me.onRequestAuthParams(me);
if (authParams.authSchema === 'UBIP'){
if (isRepeat) {
throw new Error('UBIP authentication must not return false on the prev.step')
}
resp = me.xhr({endpoint: 'auth', headers: {'Authorization': authParams.authSchema + ' ' + authParams.login}});
ubSession = new UBSession(resp, '', authParams.authSchema);
} else {
resp = me.get('auth', {
AUTHTYPE: authParams.authSchema || 'UB',
userName: authParams.login
});
clientNonce = nsha256(new Date().toISOString().substr(0, 16));
request2.clientNonce = clientNonce;
if (resp.connectionID) {
request2.connectionID = resp.connectionID;
}
request2.AUTHTYPE = authParams.authSchema || 'UB';
request2.userName = authParams.login;
if (resp['realm']) { // LDAP
serverNonce = resp['nonce'];
if (!serverNonce) {
throw new Error('invalid LDAP auth response');
}
if (resp['useSasl']) {
pwdHash = CryptoJS.MD5(authParams.login.split('\\')[1].toUpperCase() + ':' + resp.realm + ':' + authParams.password);
// we must calculate md5(login + ':' + realm + ':' + password) in binary format
pwdHash.concat(CryptoJS.enc.Utf8.parse(':' + serverNonce + ':' + clientNonce));
request2.password = CryptoJS.MD5(pwdHash).toString();
secretWord = request2.password; //todo - must be pwdHash but UB server do not know it :( medium unsecured
} else {
request2.password = btoa(authParams.password);
secretWord = request2.password; //todo - very unsecured!!
}
} else {
serverNonce = resp.result;
if (!serverNonce) {
throw new Error('invalid auth response');
}
if (authParams.apiKey){
pwdHash = authParams.apiKey;
} else {
pwdHash = nsha256('salt' + authParams.password);
}
request2.password = nsha256(appName.toLowerCase() + serverNonce + clientNonce + authParams.login + pwdHash).toString();
secretWord = pwdHash;
}
resp = me.get('auth', request2);
ubSession = new UBSession(resp, secretWord, authParams.authSchema);
}
}
return ubSession;
};
/**
* Check is current connection already perform authentication request
* @returns {boolean}
*/
this.isAuthorized = function(){
return Boolean(ubSession);
};
/**
* Return current user logon or '' in case not logged in
* @returns {String}
*/
this.userLogin = function(){
return this.isAuthorized() ? ubSession.logonname : '';
};
/**
* Return current user language or 'en' in case not logged in
* @returns {String}
*/
this.userLang = function(){
return this.userData('lang');
};
/**
* Return custom data for logged in user, or {lang: 'en'} in case not logged in
*
* If key is provided - return only key part of user data:
*
* $App.connection.userData('lang');
* // or the same but dedicated alias
* $App.connection.userLang()
*
* @param {String} [key] Optional key
* @returns {*}
*/
this.userData = function(key){
var uData = this.isAuthorized() ? ubSession.userData : userDataDefault;
return key ? uData[key] : uData;
};
/**
* Lookup value in entity using aCondition.
*
* // create condition using UB.Repository
* var myID = conn.lookup('ubm_enum', 'ID',
* UB.Repository('ubm_enum').where('eGroup', '=', 'UBA_RULETYPE').where('code', '=', 'A').ubRequest().whereList
* );
* // or pass condition directly
* var adminID = conn.lookup('uba_user', 'ID', {
* expression: 'name', condition: 'equal', values: {nameVal: 'admin'}
* });
*
* @param {String} aEntity - entity to lookup
* @param {String} lookupAttribute - attribute to lookup
* @param {String|Object} aCondition - lookup condition. String in case of custom expression,
* or whereListItem {expression: condition: values: },
* or whereList {condition1: {expression: condition: values: }, condition2: {}, ....}
* @param {Boolean} [doNotUseCache=false]
* @return {*} `lookupAttribute` value of first result row or null if not found.
*/
this.lookup = function(aEntity, lookupAttribute, aCondition, doNotUseCache) {
var
me = this,
cKey = aEntity + JSON.stringify(aCondition) + lookupAttribute,
resData, request;
if (!doNotUseCache && lookupCache.hasOwnProperty(cKey)) {
return lookupCache[cKey]; // found in cache
} else {
request = {
entity: aEntity,
method: 'select',
fieldList: [lookupAttribute],
__nativeDatasetFormat: true,
options: {
start: 0,
limit: 1
}
};
if (typeof aCondition === 'string'){
request.whereList = {lookup: {expression: aCondition, condition: 'custom'}};
} else if (aCondition.expression && (typeof aCondition.expression === 'string')){
request.whereList = {lookup: aCondition};
} else {
request.whereList = aCondition;
}
resData = me.query(request).resultData;
if ((resData.length === 1) && (resData[0][lookupAttribute] != null)) { // `!= null` is equal to (not null && not undefined)
if (!doNotUseCache) {
lookupCache[cKey] = resData[0][lookupAttribute];
}
return resData[0][lookupAttribute];
} else {
return null;
}
}
};
}
/**
* Perform authorized UBQL request.
* Can take one QB Query or an array of UB Query and execute it at once.
* @param {UBQ|Array<UBQ>} ubq
* @returns {Object|Array}
*/
UBConnection.prototype.query = function(ubq){
if (Array.isArray(ubq)) {
return this.xhr({endpoint: this.queryMethod, data: ubq});
} else {
return this.xhr({endpoint: this.queryMethod, data: [ubq]})[0];
}
};
/**
* HTTP request to UB server. In case of success response return body parsed to {Object} or {ArrayBuffer} depending of Content-Type response header
*
* @example
* conn.xhr({
* endpoint: 'runSQL',
* URLParams: {CONNECTION: 'dba'},
* data: 'DROP SCHEMA IF EXISTS ub_autotest CASCADE; DROP USER IF EXISTS ub_autotest;'
* });
*
* @protected
* @param {Object} options
* @param {String} options.endpoint
* @param {String} [options.UBMethod] This parameter is **DEPRECATED**. Use `options.endpoint` instead
* @param {String} [options.HTTPMethod='POST']
* @param {Object} [options.headers] Optional request headers in format {headerName: headerValue, ..}
* @param {Boolean} [options.simpleTextResult=false] do not parse response and return it as is even if response content type is JSON
* @param {*} [options.URLParams] Optional parameters added to URL using http.buildURL
* @param {ArrayBuffer|Object|String} [options.data] Optional body
* @param {String} [options.responseType] see <a href="https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType">responseType</a>.
* Currently only `arraybuffer` suported.
* @returns {ArrayBuffer|Object|String}
*/
UBConnection.prototype.xhr = function(options){
var
me=this,
req = this.clientRequest, resp, result = {};
var path = this.servicePath + (options.endpoint || options.UBMethod);
if (options.URLParams){
path = http.buildURL(path, options.URLParams);
}
if (me.authNeed && !NON_AUTH_URLS_RE.test(path) ) { //request need authentication
var session = me.authorize(me._inRelogin);
req.setHeader('Authorization', session.authHeader());
}
req.setMethod(options.HTTPMethod || 'POST'); // must be after auth request!
req.setPath(path);
if (options.headers){
_.forEach(options.headers, function(val, key){
req.setHeader(key, val);
})
}
if (options.data){
if (options.data.toString() === '[object ArrayBuffer]'){
req.setHeader('Content-Type', 'application/octet-stream');
} else {
req.setHeader('Content-Type', 'application/json;charset=utf-8');
}
resp = req.end(options.data);
} else {
resp = req.end();
}
var status = resp.statusCode;
if (200 <= status && status < 300){
if (options.responseType === 'arraybuffer'){
result = resp.read('bin');
} else if ( ((resp.headers('content-type') || '').indexOf('json') >= 0) && !options.simpleTextResult) {
result = JSON.parse(resp.read());
} else {
result = resp.read(); // return string readed as UTF-8
}
} else if (status === 401 && me.isAuthorized()) { // relogin
if (me._inRelogin){
me._inRelogin = false;
throw new Error('invalid user name or password');
}
me._inRelogin = true;
console.debug('Session expire - repeat auth request');
try {
result = me.xhr(options);
} finally {
me._inRelogin = false;
}
} else {
if ((status === 500) && ((resp.headers('content-type') || '').indexOf('json') >= 0)) { // server report error and body is JSON
var respObj = JSON.parse(resp.read());
if (respObj.errMsg){
throw new Error('Server error: "' + respObj.errMsg);
} else {
throw new Error('HTTP communication error: "' + status + ': ' + http.STATUS_CODES[status] + '" during request to: ' + path);
}
} else {
throw new Error('HTTP communication error: "' + status + ': ' + http.STATUS_CODES[status] + '" during request to: ' + path);
}
}
return result;
};
/**
* Perform get request to `endpoint` with optional URLParams.
* @param {String} endpoint
* @param {*} [URLParams]
* @returns {ArrayBuffer|Object|String}
*/
UBConnection.prototype.get = function(endpoint, URLParams){
var params = {
endpoint: endpoint, HTTPMethod: 'GET'
};
if (URLParams) { params.URLParams = URLParams; }
return this.xhr(params);
};
/**
* Shortcut method to perform authorized `POST` request to application we connected
* @param {String} endpoint
* @param {ArrayBuffer|Object|String} data
* @returns {ArrayBuffer|Object|String}
*/
UBConnection.prototype.post = function(endpoint, data){
return this.xhr({endpoint: endpoint, data: data});
};
/**
* Shortcut method to perform authorized `POST` request to `ubql` endpoint
* @deprecated Since UB 1.11 use UBConnection.query
* @param {Array.<ubRequest>} runListData
* @returns {Object}
*/
UBConnection.prototype.runList = function(runListData){
return this.xhr({endpoint: this.queryMethod, data: runListData});
};
/**
* Send request to any endpoint. For entity-level method execution (`ubql` endpoint) better to use {@link UBConnection.query}
* @returns {*} body of HTTP request result. If !simpleTextResult and response type is json - then parsed to object
*/
UBConnection.prototype.runCustom = function(endpoint, aBody, aURLParams, simpleTextResult, aHTTPMethod){
return this.xhr({HTTPMethod: aHTTPMethod ? aHTTPMethod : 'POST', endpoint: endpoint, data: aBody, simpleTextResult: simpleTextResult});
//throw new Error ('Use one of runList/run/post/xhr UBConnection methods');
};
/**
* Shortcut method to perform authorized `POST` request to `ubql` endpoint.
* Can take one ubRequest and wrap it to array
* @deprecated Since UB 1.11 use UBConnection.query
* @param {ubRequest} request
* @returns {Object}
*/
UBConnection.prototype.run = function(request){
return this.xhr({endpoint: this.queryMethod, data: [request]})[0];
};
/**
* Retrieve application domain information.
* @method getDomainInfo
* @return {Object}
*/
UBConnection.prototype.getDomainInfo = function(){
return this.get('getDomainInfo');
};
/**
* Logout from server if logged in
*/
UBConnection.prototype.logout = function(){
if (this.isAuthorized()) {
try {
this.post('logout', '');
} catch (e) {
}
}
};
/**
* Execute insert method by add method: 'insert' to `ubq` query (if req.method not already set)
*
* If `ubq.fieldList` contain only `ID` return inserted ID, else return array of attribute values passed to `fieldList`.
* If no field list passed at all - return response.resultData (null usually).
*
var testRole = conn.insert({
entity: 'uba_role',
fieldList: ['ID', 'mi_modifyDate'],
execParams: {
name: 'testRole1',
allowedAppMethods: 'runList'
}
});
console.log(testRole); //[3000000000200,"2014-10-21T11:56:37Z"]
var testRoleID = conn.insert({
entity: 'uba_role',
fieldList: ['ID'],
execParams: {
name: 'testRole1',
allowedAppMethods: 'runList'
}
});
console.log(testRoleID); //3000000000200
*
* @param {ubRequest} ubq
* @return {*}
*/
UBConnection.prototype.insert = function(ubq){
var req = _.clone(ubq, true);
req.method = req.method || 'insert';
var res = this.query(req);
if (req.fieldList) {
return (req.fieldList.length === 1) && (req.fieldList[0] = 'ID') ? res.resultData.data[0][0] : res.resultData.data[0];
} else {
return res.resultData;
}
};
module.exports = UBConnection;