Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10,266 changes: 4,649 additions & 5,617 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

105 changes: 100 additions & 5 deletions src/entrypoints/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
getNetworkProxyProfile,
isPopupShortcutTab,
migrateLocalToSync,
networkFeaturesEnabled,
networkMode,
networkProxyBypassList,
networkProxyManaged,
Expand Down Expand Up @@ -565,7 +566,11 @@ function syncMicrosoftRequestRulesSafely() {

/** 按开关状态安装 / 移除 CORS 动态规则。 */
async function syncCorsBypassRules(): Promise<boolean> {
const enabled = await enableCorsBypass.getValue()
const [corsEnabled, featuresEnabled] = await Promise.all([
enableCorsBypass.getValue(),
networkFeaturesEnabled.getValue(),
])
const enabled = corsEnabled && featuresEnabled

await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [CORS_BYPASS_RULE_ID],
Expand Down Expand Up @@ -634,6 +639,19 @@ function setChromeProxyConfig(config: chrome.proxy.ProxyConfig): Promise<void> {
})
}

function clearChromeProxyConfig(): Promise<void> {
return new Promise((resolve, reject) => {
getProxySettings().clear({ scope: 'regular' }, () => {
const error = chromeLastError()
if (error) {
reject(error)
return
}
resolve()
})
})
}

function getChromeProxyConfig(): Promise<chrome.types.ChromeSettingGetResultDetails> {
return new Promise((resolve, reject) => {
getProxySettings().get({ incognito: false }, (details) => {
Expand Down Expand Up @@ -696,16 +714,21 @@ function buildNetworkProxyConfig(
}

async function getNetworkStatus(error?: unknown): Promise<NetworkProxyStatus> {
const [mode, managed] = await Promise.all([
const [mode, managed, enabled, corsEnabled] = await Promise.all([
networkMode.getValue(),
networkProxyManaged.getValue(),
networkFeaturesEnabled.getValue(),
enableCorsBypass.getValue(),
])
const corsActive = corsEnabled && enabled

if (error) {
return {
ok: false,
mode,
managed,
enabled,
corsActive,
error: error instanceof Error ? error.message : String(error),
}
}
Expand All @@ -716,19 +739,75 @@ async function getNetworkStatus(error?: unknown): Promise<NetworkProxyStatus> {
ok: true,
mode,
managed,
enabled,
corsActive,
levelOfControl: details.levelOfControl,
}
} catch (statusError) {
return {
ok: false,
mode,
managed,
enabled,
corsActive,
error: statusError instanceof Error ? statusError.message : String(statusError),
}
}
}

async function releaseNetworkFeatures(): Promise<NetworkProxyStatus> {
await networkFeaturesEnabled.setValue(false)
await networkProxyManaged.setValue(false)

try {
await clearChromeProxyConfig()
await syncCorsBypassRules()
updateNetworkActionIconSafely()
return getNetworkStatus()
} catch (releaseError) {
await syncCorsBypassRules().catch(() => false)
updateNetworkActionIconSafely()
return getNetworkStatus(releaseError)
}
}

async function applyNetworkFeatures(): Promise<NetworkProxyStatus> {
await networkFeaturesEnabled.setValue(true)
await networkProxyManaged.setValue(true)

try {
const [mode, profile, ruleList] = await Promise.all([
networkMode.getValue(),
getNetworkProxyProfile(),
networkRuleList.getValue(),
])
await setChromeProxyConfig(buildNetworkProxyConfig(mode, profile, ruleList))
await syncCorsBypassRules()
updateNetworkActionIconSafely()
return getNetworkStatus()
} catch (error) {
return getNetworkStatus(error)
}
}

async function setNetworkFeaturesEnabled(enabled: boolean): Promise<NetworkProxyStatus> {
if (enabled) {
return applyNetworkFeatures()
}
return releaseNetworkFeatures()
}

function syncNetworkFeaturesSafely() {
networkFeaturesEnabled
.getValue()
.then((enabled) => setNetworkFeaturesEnabled(enabled))
.catch((error) => {
console.warn('[DevGo] sync network features failed:', error)
})
}

async function applyNetworkMode(mode: NetworkMode): Promise<NetworkProxyStatus> {
await networkFeaturesEnabled.setValue(true)
await networkMode.setValue(mode)
await networkProxyManaged.setValue(true)

Expand All @@ -738,16 +817,20 @@ async function applyNetworkMode(mode: NetworkMode): Promise<NetworkProxyStatus>
networkRuleList.getValue(),
])
await setChromeProxyConfig(buildNetworkProxyConfig(mode, profile, ruleList))
updateNetworkActionIconSafely()
return getNetworkStatus()
} catch (error) {
return getNetworkStatus(error)
}
}

async function syncNetworkProxy(): Promise<NetworkProxyStatus> {
const managed = await networkProxyManaged.getValue()
const [managed, featuresEnabled] = await Promise.all([
networkProxyManaged.getValue(),
networkFeaturesEnabled.getValue(),
])

if (!managed) {
if (!managed || !featuresEnabled) {
return getNetworkStatus()
}

Expand Down Expand Up @@ -831,7 +914,12 @@ async function downloadNetworkRuleList(url: string): Promise<NetworkRuleListDown
}

async function proxyCorsRequest(request: CorsProxyRequest): Promise<CorsProxyResponse> {
if (!(await enableCorsBypass.getValue())) {
const [corsEnabled, featuresEnabled] = await Promise.all([
enableCorsBypass.getValue(),
networkFeaturesEnabled.getValue(),
])

if (!corsEnabled || !featuresEnabled) {
return {
url: request.url,
status: 0,
Expand Down Expand Up @@ -1068,6 +1156,11 @@ export default defineBackground(() => {
return true
}

if (message?.type === 'set-network-features-enabled') {
setNetworkFeaturesEnabled(message.enabled).then(sendResponse)
return true
}

if (message?.type === 'download-network-rule-list') {
downloadNetworkRuleList(message.url).then(sendResponse)
return true
Expand Down Expand Up @@ -1170,6 +1263,7 @@ export default defineBackground(() => {
})

enableCorsBypass.watch(syncCorsBypassRulesSafely)
networkFeaturesEnabled.watch(syncNetworkFeaturesSafely)
networkMode.watch(syncNetworkProxySafely)
networkProxyProfile.watch(syncNetworkProxySafely)
networkProxyBypassList.watch(syncNetworkProxySafely)
Expand All @@ -1179,6 +1273,7 @@ export default defineBackground(() => {
updateNetworkActionIconSafely()
networkMode.watch(updateNetworkActionIconSafely)
networkProxyManaged.watch(updateNetworkActionIconSafely)
networkFeaturesEnabled.watch(updateNetworkActionIconSafely)

chrome.proxy?.onProxyError?.addListener((details) => {
console.warn('[DevGo] proxy error:', details)
Expand Down
40 changes: 29 additions & 11 deletions src/entrypoints/cors-bridge.content.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { injectScript, type ScriptPublicPath } from 'wxt/utils/inject-script'
import { sendRuntimeMessage, type CorsProxyRequest } from '@/utils/messaging'
import { enableCorsBypass } from '@/utils/settings'
import { enableCorsBypass, networkFeaturesEnabled } from '@/utils/settings'

const PAGE_SOURCE = 'devgo-cors-proxy-page'
const CONTENT_SOURCE = 'devgo-cors-proxy-content'
Expand Down Expand Up @@ -33,6 +33,14 @@ function postEnabled(enabled: boolean) {
)
}

async function isCorsRuntimeEnabled(): Promise<boolean> {
const [corsEnabled, featuresEnabled] = await Promise.all([
enableCorsBypass.getValue(),
networkFeaturesEnabled.getValue(),
])
return corsEnabled && featuresEnabled
}

export default defineContentScript({
matches: ['<all_urls>'],
runAt: 'document_start',
Expand Down Expand Up @@ -100,25 +108,35 @@ export default defineContentScript({
})
}

// 文档开始阶段读取一次开关:开启则立即注入,以尽量赶在页面脚本之前接管 fetch/XHR。
if (await enableCorsBypass.getValue()) {
await ensureInjected()
postEnabled(true)
}

const unwatch = enableCorsBypass.watch(async (enabled) => {
if (enabled) {
async function syncCorsRuntimeState() {
if (await isCorsRuntimeEnabled()) {
// 运行时才开启:注入后接管的是「此刻之后」发起的请求;页面已缓存的
// fetch/XHR 引用不受影响,必要时提示用户刷新页面。
await ensureInjected()
postEnabled(true)
} else if (injected) {
return
}

if (injected) {
// 关闭:无法移除已注入的页面脚本,但通过状态消息让其变为原生透传,
// 不再代理任何请求。彻底回到原生环境需刷新页面。
postEnabled(false)
}
}

// 文档开始阶段读取一次开关:开启则立即注入,以尽量赶在页面脚本之前接管 fetch/XHR。
await syncCorsRuntimeState()

const unwatchCors = enableCorsBypass.watch(() => {
void syncCorsRuntimeState()
})
const unwatchFeatures = networkFeaturesEnabled.watch(() => {
void syncCorsRuntimeState()
})

ctx.onInvalidated(() => unwatch())
ctx.onInvalidated(() => {
unwatchCors()
unwatchFeatures()
})
},
})
50 changes: 50 additions & 0 deletions src/features/popup/NetworkFeaturesSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useEffect, useState } from 'react'

import { sendRuntimeMessage } from '@/utils/messaging'
import { networkFeaturesEnabled } from '@/utils/settings'
import Switch from '@/ui/Switch'

export default function NetworkFeaturesSwitch() {
const [enabled, setEnabled] = useState(false)
const [busy, setBusy] = useState(false)

useEffect(() => {
let active = true

networkFeaturesEnabled.getValue().then((value) => {
if (active) setEnabled(value)
})

const unwatch = networkFeaturesEnabled.watch((value) => {
if (active) setEnabled(value)
})

return () => {
active = false
unwatch()
}
}, [])

const toggle = async (checked: boolean) => {
if (busy) return

const previous = enabled
setEnabled(checked)
setBusy(true)

try {
const status = await sendRuntimeMessage({
type: 'set-network-features-enabled',
enabled: checked,
})
setEnabled(status.enabled ?? checked)
} catch (error) {
console.warn('[DevGo] toggle network features failed:', error)
setEnabled(previous)
} finally {
setBusy(false)
}
}

return <Switch checked={enabled} disabled={busy} onChange={toggle} />
}
Loading