Skip to content

Fix terminal plugin bugs - #238

Merged
melvincarvalho merged 2 commits into
gh-pagesfrom
issue-237-terminal-fixes
Mar 24, 2026
Merged

Fix terminal plugin bugs#238
melvincarvalho merged 2 commits into
gh-pagesfrom
issue-237-terminal-fixes

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Summary

  • Socket reference: Use connection.socket || connection fallback for @fastify/websocket compatibility across versions
  • Shell command: Spawn /bin/bash -i (interactive) instead of /bin/sh for proper prompt and shell features
  • PTY newlines: Convert \n to \r\n on stdout/stderr before sending to WebSocket, fixing terminal rendering
  • Public mode auth: Skip authentication when options.public is true; pass public option from server.js to the terminal plugin

Fixes #237

Test plan

  • Start server with --terminal --public and connect via WebSocket — should get a shell without auth
  • Start server with --terminal (no --public) without auth token — should get "Authentication required" error
  • Verify terminal output renders newlines correctly (no missing carriage returns)
  • Verify bash prompt appears (interactive shell)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adjusts the --terminal WebSocket shell feature to improve runtime compatibility with newer @fastify/websocket handler shapes, improve interactive shell UX, and change authentication behavior when the server is started in public mode.

Changes:

  • Use connection.socket || connection to support both old and new @fastify/websocket connection handler shapes.
  • Spawn an interactive bash (/bin/bash -i) instead of /bin/sh.
  • Normalize PTY output newlines by translating \n to \r\n before sending over WebSocket.
  • Pass public from server.js into the terminal plugin and skip auth when public is enabled.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/terminal/index.js Updates WebSocket socket selection, authentication gating, shell spawning, and output newline normalization.
src/server.js Passes public option into terminal plugin registration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/terminal/index.js
Comment on lines 75 to 78
shell.stdout.on('data', (data) => {
if (socket.readyState === 1) {
try { socket.send(data); } catch { /* socket closed */ }
try { socket.send(data.toString().replace(/\n/g, '\r\n')); } catch { /* socket closed */ }
}

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replacing every \n with \r\n will turn existing CRLF sequences into \r\r\n, which can cause extra blank lines in terminals that already emit CRLF. Use a pattern that normalizes both LF and CRLF (e.g., replace \r?\n), or otherwise avoid doubling carriage returns.

Copilot uses AI. Check for mistakes.
Comment thread src/terminal/index.js
Comment on lines 82 to 85
shell.stderr.on('data', (data) => {
if (socket.readyState === 1) {
try { socket.send(data); } catch { /* socket closed */ }
try { socket.send(data.toString().replace(/\n/g, '\r\n')); } catch { /* socket closed */ }
}

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same CRLF-normalization issue on stderr: converting all \n to \r\n can double existing CRLF (\r\r\n). Normalize \r?\n instead to avoid introducing extra carriage returns.

Copilot uses AI. Check for mistakes.
Comment thread src/terminal/index.js Outdated

// Spawn shell
const shell = spawn('/bin/sh', [], {
const shell = spawn('/bin/bash', ['-i'], {

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coding /bin/bash reduces portability (e.g., minimal/distroless images or Alpine may not have bash at that path) and may cause the terminal feature to fail at runtime. Consider resolving the shell from process.env.SHELL, using bash via PATH, or falling back to /bin/sh when bash isn't available.

Suggested change
const shell = spawn('/bin/bash', ['-i'], {
const shellCommand = process.env.SHELL || 'bash';
const shell = spawn(shellCommand, ['-i'], {

Copilot uses AI. Check for mistakes.
Comment thread src/terminal/index.js
Comment on lines +60 to 64
if (!webId && !options.public) {
socket.send(JSON.stringify({ type: 'error', message: 'Authentication required' }));
socket.close();
return;
}

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are existing WebSocket plugin tests (e.g. tunnel/webrtc), but no automated coverage around the terminal plugin’s auth behavior. Given the changed semantics (public vs authenticated), add tests to ensure unauthenticated connections are rejected by default and only allowed under an explicit opt-in, and that newline normalization behaves as expected.

Copilot uses AI. Check for mistakes.
Comment thread src/terminal/index.js
Comment on lines +60 to 64
if (!webId && !options.public) {
socket.send(JSON.stringify({ type: 'error', message: 'Authentication required' }));
socket.close();
return;
}

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tying terminal authentication to options.public effectively makes /.terminal an unauthenticated remote shell whenever the server runs in public mode. Since public is intended to bypass WAC for resource access, reusing it here is a major security footgun. Consider requiring a dedicated explicit opt-in (e.g. terminalPublic / allowUnauthenticatedTerminal) and/or restricting unauthenticated access to loopback only, while keeping auth required by default even in public mode.

Copilot uses AI. Check for mistakes.
Comment thread src/server.js
// Register terminal (WebSocket shell) if enabled
if (terminalEnabled) {
fastify.register(terminalPlugin, { path: '/.terminal' });
fastify.register(terminalPlugin, { path: '/.terminal', public: options.public || false });

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing public: options.public into the terminal plugin means starting the server in public mode will expose an unauthenticated WebSocket shell. This is likely too dangerous for a flag that is meant for public resource access; consider a separate CLI/server option to explicitly enable unauthenticated terminal access (or omit this option entirely).

Suggested change
fastify.register(terminalPlugin, { path: '/.terminal', public: options.public || false });
fastify.register(terminalPlugin, { path: '/.terminal' });

Copilot uses AI. Check for mistakes.
@melvincarvalho
melvincarvalho requested a review from Copilot March 24, 2026 06:04
@melvincarvalho
melvincarvalho merged commit 2e6c586 into gh-pages Mar 24, 2026
2 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/terminal/index.js
Comment on lines +67 to 71
const shellCommand = process.env.SHELL || 'bash';
const shell = spawn(shellCommand, ['-i'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, TERM: 'xterm-256color' },
});

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shellCommand falls back to 'bash', which can fail on systems where bash isn’t installed (e.g., minimal images) and is a behavior change from the previous /bin/sh. Consider falling back to /bin/sh (or /bin/bash if present) when process.env.SHELL is unset or invalid, and/or allow an explicit config option/env var to choose the shell.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix terminal plugin: socket reference, PTY newlines, public mode auth

2 participants