diff --git a/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js b/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js index df0d35df7..3f0981f8d 100644 --- a/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js @@ -19,7 +19,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => { port: Yup.number() .when('enabled', { is: 'enabled', - then: (port) => port.typeError('Specify port between 1 and 65535'), + 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) @@ -208,7 +208,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => { value={formik.values.hostname || ''} /> {formik.touched.hostname && formik.errors.hostname ? ( -
{formik.errors.hostname}
+
{formik.errors.hostname}
) : null}
@@ -227,7 +227,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => { onChange={formik.handleChange} value={formik.values.port} /> - {formik.touched.port && formik.errors.port ?
{formik.errors.port}
: null} + {formik.touched.port && formik.errors.port ? ( +
{formik.errors.port}
+ ) : null}
@@ -278,7 +280,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => { onChange={formik.handleChange} /> {formik.touched.auth?.password && formik.errors.auth?.password ? ( -
{formik.errors.auth.password}
+
{formik.errors.auth.password}
) : null}
@@ -299,7 +301,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => { value={formik.values.noProxy || ''} /> {formik.touched.noProxy && formik.errors.noProxy ? ( -
{formik.errors.noProxy}
+
{formik.errors.noProxy}
) : null}
diff --git a/packages/bruno-app/src/components/Preferences/ProxySettings/index.js b/packages/bruno-app/src/components/Preferences/ProxySettings/index.js index 37be01554..d40f98792 100644 --- a/packages/bruno-app/src/components/Preferences/ProxySettings/index.js +++ b/packages/bruno-app/src/components/Preferences/ProxySettings/index.js @@ -2,12 +2,55 @@ 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 { usePreferences } from 'providers/Preferences'; +import { useDispatch, useSelector } from 'react-redux'; -const ProxySettings = () => { - const { preferences, setPreferences } = usePreferences(); +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: { @@ -22,35 +65,28 @@ const ProxySettings = () => { }, noProxy: preferences.proxy.noProxy || '' }, - validationSchema: Yup.object({ - enabled: Yup.boolean(), - protocol: Yup.string().oneOf(['http', 'https', 'socks4', '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) - }), - noProxy: Yup.string().max(1024) - }), + validationSchema: proxySchema, onSubmit: (values) => { onUpdate(values); } }); const onUpdate = (values) => { - const updatedPreferences = { - ...preferences, - proxy: values - }; - - setPreferences(updatedPreferences) - .then(() => { - toast.success('Proxy settings updated successfully.'); + proxySchema + .validate(values, { abortEarly: true }) + .then((validatedProxy) => { + dispatch( + savePreferences({ + ...preferences, + proxy: validatedProxy + }) + ).then(() => { + close(); + }); }) - .catch((err) => { - console.error(err); + .catch((error) => { + let errMsg = error.message || 'Preferences validation error'; + toast.error(errMsg); }); }; @@ -147,7 +183,7 @@ const ProxySettings = () => { value={formik.values.hostname || ''} /> {formik.touched.hostname && formik.errors.hostname ? ( -
{formik.errors.hostname}
+
{formik.errors.hostname}
) : null}
@@ -166,7 +202,9 @@ const ProxySettings = () => { onChange={formik.handleChange} value={formik.values.port} /> - {formik.touched.port && formik.errors.port ?
{formik.errors.port}
: null} + {formik.touched.port && formik.errors.port ? ( +
{formik.errors.port}
+ ) : null}
@@ -217,7 +255,7 @@ const ProxySettings = () => { onChange={formik.handleChange} /> {formik.touched.auth?.password && formik.errors.auth?.password ? ( -
{formik.errors.auth.password}
+
{formik.errors.auth.password}
) : null}
@@ -238,7 +276,7 @@ const ProxySettings = () => { value={formik.values.noProxy || ''} /> {formik.touched.noProxy && formik.errors.noProxy ? ( -
{formik.errors.noProxy}
+
{formik.errors.noProxy}
) : null}
diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 8c98ff5e3..b0cffb900 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -87,22 +87,22 @@ const runSingleRequest = async function ( const httpsAgentRequestFields = {}; if (insecure) { httpsAgentRequestFields['rejectUnauthorized'] = false; - } - - const caCertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS]; - const caCert = caCertArray.find((el) => el); - if (caCert && caCert.length > 1) { - try { - httpsAgentRequestFields['ca'] = fs.readFileSync(caCert); - } catch (err) { - console.log('Error reading CA cert file:' + caCert, err); + } else { + const caCertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS]; + const caCert = caCertArray.find((el) => el); + if (caCert && caCert.length > 1) { + try { + httpsAgentRequestFields['ca'] = fs.readFileSync(caCert); + } catch (err) { + console.log('Error reading CA cert file:' + caCert, err); + } } } // set proxy if enabled const proxyEnabled = get(brunoConfig, 'proxy.enabled', false); - const proxyByPass = shouldUseProxy(request.url, get(brunoConfig, 'proxy.noProxy', '')); - if (proxyEnabled && !proxyByPass) { + const shouldProxy = shouldUseProxy(request.url, get(brunoConfig, 'proxy.noProxy', '')); + if (proxyEnabled && shouldProxy) { let proxyUri; const interpolationOptions = { envVars: envVariables, diff --git a/packages/bruno-electron/src/ipc/application.js b/packages/bruno-electron/src/ipc/application.js deleted file mode 100644 index 396de498a..000000000 --- a/packages/bruno-electron/src/ipc/application.js +++ /dev/null @@ -1,72 +0,0 @@ -const { ipcMain } = require('electron'); -const chokidar = require('chokidar'); -const stores = require('../store'); - -const registerApplicationIpc = (mainWindow, preferences) => { - const change = async (pathname, store) => { - if (store === stores.PREFERENCES) { - mainWindow.webContents.send('main:preferences-read', preferences.getAll()); - } - }; - - 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(preferences.getPath(), stores.PREFERENCES); - - ipcMain.handle('renderer:ready-application', async () => { - mainWindow.webContents.send('main:preferences-read', preferences.getAll()); - }); - - ipcMain.handle('renderer:set-preferences', async (event, newPreferences) => { - preferences.setPreferences(newPreferences); - }); - - ipcMain.handle('renderer:migrate-preferences', async (event, sslVerification) => { - preferences.migrateSslVerification(sslVerification); - }); -}; - -module.exports = registerApplicationIpc; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 1a6f60925..a63aba0dc 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -16,7 +16,7 @@ const { uuid } = require('../../utils/common'); const interpolateVars = require('./interpolate-vars'); const { interpolateString } = require('./interpolate-string'); const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper'); -const preferences = require('../../store/preferences'); +const { preferences } = require('../../store/preferences'); const { getProcessEnvVars } = require('../../store/process-env'); const { getBrunoConfig } = require('../../store/bruno-config'); const { HttpsProxyAgent } = require('https-proxy-agent'); @@ -84,23 +84,81 @@ const getSize = (data) => { return 0; }; -function getHttpsAgentRequestFields() { +const configureRequest = async (collectionUid, request, envVars, collectionVariables, processEnvVars) => { const httpsAgentRequestFields = {}; if (!preferences.isTlsVerification()) { httpsAgentRequestFields['rejectUnauthorized'] = false; - } - - const cacCrtArray = [preferences.getCaCert(), process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS]; - let caCertFile = cacCrtArray.find((el) => el); - if (caCertFile && caCertFile.length > 1) { - try { - httpsAgentRequestFields['ca'] = fs.readFileSync(caCertFile); - } catch (err) { - console.log('Error reading CA cert file:' + caCertFile, err); + } else { + const cacCrtArray = [preferences.getCaCert(), process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS]; + let caCertFile = cacCrtArray.find((el) => el); + if (caCertFile && caCertFile.length > 1) { + try { + httpsAgentRequestFields['ca'] = fs.readFileSync(caCertFile); + } catch (err) { + console.log('Error reading CA cert file:' + caCertFile, err); + } } } - return httpsAgentRequestFields; -} + + // proxy configuration + const brunoConfig = getBrunoConfig(collectionUid); + 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) { + let proxyUri; + const interpolationOptions = { + envVars, + collectionVariables, + processEnvVars + }; + + 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'); + + 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; + } + + return axiosInstance; +}; const registerNetworkIpc = (mainWindow) => { // handler for sending http request @@ -224,64 +282,13 @@ const registerNetworkIpc = (mainWindow) => { cancelTokenUid }); - const httpsAgentRequestFields = getHttpsAgentRequestFields(); - - // proxy configuration - const brunoConfig = getBrunoConfig(collectionUid); - let proxyConfig = get(brunoConfig, 'proxy', {}); - let proxyEnabled = get(proxyConfig, 'enabled', 'disabled'); - if (proxyEnabled === 'global') { - proxyConfig = preferences.getProxyConfig(); - proxyEnabled = get(proxyConfig, 'enabled', false); - } - const proxyByPass = shouldUseProxy(request.url, get(proxyConfig, 'noProxy', '')); - if ((proxyEnabled === true || proxyEnabled === 'enabled') && !proxyByPass) { - let proxyUri; - const interpolationOptions = { - envVars, - collectionVariables, - processEnvVars - }; - - 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'); - - 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; - } + const axiosInstance = await configureRequest( + collectionUid, + request, + envVars, + collectionVariables, + processEnvVars + ); /** @type {import('axios').AxiosResponse} */ const response = await axiosInstance(request); @@ -684,60 +691,17 @@ const registerNetworkIpc = (mainWindow) => { ...eventData }); - const httpsAgentRequestFields = getHttpsAgentRequestFields(); + const axiosInstance = await configureRequest( + collectionUid, + request, + envVars, + collectionVariables, + processEnvVars + ); - // proxy configuration - const brunoConfig = getBrunoConfig(collectionUid); - let proxyConfig = get(brunoConfig, 'proxy', {}); - let proxyEnabled = get(proxyConfig, 'enabled', 'disabled'); - if (proxyEnabled === 'global') { - proxyConfig = preferences.getProxyConfig(); - proxyEnabled = get(proxyConfig, 'enabled', false); - } - const proxyByPass = shouldUseProxy(request.url, get(proxyConfig, 'noProxy', '')); - if ((proxyEnabled === true || proxyEnabled === 'enabled') && !proxyByPass) { - let proxyUri; - const interpolationOptions = { - envVars, - collectionVariables, - processEnvVars - }; - - 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'); - - 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 - }); - } - - // send request timeStart = Date.now(); - const response = await axios(request); + /** @type {import('axios').AxiosResponse} */ + const response = await axiosInstance(request); timeEnd = Date.now(); // run post-response vars diff --git a/packages/bruno-electron/src/ipc/preferences.js b/packages/bruno-electron/src/ipc/preferences.js index f93ec5e6f..602de92b9 100644 --- a/packages/bruno-electron/src/ipc/preferences.js +++ b/packages/bruno-electron/src/ipc/preferences.js @@ -1,9 +1,64 @@ const { ipcMain } = require('electron'); -const { getPreferences, savePreferences } = require('../store/preferences'); +const { getPreferences, savePreferences, getPath } = require('../store/preferences'); const { isDirectory } = require('../utils/filesystem'); const { openCollection } = require('../app/collections'); +const stores = require('../store'); +const chokidar = require('chokidar'); 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) => { // load preferences const preferences = getPreferences(); @@ -15,7 +70,7 @@ const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => { if (lastOpened && lastOpened.length) { for (let collectionPath of lastOpened) { if (isDirectory(collectionPath)) { - openCollection(mainWindow, watcher, collectionPath, { + await openCollection(mainWindow, watcher, collectionPath, { dontSendDisplayErrors: true }); } diff --git a/packages/bruno-electron/src/store/preferences.js b/packages/bruno-electron/src/store/preferences.js index 62e17a564..93fa3b80e 100644 --- a/packages/bruno-electron/src/store/preferences.js +++ b/packages/bruno-electron/src/store/preferences.js @@ -47,6 +47,18 @@ const preferencesSchema = Yup.object().shape({ }), font: Yup.object().shape({ 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) }) }); @@ -58,9 +70,13 @@ class PreferencesStore { }); } + getPath() { + return this.store.path; + } + getPreferences() { return { - defaultPreferences, + ...defaultPreferences, ...this.store.get('preferences') }; } @@ -90,17 +106,13 @@ const savePreferences = async (newPreferences) => { }); }; +const getPath = () => { + return preferencesStore.getPath(); +}; + const preferences = { - getAll() { - return getPreferences(); - }, - - getPath() { - return preferencesStore.getPath(); - }, - isTlsVerification: () => { - return get(getPreferences(), 'request.tlsVerification', true); + return get(getPreferences(), 'request.sslVerification', true); }, getCaCert: () => { @@ -115,5 +127,6 @@ const preferences = { module.exports = { getPreferences, savePreferences, + getPath, preferences };