feat: update proxy implementation in preferences

This commit is contained in:
Anoop M D 2024-08-30 12:43:23 +05:30
parent c1ec95dc29
commit adb04fb0ff
6 changed files with 250 additions and 250 deletions

View File

@ -6,28 +6,14 @@ import * as Yup from 'yup';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { IconEye, IconEyeOff } from '@tabler/icons'; import { IconEye, IconEyeOff } from '@tabler/icons';
import { useState } from 'react'; import { useState } from 'react';
import { useMemo } from 'react';
import { cloneDeep } from 'lodash';
const ProxySettings = ({ proxyConfig: _proxyConfig, onUpdate }) => {
const proxyConfig = useMemo(() => {
const proxyConfigCopy = cloneDeep(_proxyConfig);
// backward compatibility check
if (proxyConfigCopy?.enabled == 'true') proxyConfigCopy.enabled = true;
if (proxyConfigCopy?.enabled == 'false') proxyConfigCopy.enabled = false;
proxyConfigCopy.enabled = ['string', 'boolean'].includes(typeof proxyConfigCopy?.enabled)
? proxyConfigCopy?.enabled
: 'global';
return proxyConfigCopy;
}, [_proxyConfig]);
const ProxySettings = ({ proxyConfig, onUpdate }) => {
const proxySchema = Yup.object({ const proxySchema = Yup.object({
enabled: Yup.mixed().oneOf([false, true, 'global']), enabled: Yup.string().oneOf(['global', 'true', 'false']),
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']), protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string() hostname: Yup.string()
.when('enabled', { .when('enabled', {
is: true, is: 'true',
then: (hostname) => hostname.required('Specify the hostname for your proxy.'), then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
otherwise: (hostname) => hostname.nullable() otherwise: (hostname) => hostname.nullable()
}) })
@ -40,7 +26,7 @@ const ProxySettings = ({ proxyConfig: _proxyConfig, onUpdate }) => {
.transform((_, val) => (val ? Number(val) : null)), .transform((_, val) => (val ? Number(val) : null)),
auth: Yup.object() auth: Yup.object()
.when('enabled', { .when('enabled', {
is: true, is: 'true',
then: Yup.object({ then: Yup.object({
enabled: Yup.boolean(), enabled: Yup.boolean(),
username: Yup.string() username: Yup.string()
@ -63,7 +49,7 @@ const ProxySettings = ({ proxyConfig: _proxyConfig, onUpdate }) => {
const formik = useFormik({ const formik = useFormik({
initialValues: { initialValues: {
enabled: proxyConfig.enabled, enabled: proxyConfig.enabled || 'global',
protocol: proxyConfig.protocol || 'http', protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '', hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '', port: proxyConfig.port || '',
@ -79,6 +65,13 @@ const ProxySettings = ({ proxyConfig: _proxyConfig, onUpdate }) => {
proxySchema proxySchema
.validate(values, { abortEarly: true }) .validate(values, { abortEarly: true })
.then((validatedProxy) => { .then((validatedProxy) => {
// serialize 'enabled' to boolean
if (validatedProxy.enabled === 'true') {
validatedProxy.enabled = true;
} else if (validatedProxy.enabled === 'false') {
validatedProxy.enabled = false;
}
onUpdate(validatedProxy); onUpdate(validatedProxy);
}) })
.catch((error) => { .catch((error) => {
@ -91,7 +84,7 @@ const ProxySettings = ({ proxyConfig: _proxyConfig, onUpdate }) => {
useEffect(() => { useEffect(() => {
formik.setValues({ formik.setValues({
enabled: proxyConfig.enabled, enabled: proxyConfig.enabled === true ? 'true' : proxyConfig.enabled === false ? 'false' : 'global',
protocol: proxyConfig.protocol || 'http', protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '', hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '', port: proxyConfig.port || '',
@ -125,229 +118,214 @@ const ProxySettings = ({ proxyConfig: _proxyConfig, onUpdate }) => {
/> />
</label> </label>
<div className="flex items-center"> <div className="flex items-center">
<label className="flex items-center cursor-pointer"> <label className="flex items-center">
<input <input
type="radio" type="radio"
name="enabled" name="enabled"
value="global" value="global"
checked={formik.values.enabled === 'global'} checked={formik.values.enabled === 'global'}
onChange={formik.handleChange} onChange={formik.handleChange}
className="mr-1 cursor-pointer" className="mr-1"
/> />
global global
</label> </label>
<label className="flex items-center ml-4 cursor-pointer"> <label className="flex items-center ml-4">
<input <input
type="radio" type="radio"
name="enabled" name="enabled"
value={'true'} value={'true'}
checked={formik.values.enabled === true} checked={formik.values.enabled === 'true'}
onChange={(e) => { onChange={formik.handleChange}
formik.setFieldValue('enabled', true); className="mr-1"
}}
className="mr-1 cursor-pointer"
/> />
enabled enabled
</label> </label>
<label className="flex items-center ml-4 cursor-pointer"> <label className="flex items-center ml-4">
<input <input
type="radio" type="radio"
name="enabled" name="enabled"
value={'false'} value={'false'}
checked={formik.values.enabled === false} checked={formik.values.enabled === 'false'}
onChange={(e) => { onChange={formik.handleChange}
formik.setFieldValue('enabled', false); className="mr-1"
}}
className="mr-1 cursor-pointer"
/> />
disabled disabled
</label> </label>
</div> </div>
</div> </div>
{formik.values.enabled === 'global' ? ( <div className="mb-3 flex items-center">
<div className="opacity-50">Global proxy can be configured in the app preferences.</div> <label className="settings-label" htmlFor="protocol">
) : null} Protocol
{formik.values.enabled === true ? ( </label>
<> <div className="flex items-center">
<div className="mb-3 flex items-center"> <label className="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="socks4"
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="mb-3 flex items-center">
<label className="settings-label" htmlFor="hostname">
Hostname
</label>
<input <input
id="hostname" type="radio"
type="text" name="protocol"
name="hostname" value="http"
className="block textbox" 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="socks4"
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="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="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="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="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="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.password">
Password
</label>
<div className="textbox flex flex-row items-center w-[13.2rem] h-[1.70rem] relative">
<input
id="auth.password"
type={passwordVisible ? 'text' : 'password'}
name="auth.password"
className="outline-none bg-transparent w-[10.5rem]"
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
spellCheck="false" spellCheck="false"
onChange={formik.handleChange} value={formik.values.auth.password}
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="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="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} onChange={formik.handleChange}
/> />
<button
type="button"
className="btn btn-sm absolute right-0"
onClick={() => setPasswordVisible(!passwordVisible)}
>
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
</button>
</div> </div>
<div> {formik.touched.auth?.password && formik.errors.auth?.password ? (
<div className="mb-3 flex items-center"> <div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
<label className="settings-label" htmlFor="auth.username"> ) : null}
Username </div>
</label> </div>
<input <div className="mb-3 flex items-center">
id="auth.username" <label className="settings-label" htmlFor="bypassProxy">
type="text" Proxy Bypass
name="auth.username" </label>
className="block textbox" <input
autoComplete="off" id="bypassProxy"
autoCorrect="off" type="text"
autoCapitalize="off" name="bypassProxy"
spellCheck="false" className="block textbox"
value={formik.values.auth.username} autoComplete="off"
onChange={formik.handleChange} autoCorrect="off"
/> autoCapitalize="off"
{formik.touched.auth?.username && formik.errors.auth?.username ? ( spellCheck="false"
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div> onChange={formik.handleChange}
) : null} value={formik.values.bypassProxy || ''}
</div> />
<div className="mb-3 flex items-center"> {formik.touched.bypassProxy && formik.errors.bypassProxy ? (
<label className="settings-label" htmlFor="auth.password"> <div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
Password ) : null}
</label> </div>
<div className="textbox flex flex-row items-center w-[13.2rem] h-[1.70rem] relative">
<input
id="auth.password"
type={passwordVisible ? 'text' : 'password'}
name="auth.password"
className="outline-none bg-transparent w-[10.5rem]"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.password}
onChange={formik.handleChange}
/>
<button
type="button"
className="btn btn-sm absolute right-0"
onClick={() => setPasswordVisible(!passwordVisible)}
>
{passwordVisible ? (
<IconEyeOff size={18} strokeWidth={1.5} />
) : (
<IconEye size={18} strokeWidth={1.5} />
)}
</button>
</div>
{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="mb-3 flex items-center">
<label className="settings-label" htmlFor="bypassProxy">
Proxy Bypass
</label>
<input
id="bypassProxy"
type="text"
name="bypassProxy"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.bypassProxy || ''}
/>
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
) : null}
</div>
</>
) : null}
<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
@ -358,4 +336,4 @@ const ProxySettings = ({ proxyConfig: _proxyConfig, onUpdate }) => {
); );
}; };
export default ProxySettings; export default ProxySettings;

View File

@ -20,6 +20,12 @@ const StyledWrapper = styled.div`
outline: none !important; outline: none !important;
} }
} }
.system-proxy-settings {
label {
color: ${(props) => props.theme.colors.text.yellow};
}
}
`; `;
export default StyledWrapper; export default StyledWrapper;

View File

@ -8,26 +8,13 @@ import StyledWrapper from './StyledWrapper';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { IconEye, IconEyeOff } from '@tabler/icons'; import { IconEye, IconEyeOff } from '@tabler/icons';
import { useState } from 'react'; import { useState } from 'react';
import { useMemo } from 'react';
import { cloneDeep } from 'lodash';
const ProxySettings = ({ close }) => { const ProxySettings = ({ close }) => {
const _preferences = useSelector((state) => state.app.preferences); const preferences = useSelector((state) => state.app.preferences);
const systemProxyEnvVariables = useSelector((state) => state.app.systemProxyEnvVariables); const systemProxyEnvVariables = useSelector((state) => state.app.systemProxyEnvVariables);
const { http_proxy, https_proxy, no_proxy } = systemProxyEnvVariables || {}; const { http_proxy, https_proxy, no_proxy } = systemProxyEnvVariables || {};
const dispatch = useDispatch(); const dispatch = useDispatch();
console.log(preferences);
const preferences = useMemo(() => {
const preferencesCopy = cloneDeep(_preferences);
// backward compatibility check
if (typeof preferencesCopy?.proxy?.enabled === 'boolean') {
preferencesCopy.proxy.mode = preferencesCopy?.proxy?.enabled ? 'on' : 'off';
} else {
preferencesCopy.proxy.mode =
typeof preferencesCopy?.proxy?.mode === 'string' ? preferencesCopy?.proxy?.mode : 'off';
}
return preferencesCopy;
}, [_preferences]);
const proxySchema = Yup.object({ const proxySchema = Yup.object({
mode: Yup.string().oneOf(['off', 'on', 'system']), mode: Yup.string().oneOf(['off', 'on', 'system']),
@ -142,7 +129,7 @@ const ProxySettings = ({ close }) => {
}} }}
className="mr-1 cursor-pointer" className="mr-1 cursor-pointer"
/> />
off Off
</label> </label>
<label className="flex items-center ml-4 cursor-pointer"> <label className="flex items-center ml-4 cursor-pointer">
<input <input
@ -155,7 +142,7 @@ const ProxySettings = ({ close }) => {
}} }}
className="mr-1 cursor-pointer" className="mr-1 cursor-pointer"
/> />
on On
</label> </label>
<label className="flex items-center ml-4 cursor-pointer"> <label className="flex items-center ml-4 cursor-pointer">
<input <input
@ -166,26 +153,30 @@ const ProxySettings = ({ close }) => {
onChange={formik.handleChange} onChange={formik.handleChange}
className="mr-1 cursor-pointer" className="mr-1 cursor-pointer"
/> />
system System Proxy
</label> </label>
</div> </div>
</div> </div>
{formik?.values?.mode === 'system' ? ( {formik?.values?.mode === 'system' ? (
<div className="mb-3 flex items-start pb-3"> <div className="mb-3 pt-1 text-muted system-proxy-settings">
<div className="flex flex-col gap-2 justify-start items-start"> <small>
<div className="mb-3 flex items-center"> Below values are sourced from your system environment variables and cannot be directly updated in Bruno.<br/>
Please refer to your OS documentation to change these values.
</small>
<div className="flex flex-col justify-start items-start pt-2">
<div className="mb-1 flex items-center">
<label className="settings-label" htmlFor="http_proxy"> <label className="settings-label" htmlFor="http_proxy">
http_proxy http_proxy
</label> </label>
<div className="opacity-80">{http_proxy || '-'}</div> <div className="opacity-80">{http_proxy || '-'}</div>
</div> </div>
<div className="mb-3 flex items-center"> <div className="mb-1 flex items-center">
<label className="settings-label" htmlFor="https_proxy"> <label className="settings-label" htmlFor="https_proxy">
https_proxy https_proxy
</label> </label>
<div className="opacity-80">{https_proxy || '-'}</div> <div className="opacity-80">{https_proxy || '-'}</div>
</div> </div>
<div className="mb-3 flex items-center"> <div className="mb-1 flex items-center">
<label className="settings-label" htmlFor="no_proxy"> <label className="settings-label" htmlFor="no_proxy">
no_proxy no_proxy
</label> </label>

View File

@ -8,9 +8,7 @@ const axios = require('axios');
*/ */
function makeAxiosInstance() { function makeAxiosInstance() {
/** @type {axios.AxiosInstance} */ /** @type {axios.AxiosInstance} */
const instance = axios.create({ const instance = axios.create();
proxy: false
});
instance.interceptors.request.use((config) => { instance.interceptors.request.use((config) => {
config.headers['request-start-time'] = Date.now(); config.headers['request-start-time'] = Date.now();

View File

@ -165,17 +165,32 @@ const configureRequest = async (
} }
} }
// proxy configuration /**
let proxyConfig = get(brunoConfig, 'proxy', {}); * Proxy configuration
let proxyMode = get(proxyConfig, 'enabled', 'global'); *
if (proxyMode === 'global') { * 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(); proxyConfig = preferencesUtil.getGlobalProxyConfig();
proxyMode = get(proxyConfig, 'mode', false); proxyMode = get(proxyConfig, 'mode', 'off');
} }
// proxyMode is true, if the collection-level proxy is enabled. if (proxyMode === 'on') {
// proxyMode is 'on', if the app-level proxy mode is turned on.
if (proxyMode === true || proxyMode === 'on') {
const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', '')); const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', ''));
if (shouldProxy) { if (shouldProxy) {
const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions); const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions);

View File

@ -1,6 +1,6 @@
const Yup = require('yup'); const Yup = require('yup');
const Store = require('electron-store'); const Store = require('electron-store');
const { get } = require('lodash'); const { get, merge } = require('lodash');
/** /**
* The preferences are stored in the electron store 'preferences.json'. * The preferences are stored in the electron store 'preferences.json'.
@ -81,10 +81,22 @@ class PreferencesStore {
} }
getPreferences() { getPreferences() {
return { let preferences = this.store.get('preferences', {});
...defaultPreferences,
...this.store.get('preferences') // This to support the old preferences format
}; // In the old format, we had a proxy.enabled flag
// In the new format, this maps to proxy.mode = 'on'
if (preferences?.proxy?.enabled) {
preferences.proxy.mode = 'on';
}
// Delete the proxy.enabled property if it exists, regardless of its value
// This is a part of migration to the new preferences format
if (preferences?.proxy && 'enabled' in preferences.proxy) {
delete preferences.proxy.enabled;
}
return merge({}, defaultPreferences, preferences);
} }
savePreferences(newPreferences) { savePreferences(newPreferences) {