typescript-virtual-container 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +50 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +31 -0
- package/.github/dependabot.yml +27 -0
- package/.github/pull_request_template.md +21 -0
- package/.github/workflows/create-pull-request.yml +83 -0
- package/.github/workflows/test-battery.yml +57 -0
- package/CHANGELOG.md +27 -0
- package/CODE_OF_CONDUCT.md +39 -0
- package/CONTRIBUTING.md +59 -0
- package/LICENSE +21 -0
- package/README.md +1283 -0
- package/SECURITY.md +33 -0
- package/biome.json +20 -0
- package/bun.lock +99 -0
- package/package.json +38 -0
- package/src/SSHMimic/client.ts +248 -0
- package/src/SSHMimic/commands/adduser.ts +22 -0
- package/src/SSHMimic/commands/cat.ts +16 -0
- package/src/SSHMimic/commands/cd.ts +20 -0
- package/src/SSHMimic/commands/clear.ts +7 -0
- package/src/SSHMimic/commands/curl.ts +27 -0
- package/src/SSHMimic/commands/deluser.ts +19 -0
- package/src/SSHMimic/commands/exit.ts +7 -0
- package/src/SSHMimic/commands/help.ts +9 -0
- package/src/SSHMimic/commands/helpers.ts +137 -0
- package/src/SSHMimic/commands/hostname.ts +7 -0
- package/src/SSHMimic/commands/htop.ts +13 -0
- package/src/SSHMimic/commands/index.ts +120 -0
- package/src/SSHMimic/commands/ls.ts +14 -0
- package/src/SSHMimic/commands/mkdir.ts +17 -0
- package/src/SSHMimic/commands/nano.ts +30 -0
- package/src/SSHMimic/commands/pwd.ts +7 -0
- package/src/SSHMimic/commands/rm.ts +26 -0
- package/src/SSHMimic/commands/su.ts +31 -0
- package/src/SSHMimic/commands/sudo.ts +90 -0
- package/src/SSHMimic/commands/touch.ts +20 -0
- package/src/SSHMimic/commands/tree.ts +11 -0
- package/src/SSHMimic/commands/wget.ts +33 -0
- package/src/SSHMimic/commands/who.ts +18 -0
- package/src/SSHMimic/commands/whoami.ts +7 -0
- package/src/SSHMimic/exec.ts +37 -0
- package/src/SSHMimic/hostKey.ts +21 -0
- package/src/SSHMimic/index.ts +203 -0
- package/src/SSHMimic/loginFormat.ts +10 -0
- package/src/SSHMimic/prompt.ts +14 -0
- package/src/SSHMimic/shell.ts +740 -0
- package/src/SSHMimic/users.ts +336 -0
- package/src/VirtualFileSystem.ts +420 -0
- package/src/index.ts +34 -0
- package/src/standalone.ts +14 -0
- package/src/types/commands.ts +98 -0
- package/src/types/streams.ts +32 -0
- package/src/types/tar-stream.d.ts +38 -0
- package/src/types/vfs.ts +81 -0
- package/src/vfs/archive.ts +74 -0
- package/src/vfs/internalTypes.ts +19 -0
- package/src/vfs/path.ts +74 -0
- package/src/vfs/snapshot.ts +84 -0
- package/src/vfs/tree.ts +34 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Server as SshServer } from "ssh2";
|
|
2
|
+
import VirtualFileSystem from "../VirtualFileSystem";
|
|
3
|
+
import { runExec } from "./exec";
|
|
4
|
+
import { loadOrCreateHostKey } from "./hostKey";
|
|
5
|
+
import { startShell } from "./shell";
|
|
6
|
+
import { VirtualUserManager } from "./users";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SSH server wrapper that exposes virtual shell and exec sessions.
|
|
10
|
+
*
|
|
11
|
+
* Create an instance, call {@link SshMimic.start}, and stop it with
|
|
12
|
+
* {@link SshMimic.stop} when your process exits.
|
|
13
|
+
*/
|
|
14
|
+
class SshMimic {
|
|
15
|
+
private port: number;
|
|
16
|
+
private hostname: string;
|
|
17
|
+
private server: SshServer | null;
|
|
18
|
+
private vfs: VirtualFileSystem | null = null;
|
|
19
|
+
private users: VirtualUserManager | null = null;
|
|
20
|
+
private basePath: string = ".";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a new SSH mimic server instance.
|
|
24
|
+
*
|
|
25
|
+
* @param port TCP port to bind on localhost.
|
|
26
|
+
* @param hostname SSH ident hostname suffix and virtual host label.
|
|
27
|
+
* @param basePath Optional base path for virtual filesystem (default: current directory).
|
|
28
|
+
*/
|
|
29
|
+
constructor({
|
|
30
|
+
port,
|
|
31
|
+
hostname = "typescript-vm",
|
|
32
|
+
basePath = ".",
|
|
33
|
+
}: {
|
|
34
|
+
port: number;
|
|
35
|
+
hostname?: string;
|
|
36
|
+
basePath?: string;
|
|
37
|
+
}) {
|
|
38
|
+
this.port = port;
|
|
39
|
+
this.hostname = hostname;
|
|
40
|
+
this.basePath = basePath;
|
|
41
|
+
this.server = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Starts server and initializes virtual filesystem, users, and handlers.
|
|
46
|
+
*
|
|
47
|
+
* @returns Promise resolved with bound listening port.
|
|
48
|
+
*/
|
|
49
|
+
public async start(): Promise<number> {
|
|
50
|
+
const privateKey = loadOrCreateHostKey();
|
|
51
|
+
this.vfs = new VirtualFileSystem(this.basePath);
|
|
52
|
+
await this.vfs.restoreMirror();
|
|
53
|
+
this.users = new VirtualUserManager(
|
|
54
|
+
this.vfs,
|
|
55
|
+
process.env.SSH_MIMIC_ROOT_PASSWORD ?? "root",
|
|
56
|
+
);
|
|
57
|
+
await this.users.initialize();
|
|
58
|
+
|
|
59
|
+
this.server = new SshServer(
|
|
60
|
+
{
|
|
61
|
+
hostKeys: [privateKey],
|
|
62
|
+
ident: `SSH-2.0-${this.hostname}`,
|
|
63
|
+
},
|
|
64
|
+
(client) => {
|
|
65
|
+
let authUser = "root";
|
|
66
|
+
let remoteAddress = "unknown";
|
|
67
|
+
let sessionId: string | null = null;
|
|
68
|
+
|
|
69
|
+
client.on("authentication", (ctx) => {
|
|
70
|
+
if (ctx.method === "password") {
|
|
71
|
+
const candidateUser = ctx.username || "root";
|
|
72
|
+
remoteAddress = (ctx as { ip?: string }).ip ?? remoteAddress;
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
!this.users!.verifyPassword(candidateUser, ctx.password ?? "")
|
|
76
|
+
) {
|
|
77
|
+
ctx.reject();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
authUser = candidateUser;
|
|
82
|
+
sessionId = this.users!.registerSession(authUser, remoteAddress).id;
|
|
83
|
+
|
|
84
|
+
const homePath = `/home/${authUser}`;
|
|
85
|
+
if (!this.vfs!.exists(homePath)) {
|
|
86
|
+
this.vfs!.mkdir(homePath, 0o755);
|
|
87
|
+
this.vfs!.writeFile(
|
|
88
|
+
`${homePath}/README.txt`,
|
|
89
|
+
`Welcome to ${this.hostname}`,
|
|
90
|
+
);
|
|
91
|
+
void this.vfs!.flushMirror();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ctx.accept();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
ctx.reject();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
client.on("close", () => {
|
|
102
|
+
this.users!.unregisterSession(sessionId);
|
|
103
|
+
sessionId = null;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
client.on("ready", () => {
|
|
107
|
+
client.on("session", (accept) => {
|
|
108
|
+
const session = accept();
|
|
109
|
+
const terminalSize = { cols: 80, rows: 24 };
|
|
110
|
+
|
|
111
|
+
session.on("pty", (acceptPty, _rejectPty, info) => {
|
|
112
|
+
terminalSize.cols = info?.cols ?? terminalSize.cols;
|
|
113
|
+
terminalSize.rows = info?.rows ?? terminalSize.rows;
|
|
114
|
+
acceptPty();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
session.on(
|
|
118
|
+
"window-change",
|
|
119
|
+
(_acceptChange, _rejectChange, info) => {
|
|
120
|
+
terminalSize.cols = info?.cols ?? terminalSize.cols;
|
|
121
|
+
terminalSize.rows = info?.rows ?? terminalSize.rows;
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
session.on("shell", (acceptShell) => {
|
|
126
|
+
const stream = acceptShell();
|
|
127
|
+
startShell(
|
|
128
|
+
stream,
|
|
129
|
+
authUser,
|
|
130
|
+
this.vfs!,
|
|
131
|
+
this.hostname,
|
|
132
|
+
this.users!,
|
|
133
|
+
sessionId,
|
|
134
|
+
remoteAddress,
|
|
135
|
+
terminalSize,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
session.on("exec", (acceptExec, _rejectExec, info) => {
|
|
140
|
+
const stream = acceptExec();
|
|
141
|
+
runExec(
|
|
142
|
+
stream,
|
|
143
|
+
info.command.trim(),
|
|
144
|
+
authUser,
|
|
145
|
+
this.hostname,
|
|
146
|
+
this.users!,
|
|
147
|
+
this.vfs!,
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return new Promise<number>((resolve, reject) => {
|
|
156
|
+
this.server?.once("error", (err: unknown) => reject(err));
|
|
157
|
+
this.server?.listen(this.port, "127.0.0.1", () => {
|
|
158
|
+
console.log(`SSH Mimic listening on port ${this.port}`);
|
|
159
|
+
resolve(this.port);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Stops server if running.
|
|
166
|
+
*/
|
|
167
|
+
public stop(): void {
|
|
168
|
+
if (this.server) {
|
|
169
|
+
this.server.close(() => {
|
|
170
|
+
console.log("SSH Mimic stopped");
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Returns virtual filesystem instance after server started.
|
|
177
|
+
*
|
|
178
|
+
* @returns VirtualFileSystem or null when not started.
|
|
179
|
+
*/
|
|
180
|
+
public getVfs(): VirtualFileSystem | null {
|
|
181
|
+
return this.vfs;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Returns user manager instance after server started.
|
|
186
|
+
*
|
|
187
|
+
* @returns VirtualUserManager or null when not started.
|
|
188
|
+
*/
|
|
189
|
+
public getUsers(): VirtualUserManager | null {
|
|
190
|
+
return this.users;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Returns hostname shown in prompts and idents.
|
|
195
|
+
*
|
|
196
|
+
* @returns Configured hostname label.
|
|
197
|
+
*/
|
|
198
|
+
public getHostname(): string {
|
|
199
|
+
return this.hostname;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export { SshMimic };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function formatLoginDate(date: Date): string {
|
|
2
|
+
const weekday = date.toLocaleString("en-US", { weekday: "short" });
|
|
3
|
+
const month = date.toLocaleString("en-US", { month: "short" });
|
|
4
|
+
const day = date.getDate().toString().padStart(2, "0");
|
|
5
|
+
const hh = date.getHours().toString().padStart(2, "0");
|
|
6
|
+
const mm = date.getMinutes().toString().padStart(2, "0");
|
|
7
|
+
const ss = date.getSeconds().toString().padStart(2, "0");
|
|
8
|
+
const year = date.getFullYear();
|
|
9
|
+
return `${weekday} ${month} ${day} ${hh}:${mm}:${ss} ${year}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function buildPrompt(
|
|
2
|
+
user: string,
|
|
3
|
+
host: string,
|
|
4
|
+
cwdName: string,
|
|
5
|
+
): string {
|
|
6
|
+
const isRoot = user === "root";
|
|
7
|
+
const colorUser = isRoot ? "\u001b[31;1m" : "\u001b[35;1m";
|
|
8
|
+
const colorWhite = "\u001b[37;1m";
|
|
9
|
+
const colorBlue = "\u001b[34;1m";
|
|
10
|
+
const colorReset = "\u001b[0m";
|
|
11
|
+
const symbol = isRoot ? "#" : "$";
|
|
12
|
+
|
|
13
|
+
return `${colorWhite}[${colorUser}${user}${colorWhite}@${colorBlue}${host}${colorReset} ${cwdName}${colorWhite}]${colorReset}${symbol} `;
|
|
14
|
+
}
|