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:
51
package-lock.json
generated
51
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "void-server",
|
"name": "void-server",
|
||||||
"version": "2.0.0-alpha.6",
|
"version": "2.0.0-alpha.16",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "void-server",
|
"name": "void-server",
|
||||||
"version": "2.0.0-alpha.6",
|
"version": "2.0.0-alpha.16",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"@mozilla/readability": "^0.6.0",
|
"@mozilla/readability": "^0.6.0",
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"dompurify": "^3.4.7",
|
"dompurify": "^3.4.7",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"http-proxy": "^1.18.1",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
"marked": "^18.0.4",
|
"marked": "^18.0.4",
|
||||||
"multer": "^2.1.1",
|
"multer": "^2.1.1",
|
||||||
@@ -1548,6 +1549,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/eventsource": {
|
"node_modules/eventsource": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
||||||
@@ -1713,6 +1720,26 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||||
|
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
@@ -1966,6 +1993,20 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/http-proxy": {
|
||||||
|
"version": "1.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||||
|
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"eventemitter3": "^4.0.0",
|
||||||
|
"follow-redirects": "^1.0.0",
|
||||||
|
"requires-port": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.7.2",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||||
@@ -3185,6 +3226,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/requires-port": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"dompurify": "^3.4.7",
|
"dompurify": "^3.4.7",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"http-proxy": "^1.18.1",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
"marked": "^18.0.4",
|
"marked": "^18.0.4",
|
||||||
"multer": "^2.1.1",
|
"multer": "^2.1.1",
|
||||||
|
|||||||
25
server.js
25
server.js
@@ -11,11 +11,29 @@ import { startCron } from './lib/cron/index.js';
|
|||||||
import { seedFromConfig } from './lib/health/registry.js';
|
import { seedFromConfig } from './lib/health/registry.js';
|
||||||
import { mcpAuth } from './lib/api/middleware/mcp_auth.js';
|
import { mcpAuth } from './lib/api/middleware/mcp_auth.js';
|
||||||
import { handleMcp } from './lib/mcp/http.js';
|
import { handleMcp } from './lib/mcp/http.js';
|
||||||
|
import httpProxy from 'http-proxy';
|
||||||
|
|
||||||
const VERSION = '2.0.0-alpha.16';
|
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() {
|
export function createApp() {
|
||||||
const app = express();
|
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({
|
app.use(express.json({
|
||||||
limit: '10mb',
|
limit: '10mb',
|
||||||
verify: (req, _res, buf) => { req.rawBody = buf; }
|
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.
|
// 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'); })
|
seedFromConfig().then(n => { if (n) log.info({ seeded: n }, 'monitored_services seeded from config'); })
|
||||||
.catch(err => log.error({ err }, 'service registry seed failed'));
|
.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']) {
|
for (const sig of ['SIGTERM', 'SIGINT']) {
|
||||||
process.on(sig, async () => {
|
process.on(sig, async () => {
|
||||||
log.info({ sig }, 'shutting down');
|
log.info({ sig }, 'shutting down');
|
||||||
|
|||||||
Reference in New Issue
Block a user