js-cfclient
Version:
CF API client NPM
304 lines (285 loc) • 10.1 kB
JavaScript
/**
* Copyright 2017 Jarrett Bariel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Library to connect using OAuth 2 to the CF API.
*
* @see CF API: API Docs @ https://apidocs.cloudfoundry.org
*/
/**
* Node dependencies
*/
const Promise = require('promise');
const ClientOAuth2 = require('client-oauth2');
const https = require('https');
const extend = require('util')._extend
/**
* CF API Version
*
* @see CF API Docs @ https://apidocs.cloudfoundry.org
*/
const API_VERSION = '/v2/';
/**
* Custom exception
*
* @param {String} err
* @param {String} cause
* @returns {object} CFClientException that can be accessed for more information
* properties are:
* <ul>
* <li><b>name</b> name of the error</li>
* <li><b>type</b> 'CFClientException'</li>
* <li><b>message</b> message provided when error was generated</li>
* <li><b>cause</b> deeper cause</li>
* <li><b>fileName</b> 'cfclient.js'</li>
* <li><b>lineNumber</b> line number of exception (if provided)</li>
* <li><b>stack</b> call stack of error</li>
* <li><b>toString()</b> clean print of message + cause</li>
* </ul>
*/
function CFClientException(err, cause) {
var rtn = {
name: 'CFClientException',
type: 'CFClientException',
message: '',
cause: cause,
fileName: 'cfclient.js',
lineNumber: '',
stack: '',
toString: function () {
return this.message + ' ::: ' + this.cause;
}
};
if (typeof err === 'object' && err.name === 'Error') {
rtn.name = err.name;
rtn.message = err.message;
rtn.fileName = err.fileName || 'cfclient.js';
rtn.lineNumber = err.lineNumber;
rtn.stack = err.stack;
}
return rtn;
}
/**
* Constructor for the CFClient.
*
* @param {CFConfig} config - configuration as defined in the README.md
*/
function CFClient(config) {
if (!(config instanceof CFConfig)) {
throw CFClientException(
new Error('Given tokens must be an instance of CFConfig'),
'tokens must be an instance of CFConfig that contains: "protocol", "host", "username", "password", "skipSslValidation');
}
this.config = config;
this.infoData = null;
this.client = null;
}
/**
* Internal call to get the API info from the given host.
*
* @promise fulfill({JSON} infoData)
* @promise reject({CFClientException} err)
*/
CFClient.prototype._getCfApiInfo = function () {
const cf = this;
return new Promise((fulfill, reject) => {
var req = https.request({
host: cf.config.host,
port: cf.config.port,
path: '/v2/info',
method: 'GET',
rejectUnauthorized: !cf.config.skipSslValidation,
headers: {
'Content-Type': 'application/json'
}
}, (res) => {
res.setEncoding('utf-8');
if (200 != res.statusCode) {
reject(CFClientException('Failed with status code: ' + res.statusCode, 'Failed with status code: ' + res.statusCode));
} else {
var respStr = '';
res.on('data', function (data) {
respStr += data;
});
res.on('end', function () {
fulfill(JSON.parse(respStr));
});
}
});
req.end();
req.on('error', (e) => {
reject(CFClientException('Error connecting', e));
});
});
};
/**
* Internal call to get the OAuth2 token for the given credentials.
*
* @promise fulfill({ClientOAuth2Token} client)
* @promise reject({object} err)
*/
CFClient.prototype._getCfOauth2Token = function () {
const cf = this;
return new Promise((fulfill, reject) => {
if (!cf.infoData) {
reject(CFClientException('Info data is not set', 'Need to set info data...'));
} else {
var cfAuth = new ClientOAuth2({
clientId: 'cf',
scopes: [''],
authorizationUri: cf.infoData.authorization_endpoint + '/oauth/auth',
accessTokenUri: cf.infoData.token_endpoint + '/oauth/token'
});
cfAuth.owner.getToken(cf.config.username, cf.config.password, {
options: {
rejectUnauthorized: !cf.config.skipSslValidation
}
}).then(fulfill, reject);
}
});
};
/**
* Connects using the given credentials.
*
* @see #_getCfApiInfo
* @see #_getCfOauth2Token
*
* @promise fulfill() - setup and ready to make requests
* @promise reject({object} err)
*/
CFClient.prototype.connect = function () {
const cf = this;
return new Promise((fulfill, reject) => {
cf._getCfApiInfo().then((infoData) => {
cf.infoData = infoData;
cf._getCfOauth2Token().then((tokenClient) => {
cf.client = tokenClient;
fulfill();
}, reject);
}, reject);
});
};
/**
* Make a request using the given client - will check to make sure the token is valid before the request.
*
* @param {String} uri - URI that follows the API version (e.g. 'organizations' instead of /v2/organizations)
* @param {String} method - optional param that specifies the request method. Defaults to 'GET'
*
* @promise fulfill({JSON} responseBody)
* @promise reject({object} err)
*/
CFClient.prototype.request = function (uri, method) {
const cf = this;
return new Promise((fulfill, reject) => {
if (!cf.client) {
reject(CFClientException('Client is not set', 'Need to set client...'));
} else {
if (cf.client.expired()) {
cf.client.refresh().then(
(refreshedToken) => {
cf.client = refreshedToken;
cf._doRequest(uri, method).then(fulfill, reject);
},
() => {
cf.connect().then(
() => {
cf._doRequest(uri, method).then(fulfill, reject);
}, reject);
});
}
else {
cf._doRequest(uri, method).then(fulfill, reject);
}
}
});
};
/**
* Make a request using the given client - which we assume to be connected and happy
*
* @param {String} uri - URI that follows the API version (e.g. 'organizations' instead of /v2/organizations)
* @param {String} method - optional param that specifies the request method. Defaults to 'GET'
*
* @promise fulfill({JSON} responseBody)
* @promise reject({object} err)
*/
CFClient.prototype._doRequest = function (uri, method) {
const cf = this;
return new Promise((fulfill, reject) => {
cf.client.request({
url: cf.config.protocol + '://' + cf.config.host + API_VERSION + uri,
method: method || 'GET',
options: {
rejectUnauthorized: !cf.config.skipSslValidation
},
headers: {
'Content-Type': 'application/json'
}
}).then((res) => {
if (200 != res.status) {
reject(CFClientException('Failed with status code: ' + res.statusCode, 'Failed with status code: ' + res.statusCode));
} else {
fulfill(JSON.stringify(res.body));
}
}, reject);
});
};
/**
* Object that helps to manage the configuration options.
*
* @param {Object} config values
* properties are:
* <ul>
* <li><b>protocol</b> 'http' or 'https'</li>
* <li><b>host</b> FQDN or IP (e.g. api.mydomain.com)</li>
* <li><b>username</b> username for the CF API</li>
* <li><b>password</b> password for the given username</li>
* <li><b>skipSslValidation</b> enable for self-signed certs</li>
* </ul>
*/
function CFConfig(config) {
const cf = this;
extend(extend(cf, {
protocol: 'http',
host: 'api.bosh-lite.com',
//port : null, // omitted to let it be based on config or protocol
username: 'admin',
password: 'admin',
skipSslValidation: false
}), config);
cf.port = calculatePort(cf.port, cf.protocol);
if (cf.skipSslValidation instanceof String) {
cf.skipSslValidation = ('true' === cf.skipSslValidation);
}
}
/**
* Given a specific port (optionally null) and protocol, will calculate the best return.
*
* @param {Number} currentPort => current port if it exists
* @param {String} protocol => protocol defined
*
* @returns {Number} port number that should be used. This will always return {currentPort} unless it is null. If it is null, it will return '443' unless protocol === 'http'
*/
function calculatePort(currentPort, protocol) {
return ((!currentPort || currentPort === undefined) ? (('http' == protocol) ? 80 : 443) : currentPort);
}
/**
* All exports
*/
module.exports = {
CFClient: CFClient,
CFConfig: CFConfig,
__calculatePort: calculatePort
};