From bddb5916d69976583a94f5955292352ca48ee7df Mon Sep 17 00:00:00 2001 From: wumode Date: Thu, 29 May 2025 18:14:25 +0800 Subject: [PATCH] add: ClashRuleProvider --- icons/Mihomo_Meta_A.png | Bin 0 -> 59288 bytes package.v2.json | 12 + plugins.v2/clashruleprovider/__init__.py | 624 ++++++++++++++++++ .../clashruleprovider/clash_rule_parser.py | 486 ++++++++++++++ 4 files changed, 1122 insertions(+) create mode 100755 icons/Mihomo_Meta_A.png create mode 100644 plugins.v2/clashruleprovider/__init__.py create mode 100644 plugins.v2/clashruleprovider/clash_rule_parser.py diff --git a/icons/Mihomo_Meta_A.png b/icons/Mihomo_Meta_A.png new file mode 100755 index 0000000000000000000000000000000000000000..eacd778118c3b42f47a2e85043e7d2120cf8dd94 GIT binary patch literal 59288 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

tbs*&%G@7Q1vEOdcQc3(5Be5i3;)p23;df$}7 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 ap!3cY#wcW6_xXnd{BHbhb`5>S8Tnu43Ges- literal 0 HcmV?d00001 diff --git a/package.v2.json b/package.v2.json index bb3d819..f65a1a1 100644 --- a/package.v2.json +++ b/package.v2.json @@ -435,5 +435,17 @@ "v1.1": "推荐支持IMDB数据源; 优化海报尺寸,减少卡顿", "v1.0": "探索支持IMDb数据源" } + }, + "ClashRuleProvider": { + "name": "Clash Rule Provider", + "description": "随时为Clash添加一些额外的规则。", + "labels": "工具", + "version": "0.1.0", + "icon": "Mihomo_Meta_A.png", + "author": "wumode", + "level": 1, + "history": { + "v0.1.0": "新增ClashRuleProvider" + } } } diff --git a/plugins.v2/clashruleprovider/__init__.py b/plugins.v2/clashruleprovider/__init__.py new file mode 100644 index 0000000..cc1605b --- /dev/null +++ b/plugins.v2/clashruleprovider/__init__.py @@ -0,0 +1,624 @@ +import requests +import re +from typing import Any, Optional, List, Dict, Tuple, Union +import time +import yaml +import hashlib +from fastapi import Body, Response +from datetime import datetime, timedelta +import pytz + +from apscheduler.schedulers.background import BackgroundScheduler +from cachetools import cached, TTLCache +from apscheduler.triggers.cron import CronTrigger + +from app.core.config import settings +from app.core.event import eventmanager, Event +from app.log import logger +from app.plugins import _PluginBase +from app.schemas.types import EventType, NotificationType +from app.utils.http import RequestUtils +from app.plugins.clashruleprovider.clash_rule_parser import ClashRuleParser +from app.plugins.clashruleprovider.clash_rule_parser import Action, RuleType, ClashRule, MatchRule, LogicRule + + +class ClashRuleProvider(_PluginBase): + # 插件名称 + plugin_name = "Clash Rule Provider" + # 插件描述 + plugin_desc = "随时为Clash添加一些额外的规则。" + # 插件图标 + plugin_icon = ("https://raw.githubusercontent.com/wumode/MoviePilot-Plugins/" + "refs/heads/imdbsource_assets/icons/Mihomo_Meta_A.png") + # 插件版本 + plugin_version = "0.1.0" + # 插件作者 + plugin_author = "wumode" + # 作者主页 + author_url = "https://github.com/wumode" + # 插件配置项ID前缀 + plugin_config_prefix = "clashruleprovider_" + # 加载顺序 + plugin_order = 99 + # 可使用的用户级别 + auth_level = 1 + + # 插件配置 + # 启用插件 + _enabled = False + _proxy = False + _notify = False + # 订阅链接 + _sub_links = [] + # Clash 面板 URL + _clash_dashboard_url = None + # Clash 面板密钥 + _clash_dashboard_secret = None + # MoviePilot URL + _movie_pilot_url = None + _cron = '' + _timeout = 10 + _retry_times = 3 + _filter_keywords = [] + _auto_update_subscriptions = True + _ruleset_prefix = '📂<-' + + # 插件数据 + _clash_config = None + _top_rules: List[str] = [] + _ruleset_rules: List[str] = [] + _rule_provider: Dict[str, Any] = {} + _subscription_info = {} + _ruleset_names: Dict[str, str] = {} + + # protected variables + _clash_rule_parser = None + _ruleset_rule_parser = None + _custom_rule_sets = None + _scheduler: Optional[BackgroundScheduler] = None + + def init_plugin(self, config: dict = None): + self._clash_config = self.get_data("clash_config") + self._ruleset_rules = self.get_data("ruleset_rules") + self._top_rules = self.get_data("top_rules") + self._subscription_info = self.get_data("subscription_info") or \ + {"download": 0, "upload": 0, "total": 0, "expire": 0, "last_update": 0} + self._rule_provider = self.get_data("rule_provider") or {} + self._ruleset_names = self.get_data("ruleset_names") or {} + if config: + self._enabled = config.get("enabled") + self._proxy = config.get("proxy") + self._notify = config.get("notify"), + self._sub_links = config.get("sub_links") + self._clash_dashboard_url = config.get("clash_dashboard_url") + self._clash_dashboard_secret = config.get("clash_dashboard_secret") + self._movie_pilot_url = config.get("movie_pilot_url") + if self._movie_pilot_url[-1] == '/': + self._movie_pilot_url = self._movie_pilot_url[:-1] + self._cron = config.get("cron_string") + self._timeout = config.get("timeout") + self._retry_times = config.get("retry_times") + self._filter_keywords = config.get("filter_keywords") + self._ruleset_prefix = config.get("ruleset_prefix", "Custom_") + self._auto_update_subscriptions = config.get("auto_update_subscriptions") + self._clash_rule_parser = ClashRuleParser() + self._ruleset_rule_parser = ClashRuleParser() + if self._enabled: + self.__parse_config() + self._scheduler = BackgroundScheduler(timezone=settings.TZ) + self._scheduler.start() + + def get_state(self) -> bool: + return self._enabled + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + pass + + def get_api(self) -> List[Dict[str, Any]]: + return [ + { + "path": "/connectivity", + "endpoint": self.test_connectivity, + "methods": ["POST"], + "auth": "bear", + "summary": "测试连接", + "description": "测试连接" + }, + { + "path": "/clash_outbound", + "endpoint": self.get_clash_outbound, + "methods": ["GET"], + "auth": "bear", + "summary": "clash outbound", + "description": "clash outbound" + }, + { + "path": "/status", + "endpoint": self.get_status, + "methods": ["GET"], + "auth": "bear", + "summary": "stated", + "description": "state" + }, + { + "path": "/rules", + "endpoint": self.get_rules, + "methods": ["GET"], + "auth": "bear", + "summary": "clash rules", + "description": "clash rules" + }, + { + "path": "/rules", + "endpoint": self.update_rules, + "methods": ["PUT"], + "auth": "bear", + "summary": "clash rules", + "description": "clash rules" + }, + { + "path": "/reorder-rules", + "endpoint": self.reorder_rules, + "methods": ["PUT"], + "auth": "bear", + "summary": "clash rules", + "description": "clash rules" + }, + { + "path": "/rule", + "endpoint": self.update_rule, + "methods": ["PUT"], + "auth": "bear", + "summary": "clash rules", + "description": "clash rules" + }, + { + "path": "/rule", + "endpoint": self.add_rule, + "methods": ["POSt"], + "auth": "bear", + "summary": "clash rules", + "description": "clash rules" + }, + { + "path": "/rule", + "endpoint": self.delete_rule, + "methods": ["DELETE"], + "auth": "bear", + "summary": "clash rules", + "description": "clash rules" + }, + { + "path": "/subscription", + "endpoint": self.get_subscription, + "methods": ["GET"], + "auth": "bear", + "summary": "clash rules", + "description": "clash rules" + }, + { + "path": "/subscription", + "endpoint": self.update_subscription, + "methods": ["PUT"], + "auth": "bear", + "summary": "update clash rules", + "description": "update clash rules" + }, + { + "path": "/rule_providers", + "endpoint": self.get_rule_providers, + "methods": ["GET"], + "auth": "bear", + "summary": "update rule providers", + "description": "update rule providers" + }, + { + "path": "/ruleset", + "endpoint": self.get_ruleset, + "methods": ["GET"], + "summary": "update rule providers", + "description": "update rule providers" + }, + { + "path": "/config", + "endpoint": self.get_clash_config, + "methods": ["GET"], + "summary": "update rule providers", + "description": "update rule providers" + } + ] + + def get_render_mode(self) -> Tuple[str, str]: + """ + 获取插件渲染模式 + :return: 1、渲染模式,支持:vue/vuetify,默认vuetify + :return: 2、组件路径,默认 dist/assets + """ + return "vue", "dist/assets" + + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: + """ + 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 + """ + return [], {} + + def get_page(self) -> List[dict]: + return [] + + def stop_service(self): + """ + 退出插件 + """ + pass + + def get_service(self) -> List[Dict[str, Any]]: + if self.get_state() and self._auto_update_subscriptions: + return [{ + "id": "ClashRuleProvider", + "name": "Clash Rule Provider 服务", + "trigger": CronTrigger.from_crontab(self._cron), + "func": self.update_subscription_service, + "kwargs": {} + }] + return [] + + def __update_config(self): + # 保存配置 + self.update_config( + { + "enabled": self._enabled, + "cron": self._cron, + "proxy": self._proxy, + "notify": self._notify, + "sub_links": self._sub_links, + "clash_dashboard_url": self._clash_dashboard_url, + "clash_dashboard_secret": self._clash_dashboard_secret, + "movie_pilot_url": self._movie_pilot_url, + "retry_times": self._retry_times, + "timeout": self._timeout, + }) + + def __save_data(self): + self.__insert_ruleset() + self._top_rules = self._clash_rule_parser.to_string() + self._ruleset_rules = self._ruleset_rule_parser.to_string() + self.save_data('clash_config', self._clash_config) + self.save_data('ruleset_rules', self._ruleset_rules) + self.save_data('top_rules', self._top_rules) + self.save_data('subscription_info', self._subscription_info) + self.save_data('ruleset_names', self._ruleset_names) + self.save_data('rule_provider', self._rule_provider) + + def __parse_config(self): + if not self._top_rules: + return + self._clash_rule_parser.parse_rules_from_list(self._top_rules) + if not self._ruleset_rules: + return + self._ruleset_rule_parser.parse_rules_from_list(self._ruleset_rules) + + def test_connectivity(self, params: Dict[str, Any]) -> Dict[str, Any]: + if not self._enabled: + return {"success": False, "message": ""} + if not params.get('clash_dashboard_url') or not params.get('clash_dashboard_secret')\ + or not params.get('sub_link'): + return {"success": False, "message": "missing params"} + clash_version_url = f"{params.get('clash_dashboard_url')}/version" + ret = RequestUtils(accept_type="application/json", + headers={"authorization": f"Bearer {params.get('clash_dashboard_secret')}"} + ).get(clash_version_url) + if not ret: + return {"success": False, "message": "无法连接到Clash"} + ret = RequestUtils(accept_type="text/html", + proxies=settings.PROXY if self._proxy else None + ).get(params.get('sub_link')) + if not ret: + return {"success": False, "message": f"Unable to get {params.get('sub_link')}"} + return {"success": True, "message": "测试连接成功"} + + def get_ruleset(self, name): + if not self._ruleset_names.get(name): + return None + name = self._ruleset_names.get(name) + rules = self.__get_ruleset(name) + # if rules or ruleset in self._rule_provider: + # self._rule_provider[ruleset] = rules + res = yaml.dump({"payload": rules}, allow_unicode=True) + return Response(content=res, media_type="text/yaml") + + def get_clash_outbound(self): + outbound = self.clash_outbound(self._clash_config) + return {"success": True, "message": None, "data": {"outbound": outbound}} + + def get_status(self): + return {"success": True, "message": "", + "data": {"state": self._enabled, + "ruleset_prefix": self._ruleset_prefix, + "clash": {"rule_size": len(self._clash_config.get("rules", []))}, + "subscription_info": self._subscription_info, + "sub_url": f"{self._movie_pilot_url}/api/v1/plugin/ClashRuleProvider/config?" + f"apikey={settings.API_TOKEN}"}} + + def get_clash_config(self): + config = self.clash_config() + if not config: + return {"success": False, "message": ""} + res = yaml.dump(config, allow_unicode=True) + headers = {'Subscription-Userinfo': f'upload={self._subscription_info["upload"]}; ' + f'download={self._subscription_info["download"]}; ' + f'total={self._subscription_info["total"]}; ' + f'expire={self._subscription_info["expire"]}'} + return Response(headers=headers, content=res, media_type="text/yaml") + + def get_rules(self, rule_type: str) -> Dict[str, Any]: + if rule_type == 'ruleset': + return {"success": True, "message": None, "data": {"rules": self._ruleset_rule_parser.to_dict()}} + return {"success": True, "message": None, "data": {"rules": self._clash_rule_parser.to_dict()}} + + def delete_rule(self, params: dict = Body(...)): + if not self._enabled: + return {"success": False, "message": ""} + if params.get('type') == 'ruleset': + res = self.delete_rule_by_priority(params.get('priority'), self._ruleset_rule_parser) + if res: + self.__add_notification_job(f"{self._ruleset_prefix}{res.action.value if isinstance(res.action, Action) else res.action}") + else: + res = self.delete_rule_by_priority(params.get('priority'), self._clash_rule_parser) + return {"success": res, "message": None} + + def reorder_rules(self, params: Dict[str, Any]): + if not self._enabled: + return {"success": False, "message": ""} + moved_priority = params.get('moved_priority') + target_priority = params.get('target_priority') + try: + if params.get('type') == 'ruleset': + self.__reorder_rules(self._ruleset_rule_parser, moved_priority, target_priority) + self.__add_notification_job(f"{self._ruleset_prefix}{params.get('rule_data').get('action')}") + else: + self.__reorder_rules(self._clash_rule_parser, moved_priority, target_priority) + except Exception as e: + return {"success": False, "message": str(e)} + return {"success": True, "message": None} + + def update_rules(self, params: Dict[str, Any]): + if not self._enabled: + return {"success": False, "message": ""} + if params.get('type') == 'ruleset': + self.__update_rules(params.get('rules'), self._ruleset_rule_parser) + else: + self.__update_rules(params.get('rules'), self._clash_rule_parser) + return {"success": True, "message": None} + + def update_rule(self, params: Dict[str, Any]) -> Dict[str, Any]: + if not self._enabled: + return {"success": False, "message": ""} + if params.get('type') == 'ruleset': + res = self.update_rule_by_priority(params.get('rule_data'), self._ruleset_rule_parser) + if res: + self.__add_notification_job(f"{self._ruleset_prefix}{params.get('rule_data').get('action')}") + else: + res = self.update_rule_by_priority(params.get('rule_data'), self._clash_rule_parser) + return {"success": bool(res), "message": None} + + def add_rule(self, params: Dict[str, Any]) -> Dict[str, Any]: + if not self._enabled: + return {"success": False, "message": ""} + if params.get('type') == 'ruleset': + res = self.add_rule_by_priority(params.get('rule_data'), self._ruleset_rule_parser) + if res: + self.__add_notification_job(f"{self._ruleset_prefix}{params.get('rule_data').get('action')}") + else: + res = self.add_rule_by_priority(params.get('rule_data'), self._clash_rule_parser) + return {"success": bool(res), "message": None} + + def get_subscription(self): + if not self._sub_links: + return None + return {"success": True, "message": None, "data": {"url": self._sub_links[0]}} + + def update_subscription(self, params: Dict[str, Any]): + if not self._enabled: + return {"success": False, "message": ""} + url = params.get('url') + if not url: + return {"success": False, "message": "missing params"} + res = self.update_subscription_service() + if not res: + return {"success": True, "message": f"订阅链接 {self._sub_links[0]} 更新失败"} + return {"success": True, "message": "订阅更新成功"} + + def get_rule_providers(self): + return {"success": True, "message": None, "data": self.rule_providers()} + + @staticmethod + def clash_outbound(clash_config: Dict[str, Any]) -> Optional[List]: + if not clash_config: + return [] + outbound = [{'name': proxy_group.get("name")} for proxy_group in clash_config.get("proxy-groups")] + outbound.extend([{'name': proxy.get("name")} for proxy in clash_config.get("proxies")]) + return outbound + + def rule_providers(self) -> Optional[Dict[str, Any]]: + if not self._clash_config: + return None + rule_providers = {} + for key, value in self._clash_config.get("rule-providers", {}): + if value.get("path", '').startwith("./CRP/"): + continue + rule_providers[key] = value + return rule_providers + + def __update_rules(self, rules: List[Dict[str, Any]], rule_parser: ClashRuleParser): + rule_parser.rules = [] + for rule in rules: + clash_rule = ClashRuleParser.parse_rule_dict(rule) + rule_parser.insert_rule_at_priority(clash_rule, rule.get("priority")) + self.__save_data() + + def __reorder_rules(self, rule_parser: ClashRuleParser, moved_priority, target_priority): + rule_parser.reorder_rules(moved_priority, target_priority) + self.__save_data() + + def __get_ruleset(self, ruleset: str) -> List[str]: + if ruleset.startswith(self._ruleset_prefix): + action = ruleset[len(self._ruleset_prefix):] + else: + return [] + try: + action_enum = Action(action.upper()) + final_action = action_enum + except ValueError: + final_action = action + rules = self._ruleset_rule_parser.filter_rules_by_action(final_action) + res = [] + for rule in rules: + res.append(rule.condition_string()) + return res + + def __insert_ruleset(self): + outbounds = [] + for rule in self._ruleset_rule_parser.rules: + action_str = f"{rule.action.value}" if isinstance(rule.action, Action) else rule.action + if action_str not in outbounds: + outbounds.append(action_str) + self._clash_rule_parser.remove_rules(lambda r: r.rule_type == RuleType.RULE_SET and + r.payload.startswith(self._ruleset_prefix)) + for outbound in outbounds: + clash_rule = ClashRuleParser.parse_rule_line(f"RULE-SET,{self._ruleset_prefix}{outbound},{outbound}") + if not self._clash_rule_parser.has_rule(clash_rule): + self._clash_rule_parser.insert_rule_at_priority(clash_rule, 0) + + def update_rule_by_priority(self, rule: Dict[str, Any], rule_parser: ClashRuleParser) -> bool: + if not isinstance(rule.get("priority"), int): + return False + clash_rule = ClashRuleParser.parse_rule_dict(rule) + if not clash_rule: + return False + res = rule_parser.update_rule_at_priority(clash_rule, rule.get("priority")) + self.__save_data() + return res + + def add_rule_by_priority(self, rule: Dict[str, Any], rule_parser: ClashRuleParser) -> bool: + if not isinstance(rule.get("priority"), int): + return False + try: + clash_rule = self._clash_rule_parser.parse_rule_dict(rule) + except ValueError: + logger.warn(f"无效的输入规则: {rule}") + return False + if not clash_rule: + return False + rule_parser.insert_rule_at_priority(clash_rule, rule.get("priority")) + self.__save_data() + return True + + def delete_rule_by_priority(self, priority: int, rule_parser: ClashRuleParser + ) -> Optional[Union[ClashRule, LogicRule, MatchRule]]: + if not isinstance(priority, int): + return None + res = rule_parser.remove_rule_at_priority(priority) + self.__save_data() + return res + + @eventmanager.register(EventType.PluginAction) + def update_subscription_service(self) -> bool: + if not self._sub_links: + return False + url = self._sub_links[0] + ret = RequestUtils(accept_type="text/html", + proxies=settings.PROXY if self._proxy else None + ).get_res(url) + if not ret: + return False + try: + rs = yaml.load(ret.content, Loader=yaml.FullLoader) + self._clash_config = self.__remove_nodes_by_keywords(rs) + except Exception as e: + logger.error(f"解析配置出错: {e}") + return False + if 'Subscription-Userinfo' in ret.headers: + matches = re.findall(r'(\w+)=(\d+)', ret.headers['Subscription-Userinfo']) + variables = {key: int(value) for key, value in matches} + self._subscription_info['download'] = variables['download'] + self._subscription_info['upload'] = variables['upload'] + self._subscription_info['total'] = variables['total'] + self._subscription_info['expire'] = variables['expire'] + self._subscription_info["last_update"] = int(time.time()) + self.save_data('subscription_info', self._subscription_info) + self.save_data('clash_config', self._clash_config) + return True + + def notify_clash(self, ruleset: str): + url = f'{self._clash_dashboard_url}/providers/rules/{ruleset}' + RequestUtils(content_type="application/json", + headers={"authorization": f"Bearer {self._clash_dashboard_secret}"} + ).put(url) + + def __add_notification_job(self, ruleset: str): + if ruleset in self._rule_provider: + self._scheduler.add_job(self.notify_clash, "date", + run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=30), + args=[ruleset], + id='CRP-notify-clash', + replace_existing=True + ) + + def __remove_nodes_by_keywords(self, clash_config: Dict[str, Any]) -> Dict[str, Any]: + removed_proxies = [] + proxies = [] + for proxy in clash_config.get("proxies", []): + has_keywords = bool(len([x for x in self._filter_keywords if x in proxy.get("name", '')])) + if has_keywords: + removed_proxies.append(proxy.get("name")) + else: + proxies.append(proxy) + if proxies: + clash_config["proxies"] = proxies + else: + logger.warn(f"关键词过滤后无可用节点,跳过过滤") + removed_proxies = [] + for proxy_group in clash_config.get("proxy-groups", []): + proxy_group['proxies'] = [x for x in proxy_group.get('proxies') if x not in removed_proxies] + clash_config["proxy-groups"] = [x for x in clash_config.get("proxy-groups", []) if x.get("proxies")] + return clash_config + + def clash_config(self) -> Optional[Dict[str, Any]]: + if not self._clash_config: + return + self.__insert_ruleset() + self._top_rules = self._clash_rule_parser.to_string() + clash_config = self._clash_config.copy() + top_rules = [] + for rule in self._clash_rule_parser.rules: + if (not isinstance(rule.action, Action) and + not len([x for x in self.clash_outbound(clash_config) if rule.action == x.get("name", '')])): + logger.warn(f"出站 {rule.action} 不存在, 绕过 {rule.raw_rule}") + continue + top_rules.append(rule.raw_rule) + clash_config["rules"] = self._top_rules + clash_config.get("rules", []) + self._rule_provider = {} + for r in self._clash_rule_parser.rules: + if r.rule_type == RuleType.RULE_SET and r.payload.startswith(self._ruleset_prefix): + action_str = f"{r.action.value}" if isinstance(r.action, Action) else r.action + path_name = hashlib.sha256(action_str.encode('utf-8')).hexdigest()[:10] + self._ruleset_names[path_name] = r.payload + sub_url = (f"{self._movie_pilot_url}/api/v1/plugin/ClashRuleProvider/ruleset?" + f"name={path_name}&apikey={settings.API_TOKEN}") + self._rule_provider[r.payload] = {"behavior": "classical", + "format": "yaml", + "interval": 3600, + "path": f"./CRP/{path_name}.yaml", + "type": "http", + "url": sub_url} + if clash_config.get("rule-providers"): + clash_config['rule-providers'].update(self._rule_provider) + else: + clash_config['rule-providers'] = self._rule_provider + for key, item in self._ruleset_names.items(): + if item not in clash_config['rule-providers']: + del self._ruleset_names[key] + self.save_data('ruleset_names', self._ruleset_names) + self.save_data('rule_provider', self._rule_provider) + return clash_config diff --git a/plugins.v2/clashruleprovider/clash_rule_parser.py b/plugins.v2/clashruleprovider/clash_rule_parser.py new file mode 100644 index 0000000..071d427 --- /dev/null +++ b/plugins.v2/clashruleprovider/clash_rule_parser.py @@ -0,0 +1,486 @@ +import re +from typing import List, Dict, Any, Optional, Union, Callable +from dataclasses import dataclass +from enum import Enum + + +class RuleType(Enum): + """Enumeration of all supported Clash rule types""" + DOMAIN = "DOMAIN" + DOMAIN_SUFFIX = "DOMAIN-SUFFIX" + DOMAIN_KEYWORD = "DOMAIN-KEYWORD" + DOMAIN_REGEX = "DOMAIN-REGEX" + GEOSITE = "GEOSITE" + + IP_CIDR = "IP-CIDR" + IP_CIDR6 = "IP-CIDR6" + IP_SUFFIX = "IP-SUFFIX" + IP_ASN = "IP-ASN" + GEOIP = "GEOIP" + + SRC_GEOIP = "SRC-GEOIP" + SRC_IP_ASN = "SRC-IP-ASN" + SRC_IP_CIDR = "SRC-IP-CIDR" + SRC_IP_SUFFIX = "SRC-IP-SUFFIX" + + DST_PORT = "DST-PORT" + SRC_PORT = "SRC-PORT" + + IN_PORT = "IN-PORT" + IN_TYPE = "IN-TYPE" + IN_USER = "IN-USER" + IN_NAME = "IN-NAME" + + PROCESS_PATH = "PROCESS-PATH" + PROCESS_PATH_REGEX = "PROCESS-PATH-REGEX" + PROCESS_NAME = "PROCESS-NAME" + PROCESS_NAME_REGEX = "PROCESS-NAME-REGEX" + + UID = "UID" + NETWORK = "NETWORK" + DSCP = "DSCP" + + RULE_SET = "RULE-SET" + AND = "AND" + OR = "OR" + NOT = "NOT" + SUB_RULE = "SUB-RULE" + + MATCH = "MATCH" + + +class Action(Enum): + """Enumeration of rule actions""" + DIRECT = "DIRECT" + REJECT = "REJECT" + REJECT_DROP = "REJECT-DROP" + PASS = "PASS" + COMPATIBLE = "COMPATIBLE" + + +@dataclass +class ClashRule: + """Represents a parsed Clash routing rule""" + rule_type: RuleType + payload: str + action: Union[Action, str] # Can be Action enum or custom proxy group name + additional_params: Optional[List[str]] = None + raw_rule: str = "" + priority: int = 0 + + def __post_init__(self): + if self.additional_params is None: + self.additional_params = [] + + def condition_string(self) -> str: + return f"{self.rule_type.value},{self.payload}" + + +@dataclass +class LogicRule: + """Represents a logic rule (AND, OR, NOT)""" + logic_type: RuleType + conditions: List[Union[ClashRule, 'LogicRule']] + action: Union[Action, str] + raw_rule: str = "" + priority: int = 0 + + def condition_string(self) -> str: + conditions_str = ','.join([f"({c.condition_string()})" for c in self.conditions]) + return f"{self.logic_type.value},({conditions_str})" + + +@dataclass +class MatchRule: + """Represents a match rule""" + action: Union[Action, str] + raw_rule: str = "" + priority: int = 0 + rule_type: RuleType = RuleType.MATCH + + @staticmethod + def condition_string() -> str: + return "MATCH" + + +class ClashRuleParser: + """Parser for Clash routing rules""" + + def __init__(self): + self.rules: List[Union[ClashRule, LogicRule, MatchRule]] = [] + + @staticmethod + def parse_rule_line(line: str) -> Optional[Union[ClashRule, LogicRule, MatchRule]]: + """Parse a single rule line""" + line = line.strip() + try: + # Handle logic rules (AND, OR, NOT) + + if line.startswith(('AND,', 'OR,', 'NOT,')): + return ClashRuleParser._parse_logic_rule(line) + elif line.startswith('MATCH'): + return ClashRuleParser._parse_match_rule(line) + # Handle regular rules + return ClashRuleParser._parse_regular_rule(line) + + except Exception as e: + print(f"Error parsing rule '{line}': {e}") + return None + + @staticmethod + def parse_rule_dict(clash_rule: Dict[str, Any]) -> Optional[Union[ClashRule, LogicRule, MatchRule]]: + if not clash_rule: + return None + if clash_rule.get("type") in ('AND', 'OR', 'NOT'): + conditions = clash_rule.get("conditions") + if not conditions: + return None + conditions_str = '' + for condition in conditions: + conditions_str += f'({condition.get("type")},{condition.get("payload")})' + conditions_str = f"({conditions_str})" + raw_rule = f"{clash_rule.get('type')},{conditions_str},{clash_rule.get('action')}" + return ClashRuleParser._parse_logic_rule(raw_rule) + elif clash_rule.get("type") == 'MATCH': + raw_rule = f"{clash_rule.get('type')},{clash_rule.get('action')}" + return ClashRuleParser._parse_match_rule(raw_rule) + else: + raw_rule = f"{clash_rule.get('type')},{clash_rule.get('payload')},{clash_rule.get('action')}" + return ClashRuleParser._parse_regular_rule(raw_rule) + + @staticmethod + def _parse_match_rule(line: str) -> MatchRule: + parts = line.split(',') + if len(parts) < 2: + raise ValueError(f"Invalid rule format: {line}") + action = parts[1] + # Validate rule type + try: + action_enum = Action(action.upper()) + final_action = action_enum + except ValueError: + final_action = action + + return MatchRule( + action=final_action, + raw_rule=line + ) + + @staticmethod + def _parse_regular_rule(line: str) -> ClashRule: + """Parse a regular (non-logic) rule""" + parts = line.split(',') + + if len(parts) < 3: + raise ValueError(f"Invalid rule format: {line}") + + rule_type_str = parts[0].upper() + payload = parts[1] + action = parts[2] + + if not payload or not rule_type_str: + raise ValueError(f"Invalid rule format: {line}") + + additional_params = parts[3:] if len(parts) > 3 else [] + + # Validate rule type + try: + rule_type = RuleType(rule_type_str) + except ValueError: + raise ValueError(f"Unknown rule type: {rule_type_str}") + + # Try to convert action to enum, otherwise keep as string (custom proxy group) + try: + action_enum = Action(action.upper()) + final_action = action_enum + except ValueError: + final_action = action + + return ClashRule( + rule_type=rule_type, + payload=payload, + action=final_action, + additional_params=additional_params, + raw_rule=line + ) + + @staticmethod + def _parse_logic_rule(line: str) -> LogicRule: + """Parse a logic rule (AND, OR, NOT)""" + # Extract logic type + logic_rule_match = re.match(r'^(AND|OR|NOT),\((.+)\),([^,]+)$', line) + if not logic_rule_match: + raise ValueError(f"Cannot extract action from logic rule: {line}") + logic_type_str = logic_rule_match.group(1).upper() + logic_type = RuleType(logic_type_str) + action = logic_rule_match.group(3) + # Try to convert action to enum + try: + action_enum = Action(action.upper()) + final_action = action_enum + except ValueError: + final_action = action + conditions_str = logic_rule_match.group(2) + conditions = ClashRuleParser._parse_logic_conditions(conditions_str) + + return LogicRule( + logic_type=logic_type, + conditions=conditions, + action=final_action, + raw_rule=line + ) + + @staticmethod + def _parse_logic_conditions(conditions_str: str) -> List[ClashRule]: + """Parse conditions within logic rules""" + conditions = [] + + # Simple parser for conditions like (DOMAIN,baidu.com),(NETWORK,UDP) + # This is a basic implementation - more complex nested logic would need a proper parser + condition_pattern = r'\(([^,]+),([^)]+)\)' + matches = re.findall(condition_pattern, conditions_str) + + for rule_type_str, payload in matches: + try: + rule_type = RuleType(rule_type_str.upper()) + condition = ClashRule( + rule_type=rule_type, + payload=payload, + action="", # Logic conditions don't have actions + raw_rule=f"{rule_type_str},{payload}" + ) + conditions.append(condition) + except ValueError: + print(f"Unknown rule type in logic condition: {rule_type_str}") + + return conditions + + def parse_rules(self, rules_text: str) -> List[Union[ClashRule, LogicRule, MatchRule]]: + """Parse multiple rules from text, preserving order and priority""" + self.rules = [] + lines = rules_text.strip().split('\n') + priority = 0 + + for line in lines: + rule = self.parse_rule_line(line) + if rule: + rule.priority = priority # Assign priority based on position + self.rules.append(rule) + priority += 1 + + return self.rules + + def parse_rules_from_list(self, rules_list: List[str]) -> List[Union[ClashRule, LogicRule, MatchRule]]: + """Parse rules from a list of rule strings, preserving order and priority""" + self.rules = [] + + for priority, rule_str in enumerate(rules_list): + rule = self.parse_rule_line(rule_str) + if rule: + rule.priority = priority # Assign priority based on list position + self.rules.append(rule) + + return self.rules + + def validate_rule(self, rule: ClashRule) -> bool: + """Validate a parsed rule""" + try: + # Basic validation based on rule type + if rule.rule_type in [RuleType.IP_CIDR, RuleType.IP_CIDR6]: + # Validate CIDR format + return '/' in rule.payload + + elif rule.rule_type == RuleType.DST_PORT or rule.rule_type == RuleType.SRC_PORT: + # Validate port number/range + return rule.payload.isdigit() or '-' in rule.payload + + elif rule.rule_type == RuleType.NETWORK: + # Validate network type + return rule.payload.lower() in ['tcp', 'udp'] + + elif rule.rule_type == RuleType.DOMAIN_REGEX or rule.rule_type == RuleType.PROCESS_PATH_REGEX: + # Try to compile regex + re.compile(rule.payload) + return True + + return True + + except Exception: + return False + + def to_string(self) -> List[str]: + result = [] + for rule in self.rules: + result.append(rule.raw_rule) + return result + + def to_dict(self) -> List[Dict[str, Any]]: + """Convert parsed rules to dictionary format""" + result = [] + + for rule in self.rules: + if isinstance(rule, ClashRule): + rule_dict = { + 'type': rule.rule_type.value, + 'payload': rule.payload, + 'action': rule.action.value if isinstance(rule.action, Action) else rule.action, + 'additional_params': rule.additional_params, + 'priority': rule.priority, + 'raw': rule.raw_rule + } + result.append(rule_dict) + + elif isinstance(rule, LogicRule): + conditions_dict = [] + for condition in rule.conditions: + if isinstance(condition, ClashRule): + conditions_dict.append({ + 'type': condition.rule_type.value, + 'payload': condition.payload + }) + + rule_dict = { + 'type': rule.logic_type.value, + 'conditions': conditions_dict, + 'action': rule.action.value if isinstance(rule.action, Action) else rule.action, + 'priority': rule.priority, + 'raw': rule.raw_rule + } + result.append(rule_dict) + elif isinstance(rule, MatchRule): + rule_dict = { + 'type': 'MATCH', + 'action': rule.action.value if isinstance(rule.action, Action) else rule.action, + 'priority': rule.priority, + 'raw': rule.raw_rule + } + result.append(rule_dict) + return result + + def get_rules_by_priority(self) -> List[Union[ClashRule, LogicRule, MatchRule]]: + """Get rules sorted by priority (highest priority first)""" + return sorted(self.rules, key=lambda rule: rule.priority) + + def append_rule(self, rule: Union[ClashRule, LogicRule, MatchRule]) -> None: + max_priority = max(rule.priority for rule in self.rules) if len(self.rules) else 0 + rule.priority = max_priority + 1 + self.rules.append(rule) + # Re-sort rules to maintain order + self.rules.sort(key=lambda r: r.priority) + + def insert_rule_at_priority(self, rule: Union[ClashRule, LogicRule, MatchRule], priority: int): + """Insert a rule at a specific priority position, adjusting other rules""" + # Adjust priorities of existing rules + for existing_rule in self.rules: + if existing_rule.priority >= priority: + existing_rule.priority += 1 + rule.priority = priority + self.rules.append(rule) + + # Re-sort rules to maintain order + self.rules.sort(key=lambda r: r.priority) + + def update_rule_at_priority(self, clash_rule: Union[ClashRule, LogicRule], priority: int) -> bool: + for index, existing_rule in enumerate(self.rules): + if existing_rule.priority == priority: + self.rules[index] = clash_rule + self.rules[index].priority = priority + return True + return False + + def remove_rule_at_priority(self, priority: int) -> Optional[Union[ClashRule, LogicRule, MatchRule]]: + """Remove rule at specific priority and adjust remaining priorities""" + rule_to_remove = None + for rule in self.rules: + if rule.priority == priority: + rule_to_remove = rule + break + + if rule_to_remove: + self.rules.remove(rule_to_remove) + + # Adjust priorities of remaining rules + for rule in self.rules: + if rule.priority > priority: + rule.priority -= 1 + + return rule_to_remove + return None + + def remove_rules(self, condition: Callable[[Union[ClashRule, LogicRule, MatchRule]], bool]): + """Remove rules by lambda""" + i = 0 + while i < len(self.rules): + if condition(self.rules[i]): + priority = self.rules[i].priority + for rule in self.rules: + if rule.priority > priority: + rule.priority -= 1 + del self.rules[i] + else: + i += 1 + + def move_rule_priority(self, from_priority: int, to_priority: int) -> bool: + """Move a rule from one priority position to another""" + rule_to_move = None + for rule in self.rules: + if rule.priority == from_priority: + rule_to_move = rule + break + + if not rule_to_move: + return False + + # Remove rule temporarily + self.remove_rule_at_priority(from_priority) + + # Insert at new priority + self.insert_rule_at_priority(rule_to_move, to_priority) + + return True + + def filter_rules_by_type(self, rule_type: RuleType) -> List[ClashRule]: + """Filter rules by type""" + return [rule for rule in self.rules + if isinstance(rule, ClashRule) and rule.rule_type == rule_type] + + def filter_rules_by_action(self, action: Union[Action, str]) -> List[Union[ClashRule, LogicRule, MatchRule]]: + """Filter rules by action""" + return [rule for rule in self.rules if rule.action == action] + + def has_rule(self, clash_rule: Union[ClashRule, LogicRule, MatchRule]) -> bool: + for rule in self.rules: + if rule.rule_type == clash_rule.rule_type and rule.action == clash_rule.action \ + and rule.payload == clash_rule.payload: + return True + return False + + def reorder_rules( + self, + moved_rule_priority: int, + target_priority: int, + ): + """ + Reorder rules + + :param moved_rule_priority: 被移动规则的原始优先级 + :param target_priority: 目标位置的优先级 + """ + moved_index = next(i for i, r in enumerate(self.rules) if r.priority == moved_rule_priority) + target_index = next( + (i for i, r in enumerate(self.rules) if r.priority == target_priority), + len(self.rules) + ) + # 直接修改被移动规则的优先级 + moved_rule = self.rules[moved_index] + moved_rule.priority = target_priority + + if moved_index < target_index: + # 向后移动:原位置到目标位置之间的规则优先级 -1 + for i in range(moved_index + 1, target_index + 1): + self.rules[i].priority -= 1 + elif moved_index > target_index: + # 向前移动:目标位置到原位置之间的规则优先级 +1 + for i in range(target_index, moved_index): + self.rules[i].priority += 1 + self.rules.sort(key=lambda x: x.priority)