mirror of
https://github.com/usebruno/bruno.git
synced 2025-08-23 21:05:30 +02:00
~ reverting the bruno-electron ipc-network files refactoring work to keep the diff minimal
This commit is contained in:
@@ -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();
|
||||
|
57
packages/bruno-electron/src/ipc/network/awsv4auth-helper.js
Normal file
57
packages/bruno-electron/src/ipc/network/awsv4auth-helper.js
Normal file
@@ -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
|
||||
};
|
@@ -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';
|
@@ -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
|
||||
}
|
||||
module.exports = { addDigestInterceptor };
|
@@ -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;
|
||||
|
@@ -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;
|
@@ -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
|
||||
}
|
@@ -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();
|
||||
|
@@ -11,5 +11,5 @@ get {
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{$oauth2.keycloak.access_token}}
|
||||
token: {{$oauth2.credentials.access_token}}
|
||||
}
|
||||
|
Reference in New Issue
Block a user