diff --git a/.gitignore b/.gitignore index bb6d85e..9c567ce 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,6 @@ node_modules/ .env* sas/ sasjs_root/ -api/mocks/custom/* -!api/mocks/custom/.keep tmp/ build/ sasjsbuild/ diff --git a/README.md b/README.md index 7b1c173..4a1d1a6 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,11 @@ PORT= # If not present, mocking function is disabled MOCK_SERVERTYPE= +# default: /api/mocks +# Path to mocking folder, for generic responses, it's sub directories should be: sas9, viya, sasjs +# Server will automatically use subdirectory accordingly +STATIC_MOCK_LOCATION= + # ## Additional SAS Options # diff --git a/api/mocks/custom/.keep b/api/mocks/custom/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/api/mocks/generic/sas9/logged-in b/api/mocks/sas9/generic/logged-in similarity index 100% rename from api/mocks/generic/sas9/logged-in rename to api/mocks/sas9/generic/logged-in diff --git a/api/mocks/generic/sas9/logged-out b/api/mocks/sas9/generic/logged-out similarity index 100% rename from api/mocks/generic/sas9/logged-out rename to api/mocks/sas9/generic/logged-out diff --git a/api/mocks/generic/sas9/login b/api/mocks/sas9/generic/login similarity index 91% rename from api/mocks/generic/sas9/login rename to api/mocks/sas9/generic/login index a34b391..0e75b57 100644 --- a/api/mocks/generic/sas9/login +++ b/api/mocks/sas9/generic/login @@ -9,7 +9,7 @@
- + diff --git a/api/mocks/generic/sas9/public-access-denied b/api/mocks/sas9/generic/public-access-denied similarity index 100% rename from api/mocks/generic/sas9/public-access-denied rename to api/mocks/sas9/generic/public-access-denied diff --git a/api/mocks/generic/sas9/sas-stored-process b/api/mocks/sas9/generic/sas-stored-process similarity index 100% rename from api/mocks/generic/sas9/sas-stored-process rename to api/mocks/sas9/generic/sas-stored-process diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index e96e7c4..b7df0ab 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -28,6 +28,7 @@ interface ExecuteFileParams { returnJson?: boolean session?: Session runTime: RunTimeType + forceStringResult?: boolean } interface ExecuteProgramParams extends Omit { @@ -42,7 +43,8 @@ export class ExecutionController { otherArgs, returnJson, session, - runTime + runTime, + forceStringResult }: ExecuteFileParams) { const program = await readFile(programPath) @@ -53,7 +55,8 @@ export class ExecutionController { otherArgs, returnJson, session, - runTime + runTime, + forceStringResult }) } @@ -63,7 +66,8 @@ export class ExecutionController { vars, otherArgs, session: sessionByFileUpload, - runTime + runTime, + forceStringResult }: ExecuteProgramParams): Promise { const sessionController = getSessionController(runTime) @@ -104,7 +108,7 @@ export class ExecutionController { const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type') const webout = (await fileExists(weboutPath)) - ? fileResponse + ? fileResponse && !forceStringResult ? await readFileBinary(weboutPath) : await readFile(weboutPath) : '' diff --git a/api/src/controllers/internal/processProgram.ts b/api/src/controllers/internal/processProgram.ts index ca4a9d4..ff52c9d 100644 --- a/api/src/controllers/internal/processProgram.ts +++ b/api/src/controllers/internal/processProgram.ts @@ -110,17 +110,13 @@ export const processProgram = async ( // create a stream that will write to console outputs to log file const writeStream = fs.createWriteStream(logPath) - // waiting for the open event so that we can have underlying file descriptor await once(writeStream, 'open') - execFileSync(executablePath, [codePath], { stdio: ['ignore', writeStream, writeStream] }) - // copy the code file to log and end write stream writeStream.end(program) - session.completed = true console.log('session completed', session) } catch (err: any) { diff --git a/api/src/controllers/mock-sas9.ts b/api/src/controllers/mock-sas9.ts index 8a78778..d4f7282 100644 --- a/api/src/controllers/mock-sas9.ts +++ b/api/src/controllers/mock-sas9.ts @@ -2,6 +2,16 @@ import { readFile } from '@sasjs/utils' import express from 'express' import path from 'path' import { Request, Post, Get } from 'tsoa' +import dotenv from 'dotenv' +import { ExecutionController } from './internal' +import { + getPreProgramVariables, + getRunTimeAndFilePath, + makeFilesNamesMap +} from '../utils' +import { MulterFile } from '../types/Upload' + +dotenv.config() export interface Sas9Response { content: string @@ -16,9 +26,17 @@ export interface MockFileRead { export class MockSas9Controller { private loggedIn: string | undefined + private mocksPath = process.env.STATIC_MOCK_LOCATION || 'mocks' @Get('/SASStoredProcess') - public async sasStoredProcess(): Promise { + public async sasStoredProcess( + @Request() req: express.Request + ): Promise { + const username = req.query._username?.toString() || undefined + const password = req.query._password?.toString() || undefined + + if (username && password) this.loggedIn = req.body.username + if (!this.loggedIn) { return { content: '', @@ -26,17 +44,87 @@ export class MockSas9Controller { } } + let program = req.query._program?.toString() || undefined + const filePath: string[] = program + ? program.replace('/', '').split('/') + : ['generic', 'sas-stored-process'] + + if (program) { + return await getMockResponseFromFile([ + process.cwd(), + this.mocksPath, + 'sas9', + ...filePath + ]) + } + return await getMockResponseFromFile([ process.cwd(), 'mocks', - 'generic', 'sas9', - 'sas-stored-process' + ...filePath + ]) + } + + @Get('/SASStoredProcess/do') + public async sasStoredProcessDoGet( + @Request() req: express.Request + ): Promise { + const username = req.query._username?.toString() || undefined + const password = req.query._password?.toString() || undefined + + if (username && password) this.loggedIn = username + + if (!this.loggedIn) { + return { + content: '', + redirect: '/SASLogon/login' + } + } + + const program = req.query._program ?? req.body?._program + const filePath: string[] = ['generic', 'sas-stored-process'] + + if (program) { + const vars = { ...req.query, ...req.body, _requestMethod: req.method } + const otherArgs = {} + + try { + const { codePath, runTime } = await getRunTimeAndFilePath( + program + '.js' + ) + + const result = await new ExecutionController().executeFile({ + programPath: codePath, + preProgramVariables: getPreProgramVariables(req), + vars: vars, + otherArgs: otherArgs, + runTime, + forceStringResult: true + }) + + return { + content: result.result as string + } + } catch (err) { + console.log('err', err) + } + + return { + content: 'No webout returned.' + } + } + + return await getMockResponseFromFile([ + process.cwd(), + 'mocks', + 'sas9', + ...filePath ]) } @Post('/SASStoredProcess/do/') - public async sasStoredProcessDo( + public async sasStoredProcessDoPost( @Request() req: express.Request ): Promise { if (!this.loggedIn) { @@ -53,23 +141,38 @@ export class MockSas9Controller { } } - let program = req.query._program?.toString() || '' - program = program.replace('/', '') + const program = req.query._program ?? req.body?._program + const vars = { + ...req.query, + ...req.body, + _requestMethod: req.method, + _driveLoc: process.driveLoc + } + const filesNamesMap = req.files?.length + ? makeFilesNamesMap(req.files as MulterFile[]) + : null + const otherArgs = { filesNamesMap: filesNamesMap } + const { codePath, runTime } = await getRunTimeAndFilePath(program + '.js') + try { + const result = await new ExecutionController().executeFile({ + programPath: codePath, + preProgramVariables: getPreProgramVariables(req), + vars: vars, + otherArgs: otherArgs, + runTime, + session: req.sasjsSession, + forceStringResult: true + }) - const content = await getMockResponseFromFile([ - process.cwd(), - 'mocks', - ...program.split('/') - ]) - - if (content.error) { - return content + return { + content: result.result as string + } + } catch (err) { + console.log('err', err) } - const parsedContent = parseJsonIfValid(content.content) - return { - content: parsedContent + content: 'No webout returned.' } } @@ -85,8 +188,8 @@ export class MockSas9Controller { return await getMockResponseFromFile([ process.cwd(), 'mocks', - 'generic', 'sas9', + 'generic', 'logged-in' ]) } @@ -95,21 +198,27 @@ export class MockSas9Controller { return await getMockResponseFromFile([ process.cwd(), 'mocks', - 'generic', 'sas9', + 'generic', 'login' ]) } @Post('/SASLogon/login') public async loginPost(req: express.Request): Promise { + if (req.body.lt && req.body.lt !== 'validtoken') + return { + content: '', + redirect: '/SASLogon/login' + } + this.loggedIn = req.body.username return await getMockResponseFromFile([ process.cwd(), 'mocks', - 'generic', 'sas9', + 'generic', 'logged-in' ]) } @@ -122,8 +231,8 @@ export class MockSas9Controller { return await getMockResponseFromFile([ process.cwd(), 'mocks', - 'generic', 'sas9', + 'generic', 'public-access-denied' ]) } @@ -131,8 +240,8 @@ export class MockSas9Controller { return await getMockResponseFromFile([ process.cwd(), 'mocks', - 'generic', 'sas9', + 'generic', 'logged-out' ]) } @@ -152,23 +261,6 @@ export class MockSas9Controller { private isPublicAccount = () => this.loggedIn?.toLowerCase() === 'public' } -/** - * If JSON is valid it will be parsed otherwise will return text unaltered - * @param content string to be parsed - * @returns JSON or string - */ -const parseJsonIfValid = (content: string) => { - let fileContent = '' - - try { - fileContent = JSON.parse(content) - } catch (err: any) { - fileContent = content - } - - return fileContent -} - const getMockResponseFromFile = async ( filePath: string[] ): Promise => { diff --git a/api/src/routes/setupRoutes.ts b/api/src/routes/setupRoutes.ts index a31aae6..a189d1d 100644 --- a/api/src/routes/setupRoutes.ts +++ b/api/src/routes/setupRoutes.ts @@ -15,5 +15,5 @@ export const setupRoutes = (app: Express) => { appStreamRouter(req, res, next) }) - app.use('/', csrfProtection, webRouter) + app.use('/', webRouter) } diff --git a/api/src/routes/web/index.ts b/api/src/routes/web/index.ts index 7bd4b82..bcd3aa1 100644 --- a/api/src/routes/web/index.ts +++ b/api/src/routes/web/index.ts @@ -3,6 +3,7 @@ import sas9WebRouter from './sas9-web' import sasViyaWebRouter from './sasviya-web' import webRouter from './web' import { MOCK_SERVERTYPEType } from '../../utils' +import { csrfProtection } from '../../middlewares' const router = express.Router() @@ -18,7 +19,7 @@ switch (MOCK_SERVERTYPE) { break } default: { - router.use('/', webRouter) + router.use('/', csrfProtection, webRouter) } } diff --git a/api/src/routes/web/sas9-web.ts b/api/src/routes/web/sas9-web.ts index f48577a..ce9cddd 100644 --- a/api/src/routes/web/sas9-web.ts +++ b/api/src/routes/web/sas9-web.ts @@ -2,12 +2,25 @@ import express from 'express' import { generateCSRFToken } from '../../middlewares' import { WebController } from '../../controllers' import { MockSas9Controller } from '../../controllers/mock-sas9' +import multer from 'multer' +import path from 'path' +import dotenv from 'dotenv' +import { FileUploadController } from '../../controllers/internal' + +dotenv.config() const sas9WebRouter = express.Router() const webController = new WebController() // Mock controller must be singleton because it keeps the states // for example `isLoggedIn` and potentially more in future mocks const controller = new MockSas9Controller() +const fileUploadController = new FileUploadController() + +const mockPath = process.env.STATIC_MOCK_LOCATION || 'mocks' + +const upload = multer({ + dest: path.join(process.cwd(), mockPath, 'sas9', 'files-received') +}) sas9WebRouter.get('/', async (req, res) => { let response @@ -27,7 +40,7 @@ sas9WebRouter.get('/', async (req, res) => { }) sas9WebRouter.get('/SASStoredProcess', async (req, res) => { - const response = await controller.sasStoredProcess() + const response = await controller.sasStoredProcess(req) if (response.redirect) { res.redirect(response.redirect) @@ -41,8 +54,8 @@ sas9WebRouter.get('/SASStoredProcess', async (req, res) => { } }) -sas9WebRouter.post('/SASStoredProcess/do/', async (req, res) => { - const response = await controller.sasStoredProcessDo(req) +sas9WebRouter.get('/SASStoredProcess/do/', async (req, res) => { + const response = await controller.sasStoredProcessDoGet(req) if (response.redirect) { res.redirect(response.redirect) @@ -56,6 +69,26 @@ sas9WebRouter.post('/SASStoredProcess/do/', async (req, res) => { } }) +sas9WebRouter.post( + '/SASStoredProcess/do/', + fileUploadController.preUploadMiddleware, + fileUploadController.getMulterUploadObject().any(), + async (req, res) => { + const response = await controller.sasStoredProcessDoPost(req) + + if (response.redirect) { + res.redirect(response.redirect) + return + } + + try { + res.send(response.content) + } catch (err: any) { + res.status(403).send(err.toString()) + } + } +) + sas9WebRouter.get('/SASLogon/login', async (req, res) => { const response = await controller.loginGet() diff --git a/api/src/utils/getPreProgramVariables.ts b/api/src/utils/getPreProgramVariables.ts index 396fa9f..5e2d26a 100644 --- a/api/src/utils/getPreProgramVariables.ts +++ b/api/src/utils/getPreProgramVariables.ts @@ -18,10 +18,12 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => { if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`) + //In desktop mode when mocking mode is enabled, user was undefined. + //So this is workaround. return { - username: user!.username, - userId: user!.userId, - displayName: user!.displayName, + username: user ? user.username : 'demo', + userId: user ? user.userId : 0, + displayName: user ? user.displayName : 'demo', serverUrl: protocol + host, httpHeaders } diff --git a/api/src/utils/setProcessVariables.ts b/api/src/utils/setProcessVariables.ts index 395e5fc..574508b 100644 --- a/api/src/utils/setProcessVariables.ts +++ b/api/src/utils/setProcessVariables.ts @@ -32,7 +32,6 @@ export const setProcessVariables = async () => { process.rLoc = process.env.R_PATH } else { const { sasLoc, nodeLoc, pythonLoc, rLoc } = await getDesktopFields() - process.sasLoc = sasLoc process.nodeLoc = nodeLoc process.pythonLoc = pythonLoc