Files
2026-07-17 12:57:18 +03:00

183 lines
5.9 KiB
Python

import os
import re
import shlex
import shutil
import socket
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
from typing import Any
_HOST_RE = re.compile(r"^[a-zA-Z0-9.\-:_]+$")
def clean_target_host(raw: Any) -> str:
value = str(raw or "").strip()
if not value:
return ""
if value.startswith("http://") or value.startswith("https://"):
parsed = urllib.parse.urlparse(value)
return (parsed.netloc or "").strip().strip("/")
return value.strip().strip("/")
def validate_target_host(raw: Any) -> str:
host = clean_target_host(raw)
if not host:
raise ValueError("IP/хост не указан")
if len(host) > 255 or not _HOST_RE.fullmatch(host):
raise ValueError("Недопустимый формат IP/хоста")
return host
def normalize_extra_target(item: Any) -> dict:
if not isinstance(item, dict):
raise ValueError("Элемент списка дополнительных точек должен быть объектом")
role = str(item.get("role") or "").strip()[:64]
host = validate_target_host(item.get("host"))
title = str(item.get("title") or role or host).strip()[:100] or host
return {"role": role, "host": host, "title": title}
def normalize_extra_targets(raw_items: Any) -> list[dict]:
if raw_items is None:
return []
if not isinstance(raw_items, list):
raise ValueError("Список дополнительных точек должен быть массивом")
out: list[dict] = []
for item in raw_items:
out.append(normalize_extra_target(item))
if len(out) >= 40:
break
return out
def _tool_exists(name: str) -> bool:
return bool(shutil.which(name))
def _run_command(
args: list[str],
timeout_sec: int,
max_output_lines: int | None = 20,
) -> dict:
try:
proc = subprocess.run(
args,
capture_output=True,
text=True,
timeout=max(1, int(timeout_sec)),
check=False,
)
output = (proc.stdout or "") + ("\n" + proc.stderr if proc.stderr else "")
lines = [x for x in output.splitlines() if x.strip()]
if max_output_lines is not None:
lines = lines[: max(1, int(max_output_lines))]
return {
"ok": proc.returncode == 0,
"exit_code": int(proc.returncode),
"command": " ".join(shlex.quote(x) for x in args),
"lines": lines,
}
except subprocess.TimeoutExpired:
return {
"ok": False,
"exit_code": None,
"command": " ".join(shlex.quote(x) for x in args),
"error": "timeout",
"lines": [],
}
except FileNotFoundError:
return {
"ok": False,
"exit_code": None,
"command": " ".join(shlex.quote(x) for x in args),
"error": "not_available",
"lines": [],
}
except Exception as exc:
return {
"ok": False,
"exit_code": None,
"command": " ".join(shlex.quote(x) for x in args),
"error": str(exc),
"lines": [],
}
def ping_host(host: str, timeout_sec: int = 2) -> dict:
safe_host = validate_target_host(host)
if sys.platform.startswith("darwin"):
args = ["ping", "-n", "-c", "1", "-W", str(max(1000, timeout_sec * 1000)), safe_host]
else:
args = ["ping", "-n", "-c", "1", "-W", str(max(1, timeout_sec)), safe_host]
result = _run_command(args, timeout_sec=max(2, timeout_sec + 1))
result["host"] = safe_host
return result
def trace_host(
host: str,
timeout_sec: int = 8,
prefer_mtr: bool = False,
max_output_lines: int | None = 80,
) -> dict:
safe_host = validate_target_host(host)
tool = "traceroute"
args: list[str] | None = None
if prefer_mtr and _tool_exists("mtr"):
tool = "mtr"
args = ["mtr", "--report", "--report-cycles", "3", "--no-dns", safe_host]
elif _tool_exists("traceroute"):
args = ["traceroute", "-n", "-w", "1", "-q", "1", "-m", "10", safe_host]
elif _tool_exists("tracepath"):
tool = "tracepath"
args = ["tracepath", "-n", safe_host]
if not args:
return {
"ok": False,
"host": safe_host,
"tool": None,
"error": "not_available",
"lines": [],
}
result = _run_command(
args,
timeout_sec=max(3, timeout_sec),
max_output_lines=max_output_lines,
)
result["host"] = safe_host
result["tool"] = tool
return result
def tcp_probe(host: str, port: int, timeout_sec: int = 2) -> dict:
safe_host = validate_target_host(host)
safe_port = int(port)
if safe_port < 1 or safe_port > 65535:
raise ValueError("Некорректный TCP-порт")
try:
with socket.create_connection((safe_host, safe_port), timeout=max(1.0, float(timeout_sec))):
return {"ok": True, "host": safe_host, "port": safe_port}
except Exception as exc:
return {"ok": False, "host": safe_host, "port": safe_port, "error": str(exc)}
def http_probe(url: str, timeout_sec: int = 3) -> dict:
value = str(url or "").strip()
if not value.startswith("http://") and not value.startswith("https://"):
raise ValueError("URL должен начинаться с http:// или https://")
req = urllib.request.Request(value, method="GET")
try:
with urllib.request.urlopen(req, timeout=max(1.0, float(timeout_sec))) as resp:
status = int(resp.status)
return {"ok": 200 <= status < 400, "url": value, "status_code": status}
except urllib.error.HTTPError as err:
return {"ok": False, "url": value, "status_code": int(err.code), "error": str(err)}
except Exception as exc:
return {"ok": False, "url": value, "status_code": None, "error": str(exc)}