mirror of
https://github.com/jxxghp/MoviePilot-Plugins.git
synced 2026-03-27 10:05:57 +00:00
Merge pull request #924 from wumode/tobypasstrackers
This commit is contained in:
@@ -437,11 +437,12 @@
|
||||
"name": "绕过Trackers",
|
||||
"description": "提供tracker服务器IP地址列表,帮助IPv6连接绕过OpenClash。",
|
||||
"labels": "工具",
|
||||
"version": "1.4.3",
|
||||
"version": "1.5.0",
|
||||
"icon": "Clash_A.png",
|
||||
"author": "wumode",
|
||||
"level": 2,
|
||||
"history": {
|
||||
"v1.5.0": "新增 Page 界面; 支持通过`/check_ip` 命令检查IP; 改进 UI",
|
||||
"v1.4.3": "修复 bug",
|
||||
"v1.4.2": "修复插件动作",
|
||||
"v1.4.1": "修复通知类型错误",
|
||||
@@ -491,12 +492,13 @@
|
||||
"name": "Clash Rule Provider",
|
||||
"description": "随时为Clash添加一些额外的规则。",
|
||||
"labels": "工具",
|
||||
"version": "2.0.9",
|
||||
"version": "2.0.10",
|
||||
"icon": "Mihomo_Meta_A.png",
|
||||
"author": "wumode",
|
||||
"level": 1,
|
||||
"release": true,
|
||||
"history": {
|
||||
"v2.0.10": "适配 MoviePilot 2.8.4",
|
||||
"v2.0.9": "适配 Pydantic V2 (主程序版本需高于 2.8.1-1)",
|
||||
"v2.0.8": "修复已知问题",
|
||||
"v2.0.7": "修复子规则比较错误",
|
||||
@@ -532,11 +534,12 @@
|
||||
"name": "美剧生词标注",
|
||||
"description": "根据CEFR等级,为英语影视剧标注高级词汇。",
|
||||
"labels": "英语",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.4",
|
||||
"icon": "LexiAnnot.png",
|
||||
"author": "wumode",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v1.1.4": "优化字幕选择决策",
|
||||
"v1.1.3": "适配 Pydantic V2 (主程序版本需高于 2.8.1-1)",
|
||||
"v1.1.2": "使用子进程避免 spaCy 模型常驻内存",
|
||||
"v1.1.1": "添加任务页面; 改进 spaCy 模型加载逻辑",
|
||||
|
||||
@@ -9,11 +9,10 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.config import settings, global_vars
|
||||
from app.core.event import eventmanager, Event
|
||||
from app.log import logger
|
||||
from app.schemas.types import EventType, NotificationType
|
||||
from app.scheduler import Scheduler
|
||||
|
||||
from .api import ClashRuleProviderApi, apis
|
||||
from .base import _ClashRuleProviderBase
|
||||
@@ -32,7 +31,7 @@ class ClashRuleProvider(_ClashRuleProviderBase):
|
||||
# 插件图标
|
||||
plugin_icon = "Mihomo_Meta_A.png"
|
||||
# 插件版本
|
||||
plugin_version = "2.0.9"
|
||||
plugin_version = "2.0.10"
|
||||
# 插件作者
|
||||
plugin_author = "wumode"
|
||||
# 作者主页
|
||||
@@ -92,11 +91,7 @@ class ClashRuleProvider(_ClashRuleProviderBase):
|
||||
self.state.ruleset_rules_manager.clear()
|
||||
|
||||
if ClashRuleProvider.event_loop is None:
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = Scheduler().loop
|
||||
ClashRuleProvider.event_loop = loop
|
||||
ClashRuleProvider.event_loop = global_vars.loop
|
||||
self.scheduler = AsyncIOScheduler(timezone=settings.TZ, event_loop=ClashRuleProvider.event_loop)
|
||||
self.services = ClashRuleProviderService(self.__class__.__name__, self.config, self.state, self.store,
|
||||
self.scheduler)
|
||||
|
||||
@@ -83,7 +83,7 @@ class LexiAnnot(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "LexiAnnot.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.1.3"
|
||||
plugin_version = "1.1.4"
|
||||
# 插件作者
|
||||
plugin_author = "wumode"
|
||||
# 作者主页
|
||||
@@ -109,7 +109,7 @@ class LexiAnnot(_PluginBase):
|
||||
_context_window: int = 0
|
||||
_max_retries: int = 0
|
||||
_request_interval: int = 0
|
||||
_ffmpeg_path = ''
|
||||
_ffmpeg_path: str = 'ffmpeg'
|
||||
_english_only = False
|
||||
_when_file_trans = False
|
||||
_model_temperature = ''
|
||||
@@ -154,7 +154,7 @@ class LexiAnnot(_PluginBase):
|
||||
self._context_window = int(config.get("context_window") or 10)
|
||||
self._max_retries = int(config.get("max_retries") or 3)
|
||||
self._request_interval = int(config.get("request_interval") or 3)
|
||||
self._ffmpeg_path = config.get("ffmpeg_path")
|
||||
self._ffmpeg_path = config.get("ffmpeg_path") or 'ffmpeg'
|
||||
self._english_only = config.get("english_only")
|
||||
self._when_file_trans = config.get("when_file_trans")
|
||||
self._model_temperature = config.get("model_temperature") or '0.3'
|
||||
@@ -975,31 +975,23 @@ class LexiAnnot(_PluginBase):
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'class': 'd-none d-sm-block',
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'component': 'VDataTableVirtual',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VDataTableVirtual',
|
||||
'props': {
|
||||
'class': 'text-sm',
|
||||
'headers': headers,
|
||||
'items': items,
|
||||
'height': '30rem',
|
||||
'density': 'compact',
|
||||
'fixed-header': True,
|
||||
'hide-no-data': True,
|
||||
'hover': True
|
||||
}
|
||||
}
|
||||
]
|
||||
'class': 'text-sm',
|
||||
'headers': headers,
|
||||
'items': items,
|
||||
'height': '30rem',
|
||||
'density': 'compact',
|
||||
'fixed-header': True,
|
||||
'hide-no-data': True,
|
||||
'hover': True
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1213,7 +1205,11 @@ class LexiAnnot(_PluginBase):
|
||||
embedded_subtitles = LexiAnnot._extract_subtitles_by_lang(path, eng_mark, ffmpeg_path)
|
||||
if not embedded_subtitles:
|
||||
return TaskStatus.CANCELED
|
||||
embedded_subtitles = sorted(embedded_subtitles, key=lambda track: 'SDH' in track['title'])
|
||||
# order factor = 0, if 'SDH' in track['title']
|
||||
# order factor = track['duration'], otherwise
|
||||
embedded_subtitles = sorted(embedded_subtitles,
|
||||
key=lambda track: track['duration']*(1-int('SDH' in track['title'])),
|
||||
reverse=True)
|
||||
ret_message = ''
|
||||
if embedded_subtitles:
|
||||
logger.info(f'提取到 {len(embedded_subtitles)} 条英语文本字幕')
|
||||
@@ -1705,7 +1701,8 @@ class LexiAnnot(_PluginBase):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _extract_subtitles_by_lang(video_path: str, lang: str | list = 'en', ffmpeg: str = 'ffmpeg') -> Optional[List[Dict]]:
|
||||
def _extract_subtitles_by_lang(video_path: str, lang: str | list = 'en', ffmpeg: str = 'ffmpeg'
|
||||
) -> Optional[List[Dict]]:
|
||||
"""
|
||||
提取视频文件中的内嵌英文字幕,使用 MediaInfo 查找字幕流。
|
||||
"""
|
||||
@@ -1720,12 +1717,19 @@ class LexiAnnot(_PluginBase):
|
||||
try:
|
||||
media_info: pymediainfo.MediaInfo = pymediainfo.MediaInfo.parse(video_path)
|
||||
for track in media_info.tracks:
|
||||
if track.track_type == 'Text' and check_lang(track_lang=track.language) and track.codec_id in supported_codec:
|
||||
if (track.track_type == 'Text' and check_lang(track_lang=track.language)
|
||||
and track.codec_id in supported_codec):
|
||||
subtitle_stream_index = track.stream_identifier # MediaInfo 的 stream_id 从 1 开始,ffmpeg 从 0 开始
|
||||
subtitle = LexiAnnot.__extract_subtitle(video_path, subtitle_stream_index, ffmpeg)
|
||||
duration = 0
|
||||
if hasattr(track, 'duration'):
|
||||
try:
|
||||
duration = int(float(track.duration))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if subtitle:
|
||||
subtitles.append({'title': track.title or '', 'subtitle': subtitle, 'codec_id': track.codec_id,
|
||||
'stream_id': subtitle_stream_index})
|
||||
'stream_id': subtitle_stream_index, 'duration': duration})
|
||||
if subtitles:
|
||||
return subtitles
|
||||
else:
|
||||
@@ -1761,7 +1765,7 @@ class LexiAnnot(_PluginBase):
|
||||
)
|
||||
|
||||
if not response.success:
|
||||
logger.warning(f"Error in subprocess response: {response.message}")
|
||||
logger.warning(f"Error in response: {response.message}")
|
||||
return tasks.tasks
|
||||
|
||||
self._total_token_count += response.total_token_count
|
||||
@@ -1918,7 +1922,7 @@ Only complete the `Chinese` field. Do not include pinyin, explanations, or any a
|
||||
)
|
||||
i = 0
|
||||
dialog_trans_instruction = '''You are an expert translator. You will be given a list of dialogue translation tasks in JSON format. For each entry, provide the most appropriate translation in Simplified Chinese based on the context.
|
||||
Only complete the `Chinese` field. Do not include pinyin, explanations, or any additional information.'''
|
||||
Only complete the `Chinese` field. Do not include pinyin, explanations, or any additional information.'''
|
||||
while i < len(translation_tasks):
|
||||
if self._shutdown_event.is_set():
|
||||
return lines_to_process
|
||||
@@ -2044,4 +2048,8 @@ Only complete the `Chinese` field. Do not include pinyin, explanations, or any a
|
||||
if chinese and chinese[-1] in ['。', ',']:
|
||||
chinese = chinese[:-1]
|
||||
main_dialogue[line_data['index']].text = main_dialogue[line_data['index']].text + f"\\N{chinese}"
|
||||
|
||||
# 避免 Infuse 显示乱码
|
||||
unexplainable_line = pysubs2.SSAEvent(start=0, end=0, text=f"{{\\rAnnotation ZH}}{self.plugin_name}{{\\r}}")
|
||||
ass_file.insert(0, unexplainable_line)
|
||||
return ass_file
|
||||
|
||||
@@ -68,13 +68,14 @@ def translate(
|
||||
returns: GeminiResponse containing the results
|
||||
"""
|
||||
|
||||
client = genai.Client(api_key=api_key)
|
||||
|
||||
messages = []
|
||||
|
||||
response_schema = type(translation_tasks)
|
||||
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
client = genai.Client(api_key=api_key)
|
||||
response = client.models.generate_content(
|
||||
model=gemini_model,
|
||||
contents=translation_tasks.model_dump_json(),
|
||||
@@ -100,7 +101,7 @@ def translate(
|
||||
except Exception as e:
|
||||
messages.append(f"Attempt {attempt} failed: {str(e)}")
|
||||
if attempt < max_retries:
|
||||
time.sleep(retry_delay)
|
||||
time.sleep(attempt*retry_delay)
|
||||
|
||||
return GeminiResponse(
|
||||
tasks=[],
|
||||
|
||||
@@ -3,22 +3,48 @@ import base64
|
||||
import ipaddress
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, List, Dict, Tuple, Optional
|
||||
from ipaddress import IPv4Network, IPv6Network, IPv4Address, IPv6Address
|
||||
from typing import Any, List, Dict, Tuple, Optional, Literal, overload
|
||||
|
||||
import pytz
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from fastapi import Response
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.event import eventmanager, Event
|
||||
from app.db.site_oper import SiteOper
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.plugins.tobypasstrackers.dns_helper import DnsHelper
|
||||
from app.schemas.types import EventType, NotificationType
|
||||
from app.utils.http import RequestUtils
|
||||
from .dns_helper import DnsHelper
|
||||
|
||||
|
||||
class IpCidrItem(BaseModel):
|
||||
# IP CIDR
|
||||
ip_cidr: str
|
||||
# 解析时间
|
||||
timestamp: int = Field(default=0)
|
||||
# DNS
|
||||
nameserver: str | None = Field(default=None)
|
||||
# 域名
|
||||
domain: str | None = Field(default=None)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
if self.timestamp:
|
||||
dns_time = datetime.fromtimestamp(int(self.timestamp)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
dns_time = '-'
|
||||
return {
|
||||
'ip_cidr': self.ip_cidr,
|
||||
'domain': self.domain or '',
|
||||
'nameserver': self.nameserver or '-',
|
||||
'datetime': dns_time
|
||||
}
|
||||
|
||||
|
||||
class ToBypassTrackers(_PluginBase):
|
||||
@@ -29,7 +55,7 @@ class ToBypassTrackers(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "Clash_A.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.4.3"
|
||||
plugin_version = "1.5.0"
|
||||
# 插件作者
|
||||
plugin_author = "wumode"
|
||||
# 作者主页
|
||||
@@ -40,13 +66,15 @@ class ToBypassTrackers(_PluginBase):
|
||||
plugin_order = 21
|
||||
# 可使用的用户级别
|
||||
auth_level = 2
|
||||
|
||||
# CN IP lists
|
||||
chn_route6_lists_url = "https://ispip.clang.cn/all_cn_ipv6.txt"
|
||||
chn_route_lists_url = "https://ispip.clang.cn/all_cn.txt"
|
||||
# 定时器
|
||||
_scheduler: Optional[BackgroundScheduler] = None
|
||||
# 开关
|
||||
_enabled: bool = False
|
||||
_cron: str = ""
|
||||
_notify = False
|
||||
_notify: bool = False
|
||||
_onlyonce: bool = False
|
||||
_custom_trackers: str = ""
|
||||
_exempted_domains: str = ""
|
||||
@@ -55,20 +83,16 @@ class ToBypassTrackers(_PluginBase):
|
||||
_china_ipv6_route: bool = True
|
||||
_bypass_ipv4: bool = True
|
||||
_bypass_ipv6: bool = True
|
||||
_dns_input: str = ""
|
||||
ipv6_txt: str = ""
|
||||
ipv4_txt: str = ""
|
||||
_dns_input: str | None = None
|
||||
trackers: Dict[str, List[str]] = {}
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
|
||||
self.stop_service()
|
||||
|
||||
self.trackers = {}
|
||||
self.ipv6_txt = self.get_data("ipv6_txt") if self.get_data("ipv6_txt") else ""
|
||||
self.ipv4_txt = self.get_data("ipv4_txt") if self.get_data("ipv4_txt") else ""
|
||||
|
||||
try:
|
||||
site_file = settings.ROOT_PATH/'app'/'plugins'/'tobypasstrackers'/'sites'/'trackers'
|
||||
site_file = settings.ROOT_PATH/'app'/'plugins'/self.__class__.__name__.lower()/'sites'/'trackers'
|
||||
with open(site_file, "r", encoding="utf-8") as f:
|
||||
base64_str = f.read()
|
||||
self.trackers = json.loads(base64.b64decode(base64_str).decode("utf-8"))
|
||||
@@ -76,18 +100,18 @@ class ToBypassTrackers(_PluginBase):
|
||||
logger.error(f"插件加载错误:{e}")
|
||||
# 配置
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._cron = config.get("cron")
|
||||
self._onlyonce = config.get("onlyonce")
|
||||
self._notify = config.get("notify")
|
||||
self._custom_trackers = config.get("custom_trackers")
|
||||
self._exempted_domains = config.get("exempted_domains")
|
||||
self._enabled = bool(config.get("enabled"))
|
||||
self._cron = config.get("cron") or "0 4 * * *"
|
||||
self._onlyonce = bool(config.get("onlyonce"))
|
||||
self._notify = bool(config.get("notify"))
|
||||
self._custom_trackers = config.get("custom_trackers") or ""
|
||||
self._exempted_domains = config.get("exempted_domains") or ""
|
||||
self._bypassed_sites = config.get("bypassed_sites") or []
|
||||
self._bypass_ipv4 = config.get("bypass_ipv4")
|
||||
self._bypass_ipv6 = config.get("bypass_ipv6")
|
||||
self._dns_input = config.get("dns_input")
|
||||
self._china_ipv6_route = config.get("china_ipv6_route")
|
||||
self._china_ip_route = config.get("china_ip_route")
|
||||
self._bypass_ipv4 = bool(config.get("bypass_ipv4"))
|
||||
self._bypass_ipv6 = bool(config.get("bypass_ipv6"))
|
||||
self._dns_input: str | None = config.get("dns_input")
|
||||
self._china_ipv6_route = bool(config.get("china_ipv6_route"))
|
||||
self._china_ip_route = bool(config.get("china_ip_route"))
|
||||
# 过滤掉已删除的站点
|
||||
all_sites = [site.id for site in SiteOper().list_order_by_pri()]
|
||||
self._bypassed_sites = [site_id for site_id in all_sites if site_id in self._bypassed_sites]
|
||||
@@ -95,7 +119,7 @@ class ToBypassTrackers(_PluginBase):
|
||||
if self._enabled or self._onlyonce:
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
if self._onlyonce:
|
||||
logger.info(f"立即运行一次")
|
||||
logger.info("立即运行一次")
|
||||
self._scheduler.add_job(self.update_ips, "date",
|
||||
run_date=datetime.now(
|
||||
tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3)
|
||||
@@ -128,14 +152,24 @@ class ToBypassTrackers(_PluginBase):
|
||||
|
||||
@staticmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
return [{
|
||||
"cmd": "/refresh_tracker_ips",
|
||||
"event": EventType.PluginAction,
|
||||
"desc": "更新 Tracker IP 列表",
|
||||
"data": {
|
||||
"action": "refresh_tracker_ips"
|
||||
return [
|
||||
{
|
||||
"cmd": "/refresh_tracker_ips",
|
||||
"event": EventType.PluginAction,
|
||||
"desc": "更新 Tracker IP 列表",
|
||||
"data": {
|
||||
"action": "refresh_tracker_ips"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cmd": "/check_ip",
|
||||
"event": EventType.PluginAction,
|
||||
"desc": "检测 IP 是否在绕过列表中: /check_ip <域名或IP>",
|
||||
"data": {
|
||||
"action": "check_ip"
|
||||
}
|
||||
}
|
||||
}]
|
||||
]
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
@@ -147,13 +181,15 @@ class ToBypassTrackers(_PluginBase):
|
||||
"summary": "API说明"
|
||||
}]
|
||||
"""
|
||||
return [{
|
||||
"path": "/bypassed_ips",
|
||||
"endpoint": self.bypassed_ips,
|
||||
"methods": ["GET"],
|
||||
"summary": "绕过的IP",
|
||||
"description": "绕过Clash核心的IP地址列表",
|
||||
}]
|
||||
return [
|
||||
{
|
||||
"path": "/bypassed_ips",
|
||||
"endpoint": self.bypassed_ips,
|
||||
"methods": ["GET"],
|
||||
"summary": "绕过的 IP",
|
||||
"description": "绕过 Clash 核心的 IP 地址列表",
|
||||
}
|
||||
]
|
||||
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
site_options = ([{"title": site.name, "value": site.id}
|
||||
@@ -261,7 +297,7 @@ class ToBypassTrackers(_PluginBase):
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'china_ip_route',
|
||||
'label': '合并中国大陆IPv4列表',
|
||||
'label': '合并中国大陆 IPv4 列表',
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -277,7 +313,7 @@ class ToBypassTrackers(_PluginBase):
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'china_ipv6_route',
|
||||
'label': '合并中国大陆IPv6列表',
|
||||
'label': '合并中国大陆 IPv6 列表',
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -316,7 +352,7 @@ class ToBypassTrackers(_PluginBase):
|
||||
'props': {
|
||||
'model': 'dns_input',
|
||||
'label': 'DNS 服务器',
|
||||
'placeholder': '留空则使用本地DNS'
|
||||
'placeholder': '留空则使用本地 DNS'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -357,9 +393,9 @@ class ToBypassTrackers(_PluginBase):
|
||||
'component': 'VTextarea',
|
||||
'props': {
|
||||
'model': 'custom_trackers',
|
||||
'label': '自定义Tracker服务器',
|
||||
'label': '自定义 Tracker 服务器',
|
||||
'rows': 3,
|
||||
'placeholder': '每行一个域名或IP'
|
||||
'placeholder': '每行一个域名或 IP'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -375,9 +411,76 @@ class ToBypassTrackers(_PluginBase):
|
||||
'component': 'VTextarea',
|
||||
'props': {
|
||||
'model': 'exempted_domains',
|
||||
'label': '排除的域名和IP',
|
||||
'label': '排除的域名和 IP',
|
||||
'rows': 3,
|
||||
'placeholder': '每行一个域名或IP'
|
||||
'placeholder': '每行一个域名或 IP'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCard',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCardItem',
|
||||
'props': {
|
||||
'prepend-icon': 'mdi-link-variant',
|
||||
'title': '订阅 URL',
|
||||
'subtitle': '请先在 MoviePilot 设置中配置「访问域名」',
|
||||
'class': 'pb-0'
|
||||
},
|
||||
},
|
||||
{
|
||||
'component': 'VCardActions',
|
||||
'props': {
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VBtn',
|
||||
'text': 'IPv4',
|
||||
'props': {
|
||||
'append-icon': 'mdi-open-in-new',
|
||||
'href': self.api_url(protocol=4),
|
||||
'target': '_blank'
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
'component': 'VBtn',
|
||||
'text': 'IPv6',
|
||||
'props': {
|
||||
'append-icon': 'mdi-open-in-new',
|
||||
'href': self.api_url(protocol=6),
|
||||
'target': '_blank'
|
||||
},
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'title': 'DNS 服务器示例',
|
||||
'border': 'start',
|
||||
'variant': 'tonal',
|
||||
'text': '仅填一个: '
|
||||
'「223.5.5.5」、'
|
||||
'「[2400:3200::1]:53」、'
|
||||
'「quic://dns.alidns.com:853」、'
|
||||
'「https://dns.alidns.com/dns-query」。'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -396,60 +499,13 @@ class ToBypassTrackers(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': 'DNS 服务器示例 (仅填一个): '
|
||||
'「94.140.14.140」、'
|
||||
'「94.140.14.140:53」、'
|
||||
'「[2a10:50c0::1:ff]:53」、'
|
||||
'「https://unfiltered.adguard-dns.com/dns-query」。'
|
||||
'仅支持UDP和HTTPS方法, 留空使用本地DNS查询。'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '【订阅URL】'
|
||||
f'「IPv4 API」: /api/v1/plugin/ToBypassTrackers/bypassed_ips?apikey={settings.API_TOKEN}&protocol=4; '
|
||||
f'「IPv6 API」: /api/v1/plugin/ToBypassTrackers/bypassed_ips?apikey={settings.API_TOKEN}&protocol=6'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '【如何使用】'
|
||||
'在「OpenClash->插件设置->中国大陆IP路由」选择「绕过中国大陆」; '
|
||||
'在「OpenClash->插件设置->Chnroute Update」填入「订阅URL」。'
|
||||
'color': 'info',
|
||||
'border': 'start',
|
||||
'title': '如何使用',
|
||||
'text': '在「OpenClash->插件设置->流量控制->绕过指定区域 IP」选择「绕过中国大陆」; '
|
||||
'在「OpenClash->插件设置->大陆白名单订阅」填入「订阅 URL」。'
|
||||
'使用聊天命令`/check_ip <域名或IP>`检查 IP 是否在绕过列表。'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -474,7 +530,95 @@ class ToBypassTrackers(_PluginBase):
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
pass
|
||||
headers = [
|
||||
{'title': 'IP CIDR', 'key': 'ip_cidr', 'sortable': True},
|
||||
{'title': '域名', 'key': 'domain', 'sortable': True},
|
||||
{'title': 'DNS', 'key': 'nameserver', 'sortable': True},
|
||||
{'title': '解析时间', 'key': 'datetime', 'sortable': True},
|
||||
]
|
||||
items = [IpCidrItem.model_validate(detail).to_dict()
|
||||
for detail in (self.get_data("cidr_details") or []) if detail.get('domain') != 'CN']
|
||||
excluded_items = [IpCidrItem.model_validate(detail).to_dict()
|
||||
for detail in (self.get_data("excluded_cidr_details") or [])]
|
||||
|
||||
return [
|
||||
{
|
||||
'component': 'VWindow',
|
||||
'props': {
|
||||
'show-arrows': 'hover',
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VWindowItem',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCard',
|
||||
'props': {
|
||||
'class': 'pa-0',
|
||||
'title': '绕过的 Tracker 服务器 IP 列表',
|
||||
'subtitle': '以下是已解析并添加到绕过列表中的 Tracker 服务器 IP 地址,'
|
||||
'请在 OpenClash 中配置「绕过中国大陆 IP」并订阅本列表以实现绕过效果。',
|
||||
'variant': 'elevated',
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VDataTableVirtual',
|
||||
'props': {
|
||||
'class': 'text-sm',
|
||||
'headers': headers,
|
||||
'items': items,
|
||||
'height': '30rem',
|
||||
'density': 'compact',
|
||||
'fixed-header': True,
|
||||
'hide-no-data': True,
|
||||
'hover': True
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VWindowItem',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCard',
|
||||
'props': {
|
||||
'class': 'mb-4',
|
||||
'title': '排除的 IP 列表',
|
||||
'variant': 'elevated',
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VDataTableVirtual',
|
||||
'props': {
|
||||
'class': 'text-sm',
|
||||
'headers': headers,
|
||||
'items': excluded_items,
|
||||
'height': '30rem',
|
||||
'density': 'compact',
|
||||
'fixed-header': True,
|
||||
'hide-no-data': True,
|
||||
'hover': True
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
def get_dashboard(self, key: str = None, **kwargs) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]:
|
||||
"""
|
||||
@@ -491,10 +635,14 @@ class ToBypassTrackers(_PluginBase):
|
||||
"""
|
||||
pass
|
||||
|
||||
def api_url(self, protocol: int = 4) -> str:
|
||||
return settings.MP_DOMAIN(f'/api/v1/plugin/{self.__class__.__name__}/bypassed_ips?apikey={settings.API_TOKEN}'
|
||||
f'&protocol={protocol}')
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
退出插件
|
||||
"""
|
||||
退出插件
|
||||
"""
|
||||
try:
|
||||
if self._scheduler:
|
||||
self._scheduler.remove_all_jobs()
|
||||
@@ -525,77 +673,143 @@ class ToBypassTrackers(_PluginBase):
|
||||
}]
|
||||
return []
|
||||
|
||||
def bypassed_ips(self, protocol: str) -> Response:
|
||||
if protocol == '6':
|
||||
return Response(content=self.ipv6_txt, media_type="text/plain")
|
||||
return Response(content=self.ipv4_txt, media_type="text/plain")
|
||||
def bypassed_ips(self, protocol: Literal['4', '6']) -> Response:
|
||||
data_key = "ipv4_txt" if protocol == '4' else "ipv6_txt"
|
||||
data = self.get_data(data_key) or ""
|
||||
return Response(content=data, media_type="text/plain")
|
||||
|
||||
@eventmanager.register(EventType.PluginAction)
|
||||
def update_ips(self, event: Optional[Event]=None):
|
||||
def __is_ip_in_subnet(ip_input: str, su_bnet: str) -> bool:
|
||||
"""
|
||||
Check if the given IP address is in the specified subnet.
|
||||
def check_ip(self, event: Event):
|
||||
"""检查 IP 地址 是否在绕过列表"""
|
||||
event_data = event.event_data
|
||||
if not event_data or event_data.get("action") != "check_ip":
|
||||
return
|
||||
host = event_data.get("arg_str")
|
||||
channel = event_data.get("channel")
|
||||
userid = event_data.get("userid")
|
||||
logger.info(f"检查 IP 是否绕过: {host} (来自用户 {userid},渠道 {channel})")
|
||||
ip_list, bypassed, excluded = self._check_details(host)
|
||||
if not ip_list:
|
||||
self.post_message(channel=channel, user=userid, text=f"无法解析 host: {host}", title=f"{host}")
|
||||
return
|
||||
message = ""
|
||||
for ip in ip_list:
|
||||
detail = bypassed.get(ip)
|
||||
excluded_detail = excluded.get(ip)
|
||||
if ip in excluded and excluded_detail is not None:
|
||||
detail_msg = '\n'.join(f"{k}: {v}" for k,v in excluded_detail.to_dict().items())
|
||||
message += f"\nIP 地址 {ip} 在排除列表中,不会被绕过:\n{detail_msg}\n"
|
||||
elif ip in bypassed and detail is not None:
|
||||
detail_msg = '\n'.join(f"{k}: {v}" for k,v in detail.to_dict().items())
|
||||
message += f"\nIP 地址 {ip} 会被绕过:\n{detail_msg}\n"
|
||||
else:
|
||||
message += f"\nIP 地址 {ip} 不在绕过列表中。\n"
|
||||
self.post_message(channel=channel, user=userid, text=message, title=f"{host}")
|
||||
|
||||
:param ip_input: IP address as a string (e.g., '192.168.1.1')
|
||||
:param su_bnet: Subnet in CIDR notation (e.g., '192.168.1.0/24')
|
||||
:return: True if IP is in the subnet, False otherwise
|
||||
"""
|
||||
ip_obj = ipaddress.ip_address(ip_input)
|
||||
subnet_obj = ipaddress.ip_network(su_bnet, strict=False)
|
||||
return ip_obj in subnet_obj
|
||||
@overload
|
||||
def _load_cn_ip_lists(self, family: type[IPv4Network]) -> list[IPv4Network]: ...
|
||||
|
||||
def __search_ip(_ip, ips_list):
|
||||
i = 0
|
||||
for ip_range in ips_list:
|
||||
if __is_ip_in_subnet(_ip, ip_range):
|
||||
return i
|
||||
i += 1
|
||||
return -1
|
||||
@overload
|
||||
def _load_cn_ip_lists(self, family: type[IPv6Network]) -> list[IPv6Network]: ...
|
||||
|
||||
def __exclude_ip_range(range_b: str, range_a: str):
|
||||
"""
|
||||
Exclude IP range A from IP range B and return the remaining subranges.
|
||||
def _load_cn_ip_lists(self, family: type[IPv4Network] | type[IPv6Network] = IPv4Network
|
||||
) -> list[IPv4Network | IPv6Network]:
|
||||
ip_list: list[IPv4Network | IPv6Network] = []
|
||||
if family is IPv4Network:
|
||||
url = self.chn_route_lists_url
|
||||
elif family is IPv6Network:
|
||||
url = self.chn_route6_lists_url
|
||||
else:
|
||||
raise NotImplementedError(f"unknown address family {family}")
|
||||
res = RequestUtils().get_res(url=url, raise_exception=True)
|
||||
if res is None or res.status_code != 200:
|
||||
logger.warn(f"无法获取 CN IP 列表: {url}")
|
||||
raise ConnectionError
|
||||
route_list = res.text.strip().split('\n')
|
||||
for cn_ip_cidr in route_list:
|
||||
subnet = ipaddress.ip_network(cn_ip_cidr, strict=False)
|
||||
if isinstance(subnet, family):
|
||||
ip_list.append(subnet)
|
||||
return ip_list
|
||||
|
||||
:param range_b: The larger IP range in CIDR notation (must include range_a).
|
||||
:param range_a: The smaller IP range to exclude in CIDR notation.
|
||||
:return: List of remaining IP subranges in CIDR notation.
|
||||
"""
|
||||
net_b = ipaddress.ip_network(range_b, strict=False)
|
||||
net_a = ipaddress.ip_network(range_a, strict=False)
|
||||
def _search_details(self, ip_list: list[IPv4Address | IPv6Address], data_key: str) -> dict[str, IpCidrItem | None]:
|
||||
cidr_details = [IpCidrItem.model_validate(detail) for detail in (self.get_data(data_key) or [])]
|
||||
ip_cidr_list = [ipaddress.ip_network(item.ip_cidr, strict=False) for item in cidr_details]
|
||||
details: dict[str, IpCidrItem | None] = {}
|
||||
for ip in ip_list:
|
||||
index = ToBypassTrackers._search_ip(ip, ip_cidr_list)
|
||||
if index == -1:
|
||||
details[str(ip)] = None
|
||||
continue
|
||||
details[str(ip)] = cidr_details[index]
|
||||
return details
|
||||
|
||||
if not (net_a.subnet_of(net_b)):
|
||||
raise ValueError("Range A is not fully contained within Range B.")
|
||||
def _check_details(self, host: str) -> tuple[list[str], dict[str, IpCidrItem | None], dict[str, IpCidrItem | None]]:
|
||||
try:
|
||||
ip_list = [ipaddress.ip_address(host)]
|
||||
except ValueError:
|
||||
dns = DnsHelper(dns_server=self._dns_input)
|
||||
resolved = asyncio.run(dns.resolve_name(host))
|
||||
if resolved is None:
|
||||
return [], {}, {}
|
||||
ip_list = [ipaddress.ip_address(ip) for ip in resolved]
|
||||
details = self._search_details(ip_list, "cidr_details")
|
||||
excluded = self._search_details(ip_list, "excluded_cidr_details")
|
||||
return [str(ip) for ip in ip_list], details, excluded
|
||||
|
||||
remaining_ranges = list(net_b.address_exclude(net_a))
|
||||
@staticmethod
|
||||
def _search_ip(ip: IPv4Address | IPv6Address, ips_list: list[IPv4Network | IPv6Network]) -> int:
|
||||
i = 0
|
||||
for ip_range in ips_list:
|
||||
if ip in ip_range:
|
||||
return i
|
||||
i += 1
|
||||
return -1
|
||||
|
||||
return [str(sub_net) for sub_net in remaining_ranges]
|
||||
@staticmethod
|
||||
def _search_subnet(ip: IPv4Network | IPv6Network, ips_list: list[IPv4Network | IPv6Network]) -> int:
|
||||
i = 0
|
||||
for ip_range in ips_list:
|
||||
if ip.subnet_of(ip_range):
|
||||
return i
|
||||
i += 1
|
||||
return -1
|
||||
|
||||
async def resolve_and_check(domain_, results_, failed_msg_, dns_type_, ip_list_):
|
||||
@eventmanager.register(EventType.PluginAction)
|
||||
def update_ips(self, event: Optional[Event] = None):
|
||||
|
||||
async def resolve_and_check(domain_: str, results_: dict[str, bool], failed_msg_: list[str],
|
||||
family: int, ip_list_: list[IPv4Network | IPv6Network],
|
||||
cidr_details_: list[IpCidrItem]):
|
||||
try:
|
||||
addresses = await query_helper.query_dns(domain_, dns_type_)
|
||||
addresses = await query_helper.resolve_name(domain_, family)
|
||||
if addresses is None:
|
||||
failed_msg_.append(f"【{domain_name_map.get(domain_, domain_)}】 {domain_}: {dns_type_} 记录查询失败")
|
||||
dns_type = "AAAA" if family == socket.AF_INET6 else "A"
|
||||
failed_msg_.append(f"【{domain_name_map.get(domain_, domain_)}】 {domain_}: {dns_type} 记录查询失败")
|
||||
results_[domain_name_map.get(domain_, domain_)] = False
|
||||
return
|
||||
|
||||
for address in addresses:
|
||||
has_flag = any(__is_ip_in_subnet(address, subnet) for subnet in ip_list_)
|
||||
for ip_str in addresses:
|
||||
ip_obj = ipaddress.ip_address(ip_str)
|
||||
has_flag = any(ip_obj in sub_net for sub_net in ip_list_)
|
||||
if not has_flag:
|
||||
if dns_type_ == "AAAA":
|
||||
ip_list_.append(address)
|
||||
else:
|
||||
ip_list_.append(address)
|
||||
logger.info(f"Resolving【{domain_name_map.get(domain_, domain_)}】{address} ({domain_})")
|
||||
net_obj = ipaddress.ip_network(ip_obj, strict=False)
|
||||
ip_list_.append(net_obj)
|
||||
ip_cidr_item = IpCidrItem(ip_cidr=str(net_obj), domain=domain_,
|
||||
timestamp=int(time.time()), nameserver=query_helper.nameserver)
|
||||
cidr_details_.append(ip_cidr_item)
|
||||
logger.info(f"Resolving【{domain_name_map.get(domain_, domain_)}】{ip_str} ({domain_})")
|
||||
except Exception as e:
|
||||
logger.exception(f"处理 {domain_} 出错: {e}")
|
||||
logger.warn(f"处理 {domain_} 出错: {e}")
|
||||
results_[domain_name_map.get(domain_, domain_)] = False
|
||||
|
||||
async def resolve_all(domains_, ipv6_list_, ip_list_):
|
||||
async def resolve_all(domains_: list[str], ipv6_list_: list[IPv6Network], ip_list_: list[IPv4Network],
|
||||
details: list[IpCidrItem]):
|
||||
tasks = [
|
||||
resolve_and_check(domain_, results_v6, failed_msg, "AAAA", ipv6_list_)
|
||||
resolve_and_check(domain_, results_v6, failed_msg, socket.AF_INET6, ipv6_list_, details)
|
||||
for domain_ in domains_
|
||||
]
|
||||
tasks.extend([resolve_and_check(domain_, results, failed_msg, "A", ip_list_)
|
||||
tasks.extend([resolve_and_check(domain_, results, failed_msg, socket.AF_INET, ip_list_, details)
|
||||
for domain_ in domains_])
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
@@ -604,29 +818,27 @@ class ToBypassTrackers(_PluginBase):
|
||||
if not event_data or event_data.get("action") != "refresh_tracker_ips":
|
||||
return
|
||||
query_helper = DnsHelper(self._dns_input)
|
||||
logger.info(f"开始通过 {query_helper.method_name} 解析DNS")
|
||||
chnroute6_lists_url = "https://ispip.clang.cn/all_cn_ipv6.txt"
|
||||
chnroute_lists_url = "https://ispip.clang.cn/all_cn.txt"
|
||||
ipv6_list = []
|
||||
ip_list = []
|
||||
logger.info(f"开始通过 {query_helper.nameserver} 解析DNS")
|
||||
|
||||
ipv6_list: list[IPv6Network] = []
|
||||
ip_list: list[IPv4Network] = []
|
||||
domains = []
|
||||
success_msg = []
|
||||
failed_msg = []
|
||||
results = {}
|
||||
results: dict[str, bool] = {} # 解析结果
|
||||
unsupported_msg = []
|
||||
results_v6 = {}
|
||||
results_v6: dict[str, bool] = {}
|
||||
cidr_details: list[IpCidrItem] = []
|
||||
exempted_cidr_details: list[IpCidrItem] = []
|
||||
|
||||
# 加载 CN IP 列表
|
||||
if self._china_ipv6_route:
|
||||
# Load Chnroute6 Lists
|
||||
res = RequestUtils().get_res(url=chnroute6_lists_url)
|
||||
if res is not None and res.status_code == 200:
|
||||
chnroute6_lists = res.text.strip().split('\n')
|
||||
ipv6_list = [*chnroute6_lists]
|
||||
ipv6_list = self._load_cn_ip_lists(family=IPv6Network)
|
||||
if self._china_ip_route:
|
||||
# Load Chnroute Lists
|
||||
res = RequestUtils().get_res(url=chnroute_lists_url)
|
||||
if res is not None and res.status_code == 200:
|
||||
chnroute_lists = res.text.strip().split('\n')
|
||||
ip_list = [*chnroute_lists]
|
||||
ip_list = self._load_cn_ip_lists(family=IPv4Network)
|
||||
for ip in ipv6_list + ip_list:
|
||||
cidr_details.append(IpCidrItem(ip_cidr=str(ip), domain="CN", timestamp=int(time.time())))
|
||||
|
||||
do_sites = {site.domain: site.name for site in SiteOper().list_order_by_pri() if
|
||||
site.id in self._bypassed_sites}
|
||||
domain_name_map = {}
|
||||
@@ -643,70 +855,74 @@ class ToBypassTrackers(_PluginBase):
|
||||
for custom_tracker in self._custom_trackers.split('\n'):
|
||||
if custom_tracker:
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET, custom_tracker)
|
||||
if self._bypass_ipv4:
|
||||
ip_list.append(f"{custom_tracker}/32")
|
||||
except socket.error:
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, custom_tracker)
|
||||
address = ipaddress.ip_address(custom_tracker)
|
||||
net = ipaddress.ip_network(address)
|
||||
if isinstance(net, IPv4Network):
|
||||
if self._bypass_ipv4:
|
||||
ip_list.append(net)
|
||||
elif isinstance(net, IPv6Network):
|
||||
if self._bypass_ipv6:
|
||||
ipv6_list.append(ipaddress.ip_network(f"{custom_tracker}/128", strict=False).compressed)
|
||||
except socket.error:
|
||||
ipv6_list.append(net)
|
||||
except ValueError:
|
||||
domains.append(custom_tracker)
|
||||
v6_ips = []
|
||||
v4_ips = []
|
||||
asyncio.run(resolve_all(domains, v6_ips, v4_ips))
|
||||
ipv6_list.extend([ipaddress.ip_network(f"{ad}/128", strict=False).compressed for ad in v6_ips])
|
||||
ip_list.extend([f"{ad}/32" for ad in v4_ips])
|
||||
v6_nets = []
|
||||
v4_nets = []
|
||||
asyncio.run(resolve_all(domains, v6_nets, v4_nets, cidr_details))
|
||||
ipv6_list.extend(v6_nets)
|
||||
ip_list.extend(v4_nets)
|
||||
for result in results:
|
||||
if results[result]:
|
||||
success_msg.append(f"【{result}】 Trackers已被添加")
|
||||
exempted_ip = []
|
||||
exempted_ipv6 = []
|
||||
exempted_ip: list[IPv4Network] = []
|
||||
exempted_ipv6: list[IPv6Network] = []
|
||||
exempted_domains = []
|
||||
for exempted_domain in self._exempted_domains.split('\n'):
|
||||
if exempted_domain:
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET, exempted_domain)
|
||||
if self._bypass_ipv4:
|
||||
exempted_ip.append(f"{exempted_domain}")
|
||||
except socket.error:
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, exempted_domain)
|
||||
address = ipaddress.ip_address(exempted_domain)
|
||||
net = ipaddress.ip_network(address)
|
||||
|
||||
if isinstance(net, IPv4Network):
|
||||
if self._bypass_ipv4:
|
||||
exempted_ip.append(net)
|
||||
elif isinstance(net, IPv6Network):
|
||||
if self._bypass_ipv6:
|
||||
exempted_ipv6.append(f"{exempted_domain}")
|
||||
except socket.error:
|
||||
exempted_ipv6.append(net)
|
||||
exempted_cidr_details.append(IpCidrItem(ip_cidr=str(net), domain=exempted_domain,
|
||||
timestamp=int(time.time())))
|
||||
except ValueError:
|
||||
exempted_domains.append(exempted_domain)
|
||||
|
||||
asyncio.run(resolve_all(exempted_domains, exempted_ip, exempted_ipv6))
|
||||
asyncio.run(resolve_all(exempted_domains, exempted_ipv6, exempted_ip, exempted_cidr_details))
|
||||
for ip in exempted_ip:
|
||||
index = __search_ip(ip, ip_list)
|
||||
index = ToBypassTrackers._search_subnet(ip, ip_list)
|
||||
if index == -1:
|
||||
continue
|
||||
ip_larger = ip_list[index]
|
||||
subnet = ip_list[index]
|
||||
ip_list.pop(index)
|
||||
length = int(ip_larger.split('/')[1])
|
||||
if length < 12:
|
||||
remaining_ip = __exclude_ip_range(ip_larger, f"{ip}/{length + 8}")
|
||||
ip_list.extend(remaining_ip)
|
||||
if subnet.prefixlen < 12:
|
||||
new_subnet = IPv4Network((ip.network_address, subnet.prefixlen + 8), strict=False)
|
||||
ip_list.extend(subnet.address_exclude(new_subnet))
|
||||
for ip in exempted_ipv6:
|
||||
index = __search_ip(ip, ipv6_list)
|
||||
index = ToBypassTrackers._search_subnet(ip, ipv6_list)
|
||||
if index == -1:
|
||||
continue
|
||||
ip_larger = ipv6_list[index]
|
||||
subnet = ipv6_list[index]
|
||||
ipv6_list.pop(index)
|
||||
length = int(ip_larger.split('/')[1])
|
||||
if length < 32:
|
||||
remaining_ip = __exclude_ip_range(ip_larger, f"{ip}/{min(32, length + 8)}")
|
||||
ipv6_list.extend(remaining_ip)
|
||||
self.ipv4_txt = "\n".join(ip_list)
|
||||
self.ipv6_txt = "\n".join(ipv6_list)
|
||||
self.save_data("ipv4_txt", self.ipv4_txt)
|
||||
self.save_data("ipv6_txt", self.ipv6_txt)
|
||||
if subnet.prefixlen < 32:
|
||||
new_subnet = IPv6Network((ip.network_address, min(32, subnet.prefixlen + 8)), strict=False)
|
||||
ipv6_list.extend(subnet.address_exclude(new_subnet))
|
||||
ipv4_txt = "\n".join(str(net) for net in ip_list)
|
||||
ipv6_txt = "\n".join(str(net) for net in ipv6_list)
|
||||
self.save_data("ipv4_txt", ipv4_txt)
|
||||
self.save_data("ipv6_txt", ipv6_txt)
|
||||
self.save_data("cidr_details", [detail.model_dump() for detail in cidr_details])
|
||||
self.save_data("excluded_cidr_details", [detail.model_dump() for detail in exempted_cidr_details])
|
||||
if self._notify:
|
||||
res_message = success_msg + failed_msg
|
||||
res_message = "\n".join(res_message)
|
||||
self.post_message(title=f"【绕过Trackers】",
|
||||
mtype=NotificationType.Plugin,
|
||||
text=f"{res_message}"
|
||||
)
|
||||
self.post_message(
|
||||
title=f"【{self.plugin_name}】",
|
||||
mtype=NotificationType.Plugin,
|
||||
text=f"{res_message}"
|
||||
)
|
||||
|
||||
@@ -1,86 +1,108 @@
|
||||
import re
|
||||
from typing import Optional, List, Callable
|
||||
import ipaddress
|
||||
import socket
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import dns.asyncresolver
|
||||
import dns.resolver
|
||||
from dns import asyncresolver, query
|
||||
from dns.nameserver import Do53Nameserver, DoHNameserver, DoTNameserver, DoQNameserver
|
||||
from dns.resolver import NoAnswer, NXDOMAIN
|
||||
|
||||
from app.log import logger
|
||||
|
||||
|
||||
class DnsHelper:
|
||||
def __init__(self, dns_server: str):
|
||||
self.method_name = "Local"
|
||||
self.doh_url = "https://dns.alidns.com/dns-query"
|
||||
self.__resolver = dns.asyncresolver.Resolver()
|
||||
self.__dns_query_method = self.__query_method(dns_server)
|
||||
|
||||
def __query_method(self, dns_input: str) -> Callable:
|
||||
if not dns_input:
|
||||
return self.query_dns_local
|
||||
if dns_input.startswith('https://'):
|
||||
self.doh_url = dns_input
|
||||
self.method_name = dns_input
|
||||
return self.query_dns_doh
|
||||
udp_match = re.match(r"^(?:udp://)?(\[?.+?]?)(?::(\d+))?$", dns_input)
|
||||
if udp_match:
|
||||
try:
|
||||
self.__resolver.nameservers = [udp_match.group(1).strip('[]')]
|
||||
if udp_match.group(2):
|
||||
self.__resolver.port = int(udp_match.group(2))
|
||||
self.method_name = f"udp://{self.__resolver.nameservers[0]}:{self.__resolver.port}"
|
||||
except Exception as e:
|
||||
logger.warn(f'{e}, using default resolver')
|
||||
return self.query_dns_local
|
||||
return self.query_dns_udp
|
||||
logger.warn(f'Unknown method {dns_input}, using default resolver')
|
||||
return self.query_dns_local
|
||||
def __init__(self, dns_server: str | None = None):
|
||||
self._resolver = asyncresolver.Resolver()
|
||||
self._use_tcp: bool = False
|
||||
if dns_server:
|
||||
self.nameserver = dns_server
|
||||
|
||||
async def query_dns(self, domain: str, dns_type: str = "A") -> Optional[List[str]]:
|
||||
answers = await self.__dns_query_method(domain, dns_type)
|
||||
return answers
|
||||
@property
|
||||
def nameserver(self) ->str:
|
||||
nameserver = self._resolver.nameservers[0]
|
||||
return str(nameserver)
|
||||
|
||||
async def query_dns_local(self, domain: str, dns_type: str = "A") -> Optional[List[str]]:
|
||||
@nameserver.setter
|
||||
def nameserver(self, value: str | None):
|
||||
if value is None:
|
||||
self._resolver = asyncresolver.Resolver()
|
||||
return
|
||||
self._parse_dns_server(value)
|
||||
|
||||
@staticmethod
|
||||
def get_ip_from_hostname(hostname) -> str | None:
|
||||
try:
|
||||
answer = await self.__resolver.resolve(domain, dns_type)
|
||||
return [record.address for record in answer if hasattr(record, "address")]
|
||||
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
|
||||
return []
|
||||
except Exception as e:
|
||||
# logger.error(f"本地DNS查询错误: {e} {domain}")
|
||||
# 获取IP地址
|
||||
ip = socket.gethostbyname(hostname)
|
||||
return ip
|
||||
except socket.gaierror:
|
||||
return None
|
||||
|
||||
async def query_dns_doh(self, domain: str, dns_type: str = 'A') -> Optional[List[str]]:
|
||||
@staticmethod
|
||||
def is_ip_address(hostname):
|
||||
try:
|
||||
# 尝试解析为IP地址
|
||||
ipaddress.ip_address(hostname)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def _parse_dns_server(self, dns_server: str):
|
||||
if "://" not in dns_server:
|
||||
dns_server = f"udp://{dns_server}"
|
||||
parsed = urlparse(dns_server)
|
||||
|
||||
# check and resolve the hostname
|
||||
hostname = parsed.hostname
|
||||
if hostname is None:
|
||||
return
|
||||
if DnsHelper.is_ip_address(hostname):
|
||||
address = hostname
|
||||
hostname = None
|
||||
else:
|
||||
address = DnsHelper.get_ip_from_hostname(hostname)
|
||||
if address is None:
|
||||
return
|
||||
|
||||
nameserver = None
|
||||
match parsed.scheme:
|
||||
case "udp":
|
||||
nameserver = Do53Nameserver(address, parsed.port or 53)
|
||||
case "tcp":
|
||||
nameserver = Do53Nameserver(address, parsed.port or 53)
|
||||
self._use_tcp = True
|
||||
case "https":
|
||||
nameserver = DoHNameserver(url=dns_server)
|
||||
case "tls":
|
||||
nameserver = DoTNameserver(address=address, port=parsed.port or 853, hostname=hostname)
|
||||
case "h3":
|
||||
nameserver = DoHNameserver(url=dns_server.replace("h3://", "https://"),
|
||||
http_version=query.HTTPVersion.H3)
|
||||
case "quic":
|
||||
nameserver = DoQNameserver(address=address, port=parsed.port or 853, server_hostname=hostname)
|
||||
case _:
|
||||
nameserver = None
|
||||
if nameserver is None:
|
||||
self._resolver = asyncresolver.Resolver()
|
||||
return
|
||||
self._resolver.nameservers = [nameserver]
|
||||
|
||||
async def resolve_name(self, domain: str, family: int = socket.AF_UNSPEC) -> list[str] | None:
|
||||
"""
|
||||
使用 DNS-over-HTTPS (DoH) 异步解析域名。
|
||||
异步解析域名
|
||||
|
||||
:param domain: 要解析的域名
|
||||
:param dns_type: DNS 记录类型,例如 'A', 'AAAA'
|
||||
:param family: The address family
|
||||
- socket.AF_UNSPEC: both IPv4 and IPv6 addresses
|
||||
- socket.AF_INET6: IPv6 addresses only
|
||||
- socket.AF_INET: IPv4 addresses only
|
||||
:return: IP 地址列表,或 None
|
||||
"""
|
||||
|
||||
try:
|
||||
query = dns.message.make_query(domain, dns_type)
|
||||
response = await dns.asyncquery.https(query, self.doh_url)
|
||||
return [
|
||||
item.address for rrset in response.answer for item in rrset.items
|
||||
if hasattr(item, "address")
|
||||
]
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
async def query_dns_udp(self, domain: str, dns_type: str = 'A') -> Optional[List[str]]:
|
||||
"""
|
||||
使用 UDP 异步方式解析域名
|
||||
|
||||
:param domain: 域名
|
||||
:param dns_type: 记录类型,如 A、AAAA
|
||||
:return: IP地址列表 或 None
|
||||
"""
|
||||
|
||||
try:
|
||||
answer = await self.__resolver.resolve(domain, dns_type)
|
||||
return [record.address for record in answer]
|
||||
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
|
||||
answer = await self._resolver.resolve_name(domain, family=family, tcp=self._use_tcp)
|
||||
return [a for a in answer.addresses()]
|
||||
except (NoAnswer, NXDOMAIN):
|
||||
return []
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.debug(f"DNS查询出错 ({domain}): {e} ")
|
||||
return None
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
dnspython~=2.7.0
|
||||
aioquic~=1.2.0
|
||||
dnspython~=2.8.0
|
||||
aioquic~=1.2.0
|
||||
@@ -1 +1 @@
|
||||
eyI1MnB0LnNpdGUiOiBbIjUycHQuc2l0ZSJdLCAiYXVkaWVuY2VzLm1lIjogWyJ0LmF1ZGllbmNlcy5tZSIsICJ0cmFja2VyLmNpbmVmaWxlcy5pbmZvIl0sICJidHNjaG9vbC5jbHViIjogWyJwdC5idHNjaG9vbC5jbHViIl0sICJieXIucHQiOiBbInRyYWNrZXIuYnlyLnB0Il0sICJjYXJwdC5uZXQiOiBbInRyYWNrZXIuY2FycHQubmV0Il0sICJjcmFicHQudmlwIjogWyJjcmFicHQudmlwIl0sICJjc3B0LnRvcCI6IFsidHJhY2tlci5jc3B0LnRvcCIsICJ0cmFja2VyLmNzcHQuY2MiLCAidHJhY2tlci5jc3B0LmRhdGUiXSwgImRpc2NmYW4ubmV0IjogWyJkaXNjZmFuLnh5eiJdLCAiZWFzdGdhbWUub3JnIjogWyJwdC5lYXN0Z2FtZS5vcmciXSwgImV0OC5vcmciOiBbImV0OC5vcmciLCAidC5ldDgub3JnIl0sICJnYW1lZ2FtZXB0LmNvbSI6IFsid3d3LmdhbWVnYW1lcHQuY29tIl0sICJoZGFyZWEuY2x1YiI6IFsidHJhY2tlci5oZGFyZWEuY2x1YiJdLCAiaGRkb2xieS5jb20iOiBbInQuaGRkb2xieS5jb20iXSwgImhkZmFucy5vcmciOiBbImhkZmFucy5vcmciXSwgImhka3lsLmluIjogWyJ0cmFja2VyLmhka3lsLmluIl0sICJoZHRpbWUub3JnIjogWyJoZHRpbWUub3JnIl0sICJoaXRwdC5jb20iOiBbImhpdHB0LmNvbSJdLCAiaHVkYnQuaHVzdC5lZHUuY24iOiBbImh1ZGJ0Lmh1c3QuZWR1LmNuIl0sICJpY2MyMDIyLmNvbSI6IFsidHJhY2tlci5pY2MyMDIyLnh5eiJdLCAiaWxvbGljb24uY29tIjogWyJ0cmFja2VyLmlsb2xpY29uLmNjIl0sICJrZWVwZnJkcy5jb20iOiBbInRyYWNrZXIua2VlcGZyZHMuY29tIl0sICJtLXRlYW0uY2MiOiBbInRyYWNrZXIubS10ZWFtLmNjIiwgInRyYWNrZXIubS10ZWFtLmlvIl0sICJtb25pa2FkZXNpZ24udWsiOiBbInRyYWNrZXIubW9uaWthZGVzaWduLnVrIiwgImRhaWtpcmFpLm1vbmlrYWRlc2lnbi51ayIsICJhbmltZS1uby1pbmRleC5jb20iXSwgIm5pY2VwdC5uZXQiOiBbInd3dy5uaWNlcHQubmV0Il0sICJva3B0Lm5ldCI6IFsid3d3Lm9rcHQubmV0Il0sICJwdGhvbWUubmV0IjogWyJwdGhvbWUubmV0Il0sICJwdGxncy5vcmciOiBbInB0bC5ncyIsICJyZWxheTAxLnB0bC5ncyJdLCAicHRzYmFvLmNsdWIiOiBbInB0c2Jhby5jbHViIl0sICJwdHRpbWUub3JnIjogWyJ3d3cucHR0aW1lLm9yZyJdLCAicHR6b25lLnh5eiI6IFsicHR6b25lLnh5eiJdLCAicWluZ3dhcHQuY29tIjogWyJ0cmFja2VyLnFpbmd3YS5wcm8iLCAidHJhY2tlci5xaW5nd2FwdC5jb20iXSwgInJhaW5nZmgudG9wIjogWyJyYWluZ2ZoLnRvcCJdLCAicm91c2kuemlwIjogWyJoaXRwdC5jb20iXSwgInNwcmluZ3N1bmRheS5uZXQiOiBbIm9uNi5zcHJpbmdzdW5kYXkubmV0IiwgIm9uLnNwcmluZ3N1bmRheS5uZXQiXSwgInRqdXB0Lm9yZyI6IFsidHJhY2tlci1wdWJsaWMudGp1cHQub3JnIl0sICJ0b3RoZWdsb3J5LmltIjogWyJ0cmFja2VyLnRvdGhlZ2xvcnkuaW0iXSwgInUyLmRtaHkub3JnIjogWyJkYXlkcmVhbS5kbWh5LmJlc3QiXSwgInhpbmd5dW5nZS50b3AiOiBbInRyYWNrZXIueGluZ3l1bmdlLnRvcCIsICJ0cmFja2VyLnhpbmd5dW5nZS5zYnMiXSwgInptcHQuY2MiOiBbInptcHQuY2MiXSwgImhoYW5jbHViLnRvcCI6IFsidHJhY2tlci5oaGFuY2x1Yi50b3AiXSwgImhkY2l0eS5jaXR5IjogWyJzeW5jLmxlbml0ZXIub3JnIl19
|
||||
eyI1MnB0LnNpdGUiOiBbIjUycHQuc2l0ZSJdLCAiYXVkaWVuY2VzLm1lIjogWyJ0LmF1ZGllbmNlcy5tZSIsICJ0cmFja2VyLmNpbmVmaWxlcy5pbmZvIl0sICJidHNjaG9vbC5jbHViIjogWyJwdC5idHNjaG9vbC5jbHViIl0sICJieXIucHQiOiBbInRyYWNrZXIuYnlyLnB0Il0sICJjYXJwdC5uZXQiOiBbInRyYWNrZXIuY2FycHQubmV0Il0sICJjcmFicHQudmlwIjogWyJjcmFicHQudmlwIl0sICJjc3B0LnRvcCI6IFsidHJhY2tlci5jc3B0LnRvcCIsICJ0cmFja2VyLmNzcHQuY2MiLCAidHJhY2tlci5jc3B0LmRhdGUiXSwgImRpc2NmYW4ubmV0IjogWyJkaXNjZmFuLnh5eiJdLCAiZWFzdGdhbWUub3JnIjogWyJwdC5lYXN0Z2FtZS5vcmciXSwgImV0OC5vcmciOiBbImV0OC5vcmciLCAidC5ldDgub3JnIl0sICJnYW1lZ2FtZXB0LmNvbSI6IFsid3d3LmdhbWVnYW1lcHQuY29tIl0sICJoZGFyZWEuY2x1YiI6IFsidHJhY2tlci5oZGFyZWEuY2x1YiJdLCAiaGRkb2xieS5jb20iOiBbInQuaGRkb2xieS5jb20iXSwgImhkZmFucy5vcmciOiBbImhkZmFucy5vcmciXSwgImhka3lsLmluIjogWyJ0cmFja2VyLmhka3lsLmluIiwgInd3dy5oZGt5bGluLnRvcCJdLCAiaGR0aW1lLm9yZyI6IFsiaGR0aW1lLm9yZyJdLCAiaGl0cHQuY29tIjogWyJoaXRwdC5jb20iXSwgImh1ZGJ0Lmh1c3QuZWR1LmNuIjogWyJodWRidC5odXN0LmVkdS5jbiJdLCAiaWNjMjAyMi5jb20iOiBbInRyYWNrZXIuaWNjMjAyMi54eXoiXSwgImlsb2xpY29uLmNvbSI6IFsidHJhY2tlci5pbG9saWNvbi5jYyJdLCAia2VlcGZyZHMuY29tIjogWyJ0cmFja2VyLmtlZXBmcmRzLmNvbSJdLCAibS10ZWFtLmNjIjogWyJ0cmFja2VyLm0tdGVhbS5jYyIsICJ0cmFja2VyLm0tdGVhbS5pbyJdLCAibW9uaWthZGVzaWduLnVrIjogWyJ0cmFja2VyLm1vbmlrYWRlc2lnbi51ayIsICJkYWlraXJhaS5tb25pa2FkZXNpZ24udWsiLCAiYW5pbWUtbm8taW5kZXguY29tIl0sICJuaWNlcHQubmV0IjogWyJ3d3cubmljZXB0Lm5ldCJdLCAib2twdC5uZXQiOiBbInd3dy5va3B0Lm5ldCJdLCAicHRob21lLm5ldCI6IFsicHRob21lLm5ldCJdLCAicHRsZ3Mub3JnIjogWyJwdGwuZ3MiLCAicmVsYXkwMS5wdGwuZ3MiXSwgInB0c2Jhby5jbHViIjogWyJwdHNiYW8uY2x1YiJdLCAicHR0aW1lLm9yZyI6IFsid3d3LnB0dGltZS5vcmciXSwgInB0em9uZS54eXoiOiBbInB0em9uZS54eXoiXSwgInFpbmd3YXB0LmNvbSI6IFsidHJhY2tlci5xaW5nd2EucHJvIiwgInRyYWNrZXIucWluZ3dhcHQuY29tIiwgInRyYWNrZXIucWluZ3dhcHQub3JnIl0sICJyYWluZ2ZoLnRvcCI6IFsicmFpbmdmaC50b3AiXSwgInJvdXNpLnppcCI6IFsiaGl0cHQuY29tIl0sICJzcHJpbmdzdW5kYXkubmV0IjogWyJvbjYuc3ByaW5nc3VuZGF5Lm5ldCIsICJvbi5zcHJpbmdzdW5kYXkubmV0Il0sICJ0anVwdC5vcmciOiBbInRyYWNrZXItcHVibGljLnRqdXB0Lm9yZyJdLCAidG90aGVnbG9yeS5pbSI6IFsidHJhY2tlci50b3RoZWdsb3J5LmltIl0sICJ1Mi5kbWh5Lm9yZyI6IFsiZGF5ZHJlYW0uZG1oeS5iZXN0Il0sICJ4aW5neXVuZ2UudG9wIjogWyJ0cmFja2VyLnhpbmd5dW5nZS50b3AiLCAidHJhY2tlci54aW5neXVuZ2Uuc2JzIl0sICJ6bXB0LmNjIjogWyJ6bXB0LmNjIl0sICJoaGFuY2x1Yi50b3AiOiBbInRyYWNrZXIuaGhhbmNsdWIudG9wIl0sICJoZGNpdHkuY2l0eSI6IFsic3luYy5sZW5pdGVyLm9yZyJdfQ==
|
||||
Reference in New Issue
Block a user