feat: support setting proxy for app (#452)

* feat: implement proxy configuration management

* feat: add proxy configuration setup during application boot

* refactpr: update proxy configuration handling

* feat: add custom hook for proxy configuration management

* feat: add proxy configuration settings to general settings

* fix: conditionally render NettingSetting based on electron env

* fix: enhance proxy URL validation to check hostname and port

* chore: clean code

* chore: update Chinese localization

* fix: update import path for tipcClient in useProxySetting hook
This commit is contained in:
Whitewater 2024-09-19 15:52:51 +08:00 committed by GitHub
parent 324eee23cb
commit cfd527545a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 144 additions and 0 deletions

View File

@ -7,6 +7,7 @@ import squirrelStartup from "electron-squirrel-startup"
import { isDev, isMacOS } from "./env"
import { initializeAppStage0, initializeAppStage1 } from "./init"
import { updateProxy } from "./lib/proxy"
import { setAuthSessionToken } from "./lib/user"
import { registerUpdater } from "./updater"
import { createMainWindow, createWindow } from "./window"
@ -52,6 +53,7 @@ function bootstrap() {
mainWindow = createMainWindow()
updateProxy()
registerUpdater()
//remove Electron, Follow from user agent

View File

@ -0,0 +1,76 @@
import { session } from "electron"
import { logger } from "../logger"
import { store } from "./store"
// Sets up the proxy configuration for the app.
//
// See https://www.electronjs.org/docs/latest/api/session#sessetproxyconfig
// for more information about the proxy API.
//
// The open-source project [poooi/poi](https://github.com/poooi/poi) is doing well in proxy configuration
// refer the following files for more details:
//
// https://github.com/poooi/poi/blob/5741d0d02c0a08626dd53196b094223457014491/lib/proxy.ts#L36
// https://github.com/poooi/poi/blob/5741d0d02c0a08626dd53196b094223457014491/views/components/settings/network/index.es
export const setProxyConfig = (inputProxy: string) => {
const proxyUri = normalizeProxyUri(inputProxy)
if (!proxyUri) {
return false
}
store.set("proxy", inputProxy)
return true
}
export const getProxyConfig = () => {
const proxyConfig = store.get("proxy") as string | undefined
if (!proxyConfig) {
return
}
const proxyUri = normalizeProxyUri(proxyConfig)
return proxyUri
}
const URL_SCHEME = new Set(["http:", "https:", "ftp:", "socks:", "socks4:", "socks5:"])
const normalizeProxyUri = (userProxy: string) => {
if (!userProxy) {
return
}
try {
const proxyUrl = new URL(userProxy)
if (!URL_SCHEME.has(proxyUrl.protocol) || !proxyUrl.hostname || !proxyUrl.port) {
return
}
// There are multiple ways to specify a proxy in Electron,
// but for security reasons, we only support simple proxy URLs for now.
return [
`${proxyUrl.protocol}//${proxyUrl.hostname}:${proxyUrl.port}`,
// Failing over to using no proxy if the proxy is unavailable
"direct://",
].join(",")
} catch {
return
}
}
const BYPASS_RULES = ["<local>"].join(";")
export const updateProxy = () => {
const proxyUri = getProxyConfig()
if (!proxyUri) {
session.defaultSession.setProxy({
// Note that the system mode is different from setting no proxy configuration.
// In the latter case, Electron falls back to the system settings only if no command-line options influence the proxy configuration.
mode: "system",
})
return
}
logger.log(`Loading proxy: ${proxyUri}`)
session.defaultSession.setProxy({
proxyRules: proxyUri,
proxyBypassRules: BYPASS_RULES,
})
}

View File

@ -3,6 +3,7 @@ import { createRequire } from "node:module"
import { app, nativeTheme } from "electron"
import { setDockCount } from "../lib/dock"
import { setProxyConfig, updateProxy } from "../lib/proxy"
import { store } from "../lib/store"
import { createSettingWindow } from "../window"
import { t } from "./_instance"
@ -39,4 +40,10 @@ export const settingRoute = {
setDockBadge: t.procedure.input<number>().action(async ({ input }) => {
setDockCount(input)
}),
getProxyConfig: t.procedure.action(async () => store.get("proxy")),
setProxyConfig: t.procedure.input<string>().action(async ({ input }) => {
const result = setProxyConfig(input)
updateProxy()
return result
}),
}

View File

@ -0,0 +1,28 @@
import { atom, useAtomValue, useSetAtom } from "jotai"
import { useCallback } from "react"
import { tipcClient } from "~/lib/client"
const proxyAtom = atom("")
proxyAtom.onMount = (setAtom) => {
tipcClient?.getProxyConfig().then((proxy: string) => {
setAtom(proxy)
})
}
export const useProxyValue = () => useAtomValue(proxyAtom)
export const useSetProxy = () => {
const setProxy = useSetAtom(proxyAtom)
return useCallback(
(proxyString: string) => {
if (!window.electron) {
return
}
setProxy(proxyString)
tipcClient?.setProxyConfig(proxyString)
},
[setProxy],
)
}

View File

@ -22,12 +22,15 @@ import {
SelectTrigger,
SelectValue,
} from "~/components/ui/select"
import { useProxyValue, useSetProxy } from "~/hooks/common/useProxySetting"
import { fallbackLanguage } from "~/i18n"
import { initPostHog } from "~/initialize/posthog"
import { tipcClient } from "~/lib/client"
import { cn } from "~/lib/utils"
import { clearLocalPersistStoreData } from "~/store/utils/clear"
import { SettingDescription, SettingInput } from "../control"
import { SettingItemGroup } from "../section"
import { SettingsTitle } from "../title"
const { defineSettingItem, SettingBuilder } = createSetting(
@ -158,6 +161,9 @@ export const SettingGeneral = () => {
description: t("general.rebuild_database.description"),
buttonText: t("general.rebuild_database.button"),
},
{ type: "title", value: t("general.network"), disabled: !window.electron },
window.electron && NettingSetting,
]}
/>
</div>
@ -244,3 +250,22 @@ export const LanguageSelector = () => {
</div>
)
}
const NettingSetting = () => {
const { t } = useTranslation("settings")
const proxyConfig = useProxyValue()
const setProxyConfig = useSetProxy()
return (
<SettingItemGroup>
<SettingInput
type="text"
label={t("general.proxy")}
labelClassName="w-[150px]"
value={proxyConfig}
onChange={(event) => setProxyConfig(event.target.value.trim())}
/>
<SettingDescription>{t("general.proxy.description")}</SettingDescription>
</SettingItemGroup>
)
}

View File

@ -92,7 +92,10 @@
"general.mark_as_read.render.label": "Mark as read when in the view",
"general.mark_as_read.scroll.description": "Automatically mark entries as read when scrolled out of the view.",
"general.mark_as_read.scroll.label": "Mark as read when scrolling",
"general.network": "Network",
"general.privacy_data": "Privacy & Data",
"general.proxy": "Proxy",
"general.proxy.description": "Set proxy for network traffic routing, e.g., socks://proxy.example.com:1080",
"general.rebuild_database.button": "Rebuild",
"general.rebuild_database.description": "If you are experiencing rendering issues, rebuilding the database may solve them.",
"general.rebuild_database.label": "Rebuild Database",

View File

@ -92,7 +92,10 @@
"general.mark_as_read.render.label": "在窗口中时标记为已读",
"general.mark_as_read.scroll.description": "当条目滚动出窗口时自动标记为已读",
"general.mark_as_read.scroll.label": "滚动时标记为已读",
"general.network": "网络",
"general.privacy_data": "隐私与数据",
"general.proxy": "代理",
"general.proxy.description": "代理网络请求示例socks://proxy.example.com:1080",
"general.rebuild_database.button": "重建",
"general.rebuild_database.description": "尝试重建数据库可以解决部分渲染或其他类型问题",
"general.rebuild_database.label": "重建数据库",