From f0ecfa57e585abf2e2198362ae3b9e64703d634c Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Tue, 28 Sep 2021 16:02:12 +0500 Subject: [PATCH 1/8] BREAKING CHANGE: boolean allowInsecure is replaced configuration of Https Agent --- src/SAS9ApiClient.ts | 8 +++++-- src/SASjs.ts | 14 ++++++------ src/job-execution/Sas9JobExecutor.ts | 8 +++++-- src/request/RequestClient.ts | 34 ++++++++++++++++++---------- src/request/Sas9RequestClient.ts | 5 ++-- src/types/HttpsAgent.ts | 10 ++++++++ src/types/SASjsConfig.ts | 3 ++- src/types/index.ts | 1 + 8 files changed, 57 insertions(+), 26 deletions(-) create mode 100644 src/types/HttpsAgent.ts diff --git a/src/SAS9ApiClient.ts b/src/SAS9ApiClient.ts index c7e171c..6a66caf 100644 --- a/src/SAS9ApiClient.ts +++ b/src/SAS9ApiClient.ts @@ -1,6 +1,7 @@ import { generateTimestamp } from '@sasjs/utils/time' import * as NodeFormData from 'form-data' import { Sas9RequestClient } from './request/Sas9RequestClient' +import { HttpsAgent } from './types/HttpsAgent' import { isUrl } from './utils' /** @@ -13,10 +14,13 @@ export class SAS9ApiClient { constructor( private serverUrl: string, private jobsPath: string, - allowInsecureRequests: boolean + httpsAgentConfiguration?: HttpsAgent ) { if (serverUrl) isUrl(serverUrl) - this.requestClient = new Sas9RequestClient(serverUrl, allowInsecureRequests) + this.requestClient = new Sas9RequestClient( + serverUrl, + httpsAgentConfiguration + ) } /** diff --git a/src/SASjs.ts b/src/SASjs.ts index 74831a5..d5f7853 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -36,7 +36,7 @@ const defaultConfig: SASjsConfig = { debug: false, contextName: 'SAS Job Execution compute context', useComputeApi: null, - allowInsecureRequests: false, + httpsAgentConfiguration: {}, loginMechanism: LoginMechanism.Default } @@ -57,7 +57,7 @@ export default class SASjs { private jesJobExecutor: JobExecutor | null = null private sas9JobExecutor: JobExecutor | null = null - constructor(config?: any) { + constructor(config?: Partial) { this.sasjsConfig = { ...defaultConfig, ...config @@ -792,7 +792,7 @@ export default class SASjs { sasApiClient = new SAS9ApiClient( serverUrl, this.jobsPath, - this.sasjsConfig.allowInsecureRequests + this.sasjsConfig.httpsAgentConfiguration ) } } else { @@ -951,12 +951,12 @@ export default class SASjs { if (!this.requestClient) { this.requestClient = new RequestClient( this.sasjsConfig.serverUrl, - this.sasjsConfig.allowInsecureRequests + this.sasjsConfig.httpsAgentConfiguration ) } else { this.requestClient.setConfig( this.sasjsConfig.serverUrl, - this.sasjsConfig.allowInsecureRequests + this.sasjsConfig.httpsAgentConfiguration ) } @@ -995,7 +995,7 @@ export default class SASjs { this.sas9ApiClient = new SAS9ApiClient( this.sasjsConfig.serverUrl, this.jobsPath, - this.sasjsConfig.allowInsecureRequests + this.sasjsConfig.httpsAgentConfiguration ) } @@ -1018,7 +1018,7 @@ export default class SASjs { this.sasjsConfig.serverUrl, this.sasjsConfig.serverType!, this.jobsPath, - this.sasjsConfig.allowInsecureRequests + this.sasjsConfig.httpsAgentConfiguration ) this.computeJobExecutor = new ComputeJobExecutor( diff --git a/src/job-execution/Sas9JobExecutor.ts b/src/job-execution/Sas9JobExecutor.ts index 892be05..b4fb549 100644 --- a/src/job-execution/Sas9JobExecutor.ts +++ b/src/job-execution/Sas9JobExecutor.ts @@ -4,6 +4,7 @@ import { ErrorResponse } from '../types/errors' import { convertToCSV, isRelativePath } from '../utils' import { BaseJobExecutor } from './JobExecutor' import { Sas9RequestClient } from '../request/Sas9RequestClient' +import { HttpsAgent } from '../types/HttpsAgent' /** * Job executor for SAS9 servers for use in Node.js environments. @@ -17,10 +18,13 @@ export class Sas9JobExecutor extends BaseJobExecutor { serverUrl: string, serverType: ServerType, private jobsPath: string, - allowInsecureRequests: boolean + httpsAgentConfiguration: HttpsAgent ) { super(serverUrl, serverType) - this.requestClient = new Sas9RequestClient(serverUrl, allowInsecureRequests) + this.requestClient = new Sas9RequestClient( + serverUrl, + httpsAgentConfiguration + ) } async execute(sasJob: string, data: any, config: any) { diff --git a/src/request/RequestClient.ts b/src/request/RequestClient.ts index c84face..f4a0f29 100644 --- a/src/request/RequestClient.ts +++ b/src/request/RequestClient.ts @@ -13,6 +13,7 @@ import { parseWeboutResponse } from '../utils/parseWeboutResponse' import { prefixMessage } from '@sasjs/utils/error' import { SAS9AuthError } from '../types/errors/SAS9AuthError' import { parseGeneratedCode, parseSourceCode } from '../utils' +import { HttpsAgent } from '../types/HttpsAgent' export interface HttpClient { get( @@ -54,12 +55,12 @@ export class RequestClient implements HttpClient { protected fileUploadCsrfToken: CsrfToken | undefined protected httpClient!: AxiosInstance - constructor(protected baseUrl: string, allowInsecure = false) { - this.createHttpClient(baseUrl, allowInsecure) + constructor(protected baseUrl: string, httpsAgentConfiguration?: HttpsAgent) { + this.createHttpClient(baseUrl, httpsAgentConfiguration) } - public setConfig(baseUrl: string, allowInsecure = false) { - this.createHttpClient(baseUrl, allowInsecure) + public setConfig(baseUrl: string, httpsAgentConfiguration?: HttpsAgent) { + this.createHttpClient(baseUrl, httpsAgentConfiguration) } public getCsrfToken(type: 'general' | 'file' = 'general') { @@ -511,15 +512,24 @@ export class RequestClient implements HttpClient { return responseToReturn } - private createHttpClient(baseUrl: string, allowInsecure = false) { + private createHttpClient( + baseUrl: string, + httpsAgentConfiguration: HttpsAgent = {} + ) { const https = require('https') - if (allowInsecure && https.Agent) { - this.httpClient = axios.create({ - baseURL: baseUrl, - httpsAgent: new https.Agent({ - rejectUnauthorized: !allowInsecure - }) - }) + const { selfSigned, clientCA, allowInsecure } = httpsAgentConfiguration + + const httpsAgentConfig = selfSigned + ? { ca: selfSigned.ca } + : clientCA + ? { key: clientCA.key, cert: clientCA.cert, requestCert: true } + : allowInsecure + ? { rejectUnauthorized: !allowInsecure } + : undefined + + if (httpsAgentConfig) { + const httpsAgent = new https.Agent(httpsAgentConfig) + this.httpClient = axios.create({ httpsAgent }) } else { this.httpClient = axios.create({ baseURL: baseUrl diff --git a/src/request/Sas9RequestClient.ts b/src/request/Sas9RequestClient.ts index 5fb4750..9f7fe2b 100644 --- a/src/request/Sas9RequestClient.ts +++ b/src/request/Sas9RequestClient.ts @@ -3,14 +3,15 @@ import axiosCookieJarSupport from 'axios-cookiejar-support' import * as tough from 'tough-cookie' import { prefixMessage } from '@sasjs/utils/error' import { RequestClient, throwIfError } from './RequestClient' +import { HttpsAgent } from '../types/HttpsAgent' /** * Specific request client for SAS9 in Node.js environments. * Handles redirects and cookie management. */ export class Sas9RequestClient extends RequestClient { - constructor(baseUrl: string, allowInsecure = false) { - super(baseUrl, allowInsecure) + constructor(baseUrl: string, httpsAgentConfiguration?: HttpsAgent) { + super(baseUrl, httpsAgentConfiguration) this.httpClient.defaults.maxRedirects = 0 this.httpClient.defaults.validateStatus = (status) => status >= 200 && status < 303 diff --git a/src/types/HttpsAgent.ts b/src/types/HttpsAgent.ts new file mode 100644 index 0000000..5932599 --- /dev/null +++ b/src/types/HttpsAgent.ts @@ -0,0 +1,10 @@ +export interface HttpsAgent { + selfSigned?: { + ca: string[] + } + clientCA?: { + key: string + cert: string + } + allowInsecure?: boolean +} diff --git a/src/types/SASjsConfig.ts b/src/types/SASjsConfig.ts index b562dcb..5fb6dad 100644 --- a/src/types/SASjsConfig.ts +++ b/src/types/SASjsConfig.ts @@ -1,4 +1,5 @@ import { ServerType } from '@sasjs/utils/types' +import { HttpsAgent } from './HttpsAgent' /** * Specifies the configuration for the SASjs instance - eg where and how to @@ -58,7 +59,7 @@ export class SASjsConfig { * When set to `true`, the adapter will allow requests to SAS servers that use a self-signed SSL certificate. * Changing this setting is not recommended. */ - allowInsecureRequests = false + httpsAgentConfiguration: HttpsAgent = {} /** * Supported login mechanisms are - Redirected and Default */ diff --git a/src/types/index.ts b/src/types/index.ts index 2303619..3998768 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,6 +2,7 @@ export * from './Context' export * from './CsrfToken' export * from './Folder' export * from './File' +export * from './HttpsAgent' export * from './Job' export * from './JobDefinition' export * from './JobResult' From e975e7de9773ea3b3276a2aac1293903fedef687 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Fri, 1 Oct 2021 12:03:44 +0500 Subject: [PATCH 2/8] test(RequestClient): specs for communicating with self-signed server --- package.json | 4 + src/request/RequestClient.ts | 7 +- src/test/RequestClient.spec.ts | 163 +++++++++++++++++++++++++++++++++ src/test/SAS_server_app.ts | 38 ++++++++ 4 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 src/test/RequestClient.spec.ts create mode 100644 src/test/SAS_server_app.ts diff --git a/package.json b/package.json index fcf32f6..4f1f120 100644 --- a/package.json +++ b/package.json @@ -41,17 +41,21 @@ "license": "ISC", "devDependencies": { "@types/axios": "^0.14.0", + "@types/express": "^4.17.13", "@types/form-data": "^2.5.0", "@types/jest": "^27.0.1", "@types/mime": "^2.0.3", + "@types/pem": "^1.9.6", "@types/tough-cookie": "^4.0.1", "copyfiles": "^2.4.1", "cp": "^0.2.0", "dotenv": "^10.0.0", + "express": "^4.17.1", "jest": "^27.2.0", "jest-extended": "^0.11.5", "node-polyfill-webpack-plugin": "^1.1.4", "path": "^0.12.7", + "pem": "^1.14.4", "process": "^0.11.10", "rimraf": "^3.0.2", "semantic-release": "^17.4.7", diff --git a/src/request/RequestClient.ts b/src/request/RequestClient.ts index f4a0f29..ba2d3ce 100644 --- a/src/request/RequestClient.ts +++ b/src/request/RequestClient.ts @@ -527,9 +527,12 @@ export class RequestClient implements HttpClient { ? { rejectUnauthorized: !allowInsecure } : undefined - if (httpsAgentConfig) { + if (httpsAgentConfig && https.Agent) { const httpsAgent = new https.Agent(httpsAgentConfig) - this.httpClient = axios.create({ httpsAgent }) + this.httpClient = axios.create({ + baseURL: baseUrl, + httpsAgent + }) } else { this.httpClient = axios.create({ baseURL: baseUrl diff --git a/src/test/RequestClient.spec.ts b/src/test/RequestClient.spec.ts new file mode 100644 index 0000000..b40c578 --- /dev/null +++ b/src/test/RequestClient.spec.ts @@ -0,0 +1,163 @@ +import * as pem from 'pem' +import * as http from 'http' +import * as https from 'https' +import { app, mockedAuthResponse } from './SAS_server_app' +import { ServerType } from '@sasjs/utils' +import SASjs from '../SASjs' + +const PORT = 8000 +const SERVER_URL = `https://localhost:${PORT}/` + +describe('RequestClient', () => { + let server: http.Server + + const adapter = new SASjs({ + serverUrl: `http://localhost:${PORT}/`, + serverType: ServerType.SasViya + }) + + beforeAll(async () => { + await new Promise((resolve: any, reject: any) => { + server = app + .listen(PORT, () => resolve()) + .on('error', (err: any) => reject(err)) + }) + }) + + afterAll(() => { + server.close() + }) + + it('should response the POST method', async () => { + const authResponse = await adapter.getAccessToken( + 'clientId', + 'clientSecret', + 'authCode' + ) + + expect(authResponse.access_token).toBe(mockedAuthResponse.access_token) + }) + + it('should response the POST method with Unauthorized', async () => { + await expect( + adapter.getAccessToken('clientId', 'clientSecret', 'incorrect') + ).rejects.toEqual( + new Error( + 'Error while getting access tokenRequest failed with status code 401' + ) + ) + }) +}) + +describe('RequestClient - Self Signed Server', () => { + let adapter: SASjs + + let httpsServer: https.Server + let sslConfig: pem.CertificateCreationResult + + beforeAll(async () => { + ;({ httpsServer, keys: sslConfig } = await setupSelfSignedServer()) + await new Promise((resolve: any, reject: any) => { + httpsServer + .listen(PORT, () => resolve()) + .on('error', (err: any) => reject(err)) + }) + + adapter = new SASjs({ + serverUrl: SERVER_URL, + serverType: ServerType.SasViya, + httpsAgentConfiguration: { + selfSigned: { ca: [sslConfig.certificate] } + } + }) + }) + + afterAll(() => { + httpsServer.close() + }) + + it('should throw error for not providing certificate', async () => { + const adapterWithoutCertificate = new SASjs({ + serverUrl: SERVER_URL, + serverType: ServerType.SasViya + }) + + await expect( + adapterWithoutCertificate.getAccessToken( + 'clientId', + 'clientSecret', + 'authCode' + ) + ).rejects.toEqual( + expect.objectContaining({ + message: 'Error while getting access tokenself signed certificate' + }) + ) + }) + + it('should response the POST method using insecure flag', async () => { + const adapterAllowInsecure = new SASjs({ + serverUrl: SERVER_URL, + serverType: ServerType.SasViya, + httpsAgentConfiguration: { + allowInsecure: true + } + }) + + const authResponse = await adapterAllowInsecure.getAccessToken( + 'clientId', + 'clientSecret', + 'authCode' + ) + + expect(authResponse.access_token).toBe(mockedAuthResponse.access_token) + }) + + it('should response the POST method', async () => { + const authResponse = await adapter.getAccessToken( + 'clientId', + 'clientSecret', + 'authCode' + ) + + expect(authResponse.access_token).toBe(mockedAuthResponse.access_token) + }) + + it('should response the POST method with Unauthorized', async () => { + await expect( + adapter.getAccessToken('clientId', 'clientSecret', 'incorrect') + ).rejects.toEqual( + new Error( + 'Error while getting access tokenRequest failed with status code 401' + ) + ) + }) +}) + +const setupSelfSignedServer = async (): Promise<{ + httpsServer: https.Server + keys: pem.CertificateCreationResult +}> => { + return await new Promise(async (resolve) => { + const keys = await createCertificate() + + const httpsServer = https.createServer( + { key: keys.clientKey, cert: keys.certificate }, + app + ) + + resolve({ httpsServer, keys }) + }) +} + +const createCertificate = async (): Promise => { + return await new Promise((resolve, reject) => { + pem.createCertificate( + { days: 1, selfSigned: true }, + (error: any, keys: pem.CertificateCreationResult) => { + if (error) reject(false) + resolve(keys) + } + ) + }) +} diff --git a/src/test/SAS_server_app.ts b/src/test/SAS_server_app.ts new file mode 100644 index 0000000..e4cff7a --- /dev/null +++ b/src/test/SAS_server_app.ts @@ -0,0 +1,38 @@ +import express = require('express') + +export const app = express() + +export const mockedAuthResponse = { + access_token: 'access_token', + token_type: 'bearer', + id_token: 'id_token', + refresh_token: 'refresh_token', + expires_in: 43199, + scope: 'openid', + jti: 'jti' +} + +app.get('/', function (req: any, res: any) { + res.send('Hello World') +}) + +app.post('/SASLogon/oauth/token', function (req: any, res: any) { + let valid = true + // capture the encoded form data + req.on('data', (data: any) => { + const resData = data.toString() + + if (resData.includes('incorrect')) valid = false + }) + + // send a response when finished reading + // the encoded form data + req.on('end', () => { + if (valid) res.status(200).send(mockedAuthResponse) + else + res.status(401).send({ + error: 'unauthorized', + error_description: 'Bad credentials' + }) + }) +}) From 7f590c35da58d710ccea0e7970a0912b359af460 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Fri, 1 Oct 2021 15:42:04 +0500 Subject: [PATCH 3/8] test(RequestClient): fixed to use actual axios --- src/request/RequestClient.ts | 25 ++++++++++++------------- src/test/RequestClient.spec.ts | 9 +++++++++ src/test/SessionManager.spec.ts | 1 - src/utils/createAxiosInstance.ts | 7 +++++++ src/utils/index.ts | 1 + 5 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 src/utils/createAxiosInstance.ts diff --git a/src/request/RequestClient.ts b/src/request/RequestClient.ts index ba2d3ce..409b6ba 100644 --- a/src/request/RequestClient.ts +++ b/src/request/RequestClient.ts @@ -12,7 +12,11 @@ import { SASjsRequest } from '../types' import { parseWeboutResponse } from '../utils/parseWeboutResponse' import { prefixMessage } from '@sasjs/utils/error' import { SAS9AuthError } from '../types/errors/SAS9AuthError' -import { parseGeneratedCode, parseSourceCode } from '../utils' +import { + parseGeneratedCode, + parseSourceCode, + createAxiosInstance +} from '../utils' import { HttpsAgent } from '../types/HttpsAgent' export interface HttpClient { @@ -519,7 +523,7 @@ export class RequestClient implements HttpClient { const https = require('https') const { selfSigned, clientCA, allowInsecure } = httpsAgentConfiguration - const httpsAgentConfig = selfSigned + const httpsAgentOptions = selfSigned ? { ca: selfSigned.ca } : clientCA ? { key: clientCA.key, cert: clientCA.cert, requestCert: true } @@ -527,17 +531,12 @@ export class RequestClient implements HttpClient { ? { rejectUnauthorized: !allowInsecure } : undefined - if (httpsAgentConfig && https.Agent) { - const httpsAgent = new https.Agent(httpsAgentConfig) - this.httpClient = axios.create({ - baseURL: baseUrl, - httpsAgent - }) - } else { - this.httpClient = axios.create({ - baseURL: baseUrl - }) - } + const httpsAgent = + httpsAgentOptions && https.Agent + ? new https.Agent(httpsAgentOptions) + : undefined + + this.httpClient = createAxiosInstance(baseUrl, httpsAgent) this.httpClient.defaults.validateStatus = (status) => status >= 200 && status < 305 diff --git a/src/test/RequestClient.spec.ts b/src/test/RequestClient.spec.ts index b40c578..f947f1b 100644 --- a/src/test/RequestClient.spec.ts +++ b/src/test/RequestClient.spec.ts @@ -4,6 +4,15 @@ import * as https from 'https' import { app, mockedAuthResponse } from './SAS_server_app' import { ServerType } from '@sasjs/utils' import SASjs from '../SASjs' +import * as axiosModules from '../utils/createAxiosInstance' + +const axiosActual = jest.requireActual('axios') + +jest + .spyOn(axiosModules, 'createAxiosInstance') + .mockImplementation((baseURL: string, httpsAgent?: https.Agent) => + axiosActual.create({ baseURL, httpsAgent }) + ) const PORT = 8000 const SERVER_URL = `https://localhost:${PORT}/` diff --git a/src/test/SessionManager.spec.ts b/src/test/SessionManager.spec.ts index ace8526..d37305b 100644 --- a/src/test/SessionManager.spec.ts +++ b/src/test/SessionManager.spec.ts @@ -1,6 +1,5 @@ import { SessionManager } from '../SessionManager' import { RequestClient } from '../request/RequestClient' -import { NoSessionStateError } from '../types/errors' import * as dotenv from 'dotenv' import axios from 'axios' import { Logger, LogLevel } from '@sasjs/utils' diff --git a/src/utils/createAxiosInstance.ts b/src/utils/createAxiosInstance.ts new file mode 100644 index 0000000..d129a01 --- /dev/null +++ b/src/utils/createAxiosInstance.ts @@ -0,0 +1,7 @@ +import axios from 'axios' +import * as https from 'https' + +export const createAxiosInstance = ( + baseURL: string, + httpsAgent?: https.Agent +) => axios.create({ baseURL, httpsAgent }) diff --git a/src/utils/index.ts b/src/utils/index.ts index 14def48..065dd87 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,7 @@ export * from './asyncForEach' export * from './compareTimestamps' export * from './convertToCsv' +export * from './createAxiosInstance' export * from './delay' export * from './isNode' export * from './isRelativePath' From 2849e6ed078f3454ae4e105c41bd484fe070bd88 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Wed, 6 Oct 2021 14:06:00 +0500 Subject: [PATCH 4/8] test(RequestClient): updated --- src/auth/getAccessToken.ts | 2 +- src/request/RequestClient.ts | 2 +- src/test/RequestClient.spec.ts | 29 +++++++------ src/test/serverUtils.ts | 77 ++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 src/test/serverUtils.ts diff --git a/src/auth/getAccessToken.ts b/src/auth/getAccessToken.ts index d51833e..f42e442 100644 --- a/src/auth/getAccessToken.ts +++ b/src/auth/getAccessToken.ts @@ -46,7 +46,7 @@ export async function getAccessToken( ) .then((res) => res.result as SasAuthResponse) .catch((err) => { - throw prefixMessage(err, 'Error while getting access token') + throw prefixMessage(err, 'Error while getting access token. ') }) return authResponse diff --git a/src/request/RequestClient.ts b/src/request/RequestClient.ts index 409b6ba..c65cf0e 100644 --- a/src/request/RequestClient.ts +++ b/src/request/RequestClient.ts @@ -1,4 +1,4 @@ -import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' +import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' import { CsrfToken } from '..' import { isAuthorizeFormRequired, isLogInRequired } from '../auth' import { diff --git a/src/test/RequestClient.spec.ts b/src/test/RequestClient.spec.ts index f947f1b..d74397a 100644 --- a/src/test/RequestClient.spec.ts +++ b/src/test/RequestClient.spec.ts @@ -5,6 +5,12 @@ import { app, mockedAuthResponse } from './SAS_server_app' import { ServerType } from '@sasjs/utils' import SASjs from '../SASjs' import * as axiosModules from '../utils/createAxiosInstance' +import { + clientCert, + createCertificates, + rootCaCert, + serverCert +} from './serverUtils' const axiosActual = jest.requireActual('axios') @@ -17,6 +23,11 @@ jest const PORT = 8000 const SERVER_URL = `https://localhost:${PORT}/` +const ERROR_MESSAGES = { + selfSigned: 'self signed certificate', + CCA: 'unable to verify the first certificate' +} + describe('RequestClient', () => { let server: http.Server @@ -50,10 +61,8 @@ describe('RequestClient', () => { it('should response the POST method with Unauthorized', async () => { await expect( adapter.getAccessToken('clientId', 'clientSecret', 'incorrect') - ).rejects.toEqual( - new Error( - 'Error while getting access tokenRequest failed with status code 401' - ) + ).rejects.toThrow( + 'Error while getting access token. Request failed with status code 401' ) }) }) @@ -97,10 +106,8 @@ describe('RequestClient - Self Signed Server', () => { 'clientSecret', 'authCode' ) - ).rejects.toEqual( - expect.objectContaining({ - message: 'Error while getting access tokenself signed certificate' - }) + ).rejects.toThrow( + `Error while getting access token. ${ERROR_MESSAGES.selfSigned}` ) }) @@ -135,10 +142,8 @@ describe('RequestClient - Self Signed Server', () => { it('should response the POST method with Unauthorized', async () => { await expect( adapter.getAccessToken('clientId', 'clientSecret', 'incorrect') - ).rejects.toEqual( - new Error( - 'Error while getting access tokenRequest failed with status code 401' - ) + ).rejects.toThrow( + 'Error while getting access token. Request failed with status code 401' ) }) }) diff --git a/src/test/serverUtils.ts b/src/test/serverUtils.ts new file mode 100644 index 0000000..91fe35e --- /dev/null +++ b/src/test/serverUtils.ts @@ -0,0 +1,77 @@ +var https = require('https') +var { Cert } = require('selfsigned-ca') + +// Root CA certificate used to sign other certificates. +// argument(s) point to .crt and .key file paths - ./selfsigned.root-ca.crt & ./selfsigned.root-ca.key +export const rootCaCert = new Cert('selfsigned.root-ca') +// The certificate generated for use in the HTTP server. It is signed by the CA certificate. +// That way you can create any amount of certificates and they will be all trusted as long +// as the Root CA certificate is trusted (installed to device's keychain). +// argument(s) point to .crt and .key file paths - ./selfsigned.localhost.crt & ./selfsigned.localhost.key +export const serverCert = new Cert(`selfsigned.localhost`) +export const clientCert = new Cert(`selfsigned.client`) + +// .then(startHttpsServer) +// .then(() => console.log('certificates ready, server listening')) +// .catch(console.error) + +export async function createCertificates() { + // await createRootCertificate() + console.log('creating server certificate') + createServerCertificate() + console.log('server certificate created & stored') +} + +function startHttpsServer() { + var server = https.createServer(serverCert, (req: any, res: any) => { + res.writeHead(200) + res.end('hello world\n') + }) + server.listen(443) +} + +async function loadRootCertificate() { + await rootCaCert.load() + if (!(await rootCaCert.isInstalled())) { + // Make sure the CA is installed to device's keychain so that all server certificates + // signed by the CA are automatically trusted and green. + await rootCaCert.install() + } +} + +async function createRootCertificate() { + console.log('createRootCertificate') + // Couldn't load existing root CA certificate. Generate new one. + rootCaCert.createRootCa({ + subject: { + commonName: 'My Trusted Certificate Authority' + } + }) + console.log('rootCaCert', rootCaCert) + // console.log('createRootCertificate saving') + // await rootCaCert.save() + // console.log('createRootCertificate saved') + // Install the newly created CA to device's keychain so that all server certificates + // signed by the CA are automatically trusted and green. + // await rootCaCert.install() + // console.log('createRootCertificate installed') +} + +async function createServerCertificate() { + var serverCertOptions = { + subject: { + commonName: 'localhost' + }, + extensions: [ + { + name: 'subjectAltName', + altNames: [ + { type: 2, value: 'localhost' }, // DNS + { type: 7, ip: '127.0.0.1' } // IP + ] + } + ] + } + serverCert.create(serverCertOptions, rootCaCert) + await serverCert.save() +} From 6ff8eece7b35654a1e31265ffa3b6dd63ee21a3f Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Thu, 7 Oct 2021 13:45:50 +0500 Subject: [PATCH 5/8] chore: removed httpsAgent type + clean up --- src/SAS9ApiClient.ts | 9 ++-- src/SASjs.ts | 11 ++-- src/job-execution/Sas9JobExecutor.ts | 9 ++-- src/job-execution/fixit.txt | 15 ++++++ src/request/RequestClient.ts | 31 ++++------- src/request/Sas9RequestClient.ts | 6 +-- src/test/RequestClient.spec.ts | 14 +---- src/test/serverUtils.ts | 77 ---------------------------- src/types/HttpsAgent.ts | 10 ---- src/types/SASjsConfig.ts | 6 ++- src/types/index.ts | 1 - 11 files changed, 46 insertions(+), 143 deletions(-) create mode 100644 src/job-execution/fixit.txt delete mode 100644 src/test/serverUtils.ts delete mode 100644 src/types/HttpsAgent.ts diff --git a/src/SAS9ApiClient.ts b/src/SAS9ApiClient.ts index 6a66caf..b3f31a0 100644 --- a/src/SAS9ApiClient.ts +++ b/src/SAS9ApiClient.ts @@ -1,7 +1,7 @@ +import * as https from 'https' import { generateTimestamp } from '@sasjs/utils/time' import * as NodeFormData from 'form-data' import { Sas9RequestClient } from './request/Sas9RequestClient' -import { HttpsAgent } from './types/HttpsAgent' import { isUrl } from './utils' /** @@ -14,13 +14,10 @@ export class SAS9ApiClient { constructor( private serverUrl: string, private jobsPath: string, - httpsAgentConfiguration?: HttpsAgent + httpsAgentOptions?: https.AgentOptions ) { if (serverUrl) isUrl(serverUrl) - this.requestClient = new Sas9RequestClient( - serverUrl, - httpsAgentConfiguration - ) + this.requestClient = new Sas9RequestClient(serverUrl, httpsAgentOptions) } /** diff --git a/src/SASjs.ts b/src/SASjs.ts index d5f7853..dc2a034 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -36,7 +36,6 @@ const defaultConfig: SASjsConfig = { debug: false, contextName: 'SAS Job Execution compute context', useComputeApi: null, - httpsAgentConfiguration: {}, loginMechanism: LoginMechanism.Default } @@ -792,7 +791,7 @@ export default class SASjs { sasApiClient = new SAS9ApiClient( serverUrl, this.jobsPath, - this.sasjsConfig.httpsAgentConfiguration + this.sasjsConfig.httpsAgentOptions ) } } else { @@ -951,12 +950,12 @@ export default class SASjs { if (!this.requestClient) { this.requestClient = new RequestClient( this.sasjsConfig.serverUrl, - this.sasjsConfig.httpsAgentConfiguration + this.sasjsConfig.httpsAgentOptions ) } else { this.requestClient.setConfig( this.sasjsConfig.serverUrl, - this.sasjsConfig.httpsAgentConfiguration + this.sasjsConfig.httpsAgentOptions ) } @@ -995,7 +994,7 @@ export default class SASjs { this.sas9ApiClient = new SAS9ApiClient( this.sasjsConfig.serverUrl, this.jobsPath, - this.sasjsConfig.httpsAgentConfiguration + this.sasjsConfig.httpsAgentOptions ) } @@ -1018,7 +1017,7 @@ export default class SASjs { this.sasjsConfig.serverUrl, this.sasjsConfig.serverType!, this.jobsPath, - this.sasjsConfig.httpsAgentConfiguration + this.sasjsConfig.httpsAgentOptions ) this.computeJobExecutor = new ComputeJobExecutor( diff --git a/src/job-execution/Sas9JobExecutor.ts b/src/job-execution/Sas9JobExecutor.ts index b4fb549..7325bdf 100644 --- a/src/job-execution/Sas9JobExecutor.ts +++ b/src/job-execution/Sas9JobExecutor.ts @@ -1,10 +1,10 @@ +import * as https from 'https' import { ServerType } from '@sasjs/utils/types' import * as NodeFormData from 'form-data' import { ErrorResponse } from '../types/errors' import { convertToCSV, isRelativePath } from '../utils' import { BaseJobExecutor } from './JobExecutor' import { Sas9RequestClient } from '../request/Sas9RequestClient' -import { HttpsAgent } from '../types/HttpsAgent' /** * Job executor for SAS9 servers for use in Node.js environments. @@ -18,13 +18,10 @@ export class Sas9JobExecutor extends BaseJobExecutor { serverUrl: string, serverType: ServerType, private jobsPath: string, - httpsAgentConfiguration: HttpsAgent + httpsAgentOptions?: https.AgentOptions ) { super(serverUrl, serverType) - this.requestClient = new Sas9RequestClient( - serverUrl, - httpsAgentConfiguration - ) + this.requestClient = new Sas9RequestClient(serverUrl, httpsAgentOptions) } async execute(sasJob: string, data: any, config: any) { diff --git a/src/job-execution/fixit.txt b/src/job-execution/fixit.txt new file mode 100644 index 0000000..8ff967a --- /dev/null +++ b/src/job-execution/fixit.txt @@ -0,0 +1,15 @@ +>> self signed certificate in certificate chain + + + + const { key, cert } = serverCert + const httpsServer = https.createServer( + { + key, + cert, + ca: rootCaCert.cert, + requestCert: true, + rejectUnauthorized: false + }, + app + ) \ No newline at end of file diff --git a/src/request/RequestClient.ts b/src/request/RequestClient.ts index c65cf0e..1392599 100644 --- a/src/request/RequestClient.ts +++ b/src/request/RequestClient.ts @@ -1,4 +1,5 @@ import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' +import * as https from 'https' import { CsrfToken } from '..' import { isAuthorizeFormRequired, isLogInRequired } from '../auth' import { @@ -17,7 +18,6 @@ import { parseSourceCode, createAxiosInstance } from '../utils' -import { HttpsAgent } from '../types/HttpsAgent' export interface HttpClient { get( @@ -59,12 +59,15 @@ export class RequestClient implements HttpClient { protected fileUploadCsrfToken: CsrfToken | undefined protected httpClient!: AxiosInstance - constructor(protected baseUrl: string, httpsAgentConfiguration?: HttpsAgent) { - this.createHttpClient(baseUrl, httpsAgentConfiguration) + constructor( + protected baseUrl: string, + httpsAgentOptions?: https.AgentOptions + ) { + this.createHttpClient(baseUrl, httpsAgentOptions) } - public setConfig(baseUrl: string, httpsAgentConfiguration?: HttpsAgent) { - this.createHttpClient(baseUrl, httpsAgentConfiguration) + public setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions) { + this.createHttpClient(baseUrl, httpsAgentOptions) } public getCsrfToken(type: 'general' | 'file' = 'general') { @@ -518,24 +521,12 @@ export class RequestClient implements HttpClient { private createHttpClient( baseUrl: string, - httpsAgentConfiguration: HttpsAgent = {} + httpsAgentOptions?: https.AgentOptions ) { - const https = require('https') - const { selfSigned, clientCA, allowInsecure } = httpsAgentConfiguration - - const httpsAgentOptions = selfSigned - ? { ca: selfSigned.ca } - : clientCA - ? { key: clientCA.key, cert: clientCA.cert, requestCert: true } - : allowInsecure - ? { rejectUnauthorized: !allowInsecure } + const httpsAgent = httpsAgentOptions + ? new https.Agent(httpsAgentOptions) : undefined - const httpsAgent = - httpsAgentOptions && https.Agent - ? new https.Agent(httpsAgentOptions) - : undefined - this.httpClient = createAxiosInstance(baseUrl, httpsAgent) this.httpClient.defaults.validateStatus = (status) => diff --git a/src/request/Sas9RequestClient.ts b/src/request/Sas9RequestClient.ts index 9f7fe2b..f5575c5 100644 --- a/src/request/Sas9RequestClient.ts +++ b/src/request/Sas9RequestClient.ts @@ -1,17 +1,17 @@ +import * as https from 'https' import { AxiosRequestConfig } from 'axios' import axiosCookieJarSupport from 'axios-cookiejar-support' import * as tough from 'tough-cookie' import { prefixMessage } from '@sasjs/utils/error' import { RequestClient, throwIfError } from './RequestClient' -import { HttpsAgent } from '../types/HttpsAgent' /** * Specific request client for SAS9 in Node.js environments. * Handles redirects and cookie management. */ export class Sas9RequestClient extends RequestClient { - constructor(baseUrl: string, httpsAgentConfiguration?: HttpsAgent) { - super(baseUrl, httpsAgentConfiguration) + constructor(baseUrl: string, httpsAgentOptions?: https.AgentOptions) { + super(baseUrl, httpsAgentOptions) this.httpClient.defaults.maxRedirects = 0 this.httpClient.defaults.validateStatus = (status) => status >= 200 && status < 303 diff --git a/src/test/RequestClient.spec.ts b/src/test/RequestClient.spec.ts index d74397a..961043c 100644 --- a/src/test/RequestClient.spec.ts +++ b/src/test/RequestClient.spec.ts @@ -5,12 +5,6 @@ import { app, mockedAuthResponse } from './SAS_server_app' import { ServerType } from '@sasjs/utils' import SASjs from '../SASjs' import * as axiosModules from '../utils/createAxiosInstance' -import { - clientCert, - createCertificates, - rootCaCert, - serverCert -} from './serverUtils' const axiosActual = jest.requireActual('axios') @@ -84,9 +78,7 @@ describe('RequestClient - Self Signed Server', () => { adapter = new SASjs({ serverUrl: SERVER_URL, serverType: ServerType.SasViya, - httpsAgentConfiguration: { - selfSigned: { ca: [sslConfig.certificate] } - } + httpsAgentOptions: { ca: [sslConfig.certificate] } }) }) @@ -115,9 +107,7 @@ describe('RequestClient - Self Signed Server', () => { const adapterAllowInsecure = new SASjs({ serverUrl: SERVER_URL, serverType: ServerType.SasViya, - httpsAgentConfiguration: { - allowInsecure: true - } + httpsAgentOptions: { rejectUnauthorized: false } }) const authResponse = await adapterAllowInsecure.getAccessToken( diff --git a/src/test/serverUtils.ts b/src/test/serverUtils.ts deleted file mode 100644 index 91fe35e..0000000 --- a/src/test/serverUtils.ts +++ /dev/null @@ -1,77 +0,0 @@ -var https = require('https') -var { Cert } = require('selfsigned-ca') - -// Root CA certificate used to sign other certificates. -// argument(s) point to .crt and .key file paths - ./selfsigned.root-ca.crt & ./selfsigned.root-ca.key -export const rootCaCert = new Cert('selfsigned.root-ca') -// The certificate generated for use in the HTTP server. It is signed by the CA certificate. -// That way you can create any amount of certificates and they will be all trusted as long -// as the Root CA certificate is trusted (installed to device's keychain). -// argument(s) point to .crt and .key file paths - ./selfsigned.localhost.crt & ./selfsigned.localhost.key -export const serverCert = new Cert(`selfsigned.localhost`) -export const clientCert = new Cert(`selfsigned.client`) - -// .then(startHttpsServer) -// .then(() => console.log('certificates ready, server listening')) -// .catch(console.error) - -export async function createCertificates() { - // await createRootCertificate() - console.log('creating server certificate') - createServerCertificate() - console.log('server certificate created & stored') -} - -function startHttpsServer() { - var server = https.createServer(serverCert, (req: any, res: any) => { - res.writeHead(200) - res.end('hello world\n') - }) - server.listen(443) -} - -async function loadRootCertificate() { - await rootCaCert.load() - if (!(await rootCaCert.isInstalled())) { - // Make sure the CA is installed to device's keychain so that all server certificates - // signed by the CA are automatically trusted and green. - await rootCaCert.install() - } -} - -async function createRootCertificate() { - console.log('createRootCertificate') - // Couldn't load existing root CA certificate. Generate new one. - rootCaCert.createRootCa({ - subject: { - commonName: 'My Trusted Certificate Authority' - } - }) - console.log('rootCaCert', rootCaCert) - // console.log('createRootCertificate saving') - // await rootCaCert.save() - // console.log('createRootCertificate saved') - // Install the newly created CA to device's keychain so that all server certificates - // signed by the CA are automatically trusted and green. - // await rootCaCert.install() - // console.log('createRootCertificate installed') -} - -async function createServerCertificate() { - var serverCertOptions = { - subject: { - commonName: 'localhost' - }, - extensions: [ - { - name: 'subjectAltName', - altNames: [ - { type: 2, value: 'localhost' }, // DNS - { type: 7, ip: '127.0.0.1' } // IP - ] - } - ] - } - serverCert.create(serverCertOptions, rootCaCert) - await serverCert.save() -} diff --git a/src/types/HttpsAgent.ts b/src/types/HttpsAgent.ts deleted file mode 100644 index 5932599..0000000 --- a/src/types/HttpsAgent.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface HttpsAgent { - selfSigned?: { - ca: string[] - } - clientCA?: { - key: string - cert: string - } - allowInsecure?: boolean -} diff --git a/src/types/SASjsConfig.ts b/src/types/SASjsConfig.ts index 5fb6dad..2a8f64c 100644 --- a/src/types/SASjsConfig.ts +++ b/src/types/SASjsConfig.ts @@ -1,5 +1,5 @@ +import * as https from 'https' import { ServerType } from '@sasjs/utils/types' -import { HttpsAgent } from './HttpsAgent' /** * Specifies the configuration for the SASjs instance - eg where and how to @@ -59,7 +59,9 @@ export class SASjsConfig { * When set to `true`, the adapter will allow requests to SAS servers that use a self-signed SSL certificate. * Changing this setting is not recommended. */ - httpsAgentConfiguration: HttpsAgent = {} + + httpsAgentOptions?: https.AgentOptions + /** * Supported login mechanisms are - Redirected and Default */ diff --git a/src/types/index.ts b/src/types/index.ts index 3998768..2303619 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,7 +2,6 @@ export * from './Context' export * from './CsrfToken' export * from './Folder' export * from './File' -export * from './HttpsAgent' export * from './Job' export * from './JobDefinition' export * from './JobResult' From ff4915f7f3ca182e48f9d2f0747827b53709a730 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Thu, 7 Oct 2021 13:47:28 +0500 Subject: [PATCH 6/8] chore: comment file removed --- src/job-execution/fixit.txt | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 src/job-execution/fixit.txt diff --git a/src/job-execution/fixit.txt b/src/job-execution/fixit.txt deleted file mode 100644 index 8ff967a..0000000 --- a/src/job-execution/fixit.txt +++ /dev/null @@ -1,15 +0,0 @@ ->> self signed certificate in certificate chain - - - - const { key, cert } = serverCert - const httpsServer = https.createServer( - { - key, - cert, - ca: rootCaCert.cert, - requestCert: true, - rejectUnauthorized: false - }, - app - ) \ No newline at end of file From 9dc0499f66f3e3854a9240c135494680ddf01318 Mon Sep 17 00:00:00 2001 From: Mihajlo Date: Wed, 27 Oct 2021 13:20:59 +0200 Subject: [PATCH 7/8] chore: allow insecure remove --- sasjs-tests/src/testSuites/Basic.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/sasjs-tests/src/testSuites/Basic.ts b/sasjs-tests/src/testSuites/Basic.ts index d3e9f21..38582b7 100644 --- a/sasjs-tests/src/testSuites/Basic.ts +++ b/sasjs-tests/src/testSuites/Basic.ts @@ -13,7 +13,6 @@ const defaultConfig: SASjsConfig = { debug: false, contextName: 'SAS Job Execution compute context', useComputeApi: false, - allowInsecureRequests: false, loginMechanism: LoginMechanism.Default } From bc82cb5f5e6ccef578c9d426bc873ada9bb53685 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Fri, 29 Oct 2021 00:29:14 +0500 Subject: [PATCH 8/8] chore: TSDoc comment updated --- src/types/SASjsConfig.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/types/SASjsConfig.ts b/src/types/SASjsConfig.ts index 2a8f64c..af26204 100644 --- a/src/types/SASjsConfig.ts +++ b/src/types/SASjsConfig.ts @@ -55,13 +55,11 @@ export class SASjsConfig { */ useComputeApi: boolean | null = null /** - * Defaults to `false`. - * When set to `true`, the adapter will allow requests to SAS servers that use a self-signed SSL certificate. - * Changing this setting is not recommended. + * Optional settings to configure HTTPS Agent. + * By providing `key`, `cert`, `ca` to connect with server + * Other options can be set `rejectUnauthorized` and `requestCert` */ - httpsAgentOptions?: https.AgentOptions - /** * Supported login mechanisms are - Redirected and Default */