This commit is contained in:
yangfan
2025-08-14 11:33:08 +08:00
parent 9838382619
commit 63b7800b59
11 changed files with 667 additions and 9 deletions

194
app.py Normal file
View File

@@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""
Flask Web Service for Clash Subscription
提供订阅服务接口 host:4090/sub/[token]
"""
import os
import base64
import urllib.parse
import yaml
from flask import Flask, request, jsonify, Response
from merger import ClashSubscriptionMerger
app = Flask(__name__)
class SubscriptionService:
def __init__(self, token_file: str = "token.txt"):
self.token_file = token_file
self.merger = ClashSubscriptionMerger()
def load_valid_tokens(self) -> set:
"""从token.txt加载有效token"""
try:
if not os.path.exists(self.token_file):
return set()
with open(self.token_file, 'r', encoding='utf-8') as f:
tokens = set()
for line in f:
token = line.strip()
if token:
tokens.add(token)
return tokens
except Exception as e:
print(f"加载token文件失败: {e}")
return set()
def validate_token(self, token: str) -> bool:
"""验证token是否有效"""
valid_tokens = self.load_valid_tokens()
return token in valid_tokens
def clash_to_ss_url(self, proxy: dict) -> str:
"""将Clash节点转换为SS URL格式"""
try:
proxy_type = proxy.get('type', '').lower()
name = proxy.get('name', '')
server = proxy.get('server', '')
port = proxy.get('port', 0)
# 只处理支持的代理类型
if proxy_type == 'ss':
cipher = proxy.get('cipher', '')
password = proxy.get('password', '')
# 构建SS URL: ss://method:password@server:port#name
auth_string = f"{cipher}:{password}"
encoded_auth = base64.b64encode(auth_string.encode('utf-8')).decode('utf-8')
encoded_name = urllib.parse.quote(name, safe='')
return f"ss://{encoded_auth}@{server}:{port}#{encoded_name}"
elif proxy_type == 'trojan':
password = proxy.get('password', '')
encoded_name = urllib.parse.quote(name, safe='')
# Trojan URL格式: trojan://password@server:port#name
return f"trojan://{password}@{server}:{port}#{encoded_name}"
elif proxy_type == 'vmess':
# VMess需要更复杂的处理这里提供基本实现
uuid = proxy.get('uuid', '')
encoded_name = urllib.parse.quote(name, safe='')
# 简化的VMess URL格式
return f"vmess://{uuid}@{server}:{port}#{encoded_name}"
else:
# 对于不支持的类型,使用通用格式
encoded_name = urllib.parse.quote(name, safe='')
return f"{proxy_type}://{server}:{port}#{encoded_name}"
except Exception as e:
print(f"转换节点失败 {proxy.get('name', '')}: {e}")
return ""
def generate_ss_subscription(self) -> str:
"""生成SS格式的订阅内容并返回Base64编码"""
try:
# 运行合并处理
subscriptions = self.merger.load_subscriptions()
if not subscriptions:
raise Exception("没有找到有效的订阅配置")
# 获取并保存每个订阅的内容
for subscription in subscriptions:
group_name = subscription.get('group_name', '')
url = subscription.get('url', '')
if not group_name or not url:
continue
temp_file = self.merger.temp_dir / f"{group_name}.yaml"
if temp_file.exists():
continue
content = self.merger.fetch_subscription(url)
if content:
self.merger.save_temp_yaml(group_name, content)
# 处理临时文件并合并
merged_data = self.merger.process_temp_files()
# 收集所有代理节点并转换为SS URL格式
ss_urls = []
for proxy in merged_data['proxies']:
if not self.merger.should_filter_proxy(proxy.get('name', '')):
ss_url = self.clash_to_ss_url(proxy)
if ss_url:
ss_urls.append(ss_url)
if not ss_urls:
raise Exception("没有找到有效的代理节点")
# 将SS URL列表合并为一个字符串每个URL一行
ss_content = '\n'.join(ss_urls)
# 编码为Base64
encoded_content = base64.b64encode(ss_content.encode('utf-8')).decode('utf-8')
return encoded_content
except Exception as e:
raise Exception(f"生成SS订阅失败: {e}")
def generate_subscription(self) -> str:
"""生成订阅内容并返回Base64编码 - 保持兼容性"""
return self.generate_ss_subscription()
# 创建服务实例
service = SubscriptionService()
@app.route('/sub/<token>', methods=['GET'])
def get_subscription(token):
"""订阅接口"""
try:
# 验证token
if not service.validate_token(token):
return jsonify({
'error': 'Invalid token',
'message': 'Token验证失败请检查token是否正确'
}), 401
# 生成订阅内容
subscription_content = service.generate_subscription()
# 返回Base64编码的订阅内容
return Response(
subscription_content,
mimetype='text/plain',
headers={
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'no-cache',
'Subscription-Userinfo': f'upload=0; download=0; total=0; expire=0'
}
)
except Exception as e:
return jsonify({
'error': 'Subscription generation failed',
'message': str(e)
}), 500
@app.route('/health', methods=['GET'])
def health_check():
"""健康检查接口"""
return jsonify({
'status': 'ok',
'service': 'Clash Subscription Service',
'version': '1.0.0'
})
@app.errorhandler(404)
def not_found(error):
"""404错误处理"""
return jsonify({
'error': 'Not found',
'message': '请求的接口不存在,正确格式: /sub/[token]'
}), 404
if __name__ == '__main__':
print("启动Clash订阅服务...")
print("接口地址: http://localhost:4090/sub/[token]")
print("健康检查: http://localhost:4090/health")
app.run(host='0.0.0.0', port=4090, debug=False)