termites 1.0.34 → 1.0.36

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.
Files changed (3) hide show
  1. package/package.json +3 -4
  2. package/server.js +17 -4
  3. package/index.js +0 -472
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "termites",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "Local multi-terminal manager with web interface",
5
- "main": "index.js",
5
+ "main": "server.js",
6
6
  "scripts": {
7
- "start": "node index.js",
8
- "server": "node server.js"
7
+ "start": "node server.js"
9
8
  },
10
9
  "bin": {
11
10
  "termites": "bin/termites.js"
package/server.js CHANGED
@@ -8,7 +8,7 @@ const os = require('os');
8
8
  const pty = require('node-pty');
9
9
 
10
10
  const PORT = process.env.PORT || 6789;
11
- const CONFIG_FILE = path.join(__dirname, '.termites.json');
11
+ const CONFIG_FILE = path.join(os.homedir(), '.termites.json');
12
12
  const SHELL = process.env.SHELL || '/bin/bash';
13
13
 
14
14
  // Load or create config
@@ -25,8 +25,21 @@ function saveConfig(config) {
25
25
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
26
26
  }
27
27
 
28
- function hashPassword(password) {
29
- return crypto.createHash('sha256').update(password).digest('hex');
28
+ // Use scrypt for secure password hashing
29
+ const SCRYPT_SALT_LENGTH = 16;
30
+ const SCRYPT_KEY_LENGTH = 64;
31
+
32
+ function hashPassword(password, salt) {
33
+ salt = salt || crypto.randomBytes(SCRYPT_SALT_LENGTH).toString('hex');
34
+ const hash = crypto.scryptSync(password, salt, SCRYPT_KEY_LENGTH).toString('hex');
35
+ return `${salt}:${hash}`;
36
+ }
37
+
38
+ function verifyPassword(password, storedHash) {
39
+ const [salt, hash] = storedHash.split(':');
40
+ if (!salt || !hash) return false;
41
+ const newHash = crypto.scryptSync(password, salt, SCRYPT_KEY_LENGTH).toString('hex');
42
+ return crypto.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(newHash, 'hex'));
30
43
  }
31
44
 
32
45
  class TermitesServer {
@@ -385,7 +398,7 @@ class TermitesServer {
385
398
  req.on('end', () => {
386
399
  try {
387
400
  const { password } = JSON.parse(body);
388
- if (hashPassword(password) === this.config.passwordHash) {
401
+ if (verifyPassword(password, this.config.passwordHash)) {
389
402
  res.writeHead(200, {
390
403
  'Content-Type': 'application/json',
391
404
  'Set-Cookie': `session=${this.sessionToken}; Path=/; HttpOnly; SameSite=Strict`
package/index.js DELETED
@@ -1,472 +0,0 @@
1
- #!/usr/bin/env node
2
- const http = require('http');
3
- const os = require('os');
4
- const WebSocket = require('ws');
5
-
6
- const PORT = process.env.PORT || 6789;
7
- const SHELL = process.env.SHELL || '/bin/bash';
8
-
9
- function getSystemInfo() {
10
- const username = os.userInfo().username;
11
- const hostname = os.hostname();
12
- const cwd = process.cwd().replace(os.homedir(), '~');
13
- return { username, hostname, cwd };
14
- }
15
-
16
- class WebTerminalServer {
17
- constructor(port) {
18
- this.port = port;
19
- this.pty = null;
20
- this.outputBuffer = [];
21
- this.clients = new Set();
22
- this.isRunning = false;
23
- this.systemInfo = getSystemInfo();
24
-
25
- this.startServer();
26
- }
27
-
28
- startServer() {
29
- this.httpServer = http.createServer((req, res) => {
30
- this.handleHttpRequest(req, res);
31
- });
32
-
33
- this.wss = new WebSocket.Server({ server: this.httpServer });
34
-
35
- this.wss.on('connection', (ws) => {
36
- console.log('新的 WebSocket 连接');
37
- this.clients.add(ws);
38
-
39
- ws.send(JSON.stringify({
40
- type: 'status',
41
- isRunning: this.isRunning,
42
- systemInfo: this.systemInfo
43
- }));
44
-
45
- this.outputBuffer.forEach(data => {
46
- ws.send(JSON.stringify({ type: 'output', data }));
47
- });
48
-
49
- ws.on('message', (message) => {
50
- try {
51
- const data = JSON.parse(message);
52
- this.handleClientMessage(data);
53
- } catch (e) {
54
- console.error('解析消息失败:', e);
55
- }
56
- });
57
-
58
- ws.on('close', () => {
59
- this.clients.delete(ws);
60
- });
61
- });
62
-
63
- this.httpServer.listen(this.port, () => {
64
- console.log(`Web Terminal 启动: http://localhost:${this.port}`);
65
- // 自动启动终端
66
- this.startTerminal();
67
- });
68
- }
69
-
70
- handleClientMessage(data) {
71
- switch (data.type) {
72
- case 'start':
73
- this.startTerminal();
74
- break;
75
- case 'stop':
76
- this.stopTerminal();
77
- break;
78
- case 'input':
79
- if (this.pty) this.pty.write(data.text);
80
- break;
81
- case 'resize':
82
- if (this.pty) this.pty.resize(data.cols, data.rows);
83
- break;
84
- }
85
- }
86
-
87
- broadcast(data) {
88
- const msg = JSON.stringify(data);
89
- this.clients.forEach(c => {
90
- if (c.readyState === WebSocket.OPEN) c.send(msg);
91
- });
92
- }
93
-
94
- startTerminal() {
95
- if (this.isRunning) return;
96
-
97
- const pty = require('node-pty');
98
- this.pty = pty.spawn(SHELL, ['-l'], {
99
- name: 'xterm-256color',
100
- cols: 120,
101
- rows: 40,
102
- cwd: process.cwd(),
103
- env: process.env
104
- });
105
-
106
- this.isRunning = true;
107
- this.broadcast({ type: 'started' });
108
-
109
- this.pty.onData((data) => {
110
- this.outputBuffer.push(data);
111
- if (this.outputBuffer.length > 1000) {
112
- this.outputBuffer = this.outputBuffer.slice(-500);
113
- }
114
- this.broadcast({ type: 'output', data });
115
- });
116
-
117
- this.pty.onExit(() => {
118
- this.isRunning = false;
119
- this.pty = null;
120
- this.broadcast({ type: 'stopped' });
121
- });
122
- }
123
-
124
- stopTerminal() {
125
- if (this.pty) {
126
- this.pty.kill();
127
- this.pty = null;
128
- this.isRunning = false;
129
- this.broadcast({ type: 'stopped' });
130
- }
131
- }
132
-
133
- handleHttpRequest(req, res) {
134
- if (req.url === '/' || req.url === '/index.html') {
135
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
136
- res.end(this.getHtmlPage());
137
- } else {
138
- res.writeHead(404);
139
- res.end('Not Found');
140
- }
141
- }
142
-
143
- getHtmlPage() {
144
- return `<!DOCTYPE html>
145
- <html lang="zh-CN">
146
- <head>
147
- <meta charset="UTF-8">
148
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
149
- <title>Web Terminal</title>
150
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
151
- <style>
152
- * { margin: 0; padding: 0; box-sizing: border-box; }
153
- body { height: 100vh; display: flex; flex-direction: column; transition: background 0.3s; }
154
- .header {
155
- padding: 8px 16px;
156
- display: flex; justify-content: space-between; align-items: center;
157
- border-bottom: 1px solid; font-family: monospace; transition: all 0.3s;
158
- }
159
- .header-left { display: flex; align-items: center; gap: 12px; }
160
- .path-info { font-size: 14px; }
161
- .path-info .user { font-weight: bold; color: var(--user-color, #859900); }
162
- .path-info .host { color: var(--host-color, #859900); }
163
- .path-info .sep { color: var(--sep-color, #657b83); }
164
- .path-info .path { color: var(--path-color, #268bd2); }
165
- .status.running { background: var(--status-running, #859900); color: #fff; }
166
- .status.stopped { background: var(--status-stopped, #dc322f); color: #fff; }
167
- .status { padding: 3px 10px; border-radius: 10px; font-size: 11px; }
168
- #terminal-container { flex: 1; padding: 5px; transition: background 0.3s; }
169
- .xterm { height: 100%; }
170
- .settings-btn {
171
- background: none; border: 1px solid; padding: 4px 10px;
172
- border-radius: 4px; cursor: pointer; font-size: 12px;
173
- font-family: monospace; transition: all 0.2s;
174
- }
175
- .settings-btn:hover { opacity: 0.8; }
176
- .settings-panel {
177
- position: fixed; top: 0; right: -320px; width: 300px; height: 100%;
178
- padding: 20px; box-shadow: -2px 0 10px rgba(0,0,0,0.2);
179
- transition: right 0.3s; z-index: 100; overflow-y: auto;
180
- }
181
- .settings-panel.open { right: 0; }
182
- .settings-panel h3 { margin-bottom: 20px; font-size: 16px; }
183
- .setting-group { margin-bottom: 20px; }
184
- .setting-group label { display: block; margin-bottom: 8px; font-size: 13px; font-weight: bold; }
185
- .setting-group select, .setting-group input {
186
- width: 100%; padding: 8px; border: 1px solid; border-radius: 4px;
187
- font-size: 13px; background: inherit; color: inherit;
188
- }
189
- .close-btn {
190
- position: absolute; top: 15px; right: 15px; background: none;
191
- border: none; font-size: 20px; cursor: pointer; color: inherit;
192
- }
193
- .overlay {
194
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
195
- background: rgba(0,0,0,0.3); z-index: 99; display: none;
196
- }
197
- .overlay.open { display: block; }
198
- </style>
199
- </head>
200
- <body>
201
- <div class="overlay" id="overlay"></div>
202
- <div class="settings-panel" id="settings-panel">
203
- <button class="close-btn" id="close-settings">&times;</button>
204
- <h3>设置</h3>
205
- <div class="setting-group">
206
- <label>主题</label>
207
- <select id="theme-select">
208
- <option value="solarized-light">Solarized Light</option>
209
- <option value="solarized-dark">Solarized Dark</option>
210
- <option value="monokai">Monokai</option>
211
- <option value="dracula">Dracula</option>
212
- <option value="nord">Nord</option>
213
- <option value="github-dark">GitHub Dark</option>
214
- </select>
215
- </div>
216
- <div class="setting-group">
217
- <label>字体</label>
218
- <select id="font-select">
219
- <option value="Monaco, Menlo, monospace">Monaco</option>
220
- <option value="'Fira Code', monospace">Fira Code</option>
221
- <option value="'JetBrains Mono', monospace">JetBrains Mono</option>
222
- <option value="'Source Code Pro', monospace">Source Code Pro</option>
223
- <option value="Consolas, monospace">Consolas</option>
224
- <option value="'Courier New', monospace">Courier New</option>
225
- </select>
226
- </div>
227
- <div class="setting-group">
228
- <label>字体大小</label>
229
- <input type="range" id="font-size" min="10" max="24" value="14">
230
- <span id="font-size-value">14px</span>
231
- </div>
232
- </div>
233
- <div class="header">
234
- <div class="header-left">
235
- <span id="path-info" class="path-info"></span>
236
- <span id="status" class="status stopped">已停止</span>
237
- </div>
238
- <button class="settings-btn" id="settings-btn">⚙ 设置</button>
239
- </div>
240
- <div id="terminal-container"></div>
241
-
242
- <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
243
- <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
244
- <script>
245
- let ws, term, fitAddon, isRunning = false;
246
-
247
- const themes = {
248
- 'solarized-light': {
249
- background: '#fdf6e3', foreground: '#657b83', cursor: '#657b83',
250
- cursorAccent: '#fdf6e3', selectionBackground: '#93a1a1',
251
- black: '#073642', red: '#dc322f', green: '#859900', yellow: '#b58900',
252
- blue: '#268bd2', magenta: '#d33682', cyan: '#2aa198', white: '#eee8d5',
253
- brightBlack: '#002b36', brightRed: '#cb4b16', brightGreen: '#586e75',
254
- brightYellow: '#657b83', brightBlue: '#839496', brightMagenta: '#6c71c4',
255
- brightCyan: '#93a1a1', brightWhite: '#fdf6e3',
256
- headerBg: '#eee8d5', headerBorder: '#93a1a1', userColor: '#859900',
257
- hostColor: '#859900', sepColor: '#657b83', pathColor: '#268bd2',
258
- statusRunning: '#859900', statusStopped: '#dc322f'
259
- },
260
- 'solarized-dark': {
261
- background: '#002b36', foreground: '#839496', cursor: '#839496',
262
- cursorAccent: '#002b36', selectionBackground: '#073642',
263
- black: '#073642', red: '#dc322f', green: '#859900', yellow: '#b58900',
264
- blue: '#268bd2', magenta: '#d33682', cyan: '#2aa198', white: '#eee8d5',
265
- brightBlack: '#002b36', brightRed: '#cb4b16', brightGreen: '#586e75',
266
- brightYellow: '#657b83', brightBlue: '#839496', brightMagenta: '#6c71c4',
267
- brightCyan: '#93a1a1', brightWhite: '#fdf6e3',
268
- headerBg: '#073642', headerBorder: '#586e75', userColor: '#859900',
269
- hostColor: '#859900', sepColor: '#839496', pathColor: '#268bd2',
270
- statusRunning: '#859900', statusStopped: '#dc322f'
271
- },
272
- 'monokai': {
273
- background: '#272822', foreground: '#f8f8f2', cursor: '#f8f8f2',
274
- cursorAccent: '#272822', selectionBackground: '#49483e',
275
- black: '#272822', red: '#f92672', green: '#a6e22e', yellow: '#f4bf75',
276
- blue: '#66d9ef', magenta: '#ae81ff', cyan: '#a1efe4', white: '#f8f8f2',
277
- brightBlack: '#75715e', brightRed: '#f92672', brightGreen: '#a6e22e',
278
- brightYellow: '#f4bf75', brightBlue: '#66d9ef', brightMagenta: '#ae81ff',
279
- brightCyan: '#a1efe4', brightWhite: '#f9f8f5',
280
- headerBg: '#1e1f1c', headerBorder: '#49483e', userColor: '#a6e22e',
281
- hostColor: '#a6e22e', sepColor: '#f8f8f2', pathColor: '#66d9ef',
282
- statusRunning: '#a6e22e', statusStopped: '#f92672'
283
- },
284
- 'dracula': {
285
- background: '#282a36', foreground: '#f8f8f2', cursor: '#f8f8f2',
286
- cursorAccent: '#282a36', selectionBackground: '#44475a',
287
- black: '#21222c', red: '#ff5555', green: '#50fa7b', yellow: '#f1fa8c',
288
- blue: '#bd93f9', magenta: '#ff79c6', cyan: '#8be9fd', white: '#f8f8f2',
289
- brightBlack: '#6272a4', brightRed: '#ff6e6e', brightGreen: '#69ff94',
290
- brightYellow: '#ffffa5', brightBlue: '#d6acff', brightMagenta: '#ff92df',
291
- brightCyan: '#a4ffff', brightWhite: '#ffffff',
292
- headerBg: '#21222c', headerBorder: '#44475a', userColor: '#50fa7b',
293
- hostColor: '#50fa7b', sepColor: '#f8f8f2', pathColor: '#bd93f9',
294
- statusRunning: '#50fa7b', statusStopped: '#ff5555'
295
- },
296
- 'nord': {
297
- background: '#2e3440', foreground: '#d8dee9', cursor: '#d8dee9',
298
- cursorAccent: '#2e3440', selectionBackground: '#434c5e',
299
- black: '#3b4252', red: '#bf616a', green: '#a3be8c', yellow: '#ebcb8b',
300
- blue: '#81a1c1', magenta: '#b48ead', cyan: '#88c0d0', white: '#e5e9f0',
301
- brightBlack: '#4c566a', brightRed: '#bf616a', brightGreen: '#a3be8c',
302
- brightYellow: '#ebcb8b', brightBlue: '#81a1c1', brightMagenta: '#b48ead',
303
- brightCyan: '#8fbcbb', brightWhite: '#eceff4',
304
- headerBg: '#3b4252', headerBorder: '#4c566a', userColor: '#a3be8c',
305
- hostColor: '#a3be8c', sepColor: '#d8dee9', pathColor: '#81a1c1',
306
- statusRunning: '#a3be8c', statusStopped: '#bf616a'
307
- },
308
- 'github-dark': {
309
- background: '#0d1117', foreground: '#c9d1d9', cursor: '#c9d1d9',
310
- cursorAccent: '#0d1117', selectionBackground: '#3b5070',
311
- black: '#0d1117', red: '#ff7b72', green: '#7ee787', yellow: '#d29922',
312
- blue: '#79c0ff', magenta: '#d2a8ff', cyan: '#a5d6ff', white: '#c9d1d9',
313
- brightBlack: '#484f58', brightRed: '#ffa198', brightGreen: '#aff5b4',
314
- brightYellow: '#e3b341', brightBlue: '#a5d6ff', brightMagenta: '#d2a8ff',
315
- brightCyan: '#b6e3ff', brightWhite: '#f0f6fc',
316
- headerBg: '#161b22', headerBorder: '#30363d', userColor: '#7ee787',
317
- hostColor: '#7ee787', sepColor: '#c9d1d9', pathColor: '#79c0ff',
318
- statusRunning: '#7ee787', statusStopped: '#ff7b72'
319
- }
320
- };
321
-
322
- let currentTheme = 'solarized-light';
323
- let currentFont = 'Monaco, Menlo, monospace';
324
- let currentFontSize = 14;
325
-
326
- function loadSettings() {
327
- const saved = localStorage.getItem('webterm-settings');
328
- if (saved) {
329
- const s = JSON.parse(saved);
330
- currentTheme = s.theme || currentTheme;
331
- currentFont = s.font || currentFont;
332
- currentFontSize = s.fontSize || currentFontSize;
333
- }
334
- document.getElementById('theme-select').value = currentTheme;
335
- document.getElementById('font-select').value = currentFont;
336
- document.getElementById('font-size').value = currentFontSize;
337
- document.getElementById('font-size-value').textContent = currentFontSize + 'px';
338
- }
339
-
340
- function saveSettings() {
341
- localStorage.setItem('webterm-settings', JSON.stringify({
342
- theme: currentTheme, font: currentFont, fontSize: currentFontSize
343
- }));
344
- }
345
-
346
- function applyTheme(themeName) {
347
- const t = themes[themeName];
348
- if (!t) return;
349
- currentTheme = themeName;
350
- document.body.style.background = t.background;
351
- document.getElementById('terminal-container').style.background = t.background;
352
- const header = document.querySelector('.header');
353
- header.style.background = t.headerBg;
354
- header.style.borderColor = t.headerBorder;
355
- header.style.color = t.foreground;
356
- document.querySelector('.settings-btn').style.borderColor = t.foreground;
357
- document.querySelector('.settings-btn').style.color = t.foreground;
358
- const panel = document.getElementById('settings-panel');
359
- panel.style.background = t.headerBg;
360
- panel.style.color = t.foreground;
361
- const style = document.documentElement.style;
362
- style.setProperty('--user-color', t.userColor);
363
- style.setProperty('--host-color', t.hostColor);
364
- style.setProperty('--sep-color', t.sepColor);
365
- style.setProperty('--path-color', t.pathColor);
366
- style.setProperty('--status-running', t.statusRunning);
367
- style.setProperty('--status-stopped', t.statusStopped);
368
- if (term) {
369
- term.options.theme = t;
370
- }
371
- saveSettings();
372
- }
373
-
374
- function applyFont(font) {
375
- currentFont = font;
376
- if (term) {
377
- term.options.fontFamily = font;
378
- fitAddon.fit();
379
- }
380
- saveSettings();
381
- }
382
-
383
- function applyFontSize(size) {
384
- currentFontSize = size;
385
- document.getElementById('font-size-value').textContent = size + 'px';
386
- if (term) {
387
- term.options.fontSize = size;
388
- fitAddon.fit();
389
- }
390
- saveSettings();
391
- }
392
-
393
- function setupSettings() {
394
- const btn = document.getElementById('settings-btn');
395
- const panel = document.getElementById('settings-panel');
396
- const overlay = document.getElementById('overlay');
397
- const closeBtn = document.getElementById('close-settings');
398
-
399
- btn.onclick = () => { panel.classList.add('open'); overlay.classList.add('open'); };
400
- closeBtn.onclick = () => { panel.classList.remove('open'); overlay.classList.remove('open'); };
401
- overlay.onclick = () => { panel.classList.remove('open'); overlay.classList.remove('open'); };
402
-
403
- document.getElementById('theme-select').onchange = e => applyTheme(e.target.value);
404
- document.getElementById('font-select').onchange = e => applyFont(e.target.value);
405
- document.getElementById('font-size').oninput = e => applyFontSize(parseInt(e.target.value));
406
- }
407
-
408
- function init() {
409
- loadSettings();
410
- setupSettings();
411
-
412
- term = new Terminal({
413
- theme: themes[currentTheme], fontFamily: currentFont,
414
- fontSize: currentFontSize, cursorBlink: true
415
- });
416
- fitAddon = new FitAddon.FitAddon();
417
- term.loadAddon(fitAddon);
418
- term.open(document.getElementById('terminal-container'));
419
- fitAddon.fit();
420
-
421
- term.onData(data => {
422
- if (ws?.readyState === WebSocket.OPEN) {
423
- ws.send(JSON.stringify({ type: 'input', text: data }));
424
- }
425
- });
426
-
427
- window.addEventListener('resize', () => {
428
- fitAddon.fit();
429
- if (ws?.readyState === WebSocket.OPEN) {
430
- ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
431
- }
432
- });
433
-
434
- connect();
435
- applyTheme(currentTheme);
436
- }
437
-
438
- function connect() {
439
- ws = new WebSocket('ws://' + location.host);
440
- ws.onopen = () => {
441
- ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
442
- };
443
- ws.onmessage = e => {
444
- const d = JSON.parse(e.data);
445
- if (d.type === 'output') term.write(d.data);
446
- else if (d.type === 'status' || d.type === 'started' || d.type === 'stopped') {
447
- isRunning = d.type === 'started' || (d.type === 'status' && d.isRunning);
448
- document.getElementById('status').textContent = isRunning ? '运行中' : '已停止';
449
- document.getElementById('status').className = 'status ' + (isRunning ? 'running' : 'stopped');
450
- if (d.systemInfo) {
451
- const i = d.systemInfo;
452
- document.getElementById('path-info').innerHTML =
453
- '<span class="user">' + i.username + '</span>' +
454
- '<span class="sep">@</span><span class="host">' + i.hostname + '</span>' +
455
- '<span class="sep">:</span><span class="path">' + i.cwd + '</span>';
456
- }
457
- }
458
- };
459
- ws.onclose = () => setTimeout(connect, 3000);
460
- }
461
-
462
- init();
463
- </script>
464
- </body>
465
- </html>`;
466
- }
467
- }
468
-
469
- new WebTerminalServer(PORT);
470
-
471
- process.on('SIGINT', () => process.exit(0));
472
- process.on('SIGTERM', () => process.exit(0));