mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-30 16:14:06 +00:00
use system-ca library for ca certs
This commit is contained in:
@@ -93,7 +93,7 @@
|
||||
"@rsbuild/plugin-react": "^1.0.7",
|
||||
"@rsbuild/plugin-sass": "^1.1.0",
|
||||
"@rsbuild/plugin-styled-components": "1.1.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"system-ca": "^2.0.1",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"yargs": "^17.6.2"
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ const { createFormData } = require('../utils/form-data');
|
||||
const { getOAuth2Token } = require('./oauth2');
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
const { NtlmClient } = require('axios-ntlm');
|
||||
const { addDigestInterceptor, getCACertificates } = require('@usebruno/requests');
|
||||
const { addDigestInterceptor } = require('@usebruno/requests');
|
||||
const { getCACertificates } = require('../utils/ca-cert');
|
||||
const { encodeUrl } = require('@usebruno/common').utils;
|
||||
|
||||
const onConsoleLog = (type, args) => {
|
||||
@@ -157,8 +158,8 @@ const runSingleRequest = async function (
|
||||
} else {
|
||||
const caCertArray = [options['cacert'], process.env.SSL_CERT_FILE];
|
||||
const caCertFilePath = caCertArray.find((el) => el);
|
||||
let caCertificatesWithCertType = getCACertificates({ caCertFilePath, shouldKeepDefaultCerts: !options['ignoreTruststore'] });
|
||||
let caCertificates = caCertificatesWithCertType.map(certData => certData.certificate);
|
||||
let caCertificatesData = await getCACertificates({ caCertFilePath, shouldKeepDefaultCerts: !options['ignoreTruststore'] });
|
||||
let caCertificates = caCertificatesData.caCertificates;
|
||||
if (caCertificates?.length > 0) {
|
||||
httpsAgentRequestFields['ca'] = caCertificates;
|
||||
}
|
||||
|
||||
119
packages/bruno-cli/src/utils/ca-cert.js
Normal file
119
packages/bruno-cli/src/utils/ca-cert.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const { systemCertsAsync } = require('system-ca');
|
||||
const { rootCertificates } = require('node:tls');
|
||||
const fs = require('node:fs');
|
||||
|
||||
let systemCertsCache;
|
||||
|
||||
async function getSystemCerts(systemCAOpts = {}) {
|
||||
if (systemCertsCache) return systemCertsCache;
|
||||
|
||||
try {
|
||||
systemCertsCache = await systemCertsAsync(systemCAOpts);
|
||||
|
||||
return systemCertsCache;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function certToString(cert) {
|
||||
return typeof cert === 'string'
|
||||
? cert
|
||||
: Buffer.from(cert.buffer, cert.byteOffset, cert.byteLength).toString('utf8');
|
||||
}
|
||||
|
||||
function mergeCA(...args) {
|
||||
const ca = new Set();
|
||||
for (const item of args) {
|
||||
if (!item) continue;
|
||||
const caList = Array.isArray(item) ? item : [item];
|
||||
for (const cert of caList) {
|
||||
ca.add(certToString(cert));
|
||||
}
|
||||
}
|
||||
return [...ca].join('\n');
|
||||
}
|
||||
|
||||
function getNodeExtraCACerts() {
|
||||
const extraCACertPath = process.env.NODE_EXTRA_CA_CERTS;
|
||||
if (!extraCACertPath) return [];
|
||||
|
||||
try {
|
||||
if (fs.existsSync(extraCACertPath)) {
|
||||
const extraCACert = fs.readFileSync(extraCACertPath, 'utf8');
|
||||
if (extraCACert && extraCACert.trim()) {
|
||||
return [extraCACert];
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to read NODE_EXTRA_CA_CERTS from ${extraCACertPath}:`, err.message);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const getCACertificates = async ({ caCertFilePath, shouldKeepDefaultCerts = true }) => {
|
||||
try {
|
||||
let caCertificates = '';
|
||||
let caCertificatesCount = {
|
||||
system: 0,
|
||||
root: 0,
|
||||
custom: 0,
|
||||
extra: 0
|
||||
}
|
||||
|
||||
let systemCerts = [];
|
||||
let rootCerts = [];
|
||||
let customCerts = [];
|
||||
let nodeExtraCerts = [];
|
||||
|
||||
if (shouldKeepDefaultCerts) {
|
||||
// get system certs
|
||||
systemCerts = await getSystemCerts();
|
||||
caCertificatesCount.system = systemCerts.length;
|
||||
|
||||
// get root certs
|
||||
rootCerts = [...rootCertificates];
|
||||
caCertificatesCount.root = rootCerts.length;
|
||||
}
|
||||
|
||||
// handle user-provided custom CA certificate file with optional default certificates
|
||||
if (caCertFilePath) {
|
||||
// validate custom CA certificate file
|
||||
if (fs.existsSync(caCertFilePath)) {
|
||||
try {
|
||||
const customCert = fs.readFileSync(caCertFilePath, 'utf8');
|
||||
if (customCert && customCert.trim()) {
|
||||
customCerts.push(customCert);
|
||||
caCertificatesCount.custom = customCerts.length;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to read custom CA certificate from ${caCertFilePath}:`, (err).message);
|
||||
throw new Error(`Unable to load custom CA certificate: ${(err).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get NODE_EXTRA_CA_CERTS
|
||||
nodeExtraCerts = getNodeExtraCACerts();
|
||||
caCertificatesCount.extra = nodeExtraCerts.length;
|
||||
|
||||
// merge certs
|
||||
const mergedCerts = mergeCA(systemCerts, rootCerts, customCerts, nodeExtraCerts);
|
||||
caCertificates = mergedCerts;
|
||||
|
||||
return {
|
||||
caCertificates,
|
||||
caCertificatesCount
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error configuring CA certificates:', (err).message);
|
||||
throw err; // Re-throw certificate loading errors as they're critical
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCACertificates
|
||||
};
|
||||
@@ -67,6 +67,7 @@
|
||||
"nanoid": "3.3.8",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"system-ca": "^2.0.1",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"uuid": "^9.0.0",
|
||||
"yup": "^0.32.11"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('path');
|
||||
const { get } = require('lodash');
|
||||
const { getCACertificates } = require('@usebruno/requests');
|
||||
const { preferencesUtil } = require('../../store/preferences');
|
||||
const { getBrunoConfig } = require('../../store/bruno-config');
|
||||
const { interpolateString } = require('./interpolate-string');
|
||||
const { getCACertificates } = require('../../utils/ca-cert');
|
||||
|
||||
/**
|
||||
* Gets certificates and proxy configuration for a request
|
||||
@@ -27,22 +27,13 @@ const getCertsAndProxyConfig = async ({
|
||||
}
|
||||
|
||||
let caCertFilePath = preferencesUtil.shouldUseCustomCaCertificate() && preferencesUtil.getCustomCaCertificateFilePath();
|
||||
let caCertificatesWithCertType = getCACertificates({
|
||||
let caCertificatesData = await getCACertificates({
|
||||
caCertFilePath,
|
||||
shouldKeepDefaultCerts: preferencesUtil.shouldKeepDefaultCaCertificates()
|
||||
});
|
||||
|
||||
let caCertificates = caCertificatesWithCertType.map(certData => certData.certificate);
|
||||
let caCertificateDetails = caCertificatesWithCertType.reduce((details, certificateData) => {
|
||||
// get the count for each certificate type
|
||||
details[certificateData.type] += 1;
|
||||
return details;
|
||||
}, {
|
||||
custom: 0,
|
||||
bundled: 0,
|
||||
system: 0,
|
||||
extra: 0
|
||||
});
|
||||
let caCertificates = caCertificatesData.caCertificates;
|
||||
let caCertificateDetails = caCertificatesData.caCertificatesCount;
|
||||
|
||||
// configure HTTPS agent with aggregated CA certificates
|
||||
if (caCertificates?.length > 0) {
|
||||
|
||||
119
packages/bruno-electron/src/utils/ca-cert.js
Normal file
119
packages/bruno-electron/src/utils/ca-cert.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const { systemCertsAsync } = require('system-ca');
|
||||
const { rootCertificates } = require('node:tls');
|
||||
const fs = require('node:fs');
|
||||
|
||||
let systemCertsCache;
|
||||
|
||||
async function getSystemCerts(systemCAOpts = {}) {
|
||||
if (systemCertsCache) return systemCertsCache;
|
||||
|
||||
try {
|
||||
systemCertsCache = await systemCertsAsync(systemCAOpts);
|
||||
|
||||
return systemCertsCache;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function certToString(cert) {
|
||||
return typeof cert === 'string'
|
||||
? cert
|
||||
: Buffer.from(cert.buffer, cert.byteOffset, cert.byteLength).toString('utf8');
|
||||
}
|
||||
|
||||
function mergeCA(...args) {
|
||||
const ca = new Set();
|
||||
for (const item of args) {
|
||||
if (!item) continue;
|
||||
const caList = Array.isArray(item) ? item : [item];
|
||||
for (const cert of caList) {
|
||||
ca.add(certToString(cert));
|
||||
}
|
||||
}
|
||||
return [...ca].join('\n');
|
||||
}
|
||||
|
||||
function getNodeExtraCACerts() {
|
||||
const extraCACertPath = process.env.NODE_EXTRA_CA_CERTS;
|
||||
if (!extraCACertPath) return [];
|
||||
|
||||
try {
|
||||
if (fs.existsSync(extraCACertPath)) {
|
||||
const extraCACert = fs.readFileSync(extraCACertPath, 'utf8');
|
||||
if (extraCACert && extraCACert.trim()) {
|
||||
return [extraCACert];
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to read NODE_EXTRA_CA_CERTS from ${extraCACertPath}:`, err.message);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const getCACertificates = async ({ caCertFilePath, shouldKeepDefaultCerts = true }) => {
|
||||
try {
|
||||
let caCertificates = '';
|
||||
let caCertificatesCount = {
|
||||
system: 0,
|
||||
root: 0,
|
||||
custom: 0,
|
||||
extra: 0
|
||||
}
|
||||
|
||||
let systemCerts = [];
|
||||
let rootCerts = [];
|
||||
let customCerts = [];
|
||||
let nodeExtraCerts = [];
|
||||
|
||||
if (shouldKeepDefaultCerts) {
|
||||
// get system certs
|
||||
systemCerts = await getSystemCerts();
|
||||
caCertificatesCount.system = systemCerts.length;
|
||||
|
||||
// get root certs
|
||||
rootCerts = [...rootCertificates];
|
||||
caCertificatesCount.root = rootCerts.length;
|
||||
}
|
||||
|
||||
// handle user-provided custom CA certificate file with optional default certificates
|
||||
if (caCertFilePath) {
|
||||
// validate custom CA certificate file
|
||||
if (fs.existsSync(caCertFilePath)) {
|
||||
try {
|
||||
const customCert = fs.readFileSync(caCertFilePath, 'utf8');
|
||||
if (customCert && customCert.trim()) {
|
||||
customCerts.push(customCert);
|
||||
caCertificatesCount.custom = customCerts.length;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to read custom CA certificate from ${caCertFilePath}:`, (err).message);
|
||||
throw new Error(`Unable to load custom CA certificate: ${(err).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get NODE_EXTRA_CA_CERTS
|
||||
nodeExtraCerts = getNodeExtraCACerts();
|
||||
caCertificatesCount.extra = nodeExtraCerts.length;
|
||||
|
||||
// merge certs
|
||||
const mergedCerts = mergeCA(systemCerts, rootCerts, customCerts, nodeExtraCerts);
|
||||
caCertificates = mergedCerts;
|
||||
|
||||
return {
|
||||
caCertificates,
|
||||
caCertificatesCount
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error configuring CA certificates:', (err).message);
|
||||
throw err; // Re-throw certificate loading errors as they're critical
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCACertificates
|
||||
};
|
||||
@@ -152,7 +152,7 @@ function createTimelineAgentClass(BaseAgentClass) {
|
||||
});
|
||||
}
|
||||
|
||||
const bundledCerts = this.caCertificateDetails.bundled || 0;
|
||||
const rootCerts = this.caCertificateDetails.root || 0;
|
||||
const systemCerts = this.caCertificateDetails.system || 0;
|
||||
const extraCerts = this.caCertificateDetails.extra || 0;
|
||||
const customCerts = this.caCertificateDetails.custom || 0;
|
||||
@@ -160,7 +160,7 @@ function createTimelineAgentClass(BaseAgentClass) {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'tls',
|
||||
message: `CA Certificates: ${bundledCerts} bundled, ${systemCerts} system, ${extraCerts} extra, ${customCerts} custom`,
|
||||
message: `CA Certificates: ${rootCerts} root, ${systemCerts} system, ${extraCerts} extra, ${customCerts} custom`,
|
||||
});
|
||||
|
||||
// Log "Trying host:port..."
|
||||
|
||||
@@ -2,6 +2,4 @@ export { addDigestInterceptor, getOAuth2Token } from './auth';
|
||||
export { GrpcClient, generateGrpcSampleMessage } from './grpc';
|
||||
export { default as cookies } from './cookies';
|
||||
|
||||
export { getCACertificates } from './network';
|
||||
|
||||
export * as scripting from './scripting';
|
||||
@@ -1,165 +0,0 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
type T_CACertSource = 'bundled' | 'system' | 'extra'
|
||||
|
||||
type T_CACertificateData = {
|
||||
type: T_CACertSource | 'custom';
|
||||
certificate: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely executes tls.getCACertificates in a separate Node.js process
|
||||
* Returns empty array if the process fails or exits
|
||||
*/
|
||||
const safeTlsGetCACertificates = (certType: T_CACertSource): string[] => {
|
||||
try {
|
||||
|
||||
// adding seperate script for each cert type
|
||||
// to make sure no unexpected code can be included in the script
|
||||
|
||||
const getBundledCACertificatesScript = `
|
||||
const tls = require('node:tls');
|
||||
try {
|
||||
const result = tls.getCACertificates('bundled');
|
||||
console.log(JSON.stringify(result || []));
|
||||
} catch (error) {
|
||||
console.log('[]');
|
||||
}
|
||||
`;
|
||||
|
||||
const getSystemCACertificatesScript = `
|
||||
const tls = require('node:tls');
|
||||
try {
|
||||
const result = tls.getCACertificates('system');
|
||||
console.log(JSON.stringify(result || []));
|
||||
} catch (error) {
|
||||
console.log('[]');
|
||||
}
|
||||
`;
|
||||
|
||||
const getExtraCACertificatesScript = `
|
||||
const tls = require('node:tls');
|
||||
try {
|
||||
const result = tls.getCACertificates('extra');
|
||||
console.log(JSON.stringify(result || []));
|
||||
} catch (error) {
|
||||
console.log('[]');
|
||||
}
|
||||
`;
|
||||
|
||||
// bundled
|
||||
let script = getBundledCACertificatesScript;
|
||||
|
||||
// system
|
||||
if (certType === 'system') script = getSystemCACertificatesScript;
|
||||
|
||||
// extra
|
||||
if (certType === 'extra') script = getExtraCACertificatesScript;
|
||||
|
||||
const result = spawnSync('node', ['-e', script], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000, // 5 second timeout
|
||||
stdio: 'pipe',
|
||||
maxBuffer: 1024 * 1024 * 50
|
||||
});
|
||||
|
||||
if (result.error || result.status !== 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const output = result.stdout.trim();
|
||||
|
||||
return JSON.parse(output);
|
||||
} catch (error) {
|
||||
// Return empty array if child process fails
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* retrieves default CA certificates from multiple sources using Node.js TLS API
|
||||
*
|
||||
* this function aggregates CA certificates from three sources:
|
||||
* - 'bundled': mozilla CA certificates bundled with Node.js (same as tls.rootCertificates)
|
||||
* - 'system': CA certificates from the system's trusted certificate store
|
||||
* - 'extra': additional CA certificates loaded from `NODE_EXTRA_CA_CERTS` environment variable
|
||||
*
|
||||
* @returns {string[]} Array of PEM-encoded CA certificate strings
|
||||
* @see https://nodejs.org/docs/latest-v22.x/api/tls.html#tlsgetcacertificatestype
|
||||
*/
|
||||
const getCerts = (sources: T_CACertSource[] = ['bundled', 'system', 'extra']): T_CACertificateData[] => {
|
||||
let certificates: T_CACertificateData[] = [];
|
||||
|
||||
// iterate through different certificate store types to build comprehensive CA list
|
||||
(sources).forEach(certType => {
|
||||
try {
|
||||
// get certificates from specific store type
|
||||
const certList = safeTlsGetCACertificates(certType);
|
||||
|
||||
if (certList && Array.isArray(certList)) {
|
||||
// filter out empty/invalid certificates to ensure we only include valid data
|
||||
const validCertificates = certList.filter(cert => cert && cert.trim());
|
||||
const validCertificatesWithCertType = validCertificates.map(certificate => ({
|
||||
type: certType,
|
||||
certificate
|
||||
}));
|
||||
certificates.push(...validCertificatesWithCertType);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to load ${certType} CA certificates:`, (err as Error).message);
|
||||
}
|
||||
});
|
||||
|
||||
return certificates;
|
||||
};
|
||||
|
||||
const getCACertificates = ({ caCertFilePath, shouldKeepDefaultCerts = true }: { caCertFilePath: string, shouldKeepDefaultCerts: boolean }) : T_CACertificateData[] => {
|
||||
// CA certificate configuration
|
||||
try {
|
||||
let caCertificates: T_CACertificateData[] = [];
|
||||
|
||||
// handle user-provided custom CA certificate file with optional default certificates
|
||||
if (caCertFilePath) {
|
||||
|
||||
// validate custom CA certificate file
|
||||
if (fs.existsSync(caCertFilePath)) {
|
||||
try {
|
||||
const customCert = fs.readFileSync(caCertFilePath, 'utf8');
|
||||
if (customCert && customCert.trim()) {
|
||||
caCertificates.push({
|
||||
type: 'custom',
|
||||
certificate: customCert.trim()
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to read custom CA certificate from ${caCertFilePath}:`, (err as Error).message);
|
||||
throw new Error(`Unable to load custom CA certificate: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// optionally augment custom CA with default certificates
|
||||
if (shouldKeepDefaultCerts) {
|
||||
const defaultCertificates = getCerts(['bundled', 'system', 'extra']);
|
||||
if (defaultCertificates?.length > 0) {
|
||||
caCertificates.push(...defaultCertificates);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// use default CA certificates when no custom configuration is specified
|
||||
const defaultCertificates = getCerts(['bundled', 'system', 'extra']);
|
||||
if (defaultCertificates?.length > 0) {
|
||||
caCertificates.push(...defaultCertificates);
|
||||
}
|
||||
}
|
||||
|
||||
return caCertificates;
|
||||
} catch (err) {
|
||||
console.error('Error configuring CA certificates:', (err as Error).message);
|
||||
throw err; // Re-throw certificate loading errors as they're critical
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getCACertificates
|
||||
};
|
||||
@@ -1,3 +1 @@
|
||||
export { makeAxiosInstance } from './axios-instance';
|
||||
|
||||
export { getCACertificates } from './ca-cert';
|
||||
export { makeAxiosInstance } from './axios-instance';
|
||||
Reference in New Issue
Block a user