Spaces:
Paused
Paused
Upload 17 files
Browse files- app/main.py +88 -2
- requirements.txt +4 -1
app/main.py
CHANGED
|
@@ -7,11 +7,16 @@ Simple Clash Relay - Flask 应用入口
|
|
| 7 |
|
| 8 |
import os
|
| 9 |
import logging
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
from .clash_manager import ClashManager
|
| 12 |
from .sub_manager import SubscriptionManager
|
| 13 |
from .auth import authenticate
|
| 14 |
-
import requests
|
| 15 |
|
| 16 |
# 配置日志
|
| 17 |
logging.basicConfig(
|
|
@@ -421,6 +426,87 @@ def index():
|
|
| 421 |
</html>
|
| 422 |
"""
|
| 423 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
if __name__ == "__main__":
|
| 425 |
# 如果直接运行此文件,将初始化应用并启动Flask服务器
|
| 426 |
initialize()
|
|
|
|
| 7 |
|
| 8 |
import os
|
| 9 |
import logging
|
| 10 |
+
import time
|
| 11 |
+
import signal
|
| 12 |
+
import sys
|
| 13 |
+
import json
|
| 14 |
+
import requests
|
| 15 |
+
from flask import Flask, request, jsonify, Response, send_from_directory
|
| 16 |
+
from werkzeug.exceptions import HTTPException
|
| 17 |
from .clash_manager import ClashManager
|
| 18 |
from .sub_manager import SubscriptionManager
|
| 19 |
from .auth import authenticate
|
|
|
|
| 20 |
|
| 21 |
# 配置日志
|
| 22 |
logging.basicConfig(
|
|
|
|
| 426 |
</html>
|
| 427 |
"""
|
| 428 |
|
| 429 |
+
# --- Clash API 反向代理 ---
|
| 430 |
+
@app.route('/ui/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'])
|
| 431 |
+
def proxy_clash_api(path):
|
| 432 |
+
"""将 /ui/ 下的请求转发给内部的 Clash API (127.0.0.1:9090)"""
|
| 433 |
+
if not clash_manager:
|
| 434 |
+
return jsonify({"error": "Clash Manager 未初始化"}), 503
|
| 435 |
+
|
| 436 |
+
clash_api_base_url = f"http://127.0.0.1:{clash_manager.api_port}/"
|
| 437 |
+
target_url = clash_api_base_url + path
|
| 438 |
+
|
| 439 |
+
# 准备转发请求头,移除 Host 头,可能需要添加原始 Host 或 Forwarded 头
|
| 440 |
+
headers = {key: value for key, value in request.headers if key.lower() != 'host'}
|
| 441 |
+
# 确保传递原始的 X-Real-IP 或 X-Forwarded-For (如果存在)
|
| 442 |
+
# headers['X-Forwarded-For'] = request.remote_addr
|
| 443 |
+
|
| 444 |
+
# 处理 Websocket 连接 (Clash API 日志/流量推送可能使用)
|
| 445 |
+
if request.headers.get('Upgrade', '').lower() == 'websocket':
|
| 446 |
+
logger.info(f"检测到 WebSocket 请求: {path}")
|
| 447 |
+
# Flask 本身不直接支持代理 WebSocket,需要额外库或特殊处理
|
| 448 |
+
# 这里返回错误,表明此代理不支持 WebSocket
|
| 449 |
+
# 可以考虑使用 flask_sock 或其他库来支持
|
| 450 |
+
logger.warning("当前反向代理不支持 WebSocket 连接")
|
| 451 |
+
return jsonify({"error": "WebSocket proxying not supported"}), 501 # Not Implemented
|
| 452 |
+
|
| 453 |
+
try:
|
| 454 |
+
resp = requests.request(
|
| 455 |
+
method=request.method,
|
| 456 |
+
url=target_url,
|
| 457 |
+
headers=headers,
|
| 458 |
+
data=request.get_data(),
|
| 459 |
+
params=request.args,
|
| 460 |
+
stream=True, # 使用流式传输以处理大数据和流式API (如日志)
|
| 461 |
+
timeout=30 # 设置超时
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
# 准备转发响应头
|
| 465 |
+
response_headers = []
|
| 466 |
+
for name, value in resp.raw.headers.items():
|
| 467 |
+
# 避免转发某些头 (例如 'Transfer-Encoding': 'chunked' 可能导致问题)
|
| 468 |
+
if name.lower() not in ['content-encoding', 'content-length', 'transfer-encoding', 'connection']:
|
| 469 |
+
response_headers.append((name, value))
|
| 470 |
+
|
| 471 |
+
# 创建Flask响应对象
|
| 472 |
+
# 使用 resp.content 而不是迭代器,对于非流式响应更简单可靠
|
| 473 |
+
response = Response(resp.content, status=resp.status_code, headers=response_headers)
|
| 474 |
+
return response
|
| 475 |
+
|
| 476 |
+
except requests.exceptions.ConnectionError:
|
| 477 |
+
logger.error(f"无法连接到内部Clash API: {target_url}")
|
| 478 |
+
return jsonify({"error": "无法连接到内部 Clash API"}), 502 # Bad Gateway
|
| 479 |
+
except requests.exceptions.Timeout:
|
| 480 |
+
logger.error(f"连接到内部Clash API超时: {target_url}")
|
| 481 |
+
return jsonify({"error": "内部 Clash API 请求超时"}), 504 # Gateway Timeout
|
| 482 |
+
except Exception as e:
|
| 483 |
+
logger.error(f"代理到 Clash API 时出错: {str(e)}")
|
| 484 |
+
return jsonify({"error": f"代理请求失败: {str(e)}"}), 500
|
| 485 |
+
|
| 486 |
+
# --- 错误处理 ---
|
| 487 |
+
@app.errorhandler(HTTPException)
|
| 488 |
+
def handle_exception(e):
|
| 489 |
+
"""返回JSON格式的HTTP错误"""
|
| 490 |
+
response = e.get_response()
|
| 491 |
+
response.data = json.dumps({
|
| 492 |
+
"code": e.code,
|
| 493 |
+
"name": e.name,
|
| 494 |
+
"description": e.description,
|
| 495 |
+
})
|
| 496 |
+
response.content_type = "application/json"
|
| 497 |
+
return response
|
| 498 |
+
|
| 499 |
+
@app.errorhandler(Exception)
|
| 500 |
+
def handle_generic_exception(e):
|
| 501 |
+
"""处理未捕获的异常"""
|
| 502 |
+
logger.error(f"未捕获的异常: {str(e)}", exc_info=True)
|
| 503 |
+
# 对于非HTTP异常,返回500 Internal Server Error
|
| 504 |
+
return jsonify({
|
| 505 |
+
"code": 500,
|
| 506 |
+
"name": "Internal Server Error",
|
| 507 |
+
"description": f"发生内部错误: {str(e)}"
|
| 508 |
+
}), 500
|
| 509 |
+
|
| 510 |
if __name__ == "__main__":
|
| 511 |
# 如果直接运行此文件,将初始化应用并启动Flask服务器
|
| 512 |
initialize()
|
requirements.txt
CHANGED
|
@@ -4,4 +4,7 @@ requests==2.28.1
|
|
| 4 |
Werkzeug==2.0.1
|
| 5 |
PyYAML==6.0.1
|
| 6 |
# pyyaml==6.0 # 已通过 apk 安装
|
| 7 |
-
# 添加与 Flask==2.0.1 兼容的版本
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
Werkzeug==2.0.1
|
| 5 |
PyYAML==6.0.1
|
| 6 |
# pyyaml==6.0 # 已通过 apk 安装
|
| 7 |
+
# 添加与 Flask==2.0.1 兼容的版本
|
| 8 |
+
|
| 9 |
+
# 确保 requests 库被包含,因为反向代理需要它
|
| 10 |
+
# (requests 已在上面,无需重复添加)
|