Files
ClashSubscribeMerge/app.py
yangfan 63b7800b59 xx
2025-08-14 11:33:08 +08:00

194 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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)