194 lines
6.9 KiB
Python
194 lines
6.9 KiB
Python
#!/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) |