xx
This commit is contained in:
194
app.py
Normal file
194
app.py
Normal 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)
|
||||
Reference in New Issue
Block a user