import ipaddress import logging import posixpath import socket from urllib.parse import urlsplit, urlunsplit logger = logging.getLogger(__name__) def is_private_ip(hostname: str) -> bool: """ Check if a hostname resolves to a private/internal IP address. Protects against SSRF attacks by blocking access to internal networks. """ try: # Try to parse as IP address directly ip = ipaddress.ip_address(hostname) return ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved except ValueError: # Not a direct IP, try to resolve hostname try: # Get all IP addresses for this hostname addr_info = socket.getaddrinfo(hostname, None) for info in addr_info: ip_str = info[4][0] ip = ipaddress.ip_address(ip_str) # Block if ANY resolved IP is private/internal if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved: return True return False except (socket.gaierror, socket.error): # Cannot resolve. # Fail securely: block unresolved domains to prevent DNS rebinding # and SSRF bypasses via unresolvable addresses. logger.warning(f"Could not resolve hostname (blocking securely): {hostname}") return True def join_url(base: str, *parts: str) -> str: """ Safely join a base URL with one or more path parts. Uses urllib.parse to correctly handle scheme/netloc/query/fragment so that only the path component is normalised (double slashes removed via posixpath.join). The scheme separator ``://`` is therefore never at risk of being collapsed. Examples: join_url("https://example.com/dav/", "/remote/", "file.pdf") -> "https://example.com/dav/remote/file.pdf" """ parsed = urlsplit(base) # Strip leading/trailing slashes from every part so posixpath.join # produces a clean joined path without accidental double slashes. stripped_parts = [p.strip("/") for p in parts if p.strip("/")] base_path = parsed.path.rstrip("/") if stripped_parts: new_path = base_path + "/" + "/".join(stripped_parts) else: new_path = base_path # Normalise any remaining double slashes in the path only. new_path = posixpath.normpath(new_path) if new_path else "/" return urlunsplit((parsed.scheme, parsed.netloc, new_path, parsed.query, parsed.fragment))