Skip to content

Commit

Permalink
feat: support client certificates
Browse files Browse the repository at this point in the history
  • Loading branch information
phoval committed Oct 13, 2023
1 parent 102f7a5 commit d6628d9
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import styled from 'styled-components';

const StyledWrapper = styled.div`
.settings-label {
width: 80px;
}
input {
width: 300px;
}
.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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React, { useEffect } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';

import StyledWrapper from './StyledWrapper';

const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
const formik = useFormik({
initialValues: {
domain: '',
certFilePath: '',
keyFilePath: '',
passphrase: ''
},
validationSchema: Yup.object({
domain: Yup.string().required(),
certFilePath: Yup.string().required(),
keyFilePath: Yup.string().required(),
passphrase: Yup.string()
}),
onSubmit: (values) => {
onUpdate(values);
}
});

const getFile = (e) => {
formik.values[e.name] = e.files[0].path;
};

return (
<StyledWrapper>
<h1 className="font-semibold mt-4 mb-2">Current client certificates</h1>
<ul>
{!clientCertConfig.length
? 'None'
: clientCertConfig.map((clientCert) => (
<li>
Domain: {clientCert.domain}
<button onClick={() => onRemove(clientCert)} className="submit btn btn-sm btn-secondary ml-2">
Delete
</button>
</li>
))}
</ul>
<h1 className="font-semibold mt-4 mb-2">New client certicate</h1>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="domain">
Domain
</label>
<input
id="domain"
type="text"
name="domain"
placeholder="*.example.org"
className="block textbox"
onChange={formik.handleChange}
value={formik.values.domain || ''}
/>
{formik.touched.domain && formik.errors.domain ? (
<div className="ml-1 text-red-500">{formik.errors.domain}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="certFilePath">
Cert file
</label>
<input
id="certFilePath"
type="file"
name="certFilePath"
className="block"
onChange={(e) => getFile(e.target)}
/>
{formik.touched.certFilePath && formik.errors.certFilePath ? (
<div className="ml-1 text-red-500">{formik.errors.certFilePath}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="keyFilePath">
Key file
</label>
<input
id="keyFilePath"
type="file"
name="keyFilePath"
className="block"
onChange={(e) => getFile(e.target)}
/>
{formik.touched.keyFilePath && formik.errors.keyFilePath ? (
<div className="ml-1 text-red-500">{formik.errors.keyFilePath}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="passphrase">
Passphrase
</label>
<input
id="passphrase"
type="text"
name="passphrase"
className="block textbox"
onChange={formik.handleChange}
value={formik.values.passphrase || ''}
/>
{formik.touched.passphrase && formik.errors.passphrase ? (
<div className="ml-1 text-red-500">{formik.errors.passphrase}</div>
) : null}
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary">
Add
</button>
</div>
</form>
</StyledWrapper>
);
};

export default ClientCertSettings;
37 changes: 37 additions & 0 deletions packages/bruno-app/src/components/CollectionSettings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,6 +29,8 @@ const CollectionSettings = ({ collection }) => {

const proxyConfig = get(collection, 'brunoConfig.proxy', {});

const clientCertConfig = get(collection, 'brunoConfig.clientCertificates', []);

const onProxySettingsUpdate = (config) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
brunoConfig.proxy = config;
Expand All @@ -38,6 +41,28 @@ const CollectionSettings = ({ collection }) => {
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
};

const onClientCertSettingsUpdate = (config) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
brunoConfig.clientCertificates
? brunoConfig.clientCertificates.push(config)
: (brunoConfig.clientCertificates = [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 onClientCertSettingsRemove = (config) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
brunoConfig.clientCertificates = brunoConfig.clientCertificates.filter((item) => item.domain != config.domain);
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': {
Expand All @@ -55,6 +80,15 @@ const CollectionSettings = ({ collection }) => {
case 'proxy': {
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
}
case 'clientCert': {
return (
<ClientCertSettings
clientCertConfig={clientCertConfig}
onUpdate={onClientCertSettingsUpdate}
onRemove={onClientCertSettingsRemove}
/>
);
}
case 'docs': {
return <Docs collection={collection} />;
}
Expand Down Expand Up @@ -85,6 +119,9 @@ const CollectionSettings = ({ collection }) => {
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
Proxy
</div>
<div className={getTabClassname('clientCert')} role="tab" onClick={() => setTab('clientCert')}>
Client certificate
</div>
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
Docs
</div>
Expand Down
39 changes: 31 additions & 8 deletions packages/bruno-electron/src/ipc/network/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const os = require('os');
const fs = require('fs');
const qs = require('qs');
const https = require('https');
const axios = require('axios');
Expand Down Expand Up @@ -214,7 +215,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) {
Expand All @@ -223,18 +223,41 @@ const registerNetworkIpc = (mainWindow) => {
}
}

// proxy configuration
const brunoConfig = getBrunoConfig(collectionUid);
const interpolationOptions = {
envVars,
collectionVariables,
processEnvVars
};

// client certificate config
const clientCertConfig = get(brunoConfig, 'clientCertificates', []);

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 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);
Expand Down

0 comments on commit d6628d9

Please sign in to comment.