wwww
import re
class DomainError(ValueError):
pass
_LABEL_RE_ASCII = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$", re.IGNORECASE)
def _to_idna(label: str) -> str:
"""
Convert a potentially-unicode label to ASCII using IDNA.
Empty labels are returned as-is (used to skip optional parts).
"""
if label is None:
return ""
label = label.strip()
if not label:
return ""
try:
return label.encode("idna").decode("ascii")
except Exception as e:
raise DomainError(f"Invalid internationalized label {label!r}: {e}") from e
def _validate_ascii_label(label: str, *, part_name: str) -> None:
if not (1 <= len(label) <= 63):
raise DomainError(f"{part_name} label length must be 1..63, got {len(label)} for {label!r}")
if not _LABEL_RE_ASCII.match(label):
raise DomainError(
f"{part_name} label {label!r} must be alphanumeric or hyphen, "
"not start/end with hyphen."
)
def create_fqdn(subdomain: str | None, sld: str, tld: str, *, trailing_dot: bool = False) -> str:
"""
Build a validated FQDN from subdomain, SLD, and TLD.
Args:
subdomain: e.g., "www", "api", "a.b" or None / "" for no subdomain.
Can include multiple dot-separated labels.
sld: second-level domain (e.g., "example").
tld: top-level domain (e.g., "com" or "co.uk" — multi-label TLDs supported).
trailing_dot: append a final dot (.) for absolute FQDN (useful for DNS zones).
Returns:
The ASCII (IDNA/punycode) FQDN in lowercase.
Raises:
DomainError on any validation problem.
"""
# Normalize to IDNA ASCII (handles Unicode inputs)
sub_ascii = _to_idna(subdomain)
sld_ascii = _to_idna(sld)
tld_ascii = _to_idna(tld)
if not sld_ascii:
raise DomainError("SLD is required.")
if not tld_ascii:
raise DomainError("TLD is required.")
# Split into labels
sub_labels = [l for l in sub_ascii.split(".") if l] if sub_ascii else []
sld_labels = [sld_ascii]
tld_labels = [l for l in tld_ascii.split(".") if l]
# Validate each ASCII label (RFC 1035/1123 style)
for lbl in sub_labels:
_validate_ascii_label(lbl, part_name="Subdomain")
for lbl in sld_labels:
_validate_ascii_label(lbl, part_name="SLD")
for lbl in tld_labels:
_validate_ascii_label(lbl, part_name="TLD")
labels = [*sub_labels, *sld_labels, *tld_labels]
fqdn = ".".join(labels).lower()
if len(fqdn) > 253:
# RFC says 255 octets incl. trailing dot; here we enforce 253 without it
raise DomainError(f"FQDN too long ({len(fqdn)} characters, max 253): {fqdn!r}")
if trailing_dot:
fqdn += "."
return fqdn
# ------------------
# Example usages
# ------------------
if __name__ == "__main__":
print(create_fqdn("blog", "example", "com")) # blog.example.com
print(create_fqdn("a.b", "example", "co.uk")) # a.b.example.co.uk
print(create_fqdn("", "bücher", "de")) # xn--bcher-kva.de
print(create_fqdn("服务", "例子", "中国", trailing_dot=True)) # xn--serv-1s1u.xn--fsqu00a.xn--fiqs8s.
Comments
Post a Comment