diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 4dc7b9a07..fc435c7ec 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -37,7 +37,7 @@ const interpolateVars = require('./network/interpolate-vars'); const { getEnvVars, getTreePathFromCollectionToItem, mergeVars } = require('../utils/collection'); const { getProcessEnvVars } = require('../store/process-env'); const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, refreshOauth2Token } = require('../utils/oauth2'); -const { configureRequestWithCertsAndProxy } = require('../utils/request'); +const { configureRequestWithCertsAndProxy } = require('./network'); const environmentSecretsStore = new EnvironmentSecretsStore(); const collectionSecurityStore = new CollectionSecurityStore(); diff --git a/packages/bruno-electron/src/ipc/network/awsv4auth-helper.js b/packages/bruno-electron/src/ipc/network/awsv4auth-helper.js new file mode 100644 index 000000000..8714ae39c --- /dev/null +++ b/packages/bruno-electron/src/ipc/network/awsv4auth-helper.js @@ -0,0 +1,57 @@ +const { fromIni } = require('@aws-sdk/credential-providers'); +const { aws4Interceptor } = require('aws4-axios'); + +function isStrPresent(str) { + return str && str !== '' && str !== 'undefined'; +} + +async function resolveAwsV4Credentials(request) { + const awsv4 = request.awsv4config; + if (isStrPresent(awsv4.profileName)) { + try { + credentialsProvider = fromIni({ + profile: awsv4.profileName, + ignoreCache: true + }); + credentials = await credentialsProvider(); + awsv4.accessKeyId = credentials.accessKeyId; + awsv4.secretAccessKey = credentials.secretAccessKey; + awsv4.sessionToken = credentials.sessionToken; + } catch { + console.error('Failed to fetch credentials from AWS profile.'); + } + } + return awsv4; +} + +function addAwsV4Interceptor(axiosInstance, request) { + if (!request.awsv4config) { + console.warn('No Auth Config found!'); + return; + } + + const awsv4 = request.awsv4config; + if (!isStrPresent(awsv4.accessKeyId) || !isStrPresent(awsv4.secretAccessKey)) { + console.warn('Required Auth Fields are not present'); + return; + } + + const interceptor = aws4Interceptor({ + options: { + region: awsv4.region, + service: awsv4.service + }, + credentials: { + accessKeyId: awsv4.accessKeyId, + secretAccessKey: awsv4.secretAccessKey, + sessionToken: awsv4.sessionToken + } + }); + + axiosInstance.interceptors.request.use(interceptor); +} + +module.exports = { + addAwsV4Interceptor, + resolveAwsV4Credentials +}; diff --git a/packages/bruno-electron/src/utils/axios-instance.js b/packages/bruno-electron/src/ipc/network/axios-instance.js similarity index 98% rename from packages/bruno-electron/src/utils/axios-instance.js rename to packages/bruno-electron/src/ipc/network/axios-instance.js index 9ded8183b..f5218c46d 100644 --- a/packages/bruno-electron/src/utils/axios-instance.js +++ b/packages/bruno-electron/src/ipc/network/axios-instance.js @@ -3,9 +3,9 @@ const Socket = require('net').Socket; const axios = require('axios'); const connectionCache = new Map(); // Cache to store checkConnection() results const electronApp = require("electron"); -const { setupProxyAgents } = require('./proxy-util'); -const { addCookieToJar, getCookieStringForUrl } = require('./cookies'); -const { preferencesUtil } = require('../store/preferences'); +const { setupProxyAgents } = require('../../utils/proxy-util'); +const { addCookieToJar, getCookieStringForUrl } = require('../../utils/cookies'); +const { preferencesUtil } = require('../../store/preferences'); const LOCAL_IPV6 = '::1'; const LOCAL_IPV4 = '127.0.0.1'; diff --git a/packages/bruno-electron/src/utils/auth.js b/packages/bruno-electron/src/ipc/network/digestauth-helper.js similarity index 72% rename from packages/bruno-electron/src/utils/auth.js rename to packages/bruno-electron/src/ipc/network/digestauth-helper.js index 91f8b23b1..f01ba86df 100644 --- a/packages/bruno-electron/src/utils/auth.js +++ b/packages/bruno-electron/src/ipc/network/digestauth-helper.js @@ -1,49 +1,28 @@ -const { fromIni } = require('@aws-sdk/credential-providers'); -const { aws4Interceptor } = require('aws4-axios'); +const crypto = require('crypto'); +const { URL } = require('url'); -async function resolveAwsV4Credentials(request) { - const awsv4 = request.awsv4config; - if (isStrPresent(awsv4.profileName)) { - try { - credentialsProvider = fromIni({ - profile: awsv4.profileName - }); - credentials = await credentialsProvider(); - awsv4.accessKeyId = credentials.accessKeyId; - awsv4.secretAccessKey = credentials.secretAccessKey; - awsv4.sessionToken = credentials.sessionToken; - } catch { - console.error('Failed to fetch credentials from AWS profile.'); - } - } - return awsv4; +function isStrPresent(str) { + return str && str.trim() !== '' && str.trim() !== 'undefined'; } -function addAwsV4Interceptor(axiosInstance, request) { - if (!request.awsv4config) { - console.warn('No Auth Config found!'); - return; - } +function stripQuotes(str) { + return str.replace(/"/g, ''); +} - const awsv4 = request.awsv4config; - if (!isStrPresent(awsv4.accessKeyId) || !isStrPresent(awsv4.secretAccessKey)) { - console.warn('Required Auth Fields are not present'); - return; - } +function containsDigestHeader(response) { + const authHeader = response?.headers?.['www-authenticate']; + return authHeader ? authHeader.trim().toLowerCase().startsWith('digest') : false; +} - const interceptor = aws4Interceptor({ - options: { - region: awsv4.region, - service: awsv4.service - }, - credentials: { - accessKeyId: awsv4.accessKeyId, - secretAccessKey: awsv4.secretAccessKey, - sessionToken: awsv4.sessionToken - } - }); +function containsAuthorizationHeader(originalRequest) { + return Boolean( + originalRequest.headers['Authorization'] || + originalRequest.headers['authorization'] + ); +} - axiosInstance.interceptors.request.use(interceptor); +function md5(input) { + return crypto.createHash('md5').update(input).digest('hex'); } function addDigestInterceptor(axiosInstance, request) { @@ -144,32 +123,4 @@ function addDigestInterceptor(axiosInstance, request) { ); } -function containsDigestHeader(response) { - const authHeader = response?.headers?.['www-authenticate']; - return authHeader ? authHeader.trim().toLowerCase().startsWith('digest') : false; -} - -function containsAuthorizationHeader(originalRequest) { - return Boolean( - originalRequest.headers['Authorization'] || - originalRequest.headers['authorization'] - ); -} - -function md5(input) { - return crypto.createHash('md5').update(input).digest('hex'); -} - -function isStrPresent(str) { - return str && str !== '' && str !== 'undefined'; -} - -function stripQuotes(str) { - return str.replace(/"/g, ''); -} - -module.exports = { - addAwsV4Interceptor, - resolveAwsV4Credentials, - addDigestInterceptor -} \ No newline at end of file +module.exports = { addDigestInterceptor }; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index f1f8dca51..e68c0affc 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -3,24 +3,35 @@ const https = require('https'); const axios = require('axios'); const path = require('path'); const decomment = require('decomment'); +const iconv = require('iconv-lite'); +const fs = require('fs'); +const tls = require('tls'); const contentDispositionParser = require('content-disposition'); const mime = require('mime-types'); +const FormData = require('form-data'); const { ipcMain } = require('electron'); const { each, get, extend, cloneDeep } = require('lodash'); +const { NtlmClient } = require('axios-ntlm'); const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js'); -const { prepareRequest, prepareGqlIntrospectionRequest, getJsSandboxRuntime, configureRequest, parseDataFromResponse } = require('../../utils/request'); +const { interpolateString } = require('./interpolate-string'); +const { resolveAwsV4Credentials, addAwsV4Interceptor } = require('./awsv4auth-helper'); +const { addDigestInterceptor } = require('./digestauth-helper'); +const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request'); +const { prepareRequest } = require('./prepare-request'); +const interpolateVars = require('./interpolate-vars'); +const { makeAxiosInstance } = require('./axios-instance'); const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token'); const { uuid, safeStringifyJSON, safeParseJSON } = require('../../utils/common'); -const interpolateVars = require('./interpolate-vars'); +const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem'); +const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies'); +const { createFormData } = require('../../utils/form-data'); +const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars } = require('../../utils/collection'); +const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials } = require('../../utils/oauth2'); +const { setupProxyAgents } = require('../../utils/proxy-util'); const { preferencesUtil } = require('../../store/preferences'); const { getProcessEnvVars } = require('../../store/process-env'); const { getBrunoConfig } = require('../../store/bruno-config'); -const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem'); -const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies'); const Oauth2Store = require('../../store/oauth2'); -const FormData = require('form-data'); -const { createFormData } = require('../../utils/form-data'); -const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars } = require('../../utils/collection'); const saveCookies = (url, headers) => { if (preferencesUtil.shouldStoreCookies()) { @@ -38,6 +49,293 @@ const saveCookies = (url, headers) => { } } +const getJsSandboxRuntime = (collection) => { + const securityConfig = get(collection, 'securityConfig', {}); + return securityConfig.jsSandboxMode === 'safe' ? 'quickjs' : 'vm2'; +}; + +const parseDataFromResponse = (response, disableParsingResponseJson = false) => { + // Parse the charset from content type: https://stackoverflow.com/a/33192813 + const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || ''); + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals + const charsetValue = charsetMatch?.[1]; + const dataBuffer = Buffer.from(response.data); + // Overwrite the original data for backwards compatibility + let data; + if (iconv.encodingExists(charsetValue)) { + data = iconv.decode(dataBuffer, charsetValue); + } else { + data = iconv.decode(dataBuffer, 'utf-8'); + } + // Try to parse response to JSON, this can quietly fail + try { + // Filter out ZWNBSP character + // https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d + data = data.replace(/^\uFEFF/, ''); + + // If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed + if ( !disableParsingResponseJson && ! (typeof data === 'string' && data.startsWith("\"") && data.endsWith("\""))) { + data = Buffer?.isBuffer(data)? JSON.parse(data?.toString()) : JSON.parse(data); + } + } catch(error) { + console.error(error); + console.log('Failed to parse response data as JSON'); + } + + return { data, dataBuffer }; +}; + +const configureRequestWithCertsAndProxy = async ({ + collectionUid, + request, + envVars, + runtimeVariables, + processEnvVars, + collectionPath +}) => { + /** + * @see https://github.com/usebruno/bruno/issues/211 set keepAlive to true, this should fix socket hang up errors + * @see https://github.com/nodejs/node/pull/43522 keepAlive was changed to true globally on Node v19+ + */ + const httpsAgentRequestFields = { keepAlive: true }; + if (!preferencesUtil.shouldVerifyTls()) { + httpsAgentRequestFields['rejectUnauthorized'] = false; + } + + if (preferencesUtil.shouldUseCustomCaCertificate()) { + const caCertFilePath = preferencesUtil.getCustomCaCertificateFilePath(); + if (caCertFilePath) { + let caCertBuffer = fs.readFileSync(caCertFilePath); + if (preferencesUtil.shouldKeepDefaultCaCertificates()) { + caCertBuffer += '\n' + tls.rootCertificates.join('\n'); // Augment default truststore with custom CA certificates + } + httpsAgentRequestFields['ca'] = caCertBuffer; + } + } + + const brunoConfig = getBrunoConfig(collectionUid); + const interpolationOptions = { + envVars, + runtimeVariables, + processEnvVars + }; + + // client certificate config + const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []); + + for (let clientCert of clientCertConfig) { + const domain = interpolateString(clientCert?.domain, interpolationOptions); + const type = clientCert?.type || 'cert'; + if (domain) { + const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*'); + if (request.url.match(hostRegex)) { + if (type === 'cert') { + try { + let certFilePath = interpolateString(clientCert?.certFilePath, interpolationOptions); + certFilePath = path.isAbsolute(certFilePath) ? certFilePath : path.join(collectionPath, certFilePath); + let keyFilePath = interpolateString(clientCert?.keyFilePath, interpolationOptions); + keyFilePath = path.isAbsolute(keyFilePath) ? keyFilePath : path.join(collectionPath, keyFilePath); + + httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath); + httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath); + } catch (err) { + console.error('Error reading cert/key file', err); + throw new Error('Error reading cert/key file' + err); + } + } else if (type === 'pfx') { + try { + let pfxFilePath = interpolateString(clientCert?.pfxFilePath, interpolationOptions); + pfxFilePath = path.isAbsolute(pfxFilePath) ? pfxFilePath : path.join(collectionPath, pfxFilePath); + httpsAgentRequestFields['pfx'] = fs.readFileSync(pfxFilePath); + } catch (err) { + console.error('Error reading pfx file', err); + throw new Error('Error reading pfx file' + err); + } + } + httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions); + break; + } + } + } + + /** + * Proxy configuration + * + * Preferences proxyMode has three possible values: on, off, system + * Collection proxyMode has three possible values: true, false, global + * + * When collection proxyMode is true, it overrides the app-level proxy settings + * When collection proxyMode is false, it ignores the app-level proxy settings + * When collection proxyMode is global, it uses the app-level proxy settings + * + * Below logic calculates the proxyMode and proxyConfig to be used for the request + */ + let proxyMode = 'off'; + let proxyConfig = {}; + + const collectionProxyConfig = get(brunoConfig, 'proxy', {}); + const collectionProxyEnabled = get(collectionProxyConfig, 'enabled', 'global'); + if (collectionProxyEnabled === true) { + proxyConfig = collectionProxyConfig; + proxyMode = 'on'; + } else if (collectionProxyEnabled === 'global') { + proxyConfig = preferencesUtil.getGlobalProxyConfig(); + proxyMode = get(proxyConfig, 'mode', 'off'); + } + + setupProxyAgents({ + requestConfig: request, + proxyMode, + proxyConfig, + httpsAgentRequestFields, + interpolationOptions + }); + + return {proxyMode, newRequest: request, proxyConfig, httpsAgentRequestFields, interpolationOptions}; +} + +const configureRequest = async ( + collectionUid, + request, + envVars, + runtimeVariables, + processEnvVars, + collectionPath +) => { + const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; + if (!protocolRegex.test(request.url)) { + request.url = `http://${request.url}`; + } + + const {proxyMode, newRequest, proxyConfig, httpsAgentRequestFields, interpolationOptions} = await configureRequestWithCertsAndProxy({ + collectionUid, + request, + envVars, + runtimeVariables, + processEnvVars, + collectionPath + }); + + request = newRequest + let requestMaxRedirects = request.maxRedirects + // Don't override maxRedirects here, let it be controlled by the request object + // request.maxRedirects = 0 + + // Set default value for requestMaxRedirects if not explicitly set + if (requestMaxRedirects === undefined) { + requestMaxRedirects = 5; // Default to 5 redirects + } + + let axiosInstance = makeAxiosInstance({ + proxyMode, + proxyConfig, + requestMaxRedirects, + httpsAgentRequestFields, + interpolationOptions + }); + + if (request.ntlmConfig) { + axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults) + delete request.ntlmConfig; + } + + if (request.oauth2) { + let requestCopy = cloneDeep(request); + const { oauth2: { grantType, tokenPlacement, tokenHeaderPrefix, tokenQueryKey } = {} } = requestCopy || {}; + let credentials, credentialsId; + switch (grantType) { + case 'authorization_code': + interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); + ({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid })); + request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid }; + if (tokenPlacement == 'header') { + request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`; + } + else { + try { + const url = new URL(request.url); + url?.searchParams?.set(tokenQueryKey, credentials?.access_token); + request.url = url?.toString(); + } + catch(error) {} + } + break; + case 'client_credentials': + interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); + ({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid })); + request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid }; + if (tokenPlacement == 'header') { + request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`; + } + else { + try { + const url = new URL(request.url); + url?.searchParams?.set(tokenQueryKey, credentials?.access_token); + request.url = url?.toString(); + } + catch(error) {} + } + break; + case 'password': + interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); + ({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid })); + request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid }; + if (tokenPlacement == 'header') { + request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`; + } + else { + try { + const url = new URL(request.url); + url?.searchParams?.set(tokenQueryKey, credentials?.access_token); + request.url = url?.toString(); + } + catch(error) {} + } + break; + } + } + + if (request.awsv4config) { + request.awsv4config = await resolveAwsV4Credentials(request); + addAwsV4Interceptor(axiosInstance, request); + delete request.awsv4config; + } + + if (request.digestConfig) { + addDigestInterceptor(axiosInstance, request); + } + + request.timeout = preferencesUtil.getRequestTimeout(); + + // add cookies to request + if (preferencesUtil.shouldSendCookies()) { + const cookieString = getCookieStringForUrl(request.url); + if (cookieString && typeof cookieString === 'string' && cookieString.length) { + request.headers['cookie'] = cookieString; + } + } + + // Add API key to the URL + if (request.apiKeyAuthValueForQueryParams && request.apiKeyAuthValueForQueryParams.placement === 'queryparams') { + const urlObj = new URL(request.url); + + // Interpolate key and value as they can be variables before adding to the URL. + const key = interpolateString(request.apiKeyAuthValueForQueryParams.key, interpolationOptions); + const value = interpolateString(request.apiKeyAuthValueForQueryParams.value, interpolationOptions); + + urlObj.searchParams.set(key, value); + request.url = urlObj.toString(); + } + + // Remove pathParams, already in URL (Issue #2439) + delete request.pathParams; + + // Remove apiKeyAuthValueForQueryParams, already interpolated and added to URL + delete request.apiKeyAuthValueForQueryParams; + + return axiosInstance; +}; + const registerNetworkIpc = (mainWindow) => { const onConsoleLog = (type, args) => { console[type](...args); @@ -986,3 +1284,4 @@ const registerNetworkIpc = (mainWindow) => { module.exports = registerNetworkIpc; module.exports.configureRequest = configureRequest; +module.exports.configureRequestWithCertsAndProxy = configureRequestWithCertsAndProxy; diff --git a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js new file mode 100644 index 000000000..c137c4b33 --- /dev/null +++ b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js @@ -0,0 +1,48 @@ +const { get, each } = require('lodash'); +const { interpolate } = require('@usebruno/common'); +const { getIntrospectionQuery } = require('graphql'); +const { setAuthHeaders } = require('./prepare-request'); + +const prepareGqlIntrospectionRequest = (endpoint, envVars, request, collectionRoot) => { + if (endpoint && endpoint.length) { + endpoint = interpolate(endpoint, envVars); + } + + const queryParams = { + query: getIntrospectionQuery() + }; + + let axiosRequest = { + method: 'POST', + url: endpoint, + headers: { + ...mapHeaders(request.headers, get(collectionRoot, 'request.headers', [])), + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(queryParams) + }; + + return setAuthHeaders(axiosRequest, request, collectionRoot); +}; + +const mapHeaders = (requestHeaders, collectionHeaders) => { + const headers = {}; + + each(requestHeaders, (h) => { + if (h.enabled) { + headers[h.name] = h.value; + } + }); + + // collection headers + each(collectionHeaders, (h) => { + if (h.enabled) { + headers[h.name] = h.value; + } + }); + + return headers; +}; + +module.exports = prepareGqlIntrospectionRequest; diff --git a/packages/bruno-electron/src/utils/request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js similarity index 55% rename from packages/bruno-electron/src/utils/request.js rename to packages/bruno-electron/src/ipc/network/prepare-request.js index 44f5cc376..18436d423 100644 --- a/packages/bruno-electron/src/utils/request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -1,27 +1,8 @@ -const https = require('https'); -const fs = require('fs'); -const path = require('path'); -const tls = require('tls'); -const { isUndefined, isNull, each, get, cloneDeep, filter } = require('lodash'); +const { each, get, filter } = require('lodash'); const decomment = require('decomment'); const crypto = require('node:crypto'); -const { getIntrospectionQuery } = require('graphql'); -const { HttpProxyAgent } = require('http-proxy-agent'); -const { SocksProxyAgent } = require('socks-proxy-agent'); -const iconv = require('iconv-lite'); -const { interpolate } = require('@usebruno/common'); -const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars, getFormattedCollectionOauth2Credentials, mergeAuth } = require('./collection'); -const { buildFormUrlEncodedPayload } = require('./form-data'); -const { setupProxyAgents } = require('./proxy-util'); -const { makeAxiosInstance } = require('./axios-instance'); -const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials } = require('./oauth2'); -const { resolveAwsV4Credentials, addAwsV4Interceptor, addDigestInterceptor } = require('./auth'); -const { getCookieStringForUrl } = require('./cookies'); -const { preferencesUtil } = require('../store/preferences'); -const { getBrunoConfig } = require('../store/bruno-config'); -const { interpolateString } = require('../ipc/network/interpolate-string'); -const interpolateVars = require('../ipc/network/interpolate-vars'); -const { NtlmClient } = require('axios-ntlm'); +const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars, getFormattedCollectionOauth2Credentials, mergeAuth } = require('../../utils/collection'); +const { buildFormUrlEncodedPayload, createFormData } = require('../../utils/form-data'); const setAuthHeaders = (axiosRequest, request, collectionRoot) => { const collectionAuth = get(collectionRoot, 'request.auth'); @@ -404,343 +385,7 @@ const prepareRequest = (item, collection) => { return axiosRequest; }; -const prepareGqlIntrospectionRequest = (endpoint, envVars, request, collectionRoot) => { - if (endpoint && endpoint.length) { - endpoint = interpolate(endpoint, envVars); - } - - const queryParams = { - query: getIntrospectionQuery() - }; - - let axiosRequest = { - method: 'POST', - url: endpoint, - headers: { - ...mapHeaders(request.headers, get(collectionRoot, 'request.headers', [])), - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - data: JSON.stringify(queryParams) - }; - - return setAuthHeaders(axiosRequest, request, collectionRoot); -}; - -const mapHeaders = (requestHeaders, collectionHeaders) => { - const headers = {}; - - each(requestHeaders, (h) => { - if (h.enabled) { - headers[h.name] = h.value; - } - }); - - // collection headers - each(collectionHeaders, (h) => { - if (h.enabled) { - headers[h.name] = h.value; - } - }); - - return headers; -}; - -const configureRequestWithCertsAndProxy = async ({ - collectionUid, - request, - envVars, - runtimeVariables, - processEnvVars, - collectionPath -}) => { - /** - * @see https://github.com/usebruno/bruno/issues/211 set keepAlive to true, this should fix socket hang up errors - * @see https://github.com/nodejs/node/pull/43522 keepAlive was changed to true globally on Node v19+ - */ - const httpsAgentRequestFields = { keepAlive: true }; - if (!preferencesUtil.shouldVerifyTls()) { - httpsAgentRequestFields['rejectUnauthorized'] = false; - } - - if (preferencesUtil.shouldUseCustomCaCertificate()) { - const caCertFilePath = preferencesUtil.getCustomCaCertificateFilePath(); - if (caCertFilePath) { - let caCertBuffer = fs.readFileSync(caCertFilePath); - if (preferencesUtil.shouldKeepDefaultCaCertificates()) { - caCertBuffer += '\n' + tls.rootCertificates.join('\n'); // Augment default truststore with custom CA certificates - } - httpsAgentRequestFields['ca'] = caCertBuffer; - } - } - - const brunoConfig = getBrunoConfig(collectionUid); - const interpolationOptions = { - envVars, - runtimeVariables, - processEnvVars - }; - - // client certificate config - const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []); - - for (let clientCert of clientCertConfig) { - const domain = interpolateString(clientCert?.domain, interpolationOptions); - const type = clientCert?.type || 'cert'; - if (domain) { - const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*'); - if (request.url.match(hostRegex)) { - if (type === 'cert') { - try { - let certFilePath = interpolateString(clientCert?.certFilePath, interpolationOptions); - certFilePath = path.isAbsolute(certFilePath) ? certFilePath : path.join(collectionPath, certFilePath); - let keyFilePath = interpolateString(clientCert?.keyFilePath, interpolationOptions); - keyFilePath = path.isAbsolute(keyFilePath) ? keyFilePath : path.join(collectionPath, keyFilePath); - - httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath); - httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath); - } catch (err) { - console.error('Error reading cert/key file', err); - throw new Error('Error reading cert/key file' + err); - } - } else if (type === 'pfx') { - try { - let pfxFilePath = interpolateString(clientCert?.pfxFilePath, interpolationOptions); - pfxFilePath = path.isAbsolute(pfxFilePath) ? pfxFilePath : path.join(collectionPath, pfxFilePath); - httpsAgentRequestFields['pfx'] = fs.readFileSync(pfxFilePath); - } catch (err) { - console.error('Error reading pfx file', err); - throw new Error('Error reading pfx file' + err); - } - } - httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions); - break; - } - } - } - - /** - * Proxy configuration - * - * Preferences proxyMode has three possible values: on, off, system - * Collection proxyMode has three possible values: true, false, global - * - * When collection proxyMode is true, it overrides the app-level proxy settings - * When collection proxyMode is false, it ignores the app-level proxy settings - * When collection proxyMode is global, it uses the app-level proxy settings - * - * Below logic calculates the proxyMode and proxyConfig to be used for the request - */ - let proxyMode = 'off'; - let proxyConfig = {}; - - const collectionProxyConfig = get(brunoConfig, 'proxy', {}); - const collectionProxyEnabled = get(collectionProxyConfig, 'enabled', 'global'); - if (collectionProxyEnabled === true) { - proxyConfig = collectionProxyConfig; - proxyMode = 'on'; - } else if (collectionProxyEnabled === 'global') { - proxyConfig = preferencesUtil.getGlobalProxyConfig(); - proxyMode = get(proxyConfig, 'mode', 'off'); - } - - setupProxyAgents({ - requestConfig: request, - proxyMode, - proxyConfig, - httpsAgentRequestFields, - interpolationOptions - }); - - return {proxyMode, newRequest: request, proxyConfig, httpsAgentRequestFields, interpolationOptions}; -} - -const configureRequest = async ( - collectionUid, - request, - envVars, - runtimeVariables, - processEnvVars, - collectionPath -) => { - const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; - if (!protocolRegex.test(request.url)) { - request.url = `http://${request.url}`; - } - - const {proxyMode, newRequest, proxyConfig, httpsAgentRequestFields, interpolationOptions} = await configureRequestWithCertsAndProxy({ - collectionUid, - request, - envVars, - runtimeVariables, - processEnvVars, - collectionPath - }); - - request = newRequest - let requestMaxRedirects = request.maxRedirects - // Don't override maxRedirects here, let it be controlled by the request object - // request.maxRedirects = 0 - - // Set default value for requestMaxRedirects if not explicitly set - if (requestMaxRedirects === undefined) { - requestMaxRedirects = 5; // Default to 5 redirects - } - - let axiosInstance = makeAxiosInstance({ - proxyMode, - proxyConfig, - requestMaxRedirects, - httpsAgentRequestFields, - interpolationOptions - }); - - if (request.ntlmConfig) { - axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults) - delete request.ntlmConfig; - } - - if (request.oauth2) { - let requestCopy = cloneDeep(request); - const { oauth2: { grantType, tokenPlacement, tokenHeaderPrefix, tokenQueryKey } = {} } = requestCopy || {}; - let credentials, credentialsId; - switch (grantType) { - case 'authorization_code': - interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); - ({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid })); - request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid }; - if (tokenPlacement == 'header') { - request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`; - } - else { - try { - const url = new URL(request.url); - url?.searchParams?.set(tokenQueryKey, credentials?.access_token); - request.url = url?.toString(); - } - catch(error) {} - } - break; - case 'client_credentials': - interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); - ({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid })); - request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid }; - if (tokenPlacement == 'header') { - request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`; - } - else { - try { - const url = new URL(request.url); - url?.searchParams?.set(tokenQueryKey, credentials?.access_token); - request.url = url?.toString(); - } - catch(error) {} - } - break; - case 'password': - interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); - ({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid })); - request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid }; - if (tokenPlacement == 'header') { - request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`; - } - else { - try { - const url = new URL(request.url); - url?.searchParams?.set(tokenQueryKey, credentials?.access_token); - request.url = url?.toString(); - } - catch(error) {} - } - break; - } - } - - if (request.awsv4config) { - request.awsv4config = await resolveAwsV4Credentials(request); - addAwsV4Interceptor(axiosInstance, request); - delete request.awsv4config; - } - - if (request.digestConfig) { - addDigestInterceptor(axiosInstance, request); - } - - request.timeout = preferencesUtil.getRequestTimeout(); - - // add cookies to request - if (preferencesUtil.shouldSendCookies()) { - const cookieString = getCookieStringForUrl(request.url); - if (cookieString && typeof cookieString === 'string' && cookieString.length) { - request.headers['cookie'] = cookieString; - } - } - - // Add API key to the URL - if (request.apiKeyAuthValueForQueryParams && request.apiKeyAuthValueForQueryParams.placement === 'queryparams') { - const urlObj = new URL(request.url); - - // Interpolate key and value as they can be variables before adding to the URL. - const key = interpolateString(request.apiKeyAuthValueForQueryParams.key, interpolationOptions); - const value = interpolateString(request.apiKeyAuthValueForQueryParams.value, interpolationOptions); - - urlObj.searchParams.set(key, value); - request.url = urlObj.toString(); - } - - // Remove pathParams, already in URL (Issue #2439) - delete request.pathParams; - - // Remove apiKeyAuthValueForQueryParams, already interpolated and added to URL - delete request.apiKeyAuthValueForQueryParams; - - return axiosInstance; -}; - -const getJsSandboxRuntime = (collection) => { - const securityConfig = get(collection, 'securityConfig', {}); - return securityConfig.jsSandboxMode === 'safe' ? 'quickjs' : 'vm2'; -}; - - -const parseDataFromResponse = (response, disableParsingResponseJson = false) => { - // Parse the charset from content type: https://stackoverflow.com/a/33192813 - const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || ''); - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals - const charsetValue = charsetMatch?.[1]; - const dataBuffer = Buffer.from(response.data); - // Overwrite the original data for backwards compatibility - let data; - if (iconv.encodingExists(charsetValue)) { - data = iconv.decode(dataBuffer, charsetValue); - } else { - data = iconv.decode(dataBuffer, 'utf-8'); - } - // Try to parse response to JSON, this can quietly fail - try { - // Filter out ZWNBSP character - // https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d - data = data.replace(/^\uFEFF/, ''); - - // If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed - if ( !disableParsingResponseJson && ! (typeof data === 'string' && data.startsWith("\"") && data.endsWith("\""))) { - data = Buffer?.isBuffer(data)? JSON.parse(data?.toString()) : JSON.parse(data); - } - } catch(error) { - console.error(error); - console.log('Failed to parse response data as JSON'); - } - - return { data, dataBuffer }; -}; - - module.exports = { prepareRequest, - prepareGqlIntrospectionRequest, - setAuthHeaders, - getJsSandboxRuntime, - configureRequestWithCertsAndProxy, - configureRequest, - parseDataFromResponse -} + setAuthHeaders +} \ No newline at end of file diff --git a/packages/bruno-electron/src/utils/oauth2.js b/packages/bruno-electron/src/utils/oauth2.js index 7834ce60c..ae2317dec 100644 --- a/packages/bruno-electron/src/utils/oauth2.js +++ b/packages/bruno-electron/src/utils/oauth2.js @@ -2,7 +2,7 @@ const { get, cloneDeep } = require('lodash'); const crypto = require('crypto'); const { authorizeUserInWindow } = require('../ipc/network/authorize-user-in-window'); const Oauth2Store = require('../store/oauth2'); -const { makeAxiosInstance } = require('./axios-instance'); +const { makeAxiosInstance } = require('../ipc/network/axios-instance'); const { safeParseJSON, safeStringifyJSON } = require('./common'); const oauth2Store = new Oauth2Store(); diff --git a/packages/bruno-tests/keycloak-authorization_code/user_info_custom.bru b/packages/bruno-tests/keycloak-authorization_code/user_info_custom.bru index 58cadf9cf..c5a757ed0 100644 --- a/packages/bruno-tests/keycloak-authorization_code/user_info_custom.bru +++ b/packages/bruno-tests/keycloak-authorization_code/user_info_custom.bru @@ -11,5 +11,5 @@ get { } auth:bearer { - token: {{$oauth2.keycloak.access_token}} + token: {{$oauth2.credentials.access_token}} }