From 8ca507c01c8d905da830837761a5f6299ebad16c Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 17 Jul 2025 23:10:21 +0700 Subject: [PATCH] feat: proxy support for the new downloader (#5795) * feat: proxy support for the new downloader * test: remove outdated test * ci: clean up --- .github/workflows/jan-linter-and-test.yml | 12 - extensions/download-extension/src/index.ts | 4 +- extensions/llamacpp-extension/src/backend.ts | 100 ++-- extensions/llamacpp-extension/src/index.ts | 8 +- .../llamacpp-extension/src/util.test.ts | 546 ++++++++++++++++++ extensions/llamacpp-extension/src/util.ts | 68 +++ src-tauri/Cargo.toml | 1 + src-tauri/src/core/utils/download.rs | 462 ++++++++++++++- web-app/src/routes/settings/https-proxy.tsx | 57 +- web-app/src/services/__tests__/models.test.ts | 28 - web-app/src/services/models.ts | 30 - 11 files changed, 1145 insertions(+), 171 deletions(-) create mode 100644 extensions/llamacpp-extension/src/util.test.ts create mode 100644 extensions/llamacpp-extension/src/util.ts diff --git a/.github/workflows/jan-linter-and-test.yml b/.github/workflows/jan-linter-and-test.yml index 2aa871fb7..97405cc6a 100644 --- a/.github/workflows/jan-linter-and-test.yml +++ b/.github/workflows/jan-linter-and-test.yml @@ -114,10 +114,6 @@ jobs: with: node-version: 20 - - name: Install tauri-driver dependencies - run: | - cargo install tauri-driver --locked - # Clean cache, continue on error - name: 'Cleanup cache' shell: powershell @@ -154,10 +150,6 @@ jobs: with: node-version: 20 - - name: Install tauri-driver dependencies - run: | - cargo install tauri-driver --locked - - name: 'Cleanup cache' shell: powershell continue-on-error: true @@ -202,10 +194,6 @@ jobs: sudo apt update sudo apt install -y libglib2.0-dev libatk1.0-dev libpango1.0-dev libgtk-3-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev librsvg2-dev libfuse2 webkit2gtk-driver - - name: Install tauri-driver dependencies - run: | - cargo install tauri-driver --locked - - name: 'Cleanup cache' continue-on-error: true run: | diff --git a/extensions/download-extension/src/index.ts b/extensions/download-extension/src/index.ts index be193ef6c..04c34cd6c 100644 --- a/extensions/download-extension/src/index.ts +++ b/extensions/download-extension/src/index.ts @@ -9,6 +9,7 @@ export enum Settings { interface DownloadItem { url: string save_path: string + proxy?: Record } type DownloadEvent = { @@ -30,10 +31,11 @@ export default class DownloadManager extends BaseExtension { url: string, savePath: string, taskId: string, + proxyConfig: Record = {}, onProgress?: (transferred: number, total: number) => void ) { return await this.downloadFiles( - [{ url, save_path: savePath }], + [{ url, save_path: savePath, proxy: proxyConfig }], taskId, onProgress ) diff --git a/extensions/llamacpp-extension/src/backend.ts b/extensions/llamacpp-extension/src/backend.ts index 630d068b4..e710750e7 100644 --- a/extensions/llamacpp-extension/src/backend.ts +++ b/extensions/llamacpp-extension/src/backend.ts @@ -1,16 +1,14 @@ -import { - getJanDataFolderPath, - fs, - joinPath, - events, -} from '@janhq/core' +import { getJanDataFolderPath, fs, joinPath, events } from '@janhq/core' import { invoke } from '@tauri-apps/api/core' +import { getProxyConfig } from './util' // folder structure // /llamacpp/backends// // what should be available to the user for selection? -export async function listSupportedBackends(): Promise<{ version: string, backend: string }[]> { +export async function listSupportedBackends(): Promise< + { version: string; backend: string }[] +> { const sysInfo = await window.core.api.getSystemInfo() const os_type = sysInfo.os_type const arch = sysInfo.cpu.arch @@ -35,8 +33,7 @@ export async function listSupportedBackends(): Promise<{ version: string, backen // not available yet, placeholder for future else if (sysType == 'windows-aarch64') { supportedBackends.push('win-arm64') - } - else if (sysType == 'linux-x86_64') { + } else if (sysType == 'linux-x86_64') { supportedBackends.push('linux-noavx-x64') if (features.avx) supportedBackends.push('linux-avx-x64') if (features.avx2) supportedBackends.push('linux-avx2-x64') @@ -48,11 +45,9 @@ export async function listSupportedBackends(): Promise<{ version: string, backen // not available yet, placeholder for future else if (sysType === 'linux-aarch64') { supportedBackends.push('linux-arm64') - } - else if (sysType === 'macos-x86_64') { + } else if (sysType === 'macos-x86_64') { supportedBackends.push('macos-x64') - } - else if (sysType === 'macos-aarch64') { + } else if (sysType === 'macos-aarch64') { supportedBackends.push('macos-arm64') } @@ -82,39 +77,64 @@ export async function listSupportedBackends(): Promise<{ version: string, backen return backendVersions } -export async function getBackendDir(backend: string, version: string): Promise { +export async function getBackendDir( + backend: string, + version: string +): Promise { const janDataFolderPath = await getJanDataFolderPath() - const backendDir = await joinPath([janDataFolderPath, 'llamacpp', 'backends', version, backend]) + const backendDir = await joinPath([ + janDataFolderPath, + 'llamacpp', + 'backends', + version, + backend, + ]) return backendDir } -export async function getBackendExePath(backend: string, version: string): Promise { +export async function getBackendExePath( + backend: string, + version: string +): Promise { const sysInfo = await window.core.api.getSystemInfo() - const exe_name = sysInfo.os_type === 'windows' ? 'llama-server.exe' : 'llama-server' + const exe_name = + sysInfo.os_type === 'windows' ? 'llama-server.exe' : 'llama-server' const backendDir = await getBackendDir(backend, version) const exePath = await joinPath([backendDir, 'build', 'bin', exe_name]) return exePath } -export async function isBackendInstalled(backend: string, version: string): Promise { +export async function isBackendInstalled( + backend: string, + version: string +): Promise { const exePath = await getBackendExePath(backend, version) const result = await fs.existsSync(exePath) return result } -export async function downloadBackend(backend: string, version: string): Promise { +export async function downloadBackend( + backend: string, + version: string +): Promise { const janDataFolderPath = await getJanDataFolderPath() const llamacppPath = await joinPath([janDataFolderPath, 'llamacpp']) const backendDir = await getBackendDir(backend, version) const libDir = await joinPath([llamacppPath, 'lib']) - const downloadManager = window.core.extensionManager.getByName('@janhq/download-extension') + const downloadManager = window.core.extensionManager.getByName( + '@janhq/download-extension' + ) + + // Get proxy configuration from localStorage + const proxyConfig = getProxyConfig() const downloadItems = [ { url: `https://github.com/menloresearch/llama.cpp/releases/download/${version}/llama-${version}-bin-${backend}.tar.gz`, save_path: await joinPath([backendDir, 'backend.tar.gz']), - } + proxy: proxyConfig, + }, ] // also download CUDA runtime + cuBLAS + cuBLASLt if needed @@ -122,18 +142,24 @@ export async function downloadBackend(backend: string, version: string): Promise downloadItems.push({ url: `https://github.com/menloresearch/llama.cpp/releases/download/${version}/cudart-llama-bin-linux-cu11.7-x64.tar.gz`, save_path: await joinPath([libDir, 'cuda11.tar.gz']), + proxy: proxyConfig, }) } else if (backend.includes('cu12.0') && !(await _isCudaInstalled('12.0'))) { downloadItems.push({ url: `https://github.com/menloresearch/llama.cpp/releases/download/${version}/cudart-llama-bin-linux-cu12.0-x64.tar.gz`, save_path: await joinPath([libDir, 'cuda12.tar.gz']), + proxy: proxyConfig, }) } const taskId = `llamacpp-${version}-${backend}`.replace(/\./g, '-') const downloadType = 'Engine' - console.log(`Downloading backend ${backend} version ${version}: ${JSON.stringify(downloadItems)}`) + console.log( + `Downloading backend ${backend} version ${version}: ${JSON.stringify( + downloadItems + )}` + ) let downloadCompleted = false try { const onProgress = (transferred: number, total: number) => { @@ -212,13 +238,15 @@ async function _getSupportedFeatures() { async function _fetchGithubReleases( owner: string, - repo: string, + repo: string ): Promise { // by default, it's per_page=30 and page=1 -> the latest 30 releases const url = `https://api.github.com/repos/${owner}/${repo}/releases` const response = await fetch(url) if (!response.ok) { - throw new Error(`Failed to fetch releases from ${url}: ${response.statusText}`) + throw new Error( + `Failed to fetch releases from ${url}: ${response.statusText}` + ) } return response.json() } @@ -257,21 +285,25 @@ async function _isCudaInstalled(version: string): Promise { // check for libraries shipped with Jan's llama.cpp extension const janDataFolderPath = await getJanDataFolderPath() - const cudartPath = await joinPath([janDataFolderPath, 'llamacpp', 'lib', libname]) + const cudartPath = await joinPath([ + janDataFolderPath, + 'llamacpp', + 'lib', + libname, + ]) return await fs.existsSync(cudartPath) } function compareVersions(a: string, b: string): number { - const aParts = a.split('.').map(Number); - const bParts = b.split('.').map(Number); - const len = Math.max(aParts.length, bParts.length); + const aParts = a.split('.').map(Number) + const bParts = b.split('.').map(Number) + const len = Math.max(aParts.length, bParts.length) for (let i = 0; i < len; i++) { - const x = aParts[i] || 0; - const y = bParts[i] || 0; - if (x > y) return 1; - if (x < y) return -1; + const x = aParts[i] || 0 + const y = bParts[i] || 0 + if (x > y) return 1 + if (x < y) return -1 } - return 0; + return 0 } - diff --git a/extensions/llamacpp-extension/src/index.ts b/extensions/llamacpp-extension/src/index.ts index e92d5adbc..ec9ea030d 100644 --- a/extensions/llamacpp-extension/src/index.ts +++ b/extensions/llamacpp-extension/src/index.ts @@ -27,6 +27,7 @@ import { getBackendExePath, } from './backend' import { invoke } from '@tauri-apps/api/core' +import { getProxyConfig } from './util' type LlamacppConfig = { version_backend: string @@ -61,6 +62,7 @@ type LlamacppConfig = { interface DownloadItem { url: string save_path: string + proxy?: Record } interface ModelConfig { @@ -607,7 +609,11 @@ export default class llamacpp_extension extends AIEngine { // if URL, add to downloadItems, and return local path if (path.startsWith('https://')) { const localPath = `${modelDir}/${saveName}` - downloadItems.push({ url: path, save_path: localPath }) + downloadItems.push({ + url: path, + save_path: localPath, + proxy: getProxyConfig(), + }) return localPath } diff --git a/extensions/llamacpp-extension/src/util.test.ts b/extensions/llamacpp-extension/src/util.test.ts new file mode 100644 index 000000000..1891e08da --- /dev/null +++ b/extensions/llamacpp-extension/src/util.test.ts @@ -0,0 +1,546 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { getProxyConfig } from './util' + +// Mock localStorage +const mockLocalStorage = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn(), +} + +// Mock console.log and console.error to avoid noise in tests +const mockConsole = { + log: vi.fn(), + error: vi.fn(), +} + +// Set up mocks +beforeEach(() => { + // Clear all mocks + vi.clearAllMocks() + + // Mock localStorage + Object.defineProperty(window, 'localStorage', { + value: mockLocalStorage, + writable: true, + }) + + // Mock console + Object.defineProperty(console, 'log', { + value: mockConsole.log, + writable: true, + }) + Object.defineProperty(console, 'error', { + value: mockConsole.error, + writable: true, + }) +}) + +describe('getProxyConfig', () => { + it('should return null when no proxy configuration is stored', () => { + mockLocalStorage.getItem.mockReturnValue(null) + + const result = getProxyConfig() + + expect(result).toBeNull() + expect(mockLocalStorage.getItem).toHaveBeenCalledWith('setting-proxy-config') + }) + + it('should return null when proxy is disabled', () => { + const proxyConfig = { + state: { + proxyEnabled: false, + proxyUrl: 'http://proxy.example.com:8080', + proxyUsername: 'user', + proxyPassword: 'pass', + proxyIgnoreSSL: false, + verifyProxySSL: true, + verifyProxyHostSSL: true, + verifyPeerSSL: true, + verifyHostSSL: true, + noProxy: 'localhost,127.0.0.1', + }, + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + const result = getProxyConfig() + + expect(result).toBeNull() + }) + + it('should return null when proxy is enabled but no URL is provided', () => { + const proxyConfig = { + state: { + proxyEnabled: true, + proxyUrl: '', + proxyUsername: 'user', + proxyPassword: 'pass', + proxyIgnoreSSL: false, + verifyProxySSL: true, + verifyProxyHostSSL: true, + verifyPeerSSL: true, + verifyHostSSL: true, + noProxy: '', + }, + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + const result = getProxyConfig() + + expect(result).toBeNull() + }) + + it('should return basic proxy configuration with SSL settings', () => { + const proxyConfig = { + state: { + proxyEnabled: true, + proxyUrl: 'https://proxy.example.com:8080', + proxyUsername: '', + proxyPassword: '', + proxyIgnoreSSL: true, + verifyProxySSL: false, + verifyProxyHostSSL: false, + verifyPeerSSL: true, + verifyHostSSL: true, + noProxy: '', + }, + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + const result = getProxyConfig() + + expect(result).toEqual({ + url: 'https://proxy.example.com:8080', + ignore_ssl: true, + verify_proxy_ssl: false, + verify_proxy_host_ssl: false, + verify_peer_ssl: true, + verify_host_ssl: true, + }) + }) + + it('should include authentication when both username and password are provided', () => { + const proxyConfig = { + state: { + proxyEnabled: true, + proxyUrl: 'http://proxy.example.com:8080', + proxyUsername: 'testuser', + proxyPassword: 'testpass', + proxyIgnoreSSL: false, + verifyProxySSL: true, + verifyProxyHostSSL: true, + verifyPeerSSL: true, + verifyHostSSL: true, + noProxy: '', + }, + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + const result = getProxyConfig() + + expect(result).toEqual({ + url: 'http://proxy.example.com:8080', + username: 'testuser', + password: 'testpass', + ignore_ssl: false, + verify_proxy_ssl: true, + verify_proxy_host_ssl: true, + verify_peer_ssl: true, + verify_host_ssl: true, + }) + }) + + it('should not include authentication when only username is provided', () => { + const proxyConfig = { + state: { + proxyEnabled: true, + proxyUrl: 'http://proxy.example.com:8080', + proxyUsername: 'testuser', + proxyPassword: '', + proxyIgnoreSSL: false, + verifyProxySSL: true, + verifyProxyHostSSL: true, + verifyPeerSSL: true, + verifyHostSSL: true, + noProxy: '', + }, + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + const result = getProxyConfig() + + expect(result).toEqual({ + url: 'http://proxy.example.com:8080', + ignore_ssl: false, + verify_proxy_ssl: true, + verify_proxy_host_ssl: true, + verify_peer_ssl: true, + verify_host_ssl: true, + }) + expect(result?.username).toBeUndefined() + expect(result?.password).toBeUndefined() + }) + + it('should not include authentication when only password is provided', () => { + const proxyConfig = { + state: { + proxyEnabled: true, + proxyUrl: 'http://proxy.example.com:8080', + proxyUsername: '', + proxyPassword: 'testpass', + proxyIgnoreSSL: false, + verifyProxySSL: true, + verifyProxyHostSSL: true, + verifyPeerSSL: true, + verifyHostSSL: true, + noProxy: '', + }, + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + const result = getProxyConfig() + + expect(result).toEqual({ + url: 'http://proxy.example.com:8080', + ignore_ssl: false, + verify_proxy_ssl: true, + verify_proxy_host_ssl: true, + verify_peer_ssl: true, + verify_host_ssl: true, + }) + expect(result?.username).toBeUndefined() + expect(result?.password).toBeUndefined() + }) + + it('should parse no_proxy list correctly', () => { + const proxyConfig = { + state: { + proxyEnabled: true, + proxyUrl: 'http://proxy.example.com:8080', + proxyUsername: '', + proxyPassword: '', + proxyIgnoreSSL: false, + verifyProxySSL: true, + verifyProxyHostSSL: true, + verifyPeerSSL: true, + verifyHostSSL: true, + noProxy: 'localhost, 127.0.0.1, *.example.com , specific.domain.com', + }, + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + const result = getProxyConfig() + + expect(result).toEqual({ + url: 'http://proxy.example.com:8080', + no_proxy: ['localhost', '127.0.0.1', '*.example.com', 'specific.domain.com'], + ignore_ssl: false, + verify_proxy_ssl: true, + verify_proxy_host_ssl: true, + verify_peer_ssl: true, + verify_host_ssl: true, + }) + }) + + it('should handle empty no_proxy entries', () => { + const proxyConfig = { + state: { + proxyEnabled: true, + proxyUrl: 'http://proxy.example.com:8080', + proxyUsername: '', + proxyPassword: '', + proxyIgnoreSSL: false, + verifyProxySSL: true, + verifyProxyHostSSL: true, + verifyPeerSSL: true, + verifyHostSSL: true, + noProxy: 'localhost, , 127.0.0.1, ,', + }, + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + const result = getProxyConfig() + + expect(result).toEqual({ + url: 'http://proxy.example.com:8080', + no_proxy: ['localhost', '127.0.0.1'], + ignore_ssl: false, + verify_proxy_ssl: true, + verify_proxy_host_ssl: true, + verify_peer_ssl: true, + verify_host_ssl: true, + }) + }) + + it('should handle mixed SSL verification settings', () => { + const proxyConfig = { + state: { + proxyEnabled: true, + proxyUrl: 'https://proxy.example.com:8080', + proxyUsername: 'user', + proxyPassword: 'pass', + proxyIgnoreSSL: true, + verifyProxySSL: false, + verifyProxyHostSSL: true, + verifyPeerSSL: false, + verifyHostSSL: true, + noProxy: 'localhost', + }, + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + const result = getProxyConfig() + + expect(result).toEqual({ + url: 'https://proxy.example.com:8080', + username: 'user', + password: 'pass', + no_proxy: ['localhost'], + ignore_ssl: true, + verify_proxy_ssl: false, + verify_proxy_host_ssl: true, + verify_peer_ssl: false, + verify_host_ssl: true, + }) + }) + + it('should handle all SSL verification settings as false', () => { + const proxyConfig = { + state: { + proxyEnabled: true, + proxyUrl: 'http://proxy.example.com:8080', + proxyUsername: '', + proxyPassword: '', + proxyIgnoreSSL: false, + verifyProxySSL: false, + verifyProxyHostSSL: false, + verifyPeerSSL: false, + verifyHostSSL: false, + noProxy: '', + }, + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + const result = getProxyConfig() + + expect(result).toEqual({ + url: 'http://proxy.example.com:8080', + ignore_ssl: false, + verify_proxy_ssl: false, + verify_proxy_host_ssl: false, + verify_peer_ssl: false, + verify_host_ssl: false, + }) + }) + + it('should handle all SSL verification settings as true', () => { + const proxyConfig = { + state: { + proxyEnabled: true, + proxyUrl: 'https://proxy.example.com:8080', + proxyUsername: '', + proxyPassword: '', + proxyIgnoreSSL: true, + verifyProxySSL: true, + verifyProxyHostSSL: true, + verifyPeerSSL: true, + verifyHostSSL: true, + noProxy: '', + }, + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + const result = getProxyConfig() + + expect(result).toEqual({ + url: 'https://proxy.example.com:8080', + ignore_ssl: true, + verify_proxy_ssl: true, + verify_proxy_host_ssl: true, + verify_peer_ssl: true, + verify_host_ssl: true, + }) + }) + + it('should log proxy configuration details', () => { + const proxyConfig = { + state: { + proxyEnabled: true, + proxyUrl: 'https://proxy.example.com:8080', + proxyUsername: 'testuser', + proxyPassword: 'testpass', + proxyIgnoreSSL: true, + verifyProxySSL: false, + verifyProxyHostSSL: true, + verifyPeerSSL: false, + verifyHostSSL: true, + noProxy: 'localhost,127.0.0.1', + }, + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + getProxyConfig() + + expect(mockConsole.log).toHaveBeenCalledWith('Using proxy configuration:', { + url: 'https://proxy.example.com:8080', + hasAuth: true, + noProxyCount: 2, + ignoreSSL: true, + verifyProxySSL: false, + verifyProxyHostSSL: true, + verifyPeerSSL: false, + verifyHostSSL: true, + }) + }) + + it('should log proxy configuration without authentication', () => { + const proxyConfig = { + state: { + proxyEnabled: true, + proxyUrl: 'http://proxy.example.com:8080', + proxyUsername: '', + proxyPassword: '', + proxyIgnoreSSL: false, + verifyProxySSL: true, + verifyProxyHostSSL: true, + verifyPeerSSL: true, + verifyHostSSL: true, + noProxy: '', + }, + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + getProxyConfig() + + expect(mockConsole.log).toHaveBeenCalledWith('Using proxy configuration:', { + url: 'http://proxy.example.com:8080', + hasAuth: false, + noProxyCount: 0, + ignoreSSL: false, + verifyProxySSL: true, + verifyProxyHostSSL: true, + verifyPeerSSL: true, + verifyHostSSL: true, + }) + }) + + it('should return null and log error when JSON parsing fails', () => { + mockLocalStorage.getItem.mockReturnValue('invalid-json') + + const result = getProxyConfig() + + expect(result).toBeNull() + expect(mockConsole.error).toHaveBeenCalledWith( + 'Failed to parse proxy configuration:', + expect.any(SyntaxError) + ) + }) + + it('should handle missing state property gracefully', () => { + const proxyConfig = { + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + expect(() => getProxyConfig()).toThrow() + expect(mockConsole.error).toHaveBeenCalledWith( + 'Failed to parse proxy configuration:', + expect.any(Error) + ) + }) + + it('should handle SOCKS proxy URLs', () => { + const proxyConfig = { + state: { + proxyEnabled: true, + proxyUrl: 'socks5://proxy.example.com:1080', + proxyUsername: 'user', + proxyPassword: 'pass', + proxyIgnoreSSL: false, + verifyProxySSL: true, + verifyProxyHostSSL: true, + verifyPeerSSL: true, + verifyHostSSL: true, + noProxy: '', + }, + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + const result = getProxyConfig() + + expect(result).toEqual({ + url: 'socks5://proxy.example.com:1080', + username: 'user', + password: 'pass', + ignore_ssl: false, + verify_proxy_ssl: true, + verify_proxy_host_ssl: true, + verify_peer_ssl: true, + verify_host_ssl: true, + }) + }) + + it('should handle comprehensive proxy configuration', () => { + const proxyConfig = { + state: { + proxyEnabled: true, + proxyUrl: 'https://secure-proxy.example.com:8443', + proxyUsername: 'admin', + proxyPassword: 'secretpass', + proxyIgnoreSSL: true, + verifyProxySSL: false, + verifyProxyHostSSL: false, + verifyPeerSSL: true, + verifyHostSSL: true, + noProxy: 'localhost,127.0.0.1,*.local,192.168.1.0/24', + }, + version: 0, + } + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(proxyConfig)) + + const result = getProxyConfig() + + expect(result).toEqual({ + url: 'https://secure-proxy.example.com:8443', + username: 'admin', + password: 'secretpass', + no_proxy: ['localhost', '127.0.0.1', '*.local', '192.168.1.0/24'], + ignore_ssl: true, + verify_proxy_ssl: false, + verify_proxy_host_ssl: false, + verify_peer_ssl: true, + verify_host_ssl: true, + }) + }) +}) \ No newline at end of file diff --git a/extensions/llamacpp-extension/src/util.ts b/extensions/llamacpp-extension/src/util.ts new file mode 100644 index 000000000..650f1eee5 --- /dev/null +++ b/extensions/llamacpp-extension/src/util.ts @@ -0,0 +1,68 @@ +// Zustand proxy state structure +interface ProxyState { + proxyEnabled: boolean + proxyUrl: string + proxyUsername: string + proxyPassword: string + proxyIgnoreSSL: boolean + verifyProxySSL: boolean + verifyProxyHostSSL: boolean + verifyPeerSSL: boolean + verifyHostSSL: boolean + noProxy: string +} + +export function getProxyConfig(): Record< + string, + string | string[] | boolean +> | null { + try { + // Retrieve proxy configuration from localStorage + const proxyConfigString = localStorage.getItem('setting-proxy-config') + if (!proxyConfigString) { + return null + } + + const proxyConfigData = JSON.parse(proxyConfigString) + const proxyState: ProxyState = proxyConfigData.state + + // Only return proxy config if proxy is enabled + if (!proxyState.proxyEnabled || !proxyState.proxyUrl) { + return null + } + + const proxyConfig: Record = { + url: proxyState.proxyUrl, + } + + // Add username/password if both are provided + if (proxyState.proxyUsername && proxyState.proxyPassword) { + proxyConfig.username = proxyState.proxyUsername + proxyConfig.password = proxyState.proxyPassword + } + + // Parse no_proxy list if provided + if (proxyState.noProxy) { + const noProxyList = proxyState.noProxy + .split(',') + .map((s: string) => s.trim()) + .filter((s: string) => s.length > 0) + + if (noProxyList.length > 0) { + proxyConfig.no_proxy = noProxyList + } + } + + // Add SSL verification settings + proxyConfig.ignore_ssl = proxyState.proxyIgnoreSSL + proxyConfig.verify_proxy_ssl = proxyState.verifyProxySSL + proxyConfig.verify_proxy_host_ssl = proxyState.verifyProxyHostSSL + proxyConfig.verify_peer_ssl = proxyState.verifyPeerSSL + proxyConfig.verify_host_ssl = proxyState.verifyHostSSL + + return proxyConfig + } catch (error) { + console.error('Failed to parse proxy configuration:', error) + return null + } +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 87d493d07..9a7a625dd 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -45,6 +45,7 @@ uuid = { version = "1.7", features = ["v4"] } env = "1.0.1" futures-util = "0.3.31" tokio-util = "0.7.14" +url = "2.5" tauri-plugin-dialog = "2.2.1" dirs = "6.0.0" sysinfo = "0.34.2" diff --git a/src-tauri/src/core/utils/download.rs b/src-tauri/src/core/utils/download.rs index b7730ed25..a1a655e88 100644 --- a/src-tauri/src/core/utils/download.rs +++ b/src-tauri/src/core/utils/download.rs @@ -9,16 +9,31 @@ use tauri::{Emitter, State}; use tokio::fs::File; use tokio::io::AsyncWriteExt; use tokio_util::sync::CancellationToken; +use url::Url; #[derive(Default)] pub struct DownloadManagerState { pub cancel_tokens: HashMap, } +#[derive(serde::Deserialize, Clone, Debug)] +pub struct ProxyConfig { + pub url: String, + pub username: Option, + pub password: Option, + pub no_proxy: Option>, // List of domains to bypass proxy + pub ignore_ssl: Option, // Ignore SSL certificate verification + pub verify_proxy_ssl: Option, // Verify proxy SSL certificate + pub verify_proxy_host_ssl: Option, // Verify proxy host SSL certificate + pub verify_peer_ssl: Option, // Verify peer SSL certificate + pub verify_host_ssl: Option, // Verify host SSL certificate +} + #[derive(serde::Deserialize, Clone, Debug)] pub struct DownloadItem { pub url: String, pub save_path: String, + pub proxy: Option, } #[derive(serde::Serialize, Clone, Debug)] @@ -31,6 +46,130 @@ fn err_to_string(e: E) -> String { format!("Error: {}", e) } +fn validate_proxy_config(config: &ProxyConfig) -> Result<(), String> { + // Validate proxy URL format + if let Err(e) = Url::parse(&config.url) { + return Err(format!("Invalid proxy URL '{}': {}", config.url, e)); + } + + // Check if proxy URL has valid scheme + let url = Url::parse(&config.url).unwrap(); // Safe to unwrap as we just validated it + match url.scheme() { + "http" | "https" | "socks4" | "socks5" => {} + scheme => return Err(format!("Unsupported proxy scheme: {}", scheme)), + } + + // Validate authentication credentials + if config.username.is_some() && config.password.is_none() { + return Err("Username provided without password".to_string()); + } + + if config.password.is_some() && config.username.is_none() { + return Err("Password provided without username".to_string()); + } + + // Validate no_proxy entries + if let Some(no_proxy) = &config.no_proxy { + for entry in no_proxy { + if entry.is_empty() { + return Err("Empty no_proxy entry".to_string()); + } + // Basic validation for wildcard patterns + if entry.starts_with("*.") && entry.len() < 3 { + return Err(format!("Invalid wildcard pattern: {}", entry)); + } + } + } + + // SSL verification settings are all optional booleans, no validation needed + + Ok(()) +} + +fn create_proxy_from_config(config: &ProxyConfig) -> Result { + // Validate the configuration first + validate_proxy_config(config)?; + + let mut proxy = reqwest::Proxy::all(&config.url).map_err(err_to_string)?; + + // Add authentication if provided + if let (Some(username), Some(password)) = (&config.username, &config.password) { + proxy = proxy.basic_auth(username, password); + } + + Ok(proxy) +} + +fn should_bypass_proxy(url: &str, no_proxy: &[String]) -> bool { + if no_proxy.is_empty() { + return false; + } + + // Parse the URL to get the host + let parsed_url = match Url::parse(url) { + Ok(u) => u, + Err(_) => return false, + }; + + let host = match parsed_url.host_str() { + Some(h) => h, + None => return false, + }; + + // Check if host matches any no_proxy entry + for entry in no_proxy { + if entry == "*" { + return true; + } + + // Simple wildcard matching + if entry.starts_with("*.") { + let domain = &entry[2..]; + if host.ends_with(domain) { + return true; + } + } else if host == entry { + return true; + } + } + + false +} + +fn _get_client_for_item( + item: &DownloadItem, + header_map: &HeaderMap, +) -> Result { + let mut client_builder = reqwest::Client::builder() + .http2_keep_alive_timeout(Duration::from_secs(15)) + .default_headers(header_map.clone()); + + // Add proxy configuration if provided + if let Some(proxy_config) = &item.proxy { + // Handle SSL verification settings + if proxy_config.ignore_ssl.unwrap_or(false) { + client_builder = client_builder.danger_accept_invalid_certs(true); + log::info!("SSL certificate verification disabled for URL {}", item.url); + } + + // Note: reqwest doesn't have fine-grained SSL verification controls + // for verify_proxy_ssl, verify_proxy_host_ssl, verify_peer_ssl, verify_host_ssl + // These settings are handled by the underlying TLS implementation + + // Check if this URL should bypass proxy + let no_proxy = proxy_config.no_proxy.as_deref().unwrap_or(&[]); + if !should_bypass_proxy(&item.url, no_proxy) { + let proxy = create_proxy_from_config(proxy_config)?; + client_builder = client_builder.proxy(proxy); + log::info!("Using proxy {} for URL {}", proxy_config.url, item.url); + } else { + log::info!("Bypassing proxy for URL {}", item.url); + } + } + + client_builder.build().map_err(err_to_string) +} + #[tauri::command] pub async fn download_files( app: tauri::AppHandle, @@ -130,19 +269,10 @@ async fn _download_files_internal( let header_map = _convert_headers(headers).map_err(err_to_string)?; - // .read_timeout() and .connect_timeout() requires reqwest 0.12, which is not - // compatible with hyper 0.14 - let client = reqwest::Client::builder() - .http2_keep_alive_timeout(Duration::from_secs(15)) - // .read_timeout(Duration::from_secs(10)) // timeout between chunks - // .connect_timeout(Duration::from_secs(10)) // timeout for first connection - .default_headers(header_map.clone()) - .build() - .map_err(err_to_string)?; - let total_size = { let mut total_size = 0u64; for item in items.iter() { + let client = _get_client_for_item(item, &header_map).map_err(err_to_string)?; total_size += _get_file_size(&client, &item.url) .await .map_err(err_to_string)?; @@ -203,6 +333,7 @@ async fn _download_files_internal( .map_err(err_to_string)?; log::info!("Started downloading: {}", item.url); + let client = _get_client_for_item(item, &header_map).map_err(err_to_string)?; let mut download_delta = 0u64; let resp = if resume { let downloaded_size = tmp_save_path.metadata().map_err(err_to_string)?.len(); @@ -311,3 +442,314 @@ async fn _get_maybe_resume( Ok(resp) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + // Helper function to create a minimal proxy config for testing + fn create_test_proxy_config(url: &str) -> ProxyConfig { + ProxyConfig { + url: url.to_string(), + username: None, + password: None, + no_proxy: None, + ignore_ssl: None, + verify_proxy_ssl: None, + verify_proxy_host_ssl: None, + verify_peer_ssl: None, + verify_host_ssl: None, + } + } + + #[test] + fn test_validate_proxy_config() { + // Valid HTTP proxy + let config = ProxyConfig { + url: "http://proxy.example.com:8080".to_string(), + username: Some("user".to_string()), + password: Some("pass".to_string()), + no_proxy: Some(vec!["localhost".to_string(), "*.example.com".to_string()]), + ignore_ssl: Some(true), + verify_proxy_ssl: Some(false), + verify_proxy_host_ssl: Some(false), + verify_peer_ssl: Some(false), + verify_host_ssl: Some(false), + }; + assert!(validate_proxy_config(&config).is_ok()); + + // Valid HTTPS proxy + let config = ProxyConfig { + url: "https://proxy.example.com:8080".to_string(), + username: None, + password: None, + no_proxy: None, + ignore_ssl: None, + verify_proxy_ssl: None, + verify_proxy_host_ssl: None, + verify_peer_ssl: None, + verify_host_ssl: None, + }; + assert!(validate_proxy_config(&config).is_ok()); + + // Valid SOCKS5 proxy + let config = ProxyConfig { + url: "socks5://proxy.example.com:1080".to_string(), + username: None, + password: None, + no_proxy: None, + ignore_ssl: None, + verify_proxy_ssl: None, + verify_proxy_host_ssl: None, + verify_peer_ssl: None, + verify_host_ssl: None, + }; + assert!(validate_proxy_config(&config).is_ok()); + + // Invalid URL + let config = create_test_proxy_config("invalid-url"); + assert!(validate_proxy_config(&config).is_err()); + + // Unsupported scheme + let config = create_test_proxy_config("ftp://proxy.example.com:21"); + assert!(validate_proxy_config(&config).is_err()); + + // Username without password + let mut config = create_test_proxy_config("http://proxy.example.com:8080"); + config.username = Some("user".to_string()); + assert!(validate_proxy_config(&config).is_err()); + + // Password without username + let mut config = create_test_proxy_config("http://proxy.example.com:8080"); + config.password = Some("pass".to_string()); + assert!(validate_proxy_config(&config).is_err()); + + // Empty no_proxy entry + let mut config = create_test_proxy_config("http://proxy.example.com:8080"); + config.no_proxy = Some(vec!["".to_string()]); + assert!(validate_proxy_config(&config).is_err()); + + // Invalid wildcard pattern + let mut config = create_test_proxy_config("http://proxy.example.com:8080"); + config.no_proxy = Some(vec!["*.".to_string()]); + assert!(validate_proxy_config(&config).is_err()); + } + + #[test] + fn test_should_bypass_proxy() { + let no_proxy = vec![ + "localhost".to_string(), + "127.0.0.1".to_string(), + "*.example.com".to_string(), + "specific.domain.com".to_string(), + ]; + + // Should bypass for localhost + assert!(should_bypass_proxy("http://localhost:8080/path", &no_proxy)); + + // Should bypass for 127.0.0.1 + assert!(should_bypass_proxy("https://127.0.0.1:3000/api", &no_proxy)); + + // Should bypass for wildcard match + assert!(should_bypass_proxy( + "http://sub.example.com/path", + &no_proxy + )); + assert!(should_bypass_proxy("https://api.example.com/v1", &no_proxy)); + + // Should bypass for specific domain + assert!(should_bypass_proxy( + "http://specific.domain.com/test", + &no_proxy + )); + + // Should NOT bypass for other domains + assert!(!should_bypass_proxy("http://other.com/path", &no_proxy)); + assert!(!should_bypass_proxy("https://example.org/api", &no_proxy)); + + // Should bypass everything with "*" + let wildcard_no_proxy = vec!["*".to_string()]; + assert!(should_bypass_proxy( + "http://any.domain.com/path", + &wildcard_no_proxy + )); + + // Empty no_proxy should not bypass anything + let empty_no_proxy = vec![]; + assert!(!should_bypass_proxy( + "http://any.domain.com/path", + &empty_no_proxy + )); + } + + #[test] + fn test_create_proxy_from_config() { + // Valid configuration should work + let mut config = create_test_proxy_config("http://proxy.example.com:8080"); + config.username = Some("user".to_string()); + config.password = Some("pass".to_string()); + assert!(create_proxy_from_config(&config).is_ok()); + + // Invalid configuration should fail + let config = create_test_proxy_config("invalid-url"); + assert!(create_proxy_from_config(&config).is_err()); + } + + #[test] + fn test_convert_headers() { + let mut headers = HashMap::new(); + headers.insert("User-Agent".to_string(), "test-agent".to_string()); + headers.insert("Authorization".to_string(), "Bearer token".to_string()); + + let header_map = _convert_headers(&headers).unwrap(); + assert_eq!(header_map.len(), 2); + assert_eq!(header_map.get("User-Agent").unwrap(), "test-agent"); + assert_eq!(header_map.get("Authorization").unwrap(), "Bearer token"); + } + + #[test] + fn test_proxy_ssl_verification_settings() { + // Test proxy config with SSL verification settings + let mut config = create_test_proxy_config("https://proxy.example.com:8080"); + config.ignore_ssl = Some(true); + config.verify_proxy_ssl = Some(false); + config.verify_proxy_host_ssl = Some(false); + config.verify_peer_ssl = Some(true); + config.verify_host_ssl = Some(true); + + // Should validate successfully + assert!(validate_proxy_config(&config).is_ok()); + + // Test with all SSL settings as false + config.ignore_ssl = Some(false); + config.verify_proxy_ssl = Some(false); + config.verify_proxy_host_ssl = Some(false); + config.verify_peer_ssl = Some(false); + config.verify_host_ssl = Some(false); + + // Should still validate successfully + assert!(validate_proxy_config(&config).is_ok()); + } + + #[test] + fn test_proxy_config_with_mixed_ssl_settings() { + // Test with mixed SSL settings - ignore_ssl true, others false + let mut config = create_test_proxy_config("https://proxy.example.com:8080"); + config.ignore_ssl = Some(true); + config.verify_proxy_ssl = Some(false); + config.verify_proxy_host_ssl = Some(true); + config.verify_peer_ssl = Some(false); + config.verify_host_ssl = Some(true); + + assert!(validate_proxy_config(&config).is_ok()); + assert!(create_proxy_from_config(&config).is_ok()); + } + + #[test] + fn test_proxy_config_ssl_defaults() { + // Test with no SSL settings (should use None defaults) + let config = create_test_proxy_config("https://proxy.example.com:8080"); + + assert_eq!(config.ignore_ssl, None); + assert_eq!(config.verify_proxy_ssl, None); + assert_eq!(config.verify_proxy_host_ssl, None); + assert_eq!(config.verify_peer_ssl, None); + assert_eq!(config.verify_host_ssl, None); + + assert!(validate_proxy_config(&config).is_ok()); + assert!(create_proxy_from_config(&config).is_ok()); + } + + #[test] + fn test_proxy_config_serialization() { + // Test that proxy config with SSL settings can be serialized/deserialized + let mut config = create_test_proxy_config("https://proxy.example.com:8080"); + config.username = Some("user".to_string()); + config.password = Some("pass".to_string()); + config.ignore_ssl = Some(true); + config.verify_proxy_ssl = Some(false); + config.verify_proxy_host_ssl = Some(false); + config.verify_peer_ssl = Some(true); + config.verify_host_ssl = Some(true); + config.no_proxy = Some(vec!["localhost".to_string(), "*.example.com".to_string()]); + + // Serialize to JSON + let json = serde_json::to_string(&config).unwrap(); + + // Deserialize from JSON + let deserialized: ProxyConfig = serde_json::from_str(&json).unwrap(); + + // Verify all fields are preserved + assert_eq!(deserialized.url, config.url); + assert_eq!(deserialized.username, config.username); + assert_eq!(deserialized.password, config.password); + assert_eq!(deserialized.ignore_ssl, config.ignore_ssl); + assert_eq!(deserialized.verify_proxy_ssl, config.verify_proxy_ssl); + assert_eq!(deserialized.verify_proxy_host_ssl, config.verify_proxy_host_ssl); + assert_eq!(deserialized.verify_peer_ssl, config.verify_peer_ssl); + assert_eq!(deserialized.verify_host_ssl, config.verify_host_ssl); + assert_eq!(deserialized.no_proxy, config.no_proxy); + } + + #[test] + fn test_download_item_with_ssl_proxy() { + // Test that DownloadItem can be created with SSL proxy configuration + let mut proxy_config = create_test_proxy_config("https://proxy.example.com:8080"); + proxy_config.ignore_ssl = Some(true); + proxy_config.verify_proxy_ssl = Some(false); + + let download_item = DownloadItem { + url: "https://example.com/file.zip".to_string(), + save_path: "downloads/file.zip".to_string(), + proxy: Some(proxy_config), + }; + + assert!(download_item.proxy.is_some()); + let proxy = download_item.proxy.unwrap(); + assert_eq!(proxy.ignore_ssl, Some(true)); + assert_eq!(proxy.verify_proxy_ssl, Some(false)); + } + + #[test] + fn test_client_creation_with_ssl_settings() { + // Test client creation with SSL settings + let mut proxy_config = create_test_proxy_config("https://proxy.example.com:8080"); + proxy_config.ignore_ssl = Some(true); + + let download_item = DownloadItem { + url: "https://example.com/file.zip".to_string(), + save_path: "downloads/file.zip".to_string(), + proxy: Some(proxy_config), + }; + + let header_map = HeaderMap::new(); + let result = _get_client_for_item(&download_item, &header_map); + + // Should create client successfully even with SSL settings + assert!(result.is_ok()); + } + + #[test] + fn test_proxy_config_with_http_and_ssl_settings() { + // Test that SSL settings work with HTTP proxy (though not typically used) + let mut config = create_test_proxy_config("http://proxy.example.com:8080"); + config.ignore_ssl = Some(true); + config.verify_proxy_ssl = Some(false); + + assert!(validate_proxy_config(&config).is_ok()); + assert!(create_proxy_from_config(&config).is_ok()); + } + + #[test] + fn test_proxy_config_with_socks_and_ssl_settings() { + // Test that SSL settings work with SOCKS proxy + let mut config = create_test_proxy_config("socks5://proxy.example.com:1080"); + config.ignore_ssl = Some(false); + config.verify_peer_ssl = Some(true); + config.verify_host_ssl = Some(true); + + assert!(validate_proxy_config(&config).is_ok()); + assert!(create_proxy_from_config(&config).is_ok()); + } +} diff --git a/web-app/src/routes/settings/https-proxy.tsx b/web-app/src/routes/settings/https-proxy.tsx index 15469be5e..8a8cc59d0 100644 --- a/web-app/src/routes/settings/https-proxy.tsx +++ b/web-app/src/routes/settings/https-proxy.tsx @@ -7,9 +7,8 @@ import { Switch } from '@/components/ui/switch' import { useTranslation } from '@/i18n/react-i18next-compat' import { Input } from '@/components/ui/input' import { EyeOff, Eye } from 'lucide-react' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useState } from 'react' import { useProxyConfig } from '@/hooks/useProxyConfig' -import { configurePullOptions } from '@/services/models' // eslint-disable-next-line @typescript-eslint/no-explicit-any export const Route = createFileRoute(route.settings.https_proxy as any)({ @@ -45,62 +44,10 @@ function HTTPSProxy() { const toggleProxy = useCallback( (checked: boolean) => { setProxyEnabled(checked) - configurePullOptions({ - proxyUrl, - proxyEnabled: checked, - proxyUsername, - proxyPassword, - proxyIgnoreSSL, - verifyProxySSL, - verifyProxyHostSSL, - verifyPeerSSL, - verifyHostSSL, - noProxy, - }) }, - [ - noProxy, - proxyIgnoreSSL, - proxyPassword, - proxyUrl, - proxyUsername, - setProxyEnabled, - verifyHostSSL, - verifyPeerSSL, - verifyProxyHostSSL, - verifyProxySSL, - ] + [setProxyEnabled] ) - useEffect(() => { - const handler = setTimeout(() => { - configurePullOptions({ - proxyUrl, - proxyEnabled, - proxyUsername, - proxyPassword, - proxyIgnoreSSL, - verifyProxySSL, - verifyProxyHostSSL, - verifyPeerSSL, - verifyHostSSL, - noProxy, - }) - }, 300) - return () => clearTimeout(handler) - }, [ - noProxy, - proxyEnabled, - proxyIgnoreSSL, - proxyPassword, - proxyUrl, - proxyUsername, - verifyHostSSL, - verifyPeerSSL, - verifyProxyHostSSL, - verifyProxySSL, - ]) - return (
diff --git a/web-app/src/services/__tests__/models.test.ts b/web-app/src/services/__tests__/models.test.ts index 3fc5e3b84..2714ac930 100644 --- a/web-app/src/services/__tests__/models.test.ts +++ b/web-app/src/services/__tests__/models.test.ts @@ -247,32 +247,4 @@ describe('models service', () => { await expect(startModel(provider, model)).resolves.toBe(undefined) }) }) - - describe('configurePullOptions', () => { - it('should configure proxy options', async () => { - const proxyOptions = { - proxyEnabled: true, - proxyUrl: 'http://proxy.com', - proxyUsername: 'user', - proxyPassword: 'pass', - proxyIgnoreSSL: false, - verifyProxySSL: true, - verifyProxyHostSSL: true, - verifyPeerSSL: true, - verifyHostSSL: true, - noProxy: '', - } - - // Mock console.log to avoid output during tests - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - - await configurePullOptions(proxyOptions) - - expect(consoleSpy).toHaveBeenCalledWith( - 'Configuring proxy options:', - proxyOptions - ) - consoleSpy.mockRestore() - }) - }) }) diff --git a/web-app/src/services/models.ts b/web-app/src/services/models.ts index fc6a5c1fb..29be958fc 100644 --- a/web-app/src/services/models.ts +++ b/web-app/src/services/models.ts @@ -158,33 +158,3 @@ export const startModel = async ( throw error }) } - -/** - * Configures the proxy options for model downloads. - * @param param0 - */ -export const configurePullOptions = async ({ - proxyEnabled, - proxyUrl, - proxyUsername, - proxyPassword, - proxyIgnoreSSL, - verifyProxySSL, - verifyProxyHostSSL, - verifyPeerSSL, - verifyHostSSL, - noProxy, -}: ProxyOptions) => { - console.log('Configuring proxy options:', { - proxyEnabled, - proxyUrl, - proxyUsername, - proxyPassword, - proxyIgnoreSSL, - verifyProxySSL, - verifyProxyHostSSL, - verifyPeerSSL, - verifyHostSSL, - noProxy, - }) -}