shell-mirror 1.5.17 → 1.5.19
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/lib/auto-start.js +79 -42
- package/lib/network-utils.js +145 -0
- package/package.json +1 -1
- package/public/app/terminal.js +27 -6
- package/server.js +8 -3
package/lib/auto-start.js
CHANGED
|
@@ -5,6 +5,7 @@ const crypto = require('crypto');
|
|
|
5
5
|
const { spawn } = require('child_process');
|
|
6
6
|
const https = require('https');
|
|
7
7
|
const querystring = require('querystring');
|
|
8
|
+
const NetworkUtils = require('./network-utils');
|
|
8
9
|
|
|
9
10
|
class AutoStart {
|
|
10
11
|
constructor() {
|
|
@@ -400,13 +401,16 @@ NODE_ENV=development
|
|
|
400
401
|
});
|
|
401
402
|
}
|
|
402
403
|
|
|
403
|
-
async createEnvFile(authInfo) {
|
|
404
|
+
async createEnvFile(authInfo, wsConfig = null) {
|
|
405
|
+
const port = wsConfig ? wsConfig.port : 8080;
|
|
406
|
+
const host = wsConfig ? wsConfig.host : '0.0.0.0';
|
|
407
|
+
|
|
404
408
|
const envContent = `# Shell Mirror Configuration
|
|
405
409
|
# Auto-generated on ${new Date().toISOString()}
|
|
406
410
|
|
|
407
|
-
BASE_URL=http://localhost
|
|
408
|
-
PORT
|
|
409
|
-
HOST
|
|
411
|
+
BASE_URL=http://localhost:${port}
|
|
412
|
+
PORT=${port}
|
|
413
|
+
HOST=${host}
|
|
410
414
|
GOOGLE_CLIENT_ID=${this.oauthConfig.clientId}
|
|
411
415
|
GOOGLE_CLIENT_SECRET=${this.oauthConfig.clientSecret}
|
|
412
416
|
SESSION_SECRET=${crypto.randomBytes(32).toString('hex')}
|
|
@@ -416,62 +420,93 @@ NODE_ENV=development
|
|
|
416
420
|
USER_EMAIL=${authInfo.email}
|
|
417
421
|
USER_NAME=${authInfo.name}
|
|
418
422
|
ACCESS_TOKEN=${authInfo.accessToken}
|
|
423
|
+
|
|
424
|
+
# WebSocket server configuration
|
|
425
|
+
${wsConfig ? `WS_PORT=${wsConfig.port}
|
|
426
|
+
WS_HOST=${wsConfig.host}
|
|
427
|
+
WS_LOCAL_IP=${wsConfig.localIP}
|
|
428
|
+
WS_URL=${wsConfig.wsUrl}` : ''}
|
|
419
429
|
`;
|
|
420
430
|
|
|
421
431
|
await fs.writeFile(this.envFile, envContent);
|
|
422
432
|
}
|
|
423
433
|
|
|
424
|
-
displayStatus(authInfo) {
|
|
434
|
+
displayStatus(authInfo, wsConfig = null) {
|
|
425
435
|
const pkg = require('../package.json');
|
|
436
|
+
console.log('');
|
|
426
437
|
console.log(`✅ Shell Mirror v${pkg.version} - Running (${authInfo.email})`);
|
|
438
|
+
if (wsConfig) {
|
|
439
|
+
console.log(`🌐 WebSocket Server: ${wsConfig.wsUrl}`);
|
|
440
|
+
console.log(`📱 Access from browser: https://shellmirror.app`);
|
|
441
|
+
}
|
|
427
442
|
console.log('Press Ctrl+C to stop');
|
|
443
|
+
console.log('');
|
|
428
444
|
}
|
|
429
445
|
|
|
430
446
|
async startAuthenticatedServer(authInfo) {
|
|
431
447
|
console.log('');
|
|
432
|
-
console.log('🚀 Starting authenticated Shell Mirror server...');
|
|
433
|
-
|
|
434
|
-
// Create .env file with authentication
|
|
435
|
-
await this.createEnvFile(authInfo);
|
|
436
|
-
|
|
437
|
-
// Register this server as a Mac agent
|
|
438
|
-
await this.registerAsMacAgent(authInfo);
|
|
439
|
-
|
|
440
|
-
// Find the package root directory (where server.js is located)
|
|
441
|
-
const packageRoot = path.resolve(__dirname, '..');
|
|
442
|
-
const serverPath = path.join(packageRoot, 'server.js');
|
|
448
|
+
console.log('🚀 Starting authenticated Shell Mirror with local WebSocket server...');
|
|
443
449
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
+
try {
|
|
451
|
+
// Set up local WebSocket server configuration
|
|
452
|
+
const wsPort = await NetworkUtils.findAvailablePort(8080, 10);
|
|
453
|
+
const wsConfig = NetworkUtils.generateWebSocketConfig(wsPort);
|
|
454
|
+
|
|
455
|
+
// Display network information
|
|
456
|
+
NetworkUtils.displayNetworkInfo(wsConfig);
|
|
457
|
+
|
|
458
|
+
// Create .env file with authentication and WebSocket config
|
|
459
|
+
await this.createEnvFile(authInfo, wsConfig);
|
|
460
|
+
|
|
461
|
+
// Register this server as a Mac agent with WebSocket endpoint
|
|
462
|
+
await this.registerAsMacAgent(authInfo, wsConfig);
|
|
463
|
+
|
|
464
|
+
// Start the local WebSocket server process
|
|
465
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
466
|
+
const serverPath = path.join(packageRoot, 'server.js');
|
|
467
|
+
|
|
468
|
+
console.log('🔄 Starting local WebSocket server...');
|
|
469
|
+
const serverProcess = spawn('node', [serverPath], {
|
|
470
|
+
stdio: 'inherit',
|
|
471
|
+
cwd: path.dirname(this.envFile),
|
|
472
|
+
env: {
|
|
473
|
+
...process.env,
|
|
474
|
+
WS_PORT: wsConfig.port.toString(),
|
|
475
|
+
WS_HOST: wsConfig.host,
|
|
476
|
+
LOCAL_IP: wsConfig.localIP
|
|
477
|
+
}
|
|
478
|
+
});
|
|
450
479
|
|
|
451
|
-
|
|
452
|
-
|
|
480
|
+
// Display status information
|
|
481
|
+
this.displayStatus(authInfo, wsConfig);
|
|
453
482
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
483
|
+
// Handle Ctrl+C gracefully
|
|
484
|
+
process.on('SIGINT', () => {
|
|
485
|
+
console.log('');
|
|
486
|
+
console.log('🛑 Stopping Shell Mirror and WebSocket server...');
|
|
487
|
+
serverProcess.kill('SIGINT');
|
|
488
|
+
process.exit(0);
|
|
489
|
+
});
|
|
461
490
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
491
|
+
// Wait for server process
|
|
492
|
+
serverProcess.on('close', (code) => {
|
|
493
|
+
if (code !== 0) {
|
|
494
|
+
console.error(`❌ Server exited with code ${code}`);
|
|
495
|
+
process.exit(code);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
} catch (error) {
|
|
500
|
+
console.error('❌ Failed to start WebSocket server:', error.message);
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
469
503
|
}
|
|
470
504
|
|
|
471
|
-
async registerAsMacAgent(authInfo) {
|
|
472
|
-
console.log('🔗 Registering as Mac agent...');
|
|
505
|
+
async registerAsMacAgent(authInfo, wsConfig) {
|
|
506
|
+
console.log('🔗 Registering as Mac agent with WebSocket endpoint...');
|
|
473
507
|
console.log(` User: ${authInfo.email}`);
|
|
474
508
|
console.log(` Machine: ${os.hostname()}`);
|
|
509
|
+
console.log(` WebSocket: ${wsConfig.wsUrl}`);
|
|
475
510
|
|
|
476
511
|
try {
|
|
477
512
|
const agentId = `local-${os.hostname()}-${Date.now()}`;
|
|
@@ -481,9 +516,11 @@ ACCESS_TOKEN=${authInfo.accessToken}
|
|
|
481
516
|
ownerName: authInfo.name,
|
|
482
517
|
ownerToken: authInfo.accessToken,
|
|
483
518
|
machineName: os.hostname(),
|
|
484
|
-
agentVersion: '1.
|
|
519
|
+
agentVersion: '1.5.17',
|
|
485
520
|
capabilities: ['terminal', 'websocket'],
|
|
486
|
-
serverPort:
|
|
521
|
+
serverPort: wsConfig.port,
|
|
522
|
+
websocketUrl: wsConfig.wsUrl,
|
|
523
|
+
localIP: wsConfig.localIP
|
|
487
524
|
};
|
|
488
525
|
|
|
489
526
|
console.log(` Agent ID: ${agentId}`);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Network utilities for local WebSocket server setup
|
|
5
|
+
*/
|
|
6
|
+
class NetworkUtils {
|
|
7
|
+
/**
|
|
8
|
+
* Get the local IP address for WebSocket server binding
|
|
9
|
+
* Prioritizes Wi-Fi and Ethernet interfaces over others
|
|
10
|
+
*/
|
|
11
|
+
static getLocalIP() {
|
|
12
|
+
const interfaces = os.networkInterfaces();
|
|
13
|
+
|
|
14
|
+
// Priority order for interface selection
|
|
15
|
+
const priorityOrder = [
|
|
16
|
+
'Wi-Fi', // macOS Wi-Fi
|
|
17
|
+
'en0', // macOS primary interface
|
|
18
|
+
'en1', // macOS secondary interface
|
|
19
|
+
'eth0', // Linux Ethernet
|
|
20
|
+
'wlan0', // Linux Wi-Fi
|
|
21
|
+
'Ethernet', // Windows Ethernet
|
|
22
|
+
'Wi-Fi' // Windows Wi-Fi (might be different name)
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// First, try priority interfaces
|
|
26
|
+
for (const interfaceName of priorityOrder) {
|
|
27
|
+
const addresses = interfaces[interfaceName];
|
|
28
|
+
if (addresses) {
|
|
29
|
+
for (const addr of addresses) {
|
|
30
|
+
if (addr.family === 'IPv4' && !addr.internal) {
|
|
31
|
+
console.log(`🌐 Selected network interface: ${interfaceName} (${addr.address})`);
|
|
32
|
+
return addr.address;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fallback: find any non-internal IPv4 address
|
|
39
|
+
for (const interfaceName in interfaces) {
|
|
40
|
+
const addresses = interfaces[interfaceName];
|
|
41
|
+
for (const addr of addresses) {
|
|
42
|
+
if (addr.family === 'IPv4' && !addr.internal) {
|
|
43
|
+
console.log(`🌐 Fallback network interface: ${interfaceName} (${addr.address})`);
|
|
44
|
+
return addr.address;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Last resort: localhost (won't work for remote clients)
|
|
50
|
+
console.warn('⚠️ No external network interface found, using localhost');
|
|
51
|
+
return '127.0.0.1';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find an available port for the WebSocket server
|
|
56
|
+
* @param {number} startPort - Starting port to check
|
|
57
|
+
* @param {number} maxAttempts - Maximum number of ports to try
|
|
58
|
+
*/
|
|
59
|
+
static async findAvailablePort(startPort = 8080, maxAttempts = 10) {
|
|
60
|
+
const net = require('net');
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
63
|
+
const port = startPort + i;
|
|
64
|
+
const isAvailable = await this.checkPortAvailable(port);
|
|
65
|
+
if (isAvailable) {
|
|
66
|
+
console.log(`🔌 Found available port: ${port}`);
|
|
67
|
+
return port;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
throw new Error(`No available ports found in range ${startPort}-${startPort + maxAttempts - 1}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if a specific port is available
|
|
76
|
+
* @param {number} port - Port to check
|
|
77
|
+
*/
|
|
78
|
+
static checkPortAvailable(port) {
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
const net = require('net');
|
|
81
|
+
const server = net.createServer();
|
|
82
|
+
|
|
83
|
+
server.listen(port, (err) => {
|
|
84
|
+
if (err) {
|
|
85
|
+
resolve(false);
|
|
86
|
+
} else {
|
|
87
|
+
server.once('close', () => resolve(true));
|
|
88
|
+
server.close();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
server.on('error', () => resolve(false));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generate WebSocket server configuration
|
|
98
|
+
* @param {number} port - Port for WebSocket server
|
|
99
|
+
*/
|
|
100
|
+
static generateWebSocketConfig(port) {
|
|
101
|
+
const localIP = this.getLocalIP();
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
host: '0.0.0.0', // Bind to all interfaces
|
|
105
|
+
port: port,
|
|
106
|
+
localIP: localIP,
|
|
107
|
+
wsUrl: `ws://${localIP}:${port}`,
|
|
108
|
+
httpsWsUrl: `wss://${localIP}:${port}` // For future HTTPS support
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Display network connectivity information
|
|
114
|
+
* @param {object} config - WebSocket configuration
|
|
115
|
+
*/
|
|
116
|
+
static displayNetworkInfo(config) {
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log('🌐 Network Configuration:');
|
|
119
|
+
console.log(` Local IP: ${config.localIP}`);
|
|
120
|
+
console.log(` WebSocket URL: ${config.wsUrl}`);
|
|
121
|
+
console.log(` Binding: ${config.host}:${config.port}`);
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log('📱 Access from other devices:');
|
|
124
|
+
console.log(` Use this URL in browser: https://shellmirror.app`);
|
|
125
|
+
console.log(` Mac agent WebSocket: ${config.wsUrl}`);
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Validate WebSocket URL format
|
|
131
|
+
* @param {string} wsUrl - WebSocket URL to validate
|
|
132
|
+
*/
|
|
133
|
+
static validateWebSocketURL(wsUrl) {
|
|
134
|
+
try {
|
|
135
|
+
const url = new URL(wsUrl);
|
|
136
|
+
return (url.protocol === 'ws:' || url.protocol === 'wss:') &&
|
|
137
|
+
url.hostname &&
|
|
138
|
+
url.port;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = NetworkUtils;
|
package/package.json
CHANGED
package/public/app/terminal.js
CHANGED
|
@@ -54,6 +54,7 @@ let dataChannel;
|
|
|
54
54
|
let user;
|
|
55
55
|
let AGENT_ID;
|
|
56
56
|
let CLIENT_ID;
|
|
57
|
+
let SELECTED_AGENT; // Store full agent data including WebSocket URL
|
|
57
58
|
|
|
58
59
|
// Auto-discover agents on page load
|
|
59
60
|
window.addEventListener('load', () => {
|
|
@@ -100,8 +101,17 @@ connectBtn.onclick = () => {
|
|
|
100
101
|
startConnection();
|
|
101
102
|
};
|
|
102
103
|
|
|
103
|
-
function connectToAgent(
|
|
104
|
-
|
|
104
|
+
function connectToAgent(agent) {
|
|
105
|
+
if (typeof agent === 'string') {
|
|
106
|
+
// Fallback for manual connection - agent is just the ID
|
|
107
|
+
AGENT_ID = agent;
|
|
108
|
+
SELECTED_AGENT = { id: agent, websocketUrl: null };
|
|
109
|
+
} else {
|
|
110
|
+
// Full agent object from discovery
|
|
111
|
+
AGENT_ID = agent.id;
|
|
112
|
+
SELECTED_AGENT = agent;
|
|
113
|
+
}
|
|
114
|
+
console.log('[CLIENT] 🔗 Connecting to agent:', SELECTED_AGENT);
|
|
105
115
|
startConnection();
|
|
106
116
|
}
|
|
107
117
|
|
|
@@ -193,8 +203,8 @@ function displayAvailableAgents(agents) {
|
|
|
193
203
|
agentDiv.style.background = '#333';
|
|
194
204
|
};
|
|
195
205
|
agentDiv.onclick = () => {
|
|
196
|
-
console.log(`[DISCOVERY] 🖱️ User clicked on agent: ${agent.id}
|
|
197
|
-
connectToAgent(agent
|
|
206
|
+
console.log(`[DISCOVERY] 🖱️ User clicked on agent: ${agent.id}`, agent);
|
|
207
|
+
connectToAgent(agent);
|
|
198
208
|
};
|
|
199
209
|
|
|
200
210
|
agentList.appendChild(agentDiv);
|
|
@@ -216,8 +226,19 @@ function displayAvailableAgents(agents) {
|
|
|
216
226
|
|
|
217
227
|
async function initialize() {
|
|
218
228
|
console.log('[CLIENT] 🚀 Initializing WebRTC connection to agent:', AGENT_ID);
|
|
219
|
-
|
|
220
|
-
|
|
229
|
+
console.log('[CLIENT] 📋 Selected agent data:', SELECTED_AGENT);
|
|
230
|
+
|
|
231
|
+
// Determine WebSocket URL - use agent's local WebSocket server if available
|
|
232
|
+
let signalingUrl;
|
|
233
|
+
if (SELECTED_AGENT && SELECTED_AGENT.websocketUrl) {
|
|
234
|
+
signalingUrl = SELECTED_AGENT.websocketUrl;
|
|
235
|
+
console.log('[CLIENT] 🌐 Using agent\'s local WebSocket server:', signalingUrl);
|
|
236
|
+
} else {
|
|
237
|
+
// Fallback to web app domain (old behavior)
|
|
238
|
+
signalingUrl = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host;
|
|
239
|
+
console.log('[CLIENT] 🌐 Using fallback WebSocket server:', signalingUrl);
|
|
240
|
+
}
|
|
241
|
+
|
|
221
242
|
ws = new WebSocket(`${signalingUrl}?role=client`);
|
|
222
243
|
|
|
223
244
|
ws.onopen = () => {
|
package/server.js
CHANGED
|
@@ -255,10 +255,15 @@ function handleSignalingMessage(ws, rawMessage, senderRole) {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// --- Server Startup ---
|
|
258
|
-
const PORT = process.env.PORT ||
|
|
259
|
-
const HOST = process.env.HOST || '0.0.0.0';
|
|
258
|
+
const PORT = process.env.WS_PORT || process.env.PORT || 8080;
|
|
259
|
+
const HOST = process.env.WS_HOST || process.env.HOST || '0.0.0.0';
|
|
260
|
+
const LOCAL_IP = process.env.WS_LOCAL_IP || process.env.LOCAL_IP || 'localhost';
|
|
261
|
+
|
|
260
262
|
server.listen(PORT, HOST, () => {
|
|
261
|
-
|
|
263
|
+
const baseUrl = process.env.BASE_URL || `http://${LOCAL_IP}:${PORT}`;
|
|
264
|
+
logToFile(`✅ Local WebSocket server is running on ${baseUrl}`);
|
|
265
|
+
logToFile(`🌐 WebSocket URL: ws://${LOCAL_IP}:${PORT}`);
|
|
266
|
+
logToFile(`🔧 Binding to: ${HOST}:${PORT}`);
|
|
262
267
|
});
|
|
263
268
|
|
|
264
269
|
// Graceful shutdown
|