mirror of
https://github.com/usebruno/bruno.git
synced 2025-01-25 15:18:50 +01:00
feat: support client certificates
This commit is contained in:
parent
102f7a5ecb
commit
d6628d960e
@ -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;
|
@ -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;
|
@ -7,6 +7,7 @@ import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actio
|
|||||||
import { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
|
import { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import ProxySettings from './ProxySettings';
|
import ProxySettings from './ProxySettings';
|
||||||
|
import ClientCertSettings from './ClientCertSettings';
|
||||||
import Headers from './Headers';
|
import Headers from './Headers';
|
||||||
import Auth from './Auth';
|
import Auth from './Auth';
|
||||||
import Script from './Script';
|
import Script from './Script';
|
||||||
@ -28,6 +29,8 @@ const CollectionSettings = ({ collection }) => {
|
|||||||
|
|
||||||
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
||||||
|
|
||||||
|
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates', []);
|
||||||
|
|
||||||
const onProxySettingsUpdate = (config) => {
|
const onProxySettingsUpdate = (config) => {
|
||||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||||
brunoConfig.proxy = config;
|
brunoConfig.proxy = config;
|
||||||
@ -38,6 +41,28 @@ const CollectionSettings = ({ collection }) => {
|
|||||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
.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) => {
|
const getTabPanel = (tab) => {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case 'headers': {
|
case 'headers': {
|
||||||
@ -55,6 +80,15 @@ const CollectionSettings = ({ collection }) => {
|
|||||||
case 'proxy': {
|
case 'proxy': {
|
||||||
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
|
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
|
||||||
}
|
}
|
||||||
|
case 'clientCert': {
|
||||||
|
return (
|
||||||
|
<ClientCertSettings
|
||||||
|
clientCertConfig={clientCertConfig}
|
||||||
|
onUpdate={onClientCertSettingsUpdate}
|
||||||
|
onRemove={onClientCertSettingsRemove}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
case 'docs': {
|
case 'docs': {
|
||||||
return <Docs collection={collection} />;
|
return <Docs collection={collection} />;
|
||||||
}
|
}
|
||||||
@ -85,6 +119,9 @@ const CollectionSettings = ({ collection }) => {
|
|||||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||||
Proxy
|
Proxy
|
||||||
</div>
|
</div>
|
||||||
|
<div className={getTabClassname('clientCert')} role="tab" onClick={() => setTab('clientCert')}>
|
||||||
|
Client certificate
|
||||||
|
</div>
|
||||||
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
||||||
Docs
|
Docs
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
const fs = require('fs');
|
||||||
const qs = require('qs');
|
const qs = require('qs');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
@ -214,7 +215,6 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
cacertFile = cacertArray.find((el) => el);
|
cacertFile = cacertArray.find((el) => el);
|
||||||
if (cacertFile && cacertFile.length > 1) {
|
if (cacertFile && cacertFile.length > 1) {
|
||||||
try {
|
try {
|
||||||
const fs = require('fs');
|
|
||||||
caCrt = fs.readFileSync(cacertFile);
|
caCrt = fs.readFileSync(cacertFile);
|
||||||
httpsAgentRequestFields['ca'] = caCrt;
|
httpsAgentRequestFields['ca'] = caCrt;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -223,18 +223,41 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// proxy configuration
|
|
||||||
const brunoConfig = getBrunoConfig(collectionUid);
|
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);
|
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
|
||||||
if (proxyEnabled) {
|
if (proxyEnabled) {
|
||||||
let proxyUri;
|
let proxyUri;
|
||||||
|
|
||||||
const interpolationOptions = {
|
|
||||||
envVars,
|
|
||||||
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);
|
||||||
|
Loading…
Reference in New Issue
Block a user