feat(terminal): proxy /terminal (+WebSocket) through the Void app to ttyd

Makes the embedded Terminal work via the raw LAN IP too (bypasses Traefik's
/terminal route). ttyd base-path preserved; firewall on CT300 opened to the app host.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
root
2026-06-04 23:44:31 +10:00
parent 5aba750102
commit 4a55c24700
3 changed files with 74 additions and 3 deletions

View File

@@ -11,11 +11,29 @@ import { startCron } from './lib/cron/index.js';
import { seedFromConfig } from './lib/health/registry.js';
import { mcpAuth } from './lib/api/middleware/mcp_auth.js';
import { handleMcp } from './lib/mcp/http.js';
import httpProxy from 'http-proxy';
const VERSION = '2.0.0-alpha.16';
// Proxy /terminal (+ its WebSocket) to ttyd on CT 300, so the embedded terminal
// works whether the Void is reached via Traefik (void2-app.hynesy.com) OR the
// raw LAN IP (which bypasses Traefik's /terminal route). ttyd's base-path is
// /terminal, so the path is preserved as-is.
const TTYD_URL = process.env.TTYD_URL || 'http://192.168.1.212:7681';
const terminalProxy = httpProxy.createProxyServer({ target: TTYD_URL, ws: true, changeOrigin: true });
terminalProxy.on('error', (err) => log.error({ err }, 'terminal proxy error'));
const isTerminalPath = (url = '') => url === '/terminal' || url.startsWith('/terminal/') || url.startsWith('/terminal?');
export function createApp() {
const app = express();
// Terminal proxy first — before body parsing/static — so the raw request
// stream is forwarded untouched to ttyd.
app.use((req, res, next) => {
if (isTerminalPath(req.url)) return terminalProxy.web(req, res, {}, () => { res.status(502).end('terminal unavailable'); });
next();
});
app.use(express.json({
limit: '10mb',
verify: (req, _res, buf) => { req.rawBody = buf; }
@@ -71,7 +89,12 @@ if (import.meta.url === `file://${process.argv[1]}`) {
// One-time bootstrap of the service registry from config/services.json if empty.
seedFromConfig().then(n => { if (n) log.info({ seeded: n }, 'monitored_services seeded from config'); })
.catch(err => log.error({ err }, 'service registry seed failed'));
app.listen(port, () => log.info({ port }, 'void-server listening'));
const server = app.listen(port, () => log.info({ port }, 'void-server listening'));
// Proxy the terminal's WebSocket upgrade to ttyd as well.
server.on('upgrade', (req, socket, head) => {
if (isTerminalPath(req.url)) terminalProxy.ws(req, socket, head);
else socket.destroy();
});
for (const sig of ['SIGTERM', 'SIGINT']) {
process.on(sig, async () => {
log.info({ sig }, 'shutting down');