mirror of
https://github.com/voideditor/void
synced 2026-05-22 17:08:25 +00:00
certs
This commit is contained in:
parent
b35dfdf475
commit
2f80c653d5
5 changed files with 348 additions and 4 deletions
|
|
@ -2856,16 +2856,13 @@ export const SidebarChat = () => {
|
|||
|
||||
// resolve mount info
|
||||
const isResolved = chatThreadsState.allThreads[threadId]?.state.mountedInfo?.mountedIsResolvedRef.current
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (isResolved) return
|
||||
chatThreadsState.allThreads[threadId]?.state.mountedInfo?._whenMountedResolver?.({
|
||||
textAreaRef: textAreaRef,
|
||||
scrollToBottom: () => scrollToBottom(scrollContainerRef),
|
||||
})
|
||||
|
||||
// Trigger a window resize event to ensure proper layout calculations
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
}, [chatThreadsState, threadId, textAreaRef, scrollContainerRef, isResolved])
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS
|
|||
import { ILLMMessageService } from '../../../../common/sendLLMMessageService.js';
|
||||
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
|
||||
import { IVoidCertificateService } from '../../../../common/voidCertificateService.js';
|
||||
import { IExtensionTransferService } from '../../../../../../../workbench/contrib/void/browser/extensionTransferService.js'
|
||||
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
|
||||
|
|
@ -185,6 +186,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
|||
IVoidSettingsService: accessor.get(IVoidSettingsService),
|
||||
IEditCodeService: accessor.get(IEditCodeService),
|
||||
IChatThreadService: accessor.get(IChatThreadService),
|
||||
IVoidCertificateService: accessor.get(IVoidCertificateService),
|
||||
|
||||
IInstantiationService: accessor.get(IInstantiationService),
|
||||
ICodeEditorService: accessor.get(ICodeEditorService),
|
||||
|
|
|
|||
|
|
@ -904,6 +904,127 @@ export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }:
|
|||
|
||||
// full settings
|
||||
|
||||
// Certificate Manager Component
|
||||
const CertificateManager = () => {
|
||||
const accessor = useAccessor();
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService');
|
||||
const certificateService = accessor.get('IVoidCertificateService');
|
||||
const nativeHostService = accessor.get('INativeHostService');
|
||||
const notificationService = accessor.get('INotificationService');
|
||||
const settingsState = useSettingsState();
|
||||
|
||||
const [certificatePath, setCertificatePath] = useState('');
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
|
||||
// Get the list of certificates from state as URIs
|
||||
const certificates: URI[] = useMemo(() => {
|
||||
return (settingsState.globalSettings.customRootCertificates || []).map(path => URI.parse(path));
|
||||
}, [settingsState.globalSettings.customRootCertificates]);
|
||||
|
||||
const handleAddCertificate = async () => {
|
||||
if (certificatePath) {
|
||||
setIsVerifying(true);
|
||||
try {
|
||||
const certificateUri = URI.file(certificatePath);
|
||||
|
||||
// Check if certificate already exists
|
||||
if (certificates.some(cert => cert.toString() === certificateUri.toString())) {
|
||||
notificationService.info('Certificate already added');
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify certificate is valid
|
||||
const isValid = await certificateService.verifyCertificatePath(certificateUri);
|
||||
if (!isValid) {
|
||||
notificationService.error(`The certificate file could not be read or is not a valid certificate.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add certificate
|
||||
await certificateService.addCustomCertificate(certificateUri);
|
||||
setCertificatePath('');
|
||||
notificationService.info(`Certificate added successfully`);
|
||||
} catch (error) {
|
||||
notificationService.error(`Failed to add certificate: ${error}`);
|
||||
} finally {
|
||||
setIsVerifying(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveCertificate = async (certificateUri: URI) => {
|
||||
try {
|
||||
await certificateService.removeCustomCertificate(certificateUri);
|
||||
notificationService.info(`Certificate removed`);
|
||||
} catch (error) {
|
||||
notificationService.error(`Failed to remove certificate: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBrowse = async () => {
|
||||
try {
|
||||
const result = await nativeHostService.showOpenDialog({
|
||||
properties: ['openFile'], // Use properties array instead of canSelectFiles, canSelectFolders, canSelectMany
|
||||
filters: [
|
||||
{ name: 'Certificates', extensions: ['pem', 'crt', 'cert', 'cer'] }
|
||||
],
|
||||
title: 'Select Root Certificate'
|
||||
});
|
||||
|
||||
if (result && result.filePaths && result.filePaths.length > 0) {
|
||||
setCertificatePath(result.filePaths[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
notificationService.error(`Failed to browse for certificate: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<VoidSimpleInputBox
|
||||
value={certificatePath}
|
||||
onChangeValue={setCertificatePath}
|
||||
placeholder="Path to certificate (.pem, .crt, .cert, .cer)"
|
||||
compact={true}
|
||||
className="flex-grow"
|
||||
/>
|
||||
<VoidButtonBgDarken onClick={handleBrowse} className="px-3 py-1">
|
||||
Browse
|
||||
</VoidButtonBgDarken>
|
||||
<AddButton
|
||||
disabled={!certificatePath || isVerifying}
|
||||
onClick={handleAddCertificate}
|
||||
text={isVerifying ? <span className="flex items-center gap-1">Verifying <Loader2 className="size-3 animate-spin" /></span> : "Add"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{certificates.length > 0 ? (
|
||||
<div className="mt-2">
|
||||
<h4 className="text-void-fg-3 text-sm mb-1">Added Certificates:</h4>
|
||||
<div className="flex flex-col gap-1">
|
||||
{certificates.map((certUri, index) => (
|
||||
<div key={certUri.toString()} className="flex justify-between items-center py-1 px-2 bg-void-bg-2 rounded">
|
||||
<span className="text-sm truncate max-w-md" title={certUri.fsPath}>{certUri.fsPath}</span>
|
||||
<button
|
||||
onClick={() => handleRemoveCertificate(certUri)}
|
||||
className="text-void-fg-3 hover:text-void-fg-1"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-void-fg-3 text-sm italic mt-1">
|
||||
No custom certificates added yet
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Settings = () => {
|
||||
const isDark = useIsDark()
|
||||
const accessor = useAccessor()
|
||||
|
|
@ -1164,6 +1285,26 @@ export const Settings = () => {
|
|||
|
||||
|
||||
{/* General section (formerly GeneralTab) */}
|
||||
{/* Network Settings with Root Certificates */}
|
||||
<div className='mt-12'>
|
||||
<ErrorBoundary>
|
||||
<h2 className='text-3xl mb-2'>Network Settings</h2>
|
||||
<h4 className='text-void-fg-3 mb-4'>{`Configure network and certificate settings for API requests.`}</h4>
|
||||
|
||||
<div className='mb-4'>
|
||||
<h3 className='text-base mb-2'>Root Certificates</h3>
|
||||
<div className='text-sm italic text-void-fg-3 mb-2'>
|
||||
<p>Add custom root certificates for HTTPS requests to fix certificate validation errors like "unable to get local issuer certificate".</p>
|
||||
<p className="mt-1">This is useful when using a corporate proxy, gateway, or working behind a firewall that performs SSL inspection.</p>
|
||||
</div>
|
||||
|
||||
<div className='max-w-xl'>
|
||||
<CertificateManager />
|
||||
</div>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
<div className='mt-12'>
|
||||
<ErrorBoundary>
|
||||
<h2 className='text-3xl mb-2 mt-12'>One-Click Switch</h2>
|
||||
|
|
|
|||
202
src/vs/workbench/contrib/void/common/voidCertificateService.ts
Normal file
202
src/vs/workbench/contrib/void/common/voidCertificateService.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { ILogService } from '../../../../platform/log/common/log.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
import { IRequestService } from '../../../../platform/request/common/request.js';
|
||||
import { Emitter } from '../../../../base/common/event.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { IRequestContext } from '../../../../base/parts/request/common/request.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
|
||||
/**
|
||||
* Service for managing and applying custom certificate paths for secure requests.
|
||||
*/
|
||||
export interface IVoidCertificateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/**
|
||||
* Get all configured custom certificates as an array of file URIs.
|
||||
*/
|
||||
getCustomCertificates(): URI[];
|
||||
|
||||
/**
|
||||
* Add a new custom certificate path.
|
||||
*/
|
||||
addCustomCertificate(certificatePath: URI): Promise<void>;
|
||||
|
||||
/**
|
||||
* Remove a custom certificate path.
|
||||
*/
|
||||
removeCustomCertificate(certificatePath: URI): Promise<void>;
|
||||
|
||||
/**
|
||||
* Verify that a certificate path exists and is readable.
|
||||
*/
|
||||
verifyCertificatePath(certificatePath: URI): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get all certificate contents as a concatenated string for use with HTTPS requests.
|
||||
*/
|
||||
getCertificateContents(): Promise<string[]>;
|
||||
}
|
||||
|
||||
export const IVoidCertificateService = createDecorator<IVoidCertificateService>('VoidCertificateService');
|
||||
|
||||
/**
|
||||
* Service implementation for managing custom certificates.
|
||||
*/
|
||||
export class VoidCertificateService extends Disposable implements IVoidCertificateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _onCertificatesChanged = new Emitter<void>();
|
||||
|
||||
constructor(
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IRequestService private readonly requestService: IRequestService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
) {
|
||||
super();
|
||||
|
||||
// Override the original request service to include our custom certificates
|
||||
this._overrideRequestService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all configured custom certificates
|
||||
*/
|
||||
getCustomCertificates(): URI[] {
|
||||
const certificatePaths = this.voidSettingsService.state.globalSettings.customRootCertificates || [];
|
||||
return certificatePaths.map(path => URI.parse(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new custom certificate path
|
||||
*/
|
||||
async addCustomCertificate(certificatePath: URI): Promise<void> {
|
||||
// Verify the certificate path exists
|
||||
const isValid = await this.verifyCertificatePath(certificatePath);
|
||||
if (!isValid) {
|
||||
throw new Error(`Certificate file not found or not readable: ${certificatePath.toString()}`);
|
||||
}
|
||||
|
||||
// Get current certificates
|
||||
const currentCertificates = this.getCustomCertificates();
|
||||
|
||||
// Check if already exists
|
||||
if (currentCertificates.some(cert => cert.toString() === certificatePath.toString())) {
|
||||
return; // Already exists, nothing to do
|
||||
}
|
||||
|
||||
// Add the new certificate path
|
||||
const newCertificates = [...currentCertificates, certificatePath];
|
||||
await this.voidSettingsService.setGlobalSetting('customRootCertificates', newCertificates.map(uri => uri.toString()));
|
||||
|
||||
this._onCertificatesChanged.fire();
|
||||
this.logService.info(`Added custom certificate: ${certificatePath.toString()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a custom certificate path
|
||||
*/
|
||||
async removeCustomCertificate(certificatePath: URI): Promise<void> {
|
||||
const currentCertificates = this.getCustomCertificates();
|
||||
|
||||
// Remove the certificate
|
||||
const newCertificates = currentCertificates.filter(cert => cert.toString() !== certificatePath.toString());
|
||||
|
||||
// Update settings
|
||||
await this.voidSettingsService.setGlobalSetting('customRootCertificates', newCertificates.map(uri => uri.toString()));
|
||||
|
||||
this._onCertificatesChanged.fire();
|
||||
this.logService.info(`Removed custom certificate: ${certificatePath.toString()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a certificate path exists and is readable
|
||||
*/
|
||||
async verifyCertificatePath(certificatePath: URI): Promise<boolean> {
|
||||
try {
|
||||
// Check if file exists and is readable
|
||||
const stats = await this.fileService.stat(certificatePath);
|
||||
if (!stats.isFile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we can read the file
|
||||
await this.fileService.readFile(certificatePath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logService.error(`Error verifying certificate path: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all certificate contents
|
||||
*/
|
||||
async getCertificateContents(): Promise<string[]> {
|
||||
const certificatePaths = this.getCustomCertificates();
|
||||
const contents: string[] = [];
|
||||
|
||||
for (const certPath of certificatePaths) {
|
||||
try {
|
||||
if (await this.verifyCertificatePath(certPath)) {
|
||||
const fileContent = await this.fileService.readFile(certPath);
|
||||
contents.push(fileContent.value.toString());
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(`Error reading certificate ${certPath.toString()}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the original request service to include our custom certificates
|
||||
*/
|
||||
private _overrideRequestService(): void {
|
||||
// Store the original request method
|
||||
const originalRequest = this.requestService.request.bind(this.requestService);
|
||||
|
||||
// Override the request method to inject our certificates
|
||||
// @ts-ignore - We're monkey patching the request service
|
||||
this.requestService.request = async (options: any, token: CancellationToken): Promise<IRequestContext> => {
|
||||
// Only add certificates for HTTPS requests
|
||||
if (options.url?.startsWith('https://')) {
|
||||
try {
|
||||
// Load system certificates
|
||||
const systemCerts = await this.requestService.loadCertificates();
|
||||
|
||||
// Load our custom certificates
|
||||
const customCertContents = await this.getCertificateContents();
|
||||
|
||||
if (customCertContents.length > 0) {
|
||||
// Create custom CA option
|
||||
// Documentation in Node.js: https://nodejs.org/api/https.html#https_https_request_options_callback
|
||||
// The 'ca' option can be a string, Buffer, or array of strings/Buffers
|
||||
(options as any).ca = [...systemCerts, ...customCertContents];
|
||||
|
||||
this.logService.debug(`Added ${customCertContents.length} custom certificates to request to ${options.url}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(`Error adding custom certificates: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Forward to the original request implementation
|
||||
return originalRequest(options, token);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Register the service
|
||||
registerSingleton(IVoidCertificateService, VoidCertificateService, InstantiationType.Eager);
|
||||
|
|
@ -434,6 +434,7 @@ export type GlobalSettings = {
|
|||
showInlineSuggestions: boolean;
|
||||
includeToolLintErrors: boolean;
|
||||
isOnboardingComplete: boolean;
|
||||
customRootCertificates: string[]; // Paths to custom root certificates
|
||||
}
|
||||
|
||||
export const defaultGlobalSettings: GlobalSettings = {
|
||||
|
|
@ -447,6 +448,7 @@ export const defaultGlobalSettings: GlobalSettings = {
|
|||
showInlineSuggestions: true,
|
||||
includeToolLintErrors: true,
|
||||
isOnboardingComplete: false,
|
||||
customRootCertificates: [],
|
||||
}
|
||||
|
||||
export type GlobalSettingName = keyof GlobalSettings
|
||||
|
|
|
|||
Loading…
Reference in a new issue