UNPKG

node-rest-client

Version:
746 lines (594 loc) 29.1 kB
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<query.length;i++){ keyValue = query[i].split("="); result[keyValue[0]] = decodeURIComponent(keyValue[1]); } return result; }, serializeEncodeQueryFromArgs:function(args){ function serialize(obj, parent) { var tokens = [], propertyName; // iterate over all properties for(propertyName in obj) { // if object has property (it's not an array iteration) if (obj.hasOwnProperty(propertyName)) { // if property has parent, add nested reference var parsedProperty = parent ? parent + "[" + propertyName + "]" : propertyName, propertyValue = obj[propertyName]; // if property has value and is object (we must iterate // again, not final leaf) // iterate over object property passing current parsed // property as parent // else add encoded parsed property and value to result // array tokens.push((propertyValue !== null && typeof propertyValue === "object") ? serialize(propertyValue, parsedProperty) : encodeURIComponent(parsedProperty) + "=" + encodeURIComponent(propertyValue)); } } return tokens.join("&"); } debug("args is", args); // check args consistency if (args && typeof args !== 'object' ) self.emit('error','cannot serialize parameters: invalid type ' + (typeof args) + ' should be an object type'); return serialize(args); }, 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){ //wrapper for emit function on client var clientEmitterWrapper = function (client){ var client = client; return function(type, event){client.emit(type, event);}; }; // check args type if we use it if (callback && args && typeof args !== 'object')self.emit('error','args should be and object'); // 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, clientRequest.href=options.href, 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[CONSTANTS.HEADER_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){ if (args.headers.hasOwnProperty(headerName)) { options.headers[headerName] = args.headers[headerName]; } } } // 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 adding query // parameter separation char // check if URL already has '?' path parameter separator // char in any position that is not final // if true throw error var pathLength = options.path.length, pathParameterSepCharPos = options.path.indexOf("?"); if (pathParameterSepCharPos >= 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); };