mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-17 07:25:48 -07:00
Overhaul of Reverse Tunnel Code
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
"""Namespace package for reverse tunnel domain handlers (Engine side)."""
|
||||
|
||||
__all__ = ["remote_interactive_shell", "remote_management", "remote_video"]
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Placeholder Bash channel server (Engine side)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
|
||||
|
||||
class BashChannelServer:
|
||||
"""Stub Bash handler until the agent-side channel is implemented."""
|
||||
|
||||
protocol_name = "bash"
|
||||
|
||||
def __init__(self, bridge, service, *, channel_id: int = 1, frame_cls=None, close_frame_fn=None):
|
||||
self.bridge = bridge
|
||||
self.service = service
|
||||
self.channel_id = channel_id
|
||||
self.logger = service.logger.getChild(f"bash.{bridge.lease.tunnel_id}")
|
||||
self._open_sent = False
|
||||
self._ack_received = False
|
||||
self._closed = False
|
||||
self._output = deque()
|
||||
self._frame_cls = frame_cls
|
||||
self._close_frame_fn = close_frame_fn
|
||||
|
||||
def handle_agent_frame(self, frame) -> None:
|
||||
# No-op placeholder; output collection for future Bash support.
|
||||
try:
|
||||
if frame.msg_type == 0x04: # MSG_CHANNEL_ACK
|
||||
self._ack_received = True
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def open_channel(self, *, cols: int = 120, rows: int = 32) -> None:
|
||||
self._open_sent = True
|
||||
self.logger.info(
|
||||
"bash channel placeholder open sent tunnel_id=%s channel_id=%s cols=%s rows=%s",
|
||||
self.bridge.lease.tunnel_id,
|
||||
self.channel_id,
|
||||
cols,
|
||||
rows,
|
||||
)
|
||||
|
||||
def send_input(self, data: str) -> None:
|
||||
# Placeholder: no agent-side Bash yet.
|
||||
self.logger.info("bash placeholder send_input ignored tunnel_id=%s", self.bridge.lease.tunnel_id)
|
||||
|
||||
def send_resize(self, cols: int, rows: int) -> None:
|
||||
# Placeholder: not implemented.
|
||||
return
|
||||
|
||||
def close(self, code: int = 6, reason: str = "operator_close") -> None:
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
if callable(self._close_frame_fn):
|
||||
try:
|
||||
frame = self._close_frame_fn(self.channel_id, code, reason)
|
||||
self.bridge.operator_to_agent(frame)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def drain_output(self):
|
||||
items = []
|
||||
while self._output:
|
||||
items.append(self._output.popleft())
|
||||
return items
|
||||
|
||||
def status(self):
|
||||
return {
|
||||
"channel_id": self.channel_id,
|
||||
"open_sent": self._open_sent,
|
||||
"ack": self._ack_received,
|
||||
"closed": self._closed,
|
||||
"close_reason": None,
|
||||
"close_code": None,
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["BashChannelServer"]
|
||||
@@ -0,0 +1,139 @@
|
||||
"""Engine-side PowerShell tunnel channel helper (remote interactive shell domain)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections import deque
|
||||
from typing import Any, Deque, Dict, List, Optional
|
||||
|
||||
# Mirror framing constants to avoid circular imports.
|
||||
MSG_CHANNEL_OPEN = 0x03
|
||||
MSG_CHANNEL_ACK = 0x04
|
||||
MSG_DATA = 0x05
|
||||
MSG_CONTROL = 0x09
|
||||
MSG_CLOSE = 0x08
|
||||
CLOSE_OK = 0
|
||||
CLOSE_PROTOCOL_ERROR = 3
|
||||
CLOSE_AGENT_SHUTDOWN = 6
|
||||
|
||||
|
||||
class PowershellChannelServer:
|
||||
"""Coordinate PowerShell channel frames over a TunnelBridge."""
|
||||
|
||||
def __init__(self, bridge, service, *, channel_id: int = 1, frame_cls=None, close_frame_fn=None):
|
||||
self.bridge = bridge
|
||||
self.service = service
|
||||
self.channel_id = channel_id
|
||||
self.logger = service.logger.getChild(f"ps.{bridge.lease.tunnel_id}")
|
||||
self._open_sent = False
|
||||
self._ack_received = False
|
||||
self._closed = False
|
||||
self._output: Deque[str] = deque()
|
||||
self._close_reason: Optional[str] = None
|
||||
self._close_code: Optional[int] = None
|
||||
self._frame_cls = frame_cls
|
||||
self._close_frame_fn = close_frame_fn
|
||||
|
||||
# ------------------------------------------------------------------ Agent frame handling
|
||||
def handle_agent_frame(self, frame) -> None:
|
||||
if frame.channel_id != self.channel_id:
|
||||
return
|
||||
if frame.msg_type == MSG_CHANNEL_ACK:
|
||||
self._ack_received = True
|
||||
self.logger.info("ps channel acked tunnel_id=%s", self.bridge.lease.tunnel_id)
|
||||
return
|
||||
if frame.msg_type == MSG_DATA:
|
||||
try:
|
||||
text = frame.payload.decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
text = ""
|
||||
if text:
|
||||
self._append_output(text)
|
||||
return
|
||||
if frame.msg_type == MSG_CLOSE:
|
||||
try:
|
||||
payload = json.loads(frame.payload.decode("utf-8"))
|
||||
except Exception:
|
||||
payload = {}
|
||||
self._closed = True
|
||||
self._close_code = payload.get("code") if isinstance(payload, dict) else None
|
||||
self._close_reason = payload.get("reason") if isinstance(payload, dict) else None
|
||||
self.logger.info(
|
||||
"ps channel closed tunnel_id=%s code=%s reason=%s",
|
||||
self.bridge.lease.tunnel_id,
|
||||
self._close_code,
|
||||
self._close_reason or "-",
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ Operator actions
|
||||
def open_channel(self, *, cols: int = 120, rows: int = 32) -> None:
|
||||
if self._open_sent:
|
||||
return
|
||||
payload = json.dumps(
|
||||
{"protocol": "ps", "metadata": {"cols": cols, "rows": rows}},
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
frame = self._frame_cls(msg_type=MSG_CHANNEL_OPEN, channel_id=self.channel_id, payload=payload)
|
||||
self.bridge.operator_to_agent(frame)
|
||||
self._open_sent = True
|
||||
self.logger.info(
|
||||
"ps channel open sent tunnel_id=%s channel_id=%s cols=%s rows=%s",
|
||||
self.bridge.lease.tunnel_id,
|
||||
self.channel_id,
|
||||
cols,
|
||||
rows,
|
||||
)
|
||||
|
||||
def send_input(self, data: str) -> None:
|
||||
if self._closed:
|
||||
return
|
||||
payload = data.encode("utf-8", errors="replace")
|
||||
frame = self._frame_cls(msg_type=MSG_DATA, channel_id=self.channel_id, payload=payload)
|
||||
self.bridge.operator_to_agent(frame)
|
||||
|
||||
def send_resize(self, cols: int, rows: int) -> None:
|
||||
if self._closed:
|
||||
return
|
||||
payload = json.dumps({"cols": cols, "rows": rows}, separators=(",", ":")).encode("utf-8")
|
||||
frame = self._frame_cls(msg_type=MSG_CONTROL, channel_id=self.channel_id, payload=payload)
|
||||
self.bridge.operator_to_agent(frame)
|
||||
|
||||
def close(self, code: int = CLOSE_AGENT_SHUTDOWN, reason: str = "operator_close") -> None:
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
if callable(self._close_frame_fn):
|
||||
frame = self._close_frame_fn(self.channel_id, code, reason)
|
||||
else:
|
||||
frame = self._frame_cls(
|
||||
msg_type=MSG_CLOSE,
|
||||
channel_id=self.channel_id,
|
||||
payload=json.dumps({"code": code, "reason": reason}, separators=(",", ":")).encode("utf-8"),
|
||||
)
|
||||
self.bridge.operator_to_agent(frame)
|
||||
|
||||
# ------------------------------------------------------------------ Output polling
|
||||
def drain_output(self) -> List[str]:
|
||||
items: List[str] = []
|
||||
while self._output:
|
||||
items.append(self._output.popleft())
|
||||
return items
|
||||
|
||||
def _append_output(self, text: str) -> None:
|
||||
self._output.append(text)
|
||||
# Cap buffer to avoid unbounded memory growth.
|
||||
while len(self._output) > 500:
|
||||
self._output.popleft()
|
||||
|
||||
# ------------------------------------------------------------------ Status helpers
|
||||
def status(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"channel_id": self.channel_id,
|
||||
"open_sent": self._open_sent,
|
||||
"ack": self._ack_received,
|
||||
"closed": self._closed,
|
||||
"close_reason": self._close_reason,
|
||||
"close_code": self._close_code,
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["PowershellChannelServer"]
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Protocol handlers for remote interactive shell tunnels (Engine side)."""
|
||||
|
||||
from .Powershell import PowershellChannelServer
|
||||
from .Bash import BashChannelServer
|
||||
|
||||
__all__ = ["PowershellChannelServer", "BashChannelServer"]
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Domain handlers for remote interactive shells (PowerShell/Bash)."""
|
||||
|
||||
__all__ = ["Protocols"]
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Placeholder SSH channel server (Engine side)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
|
||||
|
||||
class SSHChannelServer:
|
||||
protocol_name = "ssh"
|
||||
|
||||
def __init__(self, bridge, service, *, channel_id: int = 1, frame_cls=None, close_frame_fn=None):
|
||||
self.bridge = bridge
|
||||
self.service = service
|
||||
self.channel_id = channel_id
|
||||
self.logger = service.logger.getChild(f"ssh.{bridge.lease.tunnel_id}")
|
||||
self._open_sent = False
|
||||
self._ack_received = False
|
||||
self._closed = False
|
||||
self._output = deque()
|
||||
self._frame_cls = frame_cls
|
||||
self._close_frame_fn = close_frame_fn
|
||||
|
||||
def handle_agent_frame(self, frame) -> None:
|
||||
try:
|
||||
if frame.msg_type == 0x04: # MSG_CHANNEL_ACK
|
||||
self._ack_received = True
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def open_channel(self, *, cols: int = 120, rows: int = 32) -> None:
|
||||
self._open_sent = True
|
||||
self.logger.info(
|
||||
"ssh channel placeholder open sent tunnel_id=%s channel_id=%s cols=%s rows=%s",
|
||||
self.bridge.lease.tunnel_id,
|
||||
self.channel_id,
|
||||
cols,
|
||||
rows,
|
||||
)
|
||||
|
||||
def send_input(self, data: str) -> None:
|
||||
self.logger.info("ssh placeholder send_input ignored tunnel_id=%s", self.bridge.lease.tunnel_id)
|
||||
|
||||
def send_resize(self, cols: int, rows: int) -> None:
|
||||
return
|
||||
|
||||
def close(self, code: int = 6, reason: str = "operator_close") -> None:
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
if callable(self._close_frame_fn):
|
||||
try:
|
||||
frame = self._close_frame_fn(self.channel_id, code, reason)
|
||||
self.bridge.operator_to_agent(frame)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def drain_output(self):
|
||||
items = []
|
||||
while self._output:
|
||||
items.append(self._output.popleft())
|
||||
return items
|
||||
|
||||
def status(self):
|
||||
return {
|
||||
"channel_id": self.channel_id,
|
||||
"open_sent": self._open_sent,
|
||||
"ack": self._ack_received,
|
||||
"closed": self._closed,
|
||||
"close_reason": None,
|
||||
"close_code": None,
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["SSHChannelServer"]
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Placeholder WinRM channel server (Engine side)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
|
||||
|
||||
class WinRMChannelServer:
|
||||
protocol_name = "winrm"
|
||||
|
||||
def __init__(self, bridge, service, *, channel_id: int = 1, frame_cls=None, close_frame_fn=None):
|
||||
self.bridge = bridge
|
||||
self.service = service
|
||||
self.channel_id = channel_id
|
||||
self.logger = service.logger.getChild(f"winrm.{bridge.lease.tunnel_id}")
|
||||
self._open_sent = False
|
||||
self._ack_received = False
|
||||
self._closed = False
|
||||
self._output = deque()
|
||||
self._frame_cls = frame_cls
|
||||
self._close_frame_fn = close_frame_fn
|
||||
|
||||
def handle_agent_frame(self, frame) -> None:
|
||||
try:
|
||||
if frame.msg_type == 0x04: # MSG_CHANNEL_ACK
|
||||
self._ack_received = True
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def open_channel(self, *, cols: int = 120, rows: int = 32) -> None:
|
||||
self._open_sent = True
|
||||
self.logger.info(
|
||||
"winrm channel placeholder open sent tunnel_id=%s channel_id=%s cols=%s rows=%s",
|
||||
self.bridge.lease.tunnel_id,
|
||||
self.channel_id,
|
||||
cols,
|
||||
rows,
|
||||
)
|
||||
|
||||
def send_input(self, data: str) -> None:
|
||||
self.logger.info("winrm placeholder send_input ignored tunnel_id=%s", self.bridge.lease.tunnel_id)
|
||||
|
||||
def send_resize(self, cols: int, rows: int) -> None:
|
||||
return
|
||||
|
||||
def close(self, code: int = 6, reason: str = "operator_close") -> None:
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
if callable(self._close_frame_fn):
|
||||
try:
|
||||
frame = self._close_frame_fn(self.channel_id, code, reason)
|
||||
self.bridge.operator_to_agent(frame)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def drain_output(self):
|
||||
items = []
|
||||
while self._output:
|
||||
items.append(self._output.popleft())
|
||||
return items
|
||||
|
||||
def status(self):
|
||||
return {
|
||||
"channel_id": self.channel_id,
|
||||
"open_sent": self._open_sent,
|
||||
"ack": self._ack_received,
|
||||
"closed": self._closed,
|
||||
"close_reason": None,
|
||||
"close_code": None,
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["WinRMChannelServer"]
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Protocol handlers for remote management tunnels (Engine side)."""
|
||||
|
||||
from .SSH import SSHChannelServer
|
||||
from .WinRM import WinRMChannelServer
|
||||
|
||||
__all__ = ["SSHChannelServer", "WinRMChannelServer"]
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Domain handlers for remote management tunnels (SSH/WinRM)."""
|
||||
|
||||
__all__ = ["Protocols"]
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Placeholder RDP channel server (Engine side)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
|
||||
|
||||
class RDPChannelServer:
|
||||
protocol_name = "rdp"
|
||||
|
||||
def __init__(self, bridge, service, *, channel_id: int = 1, frame_cls=None, close_frame_fn=None):
|
||||
self.bridge = bridge
|
||||
self.service = service
|
||||
self.channel_id = channel_id
|
||||
self.logger = service.logger.getChild(f"rdp.{bridge.lease.tunnel_id}")
|
||||
self._open_sent = False
|
||||
self._ack_received = False
|
||||
self._closed = False
|
||||
self._output = deque()
|
||||
self._frame_cls = frame_cls
|
||||
self._close_frame_fn = close_frame_fn
|
||||
|
||||
def handle_agent_frame(self, frame) -> None:
|
||||
try:
|
||||
if frame.msg_type == 0x04: # MSG_CHANNEL_ACK
|
||||
self._ack_received = True
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def open_channel(self, *, cols: int = 120, rows: int = 32) -> None:
|
||||
self._open_sent = True
|
||||
self.logger.info(
|
||||
"rdp channel placeholder open sent tunnel_id=%s channel_id=%s cols=%s rows=%s",
|
||||
self.bridge.lease.tunnel_id,
|
||||
self.channel_id,
|
||||
cols,
|
||||
rows,
|
||||
)
|
||||
|
||||
def send_input(self, data: str) -> None:
|
||||
self.logger.info("rdp placeholder send_input ignored tunnel_id=%s", self.bridge.lease.tunnel_id)
|
||||
|
||||
def send_resize(self, cols: int, rows: int) -> None:
|
||||
return
|
||||
|
||||
def close(self, code: int = 6, reason: str = "operator_close") -> None:
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
if callable(self._close_frame_fn):
|
||||
try:
|
||||
frame = self._close_frame_fn(self.channel_id, code, reason)
|
||||
self.bridge.operator_to_agent(frame)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def drain_output(self):
|
||||
items = []
|
||||
while self._output:
|
||||
items.append(self._output.popleft())
|
||||
return items
|
||||
|
||||
def status(self):
|
||||
return {
|
||||
"channel_id": self.channel_id,
|
||||
"open_sent": self._open_sent,
|
||||
"ack": self._ack_received,
|
||||
"closed": self._closed,
|
||||
"close_reason": None,
|
||||
"close_code": None,
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["RDPChannelServer"]
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Placeholder VNC channel server (Engine side)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
|
||||
|
||||
class VNCChannelServer:
|
||||
protocol_name = "vnc"
|
||||
|
||||
def __init__(self, bridge, service, *, channel_id: int = 1, frame_cls=None, close_frame_fn=None):
|
||||
self.bridge = bridge
|
||||
self.service = service
|
||||
self.channel_id = channel_id
|
||||
self.logger = service.logger.getChild(f"vnc.{bridge.lease.tunnel_id}")
|
||||
self._open_sent = False
|
||||
self._ack_received = False
|
||||
self._closed = False
|
||||
self._output = deque()
|
||||
self._frame_cls = frame_cls
|
||||
self._close_frame_fn = close_frame_fn
|
||||
|
||||
def handle_agent_frame(self, frame) -> None:
|
||||
try:
|
||||
if frame.msg_type == 0x04: # MSG_CHANNEL_ACK
|
||||
self._ack_received = True
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def open_channel(self, *, cols: int = 120, rows: int = 32) -> None:
|
||||
self._open_sent = True
|
||||
self.logger.info(
|
||||
"vnc channel placeholder open sent tunnel_id=%s channel_id=%s cols=%s rows=%s",
|
||||
self.bridge.lease.tunnel_id,
|
||||
self.channel_id,
|
||||
cols,
|
||||
rows,
|
||||
)
|
||||
|
||||
def send_input(self, data: str) -> None:
|
||||
self.logger.info("vnc placeholder send_input ignored tunnel_id=%s", self.bridge.lease.tunnel_id)
|
||||
|
||||
def send_resize(self, cols: int, rows: int) -> None:
|
||||
return
|
||||
|
||||
def close(self, code: int = 6, reason: str = "operator_close") -> None:
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
if callable(self._close_frame_fn):
|
||||
try:
|
||||
frame = self._close_frame_fn(self.channel_id, code, reason)
|
||||
self.bridge.operator_to_agent(frame)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def drain_output(self):
|
||||
items = []
|
||||
while self._output:
|
||||
items.append(self._output.popleft())
|
||||
return items
|
||||
|
||||
def status(self):
|
||||
return {
|
||||
"channel_id": self.channel_id,
|
||||
"open_sent": self._open_sent,
|
||||
"ack": self._ack_received,
|
||||
"closed": self._closed,
|
||||
"close_reason": None,
|
||||
"close_code": None,
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["VNCChannelServer"]
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Placeholder WebRTC channel server (Engine side)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
|
||||
|
||||
class WebRTCChannelServer:
|
||||
protocol_name = "webrtc"
|
||||
|
||||
def __init__(self, bridge, service, *, channel_id: int = 1, frame_cls=None, close_frame_fn=None):
|
||||
self.bridge = bridge
|
||||
self.service = service
|
||||
self.channel_id = channel_id
|
||||
self.logger = service.logger.getChild(f"webrtc.{bridge.lease.tunnel_id}")
|
||||
self._open_sent = False
|
||||
self._ack_received = False
|
||||
self._closed = False
|
||||
self._output = deque()
|
||||
self._frame_cls = frame_cls
|
||||
self._close_frame_fn = close_frame_fn
|
||||
|
||||
def handle_agent_frame(self, frame) -> None:
|
||||
try:
|
||||
if frame.msg_type == 0x04: # MSG_CHANNEL_ACK
|
||||
self._ack_received = True
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def open_channel(self, *, cols: int = 120, rows: int = 32) -> None:
|
||||
self._open_sent = True
|
||||
self.logger.info(
|
||||
"webrtc channel placeholder open sent tunnel_id=%s channel_id=%s cols=%s rows=%s",
|
||||
self.bridge.lease.tunnel_id,
|
||||
self.channel_id,
|
||||
cols,
|
||||
rows,
|
||||
)
|
||||
|
||||
def send_input(self, data: str) -> None:
|
||||
self.logger.info("webrtc placeholder send_input ignored tunnel_id=%s", self.bridge.lease.tunnel_id)
|
||||
|
||||
def send_resize(self, cols: int, rows: int) -> None:
|
||||
return
|
||||
|
||||
def close(self, code: int = 6, reason: str = "operator_close") -> None:
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
if callable(self._close_frame_fn):
|
||||
try:
|
||||
frame = self._close_frame_fn(self.channel_id, code, reason)
|
||||
self.bridge.operator_to_agent(frame)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def drain_output(self):
|
||||
items = []
|
||||
while self._output:
|
||||
items.append(self._output.popleft())
|
||||
return items
|
||||
|
||||
def status(self):
|
||||
return {
|
||||
"channel_id": self.channel_id,
|
||||
"open_sent": self._open_sent,
|
||||
"ack": self._ack_received,
|
||||
"closed": self._closed,
|
||||
"close_reason": None,
|
||||
"close_code": None,
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["WebRTCChannelServer"]
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Protocol handlers for remote video tunnels (Engine side)."""
|
||||
|
||||
from .WebRTC import WebRTCChannelServer
|
||||
from .RDP import RDPChannelServer
|
||||
from .VNC import VNCChannelServer
|
||||
|
||||
__all__ = ["WebRTCChannelServer", "RDPChannelServer", "VNCChannelServer"]
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Domain handlers for remote video/desktop tunnels (RDP/VNC/WebRTC)."""
|
||||
|
||||
__all__ = ["Protocols"]
|
||||
Reference in New Issue
Block a user