typescript-virtual-container 1.1.7 → 1.2.1
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/benchmark-results.txt +40 -0
- package/benchmark-virtualshell.ts +96 -0
- package/dist/Honeypot/index.d.ts.map +1 -1
- package/dist/Honeypot/index.js +9 -0
- package/dist/SSHClient/index.d.ts +0 -14
- package/dist/SSHClient/index.d.ts.map +1 -1
- package/dist/SSHClient/index.js +19 -0
- package/dist/SSHMimic/index.d.ts +0 -7
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +5 -0
- package/dist/SSHMimic/sftp.d.ts.map +1 -1
- package/dist/SSHMimic/sftp.js +5 -0
- package/dist/VirtualFileSystem/index.d.ts +0 -7
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +18 -0
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +14 -1
- package/dist/VirtualUserManager/index.d.ts +4 -1
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +74 -14
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/utils/perfLogger.d.ts +9 -0
- package/dist/utils/perfLogger.d.ts.map +1 -0
- package/dist/utils/perfLogger.js +49 -0
- package/package.json +1 -1
- package/src/Honeypot/index.ts +11 -0
- package/src/SSHClient/index.ts +23 -1
- package/src/SSHMimic/index.ts +6 -0
- package/src/SSHMimic/sftp.ts +8 -1
- package/src/VirtualFileSystem/index.ts +20 -0
- package/src/VirtualShell/index.ts +18 -1
- package/src/VirtualUserManager/index.ts +103 -26
- package/src/index.ts +6 -6
- package/src/utils/perfLogger.ts +72 -0
package/src/SSHClient/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { runCommand } from "../commands";
|
|
2
2
|
import type { CommandResult } from "../types/commands";
|
|
3
|
+
import type { PerfLogger } from "../utils/perfLogger";
|
|
4
|
+
import { createPerfLogger } from "../utils/perfLogger";
|
|
3
5
|
import type { VirtualShell } from "../VirtualShell";
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -16,6 +18,8 @@ import type { VirtualShell } from "../VirtualShell";
|
|
|
16
18
|
* const list = await client.ls();
|
|
17
19
|
* ```
|
|
18
20
|
*/
|
|
21
|
+
const perf: PerfLogger = createPerfLogger("SshClient");
|
|
22
|
+
|
|
19
23
|
export class SshClient {
|
|
20
24
|
private currentCwd = "/";
|
|
21
25
|
|
|
@@ -28,7 +32,9 @@ export class SshClient {
|
|
|
28
32
|
constructor(
|
|
29
33
|
private shell: VirtualShell,
|
|
30
34
|
private username: string,
|
|
31
|
-
) {
|
|
35
|
+
) {
|
|
36
|
+
perf.mark("constructor");
|
|
37
|
+
}
|
|
32
38
|
|
|
33
39
|
/**
|
|
34
40
|
* Executes raw shell command.
|
|
@@ -37,6 +43,7 @@ export class SshClient {
|
|
|
37
43
|
* @returns Command result with stdout/stderr/exitCode.
|
|
38
44
|
*/
|
|
39
45
|
async exec(command: string): Promise<CommandResult> {
|
|
46
|
+
perf.mark("exec");
|
|
40
47
|
const vfs = this.shell.getVfs();
|
|
41
48
|
const users = this.shell.getUsers();
|
|
42
49
|
const hostname = this.shell.getHostname();
|
|
@@ -69,6 +76,7 @@ export class SshClient {
|
|
|
69
76
|
* @returns Result with directory listing in stdout.
|
|
70
77
|
*/
|
|
71
78
|
async ls(path?: string): Promise<CommandResult> {
|
|
79
|
+
perf.mark("ls");
|
|
72
80
|
const target = path ?? ".";
|
|
73
81
|
return this.exec(`ls ${target}`);
|
|
74
82
|
}
|
|
@@ -79,6 +87,7 @@ export class SshClient {
|
|
|
79
87
|
* @returns Result with cwd path in stdout.
|
|
80
88
|
*/
|
|
81
89
|
async pwd(): Promise<CommandResult> {
|
|
90
|
+
perf.mark("pwd");
|
|
82
91
|
return this.exec("pwd");
|
|
83
92
|
}
|
|
84
93
|
|
|
@@ -89,6 +98,7 @@ export class SshClient {
|
|
|
89
98
|
* @returns Result; updates internal cwd on success.
|
|
90
99
|
*/
|
|
91
100
|
async cd(path: string): Promise<CommandResult> {
|
|
101
|
+
perf.mark("cd");
|
|
92
102
|
const result = await this.exec(`cd ${path}`);
|
|
93
103
|
if (result.nextCwd && result.exitCode !== 1) {
|
|
94
104
|
this.currentCwd = result.nextCwd;
|
|
@@ -103,6 +113,7 @@ export class SshClient {
|
|
|
103
113
|
* @returns Result with file content in stdout.
|
|
104
114
|
*/
|
|
105
115
|
async cat(path: string): Promise<CommandResult> {
|
|
116
|
+
perf.mark("cat");
|
|
106
117
|
return this.exec(`cat ${path}`);
|
|
107
118
|
}
|
|
108
119
|
|
|
@@ -114,6 +125,7 @@ export class SshClient {
|
|
|
114
125
|
* @returns Result from mkdir command.
|
|
115
126
|
*/
|
|
116
127
|
async mkdir(path: string, recursive = false): Promise<CommandResult> {
|
|
128
|
+
perf.mark("mkdir");
|
|
117
129
|
const flag = recursive ? "-p " : "";
|
|
118
130
|
return this.exec(`mkdir ${flag}${path}`);
|
|
119
131
|
}
|
|
@@ -125,6 +137,7 @@ export class SshClient {
|
|
|
125
137
|
* @returns Result from touch command.
|
|
126
138
|
*/
|
|
127
139
|
async touch(path: string): Promise<CommandResult> {
|
|
140
|
+
perf.mark("touch");
|
|
128
141
|
return this.exec(`touch ${path}`);
|
|
129
142
|
}
|
|
130
143
|
|
|
@@ -136,6 +149,7 @@ export class SshClient {
|
|
|
136
149
|
* @returns Result from rm command.
|
|
137
150
|
*/
|
|
138
151
|
async rm(path: string, recursive = false): Promise<CommandResult> {
|
|
152
|
+
perf.mark("rm");
|
|
139
153
|
const flag = recursive ? "-r " : "";
|
|
140
154
|
return this.exec(`rm ${flag}${path}`);
|
|
141
155
|
}
|
|
@@ -148,6 +162,7 @@ export class SshClient {
|
|
|
148
162
|
* @returns Result from touch/write simulation.
|
|
149
163
|
*/
|
|
150
164
|
async writeFile(path: string, content: string): Promise<CommandResult> {
|
|
165
|
+
perf.mark("writeFile");
|
|
151
166
|
const vfs = this.shell.getVfs();
|
|
152
167
|
if (!vfs) {
|
|
153
168
|
throw new Error("SSH client not started");
|
|
@@ -171,6 +186,7 @@ export class SshClient {
|
|
|
171
186
|
* @returns File content as string or error in result.
|
|
172
187
|
*/
|
|
173
188
|
async readFile(path: string): Promise<CommandResult> {
|
|
189
|
+
perf.mark("readFile");
|
|
174
190
|
const vfs = this.shell.getVfs();
|
|
175
191
|
if (!vfs) {
|
|
176
192
|
throw new Error("SSH client not started");
|
|
@@ -193,6 +209,7 @@ export class SshClient {
|
|
|
193
209
|
* @returns Normalized cwd path.
|
|
194
210
|
*/
|
|
195
211
|
getCwd(): string {
|
|
212
|
+
perf.mark("getCwd");
|
|
196
213
|
return this.currentCwd;
|
|
197
214
|
}
|
|
198
215
|
|
|
@@ -202,6 +219,7 @@ export class SshClient {
|
|
|
202
219
|
* @returns Associated username.
|
|
203
220
|
*/
|
|
204
221
|
getUsername(): string {
|
|
222
|
+
perf.mark("getUsername");
|
|
205
223
|
return this.username;
|
|
206
224
|
}
|
|
207
225
|
|
|
@@ -212,6 +230,7 @@ export class SshClient {
|
|
|
212
230
|
* @returns Result with ASCII tree in stdout.
|
|
213
231
|
*/
|
|
214
232
|
async tree(path?: string): Promise<CommandResult> {
|
|
233
|
+
perf.mark("tree");
|
|
215
234
|
const target = path ?? ".";
|
|
216
235
|
return this.exec(`tree ${target}`);
|
|
217
236
|
}
|
|
@@ -222,6 +241,7 @@ export class SshClient {
|
|
|
222
241
|
* @returns Result from whoami command.
|
|
223
242
|
*/
|
|
224
243
|
async whoami(): Promise<CommandResult> {
|
|
244
|
+
perf.mark("whoami");
|
|
225
245
|
return this.exec("whoami");
|
|
226
246
|
}
|
|
227
247
|
|
|
@@ -231,6 +251,7 @@ export class SshClient {
|
|
|
231
251
|
* @returns Result from hostname command.
|
|
232
252
|
*/
|
|
233
253
|
async hostname(): Promise<CommandResult> {
|
|
254
|
+
perf.mark("hostname");
|
|
234
255
|
return this.exec("hostname");
|
|
235
256
|
}
|
|
236
257
|
|
|
@@ -240,6 +261,7 @@ export class SshClient {
|
|
|
240
261
|
* @returns Result from who command.
|
|
241
262
|
*/
|
|
242
263
|
async who(): Promise<CommandResult> {
|
|
264
|
+
perf.mark("who");
|
|
243
265
|
return this.exec("who");
|
|
244
266
|
}
|
|
245
267
|
}
|
package/src/SSHMimic/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { Server as SshServer } from "ssh2";
|
|
3
3
|
import { VirtualShell } from "../VirtualShell";
|
|
4
|
+
import { createPerfLogger, type PerfLogger } from "../utils/perfLogger";
|
|
4
5
|
import { runExec } from "./exec";
|
|
5
6
|
import { loadOrCreateHostKey } from "./hostKey";
|
|
6
7
|
|
|
@@ -11,6 +12,8 @@ import { loadOrCreateHostKey } from "./hostKey";
|
|
|
11
12
|
* Create an instance, call {@link SshMimic.start}, and stop it with
|
|
12
13
|
* {@link SshMimic.stop} when your process exits.
|
|
13
14
|
*/
|
|
15
|
+
const perf: PerfLogger = createPerfLogger("SshMimic");
|
|
16
|
+
|
|
14
17
|
class SshMimic extends EventEmitter {
|
|
15
18
|
port: number;
|
|
16
19
|
server: SshServer | null;
|
|
@@ -34,6 +37,7 @@ class SshMimic extends EventEmitter {
|
|
|
34
37
|
shell?: VirtualShell;
|
|
35
38
|
}) {
|
|
36
39
|
super();
|
|
40
|
+
perf.mark("constructor");
|
|
37
41
|
this.port = port;
|
|
38
42
|
this.shellHostname = hostname;
|
|
39
43
|
this.server = null;
|
|
@@ -46,6 +50,7 @@ class SshMimic extends EventEmitter {
|
|
|
46
50
|
* @returns Promise resolved with bound listening port.
|
|
47
51
|
*/
|
|
48
52
|
public async start(): Promise<number> {
|
|
53
|
+
perf.mark("start");
|
|
49
54
|
const shell = this.shell;
|
|
50
55
|
const privateKey = loadOrCreateHostKey();
|
|
51
56
|
|
|
@@ -169,6 +174,7 @@ class SshMimic extends EventEmitter {
|
|
|
169
174
|
* Stops server if running.
|
|
170
175
|
*/
|
|
171
176
|
public stop(): void {
|
|
177
|
+
perf.mark("stop");
|
|
172
178
|
if (this.server) {
|
|
173
179
|
this.server.close(() => {
|
|
174
180
|
console.log("SSH Mimic stopped");
|
package/src/SSHMimic/sftp.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { EventEmitter } from "node:events";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AuthenticationType, KeyboardAuthContext } from "ssh2";
|
|
5
5
|
import { Server as SshServer } from "ssh2";
|
|
6
|
+
import type { VfsNodeStats } from "../types/vfs";
|
|
7
|
+
import type { PerfLogger } from "../utils/perfLogger";
|
|
8
|
+
import { createPerfLogger } from "../utils/perfLogger";
|
|
6
9
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
7
10
|
import { VirtualShell } from "../VirtualShell";
|
|
8
11
|
import type { VirtualUserManager } from "../VirtualUserManager";
|
|
9
|
-
import type { VfsNodeStats } from "../types/vfs";
|
|
10
12
|
import { loadOrCreateHostKey } from "./hostKey";
|
|
11
13
|
|
|
12
14
|
const SFTP_STATUS_CODE = {
|
|
@@ -30,6 +32,8 @@ const OPEN_MODE = {
|
|
|
30
32
|
EXCL: 0x00000020,
|
|
31
33
|
};
|
|
32
34
|
|
|
35
|
+
const perf: PerfLogger = createPerfLogger("SftpMimic");
|
|
36
|
+
|
|
33
37
|
interface SftpFileHandle {
|
|
34
38
|
type: "file";
|
|
35
39
|
path: string;
|
|
@@ -154,6 +158,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
154
158
|
users,
|
|
155
159
|
}: SftpMimicOptions) {
|
|
156
160
|
super();
|
|
161
|
+
perf.mark("constructor");
|
|
157
162
|
this.port = port;
|
|
158
163
|
this.server = null;
|
|
159
164
|
this.hostname = hostname;
|
|
@@ -184,6 +189,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
public async start(): Promise<number> {
|
|
192
|
+
perf.mark("start");
|
|
187
193
|
const privateKey = loadOrCreateHostKey();
|
|
188
194
|
|
|
189
195
|
// Ensure VirtualShell is fully initialized before accepting connections
|
|
@@ -336,6 +342,7 @@ export class SftpMimic extends EventEmitter {
|
|
|
336
342
|
}
|
|
337
343
|
|
|
338
344
|
public stop(): void {
|
|
345
|
+
perf.mark("stop");
|
|
339
346
|
if (this.server) {
|
|
340
347
|
this.server.close(() => {
|
|
341
348
|
console.log("SFTP Mimic stopped");
|
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
VfsNodeStats,
|
|
8
8
|
WriteFileOptions,
|
|
9
9
|
} from "../types/vfs";
|
|
10
|
+
import type { PerfLogger } from "../utils/perfLogger";
|
|
11
|
+
import { createPerfLogger } from "../utils/perfLogger";
|
|
10
12
|
import { normalizePath } from "./path";
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -16,6 +18,8 @@ import { normalizePath } from "./path";
|
|
|
16
18
|
* {@link VirtualFileSystem.restoreMirror} on startup and
|
|
17
19
|
* {@link VirtualFileSystem.flushMirror} to persist pending changes.
|
|
18
20
|
*/
|
|
21
|
+
const perf: PerfLogger = createPerfLogger("VirtualFileSystem");
|
|
22
|
+
|
|
19
23
|
class VirtualFileSystem extends EventEmitter {
|
|
20
24
|
private readonly mirrorRoot: string;
|
|
21
25
|
|
|
@@ -95,6 +99,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
95
99
|
*/
|
|
96
100
|
constructor(baseDir: string = process.cwd()) {
|
|
97
101
|
super();
|
|
102
|
+
perf.mark("constructor");
|
|
98
103
|
this.mirrorRoot = path.resolve(baseDir, ".vfs", "mirror");
|
|
99
104
|
}
|
|
100
105
|
|
|
@@ -104,6 +109,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
104
109
|
* If archive does not exist or cannot be read, creates fresh mirror file.
|
|
105
110
|
*/
|
|
106
111
|
public async restoreMirror(): Promise<void> {
|
|
112
|
+
perf.mark("restoreMirror");
|
|
107
113
|
this.ensureMirrorRoot();
|
|
108
114
|
}
|
|
109
115
|
|
|
@@ -113,6 +119,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
113
119
|
* No-op when nothing changed and archive already exists.
|
|
114
120
|
*/
|
|
115
121
|
public async flushMirror(): Promise<void> {
|
|
122
|
+
perf.mark("flushMirror");
|
|
116
123
|
this.ensureMirrorRoot();
|
|
117
124
|
this.emit("mirror:flush");
|
|
118
125
|
}
|
|
@@ -124,6 +131,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
124
131
|
* @param mode POSIX-like mode bits for new directories.
|
|
125
132
|
*/
|
|
126
133
|
public mkdir(targetPath: string, mode: number = 0o755): void {
|
|
134
|
+
perf.mark("mkdir");
|
|
127
135
|
this.ensureMirrorRoot();
|
|
128
136
|
const fsPath = this.resolveFsPath(targetPath);
|
|
129
137
|
if (fs.existsSync(fsPath) && !fs.statSync(fsPath).isDirectory()) {
|
|
@@ -149,6 +157,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
149
157
|
content: string | Buffer,
|
|
150
158
|
options: WriteFileOptions = {},
|
|
151
159
|
): void {
|
|
160
|
+
perf.mark("writeFile");
|
|
152
161
|
this.ensureMirrorRoot();
|
|
153
162
|
const normalized = normalizePath(targetPath);
|
|
154
163
|
const fsPath = this.resolveFsPath(normalized);
|
|
@@ -181,6 +190,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
181
190
|
* @returns UTF-8 string content.
|
|
182
191
|
*/
|
|
183
192
|
public readFile(targetPath: string): string {
|
|
193
|
+
perf.mark("readFile");
|
|
184
194
|
this.ensureMirrorRoot();
|
|
185
195
|
const fsPath = this.resolveFsPath(targetPath);
|
|
186
196
|
if (!fs.existsSync(fsPath) || !fs.statSync(fsPath).isFile()) {
|
|
@@ -201,6 +211,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
201
211
|
* @returns True when file or directory exists.
|
|
202
212
|
*/
|
|
203
213
|
public exists(targetPath: string): boolean {
|
|
214
|
+
perf.mark("exists");
|
|
204
215
|
try {
|
|
205
216
|
const fsPath = this.resolveFsPath(targetPath);
|
|
206
217
|
return fs.existsSync(fsPath);
|
|
@@ -216,6 +227,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
216
227
|
* @param mode New POSIX-like mode.
|
|
217
228
|
*/
|
|
218
229
|
public chmod(targetPath: string, mode: number): void {
|
|
230
|
+
perf.mark("chmod");
|
|
219
231
|
const fsPath = this.resolveFsPath(targetPath);
|
|
220
232
|
if (!fs.existsSync(fsPath)) {
|
|
221
233
|
throw new Error(`Path '${normalizePath(targetPath)}' does not exist.`);
|
|
@@ -230,6 +242,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
230
242
|
* @returns Typed stat object based on node type.
|
|
231
243
|
*/
|
|
232
244
|
public stat(targetPath: string): VfsNodeStats {
|
|
245
|
+
perf.mark("stat");
|
|
233
246
|
this.ensureMirrorRoot();
|
|
234
247
|
const normalized = normalizePath(targetPath);
|
|
235
248
|
const fsPath = this.resolveFsPath(normalized);
|
|
@@ -273,6 +286,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
273
286
|
* @returns Sorted child names.
|
|
274
287
|
*/
|
|
275
288
|
public list(dirPath: string = "/"): string[] {
|
|
289
|
+
perf.mark("list");
|
|
276
290
|
const fsPath = this.resolveFsPath(dirPath);
|
|
277
291
|
if (!fs.existsSync(fsPath) || !fs.statSync(fsPath).isDirectory()) {
|
|
278
292
|
throw new Error(`Cannot list '${dirPath}': not a directory.`);
|
|
@@ -288,6 +302,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
288
302
|
* @returns Multi-line tree string.
|
|
289
303
|
*/
|
|
290
304
|
public tree(dirPath: string = "/"): string {
|
|
305
|
+
perf.mark("tree");
|
|
291
306
|
const fsPath = this.resolveFsPath(dirPath);
|
|
292
307
|
if (!fs.existsSync(fsPath) || !fs.statSync(fsPath).isDirectory()) {
|
|
293
308
|
throw new Error(`Cannot render tree for '${dirPath}': not a directory.`);
|
|
@@ -308,6 +323,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
308
323
|
* @returns Total byte usage for file content under target path.
|
|
309
324
|
*/
|
|
310
325
|
public getUsageBytes(targetPath: string = "/"): number {
|
|
326
|
+
perf.mark("getUsageBytes");
|
|
311
327
|
const fsPath = this.resolveFsPath(targetPath);
|
|
312
328
|
if (!fs.existsSync(fsPath)) {
|
|
313
329
|
throw new Error(`Path '${normalizePath(targetPath)}' does not exist.`);
|
|
@@ -321,6 +337,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
321
337
|
* @param targetPath Path to file.
|
|
322
338
|
*/
|
|
323
339
|
public compressFile(targetPath: string): void {
|
|
340
|
+
perf.mark("compressFile");
|
|
324
341
|
const fsPath = this.resolveFsPath(targetPath);
|
|
325
342
|
if (!fs.existsSync(fsPath) || !fs.statSync(fsPath).isFile()) {
|
|
326
343
|
throw new Error(`Cannot compress '${targetPath}': not a file.`);
|
|
@@ -338,6 +355,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
338
355
|
* @param targetPath Path to file.
|
|
339
356
|
*/
|
|
340
357
|
public decompressFile(targetPath: string): void {
|
|
358
|
+
perf.mark("decompressFile");
|
|
341
359
|
const fsPath = this.resolveFsPath(targetPath);
|
|
342
360
|
if (!fs.existsSync(fsPath) || !fs.statSync(fsPath).isFile()) {
|
|
343
361
|
throw new Error(`Cannot decompress '${targetPath}': not a file.`);
|
|
@@ -356,6 +374,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
356
374
|
* @param options Removal options, including recursive delete.
|
|
357
375
|
*/
|
|
358
376
|
public remove(targetPath: string, options: RemoveOptions = {}): void {
|
|
377
|
+
perf.mark("remove");
|
|
359
378
|
const normalized = normalizePath(targetPath);
|
|
360
379
|
if (normalized === "/") {
|
|
361
380
|
throw new Error("Cannot remove root directory.");
|
|
@@ -390,6 +409,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
390
409
|
* @param toPath Destination path.
|
|
391
410
|
*/
|
|
392
411
|
public move(fromPath: string, toPath: string): void {
|
|
412
|
+
perf.mark("move");
|
|
393
413
|
const fromNormalized = normalizePath(fromPath);
|
|
394
414
|
const toNormalized = normalizePath(toPath);
|
|
395
415
|
|
|
@@ -3,6 +3,8 @@ import { EventEmitter } from "node:events";
|
|
|
3
3
|
import { createCustomCommand, registerCommand, runCommand } from "../commands";
|
|
4
4
|
import type { CommandContext, CommandResult } from "../types/commands";
|
|
5
5
|
import type { ShellStream } from "../types/streams";
|
|
6
|
+
import type { PerfLogger } from "../utils/perfLogger";
|
|
7
|
+
import { createPerfLogger } from "../utils/perfLogger";
|
|
6
8
|
import VirtualFileSystem from "../VirtualFileSystem";
|
|
7
9
|
import { VirtualUserManager } from "../VirtualUserManager";
|
|
8
10
|
import { startShell } from "./shell";
|
|
@@ -19,13 +21,23 @@ const defaultShellProperties: ShellProperties = {
|
|
|
19
21
|
arch: "x86_64",
|
|
20
22
|
};
|
|
21
23
|
|
|
24
|
+
const perf: PerfLogger = createPerfLogger("VirtualShell");
|
|
25
|
+
|
|
26
|
+
let cachedRootPassword: string | null = null;
|
|
27
|
+
|
|
22
28
|
function resolveRootPassword(): string {
|
|
29
|
+
if (cachedRootPassword) {
|
|
30
|
+
return cachedRootPassword;
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
const configured = process.env.SSH_MIMIC_ROOT_PASSWORD;
|
|
24
34
|
if (configured && configured.trim().length > 0) {
|
|
25
|
-
|
|
35
|
+
cachedRootPassword = configured.trim();
|
|
36
|
+
return cachedRootPassword;
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
const generated = randomBytes(18).toString("base64url");
|
|
40
|
+
cachedRootPassword = generated;
|
|
29
41
|
console.warn(
|
|
30
42
|
`[ssh-mimic] SSH_MIMIC_ROOT_PASSWORD missing; generated ephemeral root password: ${generated}`,
|
|
31
43
|
);
|
|
@@ -68,6 +80,7 @@ class VirtualShell extends EventEmitter {
|
|
|
68
80
|
basePath?: string,
|
|
69
81
|
) {
|
|
70
82
|
super();
|
|
83
|
+
perf.mark("constructor");
|
|
71
84
|
this.hostname = hostname;
|
|
72
85
|
this.properties = properties || defaultShellProperties;
|
|
73
86
|
this.basePath = basePath || ".";
|
|
@@ -95,6 +108,7 @@ class VirtualShell extends EventEmitter {
|
|
|
95
108
|
* Call this before any authentication or command execution.
|
|
96
109
|
*/
|
|
97
110
|
public async ensureInitialized(): Promise<void> {
|
|
111
|
+
perf.mark("ensureInitialized");
|
|
98
112
|
await this.initialized;
|
|
99
113
|
}
|
|
100
114
|
|
|
@@ -126,6 +140,7 @@ class VirtualShell extends EventEmitter {
|
|
|
126
140
|
* @param cwd
|
|
127
141
|
*/
|
|
128
142
|
executeCommand(rawInput: string, authUser: string, cwd: string): void {
|
|
143
|
+
perf.mark("executeCommand");
|
|
129
144
|
runCommand(rawInput, authUser, this.hostname, "shell", cwd, this);
|
|
130
145
|
this.emit("command", { command: rawInput, user: authUser, cwd });
|
|
131
146
|
}
|
|
@@ -146,6 +161,7 @@ class VirtualShell extends EventEmitter {
|
|
|
146
161
|
remoteAddress: string,
|
|
147
162
|
terminalSize: { cols: number; rows: number },
|
|
148
163
|
): void {
|
|
164
|
+
perf.mark("startInteractiveSession");
|
|
149
165
|
// Interactive shell logic
|
|
150
166
|
this.emit("session:start", { user: authUser, sessionId, remoteAddress });
|
|
151
167
|
startShell(
|
|
@@ -199,6 +215,7 @@ class VirtualShell extends EventEmitter {
|
|
|
199
215
|
targetPath: string,
|
|
200
216
|
content: string | Buffer,
|
|
201
217
|
): void {
|
|
218
|
+
perf.mark("writeFileAsUser");
|
|
202
219
|
this.users.assertWriteWithinQuota(authUser, targetPath, content);
|
|
203
220
|
this.vfs.writeFile(targetPath, content);
|
|
204
221
|
}
|