aics-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aics_cli/__init__.py +8 -0
- aics_cli/__main__.py +3 -0
- aics_cli/bundle_build.py +216 -0
- aics_cli/card.py +933 -0
- aics_cli/checkpoints.py +498 -0
- aics_cli/cli.py +298 -0
- aics_cli/commands.py +1656 -0
- aics_cli/consistency.py +280 -0
- aics_cli/dnstxt.py +319 -0
- aics_cli/encoding.py +125 -0
- aics_cli/errors.py +57 -0
- aics_cli/evidence.py +88 -0
- aics_cli/home.py +112 -0
- aics_cli/keystore.py +189 -0
- aics_cli/message.py +875 -0
- aics_cli/net.py +117 -0
- aics_cli/policy.py +618 -0
- aics_cli/published_keys.py +34 -0
- aics_cli/tiles_http.py +59 -0
- aics_cli/trust.py +243 -0
- aics_cli-0.1.0.dist-info/METADATA +420 -0
- aics_cli-0.1.0.dist-info/RECORD +25 -0
- aics_cli-0.1.0.dist-info/WHEEL +4 -0
- aics_cli-0.1.0.dist-info/entry_points.txt +3 -0
- aics_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
aics_cli/net.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""HTTP GET with the stdlib only.
|
|
2
|
+
|
|
3
|
+
* HTTPS is required, except plain HTTP to a loopback host (local testing).
|
|
4
|
+
* Redirects are not followed: a 3xx comes back as a status (tlog-tiles
|
|
5
|
+
resources and checkpoints must not redirect, AICS-1 §6.1, §12.2).
|
|
6
|
+
* Responses are size-capped. HTTP error statuses are returned, not raised;
|
|
7
|
+
only transport failures raise :class:`FetchError`.
|
|
8
|
+
* Nothing fetched here is trusted: every caller verifies what it gets
|
|
9
|
+
against keys pinned out of band.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ipaddress
|
|
15
|
+
import socket
|
|
16
|
+
import ssl
|
|
17
|
+
import urllib.error
|
|
18
|
+
import urllib.parse
|
|
19
|
+
import urllib.request
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Protocol
|
|
22
|
+
|
|
23
|
+
from . import __version__
|
|
24
|
+
from .errors import FetchError
|
|
25
|
+
|
|
26
|
+
DEFAULT_TIMEOUT = 15.0
|
|
27
|
+
DEFAULT_MAX_BYTES = 4 * 1024 * 1024
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Response:
|
|
32
|
+
status: int
|
|
33
|
+
body: bytes
|
|
34
|
+
url: str
|
|
35
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def content_type(self) -> str:
|
|
39
|
+
return self.headers.get("content-type", "").split(";")[0].strip().lower()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Fetcher(Protocol):
|
|
43
|
+
def get(self, url: str, *, max_bytes: int = DEFAULT_MAX_BYTES, accept: str | None = None) -> Response: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _is_loopback(host: str | None) -> bool:
|
|
47
|
+
if not host:
|
|
48
|
+
return False
|
|
49
|
+
if host == "localhost" or host.endswith(".localhost"):
|
|
50
|
+
return True
|
|
51
|
+
try:
|
|
52
|
+
return ipaddress.ip_address(host.strip("[]")).is_loopback
|
|
53
|
+
except ValueError:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def check_url(url: str) -> str:
|
|
58
|
+
"""Raise FetchError unless ``url`` is https, or http to a loopback host."""
|
|
59
|
+
try:
|
|
60
|
+
parts = urllib.parse.urlsplit(url)
|
|
61
|
+
except ValueError as exc:
|
|
62
|
+
raise FetchError(f"bad URL {url!r}: {exc}") from None
|
|
63
|
+
if parts.scheme == "https" and parts.hostname:
|
|
64
|
+
return url
|
|
65
|
+
if parts.scheme == "http" and _is_loopback(parts.hostname):
|
|
66
|
+
return url
|
|
67
|
+
raise FetchError(f"refusing {url!r}: only https:// (or http:// to localhost) is allowed")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class _NoRedirects(urllib.request.HTTPRedirectHandler):
|
|
71
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
72
|
+
return None # urllib then raises HTTPError, returned to the caller as a 3xx status
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class HttpFetcher:
|
|
76
|
+
"""urllib-based fetcher with a timeout, a size cap and no redirects."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, timeout: float = DEFAULT_TIMEOUT, user_agent: str | None = None) -> None:
|
|
79
|
+
self.timeout = timeout
|
|
80
|
+
self.user_agent = user_agent or f"aics/{__version__} (+https://wesupport-ai.com/card)"
|
|
81
|
+
self._opener = urllib.request.build_opener(
|
|
82
|
+
_NoRedirects(), urllib.request.HTTPSHandler(context=ssl.create_default_context())
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def get(self, url: str, *, max_bytes: int = DEFAULT_MAX_BYTES, accept: str | None = None) -> Response:
|
|
86
|
+
check_url(url)
|
|
87
|
+
headers = {"User-Agent": self.user_agent}
|
|
88
|
+
if accept:
|
|
89
|
+
headers["Accept"] = accept
|
|
90
|
+
req = urllib.request.Request(url, headers=headers, method="GET")
|
|
91
|
+
try:
|
|
92
|
+
with self._opener.open(req, timeout=self.timeout) as resp:
|
|
93
|
+
body = resp.read(max_bytes + 1)
|
|
94
|
+
status = resp.status
|
|
95
|
+
final_url = resp.geturl()
|
|
96
|
+
hdrs = {k.lower(): v for k, v in resp.headers.items()}
|
|
97
|
+
except urllib.error.HTTPError as exc:
|
|
98
|
+
try:
|
|
99
|
+
body = exc.read(min(max_bytes, 65536))
|
|
100
|
+
except Exception:
|
|
101
|
+
body = b""
|
|
102
|
+
return Response(
|
|
103
|
+
status=exc.code, body=body, url=url, headers={k.lower(): v for k, v in (exc.headers or {}).items()}
|
|
104
|
+
)
|
|
105
|
+
except FetchError:
|
|
106
|
+
raise
|
|
107
|
+
except (urllib.error.URLError, TimeoutError, ssl.SSLError, ConnectionError, OSError) as exc:
|
|
108
|
+
reason = getattr(exc, "reason", exc)
|
|
109
|
+
raise FetchError(f"cannot fetch {url}: {reason}") from None
|
|
110
|
+
if len(body) > max_bytes:
|
|
111
|
+
raise FetchError(f"response from {url} exceeds {max_bytes} bytes")
|
|
112
|
+
return Response(status=status, body=body, url=final_url, headers=hdrs)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def join_url(base: str, path: str) -> str:
|
|
116
|
+
"""``base`` + ``path`` with exactly one slash between them."""
|
|
117
|
+
return base.rstrip("/") + "/" + path.lstrip("/")
|