Initial commit

This commit is contained in:
2026-01-18 06:16:15 +01:00
commit 0c24eb19f7
21 changed files with 3069 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
"""Hostname and zone validation with punycode support."""
import re
# Valid hostname label pattern (after punycode encoding)
LABEL_PATTERN = re.compile(r'^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$', re.IGNORECASE)
class ValidationError(Exception):
"""Raised when validation fails."""
pass
def encode_hostname(hostname):
"""
Encode hostname to ASCII using punycode (IDNA).
Args:
hostname: Hostname string, possibly with unicode characters.
Returns:
ASCII-encoded hostname.
Raises:
ValidationError: If hostname is invalid.
"""
hostname = hostname.lower().strip()
if not hostname:
raise ValidationError("Hostname cannot be empty")
# Remove trailing dot if present
if hostname.endswith('.'):
hostname = hostname[:-1]
if len(hostname) > 253:
raise ValidationError("Hostname too long (max 253 characters)")
try:
# Encode each label using IDNA
labels = hostname.split('.')
encoded_labels = []
for label in labels:
if not label:
raise ValidationError("Empty label in hostname")
# Encode to punycode if needed
try:
encoded = label.encode('idna').decode('ascii')
except UnicodeError as e:
raise ValidationError(f"Invalid label '{label}': {e}")
if len(encoded) > 63:
raise ValidationError(
f"Label '{label}' too long (max 63 characters)"
)
if not LABEL_PATTERN.match(encoded):
raise ValidationError(f"Invalid label format: '{label}'")
encoded_labels.append(encoded)
return '.'.join(encoded_labels)
except ValidationError:
raise
except Exception as e:
raise ValidationError(f"Invalid hostname '{hostname}': {e}")
def encode_zone(zone):
"""
Encode zone name to ASCII using punycode (IDNA).
Args:
zone: Zone name string, possibly with unicode characters.
Returns:
ASCII-encoded zone name.
Raises:
ValidationError: If zone is invalid.
"""
if not zone:
raise ValidationError("Zone cannot be empty")
# Zone validation is same as hostname
return encode_hostname(zone)
def validate_hostname_in_zone(hostname, zone):
"""
Validate and encode hostname and zone, ensuring hostname is in zone.
Args:
hostname: Hostname string.
zone: Zone string.
Returns:
Tuple of (encoded_hostname, encoded_zone).
Raises:
ValidationError: If validation fails.
"""
encoded_hostname = encode_hostname(hostname)
encoded_zone = encode_zone(zone)
# Check hostname ends with zone
if not (encoded_hostname == encoded_zone or
encoded_hostname.endswith('.' + encoded_zone)):
raise ValidationError(
f"Hostname '{hostname}' is not in zone '{zone}'"
)
return encoded_hostname, encoded_zone
def get_relative_name(hostname, zone):
"""
Get the relative name (hostname without zone suffix).
Args:
hostname: Encoded hostname.
zone: Encoded zone.
Returns:
Relative name (e.g., 'mypc' from 'mypc.dyn.example.com').
"""
if hostname == zone:
return '@'
suffix = '.' + zone
if hostname.endswith(suffix):
return hostname[:-len(suffix)]
return hostname