platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
178
selective-vpn-gui/transport_protocol_summary.py
Normal file
178
selective-vpn-gui/transport_protocol_summary.py
Normal file
@@ -0,0 +1,178 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TransportProtocolInfo:
|
||||
protocol: str = ""
|
||||
transport: str = ""
|
||||
security: str = ""
|
||||
|
||||
def summary(self) -> str:
|
||||
proto = self.protocol if self.protocol else "n/a"
|
||||
transport = self.transport if self.transport else "n/a"
|
||||
security = self.security if self.security else "n/a"
|
||||
return f"{proto} / {transport} / {security}"
|
||||
|
||||
|
||||
def transport_protocol_info(client: Any) -> TransportProtocolInfo:
|
||||
cfg = _as_dict(getattr(client, "config", {}) or {})
|
||||
protocol = _first_non_empty(
|
||||
cfg.get("protocol"),
|
||||
cfg.get("profile_protocol"),
|
||||
cfg.get("outbound"),
|
||||
cfg.get("type"),
|
||||
).lower()
|
||||
transport = _first_non_empty(
|
||||
cfg.get("transport"),
|
||||
cfg.get("network"),
|
||||
cfg.get("stream"),
|
||||
).lower()
|
||||
security = _normalize_security(
|
||||
_first_non_empty(
|
||||
cfg.get("security"),
|
||||
cfg.get("tls_security"),
|
||||
cfg.get("security_mode"),
|
||||
)
|
||||
)
|
||||
|
||||
if not protocol or not transport or not security:
|
||||
raw_cfg = _load_raw_config_from_client_config(cfg)
|
||||
if raw_cfg:
|
||||
p2, t2, s2 = _infer_from_raw_config(raw_cfg)
|
||||
if not protocol and p2:
|
||||
protocol = p2
|
||||
if not transport and t2:
|
||||
transport = t2
|
||||
if not security and s2:
|
||||
security = s2
|
||||
|
||||
if protocol and not transport:
|
||||
transport = _default_transport_for_protocol(protocol)
|
||||
if protocol and not security:
|
||||
security = "none"
|
||||
return TransportProtocolInfo(protocol=protocol, transport=transport, security=security)
|
||||
|
||||
|
||||
def transport_protocol_summary(client: Any) -> str:
|
||||
return transport_protocol_info(client).summary()
|
||||
|
||||
|
||||
def _infer_from_raw_config(raw_cfg: dict[str, Any]) -> tuple[str, str, str]:
|
||||
outbounds = raw_cfg.get("outbounds") or []
|
||||
if isinstance(outbounds, list):
|
||||
for row in outbounds:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
out_type = str(row.get("type") or "").strip().lower()
|
||||
if not out_type or out_type in ("direct", "block", "dns"):
|
||||
continue
|
||||
tx = ""
|
||||
transport_obj = row.get("transport")
|
||||
if isinstance(transport_obj, dict):
|
||||
tx = str(transport_obj.get("type") or "").strip().lower()
|
||||
if not tx:
|
||||
tx = str(row.get("network") or "").strip().lower()
|
||||
sec = _extract_security(row)
|
||||
return out_type, tx, sec
|
||||
|
||||
inbounds = raw_cfg.get("inbounds") or []
|
||||
if isinstance(inbounds, list):
|
||||
for row in inbounds:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
in_type = str(row.get("type") or "").strip().lower()
|
||||
if not in_type:
|
||||
continue
|
||||
network = str(row.get("network") or "").strip().lower()
|
||||
sec = _extract_security(row)
|
||||
return in_type, network, sec
|
||||
|
||||
return "", "", ""
|
||||
|
||||
|
||||
def _load_raw_config_from_client_config(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
path = _first_non_empty(
|
||||
cfg.get("config_path"),
|
||||
cfg.get("singbox_config_path"),
|
||||
cfg.get("raw_config_path"),
|
||||
)
|
||||
if not path:
|
||||
return {}
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
parsed = json.load(f)
|
||||
except Exception:
|
||||
return {}
|
||||
if not isinstance(parsed, dict):
|
||||
return {}
|
||||
return parsed
|
||||
|
||||
|
||||
def _as_dict(raw: Any) -> dict[str, Any]:
|
||||
return raw if isinstance(raw, dict) else {}
|
||||
|
||||
|
||||
def _normalize_security(value: str) -> str:
|
||||
sec = str(value or "").strip().lower()
|
||||
if not sec:
|
||||
return ""
|
||||
aliases = {
|
||||
"off": "none",
|
||||
"disabled": "none",
|
||||
"plain": "none",
|
||||
"reality-tls": "reality",
|
||||
"xtls": "tls",
|
||||
}
|
||||
return aliases.get(sec, sec)
|
||||
|
||||
|
||||
def _extract_security(node: dict[str, Any]) -> str:
|
||||
sec = _normalize_security(_first_non_empty(node.get("security"), node.get("tls_security")))
|
||||
if sec:
|
||||
return sec
|
||||
|
||||
tls = _as_dict(node.get("tls"))
|
||||
if not tls:
|
||||
return ""
|
||||
enabled_raw = tls.get("enabled")
|
||||
if enabled_raw is False:
|
||||
return "none"
|
||||
|
||||
reality = _as_dict(tls.get("reality"))
|
||||
if reality:
|
||||
if _truthy(reality.get("enabled")):
|
||||
return "reality"
|
||||
if _first_non_empty(reality.get("public_key"), reality.get("short_id"), reality.get("short_ids")):
|
||||
return "reality"
|
||||
return "tls"
|
||||
|
||||
|
||||
def _truthy(raw: Any) -> bool:
|
||||
if isinstance(raw, bool):
|
||||
return raw
|
||||
if isinstance(raw, int):
|
||||
return raw != 0
|
||||
if isinstance(raw, str):
|
||||
return raw.strip().lower() in ("1", "true", "yes", "on")
|
||||
return False
|
||||
|
||||
|
||||
def _default_transport_for_protocol(protocol: str) -> str:
|
||||
p = str(protocol or "").strip().lower()
|
||||
if p in ("vless", "trojan", "shadowsocks", "socks", "http"):
|
||||
return "tcp"
|
||||
if p in ("wireguard", "hysteria2", "tuic"):
|
||||
return "udp"
|
||||
return ""
|
||||
|
||||
|
||||
def _first_non_empty(*values: Any) -> str:
|
||||
for value in values:
|
||||
s = str(value or "").strip()
|
||||
if s:
|
||||
return s
|
||||
return ""
|
||||
Reference in New Issue
Block a user