100 lines
3.0 KiB
Python
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),
|
|
)
|