Lockdown streaming browser sessions (#2725)
This commit is contained in:
@@ -79,6 +79,7 @@ class Settings(BaseSettings):
|
|||||||
BROWSER_TIMEZONE: str = "America/New_York"
|
BROWSER_TIMEZONE: str = "America/New_York"
|
||||||
BROWSER_WIDTH: int = 1920
|
BROWSER_WIDTH: int = 1920
|
||||||
BROWSER_HEIGHT: int = 1080
|
BROWSER_HEIGHT: int = 1080
|
||||||
|
BROWSER_POLICY_FILE: str = "/etc/chromium/policies/managed/policies.json"
|
||||||
|
|
||||||
# Add extension folders name here to load extension in your browser
|
# Add extension folders name here to load extension in your browser
|
||||||
EXTENSIONS_BASE_PATH: str = "./extensions"
|
EXTENSIONS_BASE_PATH: str = "./extensions"
|
||||||
|
|||||||
@@ -24,13 +24,59 @@ Loops = list[asyncio.Task] # aka "queue-less actors"; or "programs"
|
|||||||
|
|
||||||
|
|
||||||
class MessageType(IntEnum):
|
class MessageType(IntEnum):
|
||||||
keyboard = 4
|
Keyboard = 4
|
||||||
mouse = 5
|
Mouse = 5
|
||||||
|
|
||||||
|
|
||||||
|
class Keys:
|
||||||
|
"""
|
||||||
|
VNC RFB keycodes. There's likely a pithier repr (indexes 6-7). This is ok for now.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Down:
|
||||||
|
Ctrl = b"\x04\x01\x00\x00\x00\x00\xff\xe3"
|
||||||
|
Cmd = b"\x04\x01\x00\x00\x00\x00\xff\xe9"
|
||||||
|
Alt = b"\x04\x01\x00\x00\x00\x00\xff~" # option
|
||||||
|
OKey = b"\x04\x01\x00\x00\x00\x00\x00o"
|
||||||
|
|
||||||
|
class Up:
|
||||||
|
Ctrl = b"\x04\x00\x00\x00\x00\x00\xff\xe3"
|
||||||
|
Cmd = b"\x04\x00\x00\x00\x00\x00\xff\xe9"
|
||||||
|
Alt = b"\x04\x00\x00\x00\x00\x00\xff\x7e" # option
|
||||||
|
|
||||||
|
|
||||||
|
def is_rmb(data: bytes) -> bool:
|
||||||
|
return data[0:2] == b"\x05\x04"
|
||||||
|
|
||||||
|
|
||||||
|
class Mouse:
|
||||||
|
class Up:
|
||||||
|
Right = is_rmb
|
||||||
|
|
||||||
|
|
||||||
LOG = structlog.get_logger()
|
LOG = structlog.get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class KeyState:
|
||||||
|
ctrl_is_down: bool = False
|
||||||
|
alt_is_down: bool = False
|
||||||
|
cmd_is_down: bool = False
|
||||||
|
o_is_down: bool = False
|
||||||
|
|
||||||
|
def is_forbidden(self, data: bytes) -> bool:
|
||||||
|
"""
|
||||||
|
:return: True if the key is forbidden, else False
|
||||||
|
"""
|
||||||
|
return self.is_ctrl_o(data)
|
||||||
|
|
||||||
|
def is_ctrl_o(self, data: bytes) -> bool:
|
||||||
|
"""
|
||||||
|
Do not allow the opening of files.
|
||||||
|
"""
|
||||||
|
return self.ctrl_is_down and data == Keys.Down.OKey
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class Streaming:
|
class Streaming:
|
||||||
"""
|
"""
|
||||||
@@ -49,6 +95,7 @@ class Streaming:
|
|||||||
# --
|
# --
|
||||||
|
|
||||||
browser_session: AddressablePersistentBrowserSession | None = None
|
browser_session: AddressablePersistentBrowserSession | None = None
|
||||||
|
key_state: KeyState = dataclasses.field(default_factory=KeyState)
|
||||||
task: Task | None = None
|
task: Task | None = None
|
||||||
workflow_run: WorkflowRun | None = None
|
workflow_run: WorkflowRun | None = None
|
||||||
|
|
||||||
@@ -76,6 +123,20 @@ class Streaming:
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def update_key_state(self, data: bytes) -> None:
|
||||||
|
if data == Keys.Down.Ctrl:
|
||||||
|
self.key_state.ctrl_is_down = True
|
||||||
|
elif data == Keys.Up.Ctrl:
|
||||||
|
self.key_state.ctrl_is_down = False
|
||||||
|
elif data == Keys.Down.Alt:
|
||||||
|
self.key_state.alt_is_down = True
|
||||||
|
elif data == Keys.Up.Alt:
|
||||||
|
self.key_state.alt_is_down = False
|
||||||
|
elif data == Keys.Down.Cmd:
|
||||||
|
self.key_state.cmd_is_down = True
|
||||||
|
elif data == Keys.Up.Cmd:
|
||||||
|
self.key_state.cmd_is_down = False
|
||||||
|
|
||||||
|
|
||||||
async def auth(apikey: str | None, token: str | None, websocket: WebSocket) -> str | None:
|
async def auth(apikey: str | None, token: str | None, websocket: WebSocket) -> str | None:
|
||||||
"""
|
"""
|
||||||
@@ -235,6 +296,8 @@ async def get_streaming_for_workflow_run(
|
|||||||
websocket=websocket,
|
websocket=websocket,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LOG.info("Got streaming context for workflow run.", streaming=streaming)
|
||||||
|
|
||||||
loops = [
|
loops = [
|
||||||
asyncio.create_task(loop_verify_workflow_run(streaming)),
|
asyncio.create_task(loop_verify_workflow_run(streaming)),
|
||||||
asyncio.create_task(loop_stream_vnc(streaming)),
|
asyncio.create_task(loop_stream_vnc(streaming)),
|
||||||
@@ -402,10 +465,19 @@ async def loop_stream_vnc(streaming: Streaming) -> None:
|
|||||||
if data:
|
if data:
|
||||||
message_type = data[0]
|
message_type = data[0]
|
||||||
|
|
||||||
# TODO: verify 4,5 are keyboard/mouse; they seem to be
|
if message_type == MessageType.Keyboard.value:
|
||||||
|
streaming.update_key_state(data)
|
||||||
|
|
||||||
|
if streaming.key_state.is_forbidden(data):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if message_type == MessageType.Mouse.value:
|
||||||
|
if Mouse.Up.Right(data):
|
||||||
|
continue
|
||||||
|
|
||||||
if not streaming.interactor == "user" and message_type in (
|
if not streaming.interactor == "user" and message_type in (
|
||||||
MessageType.keyboard.value,
|
MessageType.Keyboard.value,
|
||||||
MessageType.mouse.value,
|
MessageType.Mouse.value,
|
||||||
):
|
):
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"Blocking user message.", task=streaming.task, organization_id=streaming.organization_id
|
"Blocking user message.", task=streaming.task, organization_id=streaming.organization_id
|
||||||
|
|||||||
Reference in New Issue
Block a user