1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-10 11:24:35 +00:00

Compare commits

...

21 Commits

Author SHA1 Message Date
semantic-release-bot
b369759f0f chore(release): 0.8.3 [skip ci]
## [0.8.3](https://github.com/sasjs/server/compare/v0.8.2...v0.8.3) (2022-07-02)

### Bug Fixes

* **deploy:** extract first json from zip file ([e290751](e290751c87))
2022-07-02 10:01:26 +00:00
Allan Bowe
ac9a835c5a Merge pull request #219 from sasjs/issue211
fix(deploy): extract first json from zip file
2022-07-02 10:57:16 +01:00
Saad Jutt
e290751c87 fix(deploy): extract first json from zip file 2022-07-02 14:39:33 +05:00
semantic-release-bot
71bcbb9134 chore(release): 0.8.2 [skip ci]
## [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](5cc85b57f8))
2022-06-22 10:18:59 +00:00
Allan Bowe
c86f0feff8 Merge pull request #214 from sasjs/fix-runtime-filePath
fix: getRuntimeAndFilePath function
2022-06-22 12:14:12 +02:00
Allan Bowe
d3d2ab9a36 Update getRunTimeAndFilePath.ts 2022-06-22 11:12:48 +01:00
5cc85b57f8 fix: getRuntimeAndFilePath function to handle the scenarion when path is provided with an extension other than runtimes 2022-06-22 14:24:06 +05:00
semantic-release-bot
ae0fc0c48c chore(release): 0.8.1 [skip ci]
## [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](1b5859ee37))
* update /logout route to /SASLogon/logout ([65380be](65380be2f3))
2022-06-21 20:10:31 +00:00
Saad Jutt
555c5d54e2 Merge pull request #212 from sasjs/update-logout-route
Update logout route
2022-06-21 13:06:30 -07:00
1b5859ee37 fix: make CA_ROOT optional in getCertificates method 2022-06-22 00:25:41 +05:00
65380be2f3 fix: update /logout route to /SASLogon/logout 2022-06-22 00:24:41 +05:00
Yury Shkoda
1933be15c2 Merge pull request #210 from sasjs/pr-template
chore(template): added pull request template
2022-06-21 19:11:19 +03:00
Yury Shkoda
56b20beb8c chore(template): added pull request template 2022-06-21 19:07:14 +03:00
semantic-release-bot
bfc5ac6a4f chore(release): 0.8.0 [skip ci]
# [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](2119e9de9a))
2022-06-21 07:24:26 +00:00
Allan Bowe
6376173de0 Merge pull request #209 from sasjs/certificate-ca-root
feat(certs): ENV variables updated and set CA Root for HTTPS server
2022-06-21 09:18:15 +02:00
Saad Jutt
3130fbeff0 chore: code refactor for getting session controller 2022-06-21 03:41:44 +05:00
Saad Jutt
01e9a1d9e9 chore: README.md and .env.example updated 2022-06-21 03:26:12 +05:00
Saad Jutt
2119e9de9a feat(certs): ENV variables updated and set CA Root for HTTPS server 2022-06-21 03:17:14 +05:00
semantic-release-bot
87dbab98f6 chore(release): 0.7.3 [skip ci]
## [0.7.3](https://github.com/sasjs/server/compare/v0.7.2...v0.7.3) (2022-06-20)

### Bug Fixes

* path descriptions and defaults ([5d5d6ce](5d5d6ce326))
2022-06-20 15:31:20 +00:00
Allan Bowe
1bf122a0a2 Merge pull request #207 from sasjs/allanbowe/sasjs-server-fails-to-205
fix: path descriptions and defaults
2022-06-20 17:26:56 +02:00
Allan Bowe
5d5d6ce326 fix: path descriptions and defaults 2022-06-20 15:07:17 +00:00
21 changed files with 160 additions and 78 deletions

View File

@@ -1,3 +1,39 @@
## [0.8.3](https://github.com/sasjs/server/compare/v0.8.2...v0.8.3) (2022-07-02)
### Bug Fixes
* **deploy:** extract first json from zip file ([e290751](https://github.com/sasjs/server/commit/e290751c872d24009482871a8c398e834357dcde))
## [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)
### Bug Fixes
* path descriptions and defaults ([5d5d6ce](https://github.com/sasjs/server/commit/5d5d6ce3265a43af2e22bcd38cda54fafaf7b3ef))
## [0.7.2](https://github.com/sasjs/server/compare/v0.7.1...v0.7.2) (2022-06-20)

19
PULL_REQUEST_TEMPLATE.md Normal file
View File

@@ -0,0 +1,19 @@
## 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.

View File

@@ -99,9 +99,10 @@ SASV9_OPTIONS= -NOXCMD
## Additional Web Server Options
#
# ENV variables required for PROTOCOL: `https`
PRIVATE_KEY=privkey.pem
FULL_CHAIN=fullchain.pem
# ENV variables for PROTOCOL: `https`
PRIVATE_KEY=privkey.pem (required)
CERT_CHAIN=certificate.pem (required)
CA_ROOT=fullchain.pem (optional)
# ENV variables required for MODE: `server`
ACCESS_TOKEN_SECRET=<secret>

View File

@@ -4,7 +4,8 @@ WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
PROTOCOL=[http|https] default considered as http
PRIVATE_KEY=privkey.pem
FULL_CHAIN=fullchain.pem
CERT_CHAIN=certificate.pem
CA_ROOT=fullchain.pem
PORT=[5000] default value is 5000

View File

@@ -624,7 +624,7 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/AuthorizePayload'
/logout:
/SASLogon/logout:
get:
operationId: Logout
responses:
@@ -633,7 +633,7 @@ paths:
content:
application/json:
schema: {}
summary: 'Accept a valid username/password'
summary: 'Destroy the session stored in cookies'
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.\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!"
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!"
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\r\nOr provide `filePath` in body as form field.\r\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\nOr provide `filePath` in body as form field.\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\r\nOr provide `filePath` in body as form field.\r\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\nOr provide `filePath` in body as form field.\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.\r\n\r\nAccepts URL parameters and file uploads. For more details, see docs:\r\n\r\nhttps://server.sasjs.io/storedprograms"
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"
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.\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."
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."
summary: 'Execute a Stored Program, return a JSON object'
tags:
- STP

View File

@@ -1,10 +1,6 @@
import path from 'path'
import fs from 'fs'
import {
getSASSessionController,
getJSSessionController,
processProgram
} from './'
import { getSessionController, processProgram } from './'
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
import { PreProgramVars, Session, TreeNode } from '../../types'
import {
@@ -76,10 +72,7 @@ export class ExecutionController {
session: sessionByFileUpload,
runTime
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
const sessionController =
runTime === RunTimeType.SAS
? getSASSessionController()
: getJSSessionController()
const sessionController = getSessionController(runTime)
const session =
sessionByFileUpload ?? (await sessionController.getSession())

View File

@@ -1,7 +1,7 @@
import { Request, RequestHandler } from 'express'
import multer from 'multer'
import { uuidv4 } from '@sasjs/utils'
import { getSASSessionController, getJSSessionController } from '.'
import { getSessionController } from '.'
import {
executeProgramRawValidation,
getRunTimeAndFilePath,
@@ -37,17 +37,23 @@ export class FileUploadController {
try {
;({ runTime } = await getRunTimeAndFilePath(programPath))
} catch (err: any) {
res.status(400).send({
return res.status(400).send({
status: 'failure',
message: 'Job execution failed',
error: typeof err === 'object' ? err.toString() : err
})
}
const sessionController =
runTime === RunTimeType.SAS
? getSASSessionController()
: getJSSessionController()
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 session = await sessionController.getSession()
// marking consumed true, so that it's not available

View File

@@ -5,14 +5,16 @@ import { execFile } from 'child_process'
import {
getSessionsFolder,
generateUniqueFileName,
sysInitCompiledPath
sysInitCompiledPath,
RunTimeType
} from '../../utils'
import {
deleteFolder,
createFile,
fileExists,
generateTimestamp,
readFile
readFile,
isWindows
} from '@sasjs/utils'
const execFilePromise = promisify(execFile)
@@ -88,7 +90,7 @@ ${autoExecContent}`
// Additional windows specific options to avoid the desktop popups.
execFilePromise(process.sasLoc, [
execFilePromise(process.sasLoc!, [
'-SYSIN',
codePath,
'-LOG',
@@ -99,9 +101,9 @@ ${autoExecContent}`
session.path,
'-AUTOEXEC',
autoExecPath,
process.platform === 'win32' ? '-nosplash' : '',
process.platform === 'win32' ? '-icon' : '',
process.platform === 'win32' ? '-nologo' : ''
isWindows() ? '-nosplash' : '',
isWindows() ? '-icon' : '',
isWindows() ? '-nologo' : ''
])
.then(() => {
session.completed = true
@@ -192,7 +194,21 @@ export class JSSessionController extends SessionController {
}
}
export const getSASSessionController = (): SASSessionController => {
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 => {
if (process.sasSessionController) return process.sasSessionController
process.sasSessionController = new SASSessionController()
@@ -200,7 +216,7 @@ export const getSASSessionController = (): SASSessionController => {
return process.sasSessionController
}
export const getJSSessionController = (): JSSessionController => {
const getJSSessionController = (): JSSessionController => {
if (process.jsSessionController) return process.jsSessionController
process.jsSessionController = new JSSessionController()

View File

@@ -1,3 +1,4 @@
import { isWindows } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { generateFileUploadJSCode } from '../../utils'
import { ExecutionVars } from './'
@@ -20,9 +21,7 @@ export const createJSProgram = async (
const preProgramVarStatments = `
let _webout = '';
const weboutPath = '${
process.platform === 'win32'
? weboutPath.replace(/\\/g, '\\\\')
: weboutPath
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
}';
const _sasjs_tokenfile = '${tokenFile}';
const _sasjs_username = '${preProgramVariables?.username}';

View File

@@ -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]
})

View File

@@ -49,10 +49,10 @@ export class WebController {
}
/**
* @summary Accept a valid username/password
* @summary Destroy the session stored in cookies
*
*/
@Get('/logout')
@Get('/SASLogon/logout')
public async logout(@Request() req: express.Request) {
return new Promise((resolve) => {
req.session.destroy(() => {

View File

@@ -48,7 +48,7 @@ webRouter.post(
}
)
webRouter.get('/logout', desktopRestrict, async (req, res) => {
webRouter.get('/SASLogon/logout', desktopRestrict, async (req, res) => {
try {
await controller.logout(req)
res.status(200).send('OK!')

View File

@@ -16,9 +16,9 @@ appPromise.then(async (app) => {
)
})
} else {
const { key, cert } = await getCertificates()
const { key, cert, ca } = await getCertificates()
const httpsServer = createServer({ key, cert }, app)
const httpsServer = createServer({ key, cert, ca }, app)
httpsServer.listen(sasJsPort, () => {
console.log(
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`

View File

@@ -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

View File

@@ -2,22 +2,32 @@ import path from 'path'
import { fileExists, getString, readFile } from '@sasjs/utils'
export const getCertificates = async () => {
const { PRIVATE_KEY, FULL_CHAIN } = process.env
const { PRIVATE_KEY, CERT_CHAIN, CA_ROOT } = process.env
let ca
const keyPath = PRIVATE_KEY ?? (await getFileInput('Private Key (PEM)'))
const certPath = FULL_CHAIN ?? (await getFileInput('Full Chain (PEM)'))
const certPath = CERT_CHAIN ?? (await getFileInput('Certificate Chain (PEM)'))
const caPath = CA_ROOT
console.log('keyPath: ', keyPath)
console.log('certPath: ', certPath)
if (caPath) console.log('caPath: ', caPath)
const key = await readFile(keyPath)
const cert = await readFile(certPath)
if (caPath) ca = await readFile(caPath)
return { key, cert }
return { key, cert, ca }
}
const getFileInput = async (filename: string): Promise<string> => {
const getFileInput = async (
filename: string,
required: boolean = true
): 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)))) {

View File

@@ -1,15 +1,20 @@
import path from 'path'
import { getString } from '@sasjs/utils/input'
import { createFolder, fileExists, folderExists } from '@sasjs/utils'
const isWindows = () => process.platform === 'win32'
import { createFolder, fileExists, folderExists, isWindows } from '@sasjs/utils'
import { RunTimeType } from './verifyEnvVariables'
export const getDesktopFields = async () => {
const { SAS_PATH, NODE_PATH } = process.env
const sasLoc = SAS_PATH ?? (await getSASLocation())
const nodeLoc = NODE_PATH ?? (await getNodeLocation())
// const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
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())
}
return { sasLoc, nodeLoc }
}
@@ -55,7 +60,7 @@ const getSASLocation = async (): Promise<string> => {
: '/opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe/sas'
const targetName = await getString(
'Please enter path to SAS executable (absolute path): ',
'Please enter full path to a SAS executable with UTF-8 encoding: ',
validator,
defaultLocation
)
@@ -75,11 +80,11 @@ const getNodeLocation = async (): Promise<string> => {
}
const defaultLocation = isWindows()
? 'C:\\Program Files\\nodejs\\'
: '/usr/local/nodejs/bin'
? 'C:\\Program Files\\nodejs\\node.exe'
: '/usr/local/nodejs/bin/node.sh'
const targetName = await getString(
'Please enter path to nodejs executable (absolute path): ',
'Please enter full path to a NodeJS executable: ',
validator,
defaultLocation
)

View File

@@ -5,14 +5,10 @@ import { RunTimeType } from '.'
export const getRunTimeAndFilePath = async (programPath: string) => {
const ext = path.extname(programPath)
// if program path is provided with extension we should split that into code path and ext as run time
if (ext) {
// 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)) {
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)

View File

@@ -9,11 +9,13 @@ export const setProcessVariables = async () => {
return
}
const { MODE } = process.env
const { MODE, RUN_TIMES } = process.env
process.runTimes = (RUN_TIMES?.split(',') as RunTimeType[]) ?? []
if (MODE === ModeType.Server) {
process.sasLoc = process.env.SAS_PATH as string
process.nodeLoc = process.env.NODE_PATH as string
process.sasLoc = process.env.SAS_PATH
process.nodeLoc = process.env.NODE_PATH
} else {
const { sasLoc, nodeLoc } = await getDesktopFields()
@@ -26,9 +28,6 @@ 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)

View File

@@ -129,16 +129,16 @@ const verifyPROTOCOL = (): string[] => {
}
if (process.env.PROTOCOL === ProtocolType.HTTPS) {
const { PRIVATE_KEY, FULL_CHAIN } = process.env
const { PRIVATE_KEY, CERT_CHAIN } = process.env
if (!PRIVATE_KEY)
errors.push(
`- PRIVATE_KEY is required for PROTOCOL '${ProtocolType.HTTPS}'`
)
if (!FULL_CHAIN)
if (!CERT_CHAIN)
errors.push(
`- FULL_CHAIN is required for PROTOCOL '${ProtocolType.HTTPS}'`
`- CERT_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
}

View File

@@ -28,7 +28,8 @@ export const extractJSONFromZip = async (zipFile: Express.Multer.File) => {
for await (const entry of zip) {
const fileName = entry.path as string
if (fileName.toUpperCase().endsWith('.JSON') && fileName === fileInZip) {
// grab the first json found in .zip
if (fileName.toUpperCase().endsWith('.JSON')) {
fileContent = await entry.buffer()
break
} else {

View File

@@ -88,7 +88,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
}, [])
const logout = useCallback(() => {
axios.get('/logout').then(() => {
axios.get('/SASLogon/logout').then(() => {
setLoggedIn(false)
setUsername('')
setDisplayName('')