diff --git a/conf/default/web.conf.default b/conf/default/web.conf.default index 280ba09ef05..c20e5f2e929 100644 --- a/conf/default/web.conf.default +++ b/conf/default/web.conf.default @@ -212,6 +212,12 @@ ignore_rdp_cert = false rdp_disable_wallpaper = yes rdp_disable_theming = yes rdp_enable_font_smoothing = no +# Idle timeout settings for interactive sessions +# idle_timeout_seconds: Maximum idle time before session is terminated +# Defaults to 0 (disabled) when omitted or set to an invalid value +idle_timeout_seconds = 0 +# activity_check_interval: How often to check for timeout in seconds when enabled +activity_check_interval = 30 rdp_enable_full_window_drag = no rdp_enable_desktop_composition = no rdp_enable_menu_animations = no diff --git a/lib/cuckoo/common/guac_utils.py b/lib/cuckoo/common/guac_utils.py new file mode 100644 index 00000000000..cff93af377c --- /dev/null +++ b/lib/cuckoo/common/guac_utils.py @@ -0,0 +1,15 @@ +"""Utilities for Guacamole protocol handling and activity detection.""" + +import re + +# Matches the opcode of a Guacamole instruction at message start or after ';'. +# Guacamole wire format: .,....; +# Example: 5.mouse,3.100,3.200,1.0; +_ACTIVITY_RE = re.compile(r"(?:^|;)\d+\.(key|mouse),") + + +def is_user_activity(message: str) -> bool: + """Return ``True`` if *message* contains a mouse or keyboard instruction.""" + if not message or not isinstance(message, str): + return False + return _ACTIVITY_RE.search(message) is not None diff --git a/tests/web/test_guac_consumers.py b/tests/web/test_guac_consumers.py new file mode 100644 index 00000000000..78de0ad917d --- /dev/null +++ b/tests/web/test_guac_consumers.py @@ -0,0 +1,397 @@ +import asyncio +import logging +from importlib import import_module +from types import SimpleNamespace + +import pytest +from channels.routing import URLRouter +from channels.testing import WebsocketCommunicator + +consumers = import_module("guac.consumers") +guac_routing = import_module("guac.routing") + +TEST_TOKEN = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" +TEST_VNC_PORT = 5901 + + +class FakeTask: + def __init__(self, task_id, status="running"): + self.id = task_id + self.status = status + + +class FakeDatabase: + """Minimal stand-in for Database with guac session and task helpers.""" + + def __init__(self, *, session_data=None, task=None): + self._session_data = session_data or { + "task_id": 123, + "vm_label": "win10_1", + "guest_ip": "192.168.56.10", + } + self._task = task or FakeTask(123, "running") + self.deleted_sessions = [] + + def get_guac_session(self, token): + if str(token) == TEST_TOKEN: + return dict(self._session_data) + return None + + def view_task(self, task_id): + if int(task_id) == self._task.id: + return self._task + return None + + def delete_guac_session(self, token): + self.deleted_sessions.append(str(token)) + + +class FakeGuacamoleClient: + instances = [] + + def __init__(self, host, port): + self.host = host + self.port = port + self.connected = False + self.handshake_kwargs = None + self.sent_messages = [] + self.closed = False + self.__class__.instances.append(self) + + def handshake(self, **kwargs): + self.handshake_kwargs = kwargs + self.connected = True + + def send(self, message): + self.sent_messages.append(message) + + def close(self): + self.closed = True + + +class FakeTimeoutManager: + instances = [] + + def __init__(self, vm_ip, user, session_id="unknown", task_id=None): + self.vm_ip = vm_ip + self.user = user + self.session_id = session_id + self.task_id = task_id + self.activity_updates = 0 + self.activity_check_interval = 60 + self.idle_timeout_seconds = 120 + self.is_active = True + self.__class__.instances.append(self) + + def update_activity(self): + self.activity_updates += 1 + + def set_inactive(self): + self.is_active = False + + def is_timed_out(self): + return False + + def get_idle_time_ms(self): + return 0 + + async def complete_analysis(self): + return True + + +class ExpiringFakeTimeoutManager(FakeTimeoutManager): + instances = [] + + def __init__(self, vm_ip, user, session_id="unknown", task_id=None): + super().__init__(vm_ip, user, session_id=session_id, task_id=task_id) + self.activity_check_interval = 0.01 + self.idle_timeout_seconds = 120 + self.complete_analysis_calls = 0 + + def is_timed_out(self): + return True + + def get_idle_time_ms(self): + return self.idle_timeout_seconds * 1000 + 1 + + async def complete_analysis(self): + self.complete_analysis_calls += 1 + return True + + +class DisabledTimeoutManager: + instances = [] + + def __init__(self, vm_ip, user, session_id="unknown", task_id=None): + self.vm_ip = vm_ip + self.user = user + self.session_id = session_id + self.task_id = task_id + self.activity_updates = 0 + self.activity_check_interval = None + self.idle_timeout_seconds = 0 + self.is_active = True + self.__class__.instances.append(self) + + def update_activity(self): + self.activity_updates += 1 + + def set_inactive(self): + self.is_active = False + + def is_timed_out(self): + return False + + def get_idle_time_ms(self): + return 0 + + async def complete_analysis(self): + return True + + +async def _background_task_stub(self): + await asyncio.Event().wait() + + +async def _read_guacd_tracking_stub(self): + await asyncio.Event().wait() + + +async def _cancel_then_close_read_guacd(self): + try: + await asyncio.Event().wait() + except asyncio.CancelledError: + pass + finally: + await self._close_websocket() + + +def _make_communicator(app, session_id, recording_name, token=TEST_TOKEN): + """Create a WebsocketCommunicator with the guac_session cookie injected.""" + url = f"/guac/websocket-tunnel/{session_id}/?recording_name={recording_name}" + communicator = WebsocketCommunicator(app, url, subprotocols=["guacamole"]) + communicator.scope["cookies"] = {"guac_session": token} + return communicator + + +@pytest.fixture +def guac_consumer_app_factory(monkeypatch): + def _build(*, timeout_manager_cls=None, stub_monitor_timeout=True, + read_guacd_impl=_background_task_stub, fake_db=None): + FakeGuacamoleClient.instances.clear() + FakeTimeoutManager.instances.clear() + ExpiringFakeTimeoutManager.instances.clear() + DisabledTimeoutManager.instances.clear() + + timeout_manager_cls = timeout_manager_cls or FakeTimeoutManager + db = fake_db or FakeDatabase() + + monkeypatch.setattr(consumers, "GuacamoleClient", FakeGuacamoleClient) + monkeypatch.setattr(consumers, "SessionTimeoutManager", timeout_manager_cls) + monkeypatch.setattr(consumers, "Database", lambda: db) + monkeypatch.setattr(consumers, "_get_vnc_port", lambda vm_label: TEST_VNC_PORT) + monkeypatch.setattr(consumers.GuacamoleWebSocketConsumer, "read_guacd", read_guacd_impl) + monkeypatch.setattr(consumers.GuacamoleWebSocketConsumer, "monitor_task_status", _background_task_stub) + if stub_monitor_timeout: + monkeypatch.setattr(consumers.GuacamoleWebSocketConsumer, "monitor_timeout", _background_task_stub) + monkeypatch.setattr( + consumers, + "web_cfg", + SimpleNamespace( + guacamole=SimpleNamespace( + guacd_host="localhost", + guacd_port=4822, + guacd_recording_path="/tmp/guacrecordings", + guest_protocol="vnc", + guest_width=1280, + guest_height=1024, + username="", + password="", + vnc_host="localhost", + vnc_color_depth=16, + vnc_cursor="local", + ) + ), + ) + + return URLRouter(guac_routing.websocket_urlpatterns), db + + return _build + + +@pytest.mark.asyncio +class TestGuacConsumers: + """Integration-style tests for the Guacamole websocket consumer.""" + + async def test_consumer_updates_idle_activity_for_real_guacamole_input(self, guac_consumer_app_factory): + guac_consumer_app, fake_db = guac_consumer_app_factory() + communicator = _make_communicator( + guac_consumer_app, "session123", "123_session123", + ) + timeout_manager = None + client = None + + try: + connected, subprotocol = await communicator.connect() + assert connected is True + assert subprotocol == "guacamole" + + assert len(FakeGuacamoleClient.instances) == 1 + client = FakeGuacamoleClient.instances[0] + assert client.handshake_kwargs["hostname"] == "localhost" + assert client.handshake_kwargs["port"] == TEST_VNC_PORT + assert client.handshake_kwargs["recording_name"] == "123_session123" + + assert len(FakeTimeoutManager.instances) == 1 + timeout_manager = FakeTimeoutManager.instances[0] + assert timeout_manager.vm_ip == "192.168.56.10" + assert timeout_manager.session_id == TEST_TOKEN + assert timeout_manager.task_id == "123" + + await communicator.send_to(text_data="4.size,4.1280,4.1024;") + await communicator.send_to(text_data="5.mouse,3.100,3.200,1.0;") + await communicator.send_to(text_data="3.key,2.65,1.1;") + await asyncio.sleep(0.05) + + assert timeout_manager.activity_updates == 2 + assert client.sent_messages == [ + "4.size,4.1280,4.1024;", + "5.mouse,3.100,3.200,1.0;", + "3.key,2.65,1.1;", + ] + finally: + await communicator.disconnect() + + assert timeout_manager.is_active is False + assert client.closed is True + + async def test_consumer_accepts_pending_task(self, guac_consumer_app_factory): + fake_db = FakeDatabase(task=FakeTask(123, "pending")) + guac_consumer_app, fake_db = guac_consumer_app_factory(fake_db=fake_db) + communicator = _make_communicator( + guac_consumer_app, "session_pending", "123_session_pending", + ) + + try: + connected, subprotocol = await communicator.connect() + assert connected is True + assert subprotocol == "guacamole" + assert len(FakeGuacamoleClient.instances) == 1 + assert fake_db.deleted_sessions == [] + finally: + await communicator.disconnect() + + async def test_consumer_timeout_completes_analysis_and_closes_session(self, guac_consumer_app_factory, caplog): + guac_consumer_app, fake_db = guac_consumer_app_factory( + timeout_manager_cls=ExpiringFakeTimeoutManager, + stub_monitor_timeout=False, + ) + caplog.set_level(logging.INFO, logger="guac-session") + communicator = _make_communicator( + guac_consumer_app, "session_timeout", "124_session_timeout", + ) + + connected, subprotocol = await communicator.connect() + assert connected is True + assert subprotocol == "guacamole" + + assert len(FakeGuacamoleClient.instances) == 1 + client = FakeGuacamoleClient.instances[0] + + assert len(ExpiringFakeTimeoutManager.instances) == 1 + timeout_manager = ExpiringFakeTimeoutManager.instances[0] + assert timeout_manager.task_id == "123" + + timeout_message = await asyncio.wait_for(communicator.receive_from(), timeout=1) + assert timeout_message == "5.error,35.Session timed out due to inactivity,3.522;" + + close_event = await asyncio.wait_for(communicator.receive_output(), timeout=1) + assert close_event["type"] == "websocket.close" + + await communicator.disconnect() + await asyncio.wait_for(communicator.wait(), timeout=1) + + assert timeout_manager.complete_analysis_calls == 1 + assert timeout_manager.is_active is False + assert client.closed is True + assert "idle for 120001ms (threshold: 120s)" in caplog.text + + async def test_consumer_disconnect_cancels_reader_without_double_close(self, guac_consumer_app_factory): + guac_consumer_app, fake_db = guac_consumer_app_factory(read_guacd_impl=_cancel_then_close_read_guacd) + communicator = _make_communicator( + guac_consumer_app, "session_disconnect", "125_session_disconnect", + ) + + connected, subprotocol = await communicator.connect() + assert connected is True + assert subprotocol == "guacamole" + + assert len(FakeGuacamoleClient.instances) == 1 + client = FakeGuacamoleClient.instances[0] + + assert len(FakeTimeoutManager.instances) == 1 + timeout_manager = FakeTimeoutManager.instances[0] + assert timeout_manager.task_id == "123" + + await communicator.disconnect() + await asyncio.wait_for(communicator.wait(), timeout=1) + + assert timeout_manager.is_active is False + assert client.closed is True + + async def test_consumer_skips_timeout_monitor_when_idle_timeout_disabled(self, guac_consumer_app_factory, monkeypatch): + scheduled_coroutines = [] + real_create_task = asyncio.create_task + + def tracking_create_task(coro): + scheduled_coroutines.append(coro.cr_code.co_name) + return real_create_task(coro) + + monkeypatch.setattr(consumers.asyncio, "create_task", tracking_create_task) + + guac_consumer_app, fake_db = guac_consumer_app_factory( + timeout_manager_cls=DisabledTimeoutManager, + stub_monitor_timeout=False, + read_guacd_impl=_read_guacd_tracking_stub, + ) + communicator = _make_communicator( + guac_consumer_app, "session_no_timeout", "126_session_no_timeout", + ) + + connected, subprotocol = await communicator.connect() + assert connected is True + assert subprotocol == "guacamole" + + await communicator.send_to(text_data="5.mouse,3.100,3.200,1.0;") + await asyncio.sleep(0.05) + + assert len(DisabledTimeoutManager.instances) == 1 + assert DisabledTimeoutManager.instances[0].task_id == "123" + assert DisabledTimeoutManager.instances[0].idle_timeout_seconds == 0 + assert DisabledTimeoutManager.instances[0].activity_check_interval is None + assert "_read_guacd_tracking_stub" in scheduled_coroutines + assert "monitor_timeout" not in scheduled_coroutines + + client = FakeGuacamoleClient.instances[0] + assert client.sent_messages == ["5.mouse,3.100,3.200,1.0;"] + + await communicator.disconnect() + await asyncio.wait_for(communicator.wait(), timeout=1) + + async def test_consumer_rejects_connection_without_cookie(self, guac_consumer_app_factory): + guac_consumer_app, fake_db = guac_consumer_app_factory() + url = "/guac/websocket-tunnel/session_nocookie/?recording_name=test" + communicator = WebsocketCommunicator(guac_consumer_app, url, subprotocols=["guacamole"]) + + connected, _ = await communicator.connect() + assert connected is False + + async def test_consumer_rejects_connection_with_unknown_token(self, guac_consumer_app_factory): + guac_consumer_app, fake_db = guac_consumer_app_factory() + communicator = _make_communicator( + guac_consumer_app, "unk_session", "test", + token="00000000-0000-0000-0000-000000000000", + ) + + connected, _ = await communicator.connect() + assert connected is False diff --git a/tests/web/test_guac_timeout_manager.py b/tests/web/test_guac_timeout_manager.py new file mode 100644 index 00000000000..0741ce04f40 --- /dev/null +++ b/tests/web/test_guac_timeout_manager.py @@ -0,0 +1,110 @@ +import asyncio +import hashlib +import logging +from importlib import import_module +from types import SimpleNamespace + +timeout_manager_module = import_module("guac.timeout_manager") + + +class TestSessionTimeoutManager: + def test_idle_timeout_defaults_to_zero_when_not_configured(self, monkeypatch): + monkeypatch.setattr(timeout_manager_module, "web_cfg", SimpleNamespace()) + manager = timeout_manager_module.SessionTimeoutManager("192.168.56.20", "tester") + assert manager.idle_timeout_seconds == 0 + assert manager.activity_check_interval is None + manager.last_activity = 0 + assert manager.is_timed_out() is False + + def test_idle_timeout_zero_disables_timeout_checks(self, monkeypatch): + monkeypatch.setattr( + timeout_manager_module, + "web_cfg", + SimpleNamespace(guacamole=SimpleNamespace(idle_timeout_seconds=0, activity_check_interval=1)), + ) + manager = timeout_manager_module.SessionTimeoutManager("192.168.56.21", "tester") + assert manager.idle_timeout_seconds == 0 + assert manager.activity_check_interval is None + manager.last_activity = 0 + assert manager.is_timed_out() is False + + def test_complete_analysis_creates_signal_folder(self, monkeypatch): + """Signal folder is created on the guest when task_id is available.""" + manager = timeout_manager_module.SessionTimeoutManager("192.168.56.22", "tester", task_id="321") + expected_folder = hashlib.md5("cape-321".encode()).hexdigest() + requested = {"mkdir": None} + + async def fake_get_json(vm_ip, path): + if path == "/environ": + return {"environ": {"TMP": "/tmp/cape"}} + if path == "/system": + return {"system": "Linux"} + raise AssertionError(f"Unexpected path: {path}") + + async def fake_post_form(vm_ip, path, data): + assert path == "/mkdir" + requested["mkdir"] = data["dirpath"] + return 200 + + monkeypatch.setattr(timeout_manager_module, "_agent_get_json", fake_get_json) + monkeypatch.setattr(timeout_manager_module, "_agent_post_form", fake_post_form) + assert asyncio.run(manager.complete_analysis()) is True + assert requested["mkdir"] == f"/tmp/cape/{expected_folder}" + + def test_complete_analysis_windows_path(self, monkeypatch): + """Signal folder uses backslash on Windows guests.""" + manager = timeout_manager_module.SessionTimeoutManager("192.168.56.23", "tester", task_id="654") + expected_folder = hashlib.md5("cape-654".encode()).hexdigest() + requested = {"mkdir": None} + + async def fake_get_json(vm_ip, path): + if path == "/environ": + return {"environ": {"TMP": "C:\\Temp"}} + if path == "/system": + return {"system": "Windows"} + raise AssertionError(f"Unexpected path: {path}") + + async def fake_post_form(vm_ip, path, data): + assert path == "/mkdir" + requested["mkdir"] = data["dirpath"] + assert "\\" in data["dirpath"] + return 200 + + monkeypatch.setattr(timeout_manager_module, "_agent_get_json", fake_get_json) + monkeypatch.setattr(timeout_manager_module, "_agent_post_form", fake_post_form) + assert asyncio.run(manager.complete_analysis()) is True + assert requested["mkdir"] == f"C:\\Temp\\{expected_folder}" + + def test_complete_analysis_returns_false_without_task_id(self, monkeypatch, caplog): + """Without a task_id, complete_analysis should fail gracefully.""" + manager = timeout_manager_module.SessionTimeoutManager("192.168.56.24", "tester") + caplog.set_level(logging.ERROR, logger="guac-session") + assert asyncio.run(manager.complete_analysis()) is False + assert "No task ID" in caplog.text + + def test_complete_analysis_returns_false_without_vm_ip(self, monkeypatch, caplog): + """Without a valid VM IP, complete_analysis should fail gracefully.""" + manager = timeout_manager_module.SessionTimeoutManager("unknown", "tester", task_id="999") + caplog.set_level(logging.ERROR, logger="guac-session") + assert asyncio.run(manager.complete_analysis()) is False + assert "No valid VM IP" in caplog.text + + def test_complete_analysis_returns_false_on_http_error(self, monkeypatch, caplog): + """Non-200 response from agent returns False.""" + manager = timeout_manager_module.SessionTimeoutManager("192.168.56.25", "tester", task_id="888") + caplog.set_level(logging.WARNING, logger="guac-session") + + async def fake_get_json(vm_ip, path): + if path == "/environ": + return {"environ": {"TMP": "/tmp"}} + if path == "/system": + return {"system": "Linux"} + return {} + + async def fake_post_form(vm_ip, path, data): + return 500 + + monkeypatch.setattr(timeout_manager_module, "_agent_get_json", fake_get_json) + monkeypatch.setattr(timeout_manager_module, "_agent_post_form", fake_post_form) + assert asyncio.run(manager.complete_analysis()) is False + assert "HTTP 500" in caplog.text diff --git a/tests/web/test_guacamole_activity_detection.py b/tests/web/test_guacamole_activity_detection.py new file mode 100644 index 00000000000..497cd0d986f --- /dev/null +++ b/tests/web/test_guacamole_activity_detection.py @@ -0,0 +1,92 @@ +""" +Tests for Guacamole activity detection logic. +Only mouse and keyboard events constitute user activity. +""" +import pytest + +from lib.cuckoo.common.guac_utils import is_user_activity + + +class TestGuacamoleActivityDetection: + """Test Guacamole activity detection logic.""" + + @pytest.mark.parametrize( + "message,expected,description", + [ + # Active user interactions (should return True) + ("5.mouse,3.100,3.200,1.0;", True, "Mouse move"), + ("5.mouse,3.100,3.200,1.1;", True, "Mouse click"), + ("3.key,2.65,1.1;", True, "Keyboard input"), + # Passive or non-user-driven events (should return False) + ("4.size,4.1280,4.1024;", False, "Window resize"), + ("4.sync,3.123;", False, "Sync message"), + ("3.nop;", False, "No-op"), + ("3.ack,3.456,1.0,7.SUCCESS;", False, "Acknowledgment"), + ("4.blob,4.data;", False, "Blob data"), + ("5.touch,1.1,3.100,3.200,2.10,2.10,1.0,3.0.5;", False, "Touch input"), + ("9.clipboard,5.hello;", False, "Clipboard paste"), + # Edge cases + ("", False, "Empty message"), + ("invalid", False, "Invalid format"), + ("mouse.100,200,1;", False, "Legacy fake format"), + ("6.random,4.text;", False, "Unknown instruction"), + ], + ) + def test_activity_detection(self, message, expected, description): + """Test activity detection against real Guacamole protocol messages.""" + result = is_user_activity(message) + assert result == expected, f"Failed for {description}: '{message}' -> {result} (expected {expected})" + + def test_multiple_instructions_mixed(self): + """Test activity detection with multiple instructions in one message.""" + # Mixed active and passive instructions - should detect activity + message = "4.sync,3.123;5.mouse,3.100,3.200,1.1;3.nop;" + result = is_user_activity(message) + assert result is True, "Should detect activity when mixed with passive events" + # Only passive instructions - should not detect activity + message = "4.sync,3.123;4.size,4.1280,4.1024;3.nop;" + result = is_user_activity(message) + assert result is False, "Should not detect activity with only passive events" + + def test_malformed_messages_handled_gracefully(self): + """Test that malformed messages don't cause crashes.""" + malformed_messages = [ + "5.mouse", # Missing parameters and terminator (no comma after opcode) + None, # None input + 123, # Non-string input + ] + + for message in malformed_messages: + result = is_user_activity(message) + assert result is False, f"Should return False for malformed message: {message}" + + def test_truncated_activity_message_still_detected(self): + """A truncated mouse/key instruction is still user activity.""" + assert is_user_activity("5.mouse,3.100,3.200") is True + assert is_user_activity("3.key,2.65") is True + + def test_only_non_input_events_are_passive(self): + """Verify non-input protocol events do not reset the idle timeout.""" + passive_events = [ + "4.size,4.1280,4.1024;", + "4.size,4.1920,4.1080;", + "4.sync,3.456;", + "3.nop;", + ] + + for event in passive_events: + result = is_user_activity(event) + assert result is False, f"Event '{event}' should be passive" + + def test_input_events_are_active(self): + """Verify real user-input instructions are considered activity.""" + active_events = [ + "5.mouse,3.100,3.200,1.0;", # Mouse move + "5.mouse,2.50,2.50,1.1;", # Mouse click + "3.key,2.32,1.1;", # Key press + "3.key,2.32,1.0;", # Key release + ] + + for event in active_events: + result = is_user_activity(event) + assert result is True, f"Event '{event}' should be active" diff --git a/web/guac/consumers.py b/web/guac/consumers.py index 61706283cc1..99944ea9562 100644 --- a/web/guac/consumers.py +++ b/web/guac/consumers.py @@ -1,7 +1,8 @@ import asyncio import logging -import uuid +import re import urllib.parse +import uuid from xml.etree import ElementTree as ET from asgiref.sync import sync_to_async @@ -9,8 +10,11 @@ from guacamole.client import GuacamoleClient from lib.cuckoo.common.config import Config +from lib.cuckoo.common.guac_utils import is_user_activity from lib.cuckoo.core.database import Database +from .timeout_manager import SessionTimeoutManager + try: import libvirt LIBVIRT_AVAILABLE = True @@ -24,6 +28,7 @@ machinery_dsn = getattr(Config(machinery), machinery).get("dsn", "qemu:///system") TASK_POLL_INTERVAL = 10 +ACTIVE_GUAC_TASK_STATUSES = ("pending", "running") def _get_vnc_port(vm_label): @@ -68,6 +73,39 @@ def __init__(self, *args, **kwargs): self.monitor_task = None self.guac_token = None self.guac_task_id = None + self.is_closing = False + self.timeout_manager = None + self.timeout_task = None + self._disconnect_seen = False + self._close_sent = False + self._close_lock = asyncio.Lock() + + async def _delete_guac_session(self) -> None: + """Delete the current guac session from the DB and clear the token.""" + if not self.guac_token: + return + try: + db = Database() + await sync_to_async(db.delete_guac_session)(self.guac_token) + self.guac_token = None + except Exception as e: + logger.error("Failed to delete guac session %s: %s", self.guac_token, e) + + async def _close_websocket(self): + """Close the websocket at most once across all concurrent code paths.""" + async with self._close_lock: + if self._close_sent or self._disconnect_seen: + return + + self._close_sent = True + + try: + await self.close() + except RuntimeError as error: + if "Unexpected ASGI message 'websocket.close'" in str(error): + logger.debug("Suppressing duplicate websocket.close for session") + return + raise async def connect(self): """Validate session token, look up VNC server-side, connect to guacd.""" @@ -101,13 +139,13 @@ async def connect(self): self.guac_task_id = session_data["task_id"] vm_label = session_data["vm_label"] - # 3. Verify task is still running + # 3. Verify task can still host an interactive session task = await sync_to_async(db.view_task)(self.guac_task_id) - if not task or task.status != "running": + if not task or task.status not in ACTIVE_GUAC_TASK_STATUSES: logger.warning( - "WebSocket rejected: task %s is not running", self.guac_task_id + "WebSocket rejected: task %s is not active for guac", self.guac_task_id ) - await sync_to_async(db.delete_guac_session)(token) + await self._delete_guac_session() await self.close() return @@ -133,7 +171,6 @@ async def connect(self): query_string = self.scope.get("query_string", b"").decode() params = urllib.parse.parse_qs(query_string) # Sanitize recording name — only allow alphanumeric, dash, underscore - import re raw_recording = params.get("recording_name", ["task-recording"])[0] guacd_recording_name = re.sub(r"[^a-zA-Z0-9_-]", "", raw_recording) @@ -188,18 +225,37 @@ async def connect(self): self.guac_task_id, vm_label, ) + + # 7. Initialize timeout handling + try: + vm_ip = session_data.get("guest_ip") or guest_host + self.timeout_manager = SessionTimeoutManager( + vm_ip=vm_ip, + user="unknown_user", + session_id=self.guac_token, + task_id=str(self.guac_task_id), + ) + except Exception as e: + logger.error("Failed to initialize timeout manager: %s", e) + self.timeout_manager = None + + # 8. Start background tasks self.task = asyncio.create_task(self.read_guacd()) self.monitor_task = asyncio.create_task(self.monitor_task_status()) + if self.timeout_manager and self.timeout_manager.idle_timeout_seconds > 0: + self.timeout_task = asyncio.create_task(self.monitor_timeout()) else: logger.warning("Guacamole handshake failed.") - await self.close() + self.is_closing = True + await self._close_websocket() except Exception as e: logger.error("Error during Guacamole connect: %s", str(e)) - await self.close() + self.is_closing = True + await self._close_websocket() async def monitor_task_status(self): - """Periodically check if the CAPE task is still running. Disconnect if not.""" + """Periodically check if the CAPE task can still host the session.""" try: while True: await asyncio.sleep(TASK_POLL_INTERVAL) @@ -207,14 +263,13 @@ async def monitor_task_status(self): break db = Database() task = await sync_to_async(db.view_task)(self.guac_task_id) - if not task or task.status != "running": + if not task or task.status not in ACTIVE_GUAC_TASK_STATUSES: logger.info( "Task %s no longer running, disconnecting guac session", self.guac_task_id, ) - if self.guac_token: - await sync_to_async(db.delete_guac_session)(self.guac_token) - await self.close() + await self._delete_guac_session() + await self._close_websocket() break except asyncio.CancelledError: pass @@ -223,19 +278,16 @@ async def monitor_task_status(self): async def disconnect(self, code): """Clean up on WebSocket disconnect.""" - if self.monitor_task: - self.monitor_task.cancel() - try: - await self.monitor_task - except asyncio.CancelledError: - pass + self.is_closing = True + self._disconnect_seen = True - if self.task: - self.task.cancel() - try: - await self.task - except asyncio.CancelledError: - pass + if self.timeout_manager: + self.timeout_manager.set_inactive() + + tasks = [t for t in (self.monitor_task, self.task, self.timeout_task) if t] + for t in tasks: + t.cancel() + await asyncio.gather(*tasks, return_exceptions=True) if self.client: try: @@ -243,16 +295,14 @@ async def disconnect(self, code): except Exception as e: logger.error("Error closing guacamole client: %s", str(e)) - if self.guac_token: - try: - db = Database() - await sync_to_async(db.delete_guac_session)(self.guac_token) - except Exception: - pass + await self._delete_guac_session() async def receive(self, text_data=None, bytes_data=None): """Forward data from browser to guacd.""" if text_data and self.client: + if self.timeout_manager and is_user_activity(text_data): + self.timeout_manager.update_activity() + try: await sync_to_async(self.client.send)(text_data) except Exception as e: @@ -274,4 +324,60 @@ async def read_guacd(self): except Exception as e: logger.error("Exception in Guacamole message loop: %s", e) finally: - await self.close() + await self._close_websocket() + + async def monitor_timeout(self): + """Monitor session for idle timeout and handle cleanup when timeout occurs.""" + try: + while self.timeout_manager and self.timeout_manager.is_active and not self.is_closing: + await asyncio.sleep(self.timeout_manager.activity_check_interval) + + if not self.timeout_manager or not self.timeout_manager.is_active: + break + + if self.timeout_manager.is_timed_out(): + idle_time = self.timeout_manager.get_idle_time_ms() + logger.info( + "Session timeout detected for %s, idle for %sms (threshold: %ss)", + self.timeout_manager.session_id, + idle_time, + self.timeout_manager.idle_timeout_seconds, + ) + await self.handle_timeout() + break + else: + idle_time = self.timeout_manager.get_idle_time_ms() + logger.debug("Session %s idle for %sms", self.timeout_manager.session_id, idle_time) + + except asyncio.CancelledError: + logger.debug("Timeout monitor cancelled for session %s", getattr(self.timeout_manager, "session_id", "unknown")) + except Exception as e: + logger.error("Error in timeout monitor: %s", str(e)) + + async def handle_timeout(self): + """Handle session timeout by signalling analysis completion and closing the connection.""" + if not self.timeout_manager: + return + + try: + logger.info( + "Handling timeout for session %s, VM: %s", + self.timeout_manager.session_id, + self.timeout_manager.vm_ip, + ) + success = await self.timeout_manager.complete_analysis() + if success: + logger.info("Successfully signalled analysis complete for %s", self.timeout_manager.vm_ip) + else: + logger.warning("Failed to signal analysis complete for %s", self.timeout_manager.vm_ip) + + try: + await self.send(text_data="5.error,35.Session timed out due to inactivity,3.522;") + except Exception as e: + logger.warning("Could not send timeout message to client: %s", e) + + except Exception as e: + logger.error("Error handling session timeout: %s", e) + finally: + if not self.is_closing: + await self._close_websocket() diff --git a/web/guac/templates/guac/index.html b/web/guac/templates/guac/index.html index 68c1af2bea1..41a633d663b 100644 --- a/web/guac/templates/guac/index.html +++ b/web/guac/templates/guac/index.html @@ -1,51 +1,51 @@ -{% load static %} - - - - - - Guacamole Console · CAPE Sandbox - - - - - - - - - - - - -
-
- - CAPE Sandbox - - | - Task - #{{ task_id }} - -
-
-
- -
-
- - - +{% load static %} + + + + + + Guacamole Console · CAPE Sandbox + + + + + + + + + + + + +
+
+ + CAPE Sandbox + + | + Task + #{{ task_id }} + +
+
+
+ +
+
+ + + diff --git a/web/guac/timeout_manager.py b/web/guac/timeout_manager.py new file mode 100644 index 00000000000..cd6bde08355 --- /dev/null +++ b/web/guac/timeout_manager.py @@ -0,0 +1,180 @@ +""" +Timeout management for Guacamole interactive analysis sessions. +Tracks idle time and signals the CAPE analyzer to finish when the session +has been idle for longer than the configured threshold. +""" +import asyncio +import hashlib +import ipaddress +import logging +import ntpath +import posixpath +import time +from typing import Optional + +from lib.cuckoo.common.config import Config + +try: + import aiohttp + + HAS_AIOHTTP = True +except ImportError: + aiohttp = None + HAS_AIOHTTP = False + +logger = logging.getLogger("guac-session") +web_cfg = Config("web") +REQUEST_TIMEOUT_SECONDS = 10 + + +async def _agent_get_json(vm_ip: str, path: str) -> dict: + """GET JSON from the guest agent at *vm_ip*.""" + url = f"http://{vm_ip}:8000{path}" + if HAS_AIOHTTP: + timeout = aiohttp.ClientTimeout(total=REQUEST_TIMEOUT_SECONDS) + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.get(url) as resp: + resp.raise_for_status() + return await resp.json(content_type=None) + else: + import json + import urllib.request + + def _sync(): + with urllib.request.urlopen(url, timeout=REQUEST_TIMEOUT_SECONDS) as resp: + return json.loads(resp.read().decode("utf-8")) + + return await asyncio.to_thread(_sync) + + +async def _agent_post_form(vm_ip: str, path: str, data: dict) -> int: + """POST form data to the guest agent and return the HTTP status code.""" + url = f"http://{vm_ip}:8000{path}" + if HAS_AIOHTTP: + timeout = aiohttp.ClientTimeout(total=REQUEST_TIMEOUT_SECONDS) + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.post(url, data=data) as resp: + return resp.status + else: + import urllib.parse + import urllib.request + + def _sync(): + encoded = urllib.parse.urlencode(data).encode("utf-8") + req = urllib.request.Request(url, data=encoded, method="POST") + req.add_header("Content-Type", "application/x-www-form-urlencoded") + with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT_SECONDS) as resp: + return resp.getcode() + + return await asyncio.to_thread(_sync) + + +class SessionTimeoutManager: + """Tracks idle time for a Guacamole session and signals analysis completion.""" + + def __init__( + self, + vm_ip: str, + user: str, + session_id: str = "unknown", + task_id: Optional[str] = None, + ): + self.vm_ip = vm_ip or "unknown" + self.user = user or "unknown_user" + self.session_id = session_id or "unknown_session" + self.task_id = str(task_id) if task_id else None + self.last_activity = self._now_ms() + self.is_active = True + + try: + self.idle_timeout_seconds = max(int(getattr(web_cfg.guacamole, "idle_timeout_seconds", 0)), 0) + if self.idle_timeout_seconds > 0: + self.activity_check_interval = max(int(getattr(web_cfg.guacamole, "activity_check_interval", 30)), 1) + else: + self.activity_check_interval = None + except (AttributeError, TypeError, ValueError): + self.idle_timeout_seconds = 0 + self.activity_check_interval = None + + if self.idle_timeout_seconds > 0: + logger.info( + "Timeout manager created: %s@%s (task=%s, %sms timeout)", + self.user, + self.vm_ip, + self.task_id, + self.idle_timeout_seconds, + ) + else: + logger.info("Timeout manager created with idle timeout disabled for %s@%s", self.user, self.vm_ip) + + @staticmethod + def _now_ms() -> int: + return int(time.monotonic() * 1000) + + def update_activity(self) -> None: + self.last_activity = self._now_ms() + + def get_idle_time_ms(self) -> int: + return self._now_ms() - self.last_activity + + def is_timed_out(self) -> bool: + return self.idle_timeout_seconds > 0 and self.get_idle_time_ms() > (self.idle_timeout_seconds * 1000) + + def set_inactive(self) -> None: + self.is_active = False + + async def complete_analysis(self) -> bool: + """Create the signal folder on the guest to end the analysis. + This is the same mechanism used by the "End Session" button in the web UI + (see ``web/apiv2/views.py :: tasks_status``). Returns True on success. + """ + if not self.vm_ip or self.vm_ip == "unknown": + logger.error("No valid VM IP for session %s — cannot signal completion", self.session_id) + return False + try: + ipaddress.ip_address(self.vm_ip) + except ValueError: + logger.error("Invalid VM IP address %r for session %s — cannot signal completion", self.vm_ip, self.session_id) + return False + if not self.task_id: + logger.error("No task ID for session %s — cannot signal completion", self.session_id) + return False + try: + guest_env, guest_system = await asyncio.gather( + _agent_get_json(self.vm_ip, "/environ"), + _agent_get_json(self.vm_ip, "/system"), + ) + completion_folder = hashlib.md5(f"cape-{self.task_id}".encode()).hexdigest() + dest = self._build_folder_path(guest_env, guest_system, completion_folder) + logger.info( + "Creating completion folder for task %s on %s: %s", + self.task_id, + self.vm_ip, + dest, + ) + status_code = await _agent_post_form(self.vm_ip, "/mkdir", {"dirpath": dest}) + if status_code == 200: + logger.info("Completion folder created for task %s on %s (HTTP %s)", self.task_id, self.vm_ip, status_code) + return True + logger.warning( + "Completion folder request returned HTTP %s for task %s on %s", + status_code, + self.task_id, + self.vm_ip, + ) + return False + except Exception as exc: + logger.error("Failed to signal completion for task %s on %s: %s", self.task_id, self.vm_ip, exc) + return False + + @staticmethod + def _build_folder_path(guest_env: dict, guest_system: dict, folder_name: str) -> str: + environ = guest_env.get("environ", {}) + system_name = str(guest_system.get("system", "")).lower() + + if system_name == "windows": + temp = environ.get("TMP", "C:\\Temp") + return ntpath.join(temp, folder_name) + + temp = environ.get("TMP", "/tmp") + return posixpath.join(temp, folder_name) diff --git a/web/static/js/guac-main.js b/web/static/js/guac-main.js index f7d607beab4..ec708431fd8 100644 --- a/web/static/js/guac-main.js +++ b/web/static/js/guac-main.js @@ -1,29 +1,74 @@ -function GuacMe(element, session_id, recording_name) { - "use strict"; +"use strict"; + +const KEYSYM = { + SHIFT: 0xFFE1, + CTRL: 0xFFE3, + INSERT: 0xFF63, + V_UPPER: 0x0056, + V_LOWER: 0x0076, +}; + +const PASTE_COMPONENT_KEYS = new Set([ + KEYSYM.SHIFT, KEYSYM.CTRL, KEYSYM.INSERT, + KEYSYM.V_UPPER, KEYSYM.V_LOWER, +]); + +const PASTE_DELAY_MS = 50; + +const NON_FATAL_STATUS_CODES = new Set([0, 256]); + +class GuacSession { + constructor(element, config) { + this.config = config; + this.client = null; + this.tunnel = null; + this.display = null; + this.keyboard = null; + this.connected = false; + this.ctrl = false; + this.shift = false; + this.dialogContainer = $(element).find('.guaconsole')[0]; + + this._init(); + } + + _buildWsUrl() { + return location.origin.replace(/^http(s?):/, (match, p1) => + p1 ? 'wss:' : 'ws:' + ); + } + + _isPasteShortcut(keysym) { + return (this.ctrl && this.shift && keysym === KEYSYM.V_UPPER) + || (this.ctrl && keysym === KEYSYM.V_LOWER) + || (this.shift && keysym === KEYSYM.INSERT); + } + + _init() { + const wsUrl = this._buildWsUrl(); + this.tunnel = new Guacamole.WebSocketTunnel( + wsUrl + '/guac/websocket-tunnel/' + this.config.session_id + ); + this.client = new Guacamole.Client(this.tunnel); - var terminal_connected = false; - var terminal_client; - var terminal_element; - var dialog_container; + this.connect(); - var init = function() { - dialog_container = $(element).find('.guaconsole')[0]; + this.display = this.client.getDisplay().getElement(); + $('#terminal').append(this.display); - var terminal_ws_url = location.origin.replace(/^http(s?):/, function(match, p1) { - return (p1 ? 'wss:' : 'ws:'); - }); + this._setupScaling(); - terminal_client = new Guacamole.Client( - new Guacamole.WebSocketTunnel(terminal_ws_url + '/guac/websocket-tunnel/' + session_id) - ); - terminal_connect(recording_name); + window.onunload = () => this.disconnect(); - terminal_element = terminal_client.getDisplay().getElement(); - $('#terminal').append(terminal_element); + this._setupMouse(); + this._setupKeyboard(); + this._setupClipboard(); + this._setupErrorHandler(); + } - /* Scale display to fit the browser window. */ - var scaleDisplay = function() { - var display = terminal_client.getDisplay(); + _setupScaling() { + const scaleDisplay = () => { + var display = this.client.getDisplay(); var displayWidth = display.getWidth(); var displayHeight = display.getHeight(); if (!displayWidth || !displayHeight) return; @@ -40,139 +85,136 @@ function GuacMe(element, session_id, recording_name) { display.scale(scale); }; - /* Re-scale when the display size changes (initial connect). */ - terminal_client.getDisplay().onresize = function() { + this.client.getDisplay().onresize = function() { scaleDisplay(); }; - /* Re-scale on browser window resize (debounced). */ var resizeTimeout; window.addEventListener('resize', function() { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(scaleDisplay, 100); }); + } - /* Disconnect on tab close. */ - window.onunload = function() { - terminal_client.disconnect(); - }; + _setupMouse() { + const mouse = new Guacamole.Mouse(this.display); + const sendState = (state) => this.client.sendMouseState(state, true); + mouse.onmousedown = sendState; + mouse.onmouseup = sendState; + mouse.onmousemove = sendState; + } - /* Mouse handling */ - var mouse = new Guacamole.Mouse(terminal_element); + _setupKeyboard() { + this.keyboard = new Guacamole.Keyboard(this.display); - mouse.onmousedown = - mouse.onmouseup = - mouse.onmousemove = function(mouseState) { - terminal_client.sendMouseState(mouseState, true); - }; - - var keyboard = new Guacamole.Keyboard(terminal_element); - var ctrl, shift = false; - - keyboard.onkeydown = function (keysym) { - var cancel_event = true; - - if (keysym == 0xFFE1 || keysym == 0xFFE3 || keysym == 0xFF63 - || keysym == 0x0056 || keysym == 0x0076) { - cancel_event = false; - } + this.keyboard.onkeydown = (keysym) => { + if (keysym === KEYSYM.SHIFT) this.shift = true; + else if (keysym === KEYSYM.CTRL) this.ctrl = true; - if (keysym == 0xFFE1) { shift = true; } - else if (keysym == 0xFFE3) { ctrl = true; } - - if ((ctrl && shift && keysym == 0x0056) - || (ctrl && keysym == 0x0076) - || (shift && keysym == 0xFF63)) { - window.setTimeout(function() { - terminal_client.sendKeyEvent(1, keysym); - }, 50); + if (this._isPasteShortcut(keysym)) { + setTimeout(() => this.client.sendKeyEvent(1, keysym), PASTE_DELAY_MS); } else { - terminal_client.sendKeyEvent(1, keysym); + this.client.sendKeyEvent(1, keysym); } - return !cancel_event; + return !PASTE_COMPONENT_KEYS.has(keysym); }; - keyboard.onkeyup = function (keysym) { - if (keysym == 0xFFE1) { shift = false; } - else if (keysym == 0xFFE3) { ctrl = false; } + this.keyboard.onkeyup = (keysym) => { + if (keysym === KEYSYM.SHIFT) this.shift = false; + else if (keysym === KEYSYM.CTRL) this.ctrl = false; - if ((ctrl && shift && keysym == 0x0056) - || (ctrl && keysym == 0x0076) - || (shift && keysym == 0xFF63)) { - window.setTimeout(function() { - terminal_client.sendKeyEvent(0, keysym); - }, 50); + if (this._isPasteShortcut(keysym)) { + setTimeout(() => this.client.sendKeyEvent(0, keysym), PASTE_DELAY_MS); } else { - terminal_client.sendKeyEvent(0, keysym); + this.client.sendKeyEvent(0, keysym); } }; - $(terminal_element) + $(this.display) .attr('tabindex', 1) .hover( - function() { - var x = window.scrollX, y = window.scrollY; + function () { + const x = window.scrollX, y = window.scrollY; $(this).focus(); window.scrollTo(x, y); }, - function() { $(this).blur(); } + function () { $(this).blur(); } ) - .blur(function() { keyboard.reset(); }); - - $(document).on('paste', function(e) { - var text = e.originalEvent.clipboardData.getData('text/plain'); - if ($(terminal_element).is(":focus")) { - terminal_client.setClipboard(text); + .blur(() => this.keyboard.reset()); + } + + _setupClipboard() { + $(document).on('paste', (e) => { + const text = e.originalEvent.clipboardData.getData('text/plain'); + if ($(this.display).is(':focus')) { + this.client.setClipboard(text); } }); + } + + _showError(title, detail) { + const dialog = $('#launch_error'); + dialog.find('.message').html(title); + dialog.find('.error_msg').html(detail); + dialog.dialog({ dialogClass: 'no-close' }); + dialog.dialog(this.dialogContainer); + } + + _setupErrorHandler() { + const handler = (error) => { + console.log(`guac error ${error.code}: ${error.message}`); + + if (NON_FATAL_STATUS_CODES.has(error.code)) { + return; + } - terminal_client.onerror = function(guac_error) { - terminal_client.disconnect(); - - var dialog = $('#launch_error'); - var dialog_message = - "Could not connect to guest vm. " + - "The client detected an unexpected error. " + - "The server's error message was:"; - var error_message = guac_error.message; + this.disconnect(); - var isNormalEnd = guac_error.message.toLowerCase().startsWith('aborted'); - if (isNormalEnd) { - dialog_message = "The analysis session has ended."; - error_message = ""; - } - var heading = dialog.find('#dialog-heading'); - if (isNormalEnd) { - heading.html('Session Complete'); + if (error.code === 514) { + this._showError("Connection error", "Server timeout."); + } else if (error.code === 515) { + this._showError("Session complete", "Backing VM has disconnected."); + } else if (error.code === 522) { + this._showError("Session ended", "Session timed out due to inactivity."); } else { - heading.html('Session Error'); + const _msg = `An unexpected error occurred: ${error.message}`; + this._showError("Connection error", _msg); } - dialog.find('.message').html(dialog_message); - dialog.find('.error_msg').html(error_message); - dialog.css('display', 'block'); }; - }; - var terminal_connect = function(recording_name) { - if (terminal_connected) { - terminal_client.disconnect(); - terminal_connected = false; + this.tunnel.onerror = handler; + this.client.onerror = handler; + } + + connect() { + if (this.connected) { + this.client.disconnect(); + this.connected = false; } try { - terminal_client.connect($.param({ - 'recording_name': recording_name, + this.client.connect($.param({ + 'recording_name': this.config.recording_name, })); - terminal_connected = true; + this.connected = true; } catch (e) { console.warn(e); - terminal_connected = false; + this.connected = false; throw e; } - }; + } - init(); + disconnect() { + if (this.connected) { + this.client.disconnect(); + this.connected = false; + } + } +} + +function GuacMe(element, session_id, recording_name) { + return new GuacSession(element, { session_id, recording_name }); } function getCsrfToken() { @@ -180,9 +222,11 @@ function getCsrfToken() { return match ? match[1] : ''; } -function stopTask(taskId) { +function stopTask(taskId, onSuccess, onError) { var btn = document.getElementById('stopTask'); if (btn) { btn.disabled = true; btn.innerHTML = 'Stopping...'; } + + const apiUrl = location.origin + "/apiv2/tasks/status/" + taskId + "/"; var apiUrl = location.origin + "/apiv2/tasks/status/" + taskId + "/"; fetch(apiUrl, { @@ -194,11 +238,14 @@ function stopTask(taskId) { body: JSON.stringify({ status: 'finish' }), }) .then(response => response.json()) - .then(function(data) { + .then(data => { + console.log('Response:', data); + if (onSuccess) onSuccess(data); location.replace(location.origin + '/submit/status/' + taskId + '/'); }) - .catch(function(error) { + .catch(error => { console.error('Error:', error); + if (onError) onError(error); if (btn) { btn.disabled = false; btn.innerHTML = 'End Session'; } }); } diff --git a/web/static/js/guacamole-1.4.0-all.min.js b/web/static/js/guacamole-1.4.0-all.min.js deleted file mode 100644 index 6467f8e2a82..00000000000 --- a/web/static/js/guacamole-1.4.0-all.min.js +++ /dev/null @@ -1,155 +0,0 @@ -'use strict';var Guacamole=Guacamole||{};Guacamole.ArrayBufferReader=function(b){var a=this;b.onblob=function(b){b=window.atob(b);for(var c=new ArrayBuffer(b.length),e=new Uint8Array(c),d=0;d=a.length)return a[0];var b=0;a.forEach(function(a){b+=a.length});var c=0,d=new f(b);a.forEach(function(a){d.set(a,c);c+=a.length});return d};b.ondata=function(a){m.push(new f(new f(a)));if(a=n(m)){var b= -Number.MAX_VALUE,p=a.length,g=Math.floor(.02*d.rate);for(g=Math.max(d.channels*g,d.channels*(Math.floor(a.length/d.channels)-g));gD?t(D)*t(D/3):0;r+=v*D}d[u]=r*m;q+=c.channels}return d},u=function(a){v=e.createScriptProcessor(2048, -c.channels,c.channels);v.connect(e.destination);v.onaudioprocess=function(a){f.sendData(q(a.inputBuffer).buffer)};p=e.createMediaStreamSource(a);p.connect(v);"suspended"===e.state&&e.resume();l=a},w=function(){f.sendEnd();if(d.onerror)d.onerror()};f.onack=function(a){if(a.code!==Guacamole.Status.Code.SUCCESS||l){p&&p.disconnect();v&&v.disconnect();if(l)for(var b=l.getTracks(),c=0;c=b.size){if(a.oncomplete)a.oncomplete(b)}else{a:{var e=c;var n=c+d.blobLength,g=(b.slice||b.webkitSlice||b.mozSlice).bind(b),l=n-e;if(l!==n){var p=g(e,l);if(p.size===l){e=p;break a}}e=g(e,n)}c+=d.blobLength;f.readAsArrayBuffer(e)}};f.onload=function(){d.sendData(f.result);d.onack=function(e){if(a.onack)a.onack(e); -if(!e.isError()){if(a.onprogress)a.onprogress(b,c-d.blobLength);k()}}};f.onerror=function(){if(a.onerror)a.onerror(b,c,f.error)};k()};this.sendEnd=function(){d.sendEnd()};this.oncomplete=this.onprogress=this.onerror=this.onack=null};Guacamole=Guacamole||{}; -Guacamole.Client=function(b){function a(h){if(h!=e&&(e=h,c.onstatechange))c.onstatechange(e)}function d(){return 3==e||2==e}var c=this,e=0,f=0,k=null,m={0:"butt",1:"round",2:"square"},n={0:"bevel",1:"miter",2:"round"},g=new Guacamole.Display,l={},p={},v=[],t=[],q=[],u=new Guacamole.IntegerPool,w=[];this.exportState=function(h){var a={currentState:e,currentTimestamp:f,layers:{}},b={},c;for(c in l)b[c]=l[c];g.flush(function(){for(var c in b){var d=parseInt(c),e=b[c],x=e.toCanvas(),q={width:e.width, -height:e.height};e.width&&e.height&&(q.url=x.toDataURL("image/png"));if(0a&&delete l[a]},distort:function(a){var b=parseInt(a[0]),c=parseFloat(a[1]),h=parseFloat(a[2]),d=parseFloat(a[3]),e=parseFloat(a[4]),q=parseFloat(a[5]);a=parseFloat(a[6]);0<=b&&(b=r(b),g.distort(b,c,h,d,e,q,a))},error:function(a){var b=a[0];a=parseInt(a[1]);if(c.onerror)c.onerror(new Guacamole.Status(a,b));c.disconnect()},end:function(a){a= -parseInt(a[0]);var b=t[a];if(b){if(b.onend)b.onend();delete t[a]}},file:function(a){var b=parseInt(a[0]),h=a[1];a=a[2];c.onfile?(b=t[b]=new Guacamole.InputStream(c,b),c.onfile(b,h,a)):c.sendAck(b,"File transfer unsupported",256)},filesystem:function(a){var b=parseInt(a[0]);a=a[1];c.onfilesystem&&(b=q[b]=new Guacamole.Object(c,b),c.onfilesystem(b,a))},identity:function(a){a=r(parseInt(a[0]));g.setTransform(a,1,0,0,1,0,0)},img:function(a){var b=parseInt(a[0]),h=parseInt(a[1]),d=r(parseInt(a[2])),e= -a[3],q=parseInt(a[4]);a=parseInt(a[5]);b=t[b]=new Guacamole.InputStream(c,b);g.setChannelMask(d,h);g.drawStream(d,q,a,b,e)},jpeg:function(a){var b=parseInt(a[0]),c=r(parseInt(a[1])),d=parseInt(a[2]),h=parseInt(a[3]);a=a[4];g.setChannelMask(c,b);g.draw(c,d,h,"data:image/jpeg;base64,"+a)},lfill:function(a){var b=parseInt(a[0]),c=r(parseInt(a[1]));a=r(parseInt(a[2]));g.setChannelMask(c,b);g.fillLayer(c,a)},line:function(a){var b=r(parseInt(a[0])),c=parseInt(a[1]);a=parseInt(a[2]);g.lineTo(b,c,a)},lstroke:function(a){var b= -parseInt(a[0]),c=r(parseInt(a[1]));a=r(parseInt(a[2]));g.setChannelMask(c,b);g.strokeLayer(c,a)},mouse:function(a){var b=parseInt(a[0]);a=parseInt(a[1]);g.showCursor(!0);g.moveCursor(b,a)},move:function(a){var b=parseInt(a[0]),c=parseInt(a[1]),d=parseInt(a[2]),e=parseInt(a[3]);a=parseInt(a[4]);0=a||127<=a&&159>=a?65280|a:0<=a&&255>=a?a:256<=a&&1114111>=a?16777216|a:null}function c(){var a=F();if(!a)return!1;do{var b=a;a=F()}while(null!==a);a:{for(var c in e.pressed)if(!r[c]){a=!1;break a}a= -!0}a&&e.reset();return b.defaultPrevented}var e=this,f="_GUAC_KEYBOARD_HANDLED_BY_"+Guacamole.Keyboard._nextID++;this.onkeyup=this.onkeydown=null;var k=!1,m=!1,n=!1;navigator&&navigator.platform&&(navigator.platform.match(/ipad|iphone|ipod/i)?k=!0:navigator.platform.match(/^mac/i)&&(n=m=!0));var g=function(a){var b=this;this.keyCode=a?a.which||a.keyCode:0;this.keyIdentifier=a&&a.keyIdentifier;this.key=a&&a.key;var c=a?"location"in a?a.location:"keyLocation"in a?a.keyLocation:0:0;this.location=c;this.modifiers= -a?Guacamole.Keyboard.ModifierState.fromKeyboardEvent(a):new Guacamole.Keyboard.ModifierState;this.timestamp=(new Date).getTime();this.defaultPrevented=!1;this.keysym=null;this.reliable=!1;this.getAge=function(){return(new Date).getTime()-b.timestamp}},l=function(b){g.call(this,b);this.keysym=a(this.key,this.location)||A(q[this.keyCode],this.location);this.keyupReliable=!k;if(b=this.keysym)b=this.keysym,b=!(0<=b&&255>=b||16777216===(b&4294901760));b&&(this.reliable=!0);if(b=!this.keysym){b=this.keyCode; -var c=this.keyIdentifier;if(c){var d=c.indexOf("U+");-1===d?b=!0:(c=parseInt(c.substring(d+2),16),b=b!==c||65<=b&&90>=b||48<=b&&57>=b?!0:!1)}else b=!1}b&&(this.keysym=a(this.keyIdentifier,this.location,this.modifiers.shift));this.modifiers.meta&&65511!==this.keysym&&65512!==this.keysym?this.keyupReliable=!1:65509===this.keysym&&n&&(this.keyupReliable=!1);b=!this.modifiers.ctrl&&!m;if(!this.modifiers.alt&&this.modifiers.ctrl||b&&this.modifiers.alt||this.modifiers.meta||this.modifiers.hyper)this.reliable= -!0;z[this.keyCode]=this.keysym};l.prototype=new g;var p=function(a){g.call(this,a);this.keysym=d(this.keyCode);this.reliable=!0};p.prototype=new g;var v=function(b){g.call(this,b);this.keysym=A(q[this.keyCode],this.location)||a(this.key,this.location);e.pressed[this.keysym]||(this.keysym=z[this.keyCode]||this.keysym);this.reliable=!0};v.prototype=new g;var t=[],q={8:[65288],9:[65289],12:[65291,65291,65291,65461],13:[65293],16:[65505,65505,65506],17:[65507,65507,65508],18:[65513,65513,65027],19:[65299], -20:[65509],27:[65307],32:[32],33:[65365,65365,65365,65465],34:[65366,65366,65366,65459],35:[65367,65367,65367,65457],36:[65360,65360,65360,65463],37:[65361,65361,65361,65460],38:[65362,65362,65362,65464],39:[65363,65363,65363,65462],40:[65364,65364,65364,65458],45:[65379,65379,65379,65456],46:[65535,65535,65535,65454],91:[65511],92:[65512],93:[65383],96:[65456],97:[65457],98:[65458],99:[65459],100:[65460],101:[65461],102:[65462],103:[65463],104:[65464],105:[65465],106:[65450],107:[65451],109:[65453], -110:[65454],111:[65455],112:[65470],113:[65471],114:[65472],115:[65473],116:[65474],117:[65475],118:[65476],119:[65477],120:[65478],121:[65479],122:[65480],123:[65481],144:[65407],145:[65300],225:[65027]},u={Again:[65382],AllCandidates:[65341],Alphanumeric:[65328],Alt:[65513,65513,65027],Attn:[64782],AltGraph:[65027],ArrowDown:[65364],ArrowLeft:[65361],ArrowRight:[65363],ArrowUp:[65362],Backspace:[65288],CapsLock:[65509],Cancel:[65385],Clear:[65291],Convert:[65313],Copy:[64789],Crsel:[64796],CrSel:[64796], -CodeInput:[65335],Compose:[65312],Control:[65507,65507,65508],ContextMenu:[65383],Delete:[65535],Down:[65364],End:[65367],Enter:[65293],EraseEof:[64774],Escape:[65307],Execute:[65378],Exsel:[64797],ExSel:[64797],F1:[65470],F2:[65471],F3:[65472],F4:[65473],F5:[65474],F6:[65475],F7:[65476],F8:[65477],F9:[65478],F10:[65479],F11:[65480],F12:[65481],F13:[65482],F14:[65483],F15:[65484],F16:[65485],F17:[65486],F18:[65487],F19:[65488],F20:[65489],F21:[65490],F22:[65491],F23:[65492],F24:[65493],Find:[65384], -GroupFirst:[65036],GroupLast:[65038],GroupNext:[65032],GroupPrevious:[65034],FullWidth:null,HalfWidth:null,HangulMode:[65329],Hankaku:[65321],HanjaMode:[65332],Help:[65386],Hiragana:[65317],HiraganaKatakana:[65319],Home:[65360],Hyper:[65517,65517,65518],Insert:[65379],JapaneseHiragana:[65317],JapaneseKatakana:[65318],JapaneseRomaji:[65316],JunjaMode:[65336],KanaMode:[65325],KanjiMode:[65313],Katakana:[65318],Left:[65361],Meta:[65511,65511,65512],ModeChange:[65406],NumLock:[65407],PageDown:[65366], -PageUp:[65365],Pause:[65299],Play:[64790],PreviousCandidate:[65342],PrintScreen:[65377],Redo:[65382],Right:[65363],RomanCharacters:null,Scroll:[65300],Select:[65376],Separator:[65452],Shift:[65505,65505,65506],SingleCandidate:[65340],Super:[65515,65515,65516],Tab:[65289],UIKeyInputDownArrow:[65364],UIKeyInputEscape:[65307],UIKeyInputLeftArrow:[65361],UIKeyInputRightArrow:[65363],UIKeyInputUpArrow:[65362],Up:[65362],Undo:[65381],Win:[65511,65511,65512],Zenkaku:[65320],ZenkakuHankaku:[65322]},w={65027:!0, -65505:!0,65506:!0,65507:!0,65508:!0,65509:!0,65511:!0,65512:!0,65513:!0,65514:!0,65515:!0,65516:!0};this.modifiers=new Guacamole.Keyboard.ModifierState;this.pressed={};var r={},y={},z={},h=null,x=null,A=function(a,b){return a?a[b]||a[0]:null};this.press=function(a){if(null!==a){if(!e.pressed[a]&&(e.pressed[a]=!0,e.onkeydown)){var b=e.onkeydown(a);y[a]=b;window.clearTimeout(h);window.clearInterval(x);w[a]||(h=window.setTimeout(function(){x=window.setInterval(function(){e.onkeyup(a);e.onkeydown(a)}, -50)},500));return b}return y[a]||!1}};this.release=function(a){if(e.pressed[a]&&(delete e.pressed[a],delete r[a],window.clearTimeout(h),window.clearInterval(x),null!==a&&e.onkeyup))e.onkeyup(a)};this.type=function(a){for(var b=0;b=b||97<=b&&122>=b)&&(255>=b||16777216===(b&4278190080))&&(e.release(65507),e.release(65508),e.release(65513),e.release(65514));var d=!e.press(b);z[a.keyCode]=b;a.keyupReliable||e.release(b);for(b=0;bc.width?a:c.width,b>c.height?b:c.height)}var c=this,e=document.createElement("canvas"),f=e.getContext("2d");f.save();var k=!0,m=!0,n=0,g={1:"destination-in",2:"destination-out",4:"source-in",6:"source-atop",8:"source-out",9:"destination-atop",10:"xor",11:"destination-over",12:"copy",14:"source-over",15:"lighter"},l=function(a,b){a=a||0;b=b||0;var d=64*Math.ceil(a/64),p=64*Math.ceil(b/64);if(e.width!==d||e.height!==p){var g=null; -k||0===e.width||0===e.height||(g=document.createElement("canvas"),g.width=Math.min(c.width,a),g.height=Math.min(c.height,b),g.getContext("2d").drawImage(e,0,0,g.width,g.height,0,0,g.width,g.height));var l=f.globalCompositeOperation;e.width=d;e.height=p;g&&f.drawImage(g,0,0,g.width,g.height,0,0,g.width,g.height);f.globalCompositeOperation=l;n=0;f.save()}else c.reset();c.width=a;c.height=b};this.autosize=!1;this.width=b;this.height=a;this.getCanvas=function(){return e};this.toCanvas=function(){var a= -document.createElement("canvas");a.width=c.width;a.height=c.height;a.getContext("2d").drawImage(c.getCanvas(),0,0);return a};this.resize=function(a,b){a===c.width&&b===c.height||l(a,b)};this.drawImage=function(a,b,e){c.autosize&&d(a,b,e.width,e.height);f.drawImage(e,a,b);k=!1};this.transfer=function(a,b,e,g,l,n,m,y){var p=a.getCanvas();if(!(b>=p.width||e>=p.height)&&(b+g>p.width&&(g=p.width-b),e+l>p.height&&(l=p.height-e),0!==g&&0!==l)){c.autosize&&d(n,m,g,l);a=a.getCanvas().getContext("2d").getImageData(b, -e,g,l);b=f.getImageData(n,m,g,l);for(e=0;e=p.width||e>=p.height||(b+g>p.width&&(g=p.width-b),e+l>p.height&&(l=p.height-e),0!==g&&0!==l&&(c.autosize&&d(n,m,g,l),a=a.getCanvas().getContext("2d").getImageData(b, -e,g,l),f.putImageData(a,n,m),k=!1))};this.copy=function(a,b,e,g,l,n,m){a=a.getCanvas();b>=a.width||e>=a.height||(b+g>a.width&&(g=a.width-b),e+l>a.height&&(l=a.height-e),0!==g&&0!==l&&(c.autosize&&d(n,m,g,l),f.drawImage(a,b,e,g,l,n,m,g,l),k=!1))};this.moveTo=function(a,b){m&&(f.beginPath(),m=!1);c.autosize&&d(a,b,0,0);f.moveTo(a,b)};this.lineTo=function(a,b){m&&(f.beginPath(),m=!1);c.autosize&&d(a,b,0,0);f.lineTo(a,b)};this.arc=function(a,b,e,g,l,n){m&&(f.beginPath(),m=!1);c.autosize&&d(a,b,0,0);f.arc(a, -b,e,g,l,n)};this.curveTo=function(a,b,e,g,l,n){m&&(f.beginPath(),m=!1);c.autosize&&d(l,n,0,0);f.bezierCurveTo(a,b,e,g,l,n)};this.close=function(){f.closePath();m=!0};this.rect=function(a,b,e,g){m&&(f.beginPath(),m=!1);c.autosize&&d(a,b,e,g);f.rect(a,b,e,g)};this.clip=function(){f.clip();m=!0};this.strokeColor=function(a,b,c,d,e,g,l){f.lineCap=a;f.lineJoin=b;f.lineWidth=c;f.strokeStyle="rgba("+d+","+e+","+g+","+l/255+")";f.stroke();k=!1;m=!0};this.fillColor=function(a,b,c,d){f.fillStyle="rgba("+a+ -","+b+","+c+","+d/255+")";f.fill();k=!1;m=!0};this.strokeLayer=function(a,b,c,d){f.lineCap=a;f.lineJoin=b;f.lineWidth=c;f.strokeStyle=f.createPattern(d.getCanvas(),"repeat");f.stroke();k=!1;m=!0};this.fillLayer=function(a){f.fillStyle=f.createPattern(a.getCanvas(),"repeat");f.fill();k=!1;m=!0};this.push=function(){f.save();n++};this.pop=function(){0=c.scrollThreshold){do c.click(Guacamole.Mouse.State.Buttons.DOWN),k-=c.scrollThreshold;while(k>=c.scrollThreshold);k= -0}Guacamole.Event.DOMEvent.cancelEvent(a)}Guacamole.Mouse.Event.Target.call(this);var c=this;this.touchMouseThreshold=3;this.scrollThreshold=53;this.PIXELS_PER_LINE=18;this.PIXELS_PER_PAGE=16*this.PIXELS_PER_LINE;var e=[Guacamole.Mouse.State.Buttons.LEFT,Guacamole.Mouse.State.Buttons.MIDDLE,Guacamole.Mouse.State.Buttons.RIGHT],f=0,k=0;b.addEventListener("contextmenu",function(a){Guacamole.Event.DOMEvent.cancelEvent(a)},!1);b.addEventListener("mousemove",function(a){f?(Guacamole.Event.DOMEvent.cancelEvent(a), -f--):c.move(Guacamole.Position.fromClientPosition(b,a.clientX,a.clientY),a)},!1);b.addEventListener("mousedown",function(a){if(f)Guacamole.Event.DOMEvent.cancelEvent(a);else{var b=e[a.button];b&&c.press(b,a)}},!1);b.addEventListener("mouseup",function(a){if(f)Guacamole.Event.DOMEvent.cancelEvent(a);else{var b=e[a.button];b&&c.release(b,a)}},!1);b.addEventListener("mouseout",function(a){a||(a=window.event);for(var d=a.relatedTarget||a.toElement;d;){if(d===b)return;d=d.parentNode}c.reset(a);c.out(a)}, -!1);b.addEventListener("selectstart",function(a){Guacamole.Event.DOMEvent.cancelEvent(a)},!1);b.addEventListener("touchmove",a,!1);b.addEventListener("touchstart",a,!1);b.addEventListener("touchend",a,!1);b.addEventListener("DOMMouseScroll",d,!1);b.addEventListener("mousewheel",d,!1);b.addEventListener("wheel",d,!1);var m=function(){var a=document.createElement("div");if(!("cursor"in a.style))return!1;try{a.style.cursor="url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX///+nxBvIAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg\x3d\x3d) 0 0, auto"}catch(g){return!1}return/\burl\([^()]*\)\s+0\s+0\b/.test(a.style.cursor|| -"")}();this.setCursor=function(a,c,d){return m?(a=a.toDataURL("image/png"),b.style.cursor="url("+a+") "+c+" "+d+", auto",!0):!1}};Guacamole.Mouse.State=function(b){var a=function(a,b,e,f,k,m,n){return{x:a,y:b,left:e,middle:f,right:k,up:m,down:n}};b=1=a.scrollThreshold&&(a.click(0=e.clickMoveThreshold}function d(a){a=a.touches[0];f=!0;k=a.clientX;m=a.clientY}function c(){window.clearTimeout(n);window.clearTimeout(g);f=!1}Guacamole.Mouse.Event.Target.call(this);var e=this,f=!1,k=null,m=null,n=null,g=null;this.scrollThreshold=20*(window.devicePixelRatio||1);this.clickTimingThreshold=250;this.clickMoveThreshold=16*(window.devicePixelRatio|| -1);this.longPressThreshold=500;b.addEventListener("touchend",function(d){if(f)if(0!==d.touches.length||1!==d.changedTouches.length)c();else if(window.clearTimeout(g),e.release(Guacamole.Mouse.State.Buttons.LEFT,d),!a(d)&&(d.preventDefault(),!e.currentState.left)){var l=d.changedTouches[0];e.move(Guacamole.Position.fromClientPosition(b,l.clientX,l.clientY));e.press(Guacamole.Mouse.State.Buttons.LEFT,d);n=window.setTimeout(function(){e.release(Guacamole.Mouse.State.Buttons.LEFT,d);c()},e.clickTimingThreshold)}}, -!1);b.addEventListener("touchstart",function(a){1!==a.touches.length?c():(a.preventDefault(),d(a),window.clearTimeout(n),g=window.setTimeout(function(){var d=a.touches[0];e.move(Guacamole.Position.fromClientPosition(b,d.clientX,d.clientY));e.click(Guacamole.Mouse.State.Buttons.RIGHT,a);c()},e.longPressThreshold))},!1);b.addEventListener("touchmove",function(d){if(f)if(a(d)&&window.clearTimeout(g),1!==d.touches.length)c();else if(e.currentState.left){d.preventDefault();var m=d.touches[0];e.move(Guacamole.Position.fromClientPosition(b, -m.clientX,m.clientY),d)}},!1)};Guacamole=Guacamole=Guacamole||{};Guacamole.Object=function(b,a){var d=this,c={};this.index=a;this.onbody=function(a,b,d){var e=c[d];if(e){var f=e.shift();0===e.length&&delete c[d];d=f}else d=null;d&&d(a,b)};this.onundefine=null;this.requestInputStream=function(a,f){if(f){var e=c[a];e||(e=[],c[a]=e);e.push(f)}b.requestObjectInputStream(d.index,a)};this.createOutputStream=function(a,c){return b.createObjectOutputStream(d.index,a,c)}};Guacamole.Object.ROOT_STREAM="/"; -Guacamole.Object.STREAM_INDEX_MIMETYPE="application/vnd.glyptodon.guacamole.stream-index+json";Guacamole=Guacamole||{}; -Guacamole.OnScreenKeyboard=function(b){var a=this,d={},c={},e=[],f=function(a,b){a.classList?a.classList.add(b):a.className+=" "+b},k=function(a,b){a.classList?a.classList.remove(b):a.className=a.className.replace(/([^ ]+)[ ]*/g,function(a,c){return c===b?"":a})},m=0,n=function(a,b,c,d){this.width=b;this.height=c;this.scale=function(e){a.style.width=b*e+"px";a.style.height=c*e+"px";d&&(a.style.lineHeight=c*e+"px",a.style.fontSize=e+"px")}},g=function(b){b=a.keys[b];if(!b)return null;for(var c=b.length- -1;0<=c;c--){var e=b[c];a:{var f=e.requires;for(var g=0;g=a?a:256<=a&&1114111>=a?16777216|a:null):a=null);this.keysym=a;this.modifier=b.modifier;this.requires=b.requires||[]};Guacamole=Guacamole||{};Guacamole.OutputStream=function(b,a){var d=this;this.index=a;this.onack=null;this.sendBlob=function(a){b.sendBlob(d.index,a)};this.sendEnd=function(){b.endStream(d.index)}}; -Guacamole=Guacamole||{}; -Guacamole.Parser=function(){var b=this,a="",d=[],c=-1,e=0;this.receive=function(f){4096=e&&(a=a.substring(e),c-=e,e=0);for(a+=f;c=e){f=a.substring(e,c);var k=a.substring(c,c+1);d.push(f);if(";"==k){f=d.shift();if(null!=b.oninstruction)b.oninstruction(f,d);d.length=0}else if(","!=k)throw Error("Illegal terminator.");e=c+1}f=a.indexOf(".",e);if(-1!=f){k=parseInt(a.substring(c+1,f));if(isNaN(k))throw Error("Non-numeric character in element length.");e=f+1;c=e+k}else{e=a.length; -break}}};this.oninstruction=null};Guacamole=Guacamole||{}; -Guacamole.Position=function(b){b=b||{};this.x=b.x||0;this.y=b.y||0;this.fromClientPosition=function(a,b,c){this.x=b-a.offsetLeft;this.y=c-a.offsetTop;for(a=a.offsetParent;a&&a!==document.body;)this.x-=a.offsetLeft-a.scrollLeft,this.y-=a.offsetTop-a.scrollTop,a=a.offsetParent;a&&(b=document.body.scrollTop||document.documentElement.scrollTop,this.x-=a.offsetLeft-(document.body.scrollLeft||document.documentElement.scrollLeft),this.y-=a.offsetTop-b)}}; -Guacamole.Position.fromClientPosition=function(b,a,d){var c=new Guacamole.Position;c.fromClientPosition(b,a,d);return c};Guacamole=Guacamole||{};Guacamole.RawAudioFormat=function(b){this.bytesPerSample=b.bytesPerSample;this.channels=b.channels;this.rate=b.rate}; -Guacamole.RawAudioFormat.parse=function(b){var a=null,d=1;if("audio/L8;"===b.substring(0,9)){b=b.substring(9);var c=1}else if("audio/L16;"===b.substring(0,10))b=b.substring(10),c=2;else return null;b=b.split(",");for(var e=0;ea?x(a,e-1,c):c>f&&ed.code||255=b){var c=0;var d=1}else if(2047>=b)c=192,d=2;else if(65535>=b)c=224,d=3;else if(2097151>=b)c=240,d=4;else{a(65533);return}var e=d;if(k+e>=f.length){var m=new Uint8Array(2*(k+e));m.set(f);f=m}k+=e;e=k-1;for(m=1;m>=6;f[e]=c|b}function d(b){for(var c=0;c -a.readyState)){try{var f=a.status}catch(H){f=200}d||200!==f||(d=g());if(3===a.readyState||4===a.readyState)if(e(),1===q&&(3!==a.readyState||c?4===a.readyState&&c&&clearInterval(c):c=setInterval(b,30)),0===a.status)l.disconnect();else if(200!==a.status)m(a);else{try{var t=a.responseText}catch(H){return}for(;h=k){f=t.substring(k,h);var r=t.substring(h,h+1);p.push(f);if(";"===r){f=p.shift();if(l.oninstruction)l.oninstruction(f,p);p.length=0}k=h+1}f=t.indexOf(".",k);if(-1!==f){r=parseInt(t.substring(h+ -1,f));if(0===r){c&&clearInterval(c);a.onreadystatechange=null;a.abort();d&&n(d);break}k=f+1;h=k+r}else{k=t.length;break}}}}}var c=null,d=null,f=0,h=-1,k=0,p=[];a.onreadystatechange=1===q?function(){3===a.readyState&&(f++,2<=f&&(q=0,a.onreadystatechange=b));b()}:b;b()}function g(){var a=new XMLHttpRequest;a.open("GET",v+l.uuid+":"+B++);a.setRequestHeader("Guacamole-Tunnel-Token",A);a.withCredentials=r;c(a,x);a.send(null);return a}var l=this,p=b+"?connect",v=b+"?read:",t=b+"?write:",q=1,u=!1,w="",r= -!!a,y=null,z=null,h=null,x=d||{},A=null;this.sendMessage=function(){function a(a){a=new String(a);return a.length+"."+a}if(l.isConnected()&&0!==arguments.length){for(var b=a(arguments[0]),c=1;c=a.length)return a[0];var b=0;a.forEach(function(a){b+=a.length});var c=0,d=new f(b);a.forEach(function(a){d.set(a,c);c+=a.length});return d};b.ondata=function(a){l.push(new f(new f(a)));if(a=n(l)){var b= +Number.MAX_VALUE,k=a.length,r=Math.floor(.02*d.rate);for(r=Math.max(d.channels*r,d.channels*(Math.floor(a.length/d.channels)-r));rx?q(x)*q(x/3):0;y+=r*x}d[w]=y*l;t+=c.channels}return d},x=function(a){r=e.createScriptProcessor(2048, +c.channels,c.channels);r.connect(e.destination);r.onaudioprocess=function(a){f.sendData(v(a.inputBuffer).buffer)};k=e.createMediaStreamSource(a);k.connect(r);"suspended"===e.state&&e.resume();g=a},t=function(){f.sendEnd();if(d.onerror)d.onerror()};f.onack=function(a){if(a.code!==Guacamole.Status.Code.SUCCESS||g){k&&k.disconnect();r&&r.disconnect();if(g)for(var b=g.getTracks(),c=0;c=b.size){if(a.oncomplete)a.oncomplete(b)}else{a:{var e=c;var h=c+d.blobLength,m=(b.slice||b.webkitSlice||b.mozSlice).bind(b),g=h-e;if(g!==h){var k=m(e,g);if(k.size===g){e=k;break a}}e=m(e,h)}c+=d.blobLength;f.readAsArrayBuffer(e)}};f.onload=function(){d.sendData(f.result);d.onack=function(e){if(a.onack)a.onack(e); +if(!e.isError()){if(a.onprogress)a.onprogress(b,c-d.blobLength);h()}}};f.onerror=function(){if(a.onerror)a.onerror(b,c,f.error)};h()};this.sendEnd=function(){d.sendEnd()};this.oncomplete=this.onprogress=this.onerror=this.onack=null};Guacamole=Guacamole||{}; +Guacamole.Client=function(b){function a(a){if(a!=e&&(e=a,c.onstatechange))c.onstatechange(e)}function d(){return e==Guacamole.Client.State.CONNECTED||e==Guacamole.Client.State.WAITING}var c=this,e=Guacamole.Client.State.IDLE,f=0,h=null,l=0,n={0:"butt",1:"round",2:"square"},m={0:"bevel",1:"miter",2:"round"},g=new Guacamole.Display,k={},r={},q=[],v=[],x=[],t=new Guacamole.IntegerPool,y=[];this.exportState=function(a){var p={currentState:e,currentTimestamp:f,layers:{}},b={},c;for(c in k)b[c]=k[c];g.flush(function(){for(var c in b){var d= +parseInt(c),e=b[c],w=e.toCanvas(),f={width:e.width,height:e.height};e.width&&e.height&&(f.url=w.toDataURL("image/png"));if(0a&&delete k[a]},distort:function(a){var b=parseInt(a[0]),c=parseFloat(a[1]),d=parseFloat(a[2]),e=parseFloat(a[3]),p=parseFloat(a[4]),f=parseFloat(a[5]);a=parseFloat(a[6]);0<=b&&(b=u(b),g.distort(b,c,d,e,p,f,a))},error:function(a){var b=a[0];a=parseInt(a[1]); +if(c.onerror)c.onerror(new Guacamole.Status(a,b));c.disconnect()},end:function(a){a=parseInt(a[0]);var b=v[a];if(b){if(b.onend)b.onend();delete v[a]}},file:function(a){var b=parseInt(a[0]),d=a[1];a=a[2];c.onfile?(b=v[b]=new Guacamole.InputStream(c,b),c.onfile(b,d,a)):c.sendAck(b,"File transfer unsupported",256)},filesystem:function(a){var b=parseInt(a[0]);a=a[1];c.onfilesystem&&(b=x[b]=new Guacamole.Object(c,b),c.onfilesystem(b,a))},identity:function(a){a=u(parseInt(a[0]));g.setTransform(a,1,0,0, +1,0,0)},img:function(a){var b=parseInt(a[0]),d=parseInt(a[1]),e=u(parseInt(a[2])),p=a[3],f=parseInt(a[4]);a=parseInt(a[5]);b=v[b]=new Guacamole.InputStream(c,b);g.setChannelMask(e,d);g.drawStream(e,f,a,b,p)},jpeg:function(a){var b=parseInt(a[0]),c=u(parseInt(a[1])),d=parseInt(a[2]),e=parseInt(a[3]);a=a[4];g.setChannelMask(c,b);g.draw(c,d,e,"data:image/jpeg;base64,"+a)},lfill:function(a){var b=parseInt(a[0]),c=u(parseInt(a[1]));a=u(parseInt(a[2]));g.setChannelMask(c,b);g.fillLayer(c,a)},line:function(a){var b= +u(parseInt(a[0])),c=parseInt(a[1]);a=parseInt(a[2]);g.lineTo(b,c,a)},lstroke:function(a){var b=parseInt(a[0]),c=u(parseInt(a[1]));a=u(parseInt(a[2]));g.setChannelMask(c,b);g.strokeLayer(c,a)},mouse:function(a){var b=parseInt(a[0]);a=parseInt(a[1]);g.showCursor(!0);g.moveCursor(b,a)},move:function(a){var b=parseInt(a[0]),c=parseInt(a[1]),d=parseInt(a[2]),e=parseInt(a[3]);a=parseInt(a[4]);0=a||127<=a&&159>=a?65280|a:0<=a&&255>=a?a:256<=a&&1114111>=a?16777216|a:null}function c(){var a=F();if(!a)return!1;do{var b=a;a=F()}while(null!==a);a:{for(var c in e.pressed)if(!y[c]){a=!1;break a}a= +!0}a&&e.reset();return b.defaultPrevented}var e=this,f="_GUAC_KEYBOARD_HANDLED_BY_"+Guacamole.Keyboard._nextID++;this.onkeyup=this.onkeydown=null;var h=!1,l=!1,n=!1;navigator&&navigator.platform&&(navigator.platform.match(/ipad|iphone|ipod/i)?h=!0:navigator.platform.match(/^mac/i)&&(n=l=!0));var m=function(a){var b=this;this.keyCode=a?a.which||a.keyCode:0;this.keyIdentifier=a&&a.keyIdentifier;this.key=a&&a.key;var c=a?"location"in a?a.location:"keyLocation"in a?a.keyLocation:0:0;this.location=c;this.modifiers= +a?Guacamole.Keyboard.ModifierState.fromKeyboardEvent(a):new Guacamole.Keyboard.ModifierState;this.timestamp=(new Date).getTime();this.defaultPrevented=!1;this.keysym=null;this.reliable=!1;this.getAge=function(){return(new Date).getTime()-b.timestamp}},g=function(b){m.call(this,b);this.keysym=a(this.key,this.location)||B(v[this.keyCode],this.location);this.keyupReliable=!h;if(b=this.keysym)b=this.keysym,b=!(0<=b&&255>=b||16777216===(b&4294901760));b&&(this.reliable=!0);if(b=!this.keysym){b=this.keyCode; +var c=this.keyIdentifier;if(c){var d=c.indexOf("U+");-1===d?b=!0:(c=parseInt(c.substring(d+2),16),b=b!==c||65<=b&&90>=b||48<=b&&57>=b?!0:!1)}else b=!1}b&&(this.keysym=a(this.keyIdentifier,this.location,this.modifiers.shift));this.modifiers.meta&&65511!==this.keysym&&65512!==this.keysym?this.keyupReliable=!1:65509===this.keysym&&n&&(this.keyupReliable=!1);b=!this.modifiers.ctrl&&!l;!l||65513!==this.keysym&&65514!==this.keysym||(this.keysym=65027);if(!this.modifiers.alt&&this.modifiers.ctrl||b&&this.modifiers.alt|| +this.modifiers.meta||this.modifiers.hyper)this.reliable=!0;C[this.keyCode]=this.keysym};g.prototype=new m;var k=function(a){m.call(this,a);this.keysym=d(this.keyCode);this.reliable=!0};k.prototype=new m;var r=function(b){m.call(this,b);this.keysym=B(v[this.keyCode],this.location)||a(this.key,this.location);e.pressed[this.keysym]||(this.keysym=C[this.keyCode]||this.keysym);this.reliable=!0};r.prototype=new m;var q=[],v={8:[65288],9:[65289],12:[65291,65291,65291,65461],13:[65293],16:[65505,65505,65506], +17:[65507,65507,65508],18:[65513,65513,65514],19:[65299],20:[65509],27:[65307],32:[32],33:[65365,65365,65365,65465],34:[65366,65366,65366,65459],35:[65367,65367,65367,65457],36:[65360,65360,65360,65463],37:[65361,65361,65361,65460],38:[65362,65362,65362,65464],39:[65363,65363,65363,65462],40:[65364,65364,65364,65458],45:[65379,65379,65379,65456],46:[65535,65535,65535,65454],91:[65511],92:[65512],93:[65383],96:[65456],97:[65457],98:[65458],99:[65459],100:[65460],101:[65461],102:[65462],103:[65463], +104:[65464],105:[65465],106:[65450],107:[65451],109:[65453],110:[65454],111:[65455],112:[65470],113:[65471],114:[65472],115:[65473],116:[65474],117:[65475],118:[65476],119:[65477],120:[65478],121:[65479],122:[65480],123:[65481],144:[65407],145:[65300],225:[65027]},x={Again:[65382],AllCandidates:[65341],Alphanumeric:[65328],Alt:[65513,65513,65514],Attn:[64782],AltGraph:[65027],ArrowDown:[65364],ArrowLeft:[65361],ArrowRight:[65363],ArrowUp:[65362],Backspace:[65288],CapsLock:[65509],Cancel:[65385],Clear:[65291], +Convert:[65315],Copy:[64789],Crsel:[64796],CrSel:[64796],CodeInput:[65335],Compose:[65312],Control:[65507,65507,65508],ContextMenu:[65383],Delete:[65535],Down:[65364],End:[65367],Enter:[65293],EraseEof:[64774],Escape:[65307],Execute:[65378],Exsel:[64797],ExSel:[64797],F1:[65470],F2:[65471],F3:[65472],F4:[65473],F5:[65474],F6:[65475],F7:[65476],F8:[65477],F9:[65478],F10:[65479],F11:[65480],F12:[65481],F13:[65482],F14:[65483],F15:[65484],F16:[65485],F17:[65486],F18:[65487],F19:[65488],F20:[65489],F21:[65490], +F22:[65491],F23:[65492],F24:[65493],Find:[65384],GroupFirst:[65036],GroupLast:[65038],GroupNext:[65032],GroupPrevious:[65034],FullWidth:null,HalfWidth:null,HangulMode:[65329],Hankaku:[65321],HanjaMode:[65332],Help:[65386],Hiragana:[65317],HiraganaKatakana:[65319],Home:[65360],Hyper:[65517,65517,65518],Insert:[65379],JapaneseHiragana:[65317],JapaneseKatakana:[65318],JapaneseRomaji:[65316],JunjaMode:[65336],KanaMode:[65325],KanjiMode:[65313],Katakana:[65318],Left:[65361],Meta:[65511,65511,65512],ModeChange:[65406], +NonConvert:[65314],NumLock:[65407],PageDown:[65366],PageUp:[65365],Pause:[65299],Play:[64790],PreviousCandidate:[65342],PrintScreen:[65377],Redo:[65382],Right:[65363],Romaji:[65316],RomanCharacters:null,Scroll:[65300],Select:[65376],Separator:[65452],Shift:[65505,65505,65506],SingleCandidate:[65340],Super:[65515,65515,65516],Tab:[65289],UIKeyInputDownArrow:[65364],UIKeyInputEscape:[65307],UIKeyInputLeftArrow:[65361],UIKeyInputRightArrow:[65363],UIKeyInputUpArrow:[65362],Up:[65362],Undo:[65381],Win:[65511, +65511,65512],Zenkaku:[65320],ZenkakuHankaku:[65322]},t={65027:!0,65505:!0,65506:!0,65507:!0,65508:!0,65509:!0,65511:!0,65512:!0,65513:!0,65514:!0,65515:!0,65516:!0};this.modifiers=new Guacamole.Keyboard.ModifierState;this.pressed={};var y={},u={},C={},D=null,A=null,B=function(a,b){return a?a[b]||a[0]:null};this.press=function(a){if(null!==a){if(!e.pressed[a]&&(e.pressed[a]=!0,e.onkeydown)){var b=e.onkeydown(a);u[a]=b;window.clearTimeout(D);window.clearInterval(A);t[a]||(D=window.setTimeout(function(){A= +window.setInterval(function(){e.onkeyup(a);e.onkeydown(a)},50)},500));return b}return u[a]||!1}};this.release=function(a){if(e.pressed[a]&&(delete e.pressed[a],delete y[a],window.clearTimeout(D),window.clearInterval(A),null!==a&&e.onkeyup))e.onkeyup(a)};this.type=function(a){for(var b=0;b=b||97<=b&&122>=b)&&(255>=b||16777216===(b&4278190080))&&(e.release(65507),e.release(65508),e.release(65513),e.release(65514));var d=!e.press(b);C[a.keyCode]=b;a.keyupReliable||e.release(b);for(b= +0;be||255c.width?a:c.width,b>c.height?b:c.height)}var c=this,e=document.createElement("canvas"),f=e.getContext("2d");f.save();var h=!0,l=!0,n=0,m={1:"destination-in",2:"destination-out",4:"source-in",6:"source-atop",8:"source-out",9:"destination-atop",10:"xor",11:"destination-over",12:"copy",14:"source-over",15:"lighter"},g=function(a,b){a=a||0;b=b||0;var d=64*Math.ceil(a/64),g=64*Math.ceil(b/64);if(e.width!==d||e.height!==g){var k=null; +h||0===e.width||0===e.height||(k=document.createElement("canvas"),k.width=Math.min(c.width,a),k.height=Math.min(c.height,b),k.getContext("2d").drawImage(e,0,0,k.width,k.height,0,0,k.width,k.height));var l=f.globalCompositeOperation;e.width=d;e.height=g;k&&f.drawImage(k,0,0,k.width,k.height,0,0,k.width,k.height);f.globalCompositeOperation=l;n=0;f.save()}else c.reset();c.width=a;c.height=b};this.autosize=!1;this.width=b;this.height=a;this.getCanvas=function(){return e};this.toCanvas=function(){var a= +document.createElement("canvas");a.width=c.width;a.height=c.height;a.getContext("2d").drawImage(c.getCanvas(),0,0);return a};this.resize=function(a,b){a===c.width&&b===c.height||g(a,b)};this.drawImage=function(a,b,e){c.autosize&&d(a,b,e.width,e.height);f.drawImage(e,a,b);h=!1};this.transfer=function(a,b,e,g,l,n,m,u){var k=a.getCanvas();if(!(b>=k.width||e>=k.height)&&(b+g>k.width&&(g=k.width-b),e+l>k.height&&(l=k.height-e),0!==g&&0!==l)){c.autosize&&d(n,m,g,l);a=a.getCanvas().getContext("2d").getImageData(b, +e,g,l);b=f.getImageData(n,m,g,l);for(e=0;e=k.width||e>=k.height||(b+g>k.width&&(g=k.width-b),e+l>k.height&&(l=k.height-e),0!==g&&0!==l&&(c.autosize&&d(n,m,g,l),a=a.getCanvas().getContext("2d").getImageData(b, +e,g,l),f.putImageData(a,n,m),h=!1))};this.copy=function(a,b,e,g,l,n,m){a=a.getCanvas();b>=a.width||e>=a.height||(b+g>a.width&&(g=a.width-b),e+l>a.height&&(l=a.height-e),0!==g&&0!==l&&(c.autosize&&d(n,m,g,l),f.drawImage(a,b,e,g,l,n,m,g,l),h=!1))};this.moveTo=function(a,b){l&&(f.beginPath(),l=!1);c.autosize&&d(a,b,0,0);f.moveTo(a,b)};this.lineTo=function(a,b){l&&(f.beginPath(),l=!1);c.autosize&&d(a,b,0,0);f.lineTo(a,b)};this.arc=function(a,b,e,g,h,n){l&&(f.beginPath(),l=!1);c.autosize&&d(a,b,0,0);f.arc(a, +b,e,g,h,n)};this.curveTo=function(a,b,e,g,h,n){l&&(f.beginPath(),l=!1);c.autosize&&d(h,n,0,0);f.bezierCurveTo(a,b,e,g,h,n)};this.close=function(){f.closePath();l=!0};this.rect=function(a,b,e,g){l&&(f.beginPath(),l=!1);c.autosize&&d(a,b,e,g);f.rect(a,b,e,g)};this.clip=function(){f.clip();l=!0};this.strokeColor=function(a,b,c,d,e,g,n){f.lineCap=a;f.lineJoin=b;f.lineWidth=c;f.strokeStyle="rgba("+d+","+e+","+g+","+n/255+")";f.stroke();h=!1;l=!0};this.fillColor=function(a,b,c,d){f.fillStyle="rgba("+a+ +","+b+","+c+","+d/255+")";f.fill();h=!1;l=!0};this.strokeLayer=function(a,b,c,d){f.lineCap=a;f.lineJoin=b;f.lineWidth=c;f.strokeStyle=f.createPattern(d.getCanvas(),"repeat");f.stroke();h=!1;l=!0};this.fillLayer=function(a){f.fillStyle=f.createPattern(a.getCanvas(),"repeat");f.fill();h=!1;l=!0};this.push=function(){f.save();n++};this.pop=function(){0=c.scrollThreshold){do c.click(Guacamole.Mouse.State.Buttons.DOWN),h-=c.scrollThreshold;while(h>=c.scrollThreshold);h= +0}Guacamole.Event.DOMEvent.cancelEvent(a)}Guacamole.Mouse.Event.Target.call(this);var c=this;this.touchMouseThreshold=3;this.scrollThreshold=53;this.PIXELS_PER_LINE=18;this.PIXELS_PER_PAGE=16*this.PIXELS_PER_LINE;var e=[Guacamole.Mouse.State.Buttons.LEFT,Guacamole.Mouse.State.Buttons.MIDDLE,Guacamole.Mouse.State.Buttons.RIGHT],f=0,h=0;b.addEventListener("contextmenu",function(a){Guacamole.Event.DOMEvent.cancelEvent(a)},!1);b.addEventListener("mousemove",function(a){f?(Guacamole.Event.DOMEvent.cancelEvent(a), +f--):c.move(Guacamole.Position.fromClientPosition(b,a.clientX,a.clientY),a)},!1);b.addEventListener("mousedown",function(a){if(f)Guacamole.Event.DOMEvent.cancelEvent(a);else{var b=e[a.button];b&&c.press(b,a)}},!1);b.addEventListener("mouseup",function(a){if(f)Guacamole.Event.DOMEvent.cancelEvent(a);else{var b=e[a.button];b&&c.release(b,a)}},!1);b.addEventListener("mouseout",function(a){a||(a=window.event);for(var d=a.relatedTarget||a.toElement;d;){if(d===b)return;d=d.parentNode}c.reset(a);c.out(a)}, +!1);b.addEventListener("selectstart",function(a){Guacamole.Event.DOMEvent.cancelEvent(a)},!1);b.addEventListener("touchmove",a,!1);b.addEventListener("touchstart",a,!1);b.addEventListener("touchend",a,!1);window.WheelEvent?b.addEventListener("wheel",d,!1):(b.addEventListener("DOMMouseScroll",d,!1),b.addEventListener("mousewheel",d,!1));var l=function(){var a=document.createElement("div");if(!("cursor"in a.style))return!1;try{a.style.cursor="url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX///+nxBvIAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg\x3d\x3d) 0 0, auto"}catch(m){return!1}return/\burl\([^()]*\)\s+0\s+0\b/.test(a.style.cursor|| +"")}();this.setCursor=function(a,c,d){return l?(a=a.toDataURL("image/png"),b.style.cursor="url("+a+") "+c+" "+d+", auto",!0):!1}};Guacamole.Mouse.State=function(b){var a=function(a,b,e,f,h,l,n){return{x:a,y:b,left:e,middle:f,right:h,up:l,down:n}};b=1=a.scrollThreshold&&(a.click(0=e.clickMoveThreshold}function d(a){a=a.touches[0];f=!0;h=a.clientX;l=a.clientY}function c(){window.clearTimeout(n);window.clearTimeout(m);f=!1}Guacamole.Mouse.Event.Target.call(this);var e=this,f=!1,h=null,l=null,n=null,m=null;this.scrollThreshold=20*(window.devicePixelRatio||1);this.clickTimingThreshold=250;this.clickMoveThreshold=16*(window.devicePixelRatio|| +1);this.longPressThreshold=500;b.addEventListener("touchend",function(d){if(f)if(0!==d.touches.length||1!==d.changedTouches.length)c();else if(window.clearTimeout(m),e.release(Guacamole.Mouse.State.Buttons.LEFT,d),!a(d)&&(d.preventDefault(),!e.currentState.left)){var g=d.changedTouches[0];e.move(Guacamole.Position.fromClientPosition(b,g.clientX,g.clientY));e.press(Guacamole.Mouse.State.Buttons.LEFT,d);n=window.setTimeout(function(){e.release(Guacamole.Mouse.State.Buttons.LEFT,d);c()},e.clickTimingThreshold)}}, +!1);b.addEventListener("touchstart",function(a){1!==a.touches.length?c():(a.preventDefault(),d(a),window.clearTimeout(n),m=window.setTimeout(function(){var d=a.touches[0];e.move(Guacamole.Position.fromClientPosition(b,d.clientX,d.clientY));e.click(Guacamole.Mouse.State.Buttons.RIGHT,a);c()},e.longPressThreshold))},!1);b.addEventListener("touchmove",function(d){if(f)if(a(d)&&window.clearTimeout(m),1!==d.touches.length)c();else if(e.currentState.left){d.preventDefault();var g=d.touches[0];e.move(Guacamole.Position.fromClientPosition(b, +g.clientX,g.clientY),d)}},!1)};Guacamole=Guacamole=Guacamole||{};Guacamole.Object=function(b,a){var d=this,c={};this.index=a;this.onbody=function(a,b,d){var e=c[d];if(e){var f=e.shift();0===e.length&&delete c[d];d=f}else d=null;d&&d(a,b)};this.onundefine=null;this.requestInputStream=function(a,f){if(f){var e=c[a];e||(e=[],c[a]=e);e.push(f)}b.requestObjectInputStream(d.index,a)};this.createOutputStream=function(a,c){return b.createObjectOutputStream(d.index,a,c)}};Guacamole.Object.ROOT_STREAM="/"; +Guacamole.Object.STREAM_INDEX_MIMETYPE="application/vnd.glyptodon.guacamole.stream-index+json";Guacamole=Guacamole||{}; +Guacamole.OnScreenKeyboard=function(b){var a=this,d={},c={},e=[],f=function(a,b){a.classList?a.classList.add(b):a.className+=" "+b},h=function(a,b){a.classList?a.classList.remove(b):a.className=a.className.replace(/([^ ]+)[ ]*/g,function(a,c){return c===b?"":a})},l=0,n=function(a,b,c,d){this.width=b;this.height=c;this.scale=function(e){a.style.width=b*e+"px";a.style.height=c*e+"px";d&&(a.style.lineHeight=c*e+"px",a.style.fontSize=e+"px")}},m=function(b){b=a.keys[b];if(!b)return null;for(var c=b.length- +1;0<=c;c--){var e=b[c];a:{var f=e.requires;for(var g=0;g=a?a:256<=a&&1114111>=a?16777216|a:null):a=null);this.keysym=a;this.modifier=b.modifier;this.requires=b.requires||[]};Guacamole=Guacamole||{};Guacamole.OutputStream=function(b,a){var d=this;this.index=a;this.onack=null;this.sendBlob=function(a){b.sendBlob(d.index,a)};this.sendEnd=function(){b.endStream(d.index)}}; +Guacamole=Guacamole||{}; +Guacamole.Parser=function(){var b=this,a="",d=[],c=-1,e=0,f=0;this.receive=function(h,l){l?a=h:(4096=e&&(a=a.substring(e),c-=e,e=0),a=a.length?a+h:h);for(;c=e){h=Guacamole.Parser.codePointCount(a,e,c);if(h=a.size)c&&c();else{var b=a.slice(f,f+262144); +f+=b.size;g.readAsText(b)}}};g.onload=b;b()}},D=function(a){a=a.length;for(var b=a+3;10<=a;)b++,a=Math.floor(a/10);return b};l.connect();l.getDisplay().showCursor(!1);var A=null,B=function(a,b){v+=D(a);for(var c=0;cc)return a-1}var d=Math.floor((a+b)/2),f=E(e[d].timestamp);return ca?K(a,d-1,c):c>f&&dg&&(k=E(e[n].timestamp),d.onseek(k,n-g,a-g));f.aborted||(nd.code||255=b){var c=0;var d=1}else if(2047>=b)c=192,d=2;else if(65535>=b)c=224,d=3;else if(2097151>=b)c=240,d=4;else{a(65533);return}var e=d;if(h+e>=f.length){var l=new Uint8Array(2*(h+e));l.set(f);f=l}h+=e;e=h-1;for(l=1;l>=6;f[e]=c|b}function d(b){for(var c=0;ca.readyState)){try{var f=a.status}catch(G){f=200}d||200!==f||(d=n());if(3===a.readyState||4===a.readyState)if(B(),1===q&&(3!==a.readyState||c?4===a.readyState&&c&&clearInterval(c):c=setInterval(b,30)),0===a.status)m.disconnect();else if(200!==a.status)h(a);else{try{var l=a.responseText}catch(G){return}try{g.receive(l,!0)}catch(G){e(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, +G.message))}}}}var c=null,d=null,f=0,g=new Guacamole.Parser;g.oninstruction=function N(b,e){if(b===Guacamole.Tunnel.INTERNAL_DATA_OPCODE&&0===e.length)g=new Guacamole.Parser,g.oninstruction=N,c&&clearInterval(c),a.onreadystatechange=null,a.abort(),d&&l(d);else if(b!==Guacamole.Tunnel.INTERNAL_DATA_OPCODE&&m.oninstruction)m.oninstruction(b,e)};a.onreadystatechange=1===q?function(){3===a.readyState&&(f++,2<=f&&(q=0,a.onreadystatechange=b));b()}:b;b()}function n(){var a=new XMLHttpRequest;a.open("GET", +k+m.uuid+":"+p++);a.setRequestHeader("Guacamole-Tunnel-Token",A);a.withCredentials=t;c(a,D);a.send(null);return a}var m=this,g=b+"?connect",k=b+"?read:",r=b+"?write:",q=1,v=!1,x="",t=!!a,y=null,u=null,C=null,D=d||{},A=null,B=function(){window.clearTimeout(y);window.clearTimeout(u);m.state===Guacamole.Tunnel.State.UNSTABLE&&m.setState(Guacamole.Tunnel.State.OPEN);y=window.setTimeout(function(){e(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT,"Server timeout."))},m.receiveTimeout);u=window.setTimeout(function(){m.setState(Guacamole.Tunnel.State.UNSTABLE)}, +m.unstableThreshold)};this.sendMessage=function(){m.isConnected()&&arguments.length&&(x+=Guacamole.Parser.toInstruction(arguments),v||f())};var p=0;this.connect=function(a){B();m.setState(Guacamole.Tunnel.State.CONNECTING);var b=new XMLHttpRequest;b.onreadystatechange=function(){4===b.readyState&&(200!==b.status?h(b):(B(),m.setUUID(b.responseText),(A=b.getResponseHeader("Guacamole-Tunnel-Token"))?(m.setState(Guacamole.Tunnel.State.OPEN),C=setInterval(function(){m.sendMessage("nop")},500),l(n())): +e(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND))))};b.open("POST",g,!0);b.withCredentials=t;c(b,D);b.setRequestHeader("Content-type","application/x-www-form-urlencoded; charset\x3dUTF-8");b.send(a)};this.disconnect=function(){e(new Guacamole.Status(Guacamole.Status.Code.SUCCESS,"Manually closed."))}};Guacamole.HTTPTunnel.prototype=new Guacamole.Tunnel; +Guacamole.WebSocketTunnel=function(b){function a(a){window.clearTimeout(f);window.clearTimeout(h);window.clearTimeout(l);if(d.state!==Guacamole.Tunnel.State.CLOSED){if(a.code!==Guacamole.Status.Code.SUCCESS&&d.onerror)d.onerror(a);d.setState(Guacamole.Tunnel.State.CLOSED);e.close()}}var d=this,c=null,e=null,f=null,h=null,l=null,n={"http:":"ws:","https:":"wss:"},m=0;if("ws:"!==b.substring(0,3)&&"wss:"!==b.substring(0,4))if(n=n[window.location.protocol],"/"===b.substring(0,1))b=n+"//"+window.location.host+ +b;else{var g=window.location.pathname.lastIndexOf("/");g=window.location.pathname.substring(0,g+1);b=n+"//"+window.location.host+g+b}var k=function(){var a=(new Date).getTime();d.sendMessage(Guacamole.Tunnel.INTERNAL_DATA_OPCODE,"ping",a);m=a},r=function(){window.clearTimeout(f);window.clearTimeout(h);window.clearTimeout(l);d.state===Guacamole.Tunnel.State.UNSTABLE&&d.setState(Guacamole.Tunnel.State.OPEN);f=window.setTimeout(function(){a(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, +"Server timeout."))},d.receiveTimeout);h=window.setTimeout(function(){d.setState(Guacamole.Tunnel.State.UNSTABLE)},d.unstableThreshold);var b=(new Date).getTime();b=Math.max(m+500-b,0);0 - +