var http = require('follow-redirects').http, https = require('follow-redirects').https, urlParser = require('url'), util = require("util"), events = require("events"), zlib = require("zlib"), node_debug = require("debug")("NRC"); exports.Client = function (options){ var self = this, // parser response manager parserManager = require("./nrc-parser-manager")(), serializerManager = require("./nrc-serializer-manager")(), // connection manager connectManager = new ConnectManager(this, parserManager), // io facade to parsers and serailiazers ioFacade = function(parserManager, serializerManager){ // error execution context var errorContext = function(logic){ return function(){ try{ return logic.apply(this, arguments); }catch(err){ self.emit('error',err); } }; }, result={"parsers":{}, "serializers":{}}; // parsers facade result.parsers.add = errorContext(parserManager.add); result.parsers.remove = errorContext(parserManager.remove); result.parsers.find = errorContext(parserManager.find); result.parsers.getAll = errorContext(parserManager.getAll); result.parsers.getDefault = errorContext(parserManager.getDefault); result.parsers.clean = errorContext(parserManager.clean); // serializers facade result.serializers.add = errorContext(serializerManager.add); result.serializers.remove = errorContext(serializerManager.remove); result.serializers.find = errorContext(serializerManager.find); result.serializers.getAll = errorContext(serializerManager.getAll); result.serializers.getDefault = errorContext(serializerManager.getDefault); result.serializers.clean = errorContext(serializerManager.clean); return result; }(parserManager,serializerManager), // declare util constants CONSTANTS={ HEADER_CONTENT_LENGTH:"Content-Length" }; 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 || {}; // namespaces for methods, parsers y serializers this.methods={}; this.parsers={}; this.serializers={}; // 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, href:url.href }; 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 (var proxyOption in result.proxy){ result[proxyOption] = result.proxy[proxyOption]; } 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= 0 && pathParameterSepCharPos!== pathLength -1 ) self.emit('error','parameters argument cannot be used if parameters are already defined in URL ' + options.path); options.path +=(options.path.charAt(pathLength-1) === '?'?"":"?"); // check if we have serializable parameter container, that // must be serialized and encoded // directly, as javascript object options.path = options.path.concat(Util.serializeEncodeQueryFromArgs(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); // always set Content-length header if not set previously // set Content lentgh for some servers to work (nginx, apache) if (args.data !== undefined && !options.headers.hasOwnProperty(CONSTANTS.HEADER_CONTENT_LENGTH)){ serializerManager.get(options).serialize(args.data, clientEmitterWrapper(self), function(serializedData){ options.data = serializedData; options.headers[CONSTANTS.HEADER_CONTENT_LENGTH] = Buffer.byteLength(options.data, 'utf8'); }); }else{ options.headers[CONSTANTS.HEADER_CONTENT_LENGTH] = 0; } } 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){ // this function is left for backward compatibility, but will be // deleted in future releases var parser = null; // merge mime-types passed as options to parsers if (mimetypes && typeof mimetypes === "object"){ try{ if (mimetypes.json && mimetypes.json instanceof Array && mimetypes.json.length > 0){ parser = parserManager.find("JSON"); parser.contentTypes = mimetypes.json; }else if (mimetypes.xml && mimetypes.xml instanceof Array && mimetypes.xml.length > 0){ parser = parserManager.find("XML"); parser.contentTypes = mimetypes.xml; } }catch(err){ self.emit('error', 'cannot assign custom content types to parser, cause: ' + err); } } }, createHttpMethod:function(methodName){ return function(url, args, callback){ var clientRequest = new ClientRequest(); Util.connect(methodName.toUpperCase(), url, args, callback, clientRequest); return clientRequest; }; } }, 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 = {}; } return httpMethod(completeURL, args , callback); }; }; this.get = Util.createHttpMethod("get"); this.post = Util.createHttpMethod("post"); this.put = Util.createHttpMethod("put"); this.delete = Util.createHttpMethod("delete"); this.patch = Util.createHttpMethod("patch"); 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]; }; this.addCustomHttpMethod=function(methodName){ self[methodName.toLowerCase()] = Util.createHttpMethod(methodName); }; this.parsers = ioFacade.parsers; this.serializers = ioFacade.serializers; // merge mime types with connect manager Util.mergeMimeTypes(self.mimetypes); debug("ConnectManager", connectManager); }; var ConnectManager = function(client, parserManager) { var client = client, clientEmitterWrapper = function (client){ var client = client; return function(type, event){client.emit(type, event);}; }; this.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); }; this.configureResponse = function(res,config, clientRequest){ if (config.timeout){ res.setTimeout(config.timeout, function(){ clientRequest.emit('responseTimeout',res); res.close(); }); } }; this.configureOptions = function(options){ var followRedirectsProps =["followRedirects", "maxRedirects"]; function configureProps(propsArray, optionsElement){ for (var index in propsArray){ if (optionsElement.hasOwnProperty(propsArray[index])) options[propsArray[index]] = optionsElement[propsArray[index]]; } } //add follows-redirects config configureProps(followRedirectsProps, options.requestConfig); // 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); }; this.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); } }; this.handleResponse = function(res,data,callback){ // find valid parser to be used with response content type, first one // found parserManager.get(res).parse(data, clientEmitterWrapper(client), function(parsedData){ callback(parsedData,res); }); }; this.prepareData = function(data){ var result; if ((data instanceof Buffer) || (typeof data !== 'object')){ result = data; }else{ result = JSON.stringify(data); } return result; }; this.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; self.configureOptions(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(Buffer.from(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 client.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; // find valid serializer to be used to serialize request data, // first one found // is the one to be used.if none found for match condition, // default serializer is used if(options.data)request.write(options.data); request.end(); // 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 client.emit('error',err); } }); }); // proxy tunnel error are only handled by general error handler proxyTunnel.on('error',function(e){ client.emit('error',e); }); proxyTunnel.end(); }; this.normal = function(options, callback){ var buffer = [], protocol = (options.protocol === "http")?http:https, clientRequest = options.clientRequest, requestConfig = options.requestConfig, responseConfig = options.responseConfig, self = this; self.configureOptions(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(Buffer.from(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 client.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); debug("options data", options.data); // write POST/PUT data to request body; // find valid serializer to be used to serialize request data, // first one found // is the one to be used.if none found for match condition, // default serializer is used if(options.data)request.write(options.data); request.end(); // end request when data is written // 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 client.emit('error',err); } }); }; }; // event handlers for client and ConnectManager util.inherits(exports.Client, events.EventEmitter); 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); };