's'
This commit is contained in:
310
web2_network.py
310
web2_network.py
@@ -6,11 +6,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import socket
|
||||
import platform
|
||||
import subprocess
|
||||
import time
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
import httpx
|
||||
import psutil
|
||||
|
||||
from web2_host_caps import machine_summary_payload
|
||||
from web2_ip_quality import run_ip_quality
|
||||
|
||||
# ---------- 目标 ----------
|
||||
GOOGLE_HOST = "www.google.com"
|
||||
@@ -361,3 +370,304 @@ async def run_network_dashboard() -> dict[str, Any]:
|
||||
"logic_version": NETWORK_LOGIC_VERSION,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _cli_available(command: str) -> bool:
|
||||
return shutil.which(command) is not None
|
||||
|
||||
|
||||
def _run_cli(command: list[str], timeout: float = 10.0) -> dict[str, Any]:
|
||||
kwargs: dict[str, Any] = {
|
||||
"capture_output": True,
|
||||
"text": True,
|
||||
"timeout": timeout,
|
||||
}
|
||||
if hasattr(subprocess, "CREATE_NO_WINDOW"):
|
||||
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
||||
try:
|
||||
r = subprocess.run(command, **kwargs)
|
||||
return {
|
||||
"ok": r.returncode == 0,
|
||||
"code": r.returncode,
|
||||
"stdout": (r.stdout or "").strip(),
|
||||
"stderr": (r.stderr or "").strip(),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"ok": False, "code": -1, "stdout": "", "stderr": str(e)}
|
||||
|
||||
|
||||
def _safe_json_parse(text: str) -> Optional[dict[str, Any]]:
|
||||
if not text:
|
||||
return None
|
||||
try:
|
||||
data = json.loads(text)
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _bytes_per_sec_to_mbps(value: Any) -> Optional[float]:
|
||||
try:
|
||||
n = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if n <= 0:
|
||||
return None
|
||||
# Ookla speedtest: bandwidth 为 bytes/s;speedtest-cli: download 为 bits/s
|
||||
if n > 50_000_000:
|
||||
return round(n / 1_000_000, 2)
|
||||
return round(n * 8 / 1_000_000, 2)
|
||||
|
||||
|
||||
def _summarize_speedtest(raw: Any) -> dict[str, Any]:
|
||||
if not isinstance(raw, dict):
|
||||
return {}
|
||||
download = raw.get("download")
|
||||
upload = raw.get("upload")
|
||||
ping = raw.get("ping")
|
||||
down_mbps = None
|
||||
up_mbps = None
|
||||
ping_ms = None
|
||||
if isinstance(download, dict):
|
||||
down_mbps = _bytes_per_sec_to_mbps(download.get("bandwidth"))
|
||||
elif download is not None:
|
||||
down_mbps = _bytes_per_sec_to_mbps(download)
|
||||
if isinstance(upload, dict):
|
||||
up_mbps = _bytes_per_sec_to_mbps(upload.get("bandwidth"))
|
||||
elif upload is not None:
|
||||
up_mbps = _bytes_per_sec_to_mbps(upload)
|
||||
if isinstance(ping, dict):
|
||||
try:
|
||||
ping_ms = round(float(ping.get("latency")), 1)
|
||||
except (TypeError, ValueError):
|
||||
ping_ms = None
|
||||
elif ping is not None:
|
||||
try:
|
||||
ping_ms = round(float(ping), 1)
|
||||
except (TypeError, ValueError):
|
||||
ping_ms = None
|
||||
return {
|
||||
"download_mbps": down_mbps,
|
||||
"upload_mbps": up_mbps,
|
||||
"ping_ms": ping_ms,
|
||||
}
|
||||
|
||||
|
||||
def _summarize_mtr(raw: Any) -> dict[str, Any]:
|
||||
if isinstance(raw, dict):
|
||||
hubs = raw.get("report", {}).get("hubs") if isinstance(raw.get("report"), dict) else raw.get("hubs")
|
||||
if not isinstance(hubs, list):
|
||||
hubs = []
|
||||
last_avg = None
|
||||
if hubs:
|
||||
try:
|
||||
last_avg = float(hubs[-1].get("Avg", hubs[-1].get("avg", 0)) or 0)
|
||||
except (TypeError, ValueError):
|
||||
last_avg = None
|
||||
return {"hops": len(hubs), "last_hop_avg_ms": last_avg}
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
lines = [ln for ln in raw.splitlines() if ln.strip() and not ln.startswith("Start:")]
|
||||
return {"hops": max(0, len(lines) - 1), "last_hop_avg_ms": None}
|
||||
return {}
|
||||
|
||||
|
||||
def _summarize_ipinfo(raw: Any) -> dict[str, Any]:
|
||||
if not isinstance(raw, dict):
|
||||
return {}
|
||||
return {
|
||||
"ip": raw.get("ip"),
|
||||
"city": raw.get("city"),
|
||||
"region": raw.get("region"),
|
||||
"country": raw.get("country"),
|
||||
"org": raw.get("org") or raw.get("asn"),
|
||||
}
|
||||
|
||||
|
||||
def _systeminformation_payload() -> dict[str, Any]:
|
||||
vm = psutil.virtual_memory()
|
||||
cpu = psutil.cpu_percent(interval=0.2)
|
||||
machine = machine_summary_payload()
|
||||
disks: list[dict[str, Any]] = []
|
||||
try:
|
||||
for part in psutil.disk_partitions(all=False)[:4]:
|
||||
try:
|
||||
usage = psutil.disk_usage(part.mountpoint)
|
||||
disks.append(
|
||||
{
|
||||
"mount": part.mountpoint,
|
||||
"percent": usage.percent,
|
||||
"free_gb": round(usage.free / (1024**3), 2),
|
||||
}
|
||||
)
|
||||
except (PermissionError, OSError):
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
net_io: dict[str, Any] = {}
|
||||
try:
|
||||
io = psutil.net_io_counters()
|
||||
net_io = {
|
||||
"bytes_sent_mb": round(io.bytes_sent / (1024**2), 1),
|
||||
"bytes_recv_mb": round(io.bytes_recv / (1024**2), 1),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"provider": "sebhildebrandt/systeminformation",
|
||||
"ok": True,
|
||||
"host": platform.node(),
|
||||
"platform": platform.platform(),
|
||||
"machine": machine,
|
||||
"cpu_logical": psutil.cpu_count(logical=True),
|
||||
"cpu_percent": round(float(cpu), 1),
|
||||
"memory_total_gb": round(vm.total / (1024**3), 2),
|
||||
"memory_used_gb": round(vm.used / (1024**3), 2),
|
||||
"memory_percent": round(float(vm.percent), 1),
|
||||
"disks": disks,
|
||||
"net_io": net_io,
|
||||
}
|
||||
|
||||
|
||||
def _speedtest_payload() -> dict[str, Any]:
|
||||
if _cli_available("speedtest"):
|
||||
result = _run_cli(
|
||||
["speedtest", "--accept-license", "--accept-gdpr", "-f", "json"],
|
||||
timeout=18.0,
|
||||
)
|
||||
parsed = _safe_json_parse(result.get("stdout", ""))
|
||||
summary = _summarize_speedtest(parsed)
|
||||
return {
|
||||
"provider": "librespeed/speedtest",
|
||||
"command": "speedtest",
|
||||
"ok": result.get("ok", False),
|
||||
"raw": parsed or result.get("stdout", ""),
|
||||
"summary": summary,
|
||||
"error": None if result.get("ok", False) else result.get("stderr", "") or "speedtest failed",
|
||||
}
|
||||
if _cli_available("speedtest-cli"):
|
||||
result = _run_cli(["speedtest-cli", "--json"], timeout=18.0)
|
||||
parsed = _safe_json_parse(result.get("stdout", ""))
|
||||
summary = _summarize_speedtest(parsed)
|
||||
return {
|
||||
"provider": "librespeed/speedtest",
|
||||
"command": "speedtest-cli",
|
||||
"ok": result.get("ok", False),
|
||||
"raw": parsed or result.get("stdout", ""),
|
||||
"summary": summary,
|
||||
"error": None if result.get("ok", False) else result.get("stderr", "") or "speedtest-cli failed",
|
||||
}
|
||||
return {
|
||||
"provider": "librespeed/speedtest",
|
||||
"command": None,
|
||||
"ok": False,
|
||||
"raw": None,
|
||||
"error": "未检测到 speedtest 或 speedtest-cli",
|
||||
}
|
||||
|
||||
|
||||
def _mtr_payload() -> dict[str, Any]:
|
||||
if not _cli_available("mtr"):
|
||||
return {
|
||||
"provider": "traviscross/mtr",
|
||||
"ok": False,
|
||||
"raw": None,
|
||||
"error": "未检测到 mtr 命令",
|
||||
}
|
||||
result = _run_cli(["mtr", "--report", "--json", "-c", "5", "1.1.1.1"], timeout=18.0)
|
||||
parsed = _safe_json_parse(result.get("stdout", ""))
|
||||
summary = _summarize_mtr(parsed or result.get("stdout", ""))
|
||||
return {
|
||||
"provider": "traviscross/mtr",
|
||||
"ok": result.get("ok", False),
|
||||
"raw": parsed or result.get("stdout", ""),
|
||||
"summary": summary,
|
||||
"error": None if result.get("ok", False) else result.get("stderr", "") or "mtr failed",
|
||||
}
|
||||
|
||||
|
||||
def _ipinfo_payload() -> dict[str, Any]:
|
||||
if not _cli_available("ipinfo"):
|
||||
return {
|
||||
"provider": "ipinfo/cli",
|
||||
"ok": False,
|
||||
"raw": None,
|
||||
"error": "未检测到 ipinfo 命令",
|
||||
}
|
||||
result = _run_cli(["ipinfo", "--json"], timeout=10.0)
|
||||
parsed = _safe_json_parse(result.get("stdout", ""))
|
||||
summary = _summarize_ipinfo(parsed)
|
||||
return {
|
||||
"provider": "ipinfo/cli",
|
||||
"ok": result.get("ok", False),
|
||||
"raw": parsed or result.get("stdout", ""),
|
||||
"summary": summary,
|
||||
"error": None if result.get("ok", False) else result.get("stderr", "") or "ipinfo failed",
|
||||
}
|
||||
|
||||
|
||||
def _ipquality_toolkit_payload(quality: dict[str, Any]) -> dict[str, Any]:
|
||||
script = Path(__file__).resolve().parent / "third_party" / "IPQuality" / "ip.sh"
|
||||
err = quality.get("error")
|
||||
info = quality.get("info") if isinstance(quality.get("info"), dict) else {}
|
||||
summary = quality.get("summary") if isinstance(quality.get("summary"), dict) else {}
|
||||
media = quality.get("media") if isinstance(quality.get("media"), dict) else {}
|
||||
head = quality.get("head") if isinstance(quality.get("head"), dict) else {}
|
||||
return {
|
||||
"provider": "xykt/IPQuality",
|
||||
"ok": not err,
|
||||
"script": str(script),
|
||||
"script_exists": script.is_file(),
|
||||
"endpoint": "/ip_quality",
|
||||
"summary": {
|
||||
"ip": info.get("ip") or head.get("ip"),
|
||||
"headline": summary.get("headline"),
|
||||
"proxy_like": summary.get("proxy_like"),
|
||||
"country": info.get("country"),
|
||||
"organization": info.get("organization"),
|
||||
"youtube": (media.get("Youtube") or {}).get("status_label"),
|
||||
"tiktok": (media.get("TikTok") or {}).get("status_label"),
|
||||
},
|
||||
"error": err,
|
||||
}
|
||||
|
||||
|
||||
async def run_network_toolkit() -> dict[str, Any]:
|
||||
timeout = httpx.Timeout(READ_TIMEOUT, connect=CONNECT_TIMEOUT)
|
||||
async with httpx.AsyncClient(timeout=timeout, verify=True) as client:
|
||||
egress, quality = await asyncio.gather(_probe_default_egress(client), run_ip_quality())
|
||||
country_code = str(egress.get("country_code") or "").upper()
|
||||
speed = _speedtest_payload()
|
||||
mtr = _mtr_payload()
|
||||
ipinfo = _ipinfo_payload()
|
||||
return {
|
||||
"providers": {
|
||||
"systeminformation": _systeminformation_payload(),
|
||||
"librespeed_speedtest": speed,
|
||||
"mtr": mtr,
|
||||
"ipinfo_cli": ipinfo,
|
||||
"ipquality": _ipquality_toolkit_payload(quality),
|
||||
"is_china_user": {
|
||||
"provider": "yArna/isChinaUser",
|
||||
"scope": "server_egress",
|
||||
"ok": bool(country_code == "CN"),
|
||||
"country_code": country_code or None,
|
||||
"reason": "服务端出口 IP 国家码(前端 is-china-user 为浏览器侧信号)",
|
||||
},
|
||||
"webrtc": {
|
||||
"provider": "pion/webrtc",
|
||||
"ok": True,
|
||||
"note": "前端执行 WebRTC candidate 泄漏检测",
|
||||
},
|
||||
"fingerprintjs": {
|
||||
"provider": "fingerprintjs/fingerprintjs",
|
||||
"ok": True,
|
||||
"note": "前端 @fingerprintjs/fingerprintjs 生成 visitorId",
|
||||
},
|
||||
},
|
||||
"meta": {
|
||||
"at": time.time(),
|
||||
"logic_version": 2,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user