/* eslint-disable no-console */ /* eslint-disable no-prototype-builtins */ /* eslint-disable no-param-reassign */ /* eslint-disable max-len */ /* eslint-disable consistent-return */ /* eslint-disable import/no-dynamic-require */ /* eslint-disable global-require */ import http from 'http' import { v4 } from 'uuid' import { CronJob } from 'cron' import * as dotenv from 'dotenv' import colors from 'colors' import cluster from 'cluster' import { cpus } from 'os' import { PrismaClient } from '@prisma/client' import { ServerUtils_SetAuthKeys } from './ServerUtils/SetAuthKeys/index' import { State, HttpResponseObj, PrivateState, WappiConfigFile, AuthKeys, } from './types' import { ServerUtils_GetAuthKeys } from './ServerUtils/GetAuthKeys' import { ConvertCronToText } from './ServerUtils/ConvertCronToText' import { camelCaseResponse } from './ServerUtils/CamelCaseResponse' import { initialLog, log, responseLog } from './ServerUtils/Log' import { ServerUtils_AuthValidate } from './ServerUtils/AuthValidate' import { AuthValidateResponse } from './ServerUtils/AuthValidate/types' import { ServerUtils_AuthCreate } from './ServerUtils/AuthCreate' import { getConfigFile } from './helpers' const databasePools: any = {} dotenv.config() export const startServer = async () => { const wappiConfigFile: WappiConfigFile = await getConfigFile() if (wappiConfigFile.prisma) { const prisma = new PrismaClient() databasePools.prisma = prisma } await ServerUtils_SetAuthKeys('authKeys.txt') const environment = process.env.ENVIRONMENT const showLogging = process.env.SHOW_LOGS === 'true' const apiFolder = '../API' const { Endpoints, LoginRequired } = require(`${apiFolder}`) const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => { const state: State = {} state.devErrors = [] const privateState: PrivateState = {} state.start = new Date().valueOf() state.body = {} let singleResult = false const httpResponseObj: HttpResponseObj = { complete: false, statusCode: 500, message: 'ServerError', success: false, error: 'unknown', } function httpResponse(statusCode = 200, message = 'No message', success = true, error = httpResponseObj.error) { httpResponseObj.statusCode = statusCode httpResponseObj.message = message httpResponseObj.success = success httpResponseObj.complete = true httpResponseObj.error = error if (showLogging) httpResponseObj.devMessages = state.devErrors const end: number = new Date().valueOf() - state.start const seconds = end / 1000 state.responseTime = seconds // Log out information onto the production servers, for debugging. if (environment === 'prod') { const fileNameArray0 = state.fileNameArray !== undefined ? state.fileNameArray[0] : 'unknown' const fileNameArray1 = state.fileNameArray !== undefined ? state.fileNameArray[1] : 'unknown' console.log(`Time: ${state.responseTime} secs Message: ${message} Success: ${success} Path: ${fileNameArray0}/${fileNameArray1}/${state.body.version} Query: ${state.QueryFunctionName} ${!success ? `Error: ${error}` : ''}`) } } const createAuth = (loginResponse: any) => new Promise((resolve) => { resolve(ServerUtils_GetAuthKeys('authKeys.txt') .then((authKeys) => ServerUtils_AuthCreate({ authKeys, loginResponse, apiKey: wappiConfigFile.apiKey, }))) }) const authenticateUser = () => new Promise((resolve, reject) => { privateState.access = false httpResponseObj.authenticated = false try { if (state.QueryFunction[state.QueryFunctionName] === undefined) { state.devErrors.push('Your Query is undefined.') reject(httpResponse(404, 'Oops.. Query is undefined.', false, 'Query is private or does not exist')) return } if (!Endpoints[state.QueryFunctionName]) { state.devErrors.push('The endpoint you are trying to access, isn\'t flagged as an endpoint.') state.devErrors.push('If you want it to be an endpoint, then add it to the API/index.ts file.') resolve(httpResponse(401, 'Oops.. Fail...', false, 'Access Denied.')) } if (typeof state.QueryFunction[state.QueryFunctionName] === 'function') { const needsLogin = !!LoginRequired[state.QueryFunctionName] if (needsLogin) { if (needsLogin && state.body.auth !== undefined) { // eslint-disable-next-line no-promise-executor-return return ServerUtils_GetAuthKeys( 'authKeys.txt', ).then((authKeys: AuthKeys) => ServerUtils_AuthValidate({ auth: state.body.auth, authKeys, apiKey: wappiConfigFile.apiKey, }).then((authResponse: AuthValidateResponse) => { const { access, ...authObject } = authResponse if (access) { privateState.access = true privateState.auth = authObject httpResponseObj.authenticated = true resolve(privateState.access) return } state.devErrors.push('Your auth key is not authorised. Reissue it and try again.') resolve(httpResponse(401, 'Oops.. Access Denied, please login', false, 'Access Denied. Auth Required.')) }).catch(() => reject(httpResponse(500, 'Oops.. Query Failed.', false, 'Query Failed')))) } state.devErrors.push('Auth is set to required for this endpoint, but there is no auth key') state.devErrors.push("Set `auth: '{{API_AUTH_JWT}}'` in the correct position in request body.") // eslint-disable-next-line no-promise-executor-return return resolve(httpResponse(401, 'Oops.. Fail...', false, 'Access Denied - No Auth Token in request')) } // It's a public function, they can do what ever the hell they want.. Call away!.. privateState.access = true resolve(privateState.access) return } state.devErrors.push('The endpoint does not have the type of "function" so therefore it cannot be triggered. Variables are not endpoints') reject(httpResponse(503, 'Oops.. Query is not a function.', false, 'Query is private or does not exist')) } catch (error) { log('authenticateUser Error', error, showLogging) state.devErrors.push('There was an error in authenticating the user, try reissuing the JWT, and making sure the secrets Cron Job is working') reject(httpResponse(403, 'Catch.. Oops.. Query Version is protected or does not exist.', false, 'Query is private or does not exist')) } }) const callFunctionReturnResponse = () => new Promise((resolve) => { if (privateState.access) { // log('body', state.body) state.body.data.auth = privateState.auth state.body.data.env = environment state.QueryFunction[state.QueryFunctionName](state.body.data, databasePools).then(async (apiResponse: { [x: string]: any; hasOwnProperty?: any }) => { // If it's the auth endpoint, then authorise the user and send through if (state.fileNameArray[0] === 'Core' && state.fileNameArray[1] === 'Login') { if (apiResponse.success === false) { // Do nothing apiResponse = { ...apiResponse, auth: 'error', } } else { const { auth } = await createAuth(apiResponse) as any apiResponse = { ...apiResponse, auth, } } } // Deal with Prisma Errors if (apiResponse.error?.name?.toLowerCase().includes('prisma')) { state.devErrors.push(apiResponse?.message) apiResponse = { ...apiResponse, message: 'Prisma Error. See dev messages or server logs for details', } } let includeLength = 0 let excludeLength = 0 if (state.body.results !== undefined) { if (state.body.results.include !== undefined) includeLength = state.body.results.include.length if (state.body.results.exclude !== undefined) excludeLength = state.body.results.exclude.length // include results if (includeLength === 1) { if (apiResponse[state.body.results.include[0].key] !== undefined) { singleResult = true apiResponse = apiResponse[state.body.results.include[0].key] httpResponseObj.response = apiResponse[state.body.results.include[0].key] // log('Only 1 apiResponse:::: ', apiResponse) } } else if (includeLength > 1) { const newIncludeApiResponse = {} state.body.results.include.forEach((includeObj: { key: string | number; name: string | number }) => { if (apiResponse.hasOwnProperty([includeObj.key])) { newIncludeApiResponse[includeObj.name] = apiResponse[includeObj.key] } }) apiResponse = newIncludeApiResponse // log('Multiple apiResponses::::', apiResponse) } if (excludeLength > 0) { // exclude results state.body.results.exclude.forEach((excludeObj: { key: string | number }) => { if (apiResponse.hasOwnProperty([excludeObj.key])) { delete apiResponse[excludeObj.key] } }) // log('Excluded apiResponse::: ', apiResponse) } // Put the results into the state. And name it as required. if (state.body.results.name !== undefined) { state[state.body.results.name] = camelCaseResponse(apiResponse) httpResponseObj[state.body.results.name] = camelCaseResponse(apiResponse) if (singleResult) { httpResponseObj.response = apiResponse } } } else { state.results = camelCaseResponse(apiResponse) httpResponseObj.results = camelCaseResponse(apiResponse) } resolve(httpResponse(200, 'complete', true)) }).catch((e: Error) => { log('ERROR:::: Internal Function Error', e, showLogging) state.devErrors.push('ERROR:::: Internal Function Error') resolve(httpResponse(500, 'Internal Function Error', false, 'Internal Function Error')) }) } else { state.devErrors.push('Your access is denied. Login again if the function required authentication, or change the required auth to false.') resolve(httpResponse(401, 'Access Denied.', false)) throw new Error('No Access') } }) function completeCall({ hideLog, }: { hideLog?: boolean }) { response.writeHead( httpResponseObj.statusCode, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'x-jwt, content-type', 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload', }, ) let responseState = httpResponseObj.response if (!singleResult) { responseState = httpResponseObj if (httpResponseObj.success) { delete responseState.error } } if (!hideLog) responseLog(state, showLogging) // log('FINAL RESPONSE', responseState, showLogging) response.end(JSON.stringify(responseState)) } function setFileName() { const version = state.body.version.replace(/\./g, '_') state.QueryFunctionName = `${state.body.query}_${version}` state.fileNameArray = state.body.query.split('_') } try { if (request.url === '/healthcheck') { console.info('---Wappi Health Check - Success---') httpResponse(200, 'Healthy', true, 'Health Check Passed') return completeCall({ hideLog: true }) } if (request.method === 'POST') { const body: Buffer[] = [] if (wappiConfigFile.private) { // check to make sure that the domain from url is let validDomain: boolean = false wappiConfigFile.domains?.split(',').forEach((domain) => { if (request.headers.from === domain.trim()) validDomain = true }) if (!validDomain) { state.devErrors.push(`The domain (${request.headers.from}) you are using to access the API is not whitelisted. Add the domain to the wappi.json config file. Example: "domains": "my.domain.co, www.domain.co, domain.co".`) state.devErrors.push('Or change your API to be public in the wappi.json config file. Example: "private": false . WARNING: This means that any domain can reach your API without restriction.') httpResponse(403, 'Domain is not whitelisted', false, 'Domain Blocked') return completeCall({}) } } request.on('data', (chunk) => { body.push(chunk) }).on('end', () => { try { const data = Buffer.concat(body).toString() const parsedBody = JSON.parse(data) // Default the version if (!parsedBody.version) parsedBody.version = '1.0.0' // Check if URL is coming from a webhook if (request.url?.includes('/Webhook_')) { const url = request.url?.slice(1).split('_') state.body.version = `${url[2]}.${url[3]}.${url[4]}` state.body.query = `${url[0]}_${url[1]}` state.body.data = {} state.body.data.parsed = parsedBody state.body.data.headers = request.headers state.body.data.raw = data state.body.data.correlationId = v4() } else { state.body = parsedBody if (!state.body.data.correlationId) state.body.data.correlationId = v4() } } catch (error) { console.log('ERR 1', error) state.devErrors.push('Cannot Parse the body.data obj into JSON. Check the object structure in the data obj: { \'data\': {} }') httpResponse(500, 'Fail.. Query does not exist. Check the name and version', false, 'Query does not exist') } try { // Import the Function to be executed. setFileName() state.endpointPath = `${state.fileNameArray[0]}/${state.fileNameArray[1]}/${state.body.version}` state.folderPath = `${apiFolder}/${state.endpointPath}` initialLog(state, showLogging) state.QueryFunction = require(state.folderPath) // TODO:: Foreach object here, run the following.. return authenticateUser().then(() => callFunctionReturnResponse()).then(() => { httpResponse() return completeCall({}) }).catch((e) => { log('ERR 3', e, showLogging) state.devErrors.push('An error occurred when trying to authenticate and then call the function') httpResponse(403, 'Oops.. Query is protected or does not exist.', false, 'Query is private or does not exist') return completeCall({}) }) } catch (error) { log('ERR 1', error, showLogging) state.devErrors.push('Check the query name in the body JSON is correct. { \'query\': \'Function_Name\' }') state.devErrors.push('CORE ONLY:: There are no other functions') httpResponse(500, 'Fail.. Query does not exist. Check the name and version', false, 'Query does not exist') return completeCall({}) } }) } else if (request.method === 'OPTIONS') { httpResponse(200, 'Granted', true, 'Access Granted') return completeCall({}) } else { state.devErrors.push('You can ONLY use a POST http method. Change it to POST') httpResponse(405, 'Access Denied. POST Only.', false, 'Access Denied. POST Only') return completeCall({}) } } catch (error) { console.log('ERR 2', error) state.devErrors.push('Something major is not working. Ask Waz') httpResponse(500, 'Oops, Internal Server Error.', false, 'Internal Server Error') return completeCall({}) } }) // This starts the cron job ready to be run every 59 mins const onTick = () => { console.log('Auth Keys Updating @', new Date()) return ServerUtils_SetAuthKeys('authKeys.txt') } const job = new CronJob(wappiConfigFile.authRotation, onTick) if (job.running) { console.log('The cron job is already running!') } else { job.start() } let host: any = '0.0.0.0' const port = wappiConfigFile.port || 3000 if (environment === 'localhost') { host = 'localhost' } server.listen(port, host) console.log(colors.green(' ✓ '), colors.white('Compiled and Ready')) console.log( colors.black('... -.- .. .--.'), colors.white(` Environment : ${environment} Show Logs : ${showLogging} Auth Cron : ${ConvertCronToText(wappiConfigFile.authRotation)} Auth Start : ${new Date()} WAPPI : `), colors.white.underline(`http://${host}:${port}`), colors.green.underline(`http://${host}:${port}/healthcheck`), colors.blue.italic('\n.-- .- .--. .--. ..'), ) } let numCpu = cpus().length if (process.env.ENVIRONMENT === 'localhost') { // Set the Max CPUs allowed on dev to 1 numCpu = 1 } if (cluster.isPrimary) { for (let i = 0; i < numCpu; i += 1) { cluster.fork() } } else { startServer() }