typescript-virtual-container 1.0.7 → 1.0.8
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/CHANGELOG.md +4 -0
- package/package.json +1 -1
- package/src/VirtualFileSystem/index.ts +3 -0
- package/src/VirtualShell/commands/command-helpers.ts +64 -0
- package/src/VirtualShell/commands/curl.ts +12 -90
- package/src/VirtualShell/commands/echo.ts +5 -12
- package/src/VirtualShell/commands/grep.ts +6 -14
- package/src/VirtualShell/commands/helpers.ts +74 -0
- package/src/VirtualShell/commands/index.ts +61 -33
- package/src/VirtualShell/commands/sudo.ts +9 -19
- package/src/VirtualShell/commands/wget.ts +17 -27
- package/src/VirtualShell/shell.ts +3 -0
- package/tests/parser-executor.test.ts +40 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -133,3 +133,67 @@ export function getArg(
|
|
|
133
133
|
const positionals = collectPositionals(args, options);
|
|
134
134
|
return positionals[index];
|
|
135
135
|
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Parse arguments into flags, flags with values, and positionals.
|
|
139
|
+
* @param args - Array of arguments to parse.
|
|
140
|
+
* @param options - Parsing options for flags and flags with values.
|
|
141
|
+
* @returns Parsed arguments as { flags, flagsWithValues, positionals }.
|
|
142
|
+
*/
|
|
143
|
+
export function parseArgs(
|
|
144
|
+
args: string[],
|
|
145
|
+
options: { flags?: string[]; flagsWithValue?: string[] } = {},
|
|
146
|
+
): {
|
|
147
|
+
flags: Set<string>;
|
|
148
|
+
flagsWithValues: Map<string, string>;
|
|
149
|
+
positionals: string[];
|
|
150
|
+
} {
|
|
151
|
+
const flags = new Set<string>();
|
|
152
|
+
const flagsWithValues = new Map<string, string>();
|
|
153
|
+
const positionals: string[] = [];
|
|
154
|
+
const boolFlags = new Set(options.flags ?? []);
|
|
155
|
+
const valueFlags = new Set(options.flagsWithValue ?? []);
|
|
156
|
+
let passthrough = false;
|
|
157
|
+
|
|
158
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
159
|
+
const arg = args[index]!;
|
|
160
|
+
|
|
161
|
+
if (passthrough) {
|
|
162
|
+
positionals.push(arg);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (arg === "--") {
|
|
167
|
+
passthrough = true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (boolFlags.has(arg)) {
|
|
172
|
+
flags.add(arg);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (valueFlags.has(arg)) {
|
|
177
|
+
const next = args[index + 1];
|
|
178
|
+
if (next && !next.startsWith("-")) {
|
|
179
|
+
flagsWithValues.set(arg, next);
|
|
180
|
+
index += 1;
|
|
181
|
+
} else {
|
|
182
|
+
flagsWithValues.set(arg, "");
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const inlineFlag = Array.from(valueFlags).find((flag) =>
|
|
188
|
+
arg.startsWith(`${flag}=`),
|
|
189
|
+
);
|
|
190
|
+
if (inlineFlag) {
|
|
191
|
+
flagsWithValues.set(inlineFlag, arg.slice(inlineFlag.length + 1));
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
positionals.push(arg);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { flags, flagsWithValues, positionals };
|
|
199
|
+
}
|
|
@@ -1,107 +1,31 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import type { ShellModule } from "../../types/commands";
|
|
3
|
-
import {
|
|
2
|
+
import { parseArgs } from "./command-helpers";
|
|
4
3
|
import {
|
|
5
4
|
assertPathAccess,
|
|
6
5
|
normalizeTerminalOutput,
|
|
7
6
|
resolvePath,
|
|
7
|
+
runHostCommand,
|
|
8
8
|
} from "./helpers";
|
|
9
9
|
|
|
10
|
-
function runHostCurl(args: string[]): Promise<{
|
|
11
|
-
stdout: string;
|
|
12
|
-
stderr: string;
|
|
13
|
-
exitCode: number;
|
|
14
|
-
}> {
|
|
15
|
-
return new Promise((resolve) => {
|
|
16
|
-
let childProcess: ReturnType<typeof spawn>;
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
childProcess = spawn("curl", args, {
|
|
20
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
21
|
-
});
|
|
22
|
-
} catch (error) {
|
|
23
|
-
resolve({
|
|
24
|
-
stdout: "",
|
|
25
|
-
stderr: `curl: ${error instanceof Error ? error.message : String(error)}`,
|
|
26
|
-
exitCode: 1,
|
|
27
|
-
});
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let stdout = "";
|
|
32
|
-
let stderr = "";
|
|
33
|
-
const stdoutStream = childProcess.stdout;
|
|
34
|
-
const stderrStream = childProcess.stderr;
|
|
35
|
-
|
|
36
|
-
if (!stdoutStream || !stderrStream) {
|
|
37
|
-
resolve({
|
|
38
|
-
stdout: "",
|
|
39
|
-
stderr: "curl: failed to capture process output",
|
|
40
|
-
exitCode: 1,
|
|
41
|
-
});
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
stdoutStream.setEncoding("utf8");
|
|
46
|
-
stderrStream.setEncoding("utf8");
|
|
47
|
-
|
|
48
|
-
stdoutStream.on("data", (chunk: string) => {
|
|
49
|
-
stdout += chunk;
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
stderrStream.on("data", (chunk: string) => {
|
|
53
|
-
stderr += chunk;
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
childProcess.on("error", (error) => {
|
|
57
|
-
const errorCode =
|
|
58
|
-
error instanceof Error && "code" in error
|
|
59
|
-
? String((error as NodeJS.ErrnoException).code ?? "")
|
|
60
|
-
: "";
|
|
61
|
-
resolve({
|
|
62
|
-
stdout: "",
|
|
63
|
-
stderr: `curl: ${error.message}`,
|
|
64
|
-
exitCode: errorCode === "ENOENT" ? 127 : 1,
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
childProcess.on("close", (code) => {
|
|
69
|
-
resolve({
|
|
70
|
-
stdout,
|
|
71
|
-
stderr,
|
|
72
|
-
exitCode: code ?? 1,
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
10
|
export const curlCommand: ShellModule = {
|
|
79
11
|
name: "curl",
|
|
80
12
|
params: ["[-o file] <url>"],
|
|
81
13
|
run: async ({ authUser, vfs, cwd, args }) => {
|
|
82
|
-
const
|
|
14
|
+
const { flagsWithValues, positionals } = parseArgs(args, {
|
|
15
|
+
flagsWithValue: ["-o", "--output"],
|
|
16
|
+
});
|
|
83
17
|
const outputPath =
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
: null;
|
|
87
|
-
const parserOptions = { flagsWithValue: ["-o", "--output"] };
|
|
88
|
-
const inputArgs: string[] = [];
|
|
89
|
-
for (let index = 0; ; index += 1) {
|
|
90
|
-
const arg = getArg(args, index, parserOptions);
|
|
91
|
-
if (!arg) {
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
inputArgs.push(arg);
|
|
95
|
-
}
|
|
96
|
-
const url = inputArgs[0];
|
|
97
|
-
const isHelpLike = ifFlag(args, ["-h", "--help", "-V", "--version"]);
|
|
18
|
+
flagsWithValues.get("-o") || flagsWithValues.get("--output") || null;
|
|
19
|
+
const url = positionals[0];
|
|
98
20
|
|
|
99
21
|
if (!url) {
|
|
100
22
|
return { stderr: "curl: missing URL", exitCode: 1 };
|
|
101
23
|
}
|
|
102
24
|
|
|
103
|
-
const passthroughArgs = outputPath
|
|
104
|
-
|
|
25
|
+
const passthroughArgs = outputPath
|
|
26
|
+
? [...positionals, "-o", "-"]
|
|
27
|
+
: positionals;
|
|
28
|
+
const result = await runHostCommand("curl", passthroughArgs);
|
|
105
29
|
|
|
106
30
|
if (result.exitCode !== 0) {
|
|
107
31
|
return {
|
|
@@ -125,9 +49,7 @@ export const curlCommand: ShellModule = {
|
|
|
125
49
|
}
|
|
126
50
|
|
|
127
51
|
return {
|
|
128
|
-
stdout:
|
|
129
|
-
? normalizeTerminalOutput(result.stdout)
|
|
130
|
-
: result.stdout,
|
|
52
|
+
stdout: result.stdout,
|
|
131
53
|
stderr: result.stderr
|
|
132
54
|
? normalizeTerminalOutput(result.stderr)
|
|
133
55
|
: undefined,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
-
import {
|
|
2
|
+
import { parseArgs } from "./command-helpers";
|
|
3
3
|
import { getAllEnvVars } from "./set";
|
|
4
4
|
|
|
5
5
|
function expandEnvVars(input: string, env: Record<string, string>): string {
|
|
@@ -12,18 +12,11 @@ export const echoCommand: ShellModule = {
|
|
|
12
12
|
name: "echo",
|
|
13
13
|
params: ["[options] [text...]"],
|
|
14
14
|
run: ({ args, authUser, stdin }) => {
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
for (let index = 0; ; index += 1) {
|
|
18
|
-
const value = getArg(args, index, { flags: ["-n"] });
|
|
19
|
-
if (value === undefined) {
|
|
20
|
-
break;
|
|
21
|
-
}
|
|
22
|
-
filteredArgs.push(value);
|
|
23
|
-
}
|
|
24
|
-
const env = getAllEnvVars(authUser);
|
|
15
|
+
const { flags, positionals } = parseArgs(args, { flags: ["-n"] });
|
|
16
|
+
const newline = !flags.has("-n");
|
|
25
17
|
const rawText =
|
|
26
|
-
|
|
18
|
+
positionals.length > 0 ? positionals.join(" ") : (stdin ?? "");
|
|
19
|
+
const env = getAllEnvVars(authUser);
|
|
27
20
|
const text = expandEnvVars(rawText, env);
|
|
28
21
|
|
|
29
22
|
return {
|
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
-
import {
|
|
2
|
+
import { parseArgs } from "./command-helpers";
|
|
3
3
|
import { assertPathAccess, resolvePath } from "./helpers";
|
|
4
4
|
|
|
5
5
|
export const grepCommand: ShellModule = {
|
|
6
6
|
name: "grep",
|
|
7
7
|
params: ["[-i] [-v] <pattern> [file...]"],
|
|
8
8
|
run: ({ authUser, vfs, cwd, args, stdin }) => {
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const pattern =
|
|
13
|
-
const files
|
|
14
|
-
for (let index = 1; ; index += 1) {
|
|
15
|
-
const file = getArg(args, index, parserOptions);
|
|
16
|
-
if (!file) {
|
|
17
|
-
break;
|
|
18
|
-
}
|
|
19
|
-
files.push(file);
|
|
20
|
-
}
|
|
9
|
+
const { flags, positionals } = parseArgs(args, { flags: ["-i", "-v"] });
|
|
10
|
+
const caseInsensitive = flags.has("-i");
|
|
11
|
+
const invertMatch = flags.has("-v");
|
|
12
|
+
const pattern = positionals[0];
|
|
13
|
+
const files = positionals.slice(1);
|
|
21
14
|
|
|
22
15
|
if (!pattern) {
|
|
23
16
|
return { stderr: "grep: no pattern specified", exitCode: 1 };
|
|
@@ -33,7 +26,6 @@ export const grepCommand: ShellModule = {
|
|
|
33
26
|
|
|
34
27
|
const results: string[] = [];
|
|
35
28
|
|
|
36
|
-
// If no files specified, read from stdin (pipe/input redirection).
|
|
37
29
|
if (files.length === 0) {
|
|
38
30
|
if (!stdin) {
|
|
39
31
|
return { stdout: "", exitCode: 1 };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import type VirtualFileSystem from "../../VirtualFileSystem";
|
|
3
4
|
|
|
@@ -76,6 +77,79 @@ export async function fetchResource(
|
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Run a host command like curl or wget and capture its output.
|
|
82
|
+
* @param binary - The binary to execute (e.g., "curl", "wget").
|
|
83
|
+
* @param args - Arguments to pass to the binary.
|
|
84
|
+
* @returns Promise resolving with stdout, stderr, and exit code.
|
|
85
|
+
*/
|
|
86
|
+
export function runHostCommand(
|
|
87
|
+
binary: string,
|
|
88
|
+
args: string[],
|
|
89
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
let childProcess: ReturnType<typeof spawn>;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
childProcess = spawn(binary, args, {
|
|
95
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
resolve({
|
|
99
|
+
stdout: "",
|
|
100
|
+
stderr: `${binary}: ${error instanceof Error ? error.message : String(error)}`,
|
|
101
|
+
exitCode: 1,
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let stdout = "";
|
|
107
|
+
let stderr = "";
|
|
108
|
+
const stdoutStream = childProcess.stdout;
|
|
109
|
+
const stderrStream = childProcess.stderr;
|
|
110
|
+
|
|
111
|
+
if (!stdoutStream || !stderrStream) {
|
|
112
|
+
resolve({
|
|
113
|
+
stdout: "",
|
|
114
|
+
stderr: `${binary}: failed to capture process output`,
|
|
115
|
+
exitCode: 1,
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
stdoutStream.setEncoding("utf8");
|
|
121
|
+
stderrStream.setEncoding("utf8");
|
|
122
|
+
|
|
123
|
+
stdoutStream.on("data", (chunk: string) => {
|
|
124
|
+
stdout += chunk;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
stderrStream.on("data", (chunk: string) => {
|
|
128
|
+
stderr += chunk;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
childProcess.on("error", (error) => {
|
|
132
|
+
const errorCode =
|
|
133
|
+
error instanceof Error && "code" in error
|
|
134
|
+
? String((error as NodeJS.ErrnoException).code ?? "")
|
|
135
|
+
: "";
|
|
136
|
+
resolve({
|
|
137
|
+
stdout: "",
|
|
138
|
+
stderr: `${binary}: ${error.message}`,
|
|
139
|
+
exitCode: errorCode === "ENOENT" ? 127 : 1,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
childProcess.on("close", (code) => {
|
|
144
|
+
resolve({
|
|
145
|
+
stdout,
|
|
146
|
+
stderr,
|
|
147
|
+
exitCode: code ?? 1,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
79
153
|
function levenshtein(a: string, b: string): number {
|
|
80
154
|
const dp: number[][] = Array.from({ length: a.length + 1 }, () =>
|
|
81
155
|
Array<number>(b.length + 1).fill(0),
|
|
@@ -3,7 +3,6 @@ import type { VirtualUserManager } from "../../SSHMimic/users";
|
|
|
3
3
|
import type {
|
|
4
4
|
CommandContext,
|
|
5
5
|
CommandMode,
|
|
6
|
-
CommandOutcome,
|
|
7
6
|
CommandResult,
|
|
8
7
|
ShellModule,
|
|
9
8
|
} from "../../types/commands";
|
|
@@ -77,11 +76,29 @@ const helpCommand = createHelpCommand(() =>
|
|
|
77
76
|
getCommandModules().map((cmd) => cmd.name),
|
|
78
77
|
);
|
|
79
78
|
|
|
79
|
+
const commandRegistry = new Map<string, ShellModule>();
|
|
80
|
+
let cachedCommandNames: string[] | null = null;
|
|
81
|
+
|
|
82
|
+
function invalidateCache(): void {
|
|
83
|
+
cachedCommandNames = null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildCache(): void {
|
|
87
|
+
commandRegistry.clear();
|
|
88
|
+
for (const mod of getCommandModules()) {
|
|
89
|
+
commandRegistry.set(mod.name, mod);
|
|
90
|
+
for (const alias of mod.aliases ?? []) {
|
|
91
|
+
commandRegistry.set(alias, mod);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
cachedCommandNames = Array.from(commandRegistry.keys()).sort();
|
|
95
|
+
}
|
|
96
|
+
|
|
80
97
|
function getCommandModules(): ShellModule[] {
|
|
81
98
|
return [...BASE_COMMANDS, ...customCommands, helpCommand];
|
|
82
99
|
}
|
|
83
100
|
|
|
84
|
-
function
|
|
101
|
+
function _getTakenCommandNames(modules: ShellModule[]): Set<string> {
|
|
85
102
|
const taken = new Set<string>();
|
|
86
103
|
for (const mod of modules) {
|
|
87
104
|
taken.add(mod.name);
|
|
@@ -106,13 +123,14 @@ export function registerCommand(module: ShellModule): void {
|
|
|
106
123
|
);
|
|
107
124
|
}
|
|
108
125
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
126
|
+
for (const name of names) {
|
|
127
|
+
if (commandRegistry.has(name)) {
|
|
128
|
+
throw new Error(`Command '${name}' already exists`);
|
|
129
|
+
}
|
|
130
|
+
commandRegistry.set(name, normalized);
|
|
113
131
|
}
|
|
114
132
|
|
|
115
|
-
|
|
133
|
+
invalidateCache();
|
|
116
134
|
}
|
|
117
135
|
|
|
118
136
|
export function createCustomCommand(
|
|
@@ -128,17 +146,14 @@ export function createCustomCommand(
|
|
|
128
146
|
}
|
|
129
147
|
|
|
130
148
|
export function getCommandNames(): string[] {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
149
|
+
if (!cachedCommandNames) {
|
|
150
|
+
buildCache();
|
|
151
|
+
}
|
|
152
|
+
return cachedCommandNames!;
|
|
135
153
|
}
|
|
136
154
|
|
|
137
|
-
function resolveModule(name: string): ShellModule | undefined {
|
|
138
|
-
|
|
139
|
-
return getCommandModules().find(
|
|
140
|
-
(cmd) => cmd.name === lowered || cmd.aliases?.includes(lowered),
|
|
141
|
-
);
|
|
155
|
+
export function resolveModule(name: string): ShellModule | undefined {
|
|
156
|
+
return commandRegistry.get(name.toLowerCase());
|
|
142
157
|
}
|
|
143
158
|
|
|
144
159
|
function splitArgsRespectingQuotes(input: string): string[] {
|
|
@@ -192,7 +207,7 @@ function parseInput(rawInput: string): { commandName: string; args: string[] } {
|
|
|
192
207
|
}
|
|
193
208
|
|
|
194
209
|
// Internal async function for pipeline execution
|
|
195
|
-
async function
|
|
210
|
+
async function _runCommandInternal(
|
|
196
211
|
rawInput: string,
|
|
197
212
|
authUser: string,
|
|
198
213
|
hostname: string,
|
|
@@ -271,7 +286,7 @@ async function runCommandInternal(
|
|
|
271
286
|
}
|
|
272
287
|
}
|
|
273
288
|
|
|
274
|
-
export function runCommand(
|
|
289
|
+
export async function runCommand(
|
|
275
290
|
rawInput: string,
|
|
276
291
|
authUser: string,
|
|
277
292
|
hostname: string,
|
|
@@ -281,29 +296,42 @@ export function runCommand(
|
|
|
281
296
|
shellProps: ShellProperties,
|
|
282
297
|
vfs: VirtualFileSystem,
|
|
283
298
|
stdin?: string,
|
|
284
|
-
):
|
|
299
|
+
): Promise<CommandResult> {
|
|
285
300
|
const trimmed = rawInput.trim();
|
|
286
301
|
|
|
287
302
|
if (trimmed.length === 0) {
|
|
288
303
|
return { exitCode: 0 };
|
|
289
304
|
}
|
|
290
305
|
|
|
291
|
-
// Check if input contains pipes or redirections - use async version
|
|
292
306
|
if (trimmed.includes("|") || trimmed.includes(">") || trimmed.includes("<")) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
307
|
+
const { parseShellPipeline } = await import("../shellParser");
|
|
308
|
+
const { executePipeline } = await import("../../SSHMimic/executor");
|
|
309
|
+
|
|
310
|
+
const pipeline = parseShellPipeline(trimmed);
|
|
311
|
+
if (!pipeline.isValid) {
|
|
312
|
+
return {
|
|
313
|
+
stderr: pipeline.error || "Syntax error",
|
|
314
|
+
exitCode: 1,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
return await executePipeline(
|
|
320
|
+
pipeline,
|
|
321
|
+
authUser,
|
|
322
|
+
hostname,
|
|
323
|
+
users,
|
|
324
|
+
mode,
|
|
325
|
+
cwd,
|
|
326
|
+
vfs,
|
|
327
|
+
);
|
|
328
|
+
} catch (error: unknown) {
|
|
329
|
+
const message =
|
|
330
|
+
error instanceof Error ? error.message : "Pipeline execution failed";
|
|
331
|
+
return { stderr: message, exitCode: 1 };
|
|
332
|
+
}
|
|
304
333
|
}
|
|
305
334
|
|
|
306
|
-
// Regular synchronous command execution
|
|
307
335
|
const { commandName, args } = parseInput(trimmed);
|
|
308
336
|
const mod = resolveModule(commandName);
|
|
309
337
|
|
|
@@ -315,7 +343,7 @@ export function runCommand(
|
|
|
315
343
|
}
|
|
316
344
|
|
|
317
345
|
try {
|
|
318
|
-
return mod.run({
|
|
346
|
+
return await mod.run({
|
|
319
347
|
authUser,
|
|
320
348
|
hostname,
|
|
321
349
|
users,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defaultShellProperties } from "..";
|
|
2
2
|
import type { ShellModule } from "../../types/commands";
|
|
3
|
-
import {
|
|
3
|
+
import { parseArgs } from "./command-helpers";
|
|
4
4
|
import { runCommand } from "./index";
|
|
5
5
|
|
|
6
6
|
function parseSudoArgs(args: string[]): {
|
|
@@ -8,26 +8,16 @@ function parseSudoArgs(args: string[]): {
|
|
|
8
8
|
loginShell: boolean;
|
|
9
9
|
commandLine: string | null;
|
|
10
10
|
} {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
? targetUserValue
|
|
16
|
-
: "root";
|
|
11
|
+
const { flags, flagsWithValues, positionals } = parseArgs(args, {
|
|
12
|
+
flags: ["-i", "-S"],
|
|
13
|
+
flagsWithValue: ["-u", "--user"],
|
|
14
|
+
});
|
|
17
15
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
flagsWithValue: ["-u", "--user"],
|
|
23
|
-
});
|
|
24
|
-
if (!part) {
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
commandParts.push(part);
|
|
28
|
-
}
|
|
16
|
+
const loginShell = flags.has("-i");
|
|
17
|
+
const targetUser =
|
|
18
|
+
flagsWithValues.get("-u") || flagsWithValues.get("--user") || "root";
|
|
19
|
+
const commandLine = positionals.length > 0 ? positionals.join(" ") : null;
|
|
29
20
|
|
|
30
|
-
const commandLine = commandParts.length > 0 ? commandParts.join(" ") : null;
|
|
31
21
|
return { targetUser, loginShell, commandLine };
|
|
32
22
|
}
|
|
33
23
|
export const sudoCommand: ShellModule = {
|
|
@@ -3,11 +3,12 @@ import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import type { ShellModule } from "../../types/commands";
|
|
6
|
-
import {
|
|
6
|
+
import { ifFlag, parseArgs } from "./command-helpers";
|
|
7
7
|
import {
|
|
8
8
|
assertPathAccess,
|
|
9
9
|
normalizeTerminalOutput,
|
|
10
10
|
resolvePath,
|
|
11
|
+
runHostCommand,
|
|
11
12
|
stripUrlFilename,
|
|
12
13
|
} from "./helpers";
|
|
13
14
|
|
|
@@ -83,36 +84,25 @@ export const wgetCommand: ShellModule = {
|
|
|
83
84
|
name: "wget",
|
|
84
85
|
params: ["[url]"],
|
|
85
86
|
run: async ({ authUser, vfs, cwd, args }) => {
|
|
86
|
-
const
|
|
87
|
-
"-o",
|
|
88
|
-
"-O",
|
|
89
|
-
"--output",
|
|
90
|
-
"--output-document",
|
|
91
|
-
]);
|
|
92
|
-
const outputPath =
|
|
93
|
-
typeof outputPathValue === "string" && outputPathValue.length > 0
|
|
94
|
-
? outputPathValue
|
|
95
|
-
: null;
|
|
96
|
-
const parserOptions = {
|
|
87
|
+
const { flagsWithValues, positionals } = parseArgs(args, {
|
|
97
88
|
flagsWithValue: ["-o", "-O", "--output", "--output-document"],
|
|
98
|
-
};
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
const url = inputArgs[0];
|
|
108
|
-
const isHelpLike = ifFlag(args, ["-h", "--help", "-V", "--version"]);
|
|
89
|
+
});
|
|
90
|
+
const outputPath =
|
|
91
|
+
flagsWithValues.get("-o") ||
|
|
92
|
+
flagsWithValues.get("-O") ||
|
|
93
|
+
flagsWithValues.get("--output") ||
|
|
94
|
+
flagsWithValues.get("--output-document") ||
|
|
95
|
+
null;
|
|
96
|
+
const url = positionals[0];
|
|
109
97
|
|
|
110
98
|
if (!url) {
|
|
111
99
|
return { stderr: "wget: missing URL", exitCode: 1 };
|
|
112
100
|
}
|
|
113
101
|
|
|
102
|
+
const isHelpLike = ifFlag(args, ["-h", "--help", "-V", "--version"]);
|
|
103
|
+
|
|
114
104
|
if (isHelpLike) {
|
|
115
|
-
const result = await runHostWget(
|
|
105
|
+
const result = await runHostWget(args);
|
|
116
106
|
return {
|
|
117
107
|
stdout: normalizeTerminalOutput(result.stdout),
|
|
118
108
|
stderr: result.stderr
|
|
@@ -126,8 +116,8 @@ export const wgetCommand: ShellModule = {
|
|
|
126
116
|
const tempFile = join(tempDir, "download");
|
|
127
117
|
|
|
128
118
|
try {
|
|
129
|
-
const hostArgs = [...
|
|
130
|
-
const result = await
|
|
119
|
+
const hostArgs = [...positionals, "-O", tempFile];
|
|
120
|
+
const result = await runHostCommand("wget", hostArgs);
|
|
131
121
|
|
|
132
122
|
if (result.exitCode !== 0) {
|
|
133
123
|
return {
|
|
@@ -139,7 +129,7 @@ export const wgetCommand: ShellModule = {
|
|
|
139
129
|
}
|
|
140
130
|
|
|
141
131
|
const content = await readFile(tempFile, "utf8");
|
|
142
|
-
const target = resolvePath(cwd, outputPath
|
|
132
|
+
const target = resolvePath(cwd, outputPath || stripUrlFilename(url));
|
|
143
133
|
assertPathAccess(authUser, target, "wget");
|
|
144
134
|
vfs.writeFile(target, content);
|
|
145
135
|
|
|
@@ -66,6 +66,9 @@ export function startShell(
|
|
|
66
66
|
return buildPrompt(authUser, hostname, cwdLabel);
|
|
67
67
|
};
|
|
68
68
|
const commandNames = Array.from(new Set(getCommandNames())).sort();
|
|
69
|
+
console.log(
|
|
70
|
+
`[${sessionId}] Shell started for user '${authUser}' at ${remoteAddress}`,
|
|
71
|
+
);
|
|
69
72
|
|
|
70
73
|
async function collectChildPids(parentPid: number): Promise<number[]> {
|
|
71
74
|
try {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { executePipeline } from "../src/SSHMimic/executor";
|
|
3
|
+
import { VirtualUserManager } from "../src/SSHMimic/users";
|
|
4
|
+
import VirtualFileSystem from "../src/VirtualFileSystem";
|
|
5
|
+
import { parseShellPipeline } from "../src/VirtualShell/shellParser";
|
|
6
|
+
|
|
7
|
+
describe("Pipeline parser and executor", () => {
|
|
8
|
+
test("parses simple pipeline", () => {
|
|
9
|
+
const pipeline = parseShellPipeline("echo hello | grep h");
|
|
10
|
+
expect(pipeline.isValid).toBe(true);
|
|
11
|
+
expect(pipeline.commands).toHaveLength(2);
|
|
12
|
+
expect(pipeline.commands[0]?.name).toBe("echo");
|
|
13
|
+
expect(pipeline.commands[1]?.name).toBe("grep");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("handles invalid syntax", () => {
|
|
17
|
+
const pipeline = parseShellPipeline("echo hello |");
|
|
18
|
+
expect(pipeline.isValid).toBe(false);
|
|
19
|
+
expect(pipeline.error).toBeDefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("executes simple pipeline", async () => {
|
|
23
|
+
const vfs = new VirtualFileSystem("/tmp");
|
|
24
|
+
const users = new VirtualUserManager(vfs, "root-pass");
|
|
25
|
+
const pipeline = parseShellPipeline("echo hello | grep h");
|
|
26
|
+
|
|
27
|
+
const result = await executePipeline(
|
|
28
|
+
pipeline,
|
|
29
|
+
"root",
|
|
30
|
+
"localhost",
|
|
31
|
+
users,
|
|
32
|
+
"shell",
|
|
33
|
+
"/",
|
|
34
|
+
vfs,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(result.exitCode).toBe(0);
|
|
38
|
+
expect(result.stdout).toContain("hello");
|
|
39
|
+
});
|
|
40
|
+
});
|