typescript-virtual-container 1.5.11 → 1.6.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 +236 -456
- package/dist/.tsbuildinfo +1 -1
- package/dist/Honeypot/index.d.ts +9 -0
- package/dist/Honeypot/index.js +57 -0
- package/dist/SSHMimic/exec.d.ts +4 -0
- package/dist/SSHMimic/exec.js +4 -0
- package/dist/SSHMimic/executor.d.ts +10 -1
- package/dist/SSHMimic/executor.js +18 -8
- package/dist/SSHMimic/hostKey.d.ts +5 -0
- package/dist/SSHMimic/hostKey.js +5 -0
- package/dist/SSHMimic/loginBanner.d.ts +7 -0
- package/dist/SSHMimic/loginBanner.js +4 -0
- package/dist/SSHMimic/loginFormat.d.ts +4 -0
- package/dist/SSHMimic/loginFormat.js +4 -0
- package/dist/SSHMimic/prompt.d.ts +9 -0
- package/dist/SSHMimic/prompt.js +9 -0
- package/dist/SSHMimic/scp.d.ts +18 -0
- package/dist/SSHMimic/scp.js +14 -0
- package/dist/VirtualFileSystem/binaryPack.d.ts +7 -3
- package/dist/VirtualFileSystem/binaryPack.js +32 -10
- package/dist/VirtualFileSystem/index.d.ts +29 -0
- package/dist/VirtualFileSystem/index.js +126 -5
- package/dist/VirtualFileSystem/internalTypes.d.ts +4 -0
- package/dist/VirtualFileSystem/journal.d.ts +10 -4
- package/dist/VirtualFileSystem/journal.js +12 -2
- package/dist/VirtualFileSystem/path.d.ts +23 -1
- package/dist/VirtualFileSystem/path.js +23 -3
- package/dist/VirtualPackageManager/index.js +1 -1
- package/dist/VirtualShell/index.d.ts +3 -0
- package/dist/VirtualShell/index.js +12 -3
- package/dist/VirtualUserManager/index.d.ts +20 -1
- package/dist/VirtualUserManager/index.js +52 -15
- package/dist/commands/bc.d.ts +5 -0
- package/dist/commands/bc.js +5 -0
- package/dist/commands/cat.js +2 -2
- package/dist/commands/chgrp.d.ts +7 -0
- package/dist/commands/chgrp.js +42 -0
- package/dist/commands/chown.d.ts +7 -0
- package/dist/commands/chown.js +79 -0
- package/dist/commands/cp.js +4 -3
- package/dist/commands/dd.d.ts +7 -0
- package/dist/commands/dd.js +60 -0
- package/dist/commands/declare.js +0 -2
- package/dist/commands/expr.d.ts +7 -0
- package/dist/commands/expr.js +63 -0
- package/dist/commands/fun.d.ts +5 -0
- package/dist/commands/fun.js +5 -1
- package/dist/commands/help.d.ts +5 -0
- package/dist/commands/help.js +5 -0
- package/dist/commands/helpers.d.ts +43 -0
- package/dist/commands/helpers.js +61 -0
- package/dist/commands/id.d.ts +5 -0
- package/dist/commands/id.js +5 -0
- package/dist/commands/ip.d.ts +1 -0
- package/dist/commands/ip.js +50 -23
- package/dist/commands/jobs.js +43 -9
- package/dist/commands/kill.d.ts +1 -0
- package/dist/commands/kill.js +13 -5
- package/dist/commands/last.js +1 -1
- package/dist/commands/ln.d.ts +5 -0
- package/dist/commands/ln.js +5 -0
- package/dist/commands/ls.d.ts +5 -0
- package/dist/commands/ls.js +19 -4
- package/dist/commands/lsb-release.js +1 -1
- package/dist/commands/man.d.ts +5 -0
- package/dist/commands/man.js +5 -0
- package/dist/commands/manuals-bundle.js +242 -0
- package/dist/commands/miscutils.d.ts +43 -0
- package/dist/commands/miscutils.js +233 -0
- package/dist/commands/mkdir.js +3 -2
- package/dist/commands/mv.js +4 -3
- package/dist/commands/netcat.d.ts +7 -0
- package/dist/commands/netcat.js +64 -0
- package/dist/commands/nice.d.ts +7 -0
- package/dist/commands/nice.js +22 -0
- package/dist/commands/nohup.d.ts +7 -0
- package/dist/commands/nohup.js +18 -0
- package/dist/commands/ping.d.ts +2 -1
- package/dist/commands/ping.js +46 -8
- package/dist/commands/procUtils.d.ts +13 -0
- package/dist/commands/procUtils.js +72 -0
- package/dist/commands/pwd.d.ts +5 -0
- package/dist/commands/pwd.js +5 -0
- package/dist/commands/python.js +0 -4
- package/dist/commands/read.js +0 -1
- package/dist/commands/registry.d.ts +37 -0
- package/dist/commands/registry.js +73 -0
- package/dist/commands/rm.js +3 -2
- package/dist/commands/runtime.d.ts +47 -1
- package/dist/commands/runtime.js +60 -5
- package/dist/commands/sh.d.ts +5 -0
- package/dist/commands/sh.js +5 -0
- package/dist/commands/stat.js +3 -2
- package/dist/commands/strace.js +0 -1
- package/dist/commands/sysinfo.d.ts +19 -0
- package/dist/commands/sysinfo.js +73 -0
- package/dist/commands/test.d.ts +5 -0
- package/dist/commands/test.js +5 -0
- package/dist/commands/textutils.d.ts +25 -0
- package/dist/commands/textutils.js +171 -0
- package/dist/commands/top.d.ts +7 -0
- package/dist/commands/top.js +54 -0
- package/dist/commands/touch.js +6 -2
- package/dist/commands/tr.d.ts +5 -0
- package/dist/commands/tr.js +5 -0
- package/dist/commands/w.js +1 -1
- package/dist/commands/which.d.ts +5 -0
- package/dist/commands/which.js +5 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +4 -0
- package/dist/modules/VirtualNetworkManager.d.ts +54 -0
- package/dist/modules/VirtualNetworkManager.js +144 -0
- package/dist/modules/linuxRootfs.d.ts +4 -3
- package/dist/modules/linuxRootfs.js +115 -74
- package/dist/modules/neofetch.d.ts +2 -0
- package/dist/modules/neofetch.js +3 -2
- package/dist/modules/pacmanGame.d.ts +2 -0
- package/dist/modules/pacmanGame.js +1 -0
- package/dist/modules/shellInteractive.d.ts +2 -0
- package/dist/modules/shellInteractive.js +2 -0
- package/dist/modules/shellRuntime.d.ts +7 -0
- package/dist/modules/shellRuntime.js +6 -0
- package/dist/modules/webTermRenderer.js +0 -7
- package/dist/types/commands.d.ts +1 -1
- package/dist/types/vfs.d.ts +8 -0
- package/dist/utils/argv.d.ts +22 -3
- package/dist/utils/argv.js +22 -3
- package/dist/utils/perfLogger.d.ts +10 -2
- package/dist/utils/perfLogger.js +7 -14
- package/dist/utils/shellSession.d.ts +35 -0
- package/dist/utils/shellSession.js +35 -0
- package/package.json +1 -1
package/dist/Honeypot/index.js
CHANGED
|
@@ -32,6 +32,15 @@ export class HoneyPot {
|
|
|
32
32
|
clientDisconnects: 0,
|
|
33
33
|
shellFreezes: 0,
|
|
34
34
|
shellThaws: 0,
|
|
35
|
+
keysAdded: 0,
|
|
36
|
+
keysRemoved: 0,
|
|
37
|
+
snapshotsRestored: 0,
|
|
38
|
+
snapshotsImported: 0,
|
|
39
|
+
mounts: 0,
|
|
40
|
+
unmounts: 0,
|
|
41
|
+
symlinksCreated: 0,
|
|
42
|
+
nodesRemoved: 0,
|
|
43
|
+
authLockouts: 0,
|
|
35
44
|
};
|
|
36
45
|
maxLogSize;
|
|
37
46
|
/** Reference kept so VFS events can ping the shell's idle manager. */
|
|
@@ -114,6 +123,33 @@ export class HoneyPot {
|
|
|
114
123
|
vfs.on("mirror:flush", () => {
|
|
115
124
|
this.log("VirtualFileSystem", "mirror:flush", {});
|
|
116
125
|
});
|
|
126
|
+
vfs.on("snapshot:restore", (data) => {
|
|
127
|
+
this.stats.snapshotsRestored++;
|
|
128
|
+
this.log("VirtualFileSystem", "snapshot:restore", data);
|
|
129
|
+
});
|
|
130
|
+
vfs.on("snapshot:import", (data) => {
|
|
131
|
+
this.stats.snapshotsImported++;
|
|
132
|
+
this.log("VirtualFileSystem", "snapshot:import", data);
|
|
133
|
+
});
|
|
134
|
+
vfs.on("mount", (data) => {
|
|
135
|
+
this.stats.mounts++;
|
|
136
|
+
this._shell?.pingIdle();
|
|
137
|
+
this.log("VirtualFileSystem", "mount", data);
|
|
138
|
+
});
|
|
139
|
+
vfs.on("unmount", (data) => {
|
|
140
|
+
this.stats.unmounts++;
|
|
141
|
+
this.log("VirtualFileSystem", "unmount", data);
|
|
142
|
+
});
|
|
143
|
+
vfs.on("symlink:create", (data) => {
|
|
144
|
+
this.stats.symlinksCreated++;
|
|
145
|
+
this._shell?.pingIdle();
|
|
146
|
+
this.log("VirtualFileSystem", "symlink:create", data);
|
|
147
|
+
});
|
|
148
|
+
vfs.on("node:remove", (data) => {
|
|
149
|
+
this.stats.nodesRemoved++;
|
|
150
|
+
this._shell?.pingIdle();
|
|
151
|
+
this.log("VirtualFileSystem", "node:remove", data);
|
|
152
|
+
});
|
|
117
153
|
}
|
|
118
154
|
/**
|
|
119
155
|
* Attaches to VirtualUserManager events.
|
|
@@ -137,6 +173,14 @@ export class HoneyPot {
|
|
|
137
173
|
this.stats.sessionEnds++;
|
|
138
174
|
this.log("VirtualUserManager", "session:unregister", data);
|
|
139
175
|
});
|
|
176
|
+
users.on("key:add", (data) => {
|
|
177
|
+
this.stats.keysAdded++;
|
|
178
|
+
this.log("VirtualUserManager", "key:add", data);
|
|
179
|
+
});
|
|
180
|
+
users.on("key:remove", (data) => {
|
|
181
|
+
this.stats.keysRemoved++;
|
|
182
|
+
this.log("VirtualUserManager", "key:remove", data);
|
|
183
|
+
});
|
|
140
184
|
}
|
|
141
185
|
/**
|
|
142
186
|
* Attaches to SshMimic events.
|
|
@@ -158,6 +202,10 @@ export class HoneyPot {
|
|
|
158
202
|
this.stats.authFailures++;
|
|
159
203
|
this.log("SshMimic", "auth:failure", data);
|
|
160
204
|
});
|
|
205
|
+
ssh.on("auth:lockout", (data) => {
|
|
206
|
+
this.stats.authLockouts++;
|
|
207
|
+
this.log("SshMimic", "auth:lockout", data);
|
|
208
|
+
});
|
|
161
209
|
ssh.on("client:connect", () => {
|
|
162
210
|
this.stats.clientConnects++;
|
|
163
211
|
this.log("SshMimic", "client:connect", {});
|
|
@@ -259,6 +307,15 @@ export class HoneyPot {
|
|
|
259
307
|
clientDisconnects: 0,
|
|
260
308
|
shellFreezes: 0,
|
|
261
309
|
shellThaws: 0,
|
|
310
|
+
keysAdded: 0,
|
|
311
|
+
keysRemoved: 0,
|
|
312
|
+
snapshotsRestored: 0,
|
|
313
|
+
snapshotsImported: 0,
|
|
314
|
+
mounts: 0,
|
|
315
|
+
unmounts: 0,
|
|
316
|
+
symlinksCreated: 0,
|
|
317
|
+
nodesRemoved: 0,
|
|
318
|
+
authLockouts: 0,
|
|
262
319
|
};
|
|
263
320
|
}
|
|
264
321
|
/**
|
package/dist/SSHMimic/exec.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { ExecStream } from "../types/streams";
|
|
2
2
|
import type { VirtualShell } from "../VirtualShell";
|
|
3
|
+
/**
|
|
4
|
+
* Handles SSH exec channel requests. Runs the given command in a non-interactive
|
|
5
|
+
* shell session and writes stdout/stderr to the stream, then signals exit.
|
|
6
|
+
*/
|
|
3
7
|
export declare function runExec(stream: ExecStream, cmd: string, authUser: string, hostname: string, shell: VirtualShell): void;
|
package/dist/SSHMimic/exec.js
CHANGED
|
@@ -5,6 +5,10 @@ function toTtyLines(text) {
|
|
|
5
5
|
.replace(/\r/g, "\n")
|
|
6
6
|
.replace(/\n/g, "\r\n");
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Handles SSH exec channel requests. Runs the given command in a non-interactive
|
|
10
|
+
* shell session and writes stdout/stderr to the stream, then signals exit.
|
|
11
|
+
*/
|
|
8
12
|
export function runExec(stream, cmd, authUser, hostname, shell) {
|
|
9
13
|
Promise.resolve(runCommand(cmd, authUser, hostname, "exec", userHome(authUser), shell, undefined, makeDefaultEnv(authUser, hostname)))
|
|
10
14
|
.then((result) => {
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { CommandMode, CommandResult, ShellEnv } from "../types/commands";
|
|
2
2
|
import type { Pipeline, Statement } from "../types/pipeline";
|
|
3
3
|
import type { VirtualShell } from "../VirtualShell";
|
|
4
|
+
/**
|
|
5
|
+
* Executes a list of shell statements sequentially, respecting `&&`, `||`, and `;`
|
|
6
|
+
* operators. Accumulates stdout across statements and tracks cwd changes.
|
|
7
|
+
*/
|
|
4
8
|
export declare function executeStatements(statements: Statement[], authUser: string, hostname: string, mode: CommandMode, cwd: string, shell: VirtualShell, env: ShellEnv): Promise<CommandResult>;
|
|
5
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Executes a shell pipeline of commands connected by pipes. Handles redirections,
|
|
11
|
+
* input/output files, and stderr redirects. Delegates to the single-command or
|
|
12
|
+
* chained-pipeline executor based on command count.
|
|
13
|
+
*/
|
|
14
|
+
export declare function executePipeline(pipeline: Pipeline, authUser: string, hostname: string, mode: CommandMode, cwd: string, shell: VirtualShell, env?: ShellEnv, abortController?: AbortController): Promise<CommandResult>;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { runCommandDirect } from "../commands";
|
|
2
2
|
import { resolvePath } from "../commands/helpers";
|
|
3
3
|
// ── Script executor (handles &&/||/;) ────────────────────────────────────────
|
|
4
|
+
/**
|
|
5
|
+
* Executes a list of shell statements sequentially, respecting `&&`, `||`, and `;`
|
|
6
|
+
* operators. Accumulates stdout across statements and tracks cwd changes.
|
|
7
|
+
*/
|
|
4
8
|
export async function executeStatements(statements, authUser, hostname, mode, cwd, shell, env) {
|
|
5
9
|
let last = { exitCode: 0 };
|
|
6
10
|
const accumulatedStdout = [];
|
|
@@ -9,9 +13,9 @@ export async function executeStatements(statements, authUser, hostname, mode, cw
|
|
|
9
13
|
while (i < statements.length) {
|
|
10
14
|
const stmt = statements[i];
|
|
11
15
|
if (stmt.background) {
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
// Background job: fire with AbortController so kill can cancel it.
|
|
17
|
+
const ac = new AbortController();
|
|
18
|
+
executePipeline(stmt.pipeline, authUser, hostname, "background", currentCwd, shell, env, ac);
|
|
15
19
|
last = { exitCode: 0 };
|
|
16
20
|
env.lastExitCode = 0;
|
|
17
21
|
i++;
|
|
@@ -58,18 +62,23 @@ export async function executeStatements(statements, authUser, hostname, mode, cw
|
|
|
58
62
|
return { ...last, stdout: merged || last.stdout, nextCwd: currentCwd !== cwd ? currentCwd : undefined };
|
|
59
63
|
}
|
|
60
64
|
// ── Pipeline executor ─────────────────────────────────────────────────────────
|
|
61
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Executes a shell pipeline of commands connected by pipes. Handles redirections,
|
|
67
|
+
* input/output files, and stderr redirects. Delegates to the single-command or
|
|
68
|
+
* chained-pipeline executor based on command count.
|
|
69
|
+
*/
|
|
70
|
+
export async function executePipeline(pipeline, authUser, hostname, mode, cwd, shell, env, abortController) {
|
|
62
71
|
if (!pipeline.isValid)
|
|
63
72
|
return { stderr: pipeline.error || "Syntax error", exitCode: 1 };
|
|
64
73
|
if (pipeline.commands.length === 0)
|
|
65
74
|
return { exitCode: 0 };
|
|
66
75
|
const shellEnv = env ?? { vars: {}, lastExitCode: 0 };
|
|
67
76
|
if (pipeline.commands.length === 1) {
|
|
68
|
-
return executeSingleCommandWithRedirections(pipeline.commands[0], authUser, hostname, mode, cwd, shell, shellEnv);
|
|
77
|
+
return executeSingleCommandWithRedirections(pipeline.commands[0], authUser, hostname, mode, cwd, shell, shellEnv, abortController);
|
|
69
78
|
}
|
|
70
79
|
return executePipelineChain(pipeline.commands, authUser, hostname, mode, cwd, shell, shellEnv);
|
|
71
80
|
}
|
|
72
|
-
async function executeSingleCommandWithRedirections(cmd, authUser, hostname, mode, cwd, shell, env) {
|
|
81
|
+
async function executeSingleCommandWithRedirections(cmd, authUser, hostname, mode, cwd, shell, env, abortController) {
|
|
73
82
|
let stdin;
|
|
74
83
|
if (cmd.inputFile) {
|
|
75
84
|
const inputPath = resolvePath(cwd, cmd.inputFile);
|
|
@@ -83,7 +92,8 @@ async function executeSingleCommandWithRedirections(cmd, authUser, hostname, mod
|
|
|
83
92
|
};
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
|
-
const
|
|
95
|
+
const isBackground = mode === "background";
|
|
96
|
+
const result = await runCommandDirect(cmd.name, cmd.args, authUser, hostname, mode, cwd, shell, stdin, env, isBackground, abortController);
|
|
87
97
|
if (cmd.outputFile) {
|
|
88
98
|
const outputPath = resolvePath(cwd, cmd.outputFile);
|
|
89
99
|
const output = result.stdout || "";
|
|
@@ -149,7 +159,7 @@ async function executePipelineChain(commands, authUser, hostname, mode, cwd, she
|
|
|
149
159
|
} })();
|
|
150
160
|
shell.writeFileAsUser(authUser, sp, cmd.stderrAppend ? ex + effectiveResult.stderr : effectiveResult.stderr);
|
|
151
161
|
}
|
|
152
|
-
catch { }
|
|
162
|
+
catch { /* best-effort stderr write */ }
|
|
153
163
|
}
|
|
154
164
|
if (i === commands.length - 1 && cmd.outputFile) {
|
|
155
165
|
const outputPath = resolvePath(cwd, cmd.outputFile);
|
|
@@ -1 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads an existing PEM-encoded RSA host key from `.ssh-mimic/host_rsa` under
|
|
3
|
+
* the given base directory, or generates a new 2048-bit key pair and persists
|
|
4
|
+
* it to disk. Returns the private key in PEM format.
|
|
5
|
+
*/
|
|
1
6
|
export declare function loadOrCreateHostKey(baseDir?: string): string;
|
package/dist/SSHMimic/hostKey.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { generateKeyPairSync } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Loads an existing PEM-encoded RSA host key from `.ssh-mimic/host_rsa` under
|
|
6
|
+
* the given base directory, or generates a new 2048-bit key pair and persists
|
|
7
|
+
* it to disk. Returns the private key in PEM format.
|
|
8
|
+
*/
|
|
4
9
|
export function loadOrCreateHostKey(baseDir = process.cwd()) {
|
|
5
10
|
const hostKeyPath = resolve(baseDir, ".ssh-mimic", "host_rsa");
|
|
6
11
|
if (existsSync(hostKeyPath)) {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { ShellProperties } from "../VirtualShell";
|
|
2
|
+
/**
|
|
3
|
+
* Tracks the timestamp and origin of the user's last login for the login banner.
|
|
4
|
+
*/
|
|
2
5
|
export interface LoginBannerState {
|
|
3
6
|
at: string;
|
|
4
7
|
from: string;
|
|
5
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Builds the SSH login banner displaying OS info, warranty notice, and the
|
|
11
|
+
* last login timestamp and origin.
|
|
12
|
+
*/
|
|
6
13
|
export declare function buildLoginBanner(hostname: string, properties: ShellProperties, lastLogin: LoginBannerState | null): string;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { formatLoginDate } from "./loginFormat";
|
|
2
|
+
/**
|
|
3
|
+
* Builds the SSH login banner displaying OS info, warranty notice, and the
|
|
4
|
+
* last login timestamp and origin.
|
|
5
|
+
*/
|
|
2
6
|
export function buildLoginBanner(hostname, properties, lastLogin) {
|
|
3
7
|
const lines = [
|
|
4
8
|
`Linux ${hostname} ${properties.kernel} ${properties.arch}`,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a Date object into the SSH last-login display format:
|
|
3
|
+
* e.g. "Mon Jan 02 15:04:05 2024"
|
|
4
|
+
*/
|
|
1
5
|
export function formatLoginDate(date) {
|
|
2
6
|
const weekday = date.toLocaleString("en-US", { weekday: "short" });
|
|
3
7
|
const month = date.toLocaleString("en-US", { month: "short" });
|
|
@@ -1,2 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expands a PS1 template string by replacing escape sequences (\\u, \\h, \\w,
|
|
3
|
+
* \\$, etc.) with the corresponding user, host, and directory values.
|
|
4
|
+
*/
|
|
1
5
|
export declare function expandPs1(ps1: string, user: string, host: string, cwd: string, readlineMode?: boolean): string;
|
|
6
|
+
/**
|
|
7
|
+
* Builds the complete shell prompt string. If a PS1 template is provided it is
|
|
8
|
+
* expanded via expandPs1; otherwise a traditional `[user@host cwd]$` prompt
|
|
9
|
+
* with ANSI color codes is returned.
|
|
10
|
+
*/
|
|
2
11
|
export declare function buildPrompt(user: string, host: string, cwdName: string, ps1?: string, fullCwd?: string, readlineMode?: boolean): string;
|
package/dist/SSHMimic/prompt.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expands a PS1 template string by replacing escape sequences (\\u, \\h, \\w,
|
|
3
|
+
* \\$, etc.) with the corresponding user, host, and directory values.
|
|
4
|
+
*/
|
|
1
5
|
export function expandPs1(ps1, user, host, cwd, readlineMode = false) {
|
|
2
6
|
const home = user === "root" ? "/root" : `/home/${user}`;
|
|
3
7
|
const withTilde = cwd === home ? "~"
|
|
@@ -16,6 +20,11 @@ export function expandPs1(ps1, user, host, cwd, readlineMode = false) {
|
|
|
16
20
|
.replace(/\\n/g, "\n")
|
|
17
21
|
.replace(/\\\\/g, "\\");
|
|
18
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Builds the complete shell prompt string. If a PS1 template is provided it is
|
|
25
|
+
* expanded via expandPs1; otherwise a traditional `[user@host cwd]$` prompt
|
|
26
|
+
* with ANSI color codes is returned.
|
|
27
|
+
*/
|
|
19
28
|
export function buildPrompt(user, host, cwdName, ps1, fullCwd, readlineMode = false) {
|
|
20
29
|
if (ps1)
|
|
21
30
|
return expandPs1(ps1, user, host, fullCwd ?? cwdName, readlineMode);
|
package/dist/SSHMimic/scp.d.ts
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
* Acknowledgement byte: \0 = ok, \1 = warning, \2 = error.
|
|
19
19
|
*/
|
|
20
20
|
import type { VirtualShell } from "../VirtualShell";
|
|
21
|
+
/**
|
|
22
|
+
* Minimal stream interface representing an SSH channel for SCP transfers.
|
|
23
|
+
* Mirrors the ssh2 ServerChannel subset used by the SCP protocol.
|
|
24
|
+
*/
|
|
21
25
|
interface ScpStream {
|
|
22
26
|
write(data: Buffer | string): boolean;
|
|
23
27
|
on(event: "data", listener: (chunk: Buffer) => void): this;
|
|
@@ -28,7 +32,21 @@ interface ScpStream {
|
|
|
28
32
|
exit(code: number): void;
|
|
29
33
|
end(): void;
|
|
30
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Handles SCP sink mode (receiving files from the client). Parses the SCP wire
|
|
37
|
+
* protocol control messages (C, D, E, T) and writes file data into the virtual
|
|
38
|
+
* filesystem.
|
|
39
|
+
*/
|
|
31
40
|
export declare function runScpSink(stream: ScpStream, destArg: string, authUser: string, shell: VirtualShell, recursive: boolean): void;
|
|
41
|
+
/**
|
|
42
|
+
* Handles SCP source mode (sending files to the client). Traverses the virtual
|
|
43
|
+
* filesystem and streams file contents using the SCP wire protocol control
|
|
44
|
+
* messages (C, D, E) in response to client acknowledgements.
|
|
45
|
+
*/
|
|
32
46
|
export declare function runScpSource(stream: ScpStream, srcArg: string, _authUser: string, shell: VirtualShell, recursive: boolean): void;
|
|
47
|
+
/**
|
|
48
|
+
* Handles SCP protocol transfer by parsing the exec arguments to determine
|
|
49
|
+
* sink (upload) or source (download) mode and delegating accordingly.
|
|
50
|
+
*/
|
|
33
51
|
export declare function handleScp(stream: ScpStream, rawArgs: string[], authUser: string, shell: VirtualShell): void;
|
|
34
52
|
export {};
|
package/dist/SSHMimic/scp.js
CHANGED
|
@@ -39,6 +39,11 @@ function parseArgs(args) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
// ── Sink mode (upload: client → server) ──────────────────────────────────────
|
|
42
|
+
/**
|
|
43
|
+
* Handles SCP sink mode (receiving files from the client). Parses the SCP wire
|
|
44
|
+
* protocol control messages (C, D, E, T) and writes file data into the virtual
|
|
45
|
+
* filesystem.
|
|
46
|
+
*/
|
|
42
47
|
export function runScpSink(stream, destArg, authUser, shell, recursive) {
|
|
43
48
|
// Buffered reader that handles arbitrary chunk boundaries
|
|
44
49
|
let buf = Buffer.alloc(0);
|
|
@@ -166,6 +171,11 @@ export function runScpSink(stream, destArg, authUser, shell, recursive) {
|
|
|
166
171
|
});
|
|
167
172
|
}
|
|
168
173
|
// ── Source mode (download: server → client) ───────────────────────────────────
|
|
174
|
+
/**
|
|
175
|
+
* Handles SCP source mode (sending files to the client). Traverses the virtual
|
|
176
|
+
* filesystem and streams file contents using the SCP wire protocol control
|
|
177
|
+
* messages (C, D, E) in response to client acknowledgements.
|
|
178
|
+
*/
|
|
169
179
|
export function runScpSource(stream, srcArg, _authUser, shell, recursive) {
|
|
170
180
|
const srcPath = resolvePath("/", srcArg);
|
|
171
181
|
if (!shell.vfs.exists(srcPath)) {
|
|
@@ -266,6 +276,10 @@ export function runScpSource(stream, srcArg, _authUser, shell, recursive) {
|
|
|
266
276
|
// (processAcks will call sendNext on first ack)
|
|
267
277
|
}
|
|
268
278
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
279
|
+
/**
|
|
280
|
+
* Handles SCP protocol transfer by parsing the exec arguments to determine
|
|
281
|
+
* sink (upload) or source (download) mode and delegating accordingly.
|
|
282
|
+
*/
|
|
269
283
|
export function handleScp(stream, rawArgs, authUser, shell) {
|
|
270
284
|
const { sink, source, recursive, target } = parseArgs(rawArgs);
|
|
271
285
|
if (!sink && !source) {
|
|
@@ -7,13 +7,15 @@
|
|
|
7
7
|
*
|
|
8
8
|
* File header:
|
|
9
9
|
* [4] magic = 0x56 0x46 0x53 0x21 ("VFS!")
|
|
10
|
-
* [1] version =
|
|
10
|
+
* [1] version = 0x02
|
|
11
11
|
*
|
|
12
12
|
* Node (recursive):
|
|
13
13
|
* [1] type = 0x01 (file) | 0x02 (directory)
|
|
14
14
|
* [2] name length (uint16)
|
|
15
15
|
* [N] name bytes (utf8)
|
|
16
16
|
* [4] mode (uint32)
|
|
17
|
+
* [4] uid (uint32)
|
|
18
|
+
* [4] gid (uint32)
|
|
17
19
|
* [8] createdAt ms (float64)
|
|
18
20
|
* [8] updatedAt ms (float64)
|
|
19
21
|
*
|
|
@@ -49,7 +51,9 @@ export declare function forkDirTree(base: InternalDirectoryNode): InternalDirect
|
|
|
49
51
|
*/
|
|
50
52
|
export declare function decodeVfs(buf: Buffer): InternalDirectoryNode;
|
|
51
53
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
+
* Checks whether `buf` starts with the VFS binary magic bytes, indicating a valid
|
|
55
|
+
* binary snapshot produced by {@link encodeVfs}.
|
|
56
|
+
* @param buf - The buffer to inspect.
|
|
57
|
+
* @returns `true` if the buffer begins with the VFS magic header (`"VFS!"`).
|
|
54
58
|
*/
|
|
55
59
|
export declare function isBinarySnapshot(buf: Buffer): boolean;
|
|
@@ -7,13 +7,15 @@
|
|
|
7
7
|
*
|
|
8
8
|
* File header:
|
|
9
9
|
* [4] magic = 0x56 0x46 0x53 0x21 ("VFS!")
|
|
10
|
-
* [1] version =
|
|
10
|
+
* [1] version = 0x02
|
|
11
11
|
*
|
|
12
12
|
* Node (recursive):
|
|
13
13
|
* [1] type = 0x01 (file) | 0x02 (directory)
|
|
14
14
|
* [2] name length (uint16)
|
|
15
15
|
* [N] name bytes (utf8)
|
|
16
16
|
* [4] mode (uint32)
|
|
17
|
+
* [4] uid (uint32)
|
|
18
|
+
* [4] gid (uint32)
|
|
17
19
|
* [8] createdAt ms (float64)
|
|
18
20
|
* [8] updatedAt ms (float64)
|
|
19
21
|
*
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
* Binary pack : ~1.00 MB + ~40 bytes/node header → ~27% smaller, no string parsing
|
|
32
34
|
*/
|
|
33
35
|
const MAGIC = Buffer.from([0x56, 0x46, 0x53, 0x21]); // "VFS!"
|
|
34
|
-
const VERSION =
|
|
36
|
+
const VERSION = 0x02;
|
|
35
37
|
const TYPE_FILE = 0x01;
|
|
36
38
|
const TYPE_DIR = 0x02;
|
|
37
39
|
// ── Encoder ───────────────────────────────────────────────────────────────────
|
|
@@ -79,6 +81,8 @@ function encodeNode(enc, node) {
|
|
|
79
81
|
enc.writeUint8(TYPE_FILE);
|
|
80
82
|
enc.writeString(f.name);
|
|
81
83
|
enc.writeUint32(f.mode);
|
|
84
|
+
enc.writeUint32(f.uid);
|
|
85
|
+
enc.writeUint32(f.gid);
|
|
82
86
|
enc.writeFloat64(f.createdAt);
|
|
83
87
|
enc.writeFloat64(f.updatedAt);
|
|
84
88
|
enc.writeUint8(f.compressed ? 0x01 : 0x00);
|
|
@@ -90,6 +94,8 @@ function encodeNode(enc, node) {
|
|
|
90
94
|
enc.writeUint8(TYPE_FILE);
|
|
91
95
|
enc.writeString(s.name);
|
|
92
96
|
enc.writeUint32(s.mode);
|
|
97
|
+
enc.writeUint32(s.uid);
|
|
98
|
+
enc.writeUint32(s.gid);
|
|
93
99
|
enc.writeFloat64(s.createdAt);
|
|
94
100
|
enc.writeFloat64(s.updatedAt);
|
|
95
101
|
enc.writeUint8(0x00); // not compressed
|
|
@@ -100,6 +106,8 @@ function encodeNode(enc, node) {
|
|
|
100
106
|
enc.writeUint8(TYPE_DIR);
|
|
101
107
|
enc.writeString(d.name);
|
|
102
108
|
enc.writeUint32(d.mode);
|
|
109
|
+
enc.writeUint32(d.uid);
|
|
110
|
+
enc.writeUint32(d.gid);
|
|
103
111
|
enc.writeFloat64(d.createdAt);
|
|
104
112
|
enc.writeFloat64(d.updatedAt);
|
|
105
113
|
const children = Object.values(d.children);
|
|
@@ -160,10 +168,12 @@ class Decoder {
|
|
|
160
168
|
return this.buf.length - this.pos;
|
|
161
169
|
}
|
|
162
170
|
}
|
|
163
|
-
function decodeNode(dec) {
|
|
171
|
+
function decodeNode(dec, includeUidGid) {
|
|
164
172
|
const type = dec.readUint8();
|
|
165
173
|
const name = internName(dec.readString());
|
|
166
174
|
const mode = dec.readUint32();
|
|
175
|
+
const uid = includeUidGid ? dec.readUint32() : 0;
|
|
176
|
+
const gid = includeUidGid ? dec.readUint32() : 0;
|
|
167
177
|
const createdAt = dec.readFloat64();
|
|
168
178
|
const updatedAt = dec.readFloat64();
|
|
169
179
|
if (type === TYPE_FILE) {
|
|
@@ -173,6 +183,8 @@ function decodeNode(dec) {
|
|
|
173
183
|
type: "file",
|
|
174
184
|
name,
|
|
175
185
|
mode,
|
|
186
|
+
uid,
|
|
187
|
+
gid,
|
|
176
188
|
createdAt,
|
|
177
189
|
updatedAt,
|
|
178
190
|
compressed,
|
|
@@ -183,13 +195,15 @@ function decodeNode(dec) {
|
|
|
183
195
|
const count = dec.readUint32();
|
|
184
196
|
const children = Object.create(null);
|
|
185
197
|
for (let i = 0; i < count; i++) {
|
|
186
|
-
const child = decodeNode(dec);
|
|
198
|
+
const child = decodeNode(dec, includeUidGid);
|
|
187
199
|
children[child.name] = child;
|
|
188
200
|
}
|
|
189
201
|
return {
|
|
190
202
|
type: "directory",
|
|
191
203
|
name,
|
|
192
204
|
mode,
|
|
205
|
+
uid,
|
|
206
|
+
gid,
|
|
193
207
|
createdAt,
|
|
194
208
|
updatedAt,
|
|
195
209
|
children,
|
|
@@ -227,6 +241,8 @@ export function forkDirTree(base) {
|
|
|
227
241
|
type: "directory",
|
|
228
242
|
name: base.name,
|
|
229
243
|
mode: base.mode,
|
|
244
|
+
uid: base.uid,
|
|
245
|
+
gid: base.gid,
|
|
230
246
|
createdAt: base.createdAt,
|
|
231
247
|
updatedAt: base.updatedAt,
|
|
232
248
|
children,
|
|
@@ -246,18 +262,24 @@ export function decodeVfs(buf) {
|
|
|
246
262
|
throw new Error("[VFS binary] Invalid magic — not a VFS binary snapshot");
|
|
247
263
|
}
|
|
248
264
|
const dec = new Decoder(buf);
|
|
249
|
-
// skip magic (4)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
265
|
+
// skip magic (4)
|
|
266
|
+
dec.readUint8();
|
|
267
|
+
dec.readUint8();
|
|
268
|
+
dec.readUint8();
|
|
269
|
+
dec.readUint8();
|
|
270
|
+
const version = dec.readUint8();
|
|
271
|
+
const includeUidGid = version >= 0x02;
|
|
272
|
+
const root = decodeNode(dec, includeUidGid);
|
|
253
273
|
if (root.type !== "directory") {
|
|
254
274
|
throw new Error("[VFS binary] Root node must be a directory");
|
|
255
275
|
}
|
|
256
276
|
return root;
|
|
257
277
|
}
|
|
258
278
|
/**
|
|
259
|
-
*
|
|
260
|
-
*
|
|
279
|
+
* Checks whether `buf` starts with the VFS binary magic bytes, indicating a valid
|
|
280
|
+
* binary snapshot produced by {@link encodeVfs}.
|
|
281
|
+
* @param buf - The buffer to inspect.
|
|
282
|
+
* @returns `true` if the buffer begins with the VFS magic header (`"VFS!"`).
|
|
261
283
|
*/
|
|
262
284
|
export function isBinarySnapshot(buf) {
|
|
263
285
|
return buf.length >= 4 && buf.slice(0, 4).equals(MAGIC);
|
|
@@ -88,6 +88,12 @@ declare class VirtualFileSystem extends EventEmitter {
|
|
|
88
88
|
private readonly mounts;
|
|
89
89
|
/** Sorted mounts cache (longest-path-first). Rebuilt lazily on mount/unmount. */
|
|
90
90
|
private _sortedMounts;
|
|
91
|
+
/** Read hooks: path prefix → callback invoked before reading any file under that prefix. */
|
|
92
|
+
private readonly readHooks;
|
|
93
|
+
/** Sorted read hook prefixes (longest-first) for matching. */
|
|
94
|
+
private _sortedReadHooks;
|
|
95
|
+
/** Re-entrancy guard for read hooks — prevents infinite loop when hook triggers another read. */
|
|
96
|
+
private _inReadHook;
|
|
91
97
|
/** True when running in a browser environment (no host FS access). */
|
|
92
98
|
private static readonly isBrowser;
|
|
93
99
|
constructor(options?: VfsOptions);
|
|
@@ -225,6 +231,15 @@ declare class VirtualFileSystem extends EventEmitter {
|
|
|
225
231
|
hostPath: string;
|
|
226
232
|
readOnly: boolean;
|
|
227
233
|
}>;
|
|
234
|
+
/**
|
|
235
|
+
* Register a callback that is invoked before any read under `prefix`.
|
|
236
|
+
* Used by /proc to refresh dynamic content on every access.
|
|
237
|
+
*/
|
|
238
|
+
onBeforeRead(prefix: string, cb: () => void): void;
|
|
239
|
+
/** Remove a previously registered read hook. */
|
|
240
|
+
offBeforeRead(prefix: string): void;
|
|
241
|
+
/** Invoke any matching read hook for `normalizedPath`. */
|
|
242
|
+
private _triggerReadHook;
|
|
228
243
|
/**
|
|
229
244
|
* If `targetPath` is inside a mount, return `{ hostPath, readOnly, relPath }`.
|
|
230
245
|
* `relPath` is the path relative to the mount's host directory.
|
|
@@ -248,6 +263,20 @@ declare class VirtualFileSystem extends EventEmitter {
|
|
|
248
263
|
exists(targetPath: string): boolean;
|
|
249
264
|
/** Updates mode bits on a node. */
|
|
250
265
|
chmod(targetPath: string, mode: number): void;
|
|
266
|
+
/** Changes ownership (uid/gid) of a file or directory. */
|
|
267
|
+
chown(targetPath: string, uid: number, gid: number): void;
|
|
268
|
+
/** Returns the uid and gid of a node. */
|
|
269
|
+
getOwner(targetPath: string): {
|
|
270
|
+
uid: number;
|
|
271
|
+
gid: number;
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* POSIX-style access check: does `uid`/`gid` have `want` permission on `targetPath`?
|
|
275
|
+
* `want` is a bitmask of R_OK (4), W_OK (2), X_OK (1).
|
|
276
|
+
* Root (uid === 0) is granted everything except X_OK without at least one x bit set.
|
|
277
|
+
* Returns true when access is granted.
|
|
278
|
+
*/
|
|
279
|
+
checkAccess(targetPath: string, uid: number, gid: number, want: number): boolean;
|
|
251
280
|
/** Returns metadata for a file or directory. */
|
|
252
281
|
stat(targetPath: string): VfsNodeStats;
|
|
253
282
|
/**
|