import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' import * as https from 'https' import { CsrfToken } from '..' import { isAuthorizeFormRequired, isLogInRequired } from '../auth' import { AuthorizeError, LoginRequiredError, NotFoundError, InternalServerError, JobExecutionError, CertificateError } from '../types/errors' import { SASjsRequest } from '../types' import { parseWeboutResponse } from '../utils/parseWeboutResponse' import { prefixMessage } from '@sasjs/utils/error' import { SAS9AuthError } from '../types/errors/SAS9AuthError' import { parseGeneratedCode, parseSourceCode, createAxiosInstance } from '../utils' import { InvalidSASjsCsrfError } from '../types/errors/InvalidSASjsCsrfError' export interface HttpClient { get( url: string, accessToken: string | undefined, contentType: string, overrideHeaders: { [key: string]: string | number } ): Promise<{ result: T; etag: string }> post( url: string, data: any, accessToken: string | undefined, contentType: string, overrideHeaders: { [key: string]: string | number } ): Promise<{ result: T; etag: string }> put( url: string, data: any, accessToken: string | undefined, overrideHeaders: { [key: string]: string | number } ): Promise<{ result: T; etag: string }> delete( url: string, accessToken: string | undefined ): Promise<{ result: T; etag: string }> getCsrfToken(type: 'general' | 'file'): CsrfToken | undefined saveLocalStorageToken(accessToken: string, refreshToken: string): void clearCsrfTokens(): void clearLocalStorageTokens(): void getBaseUrl(): string } export class RequestClient implements HttpClient { private requests: SASjsRequest[] = [] private requestsLimit: number = 10 protected csrfToken: CsrfToken = { headerName: '', value: '' } protected fileUploadCsrfToken: CsrfToken | undefined protected httpClient!: AxiosInstance constructor( protected baseUrl: string, httpsAgentOptions?: https.AgentOptions, requestsLimit?: number ) { this.createHttpClient(baseUrl, httpsAgentOptions) if (requestsLimit) this.requestsLimit = requestsLimit } public setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions) { this.createHttpClient(baseUrl, httpsAgentOptions) } public saveLocalStorageToken(accessToken: string, refreshToken: string) { localStorage.setItem('accessToken', accessToken) localStorage.setItem('refreshToken', refreshToken) } public getCsrfToken(type: 'general' | 'file' = 'general') { return type === 'file' ? this.fileUploadCsrfToken : this.csrfToken } public clearCsrfTokens() { this.csrfToken = { headerName: '', value: '' } this.fileUploadCsrfToken = { headerName: '', value: '' } } public clearLocalStorageTokens() { localStorage.setItem('accessToken', '') localStorage.setItem('refreshToken', '') } public getBaseUrl() { return this.httpClient.defaults.baseURL || '' } /** * this method returns all requests, an array of SASjsRequest type * @returns SASjsRequest[] */ public getRequests = () => this.requests /** * this method clears the requests array, i.e set to empty */ public clearRequests = () => { this.requests = [] } /** * this method appends the response from sasjs request to requests array * @param response - response from sasjs request * @param program - name of program * @param debug - a boolean that indicates whether debug was enabled or not */ public appendRequest(response: any, program: string, debug: boolean) { let sourceCode = '' let generatedCode = '' let sasWork = null if (debug) { if (response?.log) { sourceCode = parseSourceCode(response.log) generatedCode = parseGeneratedCode(response.log) if (response?.result) { sasWork = response.result.WORK } else { sasWork = response.log } } else if (response?.result) { // We parse only if it's a string, otherwise it would throw error if (typeof response.result === 'string') { sourceCode = parseSourceCode(response.result) generatedCode = parseGeneratedCode(response.result) } sasWork = response.result.WORK } } const stringifiedResult = typeof response?.result === 'string' ? response?.result : JSON.stringify(response?.result, null, 2) this.requests.push({ logFile: response?.log || stringifiedResult || response, serviceLink: program, timestamp: new Date(), sourceCode, generatedCode, SASWORK: sasWork }) if (this.requests.length > this.requestsLimit) { this.requests.splice(0, 1) } } public async get( url: string, accessToken: string | undefined, contentType: string = 'application/json', overrideHeaders: { [key: string]: string | number } = {}, debug: boolean = false ): Promise<{ result: T; etag: string; status: number }> { const headers = { ...this.getHeaders(accessToken, contentType), ...overrideHeaders } const requestConfig: AxiosRequestConfig = { headers, responseType: contentType === 'text/plain' ? 'text' : 'json', withCredentials: true } if (contentType === 'text/plain') { requestConfig.transformResponse = undefined } return this.httpClient .get(url, requestConfig) .then((response) => { throwIfError(response) return this.parseResponse(response) }) .catch(async (e: any) => { return await this.handleError( e, () => this.get(url, accessToken, contentType, overrideHeaders).catch( (err) => { throw prefixMessage( err, 'Error while executing handle error callback. ' ) } ), debug ) }) } public async post( url: string, data: any, accessToken: string | undefined, contentType = 'application/json', overrideHeaders: { [key: string]: string | number } = {}, additionalSettings: { [key: string]: string | number } = {} ): Promise<{ result: T; etag: string }> { const headers = { ...this.getHeaders(accessToken, contentType), ...overrideHeaders } return this.httpClient .post(url, data, { headers, withCredentials: true, ...additionalSettings }) .then((response) => { throwIfError(response) return this.parseResponse(response) }) .catch(async (e: any) => { return await this.handleError(e, () => this.post(url, data, accessToken, contentType, overrideHeaders) ) }) } public async put( url: string, data: any, accessToken: string | undefined, overrideHeaders: { [key: string]: string | number } = {} ): Promise<{ result: T; etag: string }> { const headers = { ...this.getHeaders(accessToken, 'application/json'), ...overrideHeaders } return this.httpClient .put(url, data, { headers, withCredentials: true }) .then((response) => { throwIfError(response) return this.parseResponse(response) }) .catch(async (e: any) => { return await this.handleError(e, () => this.put(url, data, accessToken, overrideHeaders) ) }) } public async delete( url: string, accessToken?: string ): Promise<{ result: T; etag: string }> { const headers = this.getHeaders(accessToken, 'application/json') return this.httpClient .delete(url, { headers, withCredentials: true }) .then((response) => { throwIfError(response) return this.parseResponse(response) }) .catch(async (e: any) => { return await this.handleError(e, () => this.delete(url, accessToken)) }) } public async patch( url: string, data: any = {}, accessToken?: string ): Promise<{ result: T; etag: string }> { const headers = this.getHeaders(accessToken, 'application/json') return this.httpClient .patch(url, data, { headers, withCredentials: true }) .then((response) => { throwIfError(response) return this.parseResponse(response) }) .catch(async (e: any) => { return await this.handleError(e, () => this.patch(url, data, accessToken) ) }) } public async uploadFile( url: string, content: string, accessToken?: string ): Promise { const headers = this.getHeaders(accessToken, 'application/json') if (this.fileUploadCsrfToken?.value) { headers[this.fileUploadCsrfToken.headerName] = this.fileUploadCsrfToken.value } try { const response = await this.httpClient.post(url, content, { headers, transformRequest: (requestBody) => requestBody }) return { result: response.data, etag: response.headers['etag'] as string } } catch (e: any) { const response = e.response as AxiosResponse if (response?.status === 403 || response?.status === 449) { this.parseAndSetFileUploadCsrfToken(response) if (this.fileUploadCsrfToken) { return this.uploadFile(url, content, accessToken) } throw e } throw e } } public authorize = async (response: string) => { let authUrl: string | null = null const params: any = {} const responseBody = response.split('')[1].split('')[0] const bodyElement = document.createElement('div') bodyElement.innerHTML = responseBody const form = bodyElement.querySelector('#application_authorization') authUrl = form ? this.baseUrl + form.getAttribute('action') : null const inputs: any = form?.querySelectorAll('input') for (const input of inputs) { if (input.name === 'user_oauth_approval') { input.value = 'true' } params[input.name] = input.value } const csrfTokenKey = Object.keys(params).find((k) => k?.toLowerCase().includes('csrf') ) if (csrfTokenKey) { this.csrfToken.value = params[csrfTokenKey] this.csrfToken.headerName = this.csrfToken.headerName || 'x-csrf-token' } const formData = new FormData() for (const key in params) { if (params.hasOwnProperty(key)) { formData.append(key, params[key]) } } if (!authUrl) { throw new Error('Auth Form URL is null or undefined.') } return await this.httpClient .post(authUrl, formData, { responseType: 'text', headers: { Accept: '*/*', 'Content-Type': 'text/plain' } }) .then((res) => res.data) .catch((error) => { const logger = process.logger || console logger.error(error) }) } protected getHeaders = ( accessToken: string | undefined, contentType: string ) => { const headers: any = {} if (contentType !== 'application/x-www-form-urlencoded') { headers['Content-Type'] = contentType } if (contentType === 'application/json') { headers.Accept = 'application/json' } else { headers.Accept = '*/*' } if (accessToken) { headers.Authorization = `Bearer ${accessToken}` } if (this.csrfToken.headerName && this.csrfToken.value) { headers[this.csrfToken.headerName] = this.csrfToken.value } return headers } protected parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => { const token = this.parseCsrfToken(response) if (token) { this.fileUploadCsrfToken = token } } protected parseAndSetCsrfToken = (response: AxiosResponse) => { const token = this.parseCsrfToken(response) if (token) { this.csrfToken = token } } private parseCsrfToken = (response: AxiosResponse): CsrfToken | undefined => { const tokenHeader = ( response.headers['x-csrf-header'] as string )?.toLowerCase() if (tokenHeader) { const token = response.headers[tokenHeader] const csrfToken = { headerName: tokenHeader, value: token || '' } return csrfToken } } protected handleError = async ( e: any, callback: any, debug: boolean = false ) => { const response = e.response as AxiosResponse if (e instanceof AuthorizeError) { const res = await this.httpClient .get(e.confirmUrl, { responseType: 'text', headers: { 'Content-Type': 'text/plain', Accept: '*/*' } }) .catch((err) => { throw prefixMessage(err, 'Error while getting error confirmUrl. ') }) if (isAuthorizeFormRequired(res?.data as string)) { await this.authorize(res.data as string).catch((err) => { throw prefixMessage(err, 'Error while authorizing request. ') }) } return await callback().catch((err: any) => { throw prefixMessage( err, 'Error while executing callback in handleError. ' ) }) } if (e instanceof LoginRequiredError) { this.clearCsrfTokens() throw e } if (e instanceof InvalidSASjsCsrfError) { // Fetching root and creating CSRF cookie await this.httpClient .get('/', { withCredentials: true }) .then((response) => { const cookie = /