test_terminal / server.js
Henry
test
eaef622
const http = require('http');
const express = require('express');
const path = require('path');
const fs = require('fs');
const { WebSocketServer } = require('ws');
const { spawn } = require('child_process');
const pty = require('node-pty');
const app = express();
const server = http.createServer(app);
const PORT = process.env.PORT || 7860;
const TERMINAL_TOKEN = process.env.TERMINAL_TOKEN || ""; // set in env for security
const JOB_WEBHOOK_TOKEN = process.env.JOB_WEBHOOK_TOKEN || ""; // bearer token for webhook auth
const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || ""; // xoxb- token from Slack app
const SLACK_CHANNEL_ID = process.env.SLACK_CHANNEL_ID || ""; // channel ID to post heartbeat
const HEARTBEAT_LOG_PATH = process.env.HEARTBEAT_LOG_PATH || '/tmp/heartbeat.log';
// If behind a reverse proxy (nginx/traefik), trust X-Forwarded-* for req.ip
app.set('trust proxy', true);
// Serve static client
app.use(express.static(path.join(__dirname, 'public')));
// JSON parser for webhook
app.use(express.json());
// Basic liveness endpoint
app.get('/health', (_req, res) => res.status(200).send('ok'));
// Minimal webhook to send a heartbeat message to Slack
app.post('/tasks/heartbeat', async (req, res) => {
try {
const authHeader = req.headers['authorization'] || '';
const bearer = authHeader.startsWith('Bearer ')
? authHeader.slice('Bearer '.length)
: (req.query.token || '');
// Enforce only when JOB_WEBHOOK_TOKEN is set
if (JOB_WEBHOOK_TOKEN && bearer !== JOB_WEBHOOK_TOKEN) {
return res.sendStatus(401);
}
if (!SLACK_BOT_TOKEN || !SLACK_CHANNEL_ID) {
return res.status(500).json({ error: 'Slack not configured' });
}
const now = new Date().toISOString();
const host = req.headers.host || 'unknown-host';
const text = `Heartbeat: VPS web terminal alive at ${now} (host: ${host})`;
const resp = await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: {
'Authorization': `Bearer ${SLACK_BOT_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ channel: SLACK_CHANNEL_ID, text })
});
const data = await resp.json().catch(() => ({}));
const ip = req.ip;
const ua = req.headers['user-agent'] || '';
const logLine = `${now}\tip=${ip}\tua=${JSON.stringify(ua)}\tstatus=${data && data.ok ? 'ok' : 'error'}\n`;
try {
const dir = path.dirname(HEARTBEAT_LOG_PATH);
fs.mkdirSync(dir, { recursive: true });
fs.appendFileSync(HEARTBEAT_LOG_PATH, logLine, 'utf8');
} catch (_) { /* ignore log failures */ }
// Kick off pCloud backup: ~/.claude -> /claude-vps/.claude and ~/.claude.json -> /claude-vps (non-blocking)
try {
const child = spawn('python', ['scripts/upload_claude_to_pcloud.py'], {
cwd: __dirname,
stdio: 'inherit',
env: process.env,
detached: false
});
// no await; run in background
} catch (_) { /* ignore spawn errors */ }
if (!data.ok) {
return res.status(502).json({ error: 'Slack API error', details: data });
}
return res.status(202).json({ ok: true, ts: data.ts, channel: data.channel });
} catch (err) {
return res.status(500).json({ error: 'server_error' });
}
});
// WebSocket for terminal
const wss = new WebSocketServer({ server, path: '/ws' });
wss.on('connection', (ws, req) => {
try {
// Simple shared-secret check via query string: /ws?token=...
const url = new URL(req.url, `http://${req.headers.host}`);
const token = url.searchParams.get('token') || '';
if (TERMINAL_TOKEN && token !== TERMINAL_TOKEN) {
ws.close(1008, 'Invalid token'); // Policy Violation
return;
}
const shell = process.env.SHELL || '/bin/bash';
const ptyProcess = pty.spawn(shell, ['-l'], {
name: 'xterm-256color',
cols: 80,
rows: 24,
cwd: process.env.HOME || process.cwd(),
env: process.env
});
ptyProcess.onData(data => {
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({ type: 'output', data }));
}
});
ws.on('message', (msg) => {
try {
const { type, data, cols, rows } = JSON.parse(msg.toString());
if (type === 'input' && typeof data === 'string') {
ptyProcess.write(data);
} else if (type === 'resize' && Number.isInteger(cols) && Number.isInteger(rows)) {
ptyProcess.resize(cols, rows);
}
} catch (_) {/* ignore malformed */}
});
ws.on('close', () => {
try { ptyProcess.kill(); } catch (_) {}
});
} catch (err) {
try { ws.close(1011, 'Server error'); } catch (_) {}
}
});
server.listen(PORT, '0.0.0.0', () => {
console.log(`Web terminal listening on :${PORT}`);
});