mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
1 Commits
v0.8.2
...
testing-ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1a02c0da5 |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,25 +1,3 @@
|
||||
## [0.8.2](https://github.com/sasjs/server/compare/v0.8.1...v0.8.2) (2022-06-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* getRuntimeAndFilePath function to handle the scenarion when path is provided with an extension other than runtimes ([5cc85b5](https://github.com/sasjs/server/commit/5cc85b57f80b13296156811fe966d7b37d45f213))
|
||||
|
||||
## [0.8.1](https://github.com/sasjs/server/compare/v0.8.0...v0.8.1) (2022-06-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make CA_ROOT optional in getCertificates method ([1b5859e](https://github.com/sasjs/server/commit/1b5859ee37ae73c419115b9debfd5141a79733de))
|
||||
* update /logout route to /SASLogon/logout ([65380be](https://github.com/sasjs/server/commit/65380be2f3945bae559f1749064845b514447a53))
|
||||
|
||||
# [0.8.0](https://github.com/sasjs/server/compare/v0.7.3...v0.8.0) (2022-06-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **certs:** ENV variables updated and set CA Root for HTTPS server ([2119e9d](https://github.com/sasjs/server/commit/2119e9de9ab1e5ce1222658f554ac74f4f35cf4d))
|
||||
|
||||
## [0.7.3](https://github.com/sasjs/server/compare/v0.7.2...v0.7.3) (2022-06-20)
|
||||
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
## Issue
|
||||
|
||||
Link any related issue(s) in this section.
|
||||
|
||||
## Intent
|
||||
|
||||
What this PR intends to achieve.
|
||||
|
||||
## Implementation
|
||||
|
||||
What code changes have been made to achieve the intent.
|
||||
|
||||
## Checks
|
||||
|
||||
- [ ] Code is formatted correctly (`npm run lint:fix`).
|
||||
- [ ] Any new functionality has been unit tested.
|
||||
- [ ] All unit tests are passing (`npm test`).
|
||||
- [ ] All CI checks are green.
|
||||
- [ ] Reviewer is assigned.
|
||||
@@ -99,10 +99,9 @@ SASV9_OPTIONS= -NOXCMD
|
||||
## Additional Web Server Options
|
||||
#
|
||||
|
||||
# ENV variables for PROTOCOL: `https`
|
||||
PRIVATE_KEY=privkey.pem (required)
|
||||
CERT_CHAIN=certificate.pem (required)
|
||||
CA_ROOT=fullchain.pem (optional)
|
||||
# ENV variables required for PROTOCOL: `https`
|
||||
PRIVATE_KEY=privkey.pem
|
||||
FULL_CHAIN=fullchain.pem
|
||||
|
||||
# ENV variables required for MODE: `server`
|
||||
ACCESS_TOKEN_SECRET=<secret>
|
||||
|
||||
@@ -4,8 +4,7 @@ WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
|
||||
|
||||
PROTOCOL=[http|https] default considered as http
|
||||
PRIVATE_KEY=privkey.pem
|
||||
CERT_CHAIN=certificate.pem
|
||||
CA_ROOT=fullchain.pem
|
||||
FULL_CHAIN=fullchain.pem
|
||||
|
||||
PORT=[5000] default value is 5000
|
||||
|
||||
|
||||
@@ -624,7 +624,7 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthorizePayload'
|
||||
/SASLogon/logout:
|
||||
/logout:
|
||||
get:
|
||||
operationId: Logout
|
||||
responses:
|
||||
@@ -633,7 +633,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
summary: 'Destroy the session stored in cookies'
|
||||
summary: 'Accept a valid username/password'
|
||||
tags:
|
||||
- Web
|
||||
security: []
|
||||
@@ -763,7 +763,7 @@ paths:
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: failure, message: 'Deployment failed!'}
|
||||
description: "Accepts JSON file and zipped compressed JSON file as well.\nCompressed file should only contain one JSON file and should have same name\nas of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip\nAny other file or JSON file in zipped will be ignored!"
|
||||
description: "Accepts JSON file and zipped compressed JSON file as well.\r\nCompressed file should only contain one JSON file and should have same name\r\nas of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip\r\nAny other file or JSON file in zipped will be ignored!"
|
||||
summary: 'Creates/updates files within SASjs Drive using uploaded JSON/compressed JSON file.'
|
||||
tags:
|
||||
- Drive
|
||||
@@ -851,7 +851,7 @@ paths:
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: failure, message: 'File request failed.'}
|
||||
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
|
||||
description: "It's optional to either provide `_filePath` in url as query parameter\r\nOr provide `filePath` in body as form field.\r\nBut it's required to provide else API will respond with Bad Request."
|
||||
summary: 'Create a file in SASjs Drive'
|
||||
tags:
|
||||
- Drive
|
||||
@@ -902,7 +902,7 @@ paths:
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: failure, message: 'File request failed.'}
|
||||
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
|
||||
description: "It's optional to either provide `_filePath` in url as query parameter\r\nOr provide `filePath` in body as form field.\r\nBut it's required to provide else API will respond with Bad Request."
|
||||
summary: 'Modify a file in SASjs Drive'
|
||||
tags:
|
||||
- Drive
|
||||
@@ -1454,7 +1454,7 @@ paths:
|
||||
anyOf:
|
||||
- {type: string}
|
||||
- {type: string, format: byte}
|
||||
description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
|
||||
description: "Trigger a SAS or JS program using the _program URL parameter.\r\n\r\nAccepts URL parameters and file uploads. For more details, see docs:\r\n\r\nhttps://server.sasjs.io/storedprograms"
|
||||
summary: 'Execute a Stored Program, returns raw _webout content.'
|
||||
tags:
|
||||
- STP
|
||||
@@ -1482,7 +1482,7 @@ paths:
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: success, _webout: 'webout content', log: [], httpHeaders: {Content-type: application/zip, Cache-Control: 'public, max-age=1000'}}
|
||||
description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms\n\nThe response will be a JSON object with the following root attributes:\nlog, webout, headers.\n\nThe webout attribute will be nested JSON ONLY if the response-header\ncontains a content-type of application/json AND it is valid JSON.\nOtherwise it will be a stringified version of the webout content."
|
||||
description: "Trigger a SAS or JS program using the _program URL parameter.\r\n\r\nAccepts URL parameters and file uploads. For more details, see docs:\r\n\r\nhttps://server.sasjs.io/storedprograms\r\n\r\nThe response will be a JSON object with the following root attributes:\r\nlog, webout, headers.\r\n\r\nThe webout attribute will be nested JSON ONLY if the response-header\r\ncontains a content-type of application/json AND it is valid JSON.\r\nOtherwise it will be a stringified version of the webout content."
|
||||
summary: 'Execute a Stored Program, return a JSON object'
|
||||
tags:
|
||||
- STP
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { getSessionController, processProgram } from './'
|
||||
import {
|
||||
getSASSessionController,
|
||||
getJSSessionController,
|
||||
processProgram
|
||||
} from './'
|
||||
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
|
||||
import { PreProgramVars, Session, TreeNode } from '../../types'
|
||||
import {
|
||||
@@ -72,7 +76,10 @@ export class ExecutionController {
|
||||
session: sessionByFileUpload,
|
||||
runTime
|
||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
||||
const sessionController = getSessionController(runTime)
|
||||
const sessionController =
|
||||
runTime === RunTimeType.SAS
|
||||
? getSASSessionController()
|
||||
: getJSSessionController()
|
||||
|
||||
const session =
|
||||
sessionByFileUpload ?? (await sessionController.getSession())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, RequestHandler } from 'express'
|
||||
import multer from 'multer'
|
||||
import { uuidv4 } from '@sasjs/utils'
|
||||
import { getSessionController } from '.'
|
||||
import { getSASSessionController, getJSSessionController } from '.'
|
||||
import {
|
||||
executeProgramRawValidation,
|
||||
getRunTimeAndFilePath,
|
||||
@@ -37,23 +37,17 @@ export class FileUploadController {
|
||||
try {
|
||||
;({ runTime } = await getRunTimeAndFilePath(programPath))
|
||||
} catch (err: any) {
|
||||
return res.status(400).send({
|
||||
res.status(400).send({
|
||||
status: 'failure',
|
||||
message: 'Job execution failed',
|
||||
error: typeof err === 'object' ? err.toString() : err
|
||||
})
|
||||
}
|
||||
|
||||
let sessionController
|
||||
try {
|
||||
sessionController = getSessionController(runTime)
|
||||
} catch (err: any) {
|
||||
return res.status(400).send({
|
||||
status: 'failure',
|
||||
message: err.message,
|
||||
error: typeof err === 'object' ? err.toString() : err
|
||||
})
|
||||
}
|
||||
const sessionController =
|
||||
runTime === RunTimeType.SAS
|
||||
? getSASSessionController()
|
||||
: getJSSessionController()
|
||||
|
||||
const session = await sessionController.getSession()
|
||||
// marking consumed true, so that it's not available
|
||||
|
||||
@@ -5,16 +5,14 @@ import { execFile } from 'child_process'
|
||||
import {
|
||||
getSessionsFolder,
|
||||
generateUniqueFileName,
|
||||
sysInitCompiledPath,
|
||||
RunTimeType
|
||||
sysInitCompiledPath
|
||||
} from '../../utils'
|
||||
import {
|
||||
deleteFolder,
|
||||
createFile,
|
||||
fileExists,
|
||||
generateTimestamp,
|
||||
readFile,
|
||||
isWindows
|
||||
readFile
|
||||
} from '@sasjs/utils'
|
||||
|
||||
const execFilePromise = promisify(execFile)
|
||||
@@ -90,7 +88,7 @@ ${autoExecContent}`
|
||||
|
||||
// Additional windows specific options to avoid the desktop popups.
|
||||
|
||||
execFilePromise(process.sasLoc!, [
|
||||
execFilePromise(process.sasLoc, [
|
||||
'-SYSIN',
|
||||
codePath,
|
||||
'-LOG',
|
||||
@@ -101,9 +99,9 @@ ${autoExecContent}`
|
||||
session.path,
|
||||
'-AUTOEXEC',
|
||||
autoExecPath,
|
||||
isWindows() ? '-nosplash' : '',
|
||||
isWindows() ? '-icon' : '',
|
||||
isWindows() ? '-nologo' : ''
|
||||
process.platform === 'win32' ? '-nosplash' : '',
|
||||
process.platform === 'win32' ? '-icon' : '',
|
||||
process.platform === 'win32' ? '-nologo' : ''
|
||||
])
|
||||
.then(() => {
|
||||
session.completed = true
|
||||
@@ -194,21 +192,7 @@ export class JSSessionController extends SessionController {
|
||||
}
|
||||
}
|
||||
|
||||
export const getSessionController = (
|
||||
runTime: RunTimeType
|
||||
): SASSessionController | JSSessionController => {
|
||||
if (runTime === RunTimeType.SAS) {
|
||||
return getSASSessionController()
|
||||
}
|
||||
|
||||
if (runTime === RunTimeType.JS) {
|
||||
return getJSSessionController()
|
||||
}
|
||||
|
||||
throw new Error('No Runtime is configured')
|
||||
}
|
||||
|
||||
const getSASSessionController = (): SASSessionController => {
|
||||
export const getSASSessionController = (): SASSessionController => {
|
||||
if (process.sasSessionController) return process.sasSessionController
|
||||
|
||||
process.sasSessionController = new SASSessionController()
|
||||
@@ -216,7 +200,7 @@ const getSASSessionController = (): SASSessionController => {
|
||||
return process.sasSessionController
|
||||
}
|
||||
|
||||
const getJSSessionController = (): JSSessionController => {
|
||||
export const getJSSessionController = (): JSSessionController => {
|
||||
if (process.jsSessionController) return process.jsSessionController
|
||||
|
||||
process.jsSessionController = new JSSessionController()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isWindows } from '@sasjs/utils'
|
||||
import { PreProgramVars, Session } from '../../types'
|
||||
import { generateFileUploadJSCode } from '../../utils'
|
||||
import { ExecutionVars } from './'
|
||||
@@ -21,7 +20,9 @@ export const createJSProgram = async (
|
||||
const preProgramVarStatments = `
|
||||
let _webout = '';
|
||||
const weboutPath = '${
|
||||
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
|
||||
process.platform === 'win32'
|
||||
? weboutPath.replace(/\\/g, '\\\\')
|
||||
: weboutPath
|
||||
}';
|
||||
const _sasjs_tokenfile = '${tokenFile}';
|
||||
const _sasjs_username = '${preProgramVariables?.username}';
|
||||
|
||||
@@ -40,7 +40,7 @@ export const processProgram = async (
|
||||
// waiting for the open event so that we can have underlying file descriptor
|
||||
await once(writeStream, 'open')
|
||||
|
||||
execFileSync(process.nodeLoc!, [codePath], {
|
||||
execFileSync(process.nodeLoc, [codePath], {
|
||||
stdio: ['ignore', writeStream, writeStream]
|
||||
})
|
||||
|
||||
|
||||
@@ -49,10 +49,10 @@ export class WebController {
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Destroy the session stored in cookies
|
||||
* @summary Accept a valid username/password
|
||||
*
|
||||
*/
|
||||
@Get('/SASLogon/logout')
|
||||
@Get('/logout')
|
||||
public async logout(@Request() req: express.Request) {
|
||||
return new Promise((resolve) => {
|
||||
req.session.destroy(() => {
|
||||
|
||||
@@ -48,7 +48,7 @@ webRouter.post(
|
||||
}
|
||||
)
|
||||
|
||||
webRouter.get('/SASLogon/logout', desktopRestrict, async (req, res) => {
|
||||
webRouter.get('/logout', desktopRestrict, async (req, res) => {
|
||||
try {
|
||||
await controller.logout(req)
|
||||
res.status(200).send('OK!')
|
||||
|
||||
4
api/src/types/system/process.d.ts
vendored
4
api/src/types/system/process.d.ts
vendored
@@ -1,7 +1,7 @@
|
||||
declare namespace NodeJS {
|
||||
export interface Process {
|
||||
sasLoc?: string
|
||||
nodeLoc?: string
|
||||
sasLoc: string
|
||||
nodeLoc: string
|
||||
driveLoc: string
|
||||
sasSessionController?: import('../../controllers/internal').SASSessionController
|
||||
jsSessionController?: import('../../controllers/internal').JSSessionController
|
||||
|
||||
@@ -2,32 +2,25 @@ import path from 'path'
|
||||
import { fileExists, getString, readFile } from '@sasjs/utils'
|
||||
|
||||
export const getCertificates = async () => {
|
||||
const { PRIVATE_KEY, CERT_CHAIN, CA_ROOT } = process.env
|
||||
|
||||
let ca
|
||||
const { PRIVATE_KEY, FULL_CHAIN, CA } = process.env
|
||||
|
||||
const keyPath = PRIVATE_KEY ?? (await getFileInput('Private Key (PEM)'))
|
||||
const certPath = CERT_CHAIN ?? (await getFileInput('Certificate Chain (PEM)'))
|
||||
const caPath = CA_ROOT
|
||||
const certPath = FULL_CHAIN ?? (await getFileInput('Full Chain (PEM)'))
|
||||
const caPath = CA ?? (await getFileInput('Full Chain (PEM)'))
|
||||
|
||||
console.log('keyPath: ', keyPath)
|
||||
console.log('certPath: ', certPath)
|
||||
if (caPath) console.log('caPath: ', caPath)
|
||||
console.log('caPath: ', caPath)
|
||||
|
||||
const key = await readFile(keyPath)
|
||||
const cert = await readFile(certPath)
|
||||
if (caPath) ca = await readFile(caPath)
|
||||
const ca = await readFile(caPath)
|
||||
|
||||
return { key, cert, ca }
|
||||
}
|
||||
|
||||
const getFileInput = async (
|
||||
filename: string,
|
||||
required: boolean = true
|
||||
): Promise<string> => {
|
||||
const getFileInput = async (filename: string): Promise<string> => {
|
||||
const validator = async (filePath: string) => {
|
||||
if (!required) return true
|
||||
|
||||
if (!filePath) return `Path to ${filename} is required.`
|
||||
|
||||
if (!(await fileExists(path.join(process.cwd(), filePath)))) {
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import path from 'path'
|
||||
import { getString } from '@sasjs/utils/input'
|
||||
import { createFolder, fileExists, folderExists, isWindows } from '@sasjs/utils'
|
||||
import { RunTimeType } from './verifyEnvVariables'
|
||||
import { createFolder, fileExists, folderExists } from '@sasjs/utils'
|
||||
|
||||
const isWindows = () => process.platform === 'win32'
|
||||
|
||||
export const getDesktopFields = async () => {
|
||||
const { SAS_PATH, NODE_PATH } = process.env
|
||||
|
||||
let sasLoc, nodeLoc
|
||||
|
||||
if (process.runTimes.includes(RunTimeType.SAS)) {
|
||||
sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||
}
|
||||
|
||||
if (process.runTimes.includes(RunTimeType.JS)) {
|
||||
nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
||||
}
|
||||
const sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||
const nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
||||
// const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
|
||||
|
||||
return { sasLoc, nodeLoc }
|
||||
}
|
||||
|
||||
@@ -5,10 +5,14 @@ import { RunTimeType } from '.'
|
||||
|
||||
export const getRunTimeAndFilePath = async (programPath: string) => {
|
||||
const ext = path.extname(programPath)
|
||||
// If programPath (_program) is provided with a ".sas" or ".js" extension
|
||||
// we should use that extension to determine the appropriate runTime
|
||||
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
|
||||
// if program path is provided with extension we should split that into code path and ext as run time
|
||||
if (ext) {
|
||||
const runTime = ext.slice(1)
|
||||
const runTimeTypes = Object.values(RunTimeType)
|
||||
|
||||
if (!runTimeTypes.includes(runTime as RunTimeType)) {
|
||||
throw `The '${runTime}' runtime is not supported.`
|
||||
}
|
||||
|
||||
const codePath = path
|
||||
.join(getFilesFolder(), programPath)
|
||||
|
||||
@@ -9,13 +9,11 @@ export const setProcessVariables = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
const { MODE, RUN_TIMES } = process.env
|
||||
|
||||
process.runTimes = (RUN_TIMES?.split(',') as RunTimeType[]) ?? []
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE === ModeType.Server) {
|
||||
process.sasLoc = process.env.SAS_PATH
|
||||
process.nodeLoc = process.env.NODE_PATH
|
||||
process.sasLoc = process.env.SAS_PATH as string
|
||||
process.nodeLoc = process.env.NODE_PATH as string
|
||||
} else {
|
||||
const { sasLoc, nodeLoc } = await getDesktopFields()
|
||||
|
||||
@@ -28,6 +26,9 @@ export const setProcessVariables = async () => {
|
||||
await createFolder(absPath)
|
||||
process.driveLoc = getRealPath(absPath)
|
||||
|
||||
const { RUN_TIMES } = process.env
|
||||
process.runTimes = (RUN_TIMES as string).split(',') as RunTimeType[]
|
||||
|
||||
console.log('sasLoc: ', process.sasLoc)
|
||||
console.log('sasDrive: ', process.driveLoc)
|
||||
console.log('runTimes: ', process.runTimes)
|
||||
|
||||
@@ -129,16 +129,16 @@ const verifyPROTOCOL = (): string[] => {
|
||||
}
|
||||
|
||||
if (process.env.PROTOCOL === ProtocolType.HTTPS) {
|
||||
const { PRIVATE_KEY, CERT_CHAIN } = process.env
|
||||
const { PRIVATE_KEY, FULL_CHAIN } = process.env
|
||||
|
||||
if (!PRIVATE_KEY)
|
||||
errors.push(
|
||||
`- PRIVATE_KEY is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
||||
)
|
||||
|
||||
if (!CERT_CHAIN)
|
||||
if (!FULL_CHAIN)
|
||||
errors.push(
|
||||
`- CERT_CHAIN is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
||||
`- FULL_CHAIN is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -258,5 +258,5 @@ const DEFAULTS = {
|
||||
PORT: '5000',
|
||||
HELMET_COEP: HelmetCoepType.TRUE,
|
||||
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
|
||||
RUN_TIMES: RunTimeType.SAS
|
||||
RUN_TIMES: `${RunTimeType.SAS}`
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
}, [])
|
||||
|
||||
const logout = useCallback(() => {
|
||||
axios.get('/SASLogon/logout').then(() => {
|
||||
axios.get('/logout').then(() => {
|
||||
setLoggedIn(false)
|
||||
setUsername('')
|
||||
setDisplayName('')
|
||||
|
||||
Reference in New Issue
Block a user