diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index b4f9ebd..ccb2f70 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -539,7 +539,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ExecuteReturnJsonResponse' + anyOf: + - {$ref: '#/components/schemas/ExecuteReturnJsonResponse'} + - {type: string, format: byte} description: 'Execute SAS code.' summary: 'Run SAS Code and returns log' tags: @@ -1054,7 +1056,9 @@ paths: content: application/json: schema: - type: string + anyOf: + - {type: string} + - {type: string, format: byte} description: "Trigger a SAS program using it's location in the _program parameter.\nEnable debugging using the _debug parameter.\nAdditional URL parameters are turned into SAS macro variables.\nAny files provided are placed into the session and\ncorresponding _WEBIN_XXX variables are created." summary: 'Execute Stored Program, return raw content' tags: @@ -1078,7 +1082,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ExecuteReturnJsonResponse' + anyOf: + - {$ref: '#/components/schemas/ExecuteReturnJsonResponse'} + - {type: string, format: byte} examples: 'Example 1': value: {status: success, _webout: 'webout content', log: [], httpHeaders: {Content-type: application/zip, Cache-Control: 'public, max-age=1000'}} diff --git a/api/src/controllers/code.ts b/api/src/controllers/code.ts index c5c8e65..85e3d54 100644 --- a/api/src/controllers/code.ts +++ b/api/src/controllers/code.ts @@ -25,7 +25,7 @@ export class CodeController { public async executeSASCode( @Request() request: express.Request, @Body() body: ExecuteSASCodePayload - ): Promise { + ): Promise { return executeSASCode(request, body) } } @@ -41,6 +41,11 @@ const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => { true )) as ExecuteReturnJson + if (webout instanceof Buffer) { + ;(req as any).sasHeaders = httpHeaders + return webout + } + return { status: 'success', _webout: webout, diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index f452912..6856923 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -1,7 +1,13 @@ import path from 'path' import fs from 'fs' import { getSessionController } from './' -import { readFile, fileExists, createFile, moveFile } from '@sasjs/utils' +import { + readFile, + fileExists, + createFile, + moveFile, + readFileBinary +} from '@sasjs/utils' import { PreProgramVars, TreeNode } from '../../types' import { extractHeaders, @@ -16,12 +22,12 @@ export interface ExecutionVars { export interface ExecuteReturnRaw { httpHeaders: HTTPHeaders - result: string + result: string | Buffer } export interface ExecuteReturnJson { httpHeaders: HTTPHeaders - webout: string + webout: string | Buffer log?: string } @@ -138,15 +144,17 @@ ${program}` } const log = (await fileExists(logPath)) ? await readFile(logPath) : '' - const webout = (await fileExists(weboutPath)) - ? await readFile(weboutPath) - : '' const headersContent = (await fileExists(headersPath)) ? await readFile(headersPath) : '' - const httpHeaders: HTTPHeaders = headersContent - ? extractHeaders(headersContent) - : {} + const httpHeaders: HTTPHeaders = extractHeaders(headersContent) + const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type') + + const webout = (await fileExists(weboutPath)) + ? fileResponse + ? await readFileBinary(weboutPath) + : await readFile(weboutPath) + : '' const debugValue = typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug @@ -165,10 +173,11 @@ ${program}` return { httpHeaders, - result: - (debugValue && debugValue >= 131) || session.crashed - ? `${webout}

SAS Log

${log}
` - : webout + result: fileResponse + ? webout + : (debugValue && debugValue >= 131) || session.crashed + ? `${webout}

SAS Log

${log}
` + : webout } } diff --git a/api/src/controllers/stp.ts b/api/src/controllers/stp.ts index f5fff74..2335e5e 100644 --- a/api/src/controllers/stp.ts +++ b/api/src/controllers/stp.ts @@ -59,7 +59,7 @@ export class STPController { public async executeReturnRaw( @Request() request: express.Request, @Query() _program: string - ): Promise { + ): Promise { return executeReturnRaw(request, _program) } @@ -87,7 +87,7 @@ export class STPController { @Request() request: express.Request, @Body() body?: ExecuteReturnJsonPayload, @Query() _program?: string - ): Promise { + ): Promise { const program = _program ?? body?._program return executeReturnJson(request, program!) } @@ -96,7 +96,7 @@ export class STPController { const executeReturnRaw = async ( req: express.Request, _program: string -): Promise => { +): Promise => { const query = req.query as ExecutionVars const sasCodePath = path @@ -113,7 +113,11 @@ const executeReturnRaw = async ( req.res?.set(httpHeaders) - return result as string + if (result instanceof Buffer) { + ;(req as any).sasHeaders = httpHeaders + } + + return result } catch (err: any) { throw { code: 400, @@ -127,7 +131,7 @@ const executeReturnRaw = async ( const executeReturnJson = async ( req: any, _program: string -): Promise => { +): Promise => { const sasCodePath = path .join(getTmpFilesFolderPath(), _program) @@ -145,6 +149,11 @@ const executeReturnJson = async ( true )) as ExecuteReturnJson + if (webout instanceof Buffer) { + ;(req as any).sasHeaders = httpHeaders + return webout + } + return { status: 'success', _webout: webout, diff --git a/api/src/routes/api/code.ts b/api/src/routes/api/code.ts index fb751b5..efeaccd 100644 --- a/api/src/routes/api/code.ts +++ b/api/src/routes/api/code.ts @@ -12,6 +12,12 @@ runRouter.post('/execute', async (req, res) => { try { const response = await controller.executeSASCode(req, body) + + if (response instanceof Buffer) { + res.writeHead(200, (req as any).sasHeaders) + return res.end(response) + } + res.send(response) } catch (err: any) { const statusCode = err.code diff --git a/api/src/routes/api/stp.ts b/api/src/routes/api/stp.ts index 9879a7c..425208e 100644 --- a/api/src/routes/api/stp.ts +++ b/api/src/routes/api/stp.ts @@ -14,6 +14,12 @@ stpRouter.get('/execute', async (req, res) => { try { const response = await controller.executeReturnRaw(req, query._program) + + if (response instanceof Buffer) { + res.writeHead(200, (req as any).sasHeaders) + return res.end(response) + } + res.send(response) } catch (err: any) { const statusCode = err.code @@ -40,6 +46,12 @@ stpRouter.post( body, query?._program ) + + if (response instanceof Buffer) { + res.writeHead(200, (req as any).sasHeaders) + return res.end(response) + } + res.send(response) } catch (err: any) { const statusCode = err.code diff --git a/api/src/utils/extractHeaders.ts b/api/src/utils/extractHeaders.ts index e3812f0..bf64a14 100644 --- a/api/src/utils/extractHeaders.ts +++ b/api/src/utils/extractHeaders.ts @@ -4,20 +4,20 @@ export interface HTTPHeaders { [key: string]: string } -export const extractHeaders = (content: string): HTTPHeaders => { +export const extractHeaders = (content?: string): HTTPHeaders => { const headersObj: HTTPHeaders = {} const headersArr = content - .split('\n') + ?.split('\n') .map((line) => line.trim()) .filter((line) => !!line) - headersArr.forEach((headerStr) => { + headersArr?.forEach((headerStr) => { const [key, value] = headerStr.split(':').map((data) => data.trim()) if (value && headerUtils.validateHeader(key, value)) { - headersObj[key] = value + headersObj[key.toLowerCase()] = value } else { - delete headersObj[key] + delete headersObj[key.toLowerCase()] } }) diff --git a/api/src/utils/specs/extractHeaders.spec.ts b/api/src/utils/specs/extractHeaders.spec.ts index 4e6e467..9d29f04 100644 --- a/api/src/utils/specs/extractHeaders.spec.ts +++ b/api/src/utils/specs/extractHeaders.spec.ts @@ -12,8 +12,8 @@ describe('extractHeaders', () => { `) expect(headers).toEqual({ - 'Content-type': 'application/zip', - 'Cache-Control': 'public, max-age=1000' + 'content-type': 'application/zip', + 'cache-control': 'public, max-age=1000' }) }) @@ -25,7 +25,7 @@ describe('extractHeaders', () => { Content-type: `) - expect(headers).toEqual({ 'Cache-Control': 'public, max-age=1000' }) + expect(headers).toEqual({ 'cache-control': 'public, max-age=1000' }) }) it('should return only valid http headers', () => { @@ -37,4 +37,16 @@ describe('extractHeaders', () => { expect(headers).toEqual({}) }) + + it('should return http headers if empty', () => { + const headers = extractHeaders('') + + expect(headers).toEqual({}) + }) + + it('should return http headers if not provided', () => { + const headers = extractHeaders() + + expect(headers).toEqual({}) + }) })