From 2f80c653d5d102a40ac3420256bb6de4728af7a4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 13 May 2025 19:52:26 -0700 Subject: [PATCH] certs --- .../react/src/sidebar-tsx/SidebarChat.tsx | 5 +- .../void/browser/react/src/util/services.tsx | 2 + .../react/src/void-settings-tsx/Settings.tsx | 141 ++++++++++++ .../void/common/voidCertificateService.ts | 202 ++++++++++++++++++ .../contrib/void/common/voidSettingsTypes.ts | 2 + 5 files changed, 348 insertions(+), 4 deletions(-) create mode 100644 src/vs/workbench/contrib/void/common/voidCertificateService.ts diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 3587438b..d3a2fb01 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -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]) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 82f47954..990340ed 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -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), diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 9fa3e7cb..118afb19 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -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 ( +
+
+ + + Browse + + Verifying : "Add"} + /> +
+ + {certificates.length > 0 ? ( +
+

Added Certificates:

+
+ {certificates.map((certUri, index) => ( +
+ {certUri.fsPath} + +
+ ))} +
+
+ ) : ( +
+ No custom certificates added yet +
+ )} +
+ ); +}; + 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 */} +
+ +

Network Settings

+

{`Configure network and certificate settings for API requests.`}

+ +
+

Root Certificates

+
+

Add custom root certificates for HTTPS requests to fix certificate validation errors like "unable to get local issuer certificate".

+

This is useful when using a corporate proxy, gateway, or working behind a firewall that performs SSL inspection.

+
+ +
+ +
+
+
+
+

One-Click Switch

diff --git a/src/vs/workbench/contrib/void/common/voidCertificateService.ts b/src/vs/workbench/contrib/void/common/voidCertificateService.ts new file mode 100644 index 00000000..b00dd268 --- /dev/null +++ b/src/vs/workbench/contrib/void/common/voidCertificateService.ts @@ -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; + + /** + * Remove a custom certificate path. + */ + removeCustomCertificate(certificatePath: URI): Promise; + + /** + * Verify that a certificate path exists and is readable. + */ + verifyCertificatePath(certificatePath: URI): Promise; + + /** + * Get all certificate contents as a concatenated string for use with HTTPS requests. + */ + getCertificateContents(): Promise; +} + +export const IVoidCertificateService = createDecorator('VoidCertificateService'); + +/** + * Service implementation for managing custom certificates. + */ +export class VoidCertificateService extends Disposable implements IVoidCertificateService { + readonly _serviceBrand: undefined; + + private readonly _onCertificatesChanged = new Emitter(); + + 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 { + // 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 { + 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 { + 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 { + 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 => { + // 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); \ No newline at end of file diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index a911dfe6..c43319ad 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -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