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/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
+ });