@zohodesk/request_interceptor
Version:
Intercept and modify HTTP headers and payloads before sending requests.Implement custom logic to handle requests based on specific conditions
378 lines (334 loc) • 16.8 kB
JavaScript
if (!('isRequestInterceptorAlreadyInitialize' in window)) {
window.isRequestInterceptorAlreadyInitialize = false;
}
export var RequestInterceptor = (function() {
var _allowTransformFunction = []; // Stores custom modify conditions
var _transformFunction = null; // Transforms the payload
var _allowDeTransformFunctions = []; // Stores custom modify conditions
var _deTransformFunction = null; // Transforms the response
var _wafkeyName = "Waf-Encryption-Key";
// Add custom modify conditions
function allowTransformFunction(transformCallback) {
_allowTransformFunction.push(transformCallback);
}
// Set the payload transformation function
function transformFunction(_function) {
_transformFunction = _function;
}
// Add custom modify conditions
function allowDeTransformFunction(deTransformCallback) {
_allowDeTransformFunctions.push(deTransformCallback)
}
// Set the payload transformation function
function deTransformFunction(_function) {
_deTransformFunction = _function;
}
// Retrieve query string from URL
function _retrieveQueryStringFromURL(url) {
// Check if the URL is defined and truthy
if (!url || typeof url !== 'string') {
return "";
}
// Extract the query string from the URL
var queryStringIndex = url.indexOf('?');
if (queryStringIndex === -1) {
return "";
}
// Extract the query string from the URL
const queryString = url.substring(queryStringIndex + 1);
return queryString;
}
// Concatenate original URL and transformed query string
function _concatenateURL(originalURL, transformedQueryString) {
if(typeof transformedQueryString !== "undefined" && transformedQueryString !== ""){
return originalURL && originalURL.split('?')[0] + '?' + transformedQueryString;
}
return originalURL;
}
// Create an object to store response headers
function _responseHeaderSplitUp(responseHeadersString){
var responseHeadersArray = responseHeadersString.split('\n');
var responseHeadersObject = {};
responseHeadersArray.forEach(function(header) {
var parts = header.split(': ');
var key = parts[0];
var value = parts[1];
if (key && value) {
responseHeadersObject[key] = value;
}
});
return responseHeadersObject;
}
function _modifyQueryStringAndPayload({url, payLoad, commonHeaderKey}) {
if (typeof _transformFunction === 'function') {
return new Promise((resolve, reject)=>{
let queryString = _retrieveQueryStringFromURL(url);
_transformFunction({ queryString, payLoad, commonHeaderKey}).then((transformData) => {
if (transformData) {
resolve(transformData);
}
}).catch(error => {
console.error('Error in _transformFunction:', error);
reject(error);
});
});
}
return Promise.resolve(url);
}
// Intercept XMLHttpRequest send requests
function interceptXMLHttpRequest() {
var originalAddEventListener = XMLHttpRequest.prototype.addEventListener;
// Override the addEventListener method
XMLHttpRequest.prototype.addEventListener = function(event, listener, options) {
// Intercept the 'load' event
if (event === 'loadend') {
var originalListener = listener; // Save reference to original listener function
listener = async function(e) {
var xhr = this;
var responseData = xhr.response ? xhr.response : xhr.responseText;
var responseHeaders = _responseHeaderSplitUp(xhr.getAllResponseHeaders());
var shouldAllowDeTransform = _allowDeTransformFunctions.some(function(_fun) {
return _fun(responseHeaders);
}, this);
if(shouldAllowDeTransform){
let deTransformResponse = await _deTransformFunction({payLoad: responseData},responseHeaders);
if(deTransformResponse && deTransformResponse["payLoad"]){
Object.defineProperties(this, {
response: {value: deTransformResponse["payLoad"], writable: true},
responseText: {value: deTransformResponse["payLoad"], writable: true}
});
}
if (originalListener) {
originalListener.apply(this, arguments);
}
}else{
originalListener.apply(this, arguments);
}
};
}
// Call the original addEventListener method with the provided arguments
originalAddEventListener.call(this, event, listener, options);
};
var _payloadIndex = 0;
function _sendArgumentsParse(){
let payload = arguments[_payloadIndex];
return { payload };
}
function _sendArgumentsSet(args, {payload}){
args[_payloadIndex] = payload;
return args;
}
/**To overwrite send */
var orginalSend = window.XMLHttpRequest.prototype.send;
window.XMLHttpRequest.prototype.send = async function() {
var self = this;
const { payload } = _sendArgumentsParse.apply(this, arguments);
// Check if any of the custom modify conditions return true
var shouldModify = _allowTransformFunction.some(function(_fun) {
return _fun({ _url: this._url });
}, this);
// If any condition returns true, do not modify the payload
if (!shouldModify) {
orginalSend.apply(this, arguments);
}
if(shouldModify){
// Transform the payload if a payload transformation function is provided
if (typeof _transformFunction === 'function') {
const _commonHeaderKey = this._commonHeaderKey;
const transformPayload = await _modifyQueryStringAndPayload.call(this, {payLoad: payload, commonHeaderKey: _commonHeaderKey});
if (transformPayload && transformPayload.header && transformPayload.payLoad) {
_sendArgumentsSet(arguments,{ payload : transformPayload.payLoad });
if(_commonHeaderKey){ //if waf key already set we need to delete it
transformPayload && transformPayload.header && delete transformPayload.header[_wafkeyName];
}
for (let header in transformPayload.header) {
self.setRequestHeader(header, transformPayload.header[header]);
}
orginalSend.apply(this, [arguments[0], ...arguments]);
}else if(transformPayload && transformPayload.header && self._isQueryStringHasEmpty){//If QueryString and Payload both is empty we need to set the empty wafkey
for (let header in transformPayload.header) {
self.setRequestHeader(header, transformPayload.header[header]);
}
orginalSend.apply(this, [arguments[0], ...arguments]);
}else{
orginalSend.apply(this, arguments);
}
} else {
orginalSend.apply(this, arguments);
}
}
};
var _methodIndex = 0;
var _urlIndex = 1;
var _asyncIndex = 2;
function _openArgumentsParse(){
let url = arguments[_urlIndex];
let method = arguments[_methodIndex];
let async = arguments[_methodIndex];
return { url, method, async };
}
function _openArgumentsSet(args, {url}){
args[_urlIndex] = url;
return args;
}
/**To overwrite open */
var orginalOpen = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = async function() {
let self = this;
const { url, method, async } = _openArgumentsParse.apply(this, arguments);
// Check if any of the custom modify conditions return true
var shouldModify = _allowTransformFunction.some(function(_fun) {
return _fun({ _url: url, _method: method, _async: async });
}, this);
// If any condition returns true, do not modify the payload
if (!shouldModify) {
self._url = url;
self._method = method;
orginalOpen.apply(this, arguments);
}
if(shouldModify){
if (typeof _transformFunction === 'function' && url) {
let originalObj = {url};
let transformData = await _modifyQueryStringAndPayload.call(this, originalObj);
self._url = transformData ? _concatenateURL(url, transformData.queryString) || url : url;
self._method = method;
if (transformData && transformData.header && transformData.queryString) {
_openArgumentsSet(arguments,{ url: self._url });
orginalOpen.apply(this, arguments);
for (let header in transformData.header) {
self.setRequestHeader(header, transformData.header[header]);
}
if(transformData.commonKey){
self._commonHeaderKey = transformData.commonKey;
}
}else{
self._isQueryStringHasEmpty = transformData && typeof transformData.queryString === "undefined";
orginalOpen.apply(this, arguments);
}
}else{
orginalOpen.apply(this, arguments);
}
}
};
}
// intercept fetch requests
function interceptFetch() {
var _urlIndex = 0;
var _otherIndex = 1;
var _payLoadText = "body";
let _headerText = "headers";
function _fetchParseArguments(){
let url = arguments[_urlIndex];
url = typeof url === "object" ? url.url : url;
let others = arguments && arguments[_otherIndex];
const payLoad = others && others[_payLoadText];
return { url, payLoad };
}
function _fetchArgumentsSet(args, {url, payLoad, header}){
//To set url
if(url && typeof url !== "undefined"){
args[_urlIndex] = url;
}
//To set payload
let others = args && args[_otherIndex];
if(others && typeof others !== "undefined" && payLoad && typeof payLoad !== "undefined"){
others[_payLoadText] = others && others[_payLoadText] && payLoad;
}
//To set header
if(others && typeof others !== "undefined" && header && typeof header !== "undefined"){
others[_headerText] = Object.assign(others[_headerText], header)
}
return args;
}
function _fetchResponseHeaderSplitUp(responseHeaders){
const headersObject = {};
if(typeof responseHeaders !== "undefined"){
const headers = responseHeaders;
headers.forEach((value, key) => {
headersObject[key] = value;
});
}
return headersObject;
}
var originalFetch = window.fetch;
window.fetch = async function() {
const { url, payLoad } = _fetchParseArguments.apply(this, arguments);
//Check if any of the custom modify conditions return true
var shouldModify = _allowTransformFunction.some(function(_fun) {
return _fun({_url:url});
});
if (!shouldModify) {
// If any condition returns true, do not modify the payload
return originalFetch.apply(this, arguments);
}
if(shouldModify){
// Transform the payload if a payload transformation function is provided
if (typeof _transformFunction === 'function') {
let transformData = await _modifyQueryStringAndPayload.call(this, {url, payLoad});
if (transformData && transformData.header) {
let data = {};
if(url && typeof url !== "undefined" && transformData && transformData.queryString){
let transformUrl = transformData ? _concatenateURL(url, transformData.queryString) || url : url;
data.url = transformUrl;
}
if(payLoad && typeof payLoad !== "undefined" && transformData && transformData.payLoad){
data.payLoad = transformData.payLoad;
}
if(transformData && transformData.header){
data.header = transformData.header;
}
_fetchArgumentsSet(arguments,data);
return originalFetch.apply(this,arguments).then(async function(){
var response = arguments[0];
const resClone = response.clone();
const parseHeaders = _fetchResponseHeaderSplitUp(response.headers);
const shouldAllowDeTransform = _allowDeTransformFunctions.some(function(_fun) {
return _fun(parseHeaders);
}, this);
if(shouldAllowDeTransform){
const decryptedResponse = await resClone.text().then(async (responseData) => {
let deTransformResponse = await _deTransformFunction({payLoad: responseData},parseHeaders);
if(deTransformResponse && deTransformResponse["payLoad"]){
const modifiedResponse = new Response(deTransformResponse["payLoad"] || "", {
headers: response.headers,
//ok: response.ok,
//redirected: response.redirected,
status: response.status,
statusText: response.statusText,
//type: response.type,
//url: response.url
});
return modifiedResponse;
}else{
return response;
}
});
return decryptedResponse;
}
return response;
});
}
else{
return originalFetch.apply(this, arguments);
}
}
}
};
}
// Public method to initialize the request interception
function initialize() {
if(!window.isRequestInterceptorAlreadyInitialize){
interceptXMLHttpRequest();
interceptFetch();
window.isRequestInterceptorAlreadyInitialize = true;
}
}
// Expose the initialize method, allowTransformFunction method, and transformFunction method publicly
return {
initialize: initialize,
allowTransformFunction: allowTransformFunction,
transformFunction: transformFunction,
allowDeTransformFunction: allowDeTransformFunction,
deTransformFunction: deTransformFunction
};
})();