(url: str)
| 73 | |
| 74 | |
| 75 | def validate_public_http_url(url: str) -> tuple[ipaddress._BaseAddress, ...]: |
| 76 | parsed = urlparse(url) |
| 77 | |
| 78 | if parsed.scheme not in SAFE_HTTP_SCHEMES: |
| 79 | raise UnsafeUrlError("Only http:// and https:// URLs are supported") |
| 80 | if not parsed.hostname: |
| 81 | raise UnsafeUrlError("URL hostname is required") |
| 82 | if parsed.username or parsed.password: |
| 83 | raise UnsafeUrlError("URLs with embedded credentials are not allowed") |
| 84 | |
| 85 | hostname = parsed.hostname.rstrip(".").lower() |
| 86 | if hostname == "localhost" or hostname.endswith(".localhost"): |
| 87 | raise UnsafeUrlError(f"Blocked local hostname '{hostname}'") |
| 88 | |
| 89 | ips = resolve_host_ips(hostname) |
| 90 | blocked = [str(ip) for ip in ips if not ip.is_global] |
| 91 | if blocked: |
| 92 | raise UnsafeUrlError( |
| 93 | f"Blocked non-public address resolution for '{hostname}': {', '.join(blocked)}" |
| 94 | ) |
| 95 | |
| 96 | return ips |
| 97 | |
| 98 | |
| 99 | def fetch_public_http_resource( |
no test coverage detected