shell-mirror 1.5.65 → 1.5.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mac-agent/agent.js +116 -1
- package/package.json +1 -1
- package/public/app/dashboard.js +7 -4
- package/public/app/terminal.js +198 -6
package/mac-agent/agent.js
CHANGED
|
@@ -419,7 +419,9 @@ async function sendHeartbeat() {
|
|
|
419
419
|
const heartbeatData = JSON.stringify({
|
|
420
420
|
agentId: AGENT_ID,
|
|
421
421
|
timestamp: Date.now(),
|
|
422
|
-
activeSessions: Object.keys(sessions).length
|
|
422
|
+
activeSessions: Object.keys(sessionManager.sessions).length,
|
|
423
|
+
localPort: process.env.LOCAL_PORT || 8080,
|
|
424
|
+
capabilities: ['webrtc', 'direct_websocket']
|
|
423
425
|
});
|
|
424
426
|
|
|
425
427
|
const options = {
|
|
@@ -945,6 +947,7 @@ process.on('SIGINT', () => {
|
|
|
945
947
|
console.log('\n[AGENT] Shutting down gracefully...');
|
|
946
948
|
cleanup();
|
|
947
949
|
if (ws) ws.close();
|
|
950
|
+
if (localServer) localServer.close();
|
|
948
951
|
process.exit(0);
|
|
949
952
|
});
|
|
950
953
|
|
|
@@ -952,10 +955,122 @@ process.on('SIGTERM', () => {
|
|
|
952
955
|
console.log('\n[AGENT] Received SIGTERM, shutting down...');
|
|
953
956
|
cleanup();
|
|
954
957
|
if (ws) ws.close();
|
|
958
|
+
if (localServer) localServer.close();
|
|
955
959
|
process.exit(0);
|
|
956
960
|
});
|
|
957
961
|
|
|
962
|
+
// --- Local WebSocket Server for Direct Connections ---
|
|
963
|
+
function startLocalServer() {
|
|
964
|
+
const localPort = process.env.LOCAL_PORT || 8080;
|
|
965
|
+
const localServer = require('ws').Server;
|
|
966
|
+
const wss = new localServer({ port: localPort });
|
|
967
|
+
|
|
968
|
+
logToFile(`🏠 Starting local WebSocket server on port ${localPort}`);
|
|
969
|
+
|
|
970
|
+
wss.on('connection', (localWs, request) => {
|
|
971
|
+
const clientIp = request.socket.remoteAddress;
|
|
972
|
+
logToFile(`🔗 Direct connection from ${clientIp}`);
|
|
973
|
+
|
|
974
|
+
// Handle direct browser connections
|
|
975
|
+
localWs.on('message', (data) => {
|
|
976
|
+
try {
|
|
977
|
+
const message = JSON.parse(data);
|
|
978
|
+
logToFile(`[LOCAL] Received direct message: ${message.type}`);
|
|
979
|
+
|
|
980
|
+
switch (message.type) {
|
|
981
|
+
case 'ping':
|
|
982
|
+
localWs.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
|
|
983
|
+
break;
|
|
984
|
+
|
|
985
|
+
case 'authenticate':
|
|
986
|
+
// For direct connections, we can implement simpler auth
|
|
987
|
+
localWs.send(JSON.stringify({
|
|
988
|
+
type: 'authenticated',
|
|
989
|
+
agentId: AGENT_ID,
|
|
990
|
+
timestamp: Date.now()
|
|
991
|
+
}));
|
|
992
|
+
break;
|
|
993
|
+
|
|
994
|
+
case 'create_session':
|
|
995
|
+
// Create new terminal session for direct connection
|
|
996
|
+
const sessionId = uuidv4();
|
|
997
|
+
const ptyProcess = pty.spawn(shell, [], {
|
|
998
|
+
name: 'xterm-color',
|
|
999
|
+
cols: message.cols || 120,
|
|
1000
|
+
rows: message.rows || 30,
|
|
1001
|
+
cwd: process.env.HOME,
|
|
1002
|
+
env: process.env
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
// Store session
|
|
1006
|
+
sessions[sessionId] = {
|
|
1007
|
+
pty: ptyProcess,
|
|
1008
|
+
buffer: new CircularBuffer(),
|
|
1009
|
+
lastActivity: Date.now()
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
// Send session output to direct connection
|
|
1013
|
+
ptyProcess.onData((data) => {
|
|
1014
|
+
if (localWs.readyState === WebSocket.OPEN) {
|
|
1015
|
+
localWs.send(JSON.stringify({
|
|
1016
|
+
type: 'output',
|
|
1017
|
+
sessionId,
|
|
1018
|
+
data
|
|
1019
|
+
}));
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
localWs.send(JSON.stringify({
|
|
1024
|
+
type: 'session_created',
|
|
1025
|
+
sessionId,
|
|
1026
|
+
cols: message.cols || 120,
|
|
1027
|
+
rows: message.rows || 30
|
|
1028
|
+
}));
|
|
1029
|
+
|
|
1030
|
+
logToFile(`[LOCAL] Created direct session: ${sessionId}`);
|
|
1031
|
+
break;
|
|
1032
|
+
|
|
1033
|
+
case 'input':
|
|
1034
|
+
// Handle terminal input for direct connection
|
|
1035
|
+
if (sessions[message.sessionId]) {
|
|
1036
|
+
sessions[message.sessionId].pty.write(message.data);
|
|
1037
|
+
sessions[message.sessionId].lastActivity = Date.now();
|
|
1038
|
+
}
|
|
1039
|
+
break;
|
|
1040
|
+
|
|
1041
|
+
case 'resize':
|
|
1042
|
+
// Handle terminal resize for direct connection
|
|
1043
|
+
if (sessions[message.sessionId]) {
|
|
1044
|
+
sessions[message.sessionId].pty.resize(message.cols, message.rows);
|
|
1045
|
+
}
|
|
1046
|
+
break;
|
|
1047
|
+
|
|
1048
|
+
default:
|
|
1049
|
+
logToFile(`[LOCAL] Unknown message type: ${message.type}`);
|
|
1050
|
+
}
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
logToFile(`[LOCAL] Error parsing message: ${err.message}`);
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
localWs.on('close', () => {
|
|
1057
|
+
logToFile(`[LOCAL] Direct connection from ${clientIp} closed`);
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
localWs.on('error', (error) => {
|
|
1061
|
+
logToFile(`[LOCAL] Direct connection error: ${error.message}`);
|
|
1062
|
+
});
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
logToFile(`✅ Local WebSocket server started on port ${localPort}`);
|
|
1066
|
+
return wss;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
958
1069
|
// --- Start the agent ---
|
|
959
1070
|
console.log(`[AGENT] Starting Mac Agent with ID: ${AGENT_ID}`);
|
|
1071
|
+
|
|
1072
|
+
// Start local server for direct connections
|
|
1073
|
+
const localServer = startLocalServer();
|
|
1074
|
+
|
|
960
1075
|
console.log(`[AGENT] Connecting to signaling server at: ${SIGNALING_SERVER_URL}`);
|
|
961
1076
|
connectToSignalingServer();
|
package/package.json
CHANGED
package/public/app/dashboard.js
CHANGED
|
@@ -46,7 +46,7 @@ class ShellMirrorDashboard {
|
|
|
46
46
|
this.user = authStatus.user;
|
|
47
47
|
await this.loadDashboardData();
|
|
48
48
|
this.renderAuthenticatedDashboard();
|
|
49
|
-
this.
|
|
49
|
+
this.enableHttpOnlyMode(); // Use HTTP-only mode (no persistent WebSocket)
|
|
50
50
|
this.startAutoRefresh(); // Start auto-refresh for authenticated users
|
|
51
51
|
} else {
|
|
52
52
|
this.renderUnauthenticatedDashboard();
|
|
@@ -65,12 +65,12 @@ class ShellMirrorDashboard {
|
|
|
65
65
|
clearInterval(this.refreshInterval);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
// Refresh agent data every
|
|
68
|
+
// Refresh agent data every 30 seconds (HTTP polling only)
|
|
69
69
|
this.refreshInterval = setInterval(async () => {
|
|
70
70
|
if (this.isAuthenticated && !this.isRefreshing) {
|
|
71
71
|
await this.refreshDashboardData();
|
|
72
72
|
}
|
|
73
|
-
},
|
|
73
|
+
}, 30000);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
async refreshDashboardData() {
|
|
@@ -170,7 +170,10 @@ class ShellMirrorDashboard {
|
|
|
170
170
|
console.log(`[DASHBOARD] 🔌 WebSocket closed: ${event.code} (${reason})`, event.reason);
|
|
171
171
|
|
|
172
172
|
if (event.code === 1008) {
|
|
173
|
-
console.error('[DASHBOARD] ❌ Authentication
|
|
173
|
+
console.error('[DASHBOARD] ❌ Authentication/Policy violation - switching to HTTP-only mode');
|
|
174
|
+
this.updateConnectionStatus('failed');
|
|
175
|
+
this.enableHttpOnlyMode();
|
|
176
|
+
return;
|
|
174
177
|
} else if (event.code === 1006) {
|
|
175
178
|
console.error('[DASHBOARD] ❌ Abnormal closure - WebSocket endpoint may not exist');
|
|
176
179
|
}
|
package/public/app/terminal.js
CHANGED
|
@@ -268,23 +268,215 @@ function startConnection() {
|
|
|
268
268
|
|
|
269
269
|
|
|
270
270
|
async function initialize() {
|
|
271
|
-
console.log('[CLIENT] 🚀 Initializing
|
|
271
|
+
console.log('[CLIENT] 🚀 Initializing connection to agent:', AGENT_ID);
|
|
272
272
|
console.log('[CLIENT] 📋 Selected agent data:', SELECTED_AGENT);
|
|
273
273
|
|
|
274
|
-
//
|
|
274
|
+
// First try direct connection to agent
|
|
275
|
+
const directConnectionSuccess = await tryDirectConnection();
|
|
276
|
+
|
|
277
|
+
if (directConnectionSuccess) {
|
|
278
|
+
console.log('[CLIENT] ✅ Direct connection established - no server needed!');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
console.log('[CLIENT] ⚠️ Direct connection failed, falling back to WebRTC signaling...');
|
|
283
|
+
await initializeWebRTCSignaling();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function tryDirectConnection() {
|
|
287
|
+
console.log('[CLIENT] 🔗 Attempting direct connection to agent...');
|
|
288
|
+
updateConnectionStatus('connecting');
|
|
289
|
+
|
|
290
|
+
// Get agent data from API to find local connection details
|
|
291
|
+
try {
|
|
292
|
+
const response = await fetch('/php-backend/api/agents-list.php', {
|
|
293
|
+
credentials: 'include'
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const data = await response.json();
|
|
297
|
+
if (!data.success || !data.data.agents) {
|
|
298
|
+
console.log('[CLIENT] ❌ Could not get agent list for direct connection');
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const agent = data.data.agents.find(a => a.agentId === AGENT_ID);
|
|
303
|
+
if (!agent || !agent.localPort) {
|
|
304
|
+
console.log('[CLIENT] ❌ Agent not found or no local port information');
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Try common local IPs for the agent
|
|
309
|
+
// Start with localhost/loopback, then try common private network ranges
|
|
310
|
+
const possibleIPs = [
|
|
311
|
+
'localhost',
|
|
312
|
+
'127.0.0.1',
|
|
313
|
+
// Common private network ranges
|
|
314
|
+
...generatePrivateIPCandidates()
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
for (const ip of possibleIPs) {
|
|
318
|
+
const success = await tryDirectConnectionToIP(ip, agent.localPort);
|
|
319
|
+
if (success) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
console.log('[CLIENT] ❌ Direct connection failed to all IP candidates');
|
|
325
|
+
updateConnectionStatus('disconnected');
|
|
326
|
+
return false;
|
|
327
|
+
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.log('[CLIENT] ❌ Error during direct connection attempt:', error);
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function tryDirectConnectionToIP(ip, port) {
|
|
335
|
+
return new Promise((resolve) => {
|
|
336
|
+
console.log(`[CLIENT] 🔍 Trying direct connection to ${ip}:${port}`);
|
|
337
|
+
|
|
338
|
+
const directWs = new WebSocket(`ws://${ip}:${port}`);
|
|
339
|
+
const timeout = setTimeout(() => {
|
|
340
|
+
console.log(`[CLIENT] ⏰ Connection timeout to ${ip}:${port}`);
|
|
341
|
+
directWs.close();
|
|
342
|
+
resolve(false);
|
|
343
|
+
}, 3000); // 3 second timeout
|
|
344
|
+
|
|
345
|
+
directWs.onopen = () => {
|
|
346
|
+
clearTimeout(timeout);
|
|
347
|
+
console.log(`[CLIENT] ✅ Direct connection established to ${ip}:${port}`);
|
|
348
|
+
|
|
349
|
+
// Set up the direct connection handlers
|
|
350
|
+
setupDirectConnection(directWs);
|
|
351
|
+
resolve(true);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
directWs.onerror = () => {
|
|
355
|
+
clearTimeout(timeout);
|
|
356
|
+
console.log(`[CLIENT] ❌ Connection failed to ${ip}:${port}`);
|
|
357
|
+
resolve(false);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
directWs.onclose = () => {
|
|
361
|
+
clearTimeout(timeout);
|
|
362
|
+
resolve(false);
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function setupDirectConnection(directWs) {
|
|
368
|
+
console.log('[CLIENT] 🔧 Setting up direct connection handlers');
|
|
369
|
+
|
|
370
|
+
// Store the WebSocket for global access
|
|
371
|
+
ws = directWs;
|
|
372
|
+
|
|
373
|
+
// Set up message handlers
|
|
374
|
+
directWs.onmessage = (event) => {
|
|
375
|
+
const data = JSON.parse(event.data);
|
|
376
|
+
console.log(`[CLIENT] 📨 Direct message: ${data.type}`);
|
|
377
|
+
|
|
378
|
+
switch (data.type) {
|
|
379
|
+
case 'pong':
|
|
380
|
+
console.log('[CLIENT] 🏓 Received pong from direct connection');
|
|
381
|
+
break;
|
|
382
|
+
|
|
383
|
+
case 'authenticated':
|
|
384
|
+
console.log('[CLIENT] ✅ Direct authentication successful');
|
|
385
|
+
// Request session creation
|
|
386
|
+
directWs.send(JSON.stringify({
|
|
387
|
+
type: 'create_session',
|
|
388
|
+
sessionId: requestedSessionId,
|
|
389
|
+
cols: term.cols,
|
|
390
|
+
rows: term.rows
|
|
391
|
+
}));
|
|
392
|
+
break;
|
|
393
|
+
|
|
394
|
+
case 'session_created':
|
|
395
|
+
console.log('[CLIENT] ✅ Direct session created:', data.sessionId);
|
|
396
|
+
currentSession = { id: data.sessionId };
|
|
397
|
+
updateSessionDisplay();
|
|
398
|
+
break;
|
|
399
|
+
|
|
400
|
+
case 'output':
|
|
401
|
+
// Handle terminal output
|
|
402
|
+
if (data.sessionId === currentSession?.id) {
|
|
403
|
+
term.write(data.data);
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
|
|
407
|
+
default:
|
|
408
|
+
console.log('[CLIENT] ❓ Unknown direct message type:', data.type);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
directWs.onclose = () => {
|
|
413
|
+
console.log('[CLIENT] ❌ Direct connection closed');
|
|
414
|
+
updateConnectionStatus('disconnected');
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
directWs.onerror = (error) => {
|
|
418
|
+
console.error('[CLIENT] ❌ Direct connection error:', error);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// Set up terminal input handler for direct connection
|
|
422
|
+
term.onData((data) => {
|
|
423
|
+
if (currentSession && directWs.readyState === WebSocket.OPEN) {
|
|
424
|
+
directWs.send(JSON.stringify({
|
|
425
|
+
type: 'input',
|
|
426
|
+
sessionId: currentSession.id,
|
|
427
|
+
data: data
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Send authentication
|
|
433
|
+
directWs.send(JSON.stringify({
|
|
434
|
+
type: 'authenticate',
|
|
435
|
+
agentId: AGENT_ID
|
|
436
|
+
}));
|
|
437
|
+
|
|
438
|
+
updateConnectionStatus('connected');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function generatePrivateIPCandidates() {
|
|
442
|
+
// Generate most common private network IP candidates
|
|
443
|
+
const candidates = [];
|
|
444
|
+
|
|
445
|
+
// Most common home router ranges (limit to most popular subnets)
|
|
446
|
+
const commonSubnets = [0, 1, 2, 10, 100];
|
|
447
|
+
for (const subnet of commonSubnets) {
|
|
448
|
+
// Common host IPs: router (1), common DHCP assignments
|
|
449
|
+
const hosts = [1, 2, 10, 100, 101, 150];
|
|
450
|
+
for (const host of hosts) {
|
|
451
|
+
candidates.push(`192.168.${subnet}.${host}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Common corporate/enterprise ranges (just the most common ones)
|
|
456
|
+
candidates.push(
|
|
457
|
+
'10.0.0.1', '10.0.0.2', '10.0.0.100',
|
|
458
|
+
'10.0.1.1', '10.0.1.100',
|
|
459
|
+
'172.16.0.1', '172.16.0.100'
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
return candidates;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function initializeWebRTCSignaling() {
|
|
466
|
+
console.log('[CLIENT] 🚀 Initializing WebRTC signaling connection to agent:', AGENT_ID);
|
|
467
|
+
|
|
468
|
+
// Use Heroku WebSocket server for WebRTC signaling only
|
|
275
469
|
const signalingUrl = 'wss://shell-mirror-30aa5479ceaf.herokuapp.com';
|
|
276
|
-
console.log('[CLIENT] 🌐 Using Heroku WebSocket server:', signalingUrl);
|
|
470
|
+
console.log('[CLIENT] 🌐 Using Heroku WebSocket server for signaling:', signalingUrl);
|
|
277
471
|
|
|
278
472
|
ws = new WebSocket(`${signalingUrl}?role=client`);
|
|
279
|
-
|
|
473
|
+
|
|
280
474
|
ws.onopen = () => {
|
|
281
475
|
console.log('[CLIENT] ✅ WebSocket connection to signaling server opened.');
|
|
282
476
|
};
|
|
283
|
-
|
|
284
477
|
ws.onmessage = async (message) => {
|
|
285
478
|
const data = JSON.parse(message.data);
|
|
286
479
|
console.log(`[CLIENT] Received message of type: ${data.type}`);
|
|
287
|
-
|
|
288
480
|
switch (data.type) {
|
|
289
481
|
case 'server-hello':
|
|
290
482
|
CLIENT_ID = data.id;
|