typescript-virtual-container 1.0.8 → 1.1.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/README.md +138 -87
- package/package.json +1 -1
- package/src/SSHMimic/client.ts +15 -18
- package/src/SSHMimic/exec.ts +5 -16
- package/src/SSHMimic/executor.ts +18 -29
- package/src/SSHMimic/index.ts +23 -85
- package/src/VirtualFileSystem/index.ts +0 -1
- package/src/VirtualShell/commands/adduser.ts +2 -2
- package/src/VirtualShell/commands/cat.ts +3 -3
- package/src/VirtualShell/commands/cd.ts +2 -2
- package/src/VirtualShell/commands/curl.ts +2 -2
- package/src/VirtualShell/commands/deluser.ts +2 -2
- package/src/VirtualShell/commands/grep.ts +2 -2
- package/src/VirtualShell/commands/index.ts +13 -107
- package/src/VirtualShell/commands/ls.ts +6 -4
- package/src/VirtualShell/commands/mkdir.ts +2 -2
- package/src/VirtualShell/commands/nano.ts +3 -3
- package/src/VirtualShell/commands/neofetch.ts +2 -2
- package/src/VirtualShell/commands/rm.ts +2 -2
- package/src/VirtualShell/commands/sh.ts +2 -13
- package/src/VirtualShell/commands/su.ts +2 -1
- package/src/VirtualShell/commands/sudo.ts +3 -6
- package/src/VirtualShell/commands/touch.ts +3 -3
- package/src/VirtualShell/commands/tree.ts +2 -2
- package/src/VirtualShell/commands/wget.ts +2 -2
- package/src/VirtualShell/commands/who.ts +2 -2
- package/src/VirtualShell/index.ts +114 -25
- package/src/VirtualShell/shell.ts +25 -35
- package/src/{SSHMimic/users.ts → VirtualUserManager/index.ts} +6 -3
- package/src/index.ts +4 -4
- package/src/standalone.ts +19 -14
- package/src/types/commands.ts +3 -11
- package/tests/parser-executor.test.ts +3 -6
- package/tests/users.test.ts +1 -1
package/src/SSHMimic/executor.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { CommandMode, CommandResult } from "../types/commands";
|
|
2
2
|
import type { Pipeline, PipelineCommand } from "../types/pipeline";
|
|
3
|
-
import type
|
|
4
|
-
import { defaultShellProperties } from "../VirtualShell";
|
|
3
|
+
import type { VirtualShell } from "../VirtualShell";
|
|
5
4
|
import { runCommand as runSingleCommand } from "../VirtualShell/commands";
|
|
6
5
|
import { resolvePath } from "../VirtualShell/commands/helpers";
|
|
7
|
-
import type { VirtualUserManager } from "./users";
|
|
8
6
|
|
|
9
7
|
/**
|
|
10
8
|
* Execute a parsed pipeline, chaining commands and handling redirections.
|
|
@@ -14,10 +12,9 @@ export async function executePipeline(
|
|
|
14
12
|
pipeline: Pipeline,
|
|
15
13
|
authUser: string,
|
|
16
14
|
hostname: string,
|
|
17
|
-
users: VirtualUserManager,
|
|
18
15
|
mode: CommandMode,
|
|
19
16
|
cwd: string,
|
|
20
|
-
|
|
17
|
+
shell: VirtualShell,
|
|
21
18
|
): Promise<CommandResult> {
|
|
22
19
|
if (pipeline.commands.length === 0) {
|
|
23
20
|
return { exitCode: 0 };
|
|
@@ -29,10 +26,9 @@ export async function executePipeline(
|
|
|
29
26
|
pipeline.commands[0] as PipelineCommand,
|
|
30
27
|
authUser,
|
|
31
28
|
hostname,
|
|
32
|
-
users,
|
|
33
29
|
mode,
|
|
34
30
|
cwd,
|
|
35
|
-
|
|
31
|
+
shell,
|
|
36
32
|
);
|
|
37
33
|
}
|
|
38
34
|
|
|
@@ -41,10 +37,9 @@ export async function executePipeline(
|
|
|
41
37
|
pipeline.commands as PipelineCommand[],
|
|
42
38
|
authUser,
|
|
43
39
|
hostname,
|
|
44
|
-
users,
|
|
45
40
|
mode,
|
|
46
41
|
cwd,
|
|
47
|
-
|
|
42
|
+
shell,
|
|
48
43
|
);
|
|
49
44
|
}
|
|
50
45
|
|
|
@@ -55,17 +50,16 @@ async function executeSingleCommandWithRedirections(
|
|
|
55
50
|
cmd: PipelineCommand,
|
|
56
51
|
authUser: string,
|
|
57
52
|
hostname: string,
|
|
58
|
-
users: VirtualUserManager,
|
|
59
53
|
mode: CommandMode,
|
|
60
54
|
cwd: string,
|
|
61
|
-
|
|
55
|
+
shell: VirtualShell,
|
|
62
56
|
): Promise<CommandResult> {
|
|
63
57
|
// Prepare input if input file specified
|
|
64
58
|
let stdin: string | undefined;
|
|
65
59
|
if (cmd.inputFile) {
|
|
66
60
|
const inputPath = resolvePath(cwd, cmd.inputFile);
|
|
67
61
|
try {
|
|
68
|
-
stdin = vfs.readFile(inputPath);
|
|
62
|
+
stdin = shell.vfs.readFile(inputPath);
|
|
69
63
|
} catch {
|
|
70
64
|
return {
|
|
71
65
|
stderr: `cat: ${cmd.inputFile}: No such file or directory`,
|
|
@@ -82,11 +76,9 @@ async function executeSingleCommandWithRedirections(
|
|
|
82
76
|
rawInput,
|
|
83
77
|
authUser,
|
|
84
78
|
hostname,
|
|
85
|
-
users,
|
|
86
79
|
mode,
|
|
87
80
|
cwd,
|
|
88
|
-
|
|
89
|
-
vfs,
|
|
81
|
+
shell,
|
|
90
82
|
stdin,
|
|
91
83
|
);
|
|
92
84
|
|
|
@@ -97,13 +89,13 @@ async function executeSingleCommandWithRedirections(
|
|
|
97
89
|
try {
|
|
98
90
|
if (cmd.appendOutput) {
|
|
99
91
|
try {
|
|
100
|
-
const existing = vfs.readFile(outputPath);
|
|
101
|
-
vfs.writeFile(outputPath, existing + output);
|
|
92
|
+
const existing = shell.vfs.readFile(outputPath);
|
|
93
|
+
shell.vfs.writeFile(outputPath, existing + output);
|
|
102
94
|
} catch {
|
|
103
|
-
vfs.writeFile(outputPath, output);
|
|
95
|
+
shell.vfs.writeFile(outputPath, output);
|
|
104
96
|
}
|
|
105
97
|
} else {
|
|
106
|
-
vfs.writeFile(outputPath, output);
|
|
98
|
+
shell.vfs.writeFile(outputPath, output);
|
|
107
99
|
}
|
|
108
100
|
return { ...result, stdout: "" };
|
|
109
101
|
} catch {
|
|
@@ -125,10 +117,9 @@ async function executePipelineChain(
|
|
|
125
117
|
commands: PipelineCommand[],
|
|
126
118
|
authUser: string,
|
|
127
119
|
hostname: string,
|
|
128
|
-
users: VirtualUserManager,
|
|
129
120
|
mode: CommandMode,
|
|
130
121
|
cwd: string,
|
|
131
|
-
|
|
122
|
+
shell: VirtualShell,
|
|
132
123
|
): Promise<CommandResult> {
|
|
133
124
|
let currentOutput = "";
|
|
134
125
|
let exitCode = 0;
|
|
@@ -140,7 +131,7 @@ async function executePipelineChain(
|
|
|
140
131
|
if (i === 0 && cmd.inputFile) {
|
|
141
132
|
const inputPath = resolvePath(cwd, cmd.inputFile);
|
|
142
133
|
try {
|
|
143
|
-
currentOutput = vfs.readFile(inputPath);
|
|
134
|
+
currentOutput = shell.vfs.readFile(inputPath);
|
|
144
135
|
} catch {
|
|
145
136
|
return {
|
|
146
137
|
stderr: `cat: ${cmd.inputFile}: No such file or directory`,
|
|
@@ -158,11 +149,9 @@ async function executePipelineChain(
|
|
|
158
149
|
rawInput,
|
|
159
150
|
authUser,
|
|
160
151
|
hostname,
|
|
161
|
-
users,
|
|
162
152
|
mode,
|
|
163
153
|
cwd,
|
|
164
|
-
|
|
165
|
-
vfs,
|
|
154
|
+
shell,
|
|
166
155
|
currentOutput,
|
|
167
156
|
);
|
|
168
157
|
|
|
@@ -175,13 +164,13 @@ async function executePipelineChain(
|
|
|
175
164
|
try {
|
|
176
165
|
if (cmd.appendOutput) {
|
|
177
166
|
try {
|
|
178
|
-
const existing = vfs.readFile(outputPath);
|
|
179
|
-
vfs.writeFile(outputPath, existing + output);
|
|
167
|
+
const existing = shell.vfs.readFile(outputPath);
|
|
168
|
+
shell.vfs.writeFile(outputPath, existing + output);
|
|
180
169
|
} catch {
|
|
181
|
-
vfs.writeFile(outputPath, output);
|
|
170
|
+
shell.vfs.writeFile(outputPath, output);
|
|
182
171
|
}
|
|
183
172
|
} else {
|
|
184
|
-
vfs.writeFile(outputPath, output);
|
|
173
|
+
shell.vfs.writeFile(outputPath, output);
|
|
185
174
|
}
|
|
186
175
|
currentOutput = "";
|
|
187
176
|
} catch {
|
package/src/SSHMimic/index.ts
CHANGED
|
@@ -1,67 +1,40 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
1
|
import { Server as SshServer } from "ssh2";
|
|
3
|
-
import VirtualFileSystem from "../VirtualFileSystem";
|
|
4
2
|
import { VirtualShell } from "../VirtualShell";
|
|
5
3
|
import { loadOrCreateHostKey } from "./hostKey";
|
|
6
|
-
import { VirtualUserManager } from "./users";
|
|
7
|
-
|
|
8
|
-
function resolveRootPassword(): string {
|
|
9
|
-
const configured = process.env.SSH_MIMIC_ROOT_PASSWORD;
|
|
10
|
-
if (configured && configured.trim().length > 0) {
|
|
11
|
-
return configured;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const generated = randomBytes(18).toString("base64url");
|
|
15
|
-
console.warn(
|
|
16
|
-
`[ssh-mimic] SSH_MIMIC_ROOT_PASSWORD missing; generated ephemeral root password: ${generated}`,
|
|
17
|
-
);
|
|
18
|
-
return generated;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function resolveAutoSudoForNewUsers(): boolean {
|
|
22
|
-
const configured = process.env.SSH_MIMIC_AUTO_SUDO_NEW_USERS;
|
|
23
|
-
if (!configured) {
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return !["0", "false", "no", "off"].includes(configured.toLowerCase());
|
|
28
|
-
}
|
|
29
4
|
|
|
30
5
|
/**
|
|
31
|
-
* SSH server
|
|
6
|
+
* SSH server facade that wires the virtual shell runtime into ssh2 sessions.
|
|
32
7
|
*
|
|
8
|
+
* This class is exported as `VirtualSshServer` for public API compatibility.
|
|
33
9
|
* Create an instance, call {@link SshMimic.start}, and stop it with
|
|
34
10
|
* {@link SshMimic.stop} when your process exits.
|
|
35
11
|
*/
|
|
36
12
|
class SshMimic {
|
|
37
13
|
port: number;
|
|
38
|
-
hostname: string;
|
|
39
14
|
server: SshServer | null;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
shell: VirtualShell | null = null;
|
|
43
|
-
basePath: string = ".";
|
|
15
|
+
private shell: VirtualShell;
|
|
16
|
+
private shellHostname: string;
|
|
44
17
|
|
|
45
18
|
/**
|
|
46
19
|
* Creates a new SSH mimic server instance.
|
|
47
20
|
*
|
|
48
21
|
* @param port TCP port to bind on localhost.
|
|
49
|
-
* @param hostname SSH ident
|
|
50
|
-
* @param
|
|
22
|
+
* @param hostname Virtual hostname used for the SSH ident and default shell label.
|
|
23
|
+
* @param shell Optional preconfigured virtual shell instance to reuse.
|
|
51
24
|
*/
|
|
52
25
|
constructor({
|
|
53
26
|
port,
|
|
54
27
|
hostname = "typescript-vm",
|
|
55
|
-
|
|
28
|
+
shell = new VirtualShell(hostname),
|
|
56
29
|
}: {
|
|
57
30
|
port: number;
|
|
58
31
|
hostname?: string;
|
|
59
|
-
|
|
32
|
+
shell?: VirtualShell;
|
|
60
33
|
}) {
|
|
61
34
|
this.port = port;
|
|
62
|
-
this.
|
|
63
|
-
this.basePath = basePath;
|
|
35
|
+
this.shellHostname = hostname;
|
|
64
36
|
this.server = null;
|
|
37
|
+
this.shell = shell;
|
|
65
38
|
}
|
|
66
39
|
|
|
67
40
|
/**
|
|
@@ -70,22 +43,13 @@ class SshMimic {
|
|
|
70
43
|
* @returns Promise resolved with bound listening port.
|
|
71
44
|
*/
|
|
72
45
|
public async start(): Promise<number> {
|
|
46
|
+
const shell = this.shell;
|
|
73
47
|
const privateKey = loadOrCreateHostKey();
|
|
74
|
-
this.vfs = new VirtualFileSystem(this.basePath);
|
|
75
|
-
await this.vfs.restoreMirror();
|
|
76
|
-
this.users = new VirtualUserManager(
|
|
77
|
-
this.vfs,
|
|
78
|
-
resolveRootPassword(),
|
|
79
|
-
resolveAutoSudoForNewUsers(),
|
|
80
|
-
);
|
|
81
|
-
await this.users.initialize();
|
|
82
|
-
|
|
83
|
-
this.shell = new VirtualShell(this.vfs, this.users, this.hostname);
|
|
84
48
|
|
|
85
49
|
this.server = new SshServer(
|
|
86
50
|
{
|
|
87
51
|
hostKeys: [privateKey],
|
|
88
|
-
ident: `SSH-2.0-${
|
|
52
|
+
ident: `SSH-2.0-${shell.hostname}`,
|
|
89
53
|
},
|
|
90
54
|
(client) => {
|
|
91
55
|
let authUser = "root";
|
|
@@ -93,28 +57,29 @@ class SshMimic {
|
|
|
93
57
|
let sessionId: string | null = null;
|
|
94
58
|
|
|
95
59
|
client.on("authentication", (ctx) => {
|
|
60
|
+
shell;
|
|
96
61
|
if (ctx.method === "password") {
|
|
97
62
|
const candidateUser = ctx.username || "root";
|
|
98
63
|
remoteAddress = (ctx as { ip?: string }).ip ?? remoteAddress;
|
|
99
64
|
|
|
100
65
|
if (
|
|
101
|
-
!
|
|
66
|
+
!shell.users.verifyPassword(candidateUser, ctx.password ?? "")
|
|
102
67
|
) {
|
|
103
68
|
ctx.reject();
|
|
104
69
|
return;
|
|
105
70
|
}
|
|
106
71
|
|
|
107
72
|
authUser = candidateUser;
|
|
108
|
-
sessionId =
|
|
73
|
+
sessionId = shell.users.registerSession(authUser, remoteAddress).id;
|
|
109
74
|
|
|
110
75
|
const homePath = `/home/${authUser}`;
|
|
111
|
-
if (!
|
|
112
|
-
|
|
113
|
-
|
|
76
|
+
if (!shell.vfs.exists(homePath)) {
|
|
77
|
+
shell.vfs.mkdir(homePath, 0o755);
|
|
78
|
+
shell.vfs.writeFile(
|
|
114
79
|
`${homePath}/README.txt`,
|
|
115
|
-
`Welcome to ${this.
|
|
80
|
+
`Welcome to ${shell?.hostname ?? this.shellHostname}`,
|
|
116
81
|
);
|
|
117
|
-
void
|
|
82
|
+
void shell.vfs.flushMirror();
|
|
118
83
|
}
|
|
119
84
|
|
|
120
85
|
ctx.accept();
|
|
@@ -125,7 +90,7 @@ class SshMimic {
|
|
|
125
90
|
});
|
|
126
91
|
|
|
127
92
|
client.on("close", () => {
|
|
128
|
-
|
|
93
|
+
shell.users.unregisterSession(sessionId);
|
|
129
94
|
sessionId = null;
|
|
130
95
|
});
|
|
131
96
|
|
|
@@ -150,7 +115,7 @@ class SshMimic {
|
|
|
150
115
|
|
|
151
116
|
session.on("shell", (acceptShell) => {
|
|
152
117
|
const stream = acceptShell();
|
|
153
|
-
|
|
118
|
+
shell?.startInteractiveSession(
|
|
154
119
|
stream,
|
|
155
120
|
authUser,
|
|
156
121
|
sessionId,
|
|
@@ -161,7 +126,7 @@ class SshMimic {
|
|
|
161
126
|
|
|
162
127
|
session.on("exec", (acceptExec, _rejectExec, info) => {
|
|
163
128
|
const _stream = acceptExec();
|
|
164
|
-
|
|
129
|
+
shell?.executeCommand(
|
|
165
130
|
info.command.trim(),
|
|
166
131
|
authUser,
|
|
167
132
|
`/home/${authUser}`,
|
|
@@ -191,33 +156,6 @@ class SshMimic {
|
|
|
191
156
|
});
|
|
192
157
|
}
|
|
193
158
|
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Returns virtual filesystem instance after server started.
|
|
197
|
-
*
|
|
198
|
-
* @returns VirtualFileSystem or null when not started.
|
|
199
|
-
*/
|
|
200
|
-
public getVfs(): VirtualFileSystem | null {
|
|
201
|
-
return this.vfs;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Returns user manager instance after server started.
|
|
206
|
-
*
|
|
207
|
-
* @returns VirtualUserManager or null when not started.
|
|
208
|
-
*/
|
|
209
|
-
public getUsers(): VirtualUserManager | null {
|
|
210
|
-
return this.users;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Returns hostname shown in prompts and idents.
|
|
215
|
-
*
|
|
216
|
-
* @returns Configured hostname label.
|
|
217
|
-
*/
|
|
218
|
-
public getHostname(): string {
|
|
219
|
-
return this.hostname;
|
|
220
|
-
}
|
|
221
159
|
}
|
|
222
160
|
|
|
223
161
|
export { SshMimic };
|
|
@@ -49,7 +49,6 @@ class VirtualFileSystem {
|
|
|
49
49
|
*/
|
|
50
50
|
public async restoreMirror(): Promise<void> {
|
|
51
51
|
await fs.mkdir(path.dirname(this.archivePath), { recursive: true });
|
|
52
|
-
|
|
53
52
|
try {
|
|
54
53
|
const compressed = await fs.readFile(this.archivePath);
|
|
55
54
|
const tarBuffer = gunzipSync(compressed);
|
|
@@ -3,7 +3,7 @@ import type { ShellModule } from "../../types/commands";
|
|
|
3
3
|
export const adduserCommand: ShellModule = {
|
|
4
4
|
name: "adduser",
|
|
5
5
|
params: ["<username> <password>"],
|
|
6
|
-
run: async ({ authUser,
|
|
6
|
+
run: async ({ authUser, shell, args }) => {
|
|
7
7
|
if (authUser !== "root") {
|
|
8
8
|
return { stderr: "adduser: permission denied", exitCode: 1 };
|
|
9
9
|
}
|
|
@@ -16,7 +16,7 @@ export const adduserCommand: ShellModule = {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
await users.addUser(username, password);
|
|
19
|
+
await shell.users.addUser(username, password);
|
|
20
20
|
return { stdout: `adduser: user '${username}' created`, exitCode: 0 };
|
|
21
21
|
},
|
|
22
22
|
};
|
|
@@ -5,14 +5,14 @@ import { assertPathAccess, resolveReadablePath } from "./helpers";
|
|
|
5
5
|
export const catCommand: ShellModule = {
|
|
6
6
|
name: "cat",
|
|
7
7
|
params: ["<file>"],
|
|
8
|
-
run: ({ authUser,
|
|
8
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
9
9
|
const fileArg = getArg(args, 0);
|
|
10
10
|
if (!fileArg) {
|
|
11
11
|
return { stderr: "cat: missing file operand", exitCode: 1 };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const target = resolveReadablePath(vfs, cwd, fileArg);
|
|
14
|
+
const target = resolveReadablePath(shell.vfs, cwd, fileArg);
|
|
15
15
|
assertPathAccess(authUser, target, "cat");
|
|
16
|
-
return { stdout: vfs.readFile(target), exitCode: 0 };
|
|
16
|
+
return { stdout: shell.vfs.readFile(target), exitCode: 0 };
|
|
17
17
|
},
|
|
18
18
|
};
|
|
@@ -4,10 +4,10 @@ import { assertPathAccess, resolvePath } from "./helpers";
|
|
|
4
4
|
export const cdCommand: ShellModule = {
|
|
5
5
|
name: "cd",
|
|
6
6
|
params: ["[path]"],
|
|
7
|
-
run: ({ authUser,
|
|
7
|
+
run: ({ authUser, shell, cwd, args, mode }) => {
|
|
8
8
|
const target = resolvePath(cwd, args[0] ?? "/virtual-env-js");
|
|
9
9
|
assertPathAccess(authUser, target, "cd");
|
|
10
|
-
const stats = vfs.stat(target);
|
|
10
|
+
const stats = shell.vfs.stat(target);
|
|
11
11
|
if (stats.type !== "directory") {
|
|
12
12
|
return { stderr: `cd: not a directory: ${target}`, exitCode: 1 };
|
|
13
13
|
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
export const curlCommand: ShellModule = {
|
|
11
11
|
name: "curl",
|
|
12
12
|
params: ["[-o file] <url>"],
|
|
13
|
-
run: async ({ authUser,
|
|
13
|
+
run: async ({ authUser, cwd, args, shell }) => {
|
|
14
14
|
const { flagsWithValues, positionals } = parseArgs(args, {
|
|
15
15
|
flagsWithValue: ["-o", "--output"],
|
|
16
16
|
});
|
|
@@ -39,7 +39,7 @@ export const curlCommand: ShellModule = {
|
|
|
39
39
|
if (outputPath) {
|
|
40
40
|
const target = resolvePath(cwd, outputPath);
|
|
41
41
|
assertPathAccess(authUser, target, "curl");
|
|
42
|
-
vfs.writeFile(target, result.stdout);
|
|
42
|
+
shell.vfs.writeFile(target, result.stdout);
|
|
43
43
|
return {
|
|
44
44
|
stderr: result.stderr
|
|
45
45
|
? normalizeTerminalOutput(result.stderr)
|
|
@@ -3,7 +3,7 @@ import type { ShellModule } from "../../types/commands";
|
|
|
3
3
|
export const deluserCommand: ShellModule = {
|
|
4
4
|
name: "deluser",
|
|
5
5
|
params: ["<username>"],
|
|
6
|
-
run: async ({ authUser,
|
|
6
|
+
run: async ({ authUser, args, shell }) => {
|
|
7
7
|
if (authUser !== "root") {
|
|
8
8
|
return { stderr: "deluser: permission denied", exitCode: 1 };
|
|
9
9
|
}
|
|
@@ -13,7 +13,7 @@ export const deluserCommand: ShellModule = {
|
|
|
13
13
|
return { stderr: "deluser: usage: deluser <username>", exitCode: 1 };
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
await users.deleteUser(username);
|
|
16
|
+
await shell.users.deleteUser(username);
|
|
17
17
|
return { stdout: `deluser: user '${username}' deleted`, exitCode: 0 };
|
|
18
18
|
},
|
|
19
19
|
};
|
|
@@ -5,7 +5,7 @@ import { assertPathAccess, resolvePath } from "./helpers";
|
|
|
5
5
|
export const grepCommand: ShellModule = {
|
|
6
6
|
name: "grep",
|
|
7
7
|
params: ["[-i] [-v] <pattern> [file...]"],
|
|
8
|
-
run: ({ authUser,
|
|
8
|
+
run: ({ authUser, shell, cwd, args, stdin }) => {
|
|
9
9
|
const { flags, positionals } = parseArgs(args, { flags: ["-i", "-v"] });
|
|
10
10
|
const caseInsensitive = flags.has("-i");
|
|
11
11
|
const invertMatch = flags.has("-v");
|
|
@@ -52,7 +52,7 @@ export const grepCommand: ShellModule = {
|
|
|
52
52
|
const target = resolvePath(cwd, file);
|
|
53
53
|
try {
|
|
54
54
|
assertPathAccess(authUser, target, "grep");
|
|
55
|
-
const content = vfs.readFile(target);
|
|
55
|
+
const content = shell.vfs.readFile(target);
|
|
56
56
|
const lines = content.split("\n");
|
|
57
57
|
|
|
58
58
|
for (const line of lines) {
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { VirtualUserManager } from "../../SSHMimic/users";
|
|
1
|
+
import type { VirtualShell } from "..";
|
|
3
2
|
import type {
|
|
4
3
|
CommandContext,
|
|
5
4
|
CommandMode,
|
|
6
5
|
CommandResult,
|
|
7
6
|
ShellModule,
|
|
8
7
|
} from "../../types/commands";
|
|
9
|
-
import type VirtualFileSystem from "../../VirtualFileSystem";
|
|
10
8
|
import { adduserCommand } from "./adduser";
|
|
11
9
|
import { catCommand } from "./cat";
|
|
12
10
|
import { cdCommand } from "./cd";
|
|
@@ -79,12 +77,7 @@ const helpCommand = createHelpCommand(() =>
|
|
|
79
77
|
const commandRegistry = new Map<string, ShellModule>();
|
|
80
78
|
let cachedCommandNames: string[] | null = null;
|
|
81
79
|
|
|
82
|
-
function invalidateCache(): void {
|
|
83
|
-
cachedCommandNames = null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
80
|
function buildCache(): void {
|
|
87
|
-
commandRegistry.clear();
|
|
88
81
|
for (const mod of getCommandModules()) {
|
|
89
82
|
commandRegistry.set(mod.name, mod);
|
|
90
83
|
for (const alias of mod.aliases ?? []) {
|
|
@@ -95,20 +88,16 @@ function buildCache(): void {
|
|
|
95
88
|
}
|
|
96
89
|
|
|
97
90
|
function getCommandModules(): ShellModule[] {
|
|
91
|
+
// console.log("Loading command modules...");
|
|
92
|
+
// console.log(
|
|
93
|
+
// `Base commands: ${BASE_COMMANDS.map((cmd) => cmd.name).join(", ")}`,
|
|
94
|
+
// );
|
|
95
|
+
// console.log(
|
|
96
|
+
// `Custom commands: ${customCommands.map((cmd) => cmd.name).join(", ")}`,
|
|
97
|
+
// );
|
|
98
98
|
return [...BASE_COMMANDS, ...customCommands, helpCommand];
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function _getTakenCommandNames(modules: ShellModule[]): Set<string> {
|
|
102
|
-
const taken = new Set<string>();
|
|
103
|
-
for (const mod of modules) {
|
|
104
|
-
taken.add(mod.name);
|
|
105
|
-
for (const alias of mod.aliases ?? []) {
|
|
106
|
-
taken.add(alias);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return taken;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
101
|
export function registerCommand(module: ShellModule): void {
|
|
113
102
|
const normalized: ShellModule = {
|
|
114
103
|
...module,
|
|
@@ -130,7 +119,7 @@ export function registerCommand(module: ShellModule): void {
|
|
|
130
119
|
commandRegistry.set(name, normalized);
|
|
131
120
|
}
|
|
132
121
|
|
|
133
|
-
|
|
122
|
+
buildCache();
|
|
134
123
|
}
|
|
135
124
|
|
|
136
125
|
export function createCustomCommand(
|
|
@@ -207,94 +196,14 @@ function parseInput(rawInput: string): { commandName: string; args: string[] } {
|
|
|
207
196
|
}
|
|
208
197
|
|
|
209
198
|
// Internal async function for pipeline execution
|
|
210
|
-
async function _runCommandInternal(
|
|
211
|
-
rawInput: string,
|
|
212
|
-
authUser: string,
|
|
213
|
-
hostname: string,
|
|
214
|
-
users: VirtualUserManager,
|
|
215
|
-
mode: CommandMode,
|
|
216
|
-
cwd: string,
|
|
217
|
-
shellProps: ShellProperties,
|
|
218
|
-
vfs: VirtualFileSystem,
|
|
219
|
-
stdin?: string,
|
|
220
|
-
): Promise<CommandResult> {
|
|
221
|
-
// Check if input contains pipes or redirections
|
|
222
|
-
if (
|
|
223
|
-
rawInput.includes("|") ||
|
|
224
|
-
rawInput.includes(">") ||
|
|
225
|
-
rawInput.includes("<")
|
|
226
|
-
) {
|
|
227
|
-
// Use pipeline executor
|
|
228
|
-
const { parseShellPipeline } = await import("../shellParser");
|
|
229
|
-
const { executePipeline } = await import("../../SSHMimic/executor");
|
|
230
|
-
|
|
231
|
-
const pipeline = parseShellPipeline(rawInput);
|
|
232
|
-
if (!pipeline.isValid) {
|
|
233
|
-
return {
|
|
234
|
-
stderr: pipeline.error || "Syntax error",
|
|
235
|
-
exitCode: 1,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
return await executePipeline(
|
|
241
|
-
pipeline,
|
|
242
|
-
authUser,
|
|
243
|
-
hostname,
|
|
244
|
-
users,
|
|
245
|
-
mode,
|
|
246
|
-
cwd,
|
|
247
|
-
vfs,
|
|
248
|
-
);
|
|
249
|
-
} catch (error: unknown) {
|
|
250
|
-
const message =
|
|
251
|
-
error instanceof Error ? error.message : "Pipeline execution failed";
|
|
252
|
-
return { stderr: message, exitCode: 1 };
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Regular command execution
|
|
257
|
-
const { commandName, args } = parseInput(rawInput);
|
|
258
|
-
const mod = resolveModule(commandName);
|
|
259
|
-
|
|
260
|
-
if (!mod) {
|
|
261
|
-
return {
|
|
262
|
-
stderr: `Command '${rawInput}' not found`,
|
|
263
|
-
exitCode: 127,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
try {
|
|
268
|
-
const result = mod.run({
|
|
269
|
-
authUser,
|
|
270
|
-
hostname,
|
|
271
|
-
users,
|
|
272
|
-
activeSessions: users.listActiveSessions(),
|
|
273
|
-
rawInput,
|
|
274
|
-
mode,
|
|
275
|
-
args,
|
|
276
|
-
shellProps,
|
|
277
|
-
stdin,
|
|
278
|
-
cwd,
|
|
279
|
-
vfs,
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
return await Promise.resolve(result);
|
|
283
|
-
} catch (error: unknown) {
|
|
284
|
-
const message = error instanceof Error ? error.message : "Command failed";
|
|
285
|
-
return { stderr: message, exitCode: 1 };
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
199
|
|
|
289
200
|
export async function runCommand(
|
|
290
201
|
rawInput: string,
|
|
291
202
|
authUser: string,
|
|
292
203
|
hostname: string,
|
|
293
|
-
users: VirtualUserManager,
|
|
294
204
|
mode: CommandMode,
|
|
295
205
|
cwd: string,
|
|
296
|
-
|
|
297
|
-
vfs: VirtualFileSystem,
|
|
206
|
+
shell: VirtualShell,
|
|
298
207
|
stdin?: string,
|
|
299
208
|
): Promise<CommandResult> {
|
|
300
209
|
const trimmed = rawInput.trim();
|
|
@@ -320,10 +229,9 @@ export async function runCommand(
|
|
|
320
229
|
pipeline,
|
|
321
230
|
authUser,
|
|
322
231
|
hostname,
|
|
323
|
-
users,
|
|
324
232
|
mode,
|
|
325
233
|
cwd,
|
|
326
|
-
|
|
234
|
+
shell,
|
|
327
235
|
);
|
|
328
236
|
} catch (error: unknown) {
|
|
329
237
|
const message =
|
|
@@ -346,15 +254,13 @@ export async function runCommand(
|
|
|
346
254
|
return await mod.run({
|
|
347
255
|
authUser,
|
|
348
256
|
hostname,
|
|
349
|
-
users,
|
|
350
|
-
activeSessions: users.listActiveSessions(),
|
|
257
|
+
activeSessions: shell.users.listActiveSessions(),
|
|
351
258
|
rawInput: trimmed,
|
|
352
259
|
mode,
|
|
353
260
|
args,
|
|
354
261
|
stdin,
|
|
355
262
|
cwd,
|
|
356
|
-
|
|
357
|
-
shellProps,
|
|
263
|
+
shell,
|
|
358
264
|
});
|
|
359
265
|
} catch (error: unknown) {
|
|
360
266
|
const message = error instanceof Error ? error.message : "Command failed";
|
|
@@ -29,22 +29,24 @@ function formatDate(date: Date): string {
|
|
|
29
29
|
export const lsCommand: ShellModule = {
|
|
30
30
|
name: "ls",
|
|
31
31
|
params: ["[path]"],
|
|
32
|
-
run: ({ authUser,
|
|
32
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
33
33
|
const longFormat = ifFlag(args, ["-l", "--long"]);
|
|
34
34
|
const targetArg = getArg(args, 0, { flags: ["-l", "--long"] });
|
|
35
35
|
const target = resolvePath(cwd, targetArg ?? cwd);
|
|
36
36
|
assertPathAccess(authUser, target, "ls");
|
|
37
|
-
const items = vfs
|
|
37
|
+
const items = shell.vfs
|
|
38
|
+
.list(target)
|
|
39
|
+
.filter((name) => !name.startsWith("."));
|
|
38
40
|
const rendered = longFormat
|
|
39
41
|
? items
|
|
40
42
|
.map((name) => {
|
|
41
43
|
const childPath = resolvePath(target, name);
|
|
42
|
-
const stat = vfs.stat(childPath);
|
|
44
|
+
const stat = shell.vfs.stat(childPath);
|
|
43
45
|
const size = stat.type === "file" ? stat.size : stat.childrenCount;
|
|
44
46
|
return `${formatPermissions(stat.mode, stat.type === "directory")} 1 ${size} ${formatDate(stat.updatedAt)} ${name}${stat.type === "directory" ? "/" : ""}`;
|
|
45
47
|
})
|
|
46
48
|
.join("\n")
|
|
47
|
-
: joinListWithType(target, items, (p) => vfs.stat(p));
|
|
49
|
+
: joinListWithType(target, items, (p) => shell.vfs.stat(p));
|
|
48
50
|
return { stdout: rendered, exitCode: 0 };
|
|
49
51
|
},
|
|
50
52
|
};
|