shell-mirror
Version:
Access your Mac shell from any device securely. Perfect for mobile coding with Claude Code CLI, Gemini CLI, and any shell tool.
381 lines (323 loc) • 13.1 kB
JavaScript
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const path = require('path');
const session = require('express-session');
const passport = require('passport');
const { v4: uuidv4 } = require('uuid');
const fs = require('fs');
require('dotenv').config();
require('./auth'); // Configure passport strategies
// Enhanced logging to file
const LOG_FILE = path.join(__dirname, 'server-debug.log');
function logToFile(message) {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${message}\n`;
console.log(logEntry.trim()); // Also log to console
fs.appendFileSync(LOG_FILE, logEntry);
}
// Clear previous log
fs.writeFileSync(LOG_FILE, `=== Server Debug Log Started ${new Date().toISOString()} ===\n`);
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
// --- Session Management ---
const sessionParser = session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days in milliseconds
}
});
app.use(sessionParser);
app.use(passport.initialize());
app.use(passport.session());
// --- In-memory data stores ---
const agents = new Map(); // Stores connected agent sockets
const clients = new Map(); // Stores connected client (browser) sockets
const dashboards = new Map(); // Stores connected dashboard sockets
const sessions = new Map(); // Maps session IDs to agent/client pairs
// --- Environment Detection ---
const isLocalEnvironment = process.env.BASE_URL && process.env.BASE_URL.includes('localhost');
const isProduction = process.env.NODE_ENV === 'production';
logToFile(`🔧 Environment: ${isProduction ? 'PRODUCTION' : 'DEVELOPMENT'}`);
logToFile(`🔧 Local environment: ${isLocalEnvironment ? 'YES' : 'NO'}`);
// --- Static Files and Routes ---
app.use(express.static('public'));
// Health check endpoint for Heroku
app.get('/health', (req, res) => {
res.json({
status: 'ok',
service: 'Shell Mirror WebSocket Server',
timestamp: new Date().toISOString()
});
});
// Inject environment info into static files
app.get('/', (req, res) => {
// LOCAL_TESTING_ONLY: Add local testing indicator
if (isLocalEnvironment && !isProduction) {
const indexContent = require('fs').readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');
const modifiedContent = indexContent.replace(
'<body>',
`<body>
<!-- LOCAL_TESTING_ONLY -->
<div id="local-testing-banner" style="background: #ff9800; color: white; text-align: center; padding: 10px; font-weight: bold;">
🔧 LOCAL DEVELOPMENT MODE - <a href="/app/local-test" style="color: white; text-decoration: underline;">Direct Local Testing</a>
</div>
<!-- END LOCAL_TESTING_ONLY -->`
);
res.send(modifiedContent);
} else {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
}
});
// LOCAL_TESTING_ONLY: Simple bypass for local development
if (isLocalEnvironment && !isProduction) {
app.get('/auth/local/bypass', (req, res) => {
// Create a fake user session for local testing
req.login({ id: 'local-test-user', name: 'Local Test User', email: 'test@local.dev' }, (err) => {
if (err) return res.status(500).send('Login error');
res.redirect('/app');
});
});
app.get('/app/local-test', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'app', 'local_test.html'));
});
}
app.get('/app', (req, res) => {
// LOCAL_TESTING_ONLY: Allow unauthenticated access in local environment
if (isLocalEnvironment && !isProduction) {
if (!req.isAuthenticated()) {
// Auto-login for local testing
req.login({ id: 'local-test-user', name: 'Local Test User', email: 'test@local.dev' }, (err) => {
if (err) return res.redirect('/auth/google');
return res.sendFile(path.join(__dirname, 'public', 'app', 'terminal.html'));
});
return;
}
} else {
// Production: Strict Google OAuth requirement
if (!req.isAuthenticated()) {
return res.redirect('/auth/google');
}
}
res.sendFile(path.join(__dirname, 'public', 'app', 'terminal.html'));
});
// --- Authentication Routes ---
app.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login-failed.html' }),
(req, res) => res.redirect('/app')
);
app.get('/api/auth/status', (req, res) => {
if (req.isAuthenticated()) {
res.json({ authenticated: true, user: req.user });
} else {
res.json({ authenticated: false });
}
});
app.post('/api/auth/logout', (req, res, next) => {
req.logout(err => {
if (err) { return next(err); }
res.redirect('/');
});
});
// --- WebRTC Signaling Logic ---
wss.on('connection', (ws, req) => {
const params = new URLSearchParams(req.url.slice(1));
const role = params.get('role');
const agentId = params.get('agentId');
if (role === 'dashboard') {
// --- Dashboard Connection ---
sessionParser(req, {}, () => {
let userId;
let isAuthenticated = false;
if (req.session && req.session.passport && req.session.passport.user) {
userId = req.session.passport.user.id;
isAuthenticated = true;
logToFile(`✅ Authenticated dashboard connected: ${userId}`);
} else if (isLocalEnvironment && !isProduction) {
// LOCAL_TESTING_ONLY: Allow unauthenticated connections for local testing
userId = `local-test-dashboard-${uuidv4()}`;
logToFile(`🔧 LOCAL TESTING: Unauthenticated dashboard connected: ${userId}`);
} else {
// Production: Reject unauthenticated connections
logToFile('❌ Unauthenticated dashboard rejected in production environment');
ws.close(1008, 'Authentication required');
return;
}
ws.userId = userId;
dashboards.set(userId, ws);
// Send initial agent list
const agentsList = Array.from(agents.keys()).map(id => ({ id }));
ws.send(JSON.stringify({ type: 'agent-list', agents: agentsList }));
logToFile(`📊 Sent initial agent list to dashboard: ${agentsList.length} agents`);
ws.on('close', () => {
logToFile(`🔌 Dashboard disconnected: ${userId}`);
dashboards.delete(userId);
});
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
if (data.type === 'ping') {
ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
}
} catch (error) {
logToFile(`❌ Error handling dashboard message: ${error.message}`);
}
});
});
} else if (role === 'discovery') {
// --- Agent Discovery Connection ---
logToFile('📍 Discovery client connected');
ws.on('message', (message) => {
const data = JSON.parse(message);
if (data.type === 'list-agents') {
const agentsList = Array.from(agents.keys()).map(id => ({ id }));
logToFile(`📋 Sending agent list: ${JSON.stringify(agentsList)}`);
ws.send(JSON.stringify({ type: 'agent-list', agents: agentsList }));
ws.close(); // Close discovery connection after sending list
}
});
} else if (role === 'agent') {
// --- Agent Connection ---
logToFile(`🖥️ Agent connected: ${agentId}`);
ws.agentId = agentId;
agents.set(agentId, ws);
// Notify all dashboards of new agent
notifyAgentConnected(agentId);
ws.on('close', () => {
logToFile(`🔌 Agent disconnected: ${agentId}`);
agents.delete(agentId);
// Notify all dashboards of agent disconnection
notifyAgentDisconnected(agentId);
});
ws.on('message', (message) => handleSignalingMessage(ws, message, 'agent'));
} else if (role === 'client') {
// --- Client (Browser) Connection ---
sessionParser(req, {}, () => {
let userId;
let isAuthenticated = false;
if (req.session && req.session.passport && req.session.passport.user) {
userId = req.session.passport.user.id;
isAuthenticated = true;
logToFile(`✅ Authenticated client connected: ${userId}`);
} else if (isLocalEnvironment && !isProduction) {
// LOCAL_TESTING_ONLY: Allow unauthenticated connections for local testing
userId = `local-test-client-${uuidv4()}`;
logToFile(`🔧 LOCAL TESTING: Unauthenticated client connected: ${userId}`);
} else {
// Production: Reject unauthenticated connections
logToFile('❌ Unauthenticated client rejected in production environment');
ws.close(1008, 'Authentication required');
return;
}
ws.userId = userId;
clients.set(userId, ws);
// Send the client its assigned ID
ws.send(JSON.stringify({ type: 'server-hello', id: userId }));
logToFile(`📧 Sent server-hello to client: ${userId}`);
ws.on('close', () => {
logToFile(`🔌 Client disconnected: ${userId}`);
clients.delete(userId);
// Notify agent if in a session
});
ws.on('message', (message) => handleSignalingMessage(ws, message, 'client'));
});
} else {
ws.close(1008, 'Invalid role specified');
}
});
// --- Dashboard Broadcast Functions ---
function broadcastToDashboards(message) {
const messageStr = JSON.stringify(message);
dashboards.forEach((dashboardWs, userId) => {
if (dashboardWs.readyState === WebSocket.OPEN) {
dashboardWs.send(messageStr);
logToFile(`📡 Broadcasted to dashboard ${userId}: ${message.type}`);
} else {
// Clean up closed connections
dashboards.delete(userId);
logToFile(`🔌 Removed closed dashboard connection: ${userId}`);
}
});
}
function notifyAgentConnected(agentId) {
broadcastToDashboards({
type: 'agent-connected',
agentId: agentId,
timestamp: Date.now()
});
logToFile(`📢 Notified dashboards of agent connection: ${agentId}`);
}
function notifyAgentDisconnected(agentId) {
broadcastToDashboards({
type: 'agent-disconnected',
agentId: agentId,
timestamp: Date.now()
});
logToFile(`📢 Notified dashboards of agent disconnection: ${agentId}`);
}
function handleSignalingMessage(ws, rawMessage, senderRole) {
try {
const message = JSON.parse(rawMessage);
logToFile(`📨 Handling message: type=${message.type} from=${message.from} to=${message.to} senderRole=${senderRole}`);
if (message.type === 'client-hello') {
logToFile(`🔔 CLIENT-HELLO: Client ${message.from} wants to connect to agent ${message.to}`);
}
const { to: target, from } = message;
let targetWs;
let targetType;
if (senderRole === 'agent') {
targetWs = clients.get(target);
targetType = 'client';
} else { // sender is 'client'
targetWs = agents.get(target);
targetType = 'agent';
}
logToFile(`🎯 Looking for ${targetType} with ID: ${target}`);
logToFile(`📊 Available agents: ${JSON.stringify(Array.from(agents.keys()))}`);
logToFile(`📊 Available clients: ${JSON.stringify(Array.from(clients.keys()))}`);
if (targetWs && targetWs.readyState === WebSocket.OPEN) {
logToFile(`✅ Relaying ${message.type} from ${senderRole} (${from}) to ${targetType} (${target})`);
targetWs.send(rawMessage);
if (message.type === 'client-hello') {
logToFile(`🚀 CLIENT-HELLO successfully relayed to Mac agent!`);
}
} else {
const status = targetWs ? `connection closed (readyState: ${targetWs.readyState})` : 'not found';
logToFile(`❌ FAILED to relay ${message.type}. Target ${targetType} (${target}) ${status}`);
if (message.type === 'client-hello') {
logToFile(`🚨 CLIENT-HELLO relay failed! Mac agent may have disconnected.`);
}
}
} catch (error) {
logToFile(`❌ Error handling signaling message: ${error.message} Raw message: ${rawMessage}`);
}
}
// --- Server Startup ---
const PORT = process.env.WS_PORT || process.env.PORT || 8080;
const HOST = process.env.WS_HOST || process.env.HOST || '0.0.0.0';
const LOCAL_IP = process.env.WS_LOCAL_IP || process.env.LOCAL_IP || 'localhost';
server.listen(PORT, HOST, () => {
const baseUrl = process.env.BASE_URL || `http://${LOCAL_IP}:${PORT}`;
logToFile(`✅ Local WebSocket server is running on ${baseUrl}`);
logToFile(`🌐 WebSocket URL: ws://${LOCAL_IP}:${PORT}`);
logToFile(`🔧 Binding to: ${HOST}:${PORT}`);
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully...');
// Forcefully close all connected WebSocket clients
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.terminate();
}
});
server.close(() => {
console.log('Server has been shut down.');
process.exit(0);
});
});