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:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
- name: Install tauri-driver dependencies
|
|
||||||
run: |
|
|
||||||
cargo install tauri-driver --locked
|
|
||||||
|
|
||||||
# Clean cache, continue on error
|
# Clean cache, continue on error
|
||||||
- name: 'Cleanup cache'
|
- name: 'Cleanup cache'
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@ -154,10 +150,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
- name: Install tauri-driver dependencies
|
|
||||||
run: |
|
|
||||||
cargo install tauri-driver --locked
|
|
||||||
|
|
||||||
- name: 'Cleanup cache'
|
- name: 'Cleanup cache'
|
||||||
shell: powershell
|
shell: powershell
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
@ -202,10 +194,6 @@ jobs:
|
|||||||
sudo apt update
|
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
|
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'
|
- name: 'Cleanup cache'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export enum Settings {
|
|||||||
interface DownloadItem {
|
interface DownloadItem {
|
||||||
url: string
|
url: string
|
||||||
save_path: string
|
save_path: string
|
||||||
|
proxy?: Record<string, string | string[] | boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
type DownloadEvent = {
|
type DownloadEvent = {
|
||||||
@ -30,10 +31,11 @@ export default class DownloadManager extends BaseExtension {
|
|||||||
url: string,
|
url: string,
|
||||||
savePath: string,
|
savePath: string,
|
||||||
taskId: string,
|
taskId: string,
|
||||||
|
proxyConfig: Record<string, string | string[] | boolean> = {},
|
||||||
onProgress?: (transferred: number, total: number) => void
|
onProgress?: (transferred: number, total: number) => void
|
||||||
) {
|
) {
|
||||||
return await this.downloadFiles(
|
return await this.downloadFiles(
|
||||||
[{ url, save_path: savePath }],
|
[{ url, save_path: savePath, proxy: proxyConfig }],
|
||||||
taskId,
|
taskId,
|
||||||
onProgress
|
onProgress
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
import {
|
import { getJanDataFolderPath, fs, joinPath, events } from '@janhq/core'
|
||||||
getJanDataFolderPath,
|
|
||||||
fs,
|
|
||||||
joinPath,
|
|
||||||
events,
|
|
||||||
} from '@janhq/core'
|
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { getProxyConfig } from './util'
|
||||||
|
|
||||||
// folder structure
|
// folder structure
|
||||||
// <Jan's data folder>/llamacpp/backends/<backend_version>/<backend_type>
|
// <Jan's data folder>/llamacpp/backends/<backend_version>/<backend_type>
|
||||||
|
|
||||||
// what should be available to the user for selection?
|
// 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 sysInfo = await window.core.api.getSystemInfo()
|
||||||
const os_type = sysInfo.os_type
|
const os_type = sysInfo.os_type
|
||||||
const arch = sysInfo.cpu.arch
|
const arch = sysInfo.cpu.arch
|
||||||
@ -35,8 +33,7 @@ export async function listSupportedBackends(): Promise<{ version: string, backen
|
|||||||
// not available yet, placeholder for future
|
// not available yet, placeholder for future
|
||||||
else if (sysType == 'windows-aarch64') {
|
else if (sysType == 'windows-aarch64') {
|
||||||
supportedBackends.push('win-arm64')
|
supportedBackends.push('win-arm64')
|
||||||
}
|
} else if (sysType == 'linux-x86_64') {
|
||||||
else if (sysType == 'linux-x86_64') {
|
|
||||||
supportedBackends.push('linux-noavx-x64')
|
supportedBackends.push('linux-noavx-x64')
|
||||||
if (features.avx) supportedBackends.push('linux-avx-x64')
|
if (features.avx) supportedBackends.push('linux-avx-x64')
|
||||||
if (features.avx2) supportedBackends.push('linux-avx2-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
|
// not available yet, placeholder for future
|
||||||
else if (sysType === 'linux-aarch64') {
|
else if (sysType === 'linux-aarch64') {
|
||||||
supportedBackends.push('linux-arm64')
|
supportedBackends.push('linux-arm64')
|
||||||
}
|
} else if (sysType === 'macos-x86_64') {
|
||||||
else if (sysType === 'macos-x86_64') {
|
|
||||||
supportedBackends.push('macos-x64')
|
supportedBackends.push('macos-x64')
|
||||||
}
|
} else if (sysType === 'macos-aarch64') {
|
||||||
else if (sysType === 'macos-aarch64') {
|
|
||||||
supportedBackends.push('macos-arm64')
|
supportedBackends.push('macos-arm64')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,39 +77,64 @@ export async function listSupportedBackends(): Promise<{ version: string, backen
|
|||||||
return backendVersions
|
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 janDataFolderPath = await getJanDataFolderPath()
|
||||||
const backendDir = await joinPath([janDataFolderPath, 'llamacpp', 'backends', version, backend])
|
const backendDir = await joinPath([
|
||||||
|
janDataFolderPath,
|
||||||
|
'llamacpp',
|
||||||
|
'backends',
|
||||||
|
version,
|
||||||
|
backend,
|
||||||
|
])
|
||||||
return backendDir
|
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 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 backendDir = await getBackendDir(backend, version)
|
||||||
const exePath = await joinPath([backendDir, 'build', 'bin', exe_name])
|
const exePath = await joinPath([backendDir, 'build', 'bin', exe_name])
|
||||||
return exePath
|
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 exePath = await getBackendExePath(backend, version)
|
||||||
const result = await fs.existsSync(exePath)
|
const result = await fs.existsSync(exePath)
|
||||||
return result
|
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 janDataFolderPath = await getJanDataFolderPath()
|
||||||
const llamacppPath = await joinPath([janDataFolderPath, 'llamacpp'])
|
const llamacppPath = await joinPath([janDataFolderPath, 'llamacpp'])
|
||||||
const backendDir = await getBackendDir(backend, version)
|
const backendDir = await getBackendDir(backend, version)
|
||||||
const libDir = await joinPath([llamacppPath, 'lib'])
|
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 = [
|
const downloadItems = [
|
||||||
{
|
{
|
||||||
url: `https://github.com/menloresearch/llama.cpp/releases/download/${version}/llama-${version}-bin-${backend}.tar.gz`,
|
url: `https://github.com/menloresearch/llama.cpp/releases/download/${version}/llama-${version}-bin-${backend}.tar.gz`,
|
||||||
save_path: await joinPath([backendDir, 'backend.tar.gz']),
|
save_path: await joinPath([backendDir, 'backend.tar.gz']),
|
||||||
}
|
proxy: proxyConfig,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// also download CUDA runtime + cuBLAS + cuBLASLt if needed
|
// also download CUDA runtime + cuBLAS + cuBLASLt if needed
|
||||||
@ -122,18 +142,24 @@ export async function downloadBackend(backend: string, version: string): Promise
|
|||||||
downloadItems.push({
|
downloadItems.push({
|
||||||
url: `https://github.com/menloresearch/llama.cpp/releases/download/${version}/cudart-llama-bin-linux-cu11.7-x64.tar.gz`,
|
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']),
|
save_path: await joinPath([libDir, 'cuda11.tar.gz']),
|
||||||
|
proxy: proxyConfig,
|
||||||
})
|
})
|
||||||
} else if (backend.includes('cu12.0') && !(await _isCudaInstalled('12.0'))) {
|
} else if (backend.includes('cu12.0') && !(await _isCudaInstalled('12.0'))) {
|
||||||
downloadItems.push({
|
downloadItems.push({
|
||||||
url: `https://github.com/menloresearch/llama.cpp/releases/download/${version}/cudart-llama-bin-linux-cu12.0-x64.tar.gz`,
|
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']),
|
save_path: await joinPath([libDir, 'cuda12.tar.gz']),
|
||||||
|
proxy: proxyConfig,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskId = `llamacpp-${version}-${backend}`.replace(/\./g, '-')
|
const taskId = `llamacpp-${version}-${backend}`.replace(/\./g, '-')
|
||||||
const downloadType = 'Engine'
|
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
|
let downloadCompleted = false
|
||||||
try {
|
try {
|
||||||
const onProgress = (transferred: number, total: number) => {
|
const onProgress = (transferred: number, total: number) => {
|
||||||
@ -212,13 +238,15 @@ async function _getSupportedFeatures() {
|
|||||||
|
|
||||||
async function _fetchGithubReleases(
|
async function _fetchGithubReleases(
|
||||||
owner: string,
|
owner: string,
|
||||||
repo: string,
|
repo: string
|
||||||
): Promise<any[]> {
|
): Promise<any[]> {
|
||||||
// by default, it's per_page=30 and page=1 -> the latest 30 releases
|
// 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 url = `https://api.github.com/repos/${owner}/${repo}/releases`
|
||||||
const response = await fetch(url)
|
const response = await fetch(url)
|
||||||
if (!response.ok) {
|
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()
|
return response.json()
|
||||||
}
|
}
|
||||||
@ -257,21 +285,25 @@ async function _isCudaInstalled(version: string): Promise<boolean> {
|
|||||||
|
|
||||||
// check for libraries shipped with Jan's llama.cpp extension
|
// check for libraries shipped with Jan's llama.cpp extension
|
||||||
const janDataFolderPath = await getJanDataFolderPath()
|
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)
|
return await fs.existsSync(cudartPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareVersions(a: string, b: string): number {
|
function compareVersions(a: string, b: string): number {
|
||||||
const aParts = a.split('.').map(Number);
|
const aParts = a.split('.').map(Number)
|
||||||
const bParts = b.split('.').map(Number);
|
const bParts = b.split('.').map(Number)
|
||||||
const len = Math.max(aParts.length, bParts.length);
|
const len = Math.max(aParts.length, bParts.length)
|
||||||
|
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
const x = aParts[i] || 0;
|
const x = aParts[i] || 0
|
||||||
const y = bParts[i] || 0;
|
const y = bParts[i] || 0
|
||||||
if (x > y) return 1;
|
if (x > y) return 1
|
||||||
if (x < y) return -1;
|
if (x < y) return -1
|
||||||
}
|
}
|
||||||
return 0;
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import {
|
|||||||
getBackendExePath,
|
getBackendExePath,
|
||||||
} from './backend'
|
} from './backend'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { getProxyConfig } from './util'
|
||||||
|
|
||||||
type LlamacppConfig = {
|
type LlamacppConfig = {
|
||||||
version_backend: string
|
version_backend: string
|
||||||
@ -61,6 +62,7 @@ type LlamacppConfig = {
|
|||||||
interface DownloadItem {
|
interface DownloadItem {
|
||||||
url: string
|
url: string
|
||||||
save_path: string
|
save_path: string
|
||||||
|
proxy?: Record<string, string | string[] | boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModelConfig {
|
interface ModelConfig {
|
||||||
@ -607,7 +609,11 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
// if URL, add to downloadItems, and return local path
|
// if URL, add to downloadItems, and return local path
|
||||||
if (path.startsWith('https://')) {
|
if (path.startsWith('https://')) {
|
||||||
const localPath = `${modelDir}/${saveName}`
|
const localPath = `${modelDir}/${saveName}`
|
||||||
downloadItems.push({ url: path, save_path: localPath })
|
downloadItems.push({
|
||||||
|
url: path,
|
||||||
|
save_path: localPath,
|
||||||
|
proxy: getProxyConfig(),
|
||||||
|
})
|
||||||
return localPath
|
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"
|
env = "1.0.1"
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
tokio-util = "0.7.14"
|
tokio-util = "0.7.14"
|
||||||
|
url = "2.5"
|
||||||
tauri-plugin-dialog = "2.2.1"
|
tauri-plugin-dialog = "2.2.1"
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
sysinfo = "0.34.2"
|
sysinfo = "0.34.2"
|
||||||
|
|||||||
@ -9,16 +9,31 @@ use tauri::{Emitter, State};
|
|||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct DownloadManagerState {
|
pub struct DownloadManagerState {
|
||||||
pub cancel_tokens: HashMap<String, CancellationToken>,
|
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)]
|
#[derive(serde::Deserialize, Clone, Debug)]
|
||||||
pub struct DownloadItem {
|
pub struct DownloadItem {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub save_path: String,
|
pub save_path: String,
|
||||||
|
pub proxy: Option<ProxyConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, Clone, Debug)]
|
#[derive(serde::Serialize, Clone, Debug)]
|
||||||
@ -31,6 +46,130 @@ fn err_to_string<E: std::fmt::Display>(e: E) -> String {
|
|||||||
format!("Error: {}", e)
|
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]
|
#[tauri::command]
|
||||||
pub async fn download_files(
|
pub async fn download_files(
|
||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
@ -130,19 +269,10 @@ async fn _download_files_internal(
|
|||||||
|
|
||||||
let header_map = _convert_headers(headers).map_err(err_to_string)?;
|
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 total_size = {
|
||||||
let mut total_size = 0u64;
|
let mut total_size = 0u64;
|
||||||
for item in items.iter() {
|
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)
|
total_size += _get_file_size(&client, &item.url)
|
||||||
.await
|
.await
|
||||||
.map_err(err_to_string)?;
|
.map_err(err_to_string)?;
|
||||||
@ -203,6 +333,7 @@ async fn _download_files_internal(
|
|||||||
.map_err(err_to_string)?;
|
.map_err(err_to_string)?;
|
||||||
|
|
||||||
log::info!("Started downloading: {}", item.url);
|
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 mut download_delta = 0u64;
|
||||||
let resp = if resume {
|
let resp = if resume {
|
||||||
let downloaded_size = tmp_save_path.metadata().map_err(err_to_string)?.len();
|
let downloaded_size = tmp_save_path.metadata().map_err(err_to_string)?.len();
|
||||||
@ -311,3 +442,314 @@ async fn _get_maybe_resume(
|
|||||||
Ok(resp)
|
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 { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { EyeOff, Eye } from 'lucide-react'
|
import { EyeOff, Eye } from 'lucide-react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useProxyConfig } from '@/hooks/useProxyConfig'
|
import { useProxyConfig } from '@/hooks/useProxyConfig'
|
||||||
import { configurePullOptions } from '@/services/models'
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const Route = createFileRoute(route.settings.https_proxy as any)({
|
export const Route = createFileRoute(route.settings.https_proxy as any)({
|
||||||
@ -45,62 +44,10 @@ function HTTPSProxy() {
|
|||||||
const toggleProxy = useCallback(
|
const toggleProxy = useCallback(
|
||||||
(checked: boolean) => {
|
(checked: boolean) => {
|
||||||
setProxyEnabled(checked)
|
setProxyEnabled(checked)
|
||||||
configurePullOptions({
|
|
||||||
proxyUrl,
|
|
||||||
proxyEnabled: checked,
|
|
||||||
proxyUsername,
|
|
||||||
proxyPassword,
|
|
||||||
proxyIgnoreSSL,
|
|
||||||
verifyProxySSL,
|
|
||||||
verifyProxyHostSSL,
|
|
||||||
verifyPeerSSL,
|
|
||||||
verifyHostSSL,
|
|
||||||
noProxy,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
[
|
[setProxyEnabled]
|
||||||
noProxy,
|
|
||||||
proxyIgnoreSSL,
|
|
||||||
proxyPassword,
|
|
||||||
proxyUrl,
|
|
||||||
proxyUsername,
|
|
||||||
setProxyEnabled,
|
|
||||||
verifyHostSSL,
|
|
||||||
verifyPeerSSL,
|
|
||||||
verifyProxyHostSSL,
|
|
||||||
verifyProxySSL,
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<HeaderPage>
|
<HeaderPage>
|
||||||
|
|||||||
@ -247,32 +247,4 @@ describe('models service', () => {
|
|||||||
await expect(startModel(provider, model)).resolves.toBe(undefined)
|
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
|
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