@@ -0,0 +1,182 @@
|
||||
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)}
|
||||
|
||||
Reference in New Issue
Block a user