typescript-virtual-container 0.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/.github/ISSUE_TEMPLATE/bug_report.yml +50 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +31 -0
- package/.github/dependabot.yml +27 -0
- package/.github/pull_request_template.md +21 -0
- package/.github/workflows/create-pull-request.yml +83 -0
- package/.github/workflows/test-battery.yml +57 -0
- package/CHANGELOG.md +27 -0
- package/CODE_OF_CONDUCT.md +39 -0
- package/CONTRIBUTING.md +59 -0
- package/LICENSE +21 -0
- package/README.md +1283 -0
- package/SECURITY.md +33 -0
- package/biome.json +20 -0
- package/bun.lock +99 -0
- package/package.json +38 -0
- package/src/SSHMimic/client.ts +248 -0
- package/src/SSHMimic/commands/adduser.ts +22 -0
- package/src/SSHMimic/commands/cat.ts +16 -0
- package/src/SSHMimic/commands/cd.ts +20 -0
- package/src/SSHMimic/commands/clear.ts +7 -0
- package/src/SSHMimic/commands/curl.ts +27 -0
- package/src/SSHMimic/commands/deluser.ts +19 -0
- package/src/SSHMimic/commands/exit.ts +7 -0
- package/src/SSHMimic/commands/help.ts +9 -0
- package/src/SSHMimic/commands/helpers.ts +137 -0
- package/src/SSHMimic/commands/hostname.ts +7 -0
- package/src/SSHMimic/commands/htop.ts +13 -0
- package/src/SSHMimic/commands/index.ts +120 -0
- package/src/SSHMimic/commands/ls.ts +14 -0
- package/src/SSHMimic/commands/mkdir.ts +17 -0
- package/src/SSHMimic/commands/nano.ts +30 -0
- package/src/SSHMimic/commands/pwd.ts +7 -0
- package/src/SSHMimic/commands/rm.ts +26 -0
- package/src/SSHMimic/commands/su.ts +31 -0
- package/src/SSHMimic/commands/sudo.ts +90 -0
- package/src/SSHMimic/commands/touch.ts +20 -0
- package/src/SSHMimic/commands/tree.ts +11 -0
- package/src/SSHMimic/commands/wget.ts +33 -0
- package/src/SSHMimic/commands/who.ts +18 -0
- package/src/SSHMimic/commands/whoami.ts +7 -0
- package/src/SSHMimic/exec.ts +37 -0
- package/src/SSHMimic/hostKey.ts +21 -0
- package/src/SSHMimic/index.ts +203 -0
- package/src/SSHMimic/loginFormat.ts +10 -0
- package/src/SSHMimic/prompt.ts +14 -0
- package/src/SSHMimic/shell.ts +740 -0
- package/src/SSHMimic/users.ts +336 -0
- package/src/VirtualFileSystem.ts +420 -0
- package/src/index.ts +34 -0
- package/src/standalone.ts +14 -0
- package/src/types/commands.ts +98 -0
- package/src/types/streams.ts +32 -0
- package/src/types/tar-stream.d.ts +38 -0
- package/src/types/vfs.ts +81 -0
- package/src/vfs/archive.ts +74 -0
- package/src/vfs/internalTypes.ts +19 -0
- package/src/vfs/path.ts +74 -0
- package/src/vfs/snapshot.ts +84 -0
- package/src/vfs/tree.ts +34 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type VirtualFileSystem from "../../VirtualFileSystem";
|
|
3
|
+
|
|
4
|
+
export function resolvePath(cwd: string, inputPath: string): string {
|
|
5
|
+
if (!inputPath || inputPath.trim() === "") {
|
|
6
|
+
return cwd;
|
|
7
|
+
}
|
|
8
|
+
return inputPath.startsWith("/")
|
|
9
|
+
? path.posix.normalize(inputPath)
|
|
10
|
+
: path.posix.normalize(path.posix.join(cwd, inputPath));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseOutputPath(args: string[]): {
|
|
14
|
+
outputPath: string | null;
|
|
15
|
+
inputArgs: string[];
|
|
16
|
+
} {
|
|
17
|
+
const filtered: string[] = [];
|
|
18
|
+
let outputPath: string | null = null;
|
|
19
|
+
|
|
20
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
21
|
+
const arg = args[index]!;
|
|
22
|
+
|
|
23
|
+
if (
|
|
24
|
+
arg === "-o" ||
|
|
25
|
+
arg === "-O" ||
|
|
26
|
+
arg === "--output" ||
|
|
27
|
+
arg === "--output-document"
|
|
28
|
+
) {
|
|
29
|
+
outputPath = args[index + 1] ?? null;
|
|
30
|
+
index += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (arg.startsWith("-o=")) {
|
|
35
|
+
outputPath = arg.slice(3);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (arg.startsWith("-O=")) {
|
|
40
|
+
outputPath = arg.slice(3);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
filtered.push(arg);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { outputPath, inputArgs: filtered };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function stripUrlFilename(url: string): string {
|
|
51
|
+
const cleaned = url.split("?")[0]?.split("#")[0] ?? url;
|
|
52
|
+
const lastPart = cleaned.split("/").filter(Boolean).pop();
|
|
53
|
+
return lastPart && lastPart.length > 0 ? lastPart : "index.html";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function fetchResource(
|
|
57
|
+
url: string,
|
|
58
|
+
): Promise<{ text: string; status: number; contentType: string | null }> {
|
|
59
|
+
const response = await fetch(url);
|
|
60
|
+
const contentType = response.headers.get("content-type");
|
|
61
|
+
return {
|
|
62
|
+
text: await response.text(),
|
|
63
|
+
status: response.status,
|
|
64
|
+
contentType,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function levenshtein(a: string, b: string): number {
|
|
69
|
+
const dp: number[][] = Array.from({ length: a.length + 1 }, () =>
|
|
70
|
+
Array<number>(b.length + 1).fill(0),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i <= a.length; i += 1) {
|
|
74
|
+
dp[i]![0] = i;
|
|
75
|
+
}
|
|
76
|
+
for (let j = 0; j <= b.length; j += 1) {
|
|
77
|
+
dp[0]![j] = j;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
81
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
82
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
83
|
+
dp[i]![j] = Math.min(
|
|
84
|
+
dp[i - 1]![j]! + 1,
|
|
85
|
+
dp[i]![j - 1]! + 1,
|
|
86
|
+
dp[i - 1]![j - 1]! + cost,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return dp[a.length]![b.length]!;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function resolveReadablePath(
|
|
95
|
+
vfs: VirtualFileSystem,
|
|
96
|
+
cwd: string,
|
|
97
|
+
inputPath: string,
|
|
98
|
+
): string {
|
|
99
|
+
const exactPath = resolvePath(cwd, inputPath);
|
|
100
|
+
if (vfs.exists(exactPath)) {
|
|
101
|
+
return exactPath;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const parent = path.posix.dirname(exactPath);
|
|
105
|
+
const fileName = path.posix.basename(exactPath);
|
|
106
|
+
const siblings = vfs.list(parent);
|
|
107
|
+
|
|
108
|
+
const caseInsensitive = siblings.filter(
|
|
109
|
+
(name) => name.toLowerCase() === fileName.toLowerCase(),
|
|
110
|
+
);
|
|
111
|
+
if (caseInsensitive.length === 1) {
|
|
112
|
+
return path.posix.join(parent, caseInsensitive[0]!);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const near = siblings.filter(
|
|
116
|
+
(name) => levenshtein(name.toLowerCase(), fileName.toLowerCase()) <= 1,
|
|
117
|
+
);
|
|
118
|
+
if (near.length === 1) {
|
|
119
|
+
return path.posix.join(parent, near[0]!);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return exactPath;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function joinListWithType(
|
|
126
|
+
cwd: string,
|
|
127
|
+
items: string[],
|
|
128
|
+
statAt: (p: string) => { type: "file" | "directory" },
|
|
129
|
+
): string {
|
|
130
|
+
return items
|
|
131
|
+
.map((name) => {
|
|
132
|
+
const childPath = resolvePath(cwd, name);
|
|
133
|
+
const stats = statAt(childPath);
|
|
134
|
+
return stats.type === "directory" ? `${name}/` : name;
|
|
135
|
+
})
|
|
136
|
+
.join(" ");
|
|
137
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
|
|
3
|
+
export const htopCommand: ShellModule = {
|
|
4
|
+
name: "htop",
|
|
5
|
+
params: [],
|
|
6
|
+
run: ({ mode }) => {
|
|
7
|
+
if (mode === "exec") {
|
|
8
|
+
return { stderr: "htop: interactive terminal required", exitCode: 1 };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return { openHtop: true, exitCode: 0 };
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CommandMode,
|
|
3
|
+
CommandOutcome,
|
|
4
|
+
ShellModule,
|
|
5
|
+
} from "../../types/commands";
|
|
6
|
+
import type VirtualFileSystem from "../../VirtualFileSystem";
|
|
7
|
+
import type { VirtualUserManager } from "../users";
|
|
8
|
+
import { adduserCommand } from "./adduser";
|
|
9
|
+
import { catCommand } from "./cat";
|
|
10
|
+
import { cdCommand } from "./cd";
|
|
11
|
+
import { clearCommand } from "./clear";
|
|
12
|
+
import { curlCommand } from "./curl";
|
|
13
|
+
import { deluserCommand } from "./deluser";
|
|
14
|
+
import { exitCommand } from "./exit";
|
|
15
|
+
import { createHelpCommand } from "./help";
|
|
16
|
+
import { hostnameCommand } from "./hostname";
|
|
17
|
+
import { htopCommand } from "./htop";
|
|
18
|
+
import { lsCommand } from "./ls";
|
|
19
|
+
import { mkdirCommand } from "./mkdir";
|
|
20
|
+
import { nanoCommand } from "./nano";
|
|
21
|
+
import { pwdCommand } from "./pwd";
|
|
22
|
+
import { rmCommand } from "./rm";
|
|
23
|
+
import { suCommand } from "./su";
|
|
24
|
+
import { sudoCommand } from "./sudo";
|
|
25
|
+
import { touchCommand } from "./touch";
|
|
26
|
+
import { treeCommand } from "./tree";
|
|
27
|
+
import { wgetCommand } from "./wget";
|
|
28
|
+
import { whoCommand } from "./who";
|
|
29
|
+
import { whoamiCommand } from "./whoami";
|
|
30
|
+
|
|
31
|
+
const BASE_COMMANDS: ShellModule[] = [
|
|
32
|
+
pwdCommand,
|
|
33
|
+
whoamiCommand,
|
|
34
|
+
whoCommand,
|
|
35
|
+
hostnameCommand,
|
|
36
|
+
lsCommand,
|
|
37
|
+
cdCommand,
|
|
38
|
+
catCommand,
|
|
39
|
+
mkdirCommand,
|
|
40
|
+
touchCommand,
|
|
41
|
+
rmCommand,
|
|
42
|
+
treeCommand,
|
|
43
|
+
nanoCommand,
|
|
44
|
+
htopCommand,
|
|
45
|
+
adduserCommand,
|
|
46
|
+
deluserCommand,
|
|
47
|
+
sudoCommand,
|
|
48
|
+
suCommand,
|
|
49
|
+
curlCommand,
|
|
50
|
+
wgetCommand,
|
|
51
|
+
clearCommand,
|
|
52
|
+
exitCommand,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const COMMANDS: ShellModule[] = [
|
|
56
|
+
...BASE_COMMANDS,
|
|
57
|
+
createHelpCommand(() => COMMANDS.map((cmd) => cmd.name)),
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export function getCommandNames(): string[] {
|
|
61
|
+
return COMMANDS.flatMap((cmd) => [cmd.name, ...(cmd.aliases ?? [])]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveModule(name: string): ShellModule | undefined {
|
|
65
|
+
const lowered = name.toLowerCase();
|
|
66
|
+
return COMMANDS.find(
|
|
67
|
+
(cmd) => cmd.name === lowered || cmd.aliases?.includes(lowered),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseInput(rawInput: string): { commandName: string; args: string[] } {
|
|
72
|
+
const parts = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
73
|
+
return {
|
|
74
|
+
commandName: parts[0]?.toLowerCase() ?? "",
|
|
75
|
+
args: parts.slice(1),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function runCommand(
|
|
80
|
+
rawInput: string,
|
|
81
|
+
authUser: string,
|
|
82
|
+
hostname: string,
|
|
83
|
+
users: VirtualUserManager,
|
|
84
|
+
mode: CommandMode,
|
|
85
|
+
cwd: string,
|
|
86
|
+
vfs: VirtualFileSystem,
|
|
87
|
+
): CommandOutcome {
|
|
88
|
+
const trimmed = rawInput.trim();
|
|
89
|
+
|
|
90
|
+
if (trimmed.length === 0) {
|
|
91
|
+
return { exitCode: 0 };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { commandName, args } = parseInput(trimmed);
|
|
95
|
+
const mod = resolveModule(commandName);
|
|
96
|
+
|
|
97
|
+
if (!mod) {
|
|
98
|
+
return {
|
|
99
|
+
stderr: `Command '${trimmed}' not found`,
|
|
100
|
+
exitCode: 127,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
return mod.run({
|
|
106
|
+
authUser,
|
|
107
|
+
hostname,
|
|
108
|
+
users,
|
|
109
|
+
activeSessions: users.listActiveSessions(),
|
|
110
|
+
rawInput: trimmed,
|
|
111
|
+
mode,
|
|
112
|
+
args,
|
|
113
|
+
cwd,
|
|
114
|
+
vfs,
|
|
115
|
+
});
|
|
116
|
+
} catch (error: unknown) {
|
|
117
|
+
const message = error instanceof Error ? error.message : "Command failed";
|
|
118
|
+
return { stderr: message, exitCode: 1 };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { joinListWithType, resolvePath } from "./helpers";
|
|
3
|
+
|
|
4
|
+
export const lsCommand: ShellModule = {
|
|
5
|
+
name: "ls",
|
|
6
|
+
params: ["[path]"],
|
|
7
|
+
run: ({ vfs, cwd, args }) => {
|
|
8
|
+
const targetArg = args.find((arg) => !arg.startsWith("-"));
|
|
9
|
+
const target = resolvePath(cwd, targetArg ?? cwd);
|
|
10
|
+
const items = vfs.list(target).filter((name) => !name.startsWith("."));
|
|
11
|
+
const rendered = joinListWithType(target, items, (p) => vfs.stat(p));
|
|
12
|
+
return { stdout: rendered, exitCode: 0 };
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { resolvePath } from "./helpers";
|
|
3
|
+
|
|
4
|
+
export const mkdirCommand: ShellModule = {
|
|
5
|
+
name: "mkdir",
|
|
6
|
+
params: ["<dir>"],
|
|
7
|
+
run: ({ vfs, cwd, args }) => {
|
|
8
|
+
if (args.length === 0) {
|
|
9
|
+
return { stderr: "mkdir: missing operand", exitCode: 1 };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
for (const dir of args) {
|
|
13
|
+
vfs.mkdir(resolvePath(cwd, dir));
|
|
14
|
+
}
|
|
15
|
+
return { exitCode: 0 };
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { ShellModule } from "../../types/commands";
|
|
3
|
+
import { resolvePath } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const nanoCommand: ShellModule = {
|
|
6
|
+
name: "nano",
|
|
7
|
+
params: ["<file>"],
|
|
8
|
+
run: ({ vfs, cwd, args }) => {
|
|
9
|
+
const fileArg = args[0];
|
|
10
|
+
if (!fileArg) {
|
|
11
|
+
return { stderr: "nano: missing file operand", exitCode: 1 };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const targetPath = resolvePath(cwd, fileArg);
|
|
15
|
+
const initialContent = vfs.exists(targetPath)
|
|
16
|
+
? vfs.readFile(targetPath)
|
|
17
|
+
: "";
|
|
18
|
+
const safeName = path.posix.basename(targetPath) || "buffer";
|
|
19
|
+
const tempPath = `/tmp/sshmimic-nano-${Date.now()}-${safeName}.tmp`;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
openEditor: {
|
|
23
|
+
targetPath,
|
|
24
|
+
tempPath,
|
|
25
|
+
initialContent,
|
|
26
|
+
},
|
|
27
|
+
exitCode: 0,
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { resolvePath } from "./helpers";
|
|
3
|
+
|
|
4
|
+
export const rmCommand: ShellModule = {
|
|
5
|
+
name: "rm",
|
|
6
|
+
params: ["[-r|-rf] <path>"],
|
|
7
|
+
run: ({ vfs, cwd, args }) => {
|
|
8
|
+
if (args.length === 0) {
|
|
9
|
+
return { stderr: "rm: missing operand", exitCode: 1 };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const recursive =
|
|
13
|
+
args.includes("-r") || args.includes("-rf") || args.includes("-fr");
|
|
14
|
+
const targets = args.filter((arg) => !arg.startsWith("-"));
|
|
15
|
+
|
|
16
|
+
if (targets.length === 0) {
|
|
17
|
+
return { stderr: "rm: missing operand", exitCode: 1 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const target of targets) {
|
|
21
|
+
vfs.remove(resolvePath(cwd, target), { recursive });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { exitCode: 0 };
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
|
|
3
|
+
export const suCommand: ShellModule = {
|
|
4
|
+
name: "su",
|
|
5
|
+
params: ["- <username>"],
|
|
6
|
+
run: ({ authUser, users, args }) => {
|
|
7
|
+
const filtered = args.filter((arg) => arg !== "-");
|
|
8
|
+
const targetUser = filtered[0];
|
|
9
|
+
|
|
10
|
+
if (!targetUser) {
|
|
11
|
+
return { stderr: "su: missing username", exitCode: 1 };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!users.isSudoer(authUser) && authUser !== "root") {
|
|
15
|
+
return { stderr: "su: permission denied", exitCode: 1 };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (
|
|
19
|
+
!users.verifyPassword(targetUser, filtered[1] ?? "") &&
|
|
20
|
+
authUser !== "root"
|
|
21
|
+
) {
|
|
22
|
+
return { stderr: "su: authentication failure", exitCode: 1 };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
switchUser: targetUser,
|
|
27
|
+
nextCwd: `/home/${targetUser}`,
|
|
28
|
+
exitCode: 0,
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { runCommand } from "./index";
|
|
3
|
+
|
|
4
|
+
function parseSudoArgs(args: string[]): {
|
|
5
|
+
targetUser: string;
|
|
6
|
+
loginShell: boolean;
|
|
7
|
+
commandLine: string | null;
|
|
8
|
+
} {
|
|
9
|
+
let targetUser = "root";
|
|
10
|
+
let loginShell = false;
|
|
11
|
+
const commandParts: string[] = [];
|
|
12
|
+
|
|
13
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
14
|
+
const arg = args[index]!;
|
|
15
|
+
|
|
16
|
+
if (arg === "-i") {
|
|
17
|
+
loginShell = true;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (arg === "-S") {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (arg === "-u") {
|
|
26
|
+
targetUser = args[index + 1] ?? "root";
|
|
27
|
+
index += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (arg.startsWith("-u=")) {
|
|
32
|
+
targetUser = arg.slice(3) || "root";
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
commandParts.push(arg);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const commandLine = commandParts.length > 0 ? commandParts.join(" ") : null;
|
|
40
|
+
return { targetUser, loginShell, commandLine };
|
|
41
|
+
}
|
|
42
|
+
export const sudoCommand: ShellModule = {
|
|
43
|
+
name: "sudo",
|
|
44
|
+
params: ["<command...>"],
|
|
45
|
+
run: async ({ authUser, hostname, users, mode, cwd, vfs, args }) => {
|
|
46
|
+
const { targetUser, loginShell, commandLine } = parseSudoArgs(args);
|
|
47
|
+
|
|
48
|
+
if (authUser !== "root" && !users.isSudoer(authUser)) {
|
|
49
|
+
return { stderr: "sudo: permission denied", exitCode: 1 };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const effectiveUser = targetUser || "root";
|
|
53
|
+
const prompt = `[sudo] password for ${authUser}: `;
|
|
54
|
+
|
|
55
|
+
if (authUser === "root") {
|
|
56
|
+
if (!commandLine && loginShell) {
|
|
57
|
+
return {
|
|
58
|
+
switchUser: effectiveUser,
|
|
59
|
+
nextCwd: `/home/${effectiveUser}`,
|
|
60
|
+
exitCode: 0,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!commandLine) {
|
|
65
|
+
return { stderr: "sudo: missing command", exitCode: 1 };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return runCommand(
|
|
69
|
+
commandLine,
|
|
70
|
+
effectiveUser,
|
|
71
|
+
hostname,
|
|
72
|
+
users,
|
|
73
|
+
mode,
|
|
74
|
+
loginShell ? `/home/${effectiveUser}` : cwd,
|
|
75
|
+
vfs,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
sudoChallenge: {
|
|
81
|
+
username: authUser,
|
|
82
|
+
targetUser: effectiveUser,
|
|
83
|
+
commandLine,
|
|
84
|
+
loginShell,
|
|
85
|
+
prompt,
|
|
86
|
+
},
|
|
87
|
+
exitCode: 0,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { resolvePath } from "./helpers";
|
|
3
|
+
|
|
4
|
+
export const touchCommand: ShellModule = {
|
|
5
|
+
name: "touch",
|
|
6
|
+
params: ["<file>"],
|
|
7
|
+
run: ({ vfs, cwd, args }) => {
|
|
8
|
+
if (args.length === 0) {
|
|
9
|
+
return { stderr: "touch: missing file operand", exitCode: 1 };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
for (const file of args) {
|
|
13
|
+
const target = resolvePath(cwd, file);
|
|
14
|
+
if (!vfs.exists(target)) {
|
|
15
|
+
vfs.writeFile(target, "");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return { exitCode: 0 };
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { resolvePath } from "./helpers";
|
|
3
|
+
|
|
4
|
+
export const treeCommand: ShellModule = {
|
|
5
|
+
name: "tree",
|
|
6
|
+
params: ["[path]"],
|
|
7
|
+
run: ({ vfs, cwd, args }) => {
|
|
8
|
+
const target = resolvePath(cwd, args[0] ?? cwd);
|
|
9
|
+
return { stdout: vfs.tree(target), exitCode: 0 };
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import {
|
|
3
|
+
fetchResource,
|
|
4
|
+
parseOutputPath,
|
|
5
|
+
resolvePath,
|
|
6
|
+
stripUrlFilename,
|
|
7
|
+
} from "./helpers";
|
|
8
|
+
|
|
9
|
+
export const wgetCommand: ShellModule = {
|
|
10
|
+
name: "wget",
|
|
11
|
+
params: ["[url]"],
|
|
12
|
+
run: async ({ vfs, cwd, args }) => {
|
|
13
|
+
const { outputPath, inputArgs } = parseOutputPath(args);
|
|
14
|
+
const url = inputArgs[0];
|
|
15
|
+
|
|
16
|
+
if (!url) {
|
|
17
|
+
return { stderr: "wget: missing URL", exitCode: 1 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const result = await fetchResource(url);
|
|
21
|
+
if (result.status >= 400) {
|
|
22
|
+
return { stderr: `wget: HTTP ${result.status}`, exitCode: 8 };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const target = resolvePath(cwd, outputPath ?? stripUrlFilename(url));
|
|
26
|
+
vfs.writeFile(target, result.text);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
stdout: `saved ${target}`,
|
|
30
|
+
exitCode: 0,
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { formatLoginDate } from "../loginFormat";
|
|
3
|
+
|
|
4
|
+
export const whoCommand: ShellModule = {
|
|
5
|
+
name: "who",
|
|
6
|
+
params: [],
|
|
7
|
+
run: ({ users }) => {
|
|
8
|
+
const lines = users.listActiveSessions().map((session) => {
|
|
9
|
+
const loginAt = new Date(session.startedAt);
|
|
10
|
+
const displayDate = Number.isNaN(loginAt.getTime())
|
|
11
|
+
? session.startedAt
|
|
12
|
+
: formatLoginDate(loginAt);
|
|
13
|
+
return `${session.username} ${session.tty} ${displayDate} (${session.remoteAddress || "unknown"})`;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return { stdout: lines.join("\n"), exitCode: 0 };
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ExecStream } from "../types/streams";
|
|
2
|
+
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
3
|
+
import { runCommand } from "./commands";
|
|
4
|
+
import type { VirtualUserManager } from "./users";
|
|
5
|
+
|
|
6
|
+
export function runExec(
|
|
7
|
+
stream: ExecStream,
|
|
8
|
+
cmd: string,
|
|
9
|
+
authUser: string,
|
|
10
|
+
hostname: string,
|
|
11
|
+
users: VirtualUserManager,
|
|
12
|
+
vfs: VirtualFileSystem,
|
|
13
|
+
): void {
|
|
14
|
+
Promise.resolve(
|
|
15
|
+
runCommand(
|
|
16
|
+
cmd,
|
|
17
|
+
authUser,
|
|
18
|
+
hostname,
|
|
19
|
+
users,
|
|
20
|
+
"exec",
|
|
21
|
+
`/home/${authUser}`,
|
|
22
|
+
vfs,
|
|
23
|
+
),
|
|
24
|
+
).then((result) => {
|
|
25
|
+
if (result.stdout) {
|
|
26
|
+
stream.write(`${result.stdout}\n`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (result.stderr) {
|
|
30
|
+
stream.stderr.write(`${result.stderr}\n`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
stream.exit(result.exitCode ?? 0);
|
|
34
|
+
void vfs.flushMirror();
|
|
35
|
+
stream.end();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
export function loadOrCreateHostKey(baseDir: string = process.cwd()): string {
|
|
6
|
+
const hostKeyPath = resolve(baseDir, ".ssh-mimic", "host_rsa");
|
|
7
|
+
|
|
8
|
+
if (existsSync(hostKeyPath)) {
|
|
9
|
+
return readFileSync(hostKeyPath, "utf8");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const privateKey = generateKeyPairSync("rsa", {
|
|
13
|
+
modulusLength: 2048,
|
|
14
|
+
privateKeyEncoding: { type: "pkcs1", format: "pem" },
|
|
15
|
+
publicKeyEncoding: { type: "pkcs1", format: "pem" },
|
|
16
|
+
}).privateKey;
|
|
17
|
+
|
|
18
|
+
mkdirSync(dirname(hostKeyPath), { recursive: true });
|
|
19
|
+
writeFileSync(hostKeyPath, privateKey, { mode: 0o600 });
|
|
20
|
+
return privateKey;
|
|
21
|
+
}
|