Skip to content

Commit

Permalink
Feat/client cert types (usebruno#2482)
Browse files Browse the repository at this point in the history
* feat: pfx/cert client certificates

* ui updates

* file tooltip

* feat: updated client cert logic

* feat: updated validations

* const to let

* throw error incase of invalid file paths

* fix htmlFor label

* updated cli error messages
  • Loading branch information
lohxt1 authored and jwetzell committed Aug 2, 2024
1 parent b5f9ecd commit d751950
Show file tree
Hide file tree
Showing 3 changed files with 266 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,90 @@ import { IconEye, IconEyeOff } from '@tabler/icons';
import { useState } from 'react';

import StyledWrapper from './StyledWrapper';
import { useRef } from 'react';
import path from 'path';
import slash from 'utils/common/slash';

const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
const certFilePathInputRef = useRef();
const keyFilePathInputRef = useRef();
const pfxFilePathInputRef = useRef();

const formik = useFormik({
initialValues: {
domain: '',
type: 'cert',
certFilePath: '',
keyFilePath: '',
pfxFilePath: '',
passphrase: ''
},
validationSchema: Yup.object({
domain: Yup.string().required(),
certFilePath: Yup.string().required(),
keyFilePath: Yup.string().required(),
type: Yup.string().required().oneOf(['cert', 'pfx']),
certFilePath: Yup.string().when('type', {
is: (type) => type == 'cert',
then: Yup.string().min(1, 'certFilePath is a required field').required()
}),
keyFilePath: Yup.string().when('type', {
is: (type) => type == 'cert',
then: Yup.string().min(1, 'keyFilePath is a required field').required()
}),
pfxFilePath: Yup.string().when('type', {
is: (type) => type == 'pfx',
then: Yup.string().min(1, 'pfxFilePath is a required field').required()
}),
passphrase: Yup.string()
}),
onSubmit: (values) => {
onUpdate(values);
let relevantValues = {};
if (values.type === 'cert') {
relevantValues = {
domain: values.domain,
type: values.type,
certFilePath: values.certFilePath,
keyFilePath: values.keyFilePath,
passphrase: values.passphrase
};
} else {
relevantValues = {
domain: values.domain,
type: values.type,
pfxFilePath: values.pfxFilePath,
passphrase: values.passphrase
};
}
onUpdate(relevantValues);
formik.resetForm();
resetFileInputFields();
}
});

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

const resetFileInputFields = () => {
certFilePathInputRef.current.value = '';
keyFilePathInputRef.current.value = '';
pfxFilePathInputRef.current.value = '';
};

const [passwordVisible, setPasswordVisible] = useState(false);

const handleTypeChange = (e) => {
formik.setFieldValue('type', e.target.value);
if (e.target.value === 'cert') {
formik.setFieldValue('pfxFilePath', '');
pfxFilePathInputRef.current.value = '';
} else {
formik.setFieldValue('certFilePath', '');
certFilePathInputRef.current.value = '';
formik.setFieldValue('keyFilePath', '');
keyFilePathInputRef.current.value = '';
}
};

return (
<StyledWrapper className="w-full h-full">
<div className="text-xs mb-4 text-muted">Add client certificates to be used for specific domains.</div>
Expand Down Expand Up @@ -76,35 +134,163 @@ const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="certFilePath">
Cert file
<label id="type-label" className="settings-label">
Type
</label>
<input
id="certFilePath"
type="file"
name="certFilePath"
className="block non-passphrase-input"
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 non-passphrase-input"
onChange={(e) => getFile(e.target)}
/>
{formik.touched.keyFilePath && formik.errors.keyFilePath ? (
<div className="ml-1 text-red-500">{formik.errors.keyFilePath}</div>
) : null}
<div className="flex items-center" aria-labelledby="type-label">
<label className="flex items-center cursor-pointer" htmlFor="cert">
<input
id="cert"
type="radio"
name="type"
value="cert"
checked={formik.values.type === 'cert'}
onChange={handleTypeChange}
className="mr-1"
/>
Cert
</label>
<label className="flex items-center ml-4 cursor-pointer" htmlFor="pfx">
<input
id="pfx"
type="radio"
name="type"
value="pfx"
checked={formik.values.type === 'pfx'}
onChange={handleTypeChange}
className="mr-1"
/>
PFX
</label>
</div>
</div>
{formik.values.type === 'cert' ? (
<>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="certFilePath">
Cert file
</label>
<div className="flex flex-row gap-2 justify-start">
<input
key="certFilePath"
id="certFilePath"
type="file"
name="certFilePath"
className={`non-passphrase-input ${formik.values.certFilePath?.length ? 'hidden' : 'block'}`}
onChange={(e) => getFile(e.target)}
ref={certFilePathInputRef}
/>
{formik.values.certFilePath ? (
<div className="flex flex-row gap-2 items-center">
<div
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
title={path.basename(slash(formik.values.certFilePath))}
>
{path.basename(slash(formik.values.certFilePath))}
</div>
<IconTrash
size={18}
strokeWidth={1.5}
className="ml-2 cursor-pointer"
onClick={() => {
formik.setFieldValue('certFilePath', '');
certFilePathInputRef.current.value = '';
}}
/>
</div>
) : (
<></>
)}
</div>
{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>
<div className="flex flex-row gap-2">
<input
key="keyFilePath"
id="keyFilePath"
type="file"
name="keyFilePath"
className={`non-passphrase-input ${formik.values.keyFilePath?.length ? 'hidden' : 'block'}`}
onChange={(e) => getFile(e.target)}
ref={keyFilePathInputRef}
/>
{formik.values.keyFilePath ? (
<div className="flex flex-row gap-2 items-center">
<div
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
title={path.basename(slash(formik.values.keyFilePath))}
>
{path.basename(slash(formik.values.keyFilePath))}
</div>
<IconTrash
size={18}
strokeWidth={1.5}
className="ml-2 cursor-pointer"
onClick={() => {
formik.setFieldValue('keyFilePath', '');
keyFilePathInputRef.current.value = '';
}}
/>
</div>
) : (
<></>
)}
</div>
{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="pfxFilePath">
PFX file
</label>
<div className="flex flex-row gap-2">
<input
key="pfxFilePath"
id="pfxFilePath"
type="file"
name="pfxFilePath"
className={`non-passphrase-input ${formik.values.pfxFilePath?.length ? 'hidden' : 'block'}`}
onChange={(e) => getFile(e.target)}
ref={pfxFilePathInputRef}
/>
{formik.values.pfxFilePath ? (
<div className="flex flex-row gap-2 items-center">
<div
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
title={path.basename(slash(formik.values.pfxFilePath))}
>
{path.basename(slash(formik.values.pfxFilePath))}
</div>
<IconTrash
size={18}
strokeWidth={1.5}
className="ml-2 cursor-pointer"
onClick={() => {
formik.setFieldValue('pfxFilePath', '');
pfxFilePathInputRef.current.value = '';
}}
/>
</div>
) : (
<></>
)}
</div>
{formik.touched.pfxFilePath && formik.errors.pfxFilePath ? (
<div className="ml-1 text-red-500">{formik.errors.pfxFilePath}</div>
) : null}
</div>
</>
)}
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="passphrase">
Passphrase
Expand Down
34 changes: 23 additions & 11 deletions packages/bruno-cli/src/runner/run-single-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const { SocksProxyAgent } = require('socks-proxy-agent');
const { makeAxiosInstance } = require('../utils/axios-instance');
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');

const path = require('path');
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;

const runSingleRequest = async function (
Expand Down Expand Up @@ -127,18 +127,30 @@ const runSingleRequest = async function (
// client certificate config
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
for (let 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 domain = interpolateString(clientCert?.domain, interpolationOptions);
const type = clientCert?.type || 'cert';
if (domain) {
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);
if (type === 'cert') {
try {
let certFilePath = interpolateString(clientCert?.certFilePath, interpolationOptions);
certFilePath = path.isAbsolute(certFilePath) ? certFilePath : path.join(collectionPath, certFilePath);
let keyFilePath = interpolateString(clientCert?.keyFilePath, interpolationOptions);
keyFilePath = path.isAbsolute(keyFilePath) ? keyFilePath : path.join(collectionPath, keyFilePath);
httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);
httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);
} catch (err) {
console.log(chalk.red('Error reading cert/key file'), chalk.red(err?.message));
}
} else if (type === 'pfx') {
try {
let pfxFilePath = interpolateString(clientCert?.pfxFilePath, interpolationOptions);
pfxFilePath = path.isAbsolute(pfxFilePath) ? pfxFilePath : path.join(collectionPath, pfxFilePath);
httpsAgentRequestFields['pfx'] = fs.readFileSync(pfxFilePath);
} catch (err) {
console.log(chalk.red('Error reading pfx file'), chalk.red(err?.message));
}
}
httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions);
break;
Expand Down
47 changes: 26 additions & 21 deletions packages/bruno-electron/src/ipc/network/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,31 +124,36 @@ const configureRequest = async (

// client certificate config
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
for (let clientCert of clientCertConfig) {
const domain = interpolateString(clientCert.domain, interpolationOptions);

let certFilePath = interpolateString(clientCert.certFilePath, interpolationOptions);
certFilePath = path.isAbsolute(certFilePath) ? certFilePath : path.join(collectionPath, certFilePath);

let keyFilePath = interpolateString(clientCert.keyFilePath, interpolationOptions);
keyFilePath = path.isAbsolute(keyFilePath) ? keyFilePath : path.join(collectionPath, keyFilePath);

if (domain && certFilePath && keyFilePath) {
for (let clientCert of clientCertConfig) {
const domain = interpolateString(clientCert?.domain, interpolationOptions);
const type = clientCert?.type || 'cert';
if (domain) {
const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');

if (request.url.match(hostRegex)) {
try {
httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);
} catch (err) {
console.log('Error reading cert file', err);
}

try {
httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);
} catch (err) {
console.log('Error reading key file', err);
if (type === 'cert') {
try {
let certFilePath = interpolateString(clientCert?.certFilePath, interpolationOptions);
certFilePath = path.isAbsolute(certFilePath) ? certFilePath : path.join(collectionPath, certFilePath);
let keyFilePath = interpolateString(clientCert?.keyFilePath, interpolationOptions);
keyFilePath = path.isAbsolute(keyFilePath) ? keyFilePath : path.join(collectionPath, keyFilePath);

httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);
httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);
} catch (err) {
console.error('Error reading cert/key file', err);
throw new Error('Error reading cert/key file' + err);
}
} else if (type === 'pfx') {
try {
let pfxFilePath = interpolateString(clientCert?.pfxFilePath, interpolationOptions);
pfxFilePath = path.isAbsolute(pfxFilePath) ? pfxFilePath : path.join(collectionPath, pfxFilePath);
httpsAgentRequestFields['pfx'] = fs.readFileSync(pfxFilePath);
} catch (err) {
console.error('Error reading pfx file', err);
throw new Error('Error reading pfx file' + err);
}
}

httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions);
break;
}
Expand Down

0 comments on commit d751950

Please sign in to comment.