typescript-virtual-container 1.3.3 → 1.4.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/.vscode/settings.json +0 -1
- package/README.md +674 -1504
- package/benchmark-results.txt +21 -21
- package/builds/self-standalone.js +274 -208
- package/builds/self-standalone.js.map +4 -4
- package/builds/standalone-wo-sftp.js +201 -149
- package/builds/standalone-wo-sftp.js.map +4 -4
- package/builds/standalone.js +263 -211
- package/builds/standalone.js.map +4 -4
- package/builds/web-full-api.min.js +3 -3
- package/builds/web-full-api.min.js.map +4 -4
- package/builds/web.min.js +2 -2
- package/builds/web.min.js.map +4 -4
- package/bun.lock +14 -12
- package/dist/SSHClient/index.d.ts.map +1 -1
- package/dist/SSHClient/index.js +5 -3
- package/dist/SSHMimic/executor.d.ts +1 -3
- package/dist/SSHMimic/executor.d.ts.map +1 -1
- package/dist/SSHMimic/executor.js +20 -22
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +5 -3
- package/dist/SSHMimic/sftp.d.ts.map +1 -1
- package/dist/SSHMimic/sftp.js +26 -21
- package/dist/VirtualShell/shell.d.ts.map +1 -1
- package/dist/VirtualShell/shell.js +25 -3
- package/dist/VirtualShell/shellParser.d.ts +1 -8
- package/dist/VirtualShell/shellParser.d.ts.map +1 -1
- package/dist/VirtualShell/shellParser.js +2 -81
- package/dist/VirtualUserManager/index.d.ts +7 -1
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +47 -19
- package/dist/commands/adduser.d.ts +10 -4
- package/dist/commands/adduser.d.ts.map +1 -1
- package/dist/commands/adduser.js +75 -12
- package/dist/commands/alias.d.ts +5 -0
- package/dist/commands/alias.d.ts.map +1 -1
- package/dist/commands/alias.js +5 -0
- package/dist/commands/apt.d.ts +5 -0
- package/dist/commands/apt.d.ts.map +1 -1
- package/dist/commands/apt.js +5 -0
- package/dist/commands/awk.d.ts +10 -8
- package/dist/commands/awk.d.ts.map +1 -1
- package/dist/commands/awk.js +156 -28
- package/dist/commands/cd.d.ts.map +1 -1
- package/dist/commands/cd.js +0 -3
- package/dist/commands/clear.d.ts +5 -0
- package/dist/commands/clear.d.ts.map +1 -1
- package/dist/commands/clear.js +5 -0
- package/dist/commands/command-helpers.d.ts.map +1 -1
- package/dist/commands/command-helpers.js +8 -0
- package/dist/commands/declare.d.ts +5 -0
- package/dist/commands/declare.d.ts.map +1 -1
- package/dist/commands/declare.js +5 -0
- package/dist/commands/deluser.d.ts +12 -0
- package/dist/commands/deluser.d.ts.map +1 -1
- package/dist/commands/deluser.js +72 -6
- package/dist/commands/df.d.ts +5 -0
- package/dist/commands/df.d.ts.map +1 -1
- package/dist/commands/df.js +5 -0
- package/dist/commands/du.d.ts +5 -0
- package/dist/commands/du.d.ts.map +1 -1
- package/dist/commands/du.js +5 -0
- package/dist/commands/export.d.ts +5 -0
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +5 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +22 -4
- package/dist/commands/groups.d.ts +5 -0
- package/dist/commands/groups.d.ts.map +1 -1
- package/dist/commands/groups.js +5 -0
- package/dist/commands/gzip.d.ts +5 -2
- package/dist/commands/gzip.d.ts.map +1 -1
- package/dist/commands/gzip.js +48 -28
- package/dist/commands/head.d.ts.map +1 -1
- package/dist/commands/head.js +12 -3
- package/dist/commands/htop.d.ts +5 -0
- package/dist/commands/htop.d.ts.map +1 -1
- package/dist/commands/htop.js +5 -0
- package/dist/commands/kill.d.ts +5 -0
- package/dist/commands/kill.d.ts.map +1 -1
- package/dist/commands/kill.js +5 -0
- package/dist/commands/ln.d.ts +2 -0
- package/dist/commands/ln.d.ts.map +1 -1
- package/dist/commands/ln.js +22 -0
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +15 -0
- package/dist/commands/lsb-release.d.ts +5 -0
- package/dist/commands/lsb-release.d.ts.map +1 -1
- package/dist/commands/lsb-release.js +5 -0
- package/dist/commands/mkdir.d.ts +5 -0
- package/dist/commands/mkdir.d.ts.map +1 -1
- package/dist/commands/mkdir.js +5 -0
- package/dist/commands/mv.d.ts +5 -0
- package/dist/commands/mv.d.ts.map +1 -1
- package/dist/commands/mv.js +5 -0
- package/dist/commands/nano.d.ts +5 -0
- package/dist/commands/nano.d.ts.map +1 -1
- package/dist/commands/nano.js +5 -0
- package/dist/commands/neofetch.d.ts +5 -0
- package/dist/commands/neofetch.d.ts.map +1 -1
- package/dist/commands/neofetch.js +8 -5
- package/dist/commands/passwd.d.ts +8 -0
- package/dist/commands/passwd.d.ts.map +1 -1
- package/dist/commands/passwd.js +32 -11
- package/dist/commands/ping.d.ts +5 -0
- package/dist/commands/ping.d.ts.map +1 -1
- package/dist/commands/ping.js +5 -0
- package/dist/commands/printf.d.ts +5 -0
- package/dist/commands/printf.d.ts.map +1 -1
- package/dist/commands/printf.js +43 -12
- package/dist/commands/ps.d.ts +5 -0
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +5 -0
- package/dist/commands/read.d.ts +5 -0
- package/dist/commands/read.d.ts.map +1 -1
- package/dist/commands/read.js +5 -0
- package/dist/commands/registry.d.ts.map +1 -1
- package/dist/commands/registry.js +4 -1
- package/dist/commands/rm.d.ts +5 -0
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +5 -0
- package/dist/commands/runtime.d.ts.map +1 -1
- package/dist/commands/runtime.js +1 -57
- package/dist/commands/sed.d.ts +5 -0
- package/dist/commands/sed.d.ts.map +1 -1
- package/dist/commands/sed.js +5 -0
- package/dist/commands/set.d.ts +5 -6
- package/dist/commands/set.d.ts.map +1 -1
- package/dist/commands/set.js +5 -22
- package/dist/commands/sh.d.ts +6 -0
- package/dist/commands/sh.d.ts.map +1 -1
- package/dist/commands/sh.js +6 -0
- package/dist/commands/shift.d.ts +10 -0
- package/dist/commands/shift.d.ts.map +1 -1
- package/dist/commands/shift.js +10 -0
- package/dist/commands/sleep.d.ts +5 -0
- package/dist/commands/sleep.d.ts.map +1 -1
- package/dist/commands/sleep.js +5 -0
- package/dist/commands/sort.d.ts +5 -0
- package/dist/commands/sort.d.ts.map +1 -1
- package/dist/commands/sort.js +5 -0
- package/dist/commands/source.d.ts +5 -0
- package/dist/commands/source.d.ts.map +1 -1
- package/dist/commands/source.js +5 -0
- package/dist/commands/stat.d.ts +7 -0
- package/dist/commands/stat.d.ts.map +1 -0
- package/dist/commands/stat.js +56 -0
- package/dist/commands/su.d.ts +13 -0
- package/dist/commands/su.d.ts.map +1 -1
- package/dist/commands/su.js +45 -14
- package/dist/commands/sudo.d.ts.map +1 -1
- package/dist/commands/sudo.js +5 -0
- package/dist/commands/tail.d.ts +5 -0
- package/dist/commands/tail.d.ts.map +1 -1
- package/dist/commands/tail.js +15 -3
- package/dist/commands/tar.d.ts +5 -0
- package/dist/commands/tar.d.ts.map +1 -1
- package/dist/commands/tar.js +40 -10
- package/dist/commands/tee.d.ts +5 -0
- package/dist/commands/tee.d.ts.map +1 -1
- package/dist/commands/tee.js +5 -0
- package/dist/commands/touch.d.ts +5 -0
- package/dist/commands/touch.d.ts.map +1 -1
- package/dist/commands/touch.js +5 -0
- package/dist/commands/tr.d.ts.map +1 -1
- package/dist/commands/tr.js +45 -10
- package/dist/commands/tree.d.ts +5 -0
- package/dist/commands/tree.d.ts.map +1 -1
- package/dist/commands/tree.js +5 -0
- package/dist/commands/true.d.ts +10 -0
- package/dist/commands/true.d.ts.map +1 -1
- package/dist/commands/true.js +10 -0
- package/dist/commands/type.d.ts +5 -0
- package/dist/commands/type.d.ts.map +1 -1
- package/dist/commands/type.js +5 -0
- package/dist/commands/uname.d.ts +5 -0
- package/dist/commands/uname.d.ts.map +1 -1
- package/dist/commands/uname.js +5 -0
- package/dist/commands/uniq.d.ts +5 -0
- package/dist/commands/uniq.d.ts.map +1 -1
- package/dist/commands/uniq.js +5 -0
- package/dist/commands/unset.d.ts +5 -0
- package/dist/commands/unset.d.ts.map +1 -1
- package/dist/commands/unset.js +5 -0
- package/dist/commands/uptime.d.ts +5 -0
- package/dist/commands/uptime.d.ts.map +1 -1
- package/dist/commands/uptime.js +5 -0
- package/dist/commands/wc.d.ts +5 -0
- package/dist/commands/wc.d.ts.map +1 -1
- package/dist/commands/wc.js +5 -0
- package/dist/commands/wget.d.ts +5 -0
- package/dist/commands/wget.d.ts.map +1 -1
- package/dist/commands/wget.js +5 -0
- package/dist/commands/who.d.ts +5 -0
- package/dist/commands/who.d.ts.map +1 -1
- package/dist/commands/who.js +5 -0
- package/dist/commands/whoami.d.ts +5 -0
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +5 -0
- package/dist/commands/xargs.d.ts +5 -0
- package/dist/commands/xargs.d.ts.map +1 -1
- package/dist/commands/xargs.js +5 -0
- package/dist/self-standalone.js +254 -30
- package/dist/types/commands.d.ts +36 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/utils/tokenize.d.ts +20 -0
- package/dist/utils/tokenize.d.ts.map +1 -0
- package/dist/utils/tokenize.js +74 -0
- package/examples/web.min.js +2 -2
- package/package.json +1 -1
- package/src/SSHClient/index.ts +6 -3
- package/src/SSHMimic/executor.ts +21 -44
- package/src/SSHMimic/index.ts +7 -5
- package/src/SSHMimic/sftp.ts +28 -21
- package/src/VirtualShell/shell.ts +34 -4
- package/src/VirtualShell/shellParser.ts +2 -103
- package/src/VirtualUserManager/index.ts +44 -20
- package/src/commands/adduser.ts +86 -13
- package/src/commands/alias.ts +5 -0
- package/src/commands/apt.ts +5 -0
- package/src/commands/awk.ts +154 -29
- package/src/commands/cd.ts +0 -4
- package/src/commands/clear.ts +5 -0
- package/src/commands/command-helpers.ts +9 -0
- package/src/commands/declare.ts +5 -0
- package/src/commands/deluser.ts +84 -7
- package/src/commands/df.ts +5 -0
- package/src/commands/du.ts +5 -0
- package/src/commands/export.ts +5 -0
- package/src/commands/grep.ts +21 -8
- package/src/commands/groups.ts +5 -0
- package/src/commands/gzip.ts +54 -28
- package/src/commands/head.ts +14 -4
- package/src/commands/htop.ts +5 -0
- package/src/commands/kill.ts +5 -0
- package/src/commands/ln.ts +22 -0
- package/src/commands/ls.ts +17 -0
- package/src/commands/lsb-release.ts +5 -0
- package/src/commands/mkdir.ts +5 -0
- package/src/commands/mv.ts +5 -0
- package/src/commands/nano.ts +5 -0
- package/src/commands/neofetch.ts +8 -6
- package/src/commands/passwd.ts +35 -12
- package/src/commands/ping.ts +5 -0
- package/src/commands/printf.ts +30 -13
- package/src/commands/ps.ts +5 -0
- package/src/commands/read.ts +5 -0
- package/src/commands/registry.ts +4 -1
- package/src/commands/rm.ts +5 -0
- package/src/commands/runtime.ts +1 -61
- package/src/commands/sed.ts +5 -0
- package/src/commands/set.ts +5 -24
- package/src/commands/sh.ts +9 -3
- package/src/commands/shift.ts +10 -0
- package/src/commands/sleep.ts +5 -0
- package/src/commands/sort.ts +5 -0
- package/src/commands/source.ts +5 -0
- package/src/commands/stat.ts +61 -0
- package/src/commands/su.ts +54 -16
- package/src/commands/sudo.ts +5 -0
- package/src/commands/tail.ts +17 -3
- package/src/commands/tar.ts +38 -15
- package/src/commands/tee.ts +5 -0
- package/src/commands/touch.ts +5 -0
- package/src/commands/tr.ts +54 -10
- package/src/commands/tree.ts +5 -0
- package/src/commands/true.ts +10 -0
- package/src/commands/type.ts +5 -0
- package/src/commands/uname.ts +5 -0
- package/src/commands/uniq.ts +5 -0
- package/src/commands/unset.ts +5 -0
- package/src/commands/uptime.ts +5 -0
- package/src/commands/wc.ts +5 -0
- package/src/commands/wget.ts +5 -0
- package/src/commands/who.ts +5 -0
- package/src/commands/whoami.ts +5 -0
- package/src/commands/xargs.ts +5 -0
- package/src/self-standalone.ts +316 -33
- package/src/types/commands.ts +37 -0
- package/src/utils/tokenize.ts +78 -0
- package/builds/web-iife.min.js +0 -13
- package/builds/web-iife.min.js.map +0 -7
package/src/SSHMimic/executor.ts
CHANGED
|
@@ -4,54 +4,13 @@ import type { CommandMode, CommandResult, ShellEnv } from "../types/commands";
|
|
|
4
4
|
import type {
|
|
5
5
|
Pipeline,
|
|
6
6
|
PipelineCommand,
|
|
7
|
-
Script,
|
|
8
7
|
Statement,
|
|
9
8
|
} from "../types/pipeline";
|
|
10
9
|
import type { VirtualShell } from "../VirtualShell";
|
|
11
10
|
|
|
12
11
|
// ── Script executor (handles &&/||/;) ────────────────────────────────────────
|
|
13
12
|
|
|
14
|
-
export async function executeScript(
|
|
15
|
-
script: Script,
|
|
16
|
-
authUser: string,
|
|
17
|
-
hostname: string,
|
|
18
|
-
mode: CommandMode,
|
|
19
|
-
cwd: string,
|
|
20
|
-
shell: VirtualShell,
|
|
21
|
-
env: ShellEnv,
|
|
22
|
-
): Promise<CommandResult> {
|
|
23
|
-
if (!script.isValid)
|
|
24
|
-
return { stderr: script.error || "Syntax error", exitCode: 1 };
|
|
25
|
-
|
|
26
|
-
let lastResult: CommandResult = { exitCode: 0 };
|
|
27
13
|
|
|
28
|
-
for (const stmt of script.statements) {
|
|
29
|
-
// Decide whether to run this statement based on previous op
|
|
30
|
-
lastResult = await executePipeline(
|
|
31
|
-
stmt.pipeline,
|
|
32
|
-
authUser,
|
|
33
|
-
hostname,
|
|
34
|
-
mode,
|
|
35
|
-
cwd,
|
|
36
|
-
shell,
|
|
37
|
-
env,
|
|
38
|
-
);
|
|
39
|
-
env.lastExitCode = lastResult.exitCode ?? 0;
|
|
40
|
-
|
|
41
|
-
// Propagate session-control signals
|
|
42
|
-
if (
|
|
43
|
-
lastResult.closeSession ||
|
|
44
|
-
lastResult.switchUser ||
|
|
45
|
-
lastResult.nextCwd
|
|
46
|
-
) {
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return lastResult;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Execute statements connected by &&/||/; */
|
|
55
14
|
export async function executeStatements(
|
|
56
15
|
statements: Statement[],
|
|
57
16
|
authUser: string,
|
|
@@ -62,6 +21,8 @@ export async function executeStatements(
|
|
|
62
21
|
env: ShellEnv,
|
|
63
22
|
): Promise<CommandResult> {
|
|
64
23
|
let last: CommandResult = { exitCode: 0 };
|
|
24
|
+
const accumulatedStdout: string[] = [];
|
|
25
|
+
let currentCwd = cwd; // track cwd changes from cd, su, etc.
|
|
65
26
|
let i = 0;
|
|
66
27
|
|
|
67
28
|
while (i < statements.length) {
|
|
@@ -71,13 +32,26 @@ export async function executeStatements(
|
|
|
71
32
|
authUser,
|
|
72
33
|
hostname,
|
|
73
34
|
mode,
|
|
74
|
-
|
|
35
|
+
currentCwd,
|
|
75
36
|
shell,
|
|
76
37
|
env,
|
|
77
38
|
);
|
|
78
39
|
env.lastExitCode = last.exitCode ?? 0;
|
|
79
40
|
|
|
80
|
-
|
|
41
|
+
// Propagate cwd changes (cd, su -l, etc.)
|
|
42
|
+
if (last.nextCwd && (last.exitCode ?? 0) === 0) {
|
|
43
|
+
currentCwd = last.nextCwd;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Collect stdout from each statement (for echo a; echo b → "a\nb\n")
|
|
47
|
+
if (last.stdout) accumulatedStdout.push(last.stdout);
|
|
48
|
+
|
|
49
|
+
if (last.closeSession || last.switchUser) {
|
|
50
|
+
return {
|
|
51
|
+
...last,
|
|
52
|
+
stdout: accumulatedStdout.join("") || last.stdout,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
81
55
|
|
|
82
56
|
const op = stmt.op;
|
|
83
57
|
if (!op || op === ";") {
|
|
@@ -95,7 +69,10 @@ export async function executeStatements(
|
|
|
95
69
|
}
|
|
96
70
|
i++;
|
|
97
71
|
}
|
|
98
|
-
|
|
72
|
+
// Merge accumulated stdout (for "echo a; echo b" → "a\nb\n")
|
|
73
|
+
const merged = accumulatedStdout.join("");
|
|
74
|
+
// Preserve the deepest cwd change across the whole pipeline
|
|
75
|
+
return { ...last, stdout: merged || last.stdout, nextCwd: currentCwd !== cwd ? currentCwd : undefined };
|
|
99
76
|
}
|
|
100
77
|
|
|
101
78
|
// ── Pipeline executor ─────────────────────────────────────────────────────────
|
package/src/SSHMimic/index.ts
CHANGED
|
@@ -21,6 +21,11 @@ import { loadOrCreateHostKey } from "./hostKey";
|
|
|
21
21
|
*/
|
|
22
22
|
const perf: PerfLogger = createPerfLogger("SshMimic");
|
|
23
23
|
|
|
24
|
+
// ── Dev-mode logger ───────────────────────────────────────────────────────────
|
|
25
|
+
const DEV = !!process.env.DEV_MODE;
|
|
26
|
+
const devLog = DEV ? console.log.bind(console) : () => {};
|
|
27
|
+
|
|
28
|
+
|
|
24
29
|
interface RateLimitEntry {
|
|
25
30
|
attempts: number;
|
|
26
31
|
lockedUntil: number;
|
|
@@ -152,9 +157,6 @@ class SshMimic extends EventEmitter {
|
|
|
152
157
|
// ── Password auth ──────────────────────────────────────
|
|
153
158
|
if (ctx.method === "password") {
|
|
154
159
|
if (!shell.users.hasPassword(candidateUser)) {
|
|
155
|
-
console.log(
|
|
156
|
-
`User ${candidateUser} has no password set, allowing login without verification`,
|
|
157
|
-
);
|
|
158
160
|
authUser = candidateUser;
|
|
159
161
|
sessionId = shell.users.registerSession(
|
|
160
162
|
authUser,
|
|
@@ -297,7 +299,7 @@ class SshMimic extends EventEmitter {
|
|
|
297
299
|
return new Promise<number>((resolve, reject) => {
|
|
298
300
|
this.server?.once("error", (err: unknown) => reject(err));
|
|
299
301
|
this.server?.listen(this.port, "0.0.0.0", () => {
|
|
300
|
-
|
|
302
|
+
devLog(`SSH Mimic listening on port ${this.port}`);
|
|
301
303
|
this.emit("start", { port: this.port });
|
|
302
304
|
resolve(this.port);
|
|
303
305
|
});
|
|
@@ -311,7 +313,7 @@ class SshMimic extends EventEmitter {
|
|
|
311
313
|
perf.mark("stop");
|
|
312
314
|
if (this.server) {
|
|
313
315
|
this.server.close(() => {
|
|
314
|
-
|
|
316
|
+
devLog("SSH Mimic stopped");
|
|
315
317
|
this.emit("stop");
|
|
316
318
|
});
|
|
317
319
|
}
|
package/src/SSHMimic/sftp.ts
CHANGED
|
@@ -10,6 +10,13 @@ import type VirtualFileSystem from "../VirtualFileSystem";
|
|
|
10
10
|
import { VirtualShell } from "../VirtualShell";
|
|
11
11
|
import type { VirtualUserManager } from "../VirtualUserManager";
|
|
12
12
|
import { loadOrCreateHostKey } from "./hostKey";
|
|
13
|
+
// ── Dev-mode logger — silent in production ────────────────────────────────────
|
|
14
|
+
const DEV = !!process.env.DEV_MODE;
|
|
15
|
+
const devLog = DEV ? console.log.bind(console) : () => {};
|
|
16
|
+
const devWarn = DEV ? console.warn.bind(console) : () => {};
|
|
17
|
+
const devErr = DEV ? console.error.bind(console): () => {};
|
|
18
|
+
|
|
19
|
+
|
|
13
20
|
|
|
14
21
|
const SFTP_STATUS_CODE = {
|
|
15
22
|
OK: 0,
|
|
@@ -218,7 +225,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
218
225
|
|
|
219
226
|
// Add error handling for the client
|
|
220
227
|
client.on("error", (error: unknown) => {
|
|
221
|
-
|
|
228
|
+
devErr(`[SFTP] Client error:`, error);
|
|
222
229
|
});
|
|
223
230
|
|
|
224
231
|
const acceptSession = (username: string): void => {
|
|
@@ -248,7 +255,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
248
255
|
const candidateUser = ctx.username || "root";
|
|
249
256
|
remoteAddress = (ctx as { ip?: string }).ip ?? remoteAddress;
|
|
250
257
|
|
|
251
|
-
|
|
258
|
+
devLog(
|
|
252
259
|
`[SFTP] Auth attempt: user=${candidateUser}, method=${ctx.method}, ip=${remoteAddress}`,
|
|
253
260
|
);
|
|
254
261
|
|
|
@@ -326,7 +333,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
326
333
|
|
|
327
334
|
// Add error handling for the session
|
|
328
335
|
session.on("error", (error: unknown) => {
|
|
329
|
-
|
|
336
|
+
devErr(
|
|
330
337
|
`[SFTP] Session error for user=${authUser}:`,
|
|
331
338
|
error,
|
|
332
339
|
);
|
|
@@ -349,7 +356,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
349
356
|
address && typeof address === "object" && "port" in address
|
|
350
357
|
? address.port
|
|
351
358
|
: this.port;
|
|
352
|
-
|
|
359
|
+
devLog(`SFTP Mimic listening on port ${actualPort}`);
|
|
353
360
|
this.emit("start", { port: actualPort });
|
|
354
361
|
resolve(actualPort as number);
|
|
355
362
|
});
|
|
@@ -360,7 +367,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
360
367
|
perf.mark("stop");
|
|
361
368
|
if (this.server) {
|
|
362
369
|
this.server.close(() => {
|
|
363
|
-
|
|
370
|
+
devLog("SFTP Mimic stopped");
|
|
364
371
|
this.emit("stop");
|
|
365
372
|
});
|
|
366
373
|
}
|
|
@@ -452,7 +459,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
452
459
|
|
|
453
460
|
// Security: Confine to home directory
|
|
454
461
|
if (!this.isPathWithinHome(targetPath, authUser)) {
|
|
455
|
-
|
|
462
|
+
devWarn(
|
|
456
463
|
`[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
|
|
457
464
|
);
|
|
458
465
|
sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
|
|
@@ -507,7 +514,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
507
514
|
});
|
|
508
515
|
sftp.handle(reqid, handle);
|
|
509
516
|
} catch (error) {
|
|
510
|
-
|
|
517
|
+
devErr("SFTP OPEN error:", error);
|
|
511
518
|
sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
|
|
512
519
|
}
|
|
513
520
|
});
|
|
@@ -580,7 +587,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
580
587
|
getVfs().writeFile(entry.path, entry.buffer);
|
|
581
588
|
void getVfs().flushMirror();
|
|
582
589
|
} catch (error) {
|
|
583
|
-
|
|
590
|
+
devErr("SFTP CLOSE write error:", error);
|
|
584
591
|
sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
|
|
585
592
|
this.closeHandle(handle);
|
|
586
593
|
return;
|
|
@@ -596,7 +603,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
596
603
|
|
|
597
604
|
// Security: Confine to home directory
|
|
598
605
|
if (!this.isPathWithinHome(targetPath, authUser)) {
|
|
599
|
-
|
|
606
|
+
devWarn(
|
|
600
607
|
`[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
|
|
601
608
|
);
|
|
602
609
|
sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
|
|
@@ -649,7 +656,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
649
656
|
|
|
650
657
|
// Security: Confine to home directory
|
|
651
658
|
if (!this.isPathWithinHome(targetPath, authUser)) {
|
|
652
|
-
|
|
659
|
+
devWarn(
|
|
653
660
|
`[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
|
|
654
661
|
);
|
|
655
662
|
sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
|
|
@@ -669,7 +676,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
669
676
|
|
|
670
677
|
// Security: Confine to home directory
|
|
671
678
|
if (!this.isPathWithinHome(targetPath, authUser)) {
|
|
672
|
-
|
|
679
|
+
devWarn(
|
|
673
680
|
`[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
|
|
674
681
|
);
|
|
675
682
|
sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
|
|
@@ -711,7 +718,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
711
718
|
|
|
712
719
|
// Security: Confine to home directory
|
|
713
720
|
if (!this.isPathWithinHome(targetPath, authUser)) {
|
|
714
|
-
|
|
721
|
+
devWarn(
|
|
715
722
|
`[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
|
|
716
723
|
);
|
|
717
724
|
sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
|
|
@@ -734,7 +741,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
734
741
|
|
|
735
742
|
// Security: Confine to home directory
|
|
736
743
|
if (!this.isPathWithinHome(normalized, authUser)) {
|
|
737
|
-
|
|
744
|
+
devWarn(
|
|
738
745
|
`[SFTP] Path traversal attempt blocked: user=${authUser}, path=${normalized}`,
|
|
739
746
|
);
|
|
740
747
|
sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
|
|
@@ -762,7 +769,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
762
769
|
|
|
763
770
|
// Security: Confine to home directory
|
|
764
771
|
if (!this.isPathWithinHome(targetPath, authUser)) {
|
|
765
|
-
|
|
772
|
+
devWarn(
|
|
766
773
|
`[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
|
|
767
774
|
);
|
|
768
775
|
sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
|
|
@@ -783,7 +790,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
783
790
|
|
|
784
791
|
// Security: Confine to home directory
|
|
785
792
|
if (!this.isPathWithinHome(targetPath, authUser)) {
|
|
786
|
-
|
|
793
|
+
devWarn(
|
|
787
794
|
`[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
|
|
788
795
|
);
|
|
789
796
|
sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
|
|
@@ -804,7 +811,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
804
811
|
|
|
805
812
|
// Security: Confine to home directory
|
|
806
813
|
if (!this.isPathWithinHome(targetPath, authUser)) {
|
|
807
|
-
|
|
814
|
+
devWarn(
|
|
808
815
|
`[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
|
|
809
816
|
);
|
|
810
817
|
sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
|
|
@@ -829,7 +836,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
829
836
|
!this.isPathWithinHome(fromPath, authUser) ||
|
|
830
837
|
!this.isPathWithinHome(toPath, authUser)
|
|
831
838
|
) {
|
|
832
|
-
|
|
839
|
+
devWarn(
|
|
833
840
|
`[SFTP] Path traversal attempt blocked: user=${authUser}, from=${fromPath}, to=${toPath}`,
|
|
834
841
|
);
|
|
835
842
|
sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
|
|
@@ -854,21 +861,21 @@ export class SftpMimic extends EventEmitter {
|
|
|
854
861
|
});
|
|
855
862
|
|
|
856
863
|
sftp.on("error", (error: Error) => {
|
|
857
|
-
|
|
864
|
+
devErr(`[SFTP] Stream error for user=${authUser}:`, error);
|
|
858
865
|
});
|
|
859
866
|
|
|
860
867
|
sftp.on("close", () => {
|
|
861
|
-
|
|
868
|
+
devLog(`[SFTP] Stream closed for user=${authUser}`);
|
|
862
869
|
this.handles.clear();
|
|
863
870
|
});
|
|
864
871
|
|
|
865
872
|
sftp.on("end", () => {
|
|
866
|
-
|
|
873
|
+
devLog(`[SFTP] end event for user=${authUser}`);
|
|
867
874
|
this.handles.clear();
|
|
868
875
|
});
|
|
869
876
|
|
|
870
877
|
sftp.on("END", () => {
|
|
871
|
-
|
|
878
|
+
devLog(`[SFTP] END event for user=${authUser}`);
|
|
872
879
|
this.handles.clear();
|
|
873
880
|
});
|
|
874
881
|
}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "../modules/shellRuntime";
|
|
16
16
|
import { buildLoginBanner } from "../SSHMimic/loginBanner";
|
|
17
17
|
import { buildPrompt } from "../SSHMimic/prompt";
|
|
18
|
-
import type { ShellEnv } from "../types/commands";
|
|
18
|
+
import type { CommandResult, ShellEnv } from "../types/commands";
|
|
19
19
|
import type { ShellStream } from "../types/streams";
|
|
20
20
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
21
21
|
|
|
@@ -33,6 +33,11 @@ interface PendingSudo {
|
|
|
33
33
|
loginShell: boolean;
|
|
34
34
|
prompt: string;
|
|
35
35
|
buffer: string;
|
|
36
|
+
mode?: "sudo" | "passwd" | "confirm";
|
|
37
|
+
onPassword?: (input: string, shell: VirtualShell) => Promise<{
|
|
38
|
+
result: CommandResult | null;
|
|
39
|
+
nextPrompt?: string;
|
|
40
|
+
}>;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
export function startShell(
|
|
@@ -110,6 +115,11 @@ export function startShell(
|
|
|
110
115
|
commandLine: string | null;
|
|
111
116
|
loginShell: boolean;
|
|
112
117
|
prompt: string;
|
|
118
|
+
mode?: "sudo" | "passwd" | "confirm";
|
|
119
|
+
onPassword?: (input: string, shell: VirtualShell) => Promise<{
|
|
120
|
+
result: CommandResult | null;
|
|
121
|
+
nextPrompt?: string;
|
|
122
|
+
}>;
|
|
113
123
|
}): void {
|
|
114
124
|
pendingSudo = {
|
|
115
125
|
...challenge,
|
|
@@ -135,7 +145,9 @@ export function startShell(
|
|
|
135
145
|
|
|
136
146
|
if (!challenge.commandLine) {
|
|
137
147
|
authUser = challenge.targetUser;
|
|
138
|
-
|
|
148
|
+
if (challenge.loginShell) {
|
|
149
|
+
cwd = `/home/${authUser}`;
|
|
150
|
+
}
|
|
139
151
|
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
140
152
|
stream.write("\r\n");
|
|
141
153
|
renderLine();
|
|
@@ -443,11 +455,29 @@ export function startShell(
|
|
|
443
455
|
}
|
|
444
456
|
|
|
445
457
|
if (ch === "\r" || ch === "\n") {
|
|
446
|
-
const
|
|
458
|
+
const typed = pendingSudo.buffer;
|
|
447
459
|
pendingSudo.buffer = "";
|
|
460
|
+
|
|
461
|
+
// ── Generic onPassword handler (passwd / confirm modes) ────
|
|
462
|
+
if (pendingSudo.onPassword) {
|
|
463
|
+
const { result, nextPrompt } = await pendingSudo.onPassword(typed, shell);
|
|
464
|
+
stream.write("\r\n");
|
|
465
|
+
if (result !== null) {
|
|
466
|
+
pendingSudo = null;
|
|
467
|
+
if (result.stdout) stream.write(result.stdout.replace(/\n/g, "\r\n"));
|
|
468
|
+
if (result.stderr) stream.write(result.stderr.replace(/\n/g, "\r\n"));
|
|
469
|
+
renderLine();
|
|
470
|
+
} else {
|
|
471
|
+
if (nextPrompt) pendingSudo.prompt = nextPrompt;
|
|
472
|
+
stream.write(pendingSudo.prompt);
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ── Default sudo mode — verify current user's password ─────
|
|
448
478
|
const valid = shell.users.verifyPassword(
|
|
449
479
|
pendingSudo.username,
|
|
450
|
-
|
|
480
|
+
typed,
|
|
451
481
|
);
|
|
452
482
|
await finishSudoPrompt(valid);
|
|
453
483
|
return;
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
Statement,
|
|
6
6
|
LogicalOp,
|
|
7
7
|
} from "../types/pipeline";
|
|
8
|
+
import { tokenizeCommand } from "../utils/tokenize";
|
|
8
9
|
|
|
9
10
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
10
11
|
|
|
@@ -25,7 +26,7 @@ export function parseScript(rawInput: string): Script {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
+
/** Parse a single pipeline string (no &&/||/;) into a `Pipeline` object. */
|
|
29
30
|
export function parseShellPipeline(rawInput: string): Pipeline {
|
|
30
31
|
const trimmed = rawInput.trim();
|
|
31
32
|
if (!trimmed) return { commands: [], isValid: true };
|
|
@@ -39,49 +40,6 @@ export function parseShellPipeline(rawInput: string): Pipeline {
|
|
|
39
40
|
|
|
40
41
|
// ── Variable & tilde expansion ────────────────────────────────────────────────
|
|
41
42
|
|
|
42
|
-
/**
|
|
43
|
-
* Expand ~ and $VAR / ${VAR} / ${VAR:-default} / $(cmd placeholder) in a
|
|
44
|
-
* token, given the current env vars and home path.
|
|
45
|
-
* Command substitution $(…) is NOT executed here — it's left as a marker so
|
|
46
|
-
* the executor can handle it.
|
|
47
|
-
*/
|
|
48
|
-
export function expandToken(
|
|
49
|
-
token: string,
|
|
50
|
-
env: Record<string, string>,
|
|
51
|
-
authUser: string,
|
|
52
|
-
lastExitCode = 0,
|
|
53
|
-
): string {
|
|
54
|
-
// tilde expansion
|
|
55
|
-
token = token.replace(/^~(\/|$)/, `/home/${authUser}$1`);
|
|
56
|
-
|
|
57
|
-
// $? special var
|
|
58
|
-
token = token.replace(/\$\?/g, String(lastExitCode));
|
|
59
|
-
// $$ PID (mock)
|
|
60
|
-
token = token.replace(/\$\$/g, "1");
|
|
61
|
-
// $# argc (0 for interactive)
|
|
62
|
-
token = token.replace(/\$#/g, "0");
|
|
63
|
-
|
|
64
|
-
// ${VAR:-default} and ${VAR:+value}
|
|
65
|
-
token = token.replace(
|
|
66
|
-
/\$\{([^}:]+):-([^}]*)\}/g,
|
|
67
|
-
(_, name, def) => env[name] ?? def,
|
|
68
|
-
);
|
|
69
|
-
token = token.replace(/\$\{([^}:]+):\+([^}]*)\}/g, (_, name, val) =>
|
|
70
|
-
env[name] ? val : "",
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
// ${VAR}
|
|
74
|
-
token = token.replace(/\$\{([^}]+)\}/g, (_, name) => env[name] ?? "");
|
|
75
|
-
|
|
76
|
-
// $VAR (greedy: match longest valid identifier)
|
|
77
|
-
token = token.replace(
|
|
78
|
-
/\$([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
79
|
-
(_, name) => env[name] ?? "",
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
return token;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
43
|
/**
|
|
86
44
|
* Expand glob patterns (*, ?, [abc]) against a list of entries.
|
|
87
45
|
* Returns the original pattern if no match.
|
|
@@ -299,62 +257,3 @@ function parseCommandWithRedirections(token: string): PipelineCommand {
|
|
|
299
257
|
return { name, args: cmdParts.slice(1), inputFile, outputFile, appendOutput };
|
|
300
258
|
}
|
|
301
259
|
|
|
302
|
-
function tokenizeCommand(input: string): string[] {
|
|
303
|
-
const tokens: string[] = [];
|
|
304
|
-
let current = "";
|
|
305
|
-
let inQ = false;
|
|
306
|
-
let qChar = "";
|
|
307
|
-
let i = 0;
|
|
308
|
-
|
|
309
|
-
while (i < input.length) {
|
|
310
|
-
const ch = input[i]!;
|
|
311
|
-
const next = input[i + 1];
|
|
312
|
-
|
|
313
|
-
if ((ch === '"' || ch === "'") && !inQ) {
|
|
314
|
-
inQ = true;
|
|
315
|
-
qChar = ch;
|
|
316
|
-
i++;
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
if (inQ && ch === qChar) {
|
|
320
|
-
inQ = false;
|
|
321
|
-
qChar = "";
|
|
322
|
-
i++;
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
if (inQ) {
|
|
326
|
-
current += ch;
|
|
327
|
-
i++;
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (ch === " ") {
|
|
332
|
-
if (current) {
|
|
333
|
-
tokens.push(current);
|
|
334
|
-
current = "";
|
|
335
|
-
}
|
|
336
|
-
i++;
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if ((ch === ">" || ch === "<") && !inQ) {
|
|
341
|
-
if (current) {
|
|
342
|
-
tokens.push(current);
|
|
343
|
-
current = "";
|
|
344
|
-
}
|
|
345
|
-
if (ch === ">" && next === ">") {
|
|
346
|
-
tokens.push(">>");
|
|
347
|
-
i += 2;
|
|
348
|
-
} else {
|
|
349
|
-
tokens.push(ch);
|
|
350
|
-
i++;
|
|
351
|
-
}
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
current += ch;
|
|
356
|
-
i++;
|
|
357
|
-
}
|
|
358
|
-
if (current) tokens.push(current);
|
|
359
|
-
return tokens;
|
|
360
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash, randomBytes, randomUUID, scryptSync } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { PerfLogger } from "../utils/perfLogger";
|
|
@@ -99,15 +99,15 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
99
99
|
// this.users.set(currentUser, this.createRecord(currentUser, userPassword));
|
|
100
100
|
// this.sudoers.add(currentUser);
|
|
101
101
|
// changed = true;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
102
|
+
// }
|
|
103
|
+
|
|
104
|
+
// const homePath = `/home/root`;
|
|
105
|
+
// if (!this.vfs.exists(homePath)) {
|
|
106
|
+
// this.vfs.mkdir(homePath, 0o755);
|
|
107
|
+
// this.vfs.writeFile(
|
|
108
|
+
// `${homePath}/README.txt`,
|
|
109
|
+
// `Welcome to the virtual environment, root`,
|
|
110
|
+
// );
|
|
111
111
|
// }
|
|
112
112
|
|
|
113
113
|
if (changed) {
|
|
@@ -239,10 +239,22 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
239
239
|
perf.mark("verifyPassword");
|
|
240
240
|
const record = this.users.get(username);
|
|
241
241
|
if (!record) {
|
|
242
|
+
// Perform a dummy hash to avoid timing leakage on unknown usernames
|
|
243
|
+
this.hashPassword(password, "");
|
|
242
244
|
return false;
|
|
243
245
|
}
|
|
244
246
|
|
|
245
|
-
|
|
247
|
+
const computed = this.hashPassword(password, record.salt);
|
|
248
|
+
const expected = record.passwordHash;
|
|
249
|
+
// timingSafeEqual prevents timing-based password oracle attacks
|
|
250
|
+
try {
|
|
251
|
+
const a = Buffer.from(computed, "hex");
|
|
252
|
+
const b = Buffer.from(expected, "hex");
|
|
253
|
+
if (a.length !== b.length) return false;
|
|
254
|
+
return timingSafeEqual(a, b);
|
|
255
|
+
} catch {
|
|
256
|
+
return computed === expected;
|
|
257
|
+
}
|
|
246
258
|
}
|
|
247
259
|
|
|
248
260
|
/**
|
|
@@ -617,7 +629,8 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
617
629
|
}
|
|
618
630
|
|
|
619
631
|
private createRecord(username: string, password: string): VirtualUserRecord {
|
|
620
|
-
|
|
632
|
+
// Cache key is a hash of the inputs — never store plaintext password in memory
|
|
633
|
+
const cacheKey = createHash("sha256").update(username).update(":").update(password).digest("hex");
|
|
621
634
|
const cached = VirtualUserManager.recordCache.get(cacheKey);
|
|
622
635
|
if (cached) {
|
|
623
636
|
return cached;
|
|
@@ -627,7 +640,8 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
627
640
|
const record = {
|
|
628
641
|
username,
|
|
629
642
|
salt,
|
|
630
|
-
|
|
643
|
+
// Hash uses the generated salt — verifyPassword must use record.salt
|
|
644
|
+
passwordHash: this.hashPassword(password, salt),
|
|
631
645
|
};
|
|
632
646
|
|
|
633
647
|
VirtualUserManager.recordCache.set(cacheKey, record);
|
|
@@ -644,11 +658,12 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
644
658
|
*/
|
|
645
659
|
public hasPassword(username: string): boolean {
|
|
646
660
|
perf.mark("hasPassword");
|
|
647
|
-
if (this.getPasswordHash(username) === this.hashPassword("")) {
|
|
648
|
-
return false;
|
|
649
|
-
}
|
|
650
661
|
const record = this.users.get(username);
|
|
651
|
-
|
|
662
|
+
if (!record) return false;
|
|
663
|
+
// Empty password hash computed with the record's own salt
|
|
664
|
+
const emptyHash = this.hashPassword("", record.salt);
|
|
665
|
+
if (record.passwordHash === emptyHash) return false;
|
|
666
|
+
return !!record.passwordHash;
|
|
652
667
|
}
|
|
653
668
|
|
|
654
669
|
/**
|
|
@@ -660,12 +675,21 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
660
675
|
* @param password Plaintext password string.
|
|
661
676
|
* @returns Hex-encoded hash string.
|
|
662
677
|
*/
|
|
663
|
-
|
|
678
|
+
/**
|
|
679
|
+
* Hash a password with an optional salt.
|
|
680
|
+
* When salt is provided (verify path), the same salt is used for a
|
|
681
|
+
* deterministic hash. When omitted (create path), an empty salt is used
|
|
682
|
+
* for backward compat — callers should pass the stored salt on verify.
|
|
683
|
+
*/
|
|
684
|
+
public hashPassword(password: string, salt = ""): string {
|
|
664
685
|
if (VirtualUserManager.fastPasswordHash) {
|
|
665
|
-
return createHash("sha256")
|
|
686
|
+
return createHash("sha256")
|
|
687
|
+
.update(salt)
|
|
688
|
+
.update(password)
|
|
689
|
+
.digest("hex");
|
|
666
690
|
}
|
|
667
691
|
|
|
668
|
-
return scryptSync(password, "", 32).toString("hex");
|
|
692
|
+
return scryptSync(password, salt || "", 32).toString("hex");
|
|
669
693
|
}
|
|
670
694
|
|
|
671
695
|
private validateUsername(username: string): void {
|