Files
SillyTavern/node_modules/node-rest-client/lib/node-rest-client.js
Ashen One a417031af4 main
2022-12-22 07:22:29 +04:00

746 lines
28 KiB
JavaScript

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);
};