feat: enhance proxy management and configuration (#8164)

* feat: enhance proxy management and configuration

- Added support for new proxy modes and improved proxy configuration handling.
- Replaced AxiosProxy with direct axios usage for HTTP requests.
- Introduced fetch-socks and undici for better proxy handling.
- Updated IPC and ConfigManager to accommodate new proxy settings.
- Removed deprecated AxiosProxy service to streamline codebase.

* format code

* feat: improve proxy configuration and monitoring

- Introduced a new mechanism to monitor system proxy changes and update configurations accordingly.
- Enhanced the configureProxy method to prevent concurrent executions and added error logging with electron-log.
- Refactored proxy handling logic to streamline the setting of global and session proxies.
- Removed deprecated methods related to proxy management for cleaner code.

* update yarn.lock

* fix: update proxy configuration logic to handle direct mode

- Modified the app's ready event to check for 'direct' mode before configuring the proxy.
- Ensured that the proxy configuration is only applied when necessary, improving efficiency.

* feat: enhance proxy configuration to support authentication

- Added userId and password fields to the proxy configuration for SOCKS connections.
- Improved handling of proxy credentials to allow for authenticated proxy usage.

* refactor: remove deprecated proxy methods and streamline configuration logic

- Eliminated the setProxy and getProxy methods from ConfigManager to simplify the proxy configuration process.
- Updated ProxyManager to initialize with a default proxy configuration and removed unnecessary checks for 'direct' mode during initialization.
- Enhanced logging for proxy configuration changes to improve traceability.

* format code

* feat: enhance WebDav and ProxyManager for self-signed certificate support

- Added handling for self-signed certificates in ProxyManager to allow secure connections with custom agents.
- Updated WebDav configuration to include an https.Agent with rejectUnauthorized set to false, facilitating connections to servers with self-signed certificates.

* delete global setting for rejectUnauthorized
This commit is contained in:
beyondkmp 2025-07-16 21:46:06 +08:00 committed by GitHub
parent 8384bbfc0a
commit 2d6c05e962
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 241 additions and 131 deletions

View File

@ -177,6 +177,7 @@
"eslint-plugin-unused-imports": "^4.1.4",
"fast-diff": "^1.3.0",
"fast-xml-parser": "^5.2.0",
"fetch-socks": "1.3.2",
"franc-min": "^6.2.0",
"fs-extra": "^11.2.0",
"google-auth-library": "^9.15.1",
@ -229,6 +230,7 @@
"tiny-pinyin": "^1.3.2",
"tokenx": "^1.1.0",
"typescript": "^5.6.2",
"undici": "7.10.0",
"unified": "^11.0.5",
"uuid": "^10.0.0",
"vite": "6.2.6",

View File

@ -8,7 +8,7 @@ import { handleZoomFactor } from '@main/utils/zoom'
import { UpgradeChannel } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types'
import { BrowserWindow, dialog, ipcMain, session, shell, systemPreferences, webContents } from 'electron'
import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron'
import log from 'electron-log'
import { Notification } from 'src/renderer/src/types/notification'
@ -27,7 +27,7 @@ import MemoryService from './services/memory/MemoryService'
import NotificationService from './services/NotificationService'
import * as NutstoreService from './services/NutstoreService'
import ObsidianVaultService from './services/ObsidianVaultService'
import { ProxyConfig, proxyManager } from './services/ProxyManager'
import { proxyManager } from './services/ProxyManager'
import { pythonService } from './services/PythonService'
import { FileServiceManager } from './services/remotefile/FileServiceManager'
import { searchService } from './services/SearchService'
@ -78,9 +78,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
if (proxy === 'system') {
proxyConfig = { mode: 'system' }
} else if (proxy) {
proxyConfig = { mode: 'custom', url: proxy }
proxyConfig = { mode: 'fixed_servers', proxyRules: proxy }
} else {
proxyConfig = { mode: 'none' }
proxyConfig = { mode: 'direct' }
}
await proxyManager.configureProxy(proxyConfig)

View File

@ -1,6 +1,6 @@
import { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import AxiosProxy from '@main/services/AxiosProxy'
import { KnowledgeBaseParams } from '@types'
import axios from 'axios'
import BaseReranker from './BaseReranker'
@ -15,7 +15,7 @@ export default class GeneralReranker extends BaseReranker {
const requestBody = this.getRerankRequestBody(query, searchResults)
try {
const { data } = await AxiosProxy.axios.post(url, requestBody, { headers: this.defaultHeaders() })
const { data } = await axios.post(url, requestBody, { headers: this.defaultHeaders() })
const rerankResults = this.extractRerankResult(data)
return this.getRerankResult(searchResults, rerankResults)

View File

@ -1,29 +0,0 @@
import { AxiosInstance, default as axios_ } from 'axios'
import { ProxyAgent } from 'proxy-agent'
import { proxyManager } from './ProxyManager'
class AxiosProxy {
private cacheAxios: AxiosInstance | null = null
private proxyAgent: ProxyAgent | null = null
get axios(): AxiosInstance {
const currentProxyAgent = proxyManager.getProxyAgent()
// 如果代理发生变化或尚未初始化,则重新创建 axios 实例
if (this.cacheAxios === null || (currentProxyAgent !== null && this.proxyAgent !== currentProxyAgent)) {
this.proxyAgent = currentProxyAgent
// 创建带有代理配置的 axios 实例
this.cacheAxios = axios_.create({
proxy: false,
httpAgent: currentProxyAgent || undefined,
httpsAgent: currentProxyAgent || undefined
})
}
return this.cacheAxios
}
}
export default new AxiosProxy()

View File

@ -25,7 +25,8 @@ export enum ConfigKeys {
SelectionAssistantRemeberWinSize = 'selectionAssistantRemeberWinSize',
SelectionAssistantFilterMode = 'selectionAssistantFilterMode',
SelectionAssistantFilterList = 'selectionAssistantFilterList',
DisableHardwareAcceleration = 'disableHardwareAcceleration'
DisableHardwareAcceleration = 'disableHardwareAcceleration',
Proxy = 'proxy'
}
export class ConfigManager {

View File

@ -1,11 +1,10 @@
import { AxiosRequestConfig } from 'axios'
import axios from 'axios'
import { app, safeStorage } from 'electron'
import Logger from 'electron-log'
import fs from 'fs/promises'
import path from 'path'
import aoxisProxy from './AxiosProxy'
// 配置常量,集中管理
const CONFIG = {
GITHUB_CLIENT_ID: 'Iv1.b507a08c87ecfe98',
@ -96,7 +95,7 @@ class CopilotService {
}
}
const response = await aoxisProxy.axios.get(CONFIG.API_URLS.GITHUB_USER, config)
const response = await axios.get(CONFIG.API_URLS.GITHUB_USER, config)
return {
login: response.data.login,
avatar: response.data.avatar_url
@ -117,7 +116,7 @@ class CopilotService {
try {
this.updateHeaders(headers)
const response = await aoxisProxy.axios.post<AuthResponse>(
const response = await axios.post<AuthResponse>(
CONFIG.API_URLS.GITHUB_DEVICE_CODE,
{
client_id: CONFIG.GITHUB_CLIENT_ID,
@ -149,7 +148,7 @@ class CopilotService {
await this.delay(currentDelay)
try {
const response = await aoxisProxy.axios.post<TokenResponse>(
const response = await axios.post<TokenResponse>(
CONFIG.API_URLS.GITHUB_ACCESS_TOKEN,
{
client_id: CONFIG.GITHUB_CLIENT_ID,
@ -211,7 +210,7 @@ class CopilotService {
}
}
const response = await aoxisProxy.axios.get<CopilotTokenResponse>(CONFIG.API_URLS.COPILOT_TOKEN, config)
const response = await axios.get<CopilotTokenResponse>(CONFIG.API_URLS.COPILOT_TOKEN, config)
return response.data
} catch (error) {

View File

@ -1,38 +1,54 @@
import { ProxyConfig as _ProxyConfig, session } from 'electron'
import axios from 'axios'
import { app, ProxyConfig, session } from 'electron'
import Logger from 'electron-log'
import { socksDispatcher } from 'fetch-socks'
import http from 'http'
import https from 'https'
import { getSystemProxy } from 'os-proxy-config'
import { ProxyAgent as GeneralProxyAgent } from 'proxy-agent'
// import { ProxyAgent, setGlobalDispatcher } from 'undici'
type ProxyMode = 'system' | 'custom' | 'none'
export interface ProxyConfig {
mode: ProxyMode
url?: string
}
import { ProxyAgent } from 'proxy-agent'
import { Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from 'undici'
export class ProxyManager {
private config: ProxyConfig
private proxyAgent: GeneralProxyAgent | null = null
private config: ProxyConfig = { mode: 'direct' }
private systemProxyInterval: NodeJS.Timeout | null = null
private isSettingProxy = false
private originalGlobalDispatcher: Dispatcher
private originalSocksDispatcher: Dispatcher
// for http and https
private originalHttpGet: typeof http.get
private originalHttpRequest: typeof http.request
private originalHttpsGet: typeof https.get
private originalHttpsRequest: typeof https.request
constructor() {
this.config = {
mode: 'none'
}
}
private async setSessionsProxy(config: _ProxyConfig): Promise<void> {
const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
await Promise.all(sessions.map((session) => session.setProxy(config)))
this.originalGlobalDispatcher = getGlobalDispatcher()
this.originalSocksDispatcher = global[Symbol.for('undici.globalDispatcher.1')]
this.originalHttpGet = http.get
this.originalHttpRequest = http.request
this.originalHttpsGet = https.get
this.originalHttpsRequest = https.request
}
private async monitorSystemProxy(): Promise<void> {
// Clear any existing interval first
this.clearSystemProxyMonitor()
// Set new interval
this.systemProxyInterval = setInterval(async () => {
await this.setSystemProxy()
}, 10000)
this.systemProxyInterval = setInterval(
async () => {
const currentProxy = await getSystemProxy()
if (currentProxy && currentProxy.proxyUrl.toLowerCase() === this.config.proxyRules) {
return
}
await this.configureProxy({
mode: 'system',
proxyRules: currentProxy?.proxyUrl.toLowerCase()
})
},
// 1 minutes
1000 * 60
)
}
private clearSystemProxyMonitor(): void {
@ -43,99 +59,182 @@ export class ProxyManager {
}
async configureProxy(config: ProxyConfig): Promise<void> {
Logger.info('configureProxy', config.mode, config.proxyRules)
if (this.isSettingProxy) {
return
}
this.isSettingProxy = true
try {
if (config?.mode === this.config?.mode && config?.proxyRules === this.config?.proxyRules) {
Logger.info('proxy config is the same, skip configure')
return
}
this.config = config
this.clearSystemProxyMonitor()
if (this.config.mode === 'system') {
await this.setSystemProxy()
this.monitorSystemProxy()
} else if (this.config.mode === 'custom') {
await this.setCustomProxy()
} else {
await this.clearProxy()
if (config.mode === 'system') {
const currentProxy = await getSystemProxy()
if (currentProxy) {
Logger.info('current system proxy', currentProxy.proxyUrl)
this.config.proxyRules = currentProxy.proxyUrl.toLowerCase()
this.monitorSystemProxy()
} else {
// no system proxy, use direct mode
this.config.mode = 'direct'
}
}
this.setGlobalProxy()
} catch (error) {
console.error('Failed to config proxy:', error)
Logger.error('Failed to config proxy:', error)
throw error
} finally {
this.isSettingProxy = false
}
}
private setEnvironment(url: string): void {
if (url === '') {
delete process.env.HTTP_PROXY
delete process.env.HTTPS_PROXY
delete process.env.grpc_proxy
delete process.env.http_proxy
delete process.env.https_proxy
delete process.env.SOCKS_PROXY
delete process.env.ALL_PROXY
return
}
process.env.grpc_proxy = url
process.env.HTTP_PROXY = url
process.env.HTTPS_PROXY = url
process.env.http_proxy = url
process.env.https_proxy = url
}
private async setSystemProxy(): Promise<void> {
try {
const currentProxy = await getSystemProxy()
if (!currentProxy || currentProxy.proxyUrl === this.config.url) {
return
}
await this.setSessionsProxy({ mode: 'system' })
this.config.url = currentProxy.proxyUrl.toLowerCase()
this.setEnvironment(this.config.url)
this.proxyAgent = new GeneralProxyAgent()
} catch (error) {
console.error('Failed to set system proxy:', error)
throw error
if (url.startsWith('socks')) {
process.env.SOCKS_PROXY = url
process.env.ALL_PROXY = url
}
}
private async setCustomProxy(): Promise<void> {
try {
if (this.config.url) {
this.setEnvironment(this.config.url)
this.proxyAgent = new GeneralProxyAgent()
await this.setSessionsProxy({ proxyRules: this.config.url })
private setGlobalProxy() {
this.setEnvironment(this.config.proxyRules || '')
this.setGlobalFetchProxy(this.config)
this.setSessionsProxy(this.config)
this.setGlobalHttpProxy(this.config)
}
private setGlobalHttpProxy(config: ProxyConfig) {
const proxyUrl = config.proxyRules
if (config.mode === 'direct' || !proxyUrl) {
http.get = this.originalHttpGet
http.request = this.originalHttpRequest
https.get = this.originalHttpsGet
https.request = this.originalHttpsRequest
axios.defaults.proxy = undefined
axios.defaults.httpAgent = undefined
axios.defaults.httpsAgent = undefined
return
}
// ProxyAgent 从环境变量读取代理配置
const agent = new ProxyAgent()
// axios 使用代理
axios.defaults.proxy = false
axios.defaults.httpAgent = agent
axios.defaults.httpsAgent = agent
http.get = this.bindHttpMethod(this.originalHttpGet, agent)
http.request = this.bindHttpMethod(this.originalHttpRequest, agent)
https.get = this.bindHttpMethod(this.originalHttpsGet, agent)
https.request = this.bindHttpMethod(this.originalHttpsRequest, agent)
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
private bindHttpMethod(originalMethod: Function, agent: http.Agent | https.Agent) {
return (...args: any[]) => {
let url: string | URL | undefined
let options: http.RequestOptions | https.RequestOptions
let callback: (res: http.IncomingMessage) => void
if (typeof args[0] === 'string' || args[0] instanceof URL) {
url = args[0]
if (typeof args[1] === 'function') {
options = {}
callback = args[1]
} else {
options = {
...args[1]
}
callback = args[2]
}
} else {
options = {
...args[0]
}
callback = args[1]
}
} catch (error) {
console.error('Failed to set custom proxy:', error)
throw error
// for webdav https self-signed certificate
if (options.agent instanceof https.Agent) {
;(agent as https.Agent).options.rejectUnauthorized = options.agent.options.rejectUnauthorized
}
// 确保只设置 agent不修改其他网络选项
if (!options.agent) {
options.agent = agent
}
if (url) {
return originalMethod(url, options, callback)
}
return originalMethod(options, callback)
}
}
private clearEnvironment(): void {
delete process.env.HTTP_PROXY
delete process.env.HTTPS_PROXY
delete process.env.grpc_proxy
delete process.env.http_proxy
delete process.env.https_proxy
private setGlobalFetchProxy(config: ProxyConfig) {
const proxyUrl = config.proxyRules
if (config.mode === 'direct' || !proxyUrl) {
setGlobalDispatcher(this.originalGlobalDispatcher)
global[Symbol.for('undici.globalDispatcher.1')] = this.originalSocksDispatcher
return
}
const url = new URL(proxyUrl)
if (url.protocol === 'http:' || url.protocol === 'https:') {
setGlobalDispatcher(new EnvHttpProxyAgent())
return
}
global[Symbol.for('undici.globalDispatcher.1')] = socksDispatcher({
port: parseInt(url.port),
type: url.protocol === 'socks4:' ? 4 : 5,
host: url.hostname,
userId: url.username || undefined,
password: url.password || undefined
})
}
private async clearProxy(): Promise<void> {
this.clearEnvironment()
await this.setSessionsProxy({ mode: 'direct' })
this.config = { mode: 'none' }
this.proxyAgent = null
}
private async setSessionsProxy(config: ProxyConfig): Promise<void> {
let c = config
getProxyAgent(): GeneralProxyAgent | null {
return this.proxyAgent
}
if (config.mode === 'direct' || !config.proxyRules) {
c = { mode: 'direct' }
}
getProxyUrl(): string {
return this.config.url || ''
}
const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
await Promise.all(sessions.map((session) => session.setProxy(c)))
// setGlobalProxy() {
// const proxyUrl = this.config.url
// if (proxyUrl) {
// const [protocol, address] = proxyUrl.split('://')
// const [host, port] = address.split(':')
// if (!protocol.includes('socks')) {
// setGlobalDispatcher(new ProxyAgent(proxyUrl))
// } else {
// global[Symbol.for('undici.globalDispatcher.1')] = socksDispatcher({
// port: parseInt(port),
// type: protocol === 'socks5' ? 5 : 4,
// host: host
// })
// }
// }
// }
// set proxy for electron
app.setProxy(c)
}
}
export const proxyManager = new ProxyManager()

View File

@ -23,7 +23,9 @@ export default class WebDav {
password: params.webdavPass,
maxBodyLength: Infinity,
maxContentLength: Infinity,
httpsAgent: new https.Agent({ rejectUnauthorized: false })
httpsAgent: new https.Agent({
rejectUnauthorized: false
})
})
this.putFileContents = this.putFileContents.bind(this)

View File

@ -7182,6 +7182,7 @@ __metadata:
eslint-plugin-unused-imports: "npm:^4.1.4"
fast-diff: "npm:^1.3.0"
fast-xml-parser: "npm:^5.2.0"
fetch-socks: "npm:1.3.2"
franc-min: "npm:^6.2.0"
fs-extra: "npm:^11.2.0"
google-auth-library: "npm:^9.15.1"
@ -7245,6 +7246,7 @@ __metadata:
tokenx: "npm:^1.1.0"
turndown: "npm:7.2.0"
typescript: "npm:^5.6.2"
undici: "npm:7.10.0"
unified: "npm:^11.0.5"
uuid: "npm:^10.0.0"
vite: "npm:6.2.6"
@ -11312,6 +11314,16 @@ __metadata:
languageName: node
linkType: hard
"fetch-socks@npm:1.3.2":
version: 1.3.2
resolution: "fetch-socks@npm:1.3.2"
dependencies:
socks: "npm:^2.8.2"
undici: "npm:>=6"
checksum: 10c0/6a3f20142c82d3eaef0bfe6b53a0af61381ffbe8bfeb1fdfe5c285c863f9648159ba5ab9b771fac6d3c726e0b894ba52e1069947de0ec97dc287645b40e5d24c
languageName: node
linkType: hard
"fflate@npm:0.8.1":
version: 0.8.1
resolution: "fflate@npm:0.8.1"
@ -18578,6 +18590,16 @@ __metadata:
languageName: node
linkType: hard
"socks@npm:^2.8.2":
version: 2.8.6
resolution: "socks@npm:2.8.6"
dependencies:
ip-address: "npm:^9.0.5"
smart-buffer: "npm:^4.2.0"
checksum: 10c0/15b95db4caa359c80bfa880ff3e58f3191b9ffa4313570e501a60ee7575f51e4be664a296f4ee5c2c40544da179db6140be53433ce41ec745f9d51f342557514
languageName: node
linkType: hard
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1":
version: 1.2.1
resolution: "source-map-js@npm:1.2.1"
@ -19597,6 +19619,20 @@ __metadata:
languageName: node
linkType: hard
"undici@npm:7.10.0":
version: 7.10.0
resolution: "undici@npm:7.10.0"
checksum: 10c0/756ac876a8df845bc89eb8348c35d33a0ff63c17eb45b664075c961a7fbd4a398f94f9dce438262f55fe66e4bbb0a46aa63a3fd58ce51361c616aff11a270450
languageName: node
linkType: hard
"undici@npm:>=6":
version: 7.11.0
resolution: "undici@npm:7.11.0"
checksum: 10c0/e5dd3cc2acae9c8333f97a78d4e91108957367fa7e69918e3a5cbd84702cb453cf7de3f8c2a33bcf808850d78ead70f3bd62900a70d969912e9fed8842bbfc11
languageName: node
linkType: hard
"unified@npm:^11.0.0, unified@npm:^11.0.5":
version: 11.0.5
resolution: "unified@npm:11.0.5"