~ reverting the bruno-electron ipc-network files refactoring work to keep the diff minimal

This commit is contained in:
lohxt1
2025-03-15 21:01:31 +05:30
parent 8cda05c431
commit f99e8770f0
9 changed files with 442 additions and 442 deletions

View File

@@ -1,175 +0,0 @@
const { fromIni } = require('@aws-sdk/credential-providers');
const { aws4Interceptor } = require('aws4-axios');
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 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);
}
function addDigestInterceptor(axiosInstance, request) {
const { username, password } = request.digestConfig;
console.debug('Digest Auth Interceptor Initialized');
if (!isStrPresent(username) || !isStrPresent(password)) {
console.warn('Required Digest Auth fields (username/password) are not present');
return;
}
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
const originalRequest = error.config;
// Prevent retry loops
if (originalRequest._retry) {
return Promise.reject(error);
}
originalRequest._retry = true;
if (
error.response?.status === 401 &&
containsDigestHeader(error.response) &&
!containsAuthorizationHeader(originalRequest)
) {
console.debug('Processing Digest Authentication Challenge');
console.debug(error.response.headers['www-authenticate']);
const authDetails = error.response.headers['www-authenticate']
.split(',')
.map((pair) => pair.split('=').map((item) => item.trim()).map(stripQuotes))
.reduce((acc, [key, value]) => {
const normalizedKey = key.toLowerCase().replace('digest ', '');
if (normalizedKey && value !== undefined) {
acc[normalizedKey] = value;
}
return acc;
}, {});
// Validate required auth details
if (!authDetails.realm || !authDetails.nonce) {
console.warn('Missing required auth details (realm or nonce)');
return Promise.reject(error);
}
console.debug("Auth Details: \n", authDetails);
const nonceCount = '00000001';
const cnonce = crypto.randomBytes(24).toString('hex');
if (authDetails.algorithm && authDetails.algorithm.toUpperCase() !== 'MD5') {
console.warn(`Unsupported Digest algorithm: ${authDetails.algorithm}`);
return Promise.reject(error);
} else {
authDetails.algorithm = 'MD5';
}
const uri = new URL(request.url, request.baseURL || 'http://localhost').pathname; // Handle relative URLs
const HA1 = md5(`${username}:${authDetails.realm}:${password}`);
const HA2 = md5(`${request.method}:${uri}`);
const response = md5(
`${HA1}:${authDetails.nonce}:${nonceCount}:${cnonce}:auth:${HA2}`
);
const headerFields = [
`username="${username}"`,
`realm="${authDetails.realm}"`,
`nonce="${authDetails.nonce}"`,
`uri="${uri}"`,
`qop="auth"`,
`algorithm="${authDetails.algorithm}"`,
`response="${response}"`,
`nc="${nonceCount}"`,
`cnonce="${cnonce}"`,
];
if (authDetails.opaque) {
headerFields.push(`opaque="${authDetails.opaque}"`);
}
const authorizationHeader = `Digest ${headerFields.join(', ')}`;
// Ensure headers are initialized
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers['Authorization'] = authorizationHeader;
console.debug(`Authorization: ${originalRequest.headers['Authorization']}`);
delete originalRequest.digestConfig;
return axiosInstance(originalRequest);
}
return Promise.reject(error);
}
);
}
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
}

View File

@@ -1,339 +0,0 @@
const URL = require('url');
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 LOCAL_IPV6 = '::1';
const LOCAL_IPV4 = '127.0.0.1';
const LOCALHOST = 'localhost';
const version = electronApp?.app?.getVersion()?.substring(1) ?? "";
const redirectResponseCodes = [301, 302, 303, 307, 308];
const saveCookies = (url, headers) => {
if (preferencesUtil.shouldStoreCookies()) {
let setCookieHeaders = [];
if (headers['set-cookie']) {
setCookieHeaders = Array.isArray(headers['set-cookie'])
? headers['set-cookie']
: [headers['set-cookie']];
for (let setCookieHeader of setCookieHeaders) {
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
addCookieToJar(setCookieHeader, url);
}
}
}
}
}
const getTld = (hostname) => {
if (!hostname) {
return '';
}
return hostname.substring(hostname.lastIndexOf('.') + 1);
};
const checkConnection = (host, port) =>
new Promise((resolve) => {
const key = `${host}:${port}`;
const cachedResult = connectionCache.get(key);
if (cachedResult !== undefined) {
resolve(cachedResult);
} else {
const socket = new Socket();
socket.once('connect', () => {
socket.end();
connectionCache.set(key, true); // Cache successful connection
resolve(true);
});
socket.once('error', () => {
connectionCache.set(key, false); // Cache failed connection
resolve(false);
});
// Try to connect to the host and port
socket.connect(port, host);
}
});
/**
* Function that configures axios with timing interceptors
* Important to note here that the timings are not completely accurate.
* @see https://github.com/axios/axios/issues/695
* @returns {axios.AxiosInstance}
*/
function makeAxiosInstance({
proxyMode = 'off',
proxyConfig = {},
requestMaxRedirects = 5,
httpsAgentRequestFields = {},
interpolationOptions = {}
} = {}) {
/** @type {axios.AxiosInstance} */
const instance = axios.create({
transformRequest: function transformRequest(data, headers) {
const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
const hasJSONContentType = contentType.includes('json');
if (typeof data === 'string' && hasJSONContentType) {
return data;
}
axios.defaults.transformRequest.forEach(function (tr) {
data = tr.call(this, data, headers);
}, this);
return data;
},
proxy: false,
headers: {
"User-Agent": `bruno-runtime/${version}`
}
});
instance.interceptors.request.use(async (config) => {
const url = URL.parse(config.url);
config.metadata = config.metadata || {};
config.metadata.startTime = new Date().getTime();
config.metadata.timeline = config.metadata.timeline || [];
// Add initial request details to the timeline
config.metadata.timeline.push({
timestamp: new Date(),
type: 'info',
message: `Preparing request to ${config.url}`,
});
config.metadata.timeline.push({
timestamp: new Date(),
type: 'info',
message: `Current time is ${new Date().toISOString()}`,
});
// Add request method and headers
config.metadata.timeline.push({
timestamp: new Date(),
type: 'request',
message: `${config.method.toUpperCase()} ${config.url}`,
});
Object.entries(config.headers).forEach(([key, value]) => {
config.metadata.timeline.push({
timestamp: new Date(),
type: 'requestHeader',
message: `${key}: ${value}`,
});
});
// Add request data if available
if (config.data) {
let requestData;
try {
requestData = typeof config.data === 'string' ? config.data : JSON.stringify(config.data, null, 2);
} catch (err) {
requestData = config.data.toString();
}
config.metadata.timeline.push({
timestamp: new Date(),
type: 'requestData',
message: requestData,
});
}
// Resolve all *.localhost to localhost and check if it should use IPv6 or IPv4
// RFC: 6761 section 6.3 (https://tools.ietf.org/html/rfc6761#section-6.3)
// @see https://github.com/usebruno/bruno/issues/124
if (getTld(url.hostname) === LOCALHOST || url.hostname === LOCAL_IPV4 || url.hostname === LOCAL_IPV6) {
// use custom DNS lookup for localhost
config.lookup = (hostname, options, callback) => {
const portNumber = Number(url.port) || (url.protocol.includes('https') ? 443 : 80);
checkConnection(LOCAL_IPV6, portNumber).then((useIpv6) => {
const ip = useIpv6 ? LOCAL_IPV6 : LOCAL_IPV4;
callback(null, ip, useIpv6 ? 6 : 4);
});
};
}
config.headers['request-start-time'] = Date.now();
const agentOptions = {
...httpsAgentRequestFields,
keepAlive: true,
};
// Now call setupProxyAgents and pass the timeline
setupProxyAgents({
requestConfig: config,
proxyMode: proxyMode, // 'on', 'off', or 'system', depending on your settings
proxyConfig: proxyConfig,
httpsAgentRequestFields: agentOptions,
interpolationOptions: interpolationOptions, // Provide your interpolation options
timeline: config.metadata.timeline,
});
return config;
});
let redirectCount = 0
instance.interceptors.response.use(
(response) => {
const end = Date.now();
const start = response.config.headers['request-start-time'];
response.headers['request-duration'] = end - start;
redirectCount = 0;
const config = response.config;
const metadata = config.metadata;
const duration = end - metadata.startTime;
const httpVersion = response.request?.res?.httpVersion || '1.1';
metadata.timeline.push({
timestamp: new Date(),
type: 'response',
message: `HTTP/${httpVersion} ${response.status} ${response.statusText}`,
});
if (httpVersion.startsWith('2')) {
metadata.timeline.push({
timestamp: new Date(),
type: 'info',
message: `Using HTTP/2, server supports multiplexing`,
});
}
metadata.timeline.push({
timestamp: new Date(),
type: 'response',
message: `HTTP/${response.httpVersion || '1.1'} ${response.status} ${response.statusText}`,
});
Object.entries(response.headers).forEach(([key, value]) => {
metadata.timeline.push({
timestamp: new Date(),
type: 'responseHeader',
message: `${key}: ${value}`,
});
});
metadata.timeline.push({
timestamp: new Date(),
type: 'info',
message: `Request completed in ${duration} ms`,
});
// Attach the timeline to the response
response.timeline = metadata.timeline;
return response;
},
(error) => {
if (error.response) {
const end = Date.now();
const start = error.config.headers['request-start-time'];
error.response.headers['request-duration'] = end - start;
const config = error.config;
const metadata = config.metadata;
const duration = end - metadata.startTime;
if (error.response && redirectResponseCodes.includes(error.response.status)) {
metadata.timeline.push({
timestamp: new Date(),
type: 'response',
message: `HTTP/${error.response.httpVersion || '1.1'} ${error.response.status} ${error.response.statusText}`,
});
Object.entries(error.response.headers).forEach(([key, value]) => {
metadata.timeline.push({
timestamp: new Date(),
type: 'responseHeader',
message: `${key}: ${value}`,
});
});
metadata.timeline.push({
timestamp: new Date(),
type: 'info',
message: `Request completed in ${duration} ms`,
});
// Attach the timeline to the response
error.response.timeline = metadata.timeline;
if (redirectCount >= requestMaxRedirects) {
const dataBuffer = Buffer.from(error.response.data);
return {
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
data: error.response.data,
dataBuffer: dataBuffer.toString('base64'),
size: Buffer.byteLength(dataBuffer),
duration: error.response.headers.get('request-duration') ?? 0
};
}
// Increase redirect count
redirectCount++;
const locationHeader = error.response.headers.location;
let redirectUrl = locationHeader;
// Handle relative URLs by resolving them against the original request URL
if (locationHeader && !locationHeader.match(/^https?:\/\//i)) {
// It's a relative URL, resolve it against the original URL
redirectUrl = URL.resolve(error.config.url, locationHeader);
metadata.timeline.push({
timestamp: new Date(),
type: 'info',
message: `Resolving relative redirect URL: ${locationHeader}${redirectUrl}`,
});
}
if (preferencesUtil.shouldStoreCookies()) {
saveCookies(redirectUrl, error.response.headers);
}
// Create a new request config for the redirect
const requestConfig = {
...error.config,
url: redirectUrl,
headers: {
...error.config.headers,
},
};
if (preferencesUtil.shouldSendCookies()) {
const cookieString = getCookieStringForUrl(redirectUrl);
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
requestConfig.headers['cookie'] = cookieString;
}
}
setupProxyAgents({
requestConfig,
proxyMode,
proxyConfig,
httpsAgentRequestFields,
interpolationOptions,
timeline: metadata.timeline
});
// Make the redirected request
return instance(requestConfig);
}
}
return Promise.reject(error);
}
);
return instance;
}
module.exports = {
makeAxiosInstance
};

View File

@@ -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();

View File

@@ -1,746 +0,0 @@
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 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 setAuthHeaders = (axiosRequest, request, collectionRoot) => {
const collectionAuth = get(collectionRoot, 'request.auth');
if (collectionAuth && request.auth.mode === 'inherit') {
switch (collectionAuth.mode) {
case 'awsv4':
axiosRequest.awsv4config = {
accessKeyId: get(collectionAuth, 'awsv4.accessKeyId'),
secretAccessKey: get(collectionAuth, 'awsv4.secretAccessKey'),
sessionToken: get(collectionAuth, 'awsv4.sessionToken'),
service: get(collectionAuth, 'awsv4.service'),
region: get(collectionAuth, 'awsv4.region'),
profileName: get(collectionAuth, 'awsv4.profileName')
};
break;
case 'basic':
axiosRequest.basicAuth = {
username: get(collectionAuth, 'basic.username'),
password: get(collectionAuth, 'basic.password')
};
break;
case 'bearer':
axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`;
break;
case 'digest':
axiosRequest.digestConfig = {
username: get(collectionAuth, 'digest.username'),
password: get(collectionAuth, 'digest.password')
};
break;
case 'ntlm':
axiosRequest.ntlmConfig = {
username: get(collectionAuth, 'ntlm.username'),
password: get(collectionAuth, 'ntlm.password'),
domain: get(collectionAuth, 'ntlm.domain')
};
break;
case 'wsse':
const username = get(request, 'auth.wsse.username', '');
const password = get(request, 'auth.wsse.password', '');
const ts = new Date().toISOString();
const nonce = crypto.randomBytes(16).toString('hex');
// Create the password digest using SHA-1 as required for WSSE
const hash = crypto.createHash('sha1');
hash.update(nonce + ts + password);
const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');
// Construct the WSSE header
axiosRequest.headers[
'X-WSSE'
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
break;
case 'apikey':
const apiKeyAuth = get(collectionAuth, 'apikey');
if (apiKeyAuth.placement === 'header') {
axiosRequest.headers[apiKeyAuth.key] = apiKeyAuth.value;
} else if (apiKeyAuth.placement === 'queryparams') {
// If the API key authentication is set and its placement is 'queryparams', add it to the axios request object. This will be used in the configureRequest function to append the API key to the query parameters of the request URL.
axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth;
}
break;
case 'oauth2':
const grantType = get(collectionAuth, 'oauth2.grantType');
switch (grantType) {
case 'password':
axiosRequest.oauth2 = {
grantType: grantType,
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
refreshUrl: get(collectionAuth, 'oauth2.refreshUrl'),
username: get(collectionAuth, 'oauth2.username'),
password: get(collectionAuth, 'oauth2.password'),
clientId: get(collectionAuth, 'oauth2.clientId'),
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
scope: get(collectionAuth, 'oauth2.scope'),
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
credentialsId: get(collectionAuth, 'oauth2.credentialsId'),
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken')
};
break;
case 'authorization_code':
axiosRequest.oauth2 = {
grantType: grantType,
callbackUrl: get(collectionAuth, 'oauth2.callbackUrl'),
authorizationUrl: get(collectionAuth, 'oauth2.authorizationUrl'),
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
refreshUrl: get(collectionAuth, 'oauth2.refreshUrl'),
clientId: get(collectionAuth, 'oauth2.clientId'),
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
scope: get(collectionAuth, 'oauth2.scope'),
state: get(collectionAuth, 'oauth2.state'),
pkce: get(collectionAuth, 'oauth2.pkce'),
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
credentialsId: get(collectionAuth, 'oauth2.credentialsId'),
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken')
};
break;
case 'client_credentials':
axiosRequest.oauth2 = {
grantType: grantType,
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
refreshUrl: get(collectionAuth, 'oauth2.refreshUrl'),
clientId: get(collectionAuth, 'oauth2.clientId'),
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
scope: get(collectionAuth, 'oauth2.scope'),
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
credentialsId: get(collectionAuth, 'oauth2.credentialsId'),
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken')
};
break;
}
break;
}
}
if (request.auth) {
switch (request.auth.mode) {
case 'awsv4':
axiosRequest.awsv4config = {
accessKeyId: get(request, 'auth.awsv4.accessKeyId'),
secretAccessKey: get(request, 'auth.awsv4.secretAccessKey'),
sessionToken: get(request, 'auth.awsv4.sessionToken'),
service: get(request, 'auth.awsv4.service'),
region: get(request, 'auth.awsv4.region'),
profileName: get(request, 'auth.awsv4.profileName')
};
break;
case 'basic':
axiosRequest.basicAuth = {
username: get(request, 'auth.basic.username'),
password: get(request, 'auth.basic.password')
};
break;
case 'bearer':
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
break;
case 'digest':
axiosRequest.digestConfig = {
username: get(request, 'auth.digest.username'),
password: get(request, 'auth.digest.password')
};
break;
case 'ntlm':
axiosRequest.ntlmConfig = {
username: get(request, 'auth.ntlm.username'),
password: get(request, 'auth.ntlm.password'),
domain: get(request, 'auth.ntlm.domain')
};
case 'oauth2':
const grantType = get(request, 'auth.oauth2.grantType');
switch (grantType) {
case 'password':
axiosRequest.oauth2 = {
grantType: grantType,
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
refreshUrl: get(collectionAuth, 'oauth2.refreshUrl'),
username: get(request, 'auth.oauth2.username'),
password: get(request, 'auth.oauth2.password'),
clientId: get(request, 'auth.oauth2.clientId'),
clientSecret: get(request, 'auth.oauth2.clientSecret'),
scope: get(request, 'auth.oauth2.scope'),
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
credentialsId: get(request, 'auth.oauth2.credentialsId'),
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken')
};
break;
case 'authorization_code':
axiosRequest.oauth2 = {
grantType: grantType,
callbackUrl: get(request, 'auth.oauth2.callbackUrl'),
authorizationUrl: get(request, 'auth.oauth2.authorizationUrl'),
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
refreshUrl: get(collectionAuth, 'oauth2.refreshUrl'),
clientId: get(request, 'auth.oauth2.clientId'),
clientSecret: get(request, 'auth.oauth2.clientSecret'),
scope: get(request, 'auth.oauth2.scope'),
state: get(request, 'auth.oauth2.state'),
pkce: get(request, 'auth.oauth2.pkce'),
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
credentialsId: get(request, 'auth.oauth2.credentialsId'),
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken')
};
break;
case 'client_credentials':
axiosRequest.oauth2 = {
grantType: grantType,
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
refreshUrl: get(collectionAuth, 'oauth2.refreshUrl'),
clientId: get(request, 'auth.oauth2.clientId'),
clientSecret: get(request, 'auth.oauth2.clientSecret'),
scope: get(request, 'auth.oauth2.scope'),
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
credentialsId: get(request, 'auth.oauth2.credentialsId'),
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken')
};
break;
}
break;
case 'wsse':
const username = get(request, 'auth.wsse.username', '');
const password = get(request, 'auth.wsse.password', '');
const ts = new Date().toISOString();
const nonce = crypto.randomBytes(16).toString('hex');
// Create the password digest using SHA-1 as required for WSSE
const hash = crypto.createHash('sha1');
hash.update(nonce + ts + password);
const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');
// Construct the WSSE header
axiosRequest.headers[
'X-WSSE'
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
break;
case 'apikey':
const apiKeyAuth = get(request, 'auth.apikey');
if (apiKeyAuth.placement === 'header') {
axiosRequest.headers[apiKeyAuth.key] = apiKeyAuth.value;
} else if (apiKeyAuth.placement === 'queryparams') {
// If the API key authentication is set and its placement is 'queryparams', add it to the axios request object. This will be used in the configureRequest function to append the API key to the query parameters of the request URL.
axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth;
}
break;
}
}
return axiosRequest;
};
const prepareRequest = (item, collection) => {
const request = item.draft ? item.draft.request : item.request;
const collectionRoot = collection?.draft ? get(collection, 'draft', {}) : get(collection, 'root', {});
const collectionPath = collection.pathname;
const headers = {};
let contentTypeDefined = false;
let url = request.url;
each(get(collectionRoot, 'request.headers', []), (h) => {
if (h.enabled && h.name?.toLowerCase() === 'content-type') {
contentTypeDefined = true;
return false;
}
});
const scriptFlow = collection.brunoConfig?.scripts?.flow ?? 'sandwich';
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
if (requestTreePath && requestTreePath.length > 0) {
mergeHeaders(collection, request, requestTreePath);
mergeScripts(collection, request, requestTreePath, scriptFlow);
mergeVars(collection, request, requestTreePath);
mergeAuth(collection, request, requestTreePath);
request.globalEnvironmentVariables = collection?.globalEnvironmentVariables;
request.oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials });
}
each(get(request, 'headers', []), (h) => {
if (h.enabled && h.name.length > 0) {
headers[h.name] = h.value;
if (h.name.toLowerCase() === 'content-type') {
contentTypeDefined = true;
}
}
});
let axiosRequest = {
mode: request.body.mode,
method: request.method,
url,
headers,
pathParams: request?.params?.filter((param) => param.type === 'path'),
responseType: 'arraybuffer'
};
axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot);
if (request.body.mode === 'json') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/json';
}
try {
axiosRequest.data = decomment(request?.body?.json);
} catch (error) {
axiosRequest.data = request?.body?.json;
}
}
if (request.body.mode === 'text') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'text/plain';
}
axiosRequest.data = request.body.text;
}
if (request.body.mode === 'xml') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/xml';
}
axiosRequest.data = request.body.xml;
}
if (request.body.mode === 'sparql') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/sparql-query';
}
axiosRequest.data = request.body.sparql;
}
if (request.body.mode === 'formUrlEncoded') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
}
const enabledParams = filter(request.body.formUrlEncoded, (p) => p.enabled);
axiosRequest.data = buildFormUrlEncodedPayload(enabledParams);
}
if (request.body.mode === 'multipartForm') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'multipart/form-data';
}
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
axiosRequest.data = enabledParams;
}
if (request.body.mode === 'graphql') {
const graphqlQuery = {
query: get(request, 'body.graphql.query'),
// https://github.com/usebruno/bruno/issues/884 - we must only parse the variables after the variable interpolation
variables: decomment(get(request, 'body.graphql.variables') || '{}')
};
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/json';
}
axiosRequest.data = graphqlQuery;
}
if (request.script) {
axiosRequest.script = request.script;
}
if (request.tests) {
axiosRequest.tests = request.tests;
}
axiosRequest.vars = request.vars;
axiosRequest.collectionVariables = request.collectionVariables;
axiosRequest.folderVariables = request.folderVariables;
axiosRequest.requestVariables = request.requestVariables;
axiosRequest.globalEnvironmentVariables = request.globalEnvironmentVariables;
axiosRequest.oauth2CredentialVariables = request.oauth2CredentialVariables;
axiosRequest.assertions = request.assertions;
axiosRequest.oauth2Credentials = request.oauth2Credentials;
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
}