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