osnmarket-abstract-core
Version:
Abstract Core Functionalities
535 lines (516 loc) • 17.1 kB
JavaScript
;
var axios = require('axios');
var configYml = require('config-yml');
var crypto = require('crypto');
var NodeCache = require('node-cache');
var FormData = require('form-data');
var querystring = require('querystring');
var CryptoJS = require('crypto-js');
const appYMLConfigs = configYml.load(process.env.NODE_ENV);
// Function to generate a secure key from the password
const generateKey$1 = password => crypto.createHash('sha256').update(password).digest();
// export const to encrypt sensitive information
const secureServerEncryption = (text, password) => {
// Ensure text is converted to a string
text = typeof text === 'string' ? text : JSON.stringify(text);
const iv = crypto.randomBytes(16); // Generate a random IV (Initialization Vector)
const cipher = crypto.createCipheriv('aes-256-cbc', generateKey$1(password), iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + encrypted;
};
// Recursive function to encrypt specified keys in the data object
const encryptKeys = (data, keysToEncrypt) => {
if (Array.isArray(data)) {
return data.map(item => encryptKeys(item, keysToEncrypt));
} else if (typeof data === 'object' && data !== null) {
const newData = {};
for (const key in data) {
const value = data[key];
if (keysToEncrypt.includes(key)) {
// Encrypt the key if it's in the keysToEncrypt array
newData[key] = secureServerEncryption(value, process.env.NEXT_PUBLIC_LOCAL_URL);
} else {
// Recursively encrypt nested objects
newData[key] = encryptKeys(value, keysToEncrypt);
}
}
return newData;
} else {
return data;
}
};
const getErrStack = err => {
const {
stack,
code,
message,
response
} = err;
const errStack = {
...(code && {
code: code
}),
...(response?.request && {
instance: response.request.path
}),
...(response?.data && {
data: response.data
}),
...(message && {
message: message
})
};
if (!process.env.NODE_ENV.startsWith('prod') && stack) {
errStack.stack = stack;
}
return errStack;
};
const nodeCache = new NodeCache();
var nodeCache$1 = nodeCache;
const appConfigs = {
localBase: process.env.NEXT_PUBLIC_LOCAL_URL,
defaultApiTarget: process.env.NEXT_PUBLIC_DEFAULT_TARGET,
dispatcherRoute: process.env.NEXT_PUBLIC_DISPATCHER_ROUTE,
imageServer: process.env.NEXT_PUBLIC_IMG_SERVER,
isApplicationBuilding: process.env.NODE_APPLICATION_BUILDING,
tokenLifespan: process.env.TOKEN_LIFETIME,
pageTimeOut: process.env.NEXT_PUBLIC_LOGOUT_TTL,
redisUrl: process.env.REDIS_URL
};
var appConfigs$1 = appConfigs;
/**
* A function to manage caching using either NodeCache or Redis.
*
* @param {Object} options - The options for the caching system.
* @param {any} options.store - The data to be stored in the cache.
* @param {any} options.retrieve - The key to retrieve data from the cache.
* @param {string} options.name - The name of the cache.
* @param {string} [options.strategy='nodeCache'] - The caching strategy to use.
* @param {number} [options.ttl=appConfigs.tokenLifespan] - The time-to-live for the cache in seconds.
* @returns {Promise<any>} - The result of the caching operation.
* @throws {Error} - If an invalid caching strategy is provided.
*/
async function CachingSystem({
store,
retrieve,
name,
strategy = 'nodeCache',
ttl = appConfigs$1.tokenLifespan
}) {
if (strategy != 'nodeCache') {
throw new Error(`Invalid caching strategy: ${strategy}`);
}
try {
if (strategy == 'nodeCache') {
if (store) {
return nodeCache$1.set(name, store, ttl);
}
if (retrieve) {
try {
if (nodeCache$1.has(name)) {
return nodeCache$1.get(name);
}
return false;
} catch (err) {
throw new Error(err);
}
}
}
} catch (err) {
throw new Error(err);
}
}
const enviromentKeys = ({
needle,
data
}) => {
return Object.keys(data).filter(function (k) {
return k.indexOf(needle) == 0;
}).reduce(function (newData, k) {
newData[k] = data[k];
return newData;
}, {});
};
const targetMapper = ({
authProvider,
configs,
source,
sourceName
}) => {
return {
identifier: {
key: authProvider.identifier_key,
value: configs[`${sourceName.toUpperCase()}_IDENTIFIER`]
},
password: {
key: authProvider.password_key,
value: configs[`${sourceName.toUpperCase()}_PASSWORD`]
},
token_type: source.token_type || authProvider.token_type,
token_key: source.token_key || authProvider.token_key,
backend_url: configs[`${sourceName.toUpperCase()}_BACKEND_URL`],
authentication_path: source.authentication_path || authProvider.authentication_path,
...(configs[`${sourceName.toUpperCase()}_SCOPE`] && {
authentication_scope: configs[`${sourceName.toUpperCase()}_SCOPE`]
}),
...(configs[`${sourceName.toUpperCase()}_GRANT_TYPE`] && {
authentication_grant_type: configs[`${sourceName.toUpperCase()}_GRANT_TYPE`]
})
};
};
/**
* Authenticates a service account and retrieves an access token.
* If the token is already cached, it retrieves it from the cache.
* Otherwise, it authenticates with the specified backend and caches the token.
*
* @param {Object} params - The parameters for the function.
* @param {Object} params.source - The source configuration object.
* @param {string} params.sourceName - The name of the source.
* @param {Object} params.authProvider - The authentication provider configuration object.
* @param {string} params.preferedCache - The preferred caching strategy.
*
* @returns {Promise<string>} - A promise that resolves with the access token.
*
* @throws {Error} - If an error occurs during the authentication process.
*/
const serviceAccountLogin = async ({
source,
sourceName,
authProvider,
preferedCache
}) => {
try {
const savedToken = await CachingSystem({
retrieve: true,
name: sourceName + '_access_token',
strategy: preferedCache
});
if (savedToken) {
process.env.NODE_ENV == 'development' && console.log(Date.now(), `Got ${sourceName} token from cache ${savedToken}`);
return savedToken;
}
process.env.NODE_ENV == 'development' && console.log(Date.now(), `Authenticating ${sourceName}`);
let envConfigs = enviromentKeys({
needle: sourceName.toUpperCase(),
data: process.env
});
const application = targetMapper({
authProvider,
configs: envConfigs,
source,
sourceName
});
let body;
let axiosKey = 'post';
if (authProvider.authentication_via_form || source.authentication_via_form) {
axiosKey = 'postForm';
let formData = new FormData();
if (application.authentication_grant_type) {
formData.append('grant_type', application.authentication_grant_type);
}
if (application.authentication_scope) {
formData.append('scope', application.authentication_scope);
}
formData.append(application.identifier.key, application.identifier.value);
formData.append(application.password.key, application.password.value);
body = formData;
} else {
const payload = {
[application.identifier.key]: application.identifier.value,
[application.password.key]: application.password.value,
...(application.authentication_grant_type && {
grant_type: application.authentication_grant_type
}),
...(application.authentication_scope && {
grant_type: application.authentication_scope
})
};
if (authProvider.authentication_via_query_string || source.authentication_via_query_string) {
body = querystring.stringify(payload);
} else {
body = payload;
}
}
const {
data
} = await axios[axiosKey](`${application.backend_url}${application.authentication_path}`, body);
const applicationToken = application.token_type == 'none' ? data[application.token_key] : `${application.token_type} ${data[application.token_key]}`;
await CachingSystem({
store: applicationToken,
name: sourceName + '_access_token',
strategy: preferedCache
});
return applicationToken;
} catch (err) {
throw getErrStack(err);
}
};
/**
* Retrieves and prepares API configurations based on the targeted API.
*
* This function fetches the necessary configurations from YAML files,
* environment variables, and performs authentication based on the API provider.
*
* @param {string} targetedApi - The name of the targeted API for which configurations are required.
* @returns {Promise<Object>} - A promise that resolves to an object containing the API configurations.
* @throws {Error} - If any error occurs during the process.
*/
const getApiConfigs = async targetedApi => {
try {
const apiConfigs = appYMLConfigs.app[targetedApi];
const providerConfigs = appYMLConfigs.providers[apiConfigs.provider];
const apiEnviromentVariables = enviromentKeys({
needle: targetedApi.toUpperCase(),
data: process.env
});
let target = {
backend_url: apiEnviromentVariables[`${targetedApi.toUpperCase()}_BACKEND_URL`]
};
const apiPassword = apiEnviromentVariables[`${targetedApi.toUpperCase()}_PASSWORD`];
switch (apiConfigs.provider) {
case 'basic':
target = {
...target,
auth: {
username: apiEnviromentVariables[`${targetedApi.toUpperCase()}_IDENTIFIER`],
password: apiPassword
}
};
break;
case 'apiKey':
target = {
...target,
authorization: {
[providerConfigs.authorization_key]: `${providerConfigs.token_type} ${apiPassword}`,
authorization_key: providerConfigs.authorization_key
}
};
break;
default:
const token = await serviceAccountLogin({
source: apiConfigs,
sourceName: targetedApi,
authProvider: providerConfigs,
preferedCache: appConfigs$1.caching_sytem
});
target = {
...target,
authorization: {
[providerConfigs.authorization_key]: token
},
authorization_key: providerConfigs.authorization_key
};
break;
}
return target;
} catch (error) {
const apiEnviromentVariables = enviromentKeys({
needle: targetedApi.toUpperCase(),
data: process.env
});
if (!process.env.NODE_ENV.startsWith('prod')) {
if (error?.stack?.includes('ECONNREFUSED')) {
console.warn(`Check that your ${targetedApi} server is running`);
}
if ([405, 404, 500].includes(error?.status_code)) {
console.log(apiEnviromentVariables);
} else {
console.warn(`\nDid you try creating a "config/${process.env.NODE_ENV}.yml" file?`);
console.log(error);
}
}
throw error;
}
};
// Function to generate a secure key from the password
const generateKey = password => {
return CryptoJS.SHA256(password);
};
// Function to decrypt sensitive information
const secureContentDecryption = (encryptedText, password = process.env.NEXT_PUBLIC_LOCAL_URL) => {
const iv = CryptoJS.enc.Hex.parse(encryptedText.slice(0, 32)); // Extract IV from the encrypted text
const encrypted = encryptedText.slice(32); // Extract the actual encrypted data
const key = generateKey(password);
const decrypted = CryptoJS.AES.decrypt({
ciphertext: CryptoJS.enc.Hex.parse(encrypted)
}, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString(CryptoJS.enc.Utf8);
return decrypted;
};
const formatDate = date => {
return new Date().toISOString();
};
function getBoundary(contentType) {
const boundaryPattern = /boundary=(?:"([^"]+)"|([^;]+))/i;
const match = contentType.match(boundaryPattern);
return match ? match[1] || match[2] : '';
}
async function parseFormDataFromRequest(req, isHeaderInstance) {
const contentType = isHeaderInstance ? req.headers.get('content-type') : req.headers['content-type'];
const boundary = getBoundary(contentType);
const parts = req.body.split(boundary).filter(part => part.trim() !== '');
const formData = {};
// Iterate over each part to extract field name and value
parts.forEach(part => {
const lines = part.trim().split('\r\n');
lines.map(line => {
const [_, L_VALUE] = line.split('; ');
if (L_VALUE) {
const fieldName = L_VALUE.split('=')[1].replace(/"/g, '');
const fieldValue = lines[2];
formData[fieldName] = fieldValue;
}
});
});
return formData;
}
async function handleFormPostRequest({
req,
target,
headers,
reqMethod,
isHeaderInstance,
encHeads
}) {
let axiosMethod = reqMethod || 'post';
let body;
const contentType = isHeaderInstance ? req.headers.get('content-type') : req.headers['content-type'];
if (contentType?.includes('form')) {
body = await parseFormDataFromRequest(req, isHeaderInstance);
} else {
body = req.body;
}
try {
const {
status,
data
} = await axios[axiosMethod](target.route, body, {
headers: {
...headers,
...(target.token && {
...target.token
})
},
...(target.auth && {
auth: target.auth
})
});
let finalData = data;
if (encHeads) {
finalData = encryptKeys(data, encHeads.split(','));
}
return {
status,
data: finalData
};
} catch (e) {
return {
status: e?.response?.status ?? 500,
data: getErrStack(e)
};
}
}
const abstractHandler = async req => {
const isHeaderInstance = req.headers instanceof Headers;
const targetedApiName = isHeaderInstance ? req.headers.get('content-source') : req.headers['content-source'];
const targetedApiConfigs = appYMLConfigs.app[targetedApiName];
try {
const key = isHeaderInstance ? req.headers.get('content-secured') : req.headers['content-secured'];
if (!key) {
throw new Error('No key provided');
}
const decryption = secureContentDecryption(key, process.env.NEXT_PUBLIC_LOCAL_URL);
const currentDate = formatDate(new Date());
const timeDifference = Math.abs(new Date(currentDate) - new Date(decryption)) / 1000;
const timeTolerance = 30;
const nonceKey = decryption.replace(/ /gm, '_');
const nonce = await CachingSystem({
name: nonceKey,
retrieve: true
});
if ((!decryption || timeDifference > timeTolerance) && !nonce) {
throw new Error('Invalid Request', timeDifference, decryption, currentDate, nonce);
}
await CachingSystem({
name: nonceKey,
store: decryption,
ttl: timeTolerance
});
const apiConfigs = await getApiConfigs(targetedApiName);
let options = {};
let _vutk = isHeaderInstance ? req.headers.get('_vutk') : req.headers._vutk;
if (targetedApiConfigs.provider == 'basic') {
options = {
...(_vutk && {
token: {
[apiConfigs.authorization_key]: _vutk
}
}),
...(!_vutk && {
auth: apiConfigs.auth
})
};
} else {
options = {
token: _vutk ? {
[apiConfigs.authorization_key]: _vutk
} : apiConfigs.authorization,
auth_provider: appYMLConfigs.providers[targetedApiConfigs.provider]
};
}
const target = {
route: `${apiConfigs.backend_url}${isHeaderInstance ? req.headers.get('content-destination') : req.headers['content-destination']}`,
...options
};
const reqMethod = req.method.toLowerCase();
let hasHeads = isHeaderInstance ? req.headers.get('content-headers') : req.headers['content-headers'];
const headers = hasHeads ? JSON.parse(hasHeads) : undefined;
let encHeads = isHeaderInstance ? req.headers.get('__en') : req.headers.__en;
if (['get', 'delete', 'option', 'options'].includes(reqMethod) || reqMethod.startsWith('get')) {
const {
data,
status
} = await axios[reqMethod](target.route, {
headers: {
...headers,
...(target.token && {
...target.token
})
},
...(target.auth && {
auth: target.auth
})
});
let finalData = data;
if (encHeads) {
finalData = encryptKeys(data, encHeads.split(','));
}
return {
status,
data: finalData
};
}
return await handleFormPostRequest({
req,
target,
headers,
reqMethod,
isHeaderInstance,
encHeads
});
} catch (error) {
if (process.env.NODE_ENV.startsWith('prod')) {
delete error.stack;
}
console.log('Dispatcher error');
return {
data: getErrStack(error),
status: error?.response?.status || 500
};
}
};
exports.abstractHandler = abstractHandler;