feat: добавляет визуализацию цепочки прокси, настройки подключения и интерфейс для конфигурации резервного прокси.
This commit is contained in:
254
web/server.py
254
web/server.py
@@ -27,6 +27,14 @@ DATA_DIR = BASE_DIR / "data"
|
||||
CONFIG_FILE = DATA_DIR / "client.json"
|
||||
HWID_FILE = DATA_DIR / "hwid"
|
||||
SUBSCRIPTION_FILE = DATA_DIR / "subscription.json"
|
||||
FALLBACK_FILE = DATA_DIR / "fallback.json"
|
||||
|
||||
# Default fallback proxy settings
|
||||
DEFAULT_FALLBACK = {
|
||||
"enabled": False,
|
||||
"host": "192.168.50.111",
|
||||
"port": 8080
|
||||
}
|
||||
|
||||
|
||||
def get_hwid() -> str:
|
||||
@@ -74,6 +82,27 @@ def load_subscription() -> dict:
|
||||
return None
|
||||
|
||||
|
||||
def save_fallback_config(enabled: bool, host: str, port: int):
|
||||
"""Save fallback proxy configuration to file"""
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
data = {
|
||||
"enabled": enabled,
|
||||
"host": host,
|
||||
"port": port
|
||||
}
|
||||
FALLBACK_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
def load_fallback_config() -> dict:
|
||||
"""Load fallback proxy configuration from file"""
|
||||
if FALLBACK_FILE.exists():
|
||||
try:
|
||||
return json.loads(FALLBACK_FILE.read_text())
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return DEFAULT_FALLBACK.copy()
|
||||
|
||||
|
||||
def measure_tcp_latency(host: str, port: int, timeout: float = 2.0) -> int:
|
||||
"""Measure TCP latency to a host:port in milliseconds"""
|
||||
start_time = time.time()
|
||||
@@ -339,6 +368,10 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
|
||||
self.test_connection()
|
||||
elif self.path.startswith("/static/"):
|
||||
self.serve_static()
|
||||
elif self.path == "/fallback-config":
|
||||
self.get_fallback_config()
|
||||
elif self.path == "/active-proxy":
|
||||
self.get_active_proxy()
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
@@ -352,6 +385,8 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
|
||||
self.apply_subscription()
|
||||
elif self.path == "/ping-target":
|
||||
self.ping_target()
|
||||
elif self.path == "/fallback-config":
|
||||
self.save_fallback_config_endpoint()
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
@@ -375,6 +410,175 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def get_fallback_config(self):
|
||||
"""Get fallback proxy configuration"""
|
||||
fallback = load_fallback_config()
|
||||
self.send_json({
|
||||
"enabled": fallback.get("enabled", False),
|
||||
"host": fallback.get("host", "192.168.50.111"),
|
||||
"port": fallback.get("port", 8080)
|
||||
})
|
||||
|
||||
def get_active_proxy(self):
|
||||
"""Get information about current active proxy chain"""
|
||||
result = {
|
||||
"configured": False,
|
||||
"fallbackEnabled": False,
|
||||
"fallbackHost": None,
|
||||
"vpnTag": None,
|
||||
"vpnServer": None,
|
||||
"activeOutbound": None
|
||||
}
|
||||
|
||||
if not CONFIG_FILE.exists():
|
||||
self.send_json(result)
|
||||
return
|
||||
|
||||
try:
|
||||
config = json.loads(CONFIG_FILE.read_text())
|
||||
outbounds = config.get("outbounds", [])
|
||||
route_final = config.get("route", {}).get("final")
|
||||
|
||||
result["configured"] = True
|
||||
|
||||
for outbound in outbounds:
|
||||
out_type = outbound.get("type")
|
||||
|
||||
if out_type == "urltest":
|
||||
result["fallbackEnabled"] = True
|
||||
elif out_type == "http" and outbound.get("tag") == "fallback-proxy":
|
||||
result["fallbackHost"] = f"{outbound.get('server')}:{outbound.get('server_port')}"
|
||||
elif out_type in ["vless", "vmess", "trojan", "shadowsocks", "hysteria2"]:
|
||||
result["vpnTag"] = outbound.get("tag")
|
||||
result["vpnServer"] = outbound.get("server")
|
||||
|
||||
# Determine which is actually active
|
||||
# For now, we show the configured route
|
||||
result["activeOutbound"] = route_final
|
||||
|
||||
# Check fallback proxy reachability (quick TCP check)
|
||||
if result["fallbackEnabled"] and result["fallbackHost"]:
|
||||
try:
|
||||
host, port = result["fallbackHost"].split(":")
|
||||
latency = measure_tcp_latency(host, int(port), timeout=1.0)
|
||||
result["fallbackReachable"] = latency > 0
|
||||
result["fallbackLatency"] = latency if latency > 0 else None
|
||||
except Exception:
|
||||
result["fallbackReachable"] = False
|
||||
result["fallbackLatency"] = None
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
self.send_json(result)
|
||||
|
||||
def save_fallback_config_endpoint(self):
|
||||
"""Save fallback proxy configuration and regenerate config"""
|
||||
try:
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(content_length).decode("utf-8")
|
||||
data = json.loads(body)
|
||||
|
||||
enabled = data.get("enabled", False)
|
||||
host = data.get("host", "").strip()
|
||||
port = int(data.get("port", 8080))
|
||||
|
||||
if enabled and not host:
|
||||
self.send_json({"success": False, "error": "Host is required"}, 400)
|
||||
return
|
||||
|
||||
save_fallback_config(enabled, host, port)
|
||||
|
||||
# Regenerate current config if it exists
|
||||
regenerated = self.regenerate_current_config()
|
||||
|
||||
self.send_json({
|
||||
"success": True,
|
||||
"message": "Fallback config saved",
|
||||
"regenerated": regenerated
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
self.send_json({"success": False, "error": "Invalid JSON"}, 400)
|
||||
except Exception as e:
|
||||
self.send_json({"success": False, "error": str(e)}, 500)
|
||||
|
||||
def regenerate_current_config(self) -> bool:
|
||||
"""Regenerate current config with updated fallback settings"""
|
||||
if not CONFIG_FILE.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
config = json.loads(CONFIG_FILE.read_text())
|
||||
outbounds = config.get("outbounds", [])
|
||||
|
||||
# Find the VPN outbound (vless, vmess, etc.)
|
||||
vpn_outbound = None
|
||||
utility_outbounds = []
|
||||
|
||||
for outbound in outbounds:
|
||||
if outbound.get("type") in ["vless", "vmess", "trojan", "shadowsocks", "hysteria2"]:
|
||||
vpn_outbound = outbound
|
||||
elif outbound.get("type") in ["direct", "block", "dns"]:
|
||||
utility_outbounds.append(outbound)
|
||||
|
||||
if not vpn_outbound:
|
||||
return False
|
||||
|
||||
selected_tag = vpn_outbound.get("tag")
|
||||
|
||||
# Load fallback config
|
||||
fallback = load_fallback_config()
|
||||
fallback_enabled = fallback.get("enabled", False)
|
||||
fallback_host = fallback.get("host", "")
|
||||
fallback_port = fallback.get("port", 8080)
|
||||
|
||||
# Build new outbounds
|
||||
final_outbounds = []
|
||||
final_tag = selected_tag
|
||||
|
||||
if fallback_enabled and fallback_host:
|
||||
urltest_outbound = {
|
||||
"type": "urltest",
|
||||
"tag": "auto-select",
|
||||
"outbounds": ["fallback-proxy", selected_tag],
|
||||
"url": "http://www.gstatic.com/generate_204",
|
||||
"interval": "30s",
|
||||
"tolerance": 9999
|
||||
}
|
||||
|
||||
fallback_outbound = {
|
||||
"type": "http",
|
||||
"tag": "fallback-proxy",
|
||||
"server": fallback_host,
|
||||
"server_port": fallback_port
|
||||
}
|
||||
|
||||
final_outbounds.append(urltest_outbound)
|
||||
final_outbounds.append(fallback_outbound)
|
||||
final_tag = "auto-select"
|
||||
|
||||
final_outbounds.append(vpn_outbound)
|
||||
final_outbounds.extend(utility_outbounds)
|
||||
|
||||
config["outbounds"] = final_outbounds
|
||||
config["route"]["final"] = final_tag
|
||||
|
||||
# Write config
|
||||
CONFIG_FILE.write_text(json.dumps(config, indent=2, ensure_ascii=False))
|
||||
|
||||
# Reload sing-box
|
||||
try:
|
||||
urllib.request.urlopen("http://127.0.0.1:9090/reload", timeout=3)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[WebUI] Failed to regenerate config: {e}")
|
||||
return False
|
||||
|
||||
def get_status(self):
|
||||
"""Get current proxy status"""
|
||||
config_exists = CONFIG_FILE.exists()
|
||||
@@ -395,7 +599,8 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
|
||||
self.send_json({
|
||||
"active": config_exists,
|
||||
"tag": current_tag,
|
||||
"server": current_server
|
||||
"server": current_server,
|
||||
"proxyPort": PROXY_PORT
|
||||
})
|
||||
|
||||
def get_subscription(self):
|
||||
@@ -672,12 +877,49 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
|
||||
self.send_json({"success": False, "error": f"Сервер '{selected_tag}' не найден"}, 400)
|
||||
return
|
||||
|
||||
# Add selected server as main outbound
|
||||
new_outbounds.insert(0, selected_outbound)
|
||||
# Load fallback configuration
|
||||
fallback = load_fallback_config()
|
||||
fallback_enabled = fallback.get("enabled", False)
|
||||
fallback_host = fallback.get("host", "")
|
||||
fallback_port = fallback.get("port", 8080)
|
||||
|
||||
# Update route - remove incompatible fields and set only final
|
||||
# Build outbounds list
|
||||
final_outbounds = []
|
||||
final_tag = selected_tag
|
||||
|
||||
if fallback_enabled and fallback_host:
|
||||
# Add URLTest for automatic fallback selection
|
||||
# High tolerance (9999ms) ensures first working proxy is preferred
|
||||
urltest_outbound = {
|
||||
"type": "urltest",
|
||||
"tag": "auto-select",
|
||||
"outbounds": ["fallback-proxy", selected_tag],
|
||||
"url": "http://www.gstatic.com/generate_204",
|
||||
"interval": "30s",
|
||||
"tolerance": 9999 # Use first working proxy, not fastest
|
||||
}
|
||||
|
||||
# Add HTTP fallback proxy
|
||||
fallback_outbound = {
|
||||
"type": "http",
|
||||
"tag": "fallback-proxy",
|
||||
"server": fallback_host,
|
||||
"server_port": fallback_port
|
||||
}
|
||||
|
||||
final_outbounds.append(urltest_outbound)
|
||||
final_outbounds.append(fallback_outbound)
|
||||
final_tag = "auto-select"
|
||||
|
||||
# Add selected VPN server
|
||||
final_outbounds.append(selected_outbound)
|
||||
|
||||
# Add utility outbounds (direct, block, dns)
|
||||
final_outbounds.extend(new_outbounds)
|
||||
|
||||
# Update route
|
||||
routes = {
|
||||
"final": selected_tag,
|
||||
"final": final_tag,
|
||||
"auto_detect_interface": True
|
||||
}
|
||||
|
||||
@@ -703,7 +945,7 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
|
||||
}
|
||||
]
|
||||
|
||||
config["outbounds"] = new_outbounds
|
||||
config["outbounds"] = final_outbounds
|
||||
config["route"] = routes
|
||||
|
||||
# Ensure data directory exists
|
||||
|
||||
Reference in New Issue
Block a user