"""Low-level Docker socket client.""" from __future__ import annotations import json import os import re import socket from pathlib import Path def _decode_chunked_bytes(data: bytes) -> bytes: result = bytearray() pos = 0 while pos < len(data): end = data.find(b"\r\n", pos) if end < 0: break try: size = int(data[pos:end].split(b";")[0].strip(), 16) except ValueError: break if size == 0: break pos = end + 2 result.extend(data[pos:pos + size]) pos += size + 2 return bytes(result) def _docker_get_json(path: str) -> tuple[int, dict | list | None]: sock_path = os.environ.get("DOCKER_SOCKET", "/var/run/docker.sock") if not Path(sock_path).exists(): raise RuntimeError(f"Docker socket not found: {sock_path}") request = ( f"GET {path} HTTP/1.1\r\n" "Host: docker\r\n" "Connection: close\r\n\r\n" ).encode("utf-8") chunks: list[bytes] = [] with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: sock.settimeout(10) sock.connect(sock_path) sock.sendall(request) while True: data = sock.recv(65536) if not data: break chunks.append(data) raw = b"".join(chunks) header_end = raw.find(b"\r\n\r\n") if header_end < 0: raise RuntimeError("Invalid Docker HTTP response") header_str = raw[:header_end].decode("utf-8", errors="replace") body_bytes = raw[header_end + 4:] status_line = header_str.splitlines()[0] m = re.match(r"HTTP/\S+\s+(\d+)", status_line) if not m: raise RuntimeError(f"Invalid Docker status line: {status_line!r}") code = int(m.group(1)) if "transfer-encoding: chunked" in header_str.lower(): body_bytes = _decode_chunked_bytes(body_bytes) if not body_bytes.strip(): return code, None return code, json.loads(body_bytes) def _docker_post(path: str) -> tuple[int, str]: sock_path = os.environ.get("DOCKER_SOCKET", "/var/run/docker.sock") if not Path(sock_path).exists(): raise RuntimeError(f"Docker socket not found: {sock_path}") request = ( f"POST {path} HTTP/1.1\r\n" "Host: docker\r\n" "Content-Length: 0\r\n" "Connection: close\r\n\r\n" ).encode("utf-8") chunks = [] with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: sock.settimeout(20) sock.connect(sock_path) sock.sendall(request) while True: data = sock.recv(8192) if not data: break chunks.append(data) raw = b"".join(chunks).decode("utf-8", errors="replace") status_line = raw.splitlines()[0] if raw else "" match = re.match(r"HTTP/\S+\s+(\d+)", status_line) if not match: raise RuntimeError("Invalid Docker API response") return int(match.group(1)), raw