Termix-SSH / Termix-SSH/Support
[BUG] Desktop RDP over a remote server fails: remote-sync JWT shadows the Guacamole token
Nobody has claimed this yet.
- 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
- Run a Termix server in Docker with a separate guacd container (GUACD_HOST=guacd).
- In the Windows desktop app, connect to that server via Remote Sync and sync hosts.
- Create or sync an RDP host and open it from the desktop app.
- The connection fails; the server logs "Token validation failed" followed by "Unexpected non-whitespace character after JSON at position 27".
- 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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