typescript-virtual-container 1.2.4 → 1.2.6

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 (267) hide show
  1. package/README.md +1056 -1239
  2. package/benchmark-results.txt +20 -20
  3. package/dist/SSHMimic/exec.js +2 -2
  4. package/dist/SSHMimic/executor.d.ts +6 -7
  5. package/dist/SSHMimic/executor.d.ts.map +1 -1
  6. package/dist/SSHMimic/executor.js +77 -60
  7. package/dist/SSHMimic/index.d.ts +19 -2
  8. package/dist/SSHMimic/index.d.ts.map +1 -1
  9. package/dist/SSHMimic/index.js +106 -24
  10. package/dist/SSHMimic/sftp.d.ts.map +1 -1
  11. package/dist/SSHMimic/sftp.js +14 -0
  12. package/dist/VirtualFileSystem/index.d.ts +115 -88
  13. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  14. package/dist/VirtualFileSystem/index.js +389 -264
  15. package/dist/VirtualShell/index.d.ts +3 -4
  16. package/dist/VirtualShell/index.d.ts.map +1 -1
  17. package/dist/VirtualShell/index.js +4 -6
  18. package/dist/VirtualShell/shell.d.ts.map +1 -1
  19. package/dist/VirtualShell/shell.js +19 -2
  20. package/dist/VirtualShell/shellParser.d.ts +20 -2
  21. package/dist/VirtualShell/shellParser.d.ts.map +1 -1
  22. package/dist/VirtualShell/shellParser.js +229 -120
  23. package/dist/VirtualUserManager/index.d.ts +25 -0
  24. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  25. package/dist/VirtualUserManager/index.js +33 -0
  26. package/dist/commands/adduser.d.ts.map +1 -1
  27. package/dist/commands/adduser.js +2 -0
  28. package/dist/commands/awk.d.ts +3 -0
  29. package/dist/commands/awk.d.ts.map +1 -0
  30. package/dist/commands/awk.js +29 -0
  31. package/dist/commands/base64.d.ts +3 -0
  32. package/dist/commands/base64.d.ts.map +1 -0
  33. package/dist/commands/base64.js +20 -0
  34. package/dist/commands/cat.d.ts.map +1 -1
  35. package/dist/commands/cat.js +2 -0
  36. package/dist/commands/cd.d.ts.map +1 -1
  37. package/dist/commands/cd.js +2 -0
  38. package/dist/commands/chmod.d.ts +3 -0
  39. package/dist/commands/chmod.d.ts.map +1 -0
  40. package/dist/commands/chmod.js +33 -0
  41. package/dist/commands/clear.d.ts.map +1 -1
  42. package/dist/commands/clear.js +4 -1
  43. package/dist/commands/cp.d.ts +3 -0
  44. package/dist/commands/cp.d.ts.map +1 -0
  45. package/dist/commands/cp.js +70 -0
  46. package/dist/commands/curl.d.ts.map +1 -1
  47. package/dist/commands/curl.js +2 -0
  48. package/dist/commands/cut.d.ts +3 -0
  49. package/dist/commands/cut.d.ts.map +1 -0
  50. package/dist/commands/cut.js +27 -0
  51. package/dist/commands/date.d.ts +3 -0
  52. package/dist/commands/date.d.ts.map +1 -0
  53. package/dist/commands/date.js +22 -0
  54. package/dist/commands/deluser.d.ts.map +1 -1
  55. package/dist/commands/deluser.js +2 -0
  56. package/dist/commands/df.d.ts +3 -0
  57. package/dist/commands/df.d.ts.map +1 -0
  58. package/dist/commands/df.js +16 -0
  59. package/dist/commands/diff.d.ts +3 -0
  60. package/dist/commands/diff.d.ts.map +1 -0
  61. package/dist/commands/diff.js +40 -0
  62. package/dist/commands/du.d.ts +3 -0
  63. package/dist/commands/du.d.ts.map +1 -0
  64. package/dist/commands/du.js +39 -0
  65. package/dist/commands/echo.d.ts.map +1 -1
  66. package/dist/commands/echo.js +2 -0
  67. package/dist/commands/env.d.ts.map +1 -1
  68. package/dist/commands/env.js +6 -14
  69. package/dist/commands/export.d.ts.map +1 -1
  70. package/dist/commands/export.js +11 -21
  71. package/dist/commands/find.d.ts +3 -0
  72. package/dist/commands/find.d.ts.map +1 -0
  73. package/dist/commands/find.js +50 -0
  74. package/dist/commands/grep.d.ts.map +1 -1
  75. package/dist/commands/grep.js +58 -35
  76. package/dist/commands/groups.d.ts +3 -0
  77. package/dist/commands/groups.d.ts.map +1 -0
  78. package/dist/commands/groups.js +12 -0
  79. package/dist/commands/gzip.d.ts +4 -0
  80. package/dist/commands/gzip.d.ts.map +1 -0
  81. package/dist/commands/gzip.js +40 -0
  82. package/dist/commands/head.d.ts +3 -0
  83. package/dist/commands/head.d.ts.map +1 -0
  84. package/dist/commands/head.js +32 -0
  85. package/dist/commands/help.d.ts +1 -1
  86. package/dist/commands/help.d.ts.map +1 -1
  87. package/dist/commands/help.js +75 -3
  88. package/dist/commands/hostname.d.ts.map +1 -1
  89. package/dist/commands/hostname.js +2 -0
  90. package/dist/commands/htop.d.ts.map +1 -1
  91. package/dist/commands/htop.js +2 -0
  92. package/dist/commands/id.d.ts +3 -0
  93. package/dist/commands/id.d.ts.map +1 -0
  94. package/dist/commands/id.js +14 -0
  95. package/dist/commands/index.d.ts +5 -2
  96. package/dist/commands/index.d.ts.map +1 -1
  97. package/dist/commands/index.js +104 -87
  98. package/dist/commands/kill.d.ts +3 -0
  99. package/dist/commands/kill.d.ts.map +1 -0
  100. package/dist/commands/kill.js +13 -0
  101. package/dist/commands/ln.d.ts +3 -0
  102. package/dist/commands/ln.d.ts.map +1 -0
  103. package/dist/commands/ln.js +44 -0
  104. package/dist/commands/ls.d.ts.map +1 -1
  105. package/dist/commands/ls.js +2 -0
  106. package/dist/commands/mkdir.d.ts.map +1 -1
  107. package/dist/commands/mkdir.js +2 -0
  108. package/dist/commands/mv.d.ts +3 -0
  109. package/dist/commands/mv.d.ts.map +1 -0
  110. package/dist/commands/mv.js +37 -0
  111. package/dist/commands/nano.d.ts.map +1 -1
  112. package/dist/commands/nano.js +2 -0
  113. package/dist/commands/neofetch.d.ts.map +1 -1
  114. package/dist/commands/neofetch.js +2 -0
  115. package/dist/commands/passwd.d.ts.map +1 -1
  116. package/dist/commands/passwd.js +2 -0
  117. package/dist/commands/ping.d.ts +3 -0
  118. package/dist/commands/ping.d.ts.map +1 -0
  119. package/dist/commands/ping.js +18 -0
  120. package/dist/commands/ps.d.ts +3 -0
  121. package/dist/commands/ps.d.ts.map +1 -0
  122. package/dist/commands/ps.js +17 -0
  123. package/dist/commands/pwd.d.ts.map +1 -1
  124. package/dist/commands/pwd.js +2 -0
  125. package/dist/commands/rm.d.ts.map +1 -1
  126. package/dist/commands/rm.js +2 -0
  127. package/dist/commands/sed.d.ts +3 -0
  128. package/dist/commands/sed.d.ts.map +1 -0
  129. package/dist/commands/sed.js +47 -0
  130. package/dist/commands/set.d.ts +3 -0
  131. package/dist/commands/set.d.ts.map +1 -1
  132. package/dist/commands/set.js +19 -46
  133. package/dist/commands/sh.d.ts +0 -1
  134. package/dist/commands/sh.d.ts.map +1 -1
  135. package/dist/commands/sh.js +228 -35
  136. package/dist/commands/sleep.d.ts +3 -0
  137. package/dist/commands/sleep.d.ts.map +1 -0
  138. package/dist/commands/sleep.js +13 -0
  139. package/dist/commands/sort.d.ts +3 -0
  140. package/dist/commands/sort.d.ts.map +1 -0
  141. package/dist/commands/sort.js +37 -0
  142. package/dist/commands/su.d.ts.map +1 -1
  143. package/dist/commands/su.js +2 -0
  144. package/dist/commands/sudo.d.ts.map +1 -1
  145. package/dist/commands/sudo.js +2 -0
  146. package/dist/commands/tail.d.ts +3 -0
  147. package/dist/commands/tail.d.ts.map +1 -0
  148. package/dist/commands/tail.js +35 -0
  149. package/dist/commands/tar.d.ts +3 -0
  150. package/dist/commands/tar.d.ts.map +1 -0
  151. package/dist/commands/tar.js +64 -0
  152. package/dist/commands/tee.d.ts +3 -0
  153. package/dist/commands/tee.d.ts.map +1 -0
  154. package/dist/commands/tee.js +29 -0
  155. package/dist/commands/touch.d.ts.map +1 -1
  156. package/dist/commands/touch.js +2 -0
  157. package/dist/commands/tr.d.ts +3 -0
  158. package/dist/commands/tr.d.ts.map +1 -0
  159. package/dist/commands/tr.js +24 -0
  160. package/dist/commands/tree.d.ts.map +1 -1
  161. package/dist/commands/tree.js +2 -0
  162. package/dist/commands/uname.d.ts +3 -0
  163. package/dist/commands/uname.d.ts.map +1 -0
  164. package/dist/commands/uname.js +21 -0
  165. package/dist/commands/uniq.d.ts +3 -0
  166. package/dist/commands/uniq.d.ts.map +1 -0
  167. package/dist/commands/uniq.js +33 -0
  168. package/dist/commands/unset.d.ts.map +1 -1
  169. package/dist/commands/unset.js +6 -10
  170. package/dist/commands/wc.d.ts +3 -0
  171. package/dist/commands/wc.d.ts.map +1 -0
  172. package/dist/commands/wc.js +50 -0
  173. package/dist/commands/wget.d.ts.map +1 -1
  174. package/dist/commands/wget.js +2 -0
  175. package/dist/commands/who.d.ts.map +1 -1
  176. package/dist/commands/who.js +2 -0
  177. package/dist/commands/whoami.d.ts.map +1 -1
  178. package/dist/commands/whoami.js +2 -0
  179. package/dist/commands/xargs.d.ts +3 -0
  180. package/dist/commands/xargs.d.ts.map +1 -0
  181. package/dist/commands/xargs.js +16 -0
  182. package/dist/index.d.ts +1 -0
  183. package/dist/index.d.ts.map +1 -1
  184. package/dist/types/commands.d.ts +13 -0
  185. package/dist/types/commands.d.ts.map +1 -1
  186. package/dist/types/pipeline.d.ts +20 -0
  187. package/dist/types/pipeline.d.ts.map +1 -1
  188. package/package.json +5 -2
  189. package/scripts/publish-package.sh +70 -0
  190. package/src/SSHMimic/exec.ts +2 -2
  191. package/src/SSHMimic/executor.ts +95 -98
  192. package/src/SSHMimic/index.ts +138 -57
  193. package/src/SSHMimic/sftp.ts +15 -0
  194. package/src/VirtualFileSystem/index.ts +464 -292
  195. package/src/VirtualShell/index.ts +4 -6
  196. package/src/VirtualShell/shell.ts +19 -2
  197. package/src/VirtualShell/shellParser.ts +202 -168
  198. package/src/VirtualUserManager/index.ts +36 -0
  199. package/src/commands/adduser.ts +2 -0
  200. package/src/commands/awk.ts +30 -0
  201. package/src/commands/base64.ts +18 -0
  202. package/src/commands/cat.ts +2 -0
  203. package/src/commands/cd.ts +2 -0
  204. package/src/commands/chmod.ts +35 -0
  205. package/src/commands/clear.ts +4 -1
  206. package/src/commands/cp.ts +78 -0
  207. package/src/commands/curl.ts +2 -0
  208. package/src/commands/cut.ts +29 -0
  209. package/src/commands/date.ts +24 -0
  210. package/src/commands/deluser.ts +2 -0
  211. package/src/commands/df.ts +18 -0
  212. package/src/commands/diff.ts +29 -0
  213. package/src/commands/du.ts +39 -0
  214. package/src/commands/echo.ts +2 -0
  215. package/src/commands/env.ts +6 -16
  216. package/src/commands/export.ts +11 -24
  217. package/src/commands/find.ts +63 -0
  218. package/src/commands/grep.ts +51 -38
  219. package/src/commands/groups.ts +14 -0
  220. package/src/commands/gzip.ts +31 -0
  221. package/src/commands/head.ts +37 -0
  222. package/src/commands/help.ts +81 -3
  223. package/src/commands/hostname.ts +2 -0
  224. package/src/commands/htop.ts +2 -0
  225. package/src/commands/id.ts +16 -0
  226. package/src/commands/index.ts +114 -133
  227. package/src/commands/kill.ts +14 -0
  228. package/src/commands/ln.ts +49 -0
  229. package/src/commands/ls.ts +2 -0
  230. package/src/commands/mkdir.ts +2 -0
  231. package/src/commands/mv.ts +45 -0
  232. package/src/commands/nano.ts +2 -0
  233. package/src/commands/neofetch.ts +2 -0
  234. package/src/commands/passwd.ts +2 -0
  235. package/src/commands/ping.ts +20 -0
  236. package/src/commands/ps.ts +19 -0
  237. package/src/commands/pwd.ts +2 -0
  238. package/src/commands/rm.ts +2 -0
  239. package/src/commands/sed.ts +45 -0
  240. package/src/commands/set.ts +19 -50
  241. package/src/commands/sh.ts +192 -43
  242. package/src/commands/sleep.ts +14 -0
  243. package/src/commands/sort.ts +37 -0
  244. package/src/commands/su.ts +2 -0
  245. package/src/commands/sudo.ts +2 -0
  246. package/src/commands/tail.ts +39 -0
  247. package/src/commands/tar.ts +58 -0
  248. package/src/commands/tee.ts +25 -0
  249. package/src/commands/touch.ts +2 -0
  250. package/src/commands/tr.ts +24 -0
  251. package/src/commands/tree.ts +2 -0
  252. package/src/commands/uname.ts +20 -0
  253. package/src/commands/uniq.ts +28 -0
  254. package/src/commands/unset.ts +5 -12
  255. package/src/commands/wc.ts +50 -0
  256. package/src/commands/wget.ts +2 -0
  257. package/src/commands/who.ts +2 -0
  258. package/src/commands/whoami.ts +2 -0
  259. package/src/commands/xargs.ts +17 -0
  260. package/src/index.ts +1 -0
  261. package/src/types/commands.ts +14 -0
  262. package/src/types/pipeline.ts +23 -0
  263. package/standalone.js +93 -55
  264. package/standalone.js.map +4 -4
  265. package/tests/bun-test-shim.ts +1 -0
  266. package/tests/sftp.test.ts +115 -191
  267. package/tests/users.test.ts +42 -88
@@ -1,40 +1,40 @@
1
1
  Benchmarking VirtualShell concurrency:
2
2
 
3
3
  Running 1 shells...
4
- Initialized 1 shells in 86ms, RSS 82 MB
4
+ Initialized 1 shells in 67ms, RSS 82 MB
5
5
  Executed shell commands in 2ms, RSS now 82 MB (+0 MB)
6
6
 
7
7
  Running 2 shells...
8
8
  Initialized 2 shells in 2ms, RSS 82 MB
9
- Executed shell commands in 1ms, RSS now 82 MB (+0 MB)
9
+ Executed shell commands in 0ms, RSS now 82 MB (+0 MB)
10
10
 
11
11
  Running 5 shells...
12
- Initialized 5 shells in 5ms, RSS 82 MB
13
- Executed shell commands in 2ms, RSS now 82 MB (+0 MB)
12
+ Initialized 5 shells in 1ms, RSS 82 MB
13
+ Executed shell commands in 1ms, RSS now 82 MB (+0 MB)
14
14
 
15
15
  Running 10 shells...
16
- Initialized 10 shells in 5ms, RSS 84 MB
17
- Executed shell commands in 3ms, RSS now 84 MB (+1 MB)
16
+ Initialized 10 shells in 4ms, RSS 83 MB
17
+ Executed shell commands in 3ms, RSS now 84 MB (+0 MB)
18
18
 
19
19
  Running 20 shells...
20
- Initialized 20 shells in 12ms, RSS 84 MB
21
- Executed shell commands in 6ms, RSS now 85 MB (+1 MB)
20
+ Initialized 20 shells in 5ms, RSS 84 MB
21
+ Executed shell commands in 3ms, RSS now 86 MB (+1 MB)
22
22
 
23
23
  Running 50 shells...
24
- Initialized 50 shells in 30ms, RSS 87 MB
25
- Executed shell commands in 12ms, RSS now 88 MB (+2 MB)
24
+ Initialized 50 shells in 10ms, RSS 88 MB
25
+ Executed shell commands in 7ms, RSS now 89 MB (+1 MB)
26
26
 
27
27
  Running 100 shells...
28
- Initialized 100 shells in 52ms, RSS 91 MB
29
- Executed shell commands in 28ms, RSS now 92 MB (+2 MB)
28
+ Initialized 100 shells in 18ms, RSS 92 MB
29
+ Executed shell commands in 9ms, RSS now 95 MB (+3 MB)
30
30
 
31
31
  Summary:
32
32
 
33
- count init_ms cmd_ms init_rss final_rss
34
- 1 86 2 82 MB 82 MB 0 MB
35
- 2 2 1 82 MB 82 MB 0 MB
36
- 5 5 2 82 MB 82 MB 0 MB
37
- 10 5 3 84 MB 84 MB 1 MB
38
- 20 12 6 84 MB 85 MB 1 MB
39
- 50 30 12 87 MB 88 MB 2 MB
40
- 100 52 28 91 MB 92 MB 2 MB
33
+ count init_ms cmd_ms init_rss final_rss delta_rss
34
+ 1 67 2 82 MB 82 MB 0 MB
35
+ 2 2 0 82 MB 82 MB 0 MB
36
+ 5 1 1 82 MB 82 MB 0 MB
37
+ 10 4 3 83 MB 84 MB 0 MB
38
+ 20 5 3 84 MB 86 MB 1 MB
39
+ 50 10 7 88 MB 89 MB 1 MB
40
+ 100 18 9 92 MB 95 MB 3 MB
@@ -1,4 +1,4 @@
1
- import { runCommand } from "../commands";
1
+ import { makeDefaultEnv, runCommand } from "../commands";
2
2
  function toTtyLines(text) {
3
3
  return text
4
4
  .replace(/\r\n/g, "\n")
@@ -6,7 +6,7 @@ function toTtyLines(text) {
6
6
  .replace(/\n/g, "\r\n");
7
7
  }
8
8
  export function runExec(stream, cmd, authUser, hostname, shell) {
9
- Promise.resolve(runCommand(cmd, authUser, hostname, "exec", `/home/${authUser}`, shell))
9
+ Promise.resolve(runCommand(cmd, authUser, hostname, "exec", `/home/${authUser}`, shell, undefined, makeDefaultEnv(authUser, hostname)))
10
10
  .then((result) => {
11
11
  if (result.stdout) {
12
12
  stream.write(`${toTtyLines(result.stdout)}\r\n`);
@@ -1,9 +1,8 @@
1
- import type { CommandMode, CommandResult } from "../types/commands";
2
- import type { Pipeline } from "../types/pipeline";
1
+ import type { CommandMode, CommandResult, ShellEnv } from "../types/commands";
2
+ import type { Pipeline, Script, Statement } from "../types/pipeline";
3
3
  import type { VirtualShell } from "../VirtualShell";
4
- /**
5
- * Execute a parsed pipeline, chaining commands and handling redirections.
6
- * Manages stdout/stderr flow between commands and file I/O.
7
- */
8
- export declare function executePipeline(pipeline: Pipeline, authUser: string, hostname: string, mode: CommandMode, cwd: string, shell: VirtualShell): Promise<CommandResult>;
4
+ export declare function executeScript(script: Script, authUser: string, hostname: string, mode: CommandMode, cwd: string, shell: VirtualShell, env: ShellEnv): Promise<CommandResult>;
5
+ /** Execute statements connected by &&/||/; */
6
+ export declare function executeStatements(statements: Statement[], authUser: string, hostname: string, mode: CommandMode, cwd: string, shell: VirtualShell, env: ShellEnv): Promise<CommandResult>;
7
+ export declare function executePipeline(pipeline: Pipeline, authUser: string, hostname: string, mode: CommandMode, cwd: string, shell: VirtualShell, env?: ShellEnv): Promise<CommandResult>;
9
8
  //# sourceMappingURL=executor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/executor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,QAAQ,EAAmB,MAAM,mBAAmB,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD;;;GAGG;AACH,wBAAsB,eAAe,CACpC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,YAAY,GACjB,OAAO,CAAC,aAAa,CAAC,CA0BxB"}
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/executor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC9E,OAAO,KAAK,EAAE,QAAQ,EAAmB,MAAM,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACtF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAIpD,wBAAsB,aAAa,CAClC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,QAAQ,GACX,OAAO,CAAC,aAAa,CAAC,CAiBxB;AAED,8CAA8C;AAC9C,wBAAsB,iBAAiB,CACtC,UAAU,EAAE,SAAS,EAAE,EACvB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,QAAQ,GACX,OAAO,CAAC,aAAa,CAAC,CA4BxB;AAID,wBAAsB,eAAe,CACpC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,YAAY,EACnB,GAAG,CAAC,EAAE,QAAQ,GACZ,OAAO,CAAC,aAAa,CAAC,CAiBxB"}
@@ -1,25 +1,66 @@
1
1
  import { runCommand as runSingleCommand } from "../commands";
2
2
  import { resolvePath } from "../commands/helpers";
3
- /**
4
- * Execute a parsed pipeline, chaining commands and handling redirections.
5
- * Manages stdout/stderr flow between commands and file I/O.
6
- */
7
- export async function executePipeline(pipeline, authUser, hostname, mode, cwd, shell) {
8
- if (pipeline.commands.length === 0) {
9
- return { exitCode: 0 };
3
+ // ── Script executor (handles &&/||/;) ────────────────────────────────────────
4
+ export async function executeScript(script, authUser, hostname, mode, cwd, shell, env) {
5
+ if (!script.isValid)
6
+ return { stderr: script.error || "Syntax error", exitCode: 1 };
7
+ let lastResult = { exitCode: 0 };
8
+ for (const stmt of script.statements) {
9
+ // Decide whether to run this statement based on previous op
10
+ lastResult = await executePipeline(stmt.pipeline, authUser, hostname, mode, cwd, shell, env);
11
+ env.lastExitCode = lastResult.exitCode ?? 0;
12
+ // Propagate session-control signals
13
+ if (lastResult.closeSession || lastResult.switchUser || lastResult.nextCwd) {
14
+ break;
15
+ }
16
+ }
17
+ return lastResult;
18
+ }
19
+ /** Execute statements connected by &&/||/; */
20
+ export async function executeStatements(statements, authUser, hostname, mode, cwd, shell, env) {
21
+ let last = { exitCode: 0 };
22
+ let i = 0;
23
+ while (i < statements.length) {
24
+ const stmt = statements[i];
25
+ last = await executePipeline(stmt.pipeline, authUser, hostname, mode, cwd, shell, env);
26
+ env.lastExitCode = last.exitCode ?? 0;
27
+ if (last.closeSession || last.switchUser)
28
+ return last;
29
+ const op = stmt.op;
30
+ if (!op || op === ";") {
31
+ // always run next
32
+ }
33
+ else if (op === "&&") {
34
+ if ((last.exitCode ?? 0) !== 0) {
35
+ // skip until next ; or end
36
+ while (i < statements.length && statements[i]?.op === "&&")
37
+ i++;
38
+ }
39
+ }
40
+ else if (op === "||") {
41
+ if ((last.exitCode ?? 0) === 0) {
42
+ // skip until next ; or end
43
+ while (i < statements.length && statements[i]?.op === "||")
44
+ i++;
45
+ }
46
+ }
47
+ i++;
10
48
  }
49
+ return last;
50
+ }
51
+ // ── Pipeline executor ─────────────────────────────────────────────────────────
52
+ export async function executePipeline(pipeline, authUser, hostname, mode, cwd, shell, env) {
53
+ if (!pipeline.isValid)
54
+ return { stderr: pipeline.error || "Syntax error", exitCode: 1 };
55
+ if (pipeline.commands.length === 0)
56
+ return { exitCode: 0 };
57
+ const shellEnv = env ?? { vars: {}, lastExitCode: 0 };
11
58
  if (pipeline.commands.length === 1) {
12
- // Single command with possible redirections
13
- return executeSingleCommandWithRedirections(pipeline.commands[0], authUser, hostname, mode, cwd, shell);
59
+ return executeSingleCommandWithRedirections(pipeline.commands[0], authUser, hostname, mode, cwd, shell, shellEnv);
14
60
  }
15
- // Multiple commands in a pipeline
16
- return executePipelineChain(pipeline.commands, authUser, hostname, mode, cwd, shell);
61
+ return executePipelineChain(pipeline.commands, authUser, hostname, mode, cwd, shell, shellEnv);
17
62
  }
18
- /**
19
- * Execute a single command with input/output redirections
20
- */
21
- async function executeSingleCommandWithRedirections(cmd, authUser, hostname, mode, cwd, shell) {
22
- // Prepare input if input file specified
63
+ async function executeSingleCommandWithRedirections(cmd, authUser, hostname, mode, cwd, shell, env) {
23
64
  let stdin;
24
65
  if (cmd.inputFile) {
25
66
  const inputPath = resolvePath(cwd, cmd.inputFile);
@@ -27,29 +68,23 @@ async function executeSingleCommandWithRedirections(cmd, authUser, hostname, mod
27
68
  stdin = shell.vfs.readFile(inputPath);
28
69
  }
29
70
  catch {
30
- return {
31
- stderr: `cat: ${cmd.inputFile}: No such file or directory`,
32
- exitCode: 1,
33
- };
71
+ return { stderr: `${cmd.inputFile}: No such file or directory`, exitCode: 1 };
34
72
  }
35
73
  }
36
- // Build raw input for the command
37
74
  const rawInput = [cmd.name, ...cmd.args].join(" ");
38
- // Run the command with potential input
39
- const result = await runSingleCommand(rawInput, authUser, hostname, mode, cwd, shell, stdin);
40
- // Handle output redirection
75
+ const result = await runSingleCommand(rawInput, authUser, hostname, mode, cwd, shell, stdin, env);
41
76
  if (cmd.outputFile) {
42
77
  const outputPath = resolvePath(cwd, cmd.outputFile);
43
78
  const output = result.stdout || "";
44
79
  try {
45
80
  if (cmd.appendOutput) {
46
- try {
47
- const existing = shell.vfs.readFile(outputPath);
48
- shell.writeFileAsUser(authUser, outputPath, existing + output);
81
+ const existing = (() => { try {
82
+ return shell.vfs.readFile(outputPath);
49
83
  }
50
84
  catch {
51
- shell.writeFileAsUser(authUser, outputPath, output);
52
- }
85
+ return "";
86
+ } })();
87
+ shell.writeFileAsUser(authUser, outputPath, existing + output);
53
88
  }
54
89
  else {
55
90
  shell.writeFileAsUser(authUser, outputPath, output);
@@ -57,55 +92,40 @@ async function executeSingleCommandWithRedirections(cmd, authUser, hostname, mod
57
92
  return { ...result, stdout: "" };
58
93
  }
59
94
  catch {
60
- return {
61
- ...result,
62
- stderr: `Failed to write to ${cmd.outputFile}`,
63
- exitCode: 1,
64
- };
95
+ return { ...result, stderr: `Failed to write to ${cmd.outputFile}`, exitCode: 1 };
65
96
  }
66
97
  }
67
98
  return result;
68
99
  }
69
- /**
70
- * Execute a chain of commands connected by pipes
71
- */
72
- async function executePipelineChain(commands, authUser, hostname, mode, cwd, shell) {
100
+ async function executePipelineChain(commands, authUser, hostname, mode, cwd, shell, env) {
73
101
  let currentOutput = "";
74
102
  let exitCode = 0;
75
103
  for (let i = 0; i < commands.length; i++) {
76
104
  const cmd = commands[i];
77
- // Handle input file for first command
78
105
  if (i === 0 && cmd.inputFile) {
79
106
  const inputPath = resolvePath(cwd, cmd.inputFile);
80
107
  try {
81
108
  currentOutput = shell.vfs.readFile(inputPath);
82
109
  }
83
110
  catch {
84
- return {
85
- stderr: `cat: ${cmd.inputFile}: No such file or directory`,
86
- exitCode: 1,
87
- };
111
+ return { stderr: `${cmd.inputFile}: No such file or directory`, exitCode: 1 };
88
112
  }
89
113
  }
90
- // Build raw input
91
114
  const rawInput = [cmd.name, ...cmd.args].join(" ");
92
- // Create a modified context that might accept stdin
93
- // For now, we'll append input as an additional arg for commands that support it
94
- const result = await runSingleCommand(rawInput, authUser, hostname, mode, cwd, shell, currentOutput);
115
+ const result = await runSingleCommand(rawInput, authUser, hostname, mode, cwd, shell, currentOutput, env);
95
116
  exitCode = result.exitCode ?? 0;
96
- // Handle output redirection (only for last command)
97
117
  if (i === commands.length - 1 && cmd.outputFile) {
98
118
  const outputPath = resolvePath(cwd, cmd.outputFile);
99
119
  const output = result.stdout || "";
100
120
  try {
101
121
  if (cmd.appendOutput) {
102
- try {
103
- const existing = shell.vfs.readFile(outputPath);
104
- shell.writeFileAsUser(authUser, outputPath, existing + output);
122
+ const existing = (() => { try {
123
+ return shell.vfs.readFile(outputPath);
105
124
  }
106
125
  catch {
107
- shell.writeFileAsUser(authUser, outputPath, output);
108
- }
126
+ return "";
127
+ } })();
128
+ shell.writeFileAsUser(authUser, outputPath, existing + output);
109
129
  }
110
130
  else {
111
131
  shell.writeFileAsUser(authUser, outputPath, output);
@@ -113,19 +133,16 @@ async function executePipelineChain(commands, authUser, hostname, mode, cwd, she
113
133
  currentOutput = "";
114
134
  }
115
135
  catch {
116
- return {
117
- stderr: `Failed to write to ${cmd.outputFile}`,
118
- exitCode: 1,
119
- };
136
+ return { stderr: `Failed to write to ${cmd.outputFile}`, exitCode: 1 };
120
137
  }
121
138
  }
122
139
  else {
123
- // Pass output to next command
124
140
  currentOutput = result.stdout || "";
125
141
  }
126
- if (result.stderr && exitCode !== 0) {
142
+ if (result.stderr && exitCode !== 0)
127
143
  return { stderr: result.stderr, exitCode };
128
- }
144
+ if (result.closeSession || result.switchUser)
145
+ return result;
129
146
  }
130
147
  return { stdout: currentOutput, exitCode };
131
148
  }
@@ -5,19 +5,31 @@ declare class SshMimic extends EventEmitter {
5
5
  port: number;
6
6
  server: SshServer | null;
7
7
  private shell;
8
- private shellHostname;
8
+ /** Max failed auth attempts before an IP is temporarily locked. */
9
+ private readonly maxAuthAttempts;
10
+ /** How long (ms) a locked IP must wait before retrying. */
11
+ private readonly lockoutDurationMs;
12
+ private readonly authAttempts;
9
13
  /**
10
14
  * Creates a new SSH mimic server instance.
11
15
  *
12
16
  * @param port TCP port to bind on localhost.
13
17
  * @param hostname Virtual hostname used for the SSH ident and default shell label.
14
18
  * @param shell Optional preconfigured virtual shell instance to reuse.
19
+ * @param maxAuthAttempts Max failed attempts per IP before lockout (default: 5).
20
+ * @param lockoutDurationMs Lockout window in ms after exceeding attempts (default: 60 000).
15
21
  */
16
- constructor({ port, hostname, shell, }: {
22
+ constructor({ port, hostname, shell, maxAuthAttempts, lockoutDurationMs, }: {
17
23
  port: number;
18
24
  hostname?: string;
19
25
  shell?: VirtualShell;
26
+ maxAuthAttempts?: number;
27
+ lockoutDurationMs?: number;
20
28
  });
29
+ private isLockedOut;
30
+ private recordFailure;
31
+ private recordSuccess;
32
+ private ensureHomeDir;
21
33
  /**
22
34
  * Starts server and initializes virtual filesystem, users, and handlers.
23
35
  *
@@ -28,6 +40,11 @@ declare class SshMimic extends EventEmitter {
28
40
  * Stops server if running.
29
41
  */
30
42
  stop(): void;
43
+ /**
44
+ * Manually clears the rate-limit record for an IP address.
45
+ * Useful in tests or admin tooling.
46
+ */
47
+ clearLockout(ip: string): void;
31
48
  }
32
49
  export { SftpMimic } from "./sftp";
33
50
  export { SshMimic };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAc/C,cAAM,QAAS,SAAQ,YAAY;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,aAAa,CAAS;IAE9B;;;;;;OAMG;gBACS,EACX,IAAI,EACJ,QAA0B,EAC1B,KAAkC,GAClC,EAAE;QACF,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,YAAY,CAAC;KACrB;IASD;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAoJrC;;OAEG;IACI,IAAI,IAAI,IAAI;CASnB;AAED,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AA0B/C,cAAM,QAAS,SAAQ,YAAY;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,KAAK,CAAe;IAE5B,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,2DAA2D;IAC3D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqC;IAElE;;;;;;;;OAQG;gBACS,EACX,IAAI,EACJ,QAA0B,EAC1B,KAAkC,EAClC,eAAmB,EACnB,iBAA0B,GAC1B,EAAE;QACF,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,YAAY,CAAC;QACrB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC3B;IAYD,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,aAAa;IAcrB;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IA0JrC;;OAEG;IACI,IAAI,IAAI,IAAI;IAUnB;;;OAGG;IACI,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;CAGrC;AAED,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,CAAC"}
@@ -10,28 +10,76 @@ import { loadOrCreateHostKey } from "./hostKey";
10
10
  * This class is exported as `VirtualSshServer` for public API compatibility.
11
11
  * Create an instance, call {@link SshMimic.start}, and stop it with
12
12
  * {@link SshMimic.stop} when your process exits.
13
+ *
14
+ * Features:
15
+ * - Password authentication
16
+ * - Public-key authentication
17
+ * - Per-IP rate limiting / lockout for brute-force protection
18
+ * - Interactive shell sessions
19
+ * - Non-interactive exec sessions
13
20
  */
14
21
  const perf = createPerfLogger("SshMimic");
15
22
  class SshMimic extends EventEmitter {
16
23
  port;
17
24
  server;
18
25
  shell;
19
- shellHostname;
26
+ /** Max failed auth attempts before an IP is temporarily locked. */
27
+ maxAuthAttempts;
28
+ /** How long (ms) a locked IP must wait before retrying. */
29
+ lockoutDurationMs;
30
+ authAttempts = new Map();
20
31
  /**
21
32
  * Creates a new SSH mimic server instance.
22
33
  *
23
34
  * @param port TCP port to bind on localhost.
24
35
  * @param hostname Virtual hostname used for the SSH ident and default shell label.
25
36
  * @param shell Optional preconfigured virtual shell instance to reuse.
37
+ * @param maxAuthAttempts Max failed attempts per IP before lockout (default: 5).
38
+ * @param lockoutDurationMs Lockout window in ms after exceeding attempts (default: 60 000).
26
39
  */
27
- constructor({ port, hostname = "typescript-vm", shell = new VirtualShell(hostname), }) {
40
+ constructor({ port, hostname = "typescript-vm", shell = new VirtualShell(hostname), maxAuthAttempts = 5, lockoutDurationMs = 60_000, }) {
28
41
  super();
29
42
  perf.mark("constructor");
30
43
  this.port = port;
31
- this.shellHostname = hostname;
32
44
  this.server = null;
33
45
  this.shell = shell;
46
+ this.maxAuthAttempts = maxAuthAttempts;
47
+ this.lockoutDurationMs = lockoutDurationMs;
48
+ }
49
+ // ── Rate limiting ────────────────────────────────────────────────────────
50
+ isLockedOut(ip) {
51
+ const entry = this.authAttempts.get(ip);
52
+ if (!entry)
53
+ return false;
54
+ if (Date.now() < entry.lockedUntil)
55
+ return true;
56
+ if (entry.lockedUntil > 0) {
57
+ this.authAttempts.delete(ip);
58
+ }
59
+ return false;
60
+ }
61
+ recordFailure(ip) {
62
+ const entry = this.authAttempts.get(ip) ?? { attempts: 0, lockedUntil: 0 };
63
+ entry.attempts += 1;
64
+ if (entry.attempts >= this.maxAuthAttempts) {
65
+ entry.lockedUntil = Date.now() + this.lockoutDurationMs;
66
+ this.emit("auth:lockout", { ip, until: new Date(entry.lockedUntil) });
67
+ }
68
+ this.authAttempts.set(ip, entry);
69
+ }
70
+ recordSuccess(ip) {
71
+ this.authAttempts.delete(ip);
72
+ }
73
+ // ── Home directory bootstrap ─────────────────────────────────────────────
74
+ ensureHomeDir(authUser) {
75
+ const homePath = `/home/${authUser}`;
76
+ if (!this.shell.vfs.exists(homePath)) {
77
+ this.shell.vfs.mkdir(homePath, 0o755);
78
+ this.shell.vfs.writeFile(`${homePath}/README.txt`, `Welcome to ${this.shell.hostname}\n`);
79
+ void this.shell.vfs.flushMirror();
80
+ }
34
81
  }
82
+ // ── Server lifecycle ─────────────────────────────────────────────────────
35
83
  /**
36
84
  * Starts server and initializes virtual filesystem, users, and handlers.
37
85
  *
@@ -41,7 +89,6 @@ class SshMimic extends EventEmitter {
41
89
  perf.mark("start");
42
90
  const shell = this.shell;
43
91
  const privateKey = loadOrCreateHostKey();
44
- // Ensure VirtualShell is fully initialized before accepting connections
45
92
  await shell.ensureInitialized();
46
93
  this.server = new SshServer({
47
94
  hostKeys: [privateKey],
@@ -52,47 +99,75 @@ class SshMimic extends EventEmitter {
52
99
  let sessionId = null;
53
100
  this.emit("client:connect");
54
101
  client.on("authentication", (ctx) => {
55
- shell;
102
+ const candidateUser = ctx.username || "root";
103
+ remoteAddress = ctx.ip ?? remoteAddress;
104
+ // Rate-limit check
105
+ if (this.isLockedOut(remoteAddress)) {
106
+ this.emit("auth:failure", { username: candidateUser, remoteAddress, reason: "lockout" });
107
+ ctx.reject();
108
+ return;
109
+ }
110
+ // ── Password auth ──────────────────────────────────────
56
111
  if (ctx.method === "password") {
57
- const candidateUser = ctx.username || "root";
58
- remoteAddress = ctx.ip ?? remoteAddress;
59
112
  if (!shell.users.hasPassword(candidateUser)) {
60
113
  console.log(`User ${candidateUser} has no password set, allowing login without verification`);
61
114
  authUser = candidateUser;
62
115
  sessionId = shell.users.registerSession(authUser, remoteAddress).id;
116
+ this.recordSuccess(remoteAddress);
63
117
  this.emit("auth:success", { username: authUser, remoteAddress });
64
- const homePath = `/home/${authUser}`;
65
- if (!shell.vfs.exists(homePath)) {
66
- shell.vfs.mkdir(homePath, 0o755);
67
- shell.vfs.writeFile(`${homePath}/README.txt`, `Welcome to ${shell?.hostname ?? this.shellHostname}`);
68
- void shell.vfs.flushMirror();
69
- }
118
+ this.ensureHomeDir(authUser);
70
119
  ctx.accept();
71
120
  return;
72
121
  }
73
122
  if (!ctx.password ||
74
123
  ctx.password === "" ||
75
124
  !shell.users.verifyPassword(candidateUser, ctx.password)) {
76
- this.emit("auth:failure", {
77
- username: candidateUser,
78
- remoteAddress,
79
- });
125
+ this.recordFailure(remoteAddress);
126
+ this.emit("auth:failure", { username: candidateUser, remoteAddress });
80
127
  ctx.reject();
81
128
  return;
82
129
  }
83
130
  authUser = candidateUser;
84
131
  sessionId = shell.users.registerSession(authUser, remoteAddress).id;
132
+ this.recordSuccess(remoteAddress);
85
133
  this.emit("auth:success", { username: authUser, remoteAddress });
86
- const homePath = `/home/${authUser}`;
87
- if (!shell.vfs.exists(homePath)) {
88
- shell.vfs.mkdir(homePath, 0o755);
89
- shell.vfs.writeFile(`${homePath}/README.txt`, `Welcome to ${shell?.hostname ?? this.shellHostname}`);
90
- void shell.vfs.flushMirror();
91
- }
134
+ this.ensureHomeDir(authUser);
92
135
  ctx.accept();
93
136
  return;
94
137
  }
95
- ctx.reject();
138
+ // ── Public-key auth ────────────────────────────────────
139
+ if (ctx.method === "publickey") {
140
+ const authorizedKeys = shell.users.getAuthorizedKeys(candidateUser);
141
+ if (authorizedKeys.length === 0) {
142
+ // No keys configured — reject cleanly
143
+ ctx.reject();
144
+ return;
145
+ }
146
+ const incomingKey = ctx.key;
147
+ const keyMatches = authorizedKeys.some((k) => k.algo === incomingKey.algo &&
148
+ k.data.equals(incomingKey.data));
149
+ if (!keyMatches) {
150
+ this.recordFailure(remoteAddress);
151
+ this.emit("auth:failure", { username: candidateUser, remoteAddress, method: "publickey" });
152
+ ctx.reject();
153
+ return;
154
+ }
155
+ // Key matched — if this is a signature check step, accept
156
+ if (ctx.signature) {
157
+ authUser = candidateUser;
158
+ sessionId = shell.users.registerSession(authUser, remoteAddress).id;
159
+ this.recordSuccess(remoteAddress);
160
+ this.emit("auth:success", { username: authUser, remoteAddress, method: "publickey" });
161
+ this.ensureHomeDir(authUser);
162
+ ctx.accept();
163
+ }
164
+ else {
165
+ // Key exists but no signature yet — ssh2 will call again with signature
166
+ ctx.accept();
167
+ }
168
+ return;
169
+ }
170
+ ctx.reject(["password", "publickey"]);
96
171
  });
97
172
  client.on("close", () => {
98
173
  shell.users.unregisterSession(sessionId);
@@ -146,6 +221,13 @@ class SshMimic extends EventEmitter {
146
221
  });
147
222
  }
148
223
  }
224
+ /**
225
+ * Manually clears the rate-limit record for an IP address.
226
+ * Useful in tests or admin tooling.
227
+ */
228
+ clearLockout(ip) {
229
+ this.authAttempts.delete(ip);
230
+ }
149
231
  }
150
232
  export { SftpMimic } from "./sftp";
151
233
  export { SshMimic };
@@ -1 +1 @@
1
- {"version":3,"file":"sftp.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/sftp.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAI3C,OAAO,KAAK,iBAAiB,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AA4HhE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,GAAG,CAAC,EAAE,iBAAiB,CAAC;IACxB,KAAK,CAAC,EAAE,kBAAkB,CAAC;CAC3B;AAED,qBAAa,SAAU,SAAQ,YAAY;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,OAAO,CAAiC;gBAEpC,EACX,IAAI,EACJ,QAA0B,EAC1B,KAAK,EACL,GAAG,EACH,KAAK,GACL,EAAE,gBAAgB;IAwBnB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,QAAQ;IAIH,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAyJ9B,IAAI,IAAI,IAAI;IAUnB;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAiB1B;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,kBAAkB;CA6a1B"}
1
+ {"version":3,"file":"sftp.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/sftp.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAI3C,OAAO,KAAK,iBAAiB,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AA4HhE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,GAAG,CAAC,EAAE,iBAAiB,CAAC;IACxB,KAAK,CAAC,EAAE,kBAAkB,CAAC;CAC3B;AAED,qBAAa,SAAU,SAAQ,YAAY;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,OAAO,CAAiC;gBAEpC,EACX,IAAI,EACJ,QAA0B,EAC1B,KAAK,EACL,GAAG,EACH,KAAK,GACL,EAAE,gBAAgB;IAwBnB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,QAAQ;IAIH,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAwK9B,IAAI,IAAI,IAAI;IAUnB;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAiB1B;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,kBAAkB;CA6a1B"}
@@ -110,6 +110,13 @@ export class SftpMimic extends EventEmitter {
110
110
  remoteAddress = ctx.ip ?? remoteAddress;
111
111
  console.log(`[SFTP] Auth attempt: user=${candidateUser}, method=${ctx.method}, ip=${remoteAddress}`);
112
112
  if (ctx.method === "password") {
113
+ // If no password is set for the user, allow login without verification
114
+ if (!this.getUsers().hasPassword(candidateUser)) {
115
+ acceptSession(candidateUser);
116
+ this.emit("auth:success", { username: authUser, remoteAddress });
117
+ ctx.accept();
118
+ return;
119
+ }
113
120
  if (!this.getUsers().verifyPassword(candidateUser, ctx.password ?? "")) {
114
121
  this.emit("auth:failure", {
115
122
  username: candidateUser,
@@ -125,6 +132,13 @@ export class SftpMimic extends EventEmitter {
125
132
  }
126
133
  if (ctx.method === "keyboard-interactive") {
127
134
  const keyboardCtx = ctx;
135
+ // If no password is set, accept immediately
136
+ if (!this.getUsers().hasPassword(candidateUser)) {
137
+ acceptSession(candidateUser);
138
+ this.emit("auth:success", { username: authUser, remoteAddress });
139
+ keyboardCtx.accept();
140
+ return;
141
+ }
128
142
  keyboardCtx.prompt([{ prompt: "Password: ", echo: false }], (answers) => {
129
143
  const password = answers[0] ?? "";
130
144
  if (!this.getUsers().verifyPassword(candidateUser, password)) {