| 
3 | 3 | import re  | 
4 | 4 | import secrets  | 
5 | 5 | from pathlib import Path  | 
 | 6 | +from typing import Any  | 
6 | 7 | from urllib.parse import urlparse, urlunsplit  | 
7 | 8 | 
 
  | 
8 | 9 | from cryptography.exceptions import InvalidSignature  | 
@@ -144,6 +145,114 @@ def validate_image_data(data: str) -> None:  | 
144 | 145 |         raise ValidationError(_("Invalid base64-encoded data in data URL."))  | 
145 | 146 | 
 
  | 
146 | 147 | 
 
  | 
 | 148 | +def _is_valid_domain_format(domain: str) -> bool:  | 
 | 149 | +    """Check basic domain format requirements."""  | 
 | 150 | +    return isinstance(domain, str) and bool(domain) and len(domain) <= 255 and '.' in domain  | 
 | 151 | + | 
 | 152 | + | 
 | 153 | +def _normalize_domain(domain: str) -> str:  | 
 | 154 | +    """Normalize domain by removing trailing dot if present."""  | 
 | 155 | +    return domain[:-1] if domain.endswith('.') else domain  | 
 | 156 | + | 
 | 157 | + | 
 | 158 | +def _is_valid_label(label: str) -> bool:  | 
 | 159 | +    """Validate a single domain label according to LDH (Letter, Digit, Hyphen) rule."""  | 
 | 160 | +    return bool(label) and len(label) <= 63 and re.match(r'^[a-zA-Z0-9-]+$', label) is not None and not label.startswith('-') and not label.endswith('-')  | 
 | 161 | + | 
 | 162 | + | 
 | 163 | +def _is_valid_tld(tld: str) -> bool:  | 
 | 164 | +    """Validate the top-level domain."""  | 
 | 165 | +    return len(tld) >= 2 and not tld.isdigit() and re.search(r'[a-zA-Z]', tld) is not None  | 
 | 166 | + | 
 | 167 | + | 
 | 168 | +def validate_domain_name(domain: str) -> bool:  | 
 | 169 | +    """  | 
 | 170 | +    Validate a domain name according to RFC standards.  | 
 | 171 | +
  | 
 | 172 | +    Validates domain names according to RFC 1035, 1123, and 2181 specifications.  | 
 | 173 | +
  | 
 | 174 | +    Args:  | 
 | 175 | +        domain: The domain name to validate  | 
 | 176 | +
  | 
 | 177 | +    Returns:  | 
 | 178 | +        bool: True if the domain name is valid, False otherwise  | 
 | 179 | +
  | 
 | 180 | +    Checks:  | 
 | 181 | +        - Length limits (labels ≤ 63 chars, total ≤ 255 chars)  | 
 | 182 | +        - LDH rule (Letters, Digits, Hyphens only)  | 
 | 183 | +        - No leading/trailing hyphens in labels  | 
 | 184 | +        - Valid TLD format (not all-numeric, at least 2 chars)  | 
 | 185 | +        - At least one dot (fully qualified domain name)  | 
 | 186 | +    """  | 
 | 187 | +    # Basic format validation  | 
 | 188 | +    if not _is_valid_domain_format(domain):  | 
 | 189 | +        return False  | 
 | 190 | + | 
 | 191 | +    # Normalize and split domain into labels  | 
 | 192 | +    normalized_domain = _normalize_domain(domain)  | 
 | 193 | +    labels = normalized_domain.split('.')  | 
 | 194 | + | 
 | 195 | +    # Must have at least domain.tld  | 
 | 196 | +    if len(labels) < 2:  | 
 | 197 | +        return False  | 
 | 198 | + | 
 | 199 | +    # Validate each label  | 
 | 200 | +    for label in labels:  | 
 | 201 | +        if not _is_valid_label(label):  | 
 | 202 | +            return False  | 
 | 203 | + | 
 | 204 | +    # Validate TLD (last label)  | 
 | 205 | +    return _is_valid_tld(labels[-1])  | 
 | 206 | + | 
 | 207 | + | 
 | 208 | +def validate_port(port: Any) -> bool:  | 
 | 209 | +    """  | 
 | 210 | +    Validate a network port number.  | 
 | 211 | +
  | 
 | 212 | +    Accepts port numbers as integers or strings and validates they are within  | 
 | 213 | +    the valid TCP/UDP port range (1-65535).  | 
 | 214 | +
  | 
 | 215 | +    Args:  | 
 | 216 | +        port: Port number as int, str, or other type  | 
 | 217 | +
  | 
 | 218 | +    Returns:  | 
 | 219 | +        bool: True if the port is valid, False otherwise  | 
 | 220 | +
  | 
 | 221 | +    Examples:  | 
 | 222 | +        validate_port(80)        # True  | 
 | 223 | +        validate_port("443")     # True  | 
 | 224 | +        validate_port("0")       # False (port 0 is reserved)  | 
 | 225 | +        validate_port("65536")   # False (above valid range)  | 
 | 226 | +        validate_port(None)      # False (invalid type)  | 
 | 227 | +        validate_port("abc")     # False (non-numeric string)  | 
 | 228 | +    """  | 
 | 229 | +    # Handle None and non-string/non-integer types  | 
 | 230 | +    if port is None:  | 
 | 231 | +        return False  | 
 | 232 | + | 
 | 233 | +    # Explicitly reject boolean types (even though they're technically integers in Python)  | 
 | 234 | +    if isinstance(port, bool):  | 
 | 235 | +        return False  | 
 | 236 | + | 
 | 237 | +    # Convert to integer if it's a string  | 
 | 238 | +    if isinstance(port, str):  | 
 | 239 | +        # Reject strings with leading/trailing whitespace for stricter validation  | 
 | 240 | +        if port != port.strip():  | 
 | 241 | +            return False  | 
 | 242 | +        try:  | 
 | 243 | +            port_int = int(port)  | 
 | 244 | +        except ValueError:  | 
 | 245 | +            return False  | 
 | 246 | +    elif isinstance(port, int):  | 
 | 247 | +        port_int = port  | 
 | 248 | +    else:  | 
 | 249 | +        # Reject other types (float, list, dict, etc.)  | 
 | 250 | +        return False  | 
 | 251 | + | 
 | 252 | +    # Validate port range (1-65535)  | 
 | 253 | +    return 1 <= port_int <= 65535  | 
 | 254 | + | 
 | 255 | + | 
147 | 256 | def to_python_boolean(value, allow_none=False):  | 
148 | 257 |     value = str(value)  | 
149 | 258 |     if value.lower() in ('true', '1', 't'):  | 
 | 
0 commit comments