mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
feat: enhance proxy bypass rules with comprehensive matching (#10817)
* feat: enhance proxy bypass rules with comprehensive matching - Add support for wildcard domains (*.example.com, .example.com) - Add CIDR notation support for IPv4 and IPv6 (192.168.0.0/16, 2001:db8::/32) - Add wildcard IP matching (192.168.1.*) - Add <local> keyword for local network hostnames - Support both semicolon and comma separators in bypass rules - Add comprehensive unit tests with 22 test cases - Export matchWildcardDomain and matchIpRule for testability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * move to devDeps * delete logs * feat: enhance ProxyManager with advanced proxy bypass rule handling - Introduced comprehensive parsing and matching for proxy bypass rules, including support for wildcard domains, CIDR notation, and local network addresses. - Refactored existing functions and added new utility methods for improved clarity and maintainability. - Updated unit tests to cover new functionality and ensure robust validation of bypass rules. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * update proxy rules * fix lint * add tips * delete hostname rule * add logs --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
bf35228b49
commit
a5049d8872
@ -280,6 +280,7 @@
|
|||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"i18next": "^23.11.5",
|
"i18next": "^23.11.5",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
|
"ipaddr.js": "^2.2.0",
|
||||||
"isbinaryfile": "5.0.4",
|
"isbinaryfile": "5.0.4",
|
||||||
"jaison": "^2.0.2",
|
"jaison": "^2.0.2",
|
||||||
"jest-styled-components": "^7.2.0",
|
"jest-styled-components": "^7.2.0",
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { app, ProxyConfig, session } from 'electron'
|
|||||||
import { socksDispatcher } from 'fetch-socks'
|
import { socksDispatcher } from 'fetch-socks'
|
||||||
import http from 'http'
|
import http from 'http'
|
||||||
import https from 'https'
|
import https from 'https'
|
||||||
|
import * as ipaddr from 'ipaddr.js'
|
||||||
import { getSystemProxy } from 'os-proxy-config'
|
import { getSystemProxy } from 'os-proxy-config'
|
||||||
import { ProxyAgent } from 'proxy-agent'
|
import { ProxyAgent } from 'proxy-agent'
|
||||||
import { Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from 'undici'
|
import { Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from 'undici'
|
||||||
@ -11,41 +12,293 @@ import { Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher
|
|||||||
const logger = loggerService.withContext('ProxyManager')
|
const logger = loggerService.withContext('ProxyManager')
|
||||||
let byPassRules: string[] = []
|
let byPassRules: string[] = []
|
||||||
|
|
||||||
const isByPass = (url: string) => {
|
type HostnameMatchType = 'exact' | 'wildcardSubdomain' | 'generalWildcard'
|
||||||
if (byPassRules.length === 0) {
|
|
||||||
|
const enum ProxyBypassRuleType {
|
||||||
|
Local = 'local',
|
||||||
|
Cidr = 'cidr',
|
||||||
|
Ip = 'ip',
|
||||||
|
Domain = 'domain'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParsedProxyBypassRule {
|
||||||
|
type: ProxyBypassRuleType
|
||||||
|
matchType: HostnameMatchType
|
||||||
|
rule: string
|
||||||
|
scheme?: string
|
||||||
|
port?: string
|
||||||
|
domain?: string
|
||||||
|
regex?: RegExp
|
||||||
|
cidr?: [ipaddr.IPv4 | ipaddr.IPv6, number]
|
||||||
|
ip?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedByPassRules: ParsedProxyBypassRule[] = []
|
||||||
|
|
||||||
|
const getDefaultPortForProtocol = (protocol: string): string | null => {
|
||||||
|
switch (protocol.toLowerCase()) {
|
||||||
|
case 'http:':
|
||||||
|
return '80'
|
||||||
|
case 'https:':
|
||||||
|
return '443'
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildWildcardRegex = (pattern: string): RegExp => {
|
||||||
|
const escapedSegments = pattern.split('*').map((segment) => segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
||||||
|
return new RegExp(`^${escapedSegments.join('.*')}$`, 'i')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isWildcardIp = (value: string): boolean => {
|
||||||
|
if (!value.includes('*')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const replaced = value.replace(/\*/g, '0')
|
||||||
|
return ipaddr.isValid(replaced)
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchHostnameRule = (hostname: string, rule: ParsedProxyBypassRule): boolean => {
|
||||||
|
const normalizedHostname = hostname.toLowerCase()
|
||||||
|
|
||||||
|
switch (rule.matchType) {
|
||||||
|
case 'exact':
|
||||||
|
return normalizedHostname === rule.domain
|
||||||
|
case 'wildcardSubdomain': {
|
||||||
|
const domain = rule.domain
|
||||||
|
if (!domain) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return normalizedHostname === domain || normalizedHostname.endsWith(`.${domain}`)
|
||||||
|
}
|
||||||
|
case 'generalWildcard':
|
||||||
|
return rule.regex ? rule.regex.test(normalizedHostname) : false
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseProxyBypassRule = (rule: string): ParsedProxyBypassRule | null => {
|
||||||
|
const trimmedRule = rule.trim()
|
||||||
|
if (!trimmedRule) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmedRule === '<local>') {
|
||||||
|
return {
|
||||||
|
type: ProxyBypassRuleType.Local,
|
||||||
|
matchType: 'exact',
|
||||||
|
rule: '<local>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let workingRule = trimmedRule
|
||||||
|
let scheme: string | undefined
|
||||||
|
const schemeMatch = workingRule.match(/^([a-zA-Z][a-zA-Z\d+\-.]*):\/\//)
|
||||||
|
if (schemeMatch) {
|
||||||
|
scheme = schemeMatch[1].toLowerCase()
|
||||||
|
workingRule = workingRule.slice(schemeMatch[0].length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CIDR notation must be processed before port extraction
|
||||||
|
if (workingRule.includes('/')) {
|
||||||
|
const cleanedCidr = workingRule.replace(/^\[|\]$/g, '')
|
||||||
|
if (ipaddr.isValidCIDR(cleanedCidr)) {
|
||||||
|
return {
|
||||||
|
type: ProxyBypassRuleType.Cidr,
|
||||||
|
matchType: 'exact',
|
||||||
|
rule: workingRule,
|
||||||
|
scheme,
|
||||||
|
cidr: ipaddr.parseCIDR(cleanedCidr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract port: supports "host:port" and "[ipv6]:port" formats
|
||||||
|
let port: string | undefined
|
||||||
|
const portMatch = workingRule.match(/^(.+?):(\d+)$/)
|
||||||
|
if (portMatch) {
|
||||||
|
// For IPv6, ensure we're not splitting inside the brackets
|
||||||
|
const potentialHost = portMatch[1]
|
||||||
|
if (!potentialHost.startsWith('[') || potentialHost.includes(']')) {
|
||||||
|
workingRule = potentialHost
|
||||||
|
port = portMatch[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedHost = workingRule.replace(/^\[|\]$/g, '')
|
||||||
|
const normalizedHost = cleanedHost.toLowerCase()
|
||||||
|
|
||||||
|
if (!cleanedHost) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ipaddr.isValid(cleanedHost)) {
|
||||||
|
return {
|
||||||
|
type: ProxyBypassRuleType.Ip,
|
||||||
|
matchType: 'exact',
|
||||||
|
rule: cleanedHost,
|
||||||
|
scheme,
|
||||||
|
port,
|
||||||
|
ip: cleanedHost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWildcardIp(cleanedHost)) {
|
||||||
|
const regexPattern = cleanedHost.replace(/\./g, '\\.').replace(/\*/g, '\\d+')
|
||||||
|
return {
|
||||||
|
type: ProxyBypassRuleType.Ip,
|
||||||
|
matchType: 'generalWildcard',
|
||||||
|
rule: cleanedHost,
|
||||||
|
scheme,
|
||||||
|
port,
|
||||||
|
regex: new RegExp(`^${regexPattern}$`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workingRule.startsWith('*.')) {
|
||||||
|
const domain = normalizedHost.slice(2)
|
||||||
|
return {
|
||||||
|
type: ProxyBypassRuleType.Domain,
|
||||||
|
matchType: 'wildcardSubdomain',
|
||||||
|
rule: workingRule,
|
||||||
|
scheme,
|
||||||
|
port,
|
||||||
|
domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workingRule.startsWith('.')) {
|
||||||
|
const domain = normalizedHost.slice(1)
|
||||||
|
return {
|
||||||
|
type: ProxyBypassRuleType.Domain,
|
||||||
|
matchType: 'wildcardSubdomain',
|
||||||
|
rule: workingRule,
|
||||||
|
scheme,
|
||||||
|
port,
|
||||||
|
domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workingRule.includes('*')) {
|
||||||
|
return {
|
||||||
|
type: ProxyBypassRuleType.Domain,
|
||||||
|
matchType: 'generalWildcard',
|
||||||
|
rule: workingRule,
|
||||||
|
scheme,
|
||||||
|
port,
|
||||||
|
regex: buildWildcardRegex(normalizedHost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: ProxyBypassRuleType.Domain,
|
||||||
|
matchType: 'exact',
|
||||||
|
rule: workingRule,
|
||||||
|
scheme,
|
||||||
|
port,
|
||||||
|
domain: normalizedHost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLocalHostname = (hostname: string): boolean => {
|
||||||
|
const normalized = hostname.toLowerCase()
|
||||||
|
if (normalized === 'localhost') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleaned = hostname.replace(/^\[|\]$/g, '')
|
||||||
|
if (ipaddr.isValid(cleaned)) {
|
||||||
|
const parsed = ipaddr.parse(cleaned)
|
||||||
|
return parsed.range() === 'loopback'
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateByPassRules = (rules: string[]): void => {
|
||||||
|
byPassRules = rules
|
||||||
|
parsedByPassRules = []
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
const parsedRule = parseProxyBypassRule(rule)
|
||||||
|
if (parsedRule) {
|
||||||
|
parsedByPassRules.push(parsedRule)
|
||||||
|
} else {
|
||||||
|
logger.warn(`Skipping invalid proxy bypass rule: ${rule}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isByPass = (url: string) => {
|
||||||
|
if (parsedByPassRules.length === 0) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const subjectUrlTokens = new URL(url)
|
const parsedUrl = new URL(url)
|
||||||
for (const rule of byPassRules) {
|
const hostname = parsedUrl.hostname
|
||||||
const ruleMatch = rule.replace(/^(?<leadingDot>\.)/, '*').match(/^(?<hostname>.+?)(?::(?<port>\d+))?$/)
|
const cleanedHostname = hostname.replace(/^\[|\]$/g, '')
|
||||||
|
const protocol = parsedUrl.protocol
|
||||||
|
const protocolName = protocol.replace(':', '').toLowerCase()
|
||||||
|
const defaultPort = getDefaultPortForProtocol(protocol)
|
||||||
|
const port = parsedUrl.port || defaultPort || ''
|
||||||
|
const hostnameIsIp = ipaddr.isValid(cleanedHostname)
|
||||||
|
|
||||||
if (!ruleMatch || !ruleMatch.groups) {
|
for (const rule of parsedByPassRules) {
|
||||||
logger.warn('Failed to parse bypass rule:', { rule })
|
if (rule.scheme && rule.scheme !== protocolName) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ruleMatch.groups.hostname) {
|
if (rule.port && rule.port !== port) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const hostnameIsMatch = subjectUrlTokens.hostname === ruleMatch.groups.hostname
|
switch (rule.type) {
|
||||||
|
case ProxyBypassRuleType.Local:
|
||||||
|
if (isLocalHostname(hostname)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ProxyBypassRuleType.Ip:
|
||||||
|
if (!hostnameIsIp) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (rule.ip && cleanedHostname === rule.ip) {
|
||||||
hostnameIsMatch &&
|
return true
|
||||||
(!ruleMatch.groups ||
|
}
|
||||||
!ruleMatch.groups.port ||
|
|
||||||
(subjectUrlTokens.port && subjectUrlTokens.port === ruleMatch.groups.port))
|
if (rule.regex && rule.regex.test(cleanedHostname)) {
|
||||||
) {
|
return true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ProxyBypassRuleType.Cidr:
|
||||||
|
if (hostnameIsIp && rule.cidr) {
|
||||||
|
const parsedHost = ipaddr.parse(cleanedHostname)
|
||||||
|
const [cidrAddress, prefixLength] = rule.cidr
|
||||||
|
// Ensure IP version matches before comparing
|
||||||
|
if (parsedHost.kind() === cidrAddress.kind() && parsedHost.match([cidrAddress, prefixLength])) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
break
|
||||||
|
case ProxyBypassRuleType.Domain:
|
||||||
|
if (!hostnameIsIp && matchHostnameRule(hostname, rule)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
logger.error(`Unknown proxy bypass rule type: ${rule.type}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to check bypass:', error as Error)
|
logger.error('Failed to check bypass:', error as Error)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
class SelectiveDispatcher extends Dispatcher {
|
class SelectiveDispatcher extends Dispatcher {
|
||||||
private proxyDispatcher: Dispatcher
|
private proxyDispatcher: Dispatcher
|
||||||
@ -154,19 +407,31 @@ export class ProxyManager {
|
|||||||
this.isSettingProxy = true
|
this.isSettingProxy = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.config = config
|
|
||||||
this.clearSystemProxyMonitor()
|
this.clearSystemProxyMonitor()
|
||||||
if (config.mode === 'system') {
|
if (config.mode === 'system') {
|
||||||
const currentProxy = await getSystemProxy()
|
const currentProxy = await getSystemProxy()
|
||||||
if (currentProxy) {
|
if (currentProxy) {
|
||||||
logger.info(`current system proxy: ${currentProxy.proxyUrl}`)
|
logger.info(`current system proxy: ${currentProxy.proxyUrl}, bypass rules: ${currentProxy.noProxy.join(',')}`)
|
||||||
this.config.proxyRules = currentProxy.proxyUrl.toLowerCase()
|
config.proxyRules = currentProxy.proxyUrl.toLowerCase()
|
||||||
|
config.proxyBypassRules = currentProxy.noProxy.join(',')
|
||||||
}
|
}
|
||||||
this.monitorSystemProxy()
|
this.monitorSystemProxy()
|
||||||
}
|
}
|
||||||
|
|
||||||
byPassRules = config.proxyBypassRules?.split(',') || []
|
// Support both semicolon and comma as separators
|
||||||
this.setGlobalProxy(this.config)
|
if (config.proxyBypassRules !== this.config.proxyBypassRules) {
|
||||||
|
const rawRules = config.proxyBypassRules
|
||||||
|
? config.proxyBypassRules
|
||||||
|
.split(/[;,]/)
|
||||||
|
.map((rule) => rule.trim())
|
||||||
|
.filter((rule) => rule.length > 0)
|
||||||
|
: []
|
||||||
|
|
||||||
|
updateByPassRules(rawRules)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setGlobalProxy(config)
|
||||||
|
this.config = config
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to config proxy:', error as Error)
|
logger.error('Failed to config proxy:', error as Error)
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
86
src/main/services/__tests__/ProxyManager.test.ts
Normal file
86
src/main/services/__tests__/ProxyManager.test.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { beforeEach, describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { isByPass, updateByPassRules } from '../ProxyManager'
|
||||||
|
|
||||||
|
describe('ProxyManager - bypass evaluation', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
updateByPassRules([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches simple hostname patterns', () => {
|
||||||
|
updateByPassRules(['foobar.com'])
|
||||||
|
expect(isByPass('http://foobar.com')).toBe(true)
|
||||||
|
expect(isByPass('http://www.foobar.com')).toBe(false)
|
||||||
|
|
||||||
|
updateByPassRules(['*.foobar.com'])
|
||||||
|
expect(isByPass('http://api.foobar.com')).toBe(true)
|
||||||
|
expect(isByPass('http://foobar.com')).toBe(true)
|
||||||
|
expect(isByPass('http://foobar.org')).toBe(false)
|
||||||
|
|
||||||
|
updateByPassRules(['*foobar.com'])
|
||||||
|
expect(isByPass('http://devfoobar.com')).toBe(true)
|
||||||
|
expect(isByPass('http://foobar.com')).toBe(true)
|
||||||
|
expect(isByPass('http://foobar.company')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches hostname patterns with scheme and port qualifiers', () => {
|
||||||
|
updateByPassRules(['https://secure.example.com'])
|
||||||
|
expect(isByPass('https://secure.example.com')).toBe(true)
|
||||||
|
expect(isByPass('https://secure.example.com:443/home')).toBe(true)
|
||||||
|
expect(isByPass('http://secure.example.com')).toBe(false)
|
||||||
|
|
||||||
|
updateByPassRules(['https://secure.example.com:8443'])
|
||||||
|
expect(isByPass('https://secure.example.com:8443')).toBe(true)
|
||||||
|
expect(isByPass('https://secure.example.com')).toBe(false)
|
||||||
|
expect(isByPass('https://secure.example.com:443')).toBe(false)
|
||||||
|
|
||||||
|
updateByPassRules(['https://x.*.y.com:99'])
|
||||||
|
expect(isByPass('https://x.api.y.com:99')).toBe(true)
|
||||||
|
expect(isByPass('https://x.api.y.com')).toBe(false)
|
||||||
|
expect(isByPass('http://x.api.y.com:99')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches domain suffix patterns with leading dot', () => {
|
||||||
|
updateByPassRules(['.example.com'])
|
||||||
|
expect(isByPass('https://example.com')).toBe(true)
|
||||||
|
expect(isByPass('https://api.example.com')).toBe(true)
|
||||||
|
expect(isByPass('https://deep.api.example.com')).toBe(true)
|
||||||
|
expect(isByPass('https://example.org')).toBe(false)
|
||||||
|
|
||||||
|
updateByPassRules(['.com'])
|
||||||
|
expect(isByPass('https://anything.com')).toBe(true)
|
||||||
|
expect(isByPass('https://example.org')).toBe(false)
|
||||||
|
|
||||||
|
updateByPassRules(['http://.google.com'])
|
||||||
|
expect(isByPass('http://maps.google.com')).toBe(true)
|
||||||
|
expect(isByPass('https://maps.google.com')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches IP literals, CIDR ranges, and wildcard IPs', () => {
|
||||||
|
updateByPassRules(['127.0.0.1', '[::1]', '192.168.1.0/24', 'fefe:13::abc/33', '192.168.*.*'])
|
||||||
|
|
||||||
|
expect(isByPass('http://127.0.0.1')).toBe(true)
|
||||||
|
expect(isByPass('http://[::1]')).toBe(true)
|
||||||
|
expect(isByPass('http://192.168.1.55')).toBe(true)
|
||||||
|
expect(isByPass('http://192.168.200.200')).toBe(true)
|
||||||
|
expect(isByPass('http://192.169.1.1')).toBe(false)
|
||||||
|
expect(isByPass('http://[fefe:13::abc]')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches CIDR ranges specified with IPv6 prefix lengths', () => {
|
||||||
|
updateByPassRules(['[2001:db8::1]', '2001:db8::/32'])
|
||||||
|
|
||||||
|
expect(isByPass('http://[2001:db8::1]')).toBe(true)
|
||||||
|
expect(isByPass('http://[2001:db8:0:0:0:0:0:ffff]')).toBe(true)
|
||||||
|
expect(isByPass('http://[2001:db9::1]')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches local addresses when <local> keyword is provided', () => {
|
||||||
|
updateByPassRules(['<local>'])
|
||||||
|
|
||||||
|
expect(isByPass('http://localhost')).toBe(true)
|
||||||
|
expect(isByPass('http://127.0.0.1')).toBe(true)
|
||||||
|
expect(isByPass('http://[::1]')).toBe(true)
|
||||||
|
expect(isByPass('http://dev.localdomain')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -4228,7 +4228,8 @@
|
|||||||
"none": "No Proxy",
|
"none": "No Proxy",
|
||||||
"system": "System Proxy",
|
"system": "System Proxy",
|
||||||
"title": "Proxy Mode"
|
"title": "Proxy Mode"
|
||||||
}
|
},
|
||||||
|
"tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)"
|
||||||
},
|
},
|
||||||
"quickAssistant": {
|
"quickAssistant": {
|
||||||
"click_tray_to_show": "Click the tray icon to start",
|
"click_tray_to_show": "Click the tray icon to start",
|
||||||
|
|||||||
@ -4228,7 +4228,8 @@
|
|||||||
"none": "不使用代理",
|
"none": "不使用代理",
|
||||||
"system": "系统代理",
|
"system": "系统代理",
|
||||||
"title": "代理模式"
|
"title": "代理模式"
|
||||||
}
|
},
|
||||||
|
"tip": "支持模糊匹配(*.test.com,192.168.0.0/16)"
|
||||||
},
|
},
|
||||||
"quickAssistant": {
|
"quickAssistant": {
|
||||||
"click_tray_to_show": "点击托盘图标启动",
|
"click_tray_to_show": "点击托盘图标启动",
|
||||||
|
|||||||
@ -4228,7 +4228,8 @@
|
|||||||
"none": "不使用代理伺服器",
|
"none": "不使用代理伺服器",
|
||||||
"system": "系統代理伺服器",
|
"system": "系統代理伺服器",
|
||||||
"title": "代理伺服器模式"
|
"title": "代理伺服器模式"
|
||||||
}
|
},
|
||||||
|
"tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)"
|
||||||
},
|
},
|
||||||
"quickAssistant": {
|
"quickAssistant": {
|
||||||
"click_tray_to_show": "點選工具列圖示啟動",
|
"click_tray_to_show": "點選工具列圖示啟動",
|
||||||
|
|||||||
@ -1960,6 +1960,14 @@
|
|||||||
"rename": "μετονομασία",
|
"rename": "μετονομασία",
|
||||||
"rename_changed": "Λόγω πολιτικής ασφάλειας, το όνομα του αρχείου έχει αλλάξει από {{original}} σε {{final}}",
|
"rename_changed": "Λόγω πολιτικής ασφάλειας, το όνομα του αρχείου έχει αλλάξει από {{original}} σε {{final}}",
|
||||||
"save": "αποθήκευση στις σημειώσεις",
|
"save": "αποθήκευση στις σημειώσεις",
|
||||||
|
"search": {
|
||||||
|
"both": "[to be translated]:名称+内容",
|
||||||
|
"content": "[to be translated]:内容",
|
||||||
|
"found_results": "[to be translated]:找到 {{count}} 个结果 (名称: {{nameCount}}, 内容: {{contentCount}})",
|
||||||
|
"more_matches": "[to be translated]:个匹配",
|
||||||
|
"searching": "[to be translated]:搜索中...",
|
||||||
|
"show_less": "[to be translated]:收起"
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"data": {
|
"data": {
|
||||||
"apply": "εφαρμογή",
|
"apply": "εφαρμογή",
|
||||||
@ -2108,6 +2116,8 @@
|
|||||||
"install_code_103": "Η λήψη του OVMS runtime απέτυχε",
|
"install_code_103": "Η λήψη του OVMS runtime απέτυχε",
|
||||||
"install_code_104": "Η αποσυμπίεση του OVMS runtime απέτυχε",
|
"install_code_104": "Η αποσυμπίεση του OVMS runtime απέτυχε",
|
||||||
"install_code_105": "Ο καθαρισμός του OVMS runtime απέτυχε",
|
"install_code_105": "Ο καθαρισμός του OVMS runtime απέτυχε",
|
||||||
|
"install_code_106": "[to be translated]:创建 run.bat 失败",
|
||||||
|
"install_code_110": "[to be translated]:清理旧 OVMS runtime 失败",
|
||||||
"run": "Η εκτέλεση του OVMS απέτυχε:",
|
"run": "Η εκτέλεση του OVMS απέτυχε:",
|
||||||
"stop": "Η διακοπή του OVMS απέτυχε:"
|
"stop": "Η διακοπή του OVMS απέτυχε:"
|
||||||
},
|
},
|
||||||
@ -4218,7 +4228,8 @@
|
|||||||
"none": "χωρίς πρόξενο",
|
"none": "χωρίς πρόξενο",
|
||||||
"system": "συστηματική προξενική",
|
"system": "συστηματική προξενική",
|
||||||
"title": "κλίμακα προξενικής"
|
"title": "κλίμακα προξενικής"
|
||||||
}
|
},
|
||||||
|
"tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)"
|
||||||
},
|
},
|
||||||
"quickAssistant": {
|
"quickAssistant": {
|
||||||
"click_tray_to_show": "Επιλέξτε την εικόνα στο πίνακα για να ενεργοποιήσετε",
|
"click_tray_to_show": "Επιλέξτε την εικόνα στο πίνακα για να ενεργοποιήσετε",
|
||||||
|
|||||||
@ -1960,6 +1960,14 @@
|
|||||||
"rename": "renombrar",
|
"rename": "renombrar",
|
||||||
"rename_changed": "Debido a políticas de seguridad, el nombre del archivo ha cambiado de {{original}} a {{final}}",
|
"rename_changed": "Debido a políticas de seguridad, el nombre del archivo ha cambiado de {{original}} a {{final}}",
|
||||||
"save": "Guardar en notas",
|
"save": "Guardar en notas",
|
||||||
|
"search": {
|
||||||
|
"both": "[to be translated]:名称+内容",
|
||||||
|
"content": "[to be translated]:内容",
|
||||||
|
"found_results": "[to be translated]:找到 {{count}} 个结果 (名称: {{nameCount}}, 内容: {{contentCount}})",
|
||||||
|
"more_matches": "[to be translated]:个匹配",
|
||||||
|
"searching": "[to be translated]:搜索中...",
|
||||||
|
"show_less": "[to be translated]:收起"
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"data": {
|
"data": {
|
||||||
"apply": "aplicación",
|
"apply": "aplicación",
|
||||||
@ -2108,6 +2116,8 @@
|
|||||||
"install_code_103": "Error al descargar el tiempo de ejecución de OVMS",
|
"install_code_103": "Error al descargar el tiempo de ejecución de OVMS",
|
||||||
"install_code_104": "Error al descomprimir el tiempo de ejecución de OVMS",
|
"install_code_104": "Error al descomprimir el tiempo de ejecución de OVMS",
|
||||||
"install_code_105": "Error al limpiar el tiempo de ejecución de OVMS",
|
"install_code_105": "Error al limpiar el tiempo de ejecución de OVMS",
|
||||||
|
"install_code_106": "[to be translated]:创建 run.bat 失败",
|
||||||
|
"install_code_110": "[to be translated]:清理旧 OVMS runtime 失败",
|
||||||
"run": "Error al ejecutar OVMS:",
|
"run": "Error al ejecutar OVMS:",
|
||||||
"stop": "Error al detener OVMS:"
|
"stop": "Error al detener OVMS:"
|
||||||
},
|
},
|
||||||
@ -4218,7 +4228,8 @@
|
|||||||
"none": "No usar proxy",
|
"none": "No usar proxy",
|
||||||
"system": "Proxy del sistema",
|
"system": "Proxy del sistema",
|
||||||
"title": "Modo de proxy"
|
"title": "Modo de proxy"
|
||||||
}
|
},
|
||||||
|
"tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)"
|
||||||
},
|
},
|
||||||
"quickAssistant": {
|
"quickAssistant": {
|
||||||
"click_tray_to_show": "Haz clic en el icono de la bandeja para iniciar",
|
"click_tray_to_show": "Haz clic en el icono de la bandeja para iniciar",
|
||||||
|
|||||||
@ -1960,6 +1960,14 @@
|
|||||||
"rename": "renommer",
|
"rename": "renommer",
|
||||||
"rename_changed": "En raison de la politique de sécurité, le nom du fichier a été changé de {{original}} à {{final}}",
|
"rename_changed": "En raison de la politique de sécurité, le nom du fichier a été changé de {{original}} à {{final}}",
|
||||||
"save": "sauvegarder dans les notes",
|
"save": "sauvegarder dans les notes",
|
||||||
|
"search": {
|
||||||
|
"both": "[to be translated]:名称+内容",
|
||||||
|
"content": "[to be translated]:内容",
|
||||||
|
"found_results": "[to be translated]:找到 {{count}} 个结果 (名称: {{nameCount}}, 内容: {{contentCount}})",
|
||||||
|
"more_matches": "[to be translated]:个匹配",
|
||||||
|
"searching": "[to be translated]:搜索中...",
|
||||||
|
"show_less": "[to be translated]:收起"
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"data": {
|
"data": {
|
||||||
"apply": "application",
|
"apply": "application",
|
||||||
@ -2108,6 +2116,8 @@
|
|||||||
"install_code_103": "Échec du téléchargement du runtime OVMS",
|
"install_code_103": "Échec du téléchargement du runtime OVMS",
|
||||||
"install_code_104": "Échec de la décompression du runtime OVMS",
|
"install_code_104": "Échec de la décompression du runtime OVMS",
|
||||||
"install_code_105": "Échec du nettoyage du runtime OVMS",
|
"install_code_105": "Échec du nettoyage du runtime OVMS",
|
||||||
|
"install_code_106": "[to be translated]:创建 run.bat 失败",
|
||||||
|
"install_code_110": "[to be translated]:清理旧 OVMS runtime 失败",
|
||||||
"run": "Échec de l'exécution d'OVMS :",
|
"run": "Échec de l'exécution d'OVMS :",
|
||||||
"stop": "Échec de l'arrêt d'OVMS :"
|
"stop": "Échec de l'arrêt d'OVMS :"
|
||||||
},
|
},
|
||||||
@ -4218,7 +4228,8 @@
|
|||||||
"none": "Ne pas utiliser de proxy",
|
"none": "Ne pas utiliser de proxy",
|
||||||
"system": "Proxy système",
|
"system": "Proxy système",
|
||||||
"title": "Mode de proxy"
|
"title": "Mode de proxy"
|
||||||
}
|
},
|
||||||
|
"tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)"
|
||||||
},
|
},
|
||||||
"quickAssistant": {
|
"quickAssistant": {
|
||||||
"click_tray_to_show": "Cliquez sur l'icône dans la barre d'état système pour démarrer",
|
"click_tray_to_show": "Cliquez sur l'icône dans la barre d'état système pour démarrer",
|
||||||
|
|||||||
@ -1960,6 +1960,14 @@
|
|||||||
"rename": "名前の変更",
|
"rename": "名前の変更",
|
||||||
"rename_changed": "セキュリティポリシーにより、ファイル名は{{original}}から{{final}}に変更されました",
|
"rename_changed": "セキュリティポリシーにより、ファイル名は{{original}}から{{final}}に変更されました",
|
||||||
"save": "メモに保存する",
|
"save": "メモに保存する",
|
||||||
|
"search": {
|
||||||
|
"both": "[to be translated]:名称+内容",
|
||||||
|
"content": "[to be translated]:内容",
|
||||||
|
"found_results": "[to be translated]:找到 {{count}} 个结果 (名称: {{nameCount}}, 内容: {{contentCount}})",
|
||||||
|
"more_matches": "[to be translated]:个匹配",
|
||||||
|
"searching": "[to be translated]:搜索中...",
|
||||||
|
"show_less": "[to be translated]:收起"
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"data": {
|
"data": {
|
||||||
"apply": "応用",
|
"apply": "応用",
|
||||||
@ -2108,6 +2116,8 @@
|
|||||||
"install_code_103": "OVMSランタイムのダウンロードに失敗しました",
|
"install_code_103": "OVMSランタイムのダウンロードに失敗しました",
|
||||||
"install_code_104": "OVMSランタイムの解凍に失敗しました",
|
"install_code_104": "OVMSランタイムの解凍に失敗しました",
|
||||||
"install_code_105": "OVMSランタイムのクリーンアップに失敗しました",
|
"install_code_105": "OVMSランタイムのクリーンアップに失敗しました",
|
||||||
|
"install_code_106": "[to be translated]:创建 run.bat 失败",
|
||||||
|
"install_code_110": "[to be translated]:清理旧 OVMS runtime 失败",
|
||||||
"run": "OVMSの実行に失敗しました:",
|
"run": "OVMSの実行に失敗しました:",
|
||||||
"stop": "OVMSの停止に失敗しました:"
|
"stop": "OVMSの停止に失敗しました:"
|
||||||
},
|
},
|
||||||
@ -4218,7 +4228,8 @@
|
|||||||
"none": "プロキシを使用しない",
|
"none": "プロキシを使用しない",
|
||||||
"system": "システムプロキシ",
|
"system": "システムプロキシ",
|
||||||
"title": "プロキシモード"
|
"title": "プロキシモード"
|
||||||
}
|
},
|
||||||
|
"tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)"
|
||||||
},
|
},
|
||||||
"quickAssistant": {
|
"quickAssistant": {
|
||||||
"click_tray_to_show": "トレイアイコンをクリックして起動",
|
"click_tray_to_show": "トレイアイコンをクリックして起動",
|
||||||
|
|||||||
@ -1960,6 +1960,14 @@
|
|||||||
"rename": "renomear",
|
"rename": "renomear",
|
||||||
"rename_changed": "Devido às políticas de segurança, o nome do arquivo foi alterado de {{original}} para {{final}}",
|
"rename_changed": "Devido às políticas de segurança, o nome do arquivo foi alterado de {{original}} para {{final}}",
|
||||||
"save": "salvar em notas",
|
"save": "salvar em notas",
|
||||||
|
"search": {
|
||||||
|
"both": "[to be translated]:名称+内容",
|
||||||
|
"content": "[to be translated]:内容",
|
||||||
|
"found_results": "[to be translated]:找到 {{count}} 个结果 (名称: {{nameCount}}, 内容: {{contentCount}})",
|
||||||
|
"more_matches": "[to be translated]:个匹配",
|
||||||
|
"searching": "[to be translated]:搜索中...",
|
||||||
|
"show_less": "[to be translated]:收起"
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"data": {
|
"data": {
|
||||||
"apply": "aplicativo",
|
"apply": "aplicativo",
|
||||||
@ -2108,6 +2116,8 @@
|
|||||||
"install_code_103": "Falha ao baixar o tempo de execução do OVMS",
|
"install_code_103": "Falha ao baixar o tempo de execução do OVMS",
|
||||||
"install_code_104": "Falha ao descompactar o tempo de execução do OVMS",
|
"install_code_104": "Falha ao descompactar o tempo de execução do OVMS",
|
||||||
"install_code_105": "Falha ao limpar o tempo de execução do OVMS",
|
"install_code_105": "Falha ao limpar o tempo de execução do OVMS",
|
||||||
|
"install_code_106": "[to be translated]:创建 run.bat 失败",
|
||||||
|
"install_code_110": "[to be translated]:清理旧 OVMS runtime 失败",
|
||||||
"run": "Falha ao executar o OVMS:",
|
"run": "Falha ao executar o OVMS:",
|
||||||
"stop": "Falha ao parar o OVMS:"
|
"stop": "Falha ao parar o OVMS:"
|
||||||
},
|
},
|
||||||
@ -4218,7 +4228,8 @@
|
|||||||
"none": "Não Usar Proxy",
|
"none": "Não Usar Proxy",
|
||||||
"system": "Proxy do Sistema",
|
"system": "Proxy do Sistema",
|
||||||
"title": "Modo de Proxy"
|
"title": "Modo de Proxy"
|
||||||
}
|
},
|
||||||
|
"tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)"
|
||||||
},
|
},
|
||||||
"quickAssistant": {
|
"quickAssistant": {
|
||||||
"click_tray_to_show": "Clique no ícone da bandeja para iniciar",
|
"click_tray_to_show": "Clique no ícone da bandeja para iniciar",
|
||||||
|
|||||||
@ -1960,6 +1960,14 @@
|
|||||||
"rename": "переименовать",
|
"rename": "переименовать",
|
||||||
"rename_changed": "В связи с политикой безопасности имя файла было изменено с {{Original}} на {{final}}",
|
"rename_changed": "В связи с политикой безопасности имя файла было изменено с {{Original}} на {{final}}",
|
||||||
"save": "Сохранить в заметки",
|
"save": "Сохранить в заметки",
|
||||||
|
"search": {
|
||||||
|
"both": "[to be translated]:名称+内容",
|
||||||
|
"content": "[to be translated]:内容",
|
||||||
|
"found_results": "[to be translated]:找到 {{count}} 个结果 (名称: {{nameCount}}, 内容: {{contentCount}})",
|
||||||
|
"more_matches": "[to be translated]:个匹配",
|
||||||
|
"searching": "[to be translated]:搜索中...",
|
||||||
|
"show_less": "[to be translated]:收起"
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"data": {
|
"data": {
|
||||||
"apply": "приложение",
|
"apply": "приложение",
|
||||||
@ -2108,6 +2116,8 @@
|
|||||||
"install_code_103": "Ошибка загрузки среды выполнения OVMS",
|
"install_code_103": "Ошибка загрузки среды выполнения OVMS",
|
||||||
"install_code_104": "Ошибка распаковки среды выполнения OVMS",
|
"install_code_104": "Ошибка распаковки среды выполнения OVMS",
|
||||||
"install_code_105": "Ошибка очистки среды выполнения OVMS",
|
"install_code_105": "Ошибка очистки среды выполнения OVMS",
|
||||||
|
"install_code_106": "[to be translated]:创建 run.bat 失败",
|
||||||
|
"install_code_110": "[to be translated]:清理旧 OVMS runtime 失败",
|
||||||
"run": "Ошибка запуска OVMS:",
|
"run": "Ошибка запуска OVMS:",
|
||||||
"stop": "Ошибка остановки OVMS:"
|
"stop": "Ошибка остановки OVMS:"
|
||||||
},
|
},
|
||||||
@ -4218,7 +4228,8 @@
|
|||||||
"none": "Не использовать прокси",
|
"none": "Не использовать прокси",
|
||||||
"system": "Системный прокси",
|
"system": "Системный прокси",
|
||||||
"title": "Режим прокси"
|
"title": "Режим прокси"
|
||||||
}
|
},
|
||||||
|
"tip": "[to be translated]:支持模糊匹配(*.test.com,192.168.0.0/16)"
|
||||||
},
|
},
|
||||||
"quickAssistant": {
|
"quickAssistant": {
|
||||||
"click_tray_to_show": "Нажмите на иконку трея для запуска",
|
"click_tray_to_show": "Нажмите на иконку трея для запуска",
|
||||||
|
|||||||
@ -233,7 +233,12 @@ const GeneralSettings: FC = () => {
|
|||||||
<>
|
<>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitle>{t('settings.proxy.bypass')}</SettingRowTitle>
|
<SettingRowTitle style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
|
<span>{t('settings.proxy.bypass')}</span>
|
||||||
|
<Tooltip title={t('settings.proxy.tip')} placement="right">
|
||||||
|
<InfoCircleOutlined style={{ cursor: 'pointer' }} />
|
||||||
|
</Tooltip>
|
||||||
|
</SettingRowTitle>
|
||||||
<Input
|
<Input
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
placeholder={defaultByPassRules}
|
placeholder={defaultByPassRules}
|
||||||
|
|||||||
@ -14021,6 +14021,7 @@ __metadata:
|
|||||||
husky: "npm:^9.1.7"
|
husky: "npm:^9.1.7"
|
||||||
i18next: "npm:^23.11.5"
|
i18next: "npm:^23.11.5"
|
||||||
iconv-lite: "npm:^0.6.3"
|
iconv-lite: "npm:^0.6.3"
|
||||||
|
ipaddr.js: "npm:^2.2.0"
|
||||||
isbinaryfile: "npm:5.0.4"
|
isbinaryfile: "npm:5.0.4"
|
||||||
jaison: "npm:^2.0.2"
|
jaison: "npm:^2.0.2"
|
||||||
jest-styled-components: "npm:^7.2.0"
|
jest-styled-components: "npm:^7.2.0"
|
||||||
@ -20322,6 +20323,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ipaddr.js@npm:^2.2.0":
|
||||||
|
version: 2.2.0
|
||||||
|
resolution: "ipaddr.js@npm:2.2.0"
|
||||||
|
checksum: 10c0/e4ee875dc1bd92ac9d27e06cfd87cdb63ca786ff9fd7718f1d4f7a8ef27db6e5d516128f52d2c560408cbb75796ac2f83ead669e73507c86282d45f84c5abbb6
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"is-alphabetical@npm:^1.0.0":
|
"is-alphabetical@npm:^1.0.0":
|
||||||
version: 1.0.4
|
version: 1.0.4
|
||||||
resolution: "is-alphabetical@npm:1.0.4"
|
resolution: "is-alphabetical@npm:1.0.4"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user