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.
- smbex-0.1.0/LICENSE +21 -0
- smbex-0.1.0/PKG-INFO +338 -0
- smbex-0.1.0/README.md +300 -0
- smbex-0.1.0/pyproject.toml +70 -0
- smbex-0.1.0/setup.cfg +4 -0
- smbex-0.1.0/smbex/__init__.py +3 -0
- smbex-0.1.0/smbex/__main__.py +6 -0
- smbex-0.1.0/smbex/auth.py +239 -0
- smbex-0.1.0/smbex/backend/__init__.py +5 -0
- smbex-0.1.0/smbex/backend/base.py +76 -0
- smbex-0.1.0/smbex/backend/fake_backend.py +140 -0
- smbex-0.1.0/smbex/backend/ftp_backend.py +253 -0
- smbex-0.1.0/smbex/backend/impacket_backend.py +150 -0
- smbex-0.1.0/smbex/backend/ssh_backend.py +175 -0
- smbex-0.1.0/smbex/browser.py +168 -0
- smbex-0.1.0/smbex/cache.py +47 -0
- smbex-0.1.0/smbex/cli.py +385 -0
- smbex-0.1.0/smbex/config.py +97 -0
- smbex-0.1.0/smbex/download.py +179 -0
- smbex-0.1.0/smbex/gateway.py +249 -0
- smbex-0.1.0/smbex/mux.py +323 -0
- smbex-0.1.0/smbex/preload.py +141 -0
- smbex-0.1.0/smbex/preview.py +59 -0
- smbex-0.1.0/smbex/translate.py +260 -0
- smbex-0.1.0/smbex/ui/__init__.py +1 -0
- smbex-0.1.0/smbex/ui/app.py +753 -0
- smbex-0.1.0/smbex/ui/columns.py +192 -0
- smbex-0.1.0/smbex/ui/downloads.py +32 -0
- smbex-0.1.0/smbex/ui/help.py +93 -0
- smbex-0.1.0/smbex/ui/mux_picker.py +97 -0
- smbex-0.1.0/smbex/viewer.py +98 -0
- smbex-0.1.0/smbex.egg-info/PKG-INFO +338 -0
- smbex-0.1.0/smbex.egg-info/SOURCES.txt +66 -0
- smbex-0.1.0/smbex.egg-info/dependency_links.txt +1 -0
- smbex-0.1.0/smbex.egg-info/entry_points.txt +2 -0
- smbex-0.1.0/smbex.egg-info/requires.txt +13 -0
- smbex-0.1.0/smbex.egg-info/top_level.txt +1 -0
- smbex-0.1.0/tests/test_auth.py +131 -0
- smbex-0.1.0/tests/test_backend_ftp_integration.py +111 -0
- smbex-0.1.0/tests/test_backend_impacket_integration.py +64 -0
- smbex-0.1.0/tests/test_backend_mux_integration.py +189 -0
- smbex-0.1.0/tests/test_backend_ssh_integration.py +96 -0
- smbex-0.1.0/tests/test_browser.py +72 -0
- smbex-0.1.0/tests/test_cache.py +35 -0
- smbex-0.1.0/tests/test_cli.py +52 -0
- smbex-0.1.0/tests/test_config.py +73 -0
- smbex-0.1.0/tests/test_download.py +137 -0
- smbex-0.1.0/tests/test_gateway.py +89 -0
- smbex-0.1.0/tests/test_mux.py +146 -0
- smbex-0.1.0/tests/test_preload.py +154 -0
- smbex-0.1.0/tests/test_preview.py +148 -0
- smbex-0.1.0/tests/test_reconnect.py +125 -0
- smbex-0.1.0/tests/test_smoke.py +42 -0
- smbex-0.1.0/tests/test_ssh_backend.py +78 -0
- smbex-0.1.0/tests/test_timestamps.py +83 -0
- smbex-0.1.0/tests/test_translate.py +93 -0
- smbex-0.1.0/tests/test_translation_japanese.py +72 -0
- smbex-0.1.0/tests/test_ui_columns_toggle.py +47 -0
- smbex-0.1.0/tests/test_ui_copy.py +66 -0
- smbex-0.1.0/tests/test_ui_downloads.py +32 -0
- smbex-0.1.0/tests/test_ui_help.py +48 -0
- smbex-0.1.0/tests/test_ui_integration.py +34 -0
- smbex-0.1.0/tests/test_ui_markers.py +72 -0
- smbex-0.1.0/tests/test_ui_mux_picker.py +53 -0
- smbex-0.1.0/tests/test_ui_navigation.py +75 -0
- smbex-0.1.0/tests/test_ui_theme.py +45 -0
- smbex-0.1.0/tests/test_ui_translation.py +51 -0
- 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).
|