lobstakit-cloud 1.3.13 → 1.3.14
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/package.json +6 -4
- package/public/js/manage.js +72 -89
- package/public/manage.html +4 -5
- package/server.js +87 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lobstakit-cloud",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "LobstaKit Cloud
|
|
3
|
+
"version": "1.3.14",
|
|
4
|
+
"description": "LobstaKit Cloud \u2014 Setup wizard and management for LobstaCloud gateways",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"lobstakit-cloud": "./bin/lobstakit.js"
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"better-sqlite3": "^12.6.2",
|
|
14
14
|
"express": "^4.21.0",
|
|
15
|
-
"http-proxy-middleware": "^3.0.0"
|
|
15
|
+
"http-proxy-middleware": "^3.0.0",
|
|
16
|
+
"node-pty": "^1.1.0",
|
|
17
|
+
"ws": "^8.19.0"
|
|
16
18
|
}
|
|
17
|
-
}
|
|
19
|
+
}
|
package/public/js/manage.js
CHANGED
|
@@ -96,8 +96,8 @@ const SETTINGS_PROVIDER_FALLBACK = {
|
|
|
96
96
|
|
|
97
97
|
let settingsProviders = SETTINGS_PROVIDER_FALLBACK;
|
|
98
98
|
let currentAiSettings = null;
|
|
99
|
-
let
|
|
100
|
-
let
|
|
99
|
+
let xtermInstance = null;
|
|
100
|
+
let terminalWs = null;
|
|
101
101
|
|
|
102
102
|
async function logout() {
|
|
103
103
|
const token = localStorage.getItem('lobstakit_token');
|
|
@@ -255,103 +255,86 @@ function escapeHtml(value) {
|
|
|
255
255
|
.replace(/'/g, ''');
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
function
|
|
259
|
-
const
|
|
260
|
-
if (!
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
258
|
+
function initTerminal() {
|
|
259
|
+
const container = document.getElementById('terminal-container');
|
|
260
|
+
if (!container || typeof Terminal === 'undefined') return;
|
|
261
|
+
|
|
262
|
+
// Clean up previous instance if any
|
|
263
|
+
if (terminalWs) { try { terminalWs.close(); } catch (e) {} terminalWs = null; }
|
|
264
|
+
if (xtermInstance) { try { xtermInstance.dispose(); } catch (e) {} xtermInstance = null; }
|
|
265
|
+
|
|
266
|
+
const term = new Terminal({
|
|
267
|
+
cursorBlink: true,
|
|
268
|
+
fontSize: 13,
|
|
269
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Menlo', monospace",
|
|
270
|
+
theme: {
|
|
271
|
+
background: '#0d1117',
|
|
272
|
+
foreground: '#e6edf3',
|
|
273
|
+
cursor: '#58a6ff',
|
|
274
|
+
selectionBackground: '#264f78',
|
|
275
|
+
black: '#0d1117',
|
|
276
|
+
red: '#f85149',
|
|
277
|
+
green: '#56d364',
|
|
278
|
+
yellow: '#e3b341',
|
|
279
|
+
blue: '#58a6ff',
|
|
280
|
+
magenta: '#bc8cff',
|
|
281
|
+
cyan: '#39c5cf',
|
|
282
|
+
white: '#e6edf3'
|
|
283
|
+
},
|
|
284
|
+
allowProposedApi: true
|
|
285
|
+
});
|
|
281
286
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
terminalHistoryIndex += 1;
|
|
290
|
-
inputEl.value = terminalHistory[terminalHistoryIndex];
|
|
291
|
-
}
|
|
287
|
+
const fitAddon = new FitAddon.FitAddon();
|
|
288
|
+
term.loadAddon(fitAddon);
|
|
289
|
+
term.open(container);
|
|
290
|
+
fitAddon.fit();
|
|
291
|
+
xtermInstance = term;
|
|
292
292
|
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
293
|
+
// Connect WebSocket with auth token
|
|
294
|
+
const token = localStorage.getItem('lobstakit_token');
|
|
295
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
296
|
+
const wsUrl = `${proto}//${location.host}/ws/terminal${token ? '?token=' + encodeURIComponent(token) : ''}`;
|
|
297
|
+
const ws = new WebSocket(wsUrl);
|
|
298
|
+
terminalWs = ws;
|
|
299
|
+
|
|
300
|
+
ws.onopen = () => {
|
|
301
|
+
// Send initial resize
|
|
302
|
+
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
|
303
|
+
};
|
|
296
304
|
|
|
297
|
-
|
|
298
|
-
|
|
305
|
+
ws.onmessage = (event) => {
|
|
306
|
+
term.write(event.data);
|
|
307
|
+
};
|
|
299
308
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
setTerminalRunning(true);
|
|
309
|
+
ws.onclose = () => {
|
|
310
|
+
term.write('\r\n\x1b[31m[Connection closed]\x1b[0m\r\n');
|
|
311
|
+
};
|
|
304
312
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
headers: { 'Content-Type': 'application/json' },
|
|
309
|
-
body: JSON.stringify({ command: command })
|
|
310
|
-
}));
|
|
311
|
-
const data = await res.json();
|
|
313
|
+
ws.onerror = () => {
|
|
314
|
+
term.write('\r\n\x1b[31m[WebSocket error]\x1b[0m\r\n');
|
|
315
|
+
};
|
|
312
316
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return;
|
|
317
|
+
term.onData((data) => {
|
|
318
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
319
|
+
ws.send(data);
|
|
317
320
|
}
|
|
321
|
+
});
|
|
318
322
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
: '';
|
|
323
|
+
term.onResize(({ cols, rows }) => {
|
|
324
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
325
|
+
ws.send(JSON.stringify({ type: 'resize', cols, rows }));
|
|
326
|
+
}
|
|
327
|
+
});
|
|
325
328
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
}
|
|
329
|
+
// Responsive resize
|
|
330
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
331
|
+
try { fitAddon.fit(); } catch (e) {}
|
|
332
|
+
});
|
|
333
|
+
resizeObserver.observe(container);
|
|
333
334
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (!inputEl || !runBtn) return;
|
|
338
|
-
|
|
339
|
-
runBtn.addEventListener('click', runTerminalCommand);
|
|
340
|
-
inputEl.addEventListener('keydown', (event) => {
|
|
341
|
-
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
|
|
342
|
-
event.preventDefault();
|
|
343
|
-
runTerminalCommand();
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
if (event.key === 'ArrowUp') {
|
|
347
|
-
event.preventDefault();
|
|
348
|
-
terminalHistoryUp(inputEl);
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
if (event.key === 'ArrowDown') {
|
|
352
|
-
event.preventDefault();
|
|
353
|
-
terminalHistoryDown(inputEl);
|
|
354
|
-
}
|
|
335
|
+
// Clean up on page unload
|
|
336
|
+
window.addEventListener('beforeunload', () => {
|
|
337
|
+
if (terminalWs) { try { terminalWs.close(); } catch (e) {} }
|
|
355
338
|
});
|
|
356
339
|
}
|
|
357
340
|
|
package/public/manage.html
CHANGED
|
@@ -189,12 +189,11 @@
|
|
|
189
189
|
|
|
190
190
|
<div class="card" id="terminal-section">
|
|
191
191
|
<h3>🖥️ Terminal</h3>
|
|
192
|
-
<div
|
|
193
|
-
<div style="display:flex;gap:8px;">
|
|
194
|
-
<textarea id="terminal-input" placeholder="Enter command... (Ctrl+Enter to run)" rows="2" style="flex:1;font-family:monospace;font-size:13px;color:#e6edf3;background:#0d1117;border:1px solid #30363d;padding:6px 10px;border-radius:6px;outline:none;resize:vertical;"></textarea>
|
|
195
|
-
<button id="terminal-run-btn" class="btn btn-secondary">Run</button>
|
|
196
|
-
</div>
|
|
192
|
+
<div id="terminal-container" style="height:400px;background:#0d1117;border-radius:6px;overflow:hidden;"></div>
|
|
197
193
|
</div>
|
|
194
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
|
|
195
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
|
196
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
198
197
|
|
|
199
198
|
<!-- Channels Management -->
|
|
200
199
|
<div class="card" id="channels-card">
|
package/server.js
CHANGED
|
@@ -16,6 +16,8 @@ const crypto = require('crypto');
|
|
|
16
16
|
const config = require('./lib/config');
|
|
17
17
|
const gateway = require('./lib/gateway');
|
|
18
18
|
const proxyMiddleware = require('./lib/proxy');
|
|
19
|
+
const pty = require('node-pty');
|
|
20
|
+
const { WebSocketServer } = require('ws');
|
|
19
21
|
|
|
20
22
|
// Timing-safe string comparison to prevent timing attacks on secrets
|
|
21
23
|
function timingSafeCompare(a, b) {
|
|
@@ -2257,8 +2259,92 @@ const server = app.listen(PORT, () => {
|
|
|
2257
2259
|
}
|
|
2258
2260
|
});
|
|
2259
2261
|
|
|
2260
|
-
//
|
|
2262
|
+
// ─── Interactive Terminal (xterm.js + node-pty over WebSocket) ───────────────
|
|
2263
|
+
|
|
2264
|
+
const terminalWss = new WebSocketServer({ noServer: true });
|
|
2265
|
+
|
|
2266
|
+
terminalWss.on('connection', (ws) => {
|
|
2267
|
+
const shell = process.env.SHELL || '/bin/bash';
|
|
2268
|
+
const cwd = process.env.HOME || '/root';
|
|
2269
|
+
const ptyProcess = pty.spawn(shell, [], {
|
|
2270
|
+
name: 'xterm-256color',
|
|
2271
|
+
cols: 80,
|
|
2272
|
+
rows: 24,
|
|
2273
|
+
cwd: cwd,
|
|
2274
|
+
env: Object.assign({}, process.env, { TERM: 'xterm-256color' })
|
|
2275
|
+
});
|
|
2276
|
+
|
|
2277
|
+
ptyProcess.onData((data) => {
|
|
2278
|
+
try { ws.send(data); } catch (e) { /* ws closed */ }
|
|
2279
|
+
});
|
|
2280
|
+
|
|
2281
|
+
ptyProcess.onExit(() => {
|
|
2282
|
+
try { ws.close(); } catch (e) { /* already closed */ }
|
|
2283
|
+
});
|
|
2284
|
+
|
|
2285
|
+
ws.on('message', (msg) => {
|
|
2286
|
+
const data = msg.toString();
|
|
2287
|
+
// Check for resize messages: JSON { type: 'resize', cols, rows }
|
|
2288
|
+
if (data.startsWith('{')) {
|
|
2289
|
+
try {
|
|
2290
|
+
const parsed = JSON.parse(data);
|
|
2291
|
+
if (parsed.type === 'resize' && parsed.cols && parsed.rows) {
|
|
2292
|
+
ptyProcess.resize(Math.max(1, parsed.cols), Math.max(1, parsed.rows));
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
} catch (e) { /* not JSON, treat as input */ }
|
|
2296
|
+
}
|
|
2297
|
+
ptyProcess.write(data);
|
|
2298
|
+
});
|
|
2299
|
+
|
|
2300
|
+
ws.on('close', () => {
|
|
2301
|
+
try { ptyProcess.kill(); } catch (e) { /* already dead */ }
|
|
2302
|
+
});
|
|
2303
|
+
|
|
2304
|
+
ws.on('error', () => {
|
|
2305
|
+
try { ptyProcess.kill(); } catch (e) { /* already dead */ }
|
|
2306
|
+
});
|
|
2307
|
+
});
|
|
2308
|
+
|
|
2309
|
+
// WebSocket upgrade support — terminal + proxy
|
|
2261
2310
|
server.on('upgrade', (req, socket, head) => {
|
|
2311
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
2312
|
+
|
|
2313
|
+
if (url.pathname === '/ws/terminal') {
|
|
2314
|
+
// Auth check: require valid session token as query param
|
|
2315
|
+
const token = url.searchParams.get('token');
|
|
2316
|
+
const lobstaConfig = getLobstaKitConfig();
|
|
2317
|
+
|
|
2318
|
+
// If no password set yet, allow with setupToken
|
|
2319
|
+
if (!lobstaConfig.passwordHash) {
|
|
2320
|
+
if (!verifySetupToken(req)) {
|
|
2321
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
2322
|
+
socket.destroy();
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
} else {
|
|
2326
|
+
// Require valid session token
|
|
2327
|
+
if (!token || !activeSessions.has(token)) {
|
|
2328
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
2329
|
+
socket.destroy();
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
const session = activeSessions.get(token);
|
|
2333
|
+
if (Date.now() - session.created > SESSION_MAX_LIFETIME_MS) {
|
|
2334
|
+
activeSessions.delete(token);
|
|
2335
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
2336
|
+
socket.destroy();
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
terminalWss.handleUpgrade(req, socket, head, (ws) => {
|
|
2342
|
+
terminalWss.emit('connection', ws, req);
|
|
2343
|
+
});
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
// All other upgrades — proxy if configured
|
|
2262
2348
|
if (config.isConfigured()) {
|
|
2263
2349
|
proxyMiddleware.upgrade(req, socket, head);
|
|
2264
2350
|
} else {
|