clash-sub-test / app.py
megatrump's picture
添加了远程 Mixin 的功能
7e7a5b0
import os
import re
import ast
import yaml
import httpx
import base64
import inspect
from dotenv import load_dotenv
from fastapi import FastAPI, Response, HTTPException, Request
HEADERS = {
'accept': 'application/json, text/plain, */*',
'accept-language': 'zh-CN',
'pragma': 'no-cache',
'sec-ch-ua': '"Not?A_Brand";v="8", "Chromium";v="108"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'cross-site',
'user-agent': 'ClashforWindows/0.20.39',
}
app = FastAPI()
# 从环境变量中获取密钥
load_dotenv()
API_KEY = os.environ.get('API_KEY')
SUBSCRIBE_URLS = os.environ.get('SUBSCRIBE_URLS') # 存储真实URL的变量
INJECT_SCRIPT = os.environ.get('INJECT_SCRIPT')
def validate_function_signature(code_string):
""" 使用 ast 验证函数签名是否符合要求。
"""
try:
module = ast.parse(code_string)
if not module.body:
print("错误:代码为空。")
return False
if len(module.body) != 1 or not isinstance(module.body[0], ast.FunctionDef):
print("错误:代码必须包含单个函数定义。")
return False
function_def = module.body[0]
# Step 01. 检查函数名
if function_def.name != "handle_mixin":
print(f"错误:函数名必须为 'handle_mixin',而不是 '{function_def.name}'。")
return False
# Step 02. 检查参数个数
args = function_def.args
if args.defaults or args.kw_defaults or args.vararg or args.kwarg or args.posonlyargs:
print("错误:函数不能有默认参数、可变位置参数、可变关键字参数、或者仅位置参数")
return False
if len(args.args) != 1:
print(f"错误:函数必须恰好接受一个参数,而不是 {len(args.args)} 个。")
return False
# Step 03. 检查参数名称
if args.args[0].arg != "content":
print(f"错误:参数名称必须为 'content',而不是 '{args.args[0].arg}'。")
return False
return True
except SyntaxError as e:
print(f"语法错误:{e}")
return False
except Exception as e:
print(f"发生错误:{e}")
return False
def load_mixin_function(code_string) -> callable:
if not validate_function_signature(code_string):
return
try:
code_obj = compile(code_string, '<string>', 'exec')
local_namespace = {}
exec(code_obj, local_namespace)
handle_mixin = local_namespace['handle_mixin']
return handle_mixin
except Exception as e:
print(f"解析函数时出错:{e}")
try:
handle_mixin = load_mixin_function(INJECT_SCRIPT)
except Exception as e:
handle_mixin = lambda x:x
print(f"无法加载 MINIX 函数, 错误信息: {e}")
def subscribe_mixin(content: str) -> str:
"""输入 YAML 字符串,输出转换后的 YAML 字符串
"""
try:
d = yaml.safe_load(content)
my_auto_group_name = "AI Unrestrict"
regex_list = [
re.compile(r"美国", re.IGNORECASE),
re.compile(r"america", re.IGNORECASE),
re.compile(r"us", re.IGNORECASE),
re.compile(r"新加坡", re.IGNORECASE),
re.compile(r"singapore", re.IGNORECASE),
re.compile(r"sg", re.IGNORECASE),
re.compile(r"加拿大", re.IGNORECASE),
re.compile(r"canada", re.IGNORECASE),
re.compile(r"ca", re.IGNORECASE),
]
matching_proxies = [] # 用于存储匹配的代理名称
# 1. 查找并保存符合正则表达式的 proxy name
if "proxies" in d and isinstance(d["proxies"], list):
for proxy in d["proxies"]:
if "name" in proxy:
for regex in regex_list:
if regex.search(proxy["name"]):
matching_proxies.append(proxy["name"])
break
# 2. 创建新的 proxy-group 对象
new_proxy_group = {
"name": my_auto_group_name,
"type": "url-test",
"proxies": matching_proxies,
"url": "http://www.gstatic.com/generate_204",
"interval": 7200
}
# 3. 将新的 proxy-group 添加到 proxy-groups 数组
if "proxy-groups" in d and isinstance(d["proxy-groups"], list):
d["proxy-groups"].append(new_proxy_group)
# 4. 将 myAutoGroupName 添加到第一个 proxy-group 的 "proxies" 列表的最前面
if d["proxy-groups"] and len(d["proxy-groups"]) > 0 and \
"proxies" in d["proxy-groups"][0] and isinstance(d["proxy-groups"][0]["proxies"], list):
d["proxy-groups"][0]["proxies"].insert(0, my_auto_group_name)
else:
d["proxy-groups"] = [new_proxy_group]
d.pop('socks-port', None)
# 在此处尝试调用 mixin 函数
try:
d = handle_mixin(d)
except Exception as e:
print(f"执行 Minix 函数时出错!")
modified_yaml = yaml.dump(d, allow_unicode=True, indent=2)
return modified_yaml
except yaml.YAMLError as e:
print(f"YAML 解析错误:{e}")
return ""
except Exception as e:
print(f"其他错误:{e}")
return ""
@app.get("/getsub")
async def read_subscribe(request: Request, key: str):
# 验证API Key
if key != API_KEY:
raise HTTPException(status_code=401, detail="Unauthorized")
# 从环境变量获取URL列表
if not SUBSCRIBE_URLS:
raise HTTPException(status_code=500, detail="SUBSCRIBE_URLS not configured")
urls = SUBSCRIBE_URLS.split('\n')
proxy_urls = [
f'{request.base_url}proxy?encoded_url={base64.b64encode(url.encode()).decode()}'
for url in urls
if url.strip()
]
merged_urls: str = '|'.join(proxy_urls)
args = {
'target': 'clash',
'url': merged_urls
}
async with httpx.AsyncClient() as client:
try:
resp = await client.get('http://127.0.0.1:25500/sub', params=args, headers=HEADERS)
resp.raise_for_status()
data = resp.text
data = subscribe_mixin(data)
return Response(content=data, media_type='text/yaml')
except httpx.RequestError as e:
raise HTTPException(status_code=500, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/proxy")
async def proxy_url(encoded_url: str):
'''
有一些订阅地址, 需要检测请求头.
然而 tindy2013/subconverter 项目并不支持请求头配置.
所以将请求重定向到本服务中, 然后通过自定义请求头绕过.
'''
try:
try:
decoded_bytes = base64.b64decode(encoded_url)
target_url = decoded_bytes.decode('utf-8')
except Exception as e:
raise HTTPException(status_code=400, detail="Invalid base64 encoding")
async with httpx.AsyncClient() as client:
response = await client.get(target_url, headers=HEADERS)
return Response(
content=response.content,
status_code=response.status_code,
headers=dict(response.headers),
media_type=response.headers.get("content-type")
)
except httpx.RequestError as e:
raise HTTPException(status_code=500, detail=f"Request failed: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@app.get("/")
async def read_root():
return {"hello": 'world'}