654 lines
20 KiB
JavaScript
654 lines
20 KiB
JavaScript
var http = require('http'),
|
|
https = require('https'),
|
|
parseString = require('xml2js').parseString,
|
|
urlParser = require('url'),
|
|
util = require("util"),
|
|
events = require("events"),
|
|
zlib = require("zlib"),
|
|
node_debug = require("debug")("NRC");
|
|
|
|
exports.Client = function (options){
|
|
var self = this;
|
|
|
|
self.options = options || {},
|
|
self.useProxy = (self.options.proxy || false)?true:false,
|
|
self.useProxyTunnel = (!self.useProxy || self.options.proxy.tunnel===undefined)?false:self.options.proxy.tunnel,
|
|
self.proxy = self.options.proxy,
|
|
self.connection = self.options.connection || {},
|
|
self.mimetypes = self.options.mimetypes || {};
|
|
self.requestConfig = self.options.requestConfig || {};
|
|
self.responseConfig = self.options.responseConfig || {};
|
|
|
|
this.methods={};
|
|
|
|
|
|
// Client Request to be passed to ConnectManager and returned
|
|
// for each REST method invocation
|
|
var ClientRequest =function(){
|
|
events.EventEmitter.call(this);
|
|
};
|
|
|
|
|
|
util.inherits(ClientRequest, events.EventEmitter);
|
|
|
|
|
|
ClientRequest.prototype.end = function(){
|
|
if(this._httpRequest) {
|
|
this._httpRequest.end();
|
|
}
|
|
};
|
|
|
|
ClientRequest.prototype.setHttpRequest=function(req){
|
|
this._httpRequest = req;
|
|
};
|
|
|
|
var Util = {
|
|
|
|
createProxyPath:function(url){
|
|
var result = url.host;
|
|
// check url protocol to set path in request options
|
|
if (url.protocol === "https:"){
|
|
// port is set, leave it, otherwise use default https 443
|
|
result = (url.host.indexOf(":") == -1?url.hostname + ":443":url.host);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
createProxyHeaders:function(url){
|
|
var result ={};
|
|
// if proxy requires authentication, create Proxy-Authorization headers
|
|
if (self.proxy.user && self.proxy.password){
|
|
result["Proxy-Authorization"] = "Basic " + new Buffer([self.proxy.user,self.proxy.password].join(":")).toString("base64");
|
|
}
|
|
// no tunnel proxy connection, we add the host to the headers
|
|
if(!self.useProxyTunnel)
|
|
result["host"] = url.host;
|
|
|
|
return result;
|
|
},
|
|
createConnectOptions:function(connectURL, connectMethod){
|
|
debug("connect URL = ", connectURL);
|
|
var url = urlParser.parse(connectURL),
|
|
path,
|
|
result={},
|
|
protocol = url.protocol.indexOf(":") == -1?url.protocol:url.protocol.substring(0,url.protocol.indexOf(":")),
|
|
defaultPort = protocol === 'http'?80:443;
|
|
|
|
result ={
|
|
host: url.host.indexOf(":") == -1?url.host:url.host.substring(0,url.host.indexOf(":")),
|
|
port: url.port === undefined?defaultPort:url.port,
|
|
path: url.path,
|
|
protocol:protocol
|
|
};
|
|
|
|
if (self.useProxy) result.agent = false; // cannot use default agent in proxy mode
|
|
|
|
if (self.options.user && self.options.password){
|
|
result.auth = [self.options.user,self.options.password].join(":");
|
|
|
|
} else if (self.options.user && !self.options.password){
|
|
// some sites only needs user with no password to authenticate
|
|
result.auth = self.options.user;
|
|
}
|
|
|
|
|
|
// configure proxy connection to establish a tunnel
|
|
if (self.useProxy){
|
|
|
|
result.proxy ={
|
|
host: self.proxy.host,
|
|
port: self.proxy.port,
|
|
method: self.useProxyTunnel?'CONNECT':connectMethod,//if proxy tunnel use 'CONNECT' method, else get method from request,
|
|
path: self.useProxyTunnel?this.createProxyPath(url):connectURL, // if proxy tunnel set proxy path else get request path,
|
|
headers: this.createProxyHeaders(url) // createProxyHeaders add correct headers depending of proxy connection type
|
|
};
|
|
}
|
|
|
|
if(self.connection && typeof self.connection === 'object'){
|
|
for(var option in self.connection){
|
|
result[option] = self.connection[option];
|
|
}
|
|
}
|
|
|
|
// don't use tunnel to connect to proxy, direct request
|
|
// and delete proxy options
|
|
if (!self.useProxyTunnel){
|
|
for (option in result.proxy){
|
|
result[option] = result.proxy[option];
|
|
}
|
|
|
|
delete result.proxy;
|
|
}
|
|
|
|
// add general request and response config to connect options
|
|
|
|
result.requestConfig = self.requestConfig;
|
|
result.responseConfig = self.responseConfig;
|
|
|
|
|
|
return result;
|
|
},
|
|
decodeQueryFromURL: function(connectURL){
|
|
var url = urlParser.parse(connectURL),
|
|
query = url.query.substring(1).split("&"),
|
|
keyValue,
|
|
result={};
|
|
|
|
// create decoded args from key value elements in query+
|
|
for (var i=0;i<query.length;i++){
|
|
keyValue = query[i].split("=");
|
|
result[keyValue[0]] = decodeURIComponent(keyValue[1]);
|
|
}
|
|
|
|
return result;
|
|
|
|
},
|
|
encodeQueryFromArgs: function(args){
|
|
var result="?", counter = 1;
|
|
// create enconded URL from args
|
|
for (var key in args) {
|
|
var keyValue = "";
|
|
if ( args[key] instanceof Array ) {
|
|
/*
|
|
* We are dealing with an array in the query string ?key=Value0&key=Value1
|
|
* That a REST application translates into key=[Value0, Value1]
|
|
*/
|
|
for ( var ii=0, sizeArray = args[key].length; ii < sizeArray; ii++ ) {
|
|
result = result.concat((counter > 1 ? "&": "") + key + "=" + encodeURIComponent(args[key][ii]));
|
|
counter++;
|
|
}
|
|
} else { //No array, just a single &key=value
|
|
keyValue = key + "=" + encodeURIComponent(args[key]);
|
|
result = result.concat((counter > 1 ? "&":"") + keyValue);
|
|
}
|
|
|
|
counter++;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
parsePathParameters:function(args,url){
|
|
var result = url;
|
|
if (!args || !args.path) return url;
|
|
|
|
for (var placeholder in args.path){
|
|
var regex = new RegExp("\\$\\{" + placeholder + "\\}","i");
|
|
result = result.replace(regex,args.path[placeholder]);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
},
|
|
overrideClientConfig:function(connectOptions,methodOptions){
|
|
function validateReqResOptions(reqResOption){
|
|
return (reqResOption && typeof reqResOption === 'object');
|
|
}
|
|
// check if we have particular request or response config set on this method invocation
|
|
// and override general request/response config
|
|
if (validateReqResOptions(methodOptions.requestConfig)){
|
|
util._extend(connectOptions.requestConfig,methodOptions.requestConfig);
|
|
}
|
|
|
|
if (validateReqResOptions(methodOptions.responseConfig)){
|
|
util._extend(connectOptions.responseConfig,methodOptions.responseConfig);
|
|
}
|
|
|
|
|
|
},
|
|
connect : function(method, url, args, callback, clientRequest){
|
|
// configure connect options based on url parameter parse
|
|
var options = this.createConnectOptions(this.parsePathParameters(args,url), method);
|
|
debug("options pre connect",options);
|
|
options.method = method,
|
|
options.clientRequest = clientRequest,
|
|
options.headers= options.headers || {};
|
|
|
|
debug("args = ", args);
|
|
debug("args.data = ", args !== undefined?args.data:undefined);
|
|
// no args passed
|
|
if (typeof args === 'function'){
|
|
callback = args;
|
|
//add Content-length to POST/PUT/DELETE/PATCH methods
|
|
if (method === 'POST' || method === 'PUT' || method === 'DELETE' || method === 'PATCH'){
|
|
options.headers['Content-Length'] = 0;
|
|
}
|
|
} else if (typeof args === 'object') {
|
|
// add headers and POST/PUT/DELETE/PATCH data to connect options to be passed
|
|
// with request, but without deleting other headers like non-tunnel proxy headers
|
|
if (args.headers){
|
|
for (var headerName in args.headers){
|
|
options.headers[headerName] = args.headers[headerName];
|
|
}
|
|
|
|
}
|
|
|
|
//always set Content-length header
|
|
//set Content lentgh for some servers to work (nginx, apache)
|
|
if (args.data !== undefined){
|
|
options.data = args.data;
|
|
options.headers['Content-Length'] = Buffer.byteLength((typeof args.data === 'string' ? args.data:JSON.stringify(args.data)), 'utf8');
|
|
}else{
|
|
options.headers['Content-Length'] = 0;
|
|
}
|
|
// we have args, go and check if we have parameters
|
|
if (args.parameters && Object.keys(args.parameters).length > 0){
|
|
// validate URL consistency, and fix it
|
|
options.path +=(options.path.charAt(url.length-1) === '?'?"?":"");
|
|
options.path = options.path.concat(Util.encodeQueryFromArgs(args.parameters));
|
|
debug("options.path after request parameters = ", options.path);
|
|
}
|
|
|
|
// override client config, by the moment just for request response config
|
|
this.overrideClientConfig(options,args);
|
|
}
|
|
|
|
|
|
debug("options post connect",options);
|
|
debug("FINAL SELF object ====>", self);
|
|
|
|
if (self.useProxy && self.useProxyTunnel){
|
|
ConnectManager.proxy(options,callback);
|
|
}else{
|
|
// normal connection and direct proxy connections (no tunneling)
|
|
ConnectManager.normal(options,callback);
|
|
}
|
|
},
|
|
mergeMimeTypes:function(mimetypes){
|
|
// merge mime-types passed as options to client
|
|
if (mimetypes && typeof mimetypes === "object"){
|
|
if (mimetypes.json && mimetypes.json instanceof Array && mimetypes.json.length > 0){
|
|
ConnectManager.jsonctype = mimetypes.json;
|
|
}else if (mimetypes.xml && mimetypes.xml instanceof Array && mimetypes.xml.length > 0){
|
|
ConnectManager.xmlctype = mimetypes.xml;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
Method = function(url, method){
|
|
var httpMethod = self[method.toLowerCase()];
|
|
|
|
return function(args,callback){
|
|
var completeURL = url;
|
|
//no args
|
|
if (typeof args === 'function'){
|
|
callback = args;
|
|
args = {};
|
|
}else if (typeof args === 'object'){
|
|
// we have args, go and check if we have parameters
|
|
if (args.parameters && Object.keys(args.parameters).length > 0){
|
|
// validate URL consistency, and fix it
|
|
url +=(url.charAt(url.length-1) === '?'?"?":"");
|
|
completeURL = url.concat(Util.encodeQueryFromArgs(args.parameters));
|
|
//delete args parameters we don't need it anymore in registered
|
|
// method invocation
|
|
delete args.parameters;
|
|
}
|
|
}
|
|
return httpMethod(completeURL, args , callback);
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
this.get = function(url, args, callback){
|
|
var clientRequest = new ClientRequest();
|
|
Util.connect('GET', url, args, callback, clientRequest);
|
|
return clientRequest;
|
|
};
|
|
|
|
this.post = function(url, args, callback){
|
|
var clientRequest = new ClientRequest();
|
|
Util.connect('POST', url, args, callback, clientRequest);
|
|
return clientRequest;
|
|
};
|
|
|
|
this.put = function(url, args, callback){
|
|
var clientRequest = new ClientRequest();
|
|
Util.connect('PUT', url, args, callback, clientRequest);
|
|
return clientRequest;
|
|
};
|
|
|
|
this.delete = function(url, args, callback){
|
|
var clientRequest = new ClientRequest();
|
|
Util.connect('DELETE', url, args, callback, clientRequest);
|
|
return clientRequest;
|
|
};
|
|
|
|
this.patch = function(url, args, callback){
|
|
var clientRequest = new ClientRequest();
|
|
Util.connect('PATCH', url, args, callback, clientRequest);
|
|
return clientRequest;
|
|
};
|
|
|
|
|
|
this.registerMethod = function(name, url, method){
|
|
// create method in method registry with preconfigured REST invocation
|
|
// method
|
|
this.methods[name] = new Method(url,method);
|
|
};
|
|
|
|
this.unregisterMethod = function(name){
|
|
delete this.methods[name];
|
|
};
|
|
|
|
// handle ConnectManager events
|
|
ConnectManager.on('error',function(err){
|
|
self.emit('error',err);
|
|
});
|
|
|
|
// merge mime types with connect manager
|
|
Util.mergeMimeTypes(self.mimetypes);
|
|
debug("ConnectManager", ConnectManager);
|
|
|
|
};
|
|
|
|
|
|
var ConnectManager = {
|
|
"xmlctype":["application/xml","application/xml;charset=utf-8"],
|
|
"jsonctype":["application/json","application/json;charset=utf-8"],
|
|
"isXML":function(content){
|
|
var result = false;
|
|
if (!content) return result;
|
|
|
|
for (var i=0; i<this.xmlctype.length;i++){
|
|
result = this.xmlctype[i].toLowerCase() === content.toLowerCase();
|
|
if (result) break;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
"isJSON":function(content){
|
|
var result = false;
|
|
if (!content) return result;
|
|
|
|
for (var i=0; i<this.jsonctype.length;i++){
|
|
result = this.jsonctype[i].toLowerCase() === content.toLowerCase();
|
|
if (result) break;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
"isValidData":function(data){
|
|
return data !== undefined && (data.length !== undefined && data.length > 0);
|
|
},
|
|
"configureRequest":function(req, config, clientRequest){
|
|
|
|
if (config.timeout){
|
|
req.setTimeout(config.timeout, function(){
|
|
clientRequest.emit('requestTimeout',req);
|
|
});
|
|
}
|
|
|
|
|
|
if(config.noDelay)
|
|
req.setNoDelay(config.noDelay);
|
|
|
|
if(config.keepAlive)
|
|
req.setSocketKeepAlive(config.noDelay,config.keepAliveDelay || 0);
|
|
|
|
},
|
|
"configureResponse":function(res,config, clientRequest){
|
|
if (config.timeout){
|
|
res.setTimeout(config.timeout, function(){
|
|
clientRequest.emit('responseTimeout',res);
|
|
res.close();
|
|
});
|
|
}
|
|
},
|
|
"handleEnd":function(res,buffer,callback){
|
|
|
|
var self = this,
|
|
content = res.headers["content-type"],
|
|
encoding = res.headers["content-encoding"];
|
|
|
|
debug("content-type: ", content);
|
|
debug("content-encoding: ",encoding);
|
|
|
|
if(encoding !== undefined && encoding.indexOf("gzip") >= 0){
|
|
debug("gunzip");
|
|
zlib.gunzip(Buffer.concat(buffer),function(er,gunzipped){
|
|
self.handleResponse(res,gunzipped,callback);
|
|
});
|
|
}else if(encoding !== undefined && encoding.indexOf("deflate") >= 0){
|
|
debug("inflate");
|
|
zlib.inflate(Buffer.concat(buffer),function(er,inflated){
|
|
self.handleResponse(res,inflated,callback);
|
|
});
|
|
}else {
|
|
debug("not compressed");
|
|
self.handleResponse(res,Buffer.concat(buffer),callback);
|
|
}
|
|
},
|
|
"handleResponse":function(res,data,callback){
|
|
var content = res.headers["content-type"] && res.headers["content-type"].replace(/ /g, '');
|
|
|
|
debug("response content is ",content);
|
|
// XML data need to be parsed as JS object
|
|
if (this.isXML(content)){
|
|
parseString(data.toString(), function (err, result) {
|
|
callback(result, res);
|
|
});
|
|
}else if (this.isJSON(content)){
|
|
var jsonData,
|
|
data = data.toString();
|
|
try {
|
|
jsonData = this.isValidData(data)?JSON.parse(data):data;
|
|
} catch (err) {
|
|
// Something went wrong when parsing json. This can happen
|
|
// for many reasons, including a bad implementation on the
|
|
// server.
|
|
jsonData = 'Error parsing response. response: [' +
|
|
data + '], error: [' + err + ']';
|
|
|
|
}
|
|
callback(jsonData, res);
|
|
}else{
|
|
callback(data, res);
|
|
}
|
|
},
|
|
"prepareData":function(data){
|
|
var result;
|
|
if ((data instanceof Buffer) || (typeof data !== 'object')){
|
|
result = data;
|
|
}else{
|
|
result = JSON.stringify(data);
|
|
}
|
|
return result;
|
|
},
|
|
"proxy":function(options, callback){
|
|
|
|
debug("proxy options",options.proxy);
|
|
|
|
// creare a new proxy tunnel, and use to connect to API URL
|
|
var proxyTunnel = http.request(options.proxy),
|
|
self = this;
|
|
|
|
|
|
proxyTunnel.on('connect',function(res, socket, head){
|
|
debug("proxy connected",socket);
|
|
|
|
// set tunnel socket in request options, that's the tunnel itself
|
|
options.socket = socket;
|
|
|
|
var buffer=[],
|
|
protocol = (options.protocol =="http")?http:https,
|
|
clientRequest = options.clientRequest,
|
|
requestConfig = options.requestConfig,
|
|
responseConfig = options.responseConfig;
|
|
|
|
//remove "protocol" and "clientRequest" option from options, cos is not allowed by http/hppts node objects
|
|
delete options.protocol;
|
|
delete options.clientRequest;
|
|
delete options.requestConfig;
|
|
delete options.responseConfig;
|
|
|
|
// add request options to request returned to calling method
|
|
clientRequest.options = options;
|
|
|
|
var request = protocol.request(options, function(res){
|
|
//configure response
|
|
self.configureResponse(res,responseConfig, clientRequest);
|
|
|
|
// concurrent data chunk handler
|
|
res.on('data',function(chunk){
|
|
buffer.push(new Buffer(chunk));
|
|
});
|
|
|
|
res.on('end',function(){
|
|
self.handleEnd(res,buffer,callback);
|
|
});
|
|
|
|
|
|
// handler response errors
|
|
res.on('error',function(err){
|
|
if (clientRequest !== undefined && typeof clientRequest === 'object'){
|
|
// add request as property of error
|
|
err.request = clientRequest;
|
|
err.response = res;
|
|
// request error handler
|
|
clientRequest.emit('error',err);
|
|
}else{
|
|
// general error handler
|
|
self.emit('error',err);
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// configure request and add it to clientRequest
|
|
// and add it to request returned
|
|
self.configureRequest(request,requestConfig, clientRequest);
|
|
clientRequest.setHttpRequest(request);
|
|
|
|
|
|
// write POST/PUT data to request body;
|
|
if(options.data) request.write(self.prepareData(options.data));
|
|
|
|
|
|
// handle request errors and handle them by request or general error handler
|
|
request.on('error',function(err){
|
|
if (clientRequest !== undefined && typeof clientRequest === 'object'){
|
|
// add request as property of error
|
|
err.request = clientRequest;
|
|
|
|
// request error handler
|
|
clientRequest.emit('error',err);
|
|
}else{
|
|
// general error handler
|
|
self.emit('error',err);
|
|
}
|
|
});
|
|
|
|
|
|
request.end();
|
|
});
|
|
|
|
// proxy tunnel error are only handled by general error handler
|
|
proxyTunnel.on('error',function(e){
|
|
self.emit('error',e);
|
|
});
|
|
|
|
proxyTunnel.end();
|
|
|
|
},
|
|
"normal":function(options, callback){
|
|
|
|
var buffer = [],
|
|
protocol = (options.protocol === "http")?http:https,
|
|
clientRequest = options.clientRequest,
|
|
requestConfig = options.requestConfig,
|
|
responseConfig = options.responseConfig,
|
|
self = this;
|
|
|
|
//remove "protocol" and "clientRequest" option from options, cos is not allowed by http/hppts node objects
|
|
delete options.protocol;
|
|
delete options.clientRequest;
|
|
delete options.requestConfig;
|
|
delete options.responseConfig;
|
|
debug("options pre connect", options);
|
|
|
|
// add request options to request returned to calling method
|
|
clientRequest.options = options;
|
|
|
|
var request = protocol.request(options, function(res){
|
|
//configure response
|
|
self.configureResponse(res,responseConfig, clientRequest);
|
|
|
|
// concurrent data chunk handler
|
|
res.on('data',function(chunk){
|
|
buffer.push(new Buffer(chunk));
|
|
});
|
|
|
|
res.on('end',function(){
|
|
|
|
self.handleEnd(res,buffer,callback);
|
|
|
|
});
|
|
|
|
// handler response errors
|
|
res.on('error',function(err){
|
|
if (clientRequest !== undefined && typeof clientRequest === 'object'){
|
|
// add request as property of error
|
|
err.request = clientRequest;
|
|
err.response = res;
|
|
// request error handler
|
|
clientRequest.emit('error',err);
|
|
}else{
|
|
// general error handler
|
|
self.emit('error',err);
|
|
}
|
|
});
|
|
});
|
|
|
|
// configure request and add it to clientRequest
|
|
// and add it to request returned
|
|
self.configureRequest(request,requestConfig, clientRequest);
|
|
debug("clientRequest",clientRequest);
|
|
|
|
clientRequest.setHttpRequest(request);
|
|
|
|
// handle request errors and handle them by request or general error handler
|
|
request.on('error',function(err){
|
|
debug('request error', clientRequest);
|
|
if (clientRequest !== undefined && typeof clientRequest === 'object'){
|
|
// add request as property of error
|
|
err.request = clientRequest;
|
|
// request error handler
|
|
clientRequest.emit('error',err);
|
|
}else{
|
|
// general error handler
|
|
self.emit('error',err);
|
|
}
|
|
});
|
|
|
|
|
|
debug("options data", options.data);
|
|
// write POST/PUT data to request body;
|
|
if(options.data) request.write(this.prepareData(options.data));
|
|
|
|
request.end();
|
|
|
|
}
|
|
};
|
|
|
|
|
|
// event handlers for client and ConnectManager
|
|
util.inherits(exports.Client, events.EventEmitter);
|
|
util._extend(ConnectManager,events.EventEmitter.prototype);
|
|
|
|
|
|
var debug = function(){
|
|
if (!process.env.DEBUG) return;
|
|
|
|
var now = new Date(),
|
|
header =now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds() + " [NRC CLIENT]" + arguments.callee.caller.name + " -> ",
|
|
args = Array.prototype.slice.call(arguments);
|
|
args.splice(0,0,header);
|
|
node_debug.apply(console,args);
|
|
|
|
|
|
};
|