返回 oh-my-ppt
proxy.ts
根目录 / src / main / utils / proxy.ts
1 import log from 'electron-log/main.js'
2 import type Agent from 'undici/types/agent'
3 import type Dispatcher from 'undici/types/dispatcher'
4 import {
5 Agent as UndiciAgent,
6 Dispatcher as UndiciDispatcher,
7 ProxyAgent,
8 Socks5ProxyAgent,
9 getGlobalDispatcher,
10 setGlobalDispatcher
11 } from 'undici'
12 import { containsNoProxyWildcard, matchesNoProxy, parseNoProxyEntries } from './no-proxy'
13
14 export interface ProxySettings {
15 url: string
16 username?: string
17 password?: string
18 noProxy?: string
19 }
20
21 let originalDispatcher: Dispatcher | null = null
22 let currentProxyDispatcher: Dispatcher | null = null
23
24 function isSocksUrl(url: string): boolean {
25 return url.startsWith('socks5://') || url.startsWith('socks://')
26 }
27
28 /** 命中 no_proxy 的请求直连,其余走代理。 */
29 class NoProxyAwareDispatcher extends UndiciDispatcher {
30 private readonly directDispatcher: Dispatcher
31 private readonly proxiedDispatcher: Dispatcher
32 private readonly bypassEntries: ReturnType<typeof parseNoProxyEntries>
33
34 constructor(
35 directDispatcher: Dispatcher,
36 proxiedDispatcher: Dispatcher,
37 bypassEntries: ReturnType<typeof parseNoProxyEntries>
38 ) {
39 super()
40 this.directDispatcher = directDispatcher
41 this.proxiedDispatcher = proxiedDispatcher
42 this.bypassEntries = bypassEntries
43 }
44
45 dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean {
46 const bypass = (() => {
47 try {
48 const origin = new URL(String(options.origin ?? ''))
49 const port = origin.port
50 ? Number(origin.port)
51 : origin.protocol === 'https:'
52 ? 443
53 : 80
54 return matchesNoProxy(this.bypassEntries, origin.hostname, port)
55 } catch {
56 return false
57 }
58 })()
59 return bypass
60 ? this.directDispatcher.dispatch(options, handler)
61 : this.proxiedDispatcher.dispatch(options, handler)
62 }
63
64 close(callback: () => void): void
65 close(): Promise<void>
66 close(callback?: () => void): Promise<void> | void {
67 const closing = Promise.allSettled([
68 this.directDispatcher.close(),
69 this.proxiedDispatcher.close()
70 ])
71 if (typeof callback === 'function') {
72 void closing.then(() => callback())
73 return
74 }
75 return closing.then(() => undefined)
76 }
77
78 destroy(error: Error | null, callback: () => void): void
79 destroy(callback: () => void): void
80 destroy(error: Error | null): Promise<void>
81 destroy(): Promise<void>
82 destroy(error?: Error | null | (() => void), callback?: () => void): Promise<void> | void {
83 const reason = typeof error === 'function' ? null : (error ?? null)
84 const done = typeof error === 'function' ? error : callback
85 const teardown = Promise.allSettled([
86 this.directDispatcher.destroy(reason),
87 this.proxiedDispatcher.destroy(reason)
88 ])
89 if (typeof done === 'function') {
90 void teardown.then(() => done())
91 return
92 }
93 return teardown.then(() => undefined)
94 }
95 }
96
97 export function applyProxy(settings?: ProxySettings): void {
98 if (originalDispatcher === null) {
99 originalDispatcher = getGlobalDispatcher()
100 }
101
102 const url = (settings?.url || '').trim()
103 if (!url) {
104 clearProxy()
105 return
106 }
107
108 const username = (settings?.username || '').trim()
109 const password = settings?.password || ''
110 const noProxyValue = (settings?.noProxy || '').trim()
111
112 if (containsNoProxyWildcard(noProxyValue)) {
113 log.info('[proxy] no_proxy wildcard set, all requests bypass the proxy')
114 clearProxy()
115 return
116 }
117
118 const hasAuth = username.length > 0
119 let proxiedDispatcher: Dispatcher
120 if (isSocksUrl(url)) {
121 proxiedDispatcher = new Socks5ProxyAgent(url, {
122 username: username || undefined,
123 password: password || undefined
124 })
125 } else if (hasAuth) {
126 proxiedDispatcher = new ProxyAgent({
127 uri: url,
128 token: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
129 })
130 } else {
131 proxiedDispatcher = new ProxyAgent(url)
132 }
133
134 const bypassEntries = parseNoProxyEntries(noProxyValue)
135 const dispatcher =
136 bypassEntries.length > 0
137 ? new NoProxyAwareDispatcher(new UndiciAgent(), proxiedDispatcher, bypassEntries)
138 : proxiedDispatcher
139
140 closeCurrentProxy()
141 currentProxyDispatcher = dispatcher
142 setGlobalDispatcher(dispatcher)
143 log.info('[proxy] applied', {
144 url,
145 hasAuth,
146 noProxy: noProxyValue,
147 bypassEntries: bypassEntries.length
148 })
149 }
150
151 export function clearProxy(): void {
152 if (!originalDispatcher) return
153 closeCurrentProxy()
154 setGlobalDispatcher(originalDispatcher)
155 log.info('[proxy] cleared, restored default dispatcher')
156 }
157
158 function closeCurrentProxy(): void {
159 if (currentProxyDispatcher && typeof currentProxyDispatcher.close === 'function') {
160 void currentProxyDispatcher.close().catch(() => {})
161 }
162 currentProxyDispatcher = null
163 }
164
164 lines TYPESCRIPT