"""Private Unix socket endpoint; peer UID must be the installed Node service.""" import json import os from pathlib import Path import pwd import re import socket import socketserver import struct import threading from http.server import BaseHTTPRequestHandler from urllib.parse import urlsplit, parse_qs from .service import Service class Handler(BaseHTTPRequestHandler): def log_message(self, *_): pass def do_GET(self): self.dispatch() def do_POST(self): self.dispatch() def dispatch(self): self.connection.settimeout(10) status = 200 try: node = self.headers.get("X-Node-Id", "") if not re.fullmatch(r"[a-zA-Z0-9_.:-]{1,128}", node) or self.headers.get("Transfer-Encoding"): raise ValueError("Invalid request") if self.command == "GET" and self.path == "/inventory": result = self.server.service.inventory(node) elif self.command == "GET" and self.path.startswith("/archives/"): url = urlsplit(self.path) parts = url.path.split("/") if len(parts) not in (3, 4) or not re.fullmatch(r"vesc_[0-9a-f]{32}", parts[2]): raise ValueError("Invalid archive target") archive = self.server.service.archive if len(parts) == 4: result = archive.read("local", parts[2], parts[3]) else: before = int(parse_qs(url.query).get("before", ["0"])[0]) result = archive.listing("local", parts[2], before) elif self.command == "GET" and self.path.startswith("/archive-export?"): after = int(parse_qs(urlsplit(self.path).query).get("after", ["0"])[0]) result = self.server.service.archive.export("local", after) elif self.command == "POST" and self.path == "/operation": size = int(self.headers.get("Content-Length", "0")) if not 0 < size <= 65536 or self.headers.get("Content-Type") != "application/json": raise ValueError("Invalid command") raw = self.rfile.read(size) if len(raw) != size: raise ValueError("Truncated command") result = self.server.service.execute(json.loads(raw)) else: raise ValueError("Unknown route") except (ValueError, KeyError, TypeError, OSError): status, result = 400, {"error": "Запрос VESC отклонён. Обновите сведения об устройстве."} data = json.dumps(result, ensure_ascii=False, allow_nan=False).encode() self.send_response(status) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(data))) self.send_header("Connection", "close") self.end_headers() self.wfile.write(data) class Server(socketserver.ThreadingMixIn, socketserver.UnixStreamServer): daemon_threads = True def __init__(self, path, handler): self.slots = threading.BoundedSemaphore(8) super().__init__(path, handler) 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() def verify_request(self, request, address): _, uid, _ = struct.unpack("3i", request.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)) return uid == self.node_uid def main(): if os.geteuid() == 0: raise RuntimeError("VESC must run as its own unprivileged user") os.umask(0o007) service = Service("/var/lib/mission-core-vesc") stop = threading.Event() def scan(): while not stop.is_set(): try: service.scan() except OSError: # A transient sysfs race must not silently kill discovery. pass stop.wait(2) path = Path("/run/mission-core-vesc/driver.sock") path.unlink(missing_ok=True) with Server(str(path), Handler) as server: server.node_uid = pwd.getpwnam("mission-core-node").pw_uid server.service = service thread = threading.Thread(target=scan, daemon=True) thread.start() try: server.serve_forever() finally: stop.set() if __name__ == "__main__": main()