typescript-virtual-container 1.1.0 → 1.1.1-c
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/.vscode/settings.json +18 -0
- package/README.md +45 -5
- package/dist/SSHClient/index.d.ts +138 -0
- package/dist/SSHClient/index.d.ts.map +1 -0
- package/dist/SSHClient/index.js +216 -0
- package/dist/SSHMimic/exec.d.ts +4 -0
- package/dist/SSHMimic/exec.d.ts.map +1 -0
- package/dist/SSHMimic/exec.js +21 -0
- package/dist/SSHMimic/executor.d.ts +9 -0
- package/dist/SSHMimic/executor.d.ts.map +1 -0
- package/dist/SSHMimic/executor.js +131 -0
- package/dist/SSHMimic/hostKey.d.ts +2 -0
- package/dist/SSHMimic/hostKey.d.ts.map +1 -0
- package/dist/SSHMimic/hostKey.js +17 -0
- package/dist/SSHMimic/index.d.ts +39 -0
- package/dist/SSHMimic/index.d.ts.map +1 -0
- package/dist/SSHMimic/index.js +113 -0
- package/dist/SSHMimic/loginFormat.d.ts +2 -0
- package/dist/SSHMimic/loginFormat.d.ts.map +1 -0
- package/dist/SSHMimic/loginFormat.js +10 -0
- package/dist/SSHMimic/prompt.d.ts +2 -0
- package/dist/SSHMimic/prompt.d.ts.map +1 -0
- package/dist/SSHMimic/prompt.js +9 -0
- package/dist/VirtualFileSystem/archive.d.ts +5 -0
- package/dist/VirtualFileSystem/archive.d.ts.map +1 -0
- package/dist/VirtualFileSystem/archive.js +56 -0
- package/dist/VirtualFileSystem/index.d.ts +131 -0
- package/dist/VirtualFileSystem/index.d.ts.map +1 -0
- package/dist/VirtualFileSystem/index.js +355 -0
- package/dist/VirtualFileSystem/internalTypes.d.ts +18 -0
- package/dist/VirtualFileSystem/internalTypes.d.ts.map +1 -0
- package/dist/VirtualFileSystem/internalTypes.js +0 -0
- package/dist/VirtualFileSystem/path.d.ts +9 -0
- package/dist/VirtualFileSystem/path.d.ts.map +1 -0
- package/dist/VirtualFileSystem/path.js +49 -0
- package/dist/VirtualFileSystem/snapshot.d.ts +5 -0
- package/dist/VirtualFileSystem/snapshot.d.ts.map +1 -0
- package/dist/VirtualFileSystem/snapshot.js +59 -0
- package/dist/VirtualFileSystem/tree.d.ts +3 -0
- package/dist/VirtualFileSystem/tree.d.ts.map +1 -0
- package/dist/VirtualFileSystem/tree.js +19 -0
- package/dist/VirtualShell/index.d.ts +86 -0
- package/dist/VirtualShell/index.d.ts.map +1 -0
- package/dist/VirtualShell/index.js +129 -0
- package/dist/VirtualShell/shell.d.ts +5 -0
- package/dist/VirtualShell/shell.d.ts.map +1 -0
- package/dist/VirtualShell/shell.js +473 -0
- package/dist/VirtualShell/shellParser.d.ts +4 -0
- package/dist/VirtualShell/shellParser.d.ts.map +1 -0
- package/dist/VirtualShell/shellParser.js +207 -0
- package/dist/VirtualUserManager/index.d.ts +168 -0
- package/dist/VirtualUserManager/index.d.ts.map +1 -0
- package/dist/VirtualUserManager/index.js +375 -0
- package/dist/commands/adduser.d.ts +3 -0
- package/dist/commands/adduser.d.ts.map +1 -0
- package/dist/commands/adduser.js +18 -0
- package/dist/commands/cat.d.ts +3 -0
- package/dist/commands/cat.d.ts.map +1 -0
- package/dist/commands/cat.js +15 -0
- package/dist/commands/cd.d.ts +3 -0
- package/dist/commands/cd.d.ts.map +1 -0
- package/dist/commands/cd.js +17 -0
- package/dist/commands/clear.d.ts +3 -0
- package/dist/commands/clear.d.ts.map +1 -0
- package/dist/commands/clear.js +5 -0
- package/dist/commands/command-helpers.d.ts +23 -0
- package/dist/commands/command-helpers.d.ts.map +1 -0
- package/dist/commands/command-helpers.js +139 -0
- package/dist/commands/curl.d.ts +3 -0
- package/dist/commands/curl.d.ts.map +1 -0
- package/dist/commands/curl.js +44 -0
- package/dist/commands/deluser.d.ts +3 -0
- package/dist/commands/deluser.d.ts.map +1 -0
- package/dist/commands/deluser.js +15 -0
- package/dist/commands/echo.d.ts +3 -0
- package/dist/commands/echo.d.ts.map +1 -0
- package/dist/commands/echo.js +22 -0
- package/dist/commands/env.d.ts +3 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +18 -0
- package/dist/commands/exit.d.ts +3 -0
- package/dist/commands/exit.d.ts.map +1 -0
- package/dist/commands/exit.js +5 -0
- package/dist/commands/export.d.ts +3 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +34 -0
- package/dist/commands/grep.d.ts +3 -0
- package/dist/commands/grep.d.ts.map +1 -0
- package/dist/commands/grep.js +69 -0
- package/dist/commands/help.d.ts +3 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/help.js +7 -0
- package/dist/commands/helpers.d.ts +26 -0
- package/dist/commands/helpers.d.ts.map +1 -0
- package/dist/commands/helpers.js +160 -0
- package/dist/commands/hostname.d.ts +3 -0
- package/dist/commands/hostname.d.ts.map +1 -0
- package/dist/commands/hostname.js +5 -0
- package/dist/commands/htop.d.ts +3 -0
- package/dist/commands/htop.d.ts.map +1 -0
- package/dist/commands/htop.js +10 -0
- package/dist/commands/index.d.ts +8 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +212 -0
- package/dist/commands/ls.d.ts +3 -0
- package/dist/commands/ls.d.ts.map +1 -0
- package/dist/commands/ls.js +47 -0
- package/dist/commands/mkdir.d.ts +3 -0
- package/dist/commands/mkdir.d.ts.map +1 -0
- package/dist/commands/mkdir.js +21 -0
- package/dist/commands/nano.d.ts +3 -0
- package/dist/commands/nano.d.ts.map +1 -0
- package/dist/commands/nano.js +27 -0
- package/dist/commands/neofetch.d.ts +3 -0
- package/dist/commands/neofetch.d.ts.map +1 -0
- package/dist/commands/neofetch.js +32 -0
- package/dist/commands/pwd.d.ts +3 -0
- package/dist/commands/pwd.d.ts.map +1 -0
- package/dist/commands/pwd.js +5 -0
- package/dist/commands/rm.d.ts +3 -0
- package/dist/commands/rm.d.ts.map +1 -0
- package/dist/commands/rm.js +29 -0
- package/dist/commands/set.d.ts +7 -0
- package/dist/commands/set.d.ts.map +1 -0
- package/dist/commands/set.js +64 -0
- package/dist/commands/sh.d.ts +4 -0
- package/dist/commands/sh.d.ts.map +1 -0
- package/dist/commands/sh.js +45 -0
- package/dist/commands/su.d.ts +3 -0
- package/dist/commands/su.d.ts.map +1 -0
- package/dist/commands/su.js +24 -0
- package/dist/commands/sudo.d.ts +3 -0
- package/dist/commands/sudo.d.ts.map +1 -0
- package/dist/commands/sudo.js +47 -0
- package/dist/commands/touch.d.ts +3 -0
- package/dist/commands/touch.d.ts.map +1 -0
- package/dist/commands/touch.js +18 -0
- package/dist/commands/tree.d.ts +3 -0
- package/dist/commands/tree.d.ts.map +1 -0
- package/dist/commands/tree.js +11 -0
- package/dist/commands/unset.d.ts +3 -0
- package/dist/commands/unset.d.ts.map +1 -0
- package/dist/commands/unset.js +15 -0
- package/dist/commands/wget.d.ts +3 -0
- package/dist/commands/wget.d.ts.map +1 -0
- package/dist/commands/wget.js +113 -0
- package/dist/commands/who.d.ts +3 -0
- package/dist/commands/who.d.ts.map +1 -0
- package/dist/commands/who.js +15 -0
- package/dist/commands/whoami.d.ts +3 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +5 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/modules/neofetch.d.ts +19 -0
- package/dist/modules/neofetch.d.ts.map +1 -0
- package/dist/modules/neofetch.js +284 -0
- package/dist/modules/shellInteractive.d.ts +6 -0
- package/dist/modules/shellInteractive.d.ts.map +1 -0
- package/dist/modules/shellInteractive.js +26 -0
- package/dist/modules/shellRuntime.d.ts +11 -0
- package/dist/modules/shellRuntime.d.ts.map +1 -0
- package/dist/modules/shellRuntime.js +52 -0
- package/dist/standalone.d.ts +2 -0
- package/dist/standalone.d.ts.map +1 -0
- package/dist/standalone.js +25 -0
- package/dist/types/commands.d.ts +89 -0
- package/dist/types/commands.d.ts.map +1 -0
- package/dist/types/commands.js +0 -0
- package/dist/types/pipeline.d.ts +23 -0
- package/dist/types/pipeline.d.ts.map +1 -0
- package/dist/types/pipeline.js +0 -0
- package/dist/types/streams.d.ts +32 -0
- package/dist/types/streams.d.ts.map +1 -0
- package/dist/types/streams.js +0 -0
- package/dist/types/vfs.d.ts +71 -0
- package/dist/types/vfs.d.ts.map +1 -0
- package/dist/types/vfs.js +0 -0
- package/package.json +4 -2
- package/src/{SSHMimic/client.ts → SSHClient/index.ts} +2 -2
- package/src/SSHMimic/exec.ts +1 -1
- package/src/SSHMimic/executor.ts +8 -8
- package/src/VirtualFileSystem/index.ts +26 -0
- package/src/VirtualShell/index.ts +17 -1
- package/src/VirtualShell/shell.ts +19 -107
- package/src/VirtualShell/shellParser.ts +32 -7
- package/src/VirtualUserManager/index.ts +149 -0
- package/src/{VirtualShell/commands → commands}/adduser.ts +1 -1
- package/src/{VirtualShell/commands → commands}/cat.ts +1 -1
- package/src/{VirtualShell/commands → commands}/cd.ts +1 -1
- package/src/{VirtualShell/commands → commands}/clear.ts +1 -1
- package/src/{VirtualShell/commands → commands}/curl.ts +2 -2
- package/src/{VirtualShell/commands → commands}/deluser.ts +1 -1
- package/src/{VirtualShell/commands → commands}/echo.ts +1 -1
- package/src/{VirtualShell/commands → commands}/env.ts +1 -1
- package/src/{VirtualShell/commands → commands}/exit.ts +1 -1
- package/src/{VirtualShell/commands → commands}/export.ts +1 -1
- package/src/{VirtualShell/commands → commands}/grep.ts +1 -1
- package/src/{VirtualShell/commands → commands}/help.ts +1 -1
- package/src/{VirtualShell/commands → commands}/helpers.ts +1 -1
- package/src/{VirtualShell/commands → commands}/hostname.ts +1 -1
- package/src/{VirtualShell/commands → commands}/htop.ts +1 -1
- package/src/{VirtualShell/commands → commands}/index.ts +7 -4
- package/src/{VirtualShell/commands → commands}/ls.ts +1 -1
- package/src/{VirtualShell/commands → commands}/mkdir.ts +1 -1
- package/src/{VirtualShell/commands → commands}/nano.ts +1 -1
- package/src/{VirtualShell/commands → commands}/neofetch.ts +2 -2
- package/src/{VirtualShell/commands → commands}/pwd.ts +1 -1
- package/src/{VirtualShell/commands → commands}/rm.ts +1 -1
- package/src/{VirtualShell/commands → commands}/set.ts +1 -1
- package/src/{VirtualShell/commands → commands}/sh.ts +1 -1
- package/src/{VirtualShell/commands → commands}/su.ts +1 -1
- package/src/{VirtualShell/commands → commands}/sudo.ts +1 -1
- package/src/{VirtualShell/commands → commands}/touch.ts +2 -2
- package/src/{VirtualShell/commands → commands}/tree.ts +1 -1
- package/src/{VirtualShell/commands → commands}/unset.ts +1 -1
- package/src/{VirtualShell/commands → commands}/wget.ts +2 -2
- package/src/{VirtualShell/commands → commands}/who.ts +2 -2
- package/src/{VirtualShell/commands → commands}/whoami.ts +1 -1
- package/src/index.ts +2 -2
- package/{modules → src/modules}/neofetch.ts +56 -51
- package/src/modules/shellInteractive.ts +57 -0
- package/src/modules/shellRuntime.ts +76 -0
- package/tests/command-helpers.test.ts +1 -1
- package/tests/helpers.test.ts +1 -1
- package/tests/users.test.ts +60 -0
- package/tsconfig.json +19 -8
- /package/src/{VirtualShell/commands → commands}/command-helpers.ts +0 -0
|
@@ -9,7 +9,16 @@ export function parseShellPipeline(rawInput: string): Pipeline {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const commands: PipelineCommand[] = [];
|
|
12
|
-
const
|
|
12
|
+
const tokenized = tokenizePipeline(trimmed);
|
|
13
|
+
if (tokenized.error) {
|
|
14
|
+
return {
|
|
15
|
+
commands: [],
|
|
16
|
+
isValid: false,
|
|
17
|
+
error: tokenized.error,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const pipeTokens = tokenized.tokens;
|
|
13
22
|
|
|
14
23
|
for (const token of pipeTokens) {
|
|
15
24
|
const cmd = parseCommandWithRedirections(token);
|
|
@@ -29,7 +38,7 @@ export function parseShellPipeline(rawInput: string): Pipeline {
|
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
/** Tokenize input by pipes, respecting quoted strings */
|
|
32
|
-
function tokenizePipeline(input: string): string[] {
|
|
41
|
+
function tokenizePipeline(input: string): { tokens: string[]; error?: string } {
|
|
33
42
|
const tokens: string[] = [];
|
|
34
43
|
let current = "";
|
|
35
44
|
let inQuotes = false;
|
|
@@ -49,9 +58,13 @@ function tokenizePipeline(input: string): string[] {
|
|
|
49
58
|
current += ch;
|
|
50
59
|
i++;
|
|
51
60
|
} else if (ch === "|" && !inQuotes) {
|
|
52
|
-
if (current.trim()) {
|
|
53
|
-
|
|
61
|
+
if (!current.trim()) {
|
|
62
|
+
return {
|
|
63
|
+
tokens: [],
|
|
64
|
+
error: "Syntax error near unexpected token '|'",
|
|
65
|
+
};
|
|
54
66
|
}
|
|
67
|
+
tokens.push(current.trim());
|
|
55
68
|
current = "";
|
|
56
69
|
i++;
|
|
57
70
|
} else {
|
|
@@ -60,11 +73,23 @@ function tokenizePipeline(input: string): string[] {
|
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
75
|
|
|
63
|
-
if (
|
|
64
|
-
|
|
76
|
+
if (inQuotes) {
|
|
77
|
+
return {
|
|
78
|
+
tokens: [],
|
|
79
|
+
error: "Syntax error: unterminated quote",
|
|
80
|
+
};
|
|
65
81
|
}
|
|
66
82
|
|
|
67
|
-
|
|
83
|
+
if (!current.trim()) {
|
|
84
|
+
return {
|
|
85
|
+
tokens: [],
|
|
86
|
+
error: "Syntax error near unexpected token '|'",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
tokens.push(current.trim());
|
|
91
|
+
|
|
92
|
+
return { tokens };
|
|
68
93
|
}
|
|
69
94
|
|
|
70
95
|
interface ParseResult {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes, randomUUID, scryptSync } from "node:crypto";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
3
4
|
|
|
4
5
|
/** Persisted virtual user credential record. */
|
|
@@ -33,9 +34,11 @@ export interface VirtualActiveSession {
|
|
|
33
34
|
export class VirtualUserManager {
|
|
34
35
|
private readonly usersPath = "/virtual-env-js/.auth/htpasswd";
|
|
35
36
|
private readonly sudoersPath = "/virtual-env-js/.auth/sudoers";
|
|
37
|
+
private readonly quotasPath = "/virtual-env-js/.auth/quotas";
|
|
36
38
|
private readonly authDirPath = "/virtual-env-js/.auth";
|
|
37
39
|
private readonly users = new Map<string, VirtualUserRecord>();
|
|
38
40
|
private readonly sudoers = new Set<string>();
|
|
41
|
+
private readonly quotas = new Map<string, number>();
|
|
39
42
|
private readonly activeSessions = new Map<string, VirtualActiveSession>();
|
|
40
43
|
private nextTty = 0;
|
|
41
44
|
|
|
@@ -58,6 +61,7 @@ export class VirtualUserManager {
|
|
|
58
61
|
public async initialize(): Promise<void> {
|
|
59
62
|
this.loadFromVfs();
|
|
60
63
|
this.loadSudoersFromVfs();
|
|
64
|
+
this.loadQuotasFromVfs();
|
|
61
65
|
|
|
62
66
|
this.users.set("root", this.createRecord("root", this.defaultRootPassword));
|
|
63
67
|
|
|
@@ -66,6 +70,113 @@ export class VirtualUserManager {
|
|
|
66
70
|
await this.persist();
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Sets max allowed bytes under /home/<username>.
|
|
75
|
+
*
|
|
76
|
+
* @param username Target username.
|
|
77
|
+
* @param maxBytes Quota ceiling in bytes.
|
|
78
|
+
*/
|
|
79
|
+
public async setQuotaBytes(
|
|
80
|
+
username: string,
|
|
81
|
+
maxBytes: number,
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
this.validateUsername(username);
|
|
84
|
+
if (!this.users.has(username)) {
|
|
85
|
+
throw new Error(`quota: user '${username}' does not exist`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!Number.isFinite(maxBytes) || maxBytes < 0) {
|
|
89
|
+
throw new Error("quota: maxBytes must be a non-negative number");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.quotas.set(username, Math.floor(maxBytes));
|
|
93
|
+
await this.persist();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Removes quota for a user.
|
|
98
|
+
*
|
|
99
|
+
* @param username Target username.
|
|
100
|
+
*/
|
|
101
|
+
public async clearQuota(username: string): Promise<void> {
|
|
102
|
+
this.validateUsername(username);
|
|
103
|
+
this.quotas.delete(username);
|
|
104
|
+
await this.persist();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Gets configured quota in bytes for a user.
|
|
109
|
+
*
|
|
110
|
+
* @param username Target username.
|
|
111
|
+
* @returns Quota in bytes, or null when unlimited.
|
|
112
|
+
*/
|
|
113
|
+
public getQuotaBytes(username: string): number | null {
|
|
114
|
+
return this.quotas.get(username) ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Computes current usage under /home/<username>.
|
|
119
|
+
*
|
|
120
|
+
* @param username Target username.
|
|
121
|
+
* @returns Current usage in bytes.
|
|
122
|
+
*/
|
|
123
|
+
public getUsageBytes(username: string): number {
|
|
124
|
+
const homePath = `/home/${username}`;
|
|
125
|
+
if (!this.vfs.exists(homePath)) {
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return this.vfs.getUsageBytes(homePath);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Validates that writing file content would not exceed user quota.
|
|
134
|
+
*
|
|
135
|
+
* Quotas are enforced only for writes inside /home/<username>.
|
|
136
|
+
*
|
|
137
|
+
* @param username Authenticated user.
|
|
138
|
+
* @param targetPath Target file path.
|
|
139
|
+
* @param nextContent New file content.
|
|
140
|
+
*/
|
|
141
|
+
public assertWriteWithinQuota(
|
|
142
|
+
username: string,
|
|
143
|
+
targetPath: string,
|
|
144
|
+
nextContent: string | Buffer,
|
|
145
|
+
): void {
|
|
146
|
+
const quota = this.quotas.get(username);
|
|
147
|
+
if (quota === undefined) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const normalizedPath = normalizeVfsPath(targetPath);
|
|
152
|
+
const homePath = normalizeVfsPath(`/home/${username}`);
|
|
153
|
+
const inUserHome =
|
|
154
|
+
normalizedPath === homePath || normalizedPath.startsWith(`${homePath}/`);
|
|
155
|
+
if (!inUserHome) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const currentUsage = this.getUsageBytes(username);
|
|
160
|
+
let existingSize = 0;
|
|
161
|
+
if (this.vfs.exists(normalizedPath)) {
|
|
162
|
+
const existing = this.vfs.stat(normalizedPath);
|
|
163
|
+
if (existing.type === "file") {
|
|
164
|
+
existingSize = existing.size;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const incomingSize = Buffer.isBuffer(nextContent)
|
|
169
|
+
? nextContent.length
|
|
170
|
+
: Buffer.byteLength(nextContent, "utf8");
|
|
171
|
+
const projectedUsage = currentUsage - existingSize + incomingSize;
|
|
172
|
+
|
|
173
|
+
if (projectedUsage > quota) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`quota exceeded for '${username}': ${projectedUsage}/${quota} bytes`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
69
180
|
/**
|
|
70
181
|
* Verifies plaintext password against stored record.
|
|
71
182
|
*
|
|
@@ -291,6 +402,30 @@ export class VirtualUserManager {
|
|
|
291
402
|
}
|
|
292
403
|
}
|
|
293
404
|
|
|
405
|
+
private loadQuotasFromVfs(): void {
|
|
406
|
+
this.quotas.clear();
|
|
407
|
+
|
|
408
|
+
if (!this.vfs.exists(this.quotasPath)) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const raw = this.vfs.readFile(this.quotasPath);
|
|
413
|
+
for (const line of raw.split("\n")) {
|
|
414
|
+
const trimmed = line.trim();
|
|
415
|
+
if (trimmed.length === 0) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const [username, value] = trimmed.split(":");
|
|
420
|
+
const bytes = Number.parseInt(value ?? "", 10);
|
|
421
|
+
if (!username || !Number.isFinite(bytes) || bytes < 0) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
this.quotas.set(username, bytes);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
294
429
|
private async persist(): Promise<void> {
|
|
295
430
|
if (!this.vfs.exists(this.authDirPath)) {
|
|
296
431
|
this.vfs.mkdir(this.authDirPath, 0o700);
|
|
@@ -314,6 +449,15 @@ export class VirtualUserManager {
|
|
|
314
449
|
sudoersContent.length > 0 ? `${sudoersContent}\n` : "",
|
|
315
450
|
{ mode: 0o600 },
|
|
316
451
|
);
|
|
452
|
+
const quotasContent = Array.from(this.quotas.entries())
|
|
453
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
454
|
+
.map(([username, maxBytes]) => `${username}:${maxBytes}`)
|
|
455
|
+
.join("\n");
|
|
456
|
+
this.vfs.writeFile(
|
|
457
|
+
this.quotasPath,
|
|
458
|
+
quotasContent.length > 0 ? `${quotasContent}\n` : "",
|
|
459
|
+
{ mode: 0o600 },
|
|
460
|
+
);
|
|
317
461
|
await this.vfs.flushMirror();
|
|
318
462
|
}
|
|
319
463
|
|
|
@@ -346,3 +490,8 @@ export class VirtualUserManager {
|
|
|
346
490
|
}
|
|
347
491
|
}
|
|
348
492
|
}
|
|
493
|
+
|
|
494
|
+
function normalizeVfsPath(targetPath: string): string {
|
|
495
|
+
const normalized = path.posix.normalize(targetPath);
|
|
496
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
497
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ShellModule } from "
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
2
|
import { parseArgs } from "./command-helpers";
|
|
3
3
|
import {
|
|
4
4
|
assertPathAccess,
|
|
@@ -39,7 +39,7 @@ export const curlCommand: ShellModule = {
|
|
|
39
39
|
if (outputPath) {
|
|
40
40
|
const target = resolvePath(cwd, outputPath);
|
|
41
41
|
assertPathAccess(authUser, target, "curl");
|
|
42
|
-
shell.
|
|
42
|
+
shell.writeFileAsUser(authUser, target, result.stdout);
|
|
43
43
|
return {
|
|
44
44
|
stderr: result.stderr
|
|
45
45
|
? normalizeTerminalOutput(result.stderr)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type VirtualFileSystem from "
|
|
3
|
+
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
4
4
|
|
|
5
5
|
const PROTECTED_PREFIXES = ["/virtual-env-js/.auth"] as const;
|
|
6
6
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { VirtualShell } from "
|
|
1
|
+
import type { VirtualShell } from "../VirtualShell";
|
|
2
2
|
import type {
|
|
3
3
|
CommandContext,
|
|
4
4
|
CommandMode,
|
|
5
5
|
CommandResult,
|
|
6
6
|
ShellModule,
|
|
7
|
-
} from "
|
|
7
|
+
} from "../types/commands";
|
|
8
8
|
import { adduserCommand } from "./adduser";
|
|
9
9
|
import { catCommand } from "./cat";
|
|
10
10
|
import { cdCommand } from "./cd";
|
|
@@ -142,6 +142,9 @@ export function getCommandNames(): string[] {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
export function resolveModule(name: string): ShellModule | undefined {
|
|
145
|
+
if (!cachedCommandNames) {
|
|
146
|
+
buildCache();
|
|
147
|
+
}
|
|
145
148
|
return commandRegistry.get(name.toLowerCase());
|
|
146
149
|
}
|
|
147
150
|
|
|
@@ -213,8 +216,8 @@ export async function runCommand(
|
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
if (trimmed.includes("|") || trimmed.includes(">") || trimmed.includes("<")) {
|
|
216
|
-
const { parseShellPipeline } = await import("../shellParser");
|
|
217
|
-
const { executePipeline } = await import("
|
|
219
|
+
const { parseShellPipeline } = await import("../VirtualShell/shellParser");
|
|
220
|
+
const { executePipeline } = await import("../SSHMimic/executor");
|
|
218
221
|
|
|
219
222
|
const pipeline = parseShellPipeline(trimmed);
|
|
220
223
|
if (!pipeline.isValid) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { buildNeofetchOutput } from "
|
|
2
|
-
import type { ShellModule } from "
|
|
1
|
+
import { buildNeofetchOutput } from "../modules/neofetch";
|
|
2
|
+
import type { ShellModule } from "../types/commands";
|
|
3
3
|
import { ifFlag } from "./command-helpers";
|
|
4
4
|
import { getAllEnvVars } from "./set";
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** biome-ignore-all lint/style/useNamingConvention: env variables */
|
|
2
|
-
import type { ShellModule } from "
|
|
2
|
+
import type { ShellModule } from "../types/commands";
|
|
3
3
|
import { getArg } from "./command-helpers";
|
|
4
4
|
|
|
5
5
|
// Simple in-memory environment variables store
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ShellModule } from "
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
2
|
import { assertPathAccess, resolvePath } from "./helpers";
|
|
3
3
|
|
|
4
4
|
export const touchCommand: ShellModule = {
|
|
@@ -13,7 +13,7 @@ export const touchCommand: ShellModule = {
|
|
|
13
13
|
const target = resolvePath(cwd, file);
|
|
14
14
|
assertPathAccess(authUser, target, "touch");
|
|
15
15
|
if (!shell.vfs.exists(target)) {
|
|
16
|
-
shell.
|
|
16
|
+
shell.writeFileAsUser(authUser, target, "");
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
return { exitCode: 0 };
|
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import type { ShellModule } from "
|
|
5
|
+
import type { ShellModule } from "../types/commands";
|
|
6
6
|
import { ifFlag, parseArgs } from "./command-helpers";
|
|
7
7
|
import {
|
|
8
8
|
assertPathAccess,
|
|
@@ -131,7 +131,7 @@ export const wgetCommand: ShellModule = {
|
|
|
131
131
|
const content = await readFile(tempFile, "utf8");
|
|
132
132
|
const target = resolvePath(cwd, outputPath || stripUrlFilename(url));
|
|
133
133
|
assertPathAccess(authUser, target, "wget");
|
|
134
|
-
shell.
|
|
134
|
+
shell.writeFileAsUser(authUser, target, content);
|
|
135
135
|
|
|
136
136
|
return {
|
|
137
137
|
stdout: `saved ${target}`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { formatLoginDate } from "
|
|
2
|
-
import type { ShellModule } from "
|
|
1
|
+
import { formatLoginDate } from "../SSHMimic/loginFormat";
|
|
2
|
+
import type { ShellModule } from "../types/commands";
|
|
3
3
|
|
|
4
4
|
export const whoCommand: ShellModule = {
|
|
5
5
|
name: "who",
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SshClient } from "./
|
|
1
|
+
import { SshClient } from "./SSHClient";
|
|
2
2
|
import { SshMimic } from "./SSHMimic/index";
|
|
3
3
|
import VirtualFileSystem from "./VirtualFileSystem";
|
|
4
4
|
import { VirtualShell } from "./VirtualShell";
|
|
@@ -41,4 +41,4 @@ export {
|
|
|
41
41
|
getArg,
|
|
42
42
|
getFlag,
|
|
43
43
|
ifFlag,
|
|
44
|
-
} from "./
|
|
44
|
+
} from "./commands/command-helpers";
|