smbex 0.1.0__tar.gz

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 (68) hide show
  1. smbex-0.1.0/LICENSE +21 -0
  2. smbex-0.1.0/PKG-INFO +338 -0
  3. smbex-0.1.0/README.md +300 -0
  4. smbex-0.1.0/pyproject.toml +70 -0
  5. smbex-0.1.0/setup.cfg +4 -0
  6. smbex-0.1.0/smbex/__init__.py +3 -0
  7. smbex-0.1.0/smbex/__main__.py +6 -0
  8. smbex-0.1.0/smbex/auth.py +239 -0
  9. smbex-0.1.0/smbex/backend/__init__.py +5 -0
  10. smbex-0.1.0/smbex/backend/base.py +76 -0
  11. smbex-0.1.0/smbex/backend/fake_backend.py +140 -0
  12. smbex-0.1.0/smbex/backend/ftp_backend.py +253 -0
  13. smbex-0.1.0/smbex/backend/impacket_backend.py +150 -0
  14. smbex-0.1.0/smbex/backend/ssh_backend.py +175 -0
  15. smbex-0.1.0/smbex/browser.py +168 -0
  16. smbex-0.1.0/smbex/cache.py +47 -0
  17. smbex-0.1.0/smbex/cli.py +385 -0
  18. smbex-0.1.0/smbex/config.py +97 -0
  19. smbex-0.1.0/smbex/download.py +179 -0
  20. smbex-0.1.0/smbex/gateway.py +249 -0
  21. smbex-0.1.0/smbex/mux.py +323 -0
  22. smbex-0.1.0/smbex/preload.py +141 -0
  23. smbex-0.1.0/smbex/preview.py +59 -0
  24. smbex-0.1.0/smbex/translate.py +260 -0
  25. smbex-0.1.0/smbex/ui/__init__.py +1 -0
  26. smbex-0.1.0/smbex/ui/app.py +753 -0
  27. smbex-0.1.0/smbex/ui/columns.py +192 -0
  28. smbex-0.1.0/smbex/ui/downloads.py +32 -0
  29. smbex-0.1.0/smbex/ui/help.py +93 -0
  30. smbex-0.1.0/smbex/ui/mux_picker.py +97 -0
  31. smbex-0.1.0/smbex/viewer.py +98 -0
  32. smbex-0.1.0/smbex.egg-info/PKG-INFO +338 -0
  33. smbex-0.1.0/smbex.egg-info/SOURCES.txt +66 -0
  34. smbex-0.1.0/smbex.egg-info/dependency_links.txt +1 -0
  35. smbex-0.1.0/smbex.egg-info/entry_points.txt +2 -0
  36. smbex-0.1.0/smbex.egg-info/requires.txt +13 -0
  37. smbex-0.1.0/smbex.egg-info/top_level.txt +1 -0
  38. smbex-0.1.0/tests/test_auth.py +131 -0
  39. smbex-0.1.0/tests/test_backend_ftp_integration.py +111 -0
  40. smbex-0.1.0/tests/test_backend_impacket_integration.py +64 -0
  41. smbex-0.1.0/tests/test_backend_mux_integration.py +189 -0
  42. smbex-0.1.0/tests/test_backend_ssh_integration.py +96 -0
  43. smbex-0.1.0/tests/test_browser.py +72 -0
  44. smbex-0.1.0/tests/test_cache.py +35 -0
  45. smbex-0.1.0/tests/test_cli.py +52 -0
  46. smbex-0.1.0/tests/test_config.py +73 -0
  47. smbex-0.1.0/tests/test_download.py +137 -0
  48. smbex-0.1.0/tests/test_gateway.py +89 -0
  49. smbex-0.1.0/tests/test_mux.py +146 -0
  50. smbex-0.1.0/tests/test_preload.py +154 -0
  51. smbex-0.1.0/tests/test_preview.py +148 -0
  52. smbex-0.1.0/tests/test_reconnect.py +125 -0
  53. smbex-0.1.0/tests/test_smoke.py +42 -0
  54. smbex-0.1.0/tests/test_ssh_backend.py +78 -0
  55. smbex-0.1.0/tests/test_timestamps.py +83 -0
  56. smbex-0.1.0/tests/test_translate.py +93 -0
  57. smbex-0.1.0/tests/test_translation_japanese.py +72 -0
  58. smbex-0.1.0/tests/test_ui_columns_toggle.py +47 -0
  59. smbex-0.1.0/tests/test_ui_copy.py +66 -0
  60. smbex-0.1.0/tests/test_ui_downloads.py +32 -0
  61. smbex-0.1.0/tests/test_ui_help.py +48 -0
  62. smbex-0.1.0/tests/test_ui_integration.py +34 -0
  63. smbex-0.1.0/tests/test_ui_markers.py +72 -0
  64. smbex-0.1.0/tests/test_ui_mux_picker.py +53 -0
  65. smbex-0.1.0/tests/test_ui_navigation.py +75 -0
  66. smbex-0.1.0/tests/test_ui_theme.py +45 -0
  67. smbex-0.1.0/tests/test_ui_translation.py +51 -0
  68. smbex-0.1.0/tests/test_viewer.py +129 -0
smbex-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Philipp Herkert
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
smbex-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,338 @@
1
+ Metadata-Version: 2.4
2
+ Name: smbex
3
+ Version: 0.1.0
4
+ Summary: Terminal explorer for remote hosts over SMB, SSH/SFTP and FTP, with ranger-style navigation.
5
+ Author: Philipp Herkert
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/BearlyStable/smbex
8
+ Project-URL: Repository, https://github.com/BearlyStable/smbex
9
+ Project-URL: Issues, https://github.com/BearlyStable/smbex/issues
10
+ Keywords: smb,ssh,sftp,ftp,tui,file-manager,ranger,impacket,pentest
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: System :: Networking
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: impacket>=0.11
27
+ Requires-Dist: paramiko>=3.0
28
+ Requires-Dist: textual>=0.60
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8.0; extra == "dev"
31
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
32
+ Requires-Dist: pyftpdlib>=1.5; extra == "dev"
33
+ Requires-Dist: build>=1.0; extra == "dev"
34
+ Provides-Extra: translate
35
+ Requires-Dist: ctranslate2>=4.0; extra == "translate"
36
+ Requires-Dist: sentencepiece>=0.2; extra == "translate"
37
+ Dynamic: license-file
38
+
39
+ # smbex
40
+
41
+ A terminal (TUI) explorer for remote hosts over **SMB**, **SSH/SFTP**, and
42
+ **FTP/FTPS**, built for slow links: ranger-style navigation, in-session listing
43
+ cache, background downloads that yield to browsing, folder preloading, and offline
44
+ filename translation.
45
+
46
+ > Status: **foundation** in progress. See [CLAUDE.md](CLAUDE.md) for the full
47
+ > architecture, roadmap, and hand-off notes.
48
+
49
+ ## Install
50
+
51
+ ### pipx (PyPI)
52
+
53
+ Install the `smbex` command into its own isolated environment (needs Python 3.10+):
54
+
55
+ ```sh
56
+ pipx install smbex
57
+ # ...with offline filename translation (CTranslate2 + SentencePiece, ~65 MB):
58
+ pipx install "smbex[translate]"
59
+
60
+ smbex --help
61
+ ```
62
+
63
+ Installed this way, run it as `smbex …` (the examples below use `python3 -m smbex …`;
64
+ the two are interchangeable). Two things are **not** Python packages and so aren't
65
+ pulled in automatically: the `--mux` feature needs the system OpenSSH `ssh` client,
66
+ and translation needs a one-time language-model download (`smbex --install-lang
67
+ <lang>`; see below).
68
+
69
+ ### Kali Linux (apt only — no pip)
70
+
71
+ Every runtime/test dependency except the translation engine is packaged for Kali:
72
+
73
+ ```sh
74
+ sudo apt update
75
+ sudo apt install python3-impacket python3-paramiko python3-textual \
76
+ python3-pytest python3-pytest-asyncio
77
+ # optional: the reference impacket CLI tools (impacket-smbclient, ...)
78
+ # sudo apt install impacket-scripts
79
+ ```
80
+
81
+ Then run straight from the repo (no pip, no venv needed):
82
+
83
+ ```sh
84
+ cd /path/to/smbex
85
+ python3 -m smbex --version
86
+ ```
87
+
88
+ Filename translation is optional and needs two small wheels (**not** in apt) —
89
+ CTranslate2 + SentencePiece (~65 MB, no torch/CUDA). Add them in a venv that
90
+ inherits the apt packages, then fetch a language model (one `.argosmodel` file):
91
+
92
+ ```sh
93
+ python3 -m venv --system-site-packages ~/.venvs/smbex
94
+ ~/.venvs/smbex/bin/pip install ctranslate2 sentencepiece
95
+ ~/.venvs/smbex/bin/python -m smbex --install-lang ja # one-time, online: ja->en model (~130 MB)
96
+ ```
97
+
98
+ Translation runs entirely on this machine (no filename leaves the box); the only
99
+ network use is that one-time model download. See CLAUDE.md → *Install* for details
100
+ (offline `--model-file`, reusing existing Argos models, etc.).
101
+
102
+ ### Development (any distro, venv)
103
+
104
+ ```sh
105
+ python3 -m venv .venv
106
+ .venv/bin/pip install -e ".[dev]"
107
+ .venv/bin/python -m pytest -q
108
+ ```
109
+
110
+ ## Run
111
+
112
+ ```sh
113
+ # SMB, impacket-smbclient-style target (password is prompted if omitted):
114
+ python3 -m smbex 'DOMAIN/user:password@host'
115
+ python3 -m smbex -H :NThash --no-pass 'DOMAIN/user@host' # pass-the-hash
116
+ python3 -m smbex -k --dc-ip 10.0.0.1 'DOMAIN/user@host.domain' # Kerberos (ccache)
117
+
118
+ # SSH/SFTP (host keys auto-accepted by default; --strict-host-keys to verify):
119
+ python3 -m smbex 'ssh://user@host'
120
+ python3 -m smbex -i ~/.ssh/id_ed25519 'ssh://user@host:2222/var/log'
121
+
122
+ # SSH via an existing OpenSSH ControlMaster socket — no re-login (needs the ssh client):
123
+ python3 -m smbex --mux # pick from sockets found in ~/.ssh etc.
124
+ python3 -m smbex --mux /path/to/master.sock # ride a specific socket directly
125
+
126
+ # FTP / FTPS (no user -> anonymous; ftps:// for TLS):
127
+ python3 -m smbex 'ftp://user@host'
128
+ python3 -m smbex 'ftps://user:pass@host:21/pub'
129
+ ```
130
+
131
+ Navigate like ranger: `h/j/k/l` (or arrows), `g`/`G` for top/bottom, `q` to quit.
132
+ Listings show a compact age (`3d`, `2w`) and file sizes; `o` cycles the sort
133
+ (name → newest → oldest). A status gutter marks folders whose listing is cached
134
+ (`·`), and files/folders queued (`↓`) / downloaded (`✓`).
135
+ Download in the background: `d` (selected file, or a folder recursively), `a` (all
136
+ files in the current folder), `w` (show/hide the task panel). Downloads mirror the
137
+ remote tree under `DIR/<host>` (`--download-dir`, default `./downloads`), resume
138
+ partial files, skip complete ones, and yield to browsing so navigation stays snappy.
139
+
140
+ Optional extras: `--preload` prefetches surrounding folders (toggle with `p`);
141
+ `--translate <lang>` shows English filename translations beside the originals
142
+ (toggle with `t`), computed on-box — see *Install* for the one-time model setup.
143
+ Press `?` for the key reference. If a link drops it's reported and you press `r`
144
+ to reconnect (a deliberate new login); `--auto-reconnect` heals silently instead.
145
+ `--theme <name>` (dark/light/nord/gruvbox) sets the colour theme; `T` switches it.
146
+ `[` / `]` hide the parent / preview column to save space. A downloaded file's
147
+ content shows in the preview pane (text, or an xxd-style hex dump for binaries).
148
+ Press `l`/`Enter` on a downloaded file to open a full content viewer: text scrolls
149
+ with `j/k` (loaded lazily, so large files open instantly) and, with translation on,
150
+ shows the original and English **side by side**; a binary opens as a scrollable
151
+ xxd-style **hex** view. `h`/`Esc` goes back.
152
+
153
+ Defaults live in a config file (`~/.config/smbex/config.ini`; flags override it):
154
+
155
+ ```sh
156
+ python3 -m smbex --write-config # drop a commented sample, then edit it
157
+ ```
158
+
159
+ `smbex --help` ends with a **quickstart** covering connecting, the config file, and
160
+ the one-time translation-model setup (with the model index URL).
161
+
162
+ ## Build a release to transfer
163
+
164
+ `scripts/make_release.sh` writes transferable artifacts to `dist/`:
165
+
166
+ ```sh
167
+ bash scripts/make_release.sh
168
+ ```
169
+
170
+ - **`dist/smbex.pyz`** — a single-file [zipapp](https://docs.python.org/3/library/zipapp.html).
171
+ Copy it to the target and run it; it needs only `python3` plus the runtime deps
172
+ (on Kali: `sudo apt install python3-impacket python3-paramiko python3-textual`):
173
+
174
+ ```sh
175
+ python3 smbex.pyz 'demo:demo@127.0.0.1' --port 4455 # or: ./smbex.pyz
176
+ ```
177
+ - **`dist/smbex-<ver>.tar.gz`** — the source tree (run from it with `python3 -m smbex`).
178
+ - **`dist/QUICKSTART.txt`** — the same quickstart shown at the end of `--help`.
179
+
180
+ ## Try it out (local demo, no root)
181
+
182
+ Terminal 1 — start a throwaway SMB server with sample files on a high port:
183
+
184
+ ```sh
185
+ python3 scripts/demo_server.py
186
+ ```
187
+
188
+ Terminal 2 — connect the client (any username/password is accepted):
189
+
190
+ ```sh
191
+ python3 -m smbex 'demo:demo@127.0.0.1' --port 4455
192
+ # with Japanese filename translation (needs: --install-lang ja), then press 't':
193
+ python3 -m smbex --translate ja 'demo:demo@127.0.0.1' --port 4455
194
+ ```
195
+
196
+ The demo share has a `日本語/` folder of Japanese-named files and folders (写真/,
197
+ 仕事/, 地図.png, …) so you can watch them render as `写真 → Photos` with `t`.
198
+
199
+ ## Test
200
+
201
+ ```sh
202
+ .venv/bin/python -m pytest -q # unit + UI tests
203
+ .venv/bin/python -m pytest -m integration # local SMB/SSH server tests (Phase 1+)
204
+ ```
205
+
206
+ ## Protocol fidelity — how smbex differs from the standard tools
207
+
208
+ smbex builds on the same libraries the reference tools use (`impacket` for SMB,
209
+ `paramiko` for SSH/SFTP, stdlib `ftplib` for FTP), so the **packets themselves are
210
+ well-formed the same way** — smbex does not hand-roll protocol framing. The
211
+ differences below are all in *usage patterns* (command sequences, frequency, trust
212
+ decisions), not in the bytes on the wire. They are documented here so you can weigh
213
+ the risk against your own hosts, especially fragile or tightly-audited ones.
214
+
215
+ ### SMB — per-file tree-connect churn
216
+ smbex issues one `TREE_CONNECT`/`TREE_DISCONNECT` **per file**, where
217
+ impacket-smbclient and the Windows redirector connect a share **once** and reuse
218
+ that tree for the whole session. Each `open_file` does `connectTree` → `openFile`
219
+ and its close tears both down (`smbex/backend/impacket_backend.py`). Downloading 500
220
+ files is 500 tree cycles here vs. 1 there.
221
+
222
+ - **Why it's written this way:** statelessness. A cached tree id (`tid`) goes stale
223
+ after a dropped link (see the reconnect design), so by never holding one across
224
+ operations we can't reuse a dead `tid`. This is a *simplicity* choice, not a
225
+ performance one — there is no throughput advantage.
226
+ - **Cost:** on a host with SMB auditing this multiplies tree-connect events; it is
227
+ slightly more churn than a normal client. It is not malformed and cannot desync the
228
+ session.
229
+ - **If it matters:** cache `{share: tid}` on the backend, connect-on-first-use, and
230
+ invalidate it in `reconnect()` — a small, self-contained change that restores
231
+ one-tree-per-share parity.
232
+
233
+ ### SMB — `stat()` uses a directory FIND, not a file QUERY_INFO
234
+ `stat()` reads a single file's metadata via `listPath` (an SMB2 `QUERY_DIRECTORY`
235
+ against the parent, filename as the search pattern) rather than opening the file and
236
+ issuing `QUERY_INFO`.
237
+
238
+ - **Why:** SMB2 has no "getattr by path" — a real `QUERY_INFO` requires first
239
+ **opening the file** (`CREATE`), which emits a file-**access** audit event and
240
+ participates in sharing-mode/oplock negotiation (you can collide with a process
241
+ holding the file open). The directory FIND reads the metadata out of the parent's
242
+ enumeration **without ever opening the file**. On a sensitive host this is the
243
+ lighter, less-intrusive touch, and it matches impacket-smbclient.
244
+ - **Cost:** it is a directory-query op rather than a handle getinfo, and (today) it
245
+ carries a tree-connect cycle with it (see above).
246
+
247
+ ### SSH — default host-key policy is trust-on-first-use, not persisted
248
+ The default `auto` policy (`smbex/backend/ssh_backend.py`) **silently accepts an
249
+ unknown host key** and keeps it only in memory. OpenSSH `ssh`/`scp` instead *prompt*
250
+ on an unknown key (and refuse in batch mode), writing accepted keys to
251
+ `~/.ssh/known_hosts`.
252
+
253
+ - A **changed** key is still rejected (paramiko raises regardless of policy), so the
254
+ exposure is limited to unknown/first-contact hosts (MITM-susceptible on first
255
+ connect). Because nothing is written to `known_hosts`, every run is "first contact".
256
+ - **If it matters:** pass `--strict-host-keys` (policy `strict`) to require a
257
+ pre-known key, matching default OpenSSH behavior.
258
+
259
+ ### SSH — SFTP subsystem, not scp
260
+ smbex speaks the **SFTP subsystem** (`sftp-server`), not the scp protocol. SFTP was
261
+ chosen because it supports listing, `stat`, and seekable/resumable reads — the things
262
+ the browser, resumable downloads, and the lazy preview/viewer need; plain scp is a
263
+ transfer-only subset. On a host where the sftp subsystem is disabled
264
+ (`Subsystem sftp` removed, a `ForceCommand`/restricted shell permitting only scp, or
265
+ old dropbear builds without sftp-server), smbex will fail where `scp` works. See
266
+ **"scp support"** below for the evaluation of adding an scp backend.
267
+
268
+ ### SSH — listings probe symlink targets
269
+ When listing a directory, smbex issues an extra `STAT` on each **symlink target** to
270
+ decide whether a link is a directory (`_to_entry`, `smbex/backend/ssh_backend.py`).
271
+ `ls`/`sftp` use the `lstat` attrs already returned by readdir and do **not** follow
272
+ the link.
273
+
274
+ - **Why:** so directory-symlinks are browsable (you can `l` into them).
275
+ - **Cost:** extra round trips, and it touches paths you did not navigate to. `stat`
276
+ does not *open* the target, so the risk is low, but on a sensitive host a link could
277
+ point at a device node, an automount trigger, or a monitored path.
278
+
279
+ ### FTP — early-stopped reads drain the rest of the file instead of `ABOR`
280
+ A normal FTP client that stops a `RETR` early sends `ABOR`. smbex instead **reads the
281
+ data connection to its natural EOF** and then consumes the single clean `226`
282
+ (`_drain_and_finish`, `smbex/backend/ftp_backend.py`).
283
+
284
+ - **Why FTP forces the choice:** FTP splits **control** (commands/responses) from
285
+ **data** (file bytes), and the session invariant is that *every control response
286
+ must be read by the command that provoked it* — one response left unread and the
287
+ control channel is off-by-one and wedged. `ABOR` is genuinely awkward: it is
288
+ preceded by an out-of-band Telnet interrupt (`IAC`+`IP`, then Synch) and the server
289
+ then emits **two** control responses in a server-dependent order (typically `426`
290
+ *and* `226`; some do `226`+`225`). Real clients (`lftp`, BSD `ftp`) implement all of
291
+ that; **`ftplib`'s `abort()` does not** and is known to leave the control channel
292
+ desynced against many servers. Draining keeps the control channel provably in sync.
293
+ - **The trade-off:** correctness/robustness over bandwidth. For a fragile server the
294
+ drain is the **conservative** choice — it never leaves the control channel in a weird
295
+ state (the thing that wedges FTP sessions). The cost is bandwidth: previewing or
296
+ seeking within a **large** file can pull the rest of that file. It only triggers when
297
+ you stop **early** — a normal download-to-EOF closes cleanly with no drain.
298
+ - **If the bandwidth ever bites:** the alternative is *not* ftplib's flaky `abort()` —
299
+ it is to drop and reopen the whole control connection to abandon the transfer, which
300
+ is clean but makes a fresh login/audit event each time.
301
+
302
+ ### FTP — passive mode + `MLSD`
303
+ `ftplib` defaults to passive mode (`PASV`/`EPSV`) and smbex prefers `MLSD` for
304
+ listings (falling back to `LIST`). The classic `ftp` command defaults to *active* mode
305
+ and uses `LIST`/`NLST`. Both smbex choices match modern clients (`lftp`); if your
306
+ baseline is the traditional `ftp` client, expect a different data-connection direction
307
+ (firewall-visible) and `MLSD` where you would see `LIST`. `TYPE I` is also re-sent
308
+ before every `RETR` (extra but valid — MLSD/LIST leave the session in ASCII mode).
309
+
310
+ ### Reconnect produces additional login/auth events
311
+ By design, a dropped link plus `r` (or `--auto-reconnect`) performs a **fresh login**,
312
+ so one smbex "session" can show multiple logins from the same source — unlike a single
313
+ long-lived client session. `--auto-reconnect` makes these re-logins silent. Relevant
314
+ for audit correlation.
315
+
316
+ ### scp support (evaluated, not implemented)
317
+ Adding scp as a peer protocol is technically possible (paramiko `exec_command` running
318
+ `scp -f`) but fits smbex's model **poorly**, so it is intentionally **not** built:
319
+
320
+ - **No listing or stat.** The scp protocol only transfers files/trees; browsing would
321
+ require shelling out to remote `ls`/`find` over an exec channel — i.e. **running
322
+ commands** on the host (a larger footprint than SFTP's read-only file ops, and
323
+ shell/PATH-dependent). The ranger UI is built on `list()`/`stat()`, which scp lacks.
324
+ - **No seek/resume.** scp streams a whole file start-to-finish, so resumable downloads,
325
+ the lazy preview, and the windowed viewer (all of which seek) cannot work.
326
+ - **Shrinking need.** OpenSSH 9.0 (2022) deprecated the scp protocol and made the `scp`
327
+ command use SFTP under the hood, so on modern hosts "scp" already *is* sftp.
328
+
329
+ Where it would earn its place: appliances/embedded gear (e.g. **dropbear** without
330
+ sftp-server) or hardened boxes that permit scp but not the sftp subsystem. If such
331
+ targets exist in your fleet, the honest shape is **not** a browsing peer backend but a
332
+ restricted **download-only** mode (transfer via scp, browse via remote `ls`), with no
333
+ resume/preview. Recommendation: don't build it speculatively — revisit only if you can
334
+ enumerate real sftp-disabled hosts.
335
+
336
+ ## License
337
+
338
+ MIT — see [LICENSE](LICENSE).
smbex-0.1.0/README.md ADDED
@@ -0,0 +1,300 @@
1
+ # smbex
2
+
3
+ A terminal (TUI) explorer for remote hosts over **SMB**, **SSH/SFTP**, and
4
+ **FTP/FTPS**, built for slow links: ranger-style navigation, in-session listing
5
+ cache, background downloads that yield to browsing, folder preloading, and offline
6
+ filename translation.
7
+
8
+ > Status: **foundation** in progress. See [CLAUDE.md](CLAUDE.md) for the full
9
+ > architecture, roadmap, and hand-off notes.
10
+
11
+ ## Install
12
+
13
+ ### pipx (PyPI)
14
+
15
+ Install the `smbex` command into its own isolated environment (needs Python 3.10+):
16
+
17
+ ```sh
18
+ pipx install smbex
19
+ # ...with offline filename translation (CTranslate2 + SentencePiece, ~65 MB):
20
+ pipx install "smbex[translate]"
21
+
22
+ smbex --help
23
+ ```
24
+
25
+ Installed this way, run it as `smbex …` (the examples below use `python3 -m smbex …`;
26
+ the two are interchangeable). Two things are **not** Python packages and so aren't
27
+ pulled in automatically: the `--mux` feature needs the system OpenSSH `ssh` client,
28
+ and translation needs a one-time language-model download (`smbex --install-lang
29
+ <lang>`; see below).
30
+
31
+ ### Kali Linux (apt only — no pip)
32
+
33
+ Every runtime/test dependency except the translation engine is packaged for Kali:
34
+
35
+ ```sh
36
+ sudo apt update
37
+ sudo apt install python3-impacket python3-paramiko python3-textual \
38
+ python3-pytest python3-pytest-asyncio
39
+ # optional: the reference impacket CLI tools (impacket-smbclient, ...)
40
+ # sudo apt install impacket-scripts
41
+ ```
42
+
43
+ Then run straight from the repo (no pip, no venv needed):
44
+
45
+ ```sh
46
+ cd /path/to/smbex
47
+ python3 -m smbex --version
48
+ ```
49
+
50
+ Filename translation is optional and needs two small wheels (**not** in apt) —
51
+ CTranslate2 + SentencePiece (~65 MB, no torch/CUDA). Add them in a venv that
52
+ inherits the apt packages, then fetch a language model (one `.argosmodel` file):
53
+
54
+ ```sh
55
+ python3 -m venv --system-site-packages ~/.venvs/smbex
56
+ ~/.venvs/smbex/bin/pip install ctranslate2 sentencepiece
57
+ ~/.venvs/smbex/bin/python -m smbex --install-lang ja # one-time, online: ja->en model (~130 MB)
58
+ ```
59
+
60
+ Translation runs entirely on this machine (no filename leaves the box); the only
61
+ network use is that one-time model download. See CLAUDE.md → *Install* for details
62
+ (offline `--model-file`, reusing existing Argos models, etc.).
63
+
64
+ ### Development (any distro, venv)
65
+
66
+ ```sh
67
+ python3 -m venv .venv
68
+ .venv/bin/pip install -e ".[dev]"
69
+ .venv/bin/python -m pytest -q
70
+ ```
71
+
72
+ ## Run
73
+
74
+ ```sh
75
+ # SMB, impacket-smbclient-style target (password is prompted if omitted):
76
+ python3 -m smbex 'DOMAIN/user:password@host'
77
+ python3 -m smbex -H :NThash --no-pass 'DOMAIN/user@host' # pass-the-hash
78
+ python3 -m smbex -k --dc-ip 10.0.0.1 'DOMAIN/user@host.domain' # Kerberos (ccache)
79
+
80
+ # SSH/SFTP (host keys auto-accepted by default; --strict-host-keys to verify):
81
+ python3 -m smbex 'ssh://user@host'
82
+ python3 -m smbex -i ~/.ssh/id_ed25519 'ssh://user@host:2222/var/log'
83
+
84
+ # SSH via an existing OpenSSH ControlMaster socket — no re-login (needs the ssh client):
85
+ python3 -m smbex --mux # pick from sockets found in ~/.ssh etc.
86
+ python3 -m smbex --mux /path/to/master.sock # ride a specific socket directly
87
+
88
+ # FTP / FTPS (no user -> anonymous; ftps:// for TLS):
89
+ python3 -m smbex 'ftp://user@host'
90
+ python3 -m smbex 'ftps://user:pass@host:21/pub'
91
+ ```
92
+
93
+ Navigate like ranger: `h/j/k/l` (or arrows), `g`/`G` for top/bottom, `q` to quit.
94
+ Listings show a compact age (`3d`, `2w`) and file sizes; `o` cycles the sort
95
+ (name → newest → oldest). A status gutter marks folders whose listing is cached
96
+ (`·`), and files/folders queued (`↓`) / downloaded (`✓`).
97
+ Download in the background: `d` (selected file, or a folder recursively), `a` (all
98
+ files in the current folder), `w` (show/hide the task panel). Downloads mirror the
99
+ remote tree under `DIR/<host>` (`--download-dir`, default `./downloads`), resume
100
+ partial files, skip complete ones, and yield to browsing so navigation stays snappy.
101
+
102
+ Optional extras: `--preload` prefetches surrounding folders (toggle with `p`);
103
+ `--translate <lang>` shows English filename translations beside the originals
104
+ (toggle with `t`), computed on-box — see *Install* for the one-time model setup.
105
+ Press `?` for the key reference. If a link drops it's reported and you press `r`
106
+ to reconnect (a deliberate new login); `--auto-reconnect` heals silently instead.
107
+ `--theme <name>` (dark/light/nord/gruvbox) sets the colour theme; `T` switches it.
108
+ `[` / `]` hide the parent / preview column to save space. A downloaded file's
109
+ content shows in the preview pane (text, or an xxd-style hex dump for binaries).
110
+ Press `l`/`Enter` on a downloaded file to open a full content viewer: text scrolls
111
+ with `j/k` (loaded lazily, so large files open instantly) and, with translation on,
112
+ shows the original and English **side by side**; a binary opens as a scrollable
113
+ xxd-style **hex** view. `h`/`Esc` goes back.
114
+
115
+ Defaults live in a config file (`~/.config/smbex/config.ini`; flags override it):
116
+
117
+ ```sh
118
+ python3 -m smbex --write-config # drop a commented sample, then edit it
119
+ ```
120
+
121
+ `smbex --help` ends with a **quickstart** covering connecting, the config file, and
122
+ the one-time translation-model setup (with the model index URL).
123
+
124
+ ## Build a release to transfer
125
+
126
+ `scripts/make_release.sh` writes transferable artifacts to `dist/`:
127
+
128
+ ```sh
129
+ bash scripts/make_release.sh
130
+ ```
131
+
132
+ - **`dist/smbex.pyz`** — a single-file [zipapp](https://docs.python.org/3/library/zipapp.html).
133
+ Copy it to the target and run it; it needs only `python3` plus the runtime deps
134
+ (on Kali: `sudo apt install python3-impacket python3-paramiko python3-textual`):
135
+
136
+ ```sh
137
+ python3 smbex.pyz 'demo:demo@127.0.0.1' --port 4455 # or: ./smbex.pyz
138
+ ```
139
+ - **`dist/smbex-<ver>.tar.gz`** — the source tree (run from it with `python3 -m smbex`).
140
+ - **`dist/QUICKSTART.txt`** — the same quickstart shown at the end of `--help`.
141
+
142
+ ## Try it out (local demo, no root)
143
+
144
+ Terminal 1 — start a throwaway SMB server with sample files on a high port:
145
+
146
+ ```sh
147
+ python3 scripts/demo_server.py
148
+ ```
149
+
150
+ Terminal 2 — connect the client (any username/password is accepted):
151
+
152
+ ```sh
153
+ python3 -m smbex 'demo:demo@127.0.0.1' --port 4455
154
+ # with Japanese filename translation (needs: --install-lang ja), then press 't':
155
+ python3 -m smbex --translate ja 'demo:demo@127.0.0.1' --port 4455
156
+ ```
157
+
158
+ The demo share has a `日本語/` folder of Japanese-named files and folders (写真/,
159
+ 仕事/, 地図.png, …) so you can watch them render as `写真 → Photos` with `t`.
160
+
161
+ ## Test
162
+
163
+ ```sh
164
+ .venv/bin/python -m pytest -q # unit + UI tests
165
+ .venv/bin/python -m pytest -m integration # local SMB/SSH server tests (Phase 1+)
166
+ ```
167
+
168
+ ## Protocol fidelity — how smbex differs from the standard tools
169
+
170
+ smbex builds on the same libraries the reference tools use (`impacket` for SMB,
171
+ `paramiko` for SSH/SFTP, stdlib `ftplib` for FTP), so the **packets themselves are
172
+ well-formed the same way** — smbex does not hand-roll protocol framing. The
173
+ differences below are all in *usage patterns* (command sequences, frequency, trust
174
+ decisions), not in the bytes on the wire. They are documented here so you can weigh
175
+ the risk against your own hosts, especially fragile or tightly-audited ones.
176
+
177
+ ### SMB — per-file tree-connect churn
178
+ smbex issues one `TREE_CONNECT`/`TREE_DISCONNECT` **per file**, where
179
+ impacket-smbclient and the Windows redirector connect a share **once** and reuse
180
+ that tree for the whole session. Each `open_file` does `connectTree` → `openFile`
181
+ and its close tears both down (`smbex/backend/impacket_backend.py`). Downloading 500
182
+ files is 500 tree cycles here vs. 1 there.
183
+
184
+ - **Why it's written this way:** statelessness. A cached tree id (`tid`) goes stale
185
+ after a dropped link (see the reconnect design), so by never holding one across
186
+ operations we can't reuse a dead `tid`. This is a *simplicity* choice, not a
187
+ performance one — there is no throughput advantage.
188
+ - **Cost:** on a host with SMB auditing this multiplies tree-connect events; it is
189
+ slightly more churn than a normal client. It is not malformed and cannot desync the
190
+ session.
191
+ - **If it matters:** cache `{share: tid}` on the backend, connect-on-first-use, and
192
+ invalidate it in `reconnect()` — a small, self-contained change that restores
193
+ one-tree-per-share parity.
194
+
195
+ ### SMB — `stat()` uses a directory FIND, not a file QUERY_INFO
196
+ `stat()` reads a single file's metadata via `listPath` (an SMB2 `QUERY_DIRECTORY`
197
+ against the parent, filename as the search pattern) rather than opening the file and
198
+ issuing `QUERY_INFO`.
199
+
200
+ - **Why:** SMB2 has no "getattr by path" — a real `QUERY_INFO` requires first
201
+ **opening the file** (`CREATE`), which emits a file-**access** audit event and
202
+ participates in sharing-mode/oplock negotiation (you can collide with a process
203
+ holding the file open). The directory FIND reads the metadata out of the parent's
204
+ enumeration **without ever opening the file**. On a sensitive host this is the
205
+ lighter, less-intrusive touch, and it matches impacket-smbclient.
206
+ - **Cost:** it is a directory-query op rather than a handle getinfo, and (today) it
207
+ carries a tree-connect cycle with it (see above).
208
+
209
+ ### SSH — default host-key policy is trust-on-first-use, not persisted
210
+ The default `auto` policy (`smbex/backend/ssh_backend.py`) **silently accepts an
211
+ unknown host key** and keeps it only in memory. OpenSSH `ssh`/`scp` instead *prompt*
212
+ on an unknown key (and refuse in batch mode), writing accepted keys to
213
+ `~/.ssh/known_hosts`.
214
+
215
+ - A **changed** key is still rejected (paramiko raises regardless of policy), so the
216
+ exposure is limited to unknown/first-contact hosts (MITM-susceptible on first
217
+ connect). Because nothing is written to `known_hosts`, every run is "first contact".
218
+ - **If it matters:** pass `--strict-host-keys` (policy `strict`) to require a
219
+ pre-known key, matching default OpenSSH behavior.
220
+
221
+ ### SSH — SFTP subsystem, not scp
222
+ smbex speaks the **SFTP subsystem** (`sftp-server`), not the scp protocol. SFTP was
223
+ chosen because it supports listing, `stat`, and seekable/resumable reads — the things
224
+ the browser, resumable downloads, and the lazy preview/viewer need; plain scp is a
225
+ transfer-only subset. On a host where the sftp subsystem is disabled
226
+ (`Subsystem sftp` removed, a `ForceCommand`/restricted shell permitting only scp, or
227
+ old dropbear builds without sftp-server), smbex will fail where `scp` works. See
228
+ **"scp support"** below for the evaluation of adding an scp backend.
229
+
230
+ ### SSH — listings probe symlink targets
231
+ When listing a directory, smbex issues an extra `STAT` on each **symlink target** to
232
+ decide whether a link is a directory (`_to_entry`, `smbex/backend/ssh_backend.py`).
233
+ `ls`/`sftp` use the `lstat` attrs already returned by readdir and do **not** follow
234
+ the link.
235
+
236
+ - **Why:** so directory-symlinks are browsable (you can `l` into them).
237
+ - **Cost:** extra round trips, and it touches paths you did not navigate to. `stat`
238
+ does not *open* the target, so the risk is low, but on a sensitive host a link could
239
+ point at a device node, an automount trigger, or a monitored path.
240
+
241
+ ### FTP — early-stopped reads drain the rest of the file instead of `ABOR`
242
+ A normal FTP client that stops a `RETR` early sends `ABOR`. smbex instead **reads the
243
+ data connection to its natural EOF** and then consumes the single clean `226`
244
+ (`_drain_and_finish`, `smbex/backend/ftp_backend.py`).
245
+
246
+ - **Why FTP forces the choice:** FTP splits **control** (commands/responses) from
247
+ **data** (file bytes), and the session invariant is that *every control response
248
+ must be read by the command that provoked it* — one response left unread and the
249
+ control channel is off-by-one and wedged. `ABOR` is genuinely awkward: it is
250
+ preceded by an out-of-band Telnet interrupt (`IAC`+`IP`, then Synch) and the server
251
+ then emits **two** control responses in a server-dependent order (typically `426`
252
+ *and* `226`; some do `226`+`225`). Real clients (`lftp`, BSD `ftp`) implement all of
253
+ that; **`ftplib`'s `abort()` does not** and is known to leave the control channel
254
+ desynced against many servers. Draining keeps the control channel provably in sync.
255
+ - **The trade-off:** correctness/robustness over bandwidth. For a fragile server the
256
+ drain is the **conservative** choice — it never leaves the control channel in a weird
257
+ state (the thing that wedges FTP sessions). The cost is bandwidth: previewing or
258
+ seeking within a **large** file can pull the rest of that file. It only triggers when
259
+ you stop **early** — a normal download-to-EOF closes cleanly with no drain.
260
+ - **If the bandwidth ever bites:** the alternative is *not* ftplib's flaky `abort()` —
261
+ it is to drop and reopen the whole control connection to abandon the transfer, which
262
+ is clean but makes a fresh login/audit event each time.
263
+
264
+ ### FTP — passive mode + `MLSD`
265
+ `ftplib` defaults to passive mode (`PASV`/`EPSV`) and smbex prefers `MLSD` for
266
+ listings (falling back to `LIST`). The classic `ftp` command defaults to *active* mode
267
+ and uses `LIST`/`NLST`. Both smbex choices match modern clients (`lftp`); if your
268
+ baseline is the traditional `ftp` client, expect a different data-connection direction
269
+ (firewall-visible) and `MLSD` where you would see `LIST`. `TYPE I` is also re-sent
270
+ before every `RETR` (extra but valid — MLSD/LIST leave the session in ASCII mode).
271
+
272
+ ### Reconnect produces additional login/auth events
273
+ By design, a dropped link plus `r` (or `--auto-reconnect`) performs a **fresh login**,
274
+ so one smbex "session" can show multiple logins from the same source — unlike a single
275
+ long-lived client session. `--auto-reconnect` makes these re-logins silent. Relevant
276
+ for audit correlation.
277
+
278
+ ### scp support (evaluated, not implemented)
279
+ Adding scp as a peer protocol is technically possible (paramiko `exec_command` running
280
+ `scp -f`) but fits smbex's model **poorly**, so it is intentionally **not** built:
281
+
282
+ - **No listing or stat.** The scp protocol only transfers files/trees; browsing would
283
+ require shelling out to remote `ls`/`find` over an exec channel — i.e. **running
284
+ commands** on the host (a larger footprint than SFTP's read-only file ops, and
285
+ shell/PATH-dependent). The ranger UI is built on `list()`/`stat()`, which scp lacks.
286
+ - **No seek/resume.** scp streams a whole file start-to-finish, so resumable downloads,
287
+ the lazy preview, and the windowed viewer (all of which seek) cannot work.
288
+ - **Shrinking need.** OpenSSH 9.0 (2022) deprecated the scp protocol and made the `scp`
289
+ command use SFTP under the hood, so on modern hosts "scp" already *is* sftp.
290
+
291
+ Where it would earn its place: appliances/embedded gear (e.g. **dropbear** without
292
+ sftp-server) or hardened boxes that permit scp but not the sftp subsystem. If such
293
+ targets exist in your fleet, the honest shape is **not** a browsing peer backend but a
294
+ restricted **download-only** mode (transfer via scp, browse via remote `ls`), with no
295
+ resume/preview. Recommendation: don't build it speculatively — revisit only if you can
296
+ enumerate real sftp-disabled hosts.
297
+
298
+ ## License
299
+
300
+ MIT — see [LICENSE](LICENSE).