(url: str)
| 135 | |
| 136 | |
| 137 | def _validate_url(url: str) -> str: |
| 138 | if len(url) > _MAX_URL_LENGTH: |
| 139 | raise ToolInputError(f"URL too long ({len(url)} chars, max {_MAX_URL_LENGTH})") |
| 140 | |
| 141 | parsed = urllib.parse.urlparse(url) |
| 142 | |
| 143 | if parsed.scheme == "http": |
| 144 | url = "https" + url[4:] |
| 145 | parsed = urllib.parse.urlparse(url) |
| 146 | |
| 147 | if parsed.scheme != "https": |
| 148 | raise ToolPermissionError("only http/https URLs are allowed") |
| 149 | if not parsed.netloc: |
| 150 | raise ToolInputError("url must include a network location") |
| 151 | |
| 152 | if parsed.username or parsed.password: |
| 153 | raise ToolPermissionError("URLs with embedded credentials are not allowed") |
| 154 | |
| 155 | hostname = parsed.hostname or "" |
| 156 | if "." not in hostname and hostname not in ("localhost",): |
| 157 | raise ToolInputError("hostname must have at least 2 parts (e.g., example.com)") |
| 158 | |
| 159 | if hostname in ("localhost",) or hostname.endswith(".localhost") or _is_private_host(hostname): |
| 160 | raise ToolPermissionError("refusing to fetch localhost/private network URLs") |
| 161 | |
| 162 | return url |
| 163 | |
| 164 | |
| 165 | # -- Redirect Handling --------------------------------------------------------- |
no test coverage detected