hosts.py
python
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
9 hours ago
| 1 | """Upstream host allowlist (§HGD.6.6).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import ipaddress |
| 6 | import re |
| 7 | from urllib.parse import urlparse |
| 8 | |
| 9 | DEFAULT_ALLOWED_HOSTS = frozenset( |
| 10 | { |
| 11 | "api.github.com", |
| 12 | "raw.githubusercontent.com", |
| 13 | } |
| 14 | ) |
| 15 | |
| 16 | # Optional muse deepen hosts must be explicitly configured (finite list, no wildcards). |
| 17 | _HOSTNAME_RE = re.compile(r"^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,}$|^[A-Za-z0-9-]+$") |
| 18 | |
| 19 | |
| 20 | def _is_literal_ip(host: str) -> bool: |
| 21 | try: |
| 22 | ipaddress.ip_address(host) |
| 23 | return True |
| 24 | except ValueError: |
| 25 | return False |
| 26 | |
| 27 | |
| 28 | def _is_blocked_special(host: str) -> bool: |
| 29 | """Refuse link-local / metadata / loopback IP literals always.""" |
| 30 | try: |
| 31 | addr = ipaddress.ip_address(host) |
| 32 | except ValueError: |
| 33 | lowered = host.lower() |
| 34 | return lowered in {"localhost", "metadata.google.internal"} |
| 35 | return bool( |
| 36 | addr.is_loopback |
| 37 | or addr.is_link_local |
| 38 | or addr.is_private |
| 39 | or addr.is_reserved |
| 40 | or addr.is_multicast |
| 41 | or addr.is_unspecified |
| 42 | ) |
| 43 | |
| 44 | |
| 45 | def host_allowed(host: str, *, extra_allowed: frozenset[str] | None = None) -> bool: |
| 46 | """Return whether ``host`` may be contacted as an upstream. |
| 47 | |
| 48 | Literal IPs and link-local/metadata addresses are always refused. |
| 49 | """ |
| 50 | if not host or not isinstance(host, str): |
| 51 | return False |
| 52 | hostname = host.strip().lower().split("%", 1)[0] |
| 53 | if not hostname: |
| 54 | return False |
| 55 | if _is_literal_ip(hostname): |
| 56 | return False |
| 57 | if _is_blocked_special(hostname): |
| 58 | return False |
| 59 | allowed = DEFAULT_ALLOWED_HOSTS | (extra_allowed or frozenset()) |
| 60 | return hostname in allowed |
| 61 | |
| 62 | |
| 63 | def url_host_allowed(url: str, *, extra_allowed: frozenset[str] | None = None) -> bool: |
| 64 | """Parse ``url`` and check its hostname against the allowlist.""" |
| 65 | try: |
| 66 | parsed = urlparse(url) |
| 67 | except ValueError: |
| 68 | return False |
| 69 | if parsed.scheme not in {"https", "http"}: |
| 70 | return False |
| 71 | host = parsed.hostname |
| 72 | if host is None: |
| 73 | return False |
| 74 | return host_allowed(host, extra_allowed=extra_allowed) |
| 75 | |
| 76 | |
| 77 | def validate_extra_hosts(hosts: list[str]) -> frozenset[str]: |
| 78 | """Validate optional muse deepen hostnames (finite, no wildcards, no IPs).""" |
| 79 | out: set[str] = set() |
| 80 | for raw in hosts: |
| 81 | if not isinstance(raw, str) or not raw.strip(): |
| 82 | raise ValueError("empty muse host") |
| 83 | host = raw.strip().lower() |
| 84 | if "*" in host or host.startswith("."): |
| 85 | raise ValueError(f"wildcard muse host refused: {raw!r}") |
| 86 | if _is_literal_ip(host) or _is_blocked_special(host): |
| 87 | raise ValueError(f"disallowed muse host: {raw!r}") |
| 88 | if not _HOSTNAME_RE.match(host): |
| 89 | raise ValueError(f"invalid muse host: {raw!r}") |
| 90 | out.add(host) |
| 91 | return frozenset(out) |
File History
1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
9 hours ago