|
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