typescript-virtual-container 1.5.2 → 1.5.4

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 (364) hide show
  1. package/README.md +43 -23
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/SSHMimic/executor.js +23 -5
  4. package/dist/commands/basename.d.ts +13 -0
  5. package/dist/commands/basename.js +45 -0
  6. package/dist/commands/file.d.ts +8 -0
  7. package/dist/commands/file.js +57 -0
  8. package/dist/commands/fun.d.ts +32 -0
  9. package/dist/commands/fun.js +172 -0
  10. package/dist/commands/ifconfig.d.ts +7 -0
  11. package/dist/commands/ifconfig.js +52 -0
  12. package/dist/commands/last.d.ts +13 -0
  13. package/dist/commands/last.js +68 -0
  14. package/dist/commands/manuals-bundle.js +598 -6
  15. package/dist/commands/registry.js +24 -2
  16. package/dist/commands/runtime.js +159 -106
  17. package/dist/commands/sh.js +5 -0
  18. package/dist/commands/tput.d.ts +13 -0
  19. package/dist/commands/tput.js +76 -0
  20. package/dist/commands/w.d.ts +7 -0
  21. package/dist/commands/w.js +38 -0
  22. package/dist/utils/expand.d.ts +12 -0
  23. package/dist/utils/expand.js +84 -0
  24. package/package.json +9 -3
  25. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -50
  26. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -31
  27. package/.github/dependabot.yml +0 -27
  28. package/.github/pull_request_template.md +0 -21
  29. package/.github/workflows/create-pull-request.yml +0 -85
  30. package/.github/workflows/publish.yml +0 -25
  31. package/.github/workflows/test-battery.yml +0 -102
  32. package/.vscode/settings.json +0 -20
  33. package/CODE_OF_CONDUCT.md +0 -39
  34. package/CONTRIBUTING.md +0 -59
  35. package/HONEYPOT.md +0 -358
  36. package/SECURITY.md +0 -33
  37. package/benchmark-results.txt +0 -40
  38. package/benchmark-virtualshell.ts +0 -88
  39. package/biome.json +0 -37
  40. package/build.js +0 -22
  41. package/builds/fortune-nyx-v1.5.1-directbash-k6.1.0.mjs +0 -1768
  42. package/builds/fortune-nyx-v1.5.1-ssh-nosftp.js +0 -1768
  43. package/builds/fortune-nyx-v1.5.1-ssh.cjs +0 -1769
  44. package/builds/fortune-nyx-v1.5.1-web.min.js +0 -17022
  45. package/bun.lock +0 -244
  46. package/docs/.nojekyll +0 -1
  47. package/docs/app.js +0 -1755
  48. package/docs/assets/hierarchy.js +0 -1
  49. package/docs/assets/highlight.css +0 -162
  50. package/docs/assets/icons.js +0 -18
  51. package/docs/assets/icons.svg +0 -1
  52. package/docs/assets/main.js +0 -60
  53. package/docs/assets/navigation.js +0 -1
  54. package/docs/assets/search.js +0 -1
  55. package/docs/assets/style.css +0 -1633
  56. package/docs/classes/HoneyPot.html +0 -31
  57. package/docs/classes/IdleManager.html +0 -162
  58. package/docs/classes/SshClient.html +0 -66
  59. package/docs/classes/VirtualFileSystem.html +0 -279
  60. package/docs/classes/VirtualPackageManager.html +0 -63
  61. package/docs/classes/VirtualSftpServer.html +0 -169
  62. package/docs/classes/VirtualShell.html +0 -285
  63. package/docs/classes/VirtualSshServer.html +0 -182
  64. package/docs/classes/VirtualUserManager.html +0 -276
  65. package/docs/demo.html +0 -82
  66. package/docs/functions/assertDiff.html +0 -6
  67. package/docs/functions/diffSnapshots.html +0 -7
  68. package/docs/functions/formatDiff.html +0 -6
  69. package/docs/functions/getArg.html +0 -13
  70. package/docs/functions/getFlag.html +0 -15
  71. package/docs/functions/ifFlag.html +0 -11
  72. package/docs/hierarchy.html +0 -1
  73. package/docs/index.html +0 -1869
  74. package/docs/interfaces/AuditLogEntry.html +0 -6
  75. package/docs/interfaces/CommandContext.html +0 -22
  76. package/docs/interfaces/CommandResult.html +0 -26
  77. package/docs/interfaces/ExecStream.html +0 -11
  78. package/docs/interfaces/HoneyPotStats.html +0 -16
  79. package/docs/interfaces/IdleManagerOptions.html +0 -7
  80. package/docs/interfaces/InstalledPackage.html +0 -20
  81. package/docs/interfaces/NanoEditorSession.html +0 -8
  82. package/docs/interfaces/PackageDefinition.html +0 -30
  83. package/docs/interfaces/PackageFile.html +0 -8
  84. package/docs/interfaces/PasswordChallenge.html +0 -16
  85. package/docs/interfaces/RemoveOptions.html +0 -4
  86. package/docs/interfaces/ShellEnv.html +0 -6
  87. package/docs/interfaces/ShellModule.html +0 -14
  88. package/docs/interfaces/ShellProperties.html +0 -14
  89. package/docs/interfaces/ShellStream.html +0 -11
  90. package/docs/interfaces/SudoChallenge.html +0 -24
  91. package/docs/interfaces/VfsBaseNode.html +0 -12
  92. package/docs/interfaces/VfsDiff.html +0 -10
  93. package/docs/interfaces/VfsDiffEntry.html +0 -6
  94. package/docs/interfaces/VfsDiffModified.html +0 -10
  95. package/docs/interfaces/VfsDirectoryNode.html +0 -15
  96. package/docs/interfaces/VfsFileNode.html +0 -17
  97. package/docs/interfaces/VfsOptions.html +0 -26
  98. package/docs/interfaces/VfsSnapshot.html +0 -3
  99. package/docs/interfaces/VfsSnapshotBaseNode.html +0 -8
  100. package/docs/interfaces/VfsSnapshotDirectoryNode.html +0 -10
  101. package/docs/interfaces/VfsSnapshotFileNode.html +0 -12
  102. package/docs/interfaces/VirtualActiveSession.html +0 -12
  103. package/docs/interfaces/VirtualSftpServerOptions.html +0 -7
  104. package/docs/interfaces/VirtualShellVfsLike.html +0 -15
  105. package/docs/interfaces/VirtualShellVfsOptions.html +0 -3
  106. package/docs/interfaces/WriteFileOptions.html +0 -6
  107. package/docs/media/LICENSE +0 -21
  108. package/docs/modules.html +0 -1
  109. package/docs/types/ArgParseOptions.html +0 -4
  110. package/docs/types/CommandMode.html +0 -2
  111. package/docs/types/CommandOutcome.html +0 -2
  112. package/docs/types/IdleState.html +0 -1
  113. package/docs/types/VfsNodeStats.html +0 -2
  114. package/docs/types/VfsNodeType.html +0 -2
  115. package/docs/types/VfsPersistenceMode.html +0 -5
  116. package/docs/types/VfsSnapshotNode.html +0 -2
  117. package/examples/README.md +0 -288
  118. package/examples/app.js +0 -1755
  119. package/examples/app.ts +0 -299
  120. package/examples/build.js +0 -27
  121. package/examples/demo.html +0 -33
  122. package/examples/honeypot-audit.ts +0 -180
  123. package/examples/honeypot-export.ts +0 -253
  124. package/examples/honeypot-quickstart.ts +0 -110
  125. package/examples/index.html +0 -82
  126. package/examples/server.js +0 -55
  127. package/polyfills/buffer.js +0 -117
  128. package/polyfills/node_child_process/index.js +0 -2
  129. package/polyfills/node_crypto/index.js +0 -167
  130. package/polyfills/node_events/index.js +0 -9
  131. package/polyfills/node_fs/index.js +0 -202
  132. package/polyfills/node_fs/promises.js +0 -4
  133. package/polyfills/node_os/index.js +0 -9
  134. package/polyfills/node_path/index.js +0 -28
  135. package/polyfills/node_vm/index.js +0 -7
  136. package/polyfills/node_zlib/index.js +0 -3
  137. package/polyfills/process.js +0 -14
  138. package/polyfills/ssh2/index.js +0 -75
  139. package/scripts/build-all.mjs +0 -226
  140. package/scripts/build-names.mjs +0 -43
  141. package/scripts/generate-manuals-bundle.mjs +0 -49
  142. package/scripts/postinstall.js +0 -42
  143. package/scripts/publish-package.sh +0 -70
  144. package/src/Honeypot/index.ts +0 -457
  145. package/src/SSHClient/index.ts +0 -270
  146. package/src/SSHMimic/exec.ts +0 -49
  147. package/src/SSHMimic/executor.ts +0 -251
  148. package/src/SSHMimic/hostKey.ts +0 -21
  149. package/src/SSHMimic/index.ts +0 -337
  150. package/src/SSHMimic/loginBanner.ts +0 -36
  151. package/src/SSHMimic/loginFormat.ts +0 -10
  152. package/src/SSHMimic/prompt.ts +0 -14
  153. package/src/SSHMimic/sftp.ts +0 -883
  154. package/src/VirtualFileSystem/binaryPack.ts +0 -258
  155. package/src/VirtualFileSystem/index.ts +0 -1193
  156. package/src/VirtualFileSystem/internalTypes.ts +0 -43
  157. package/src/VirtualFileSystem/journal.ts +0 -171
  158. package/src/VirtualFileSystem/path.ts +0 -74
  159. package/src/VirtualPackageManager/index.ts +0 -1006
  160. package/src/VirtualShell/idleManager.ts +0 -137
  161. package/src/VirtualShell/index.ts +0 -475
  162. package/src/VirtualShell/shell.ts +0 -700
  163. package/src/VirtualShell/shellParser.ts +0 -285
  164. package/src/VirtualUserManager/index.ts +0 -758
  165. package/src/bun.d.ts +0 -1
  166. package/src/commands/adduser.ts +0 -103
  167. package/src/commands/alias.ts +0 -69
  168. package/src/commands/apt.ts +0 -233
  169. package/src/commands/awk.ts +0 -168
  170. package/src/commands/base64.ts +0 -29
  171. package/src/commands/cat.ts +0 -52
  172. package/src/commands/cd.ts +0 -25
  173. package/src/commands/chmod.ts +0 -85
  174. package/src/commands/clear.ts +0 -15
  175. package/src/commands/command-helpers.ts +0 -286
  176. package/src/commands/cp.ts +0 -83
  177. package/src/commands/curl.ts +0 -147
  178. package/src/commands/cut.ts +0 -36
  179. package/src/commands/date.ts +0 -30
  180. package/src/commands/declare.ts +0 -49
  181. package/src/commands/deluser.ts +0 -98
  182. package/src/commands/df.ts +0 -23
  183. package/src/commands/diff.ts +0 -43
  184. package/src/commands/dpkg.ts +0 -180
  185. package/src/commands/du.ts +0 -56
  186. package/src/commands/echo.ts +0 -58
  187. package/src/commands/env.ts +0 -23
  188. package/src/commands/exit.ts +0 -18
  189. package/src/commands/export.ts +0 -34
  190. package/src/commands/find.ts +0 -68
  191. package/src/commands/free.ts +0 -47
  192. package/src/commands/grep.ts +0 -116
  193. package/src/commands/groups.ts +0 -19
  194. package/src/commands/gzip.ts +0 -88
  195. package/src/commands/head.ts +0 -52
  196. package/src/commands/help.ts +0 -152
  197. package/src/commands/helpers.ts +0 -234
  198. package/src/commands/history.ts +0 -34
  199. package/src/commands/hostname.ts +0 -14
  200. package/src/commands/htop.ts +0 -20
  201. package/src/commands/id.ts +0 -19
  202. package/src/commands/index.ts +0 -9
  203. package/src/commands/kill.ts +0 -19
  204. package/src/commands/ln.ts +0 -71
  205. package/src/commands/ls.ts +0 -243
  206. package/src/commands/lsb-release.ts +0 -63
  207. package/src/commands/man.ts +0 -31
  208. package/src/commands/manuals/adduser.txt +0 -11
  209. package/src/commands/manuals/apt-cache.txt +0 -12
  210. package/src/commands/manuals/apt.txt +0 -20
  211. package/src/commands/manuals/awk.txt +0 -13
  212. package/src/commands/manuals/cat.txt +0 -14
  213. package/src/commands/manuals/cd.txt +0 -16
  214. package/src/commands/manuals/chmod.txt +0 -16
  215. package/src/commands/manuals/clear.txt +0 -10
  216. package/src/commands/manuals/cp.txt +0 -10
  217. package/src/commands/manuals/curl.txt +0 -20
  218. package/src/commands/manuals/date.txt +0 -14
  219. package/src/commands/manuals/declare.txt +0 -12
  220. package/src/commands/manuals/deluser.txt +0 -10
  221. package/src/commands/manuals/df.txt +0 -10
  222. package/src/commands/manuals/dpkg-query.txt +0 -11
  223. package/src/commands/manuals/dpkg.txt +0 -14
  224. package/src/commands/manuals/du.txt +0 -11
  225. package/src/commands/manuals/echo.txt +0 -11
  226. package/src/commands/manuals/false.txt +0 -10
  227. package/src/commands/manuals/find.txt +0 -11
  228. package/src/commands/manuals/free.txt +0 -12
  229. package/src/commands/manuals/grep.txt +0 -13
  230. package/src/commands/manuals/groups.txt +0 -10
  231. package/src/commands/manuals/gzip.txt +0 -11
  232. package/src/commands/manuals/head.txt +0 -10
  233. package/src/commands/manuals/help.txt +0 -11
  234. package/src/commands/manuals/history.txt +0 -11
  235. package/src/commands/manuals/hostname.txt +0 -10
  236. package/src/commands/manuals/id.txt +0 -10
  237. package/src/commands/manuals/kill.txt +0 -13
  238. package/src/commands/manuals/ls.txt +0 -20
  239. package/src/commands/manuals/lsb_release.txt +0 -14
  240. package/src/commands/manuals/mkdir.txt +0 -10
  241. package/src/commands/manuals/mv.txt +0 -10
  242. package/src/commands/manuals/nano.txt +0 -11
  243. package/src/commands/manuals/neofetch.txt +0 -10
  244. package/src/commands/manuals/node.txt +0 -13
  245. package/src/commands/manuals/npm.txt +0 -13
  246. package/src/commands/manuals/npx.txt +0 -13
  247. package/src/commands/manuals/passwd.txt +0 -11
  248. package/src/commands/manuals/ping.txt +0 -10
  249. package/src/commands/manuals/printf.txt +0 -11
  250. package/src/commands/manuals/ps.txt +0 -10
  251. package/src/commands/manuals/pwd.txt +0 -10
  252. package/src/commands/manuals/python3.txt +0 -13
  253. package/src/commands/manuals/readlink.txt +0 -10
  254. package/src/commands/manuals/return.txt +0 -10
  255. package/src/commands/manuals/rm.txt +0 -10
  256. package/src/commands/manuals/sed.txt +0 -11
  257. package/src/commands/manuals/set.txt +0 -11
  258. package/src/commands/manuals/shift.txt +0 -10
  259. package/src/commands/manuals/sleep.txt +0 -10
  260. package/src/commands/manuals/sort.txt +0 -12
  261. package/src/commands/manuals/source.txt +0 -11
  262. package/src/commands/manuals/ssh.txt +0 -11
  263. package/src/commands/manuals/stat.txt +0 -10
  264. package/src/commands/manuals/su.txt +0 -13
  265. package/src/commands/manuals/sudo.txt +0 -11
  266. package/src/commands/manuals/tail.txt +0 -10
  267. package/src/commands/manuals/tar.txt +0 -19
  268. package/src/commands/manuals/tee.txt +0 -10
  269. package/src/commands/manuals/test.txt +0 -11
  270. package/src/commands/manuals/touch.txt +0 -11
  271. package/src/commands/manuals/tr.txt +0 -10
  272. package/src/commands/manuals/trap.txt +0 -10
  273. package/src/commands/manuals/true.txt +0 -10
  274. package/src/commands/manuals/type.txt +0 -10
  275. package/src/commands/manuals/uname.txt +0 -12
  276. package/src/commands/manuals/uniq.txt +0 -12
  277. package/src/commands/manuals/unset.txt +0 -10
  278. package/src/commands/manuals/uptime.txt +0 -11
  279. package/src/commands/manuals/wc.txt +0 -12
  280. package/src/commands/manuals/wget.txt +0 -12
  281. package/src/commands/manuals/which.txt +0 -10
  282. package/src/commands/manuals/whoami.txt +0 -10
  283. package/src/commands/manuals/xargs.txt +0 -10
  284. package/src/commands/manuals-bundle.ts +0 -898
  285. package/src/commands/mkdir.ts +0 -31
  286. package/src/commands/mv.ts +0 -50
  287. package/src/commands/nano.ts +0 -38
  288. package/src/commands/neofetch.ts +0 -53
  289. package/src/commands/node.ts +0 -341
  290. package/src/commands/npm.ts +0 -132
  291. package/src/commands/passwd.ts +0 -50
  292. package/src/commands/ping.ts +0 -32
  293. package/src/commands/printf.ts +0 -129
  294. package/src/commands/ps.ts +0 -58
  295. package/src/commands/pwd.ts +0 -9
  296. package/src/commands/python.ts +0 -2229
  297. package/src/commands/read.ts +0 -46
  298. package/src/commands/registry.ts +0 -249
  299. package/src/commands/rm.ts +0 -42
  300. package/src/commands/runtime.ts +0 -378
  301. package/src/commands/sed.ts +0 -68
  302. package/src/commands/seq.ts +0 -43
  303. package/src/commands/set.ts +0 -29
  304. package/src/commands/sh.ts +0 -467
  305. package/src/commands/shift.ts +0 -63
  306. package/src/commands/sleep.ts +0 -20
  307. package/src/commands/sort.ts +0 -46
  308. package/src/commands/source.ts +0 -52
  309. package/src/commands/stat.ts +0 -61
  310. package/src/commands/su.ts +0 -72
  311. package/src/commands/sudo.ts +0 -76
  312. package/src/commands/tail.ts +0 -53
  313. package/src/commands/tar.ts +0 -102
  314. package/src/commands/tee.ts +0 -36
  315. package/src/commands/test.ts +0 -137
  316. package/src/commands/touch.ts +0 -28
  317. package/src/commands/tr.ts +0 -70
  318. package/src/commands/tree.ts +0 -20
  319. package/src/commands/true.ts +0 -27
  320. package/src/commands/type.ts +0 -48
  321. package/src/commands/uname.ts +0 -29
  322. package/src/commands/uniq.ts +0 -39
  323. package/src/commands/unset.ts +0 -17
  324. package/src/commands/uptime.ts +0 -54
  325. package/src/commands/wc.ts +0 -55
  326. package/src/commands/wget.ts +0 -148
  327. package/src/commands/which.ts +0 -37
  328. package/src/commands/who.ts +0 -25
  329. package/src/commands/whoami.ts +0 -14
  330. package/src/commands/xargs.ts +0 -31
  331. package/src/index.ts +0 -67
  332. package/src/modules/linuxRootfs.ts +0 -1961
  333. package/src/modules/neofetch.ts +0 -358
  334. package/src/modules/shellInteractive.ts +0 -57
  335. package/src/modules/shellRuntime.ts +0 -76
  336. package/src/self-standalone.ts +0 -542
  337. package/src/standalone-wo-sftp.ts +0 -38
  338. package/src/standalone.ts +0 -72
  339. package/src/types/commands.ts +0 -146
  340. package/src/types/pipeline.ts +0 -52
  341. package/src/types/streams.ts +0 -32
  342. package/src/types/tar-stream.d.ts +0 -38
  343. package/src/types/vfs.ts +0 -98
  344. package/src/utils/expand.ts +0 -491
  345. package/src/utils/perfLogger.ts +0 -72
  346. package/src/utils/tokenize.ts +0 -98
  347. package/src/utils/vfsDiff.ts +0 -275
  348. package/tests/command-helpers.test.ts +0 -116
  349. package/tests/commands-admin-net.test.ts +0 -441
  350. package/tests/commands-advanced.test.ts +0 -456
  351. package/tests/commands-core.test.ts +0 -562
  352. package/tests/commands-missing.test.ts +0 -570
  353. package/tests/commands-specific-units.test.ts +0 -327
  354. package/tests/commands-text-sys.test.ts +0 -445
  355. package/tests/expand.test.ts +0 -170
  356. package/tests/helpers.test.ts +0 -97
  357. package/tests/new-features.test.ts +0 -1036
  358. package/tests/parser-executor.test.ts +0 -37
  359. package/tests/sftp.test.ts +0 -323
  360. package/tests/ssh-exec.test.ts +0 -45
  361. package/tests/test-helper.ts +0 -79
  362. package/tests/users.test.ts +0 -86
  363. package/tsconfig.json +0 -49
  364. package/typedoc.json +0 -47
@@ -1,883 +0,0 @@
1
- /** biome-ignore-all lint/style/useNamingConvention: const as enum */
2
- import { EventEmitter } from "node:events";
3
- import * as path from "node:path";
4
- import type { AuthenticationType, KeyboardAuthContext } from "ssh2";
5
- import { Server as SshServer } from "ssh2";
6
- import type { VfsNodeStats } from "../types/vfs";
7
- import type { PerfLogger } from "../utils/perfLogger";
8
- import { createPerfLogger } from "../utils/perfLogger";
9
- import type VirtualFileSystem from "../VirtualFileSystem";
10
- import { userHome } from "../commands";
11
- import { VirtualShell } from "../VirtualShell";
12
- import type { VirtualUserManager } from "../VirtualUserManager";
13
- import { loadOrCreateHostKey } from "./hostKey";
14
- // ── Dev-mode logger — silent in production ────────────────────────────────────
15
- const DEV = !!process.env.DEV_MODE;
16
- const devLog = DEV ? console.log.bind(console) : () => {};
17
- const devWarn = DEV ? console.warn.bind(console) : () => {};
18
- const devErr = DEV ? console.error.bind(console): () => {};
19
-
20
-
21
-
22
- const SFTP_STATUS_CODE = {
23
- OK: 0,
24
- EOF: 1,
25
- NO_SUCH_FILE: 2,
26
- PERMISSION_DENIED: 3,
27
- FAILURE: 4,
28
- BAD_MESSAGE: 5,
29
- NO_CONNECTION: 6,
30
- CONNECTION_LOST: 7,
31
- OP_UNSUPPORTED: 8,
32
- };
33
-
34
- const OPEN_MODE = {
35
- READ: 0x00000001,
36
- WRITE: 0x00000002,
37
- APPEND: 0x00000004,
38
- CREAT: 0x00000008,
39
- TRUNC: 0x00000010,
40
- EXCL: 0x00000020,
41
- };
42
-
43
- const perf: PerfLogger = createPerfLogger("SftpMimic");
44
-
45
- /** @internal */
46
- interface SftpFileHandle {
47
- type: "file";
48
- path: string;
49
- flags: number;
50
- buffer: Buffer;
51
- }
52
-
53
- /** @internal */
54
- interface SftpDirHandle {
55
- type: "dir";
56
- path: string;
57
- entries: string[];
58
- index: number;
59
- }
60
-
61
- /** @internal */
62
- type SftpHandle = SftpFileHandle | SftpDirHandle;
63
-
64
- /** @internal */
65
- interface SftpAttributes {
66
- mode: number;
67
- uid: number;
68
- gid: number;
69
- size: number;
70
- atime: number | Date;
71
- mtime: number | Date;
72
- }
73
-
74
- /** @internal */
75
- interface SftpServerStream {
76
- on(
77
- event: "OPEN",
78
- listener: (reqid: number, filename: string, flags: number) => void,
79
- ): this;
80
- on(
81
- event: "READ",
82
- listener: (
83
- reqid: number,
84
- handle: Buffer,
85
- offset: number,
86
- length: number,
87
- ) => void,
88
- ): this;
89
- on(
90
- event: "WRITE",
91
- listener: (
92
- reqid: number,
93
- handle: Buffer,
94
- offset: number,
95
- data: Buffer,
96
- ) => void,
97
- ): this;
98
- on(event: "FSTAT", listener: (reqid: number, handle: Buffer) => void): this;
99
- on(event: "CLOSE", listener: (reqid: number, handle: Buffer) => void): this;
100
- on(event: "OPENDIR", listener: (reqid: number, path: string) => void): this;
101
- on(event: "READDIR", listener: (reqid: number, handle: Buffer) => void): this;
102
- on(event: "STAT", listener: (reqid: number, path: string) => void): this;
103
- on(event: "LSTAT", listener: (reqid: number, path: string) => void): this;
104
- on(
105
- event: "FSETSTAT",
106
- listener: (
107
- reqid: number,
108
- handle: Buffer,
109
- attrs: Partial<SftpAttributes>,
110
- ) => void,
111
- ): this;
112
- on(
113
- event: "SETSTAT",
114
- listener: (
115
- reqid: number,
116
- path: string,
117
- attrs: Partial<SftpAttributes>,
118
- ) => void,
119
- ): this;
120
- on(event: "REALPATH", listener: (reqid: number, path: string) => void): this;
121
- on(event: "MKDIR", listener: (reqid: number, path: string) => void): this;
122
- on(event: "RMDIR", listener: (reqid: number, path: string) => void): this;
123
- on(event: "REMOVE", listener: (reqid: number, path: string) => void): this;
124
- on(
125
- event: "RENAME",
126
- listener: (reqid: number, oldPath: string, newPath: string) => void,
127
- ): this;
128
- on(event: "READLINK", listener: (reqid: number) => void): this;
129
- on(event: "SYMLINK", listener: (reqid: number) => void): this;
130
- on(event: "END", listener: () => void): this;
131
- on(event: "end", listener: () => void): this;
132
- on(event: "error", listener: (error: Error) => void): this;
133
- on(event: "close", listener: () => void): this;
134
- status(reqid: number, code: number): void;
135
- attrs(reqid: number, attrs: SftpAttributes): void;
136
- handle(reqid: number, handle: Buffer): void;
137
- data(reqid: number, data: Buffer): void;
138
- name(
139
- reqid: number,
140
- entries: Array<{
141
- filename: string;
142
- longname: string;
143
- attrs: SftpAttributes;
144
- }>,
145
- ): void;
146
- }
147
-
148
- /** Options for {@link SftpMimic} constructor. */
149
- export interface SftpMimicOptions {
150
- port: number;
151
- hostname?: string;
152
- shell?: VirtualShell;
153
- vfs?: VirtualFileSystem;
154
- users?: VirtualUserManager;
155
- }
156
-
157
- export class SftpMimic extends EventEmitter {
158
- port: number;
159
- server: SshServer | null;
160
- private readonly hostname: string;
161
- private readonly shell: VirtualShell | null;
162
- private readonly vfs: VirtualFileSystem;
163
- private readonly users: VirtualUserManager;
164
- private nextHandleId = 0;
165
- private handles = new Map<string, SftpHandle>();
166
-
167
- constructor({
168
- port,
169
- hostname = "typescript-vm",
170
- shell,
171
- vfs,
172
- users,
173
- }: SftpMimicOptions) {
174
- super();
175
- perf.mark("constructor");
176
- this.port = port;
177
- this.server = null;
178
- this.hostname = hostname;
179
- this.shell = null;
180
-
181
- if (shell) {
182
- this.vfs = shell.vfs;
183
- this.users = shell.users;
184
- this.hostname = shell.hostname;
185
- this.shell = shell;
186
- } else if (vfs && users) {
187
- this.vfs = vfs;
188
- this.users = users;
189
- } else {
190
- const defaultShell = new VirtualShell(hostname);
191
- this.vfs = defaultShell.vfs;
192
- this.users = defaultShell.users;
193
- this.shell = defaultShell;
194
- }
195
- }
196
-
197
- private getVfs(): VirtualFileSystem {
198
- return this.shell?.vfs ?? this.vfs;
199
- }
200
-
201
- private getUsers(): VirtualUserManager {
202
- return this.shell?.users ?? this.users;
203
- }
204
-
205
- public async start(): Promise<number> {
206
- perf.mark("start");
207
- const privateKey = loadOrCreateHostKey();
208
-
209
- // Ensure VirtualShell is fully initialized before accepting connections
210
- if (this.shell) {
211
- await this.shell.ensureInitialized();
212
- } else {
213
- // If using standalone VFS+Users, initialize users now
214
- await this.users.initialize();
215
- }
216
-
217
- this.server = new SshServer(
218
- {
219
- hostKeys: [privateKey],
220
- ident: `SSH-2.0-${this.hostname}`,
221
- },
222
- (client) => {
223
- const allowedAuthMethods: AuthenticationType[] = [
224
- "password",
225
- "keyboard-interactive",
226
- ];
227
- let authUser = "root";
228
- let sessionId: string | null = null;
229
- let remoteAddress = "unknown";
230
-
231
- this.emit("client:connect");
232
-
233
- // Add error handling for the client
234
- client.on("error", (error: unknown) => {
235
- devErr(`[SFTP] Client error:`, error);
236
- });
237
-
238
- const acceptSession = (username: string): void => {
239
- authUser = username;
240
- sessionId = this.getUsers().registerSession(
241
- authUser,
242
- remoteAddress,
243
- ).id;
244
-
245
- const homeRoot = "/home";
246
- if (!this.getVfs().exists(homeRoot)) {
247
- this.getVfs().mkdir(homeRoot, 0o755);
248
- }
249
-
250
- const homePath = userHome(authUser);
251
- if (!this.getVfs().exists(homePath)) {
252
- this.getVfs().mkdir(homePath, 0o755);
253
- this.getVfs().writeFile(
254
- `${homePath}/README.txt`,
255
- `Welcome to ${this.hostname}`,
256
- );
257
- }
258
- };
259
-
260
- client.on("authentication", (ctx) => {
261
- const candidateUser = ctx.username || "root";
262
- remoteAddress = (ctx as { ip?: string }).ip ?? remoteAddress;
263
-
264
- devLog(
265
- `[SFTP] Auth attempt: user=${candidateUser}, method=${ctx.method}, ip=${remoteAddress}`,
266
- );
267
-
268
- if (ctx.method === "password") {
269
- // If no password is set for the user, allow login without verification
270
- if (!this.getUsers().hasPassword(candidateUser)) {
271
- acceptSession(candidateUser);
272
- this.emit("auth:success", { username: authUser, remoteAddress });
273
- ctx.accept();
274
- return;
275
- }
276
-
277
- if (
278
- !this.getUsers().verifyPassword(candidateUser, ctx.password ?? "")
279
- ) {
280
- this.emit("auth:failure", {
281
- username: candidateUser,
282
- remoteAddress,
283
- });
284
- ctx.reject(allowedAuthMethods);
285
- return;
286
- }
287
-
288
- acceptSession(candidateUser);
289
- this.emit("auth:success", { username: authUser, remoteAddress });
290
- ctx.accept();
291
- return;
292
- }
293
-
294
- if (ctx.method === "keyboard-interactive") {
295
- const keyboardCtx = ctx as KeyboardAuthContext;
296
- // If no password is set, accept immediately
297
- if (!this.getUsers().hasPassword(candidateUser)) {
298
- acceptSession(candidateUser);
299
- this.emit("auth:success", { username: authUser, remoteAddress });
300
- keyboardCtx.accept();
301
- return;
302
- }
303
- keyboardCtx.prompt(
304
- [{ prompt: "Password: ", echo: false }],
305
- (answers) => {
306
- const password = answers[0] ?? "";
307
- if (!this.getUsers().verifyPassword(candidateUser, password)) {
308
- this.emit("auth:failure", {
309
- username: candidateUser,
310
- remoteAddress,
311
- });
312
- keyboardCtx.reject(allowedAuthMethods);
313
- return;
314
- }
315
-
316
- acceptSession(candidateUser);
317
- this.emit("auth:success", {
318
- username: authUser,
319
- remoteAddress,
320
- });
321
- keyboardCtx.accept();
322
- },
323
- );
324
- return;
325
- }
326
-
327
- ctx.reject(allowedAuthMethods);
328
- });
329
-
330
- client.on("close", () => {
331
- this.getUsers().unregisterSession(sessionId);
332
- this.emit("client:disconnect", { user: authUser });
333
- sessionId = null;
334
- });
335
-
336
- client.on("ready", () => {
337
- client.on("session", (accept, _reject) => {
338
- const session = accept();
339
-
340
- // Add error handling for the session
341
- session.on("error", (error: unknown) => {
342
- devErr(
343
- `[SFTP] Session error for user=${authUser}:`,
344
- error,
345
- );
346
- });
347
-
348
- session.on("sftp", (acceptSftp) => {
349
- const sftp = acceptSftp();
350
- this.attachSftpHandlers(sftp, authUser);
351
- });
352
- });
353
- });
354
- },
355
- );
356
-
357
- return new Promise<number>((resolve, reject) => {
358
- this.server?.once("error", (err: unknown) => reject(err));
359
- this.server?.listen(this.port, "0.0.0.0", () => {
360
- const address = this.server?.address();
361
- const actualPort =
362
- address && typeof address === "object" && "port" in address
363
- ? address.port
364
- : this.port;
365
- devLog(`SFTP Mimic listening on port ${actualPort}`);
366
- this.emit("start", { port: actualPort });
367
- resolve(actualPort as number);
368
- });
369
- });
370
- }
371
-
372
- public stop(): void {
373
- perf.mark("stop");
374
- if (this.server) {
375
- this.server.close(() => {
376
- devLog("SFTP Mimic stopped");
377
- this.emit("stop");
378
- });
379
- }
380
- }
381
-
382
- /**
383
- * Resolves SFTP request paths with proper handling of relative paths.
384
- * Relative paths (including ".") are resolved relative to the user's home directory.
385
- * This is standard SFTP behavior where the "working directory" is always the home.
386
- */
387
- private resolveRequestPath(requestPath: string, authUser: string): string {
388
- const homePath = userHome(authUser);
389
-
390
- // Empty path or "." → resolve to home directory
391
- if (!requestPath || requestPath === ".") {
392
- return homePath;
393
- }
394
-
395
- // Relative path (doesn't start with "/") → resolve relative to home
396
- if (!requestPath.startsWith("/")) {
397
- const joined = path.posix.join(homePath, requestPath);
398
- return path.posix.normalize(joined);
399
- }
400
-
401
- // Absolute path → just normalize it
402
- return path.posix.normalize(requestPath);
403
- }
404
- /**
405
- * Verifies that a target path is confined within the user's home directory.
406
- * This implements chroot-like behavior for security.
407
- * @param targetPath - The normalized target path
408
- * @param authUser - The authenticated username
409
- * @returns true if path is within home, false if traversal attempt detected
410
- */
411
- private isPathWithinHome(targetPath: string, authUser: string): boolean {
412
- const homePath = userHome(authUser);
413
- const normalized = path.posix.normalize(targetPath);
414
-
415
- // Allow access to home directory itself
416
- if (normalized === homePath) {
417
- return true;
418
- }
419
-
420
- // Check if path is within home directory (starts with /home/username/)
421
- if (normalized.startsWith(`${homePath}/`)) {
422
- return true;
423
- }
424
-
425
- // Reject any attempt to escape home directory
426
- return false;
427
- }
428
-
429
- private createAttrs(node: VfsNodeStats): SftpAttributes {
430
- const permissions = node.mode & 0o777;
431
- const fileType = node.type === "directory" ? 0o040000 : 0o100000;
432
-
433
- return {
434
- mode: fileType | permissions,
435
- size: node.type === "file" ? node.size : 0,
436
- uid: 0,
437
- gid: 0,
438
- atime: Math.floor(node.createdAt.getTime() / 1000),
439
- mtime: Math.floor(node.updatedAt.getTime() / 1000),
440
- };
441
- }
442
-
443
- private openHandle(handleValue: SftpHandle): Buffer {
444
- const handleId = ++this.nextHandleId;
445
- const handle = Buffer.alloc(4);
446
- handle.writeUInt32BE(handleId, 0);
447
- this.handles.set(handle.toString("hex"), handleValue);
448
- return handle;
449
- }
450
-
451
- private getHandle(handle: Buffer): SftpHandle | undefined {
452
- return this.handles.get(handle.toString("hex")) as SftpHandle | undefined;
453
- }
454
-
455
- private closeHandle(handle: Buffer): void {
456
- this.handles.delete(handle.toString("hex"));
457
- }
458
-
459
- private attachSftpHandlers(sftp: SftpServerStream, authUser: string): void {
460
- const getVfs = () => this.getVfs();
461
- const getUsers = () => this.getUsers();
462
-
463
- sftp.on("OPEN", (reqid: number, filename: string, flags: number) => {
464
- const targetPath = this.resolveRequestPath(filename, authUser);
465
-
466
- // Security: Confine to home directory
467
- if (!this.isPathWithinHome(targetPath, authUser)) {
468
- devWarn(
469
- `[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
470
- );
471
- sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
472
- return;
473
- }
474
-
475
- const openMode = flags;
476
- const _canRead = Boolean(openMode & OPEN_MODE.READ);
477
- const _canWrite = Boolean(
478
- openMode & OPEN_MODE.WRITE || openMode & OPEN_MODE.APPEND,
479
- );
480
- const canCreate = Boolean(openMode & OPEN_MODE.CREAT);
481
- const shouldTruncate = Boolean(openMode & OPEN_MODE.TRUNC);
482
-
483
- try {
484
- if (!getVfs().exists(targetPath)) {
485
- if (!canCreate) {
486
- sftp.status(reqid, SFTP_STATUS_CODE.NO_SUCH_FILE);
487
- return;
488
- }
489
-
490
- getVfs().writeFile(targetPath, Buffer.alloc(0));
491
- }
492
-
493
- const stats = getVfs().stat(targetPath);
494
- if (stats.type === "directory") {
495
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
496
- return;
497
- }
498
-
499
- let buffer = Buffer.from(getVfs().readFile(targetPath), "utf8");
500
- if (shouldTruncate) {
501
- buffer = Buffer.alloc(0);
502
- }
503
-
504
- if (openMode & OPEN_MODE.APPEND) {
505
- const handle = this.openHandle({
506
- type: "file",
507
- path: targetPath,
508
- flags: openMode,
509
- buffer,
510
- });
511
- sftp.handle(reqid, handle);
512
- return;
513
- }
514
-
515
- const handle = this.openHandle({
516
- type: "file",
517
- path: targetPath,
518
- flags: openMode,
519
- buffer,
520
- });
521
- sftp.handle(reqid, handle);
522
- } catch (error) {
523
- devErr("SFTP OPEN error:", error);
524
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
525
- }
526
- });
527
-
528
- sftp.on(
529
- "READ",
530
- (reqid: number, handle: Buffer, offset: number, length: number) => {
531
- const entry = this.getHandle(handle);
532
- if (!entry || entry.type !== "file") {
533
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
534
- return;
535
- }
536
-
537
- if (offset >= entry.buffer.length) {
538
- sftp.status(reqid, SFTP_STATUS_CODE.EOF);
539
- return;
540
- }
541
-
542
- const chunk = entry.buffer.slice(offset, offset + length);
543
- sftp.data(reqid, chunk);
544
- },
545
- );
546
-
547
- sftp.on(
548
- "WRITE",
549
- async (reqid: number, handle: Buffer, offset: number, data: Buffer) => {
550
- const entry = this.getHandle(handle);
551
- if (!entry || entry.type !== "file") {
552
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
553
- return;
554
- }
555
-
556
- const end = offset + data.length;
557
- if (end > entry.buffer.length) {
558
- const nextBuffer = Buffer.alloc(end);
559
- entry.buffer.copy(nextBuffer, 0, 0, entry.buffer.length);
560
- entry.buffer = nextBuffer;
561
- }
562
-
563
- data.copy(entry.buffer, offset);
564
- sftp.status(reqid, SFTP_STATUS_CODE.OK);
565
- },
566
- );
567
-
568
- sftp.on("FSTAT", (reqid: number, handle: Buffer) => {
569
- const entry = this.getHandle(handle);
570
- if (!entry) {
571
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
572
- return;
573
- }
574
-
575
- try {
576
- const stats = getVfs().stat(entry.path);
577
- sftp.attrs(reqid, this.createAttrs(stats));
578
- } catch {
579
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
580
- }
581
- });
582
-
583
- sftp.on("CLOSE", async (reqid: number, handle: Buffer) => {
584
- const entry = this.getHandle(handle);
585
- if (!entry) {
586
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
587
- return;
588
- }
589
-
590
- if (entry.type === "file") {
591
- try {
592
- getUsers().assertWriteWithinQuota(authUser, entry.path, entry.buffer);
593
- getVfs().writeFile(entry.path, entry.buffer);
594
- } catch (error) {
595
- devErr("SFTP CLOSE write error:", error);
596
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
597
- this.closeHandle(handle);
598
- return;
599
- }
600
- }
601
-
602
- this.closeHandle(handle);
603
- sftp.status(reqid, SFTP_STATUS_CODE.OK);
604
- });
605
-
606
- sftp.on("OPENDIR", (reqid: number, requestPath: string) => {
607
- const targetPath = this.resolveRequestPath(requestPath, authUser);
608
-
609
- // Security: Confine to home directory
610
- if (!this.isPathWithinHome(targetPath, authUser)) {
611
- devWarn(
612
- `[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
613
- );
614
- sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
615
- return;
616
- }
617
-
618
- try {
619
- const stats = getVfs().stat(targetPath);
620
- if (stats.type !== "directory") {
621
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
622
- return;
623
- }
624
-
625
- const entries = getVfs().list(targetPath);
626
- const handle = this.openHandle({
627
- type: "dir",
628
- path: targetPath,
629
- entries,
630
- index: 0,
631
- });
632
-
633
- sftp.handle(reqid, handle);
634
- } catch {
635
- sftp.status(reqid, SFTP_STATUS_CODE.NO_SUCH_FILE);
636
- }
637
- });
638
-
639
- sftp.on("READDIR", (reqid: number, handle: Buffer) => {
640
- const entry = this.getHandle(handle);
641
- if (!entry || entry.type !== "dir") {
642
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
643
- return;
644
- }
645
-
646
- if (entry.index >= entry.entries.length) {
647
- sftp.status(reqid, SFTP_STATUS_CODE.EOF);
648
- return;
649
- }
650
-
651
- const filename = entry.entries[entry.index++]!;
652
- const filePath = path.posix.join(entry.path, filename);
653
- const stats = getVfs().stat(filePath);
654
- const attrs = this.createAttrs(stats);
655
- const longname = `${stats.type === "directory" ? "d" : "-"}${(stats.mode & 0o777).toString(8)} ${filename}`;
656
- return sftp.name(reqid, [{ filename, longname, attrs }]);
657
- });
658
-
659
- sftp.on("STAT", (reqid: number, requestPath: string) => {
660
- const targetPath = this.resolveRequestPath(requestPath, authUser);
661
-
662
- // Security: Confine to home directory
663
- if (!this.isPathWithinHome(targetPath, authUser)) {
664
- devWarn(
665
- `[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
666
- );
667
- sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
668
- return;
669
- }
670
-
671
- try {
672
- const stats = getVfs().stat(targetPath);
673
- sftp.attrs(reqid, this.createAttrs(stats));
674
- } catch {
675
- sftp.status(reqid, SFTP_STATUS_CODE.NO_SUCH_FILE);
676
- }
677
- });
678
-
679
- sftp.on("LSTAT", (reqid: number, requestPath: string) => {
680
- const targetPath = this.resolveRequestPath(requestPath, authUser);
681
-
682
- // Security: Confine to home directory
683
- if (!this.isPathWithinHome(targetPath, authUser)) {
684
- devWarn(
685
- `[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
686
- );
687
- sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
688
- return;
689
- }
690
-
691
- try {
692
- const stats = getVfs().stat(targetPath);
693
- sftp.attrs(reqid, this.createAttrs(stats));
694
- } catch {
695
- sftp.status(reqid, SFTP_STATUS_CODE.NO_SUCH_FILE);
696
- }
697
- });
698
-
699
- sftp.on(
700
- "FSETSTAT",
701
- (reqid: number, handle: Buffer, attrs: { mode?: number }) => {
702
- const entry = this.getHandle(handle);
703
- if (!entry) {
704
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
705
- return;
706
- }
707
-
708
- try {
709
- if (attrs.mode !== undefined) {
710
- getVfs().chmod(entry.path, attrs.mode);
711
- }
712
- sftp.status(reqid, SFTP_STATUS_CODE.OK);
713
- } catch {
714
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
715
- }
716
- },
717
- );
718
-
719
- sftp.on(
720
- "SETSTAT",
721
- (reqid: number, requestPath: string, attrs: { mode?: number }) => {
722
- const targetPath = this.resolveRequestPath(requestPath, authUser);
723
-
724
- // Security: Confine to home directory
725
- if (!this.isPathWithinHome(targetPath, authUser)) {
726
- devWarn(
727
- `[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
728
- );
729
- sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
730
- return;
731
- }
732
-
733
- try {
734
- if (attrs.mode !== undefined) {
735
- getVfs().chmod(targetPath, attrs.mode);
736
- }
737
- sftp.status(reqid, SFTP_STATUS_CODE.OK);
738
- } catch {
739
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
740
- }
741
- },
742
- );
743
-
744
- sftp.on("REALPATH", (reqid: number, requestPath: string) => {
745
- const normalized = this.resolveRequestPath(requestPath, authUser);
746
-
747
- // Security: Confine to home directory
748
- if (!this.isPathWithinHome(normalized, authUser)) {
749
- devWarn(
750
- `[SFTP] Path traversal attempt blocked: user=${authUser}, path=${normalized}`,
751
- );
752
- sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
753
- return;
754
- }
755
-
756
- sftp.name(reqid, [
757
- {
758
- filename: normalized,
759
- longname: normalized,
760
- attrs: {
761
- mode: 0o040755,
762
- uid: 0,
763
- gid: 0,
764
- size: 0,
765
- atime: 0,
766
- mtime: 0,
767
- },
768
- },
769
- ]);
770
- });
771
-
772
- sftp.on("MKDIR", (reqid: number, requestPath: string) => {
773
- const targetPath = this.resolveRequestPath(requestPath, authUser);
774
-
775
- // Security: Confine to home directory
776
- if (!this.isPathWithinHome(targetPath, authUser)) {
777
- devWarn(
778
- `[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
779
- );
780
- sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
781
- return;
782
- }
783
-
784
- try {
785
- getVfs().mkdir(targetPath, 0o755);
786
- sftp.status(reqid, SFTP_STATUS_CODE.OK);
787
- } catch {
788
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
789
- }
790
- });
791
-
792
- sftp.on("RMDIR", (reqid: number, requestPath: string) => {
793
- const targetPath = this.resolveRequestPath(requestPath, authUser);
794
-
795
- // Security: Confine to home directory
796
- if (!this.isPathWithinHome(targetPath, authUser)) {
797
- devWarn(
798
- `[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
799
- );
800
- sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
801
- return;
802
- }
803
-
804
- try {
805
- getVfs().remove(targetPath, { recursive: false });
806
- sftp.status(reqid, SFTP_STATUS_CODE.OK);
807
- } catch {
808
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
809
- }
810
- });
811
-
812
- sftp.on("REMOVE", (reqid: number, requestPath: string) => {
813
- const targetPath = this.resolveRequestPath(requestPath, authUser);
814
-
815
- // Security: Confine to home directory
816
- if (!this.isPathWithinHome(targetPath, authUser)) {
817
- devWarn(
818
- `[SFTP] Path traversal attempt blocked: user=${authUser}, path=${targetPath}`,
819
- );
820
- sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
821
- return;
822
- }
823
-
824
- try {
825
- getVfs().remove(targetPath, { recursive: false });
826
- sftp.status(reqid, SFTP_STATUS_CODE.OK);
827
- } catch {
828
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
829
- }
830
- });
831
-
832
- sftp.on("RENAME", (reqid: number, oldPath: string, newPath: string) => {
833
- const fromPath = this.resolveRequestPath(oldPath, authUser);
834
- const toPath = this.resolveRequestPath(newPath, authUser);
835
-
836
- // Security: Confine both source and destination to home directory
837
- if (
838
- !this.isPathWithinHome(fromPath, authUser) ||
839
- !this.isPathWithinHome(toPath, authUser)
840
- ) {
841
- devWarn(
842
- `[SFTP] Path traversal attempt blocked: user=${authUser}, from=${fromPath}, to=${toPath}`,
843
- );
844
- sftp.status(reqid, SFTP_STATUS_CODE.PERMISSION_DENIED);
845
- return;
846
- }
847
-
848
- try {
849
- getVfs().move(fromPath, toPath);
850
- sftp.status(reqid, SFTP_STATUS_CODE.OK);
851
- } catch {
852
- sftp.status(reqid, SFTP_STATUS_CODE.FAILURE);
853
- }
854
- });
855
-
856
- sftp.on("READLINK", (reqid: number) => {
857
- sftp.status(reqid, SFTP_STATUS_CODE.OP_UNSUPPORTED);
858
- });
859
-
860
- sftp.on("SYMLINK", (reqid: number) => {
861
- sftp.status(reqid, SFTP_STATUS_CODE.OP_UNSUPPORTED);
862
- });
863
-
864
- sftp.on("error", (error: Error) => {
865
- devErr(`[SFTP] Stream error for user=${authUser}:`, error);
866
- });
867
-
868
- sftp.on("close", () => {
869
- devLog(`[SFTP] Stream closed for user=${authUser}`);
870
- this.handles.clear();
871
- });
872
-
873
- sftp.on("end", () => {
874
- devLog(`[SFTP] end event for user=${authUser}`);
875
- this.handles.clear();
876
- });
877
-
878
- sftp.on("END", () => {
879
- devLog(`[SFTP] END event for user=${authUser}`);
880
- this.handles.clear();
881
- });
882
- }
883
- }