typescript-virtual-container 1.5.3 → 1.5.5

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.
Files changed (371) hide show
  1. package/README.md +44 -532
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/SSHMimic/executor.js +23 -5
  4. package/dist/VirtualPackageManager/index.js +10 -0
  5. package/dist/VirtualShell/shell.js +158 -11
  6. package/dist/commands/basename.d.ts +13 -0
  7. package/dist/commands/basename.js +45 -0
  8. package/dist/commands/bc.d.ts +2 -0
  9. package/dist/commands/bc.js +28 -0
  10. package/dist/commands/file.d.ts +8 -0
  11. package/dist/commands/file.js +57 -0
  12. package/dist/commands/fun.d.ts +32 -0
  13. package/dist/commands/fun.js +172 -0
  14. package/dist/commands/ip.d.ts +7 -0
  15. package/dist/commands/ip.js +52 -0
  16. package/dist/commands/jobs.d.ts +4 -0
  17. package/dist/commands/jobs.js +27 -0
  18. package/dist/commands/last.d.ts +13 -0
  19. package/dist/commands/last.js +68 -0
  20. package/dist/commands/manuals-bundle.js +598 -6
  21. package/dist/commands/registry.js +30 -2
  22. package/dist/commands/runtime.js +24 -3
  23. package/dist/commands/set.js +20 -0
  24. package/dist/commands/sh.js +74 -1
  25. package/dist/commands/tput.d.ts +13 -0
  26. package/dist/commands/tput.js +76 -0
  27. package/dist/commands/w.d.ts +7 -0
  28. package/dist/commands/w.js +38 -0
  29. package/dist/utils/expand.d.ts +12 -0
  30. package/dist/utils/expand.js +87 -1
  31. package/package.json +9 -3
  32. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -50
  33. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -31
  34. package/.github/dependabot.yml +0 -27
  35. package/.github/pull_request_template.md +0 -21
  36. package/.github/workflows/create-pull-request.yml +0 -85
  37. package/.github/workflows/publish.yml +0 -25
  38. package/.github/workflows/test-battery.yml +0 -102
  39. package/.vscode/settings.json +0 -20
  40. package/CODE_OF_CONDUCT.md +0 -39
  41. package/CONTRIBUTING.md +0 -59
  42. package/HONEYPOT.md +0 -358
  43. package/SECURITY.md +0 -33
  44. package/benchmark-results.txt +0 -40
  45. package/benchmark-virtualshell.ts +0 -88
  46. package/biome.json +0 -37
  47. package/build.js +0 -22
  48. package/builds/fortune-nyx-v1.5.3-directbash-k6.1.0.mjs +0 -1764
  49. package/builds/fortune-nyx-v1.5.3-ssh-nosftp.js +0 -1764
  50. package/builds/fortune-nyx-v1.5.3-ssh.cjs +0 -1765
  51. package/builds/fortune-nyx-v1.5.3-web.min.js +0 -17036
  52. package/bun.lock +0 -244
  53. package/docs/.nojekyll +0 -1
  54. package/docs/app.js +0 -1751
  55. package/docs/assets/hierarchy.js +0 -1
  56. package/docs/assets/highlight.css +0 -162
  57. package/docs/assets/icons.js +0 -18
  58. package/docs/assets/icons.svg +0 -1
  59. package/docs/assets/main.js +0 -60
  60. package/docs/assets/navigation.js +0 -1
  61. package/docs/assets/search.js +0 -1
  62. package/docs/assets/style.css +0 -1633
  63. package/docs/classes/HoneyPot.html +0 -31
  64. package/docs/classes/IdleManager.html +0 -162
  65. package/docs/classes/SshClient.html +0 -66
  66. package/docs/classes/VirtualFileSystem.html +0 -279
  67. package/docs/classes/VirtualPackageManager.html +0 -63
  68. package/docs/classes/VirtualSftpServer.html +0 -169
  69. package/docs/classes/VirtualShell.html +0 -285
  70. package/docs/classes/VirtualSshServer.html +0 -182
  71. package/docs/classes/VirtualUserManager.html +0 -276
  72. package/docs/demo.html +0 -82
  73. package/docs/functions/assertDiff.html +0 -6
  74. package/docs/functions/diffSnapshots.html +0 -7
  75. package/docs/functions/formatDiff.html +0 -6
  76. package/docs/functions/getArg.html +0 -13
  77. package/docs/functions/getFlag.html +0 -15
  78. package/docs/functions/ifFlag.html +0 -11
  79. package/docs/hierarchy.html +0 -1
  80. package/docs/index.html +0 -1869
  81. package/docs/interfaces/AuditLogEntry.html +0 -6
  82. package/docs/interfaces/CommandContext.html +0 -22
  83. package/docs/interfaces/CommandResult.html +0 -26
  84. package/docs/interfaces/ExecStream.html +0 -11
  85. package/docs/interfaces/HoneyPotStats.html +0 -16
  86. package/docs/interfaces/IdleManagerOptions.html +0 -7
  87. package/docs/interfaces/InstalledPackage.html +0 -20
  88. package/docs/interfaces/NanoEditorSession.html +0 -8
  89. package/docs/interfaces/PackageDefinition.html +0 -30
  90. package/docs/interfaces/PackageFile.html +0 -8
  91. package/docs/interfaces/PasswordChallenge.html +0 -16
  92. package/docs/interfaces/RemoveOptions.html +0 -4
  93. package/docs/interfaces/ShellEnv.html +0 -6
  94. package/docs/interfaces/ShellModule.html +0 -14
  95. package/docs/interfaces/ShellProperties.html +0 -14
  96. package/docs/interfaces/ShellStream.html +0 -11
  97. package/docs/interfaces/SudoChallenge.html +0 -24
  98. package/docs/interfaces/VfsBaseNode.html +0 -12
  99. package/docs/interfaces/VfsDiff.html +0 -10
  100. package/docs/interfaces/VfsDiffEntry.html +0 -6
  101. package/docs/interfaces/VfsDiffModified.html +0 -10
  102. package/docs/interfaces/VfsDirectoryNode.html +0 -15
  103. package/docs/interfaces/VfsFileNode.html +0 -17
  104. package/docs/interfaces/VfsOptions.html +0 -26
  105. package/docs/interfaces/VfsSnapshot.html +0 -3
  106. package/docs/interfaces/VfsSnapshotBaseNode.html +0 -8
  107. package/docs/interfaces/VfsSnapshotDirectoryNode.html +0 -10
  108. package/docs/interfaces/VfsSnapshotFileNode.html +0 -12
  109. package/docs/interfaces/VirtualActiveSession.html +0 -12
  110. package/docs/interfaces/VirtualSftpServerOptions.html +0 -7
  111. package/docs/interfaces/VirtualShellVfsLike.html +0 -15
  112. package/docs/interfaces/VirtualShellVfsOptions.html +0 -3
  113. package/docs/interfaces/WriteFileOptions.html +0 -6
  114. package/docs/media/LICENSE +0 -21
  115. package/docs/modules.html +0 -1
  116. package/docs/types/ArgParseOptions.html +0 -4
  117. package/docs/types/CommandMode.html +0 -2
  118. package/docs/types/CommandOutcome.html +0 -2
  119. package/docs/types/IdleState.html +0 -1
  120. package/docs/types/VfsNodeStats.html +0 -2
  121. package/docs/types/VfsNodeType.html +0 -2
  122. package/docs/types/VfsPersistenceMode.html +0 -5
  123. package/docs/types/VfsSnapshotNode.html +0 -2
  124. package/examples/README.md +0 -288
  125. package/examples/app.js +0 -1751
  126. package/examples/app.ts +0 -299
  127. package/examples/build.js +0 -27
  128. package/examples/demo.html +0 -33
  129. package/examples/honeypot-audit.ts +0 -180
  130. package/examples/honeypot-export.ts +0 -253
  131. package/examples/honeypot-quickstart.ts +0 -110
  132. package/examples/index.html +0 -82
  133. package/examples/server.js +0 -55
  134. package/polyfills/buffer.js +0 -117
  135. package/polyfills/node_child_process/index.js +0 -2
  136. package/polyfills/node_crypto/index.js +0 -167
  137. package/polyfills/node_events/index.js +0 -9
  138. package/polyfills/node_fs/index.js +0 -202
  139. package/polyfills/node_fs/promises.js +0 -4
  140. package/polyfills/node_os/index.js +0 -9
  141. package/polyfills/node_path/index.js +0 -28
  142. package/polyfills/node_vm/index.js +0 -7
  143. package/polyfills/node_zlib/index.js +0 -3
  144. package/polyfills/process.js +0 -14
  145. package/polyfills/ssh2/index.js +0 -75
  146. package/scripts/build-all.mjs +0 -226
  147. package/scripts/build-names.mjs +0 -43
  148. package/scripts/generate-manuals-bundle.mjs +0 -49
  149. package/scripts/postinstall.js +0 -42
  150. package/scripts/publish-package.sh +0 -70
  151. package/src/Honeypot/index.ts +0 -457
  152. package/src/SSHClient/index.ts +0 -270
  153. package/src/SSHMimic/exec.ts +0 -49
  154. package/src/SSHMimic/executor.ts +0 -251
  155. package/src/SSHMimic/hostKey.ts +0 -21
  156. package/src/SSHMimic/index.ts +0 -337
  157. package/src/SSHMimic/loginBanner.ts +0 -36
  158. package/src/SSHMimic/loginFormat.ts +0 -10
  159. package/src/SSHMimic/prompt.ts +0 -14
  160. package/src/SSHMimic/sftp.ts +0 -883
  161. package/src/VirtualFileSystem/binaryPack.ts +0 -258
  162. package/src/VirtualFileSystem/index.ts +0 -1193
  163. package/src/VirtualFileSystem/internalTypes.ts +0 -43
  164. package/src/VirtualFileSystem/journal.ts +0 -171
  165. package/src/VirtualFileSystem/path.ts +0 -74
  166. package/src/VirtualPackageManager/index.ts +0 -996
  167. package/src/VirtualShell/idleManager.ts +0 -137
  168. package/src/VirtualShell/index.ts +0 -475
  169. package/src/VirtualShell/shell.ts +0 -700
  170. package/src/VirtualShell/shellParser.ts +0 -285
  171. package/src/VirtualUserManager/index.ts +0 -758
  172. package/src/bun.d.ts +0 -1
  173. package/src/commands/adduser.ts +0 -103
  174. package/src/commands/alias.ts +0 -69
  175. package/src/commands/apt.ts +0 -233
  176. package/src/commands/awk.ts +0 -168
  177. package/src/commands/base64.ts +0 -29
  178. package/src/commands/cat.ts +0 -52
  179. package/src/commands/cd.ts +0 -25
  180. package/src/commands/chmod.ts +0 -85
  181. package/src/commands/clear.ts +0 -15
  182. package/src/commands/command-helpers.ts +0 -286
  183. package/src/commands/cp.ts +0 -83
  184. package/src/commands/curl.ts +0 -147
  185. package/src/commands/cut.ts +0 -36
  186. package/src/commands/date.ts +0 -30
  187. package/src/commands/declare.ts +0 -49
  188. package/src/commands/deluser.ts +0 -98
  189. package/src/commands/df.ts +0 -23
  190. package/src/commands/diff.ts +0 -43
  191. package/src/commands/dpkg.ts +0 -180
  192. package/src/commands/du.ts +0 -56
  193. package/src/commands/echo.ts +0 -58
  194. package/src/commands/env.ts +0 -23
  195. package/src/commands/exit.ts +0 -18
  196. package/src/commands/export.ts +0 -34
  197. package/src/commands/find.ts +0 -68
  198. package/src/commands/free.ts +0 -47
  199. package/src/commands/grep.ts +0 -116
  200. package/src/commands/groups.ts +0 -19
  201. package/src/commands/gzip.ts +0 -88
  202. package/src/commands/head.ts +0 -52
  203. package/src/commands/help.ts +0 -152
  204. package/src/commands/helpers.ts +0 -234
  205. package/src/commands/history.ts +0 -34
  206. package/src/commands/hostname.ts +0 -14
  207. package/src/commands/htop.ts +0 -20
  208. package/src/commands/id.ts +0 -19
  209. package/src/commands/index.ts +0 -9
  210. package/src/commands/kill.ts +0 -19
  211. package/src/commands/ln.ts +0 -71
  212. package/src/commands/ls.ts +0 -243
  213. package/src/commands/lsb-release.ts +0 -63
  214. package/src/commands/man.ts +0 -31
  215. package/src/commands/manuals/adduser.txt +0 -11
  216. package/src/commands/manuals/apt-cache.txt +0 -12
  217. package/src/commands/manuals/apt.txt +0 -20
  218. package/src/commands/manuals/awk.txt +0 -13
  219. package/src/commands/manuals/cat.txt +0 -14
  220. package/src/commands/manuals/cd.txt +0 -16
  221. package/src/commands/manuals/chmod.txt +0 -16
  222. package/src/commands/manuals/clear.txt +0 -10
  223. package/src/commands/manuals/cp.txt +0 -10
  224. package/src/commands/manuals/curl.txt +0 -20
  225. package/src/commands/manuals/date.txt +0 -14
  226. package/src/commands/manuals/declare.txt +0 -12
  227. package/src/commands/manuals/deluser.txt +0 -10
  228. package/src/commands/manuals/df.txt +0 -10
  229. package/src/commands/manuals/dpkg-query.txt +0 -11
  230. package/src/commands/manuals/dpkg.txt +0 -14
  231. package/src/commands/manuals/du.txt +0 -11
  232. package/src/commands/manuals/echo.txt +0 -11
  233. package/src/commands/manuals/false.txt +0 -10
  234. package/src/commands/manuals/find.txt +0 -11
  235. package/src/commands/manuals/free.txt +0 -12
  236. package/src/commands/manuals/grep.txt +0 -13
  237. package/src/commands/manuals/groups.txt +0 -10
  238. package/src/commands/manuals/gzip.txt +0 -11
  239. package/src/commands/manuals/head.txt +0 -10
  240. package/src/commands/manuals/help.txt +0 -11
  241. package/src/commands/manuals/history.txt +0 -11
  242. package/src/commands/manuals/hostname.txt +0 -10
  243. package/src/commands/manuals/id.txt +0 -10
  244. package/src/commands/manuals/kill.txt +0 -13
  245. package/src/commands/manuals/ls.txt +0 -20
  246. package/src/commands/manuals/lsb_release.txt +0 -14
  247. package/src/commands/manuals/mkdir.txt +0 -10
  248. package/src/commands/manuals/mv.txt +0 -10
  249. package/src/commands/manuals/nano.txt +0 -11
  250. package/src/commands/manuals/neofetch.txt +0 -10
  251. package/src/commands/manuals/node.txt +0 -13
  252. package/src/commands/manuals/npm.txt +0 -13
  253. package/src/commands/manuals/npx.txt +0 -13
  254. package/src/commands/manuals/passwd.txt +0 -11
  255. package/src/commands/manuals/ping.txt +0 -10
  256. package/src/commands/manuals/printf.txt +0 -11
  257. package/src/commands/manuals/ps.txt +0 -10
  258. package/src/commands/manuals/pwd.txt +0 -10
  259. package/src/commands/manuals/python3.txt +0 -13
  260. package/src/commands/manuals/readlink.txt +0 -10
  261. package/src/commands/manuals/return.txt +0 -10
  262. package/src/commands/manuals/rm.txt +0 -10
  263. package/src/commands/manuals/sed.txt +0 -11
  264. package/src/commands/manuals/set.txt +0 -11
  265. package/src/commands/manuals/shift.txt +0 -10
  266. package/src/commands/manuals/sleep.txt +0 -10
  267. package/src/commands/manuals/sort.txt +0 -12
  268. package/src/commands/manuals/source.txt +0 -11
  269. package/src/commands/manuals/ssh.txt +0 -11
  270. package/src/commands/manuals/stat.txt +0 -10
  271. package/src/commands/manuals/su.txt +0 -13
  272. package/src/commands/manuals/sudo.txt +0 -11
  273. package/src/commands/manuals/tail.txt +0 -10
  274. package/src/commands/manuals/tar.txt +0 -19
  275. package/src/commands/manuals/tee.txt +0 -10
  276. package/src/commands/manuals/test.txt +0 -11
  277. package/src/commands/manuals/touch.txt +0 -11
  278. package/src/commands/manuals/tr.txt +0 -10
  279. package/src/commands/manuals/trap.txt +0 -10
  280. package/src/commands/manuals/true.txt +0 -10
  281. package/src/commands/manuals/type.txt +0 -10
  282. package/src/commands/manuals/uname.txt +0 -12
  283. package/src/commands/manuals/uniq.txt +0 -12
  284. package/src/commands/manuals/unset.txt +0 -10
  285. package/src/commands/manuals/uptime.txt +0 -11
  286. package/src/commands/manuals/wc.txt +0 -12
  287. package/src/commands/manuals/wget.txt +0 -12
  288. package/src/commands/manuals/which.txt +0 -10
  289. package/src/commands/manuals/whoami.txt +0 -10
  290. package/src/commands/manuals/xargs.txt +0 -10
  291. package/src/commands/manuals-bundle.ts +0 -898
  292. package/src/commands/mkdir.ts +0 -31
  293. package/src/commands/mv.ts +0 -50
  294. package/src/commands/nano.ts +0 -38
  295. package/src/commands/neofetch.ts +0 -53
  296. package/src/commands/node.ts +0 -341
  297. package/src/commands/npm.ts +0 -132
  298. package/src/commands/passwd.ts +0 -50
  299. package/src/commands/ping.ts +0 -32
  300. package/src/commands/printf.ts +0 -129
  301. package/src/commands/ps.ts +0 -58
  302. package/src/commands/pwd.ts +0 -9
  303. package/src/commands/python.ts +0 -2229
  304. package/src/commands/read.ts +0 -46
  305. package/src/commands/registry.ts +0 -249
  306. package/src/commands/rm.ts +0 -42
  307. package/src/commands/runtime.ts +0 -421
  308. package/src/commands/sed.ts +0 -68
  309. package/src/commands/seq.ts +0 -43
  310. package/src/commands/set.ts +0 -29
  311. package/src/commands/sh.ts +0 -467
  312. package/src/commands/shift.ts +0 -63
  313. package/src/commands/sleep.ts +0 -20
  314. package/src/commands/sort.ts +0 -46
  315. package/src/commands/source.ts +0 -52
  316. package/src/commands/stat.ts +0 -61
  317. package/src/commands/su.ts +0 -72
  318. package/src/commands/sudo.ts +0 -76
  319. package/src/commands/tail.ts +0 -53
  320. package/src/commands/tar.ts +0 -102
  321. package/src/commands/tee.ts +0 -36
  322. package/src/commands/test.ts +0 -137
  323. package/src/commands/touch.ts +0 -28
  324. package/src/commands/tr.ts +0 -70
  325. package/src/commands/tree.ts +0 -20
  326. package/src/commands/true.ts +0 -27
  327. package/src/commands/type.ts +0 -48
  328. package/src/commands/uname.ts +0 -29
  329. package/src/commands/uniq.ts +0 -39
  330. package/src/commands/unset.ts +0 -17
  331. package/src/commands/uptime.ts +0 -54
  332. package/src/commands/wc.ts +0 -55
  333. package/src/commands/wget.ts +0 -148
  334. package/src/commands/which.ts +0 -37
  335. package/src/commands/who.ts +0 -25
  336. package/src/commands/whoami.ts +0 -14
  337. package/src/commands/xargs.ts +0 -31
  338. package/src/index.ts +0 -67
  339. package/src/modules/linuxRootfs.ts +0 -1961
  340. package/src/modules/neofetch.ts +0 -358
  341. package/src/modules/shellInteractive.ts +0 -57
  342. package/src/modules/shellRuntime.ts +0 -76
  343. package/src/self-standalone.ts +0 -542
  344. package/src/standalone-wo-sftp.ts +0 -38
  345. package/src/standalone.ts +0 -72
  346. package/src/types/commands.ts +0 -146
  347. package/src/types/pipeline.ts +0 -52
  348. package/src/types/streams.ts +0 -32
  349. package/src/types/tar-stream.d.ts +0 -38
  350. package/src/types/vfs.ts +0 -98
  351. package/src/utils/expand.ts +0 -491
  352. package/src/utils/perfLogger.ts +0 -72
  353. package/src/utils/tokenize.ts +0 -98
  354. package/src/utils/vfsDiff.ts +0 -275
  355. package/tests/command-helpers.test.ts +0 -116
  356. package/tests/commands-admin-net.test.ts +0 -441
  357. package/tests/commands-advanced.test.ts +0 -456
  358. package/tests/commands-core.test.ts +0 -562
  359. package/tests/commands-missing.test.ts +0 -570
  360. package/tests/commands-specific-units.test.ts +0 -327
  361. package/tests/commands-text-sys.test.ts +0 -445
  362. package/tests/expand.test.ts +0 -170
  363. package/tests/helpers.test.ts +0 -97
  364. package/tests/new-features.test.ts +0 -1036
  365. package/tests/parser-executor.test.ts +0 -37
  366. package/tests/sftp.test.ts +0 -323
  367. package/tests/ssh-exec.test.ts +0 -45
  368. package/tests/test-helper.ts +0 -79
  369. package/tests/users.test.ts +0 -86
  370. package/tsconfig.json +0 -49
  371. package/typedoc.json +0 -47
@@ -1,542 +0,0 @@
1
- import { readFile, unlink, writeFile } from "node:fs/promises";
2
- import * as path from "node:path";
3
- import { basename } from "node:path";
4
- import { stdin, stdout } from "node:process";
5
- import { createInterface, type Interface } from "node:readline";
6
-
7
- import { getCommandNames } from "./commands/registry";
8
- import { makeDefaultEnv, runCommand, userHome } from "./commands/runtime";
9
- import { spawnNanoEditorProcess } from "./modules/shellInteractive";
10
- import { resolvePath } from "./modules/shellRuntime";
11
- import { buildLoginBanner, type LoginBannerState } from "./SSHMimic/loginBanner";
12
- import { buildPrompt } from "./SSHMimic/prompt";
13
- import type { CommandResult, PasswordChallenge, SudoChallenge } from "./types/commands";
14
- import type VirtualFileSystem from "./VirtualFileSystem";
15
- import { VirtualShell } from "./VirtualShell";
16
-
17
- const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
18
- const argv = process.argv.slice(2);
19
-
20
- // ── CLI args ──────────────────────────────────────────────────────────────────
21
- console.clear();
22
- function readUserArg(): string {
23
- for (let index = 0; index < argv.length; index += 1) {
24
- const current = argv[index];
25
- if (current === "--user") {
26
- const next = argv[index + 1];
27
- if (!next || next.startsWith("--")) {
28
- throw new Error("self-standalone: --user requires a value");
29
- }
30
- return next;
31
- }
32
- if (current?.startsWith("--user=")) {
33
- return current.slice("--user=".length) || "root";
34
- }
35
- }
36
- return "root";
37
- }
38
-
39
- const initialUser = readUserArg();
40
- const virtualShell = new VirtualShell(hostname, undefined, {
41
- mode: "fs",
42
- snapshotPath: ".vfs",
43
- });
44
-
45
- // ── VFS helpers ───────────────────────────────────────────────────────────────
46
-
47
- function readLastLogin(username: string): LoginBannerState | null {
48
- const lastlogPath = `/home/${username}/.lastlog`;
49
- if (!virtualShell.vfs.exists(lastlogPath)) return null;
50
- try {
51
- return JSON.parse(virtualShell.vfs.readFile(lastlogPath)) as LoginBannerState;
52
- } catch {
53
- return null;
54
- }
55
- }
56
-
57
- function writeLastLogin(username: string, from: string): void {
58
- virtualShell.vfs.writeFile(
59
- `/home/${username}/.lastlog`,
60
- JSON.stringify({ at: new Date().toISOString(), from }),
61
- );
62
- }
63
-
64
- async function flushVfs(): Promise<void> {
65
- await virtualShell.vfs.stopAutoFlush();
66
- }
67
-
68
- function loadHistory(authUser: string): string[] {
69
- const historyPath = `${userHome(authUser)}/.bash_history`;
70
- if (!virtualShell.vfs.exists(historyPath)) {
71
- virtualShell.vfs.writeFile(historyPath, "");
72
- return [];
73
- }
74
- return virtualShell.vfs
75
- .readFile(historyPath)
76
- .split("\n")
77
- .map((line) => line.trim())
78
- .filter((line) => line.length > 0);
79
- }
80
-
81
- function saveHistory(history: string[], authUser: string): void {
82
- const data = history.length > 0 ? `${history.join("\n")}\n` : "";
83
- virtualShell.vfs.writeFile(`${userHome(authUser)}/.bash_history`, data);
84
- }
85
-
86
- // ── Tab completion ────────────────────────────────────────────────────────────
87
-
88
- function listPathCompletions(vfs: VirtualFileSystem, cwd: string, prefix: string): string[] {
89
- const slashIndex = prefix.lastIndexOf("/");
90
- const dirPart = slashIndex >= 0 ? prefix.slice(0, slashIndex + 1) : "";
91
- const namePart = slashIndex >= 0 ? prefix.slice(slashIndex + 1) : prefix;
92
- const basePath = resolvePath(cwd, dirPart || ".");
93
- try {
94
- return vfs
95
- .list(basePath)
96
- .filter((e) => !e.startsWith(".") && e.startsWith(namePart))
97
- .map((e) => {
98
- const fullPath = path.posix.join(basePath, e);
99
- const st = vfs.stat(fullPath);
100
- return `${dirPart}${e}${st.type === "directory" ? "/" : ""}`;
101
- })
102
- .sort();
103
- } catch {
104
- return [];
105
- }
106
- }
107
-
108
- function makeCompleter(getState: () => { cwd: string }) {
109
- const commandNames = Array.from(new Set(getCommandNames())).sort();
110
- return (line: string, cb: (err: null, result: [string[], string]) => void): void => {
111
- const { cwd } = getState();
112
- // Extract the token under/before cursor (last whitespace-separated word)
113
- const token = line.split(/\s+/).at(-1) ?? "";
114
- const isFirstToken = line.trimStart() === token;
115
- const cmdHits = isFirstToken ? commandNames.filter((n) => n.startsWith(token)) : [];
116
- const pathHits = listPathCompletions(virtualShell.vfs, cwd, token);
117
- const hits = Array.from(new Set([...cmdHits, ...pathHits])).sort();
118
- cb(null, [hits, token]);
119
- };
120
- }
121
-
122
- // ── Hidden password input ─────────────────────────────────────────────────────
123
-
124
- function askHiddenQuestion(rl: Interface, promptText: string): Promise<string> {
125
- return new Promise((resolve) => {
126
- if (!stdin.isTTY || !stdout.isTTY) {
127
- rl.question(promptText, resolve);
128
- return;
129
- }
130
-
131
- const wasRawMode = Boolean(stdin.isRaw);
132
- let buffer = "";
133
-
134
- const cleanup = (): void => {
135
- stdin.off("data", onData);
136
- if (!wasRawMode) stdin.setRawMode(false);
137
- };
138
-
139
- const finish = (value: string): void => {
140
- cleanup();
141
- stdout.write("\n");
142
- resolve(value);
143
- };
144
-
145
- const onData = (chunk: Buffer): void => {
146
- const input = chunk.toString("utf8");
147
- for (let i = 0; i < input.length; i += 1) {
148
- const ch = input[i]!;
149
- if (ch === "\r" || ch === "\n") { finish(buffer); return; }
150
- if (ch === "\u007f" || ch === "\b") { buffer = buffer.slice(0, -1); continue; }
151
- if (ch >= " ") buffer += ch;
152
- }
153
- };
154
-
155
- // Pause readline so it doesn't eat our raw keystrokes
156
- rl.pause();
157
- stdout.write(promptText);
158
- if (!wasRawMode) stdin.setRawMode(true);
159
- stdin.resume();
160
- stdin.on("data", onData);
161
- });
162
- }
163
-
164
- // ── Session state helper ──────────────────────────────────────────────────────
165
-
166
- function applySessionState(
167
- authUserState: string,
168
- cwdState: string,
169
- result: CommandResult,
170
- shellEnvState: ReturnType<typeof makeDefaultEnv>,
171
- ): { authUser: string; cwd: string } {
172
- let authUser = authUserState;
173
- let cwd = cwdState;
174
- if (result.switchUser) {
175
- authUser = result.switchUser;
176
- cwd = result.nextCwd ?? userHome(authUser);
177
- shellEnvState.vars.USER = authUser;
178
- shellEnvState.vars.LOGNAME = authUser;
179
- shellEnvState.vars.HOME = userHome(authUser);
180
- shellEnvState.vars.PWD = cwd;
181
- } else if (result.nextCwd) {
182
- cwd = result.nextCwd;
183
- shellEnvState.vars.PWD = cwd;
184
- }
185
- return { authUser, cwd };
186
- }
187
-
188
- // ── Demo command ──────────────────────────────────────────────────────────────
189
-
190
- virtualShell.addCommand("demo", [], () => ({
191
- stdout: "This is a demo command. It does nothing useful.",
192
- exitCode: 0,
193
- }));
194
-
195
- // ── Main shell ────────────────────────────────────────────────────────────────
196
-
197
- async function runReadlineShell(): Promise<void> {
198
- await virtualShell.ensureInitialized();
199
-
200
- const selectedUser = initialUser.trim() || "root";
201
- if (virtualShell.users.getPasswordHash(selectedUser) === null) {
202
- process.stderr.write(`self-standalone: user '${selectedUser}' does not exist\n`);
203
- process.exit(1);
204
- }
205
-
206
- // Ensure home dir and README.txt exist (mirrors SSHMimic/VirtualUserManager behaviour)
207
- const homePath = selectedUser === "root" ? "/root" : userHome(selectedUser);
208
- if (!virtualShell.vfs.exists(homePath)) {
209
- virtualShell.vfs.mkdir(homePath, selectedUser === "root" ? 0o700 : 0o755);
210
- }
211
- const readmePath = `${homePath}/README.txt`;
212
- if (!virtualShell.vfs.exists(readmePath)) {
213
- virtualShell.vfs.writeFile(
214
- readmePath,
215
- `Welcome to ${hostname}\n`,
216
- );
217
- await virtualShell.vfs.stopAutoFlush();
218
- }
219
-
220
- const shellEnv = makeDefaultEnv(selectedUser, hostname);
221
- let authUser = selectedUser;
222
- let cwd = userHome(authUser);
223
- shellEnv.vars.PWD = cwd;
224
- const remoteAddress = "localhost";
225
- const terminalSize = { cols: stdout.columns ?? 80, rows: stdout.rows ?? 24 };
226
-
227
- let history = loadHistory(authUser);
228
-
229
- // completer reads cwd via closure — always current
230
- const rl = createInterface({
231
- input: stdin,
232
- output: stdout,
233
- terminal: true,
234
- completer: makeCompleter(() => ({ cwd })),
235
- });
236
-
237
- // Sync readline's internal history with our VFS history
238
- const rlWithHistory = rl as Interface & { history: string[] };
239
- rlWithHistory.history = [...history].reverse();
240
-
241
- // ── nano editor ────────────────────────────────────────────────────────────
242
-
243
- async function startNanoEditor(
244
- targetPath: string,
245
- initialContent: string,
246
- tempPath: string,
247
- ): Promise<void> {
248
- if (virtualShell.vfs.exists(targetPath)) {
249
- await writeFile(tempPath, initialContent, "utf8");
250
- }
251
-
252
- rl.pause();
253
-
254
- const editor = spawnNanoEditorProcess(
255
- tempPath,
256
- terminalSize,
257
- {
258
- write: stdout.write.bind(stdout),
259
- exit: () => undefined,
260
- end: () => undefined,
261
- } as unknown as Parameters<typeof spawnNanoEditorProcess>[2],
262
- );
263
-
264
- const wasRawMode = Boolean(stdin.isRaw);
265
- const forwardInput = (chunk: Buffer): void => { editor.stdin.write(chunk); };
266
-
267
- stdin.resume();
268
- if (!wasRawMode) stdin.setRawMode(true);
269
- stdin.on("data", forwardInput);
270
-
271
- await new Promise<void>((resolve) => {
272
- const cleanup = (): void => {
273
- stdin.off("data", forwardInput);
274
- if (!wasRawMode) stdin.setRawMode(false);
275
- rl.resume();
276
- };
277
-
278
- editor.on("error", (error: Error) => {
279
- cleanup();
280
- stdout.write(`nano: ${error.message}\r\n`);
281
- resolve();
282
- });
283
-
284
- editor.on("close", async () => {
285
- cleanup();
286
- rl.write("", { ctrl: true, name: "u" });
287
- try {
288
- const updatedContent = await readFile(tempPath, "utf8");
289
- virtualShell.writeFileAsUser(authUser, targetPath, updatedContent);
290
- await flushVfs();
291
- } catch {
292
- // save skipped or temp file missing
293
- }
294
- await unlink(tempPath).catch(() => undefined);
295
- stdout.write("\r\n");
296
- resolve();
297
- });
298
- });
299
- }
300
-
301
- // ── challenge handlers ─────────────────────────────────────────────────────
302
-
303
- async function handleSudoChallenge(challenge: SudoChallenge): Promise<void> {
304
- if (challenge.onPassword) {
305
- let promptText = challenge.prompt;
306
- while (true) {
307
- const typed = await askHiddenQuestion(rl, promptText);
308
- const step = await challenge.onPassword(typed, virtualShell);
309
- if (step.result === null) {
310
- promptText = step.nextPrompt ?? promptText;
311
- continue;
312
- }
313
- await handleCommandResult(step.result);
314
- return;
315
- }
316
- }
317
-
318
- const password = await askHiddenQuestion(rl, challenge.prompt);
319
- if (!virtualShell.users.verifyPassword(challenge.username, password)) {
320
- process.stderr.write("Sorry, try again.\n");
321
- return;
322
- }
323
-
324
- if (!challenge.commandLine) {
325
- authUser = challenge.targetUser;
326
- cwd = userHome(authUser);
327
- shellEnv.vars.USER = authUser;
328
- shellEnv.vars.LOGNAME = authUser;
329
- shellEnv.vars.HOME = userHome(authUser);
330
- shellEnv.vars.PWD = cwd;
331
- return;
332
- }
333
-
334
- const runCwd = challenge.loginShell ? userHome(challenge.targetUser) : cwd;
335
- const nestedResult = await runCommand(
336
- challenge.commandLine,
337
- challenge.targetUser,
338
- hostname,
339
- "shell",
340
- runCwd,
341
- virtualShell,
342
- undefined,
343
- shellEnv,
344
- );
345
- await handleCommandResult(nestedResult);
346
- }
347
-
348
- async function handlePasswordChallenge(challenge: PasswordChallenge): Promise<void> {
349
- const first = await askHiddenQuestion(rl, challenge.prompt);
350
- if (challenge.confirmPrompt) {
351
- const second = await askHiddenQuestion(rl, challenge.confirmPrompt);
352
- if (second !== first) {
353
- process.stderr.write("passwords do not match\n");
354
- return;
355
- }
356
- }
357
-
358
- switch (challenge.action) {
359
- case "passwd":
360
- await virtualShell.users.setPassword(challenge.targetUsername, first);
361
- stdout.write("passwd: password updated successfully\n");
362
- break;
363
- case "adduser":
364
- if (!challenge.newUsername) {
365
- process.stderr.write("adduser: missing username\n");
366
- return;
367
- }
368
- await virtualShell.users.addUser(challenge.newUsername, first);
369
- stdout.write(`adduser: user '${challenge.newUsername}' created\n`);
370
- break;
371
- case "deluser":
372
- await virtualShell.users.deleteUser(challenge.targetUsername);
373
- stdout.write(`Removing user '${challenge.targetUsername}' ...\ndeluser: done.\n`);
374
- break;
375
- case "su":
376
- authUser = challenge.targetUsername;
377
- cwd = userHome(authUser);
378
- shellEnv.vars.USER = authUser;
379
- shellEnv.vars.LOGNAME = authUser;
380
- shellEnv.vars.HOME = userHome(authUser);
381
- shellEnv.vars.PWD = cwd;
382
- break;
383
- }
384
- }
385
-
386
- // handleCommandResult must be declared before the "line" handler
387
- async function handleCommandResult(result: CommandResult): Promise<void> {
388
- if (result.openEditor) {
389
- await startNanoEditor(
390
- result.openEditor.targetPath,
391
- result.openEditor.initialContent,
392
- result.openEditor.tempPath,
393
- );
394
- return;
395
- }
396
-
397
- if (result.sudoChallenge) {
398
- await handleSudoChallenge(result.sudoChallenge);
399
- return;
400
- }
401
-
402
- if (result.passwordChallenge) {
403
- await handlePasswordChallenge(result.passwordChallenge);
404
- return;
405
- }
406
-
407
- if (result.clearScreen) {
408
- stdout.write("\u001b[2J\u001b[H");
409
- console.clear();
410
- }
411
-
412
- if (result.stdout) {
413
- stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}\n`);
414
- }
415
-
416
- if (result.stderr) {
417
- process.stderr.write(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`);
418
- }
419
-
420
- const updated = applySessionState(authUser, cwd, result, shellEnv);
421
- authUser = updated.authUser;
422
- cwd = updated.cwd;
423
-
424
- if (result.closeSession) {
425
- await flushVfs();
426
- rl.close();
427
- process.exit(result.exitCode ?? 0);
428
- }
429
- }
430
-
431
- // ── Prompt helper ──────────────────────────────────────────────────────────
432
-
433
- const renderPrompt = (): string => {
434
- const cwdLabel = cwd === userHome(authUser) ? "~" : basename(cwd) || "/";
435
- return buildPrompt(authUser, hostname, cwdLabel);
436
- };
437
-
438
- const prompt = (): void => {
439
- rl.setPrompt(renderPrompt());
440
- rl.prompt();
441
- };
442
-
443
- // ── Auth (password gate) ───────────────────────────────────────────────────
444
-
445
- if (process.env.USER !== "root" && virtualShell.users.hasPassword(authUser)) {
446
- const password = await askHiddenQuestion(rl, `Password for ${authUser}: `);
447
- if (!virtualShell.users.verifyPassword(authUser, password)) {
448
- process.stderr.write("self-standalone: authentication failed\n");
449
- process.exit(1);
450
- }
451
- }
452
-
453
- // ── Login banner ───────────────────────────────────────────────────────────
454
-
455
- stdout.write(buildLoginBanner(hostname, virtualShell.properties, readLastLogin(authUser)));
456
- writeLastLogin(authUser, remoteAddress);
457
- await flushVfs();
458
-
459
- // ── Event-driven line handler (enables completer) ──────────────────────────
460
- //
461
- // Key insight: readline's completer only fires when readline itself owns
462
- // stdin (i.e. rl is not paused). We use the event-driven "line" pattern
463
- // instead of a while(true)+rl.once("line") loop so readline stays active
464
- // between commands. We pause only while awaiting async work, then resume
465
- // immediately before re-prompting so the next Tab press is caught.
466
-
467
- let busy = false;
468
-
469
- rl.on("line", async (inputLine: string) => {
470
- if (busy) return; // shouldn't happen but guard re-entrancy
471
- busy = true;
472
- rl.pause();
473
-
474
- const trimmed = inputLine.trim();
475
- if (trimmed.length > 0) {
476
- history.push(inputLine);
477
- if (history.length > 500) history = history.slice(history.length - 500);
478
- saveHistory(history, authUser);
479
- rlWithHistory.history = [...history].reverse();
480
- }
481
-
482
- const result = await runCommand(
483
- inputLine,
484
- authUser,
485
- hostname,
486
- "shell",
487
- cwd,
488
- virtualShell,
489
- undefined,
490
- shellEnv,
491
- );
492
- await handleCommandResult(result);
493
- await flushVfs();
494
-
495
- busy = false;
496
- // Resume before prompt so readline can handle Tab on the next input
497
- rl.resume();
498
- prompt();
499
- });
500
-
501
- rl.on("SIGINT", () => {
502
- stdout.write("^C\n");
503
- rl.write("", { ctrl: true, name: "u" });
504
- prompt();
505
- });
506
-
507
- rl.on("close", () => {
508
- void flushVfs().then(() => {
509
- console.log("");
510
- process.exit(0);
511
- });
512
- });
513
-
514
- // Initial prompt — readline is already active, completer live from first keystroke
515
- prompt();
516
- }
517
-
518
- runReadlineShell().catch((error: unknown) => {
519
- console.error("Failed to start readline SSH emulation:", error);
520
- process.exit(1);
521
- });
522
-
523
-
524
- // ── Graceful shutdown (process-level) ────────────────────────────────────────
525
- let _shuttingDown = false;
526
- async function _gracefulShutdown(signal: string): Promise<void> {
527
- if (_shuttingDown) return;
528
- _shuttingDown = true;
529
- process.stdout.write(`\n[${signal}] Saving VFS...\n`);
530
- try { await virtualShell.vfs.stopAutoFlush(); } catch {}
531
- process.exit(0);
532
- }
533
- process.on("SIGTERM", () => { void _gracefulShutdown("SIGTERM"); });
534
- process.on("beforeExit", () => { void virtualShell.vfs.stopAutoFlush(); });
535
-
536
- process.on("uncaughtException", (error) => {
537
- console.error("Uncaught exception:", error);
538
- });
539
-
540
- process.on("unhandledRejection", (error, promise) => {
541
- console.error("Unhandled rejection at:", promise, "error:", error);
542
- });
@@ -1,38 +0,0 @@
1
- import { SshMimic } from "./SSHMimic/index";
2
- import { VirtualShell } from "./VirtualShell";
3
-
4
- const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
5
- const virtualShell = new VirtualShell(hostname, undefined, {
6
- mode: "fs",
7
- snapshotPath: ".vfs",
8
- });
9
-
10
- virtualShell.addCommand("demo", [], () => {
11
- return {
12
- stdout: "This is a demo command. It does nothing useful.",
13
- exitCode: 0,
14
- };
15
- });
16
-
17
- new SshMimic({
18
- port: 2222,
19
- hostname,
20
- shell: virtualShell,
21
- })
22
- .start()
23
- .catch((error: unknown) => {
24
- console.error("Failed to start SSH Mimic:", error);
25
- process.exit(1);
26
- });
27
-
28
- process.on("uncaughtException", (error) => {
29
- console.log("Oh my god, something terrible happened: ", error);
30
- });
31
-
32
- process.on("unhandledRejection", (error, promise) => {
33
- console.log(
34
- " Oh Lord! We forgot to handle a promise rejection here: ",
35
- promise,
36
- );
37
- console.log(" The error was: ", error);
38
- });
package/src/standalone.ts DELETED
@@ -1,72 +0,0 @@
1
- import { VirtualSftpServer, VirtualShell, VirtualSshServer } from ".";
2
-
3
- const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
4
- const virtualShell = new VirtualShell(hostname, undefined, {
5
- mode: "fs",
6
- snapshotPath: ".vfs",
7
- });
8
-
9
- virtualShell.addCommand("demo", [], () => {
10
- return {
11
- stdout: "This is a demo command. It does nothing useful.",
12
- exitCode: 0,
13
- };
14
- });
15
-
16
- new VirtualSshServer({
17
- port: 2222,
18
- hostname,
19
- shell: virtualShell,
20
- })
21
- .start()
22
- .catch((error: unknown) => {
23
- console.error("Failed to start SSH Mimic:", error);
24
- process.exit(1);
25
- });
26
-
27
- new VirtualSftpServer({ port: 2223, hostname, shell: virtualShell })
28
- .start()
29
- .catch((error: unknown) => {
30
- console.error("Failed to start SFTP Mimic:", error);
31
- process.exit(1);
32
- });
33
-
34
-
35
- // ── Graceful shutdown ─────────────────────────────────────────────────────────
36
- // On SIGINT / SIGTERM: flush the WAL journal to a full checkpoint before exit.
37
- // A kill -9 or OOM crash is unrecoverable here, but the WAL journal on disk
38
- // guarantees all writes since the last checkpoint are replayed on next start.
39
- let isShuttingDown = false;
40
- async function gracefulShutdown(signal: string): Promise<void> {
41
- if (isShuttingDown) return;
42
- isShuttingDown = true;
43
- console.log(`\n[${signal}] Flushing VFS checkpoint before exit...`);
44
- try {
45
- await virtualShell.vfs.stopAutoFlush();
46
- console.log("[shutdown] Checkpoint written. Goodbye.");
47
- } catch (err) {
48
- console.error("[shutdown] Flush failed:", err);
49
- }
50
- process.exit(0);
51
- }
52
-
53
- process.on("SIGINT", () => { void gracefulShutdown("SIGINT"); });
54
- process.on("SIGTERM", () => { void gracefulShutdown("SIGTERM"); });
55
- process.on("beforeExit", () => { void virtualShell.vfs.stopAutoFlush(); });
56
-
57
- process.on("uncaughtException", (error) => {
58
- console.debug("Oh my god, something terrible happened: ", error);
59
- });
60
-
61
- process.on("unhandledRejection", (error, promise) => {
62
- console.debug(
63
- " Oh Lord! We forgot to handle a promise rejection here: ",
64
- promise,
65
- );
66
- console.debug(" The error was: ", error);
67
- });
68
-
69
- setInterval(() => {
70
- const rss = process.memoryUsage().rss; // Just keep the event loop alive and prevent exit
71
- console.debug(`Current memory usage: ${Math.round(rss / 1024 / 1024)} MB`);
72
- }, 1000);