Merge pull request #596 from mirkogolze/feature/proxy-global-and-collection

proxy settings on global and collection level
This commit is contained in:
Anoop M D 2023-10-17 23:34:45 +05:30 committed by GitHub
commit d767a144f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 937 additions and 257 deletions

View File

@ -1,7 +1,9 @@
# Description # Description
<!-- Explain here the changes your PR introduces and text to help us understand the context of this change. --> <!-- Explain here the changes your PR introduces and text to help us understand the context of this change. -->
# Contribution Checklist: # Contribution Checklist:
- [ ] **The pull request does not introduce any breaking changes** - [ ] **The pull request does not introduce any breaking changes**
- [ ] **I have read the [contribution guidelines](https://github.com/usebruno/bruno/blob/main/contributing.md).** - [ ] **I have read the [contribution guidelines](https://github.com/usebruno/bruno/blob/main/contributing.md).**
- [ ] **Create an issue and link to the pull request.** - [ ] **Create an issue and link to the pull request.**

View File

@ -1,7 +1,7 @@
import 'github-markdown-css/github-markdown.css'; import 'github-markdown-css/github-markdown.css';
import get from 'lodash/get'; import get from 'lodash/get';
import { updateCollectionDocs } from 'providers/ReduxStore/slices/collections'; import { updateCollectionDocs } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme/index'; import { useTheme } from 'providers/Theme';
import { useState } from 'react'; import { useState } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions'; import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';

View File

@ -1,13 +1,55 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import * as Yup from 'yup';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import * as Yup from 'yup';
import toast from 'react-hot-toast';
const ProxySettings = ({ proxyConfig, onUpdate }) => { const ProxySettings = ({ proxyConfig, onUpdate }) => {
const proxySchema = Yup.object({
enabled: Yup.string().oneOf(['global', 'enabled', 'disabled']),
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string()
.when('enabled', {
is: 'enabled',
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
otherwise: (hostname) => hostname.nullable()
})
.max(1024),
port: Yup.number()
.when('enabled', {
is: 'enabled',
then: (port) => port.required('Specify port between 1 and 65535').typeError('Specify port between 1 and 65535'),
otherwise: (port) => port.nullable().transform((_, val) => (val ? Number(val) : null))
})
.min(1)
.max(65535),
auth: Yup.object()
.when('enabled', {
is: 'enabled',
then: Yup.object({
enabled: Yup.boolean(),
username: Yup.string()
.when(['enabled'], {
is: true,
then: (username) => username.required('Specify username for proxy authentication.')
})
.max(1024),
password: Yup.string()
.when('enabled', {
is: true,
then: (password) => password.required('Specify password for proxy authentication.')
})
.max(1024)
})
})
.optional(),
noProxy: Yup.string().optional().max(1024)
});
const formik = useFormik({ const formik = useFormik({
initialValues: { initialValues: {
enabled: proxyConfig.enabled || false, enabled: proxyConfig.enabled || 'global',
protocol: proxyConfig.protocol || 'http', protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '', hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '', port: proxyConfig.port || '',
@ -15,27 +57,26 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false, enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '', username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
password: proxyConfig.auth ? proxyConfig.auth.password || '' : '' password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
} },
noProxy: proxyConfig.noProxy || ''
}, },
validationSchema: Yup.object({ validationSchema: proxySchema,
enabled: Yup.boolean(),
protocol: Yup.string().oneOf(['http', 'https', 'socks5']),
hostname: Yup.string().max(1024),
port: Yup.number().min(0).max(65535),
auth: Yup.object({
enabled: Yup.boolean(),
username: Yup.string().max(1024),
password: Yup.string().max(1024)
})
}),
onSubmit: (values) => { onSubmit: (values) => {
onUpdate(values); proxySchema
.validate(values, { abortEarly: true })
.then((validatedProxy) => {
onUpdate(validatedProxy);
})
.catch((error) => {
let errMsg = error.message || 'Preferences validation error';
toast.error(errMsg);
});
} }
}); });
useEffect(() => { useEffect(() => {
formik.setValues({ formik.setValues({
enabled: proxyConfig.enabled || false, enabled: proxyConfig.enabled || 'global',
protocol: proxyConfig.protocol || 'http', protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '', hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '', port: proxyConfig.port || '',
@ -43,18 +84,61 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false, enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '', username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
password: proxyConfig.auth ? proxyConfig.auth.password || '' : '' password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
} },
noProxy: proxyConfig.noProxy || ''
}); });
}, [proxyConfig]); }, [proxyConfig]);
return ( return (
<StyledWrapper> <StyledWrapper>
<h1 className="font-medium mb-3">Proxy Settings</h1>
<label className="settings-label">
<ul className="mb-3">
<li>global - use global config</li>
<li>enabled - use collection config</li>
<li>disable - disable proxy</li>
</ul>
</label>
<form className="bruno-form" onSubmit={formik.handleSubmit}> <form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="mb-3 flex items-center"> <div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="enabled"> <label className="settings-label" htmlFor="enabled">
Enabled Config
</label> </label>
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} /> <div className="flex items-center">
<label className="flex items-center">
<input
type="radio"
name="enabled"
value="global"
checked={formik.values.enabled === 'global'}
onChange={formik.handleChange}
className="mr-1"
/>
global
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="enabled"
value="enabled"
checked={formik.values.enabled === 'enabled'}
onChange={formik.handleChange}
className="mr-1"
/>
enabled
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="enabled"
value="disabled"
checked={formik.values.enabled === 'disabled'}
onChange={formik.handleChange}
className="mr-1"
/>
disabled
</label>
</div>
</div> </div>
<div className="mb-3 flex items-center"> <div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="protocol"> <label className="settings-label" htmlFor="protocol">
@ -83,6 +167,17 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
/> />
https https
</label> </label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks5"
checked={formik.values.protocol === 'socks4'}
onChange={formik.handleChange}
className="mr-1"
/>
socks4
</label>
<label className="flex items-center ml-4"> <label className="flex items-center ml-4">
<input <input
type="radio" type="radio"
@ -113,7 +208,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
value={formik.values.hostname || ''} value={formik.values.hostname || ''}
/> />
{formik.touched.hostname && formik.errors.hostname ? ( {formik.touched.hostname && formik.errors.hostname ? (
<div className="text-red-500">{formik.errors.hostname}</div> <div className="ml-3 text-red-500">{formik.errors.hostname}</div>
) : null} ) : null}
</div> </div>
<div className="mb-3 flex items-center"> <div className="mb-3 flex items-center">
@ -132,7 +227,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
onChange={formik.handleChange} onChange={formik.handleChange}
value={formik.values.port} value={formik.values.port}
/> />
{formik.touched.port && formik.errors.port ? <div className="text-red-500">{formik.errors.port}</div> : null} {formik.touched.port && formik.errors.port ? (
<div className="ml-3 text-red-500">{formik.errors.port}</div>
) : null}
</div> </div>
<div className="mb-3 flex items-center"> <div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled"> <label className="settings-label" htmlFor="auth.enabled">
@ -163,7 +260,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
onChange={formik.handleChange} onChange={formik.handleChange}
/> />
{formik.touched.auth?.username && formik.errors.auth?.username ? ( {formik.touched.auth?.username && formik.errors.auth?.username ? (
<div className="text-red-500">{formik.errors.auth.username}</div> <div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
) : null} ) : null}
</div> </div>
<div className="mb-3 flex items-center"> <div className="mb-3 flex items-center">
@ -183,10 +280,30 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
onChange={formik.handleChange} onChange={formik.handleChange}
/> />
{formik.touched.auth?.password && formik.errors.auth?.password ? ( {formik.touched.auth?.password && formik.errors.auth?.password ? (
<div className="text-red-500">{formik.errors.auth.password}</div> <div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
) : null} ) : null}
</div> </div>
</div> </div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="noProxy">
Proxy Bypass
</label>
<input
id="noProxy"
type="text"
name="noProxy"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.noProxy || ''}
/>
{formik.touched.noProxy && formik.errors.noProxy ? (
<div className="ml-3 text-red-500">{formik.errors.noProxy}</div>
) : null}
</div>
<div className="mt-6"> <div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary"> <button type="submit" className="submit btn btn-sm btn-secondary">
Save Save

View File

@ -36,7 +36,7 @@ const CollectionSettings = ({ collection }) => {
brunoConfig.proxy = config; brunoConfig.proxy = config;
dispatch(updateBrunoConfig(brunoConfig, collection.uid)) dispatch(updateBrunoConfig(brunoConfig, collection.uid))
.then(() => { .then(() => {
toast.success('Collection settings updated successfully'); toast.success('Collection settings updated successfully.');
}) })
.catch((err) => console.log(err) && toast.error('Failed to update collection settings')); .catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
}; };

View File

@ -33,7 +33,7 @@ const General = ({ close }) => {
<StyledWrapper> <StyledWrapper>
<div className="flex items-center mt-2"> <div className="flex items-center mt-2">
<label className="mr-2 select-none" style={{ minWidth: 200 }} htmlFor="ssl-cert-verification"> <label className="mr-2 select-none" style={{ minWidth: 200 }} htmlFor="ssl-cert-verification">
SSL Certificate Verification TLS Certificate Verification
</label> </label>
<input <input
id="ssl-cert-verification" id="ssl-cert-verification"

View File

@ -0,0 +1,25 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.settings-label {
width: 80px;
}
.textbox {
border: 1px solid #ccc;
padding: 0.15rem 0.45rem;
box-shadow: none;
outline: none;
transition: border-color ease-in-out 0.1s;
border-radius: 3px;
background-color: ${(props) => props.theme.modal.input.bg};
border: 1px solid ${(props) => props.theme.modal.input.border};
&:focus {
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
outline: none !important;
}
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,292 @@
import React, { useEffect } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import toast from 'react-hot-toast';
import { savePreferences } from 'providers/ReduxStore/slices/app';
import StyledWrapper from './StyledWrapper';
import { useDispatch, useSelector } from 'react-redux';
const ProxySettings = ({ close }) => {
const preferences = useSelector((state) => state.app.preferences);
const dispatch = useDispatch();
const proxySchema = Yup.object({
enabled: Yup.boolean(),
protocol: Yup.string().required().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string()
.when('enabled', {
is: true,
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
otherwise: (hostname) => hostname.nullable()
})
.max(1024),
port: Yup.number()
.when('enabled', {
is: true,
then: (port) => port.required('Specify port between 1 and 65535').typeError('Specify port between 1 and 65535'),
otherwise: (port) => port.nullable().transform((_, val) => (val ? Number(val) : null))
})
.min(1)
.max(65535),
auth: Yup.object()
.when('enabled', {
is: true,
then: Yup.object({
enabled: Yup.boolean(),
username: Yup.string()
.when(['enabled'], {
is: true,
then: (username) => username.required('Specify username for proxy authentication.')
})
.max(1024),
password: Yup.string()
.when('enabled', {
is: true,
then: (password) => password.required('Specify password for proxy authentication.')
})
.max(1024)
})
})
.optional(),
noProxy: Yup.string().optional().max(1024)
});
const formik = useFormik({
initialValues: {
enabled: preferences.proxy.enabled || false,
protocol: preferences.proxy.protocol || 'http',
hostname: preferences.proxy.hostname || '',
port: preferences.proxy.port || 0,
auth: {
enabled: preferences.proxy.auth ? preferences.proxy.auth.enabled || false : false,
username: preferences.proxy.auth ? preferences.proxy.auth.username || '' : '',
password: preferences.proxy.auth ? preferences.proxy.auth.password || '' : ''
},
noProxy: preferences.proxy.noProxy || ''
},
validationSchema: proxySchema,
onSubmit: (values) => {
onUpdate(values);
}
});
const onUpdate = (values) => {
proxySchema
.validate(values, { abortEarly: true })
.then((validatedProxy) => {
dispatch(
savePreferences({
...preferences,
proxy: validatedProxy
})
).then(() => {
close();
});
})
.catch((error) => {
let errMsg = error.message || 'Preferences validation error';
toast.error(errMsg);
});
};
useEffect(() => {
formik.setValues({
enabled: preferences.proxy.enabled || false,
protocol: preferences.proxy.protocol || 'http',
hostname: preferences.proxy.hostname || '',
port: preferences.proxy.port || '',
auth: {
enabled: preferences.proxy.auth ? preferences.proxy.auth.enabled || false : false,
username: preferences.proxy.auth ? preferences.proxy.auth.username || '' : '',
password: preferences.proxy.auth ? preferences.proxy.auth.password || '' : ''
},
noProxy: preferences.proxy.noProxy || ''
});
}, [preferences]);
return (
<StyledWrapper>
<h1 className="font-medium mb-3">Proxy Settings</h1>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="enabled">
Enabled
</label>
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="protocol">
Protocol
</label>
<div className="flex items-center">
<label className="flex items-center">
<input
type="radio"
name="protocol"
value="http"
checked={formik.values.protocol === 'http'}
onChange={formik.handleChange}
className="mr-1"
/>
http
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="https"
checked={formik.values.protocol === 'https'}
onChange={formik.handleChange}
className="mr-1"
/>
https
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks5"
checked={formik.values.protocol === 'socks4'}
onChange={formik.handleChange}
className="mr-1"
/>
socks4
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks5"
checked={formik.values.protocol === 'socks5'}
onChange={formik.handleChange}
className="mr-1"
/>
socks5
</label>
</div>
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="hostname">
Hostname
</label>
<input
id="hostname"
type="text"
name="hostname"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.hostname || ''}
/>
{formik.touched.hostname && formik.errors.hostname ? (
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
) : null}
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="port">
Port
</label>
<input
id="port"
type="number"
name="port"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.port}
/>
{formik.touched.port && formik.errors.port ? (
<div className="ml-3 text-red-500">{formik.errors.port}</div>
) : null}
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled">
Auth
</label>
<input
type="checkbox"
name="auth.enabled"
checked={formik.values.auth.enabled}
onChange={formik.handleChange}
/>
</div>
<div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.username">
Username
</label>
<input
id="auth.username"
type="text"
name="auth.username"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.username}
onChange={formik.handleChange}
/>
{formik.touched.auth?.username && formik.errors.auth?.username ? (
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
) : null}
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.password">
Password
</label>
<input
id="auth.password"
type="text"
name="auth.password"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.password}
onChange={formik.handleChange}
/>
{formik.touched.auth?.password && formik.errors.auth?.password ? (
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
) : null}
</div>
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="noProxy">
Proxy Bypass
</label>
<input
id="noProxy"
type="text"
name="noProxy"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.noProxy || ''}
/>
{formik.touched.noProxy && formik.errors.noProxy ? (
<div className="ml-3 text-red-500">{formik.errors.noProxy}</div>
) : null}
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-md btn-secondary">
Save
</button>
</div>
</form>
</StyledWrapper>
);
};
export default ProxySettings;

View File

@ -5,6 +5,7 @@ import Support from './Support';
import General from './General'; import General from './General';
import Font from './Font'; import Font from './Font';
import Theme from './Theme'; import Theme from './Theme';
import Proxy from './ProxySettings';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
const Preferences = ({ onClose }) => { const Preferences = ({ onClose }) => {
@ -22,6 +23,10 @@ const Preferences = ({ onClose }) => {
return <General close={onClose} />; return <General close={onClose} />;
} }
case 'proxy': {
return <Proxy close={onClose} />;
}
case 'theme': { case 'theme': {
return <Theme close={onClose} />; return <Theme close={onClose} />;
} }
@ -49,6 +54,9 @@ const Preferences = ({ onClose }) => {
<div className={getTabClassname('font')} role="tab" onClick={() => setTab('font')}> <div className={getTabClassname('font')} role="tab" onClick={() => setTab('font')}>
Font Font
</div> </div>
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
Proxy
</div>
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}> <div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
Support Support
</div> </div>

View File

@ -45,6 +45,8 @@ export const fetchGqlSchema = async (endpoint, environment, request, collection)
export const cancelNetworkRequest = async (cancelTokenUid) => { export const cancelNetworkRequest = async (cancelTokenUid) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('cancel-http-request', cancelTokenUid).then(resolve).catch(reject); ipcRenderer.invoke('cancel-http-request', cancelTokenUid).then(resolve).catch(reject);
}); });
}; };

View File

@ -162,9 +162,7 @@ const getCollectionRoot = (dir) => {
} }
const content = fs.readFileSync(collectionRootPath, 'utf8'); const content = fs.readFileSync(collectionRootPath, 'utf8');
const json = collectionBruToJson(content); return collectionBruToJson(content);
return json;
}; };
const builder = async (yargs) => { const builder = async (yargs) => {

View File

@ -16,6 +16,7 @@ const { HttpsProxyAgent } = require('https-proxy-agent');
const { HttpProxyAgent } = require('http-proxy-agent'); const { HttpProxyAgent } = require('http-proxy-agent');
const { SocksProxyAgent } = require('socks-proxy-agent'); const { SocksProxyAgent } = require('socks-proxy-agent');
const { makeAxiosInstance } = require('../utils/axios-instance'); const { makeAxiosInstance } = require('../utils/axios-instance');
const { shouldUseProxy } = require('../utils/proxy-util');
const runSingleRequest = async function ( const runSingleRequest = async function (
filename, filename,
@ -47,7 +48,7 @@ const runSingleRequest = async function (
// run pre-request vars // run pre-request vars
const preRequestVars = get(bruJson, 'request.vars.req'); const preRequestVars = get(bruJson, 'request.vars.req');
if (preRequestVars && preRequestVars.length) { if (preRequestVars?.length) {
const varsRuntime = new VarsRuntime(); const varsRuntime = new VarsRuntime();
varsRuntime.runPreRequestVars( varsRuntime.runPreRequestVars(
preRequestVars, preRequestVars,
@ -64,7 +65,7 @@ const runSingleRequest = async function (
get(collectionRoot, 'request.script.req'), get(collectionRoot, 'request.script.req'),
get(bruJson, 'request.script.req') get(bruJson, 'request.script.req')
]).join(os.EOL); ]).join(os.EOL);
if (requestScriptFile && requestScriptFile.length) { if (requestScriptFile?.length) {
const scriptRuntime = new ScriptRuntime(); const scriptRuntime = new ScriptRuntime();
await scriptRuntime.runRequestScript( await scriptRuntime.runRequestScript(
decomment(requestScriptFile), decomment(requestScriptFile),
@ -87,36 +88,56 @@ const runSingleRequest = async function (
if (insecure) { if (insecure) {
httpsAgentRequestFields['rejectUnauthorized'] = false; httpsAgentRequestFields['rejectUnauthorized'] = false;
} else { } else {
const cacertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS]; const caCertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS];
const cacert = cacertArray.find((el) => el); const caCert = caCertArray.find((el) => el);
if (cacert && cacert.length > 1) { if (caCert && caCert.length > 1) {
try { try {
caCrt = fs.readFileSync(cacert); httpsAgentRequestFields['ca'] = fs.readFileSync(caCert);
httpsAgentRequestFields['ca'] = caCrt;
} catch (err) { } catch (err) {
console.log('Error reading CA cert file:' + cacert, err); console.log('Error reading CA cert file:' + caCert, err);
}
}
}
const interpolationOptions = {
envVars: envVariables,
collectionVariables,
processEnvVars
};
// client certificate config
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
for (let clientCert of clientCertConfig) {
const domain = interpolateString(clientCert.domain, interpolationOptions);
const certFilePath = interpolateString(clientCert.certFilePath, interpolationOptions);
const keyFilePath = interpolateString(clientCert.keyFilePath, interpolationOptions);
if (domain && certFilePath && keyFilePath) {
const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
if (request.url.match(hostRegex)) {
try {
httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);
httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);
} catch (err) {
console.log('Error reading cert/key file', err);
}
httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions);
break;
} }
} }
} }
// set proxy if enabled // set proxy if enabled
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false); const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
if (proxyEnabled) { const shouldProxy = shouldUseProxy(request.url, get(brunoConfig, 'proxy.noProxy', ''));
let proxyUri; if (proxyEnabled && shouldProxy) {
const interpolationOptions = {
envVars: envVariables,
collectionVariables,
processEnvVars
};
const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions); const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions);
const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions); const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions);
const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions); const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions);
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false); const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
const socksEnabled = proxyProtocol.includes('socks'); const socksEnabled = proxyProtocol.includes('socks');
interpolateString; let proxyUri;
if (proxyAuthEnabled) { if (proxyAuthEnabled) {
const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions); const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions); const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
@ -128,16 +149,13 @@ const runSingleRequest = async function (
if (socksEnabled) { if (socksEnabled) {
const socksProxyAgent = new SocksProxyAgent(proxyUri); const socksProxyAgent = new SocksProxyAgent(proxyUri);
request.httpsAgent = socksProxyAgent; request.httpsAgent = socksProxyAgent;
request.httpAgent = socksProxyAgent; request.httpAgent = socksProxyAgent;
} else { } else {
request.httpsAgent = new HttpsProxyAgent( request.httpsAgent = new HttpsProxyAgent(
proxyUri, proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
); );
request.httpAgent = new HttpProxyAgent(proxyUri); request.httpAgent = new HttpProxyAgent(proxyUri);
} }
} else if (Object.keys(httpsAgentRequestFields).length > 0) { } else if (Object.keys(httpsAgentRequestFields).length > 0) {
@ -163,7 +181,7 @@ const runSingleRequest = async function (
responseTime = response.headers.get('request-duration'); responseTime = response.headers.get('request-duration');
response.headers.delete('request-duration'); response.headers.delete('request-duration');
} catch (err) { } catch (err) {
if (err && err.response) { if (err?.response) {
response = err.response; response = err.response;
// Prevents the duration on leaking to the actual result // Prevents the duration on leaking to the actual result
@ -199,7 +217,7 @@ const runSingleRequest = async function (
// run post-response vars // run post-response vars
const postResponseVars = get(bruJson, 'request.vars.res'); const postResponseVars = get(bruJson, 'request.vars.res');
if (postResponseVars && postResponseVars.length) { if (postResponseVars?.length) {
const varsRuntime = new VarsRuntime(); const varsRuntime = new VarsRuntime();
varsRuntime.runPostResponseVars( varsRuntime.runPostResponseVars(
postResponseVars, postResponseVars,
@ -217,7 +235,7 @@ const runSingleRequest = async function (
get(collectionRoot, 'request.script.res'), get(collectionRoot, 'request.script.res'),
get(bruJson, 'request.script.res') get(bruJson, 'request.script.res')
]).join(os.EOL); ]).join(os.EOL);
if (responseScriptFile && responseScriptFile.length) { if (responseScriptFile?.length) {
const scriptRuntime = new ScriptRuntime(); const scriptRuntime = new ScriptRuntime();
await scriptRuntime.runResponseScript( await scriptRuntime.runResponseScript(
decomment(responseScriptFile), decomment(responseScriptFile),
@ -275,7 +293,7 @@ const runSingleRequest = async function (
testResults = get(result, 'results', []); testResults = get(result, 'results', []);
} }
if (testResults && testResults.length) { if (testResults?.length) {
each(testResults, (testResult) => { each(testResults, (testResult) => {
if (testResult.status === 'pass') { if (testResult.status === 'pass') {
console.log(chalk.green(``) + chalk.dim(testResult.description)); console.log(chalk.green(``) + chalk.dim(testResult.description));

View File

@ -4,10 +4,10 @@ const axios = require('axios');
* Function that configures axios with timing interceptors * Function that configures axios with timing interceptors
* Important to note here that the timings are not completely accurate. * Important to note here that the timings are not completely accurate.
* @see https://github.com/axios/axios/issues/695 * @see https://github.com/axios/axios/issues/695
* @returns {import('axios').AxiosStatic} * @returns {axios.AxiosInstance}
*/ */
function makeAxiosInstance() { function makeAxiosInstance() {
/** @type {import('axios').AxiosStatic} */ /** @type {axios.AxiosInstance} */
const instance = axios.create(); const instance = axios.create();
instance.interceptors.request.use((config) => { instance.interceptors.request.use((config) => {
@ -26,9 +26,7 @@ function makeAxiosInstance() {
if (error.response) { if (error.response) {
const end = Date.now(); const end = Date.now();
const start = error.config.headers['request-start-time']; const start = error.config.headers['request-start-time'];
if (error.response) { error.response.headers['request-duration'] = end - start;
error.response.headers['request-duration'] = end - start;
}
} }
return Promise.reject(error); return Promise.reject(error);
} }

View File

@ -0,0 +1,65 @@
const parseUrl = require('url').parse;
const DEFAULT_PORTS = {
ftp: 21,
gopher: 70,
http: 80,
https: 443,
ws: 80,
wss: 443
};
/**
* check for proxy bypass, Copied form 'proxy-from-env'
*/
const shouldUseProxy = (url, proxyByPass) => {
if (proxyByPass === '*') {
return false; // Never proxy if wildcard is set.
}
if (!proxyByPass) {
return true; // use proxy if enabled
}
const parsedUrl = typeof url === 'string' ? parseUrl(url) : url || {};
let proto = parsedUrl.protocol;
let hostname = parsedUrl.host;
let port = parsedUrl.port;
if (typeof hostname !== 'string' || !hostname || typeof proto !== 'string') {
return false; // Don't proxy URLs without a valid scheme or host.
}
proto = proto.split(':', 1)[0];
// Stripping ports in this way instead of using parsedUrl.hostname to make
// sure that the brackets around IPv6 addresses are kept.
hostname = hostname.replace(/:\d*$/, '');
port = parseInt(port) || DEFAULT_PORTS[proto] || 0;
return proxyByPass.split(/[,;\s]/).every(function (dontProxyFor) {
if (!dontProxyFor) {
return true; // Skip zero-length hosts.
}
const parsedProxy = dontProxyFor.match(/^(.+):(\d+)$/);
let parsedProxyHostname = parsedProxy ? parsedProxy[1] : dontProxyFor;
const parsedProxyPort = parsedProxy ? parseInt(parsedProxy[2]) : 0;
if (parsedProxyPort && parsedProxyPort !== port) {
return true; // Skip if ports don't match.
}
if (!/^[.*]/.test(parsedProxyHostname)) {
// No wildcards, so stop proxying if there is an exact match.
return hostname !== parsedProxyHostname;
}
if (parsedProxyHostname.charAt(0) === '*') {
// Remove leading wildcard.
parsedProxyHostname = parsedProxyHostname.slice(1);
}
// Stop proxying if the hostname ends with the no_proxy host.
return !hostname.endsWith(parsedProxyHostname);
});
};
module.exports = {
shouldUseProxy
};

View File

@ -75,7 +75,7 @@ app.on('ready', async () => {
}); });
// register all ipc handlers // register all ipc handlers
registerNetworkIpc(mainWindow, watcher, lastOpenedCollections); registerNetworkIpc(mainWindow);
registerCollectionsIpc(mainWindow, watcher, lastOpenedCollections); registerCollectionsIpc(mainWindow, watcher, lastOpenedCollections);
registerPreferencesIpc(mainWindow, watcher, lastOpenedCollections); registerPreferencesIpc(mainWindow, watcher, lastOpenedCollections);
}); });

View File

@ -18,6 +18,7 @@ const { stringifyJson } = require('../utils/common');
const { openCollectionDialog } = require('../app/collections'); const { openCollectionDialog } = require('../app/collections');
const { generateUidBasedOnHash } = require('../utils/common'); const { generateUidBasedOnHash } = require('../utils/common');
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids'); const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
const { setPreferences } = require('../store/preferences');
const EnvironmentSecretsStore = require('../store/env-secrets'); const EnvironmentSecretsStore = require('../store/env-secrets');
const environmentSecretsStore = new EnvironmentSecretsStore(); const environmentSecretsStore = new EnvironmentSecretsStore();
@ -32,9 +33,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// browse directory // browse directory
ipcMain.handle('renderer:browse-directory', async (event, pathname, request) => { ipcMain.handle('renderer:browse-directory', async (event, pathname, request) => {
try { try {
const dirPath = await browseDirectory(mainWindow); return await browseDirectory(mainWindow);
return dirPath;
} catch (error) { } catch (error) {
return Promise.reject(error); return Promise.reject(error);
} }
@ -67,8 +66,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig); mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid); ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
return;
} catch (error) { } catch (error) {
return Promise.reject(error); return Promise.reject(error);
} }
@ -93,8 +90,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
collectionPathname, collectionPathname,
newName newName
}); });
return;
} catch (error) { } catch (error) {
return Promise.reject(error); return Promise.reject(error);
} }
@ -311,7 +306,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
fs.unlinkSync(pathname); fs.unlinkSync(pathname);
} else { } else {
return Promise.reject(error); return Promise.reject();
} }
} catch (error) { } catch (error) {
return Promise.reject(error); return Promise.reject(error);

View File

@ -4,10 +4,10 @@ const axios = require('axios');
* Function that configures axios with timing interceptors * Function that configures axios with timing interceptors
* Important to note here that the timings are not completely accurate. * Important to note here that the timings are not completely accurate.
* @see https://github.com/axios/axios/issues/695 * @see https://github.com/axios/axios/issues/695
* @returns {import('axios').AxiosStatic} * @returns {axios.AxiosInstance}
*/ */
function makeAxiosInstance() { function makeAxiosInstance() {
/** @type {import('axios').AxiosStatic} */ /** @type {axios.AxiosInstance} */
const instance = axios.create(); const instance = axios.create();
instance.interceptors.request.use((config) => { instance.interceptors.request.use((config) => {

View File

@ -16,7 +16,7 @@ const { uuid } = require('../../utils/common');
const interpolateVars = require('./interpolate-vars'); const interpolateVars = require('./interpolate-vars');
const { interpolateString } = require('./interpolate-string'); const { interpolateString } = require('./interpolate-string');
const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper'); const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper');
const { getPreferences } = require('../../store/preferences'); const { preferences } = require('../../store/preferences');
const { getProcessEnvVars } = require('../../store/process-env'); const { getProcessEnvVars } = require('../../store/process-env');
const { getBrunoConfig } = require('../../store/bruno-config'); const { getBrunoConfig } = require('../../store/bruno-config');
const { HttpsProxyAgent } = require('https-proxy-agent'); const { HttpsProxyAgent } = require('https-proxy-agent');
@ -24,6 +24,7 @@ const { HttpProxyAgent } = require('http-proxy-agent');
const { SocksProxyAgent } = require('socks-proxy-agent'); const { SocksProxyAgent } = require('socks-proxy-agent');
const { makeAxiosInstance } = require('./axios-instance'); const { makeAxiosInstance } = require('./axios-instance');
const { addAwsV4Interceptor, resolveCredentials } = require('./awsv4auth-helper'); const { addAwsV4Interceptor, resolveCredentials } = require('./awsv4auth-helper');
const { shouldUseProxy } = require('../../utils/proxy-util');
// override the default escape function to prevent escaping // override the default escape function to prevent escaping
Mustache.escape = function (value) { Mustache.escape = function (value) {
@ -83,6 +84,96 @@ const getSize = (data) => {
return 0; return 0;
}; };
const configureRequest = async (collectionUid, request, envVars, collectionVariables, processEnvVars) => {
const httpsAgentRequestFields = {};
if (!preferences.isTlsVerification()) {
httpsAgentRequestFields['rejectUnauthorized'] = false;
}
const brunoConfig = getBrunoConfig(collectionUid);
const interpolationOptions = {
envVars,
collectionVariables,
processEnvVars
};
// client certificate config
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
for (let clientCert of clientCertConfig) {
const domain = interpolateString(clientCert.domain, interpolationOptions);
const certFilePath = interpolateString(clientCert.certFilePath, interpolationOptions);
const keyFilePath = interpolateString(clientCert.keyFilePath, interpolationOptions);
if (domain && certFilePath && keyFilePath) {
const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
if (request.url.match(hostRegex)) {
try {
httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);
httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);
} catch (err) {
console.log('Error reading cert/key file', err);
}
httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions);
break;
}
}
}
// proxy configuration
let proxyConfig = get(brunoConfig, 'proxy', {});
let proxyEnabled = get(proxyConfig, 'enabled', 'disabled');
if (proxyEnabled === 'global') {
proxyConfig = preferences.getProxyConfig();
proxyEnabled = get(proxyConfig, 'enabled', false);
}
const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'noProxy', ''));
if ((proxyEnabled === true || proxyEnabled === 'enabled') && shouldProxy) {
const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions);
const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions);
const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions);
const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false);
const socksEnabled = proxyProtocol.includes('socks');
let proxyUri;
if (proxyAuthEnabled) {
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
} else {
proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
}
if (socksEnabled) {
const socksProxyAgent = new SocksProxyAgent(proxyUri);
request.httpsAgent = socksProxyAgent;
request.httpAgent = socksProxyAgent;
} else {
request.httpsAgent = new HttpsProxyAgent(
proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
request.httpAgent = new HttpProxyAgent(proxyUri);
}
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
request.httpsAgent = new https.Agent({
...httpsAgentRequestFields
});
}
const axiosInstance = makeAxiosInstance();
if (request.awsv4config) {
request.awsv4config = await resolveCredentials(request);
addAwsV4Interceptor(axiosInstance, request);
delete request.awsv4config;
}
request.timeout = preferences.getTimeout();
return axiosInstance;
};
const registerNetworkIpc = (mainWindow) => { const registerNetworkIpc = (mainWindow) => {
// handler for sending http request // handler for sending http request
ipcMain.handle('send-http-request', async (event, item, collection, environment, collectionVariables) => { ipcMain.handle('send-http-request', async (event, item, collection, environment, collectionVariables) => {
@ -134,7 +225,7 @@ const registerNetworkIpc = (mainWindow) => {
// run pre-request vars // run pre-request vars
const preRequestVars = get(request, 'vars.req', []); const preRequestVars = get(request, 'vars.req', []);
if (preRequestVars && preRequestVars.length) { if (preRequestVars?.length) {
const varsRuntime = new VarsRuntime(); const varsRuntime = new VarsRuntime();
const result = varsRuntime.runPreRequestVars( const result = varsRuntime.runPreRequestVars(
preRequestVars, preRequestVars,
@ -155,15 +246,11 @@ const registerNetworkIpc = (mainWindow) => {
} }
} }
const preferences = getPreferences();
const timeout = get(preferences, 'request.timeout', 0);
request.timeout = timeout;
// run pre-request script // run pre-request script
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join( const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(
os.EOL os.EOL
); );
if (requestScript && requestScript.length) { if (requestScript?.length) {
const scriptRuntime = new ScriptRuntime(); const scriptRuntime = new ScriptRuntime();
const result = await scriptRuntime.runRequestScript( const result = await scriptRuntime.runRequestScript(
decomment(requestScript), decomment(requestScript),
@ -209,96 +296,20 @@ const registerNetworkIpc = (mainWindow) => {
cancelTokenUid cancelTokenUid
}); });
const sslVerification = get(preferences, 'request.sslVerification', true); const axiosInstance = await configureRequest(
const httpsAgentRequestFields = {}; collectionUid,
if (!sslVerification) { request,
httpsAgentRequestFields['rejectUnauthorized'] = false;
}
const brunoConfig = getBrunoConfig(collectionUid);
const interpolationOptions = {
envVars, envVars,
collectionVariables, collectionVariables,
processEnvVars processEnvVars
}; );
// client certificate config
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
for (clientCert of clientCertConfig) {
const domain = interpolateString(clientCert.domain, interpolationOptions);
const certFilePath = interpolateString(clientCert.certFilePath, interpolationOptions);
const keyFilePath = interpolateString(clientCert.keyFilePath, interpolationOptions);
if (domain && certFilePath && keyFilePath) {
const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
if (request.url.match(hostRegex)) {
try {
httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);
httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);
} catch (err) {
console.log('Error reading cert/key file', err);
}
httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions);
break;
}
}
}
// proxy configuration
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
if (proxyEnabled) {
let proxyUri;
const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions);
const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions);
const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions);
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
const socksEnabled = proxyProtocol.includes('socks');
if (proxyAuthEnabled) {
const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
} else {
proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
}
if (socksEnabled) {
const socksProxyAgent = new SocksProxyAgent(proxyUri);
request.httpsAgent = socksProxyAgent;
request.httpAgent = socksProxyAgent;
} else {
request.httpsAgent = new HttpsProxyAgent(
proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
request.httpAgent = new HttpProxyAgent(proxyUri);
}
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
request.httpsAgent = new https.Agent({
...httpsAgentRequestFields
});
}
const axiosInstance = makeAxiosInstance();
if (request.awsv4config) {
request.awsv4config = await resolveCredentials(request);
addAwsV4Interceptor(axiosInstance, request);
delete request.awsv4config;
}
/** @type {import('axios').AxiosResponse} */ /** @type {import('axios').AxiosResponse} */
const response = await axiosInstance(request); const response = await axiosInstance(request);
// run post-response vars // run post-response vars
const postResponseVars = get(request, 'vars.res', []); const postResponseVars = get(request, 'vars.res', []);
if (postResponseVars && postResponseVars.length) { if (postResponseVars?.length) {
const varsRuntime = new VarsRuntime(); const varsRuntime = new VarsRuntime();
const result = varsRuntime.runPostResponseVars( const result = varsRuntime.runPostResponseVars(
postResponseVars, postResponseVars,
@ -324,7 +335,7 @@ const registerNetworkIpc = (mainWindow) => {
const responseScript = compact([get(collectionRoot, 'request.script.res'), get(request, 'script.res')]).join( const responseScript = compact([get(collectionRoot, 'request.script.res'), get(request, 'script.res')]).join(
os.EOL os.EOL
); );
if (responseScript && responseScript.length) { if (responseScript?.length) {
const scriptRuntime = new ScriptRuntime(); const scriptRuntime = new ScriptRuntime();
const result = await scriptRuntime.runResponseScript( const result = await scriptRuntime.runResponseScript(
decomment(responseScript), decomment(responseScript),
@ -427,7 +438,7 @@ const registerNetworkIpc = (mainWindow) => {
return Promise.reject(error); return Promise.reject(error);
} }
if (error && error.response) { if (error?.response) {
// run assertions // run assertions
const assertions = get(request, 'assertions'); const assertions = get(request, 'assertions');
if (assertions) { if (assertions) {
@ -519,12 +530,9 @@ const registerNetworkIpc = (mainWindow) => {
const collectionRoot = get(collection, 'root', {}); const collectionRoot = get(collection, 'root', {});
const preparedRequest = prepareGqlIntrospectionRequest(endpoint, envVars, request, collectionRoot); const preparedRequest = prepareGqlIntrospectionRequest(endpoint, envVars, request, collectionRoot);
const preferences = getPreferences(); request.timeout = preferences.getTimeout();
const timeout = get(preferences, 'request.timeout', 0);
request.timeout = timeout;
const sslVerification = get(preferences, 'request.sslVerification', true);
if (!sslVerification) { if (!preferences.isTlsVerification()) {
request.httpsAgent = new https.Agent({ request.httpsAgent = new https.Agent({
rejectUnauthorized: false rejectUnauthorized: false
}); });
@ -658,20 +666,11 @@ const registerNetworkIpc = (mainWindow) => {
} }
} }
const preferences = getPreferences();
const timeout = get(preferences, 'request.timeout', 0);
request.timeout = timeout;
const sslVerification = get(preferences, 'request.sslVerification', true);
const httpsAgentRequestFields = {};
if (!sslVerification) {
httpsAgentRequestFields['rejectUnauthorized'] = false;
}
// run pre-request script // run pre-request script
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join( const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(
os.EOL os.EOL
); );
if (requestScript && requestScript.length) { if (requestScript?.length) {
const scriptRuntime = new ScriptRuntime(); const scriptRuntime = new ScriptRuntime();
const result = await scriptRuntime.runRequestScript( const result = await scriptRuntime.runRequestScript(
decomment(requestScript), decomment(requestScript),
@ -708,92 +707,22 @@ const registerNetworkIpc = (mainWindow) => {
...eventData ...eventData
}); });
const interpolationOptions = { const axiosInstance = await configureRequest(
collectionUid,
request,
envVars, envVars,
collectionVariables, collectionVariables,
processEnvVars processEnvVars
}; );
const brunoConfig = getBrunoConfig(collectionUid);
// client certificate config
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
for (clientCert of clientCertConfig) {
const domain = interpolateString(clientCert.domain, interpolationOptions);
const certFilePath = interpolateString(clientCert.certFilePath, interpolationOptions);
const keyFilePath = interpolateString(clientCert.keyFilePath, interpolationOptions);
if (domain && certFilePath && keyFilePath) {
const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
if (request.url.match(hostRegex)) {
try {
httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);
httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);
} catch (err) {
console.log('Error reading cert/key file', err);
}
httpsAgentRequestFields['passphrase'] = interpolateString(
clientCert.passphrase,
interpolationOptions
);
break;
}
}
}
// proxy configuration
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
if (proxyEnabled) {
let proxyUri;
const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions);
const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions);
const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions);
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
const socksEnabled = proxyProtocol.includes('socks');
if (proxyAuthEnabled) {
const proxyAuthUsername = interpolateString(
get(brunoConfig, 'proxy.auth.username'),
interpolationOptions
);
const proxyAuthPassword = interpolateString(
get(brunoConfig, 'proxy.auth.password'),
interpolationOptions
);
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
} else {
proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
}
if (socksEnabled) {
const socksProxyAgent = new SocksProxyAgent(proxyUri);
request.httpsAgent = socksProxyAgent;
request.httpAgent = socksProxyAgent;
} else {
request.httpsAgent = new HttpsProxyAgent(
proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
request.httpAgent = new HttpProxyAgent(proxyUri);
}
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
request.httpsAgent = new https.Agent({
...httpsAgentRequestFields
});
}
// send request
timeStart = Date.now(); timeStart = Date.now();
const response = await axios(request); /** @type {import('axios').AxiosResponse} */
const response = await axiosInstance(request);
timeEnd = Date.now(); timeEnd = Date.now();
// run post-response vars // run post-response vars
const postResponseVars = get(request, 'vars.res', []); const postResponseVars = get(request, 'vars.res', []);
if (postResponseVars && postResponseVars.length) { if (postResponseVars?.length) {
const varsRuntime = new VarsRuntime(); const varsRuntime = new VarsRuntime();
const result = varsRuntime.runPostResponseVars( const result = varsRuntime.runPostResponseVars(
postResponseVars, postResponseVars,
@ -913,7 +842,7 @@ const registerNetworkIpc = (mainWindow) => {
duration = timeEnd - timeStart; duration = timeEnd - timeStart;
} }
if (error && error.response) { if (error?.response) {
responseReceived = { responseReceived = {
status: error.response.status, status: error.response.status,
statusText: error.response.statusText, statusText: error.response.statusText,

View File

@ -1,9 +1,64 @@
const { ipcMain } = require('electron'); const { ipcMain } = require('electron');
const { getPreferences, savePreferences } = require('../store/preferences'); const { getPreferences, savePreferences, getPath } = require('../store/preferences');
const { isDirectory } = require('../utils/filesystem'); const { isDirectory } = require('../utils/filesystem');
const { openCollection } = require('../app/collections'); const { openCollection } = require('../app/collections');
const stores = require('../store');
const chokidar = require('chokidar');
const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => { const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
const change = async (pathname, store) => {
if (store === stores.PREFERENCES) {
mainWindow.webContents.send('main:load-preferences', getPreferences());
}
};
class StoreWatcher {
constructor() {
this.watchers = {};
}
addWatcher(watchPath, store) {
console.log(`watcher add: ${watchPath} for store ${store}`);
if (this.watchers[watchPath]) {
this.watchers[watchPath].close();
}
const self = this;
setTimeout(() => {
const watcher = chokidar.watch(watchPath, {
ignoreInitial: false,
usePolling: false,
persistent: true,
ignorePermissionErrors: true,
awaitWriteFinish: {
stabilityThreshold: 80,
pollInterval: 10
},
depth: 20
});
watcher.on('change', (pathname) => change(pathname, store));
self.watchers[watchPath] = watcher;
}, 100);
}
hasWatcher(watchPath) {
return this.watchers[watchPath];
}
removeWatcher(watchPath) {
if (this.watchers[watchPath]) {
this.watchers[watchPath].close();
this.watchers[watchPath] = null;
}
}
}
const storeWatcher = new StoreWatcher();
storeWatcher.addWatcher(getPath(), stores.PREFERENCES);
ipcMain.handle('renderer:ready', async (event) => { ipcMain.handle('renderer:ready', async (event) => {
// load preferences // load preferences
const preferences = getPreferences(); const preferences = getPreferences();
@ -15,7 +70,7 @@ const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
if (lastOpened && lastOpened.length) { if (lastOpened && lastOpened.length) {
for (let collectionPath of lastOpened) { for (let collectionPath of lastOpened) {
if (isDirectory(collectionPath)) { if (isDirectory(collectionPath)) {
openCollection(mainWindow, watcher, collectionPath, { await openCollection(mainWindow, watcher, collectionPath, {
dontSendDisplayErrors: true dontSendDisplayErrors: true
}); });
} }

View File

@ -0,0 +1,7 @@
const PREFERENCES = 'PREFERENCES';
const stores = {
PREFERENCES
};
module.exports = stores;

View File

@ -1,5 +1,12 @@
const Yup = require('yup'); const Yup = require('yup');
const Store = require('electron-store'); const Store = require('electron-store');
const { get } = require('lodash');
/**
* The preferences are stored in the electron store 'preferences.json'.
* The electron process uses this module to get the preferences.
*
*/
const defaultPreferences = { const defaultPreferences = {
request: { request: {
@ -8,6 +15,18 @@ const defaultPreferences = {
}, },
font: { font: {
codeFont: 'default' codeFont: 'default'
},
proxy: {
enabled: false,
protocol: 'http',
hostnameHttp: '',
portHttp: '',
auth: {
enabled: false,
username: '',
password: ''
},
noProxy: ''
} }
}; };
@ -18,6 +37,18 @@ const preferencesSchema = Yup.object().shape({
}), }),
font: Yup.object().shape({ font: Yup.object().shape({
codeFont: Yup.string().nullable() codeFont: Yup.string().nullable()
}),
proxy: Yup.object({
enabled: Yup.boolean(),
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string().max(1024),
port: Yup.number().min(1).max(65535),
auth: Yup.object({
enabled: Yup.boolean(),
username: Yup.string().max(1024),
password: Yup.string().max(1024)
}).optional(),
noProxy: Yup.string().optional().max(1024)
}) })
}); });
@ -29,6 +60,10 @@ class PreferencesStore {
}); });
} }
getPath() {
return this.store.path;
}
getPreferences() { getPreferences() {
return { return {
...defaultPreferences, ...defaultPreferences,
@ -61,7 +96,27 @@ const savePreferences = async (newPreferences) => {
}); });
}; };
const getPath = () => {
return preferencesStore.getPath();
};
const preferences = {
isTlsVerification: () => {
return get(getPreferences(), 'request.sslVerification', true);
},
getTimeout: () => {
return get(getPreferences(), 'request.timeout', 0);
},
getProxyConfig: () => {
return get(getPreferences(), 'proxy', {});
}
};
module.exports = { module.exports = {
getPreferences, getPreferences,
savePreferences savePreferences,
getPath,
preferences
}; };

View File

@ -0,0 +1,64 @@
const parseUrl = require('url').parse;
const DEFAULT_PORTS = {
ftp: 21,
gopher: 70,
http: 80,
https: 443,
ws: 80,
wss: 443
};
/**
* check for proxy bypass, copied form 'proxy-from-env'
*/
const shouldUseProxy = (url, proxyByPass) => {
if (proxyByPass === '*') {
return false; // Never proxy if wildcard is set.
}
if (!proxyByPass) {
return true; // use proxy if enabled
}
const parsedUrl = typeof url === 'string' ? parseUrl(url) : url || {};
let proto = parsedUrl.protocol;
let hostname = parsedUrl.host;
let port = parsedUrl.port;
if (typeof hostname !== 'string' || !hostname || typeof proto !== 'string') {
return false; // Don't proxy URLs without a valid scheme or host.
}
proto = proto.split(':', 1)[0];
// Stripping ports in this way instead of using parsedUrl.hostname to make
// sure that the brackets around IPv6 addresses are kept.
hostname = hostname.replace(/:\d*$/, '');
port = parseInt(port) || DEFAULT_PORTS[proto] || 0;
return proxyByPass.split(/[,;\s]/).every(function (dontProxyFor) {
if (!dontProxyFor) {
return true; // Skip zero-length hosts.
}
const parsedProxy = dontProxyFor.match(/^(.+):(\d+)$/);
let parsedProxyHostname = parsedProxy ? parsedProxy[1] : dontProxyFor;
const parsedProxyPort = parsedProxy ? parseInt(parsedProxy[2]) : 0;
if (parsedProxyPort && parsedProxyPort !== port) {
return true; // Skip if ports don't match.
}
if (!/^[.*]/.test(parsedProxyHostname)) {
// No wildcards, so stop proxying if there is an exact match.
return hostname !== parsedProxyHostname;
}
if (parsedProxyHostname.charAt(0) === '*') {
// Remove leading wildcard.
parsedProxyHostname = parsedProxyHostname.slice(1);
}
// Stop proxying if the hostname ends with the no_proxy host.
return !hostname.endsWith(parsedProxyHostname);
});
};
module.exports = {
shouldUseProxy
};

View File

@ -0,0 +1,50 @@
const { shouldUseProxy } = require('../../src/utils/proxy-util');
test('no proxy necessary - star', () => {
const url = 'http://wwww.example.org/test';
const noProxy = '*';
expect(shouldUseProxy(url, noProxy)).toEqual(false);
});
test('no proxy necessary - no noProxy bypass', () => {
const url = 'http://wwww.example.org/test';
const noProxy = '';
expect(shouldUseProxy(url, noProxy)).toEqual(true);
});
test('no proxy necessary - wildcard match', () => {
const url = 'http://wwww.example.org/test';
const noProxy = '*example.org';
expect(shouldUseProxy(url, noProxy)).toEqual(false);
});
test('no proxy necessary - direct proxy', () => {
const url = 'http://wwww.example.org/test';
const noProxy = 'wwww.example.org';
expect(shouldUseProxy(url, noProxy)).toEqual(false);
});
test('no proxy necessary - multiple proxy', () => {
const url = 'http://wwww.example.org/test';
const noProxy = 'www.example.com,wwww.example.org';
expect(shouldUseProxy(url, noProxy)).toEqual(false);
});
test('proxy necessary - no proxy match multiple', () => {
const url = 'https://wwww.example.test/test';
const noProxy = 'www.example.com,wwww.example.org';
expect(shouldUseProxy(url, noProxy)).toEqual(true);
});
test('proxy necessary - no proxy match', () => {
const url = 'https://wwww.example.test/test';
const noProxy = 'www.example.com';
expect(shouldUseProxy(url, noProxy)).toEqual(true);
});

View File

@ -1,6 +1,6 @@
const { test, expect } = require('@playwright/test'); const { test, expect } = require('@playwright/test');
const { HomePage } = require('../tests/pages/home.page'); const { HomePage } = require('../tests/pages/home.page');
import * as faker from './utils/data-faker'; const { faker } = require('./utils/data-faker');
test.describe('bruno e2e test', () => { test.describe('bruno e2e test', () => {
let homePage; let homePage;