UNPKG

osnmarket-abstract-core

Version:
535 lines (516 loc) 17.1 kB
'use strict'; 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;