diff --git a/web/guac/consumers.py b/web/guac/consumers.py index d624fcfa736..28a0053417a 100644 --- a/web/guac/consumers.py +++ b/web/guac/consumers.py @@ -136,32 +136,34 @@ async def _close_websocket(self): 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) self.guac_task_id = session_data["task_id"] @@ -445,6 +447,37 @@ async def monitor_vm_status(self): 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 diff --git a/web/web/asgi.py b/web/web/asgi.py index 424d1d06397..f405f41a70e 100644 --- a/web/web/asgi.py +++ b/web/web/asgi.py @@ -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) @@ -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) ) ), } diff --git a/web/web/settings.py b/web/web/settings.py index 0f771c9487e..2dd4f8d5c3e 100644 --- a/web/web/settings.py +++ b/web/web/settings.py @@ -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") @@ -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 @@ -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 @@ -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",