Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 59 additions & 26 deletions web/guac/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,34 +136,36 @@
raise

async def connect(self):
"""Validate session token, look up VNC server-side, connect to guacd."""
try:
# 1. Read and validate the session cookie
cookies = self.scope.get("cookies", {})
token_str = cookies.get("guac_session")

if not token_str:
logger.warning("WebSocket rejected: no guac_session cookie")
await self.close()
return

try:
token = uuid.UUID(token_str)
except ValueError:
logger.warning("WebSocket rejected: invalid token format")
await self.close()
return

# 2. Look up session in DB
db = Database()
session_data = await sync_to_async(db.get_guac_session)(token)

if not session_data:
logger.warning("WebSocket rejected: token not found in DB")
await self.close()
return
"""
Initiate the GuacamoleClient and create a connection to it.
"""
guacd_hostname = web_cfg.guacamole.guacd_host or "localhost"
guacd_port = int(web_cfg.guacamole.guacd_port) or 4822
guacd_recording_path = web_cfg.guacamole.guacd_recording_path or ""
guest_protocol = web_cfg.guacamole.guest_protocol or "vnc"
guest_width = int(web_cfg.guacamole.guest_width) or 1280
guest_height = int(web_cfg.guacamole.guest_height) or 1024
guest_username = web_cfg.guacamole.username or ""
guest_password = web_cfg.guacamole.password or ""

params = urllib.parse.parse_qs(self.scope["query_string"].decode())

if "rdp" in guest_protocol:
hosts = params.get("guest_ip", "")
guest_host = hosts[0]
guest_port = int(web_cfg.guacamole.guest_rdp_port) or 3389
ignore_cert = "true" if web_cfg.guacamole.ignore_rdp_cert is True else "false"
else:
guest_host = web_cfg.guacamole.vnc_host or "localhost"
ports = params.get("vncport", ["5900"])
guest_port = int(ports[0])
ignore_cert = "false"

guacd_recording_name = params.get("recording_name", ["task-recording"])[0]

self.client = GuacamoleClient(guacd_hostname, guacd_port)

self.guac_token = str(token)

Check failure on line 168 in web/guac/consumers.py

View workflow job for this annotation

GitHub Actions / test (3.10)

Ruff

web/guac/consumers.py:168:1: SyntaxError: Unexpected indentation
self.guac_task_id = session_data["task_id"]
self.vm_label = session_data["vm_label"]
vm_label = self.vm_label
Expand Down Expand Up @@ -393,12 +395,12 @@
self.is_closing = True
await self._close_websocket()

except Exception as e:

Check failure on line 398 in web/guac/consumers.py

View workflow job for this annotation

GitHub Actions / test (3.10)

Ruff

web/guac/consumers.py:398:26: SyntaxError: Expected a statement

Check failure on line 398 in web/guac/consumers.py

View workflow job for this annotation

GitHub Actions / test (3.10)

Ruff

web/guac/consumers.py:398:9: SyntaxError: Expected a statement
logger.error("Error during Guacamole connect: %s", str(e))

Check failure on line 399 in web/guac/consumers.py

View workflow job for this annotation

GitHub Actions / test (3.10)

Ruff

web/guac/consumers.py:399:1: SyntaxError: Unexpected indentation

Check failure on line 399 in web/guac/consumers.py

View workflow job for this annotation

GitHub Actions / test (3.10)

Ruff

web/guac/consumers.py:398:31: SyntaxError: Expected an expression
self.is_closing = True
await self._close_websocket()

async def monitor_task_status(self):

Check failure on line 403 in web/guac/consumers.py

View workflow job for this annotation

GitHub Actions / test (3.10)

Ruff

web/guac/consumers.py:403:5: SyntaxError: Expected a statement
"""Periodically check if the CAPE task can still host the session."""
try:
while True:
Expand Down Expand Up @@ -445,6 +447,37 @@
logger.error("Error in VM monitor: %s", e)

async def disconnect(self, code):
"""
Close the GuacamoleClient connection on WebSocket disconnect.
"""
if self.task:
self.task.cancel()
if self.client:
await sync_to_async(self.client.close)()

async def receive(self, text_data=None, bytes_data=None):
"""
Handle data received in the WebSocket, send to GuacamoleClient.
"""
if text_data is not None:
# logger.debug("To server: %s", text_data)
await sync_to_async(self.client.send)(text_data)

async def open(self):
"""
Receive data from GuacamoleClient and pass it to the WebSocket
"""
try:
while True:
content = await sync_to_async(self.client.receive)()
if content:
# logger.debug("From server: %s", content)
await self.send(text_data=content)
else:
break
except Exception:
# Connection lost
pass
"""Clean up on WebSocket disconnect."""
self.is_closing = True
self._disconnect_seen = True
Expand Down Expand Up @@ -549,3 +582,3 @@
finally:
if not self.is_closing:
await self._close_websocket()
28 changes: 12 additions & 16 deletions web/web/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
import os
from os.path import abspath, dirname, join

# --- 1. SETUP PATHS FIRST (Moved from bottom to top) ---
# Add / and /web (relative to CAPE/Cuckoo install location) to our path
# This ensures imports below can actually find the modules.
# --- 1. SETUP PATHS FIRST ---
current_dir = dirname(abspath(__file__)) # The directory this file is in
webdir = abspath(join(current_dir, "..")) # The parent directory (web)

Expand All @@ -16,36 +14,34 @@
os.chdir(webdir) # Change working directory

# --- 2. DJANGO SETUP ---
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "web.guac_settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "web.settings")

# Initialize Django ASGI application early to ensure the AppRegistry
# is populated before importing code that may import ORM models.
from django.core.asgi import get_asgi_application
django_asgi_app = get_asgi_application()

# --- 3. CHANNELS IMPORTS ---
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator # Import this
from channels.security.websocket import AllowedHostsOriginValidator

# Import local routing after Django is setup
import guac.routing
# Backend-agnostic websocket auth. channels' stock AuthMiddlewareStack resolves
# scope["user"] by load_backend()-ing the session's exact auth backend; for an
# OIDC/allauth session that imports allauth.account.*, which guac_settings does NOT
# install -> RuntimeError -> ws handshake 500 for every OIDC user (local ModelBackend
# sessions load fine, masking it). GuacAuthMiddlewareStack resolves the user straight
# from the session (id + auth-hash check) without loading the backend. See guac/channels_auth.py.
from guac.channels_auth import GuacAuthMiddlewareStack

websocket_patterns = guac.routing.websocket_urlpatterns

# --- 4. APPLICATION DEFINITION ---
application = ProtocolTypeRouter(
{
"http": django_asgi_app,
# Wrap the websocket router in AllowedHostsOriginValidator
# This prevents 403 Forbidden errors that confuse the Guacamole client
# Backend-agnostic websocket auth. channels' stock AuthMiddlewareStack resolves
# scope["user"] by load_backend()-ing the session's exact auth backend; for an
# OIDC/allauth session that imports allauth.account.*, which guac_settings does NOT
# install -> RuntimeError -> ws handshake 500 for every OIDC user (local ModelBackend
# sessions load fine, masking it). GuacAuthMiddlewareStack resolves the user straight
# from the session (id + auth-hash check) without loading the backend. See guac/channels_auth.py.
"websocket": AllowedHostsOriginValidator(
GuacAuthMiddlewareStack(
URLRouter(guac.routing.websocket_urlpatterns)
URLRouter(websocket_patterns)
)
),
}
Expand Down
15 changes: 15 additions & 0 deletions web/web/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
WEB_AUTHENTICATION = web_cfg.web_auth.get("enabled", False)
WEB_OAUTH = web_cfg.oauth
REMOTE_SESSION = web_cfg.guacamole.enabled
USE_ASYNC_MONGO = web_cfg.general.get("async_enabled", False)

# Get connection options from reporting.conf.
MONGO_HOST = cfg.mongodb.get("host", "127.0.0.1")
Expand Down Expand Up @@ -100,6 +101,7 @@
ADMIN = web_cfg.admin.enabled
ANON_VIEW = web_cfg.general.anon_viewable
ALLOW_DL_REPORTS_TO_ALL = web_cfg.general.reports_dl_allowed_to_all
REAL_TIME_UPDATES = web_cfg.general.get("real_time_updates", False)
NETWORK_PROC_MAP = pro_cfg.network.process_map

# If false run next command
Expand Down Expand Up @@ -283,6 +285,18 @@
"apikey.apps.ApiKeyConfig",
]

# Channels / ASGI
if REAL_TIME_UPDATES:
INSTALLED_APPS = ["daphne", "channels"] + INSTALLED_APPS
ASGI_APPLICATION = "web.asgi.application"
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [(web_cfg.channels.get("redis_host", "127.0.0.1"), web_cfg.channels.get("redis_port", 6379))],
},
},
}
# OpenID Connect (Okta / Azure AD / Auth0 / Google Workspace / Keycloak /
# any OIDC-compliant IdP) is wired through django-allauth's generic
# `openid_connect` provider — registered conditionally so the dependency
Expand Down Expand Up @@ -398,6 +412,7 @@ def _users_app_present() -> bool:
"WEB_AUTHENTICATION",
"WEB_OAUTH",
"ZIPPED_DOWNLOAD_ALL",
"REAL_TIME_UPDATES",
"NETWORK_PROC_MAP",
"REPROCESS_TASKS",
"REPROCESS_FAILED_PROCESSING",
Expand Down
Loading