Tentacle enhancements.
This commit is contained in:
1 parent
4f9b89e38b
commit
9e15f8652d
12 files changed
+619
-22
No files matched your search
@@ -37,6 +37,56 @@ def _token(value):
|
||||
return isinstance(value, str) and len(value.encode("utf-8")) == 32
|
||||
|
||||
|
||||
def valid_remote_speech(speech):
|
||||
"""Validate typed speech including its encoded IPC bound; no delivery."""
|
||||
try:
|
||||
_fields(speech, ("kind",), ("priority", "parts", "paused"))
|
||||
priority, parts = speech.get("priority", 0), speech.get("parts", [])
|
||||
if type(priority) is not int or not isinstance(parts, list):
|
||||
return False
|
||||
if speech["kind"] == "speak":
|
||||
if not 0 <= priority <= 2 or "paused" in speech or not 1 <= len(parts) <= 128:
|
||||
return False
|
||||
for part in parts:
|
||||
_fields(part, (), ("text", "characters", "language", "end"))
|
||||
text, language = part.get("text", ""), part.get("language", "")
|
||||
characters, end = part.get("characters", False), part.get("end", False)
|
||||
if (not isinstance(text, str) or not isinstance(language, str)
|
||||
or type(characters) is not bool or type(end) is not bool):
|
||||
return False
|
||||
if end:
|
||||
if text or language or characters:
|
||||
return False
|
||||
elif (not text or len(language) > 64
|
||||
or any(char not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" for char in language)):
|
||||
return False
|
||||
elif speech["kind"] == "cancel":
|
||||
if priority or parts or "paused" in speech:
|
||||
return False
|
||||
elif speech["kind"] == "pause":
|
||||
if priority or parts or type(speech.get("paused")) is not bool:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
# Match Go's canonical omitempty representation and JSON HTML escaping
|
||||
# so both SDKs enforce the same payload bound, including escaped text.
|
||||
canonical = {"kind": speech["kind"]}
|
||||
if priority:
|
||||
canonical["priority"] = priority
|
||||
if parts:
|
||||
canonical["parts"] = [{key: value for key, value in part.items() if value}
|
||||
for part in parts]
|
||||
if "paused" in speech:
|
||||
canonical["paused"] = speech["paused"]
|
||||
encoded = _encode(canonical)
|
||||
for raw, escaped in ((b"<", b"\\u003c"), (b">", b"\\u003e"), (b"&", b"\\u0026"),
|
||||
(b"\xe2\x80\xa8", b"\\u2028"), (b"\xe2\x80\xa9", b"\\u2029")):
|
||||
encoded = encoded.replace(raw, escaped)
|
||||
return len(encoded) <= 6144
|
||||
except (ProtocolError, TypeError, ValueError, UnicodeError):
|
||||
return False
|
||||
|
||||
|
||||
class SessionError(Exception):
|
||||
def __init__(self, code, message):
|
||||
super().__init__("Tentacle session error " + str(code))
|
||||
@@ -226,13 +276,13 @@ class SessionClient:
|
||||
waiter.put_nowait(value)
|
||||
|
||||
def _event(self, event):
|
||||
_fields(event, ("sequence", "kind", "epoch"), ("action", "lease", "role", "grants", "reason"))
|
||||
_fields(event, ("sequence", "kind", "epoch"), ("action", "lease", "role", "grants", "reason", "speech"))
|
||||
if not _positive(event["sequence"]) or not _positive(event["epoch"]):
|
||||
raise ProtocolError("Invalid event sequence")
|
||||
if event["kind"] == "remote.action":
|
||||
grants = event.get("grants")
|
||||
role = event.get("role")
|
||||
if (not _token(event.get("action")) or event.get("lease", "") != "" or event.get("reason", "") != ""
|
||||
if ("speech" in event or not _token(event.get("action")) or event.get("lease", "") != "" or event.get("reason", "") != ""
|
||||
or role not in ("controller", "controlled") or not isinstance(grants, list)
|
||||
or not grants or any(not isinstance(grant, str) for grant in grants)
|
||||
or len(set(grants)) != len(grants) or not set(grants) <= REMOTE_GRANTS
|
||||
@@ -240,9 +290,14 @@ class SessionClient:
|
||||
or role == "controlled" and set(grants) & {"remote-present", "keyboard-capture"}):
|
||||
raise ProtocolError("Invalid action event")
|
||||
elif event["kind"] == "remote.revoked":
|
||||
if (not _token(event.get("lease")) or event.get("action", "") != "" or event.get("role", "") != ""
|
||||
if ("speech" in event or not _token(event.get("lease")) or event.get("action", "") != "" or event.get("role", "") != ""
|
||||
or event.get("grants", []) != [] or event.get("reason") not in ("closed", "expired", "local", "replaced")):
|
||||
raise ProtocolError("Invalid revoke event")
|
||||
elif event["kind"] == "remote.output":
|
||||
if (not _token(event.get("lease")) or event.get("action", "") != ""
|
||||
or event.get("role", "") != "" or event.get("grants", []) != []
|
||||
or event.get("reason", "") != "" or not valid_remote_speech(event.get("speech"))):
|
||||
raise ProtocolError("Invalid output event")
|
||||
else:
|
||||
raise ProtocolError("Unknown session event")
|
||||
with self._lock:
|
||||
|
||||
Reference in new issue
Block a user