Files
Nexus/server/utils/site_host.py
2026-07-08 22:31:31 +08:00

100 lines
3.0 KiB
Python

"""Resolve display site hostname from server fields (mirrors frontend serverSiteUrl.ts)."""
from __future__ import annotations
import ipaddress
import re
from typing import Any
from urllib.parse import urlparse
_IP_RE = re.compile(r"^\d{1,3}(\.\d{1,3}){3}$")
_CJK_RE = re.compile(r"[\u4e00-\u9fff]")
_ASCII_HOST_RE = re.compile(
r"^[a-z0-9](?:[a-z0-9-]*\.)+[a-z]{2,63}$",
re.IGNORECASE,
)
_SKIP_TARGET_SEGMENTS = frozenset({"shop", "crmeb", "public", "me", "crmweb"})
def is_ip_host(value: str | None) -> bool:
"""True when *value* is a literal IPv4/IPv6 address (SSH domain field)."""
text = (value or "").strip()
if not text:
return False
try:
ipaddress.ip_address(text.split("/")[0])
return True
except ValueError:
return False
def _host_from_string(raw: str) -> str | None:
trimmed = raw.strip()
if not trimmed:
return None
# 备注(中文 tonex 名等)勿当站点域名
if _CJK_RE.search(trimmed):
return None
if trimmed.startswith(("http://", "https://")):
try:
host = urlparse(trimmed).hostname
except Exception:
return None
if host and not is_ip_host(host) and _ASCII_HOST_RE.match(host):
return host.lower()
return None
host = trimmed.replace("http://", "").replace("https://", "")
host = host.split("/")[0].split(":")[0].strip()
if not host or is_ip_host(host) or not _ASCII_HOST_RE.match(host):
return None
return host.lower()
def parse_site_host_from_target_path(target_path: str | None) -> str | None:
p = (target_path or "").strip().rstrip("/")
if not p:
return None
parts = [seg for seg in p.split("/") if seg]
root_idx = parts.index("wwwroot") if "wwwroot" in parts else -1
candidates: list[str] = []
if root_idx >= 0:
candidates.extend(parts[root_idx + 1 :])
elif parts:
candidates.append(parts[-1])
for seg in reversed(candidates):
if seg.lower() in _SKIP_TARGET_SEGMENTS:
continue
if _IP_RE.match(seg):
continue
if "." in seg:
return seg.lower()
return None
def resolve_server_site_host(
*,
description: str | None = None,
target_path: str | None = None,
extra_attrs: dict[str, Any] | None = None,
) -> str | None:
extra = extra_attrs if isinstance(extra_attrs, dict) else {}
for key in ("site_url", "site_domain", "website"):
val = extra.get(key)
if isinstance(val, str):
host = _host_from_string(val)
if host:
return host
if description and description.strip():
host = _host_from_string(description)
if host:
return host
return parse_site_host_from_target_path(target_path)
def resolve_server_site_host_from_server(server: Any) -> str | None:
return resolve_server_site_host(
description=getattr(server, "description", None),
target_path=getattr(server, "target_path", None),
extra_attrs=getattr(server, "extra_attrs", None),
)