Termix-SSH / Termix-SSH/Support

[BUG] Desktop RDP over a remote server fails: remote-sync JWT shadows the Guacamole token

Open Beginner friendly
#1,219 1 comment 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug good-first-issue platform-desktop platform-docker platform-windows rdp-vnc
Dominant language
No language data
Stars
28
Forks
4
PR merge metrics
No merged PRs in 30d

Description

Title

Desktop: RDP through a remote server fails — the remote-sync JWT is appended as ?token= and shadows the Guacamole token

Platform

Desktop App - Windows

Server Installation Method

Docker

Version

2.7.1

CLI Installation Method

None

CLI Version

No response

Troubleshooting
  • I have examined logs and tried to find the issue
  • I have reviewed opened and closed issues
  • I have tried restarting the application
  • I have checked open issues and ensured this is not a duplicate
The Problem

Opening an RDP host from the Windows desktop app fails immediately with a connection error. The same host, with the same credentials, opened from the web UI of the same server works fine — so guacd, the credentials, the security mode and the RDP target are all confirmed good. Only the desktop app is affected.

Server log for a failing attempt:

[10:57:57 AM] [ERROR] [Connection #4]  Token validation failed
[10:57:57 AM] [ERROR] [Connection #4]  Closing connection with error:
    Unexpected non-whitespace character after JSON at position 27 (line 1 column 28)
[10:57:57 AM] [ERROR] Unhandled promise rejection [op:error_handling]
TypeError: Cannot read properties of undefined (reading 'connection')
    at ClientConnection.connect (/app/node_modules/guacamole-lite/lib/ClientConnection.js:83:37)
    at Server.newConnection (/app/node_modules/guacamole-lite/lib/Server.js:528:23)

position 27 is not arbitrary: {"alg":"HS256","typ":"JWT"} is exactly 27 bytes — the base64-decoded header of a Termix auth JWT. guacamole-lite is receiving a JWT in the token query parameter instead of the encrypted Guacamole token.

The failure is intermittent. Occasionally an attempt gets through (Guacamole connection opened) and then fails on the RDP layer instead, which matches the JWT being resolved asynchronously via get-remote-sync-jwt.

Setup: Termix server 2.7.1 in Docker, guacamole/guacd:1.6.0 in a separate container with GUACD_HOST=guacd, desktop app connected to that server through Remote Sync with hosts synced. Connection type RDP, so the origin is forced to remote by resolveConnectionOrigin().

How to Reproduce
  1. Run a Termix server in Docker with a separate guacd container (GUACD_HOST=guacd).
  2. In the Windows desktop app, connect to that server via Remote Sync and sync hosts.
  3. Create or sync an RDP host and open it from the desktop app.
  4. The connection fails; the server logs "Token validation failed" followed by "Unexpected non-whitespace character after JSON at position 27".
  5. Open the same host from the web UI of the same server — it connects normally.
Additional Context
Root cause

src/ui/lib/connection-origin.ts:

export async function buildOriginWsUrl({ origin, localPort, localPath, remotePath,
                                         includeLocalJwt = true }) {
  if (origin === "local") {
    let url = `ws://127.0.0.1:${localPort}${localPath}`;
    if (includeLocalJwt) {            // <-- flag honoured here
      const token = localStorage.getItem("jwt");
      if (token) url += `?token=${encodeURIComponent(token)}`;
    }
    return url;
  }

  const remote = await getRemoteConnectionTarget();
  if (!remote) return null;
  ...
  if (remote.jwt) url += `?token=${encodeURIComponent(remote.jwt)}`;   // <-- flag ignored here
  return url;
}

src/ui/features/guacamole/GuacamoleDisplay.tsx calls it with includeLocalJwt: false, so the intent to keep the JWT out of the Guacamole socket is explicit — but the remote branch appends it unconditionally. Then:

const tunnel = new Guacamole.WebSocketTunnel(wsConnection.url);  // already ...?token=<JWT>
client.connect(wsConnection.query);                              // token=<guac>&width=...

WebSocketTunnel.connect() concatenates url + "?" + data, producing two ? and two token parameters. The server-side query parser keeps the first one — the JWT.

Minimal reproduction of the parse failure
const jwtHeader = Buffer.from(JSON.stringify({alg:"HS256",typ:"JWT"})).toString('base64url');
const jwt = `${jwtHeader}.${Buffer.from(JSON.stringify({userId:"abc"})).toString('base64url')}.SIG`;
const guacToken = Buffer.from(JSON.stringify({iv:"AAAA",value:"ZmFrZQ=="})).toString('base64');

const wsBase   = `ws://server:8080/guacamole/websocket/?token=${encodeURIComponent(jwt)}`;
const finalUrl = `${wsBase}?token=${guacToken}&width=1280&height=720`;

const received = new URL(finalUrl).searchParams.get('token');
JSON.parse(Buffer.from(received, 'base64').toString('utf8'));
// SyntaxError: Unexpected non-whitespace character after JSON at position 27 (line 1 column 28)

Byte-for-byte the same message as the server log.

Why the web UI is unaffected

The non-Electron path uses buildGuacamoleWebSocketBaseUrl(), which never appends a JWT, so client.connect(query) supplies the only token parameter.

Suggested fix

Honour the existing flag on the remote branch as well:

if (includeLocalJwt && remote.jwt) url += `?token=${encodeURIComponent(remote.jwt)}`;

The parameter name would then be misleading — includeJwt reads better. Guarding against a second ? when building the tunnel URL would also make this class of bug impossible.

Impact

RDP, VNC and Telnet are unusable from the desktop app once a remote-sync JWT is present, which is always after the first successful sync. Since resolveConnectionOrigin() pins these protocols to remote, the only fallback inside the app is the native Windows RDP client.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/ui/lib/connection-origin.ts and compare the remote branch of buildOriginWsUrl with the includeLocalJwt: false call in src/ui/features/guacamole/GuacamoleDisplay.tsx. Verify that a remote-sync JWT is not added when requested, that the WebSocket URL does not create duplicate token parameters, and reproduce an RDP connection from the Windows desktop app to confirm the Guacamole token is accepted.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
desktop, networking
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
85/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.