shell-mirror 1.0.0
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/README.md +272 -0
- package/auth.js +22 -0
- package/bin/shell-mirror +127 -0
- package/lib/config-manager.js +203 -0
- package/lib/health-checker.js +192 -0
- package/lib/server-manager.js +225 -0
- package/lib/setup-wizard.js +222 -0
- package/package.json +70 -0
- package/public/app/terminal.html +867 -0
- package/public/index.html +809 -0
- package/server.js +171 -0
package/server.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const WebSocket = require('ws');
|
|
4
|
+
const pty = require('node-pty');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const session = require('express-session');
|
|
8
|
+
const passport = require('passport');
|
|
9
|
+
|
|
10
|
+
// Load environment configuration and auth setup
|
|
11
|
+
require('dotenv').config();
|
|
12
|
+
require('./auth'); // Configure passport strategies
|
|
13
|
+
|
|
14
|
+
const app = express();
|
|
15
|
+
const server = http.createServer(app);
|
|
16
|
+
const wss = new WebSocket.Server({ server });
|
|
17
|
+
|
|
18
|
+
// 1. Session Configuration
|
|
19
|
+
const sessionParser = session({
|
|
20
|
+
secret: process.env.SESSION_SECRET || 'a-secure-default-session-secret',
|
|
21
|
+
resave: false,
|
|
22
|
+
saveUninitialized: false,
|
|
23
|
+
cookie: { secure: process.env.NODE_ENV === 'production' } // Use secure cookies in production
|
|
24
|
+
});
|
|
25
|
+
app.use(sessionParser);
|
|
26
|
+
|
|
27
|
+
// 2. Passport Initialization
|
|
28
|
+
app.use(passport.initialize());
|
|
29
|
+
app.use(passport.session());
|
|
30
|
+
|
|
31
|
+
// Use the user's default shell.
|
|
32
|
+
const shell = os.platform() === 'win32' ? 'powershell.exe' : (process.env.SHELL || 'bash');
|
|
33
|
+
|
|
34
|
+
// --- Persistent Terminal Session ---
|
|
35
|
+
const term = pty.spawn(shell, [], {
|
|
36
|
+
name: 'xterm-color',
|
|
37
|
+
cols: 80,
|
|
38
|
+
rows: 30,
|
|
39
|
+
cwd: process.cwd(),
|
|
40
|
+
env: process.env
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// --- Terminal History Buffer ---
|
|
44
|
+
let history = [];
|
|
45
|
+
const MAX_HISTORY_LINES = 100;
|
|
46
|
+
term.on('data', (data) => {
|
|
47
|
+
if (history.length > MAX_HISTORY_LINES * 2) {
|
|
48
|
+
history = history.slice(history.length - MAX_HISTORY_LINES);
|
|
49
|
+
}
|
|
50
|
+
history.push(data);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Serve static files from 'public'
|
|
54
|
+
app.use(express.static('public'));
|
|
55
|
+
|
|
56
|
+
// Route for the terminal application (protected)
|
|
57
|
+
app.get('/app', (req, res) => {
|
|
58
|
+
if (!req.isAuthenticated()) {
|
|
59
|
+
return res.redirect('/auth/google');
|
|
60
|
+
}
|
|
61
|
+
res.sendFile(path.join(__dirname, 'public', 'app', 'terminal.html'));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 3. Authentication Routes
|
|
65
|
+
app.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
|
|
66
|
+
|
|
67
|
+
app.get('/auth/google/callback',
|
|
68
|
+
passport.authenticate('google', { failureRedirect: '/login-failed.html' }),
|
|
69
|
+
function(req, res) {
|
|
70
|
+
// Successful authentication, redirect home.
|
|
71
|
+
res.redirect('/');
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
app.get('/api/auth/status', (req, res) => {
|
|
76
|
+
if (req.isAuthenticated()) {
|
|
77
|
+
res.json({ authenticated: true, user: req.user });
|
|
78
|
+
} else {
|
|
79
|
+
res.json({ authenticated: false });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
app.post('/api/auth/logout', (req, res) => {
|
|
84
|
+
req.logout(function(err) {
|
|
85
|
+
if (err) { return next(err); }
|
|
86
|
+
res.redirect('/');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Environment validation
|
|
91
|
+
const requiredEnvVars = ['BASE_URL', 'GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET', 'SESSION_SECRET'];
|
|
92
|
+
const missingEnvVars = requiredEnvVars.filter(varName => !process.env[varName]);
|
|
93
|
+
|
|
94
|
+
if (missingEnvVars.length > 0) {
|
|
95
|
+
console.error('❌ Missing required environment variables:');
|
|
96
|
+
missingEnvVars.forEach(varName => {
|
|
97
|
+
console.error(` - ${varName}`);
|
|
98
|
+
});
|
|
99
|
+
console.error('\nPlease create a .env file based on .env.example and fill in the required values.');
|
|
100
|
+
console.error('See README.md for instructions on getting Google OAuth credentials.');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4. WebSocket Connection Handler with Authentication
|
|
105
|
+
wss.on('connection', function connection(ws, request) {
|
|
106
|
+
console.log('New WebSocket connection attempt');
|
|
107
|
+
|
|
108
|
+
// Parse session from the WebSocket request
|
|
109
|
+
sessionParser(request, {}, () => {
|
|
110
|
+
if (!request.session || !request.session.passport || !request.session.passport.user) {
|
|
111
|
+
console.log('❌ Unauthorized WebSocket connection rejected');
|
|
112
|
+
ws.close(1008, 'Authentication required');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(`✅ Authenticated WebSocket connection established for user: ${request.session.passport.user.displayName}`);
|
|
117
|
+
|
|
118
|
+
// Send terminal history to newly connected client
|
|
119
|
+
if (history.length > 0) {
|
|
120
|
+
const recentHistory = history.slice(-MAX_HISTORY_LINES).join('');
|
|
121
|
+
ws.send(recentHistory);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Forward terminal output to this WebSocket connection
|
|
125
|
+
const terminalDataHandler = (data) => {
|
|
126
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
127
|
+
ws.send(data);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
term.on('data', terminalDataHandler);
|
|
131
|
+
|
|
132
|
+
// Handle incoming WebSocket messages
|
|
133
|
+
ws.on('message', function message(data) {
|
|
134
|
+
try {
|
|
135
|
+
const message = JSON.parse(data);
|
|
136
|
+
|
|
137
|
+
if (message.type === 'input' && message.data) {
|
|
138
|
+
// Forward user input to terminal
|
|
139
|
+
term.write(message.data);
|
|
140
|
+
} else if (message.type === 'resize' && message.cols && message.rows) {
|
|
141
|
+
// Resize terminal
|
|
142
|
+
term.resize(message.cols, message.rows);
|
|
143
|
+
console.log(`Terminal resized to ${message.cols}x${message.rows}`);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error('Invalid WebSocket message:', error);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Handle WebSocket disconnection
|
|
151
|
+
ws.on('close', function close() {
|
|
152
|
+
console.log('WebSocket connection closed');
|
|
153
|
+
term.removeListener('data', terminalDataHandler);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Handle WebSocket errors
|
|
157
|
+
ws.on('error', function error(err) {
|
|
158
|
+
console.error('WebSocket error:', err);
|
|
159
|
+
term.removeListener('data', terminalDataHandler);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const PORT = process.env.PORT || 3000;
|
|
165
|
+
const HOST = process.env.HOST || '0.0.0.0';
|
|
166
|
+
server.listen(PORT, HOST, () => {
|
|
167
|
+
console.log(`✅ Shell Mirror server is running on ${process.env.BASE_URL}`);
|
|
168
|
+
if (process.env.BASE_URL.includes('localhost')) {
|
|
169
|
+
console.log(` Local access: http://localhost:${PORT}`);
|
|
170
|
+
}
|
|
171
|
+
});
|