typescript-virtual-container 1.0.8 → 1.1.1-b
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 +18 -0
- package/README.md +183 -92
- package/modules/shellInteractive.ts +45 -0
- package/modules/shellRuntime.ts +76 -0
- package/package.json +1 -1
- package/src/{SSHMimic/client.ts → SSHClient/index.ts} +17 -20
- package/src/SSHMimic/exec.ts +6 -17
- package/src/SSHMimic/executor.ts +20 -31
- package/src/SSHMimic/index.ts +23 -85
- package/src/VirtualFileSystem/index.ts +26 -1
- package/src/VirtualShell/index.ts +131 -26
- package/src/VirtualShell/shell.ts +43 -141
- package/src/VirtualShell/shellParser.ts +32 -7
- package/src/{SSHMimic/users.ts → VirtualUserManager/index.ts} +155 -3
- package/src/{VirtualShell/commands → commands}/adduser.ts +3 -3
- package/src/{VirtualShell/commands → commands}/cat.ts +4 -4
- package/src/{VirtualShell/commands → commands}/cd.ts +3 -3
- package/src/{VirtualShell/commands → commands}/clear.ts +1 -1
- package/src/{VirtualShell/commands → commands}/curl.ts +3 -3
- package/src/{VirtualShell/commands → commands}/deluser.ts +3 -3
- package/src/{VirtualShell/commands → commands}/echo.ts +1 -1
- package/src/{VirtualShell/commands → commands}/env.ts +1 -1
- package/src/{VirtualShell/commands → commands}/exit.ts +1 -1
- package/src/{VirtualShell/commands → commands}/export.ts +1 -1
- package/src/{VirtualShell/commands → commands}/grep.ts +3 -3
- package/src/{VirtualShell/commands → commands}/help.ts +1 -1
- package/src/{VirtualShell/commands → commands}/helpers.ts +1 -1
- package/src/{VirtualShell/commands → commands}/hostname.ts +1 -1
- package/src/{VirtualShell/commands → commands}/htop.ts +1 -1
- package/src/{VirtualShell/commands → commands}/index.ts +19 -110
- package/src/{VirtualShell/commands → commands}/ls.ts +7 -5
- package/src/{VirtualShell/commands → commands}/mkdir.ts +3 -3
- package/src/{VirtualShell/commands → commands}/nano.ts +4 -4
- package/src/{VirtualShell/commands → commands}/neofetch.ts +4 -4
- package/src/{VirtualShell/commands → commands}/pwd.ts +1 -1
- package/src/{VirtualShell/commands → commands}/rm.ts +3 -3
- package/src/{VirtualShell/commands → commands}/set.ts +1 -1
- package/src/{VirtualShell/commands → commands}/sh.ts +3 -14
- package/src/{VirtualShell/commands → commands}/su.ts +3 -2
- package/src/{VirtualShell/commands → commands}/sudo.ts +4 -7
- package/src/{VirtualShell/commands → commands}/touch.ts +4 -4
- package/src/{VirtualShell/commands → commands}/tree.ts +3 -3
- package/src/{VirtualShell/commands → commands}/unset.ts +1 -1
- package/src/{VirtualShell/commands → commands}/wget.ts +3 -3
- package/src/{VirtualShell/commands → commands}/who.ts +4 -4
- package/src/{VirtualShell/commands → commands}/whoami.ts +1 -1
- package/src/index.ts +6 -6
- package/src/standalone.ts +19 -14
- package/src/types/commands.ts +3 -11
- package/tests/command-helpers.test.ts +1 -1
- package/tests/helpers.test.ts +1 -1
- package/tests/parser-executor.test.ts +3 -6
- package/tests/users.test.ts +61 -1
- /package/src/{VirtualShell/commands → commands}/command-helpers.ts +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import type { ShellModule } from "
|
|
2
|
+
import type { ShellModule } from "../types/commands";
|
|
3
3
|
import { assertPathAccess, resolvePath } from "./helpers";
|
|
4
4
|
|
|
5
5
|
export const nanoCommand: ShellModule = {
|
|
6
6
|
name: "nano",
|
|
7
7
|
params: ["<file>"],
|
|
8
|
-
run: ({ authUser,
|
|
8
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
9
9
|
const fileArg = args[0];
|
|
10
10
|
if (!fileArg) {
|
|
11
11
|
return { stderr: "nano: missing file operand", exitCode: 1 };
|
|
@@ -13,8 +13,8 @@ export const nanoCommand: ShellModule = {
|
|
|
13
13
|
|
|
14
14
|
const targetPath = resolvePath(cwd, fileArg);
|
|
15
15
|
assertPathAccess(authUser, targetPath, "nano");
|
|
16
|
-
const initialContent = vfs.exists(targetPath)
|
|
17
|
-
? vfs.readFile(targetPath)
|
|
16
|
+
const initialContent = shell.vfs.exists(targetPath)
|
|
17
|
+
? shell.vfs.readFile(targetPath)
|
|
18
18
|
: "";
|
|
19
19
|
const safeName = path.posix.basename(targetPath) || "buffer";
|
|
20
20
|
const tempPath = `/tmp/sshmimic-nano-${Date.now()}-${safeName}.tmp`;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { buildNeofetchOutput } from "
|
|
2
|
-
import type { ShellModule } from "
|
|
1
|
+
import { buildNeofetchOutput } from "../../modules/neofetch";
|
|
2
|
+
import type { ShellModule } from "../types/commands";
|
|
3
3
|
import { ifFlag } from "./command-helpers";
|
|
4
4
|
import { getAllEnvVars } from "./set";
|
|
5
5
|
|
|
6
6
|
export const neofetchCommand: ShellModule = {
|
|
7
7
|
name: "neofetch",
|
|
8
8
|
params: ["[--off]"],
|
|
9
|
-
run: ({ args, authUser, hostname,
|
|
9
|
+
run: ({ args, authUser, hostname, shell }) => {
|
|
10
10
|
const env = getAllEnvVars(authUser);
|
|
11
11
|
|
|
12
12
|
if (ifFlag(args, "--help")) {
|
|
@@ -28,7 +28,7 @@ export const neofetchCommand: ShellModule = {
|
|
|
28
28
|
user: authUser,
|
|
29
29
|
host: hostname,
|
|
30
30
|
shell: env.SHELL,
|
|
31
|
-
shellProps:
|
|
31
|
+
shellProps: shell.properties,
|
|
32
32
|
terminal: env.TERM,
|
|
33
33
|
}),
|
|
34
34
|
exitCode: 0,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { ShellModule } from "
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
2
|
import { getArg, ifFlag } from "./command-helpers";
|
|
3
3
|
import { assertPathAccess, resolvePath } from "./helpers";
|
|
4
4
|
|
|
5
5
|
export const rmCommand: ShellModule = {
|
|
6
6
|
name: "rm",
|
|
7
7
|
params: ["[-r|-rf] <path>"],
|
|
8
|
-
run: ({ authUser,
|
|
8
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
9
9
|
if (args.length === 0) {
|
|
10
10
|
return { stderr: "rm: missing operand", exitCode: 1 };
|
|
11
11
|
}
|
|
@@ -27,7 +27,7 @@ export const rmCommand: ShellModule = {
|
|
|
27
27
|
for (const target of targets) {
|
|
28
28
|
const resolvedTarget = resolvePath(cwd, target);
|
|
29
29
|
assertPathAccess(authUser, resolvedTarget, "rm");
|
|
30
|
-
vfs.remove(resolvedTarget, { recursive });
|
|
30
|
+
shell.vfs.remove(resolvedTarget, { recursive });
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
return { exitCode: 0 };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** biome-ignore-all lint/style/useNamingConvention: env variables */
|
|
2
|
-
import type { ShellModule } from "
|
|
2
|
+
import type { ShellModule } from "../types/commands";
|
|
3
3
|
import { getArg } from "./command-helpers";
|
|
4
4
|
|
|
5
5
|
// Simple in-memory environment variables store
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { CommandContext, ShellModule } from "../../types/commands";
|
|
1
|
+
import type { CommandContext, ShellModule } from "../types/commands";
|
|
3
2
|
import { getArg, getFlag } from "./command-helpers";
|
|
4
3
|
import { runCommand } from "./index";
|
|
5
4
|
|
|
@@ -9,8 +8,7 @@ export const shCommand: ShellModule = {
|
|
|
9
8
|
params: ["-c <script>", "[<file>]"],
|
|
10
9
|
aliases: ["bash"],
|
|
11
10
|
run: async (ctx: CommandContext) => {
|
|
12
|
-
const {
|
|
13
|
-
|
|
11
|
+
const { args, authUser, hostname, mode, cwd } = ctx;
|
|
14
12
|
// Handle -c option: sh -c "command"
|
|
15
13
|
if (getFlag(args, "-c") && args.length >= 2) {
|
|
16
14
|
const script = getArg(args, 1) ?? "";
|
|
@@ -39,16 +37,7 @@ export const shCommand: ShellModule = {
|
|
|
39
37
|
|
|
40
38
|
// Execute the command
|
|
41
39
|
const result = await Promise.resolve(
|
|
42
|
-
runCommand(
|
|
43
|
-
command,
|
|
44
|
-
authUser,
|
|
45
|
-
hostname,
|
|
46
|
-
users,
|
|
47
|
-
mode,
|
|
48
|
-
cwd,
|
|
49
|
-
defaultShellProperties,
|
|
50
|
-
vfs,
|
|
51
|
-
),
|
|
40
|
+
runCommand(command, authUser, hostname, mode, cwd, ctx.shell),
|
|
52
41
|
);
|
|
53
42
|
|
|
54
43
|
if (result.stdout) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { ShellModule } from "
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
2
|
import { getArg } from "./command-helpers";
|
|
3
3
|
|
|
4
4
|
export const suCommand: ShellModule = {
|
|
5
5
|
name: "su",
|
|
6
6
|
params: ["- <username>"],
|
|
7
|
-
run: ({ authUser,
|
|
7
|
+
run: ({ authUser, shell, args }) => {
|
|
8
|
+
const users = shell.users!;
|
|
8
9
|
const targetUser = getArg(args, 0, { flags: ["-"] });
|
|
9
10
|
|
|
10
11
|
if (!targetUser) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { ShellModule } from "../../types/commands";
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
3
2
|
import { parseArgs } from "./command-helpers";
|
|
4
3
|
import { runCommand } from "./index";
|
|
5
4
|
|
|
@@ -23,10 +22,10 @@ function parseSudoArgs(args: string[]): {
|
|
|
23
22
|
export const sudoCommand: ShellModule = {
|
|
24
23
|
name: "sudo",
|
|
25
24
|
params: ["<command...>"],
|
|
26
|
-
run: async ({ authUser, hostname,
|
|
25
|
+
run: async ({ authUser, hostname, mode, cwd, shell, args }) => {
|
|
27
26
|
const { targetUser, loginShell, commandLine } = parseSudoArgs(args);
|
|
28
27
|
|
|
29
|
-
if (authUser !== "root" && !users.isSudoer(authUser)) {
|
|
28
|
+
if (authUser !== "root" && !shell.users.isSudoer(authUser)) {
|
|
30
29
|
return { stderr: "sudo: permission denied", exitCode: 1 };
|
|
31
30
|
}
|
|
32
31
|
|
|
@@ -50,11 +49,9 @@ export const sudoCommand: ShellModule = {
|
|
|
50
49
|
commandLine,
|
|
51
50
|
effectiveUser,
|
|
52
51
|
hostname,
|
|
53
|
-
users,
|
|
54
52
|
mode,
|
|
55
53
|
loginShell ? `/home/${effectiveUser}` : cwd,
|
|
56
|
-
|
|
57
|
-
vfs,
|
|
54
|
+
shell,
|
|
58
55
|
);
|
|
59
56
|
}
|
|
60
57
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { ShellModule } from "
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
2
|
import { assertPathAccess, resolvePath } from "./helpers";
|
|
3
3
|
|
|
4
4
|
export const touchCommand: ShellModule = {
|
|
5
5
|
name: "touch",
|
|
6
6
|
params: ["<file>"],
|
|
7
|
-
run: ({ authUser,
|
|
7
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
8
8
|
if (args.length === 0) {
|
|
9
9
|
return { stderr: "touch: missing file operand", exitCode: 1 };
|
|
10
10
|
}
|
|
@@ -12,8 +12,8 @@ export const touchCommand: ShellModule = {
|
|
|
12
12
|
for (const file of args) {
|
|
13
13
|
const target = resolvePath(cwd, file);
|
|
14
14
|
assertPathAccess(authUser, target, "touch");
|
|
15
|
-
if (!vfs.exists(target)) {
|
|
16
|
-
|
|
15
|
+
if (!shell.vfs.exists(target)) {
|
|
16
|
+
shell.writeFileAsUser(authUser, target, "");
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
return { exitCode: 0 };
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type { ShellModule } from "
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
2
|
import { getArg } from "./command-helpers";
|
|
3
3
|
import { assertPathAccess, resolvePath } from "./helpers";
|
|
4
4
|
|
|
5
5
|
export const treeCommand: ShellModule = {
|
|
6
6
|
name: "tree",
|
|
7
7
|
params: ["[path]"],
|
|
8
|
-
run: ({ authUser,
|
|
8
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
9
9
|
const target = resolvePath(cwd, getArg(args, 0) ?? cwd);
|
|
10
10
|
assertPathAccess(authUser, target, "tree");
|
|
11
|
-
return { stdout: vfs.tree(target), exitCode: 0 };
|
|
11
|
+
return { stdout: shell.vfs.tree(target), exitCode: 0 };
|
|
12
12
|
},
|
|
13
13
|
};
|
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import type { ShellModule } from "
|
|
5
|
+
import type { ShellModule } from "../types/commands";
|
|
6
6
|
import { ifFlag, parseArgs } from "./command-helpers";
|
|
7
7
|
import {
|
|
8
8
|
assertPathAccess,
|
|
@@ -83,7 +83,7 @@ function runHostWget(args: string[]): Promise<{
|
|
|
83
83
|
export const wgetCommand: ShellModule = {
|
|
84
84
|
name: "wget",
|
|
85
85
|
params: ["[url]"],
|
|
86
|
-
run: async ({ authUser,
|
|
86
|
+
run: async ({ authUser, cwd, args, shell }) => {
|
|
87
87
|
const { flagsWithValues, positionals } = parseArgs(args, {
|
|
88
88
|
flagsWithValue: ["-o", "-O", "--output", "--output-document"],
|
|
89
89
|
});
|
|
@@ -131,7 +131,7 @@ export const wgetCommand: ShellModule = {
|
|
|
131
131
|
const content = await readFile(tempFile, "utf8");
|
|
132
132
|
const target = resolvePath(cwd, outputPath || stripUrlFilename(url));
|
|
133
133
|
assertPathAccess(authUser, target, "wget");
|
|
134
|
-
|
|
134
|
+
shell.writeFileAsUser(authUser, target, content);
|
|
135
135
|
|
|
136
136
|
return {
|
|
137
137
|
stdout: `saved ${target}`,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { formatLoginDate } from "
|
|
2
|
-
import type { ShellModule } from "
|
|
1
|
+
import { formatLoginDate } from "../SSHMimic/loginFormat";
|
|
2
|
+
import type { ShellModule } from "../types/commands";
|
|
3
3
|
|
|
4
4
|
export const whoCommand: ShellModule = {
|
|
5
5
|
name: "who",
|
|
6
6
|
params: [],
|
|
7
|
-
run: ({
|
|
8
|
-
const lines = users.listActiveSessions().map((session) => {
|
|
7
|
+
run: ({ shell }) => {
|
|
8
|
+
const lines = shell.users.listActiveSessions().map((session) => {
|
|
9
9
|
const loginAt = new Date(session.startedAt);
|
|
10
10
|
const displayDate = Number.isNaN(loginAt.getTime())
|
|
11
11
|
? session.startedAt
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { SshClient } from "./
|
|
1
|
+
import { SshClient } from "./SSHClient";
|
|
2
2
|
import { SshMimic } from "./SSHMimic/index";
|
|
3
|
-
import { VirtualUserManager } from "./SSHMimic/users";
|
|
4
3
|
import VirtualFileSystem from "./VirtualFileSystem";
|
|
5
|
-
import
|
|
4
|
+
import { VirtualShell } from "./VirtualShell";
|
|
5
|
+
import { VirtualUserManager } from "./VirtualUserManager";
|
|
6
6
|
|
|
7
7
|
export type {
|
|
8
8
|
CommandContext,
|
|
@@ -32,13 +32,13 @@ export type {
|
|
|
32
32
|
export {
|
|
33
33
|
SshClient,
|
|
34
34
|
VirtualFileSystem,
|
|
35
|
-
|
|
35
|
+
VirtualShell,
|
|
36
|
+
SshMimic as VirtualSshServer,
|
|
36
37
|
VirtualUserManager,
|
|
37
|
-
type VirtualShell,
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
export {
|
|
41
41
|
getArg,
|
|
42
42
|
getFlag,
|
|
43
43
|
ifFlag,
|
|
44
|
-
} from "./
|
|
44
|
+
} from "./commands/command-helpers";
|
package/src/standalone.ts
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { VirtualShell, VirtualSshServer } from ".";
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
3
|
+
const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
|
|
4
|
+
const virtualShell = new VirtualShell(hostname);
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
virtualShell.addCommand("demo", [], () => {
|
|
7
|
+
return {
|
|
8
|
+
stdout: "This is a demo command. It does nothing useful.",
|
|
9
|
+
exitCode: 0,
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
new VirtualSshServer({
|
|
14
|
+
port: 2222,
|
|
15
|
+
hostname,
|
|
16
|
+
shell: virtualShell,
|
|
17
|
+
})
|
|
7
18
|
.start()
|
|
8
19
|
.then((port: number) => {
|
|
9
|
-
if (!sshMimic
|
|
10
|
-
else {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return {
|
|
14
|
-
stdout: "This is a demo command. It does nothing useful.",
|
|
15
|
-
exitCode: 0,
|
|
16
|
-
};
|
|
17
|
-
});
|
|
18
|
-
}
|
|
20
|
+
// if (!sshMimic) console.error("Failed to initialize SSH Mimic shell.");
|
|
21
|
+
// else {
|
|
22
|
+
console.log(`SSH Mimic initialized. Listening on port ${port}.`);
|
|
23
|
+
// }
|
|
19
24
|
})
|
|
20
25
|
.catch((error: unknown) => {
|
|
21
26
|
console.error("Failed to start SSH Mimic:", error);
|
package/src/types/commands.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
/** Command invocation mode used by shell runtime. */
|
|
2
2
|
export type CommandMode = "shell" | "exec";
|
|
3
3
|
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
VirtualUserManager,
|
|
7
|
-
} from "../SSHMimic/users";
|
|
8
|
-
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
9
|
-
import type { ShellProperties } from "../VirtualShell";
|
|
4
|
+
import type { VirtualShell } from "../VirtualShell";
|
|
5
|
+
import type { VirtualActiveSession } from "../VirtualUserManager";
|
|
10
6
|
|
|
11
7
|
/**
|
|
12
8
|
* Normalized command execution output.
|
|
@@ -67,8 +63,6 @@ export interface CommandContext {
|
|
|
67
63
|
authUser: string;
|
|
68
64
|
/** Virtual hostname shown in prompt and banners. */
|
|
69
65
|
hostname: string;
|
|
70
|
-
/** User and session manager instance. */
|
|
71
|
-
users: VirtualUserManager;
|
|
72
66
|
/** Snapshot of currently active user sessions. */
|
|
73
67
|
activeSessions: VirtualActiveSession[];
|
|
74
68
|
/** Original unparsed command line input. */
|
|
@@ -78,13 +72,11 @@ export interface CommandContext {
|
|
|
78
72
|
/** Tokenized arguments excluding command name. */
|
|
79
73
|
args: string[];
|
|
80
74
|
/** Virtual shell instance. */
|
|
81
|
-
|
|
75
|
+
shell: VirtualShell;
|
|
82
76
|
/** Optional stdin payload (used by pipes/redirections). */
|
|
83
77
|
stdin?: string;
|
|
84
78
|
/** Current working directory for command execution. */
|
|
85
79
|
cwd: string;
|
|
86
|
-
/** Virtual filesystem instance for IO operations. */
|
|
87
|
-
vfs: VirtualFileSystem;
|
|
88
80
|
}
|
|
89
81
|
|
|
90
82
|
/** Contract implemented by each shell command module. */
|
package/tests/helpers.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { assertPathAccess } from "../src/
|
|
2
|
+
import { assertPathAccess } from "../src/commands/helpers";
|
|
3
3
|
|
|
4
4
|
describe("assertPathAccess", () => {
|
|
5
5
|
test("blocks non-root access to auth store", () => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { VirtualShell } from "../src";
|
|
2
3
|
import { executePipeline } from "../src/SSHMimic/executor";
|
|
3
|
-
import { VirtualUserManager } from "../src/SSHMimic/users";
|
|
4
|
-
import VirtualFileSystem from "../src/VirtualFileSystem";
|
|
5
4
|
import { parseShellPipeline } from "../src/VirtualShell/shellParser";
|
|
6
5
|
|
|
7
6
|
describe("Pipeline parser and executor", () => {
|
|
@@ -20,18 +19,16 @@ describe("Pipeline parser and executor", () => {
|
|
|
20
19
|
});
|
|
21
20
|
|
|
22
21
|
test("executes simple pipeline", async () => {
|
|
23
|
-
const
|
|
24
|
-
const users = new VirtualUserManager(vfs, "root-pass");
|
|
22
|
+
const shell = new VirtualShell("localhost");
|
|
25
23
|
const pipeline = parseShellPipeline("echo hello | grep h");
|
|
26
24
|
|
|
27
25
|
const result = await executePipeline(
|
|
28
26
|
pipeline,
|
|
29
27
|
"root",
|
|
30
28
|
"localhost",
|
|
31
|
-
users,
|
|
32
29
|
"shell",
|
|
33
30
|
"/",
|
|
34
|
-
|
|
31
|
+
shell
|
|
35
32
|
);
|
|
36
33
|
|
|
37
34
|
expect(result.exitCode).toBe(0);
|
package/tests/users.test.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { VirtualUserManager } from "../src/SSHMimic/users";
|
|
6
5
|
import VirtualFileSystem from "../src/VirtualFileSystem";
|
|
6
|
+
import { VirtualUserManager } from "../src/VirtualUserManager";
|
|
7
7
|
|
|
8
8
|
async function withTempVfs(
|
|
9
9
|
run: (vfs: VirtualFileSystem) => Promise<void>,
|
|
@@ -39,3 +39,63 @@ describe("VirtualUserManager auto sudo", () => {
|
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
describe("VirtualUserManager quotas", () => {
|
|
44
|
+
test("enforces quota for writes inside user home", async () => {
|
|
45
|
+
await withTempVfs(async (vfs) => {
|
|
46
|
+
const users = new VirtualUserManager(vfs, "root-pass");
|
|
47
|
+
await users.initialize();
|
|
48
|
+
await users.addUser("alice", "alice-pass");
|
|
49
|
+
const startingUsage = users.getUsageBytes("alice");
|
|
50
|
+
await users.setQuotaBytes("alice", startingUsage + 5);
|
|
51
|
+
|
|
52
|
+
expect(() => {
|
|
53
|
+
users.assertWriteWithinQuota("alice", "/home/alice/note.txt", "hello");
|
|
54
|
+
}).not.toThrow();
|
|
55
|
+
|
|
56
|
+
vfs.writeFile("/home/alice/note.txt", "hello");
|
|
57
|
+
|
|
58
|
+
expect(() => {
|
|
59
|
+
users.assertWriteWithinQuota(
|
|
60
|
+
"alice",
|
|
61
|
+
"/home/alice/note.txt",
|
|
62
|
+
"this exceeds the configured quota",
|
|
63
|
+
);
|
|
64
|
+
}).toThrow("quota exceeded for 'alice'");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("does not enforce home quota outside user home", async () => {
|
|
69
|
+
await withTempVfs(async (vfs) => {
|
|
70
|
+
const users = new VirtualUserManager(vfs, "root-pass");
|
|
71
|
+
await users.initialize();
|
|
72
|
+
await users.addUser("bob", "bob-pass");
|
|
73
|
+
await users.setQuotaBytes("bob", 1);
|
|
74
|
+
|
|
75
|
+
expect(() => {
|
|
76
|
+
users.assertWriteWithinQuota("bob", "/tmp/shared.txt", "large-content");
|
|
77
|
+
}).not.toThrow();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("clearQuota removes enforced limit", async () => {
|
|
82
|
+
await withTempVfs(async (vfs) => {
|
|
83
|
+
const users = new VirtualUserManager(vfs, "root-pass");
|
|
84
|
+
await users.initialize();
|
|
85
|
+
await users.addUser("charlie", "charlie-pass");
|
|
86
|
+
await users.setQuotaBytes("charlie", 2);
|
|
87
|
+
|
|
88
|
+
expect(users.getQuotaBytes("charlie")).toBe(2);
|
|
89
|
+
await users.clearQuota("charlie");
|
|
90
|
+
expect(users.getQuotaBytes("charlie")).toBeNull();
|
|
91
|
+
|
|
92
|
+
expect(() => {
|
|
93
|
+
users.assertWriteWithinQuota(
|
|
94
|
+
"charlie",
|
|
95
|
+
"/home/charlie/file.txt",
|
|
96
|
+
"long-content",
|
|
97
|
+
);
|
|
98
|
+
}).not.toThrow();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
File without changes
|