diff --git a/api/package-lock.json b/api/package-lock.json index e670a4a..7c3b2bb 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -38,6 +38,7 @@ "@types/supertest": "^2.0.11", "@types/swagger-ui-express": "^4.1.3", "dotenv": "^10.0.0", + "http-headers-validation": "^0.0.1", "jest": "^27.0.6", "mongodb-memory-server": "^8.0.0", "nodemon": "^2.0.7", @@ -4651,6 +4652,12 @@ "node": ">= 0.6" } }, + "node_modules/http-headers-validation": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/http-headers-validation/-/http-headers-validation-0.0.1.tgz", + "integrity": "sha1-0xUbTFjQjySTSnbKgYVIzPBa+3g=", + "dev": true + }, "node_modules/http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", @@ -13747,6 +13754,12 @@ "toidentifier": "1.0.0" } }, + "http-headers-validation": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/http-headers-validation/-/http-headers-validation-0.0.1.tgz", + "integrity": "sha1-0xUbTFjQjySTSnbKgYVIzPBa+3g=", + "dev": true + }, "http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", diff --git a/api/package.json b/api/package.json index 1232e07..02ba0b2 100644 --- a/api/package.json +++ b/api/package.json @@ -70,6 +70,7 @@ "@types/supertest": "^2.0.11", "@types/swagger-ui-express": "^4.1.3", "dotenv": "^10.0.0", + "http-headers-validation": "^0.0.1", "jest": "^27.0.6", "mongodb-memory-server": "^8.0.0", "nodemon": "^2.0.7", diff --git a/api/src/controllers/code.ts b/api/src/controllers/code.ts index 52e3266..939f098 100644 --- a/api/src/controllers/code.ts +++ b/api/src/controllers/code.ts @@ -1,6 +1,6 @@ import express from 'express' import { Request, Security, Route, Tags, Post, Body } from 'tsoa' -import { ExecutionController } from './internal' +import { ExecuteReturnRaw, ExecutionController } from './internal' import { PreProgramVars } from '../types' interface ExecuteSASCodePayload { @@ -30,13 +30,13 @@ export class CodeController { const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => { try { - const result = await new ExecutionController().executeProgram( + const { result } = (await new ExecutionController().executeProgram( code, getPreProgramVariables(req), { ...req.query, _debug: 131 }, undefined, true - ) + )) as ExecuteReturnRaw return result as string } catch (err: any) { diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index 0a02f9f..f452912 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -3,12 +3,28 @@ import fs from 'fs' import { getSessionController } from './' import { readFile, fileExists, createFile, moveFile } from '@sasjs/utils' import { PreProgramVars, TreeNode } from '../../types' -import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils' +import { + extractHeaders, + generateFileUploadSasCode, + getTmpFilesFolderPath, + HTTPHeaders +} from '../../utils' export interface ExecutionVars { [key: string]: string | number | undefined } +export interface ExecuteReturnRaw { + httpHeaders: HTTPHeaders + result: string +} + +export interface ExecuteReturnJson { + httpHeaders: HTTPHeaders + webout: string + log?: string +} + export class ExecutionController { async executeFile( programPath: string, @@ -30,13 +46,14 @@ export class ExecutionController { returnJson ) } + async executeProgram( program: string, preProgramVariables: PreProgramVars, vars: ExecutionVars, otherArgs?: any, returnJson?: boolean - ) { + ): Promise { const sessionController = getSessionController() const session = await sessionController.getSession() @@ -44,11 +61,11 @@ export class ExecutionController { session.consumed = true const logPath = path.join(session.path, 'log.log') - + const headersPath = path.join(session.path, 'stpsrv_header.txt') const weboutPath = path.join(session.path, 'webout.txt') - await createFile(weboutPath, '') - const tokenFile = path.join(session.path, 'accessToken.txt') + + await createFile(weboutPath, '') await createFile( tokenFile, preProgramVariables?.accessToken ?? 'accessToken' @@ -124,6 +141,12 @@ ${program}` const webout = (await fileExists(weboutPath)) ? await readFile(weboutPath) : '' + const headersContent = (await fileExists(headersPath)) + ? await readFile(headersPath) + : '' + const httpHeaders: HTTPHeaders = headersContent + ? extractHeaders(headersContent) + : {} const debugValue = typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug @@ -133,15 +156,20 @@ ${program}` if (returnJson) { return { + httpHeaders, webout, log: (debugValue && debugValue >= 131) || session.crashed ? log : undefined } } - return (debugValue && debugValue >= 131) || session.crashed - ? `${webout}

SAS Log

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

SAS Log

${log}
` + : webout + } } buildDirectoryTree() { diff --git a/api/src/controllers/stp.ts b/api/src/controllers/stp.ts index fd94ce7..8d41315 100644 --- a/api/src/controllers/stp.ts +++ b/api/src/controllers/stp.ts @@ -1,7 +1,12 @@ import express from 'express' import path from 'path' import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa' -import { ExecutionController, ExecutionVars } from './internal' +import { + ExecuteReturnJson, + ExecuteReturnRaw, + ExecutionController, + ExecutionVars +} from './internal' import { PreProgramVars } from '../types' import { getTmpFilesFolderPath, makeFilesNamesMap } from '../utils' @@ -73,11 +78,16 @@ const executeReturnRaw = async ( .replace(new RegExp('/', 'g'), path.sep) + '.sas' try { - const result = await new ExecutionController().executeFile( - sasCodePath, - getPreProgramVariables(req), - query - ) + const { result, httpHeaders } = + (await new ExecutionController().executeFile( + sasCodePath, + getPreProgramVariables(req), + query + )) as ExecuteReturnRaw + + Object.entries(httpHeaders ?? {}).forEach(([key, value]) => { + req.res?.set(key, value) + }) return result as string } catch (err: any) { @@ -102,16 +112,22 @@ const executeReturnJson = async ( const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null try { - const { webout, log } = (await new ExecutionController().executeFile( - sasCodePath, - getPreProgramVariables(req), - { ...req.query, ...req.body }, - { filesNamesMap: filesNamesMap }, - true - )) as { webout: string; log: string } + const { webout, log, httpHeaders } = + (await new ExecutionController().executeFile( + sasCodePath, + getPreProgramVariables(req), + { ...req.query, ...req.body }, + { filesNamesMap: filesNamesMap }, + true + )) as ExecuteReturnJson + + Object.entries(httpHeaders ?? {}).forEach(([key, value]) => { + req.res?.set(key, value) + }) + return { status: 'success', - _webout: webout, + _webout: webout as string, log } } catch (err: any) { diff --git a/api/src/server.ts b/api/src/server.ts index 927f047..6cf0e42 100644 --- a/api/src/server.ts +++ b/api/src/server.ts @@ -7,6 +7,8 @@ appPromise.then(async (app) => { const protocol = process.env.PROTOCOL ?? 'http' const sasJsPort = process.env.PORT ?? 5000 + console.log('PROTOCOL: ', protocol) + if (protocol !== 'https') { app.listen(sasJsPort, () => { console.log( diff --git a/api/src/utils/extractHeaders.ts b/api/src/utils/extractHeaders.ts new file mode 100644 index 0000000..05a4207 --- /dev/null +++ b/api/src/utils/extractHeaders.ts @@ -0,0 +1,25 @@ +const headerUtils = require('http-headers-validation') + +export interface HTTPHeaders { + [key: string]: string | undefined +} + +export const extractHeaders = (content: string): HTTPHeaders => { + const headersObj: HTTPHeaders = {} + const headersArr = content + .split('\n') + .map((line) => line.trim()) + .filter((line) => !!line) + + headersArr.forEach((headerStr) => { + const [key, value] = headerStr.split(':').map((data) => data.trim()) + + if (value && headerUtils.validateHeader(key, value)) { + headersObj[key] = value + } else { + delete headersObj[key] + } + }) + + return headersObj +} diff --git a/api/src/utils/getCertificates.ts b/api/src/utils/getCertificates.ts index 3fbc22b..e1e96e7 100644 --- a/api/src/utils/getCertificates.ts +++ b/api/src/utils/getCertificates.ts @@ -7,6 +7,9 @@ export const getCertificates = async () => { const keyPath = PRIVATE_KEY ?? (await getFileInput('Private Key (PEM)')) const certPath = FULL_CHAIN ?? (await getFileInput('Full Chain (PEM)')) + console.log('keyPath: ', keyPath) + console.log('certPath: ', certPath) + const key = await readFile(keyPath) const cert = await readFile(certPath) diff --git a/api/src/utils/index.ts b/api/src/utils/index.ts index 746bcd8..4f78125 100644 --- a/api/src/utils/index.ts +++ b/api/src/utils/index.ts @@ -1,4 +1,5 @@ export * from './connectDB' +export * from './extractHeaders' export * from './file' export * from './generateAccessToken' export * from './generateAuthCode' diff --git a/api/src/utils/specs/extractHeaders.spec.ts b/api/src/utils/specs/extractHeaders.spec.ts new file mode 100644 index 0000000..4e6e467 --- /dev/null +++ b/api/src/utils/specs/extractHeaders.spec.ts @@ -0,0 +1,40 @@ +import { extractHeaders } from '..' + +describe('extractHeaders', () => { + it('should return valid http headers', () => { + const headers = extractHeaders(` + Content-type: application/csv + Cache-Control: public, max-age=2000 + Content-type: application/text + Cache-Control: public, max-age=1500 + Content-type: application/zip + Cache-Control: public, max-age=1000 + `) + + expect(headers).toEqual({ + 'Content-type': 'application/zip', + 'Cache-Control': 'public, max-age=1000' + }) + }) + + it('should not return http headers if last occurrence is blank', () => { + const headers = extractHeaders(` + Content-type: application/csv + Cache-Control: public, max-age=1000 + Content-type: application/text + Content-type: + `) + + expect(headers).toEqual({ 'Cache-Control': 'public, max-age=1000' }) + }) + + it('should return only valid http headers', () => { + const headers = extractHeaders(` + Content-type[]: application/csv + Content//-type: application/text + Content()-type: application/zip + `) + + expect(headers).toEqual({}) + }) +})