Initial curated Unraid-AppOps-Gateway source from 3bd6f6fee310125bfbb4d4650389a817f498724d
This commit is contained in:
commit
1dfbca08a1
528 files changed
+131054
No files matched your search
@@ -0,0 +1,70 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import socket
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
|
||||
class UnsafeConnectorUrl(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
_BLOCKED_HOSTS = {"localhost", "localhost.localdomain", "metadata.google.internal"}
|
||||
_BLOCKED_IPS = {
|
||||
ipaddress.ip_address("169.254.169.254"),
|
||||
ipaddress.ip_address("100.100.100.200"),
|
||||
}
|
||||
|
||||
|
||||
def _address_is_forbidden(address: ipaddress.IPv4Address | ipaddress.IPv6Address, *, allow_loopback: bool) -> bool:
|
||||
return (
|
||||
address in _BLOCKED_IPS
|
||||
or address.is_link_local
|
||||
or address.is_multicast
|
||||
or address.is_unspecified
|
||||
or (address.is_loopback and not allow_loopback)
|
||||
)
|
||||
|
||||
|
||||
def validate_connector_url(value: str, *, allow_loopback: bool = False) -> str:
|
||||
"""Validate a configured LAN connector URL and return a normalized value."""
|
||||
raw = str(value or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
parsed = urlsplit(raw)
|
||||
if parsed.scheme.lower() not in {"http", "https"} or not parsed.hostname:
|
||||
raise UnsafeConnectorUrl("connector_url_requires_http_or_https")
|
||||
if parsed.username or parsed.password or parsed.fragment:
|
||||
raise UnsafeConnectorUrl("connector_url_contains_forbidden_components")
|
||||
try:
|
||||
port = parsed.port
|
||||
except ValueError as exc:
|
||||
raise UnsafeConnectorUrl("connector_url_has_invalid_port") from exc
|
||||
if port is not None and not 1 <= port <= 65535:
|
||||
raise UnsafeConnectorUrl("connector_url_has_invalid_port")
|
||||
|
||||
hostname = parsed.hostname.rstrip(".").lower()
|
||||
if hostname in _BLOCKED_HOSTS and not allow_loopback:
|
||||
raise UnsafeConnectorUrl("connector_url_loopback_blocked")
|
||||
try:
|
||||
address = ipaddress.ip_address(hostname)
|
||||
except ValueError:
|
||||
address = None
|
||||
if address is not None:
|
||||
if _address_is_forbidden(address, allow_loopback=allow_loopback):
|
||||
raise UnsafeConnectorUrl("connector_url_reserved_address_blocked")
|
||||
else:
|
||||
# Inspect currently resolvable names as well as literal IPs. Unresolved LAN
|
||||
# names remain configurable, but DNS names resolving to local-only system
|
||||
# addresses cannot bypass the literal-address checks.
|
||||
try:
|
||||
resolved = {item[4][0] for item in socket.getaddrinfo(hostname, port or 0, type=socket.SOCK_STREAM)}
|
||||
except socket.gaierror:
|
||||
resolved = set()
|
||||
for resolved_value in resolved:
|
||||
resolved_address = ipaddress.ip_address(resolved_value)
|
||||
if _address_is_forbidden(resolved_address, allow_loopback=allow_loopback):
|
||||
raise UnsafeConnectorUrl("connector_url_resolves_to_reserved_address")
|
||||
|
||||
normalized_path = parsed.path.rstrip("/")
|
||||
return urlunsplit((parsed.scheme.lower(), parsed.netloc, normalized_path, parsed.query, ""))
|
||||
Reference in new issue
Block a user