feat: Реализовано включение/выключение прокси через веб-интерфейс с сохранением состояния и обновлением конфигурации, а также добавлен соответствующий UI.
This commit is contained in:
962
web/index.html
962
web/index.html
File diff suppressed because it is too large
Load Diff
141
web/server.py
141
web/server.py
@@ -14,6 +14,8 @@ import urllib.error
|
|||||||
import uuid
|
import uuid
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
from urllib.parse import parse_qs, unquote
|
from urllib.parse import parse_qs, unquote
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -27,7 +29,10 @@ DATA_DIR = BASE_DIR / "data"
|
|||||||
CONFIG_FILE = DATA_DIR / "client.json"
|
CONFIG_FILE = DATA_DIR / "client.json"
|
||||||
HWID_FILE = DATA_DIR / "hwid"
|
HWID_FILE = DATA_DIR / "hwid"
|
||||||
SUBSCRIPTION_FILE = DATA_DIR / "subscription.json"
|
SUBSCRIPTION_FILE = DATA_DIR / "subscription.json"
|
||||||
|
SUBSCRIPTION_FILE = DATA_DIR / "subscription.json"
|
||||||
FALLBACK_FILE = DATA_DIR / "fallback.json"
|
FALLBACK_FILE = DATA_DIR / "fallback.json"
|
||||||
|
PROXY_ENABLED_FILE = DATA_DIR / "proxy_enabled.json"
|
||||||
|
START_TIME_FILE = DATA_DIR / "start_time.json"
|
||||||
|
|
||||||
# Default fallback proxy settings
|
# Default fallback proxy settings
|
||||||
DEFAULT_FALLBACK = {
|
DEFAULT_FALLBACK = {
|
||||||
@@ -103,6 +108,40 @@ def load_fallback_config() -> dict:
|
|||||||
return DEFAULT_FALLBACK.copy()
|
return DEFAULT_FALLBACK.copy()
|
||||||
|
|
||||||
|
|
||||||
|
def save_proxy_enabled(enabled: bool):
|
||||||
|
"""Save proxy enabled state to file"""
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
PROXY_ENABLED_FILE.write_text(json.dumps({"enabled": enabled}))
|
||||||
|
|
||||||
|
|
||||||
|
def load_proxy_enabled() -> bool:
|
||||||
|
"""Load proxy enabled state from file"""
|
||||||
|
if PROXY_ENABLED_FILE.exists():
|
||||||
|
try:
|
||||||
|
data = json.loads(PROXY_ENABLED_FILE.read_text())
|
||||||
|
return data.get("enabled", True)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
return True # Default: proxy enabled
|
||||||
|
|
||||||
|
|
||||||
|
def save_start_time(start_time: float):
|
||||||
|
"""Save VPN start time to file"""
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
START_TIME_FILE.write_text(json.dumps({"startTime": start_time}))
|
||||||
|
|
||||||
|
|
||||||
|
def load_start_time() -> float:
|
||||||
|
"""Load VPN start time from file"""
|
||||||
|
if START_TIME_FILE.exists():
|
||||||
|
try:
|
||||||
|
data = json.loads(START_TIME_FILE.read_text())
|
||||||
|
return data.get("startTime", 0.0)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
def measure_tcp_latency(host: str, port: int, timeout: float = 2.0) -> int:
|
def measure_tcp_latency(host: str, port: int, timeout: float = 2.0) -> int:
|
||||||
"""Measure TCP latency to a host:port in milliseconds"""
|
"""Measure TCP latency to a host:port in milliseconds"""
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
@@ -372,6 +411,8 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
self.get_fallback_config()
|
self.get_fallback_config()
|
||||||
elif self.path == "/active-proxy":
|
elif self.path == "/active-proxy":
|
||||||
self.get_active_proxy()
|
self.get_active_proxy()
|
||||||
|
elif self.path == "/proxy-enabled":
|
||||||
|
self.get_proxy_enabled()
|
||||||
else:
|
else:
|
||||||
self.send_error(404)
|
self.send_error(404)
|
||||||
|
|
||||||
@@ -387,6 +428,8 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
self.ping_target()
|
self.ping_target()
|
||||||
elif self.path == "/fallback-config":
|
elif self.path == "/fallback-config":
|
||||||
self.save_fallback_config_endpoint()
|
self.save_fallback_config_endpoint()
|
||||||
|
elif self.path == "/proxy-enabled":
|
||||||
|
self.set_proxy_enabled()
|
||||||
else:
|
else:
|
||||||
self.send_error(404)
|
self.send_error(404)
|
||||||
|
|
||||||
@@ -584,6 +627,7 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
config_exists = CONFIG_FILE.exists()
|
config_exists = CONFIG_FILE.exists()
|
||||||
current_tag = None
|
current_tag = None
|
||||||
current_server = None
|
current_server = None
|
||||||
|
proxy_enabled = load_proxy_enabled()
|
||||||
|
|
||||||
if config_exists:
|
if config_exists:
|
||||||
try:
|
try:
|
||||||
@@ -597,12 +641,102 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
self.send_json({
|
self.send_json({
|
||||||
"active": config_exists,
|
"active": config_exists and proxy_enabled,
|
||||||
"tag": current_tag,
|
"tag": current_tag,
|
||||||
"server": current_server,
|
"server": current_server,
|
||||||
"proxyPort": PROXY_PORT
|
"proxyPort": PROXY_PORT,
|
||||||
|
"proxyEnabled": proxy_enabled,
|
||||||
|
"startTime": load_start_time() if config_exists and proxy_enabled else 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def get_proxy_enabled(self):
|
||||||
|
"""Get proxy enabled state"""
|
||||||
|
enabled = load_proxy_enabled()
|
||||||
|
self.send_json({"enabled": enabled})
|
||||||
|
|
||||||
|
def set_proxy_enabled(self):
|
||||||
|
"""Set proxy enabled state 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", True)
|
||||||
|
save_proxy_enabled(enabled)
|
||||||
|
|
||||||
|
# Regenerate config based on state
|
||||||
|
if enabled:
|
||||||
|
# Restore normal VPN config
|
||||||
|
regenerated = self.regenerate_current_config()
|
||||||
|
if regenerated:
|
||||||
|
# Only update start time if actually enabling VPN
|
||||||
|
save_start_time(datetime.now(timezone.utc).timestamp())
|
||||||
|
else:
|
||||||
|
# Generate direct config (bypass proxy)
|
||||||
|
regenerated = self.generate_direct_config()
|
||||||
|
save_start_time(0)
|
||||||
|
|
||||||
|
self.send_json({
|
||||||
|
"success": True,
|
||||||
|
"enabled": enabled,
|
||||||
|
"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 generate_direct_config(self) -> bool:
|
||||||
|
"""Generate a direct connection config (bypass all proxies)"""
|
||||||
|
try:
|
||||||
|
config = {
|
||||||
|
"dns": {
|
||||||
|
"independent_cache": True
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"level": "debug",
|
||||||
|
"disabled": True,
|
||||||
|
"timestamp": True
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"final": "direct",
|
||||||
|
"auto_detect_interface": True
|
||||||
|
},
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"tag": "mixed-in",
|
||||||
|
"type": "mixed",
|
||||||
|
"sniff": True,
|
||||||
|
"users": [],
|
||||||
|
"listen": "0.0.0.0",
|
||||||
|
"listen_port": PROXY_PORT,
|
||||||
|
"set_system_proxy": False
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"tag": "direct",
|
||||||
|
"type": "direct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
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 generate direct config: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def get_subscription(self):
|
def get_subscription(self):
|
||||||
"""Get saved subscription info"""
|
"""Get saved subscription info"""
|
||||||
sub = load_subscription()
|
sub = load_subscription()
|
||||||
@@ -690,6 +824,9 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
"success": True,
|
"success": True,
|
||||||
"message": f"Конфигурация '{vless_params['tag']}' успешно применена!"
|
"message": f"Конфигурация '{vless_params['tag']}' успешно применена!"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Save new start time as connection is reset
|
||||||
|
save_start_time(datetime.now(timezone.utc).timestamp())
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
self.send_json({"success": False, "error": "Неверный JSON"}, 400)
|
self.send_json({"success": False, "error": "Неверный JSON"}, 400)
|
||||||
|
|||||||
Reference in New Issue
Block a user