Skip to content

Commit b0cb03c

Browse files
committed
Add remote_desktop host and viewer (headless)
A new utils/remote_desktop module lets one machine stream its screen and receive input from another. The wire format is a length-prefixed framing on raw TCP (no extra deps), starting with an HMAC-SHA256 challenge/response handshake; viewers that fail auth are dropped before they can see a frame. Host: capture loop encodes JPEG frames at the configured fps/quality and broadcasts them to authenticated viewers via a shared latest-frame slot + Condition, so a slow viewer drops frames instead of blocking the rest. Viewer input messages are JSON, validated against an allowlist, and applied through the existing wrapper helpers (lazy-imported so the viewer side stays platform-agnostic). Defaults bind to 127.0.0.1 — exposing this to untrusted networks should be paired with an SSH tunnel or TLS front-end. Tests cover the protocol, auth, the dispatch allowlist, and a full localhost host<->viewer round-trip including auth failure and graceful shutdown.
1 parent 1fde3cb commit b0cb03c

9 files changed

Lines changed: 1134 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Remote-desktop host/viewer for screen streaming and remote input.
2+
3+
The protocol is a minimal length-prefixed framing on raw TCP (no extra
4+
deps). The host periodically encodes the screen as JPEG and pushes it to
5+
authenticated viewers; viewers send back JSON input messages that the
6+
host dispatches via the existing mouse/keyboard wrappers. Token-based
7+
HMAC-SHA256 authentication and a default loopback bind keep casual
8+
misuse difficult — this is *not* a hardened RDP replacement, and exposing
9+
it to untrusted networks should be paired with an SSH tunnel or TLS
10+
front-end.
11+
"""
12+
from je_auto_control.utils.remote_desktop.host import RemoteDesktopHost
13+
from je_auto_control.utils.remote_desktop.input_dispatch import (
14+
InputDispatchError, dispatch_input,
15+
)
16+
from je_auto_control.utils.remote_desktop.protocol import (
17+
AuthenticationError, MessageType, ProtocolError,
18+
decode_frame_header, encode_frame,
19+
)
20+
from je_auto_control.utils.remote_desktop.viewer import RemoteDesktopViewer
21+
22+
__all__ = [
23+
"RemoteDesktopHost", "RemoteDesktopViewer",
24+
"InputDispatchError", "AuthenticationError", "ProtocolError",
25+
"MessageType", "encode_frame", "decode_frame_header",
26+
"dispatch_input",
27+
]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""HMAC-SHA256 challenge/response helpers shared by host and viewer."""
2+
import hmac
3+
import os
4+
from hashlib import sha256
5+
6+
NONCE_BYTES = 32
7+
8+
9+
def make_nonce() -> bytes:
10+
"""Return a fresh random nonce for the auth handshake."""
11+
return os.urandom(NONCE_BYTES)
12+
13+
14+
def compute_response(token: str, nonce: bytes) -> bytes:
15+
"""Return ``HMAC_SHA256(token, nonce)`` for the given token."""
16+
if not isinstance(token, str) or not token:
17+
raise ValueError("token must be a non-empty string")
18+
if not isinstance(nonce, (bytes, bytearray)) or len(nonce) != NONCE_BYTES:
19+
raise ValueError(f"nonce must be {NONCE_BYTES} bytes")
20+
return hmac.new(token.encode("utf-8"), bytes(nonce), sha256).digest()
21+
22+
23+
def verify_response(token: str, nonce: bytes, response: bytes) -> bool:
24+
"""Constant-time check that ``response`` matches the expected HMAC."""
25+
expected = compute_response(token, nonce)
26+
if not isinstance(response, (bytes, bytearray)):
27+
return False
28+
return hmac.compare_digest(expected, bytes(response))

0 commit comments

Comments
 (0)