140 lines
5.1 KiB
Python
140 lines
5.1 KiB
Python
"""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
|
|
from .remote_control import RemoteControl
|
|
|
|
|
|
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 in ("/operation", "/remote"):
|
|
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)) if self.path == "/operation"
|
|
else self.server.service.remote.feed(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")
|
|
service.remote = RemoteControl(service)
|
|
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)
|
|
|
|
def observe():
|
|
while not stop.is_set():
|
|
service.remote.observe()
|
|
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()
|
|
threading.Thread(target=observe, daemon=True, name="vesc-observer").start()
|
|
try:
|
|
server.serve_forever()
|
|
finally:
|
|
stop.set()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|