typescript-virtual-container 1.2.4 → 1.2.6
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 +1056 -1239
- package/benchmark-results.txt +20 -20
- package/dist/SSHMimic/exec.js +2 -2
- package/dist/SSHMimic/executor.d.ts +6 -7
- package/dist/SSHMimic/executor.d.ts.map +1 -1
- package/dist/SSHMimic/executor.js +77 -60
- package/dist/SSHMimic/index.d.ts +19 -2
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +106 -24
- package/dist/SSHMimic/sftp.d.ts.map +1 -1
- package/dist/SSHMimic/sftp.js +14 -0
- package/dist/VirtualFileSystem/index.d.ts +115 -88
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +389 -264
- package/dist/VirtualShell/index.d.ts +3 -4
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +4 -6
- package/dist/VirtualShell/shell.d.ts.map +1 -1
- package/dist/VirtualShell/shell.js +19 -2
- package/dist/VirtualShell/shellParser.d.ts +20 -2
- package/dist/VirtualShell/shellParser.d.ts.map +1 -1
- package/dist/VirtualShell/shellParser.js +229 -120
- package/dist/VirtualUserManager/index.d.ts +25 -0
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +33 -0
- package/dist/commands/adduser.d.ts.map +1 -1
- package/dist/commands/adduser.js +2 -0
- package/dist/commands/awk.d.ts +3 -0
- package/dist/commands/awk.d.ts.map +1 -0
- package/dist/commands/awk.js +29 -0
- package/dist/commands/base64.d.ts +3 -0
- package/dist/commands/base64.d.ts.map +1 -0
- package/dist/commands/base64.js +20 -0
- package/dist/commands/cat.d.ts.map +1 -1
- package/dist/commands/cat.js +2 -0
- package/dist/commands/cd.d.ts.map +1 -1
- package/dist/commands/cd.js +2 -0
- package/dist/commands/chmod.d.ts +3 -0
- package/dist/commands/chmod.d.ts.map +1 -0
- package/dist/commands/chmod.js +33 -0
- package/dist/commands/clear.d.ts.map +1 -1
- package/dist/commands/clear.js +4 -1
- package/dist/commands/cp.d.ts +3 -0
- package/dist/commands/cp.d.ts.map +1 -0
- package/dist/commands/cp.js +70 -0
- package/dist/commands/curl.d.ts.map +1 -1
- package/dist/commands/curl.js +2 -0
- package/dist/commands/cut.d.ts +3 -0
- package/dist/commands/cut.d.ts.map +1 -0
- package/dist/commands/cut.js +27 -0
- package/dist/commands/date.d.ts +3 -0
- package/dist/commands/date.d.ts.map +1 -0
- package/dist/commands/date.js +22 -0
- package/dist/commands/deluser.d.ts.map +1 -1
- package/dist/commands/deluser.js +2 -0
- package/dist/commands/df.d.ts +3 -0
- package/dist/commands/df.d.ts.map +1 -0
- package/dist/commands/df.js +16 -0
- package/dist/commands/diff.d.ts +3 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +40 -0
- package/dist/commands/du.d.ts +3 -0
- package/dist/commands/du.d.ts.map +1 -0
- package/dist/commands/du.js +39 -0
- package/dist/commands/echo.d.ts.map +1 -1
- package/dist/commands/echo.js +2 -0
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +6 -14
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +11 -21
- package/dist/commands/find.d.ts +3 -0
- package/dist/commands/find.d.ts.map +1 -0
- package/dist/commands/find.js +50 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +58 -35
- package/dist/commands/groups.d.ts +3 -0
- package/dist/commands/groups.d.ts.map +1 -0
- package/dist/commands/groups.js +12 -0
- package/dist/commands/gzip.d.ts +4 -0
- package/dist/commands/gzip.d.ts.map +1 -0
- package/dist/commands/gzip.js +40 -0
- package/dist/commands/head.d.ts +3 -0
- package/dist/commands/head.d.ts.map +1 -0
- package/dist/commands/head.js +32 -0
- package/dist/commands/help.d.ts +1 -1
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +75 -3
- package/dist/commands/hostname.d.ts.map +1 -1
- package/dist/commands/hostname.js +2 -0
- package/dist/commands/htop.d.ts.map +1 -1
- package/dist/commands/htop.js +2 -0
- package/dist/commands/id.d.ts +3 -0
- package/dist/commands/id.d.ts.map +1 -0
- package/dist/commands/id.js +14 -0
- package/dist/commands/index.d.ts +5 -2
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +104 -87
- package/dist/commands/kill.d.ts +3 -0
- package/dist/commands/kill.d.ts.map +1 -0
- package/dist/commands/kill.js +13 -0
- package/dist/commands/ln.d.ts +3 -0
- package/dist/commands/ln.d.ts.map +1 -0
- package/dist/commands/ln.js +44 -0
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +2 -0
- package/dist/commands/mkdir.d.ts.map +1 -1
- package/dist/commands/mkdir.js +2 -0
- package/dist/commands/mv.d.ts +3 -0
- package/dist/commands/mv.d.ts.map +1 -0
- package/dist/commands/mv.js +37 -0
- package/dist/commands/nano.d.ts.map +1 -1
- package/dist/commands/nano.js +2 -0
- package/dist/commands/neofetch.d.ts.map +1 -1
- package/dist/commands/neofetch.js +2 -0
- package/dist/commands/passwd.d.ts.map +1 -1
- package/dist/commands/passwd.js +2 -0
- package/dist/commands/ping.d.ts +3 -0
- package/dist/commands/ping.d.ts.map +1 -0
- package/dist/commands/ping.js +18 -0
- package/dist/commands/ps.d.ts +3 -0
- package/dist/commands/ps.d.ts.map +1 -0
- package/dist/commands/ps.js +17 -0
- package/dist/commands/pwd.d.ts.map +1 -1
- package/dist/commands/pwd.js +2 -0
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +2 -0
- package/dist/commands/sed.d.ts +3 -0
- package/dist/commands/sed.d.ts.map +1 -0
- package/dist/commands/sed.js +47 -0
- package/dist/commands/set.d.ts +3 -0
- package/dist/commands/set.d.ts.map +1 -1
- package/dist/commands/set.js +19 -46
- package/dist/commands/sh.d.ts +0 -1
- package/dist/commands/sh.d.ts.map +1 -1
- package/dist/commands/sh.js +228 -35
- package/dist/commands/sleep.d.ts +3 -0
- package/dist/commands/sleep.d.ts.map +1 -0
- package/dist/commands/sleep.js +13 -0
- package/dist/commands/sort.d.ts +3 -0
- package/dist/commands/sort.d.ts.map +1 -0
- package/dist/commands/sort.js +37 -0
- package/dist/commands/su.d.ts.map +1 -1
- package/dist/commands/su.js +2 -0
- package/dist/commands/sudo.d.ts.map +1 -1
- package/dist/commands/sudo.js +2 -0
- package/dist/commands/tail.d.ts +3 -0
- package/dist/commands/tail.d.ts.map +1 -0
- package/dist/commands/tail.js +35 -0
- package/dist/commands/tar.d.ts +3 -0
- package/dist/commands/tar.d.ts.map +1 -0
- package/dist/commands/tar.js +64 -0
- package/dist/commands/tee.d.ts +3 -0
- package/dist/commands/tee.d.ts.map +1 -0
- package/dist/commands/tee.js +29 -0
- package/dist/commands/touch.d.ts.map +1 -1
- package/dist/commands/touch.js +2 -0
- package/dist/commands/tr.d.ts +3 -0
- package/dist/commands/tr.d.ts.map +1 -0
- package/dist/commands/tr.js +24 -0
- package/dist/commands/tree.d.ts.map +1 -1
- package/dist/commands/tree.js +2 -0
- package/dist/commands/uname.d.ts +3 -0
- package/dist/commands/uname.d.ts.map +1 -0
- package/dist/commands/uname.js +21 -0
- package/dist/commands/uniq.d.ts +3 -0
- package/dist/commands/uniq.d.ts.map +1 -0
- package/dist/commands/uniq.js +33 -0
- package/dist/commands/unset.d.ts.map +1 -1
- package/dist/commands/unset.js +6 -10
- package/dist/commands/wc.d.ts +3 -0
- package/dist/commands/wc.d.ts.map +1 -0
- package/dist/commands/wc.js +50 -0
- package/dist/commands/wget.d.ts.map +1 -1
- package/dist/commands/wget.js +2 -0
- package/dist/commands/who.d.ts.map +1 -1
- package/dist/commands/who.js +2 -0
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +2 -0
- package/dist/commands/xargs.d.ts +3 -0
- package/dist/commands/xargs.d.ts.map +1 -0
- package/dist/commands/xargs.js +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/types/commands.d.ts +13 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/types/pipeline.d.ts +20 -0
- package/dist/types/pipeline.d.ts.map +1 -1
- package/package.json +5 -2
- package/scripts/publish-package.sh +70 -0
- package/src/SSHMimic/exec.ts +2 -2
- package/src/SSHMimic/executor.ts +95 -98
- package/src/SSHMimic/index.ts +138 -57
- package/src/SSHMimic/sftp.ts +15 -0
- package/src/VirtualFileSystem/index.ts +464 -292
- package/src/VirtualShell/index.ts +4 -6
- package/src/VirtualShell/shell.ts +19 -2
- package/src/VirtualShell/shellParser.ts +202 -168
- package/src/VirtualUserManager/index.ts +36 -0
- package/src/commands/adduser.ts +2 -0
- package/src/commands/awk.ts +30 -0
- package/src/commands/base64.ts +18 -0
- package/src/commands/cat.ts +2 -0
- package/src/commands/cd.ts +2 -0
- package/src/commands/chmod.ts +35 -0
- package/src/commands/clear.ts +4 -1
- package/src/commands/cp.ts +78 -0
- package/src/commands/curl.ts +2 -0
- package/src/commands/cut.ts +29 -0
- package/src/commands/date.ts +24 -0
- package/src/commands/deluser.ts +2 -0
- package/src/commands/df.ts +18 -0
- package/src/commands/diff.ts +29 -0
- package/src/commands/du.ts +39 -0
- package/src/commands/echo.ts +2 -0
- package/src/commands/env.ts +6 -16
- package/src/commands/export.ts +11 -24
- package/src/commands/find.ts +63 -0
- package/src/commands/grep.ts +51 -38
- package/src/commands/groups.ts +14 -0
- package/src/commands/gzip.ts +31 -0
- package/src/commands/head.ts +37 -0
- package/src/commands/help.ts +81 -3
- package/src/commands/hostname.ts +2 -0
- package/src/commands/htop.ts +2 -0
- package/src/commands/id.ts +16 -0
- package/src/commands/index.ts +114 -133
- package/src/commands/kill.ts +14 -0
- package/src/commands/ln.ts +49 -0
- package/src/commands/ls.ts +2 -0
- package/src/commands/mkdir.ts +2 -0
- package/src/commands/mv.ts +45 -0
- package/src/commands/nano.ts +2 -0
- package/src/commands/neofetch.ts +2 -0
- package/src/commands/passwd.ts +2 -0
- package/src/commands/ping.ts +20 -0
- package/src/commands/ps.ts +19 -0
- package/src/commands/pwd.ts +2 -0
- package/src/commands/rm.ts +2 -0
- package/src/commands/sed.ts +45 -0
- package/src/commands/set.ts +19 -50
- package/src/commands/sh.ts +192 -43
- package/src/commands/sleep.ts +14 -0
- package/src/commands/sort.ts +37 -0
- package/src/commands/su.ts +2 -0
- package/src/commands/sudo.ts +2 -0
- package/src/commands/tail.ts +39 -0
- package/src/commands/tar.ts +58 -0
- package/src/commands/tee.ts +25 -0
- package/src/commands/touch.ts +2 -0
- package/src/commands/tr.ts +24 -0
- package/src/commands/tree.ts +2 -0
- package/src/commands/uname.ts +20 -0
- package/src/commands/uniq.ts +28 -0
- package/src/commands/unset.ts +5 -12
- package/src/commands/wc.ts +50 -0
- package/src/commands/wget.ts +2 -0
- package/src/commands/who.ts +2 -0
- package/src/commands/whoami.ts +2 -0
- package/src/commands/xargs.ts +17 -0
- package/src/index.ts +1 -0
- package/src/types/commands.ts +14 -0
- package/src/types/pipeline.ts +23 -0
- package/standalone.js +93 -55
- package/standalone.js.map +4 -4
- package/tests/bun-test-shim.ts +1 -0
- package/tests/sftp.test.ts +115 -191
- package/tests/users.test.ts +42 -88
package/benchmark-results.txt
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
Benchmarking VirtualShell concurrency:
|
|
2
2
|
|
|
3
3
|
Running 1 shells...
|
|
4
|
-
Initialized 1 shells in
|
|
4
|
+
Initialized 1 shells in 67ms, RSS 82 MB
|
|
5
5
|
Executed shell commands in 2ms, RSS now 82 MB (+0 MB)
|
|
6
6
|
|
|
7
7
|
Running 2 shells...
|
|
8
8
|
Initialized 2 shells in 2ms, RSS 82 MB
|
|
9
|
-
Executed shell commands in
|
|
9
|
+
Executed shell commands in 0ms, RSS now 82 MB (+0 MB)
|
|
10
10
|
|
|
11
11
|
Running 5 shells...
|
|
12
|
-
Initialized 5 shells in
|
|
13
|
-
Executed shell commands in
|
|
12
|
+
Initialized 5 shells in 1ms, RSS 82 MB
|
|
13
|
+
Executed shell commands in 1ms, RSS now 82 MB (+0 MB)
|
|
14
14
|
|
|
15
15
|
Running 10 shells...
|
|
16
|
-
Initialized 10 shells in
|
|
17
|
-
Executed shell commands in 3ms, RSS now 84 MB (+
|
|
16
|
+
Initialized 10 shells in 4ms, RSS 83 MB
|
|
17
|
+
Executed shell commands in 3ms, RSS now 84 MB (+0 MB)
|
|
18
18
|
|
|
19
19
|
Running 20 shells...
|
|
20
|
-
Initialized 20 shells in
|
|
21
|
-
Executed shell commands in
|
|
20
|
+
Initialized 20 shells in 5ms, RSS 84 MB
|
|
21
|
+
Executed shell commands in 3ms, RSS now 86 MB (+1 MB)
|
|
22
22
|
|
|
23
23
|
Running 50 shells...
|
|
24
|
-
Initialized 50 shells in
|
|
25
|
-
Executed shell commands in
|
|
24
|
+
Initialized 50 shells in 10ms, RSS 88 MB
|
|
25
|
+
Executed shell commands in 7ms, RSS now 89 MB (+1 MB)
|
|
26
26
|
|
|
27
27
|
Running 100 shells...
|
|
28
|
-
Initialized 100 shells in
|
|
29
|
-
Executed shell commands in
|
|
28
|
+
Initialized 100 shells in 18ms, RSS 92 MB
|
|
29
|
+
Executed shell commands in 9ms, RSS now 95 MB (+3 MB)
|
|
30
30
|
|
|
31
31
|
Summary:
|
|
32
32
|
|
|
33
|
-
count init_ms cmd_ms init_rss final_rss
|
|
34
|
-
1
|
|
35
|
-
2 2
|
|
36
|
-
5
|
|
37
|
-
10
|
|
38
|
-
20
|
|
39
|
-
50
|
|
40
|
-
100
|
|
33
|
+
count init_ms cmd_ms init_rss final_rss delta_rss
|
|
34
|
+
1 67 2 82 MB 82 MB 0 MB
|
|
35
|
+
2 2 0 82 MB 82 MB 0 MB
|
|
36
|
+
5 1 1 82 MB 82 MB 0 MB
|
|
37
|
+
10 4 3 83 MB 84 MB 0 MB
|
|
38
|
+
20 5 3 84 MB 86 MB 1 MB
|
|
39
|
+
50 10 7 88 MB 89 MB 1 MB
|
|
40
|
+
100 18 9 92 MB 95 MB 3 MB
|
package/dist/SSHMimic/exec.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runCommand } from "../commands";
|
|
1
|
+
import { makeDefaultEnv, runCommand } from "../commands";
|
|
2
2
|
function toTtyLines(text) {
|
|
3
3
|
return text
|
|
4
4
|
.replace(/\r\n/g, "\n")
|
|
@@ -6,7 +6,7 @@ function toTtyLines(text) {
|
|
|
6
6
|
.replace(/\n/g, "\r\n");
|
|
7
7
|
}
|
|
8
8
|
export function runExec(stream, cmd, authUser, hostname, shell) {
|
|
9
|
-
Promise.resolve(runCommand(cmd, authUser, hostname, "exec", `/home/${authUser}`, shell))
|
|
9
|
+
Promise.resolve(runCommand(cmd, authUser, hostname, "exec", `/home/${authUser}`, shell, undefined, makeDefaultEnv(authUser, hostname)))
|
|
10
10
|
.then((result) => {
|
|
11
11
|
if (result.stdout) {
|
|
12
12
|
stream.write(`${toTtyLines(result.stdout)}\r\n`);
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type { CommandMode, CommandResult } from "../types/commands";
|
|
2
|
-
import type { Pipeline } from "../types/pipeline";
|
|
1
|
+
import type { CommandMode, CommandResult, ShellEnv } from "../types/commands";
|
|
2
|
+
import type { Pipeline, Script, Statement } from "../types/pipeline";
|
|
3
3
|
import type { VirtualShell } from "../VirtualShell";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export declare function executePipeline(pipeline: Pipeline, authUser: string, hostname: string, mode: CommandMode, cwd: string, shell: VirtualShell): Promise<CommandResult>;
|
|
4
|
+
export declare function executeScript(script: Script, authUser: string, hostname: string, mode: CommandMode, cwd: string, shell: VirtualShell, env: ShellEnv): Promise<CommandResult>;
|
|
5
|
+
/** Execute statements connected by &&/||/; */
|
|
6
|
+
export declare function executeStatements(statements: Statement[], authUser: string, hostname: string, mode: CommandMode, cwd: string, shell: VirtualShell, env: ShellEnv): Promise<CommandResult>;
|
|
7
|
+
export declare function executePipeline(pipeline: Pipeline, authUser: string, hostname: string, mode: CommandMode, cwd: string, shell: VirtualShell, env?: ShellEnv): Promise<CommandResult>;
|
|
9
8
|
//# sourceMappingURL=executor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/executor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/executor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC9E,OAAO,KAAK,EAAE,QAAQ,EAAmB,MAAM,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACtF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAIpD,wBAAsB,aAAa,CAClC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,QAAQ,GACX,OAAO,CAAC,aAAa,CAAC,CAiBxB;AAED,8CAA8C;AAC9C,wBAAsB,iBAAiB,CACtC,UAAU,EAAE,SAAS,EAAE,EACvB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,QAAQ,GACX,OAAO,CAAC,aAAa,CAAC,CA4BxB;AAID,wBAAsB,eAAe,CACpC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,YAAY,EACnB,GAAG,CAAC,EAAE,QAAQ,GACZ,OAAO,CAAC,aAAa,CAAC,CAiBxB"}
|
|
@@ -1,25 +1,66 @@
|
|
|
1
1
|
import { runCommand as runSingleCommand } from "../commands";
|
|
2
2
|
import { resolvePath } from "../commands/helpers";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
// ── Script executor (handles &&/||/;) ────────────────────────────────────────
|
|
4
|
+
export async function executeScript(script, authUser, hostname, mode, cwd, shell, env) {
|
|
5
|
+
if (!script.isValid)
|
|
6
|
+
return { stderr: script.error || "Syntax error", exitCode: 1 };
|
|
7
|
+
let lastResult = { exitCode: 0 };
|
|
8
|
+
for (const stmt of script.statements) {
|
|
9
|
+
// Decide whether to run this statement based on previous op
|
|
10
|
+
lastResult = await executePipeline(stmt.pipeline, authUser, hostname, mode, cwd, shell, env);
|
|
11
|
+
env.lastExitCode = lastResult.exitCode ?? 0;
|
|
12
|
+
// Propagate session-control signals
|
|
13
|
+
if (lastResult.closeSession || lastResult.switchUser || lastResult.nextCwd) {
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return lastResult;
|
|
18
|
+
}
|
|
19
|
+
/** Execute statements connected by &&/||/; */
|
|
20
|
+
export async function executeStatements(statements, authUser, hostname, mode, cwd, shell, env) {
|
|
21
|
+
let last = { exitCode: 0 };
|
|
22
|
+
let i = 0;
|
|
23
|
+
while (i < statements.length) {
|
|
24
|
+
const stmt = statements[i];
|
|
25
|
+
last = await executePipeline(stmt.pipeline, authUser, hostname, mode, cwd, shell, env);
|
|
26
|
+
env.lastExitCode = last.exitCode ?? 0;
|
|
27
|
+
if (last.closeSession || last.switchUser)
|
|
28
|
+
return last;
|
|
29
|
+
const op = stmt.op;
|
|
30
|
+
if (!op || op === ";") {
|
|
31
|
+
// always run next
|
|
32
|
+
}
|
|
33
|
+
else if (op === "&&") {
|
|
34
|
+
if ((last.exitCode ?? 0) !== 0) {
|
|
35
|
+
// skip until next ; or end
|
|
36
|
+
while (i < statements.length && statements[i]?.op === "&&")
|
|
37
|
+
i++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else if (op === "||") {
|
|
41
|
+
if ((last.exitCode ?? 0) === 0) {
|
|
42
|
+
// skip until next ; or end
|
|
43
|
+
while (i < statements.length && statements[i]?.op === "||")
|
|
44
|
+
i++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
i++;
|
|
10
48
|
}
|
|
49
|
+
return last;
|
|
50
|
+
}
|
|
51
|
+
// ── Pipeline executor ─────────────────────────────────────────────────────────
|
|
52
|
+
export async function executePipeline(pipeline, authUser, hostname, mode, cwd, shell, env) {
|
|
53
|
+
if (!pipeline.isValid)
|
|
54
|
+
return { stderr: pipeline.error || "Syntax error", exitCode: 1 };
|
|
55
|
+
if (pipeline.commands.length === 0)
|
|
56
|
+
return { exitCode: 0 };
|
|
57
|
+
const shellEnv = env ?? { vars: {}, lastExitCode: 0 };
|
|
11
58
|
if (pipeline.commands.length === 1) {
|
|
12
|
-
|
|
13
|
-
return executeSingleCommandWithRedirections(pipeline.commands[0], authUser, hostname, mode, cwd, shell);
|
|
59
|
+
return executeSingleCommandWithRedirections(pipeline.commands[0], authUser, hostname, mode, cwd, shell, shellEnv);
|
|
14
60
|
}
|
|
15
|
-
|
|
16
|
-
return executePipelineChain(pipeline.commands, authUser, hostname, mode, cwd, shell);
|
|
61
|
+
return executePipelineChain(pipeline.commands, authUser, hostname, mode, cwd, shell, shellEnv);
|
|
17
62
|
}
|
|
18
|
-
|
|
19
|
-
* Execute a single command with input/output redirections
|
|
20
|
-
*/
|
|
21
|
-
async function executeSingleCommandWithRedirections(cmd, authUser, hostname, mode, cwd, shell) {
|
|
22
|
-
// Prepare input if input file specified
|
|
63
|
+
async function executeSingleCommandWithRedirections(cmd, authUser, hostname, mode, cwd, shell, env) {
|
|
23
64
|
let stdin;
|
|
24
65
|
if (cmd.inputFile) {
|
|
25
66
|
const inputPath = resolvePath(cwd, cmd.inputFile);
|
|
@@ -27,29 +68,23 @@ async function executeSingleCommandWithRedirections(cmd, authUser, hostname, mod
|
|
|
27
68
|
stdin = shell.vfs.readFile(inputPath);
|
|
28
69
|
}
|
|
29
70
|
catch {
|
|
30
|
-
return {
|
|
31
|
-
stderr: `cat: ${cmd.inputFile}: No such file or directory`,
|
|
32
|
-
exitCode: 1,
|
|
33
|
-
};
|
|
71
|
+
return { stderr: `${cmd.inputFile}: No such file or directory`, exitCode: 1 };
|
|
34
72
|
}
|
|
35
73
|
}
|
|
36
|
-
// Build raw input for the command
|
|
37
74
|
const rawInput = [cmd.name, ...cmd.args].join(" ");
|
|
38
|
-
|
|
39
|
-
const result = await runSingleCommand(rawInput, authUser, hostname, mode, cwd, shell, stdin);
|
|
40
|
-
// Handle output redirection
|
|
75
|
+
const result = await runSingleCommand(rawInput, authUser, hostname, mode, cwd, shell, stdin, env);
|
|
41
76
|
if (cmd.outputFile) {
|
|
42
77
|
const outputPath = resolvePath(cwd, cmd.outputFile);
|
|
43
78
|
const output = result.stdout || "";
|
|
44
79
|
try {
|
|
45
80
|
if (cmd.appendOutput) {
|
|
46
|
-
try {
|
|
47
|
-
|
|
48
|
-
shell.writeFileAsUser(authUser, outputPath, existing + output);
|
|
81
|
+
const existing = (() => { try {
|
|
82
|
+
return shell.vfs.readFile(outputPath);
|
|
49
83
|
}
|
|
50
84
|
catch {
|
|
51
|
-
|
|
52
|
-
}
|
|
85
|
+
return "";
|
|
86
|
+
} })();
|
|
87
|
+
shell.writeFileAsUser(authUser, outputPath, existing + output);
|
|
53
88
|
}
|
|
54
89
|
else {
|
|
55
90
|
shell.writeFileAsUser(authUser, outputPath, output);
|
|
@@ -57,55 +92,40 @@ async function executeSingleCommandWithRedirections(cmd, authUser, hostname, mod
|
|
|
57
92
|
return { ...result, stdout: "" };
|
|
58
93
|
}
|
|
59
94
|
catch {
|
|
60
|
-
return {
|
|
61
|
-
...result,
|
|
62
|
-
stderr: `Failed to write to ${cmd.outputFile}`,
|
|
63
|
-
exitCode: 1,
|
|
64
|
-
};
|
|
95
|
+
return { ...result, stderr: `Failed to write to ${cmd.outputFile}`, exitCode: 1 };
|
|
65
96
|
}
|
|
66
97
|
}
|
|
67
98
|
return result;
|
|
68
99
|
}
|
|
69
|
-
|
|
70
|
-
* Execute a chain of commands connected by pipes
|
|
71
|
-
*/
|
|
72
|
-
async function executePipelineChain(commands, authUser, hostname, mode, cwd, shell) {
|
|
100
|
+
async function executePipelineChain(commands, authUser, hostname, mode, cwd, shell, env) {
|
|
73
101
|
let currentOutput = "";
|
|
74
102
|
let exitCode = 0;
|
|
75
103
|
for (let i = 0; i < commands.length; i++) {
|
|
76
104
|
const cmd = commands[i];
|
|
77
|
-
// Handle input file for first command
|
|
78
105
|
if (i === 0 && cmd.inputFile) {
|
|
79
106
|
const inputPath = resolvePath(cwd, cmd.inputFile);
|
|
80
107
|
try {
|
|
81
108
|
currentOutput = shell.vfs.readFile(inputPath);
|
|
82
109
|
}
|
|
83
110
|
catch {
|
|
84
|
-
return {
|
|
85
|
-
stderr: `cat: ${cmd.inputFile}: No such file or directory`,
|
|
86
|
-
exitCode: 1,
|
|
87
|
-
};
|
|
111
|
+
return { stderr: `${cmd.inputFile}: No such file or directory`, exitCode: 1 };
|
|
88
112
|
}
|
|
89
113
|
}
|
|
90
|
-
// Build raw input
|
|
91
114
|
const rawInput = [cmd.name, ...cmd.args].join(" ");
|
|
92
|
-
|
|
93
|
-
// For now, we'll append input as an additional arg for commands that support it
|
|
94
|
-
const result = await runSingleCommand(rawInput, authUser, hostname, mode, cwd, shell, currentOutput);
|
|
115
|
+
const result = await runSingleCommand(rawInput, authUser, hostname, mode, cwd, shell, currentOutput, env);
|
|
95
116
|
exitCode = result.exitCode ?? 0;
|
|
96
|
-
// Handle output redirection (only for last command)
|
|
97
117
|
if (i === commands.length - 1 && cmd.outputFile) {
|
|
98
118
|
const outputPath = resolvePath(cwd, cmd.outputFile);
|
|
99
119
|
const output = result.stdout || "";
|
|
100
120
|
try {
|
|
101
121
|
if (cmd.appendOutput) {
|
|
102
|
-
try {
|
|
103
|
-
|
|
104
|
-
shell.writeFileAsUser(authUser, outputPath, existing + output);
|
|
122
|
+
const existing = (() => { try {
|
|
123
|
+
return shell.vfs.readFile(outputPath);
|
|
105
124
|
}
|
|
106
125
|
catch {
|
|
107
|
-
|
|
108
|
-
}
|
|
126
|
+
return "";
|
|
127
|
+
} })();
|
|
128
|
+
shell.writeFileAsUser(authUser, outputPath, existing + output);
|
|
109
129
|
}
|
|
110
130
|
else {
|
|
111
131
|
shell.writeFileAsUser(authUser, outputPath, output);
|
|
@@ -113,19 +133,16 @@ async function executePipelineChain(commands, authUser, hostname, mode, cwd, she
|
|
|
113
133
|
currentOutput = "";
|
|
114
134
|
}
|
|
115
135
|
catch {
|
|
116
|
-
return {
|
|
117
|
-
stderr: `Failed to write to ${cmd.outputFile}`,
|
|
118
|
-
exitCode: 1,
|
|
119
|
-
};
|
|
136
|
+
return { stderr: `Failed to write to ${cmd.outputFile}`, exitCode: 1 };
|
|
120
137
|
}
|
|
121
138
|
}
|
|
122
139
|
else {
|
|
123
|
-
// Pass output to next command
|
|
124
140
|
currentOutput = result.stdout || "";
|
|
125
141
|
}
|
|
126
|
-
if (result.stderr && exitCode !== 0)
|
|
142
|
+
if (result.stderr && exitCode !== 0)
|
|
127
143
|
return { stderr: result.stderr, exitCode };
|
|
128
|
-
|
|
144
|
+
if (result.closeSession || result.switchUser)
|
|
145
|
+
return result;
|
|
129
146
|
}
|
|
130
147
|
return { stdout: currentOutput, exitCode };
|
|
131
148
|
}
|
package/dist/SSHMimic/index.d.ts
CHANGED
|
@@ -5,19 +5,31 @@ declare class SshMimic extends EventEmitter {
|
|
|
5
5
|
port: number;
|
|
6
6
|
server: SshServer | null;
|
|
7
7
|
private shell;
|
|
8
|
-
|
|
8
|
+
/** Max failed auth attempts before an IP is temporarily locked. */
|
|
9
|
+
private readonly maxAuthAttempts;
|
|
10
|
+
/** How long (ms) a locked IP must wait before retrying. */
|
|
11
|
+
private readonly lockoutDurationMs;
|
|
12
|
+
private readonly authAttempts;
|
|
9
13
|
/**
|
|
10
14
|
* Creates a new SSH mimic server instance.
|
|
11
15
|
*
|
|
12
16
|
* @param port TCP port to bind on localhost.
|
|
13
17
|
* @param hostname Virtual hostname used for the SSH ident and default shell label.
|
|
14
18
|
* @param shell Optional preconfigured virtual shell instance to reuse.
|
|
19
|
+
* @param maxAuthAttempts Max failed attempts per IP before lockout (default: 5).
|
|
20
|
+
* @param lockoutDurationMs Lockout window in ms after exceeding attempts (default: 60 000).
|
|
15
21
|
*/
|
|
16
|
-
constructor({ port, hostname, shell, }: {
|
|
22
|
+
constructor({ port, hostname, shell, maxAuthAttempts, lockoutDurationMs, }: {
|
|
17
23
|
port: number;
|
|
18
24
|
hostname?: string;
|
|
19
25
|
shell?: VirtualShell;
|
|
26
|
+
maxAuthAttempts?: number;
|
|
27
|
+
lockoutDurationMs?: number;
|
|
20
28
|
});
|
|
29
|
+
private isLockedOut;
|
|
30
|
+
private recordFailure;
|
|
31
|
+
private recordSuccess;
|
|
32
|
+
private ensureHomeDir;
|
|
21
33
|
/**
|
|
22
34
|
* Starts server and initializes virtual filesystem, users, and handlers.
|
|
23
35
|
*
|
|
@@ -28,6 +40,11 @@ declare class SshMimic extends EventEmitter {
|
|
|
28
40
|
* Stops server if running.
|
|
29
41
|
*/
|
|
30
42
|
stop(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Manually clears the rate-limit record for an IP address.
|
|
45
|
+
* Useful in tests or admin tooling.
|
|
46
|
+
*/
|
|
47
|
+
clearLockout(ip: string): void;
|
|
31
48
|
}
|
|
32
49
|
export { SftpMimic } from "./sftp";
|
|
33
50
|
export { SshMimic };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AA0B/C,cAAM,QAAS,SAAQ,YAAY;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,KAAK,CAAe;IAE5B,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,2DAA2D;IAC3D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqC;IAElE;;;;;;;;OAQG;gBACS,EACX,IAAI,EACJ,QAA0B,EAC1B,KAAkC,EAClC,eAAmB,EACnB,iBAA0B,GAC1B,EAAE;QACF,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,YAAY,CAAC;QACrB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC3B;IAYD,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,aAAa;IAcrB;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IA0JrC;;OAEG;IACI,IAAI,IAAI,IAAI;IAUnB;;;OAGG;IACI,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;CAGrC;AAED,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,CAAC"}
|
package/dist/SSHMimic/index.js
CHANGED
|
@@ -10,28 +10,76 @@ import { loadOrCreateHostKey } from "./hostKey";
|
|
|
10
10
|
* This class is exported as `VirtualSshServer` for public API compatibility.
|
|
11
11
|
* Create an instance, call {@link SshMimic.start}, and stop it with
|
|
12
12
|
* {@link SshMimic.stop} when your process exits.
|
|
13
|
+
*
|
|
14
|
+
* Features:
|
|
15
|
+
* - Password authentication
|
|
16
|
+
* - Public-key authentication
|
|
17
|
+
* - Per-IP rate limiting / lockout for brute-force protection
|
|
18
|
+
* - Interactive shell sessions
|
|
19
|
+
* - Non-interactive exec sessions
|
|
13
20
|
*/
|
|
14
21
|
const perf = createPerfLogger("SshMimic");
|
|
15
22
|
class SshMimic extends EventEmitter {
|
|
16
23
|
port;
|
|
17
24
|
server;
|
|
18
25
|
shell;
|
|
19
|
-
|
|
26
|
+
/** Max failed auth attempts before an IP is temporarily locked. */
|
|
27
|
+
maxAuthAttempts;
|
|
28
|
+
/** How long (ms) a locked IP must wait before retrying. */
|
|
29
|
+
lockoutDurationMs;
|
|
30
|
+
authAttempts = new Map();
|
|
20
31
|
/**
|
|
21
32
|
* Creates a new SSH mimic server instance.
|
|
22
33
|
*
|
|
23
34
|
* @param port TCP port to bind on localhost.
|
|
24
35
|
* @param hostname Virtual hostname used for the SSH ident and default shell label.
|
|
25
36
|
* @param shell Optional preconfigured virtual shell instance to reuse.
|
|
37
|
+
* @param maxAuthAttempts Max failed attempts per IP before lockout (default: 5).
|
|
38
|
+
* @param lockoutDurationMs Lockout window in ms after exceeding attempts (default: 60 000).
|
|
26
39
|
*/
|
|
27
|
-
constructor({ port, hostname = "typescript-vm", shell = new VirtualShell(hostname), }) {
|
|
40
|
+
constructor({ port, hostname = "typescript-vm", shell = new VirtualShell(hostname), maxAuthAttempts = 5, lockoutDurationMs = 60_000, }) {
|
|
28
41
|
super();
|
|
29
42
|
perf.mark("constructor");
|
|
30
43
|
this.port = port;
|
|
31
|
-
this.shellHostname = hostname;
|
|
32
44
|
this.server = null;
|
|
33
45
|
this.shell = shell;
|
|
46
|
+
this.maxAuthAttempts = maxAuthAttempts;
|
|
47
|
+
this.lockoutDurationMs = lockoutDurationMs;
|
|
48
|
+
}
|
|
49
|
+
// ── Rate limiting ────────────────────────────────────────────────────────
|
|
50
|
+
isLockedOut(ip) {
|
|
51
|
+
const entry = this.authAttempts.get(ip);
|
|
52
|
+
if (!entry)
|
|
53
|
+
return false;
|
|
54
|
+
if (Date.now() < entry.lockedUntil)
|
|
55
|
+
return true;
|
|
56
|
+
if (entry.lockedUntil > 0) {
|
|
57
|
+
this.authAttempts.delete(ip);
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
recordFailure(ip) {
|
|
62
|
+
const entry = this.authAttempts.get(ip) ?? { attempts: 0, lockedUntil: 0 };
|
|
63
|
+
entry.attempts += 1;
|
|
64
|
+
if (entry.attempts >= this.maxAuthAttempts) {
|
|
65
|
+
entry.lockedUntil = Date.now() + this.lockoutDurationMs;
|
|
66
|
+
this.emit("auth:lockout", { ip, until: new Date(entry.lockedUntil) });
|
|
67
|
+
}
|
|
68
|
+
this.authAttempts.set(ip, entry);
|
|
69
|
+
}
|
|
70
|
+
recordSuccess(ip) {
|
|
71
|
+
this.authAttempts.delete(ip);
|
|
72
|
+
}
|
|
73
|
+
// ── Home directory bootstrap ─────────────────────────────────────────────
|
|
74
|
+
ensureHomeDir(authUser) {
|
|
75
|
+
const homePath = `/home/${authUser}`;
|
|
76
|
+
if (!this.shell.vfs.exists(homePath)) {
|
|
77
|
+
this.shell.vfs.mkdir(homePath, 0o755);
|
|
78
|
+
this.shell.vfs.writeFile(`${homePath}/README.txt`, `Welcome to ${this.shell.hostname}\n`);
|
|
79
|
+
void this.shell.vfs.flushMirror();
|
|
80
|
+
}
|
|
34
81
|
}
|
|
82
|
+
// ── Server lifecycle ─────────────────────────────────────────────────────
|
|
35
83
|
/**
|
|
36
84
|
* Starts server and initializes virtual filesystem, users, and handlers.
|
|
37
85
|
*
|
|
@@ -41,7 +89,6 @@ class SshMimic extends EventEmitter {
|
|
|
41
89
|
perf.mark("start");
|
|
42
90
|
const shell = this.shell;
|
|
43
91
|
const privateKey = loadOrCreateHostKey();
|
|
44
|
-
// Ensure VirtualShell is fully initialized before accepting connections
|
|
45
92
|
await shell.ensureInitialized();
|
|
46
93
|
this.server = new SshServer({
|
|
47
94
|
hostKeys: [privateKey],
|
|
@@ -52,47 +99,75 @@ class SshMimic extends EventEmitter {
|
|
|
52
99
|
let sessionId = null;
|
|
53
100
|
this.emit("client:connect");
|
|
54
101
|
client.on("authentication", (ctx) => {
|
|
55
|
-
|
|
102
|
+
const candidateUser = ctx.username || "root";
|
|
103
|
+
remoteAddress = ctx.ip ?? remoteAddress;
|
|
104
|
+
// Rate-limit check
|
|
105
|
+
if (this.isLockedOut(remoteAddress)) {
|
|
106
|
+
this.emit("auth:failure", { username: candidateUser, remoteAddress, reason: "lockout" });
|
|
107
|
+
ctx.reject();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// ── Password auth ──────────────────────────────────────
|
|
56
111
|
if (ctx.method === "password") {
|
|
57
|
-
const candidateUser = ctx.username || "root";
|
|
58
|
-
remoteAddress = ctx.ip ?? remoteAddress;
|
|
59
112
|
if (!shell.users.hasPassword(candidateUser)) {
|
|
60
113
|
console.log(`User ${candidateUser} has no password set, allowing login without verification`);
|
|
61
114
|
authUser = candidateUser;
|
|
62
115
|
sessionId = shell.users.registerSession(authUser, remoteAddress).id;
|
|
116
|
+
this.recordSuccess(remoteAddress);
|
|
63
117
|
this.emit("auth:success", { username: authUser, remoteAddress });
|
|
64
|
-
|
|
65
|
-
if (!shell.vfs.exists(homePath)) {
|
|
66
|
-
shell.vfs.mkdir(homePath, 0o755);
|
|
67
|
-
shell.vfs.writeFile(`${homePath}/README.txt`, `Welcome to ${shell?.hostname ?? this.shellHostname}`);
|
|
68
|
-
void shell.vfs.flushMirror();
|
|
69
|
-
}
|
|
118
|
+
this.ensureHomeDir(authUser);
|
|
70
119
|
ctx.accept();
|
|
71
120
|
return;
|
|
72
121
|
}
|
|
73
122
|
if (!ctx.password ||
|
|
74
123
|
ctx.password === "" ||
|
|
75
124
|
!shell.users.verifyPassword(candidateUser, ctx.password)) {
|
|
76
|
-
this.
|
|
77
|
-
|
|
78
|
-
remoteAddress,
|
|
79
|
-
});
|
|
125
|
+
this.recordFailure(remoteAddress);
|
|
126
|
+
this.emit("auth:failure", { username: candidateUser, remoteAddress });
|
|
80
127
|
ctx.reject();
|
|
81
128
|
return;
|
|
82
129
|
}
|
|
83
130
|
authUser = candidateUser;
|
|
84
131
|
sessionId = shell.users.registerSession(authUser, remoteAddress).id;
|
|
132
|
+
this.recordSuccess(remoteAddress);
|
|
85
133
|
this.emit("auth:success", { username: authUser, remoteAddress });
|
|
86
|
-
|
|
87
|
-
if (!shell.vfs.exists(homePath)) {
|
|
88
|
-
shell.vfs.mkdir(homePath, 0o755);
|
|
89
|
-
shell.vfs.writeFile(`${homePath}/README.txt`, `Welcome to ${shell?.hostname ?? this.shellHostname}`);
|
|
90
|
-
void shell.vfs.flushMirror();
|
|
91
|
-
}
|
|
134
|
+
this.ensureHomeDir(authUser);
|
|
92
135
|
ctx.accept();
|
|
93
136
|
return;
|
|
94
137
|
}
|
|
95
|
-
|
|
138
|
+
// ── Public-key auth ────────────────────────────────────
|
|
139
|
+
if (ctx.method === "publickey") {
|
|
140
|
+
const authorizedKeys = shell.users.getAuthorizedKeys(candidateUser);
|
|
141
|
+
if (authorizedKeys.length === 0) {
|
|
142
|
+
// No keys configured — reject cleanly
|
|
143
|
+
ctx.reject();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const incomingKey = ctx.key;
|
|
147
|
+
const keyMatches = authorizedKeys.some((k) => k.algo === incomingKey.algo &&
|
|
148
|
+
k.data.equals(incomingKey.data));
|
|
149
|
+
if (!keyMatches) {
|
|
150
|
+
this.recordFailure(remoteAddress);
|
|
151
|
+
this.emit("auth:failure", { username: candidateUser, remoteAddress, method: "publickey" });
|
|
152
|
+
ctx.reject();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Key matched — if this is a signature check step, accept
|
|
156
|
+
if (ctx.signature) {
|
|
157
|
+
authUser = candidateUser;
|
|
158
|
+
sessionId = shell.users.registerSession(authUser, remoteAddress).id;
|
|
159
|
+
this.recordSuccess(remoteAddress);
|
|
160
|
+
this.emit("auth:success", { username: authUser, remoteAddress, method: "publickey" });
|
|
161
|
+
this.ensureHomeDir(authUser);
|
|
162
|
+
ctx.accept();
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// Key exists but no signature yet — ssh2 will call again with signature
|
|
166
|
+
ctx.accept();
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
ctx.reject(["password", "publickey"]);
|
|
96
171
|
});
|
|
97
172
|
client.on("close", () => {
|
|
98
173
|
shell.users.unregisterSession(sessionId);
|
|
@@ -146,6 +221,13 @@ class SshMimic extends EventEmitter {
|
|
|
146
221
|
});
|
|
147
222
|
}
|
|
148
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Manually clears the rate-limit record for an IP address.
|
|
226
|
+
* Useful in tests or admin tooling.
|
|
227
|
+
*/
|
|
228
|
+
clearLockout(ip) {
|
|
229
|
+
this.authAttempts.delete(ip);
|
|
230
|
+
}
|
|
149
231
|
}
|
|
150
232
|
export { SftpMimic } from "./sftp";
|
|
151
233
|
export { SshMimic };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sftp.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/sftp.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAI3C,OAAO,KAAK,iBAAiB,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AA4HhE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,GAAG,CAAC,EAAE,iBAAiB,CAAC;IACxB,KAAK,CAAC,EAAE,kBAAkB,CAAC;CAC3B;AAED,qBAAa,SAAU,SAAQ,YAAY;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,OAAO,CAAiC;gBAEpC,EACX,IAAI,EACJ,QAA0B,EAC1B,KAAK,EACL,GAAG,EACH,KAAK,GACL,EAAE,gBAAgB;IAwBnB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,QAAQ;IAIH,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"sftp.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/sftp.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAI3C,OAAO,KAAK,iBAAiB,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AA4HhE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,GAAG,CAAC,EAAE,iBAAiB,CAAC;IACxB,KAAK,CAAC,EAAE,kBAAkB,CAAC;CAC3B;AAED,qBAAa,SAAU,SAAQ,YAAY;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,OAAO,CAAiC;gBAEpC,EACX,IAAI,EACJ,QAA0B,EAC1B,KAAK,EACL,GAAG,EACH,KAAK,GACL,EAAE,gBAAgB;IAwBnB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,QAAQ;IAIH,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAwK9B,IAAI,IAAI,IAAI;IAUnB;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAiB1B;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,kBAAkB;CA6a1B"}
|
package/dist/SSHMimic/sftp.js
CHANGED
|
@@ -110,6 +110,13 @@ export class SftpMimic extends EventEmitter {
|
|
|
110
110
|
remoteAddress = ctx.ip ?? remoteAddress;
|
|
111
111
|
console.log(`[SFTP] Auth attempt: user=${candidateUser}, method=${ctx.method}, ip=${remoteAddress}`);
|
|
112
112
|
if (ctx.method === "password") {
|
|
113
|
+
// If no password is set for the user, allow login without verification
|
|
114
|
+
if (!this.getUsers().hasPassword(candidateUser)) {
|
|
115
|
+
acceptSession(candidateUser);
|
|
116
|
+
this.emit("auth:success", { username: authUser, remoteAddress });
|
|
117
|
+
ctx.accept();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
113
120
|
if (!this.getUsers().verifyPassword(candidateUser, ctx.password ?? "")) {
|
|
114
121
|
this.emit("auth:failure", {
|
|
115
122
|
username: candidateUser,
|
|
@@ -125,6 +132,13 @@ export class SftpMimic extends EventEmitter {
|
|
|
125
132
|
}
|
|
126
133
|
if (ctx.method === "keyboard-interactive") {
|
|
127
134
|
const keyboardCtx = ctx;
|
|
135
|
+
// If no password is set, accept immediately
|
|
136
|
+
if (!this.getUsers().hasPassword(candidateUser)) {
|
|
137
|
+
acceptSession(candidateUser);
|
|
138
|
+
this.emit("auth:success", { username: authUser, remoteAddress });
|
|
139
|
+
keyboardCtx.accept();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
128
142
|
keyboardCtx.prompt([{ prompt: "Password: ", echo: false }], (answers) => {
|
|
129
143
|
const password = answers[0] ?? "";
|
|
130
144
|
if (!this.getUsers().verifyPassword(candidateUser, password)) {
|