feat: proxy support for the new downloader (#5795)
* feat: proxy support for the new downloader * test: remove outdated test * ci: clean up
This commit is contained in:
parent
92703bceb2
commit
8ca507c01c
12
.github/workflows/jan-linter-and-test.yml
vendored
12
.github/workflows/jan-linter-and-test.yml
vendored
@ -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: |
|
||||
|
||||
@ -9,6 +9,7 @@ export enum Settings {
|
||||
interface DownloadItem {
|
||||
url: string
|
||||
save_path: string
|
||||
proxy?: Record<string, string | string[] | boolean>
|
||||
}
|
||||
|
||||
type DownloadEvent = {
|
||||
@ -30,10 +31,11 @@ export default class DownloadManager extends BaseExtension {
|
||||
url: string,
|
||||
savePath: string,
|
||||
taskId: string,
|
||||
proxyConfig: Record<string, string | string[] | boolean> = {},
|
||||
onProgress?: (transferred: number, total: number) => void
|
||||
) {
|
||||
return await this.downloadFiles(
|
||||
[{ url, save_path: savePath }],
|
||||
[{ url, save_path: savePath, proxy: proxyConfig }],
|
||||
taskId,
|
||||
onProgress
|
||||
)
|
||||
|
||||
@ -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
|
||||
// <Jan's data folder>/llamacpp/backends/<backend_version>/<backend_type>
|
||||
|
||||
// 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<string> {
|
||||
export async function getBackendDir(
|
||||
backend: string,
|
||||
version: string
|
||||
): Promise<string> {
|
||||
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<string> {
|
||||
export async function getBackendExePath(
|
||||
backend: string,
|
||||
version: string
|
||||
): Promise<string> {
|
||||
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<boolean> {
|
||||
export async function isBackendInstalled(
|
||||
backend: string,
|
||||
version: string
|
||||
): Promise<boolean> {
|
||||
const exePath = await getBackendExePath(backend, version)
|
||||
const result = await fs.existsSync(exePath)
|
||||
return result
|
||||
}
|
||||
|
||||
export async function downloadBackend(backend: string, version: string): Promise<void> {
|
||||
export async function downloadBackend(
|
||||
backend: string,
|
||||
version: string
|
||||
): Promise<void> {
|
||||
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<any[]> {
|
||||
// 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<boolean> {
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
@ -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<string, string | string[] | boolean>
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
546
extensions/llamacpp-extension/src/util.test.ts
Normal file
546
extensions/llamacpp-extension/src/util.test.ts
Normal file
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
68
extensions/llamacpp-extension/src/util.ts
Normal file
68
extensions/llamacpp-extension/src/util.ts
Normal file
@ -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<string, string | string[] | boolean> = {
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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<String, CancellationToken>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct ProxyConfig {
|
||||
pub url: String,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub no_proxy: Option<Vec<String>>, // List of domains to bypass proxy
|
||||
pub ignore_ssl: Option<bool>, // Ignore SSL certificate verification
|
||||
pub verify_proxy_ssl: Option<bool>, // Verify proxy SSL certificate
|
||||
pub verify_proxy_host_ssl: Option<bool>, // Verify proxy host SSL certificate
|
||||
pub verify_peer_ssl: Option<bool>, // Verify peer SSL certificate
|
||||
pub verify_host_ssl: Option<bool>, // Verify host SSL certificate
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct DownloadItem {
|
||||
pub url: String,
|
||||
pub save_path: String,
|
||||
pub proxy: Option<ProxyConfig>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone, Debug)]
|
||||
@ -31,6 +46,130 @@ fn err_to_string<E: std::fmt::Display>(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<reqwest::Proxy, String> {
|
||||
// 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<reqwest::Client, String> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<div className="flex flex-col h-full">
|
||||
<HeaderPage>
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user