@miraj181/ipingyou 2.1.9 → 2.1.18
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 +9 -4
- package/SECURITY.md +21 -0
- package/package.json +13 -13
- package/src/cli.js +55 -22
- package/src/lib/ai/safety.js +69 -12
- package/src/lib/broker.js +5 -5
- package/src/lib/chat.js +20 -6
- package/src/lib/checksum.js +22 -2
- package/src/lib/cleanup.js +60 -93
- package/src/lib/crypto.js +27 -0
- package/src/lib/open-url.js +28 -0
- package/src/lib/platform.js +38 -481
- package/src/lib/secure-print.js +7 -1
- package/src/lib/session-log.js +78 -3
- package/src/lib/socket-firewall.js +34 -0
- package/src/lib/ssh.js +32 -6
- package/src/lib/tunnel.js +1 -0
- package/src/lib/uid.js +6 -3
- package/src/lib/worker-runtime.js +81 -0
- package/src/lib/workers/crypto-checksum-worker.js +70 -0
- package/src/modes/ai.js +104 -31
- package/src/modes/client.js +20 -13
- package/src/modes/host.js +316 -116
- package/src/server.js +95 -18
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ No firewalls to configure. No port forwarding. No plaintext leakage.
|
|
|
20
20
|
* 📺 **Terminal Mirroring**: Wrap client SSH sessions in a multiplexed `tmux` terminal. The Host can spectate connected clients in real-time right from the dashboard to audit or assist.
|
|
21
21
|
* 🔄 **Reverse Port Forwarding (`ssh -R`)**: Clients can expose their *local* `localhost` development ports back to the Host through the secure tunnel.
|
|
22
22
|
* 📡 **Hardware Telemetry Verification**: Clients silently generate hardware footprint reports (OS, RAM, CPU, IP), encrypt them locally with the session password, and send them to the Host for authorization.
|
|
23
|
-
* 🚨 **
|
|
23
|
+
* 🚨 **Scoped Emergency Stop**: Type `ipingyou panic` and confirm locally to stop only processes and temporary credentials owned by the current iPingYou session.
|
|
24
24
|
* 👻 **Daemonization**: Run `ipingyou service install` to quietly install and run the Host listener in the background (survives system reboots using PM2).
|
|
25
25
|
* 🧭 **Approval Gate**: Require the Host to explicitly approve clients before they receive tunnel/key material.
|
|
26
26
|
* 📦 **One-Time File Share**: Serve a single file/folder over SCP and revoke after use.
|
|
@@ -48,12 +48,17 @@ npx @miraj181/ipingyou connect
|
|
|
48
48
|
|
|
49
49
|
### Global Install
|
|
50
50
|
```bash
|
|
51
|
-
|
|
51
|
+
# Install Socket Firewall once, then scan iPingYou before installation
|
|
52
|
+
npm install -g sfw
|
|
53
|
+
sfw npm install -g @miraj181/ipingyou
|
|
52
54
|
|
|
53
55
|
# Execute globally using aliases:
|
|
54
56
|
ipingyou
|
|
55
57
|
# or
|
|
56
58
|
securelink
|
|
59
|
+
|
|
60
|
+
# Verify future in-app npm installs are firewall-protected
|
|
61
|
+
ipingyou security-status
|
|
57
62
|
```
|
|
58
63
|
|
|
59
64
|
---
|
|
@@ -102,7 +107,7 @@ sequenceDiagram
|
|
|
102
107
|
|
|
103
108
|
## 🛡️ Security Scanner Disclaimer
|
|
104
109
|
|
|
105
|
-
Because **iPingYou** is a
|
|
110
|
+
Because **iPingYou** is a remote-access tool with background daemonization and SSH process management, automated security scanners may classify it as security-sensitive. Native dependencies are never downloaded or installed automatically, and emergency cleanup is limited to resources registered by the current process.
|
|
106
111
|
|
|
107
112
|
These alerts (e.g., "AI-detected potential code anomaly", "Shell access", "Network access") are **expected behavior** for a peer-to-peer tunneling utility. The source code is entirely open-source, heavily documented, and uses zero-knowledge encryption to ensure your data is safe.
|
|
108
113
|
|
|
@@ -128,7 +133,7 @@ These alerts (e.g., "AI-detected potential code anomaly", "Shell access", "Netwo
|
|
|
128
133
|
| `ipingyou connect -u <UID>` | Connect directly to a specific UID. |
|
|
129
134
|
| `ipingyou ai` | Groq-powered task assistant with guarded local/remote tools. |
|
|
130
135
|
| `ipingyou doctor` | Diagnostics for dependencies, SSH, broker, SCP, AI, and tests. |
|
|
131
|
-
| `ipingyou panic` | 🚨
|
|
136
|
+
| `ipingyou panic` | 🚨 Confirmed emergency stop for the current iPingYou session only. |
|
|
132
137
|
| `ipingyou service install` | 👻 Installs Host mode as an always-on background daemon. |
|
|
133
138
|
| `ipingyou service stop` | Stops and removes the background daemon. |
|
|
134
139
|
| `ipingyou service status` | Shows background daemon status. |
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Use this section to tell people about which versions of your project are
|
|
6
|
+
currently being supported with security updates.
|
|
7
|
+
|
|
8
|
+
| Version | Supported |
|
|
9
|
+
| ------- | ------------------ |
|
|
10
|
+
| 5.1.x | :white_check_mark: |
|
|
11
|
+
| 5.0.x | :x: |
|
|
12
|
+
| 4.0.x | :white_check_mark: |
|
|
13
|
+
| < 4.0 | :x: |
|
|
14
|
+
|
|
15
|
+
## Reporting a Vulnerability
|
|
16
|
+
|
|
17
|
+
Use this section to tell people how to report a vulnerability.
|
|
18
|
+
|
|
19
|
+
Tell them where to go, how often they can expect to get an update on a
|
|
20
|
+
reported vulnerability, what to expect if the vulnerability is accepted or
|
|
21
|
+
declined, etc.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraj181/ipingyou",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.18",
|
|
4
4
|
"description": "SecureLink-CLI — Secure peer-to-peer remote access via SSH & Cloudflare Tunnels",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
"src/lib/",
|
|
14
14
|
"src/modes/",
|
|
15
15
|
"README.md",
|
|
16
|
+
"SECURITY.md",
|
|
16
17
|
"LICENSE"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
19
20
|
"start": "node src/server.js",
|
|
20
21
|
"dev:cli": "node src/cli.js",
|
|
21
|
-
"test": "node test/test_ai_helpers.js",
|
|
22
|
+
"test": "node test/test_ai_helpers.js && node test/test_security_static.js && node test/test_e2e_crypto.js",
|
|
22
23
|
"prepublishOnly": "node -e \"require('fs').accessSync('LICENSE')\" && node src/cli.js --help"
|
|
23
24
|
},
|
|
24
25
|
"keywords": [
|
|
@@ -44,22 +45,21 @@
|
|
|
44
45
|
"url": "https://github.com/skmirajulislam/ipingyou/issues"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
|
-
"chalk": "
|
|
48
|
-
"commander": "
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"ws": "^8.20.1"
|
|
48
|
+
"chalk": "5.6.2",
|
|
49
|
+
"commander": "15.0.0",
|
|
50
|
+
"express": "4.22.2",
|
|
51
|
+
"express-rate-limit": "7.5.1",
|
|
52
|
+
"execa": "9.6.1",
|
|
53
|
+
"helmet": "8.2.0",
|
|
54
|
+
"inquirer": "12.11.1",
|
|
55
|
+
"ora": "9.4.1",
|
|
56
|
+
"ws": "8.21.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"nodemon": "^3.1.4"
|
|
60
60
|
},
|
|
61
61
|
"engines": {
|
|
62
|
-
"node": ">=
|
|
62
|
+
"node": ">=22.12.0"
|
|
63
63
|
},
|
|
64
64
|
"type": "module"
|
|
65
65
|
}
|
package/src/cli.js
CHANGED
|
@@ -23,11 +23,13 @@ import inquirer from 'inquirer';
|
|
|
23
23
|
import chalk from 'chalk';
|
|
24
24
|
import fs from 'node:fs';
|
|
25
25
|
import path from 'node:path';
|
|
26
|
+
import readline from 'node:readline';
|
|
26
27
|
import { fileURLToPath } from 'node:url';
|
|
27
28
|
|
|
28
29
|
import { detectOS, checkDependencies } from './lib/platform.js';
|
|
29
30
|
import { cleanupAll, installShutdownHandlers, executePanicMode } from './lib/cleanup.js';
|
|
30
31
|
import { cleanupSessionLog } from './lib/session-log.js';
|
|
32
|
+
import { getSocketFirewallStatus, runProtectedNpmInstall } from './lib/socket-firewall.js';
|
|
31
33
|
import { startHostMode } from './modes/host.js';
|
|
32
34
|
import { startClientMode } from './modes/client.js';
|
|
33
35
|
import { startAIMode } from './modes/ai.js';
|
|
@@ -329,10 +331,19 @@ program
|
|
|
329
331
|
|
|
330
332
|
program
|
|
331
333
|
.command('panic')
|
|
332
|
-
.description('🚨
|
|
334
|
+
.description('🚨 Stop resources owned by the current iPingYou session')
|
|
333
335
|
.action(async () => {
|
|
334
336
|
try {
|
|
335
337
|
showBanner();
|
|
338
|
+
const { confirmation } = await inquirer.prompt([{
|
|
339
|
+
type: 'input',
|
|
340
|
+
name: 'confirmation',
|
|
341
|
+
message: 'Type STOP CURRENT SESSION to confirm:',
|
|
342
|
+
}]);
|
|
343
|
+
if (confirmation !== 'STOP CURRENT SESSION') {
|
|
344
|
+
console.log(chalk.yellow(' Emergency shutdown cancelled.'));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
336
347
|
await executePanicMode();
|
|
337
348
|
} catch (err) {
|
|
338
349
|
fatal('panic', err);
|
|
@@ -351,8 +362,8 @@ program
|
|
|
351
362
|
const { execa } = await import('execa');
|
|
352
363
|
|
|
353
364
|
if (action === 'install') {
|
|
354
|
-
console.log(chalk.dim('
|
|
355
|
-
await
|
|
365
|
+
console.log(chalk.dim(' Scanning and installing PM2 through Socket Firewall...'));
|
|
366
|
+
await runProtectedNpmInstall(['-g', 'pm2'], { stdio: 'inherit' });
|
|
356
367
|
await execa('pm2', ['start', 'ipingyou', '--name', 'ipingyou-host', '--', 'host'], { stdio: 'inherit' });
|
|
357
368
|
await execa('pm2', ['save'], { stdio: 'inherit' });
|
|
358
369
|
await execa('pm2', ['startup'], { stdio: 'inherit' });
|
|
@@ -372,6 +383,26 @@ program
|
|
|
372
383
|
}
|
|
373
384
|
});
|
|
374
385
|
|
|
386
|
+
program
|
|
387
|
+
.command('security-status')
|
|
388
|
+
.description('Check Socket Firewall protection for future npm installations')
|
|
389
|
+
.action(async () => {
|
|
390
|
+
try {
|
|
391
|
+
showBanner();
|
|
392
|
+
const status = await getSocketFirewallStatus();
|
|
393
|
+
if (!status.available) {
|
|
394
|
+
console.log(chalk.yellow(' ⚠️ Socket Firewall is not available.'));
|
|
395
|
+
console.log(chalk.dim(' Install it with: npm install -g sfw'));
|
|
396
|
+
process.exitCode = 1;
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
console.log(chalk.green(` ✅ Socket Firewall active${status.version ? ` (${status.version})` : ''}`));
|
|
400
|
+
console.log(chalk.dim(' In-app npm installations are routed through: sfw npm install'));
|
|
401
|
+
} catch (err) {
|
|
402
|
+
fatal('security-status', err);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
375
406
|
program
|
|
376
407
|
.command('allowlist')
|
|
377
408
|
.description('Manage AI command allowlist — add, remove, or list safe regex patterns')
|
|
@@ -474,26 +505,28 @@ program
|
|
|
474
505
|
return;
|
|
475
506
|
}
|
|
476
507
|
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
})
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
508
|
+
const parsedCount = Number.parseInt(commandOptions.lines, 10);
|
|
509
|
+
const count = Number.isFinite(parsedCount) ? Math.max(1, Math.min(parsedCount, 10000)) : 25;
|
|
510
|
+
const filter = commandOptions.type?.toLowerCase();
|
|
511
|
+
const events = [];
|
|
512
|
+
let totalEvents = 0;
|
|
513
|
+
const lines = readline.createInterface({
|
|
514
|
+
input: fs.createReadStream(logFile, { encoding: 'utf8' }),
|
|
515
|
+
crlfDelay: Infinity,
|
|
516
|
+
});
|
|
517
|
+
for await (const line of lines) {
|
|
518
|
+
if (!line.trim()) continue;
|
|
519
|
+
totalEvents += 1;
|
|
520
|
+
try {
|
|
521
|
+
const event = JSON.parse(line);
|
|
522
|
+
if (filter && !(event.type || '').toLowerCase().includes(filter)) continue;
|
|
523
|
+
events.push(event);
|
|
524
|
+
if (events.length > count) events.shift();
|
|
525
|
+
} catch {
|
|
526
|
+
// Ignore incomplete or invalid log lines.
|
|
527
|
+
}
|
|
491
528
|
}
|
|
492
529
|
|
|
493
|
-
// Take last N events
|
|
494
|
-
const count = parseInt(commandOptions.lines) || 25;
|
|
495
|
-
events = events.slice(-count);
|
|
496
|
-
|
|
497
530
|
if (events.length === 0) {
|
|
498
531
|
console.log(chalk.dim(' No matching events found.'));
|
|
499
532
|
return;
|
|
@@ -526,7 +559,7 @@ program
|
|
|
526
559
|
|
|
527
560
|
console.log('');
|
|
528
561
|
console.log(chalk.dim(` Log file: ${logFile}`));
|
|
529
|
-
console.log(chalk.dim(` Total events in file: ${
|
|
562
|
+
console.log(chalk.dim(` Total events in file: ${totalEvents}`));
|
|
530
563
|
} catch (err) {
|
|
531
564
|
fatal('history', err);
|
|
532
565
|
}
|
package/src/lib/ai/safety.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getAllowlistRegexes } from '../allowlist.js';
|
|
2
|
+
|
|
1
3
|
const SECRET_PATTERNS = [
|
|
2
4
|
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
3
5
|
/\b(gsk_[A-Za-z0-9_-]{20,})\b/g,
|
|
@@ -20,6 +22,31 @@ const BLOCKED_PATH_PATTERNS = [
|
|
|
20
22
|
/id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/,
|
|
21
23
|
];
|
|
22
24
|
|
|
25
|
+
const BLOCKED_PATH_SEGMENTS = new Set([
|
|
26
|
+
'.ssh',
|
|
27
|
+
'.gnupg',
|
|
28
|
+
'.aws',
|
|
29
|
+
'.ipingyou',
|
|
30
|
+
'.kube',
|
|
31
|
+
'.docker',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const BLOCKED_PATH_BASENAMES = new Set([
|
|
35
|
+
'.npmrc',
|
|
36
|
+
'.netrc',
|
|
37
|
+
'.pypirc',
|
|
38
|
+
'credentials',
|
|
39
|
+
'credentials.json',
|
|
40
|
+
'known_hosts',
|
|
41
|
+
'authorized_keys',
|
|
42
|
+
'shadow',
|
|
43
|
+
'sudoers',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const PRIVATE_KEY_BASENAME = /^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/i;
|
|
47
|
+
const ENV_BASENAME = /^\.env(?:\.|$)/i;
|
|
48
|
+
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
|
|
49
|
+
|
|
23
50
|
const BLOCKED_COMMAND_PATTERNS = [
|
|
24
51
|
/^\s*(env|printenv|set)\b/,
|
|
25
52
|
/\b(export|declare)\s+-p\b/,
|
|
@@ -41,8 +68,7 @@ const DANGEROUS_COMMAND_PATTERNS = [
|
|
|
41
68
|
/\b(curl|wget)\b/,
|
|
42
69
|
/>|>>|\btee\b/,
|
|
43
70
|
];
|
|
44
|
-
|
|
45
|
-
import { getAllowlistRegexes } from '../allowlist.js';
|
|
71
|
+
const SHELL_SYNTAX_PATTERN = /(?:[;&|`$()]|\\\r?\n)/;
|
|
46
72
|
|
|
47
73
|
const READ_ONLY_COMMAND_PATTERNS = [
|
|
48
74
|
/^\s*(pwd|ls|find|rg|grep|sed|cat|head|tail|wc|git status|git diff|git log|git show|node --version|npm --version|which|date|uname)\b/,
|
|
@@ -66,6 +92,33 @@ export function commandTouchesBlockedPath(command) {
|
|
|
66
92
|
return BLOCKED_PATH_PATTERNS.some(pattern => pattern.test(command));
|
|
67
93
|
}
|
|
68
94
|
|
|
95
|
+
export function assertSafeReadablePath(filePath) {
|
|
96
|
+
const value = String(filePath ?? '');
|
|
97
|
+
if (!value || value.length > 4096) {
|
|
98
|
+
throw new Error('Invalid file path');
|
|
99
|
+
}
|
|
100
|
+
if (CONTROL_CHARACTERS.test(value)) {
|
|
101
|
+
throw new Error('File path contains control characters');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const normalized = value.replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
105
|
+
const segments = normalized.toLowerCase().split('/').filter(Boolean);
|
|
106
|
+
const basename = segments.at(-1) || '';
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
segments.some(segment => BLOCKED_PATH_SEGMENTS.has(segment))
|
|
110
|
+
|| BLOCKED_PATH_BASENAMES.has(basename)
|
|
111
|
+
|| PRIVATE_KEY_BASENAME.test(basename)
|
|
112
|
+
|| ENV_BASENAME.test(basename)
|
|
113
|
+
|| normalized.toLowerCase().includes('/.config/gh/')
|
|
114
|
+
|| normalized.toLowerCase().includes('/proc/') && basename === 'environ'
|
|
115
|
+
) {
|
|
116
|
+
throw new Error('Path references a protected secret/config location');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
69
122
|
export function classifyCommand(command) {
|
|
70
123
|
const text = String(command || '').trim();
|
|
71
124
|
if (!text) return { blocked: true, needsApproval: true, reason: 'Empty command' };
|
|
@@ -78,16 +131,6 @@ export function classifyCommand(command) {
|
|
|
78
131
|
};
|
|
79
132
|
}
|
|
80
133
|
|
|
81
|
-
// Check user-provided allowlist first (if present)
|
|
82
|
-
try {
|
|
83
|
-
const userPatterns = getAllowlistRegexes();
|
|
84
|
-
if (Array.isArray(userPatterns) && userPatterns.some(p => p.test(text))) {
|
|
85
|
-
return { blocked: false, needsApproval: false, reason: 'Matched user allowlist' };
|
|
86
|
-
}
|
|
87
|
-
} catch {
|
|
88
|
-
// ignore allowlist errors and fall back to defaults
|
|
89
|
-
}
|
|
90
|
-
|
|
91
134
|
if (BLOCKED_COMMAND_PATTERNS.some(pattern => pattern.test(text))) {
|
|
92
135
|
return {
|
|
93
136
|
blocked: true,
|
|
@@ -100,6 +143,20 @@ export function classifyCommand(command) {
|
|
|
100
143
|
return { blocked: false, needsApproval: true, reason: 'Command may modify files, install packages, or access the network' };
|
|
101
144
|
}
|
|
102
145
|
|
|
146
|
+
if (SHELL_SYNTAX_PATTERN.test(text)) {
|
|
147
|
+
return { blocked: false, needsApproval: true, reason: 'Command uses shell syntax and requires explicit approval' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// User allowlists may suppress prompts only after non-bypassable safety checks.
|
|
151
|
+
try {
|
|
152
|
+
const userPatterns = getAllowlistRegexes();
|
|
153
|
+
if (Array.isArray(userPatterns) && userPatterns.some(pattern => pattern.test(text))) {
|
|
154
|
+
return { blocked: false, needsApproval: false, reason: 'Matched user allowlist' };
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// Ignore allowlist errors and fall back to defaults.
|
|
158
|
+
}
|
|
159
|
+
|
|
103
160
|
if (READ_ONLY_COMMAND_PATTERNS.some(pattern => pattern.test(text))) {
|
|
104
161
|
return { blocked: false, needsApproval: false, reason: 'Read-only command' };
|
|
105
162
|
}
|
package/src/lib/broker.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
-
import {
|
|
4
|
+
import { decryptAsync, encryptAsync } from './crypto.js';
|
|
5
5
|
import { createSpinner, cryptoSpinner, networkSpinner } from './animations.js';
|
|
6
6
|
import { logSessionEvent } from './session-log.js';
|
|
7
7
|
|
|
@@ -51,7 +51,7 @@ export async function registerWithBroker(brokerUrl, uid, tunnelUrl, password, se
|
|
|
51
51
|
try {
|
|
52
52
|
await new Promise(r => setTimeout(r, 600));
|
|
53
53
|
const payload = JSON.stringify({ url: tunnelUrl, ...serviceConfig });
|
|
54
|
-
const encrypted =
|
|
54
|
+
const encrypted = await encryptAsync(payload, password);
|
|
55
55
|
const localHostToken = crypto.randomBytes(32).toString('hex');
|
|
56
56
|
|
|
57
57
|
spinner.text = 'Registering with broker...';
|
|
@@ -92,7 +92,7 @@ export async function registerWithBroker(brokerUrl, uid, tunnelUrl, password, se
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
export async function requestHostApproval(brokerUrl, uid, password, details) {
|
|
95
|
-
const encrypted =
|
|
95
|
+
const encrypted = await encryptAsync(JSON.stringify(details), password);
|
|
96
96
|
const res = await fetchWithLog('approval_request', `${brokerUrl}/approval-request/${uid}`, {
|
|
97
97
|
method: 'POST',
|
|
98
98
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -209,7 +209,7 @@ export async function resolveUID(brokerUrl, uid, password, silent = false, reque
|
|
|
209
209
|
|
|
210
210
|
let decryptedPayload;
|
|
211
211
|
try {
|
|
212
|
-
decryptedPayload =
|
|
212
|
+
decryptedPayload = await decryptAsync(data.iv, data.ciphertext, password, data.salt);
|
|
213
213
|
} catch {
|
|
214
214
|
if (spinner) spinner.fail('Decryption failed — incorrect password or corrupted data');
|
|
215
215
|
if (!spinner) console.error(chalk.red(' ❌ Error: Could not decrypt tunnel data. Incorrect password.'));
|
|
@@ -269,7 +269,7 @@ export async function pushTelemetry(brokerUrl, uid, password, username, action =
|
|
|
269
269
|
time: new Date().toISOString()
|
|
270
270
|
};
|
|
271
271
|
|
|
272
|
-
const { iv, ciphertext, salt } =
|
|
272
|
+
const { iv, ciphertext, salt } = await encryptAsync(JSON.stringify(telemetry), password);
|
|
273
273
|
|
|
274
274
|
await fetchWithLog('telemetry', `${parsed.origin}/client-info/${encodeURIComponent(uid)}`, {
|
|
275
275
|
method: 'POST',
|
package/src/lib/chat.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { WebSocketServer } from 'ws';
|
|
3
|
-
import
|
|
3
|
+
import { openUrl } from './open-url.js';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { secureSensitiveUrl } from './secure-print.js';
|
|
6
6
|
|
|
@@ -97,9 +97,17 @@ const HTML_CONTENT = `
|
|
|
97
97
|
let username = isHost ? 'Host' : prompt('Enter your name for the chat:', 'Client_' + Math.floor(Math.random()*1000));
|
|
98
98
|
if (!username) username = 'Anonymous';
|
|
99
99
|
|
|
100
|
+
function showBodyMessage(text, color) {
|
|
101
|
+
const message = document.createElement('h2');
|
|
102
|
+
message.style.cssText = 'text-align:center;margin-top:20vh';
|
|
103
|
+
if (color) message.style.color = color;
|
|
104
|
+
message.textContent = text;
|
|
105
|
+
document.body.replaceChildren(message);
|
|
106
|
+
}
|
|
107
|
+
|
|
100
108
|
const sessionPassword = window.location.hash.substring(1);
|
|
101
109
|
if (!sessionPassword) {
|
|
102
|
-
|
|
110
|
+
showBodyMessage('Fatal: Missing session password in URL hash. Cannot decrypt E2E chat.', 'red');
|
|
103
111
|
throw new Error("Missing password");
|
|
104
112
|
}
|
|
105
113
|
|
|
@@ -168,14 +176,20 @@ const HTML_CONTENT = `
|
|
|
168
176
|
div.textContent = msg.text;
|
|
169
177
|
} else {
|
|
170
178
|
div.className = 'message ' + (msg.sender === username ? 'self' : 'other');
|
|
171
|
-
|
|
179
|
+
const header = document.createElement('div');
|
|
180
|
+
header.className = 'message-header';
|
|
181
|
+
header.textContent = String(msg.sender || 'Unknown') + ' • ' + String(msg.time || '');
|
|
182
|
+
const body = document.createElement('div');
|
|
183
|
+
body.textContent = String(msg.text || '');
|
|
184
|
+
div.appendChild(header);
|
|
185
|
+
div.appendChild(body);
|
|
172
186
|
}
|
|
173
187
|
msgs.appendChild(div);
|
|
174
188
|
msgs.scrollTop = msgs.scrollHeight;
|
|
175
189
|
}
|
|
176
190
|
|
|
177
191
|
function updateUsers(users) {
|
|
178
|
-
usersList.
|
|
192
|
+
usersList.replaceChildren();
|
|
179
193
|
users.forEach(u => {
|
|
180
194
|
const li = document.createElement('li');
|
|
181
195
|
li.className = 'user-item';
|
|
@@ -246,7 +260,7 @@ const HTML_CONTENT = `
|
|
|
246
260
|
} else {
|
|
247
261
|
ws.close();
|
|
248
262
|
window.close();
|
|
249
|
-
|
|
263
|
+
showBodyMessage('You have left the chat. You can close this tab.');
|
|
250
264
|
}
|
|
251
265
|
};
|
|
252
266
|
</script>
|
|
@@ -321,7 +335,7 @@ export async function startChatServer(onClose) {
|
|
|
321
335
|
export async function openLocalChatUI(port, password) {
|
|
322
336
|
try {
|
|
323
337
|
const chatUrl = `http://localhost:${port}#${password}`;
|
|
324
|
-
await
|
|
338
|
+
await openUrl(chatUrl);
|
|
325
339
|
} catch {
|
|
326
340
|
console.log(chalk.dim(` Unable to auto-open browser. Visit ${secureSensitiveUrl(`http://localhost:${port}`, password)}`));
|
|
327
341
|
}
|
package/src/lib/checksum.js
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import { canUseWorkers, runWorkerTask } from './worker-runtime.js';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
const WORKER_CHECKSUM_THRESHOLD_BYTES = 2 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
async function calculateChecksumStream(filePath) {
|
|
5
8
|
return new Promise((resolve) => {
|
|
6
9
|
const hash = crypto.createHash('sha256');
|
|
7
10
|
const stream = fs.createReadStream(filePath);
|
|
8
11
|
stream.on('error', () => resolve(null));
|
|
9
|
-
stream.on('data', chunk => hash.update(chunk));
|
|
12
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
10
13
|
stream.on('end', () => resolve(hash.digest('hex')));
|
|
11
14
|
});
|
|
12
15
|
}
|
|
16
|
+
|
|
17
|
+
export async function calculateChecksum(filePath) {
|
|
18
|
+
const stat = await fs.promises.stat(filePath).catch(() => null);
|
|
19
|
+
if (!stat || !stat.isFile()) return null;
|
|
20
|
+
|
|
21
|
+
if (canUseWorkers() && stat.size >= WORKER_CHECKSUM_THRESHOLD_BYTES) {
|
|
22
|
+
try {
|
|
23
|
+
const result = await runWorkerTask('checksum', { filePath });
|
|
24
|
+
return result.digest || null;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (err?.code === 'WORKER_QUEUE_FULL') throw err;
|
|
27
|
+
return calculateChecksumStream(filePath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return calculateChecksumStream(filePath);
|
|
32
|
+
}
|