From 0da17692562b05ce9df4c6a23e9bb76572289557 Mon Sep 17 00:00:00 2001 From: nyyu Date: Sun, 1 Oct 2023 21:41:16 +0200 Subject: [PATCH] feat: initial mTLS support --- .../ClientCertSettings/StyledWrapper.js | 27 +++++ .../ClientCertSettings/index.js | 100 ++++++++++++++++++ .../components/CollectionSettings/index.js | 20 ++++ .../bruno-electron/src/ipc/network/index.js | 42 ++++++-- 4 files changed, 179 insertions(+), 10 deletions(-) create mode 100644 packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js diff --git a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/StyledWrapper.js new file mode 100644 index 0000000000..c8f1241c50 --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/StyledWrapper.js @@ -0,0 +1,27 @@ +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; + border-radius: 0px; + outline: none; + box-shadow: 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; diff --git a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js new file mode 100644 index 0000000000..f752b03472 --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js @@ -0,0 +1,100 @@ +import React, { useEffect } from 'react'; +import { useFormik } from 'formik'; +import * as Yup from 'yup'; + +import StyledWrapper from './StyledWrapper'; + +const ClientCertSettings = ({ clientCertConfig, onUpdate }) => { + const formik = useFormik({ + initialValues: { + enabled: clientCertConfig.enabled || false, + domain: clientCertConfig.domain || '', + cert: clientCertConfig.cert || '', + key: clientCertConfig.key || '', + passphrase: clientCertConfig.port || '' + }, + validationSchema: Yup.object({ + enabled: Yup.boolean(), + domain: Yup.string(), + cert: Yup.string(), + key: Yup.string(), + passphrase: Yup.string() + }), + onSubmit: (values) => { + onUpdate(values); + } + }); + + useEffect(() => { + formik.setValues({ + enabled: clientCertConfig.enabled || false, + domain: clientCertConfig.domain || '', + cert: clientCertConfig.cert || '', + key: clientCertConfig.key || '', + passphrase: clientCertConfig.port || '' + }); + }, [clientCertConfig]); + + const getFile = (e) => { + formik.values[e.name] = e.files[0].path; + }; + + return ( + +
+
+ + +
+
+ + +
+
+ + getFile(e.target)} /> +
+
+ + getFile(e.target)} /> +
+
+ + +
+
+ +
+
+
+ ); +}; + +export default ClientCertSettings; diff --git a/packages/bruno-app/src/components/CollectionSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/index.js index eaf44e7582..1f16edc40b 100644 --- a/packages/bruno-app/src/components/CollectionSettings/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/index.js @@ -7,6 +7,7 @@ import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actio import { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections'; import { useDispatch } from 'react-redux'; import ProxySettings from './ProxySettings'; +import ClientCertSettings from './ClientCertSettings'; import Headers from './Headers'; import Auth from './Auth'; import Script from './Script'; @@ -27,6 +28,8 @@ const CollectionSettings = ({ collection }) => { const proxyConfig = get(collection, 'brunoConfig.proxy', {}); + const clientCertConfig = get(collection, 'brunoConfig.clientCert', {}); + const onProxySettingsUpdate = (config) => { const brunoConfig = cloneDeep(collection.brunoConfig); brunoConfig.proxy = config; @@ -37,6 +40,17 @@ const CollectionSettings = ({ collection }) => { .catch((err) => console.log(err) && toast.error('Failed to update collection settings')); }; + const onClientCertSettingsUpdate = (config) => { + const brunoConfig = cloneDeep(collection.brunoConfig); + console.log(config); + brunoConfig.clientCert = config; + dispatch(updateBrunoConfig(brunoConfig, collection.uid)) + .then(() => { + toast.success('Collection settings updated successfully'); + }) + .catch((err) => console.log(err) && toast.error('Failed to update collection settings')); + }; + const getTabPanel = (tab) => { switch (tab) { case 'headers': { @@ -54,6 +68,9 @@ const CollectionSettings = ({ collection }) => { case 'proxy': { return ; } + case 'clientCert': { + return ; + } } }; @@ -81,6 +98,9 @@ const CollectionSettings = ({ collection }) => {
setTab('proxy')}> Proxy
+
setTab('clientCert')}> + Client certificate +
{getTabPanel(tab)}
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index f95340229d..c0047d9614 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -1,5 +1,6 @@ const os = require('os'); const qs = require('qs'); +const fs = require('fs'); const https = require('https'); const axios = require('axios'); const decomment = require('decomment'); @@ -212,7 +213,6 @@ const registerNetworkIpc = (mainWindow) => { cacertFile = cacertArray.find((el) => el); if (cacertFile && cacertFile.length > 1) { try { - const fs = require('fs'); caCrt = fs.readFileSync(cacertFile); httpsAgentRequestFields['ca'] = caCrt; } catch (err) { @@ -221,18 +221,40 @@ const registerNetworkIpc = (mainWindow) => { } } - // proxy configuration const brunoConfig = getBrunoConfig(collectionUid); + const interpolationOptions = { + envVars, + collectionVariables, + processEnvVars + }; + + // client certificate config + const clientCertEnabled = get(brunoConfig, 'clientCert.enabled', false); + if (clientCertEnabled) { + const clientCertDomain = get(brunoConfig, 'clientCert.domain', interpolationOptions); + const clientCertCert = get(brunoConfig, 'clientCert.cert', interpolationOptions); + const clientCertKey = get(brunoConfig, 'clientCert.key', interpolationOptions); + const clientCertPassphrase = get(brunoConfig, 'clientCert.passphrase', interpolationOptions); + if (clientCertDomain && clientCertCert && clientCertKey) { + const hostRegex = '^https:\\/\\/' + clientCertDomain.replaceAll('.', '\\.').replaceAll('*', '.*'); + + if (request.url.match(hostRegex)) { + try { + httpsAgentRequestFields['cert'] = fs.readFileSync(clientCertCert); + httpsAgentRequestFields['key'] = fs.readFileSync(clientCertKey); + } catch (err) { + console.log('Error loading file cert/key', err); + } + httpsAgentRequestFields['passphrase'] = clientCertPassphrase; + } + } + } + + // proxy configuration const proxyEnabled = get(brunoConfig, 'proxy.enabled', false); if (proxyEnabled) { let proxyUri; - const interpolationOptions = { - envVars, - collectionVariables, - processEnvVars - }; - const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions); const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions); const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions); @@ -711,14 +733,14 @@ const registerNetworkIpc = (mainWindow) => { if (socksEnabled) { const socksProxyAgent = new SocksProxyAgent(proxyUri); - + request.httpsAgent = socksProxyAgent; request.httpAgent = socksProxyAgent; } else { request.httpsAgent = new HttpsProxyAgent(proxyUri, { rejectUnauthorized: sslVerification }); - + request.httpAgent = new HttpProxyAgent(proxyUri); } } else if (!sslVerification) {