"""Bounded private HTTP over Unix sockets, never a public device endpoint.""" import http.client import json import socket import socketserver import struct import threading from dataclasses import dataclass from http.server import BaseHTTPRequestHandler LIMIT = 65536 INVENTORY_LIMIT = 2 * 1024 * 1024 @dataclass(frozen=True) class Binary: data: bytes def request(path, route, value=None, timeout=25, binary=False): client = http.client.HTTPConnection("driver", timeout=timeout) client.sock = socket.socket(socket.AF_UNIX) client.sock.settimeout(timeout) try: client.sock.connect(str(path)) body = None if value is None else json.dumps(value, allow_nan=False).encode() client.request( "GET" if value is None else "POST", route, body, {"Content-Type": "application/json"} ) reply = client.getresponse() limit = ( 4 * 1024 * 1024 + 1028 if binary else INVENTORY_LIMIT if route == "/inventory" else LIMIT ) raw = reply.read(limit + 1) if len(raw) > limit or reply.status != 200: raise RuntimeError("Camera service request failed") if binary: if reply.getheader("Content-Type") != "application/octet-stream": raise RuntimeError("Invalid video response") return raw return json.loads(raw) except http.client.HTTPException as error: raise RuntimeError("Camera service response is incomplete") from error finally: client.close() class Server(socketserver.ThreadingMixIn, socketserver.UnixStreamServer): daemon_threads = True block_on_close = False def __init__(self, path, dispatch, allowed_uids): self.dispatch = dispatch self.allowed_uids = frozenset(allowed_uids) self.slots = threading.BoundedSemaphore(12) super().__init__(str(path), Handler) def verify_request(self, request, address): _, uid, _ = struct.unpack( "3i", request.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12) ) return uid in self.allowed_uids def process_request(self, request, address): if not self.slots.acquire(blocking=False): self.shutdown_request(request) return try: super().process_request(request, address) except BaseException: self.slots.release() raise def process_request_thread(self, request, address): try: super().process_request_thread(request, address) finally: self.slots.release() class Handler(BaseHTTPRequestHandler): def setup(self): self.request.settimeout(10) super().setup() def log_message(self, *_): pass def do_GET(self): self.handle_request() def do_POST(self): self.handle_request() def handle_request(self): try: if self.headers.get("Transfer-Encoding"): raise ValueError("Chunked commands are not supported") value = None if self.command == "POST": length = int(self.headers.get("Content-Length", "0")) if ( not 0 < length <= LIMIT or self.headers.get("Content-Type") != "application/json" ): raise ValueError("Invalid command size") raw = self.rfile.read(length) if len(raw) != length: raise ValueError("Truncated command") value = json.loads(raw) result = self.server.dispatch(self.command, self.path, value, self.headers) status = 200 except (ValueError, KeyError, TypeError): status, result = 400, {"error": "Некорректная команда камеры."} except (RuntimeError, OSError, TimeoutError): status, result = 409, {"error": "Камера не ответила. Проверьте подключение."} binary = isinstance(result, Binary) data = ( result.data if binary else json.dumps(result, ensure_ascii=False, allow_nan=False).encode() ) limit = ( 4 * 1024 * 1024 + 1028 if binary else INVENTORY_LIMIT if self.path == "/inventory" else LIMIT ) if len(data) > limit: binary = False status, data = 500, b'{"error":"Camera response exceeds its limit"}' self.send_response(status) self.send_header( "Content-Type", "application/octet-stream" if binary else "application/json" ) self.send_header("Content-Length", str(len(data))) self.send_header("Connection", "close") self.end_headers() self.wfile.write(data)