typescript-virtual-container 1.2.7 → 1.2.9
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 +457 -42
- package/dist/SSHMimic/executor.js +3 -5
- package/dist/VirtualFileSystem/binaryPack.d.ts +49 -0
- package/dist/VirtualFileSystem/binaryPack.d.ts.map +1 -0
- package/dist/VirtualFileSystem/binaryPack.js +193 -0
- package/dist/VirtualFileSystem/index.d.ts +7 -5
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +20 -9
- package/dist/VirtualPackageManager/index.d.ts +202 -0
- package/dist/VirtualPackageManager/index.d.ts.map +1 -0
- package/dist/VirtualPackageManager/index.js +676 -0
- package/dist/VirtualShell/index.d.ts +87 -12
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +83 -12
- package/dist/VirtualUserManager/index.d.ts +52 -20
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +54 -20
- package/dist/commands/alias.d.ts +4 -0
- package/dist/commands/alias.d.ts.map +1 -0
- package/dist/commands/alias.js +58 -0
- package/dist/commands/apt.d.ts +4 -0
- package/dist/commands/apt.d.ts.map +1 -0
- package/dist/commands/apt.js +182 -0
- package/dist/commands/cat.d.ts.map +1 -1
- package/dist/commands/cat.js +27 -8
- package/dist/commands/chmod.d.ts.map +1 -1
- package/dist/commands/chmod.js +52 -3
- package/dist/commands/command-helpers.d.ts +78 -4
- package/dist/commands/command-helpers.d.ts.map +1 -1
- package/dist/commands/command-helpers.js +78 -4
- package/dist/commands/curl.d.ts.map +1 -1
- package/dist/commands/curl.js +81 -29
- package/dist/commands/dpkg.d.ts +4 -0
- package/dist/commands/dpkg.d.ts.map +1 -0
- package/dist/commands/dpkg.js +144 -0
- package/dist/commands/echo.d.ts.map +1 -1
- package/dist/commands/echo.js +24 -12
- package/dist/commands/free.d.ts +3 -0
- package/dist/commands/free.d.ts.map +1 -0
- package/dist/commands/free.js +38 -0
- package/dist/commands/helpers.d.ts +3 -0
- package/dist/commands/helpers.d.ts.map +1 -1
- package/dist/commands/helpers.js +3 -0
- package/dist/commands/history.d.ts +3 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +21 -0
- package/dist/commands/index.d.ts +8 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +120 -11
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +4 -3
- package/dist/commands/lsb-release.d.ts +3 -0
- package/dist/commands/lsb-release.d.ts.map +1 -0
- package/dist/commands/lsb-release.js +50 -0
- package/dist/commands/man.d.ts +3 -0
- package/dist/commands/man.d.ts.map +1 -0
- package/dist/commands/man.js +155 -0
- package/dist/commands/neofetch.d.ts.map +1 -1
- package/dist/commands/neofetch.js +5 -0
- package/dist/commands/ping.d.ts.map +1 -1
- package/dist/commands/ping.js +5 -2
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +27 -6
- package/dist/commands/sh.d.ts.map +1 -1
- package/dist/commands/sh.js +29 -11
- package/dist/commands/source.d.ts +3 -0
- package/dist/commands/source.d.ts.map +1 -0
- package/dist/commands/source.js +31 -0
- package/dist/commands/test.d.ts +3 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +92 -0
- package/dist/commands/type.d.ts +3 -0
- package/dist/commands/type.d.ts.map +1 -0
- package/dist/commands/type.js +34 -0
- package/dist/commands/uptime.d.ts +3 -0
- package/dist/commands/uptime.d.ts.map +1 -0
- package/dist/commands/uptime.js +40 -0
- package/dist/commands/wget.d.ts.map +1 -1
- package/dist/commands/wget.js +71 -100
- package/dist/commands/which.d.ts +3 -0
- package/dist/commands/which.d.ts.map +1 -0
- package/dist/commands/which.js +32 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/modules/linuxRootfs.d.ts +24 -0
- package/dist/modules/linuxRootfs.d.ts.map +1 -0
- package/dist/modules/linuxRootfs.js +297 -0
- package/dist/modules/neofetch.d.ts.map +1 -1
- package/dist/modules/neofetch.js +1 -0
- package/dist/standalone.js +4 -1
- package/package.json +2 -1
- package/src/SSHMimic/executor.ts +3 -5
- package/src/VirtualFileSystem/binaryPack.ts +219 -0
- package/src/VirtualFileSystem/index.ts +21 -11
- package/src/VirtualPackageManager/index.ts +820 -0
- package/src/VirtualShell/index.ts +104 -13
- package/src/VirtualUserManager/index.ts +55 -20
- package/src/commands/alias.ts +60 -0
- package/src/commands/apt.ts +198 -0
- package/src/commands/cat.ts +32 -8
- package/src/commands/chmod.ts +48 -3
- package/src/commands/command-helpers.ts +78 -4
- package/src/commands/curl.ts +78 -37
- package/src/commands/dpkg.ts +158 -0
- package/src/commands/echo.ts +30 -14
- package/src/commands/free.ts +40 -0
- package/src/commands/helpers.ts +8 -0
- package/src/commands/history.ts +29 -0
- package/src/commands/index.ts +116 -11
- package/src/commands/ls.ts +5 -4
- package/src/commands/lsb-release.ts +52 -0
- package/src/commands/man.ts +166 -0
- package/src/commands/neofetch.ts +5 -0
- package/src/commands/ping.ts +5 -2
- package/src/commands/ps.ts +28 -6
- package/src/commands/sh.ts +33 -11
- package/src/commands/source.ts +35 -0
- package/src/commands/test.ts +100 -0
- package/src/commands/type.ts +40 -0
- package/src/commands/uptime.ts +46 -0
- package/src/commands/wget.ts +70 -123
- package/src/commands/which.ts +34 -0
- package/src/index.ts +10 -0
- package/src/modules/linuxRootfs.ts +439 -0
- package/src/modules/neofetch.ts +1 -0
- package/src/standalone.ts +4 -1
- package/standalone.js +418 -103
- package/standalone.js.map +4 -4
- package/tests/new-features.test.ts +626 -0
|
@@ -6,11 +6,32 @@ import type { PerfLogger } from "../utils/perfLogger";
|
|
|
6
6
|
import { createPerfLogger } from "../utils/perfLogger";
|
|
7
7
|
import VirtualFileSystem, { type VfsOptions } from "../VirtualFileSystem";
|
|
8
8
|
import { VirtualUserManager } from "../VirtualUserManager";
|
|
9
|
+
import { VirtualPackageManager } from "../VirtualPackageManager";
|
|
10
|
+
import { bootstrapLinuxRootfs, refreshProc, syncEtcPasswd } from "../modules/linuxRootfs";
|
|
9
11
|
import { startShell } from "./shell";
|
|
10
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Virtual machine identity strings surfaced by system-info commands
|
|
15
|
+
* (`uname`, `neofetch`, `lsb_release`, `/proc/version`, `/etc/os-release`).
|
|
16
|
+
*
|
|
17
|
+
* Pass this as the second argument to `new VirtualShell()` to customise the
|
|
18
|
+
* distro name, kernel version, and CPU architecture reported inside the shell.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const shell = new VirtualShell("my-vm", {
|
|
23
|
+
* kernel: "6.1.0+custom-amd64",
|
|
24
|
+
* os: "Acme GNU/Linux x64",
|
|
25
|
+
* arch: "x86_64",
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
11
29
|
export interface ShellProperties {
|
|
30
|
+
/** Kernel version string (e.g. `"1.0.0+itsrealfortune+1-amd64"`). */
|
|
12
31
|
kernel: string;
|
|
32
|
+
/** Full OS description (e.g. `"Fortune GNU/Linux x64"`). */
|
|
13
33
|
os: string;
|
|
34
|
+
/** CPU architecture label (e.g. `"x86_64"`, `"aarch64"`). */
|
|
14
35
|
arch: string;
|
|
15
36
|
}
|
|
16
37
|
|
|
@@ -32,16 +53,40 @@ function resolveAutoSudoForNewUsers(): boolean {
|
|
|
32
53
|
}
|
|
33
54
|
|
|
34
55
|
/**
|
|
35
|
-
* Coordinates the virtual filesystem, user manager,
|
|
56
|
+
* Coordinates the virtual filesystem, user manager, package manager, and
|
|
57
|
+
* command runtime for a single isolated shell environment.
|
|
58
|
+
*
|
|
59
|
+
* Each instance owns its own VFS tree, user database, package registry, and
|
|
60
|
+
* session state — multiple instances are fully independent.
|
|
61
|
+
*
|
|
62
|
+
* Instances are consumed both by the SSH/SFTP server facades and directly via
|
|
63
|
+
* the programmatic `SshClient` API.
|
|
36
64
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const shell = new VirtualShell("my-vm");
|
|
68
|
+
* await shell.ensureInitialized();
|
|
69
|
+
* const client = new SshClient(shell, "root");
|
|
70
|
+
* const result = await client.exec("uname -a");
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* @fires VirtualShell#initialized Emitted once the VFS and users are ready.
|
|
74
|
+
* @fires VirtualShell#command Emitted after every command execution.
|
|
75
|
+
* @fires VirtualShell#session:start Emitted when an interactive session opens.
|
|
39
76
|
*/
|
|
40
77
|
class VirtualShell extends EventEmitter {
|
|
78
|
+
/** Backing virtual filesystem — use for direct path operations. */
|
|
41
79
|
vfs: VirtualFileSystem;
|
|
80
|
+
/** Virtual user database — use for auth, quotas, and session tracking. */
|
|
42
81
|
users: VirtualUserManager;
|
|
82
|
+
/** APT/dpkg package manager backed by the built-in package registry. */
|
|
83
|
+
packageManager: VirtualPackageManager;
|
|
84
|
+
/** Hostname shown in the shell prompt and SSH ident string. */
|
|
43
85
|
hostname: string;
|
|
86
|
+
/** Distro identity strings surfaced by `uname`, `neofetch`, etc. */
|
|
44
87
|
properties: ShellProperties;
|
|
88
|
+
/** Unix ms timestamp of shell creation — used by `uptime` and `/proc/uptime`. */
|
|
89
|
+
startTime: number;
|
|
45
90
|
private initialized: Promise<void>;
|
|
46
91
|
|
|
47
92
|
/**
|
|
@@ -60,17 +105,27 @@ class VirtualShell extends EventEmitter {
|
|
|
60
105
|
perf.mark("constructor");
|
|
61
106
|
this.hostname = hostname;
|
|
62
107
|
this.properties = properties || defaultShellProperties;
|
|
108
|
+
this.startTime = Date.now();
|
|
63
109
|
this.vfs = new VirtualFileSystem(vfsOptions ?? {});
|
|
64
110
|
this.users = new VirtualUserManager(this.vfs, resolveAutoSudoForNewUsers());
|
|
111
|
+
this.packageManager = new VirtualPackageManager(this.vfs, this.users);
|
|
65
112
|
|
|
66
113
|
// Store references to avoid TypeScript "used before assigned" errors
|
|
67
114
|
const vfs = this.vfs;
|
|
68
115
|
const users = this.users;
|
|
116
|
+
const pm = this.packageManager;
|
|
117
|
+
const shellProps = this.properties;
|
|
118
|
+
const shellHostname = this.hostname;
|
|
119
|
+
const startTime = this.startTime;
|
|
69
120
|
|
|
70
121
|
// Initialize both VFS mirror and users, ensuring all is ready before auth
|
|
71
122
|
this.initialized = (async () => {
|
|
72
123
|
await vfs.restoreMirror();
|
|
73
124
|
await users.initialize();
|
|
125
|
+
// Bootstrap Linux rootfs (idempotent)
|
|
126
|
+
bootstrapLinuxRootfs(vfs, users, shellHostname, shellProps, startTime);
|
|
127
|
+
// Load installed packages from dpkg status
|
|
128
|
+
pm.load();
|
|
74
129
|
this.emit("initialized");
|
|
75
130
|
})();
|
|
76
131
|
}
|
|
@@ -105,11 +160,16 @@ class VirtualShell extends EventEmitter {
|
|
|
105
160
|
}
|
|
106
161
|
|
|
107
162
|
/**
|
|
108
|
-
* Executes a command line string
|
|
163
|
+
* Executes a raw command line string programmatically.
|
|
164
|
+
*
|
|
165
|
+
* Supports the full shell operator set (`&&`, `||`, `;`, `|`, `>`, `<`,
|
|
166
|
+
* `$(cmd)`) and alias expansion. The result is emitted via the
|
|
167
|
+
* `"command"` event but not returned — use `SshClient.exec()` for a
|
|
168
|
+
* result-returning wrapper.
|
|
109
169
|
*
|
|
110
|
-
* @param rawInput
|
|
111
|
-
* @param authUser
|
|
112
|
-
* @param cwd
|
|
170
|
+
* @param rawInput Unparsed command line (e.g. `"ls -la /tmp"`).
|
|
171
|
+
* @param authUser Username to run the command as.
|
|
172
|
+
* @param cwd Current working directory for path resolution.
|
|
113
173
|
*/
|
|
114
174
|
executeCommand(rawInput: string, authUser: string, cwd: string): void {
|
|
115
175
|
perf.mark("executeCommand");
|
|
@@ -118,14 +178,19 @@ class VirtualShell extends EventEmitter {
|
|
|
118
178
|
}
|
|
119
179
|
|
|
120
180
|
/**
|
|
121
|
-
*
|
|
181
|
+
* Attaches an interactive PTY session to this shell instance.
|
|
182
|
+
*
|
|
183
|
+
* Called internally by `SshMimic` when a client opens a shell channel.
|
|
184
|
+
* The session reads from `stream` (user keystrokes) and writes back ANSI
|
|
185
|
+
* output. History, `.bashrc` sourcing, and Ctrl+W/Ctrl+U line editing are
|
|
186
|
+
* handled automatically.
|
|
122
187
|
*
|
|
123
|
-
* @param stream
|
|
124
|
-
* @param authUser
|
|
125
|
-
* @param sessionId
|
|
126
|
-
* @param remoteAddress
|
|
188
|
+
* @param stream Bidirectional SSH channel stream.
|
|
189
|
+
* @param authUser Authenticated username bound to this session.
|
|
190
|
+
* @param sessionId Stable session UUID (used for `who` output), or `null`.
|
|
191
|
+
* @param remoteAddress IP or hostname of the connecting client.
|
|
192
|
+
* @param terminalSize Initial terminal dimensions in columns and rows.
|
|
127
193
|
*/
|
|
128
|
-
|
|
129
194
|
startInteractiveSession(
|
|
130
195
|
stream: ShellStream,
|
|
131
196
|
authUser: string,
|
|
@@ -148,6 +213,32 @@ class VirtualShell extends EventEmitter {
|
|
|
148
213
|
);
|
|
149
214
|
}
|
|
150
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Refreshes the `/proc` virtual filesystem with current system state.
|
|
218
|
+
*
|
|
219
|
+
* Updates `/proc/uptime`, `/proc/meminfo`, `/proc/cpuinfo`,
|
|
220
|
+
* `/proc/version`, and `/proc/loadavg` from live host data.
|
|
221
|
+
*
|
|
222
|
+
* Called automatically during `bootstrapLinuxRootfs`. Call again before
|
|
223
|
+
* reading `/proc` files for up-to-date values (e.g. before `neofetch`
|
|
224
|
+
* or `free` in long-running processes).
|
|
225
|
+
*/
|
|
226
|
+
public refreshProcFs(): void {
|
|
227
|
+
refreshProc(this.vfs, this.properties, this.hostname, this.startTime);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Syncs `/etc/passwd`, `/etc/group`, and `/etc/shadow` from the current
|
|
232
|
+
* `VirtualUserManager` state.
|
|
233
|
+
*
|
|
234
|
+
* Called automatically during `bootstrapLinuxRootfs`. Call again after
|
|
235
|
+
* `users.addUser()`, `users.deleteUser()`, or `users.addSudoer()` to keep
|
|
236
|
+
* the classic Unix credential files in sync with the user manager.
|
|
237
|
+
*/
|
|
238
|
+
public syncPasswd(): void {
|
|
239
|
+
syncEtcPasswd(this.vfs, this.users);
|
|
240
|
+
}
|
|
241
|
+
|
|
151
242
|
/**
|
|
152
243
|
* Returns virtual filesystem instance after server started.
|
|
153
244
|
*
|
|
@@ -290,10 +290,11 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
/**
|
|
293
|
-
* Updates password for an existing user account.
|
|
293
|
+
* Updates the password for an existing user account.
|
|
294
294
|
*
|
|
295
295
|
* @param username Username to update.
|
|
296
|
-
* @param password New plaintext password.
|
|
296
|
+
* @param password New plaintext password (must be non-empty).
|
|
297
|
+
* @throws When the user does not exist or the password is empty.
|
|
297
298
|
*/
|
|
298
299
|
public async setPassword(username: string, password: string): Promise<void> {
|
|
299
300
|
perf.mark("setPassword");
|
|
@@ -309,9 +310,10 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
309
310
|
}
|
|
310
311
|
|
|
311
312
|
/**
|
|
312
|
-
* Deletes existing non-root user account.
|
|
313
|
+
* Deletes an existing non-root user account and revokes sudo access.
|
|
313
314
|
*
|
|
314
315
|
* @param username Username to remove.
|
|
316
|
+
* @throws When `username` is `"root"` or the user does not exist.
|
|
315
317
|
*/
|
|
316
318
|
public async deleteUser(username: string): Promise<void> {
|
|
317
319
|
perf.mark("deleteUser");
|
|
@@ -343,9 +345,10 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
343
345
|
}
|
|
344
346
|
|
|
345
347
|
/**
|
|
346
|
-
* Grants sudo
|
|
348
|
+
* Grants sudo privileges to an existing user.
|
|
347
349
|
*
|
|
348
350
|
* @param username Username to promote.
|
|
351
|
+
* @throws When the user does not exist.
|
|
349
352
|
*/
|
|
350
353
|
public async addSudoer(username: string): Promise<void> {
|
|
351
354
|
perf.mark("addSudoer");
|
|
@@ -359,9 +362,10 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
359
362
|
}
|
|
360
363
|
|
|
361
364
|
/**
|
|
362
|
-
* Revokes sudo
|
|
365
|
+
* Revokes sudo privileges from a user. Root cannot be demoted.
|
|
363
366
|
*
|
|
364
367
|
* @param username Username to demote.
|
|
368
|
+
* @throws When `username` is `"root"`.
|
|
365
369
|
*/
|
|
366
370
|
public async removeSudoer(username: string): Promise<void> {
|
|
367
371
|
perf.mark("removeSudoer");
|
|
@@ -375,11 +379,14 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
375
379
|
}
|
|
376
380
|
|
|
377
381
|
/**
|
|
378
|
-
* Registers active session and allocates
|
|
382
|
+
* Registers a new active session and allocates a virtual TTY identifier.
|
|
379
383
|
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
384
|
+
* Called by the SSH server when a client is authenticated. The returned
|
|
385
|
+
* descriptor is visible in `who` output and `listActiveSessions()`.
|
|
386
|
+
*
|
|
387
|
+
* @param username Authenticated username bound to the session.
|
|
388
|
+
* @param remoteAddress IP address or hostname of the connecting client.
|
|
389
|
+
* @returns The newly created `VirtualActiveSession` descriptor.
|
|
383
390
|
*/
|
|
384
391
|
public registerSession(
|
|
385
392
|
username: string,
|
|
@@ -403,9 +410,11 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
403
410
|
}
|
|
404
411
|
|
|
405
412
|
/**
|
|
406
|
-
*
|
|
413
|
+
* Removes an active session record when the connection closes.
|
|
414
|
+
*
|
|
415
|
+
* Safe to call with a `null` or `undefined` session ID — it will be a no-op.
|
|
407
416
|
*
|
|
408
|
-
* @param sessionId Session
|
|
417
|
+
* @param sessionId Session UUID returned by `registerSession()`, or nullish.
|
|
409
418
|
*/
|
|
410
419
|
public unregisterSession(sessionId: string | null | undefined): void {
|
|
411
420
|
perf.mark("unregisterSession");
|
|
@@ -425,11 +434,15 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
425
434
|
}
|
|
426
435
|
|
|
427
436
|
/**
|
|
428
|
-
* Updates username
|
|
437
|
+
* Updates the username and remote address metadata for an active session.
|
|
429
438
|
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
439
|
+
* Called internally by `su` and `sudo` when the effective user changes
|
|
440
|
+
* within a session. Silently ignored when the session ID is nullish or
|
|
441
|
+
* unknown.
|
|
442
|
+
*
|
|
443
|
+
* @param sessionId Session UUID to update, or nullish for no-op.
|
|
444
|
+
* @param username New effective username.
|
|
445
|
+
* @param remoteAddress New remote address (usually unchanged).
|
|
433
446
|
*/
|
|
434
447
|
public updateSession(
|
|
435
448
|
sessionId: string | null | undefined,
|
|
@@ -454,9 +467,11 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
454
467
|
}
|
|
455
468
|
|
|
456
469
|
/**
|
|
457
|
-
*
|
|
470
|
+
* Returns a snapshot of all currently active sessions, sorted by start time.
|
|
471
|
+
*
|
|
472
|
+
* Used by `who`, `ps`, `uptime`, and the `HoneyPot` auditor.
|
|
458
473
|
*
|
|
459
|
-
* @returns
|
|
474
|
+
* @returns Array of `VirtualActiveSession` descriptors.
|
|
460
475
|
*/
|
|
461
476
|
public listActiveSessions(): VirtualActiveSession[] {
|
|
462
477
|
perf.mark("listActiveSessions");
|
|
@@ -465,6 +480,15 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
465
480
|
);
|
|
466
481
|
}
|
|
467
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Returns a sorted list of all registered usernames.
|
|
485
|
+
*
|
|
486
|
+
* @returns Array of username strings sorted alphabetically.
|
|
487
|
+
*/
|
|
488
|
+
public listUsers(): string[] {
|
|
489
|
+
return Array.from(this.users.keys()).sort();
|
|
490
|
+
}
|
|
491
|
+
|
|
468
492
|
private loadFromVfs(): void {
|
|
469
493
|
this.users.clear();
|
|
470
494
|
|
|
@@ -610,6 +634,14 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
610
634
|
return record;
|
|
611
635
|
}
|
|
612
636
|
|
|
637
|
+
/**
|
|
638
|
+
* Returns `true` when the user has a non-empty password set.
|
|
639
|
+
*
|
|
640
|
+
* A user with no password (or whose password hash matches the empty-string
|
|
641
|
+
* hash) is allowed to authenticate without a credential check.
|
|
642
|
+
*
|
|
643
|
+
* @param username Target username.
|
|
644
|
+
*/
|
|
613
645
|
public hasPassword(username: string): boolean {
|
|
614
646
|
perf.mark("hasPassword");
|
|
615
647
|
if (this.getPasswordHash(username) === this.hashPassword("")) {
|
|
@@ -620,10 +652,13 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
620
652
|
}
|
|
621
653
|
|
|
622
654
|
/**
|
|
623
|
-
* Hashes plaintext password
|
|
655
|
+
* Hashes a plaintext password using scrypt (or SHA-256 in fast-hash mode).
|
|
656
|
+
*
|
|
657
|
+
* Set `SSH_MIMIC_FAST_PASSWORD_HASH=1` to switch to SHA-256 for test
|
|
658
|
+
* environments where scrypt latency is undesirable.
|
|
624
659
|
*
|
|
625
|
-
* @param password Plaintext password.
|
|
626
|
-
* @returns Hex-encoded
|
|
660
|
+
* @param password Plaintext password string.
|
|
661
|
+
* @returns Hex-encoded hash string.
|
|
627
662
|
*/
|
|
628
663
|
public hashPassword(password: string): string {
|
|
629
664
|
if (VirtualUserManager.fastPasswordHash) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
|
+
|
|
4
|
+
export const aliasCommand: ShellModule = {
|
|
5
|
+
name: "alias",
|
|
6
|
+
description: "Define or display aliases",
|
|
7
|
+
category: "shell",
|
|
8
|
+
params: ["[name[=value] ...]"],
|
|
9
|
+
run: ({ args, env }) => {
|
|
10
|
+
if (!env) return { exitCode: 0 };
|
|
11
|
+
|
|
12
|
+
// Aliases stored in env.vars under prefix __alias_
|
|
13
|
+
if (args.length === 0) {
|
|
14
|
+
const aliases = Object.entries(env.vars)
|
|
15
|
+
.filter(([k]) => k.startsWith("__alias_"))
|
|
16
|
+
.map(([k, v]) => `alias ${k.slice("__alias_".length)}='${v}'`);
|
|
17
|
+
return { stdout: aliases.join("\n") || "", exitCode: 0 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const lines: string[] = [];
|
|
21
|
+
for (const arg of args) {
|
|
22
|
+
const eq = arg.indexOf("=");
|
|
23
|
+
if (eq === -1) {
|
|
24
|
+
// Display single alias
|
|
25
|
+
const val = env.vars[`__alias_${arg}`];
|
|
26
|
+
if (val) lines.push(`alias ${arg}='${val}'`);
|
|
27
|
+
else return { stderr: `alias: ${arg}: not found`, exitCode: 1 };
|
|
28
|
+
} else {
|
|
29
|
+
// Set alias
|
|
30
|
+
const name = arg.slice(0, eq);
|
|
31
|
+
const val = arg.slice(eq + 1).replace(/^['"]|['"]$/g, "");
|
|
32
|
+
env.vars[`__alias_${name}`] = val;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { stdout: lines.join("\n") || undefined, exitCode: 0 };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const unaliasCommand: ShellModule = {
|
|
41
|
+
name: "unalias",
|
|
42
|
+
description: "Remove alias definitions",
|
|
43
|
+
category: "shell",
|
|
44
|
+
params: ["<name...> | -a"],
|
|
45
|
+
run: ({ args, env }) => {
|
|
46
|
+
if (!env) return { exitCode: 0 };
|
|
47
|
+
|
|
48
|
+
if (ifFlag(args, ["-a"])) {
|
|
49
|
+
for (const k of Object.keys(env.vars)) {
|
|
50
|
+
if (k.startsWith("__alias_")) delete env.vars[k];
|
|
51
|
+
}
|
|
52
|
+
return { exitCode: 0 };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const name of args) {
|
|
56
|
+
delete env.vars[`__alias_${name}`];
|
|
57
|
+
}
|
|
58
|
+
return { exitCode: 0 };
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag, } from "./command-helpers";
|
|
3
|
+
import { getPackageManager } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const aptCommand: ShellModule = {
|
|
6
|
+
name: "apt",
|
|
7
|
+
aliases: ["apt-get"],
|
|
8
|
+
description: "Package manager",
|
|
9
|
+
category: "system",
|
|
10
|
+
params: ["<install|remove|update|upgrade|search|show|list> [pkg...]"],
|
|
11
|
+
run: ({ args, shell, authUser }) => {
|
|
12
|
+
const pm = getPackageManager(shell);
|
|
13
|
+
if (!pm) return { stderr: "apt: package manager not initialised", exitCode: 1 };
|
|
14
|
+
|
|
15
|
+
const sub = args[0]?.toLowerCase();
|
|
16
|
+
const rest = args.slice(1);
|
|
17
|
+
|
|
18
|
+
const quiet = ifFlag(rest, ["-q", "--quiet", "-qq"]);
|
|
19
|
+
const purge = ifFlag(rest, ["--purge"]);
|
|
20
|
+
const pkgs = rest.filter((a) => !a.startsWith("-"));
|
|
21
|
+
|
|
22
|
+
// Non-root check
|
|
23
|
+
const restricted = ["install", "remove", "purge", "upgrade", "update"];
|
|
24
|
+
if (restricted.includes(sub ?? "") && authUser !== "root") {
|
|
25
|
+
return {
|
|
26
|
+
stderr: "E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)\nE: Unable to acquire the dpkg frontend lock, are you root?",
|
|
27
|
+
exitCode: 100,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
switch (sub) {
|
|
32
|
+
case "install": {
|
|
33
|
+
if (pkgs.length === 0)
|
|
34
|
+
return { stderr: "apt: no packages specified", exitCode: 1 };
|
|
35
|
+
const { output, exitCode } = pm.install(pkgs, { quiet });
|
|
36
|
+
return { stdout: output || undefined, exitCode };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case "remove":
|
|
40
|
+
case "purge": {
|
|
41
|
+
if (pkgs.length === 0)
|
|
42
|
+
return { stderr: "apt: no packages specified", exitCode: 1 };
|
|
43
|
+
const { output, exitCode } = pm.remove(pkgs, {
|
|
44
|
+
purge: sub === "purge" || purge,
|
|
45
|
+
quiet,
|
|
46
|
+
});
|
|
47
|
+
return { stdout: output || undefined, exitCode };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
case "update": {
|
|
51
|
+
return {
|
|
52
|
+
stdout: [
|
|
53
|
+
"Hit:1 fortune://packages.fortune.local aurora InRelease",
|
|
54
|
+
"Hit:2 fortune://security.fortune.local aurora-security InRelease",
|
|
55
|
+
"Reading package lists... Done",
|
|
56
|
+
"Building dependency tree... Done",
|
|
57
|
+
"Reading state information... Done",
|
|
58
|
+
`All packages are up to date.`,
|
|
59
|
+
].join("\n"),
|
|
60
|
+
exitCode: 0,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case "upgrade": {
|
|
65
|
+
return {
|
|
66
|
+
stdout: [
|
|
67
|
+
"Reading package lists... Done",
|
|
68
|
+
"Building dependency tree... Done",
|
|
69
|
+
"Reading state information... Done",
|
|
70
|
+
"Calculating upgrade... Done",
|
|
71
|
+
"0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.",
|
|
72
|
+
].join("\n"),
|
|
73
|
+
exitCode: 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case "search": {
|
|
78
|
+
const term = pkgs[0];
|
|
79
|
+
if (!term)
|
|
80
|
+
return { stderr: "apt: search requires a term", exitCode: 1 };
|
|
81
|
+
const results = pm.search(term);
|
|
82
|
+
if (results.length === 0)
|
|
83
|
+
return {
|
|
84
|
+
stdout: `Sorting... Done\nFull Text Search... Done\n(no results)`,
|
|
85
|
+
exitCode: 0,
|
|
86
|
+
};
|
|
87
|
+
const lines = results.map(
|
|
88
|
+
(p) => `${p.name}/${p.section ?? "misc"} ${p.version} amd64\n ${p.shortDesc ?? p.description}`,
|
|
89
|
+
);
|
|
90
|
+
return {
|
|
91
|
+
stdout: `Sorting... Done\nFull Text Search... Done\n${lines.join("\n")}`,
|
|
92
|
+
exitCode: 0,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case "show": {
|
|
97
|
+
const name = pkgs[0];
|
|
98
|
+
if (!name) return { stderr: "apt: show requires a package name", exitCode: 1 };
|
|
99
|
+
const info = pm.show(name);
|
|
100
|
+
if (!info)
|
|
101
|
+
return { stderr: `N: Unable to locate package ${name}`, exitCode: 100 };
|
|
102
|
+
return { stdout: info, exitCode: 0 };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case "list": {
|
|
106
|
+
const installedFlag = ifFlag(rest, ["--installed"]);
|
|
107
|
+
if (installedFlag) {
|
|
108
|
+
const pkgList = pm.listInstalled();
|
|
109
|
+
if (pkgList.length === 0)
|
|
110
|
+
return { stdout: "Listing... Done\n(no packages installed)", exitCode: 0 };
|
|
111
|
+
const lines = pkgList.map(
|
|
112
|
+
(p) => `${p.name}/${p.section} ${p.version} ${p.architecture} [installed]`,
|
|
113
|
+
);
|
|
114
|
+
return { stdout: `Listing... Done\n${lines.join("\n")}`, exitCode: 0 };
|
|
115
|
+
}
|
|
116
|
+
// all available
|
|
117
|
+
const all = pm.listAvailable();
|
|
118
|
+
const lines = all.map(
|
|
119
|
+
(p) => `${p.name}/${p.section ?? "misc"} ${p.version} amd64`,
|
|
120
|
+
);
|
|
121
|
+
return { stdout: `Listing... Done\n${lines.join("\n")}`, exitCode: 0 };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
default: {
|
|
125
|
+
return {
|
|
126
|
+
stdout: [
|
|
127
|
+
"Usage: apt [options] command",
|
|
128
|
+
"",
|
|
129
|
+
"Commands:",
|
|
130
|
+
" install <pkg...> Install packages",
|
|
131
|
+
" remove <pkg...> Remove packages",
|
|
132
|
+
" purge <pkg...> Remove packages and config files",
|
|
133
|
+
" update Refresh package index",
|
|
134
|
+
" upgrade Upgrade all packages",
|
|
135
|
+
" search <term> Search in package descriptions",
|
|
136
|
+
" show <pkg> Show package details",
|
|
137
|
+
" list [--installed] List packages",
|
|
138
|
+
].join("\n"),
|
|
139
|
+
exitCode: 0,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const aptCacheCommand: ShellModule = {
|
|
147
|
+
name: "apt-cache",
|
|
148
|
+
description: "Query the package cache",
|
|
149
|
+
category: "system",
|
|
150
|
+
params: ["<search|show|policy> [pkg]"],
|
|
151
|
+
run: ({ args, shell }) => {
|
|
152
|
+
const pm = getPackageManager(shell);
|
|
153
|
+
if (!pm) return { stderr: "apt-cache: package manager not initialised", exitCode: 1 };
|
|
154
|
+
|
|
155
|
+
const sub = args[0]?.toLowerCase();
|
|
156
|
+
const pkgName = args[1];
|
|
157
|
+
|
|
158
|
+
switch (sub) {
|
|
159
|
+
case "search": {
|
|
160
|
+
if (!pkgName) return { stderr: "Need a search term", exitCode: 1 };
|
|
161
|
+
const results = pm.search(pkgName);
|
|
162
|
+
return {
|
|
163
|
+
stdout: results
|
|
164
|
+
.map((p) => `${p.name} - ${p.shortDesc ?? p.description}`)
|
|
165
|
+
.join("\n") || "(no results)",
|
|
166
|
+
exitCode: 0,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
case "show": {
|
|
170
|
+
if (!pkgName) return { stderr: "Need a package name", exitCode: 1 };
|
|
171
|
+
const info = pm.show(pkgName);
|
|
172
|
+
return info
|
|
173
|
+
? { stdout: info, exitCode: 0 }
|
|
174
|
+
: { stderr: `N: Unable to locate package ${pkgName}`, exitCode: 100 };
|
|
175
|
+
}
|
|
176
|
+
case "policy": {
|
|
177
|
+
if (!pkgName) return { stderr: "Need a package name", exitCode: 1 };
|
|
178
|
+
const def = pm.findInRegistry(pkgName);
|
|
179
|
+
if (!def)
|
|
180
|
+
return { stderr: `N: Unable to locate package ${pkgName}`, exitCode: 100 };
|
|
181
|
+
const inst = pm.isInstalled(pkgName);
|
|
182
|
+
return {
|
|
183
|
+
stdout: [
|
|
184
|
+
`${pkgName}:`,
|
|
185
|
+
` Installed: ${inst ? def.version : "(none)"}`,
|
|
186
|
+
` Candidate: ${def.version}`,
|
|
187
|
+
` Version table:`,
|
|
188
|
+
` ${def.version} 500`,
|
|
189
|
+
` 500 fortune://packages.fortune.local aurora/main amd64 Packages`,
|
|
190
|
+
].join("\n"),
|
|
191
|
+
exitCode: 0,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
default:
|
|
195
|
+
return { stderr: `apt-cache: unknown command '${sub ?? ""}'`, exitCode: 1 };
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
};
|
package/src/commands/cat.ts
CHANGED
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
|
-
import {
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
3
|
import { assertPathAccess, resolveReadablePath } from "./helpers";
|
|
4
4
|
|
|
5
5
|
export const catCommand: ShellModule = {
|
|
6
6
|
name: "cat",
|
|
7
7
|
description: "Concatenate and print files",
|
|
8
8
|
category: "files",
|
|
9
|
-
params: ["<file
|
|
10
|
-
run: ({ authUser, shell, cwd, args }) => {
|
|
11
|
-
const
|
|
12
|
-
|
|
9
|
+
params: ["[-n] [-b] <file...>"],
|
|
10
|
+
run: ({ authUser, shell, cwd, args, stdin }) => {
|
|
11
|
+
const numberAll = ifFlag(args, ["-n", "--number"]);
|
|
12
|
+
const numberNonBlank = ifFlag(args, ["-b", "--number-nonblank"]);
|
|
13
|
+
const fileArgs = args.filter((a) => !a.startsWith("-"));
|
|
14
|
+
|
|
15
|
+
if (fileArgs.length === 0 && stdin !== undefined) {
|
|
16
|
+
return { stdout: stdin, exitCode: 0 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (fileArgs.length === 0) {
|
|
13
20
|
return { stderr: "cat: missing file operand", exitCode: 1 };
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
const parts: string[] = [];
|
|
24
|
+
for (const fileArg of fileArgs) {
|
|
25
|
+
const target = resolveReadablePath(shell.vfs, cwd, fileArg);
|
|
26
|
+
assertPathAccess(authUser, target, "cat");
|
|
27
|
+
parts.push(shell.vfs.readFile(target));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const combined = parts.join("");
|
|
31
|
+
|
|
32
|
+
if (!numberAll && !numberNonBlank) {
|
|
33
|
+
return { stdout: combined, exitCode: 0 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let lineNum = 1;
|
|
37
|
+
const numbered = combined.split("\n").map((line) => {
|
|
38
|
+
if (numberNonBlank && line.trim() === "") return line;
|
|
39
|
+
return `${String(lineNum++).padStart(6)}\t${line}`;
|
|
40
|
+
}).join("\n");
|
|
41
|
+
|
|
42
|
+
return { stdout: numbered, exitCode: 0 };
|
|
19
43
|
},
|
|
20
44
|
};
|