mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
fix: issue short-lived console tickets instead of JWTs in node_console MCP tool
LLM clients transcribing the console WebSocket URL into shell commands
reliably corrupted the ~200-char JWT embedded in it (dropped header
segment -> "MissingAlgorithmError: Missing 'alg' value in header" on
every connection attempt). The node_console tool now mints a short
random ticket ("gns3t_" + 16 urlsafe chars, 10 min TTL, multi-use)
stored server-side and bound to the node's console endpoints:
- new ConsoleTicketService (gns3server/services/console_tickets.py),
in-memory store with lazy expiry sweeps
- get_current_active_user_from_websocket redeems tickets through the
existing "token" query parameter, gated on websocket.path_params so a
ticket only authenticates the console/ws and console/vnc routes of
the node it was minted for; the JWT path is unchanged
- redemption reuses the existing user lookup, token_version revocation
and is_active checks, so logging out invalidates outstanding tickets
- vnc_url no longer embeds the full session JWT
- the tool docstring now tells clients to run the returned command
verbatim instead of reconstructing the URL
This commit is contained in:
9 files changed
+431
-30
No files matched your search
@@ -280,10 +280,38 @@ class TestNode:
|
||||
|
||||
def test_console(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_console_info_handler
|
||||
from gns3server.services import console_ticket_service
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"console_url": "ws://host/console"})
|
||||
result = get_node_console_info_handler({"project_id": "p1", "node_id": "n1"}, ctx)
|
||||
assert "command" in result
|
||||
# the URL embeds a short console ticket (not a JWT) redeemable for this node
|
||||
assert "?token=gns3t_" in result["ws_url"]
|
||||
ticket = result["ws_url"].split("token=")[1]
|
||||
assert ticket in result["command"]
|
||||
assert result["token_ttl_seconds"] == 600
|
||||
assert len(result["token_sha256_prefix"]) == 8
|
||||
redeemed = console_ticket_service.redeem(ticket, {"project_id": "p1", "node_id": "n1"})
|
||||
assert redeemed is not None and redeemed.username == "admin"
|
||||
assert "vnc_url" not in result # console_type is not vnc
|
||||
|
||||
def test_console_vnc_uses_same_ticket(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_console_info_handler
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"console_type": "vnc"})
|
||||
result = get_node_console_info_handler({"project_id": "p1", "node_id": "n1"}, ctx)
|
||||
ticket = result["ws_url"].split("token=")[1]
|
||||
# one node-bound ticket covers both console endpoints (identical path params)
|
||||
assert result["vnc_url"] == f"/v3/projects/p1/nodes/n1/console/vnc?token={ticket}"
|
||||
|
||||
def test_console_without_username_omits_token(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_console_info_handler
|
||||
unauthenticated_ctx = {k: v for k, v in ctx.items() if k != "jwt_username"}
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"console_url": "ws://host/console"})
|
||||
result = get_node_console_info_handler({"project_id": "p1", "node_id": "n1"}, unauthenticated_ctx)
|
||||
assert "token=" not in result["ws_url"]
|
||||
assert "token_sha256_prefix" not in result
|
||||
|
||||
@staticmethod
|
||||
def _file_conn(text):
|
||||
|
||||
Reference in new issue
Block a user