diff --git a/lite/proxy_lite.py b/lite/proxy_lite.py index 310db42..0831691 100644 --- a/lite/proxy_lite.py +++ b/lite/proxy_lite.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -import sys, os, subprocess, time, ctypes, threading, functools, base64, bisect +import sys, os, subprocess, time, ctypes, threading, functools, base64, bisect, re from pathlib import Path from PyQt5.QtWidgets import ( @@ -832,8 +832,17 @@ def _is_private_ip(host: str) -> bool: _CN_RANGES = None # 合并排序后的 (start, end) 整数段 _CN_RANGES_LOADED = False _FORCE_PROXY_SUFFIXES = None +_FORCE_PROXY_EXACT = None +_FORCE_PROXY_REGEXES = None _FORCE_PROXY_LOADED = False +# PAC 数据由 CDN 独立下发;此处保留登录/AI 服务的关键后缀兜底。 +# 即使远端表暂时未更新或不可读,这些域名也不能因解析到中国 CDN IP 而直连。 +_CORE_FORCE_PROXY_SUFFIXES = frozenset(( + "anthropic.com", "chatgpt.com", "claude.ai", "googlecdn.com", + "oaistatic.com", "oaiusercontent.com", "openai.com", +)) + def _data_path(name: str): """数据文件路径:只取打包内置目录(config.json 随 exe 内置)。""" p = Path(BASE_DIR) / name @@ -1318,8 +1327,13 @@ def _load_cn_ranges(): return _CN_RANGES def _load_force_proxy_suffixes(): - """加载 pac_domains.json:命中这些域名后缀的连接强制走隧道(优先于 IP 判定)。""" - global _FORCE_PROXY_SUFFIXES, _FORCE_PROXY_LOADED + """加载 pac_domains.json 的后缀、精确和正则规则。 + + domain-list-community 的三种规则语义:普通域名匹配自身及子域; + ``full:`` 仅匹配该主机;``regexp:`` 按正则匹配。旧逻辑把后两类 + 当成带冒号的普通后缀,导致它们永远不命中。 + """ + global _FORCE_PROXY_SUFFIXES, _FORCE_PROXY_EXACT, _FORCE_PROXY_REGEXES, _FORCE_PROXY_LOADED if _FORCE_PROXY_LOADED: return _FORCE_PROXY_SUFFIXES _FORCE_PROXY_LOADED = True @@ -1327,14 +1341,51 @@ def _load_force_proxy_suffixes(): import json path = _user_path("pac_domains.json") if path is None: - return _FORCE_PROXY_SUFFIXES # 可选文件,缺失不影响 IP 分流 + _FORCE_PROXY_SUFFIXES = sorted(_CORE_FORCE_PROXY_SUFFIXES) + _FORCE_PROXY_EXACT = frozenset() + _FORCE_PROXY_REGEXES = () + log(f"未找到 pac_domains.json,使用 {len(_FORCE_PROXY_SUFFIXES)} 条关键域名兜底") + return _FORCE_PROXY_SUFFIXES data = json.loads(Path(path).read_text("utf-8")) - suffixes = sorted({str(d).strip().lower().lstrip("*.") - for d in (data.get("domains") or []) if str(d).strip()}) + suffixes = set(_CORE_FORCE_PROXY_SUFFIXES) + exact = set() + regexes = [] + invalid_regexes = 0 + for raw in data.get("domains") or []: + raw_rule = str(raw).strip() + rule = raw_rule.lower() + if not raw_rule: + continue + if rule.startswith("full:"): + host = rule[len("full:"):].rstrip(".") + if host: + exact.add(host) + continue + if rule.startswith("regexp:"): + # 正则本身可能含大写字符类或大小写敏感字面量;只忽略匹配时的 + # 域名大小写,不能在编译前改写 pattern。 + pattern = raw_rule[len("regexp:"):] + if not pattern: + invalid_regexes += 1 + continue + try: + regexes.append(re.compile(pattern, re.IGNORECASE)) + except re.error: + invalid_regexes += 1 + continue + suffixes.add(rule.lstrip("*.").rstrip(".")) + suffixes.discard("") _FORCE_PROXY_SUFFIXES = suffixes - log(f"已加载强制代理域名: {len(suffixes)} 条 (pac_domains.json)") + _FORCE_PROXY_EXACT = frozenset(exact) + _FORCE_PROXY_REGEXES = tuple(regexes) + log(f"已加载强制代理规则: 后缀 {len(suffixes)} 条、精确 {len(exact)} 条、" + f"正则 {len(regexes)} 条 (pac_domains.json)" + + (f",跳过 {invalid_regexes} 条非法正则" if invalid_regexes else "")) except Exception as e: log_red(f"pac_domains.json 加载失败: {e}") + _FORCE_PROXY_SUFFIXES = sorted(_CORE_FORCE_PROXY_SUFFIXES) + _FORCE_PROXY_EXACT = frozenset() + _FORCE_PROXY_REGEXES = () return _FORCE_PROXY_SUFFIXES def _is_private_int(n: int) -> bool: @@ -1386,12 +1437,16 @@ def _is_china_ip(host: str): @functools.lru_cache(maxsize=8192) def _is_force_proxy(host: str) -> bool: - """pac_domains.json 命中的域名强制走隧道(如 claude.ai:国内可连但显示拒绝页)。""" + """PAC 强制规则命中的域名走隧道(后缀 / full / regexp)。""" suffixes = _load_force_proxy_suffixes() - if not suffixes: + exact = _FORCE_PROXY_EXACT or () + regexes = _FORCE_PROXY_REGEXES or () + if not suffixes and not exact and not regexes: return False h = host.rstrip(".").lower() - return any(h == s or h.endswith("." + s) for s in suffixes) + return (any(h == s or h.endswith("." + s) for s in suffixes) or + h in exact or + any(pattern.search(h) is not None for pattern in regexes)) def _resolve_host_ips(host: str): """解析域名的 IPv4/IPv6 地址集合(字面量 IP 返回自身);解析失败返回空集合。""" @@ -3092,4 +3147,4 @@ def main(): os._exit(ret) if __name__ == "__main__": - main() \ No newline at end of file + main()