saltcorn-samba 0.3.10 → 0.4.0
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.
- package/CHANGELOG.md +152 -0
- package/README.md +87 -1
- package/index.js +59 -7
- package/package.json +4 -4
- package/smb-client.js +261 -142
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,158 @@ All notable changes to `saltcorn-samba` are documented here.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.4.0] – 2026-07-05
|
|
8
|
+
|
|
9
|
+
### ⚠️ BREAKING CHANGES
|
|
10
|
+
|
|
11
|
+
- **Node.js ≥ 20 wird jetzt zwingend benötigt** (vorher war 16 möglich).
|
|
12
|
+
Grund: die neue Dependency `smb3-client` ist ein Pure-ESM-Paket und wird
|
|
13
|
+
im Plugin über dynamic `import()` aus CommonJS geladen — das ist ab
|
|
14
|
+
Node 20 stabil.
|
|
15
|
+
- **Dependency-Wechsel:** `@marsaud/smb2` wurde vollständig entfernt und
|
|
16
|
+
durch [`smb3-client`](https://www.npmjs.com/package/smb3-client) `^0.2.0`
|
|
17
|
+
ersetzt.
|
|
18
|
+
- **Zwei neue Config-Felder** in Schritt 1 des Konfigurations-Wizards:
|
|
19
|
+
- `SMB-Signing` (Werte: `if-offered` / `required` / `disabled`, Default `if-offered`)
|
|
20
|
+
- `SMB-Verschlüsselung` (Werte: `if-offered` / `required` / `disabled`, Default `if-offered`)
|
|
21
|
+
|
|
22
|
+
Es ist **kein manueller Config-Umbau nötig** — die Defaults sind für
|
|
23
|
+
fast alle Setups sinnvoll. Wer aus Sicherheitsgründen Signing oder
|
|
24
|
+
Encryption erzwingen will, kann jetzt `required` wählen.
|
|
25
|
+
|
|
26
|
+
### Fixed – **Moderne Samba-Server (`sign_algo_id=0` / AES-CMAC-Pflicht)**
|
|
27
|
+
|
|
28
|
+
Symptom nach dem 0.3.11-Update: nach dem Setzen von
|
|
29
|
+
`NODE_OPTIONS=--openssl-legacy-provider` startete Saltcorn wieder, aber im
|
|
30
|
+
Samba-Log erschien beim ersten Zugriff:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
smbd: sign_algo_id=0 in negotiate response, expected AES-CMAC (2) or higher
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
gefolgt von `STATUS_INVALID_PARAMETER` und einem 401-Fehler im Plugin.
|
|
37
|
+
|
|
38
|
+
**Ursache:** Aktuelle Samba-Versionen (Ubuntu 22.04+ / Debian 12+ /
|
|
39
|
+
RHEL 9+) verlangen im SMB-3.1.1-Handshake das moderne Signing-Verfahren
|
|
40
|
+
**AES-128-CMAC** (`SigningAlgorithmId 2`). `@marsaud/smb2` (letztes
|
|
41
|
+
Release 2020, unmaintained) implementiert diese Cipher-Suite nicht — es
|
|
42
|
+
kann nur die alten HMAC-SHA256- bzw. HMAC-MD5-Signaturen und meldet
|
|
43
|
+
sich mit `sign_algo_id=0`, was ein moderner Samba als Protokoll-Fehler
|
|
44
|
+
verwirft.
|
|
45
|
+
|
|
46
|
+
**Lösung — kompletter Wechsel der SMB-Client-Library:**
|
|
47
|
+
|
|
48
|
+
| Aspekt | Vorher (`@marsaud/smb2`) | Nachher (`smb3-client` 0.2.0) |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| Protokoll | SMB 2.0 / 2.1 | SMB 2.1 / 3.0 / 3.0.2 / **3.1.1** |
|
|
51
|
+
| Signing | HMAC-SHA256 (nur) | HMAC-SHA256 **+ AES-128-CMAC** |
|
|
52
|
+
| Encryption | – | AES-128-CCM / AES-128-GCM (optional) |
|
|
53
|
+
| Pre-Auth | – | SHA-512 Pre-Auth-Integrity |
|
|
54
|
+
| Auth | NTLM (via `ntlm`, DES-ECB) | **NTLMv2 / SPNEGO** (ohne DES-ECB) |
|
|
55
|
+
| Wartung | Letztes Release Q4 2020 | Aktiv (Mai 2026) |
|
|
56
|
+
| Runtime-Deps | `ntlm`, `iconv-lite` | **keine** |
|
|
57
|
+
| API | Callback | **Promise / async** |
|
|
58
|
+
| Modul-System | CommonJS | Pure ESM (via dynamic import geladen) |
|
|
59
|
+
|
|
60
|
+
### Removed
|
|
61
|
+
|
|
62
|
+
- Legacy-Crypto-Check-Utility (`checkLegacyCryptoAvailable`,
|
|
63
|
+
`legacyCryptoErrorMessage`) sowie zugehöriger Handshake-Block in der
|
|
64
|
+
`/sambatest`-Route und im Test-Panel — nicht mehr nötig, weil
|
|
65
|
+
`smb3-client` keine Legacy-Cipher (DES-ECB) verwendet.
|
|
66
|
+
- Troubleshooting-Abschnitt zum `--openssl-legacy-provider`-Flag im README
|
|
67
|
+
(durch neuen 0.4.0-Kompatibilitätsabschnitt ersetzt).
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- `smb-client.js` komplett neu geschrieben als dünner Wrapper um
|
|
72
|
+
`smb3-client`. Interne API (`buildClient`, `withClient`, `readdir`,
|
|
73
|
+
`readFile`, `writeFile`, `rename`, `unlink`, `mkdir`, `rmdir`) bleibt
|
|
74
|
+
identisch — kein Anpassungsbedarf in den View- oder Route-Handlern.
|
|
75
|
+
- Pfad-Konvention intern umgestellt: `smb3-client` erwartet den Share als
|
|
76
|
+
erstes Segment jedes Pfads (`share/subdir/datei.pdf`) statt separat im
|
|
77
|
+
Constructor. Der Wrapper prependet den Share-Namen transparent, sodass
|
|
78
|
+
Aufrufer weiterhin nur relative Pfade (`subdir/datei.pdf`) übergeben.
|
|
79
|
+
- Readdir-Ergebnisse werden pro Eintrag mit einem parallelen `stat()`-
|
|
80
|
+
Aufruf (Batch-Größe 16) angereichert, weil `smb3-client`-Dirents nur
|
|
81
|
+
`name` + `isFile()` + `isDirectory()` liefern — der Plugin-Filemanager
|
|
82
|
+
braucht aber weiterhin Größe und mtime.
|
|
83
|
+
- Fehler-Mapping in `/sambatest` erweitert um typische Signing-/
|
|
84
|
+
Encryption-/Pre-Auth-Fehlermeldungen aus `smb3-client` (`bad signature`,
|
|
85
|
+
`preauth integrity`, `encryption`).
|
|
86
|
+
|
|
87
|
+
### Migration
|
|
88
|
+
|
|
89
|
+
1. Saltcorn stoppen.
|
|
90
|
+
2. Plugin auf 0.4.0 aktualisieren (npm bzw. neues ZIP entpacken).
|
|
91
|
+
3. Falls gesetzt: `NODE_OPTIONS=--openssl-legacy-provider` **entfernen**
|
|
92
|
+
(nicht mehr nötig; siehe README-Abschnitt „Troubleshooting →
|
|
93
|
+
ERR_OSSL_EVP_UNSUPPORTED").
|
|
94
|
+
4. Sicherstellen, dass **Node.js ≥ 20** installiert ist (`node -v`).
|
|
95
|
+
5. Saltcorn wieder starten. Die Plugin-Config bleibt gültig — die neuen
|
|
96
|
+
Felder `SMB-Signing` und `SMB-Verschlüsselung` erhalten automatisch
|
|
97
|
+
den Default `if-offered`. Wer maximale Sicherheit möchte, setzt beide
|
|
98
|
+
auf `required` (Voraussetzung: der Samba-Server unterstützt es).
|
|
99
|
+
6. Im Config-Wizard einmal „→ Verbindung jetzt testen" klicken.
|
|
100
|
+
|
|
101
|
+
## [0.3.11] – 2026-07-05
|
|
102
|
+
|
|
103
|
+
### Fixed – **Worker-Crash / 502 durch DES-ECB auf Node 17+ / OpenSSL 3**
|
|
104
|
+
|
|
105
|
+
Symptom nach dem 0.3.10-Update:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
node:internal/crypto/cipher:117
|
|
109
|
+
Error: error:0308010C:digital envelope routines::unsupported
|
|
110
|
+
at Cipheriv.createCipherBase ...
|
|
111
|
+
at .../@marsaud/smb2/node_modules/ntlm/lib/smbhash.js:46
|
|
112
|
+
code: 'ERR_OSSL_EVP_UNSUPPORTED'
|
|
113
|
+
worker died
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Auf der Config-Seite dann: **„Antwort war kein JSON (HTTP 502)“**.
|
|
117
|
+
|
|
118
|
+
**Ursache:** `@marsaud/smb2` benutzt über das transitive Paket `ntlm` den
|
|
119
|
+
Cipher **DES-ECB** zur Berechnung der LM/NTLM-Hashes. Node.js ab Version 17
|
|
120
|
+
ist gegen OpenSSL 3 gebaut, das DES-ECB standardmäßig blockiert. Der
|
|
121
|
+
`createCipheriv("des-ecb", ...)`-Aufruf wirft dann synchron
|
|
122
|
+
`ERR_OSSL_EVP_UNSUPPORTED`. Weil der Fehler synchron aus tiefen Callback-
|
|
123
|
+
Aufrufen kommt, tötet er den Saltcorn-Worker-Prozess → der Reverse-Proxy
|
|
124
|
+
liefert 502 → die Route liefert kein JSON.
|
|
125
|
+
|
|
126
|
+
**Fixes in diesem Release:**
|
|
127
|
+
|
|
128
|
+
1. **Präventiver Check in `smb-client.js`:** Beim ersten Aufbau eines
|
|
129
|
+
SMB-Clients wird geprüft, ob `crypto.createCipheriv("des-ecb", ...)`
|
|
130
|
+
funktioniert. Wenn nicht, wird eine saubere, ausführliche deutsche
|
|
131
|
+
Fehlermeldung mit Lieferanleitung geworfen (`E_LEGACY_CRYPTO`) — der
|
|
132
|
+
fehlerhafte NTLM-Code wird gar nicht erst betreten, der Worker überlebt.
|
|
133
|
+
2. **`/sambatest`-Route:** Der Check läuft zusätzlich explizit **vor**
|
|
134
|
+
dem `withClient`-Aufruf und liefert JSON mit `code: "E_LEGACY_CRYPTO"`,
|
|
135
|
+
`error`, `hint`, `node_version` und `openssl_version` — damit steht der
|
|
136
|
+
Diagnose-Grund direkt in der UI, nicht im Server-Log.
|
|
137
|
+
3. **Fehler-Mapping erweitert:** Falls der Crash doch mal aus einem
|
|
138
|
+
anderen Pfad kommt (z. B. spätere Reconnect-Versuche), erkennt die
|
|
139
|
+
Catch-Logik jetzt auch `ERR_OSSL_EVP_UNSUPPORTED`,
|
|
140
|
+
`digital envelope routines` und `unsupported` und liefert einen
|
|
141
|
+
verständlichen Hinweis.
|
|
142
|
+
4. **README:** Neuer Abschnitt „Troubleshooting“ mit Setup-Rezepten für
|
|
143
|
+
`NODE_OPTIONS=--openssl-legacy-provider` (Umgebungsvariable, systemd,
|
|
144
|
+
Docker/Compose, PM2, direkter Aufruf) plus Tabelle mit den häufigsten
|
|
145
|
+
Verbindungsfehlern und Diagnose-Kommandos.
|
|
146
|
+
|
|
147
|
+
### Notes
|
|
148
|
+
- Das Plugin ändert keinen Node-Startparameter selbst — das können wir aus
|
|
149
|
+
Sicherheits- und Prozessgründen nicht (Node-Flags müssen beim Prozess-
|
|
150
|
+
Start gesetzt werden). Der Fix ist eine saubere Fehlererkennung mit
|
|
151
|
+
Lösungsanleitung.
|
|
152
|
+
- Sobald Saltcorn mit `NODE_OPTIONS=--openssl-legacy-provider` läuft,
|
|
153
|
+
funktionieren Verbindungstest und alle SMB-Operationen ohne weitere
|
|
154
|
+
Anpassung.
|
|
155
|
+
- **Roadmap:** Migration weg von `@marsaud/smb2` (letzter Release 2020) hin
|
|
156
|
+
zu einer aktiv gepflegten SMB-Client-Bibliothek, damit das
|
|
157
|
+
Legacy-Provider-Flag nicht mehr nötig ist.
|
|
158
|
+
|
|
7
159
|
## [0.3.10] – 2026-07-05
|
|
8
160
|
|
|
9
161
|
### Fixed – **DNS-Fehler: `getaddrinfo ENOTFOUND "host:445"`**
|
package/README.md
CHANGED
|
@@ -12,7 +12,24 @@ Features:
|
|
|
12
12
|
- 📄 **Inline-PDF-/Bild-Anzeige** direkt im `SambaFileManager` (Klick auf eine Datei), plus Download- und externe-App-Buttons.
|
|
13
13
|
- 🚀 **`smb://`-Links** — Öffnet Dateien und Ordner direkt in Nemo, Nautilus, Dolphin (Linux) oder Explorer (Windows).
|
|
14
14
|
- 🔒 **Sicherheit** — Base-Path als „Chroot", strenge Path-Traversal-Prüfung (`..`, absolute Pfade, UNC, Drive-Letters, NUL-Bytes), CSRF-Schutz auf Schreib-Routen, Filename-Sanitizer, Extension-Blocklist, rollenbasierter Zugriff (getrennte Lese-/Schreib-Rollen).
|
|
15
|
-
-
|
|
15
|
+
- 🔐 **Moderne SMB-Kryptografie** — SMB 3.1.1 mit **AES-128-CMAC-Signing** (Pflicht auf aktuellen Samba-Servern) und optionaler **AES-CCM/GCM-Verschlüsselung**. Signing- und Encryption-Modus per Config-Dropdown wählbar (`if-offered` / `required` / `disabled`). **Keine Legacy-Cipher** (DES-ECB) mehr nötig — läuft ohne `--openssl-legacy-provider` unter Node 20+.
|
|
16
|
+
- 🐳 **Docker-freundlich** — direkt per SMB2/3-Protokoll, keine System-Binaries nötig.
|
|
17
|
+
|
|
18
|
+
> **Voraussetzungen:** **Node.js ≥ 20** (wegen der ESM-basierten [`smb3-client`](https://www.npmjs.com/package/smb3-client) Library, die intern per dynamic `import()` geladen wird). Saltcorn 0.9+ wird unterstützt.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Was ist neu in 0.4.0 (Migration von 0.3.x)
|
|
23
|
+
|
|
24
|
+
Das Plugin wurde intern von der unmaintained `@marsaud/smb2` auf die moderne
|
|
25
|
+
`smb3-client` umgestellt. **Kein Config-Umbau nötig** — die neuen Felder
|
|
26
|
+
`SMB-Signing` und `SMB-Verschlüsselung` haben sinnvolle Defaults (`if-offered`).
|
|
27
|
+
Dadurch:
|
|
28
|
+
|
|
29
|
+
- ✅ funktioniert wieder gegen moderne Samba-Server, die `sign_algo_id != 0`
|
|
30
|
+
(AES-CMAC) verlangen,
|
|
31
|
+
- ✅ **kein** `--openssl-legacy-provider`-Flag mehr nötig,
|
|
32
|
+
- ⚠️ **Breaking:** Node.js ≥ 20 zwingend erforderlich (vorher 16 möglich).
|
|
16
33
|
|
|
17
34
|
---
|
|
18
35
|
|
|
@@ -109,6 +126,8 @@ Configure* ausfüllen. Die Konfiguration ist zweistufig:
|
|
|
109
126
|
| **Passwort** | *(secret)* | Samba nutzt ein eigenes Passwort (`smbpasswd`), nicht zwingend das Linux-Login. Moderne Server lehnen leere Passwörter ab. |
|
|
110
127
|
| **Basispfad** | `projekte/2026` | Optional. Relativ, mit Slashes, **ohne** führenden `/`. Beschränkt jeden Zugriff auf dieses Unterverzeichnis der Freigabe. `..` und absolute Pfade werden abgelehnt. |
|
|
111
128
|
| **TCP-Port** | `445` | Standard SMB2/3 über TCP. **SMBv1 (139) wird nicht unterstützt** – auf dem Server `min protocol = SMB2` setzen. |
|
|
129
|
+
| **SMB-Signing** | `if-offered` | `if-offered` (Standard), `required` oder `disabled`. Nutzt HMAC-SHA256 (SMB 2.x) bzw. AES-128-CMAC (SMB 3.x). Moderne Samba-Server verlangen häufig Signing → `required` oder `if-offered`. |
|
|
130
|
+
| **SMB-Verschlüsselung** | `if-offered` | `if-offered` (Standard), `required` oder `disabled`. Nutzt AES-128/256-CCM/GCM. Shares mit serverseitigem `SMB2_SHAREFLAG_ENCRYPT_DATA` erzwingen Verschlüsselung ohnehin. |
|
|
112
131
|
|
|
113
132
|
> **Tipp:** Bevor Sie speichern, klicken Sie auf **„→ Verbindung jetzt
|
|
114
133
|
> testen“** – siehe Abschnitt [Verbindung testen](#verbindung-testen).
|
|
@@ -331,6 +350,73 @@ Ab dann taucht das Plugin im Plugins-Store jeder Saltcorn-Instanz auf.
|
|
|
331
350
|
|
|
332
351
|
---
|
|
333
352
|
|
|
353
|
+
## Troubleshooting
|
|
354
|
+
|
|
355
|
+
### `ERR_MODULE_NOT_FOUND` / „Cannot find package 'smb3-client'" / Worker startet nicht
|
|
356
|
+
|
|
357
|
+
Das Plugin lädt `smb3-client` (ESM) via dynamic `import()` — das npm-Paket
|
|
358
|
+
muss also im Plugin-Ordner installiert sein.
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
cd /pfad/zu/saltcorn-samba
|
|
362
|
+
npm install
|
|
363
|
+
saltcorn restart # oder systemd/docker-Neustart
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Bei Installation über die Saltcorn-Plugin-UI (Source `npm` oder `github`)
|
|
367
|
+
führt Saltcorn `npm install` automatisch aus.
|
|
368
|
+
|
|
369
|
+
### `STATUS_INVALID_PARAMETER` / „sign_algo_id=0" im Samba-Log
|
|
370
|
+
|
|
371
|
+
Das war der Fehler in **0.3.x mit `@marsaud/smb2`**: moderne Samba-Server
|
|
372
|
+
(Ubuntu 22.04+ / Debian 12+ / RHEL 9+) verlangen **AES-CMAC-Signing**, das
|
|
373
|
+
die alte Library nicht konnte. **In 0.4.0 gelöst** — die neue `smb3-client`-
|
|
374
|
+
Library implementiert AES-CMAC. Falls die Meldung dennoch auftaucht:
|
|
375
|
+
|
|
376
|
+
- In der Plugin-Config **SMB-Signing = `required`** setzen (erzwingt
|
|
377
|
+
AES-CMAC schon beim Handshake).
|
|
378
|
+
- Auf dem Server sicherstellen, dass `server signing = mandatory` gesetzt
|
|
379
|
+
ist, dann in der Client-Config ebenfalls `required` wählen.
|
|
380
|
+
|
|
381
|
+
### `ERR_OSSL_EVP_UNSUPPORTED` / `--openssl-legacy-provider`
|
|
382
|
+
|
|
383
|
+
**Betrifft 0.4.0 nicht mehr.** Die alte Abhängigkeit `@marsaud/smb2` nutzte
|
|
384
|
+
intern DES-ECB (aus dem transitiven `ntlm`-Paket), das ab OpenSSL 3
|
|
385
|
+
deaktiviert ist. `smb3-client` verwendet ausschließlich moderne Cipher
|
|
386
|
+
(HMAC-SHA256, AES-128-CMAC, AES-CCM/GCM, SHA-512-PreAuth-Integrity).
|
|
387
|
+
**Das `NODE_OPTIONS=--openssl-legacy-provider`-Flag darf und sollte in
|
|
388
|
+
0.4.0 wieder entfernt werden.**
|
|
389
|
+
|
|
390
|
+
### Signing- und Verschlüsselungs-Modi
|
|
391
|
+
|
|
392
|
+
Beide Modi haben denselben Wertebereich:
|
|
393
|
+
|
|
394
|
+
| Wert | Verhalten |
|
|
395
|
+
|---|---|
|
|
396
|
+
| `if-offered` **(Standard)** | Wird genutzt, wenn der Server es anbietet. Kompatibel mit alten und neuen Servern. |
|
|
397
|
+
| `required` | Wird zwingend verlangt. Verbindung schlägt fehl, wenn der Server nicht mitmacht. Empfohlen für Produktionsumgebungen. |
|
|
398
|
+
| `disabled` | Wird nicht angefordert. Nur für Legacy-Server oder isolierte LAN-Setups. Nicht mit Servern kombinieren, die Signing/Encryption erzwingen. |
|
|
399
|
+
|
|
400
|
+
Typische Fehlermeldungen und was zu tun ist:
|
|
401
|
+
|
|
402
|
+
| Symptom | Wahrscheinliche Ursache | Lösung |
|
|
403
|
+
|---|---|---|
|
|
404
|
+
| `bad signature` / `SIGNATURE_MISMATCH` | Uhrzeit-Drift zwischen Client und Server, oder falsche Credentials | Zeit synchronisieren (NTP), Passwort prüfen |
|
|
405
|
+
| `preauth integrity` | Man-in-the-Middle oder Netzwerkproblem | Netzwerkpfad prüfen (VPN, Proxy) |
|
|
406
|
+
| Verbindung schlägt bei `required` fehl | Server bietet Modus nicht an | Server konfigurieren oder Modus auf `if-offered` senken |
|
|
407
|
+
|
|
408
|
+
### Weitere häufige Fehler
|
|
409
|
+
|
|
410
|
+
| Symptom | Ursache | Lösung |
|
|
411
|
+
|---|---|---|
|
|
412
|
+
| `ECONNREFUSED :445` | SMB-Dienst läuft nicht oder Firewall blockt Port 445 | `nc -vz <server> 445` vom Saltcorn-Host testen |
|
|
413
|
+
| `ETIMEDOUT` | Kein Netzwerkpfad (VLAN/Docker-Bridge/VPN) | Vom Saltcorn-Container aus `ping` + `nc -vz` prüfen |
|
|
414
|
+
| `LOGON_FAILURE` / `STATUS_LOGON_FAILURE` | User/Passwort/Domain falsch | Zugangsdaten prüfen. Moderne Samba-Server erlauben keine leeren Passwörter |
|
|
415
|
+
| `BAD_NETWORK_NAME` / `STATUS_BAD_NETWORK_NAME` | Share existiert nicht oder Schreibweise falsch | `smbclient -L //server -U user` zur Kontrolle |
|
|
416
|
+
| Nur SMBv1 verfügbar | Server bietet SMB2/3 nicht an | In `smb.conf`: `min protocol = SMB2` |
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
334
420
|
## Bekannte Grenzen
|
|
335
421
|
|
|
336
422
|
- Dateien werden komplett gepuffert (`readFile` / `writeFile`). Für
|
package/index.js
CHANGED
|
@@ -92,13 +92,15 @@ window.sambaTestConn = async function(btn) {
|
|
|
92
92
|
if (!form) { out.innerHTML = '<div class="alert alert-danger">Formular nicht gefunden.</div>'; return; }
|
|
93
93
|
function v(n){ var el = form.querySelector('[name="'+n+'"]'); return el ? el.value : ''; }
|
|
94
94
|
var payload = {
|
|
95
|
-
server:
|
|
96
|
-
share:
|
|
97
|
-
domain:
|
|
98
|
-
username:
|
|
99
|
-
password:
|
|
100
|
-
base_path:
|
|
101
|
-
port:
|
|
95
|
+
server: v('server'),
|
|
96
|
+
share: v('share'),
|
|
97
|
+
domain: v('domain'),
|
|
98
|
+
username: v('username'),
|
|
99
|
+
password: v('password'),
|
|
100
|
+
base_path: v('base_path'),
|
|
101
|
+
port: v('port'),
|
|
102
|
+
signing_mode: v('signing_mode'),
|
|
103
|
+
encryption_mode: v('encryption_mode')
|
|
102
104
|
};
|
|
103
105
|
if (!payload.server || !payload.share) {
|
|
104
106
|
out.innerHTML = '<div class="alert alert-warning">Bitte mindestens <b>Server</b> und <b>Share</b> ausfüllen.</div>';
|
|
@@ -287,6 +289,46 @@ const configuration_workflow = () =>
|
|
|
287
289
|
type: "Integer",
|
|
288
290
|
default: 445,
|
|
289
291
|
}),
|
|
292
|
+
new Field({
|
|
293
|
+
name: "signing_mode",
|
|
294
|
+
label: "SMB-Signing",
|
|
295
|
+
sublabel:
|
|
296
|
+
"Wie streng jede Nachricht kryptografisch signiert wird (HMAC-SHA256 für SMB 2.x, AES-128-CMAC für SMB 3.x). " +
|
|
297
|
+
"<b>required</b>: Signing wird zwingend verlangt; Verbindung schlägt fehl, wenn der Server nicht signiert. " +
|
|
298
|
+
"<b>if-offered</b> (Standard): Signing wird genutzt, wenn der Server es anbietet, sonst weiter ohne. " +
|
|
299
|
+
"<b>disabled</b>: kein Signing (nur auswählen, wenn der Server Signing verweigert). " +
|
|
300
|
+
"Moderne Samba-Server verlangen häufig Signing → „required“ oder „if-offered“ setzen.",
|
|
301
|
+
type: "String",
|
|
302
|
+
required: true,
|
|
303
|
+
attributes: {
|
|
304
|
+
options: [
|
|
305
|
+
{ value: "if-offered", label: "if-offered (Standard)" },
|
|
306
|
+
{ value: "required", label: "required (strikt)" },
|
|
307
|
+
{ value: "disabled", label: "disabled (aus)" },
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
default: "if-offered",
|
|
311
|
+
}),
|
|
312
|
+
new Field({
|
|
313
|
+
name: "encryption_mode",
|
|
314
|
+
label: "SMB-Verschlüsselung",
|
|
315
|
+
sublabel:
|
|
316
|
+
"Verschlüsselung der Nutzdaten auf dem Draht (AES-128/256-CCM/GCM). " +
|
|
317
|
+
"<b>required</b>: Verbindung nur mit Verschlüsselung; scheitert, wenn der Server keine anbietet. " +
|
|
318
|
+
"<b>if-offered</b> (Standard): Verschlüsselung wird genutzt, wenn der Server sie anbietet. " +
|
|
319
|
+
"<b>disabled</b>: keine Verschlüsselung anfordern (nur für Legacy-Server oder LAN-Only-Setups). " +
|
|
320
|
+
"Shares, die serverseitig <code>SMB2_SHAREFLAG_ENCRYPT_DATA</code> tragen, erzwingen Verschlüsselung ohnehin.",
|
|
321
|
+
type: "String",
|
|
322
|
+
required: true,
|
|
323
|
+
attributes: {
|
|
324
|
+
options: [
|
|
325
|
+
{ value: "if-offered", label: "if-offered (Standard)" },
|
|
326
|
+
{ value: "required", label: "required (strikt)" },
|
|
327
|
+
{ value: "disabled", label: "disabled (aus)" },
|
|
328
|
+
],
|
|
329
|
+
},
|
|
330
|
+
default: "if-offered",
|
|
331
|
+
}),
|
|
290
332
|
new Field({
|
|
291
333
|
name: "_test_html",
|
|
292
334
|
label: " ",
|
|
@@ -742,6 +784,8 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
742
784
|
password: String(body.password || ""),
|
|
743
785
|
base_path: String(body.base_path || "").trim(),
|
|
744
786
|
port: Number(body.port) || 445,
|
|
787
|
+
signing_mode: String(body.signing_mode || "").trim() || undefined,
|
|
788
|
+
encryption_mode: String(body.encryption_mode || "").trim() || undefined,
|
|
745
789
|
};
|
|
746
790
|
|
|
747
791
|
if (!testCfg.server) return jsonError(res, 400, "Please enter a Server (hostname or IP).");
|
|
@@ -798,6 +842,14 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
798
842
|
hint = "The server may be offering only SMBv1 which this plugin does not support. Enable SMB2 / SMB3 on the Samba server (min protocol = SMB2 in smb.conf).";
|
|
799
843
|
else if (m.includes("traversal") || m.includes("path"))
|
|
800
844
|
hint = "The Base path could not be validated. It must be a relative sub-directory (e.g. 'projects/2026'), never start with / or \\, and must not contain '..'.";
|
|
845
|
+
else if (m.includes("bad signature") || m.includes("sign_algo") || m.includes("signing"))
|
|
846
|
+
hint = "Signaturprüfung fehlgeschlagen. Setze in der Plugin-Config unter „SMB-Signing“ auf „if-offered“ oder prüfe auf dem Server, ob AES-CMAC unterstützt wird.";
|
|
847
|
+
else if (m.includes("pre-auth") || m.includes("preauth"))
|
|
848
|
+
hint = "Pre-Auth-Integrity fehlgeschlagen. Server und Client müssen SMB 3.1.1 sprechen. Auf Samba: 'server min protocol = SMB3_11' prüfen.";
|
|
849
|
+
else if (m.includes("encryption") || m.includes("encrypted"))
|
|
850
|
+
hint = "Verschlüsselungs-Verhandlung fehlgeschlagen. In der Plugin-Config „SMB-Verschlüsselung“ auf „if-offered“ setzen oder den Server so konfigurieren, dass er AES-GCM/CCM anbietet.";
|
|
851
|
+
else if (code === "ERR_OSSL_EVP_UNSUPPORTED" || m.includes("digital envelope routines"))
|
|
852
|
+
hint = "Node blockiert Legacy-Cipher. Ab v0.4.0 nutzt das Plugin smb3-client statt @marsaud/smb2 — diese Meldung sollte eigentlich nicht mehr auftreten. Bitte README-Abschnitt zu SMB3-Verbindung prüfen.";
|
|
801
853
|
|
|
802
854
|
return res.status(200).json({
|
|
803
855
|
ok: false,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saltcorn-samba",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Saltcorn plugin: browse, upload, rename and delete files on a Samba/CIFS share. File-manager view, directory tree, inline PDF viewer, external-app open (smb://).",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Saltcorn plugin: browse, upload, rename and delete files on a Samba/CIFS share via SMB 3.1.1 (AES-CMAC signing, optional encryption). File-manager view, directory tree, inline PDF viewer, external-app open (smb://).",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "node test/sanitize.test.js",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"url": "https://github.com/pv-host/saltcorn-samba/issues"
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|
|
34
|
-
"node": ">=
|
|
34
|
+
"node": ">=20"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"
|
|
37
|
+
"smb3-client": "^0.2.0"
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
40
|
"index.js",
|
package/smb-client.js
CHANGED
|
@@ -1,25 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SMB client wrapper
|
|
3
|
-
* around
|
|
2
|
+
* SMB client wrapper — provides a connection-scoped, `fs`-like interface
|
|
3
|
+
* around the `smb3-client` npm package (SMB 3.1.1 with AES-CMAC signing
|
|
4
|
+
* and optional AES-GCM encryption).
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* Design notes for v0.4.0:
|
|
7
|
+
*
|
|
8
|
+
* 1. Prior versions used `@marsaud/smb2` (SMB 2.0.2 / 2.1 only, tied to the
|
|
9
|
+
* unmaintained `ntlm` package that requires DES-ECB and dies on Node 17+
|
|
10
|
+
* with OpenSSL 3, and unable to satisfy `server signing = mandatory` on
|
|
11
|
+
* modern Samba servers because it lacks AES-CMAC signing).
|
|
12
|
+
*
|
|
13
|
+
* `smb3-client` speaks SMB 2.1 / 3.0 / 3.0.2 / 3.1.1, signs with either
|
|
14
|
+
* HMAC-SHA256 or AES-128-CMAC, does SHA-512 pre-auth integrity, has zero
|
|
15
|
+
* runtime dependencies, and no DES anywhere.
|
|
16
|
+
*
|
|
17
|
+
* 2. `smb3-client` is pure ESM. This plugin is CommonJS. We therefore load
|
|
18
|
+
* the module via a cached dynamic `import()` inside an async helper.
|
|
19
|
+
*
|
|
20
|
+
* 3. `smb3-client`'s path convention is "<share>/<sub>/<file>" — the share
|
|
21
|
+
* name is the FIRST segment of every path passed to `readFile`/`stat`/…
|
|
22
|
+
* We keep the plugin's external contract (callers still pass paths that
|
|
23
|
+
* are relative to the share root, e.g. "reports/2026/q1.xlsx"), and the
|
|
24
|
+
* wrapper prepends the share name and optional base_path.
|
|
25
|
+
*
|
|
26
|
+
* 4. All user-supplied path components MUST go through `sanitizeRelativePath`
|
|
27
|
+
* and `sanitizeFilename` before being handed to this wrapper. Those
|
|
28
|
+
* sanitizers are unchanged from previous versions (tests still cover them).
|
|
7
29
|
*/
|
|
8
30
|
|
|
9
31
|
const path = require("path");
|
|
10
32
|
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Dynamic ESM import cache for smb3-client
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
//
|
|
37
|
+
// The `smb3-client` package is pure ESM (`"type": "module"`), so we cannot
|
|
38
|
+
// use CommonJS `require()`. Node supports `import()` in CommonJS as a dynamic
|
|
39
|
+
// expression, which returns a Promise. We cache the resolved module.
|
|
40
|
+
let _smb3Module = null;
|
|
41
|
+
async function getSmb3Client() {
|
|
42
|
+
if (_smb3Module) return _smb3Module;
|
|
43
|
+
// Use string concatenation so bundlers do not try to statically resolve
|
|
44
|
+
// this at build time.
|
|
45
|
+
_smb3Module = await import("smb3-client");
|
|
46
|
+
return _smb3Module;
|
|
19
47
|
}
|
|
20
48
|
|
|
21
49
|
// ---------------------------------------------------------------------------
|
|
22
|
-
// Path helpers
|
|
50
|
+
// Path helpers (unchanged from previous versions — covered by unit tests)
|
|
23
51
|
// ---------------------------------------------------------------------------
|
|
24
52
|
|
|
25
53
|
/**
|
|
@@ -33,7 +61,6 @@ function sanitizeFilename(name) {
|
|
|
33
61
|
if (typeof name !== "string") throw new Error("Filename must be a string");
|
|
34
62
|
const trimmed = name.trim();
|
|
35
63
|
if (!trimmed) throw new Error("Filename must not be empty");
|
|
36
|
-
// reject leading/trailing whitespace on non-empty names (Windows problem)
|
|
37
64
|
if (name !== trimmed)
|
|
38
65
|
throw new Error("Filename must not start or end with whitespace");
|
|
39
66
|
if (trimmed.length > 255) throw new Error("Filename too long");
|
|
@@ -41,12 +68,10 @@ function sanitizeFilename(name) {
|
|
|
41
68
|
throw new Error("Filename must not be '.' or '..'");
|
|
42
69
|
if (/[\\/]/.test(trimmed)) throw new Error("Filename must not contain slashes");
|
|
43
70
|
if (/[\x00-\x1f]/.test(trimmed)) throw new Error("Filename must not contain control characters");
|
|
44
|
-
// Reject characters SMB / Windows disallow in filenames
|
|
45
71
|
if (/[<>:"|?*]/.test(trimmed))
|
|
46
72
|
throw new Error('Filename must not contain any of: < > : " | ? *');
|
|
47
73
|
if (trimmed.endsWith(".") || trimmed.endsWith(" "))
|
|
48
74
|
throw new Error("Filename must not end with a dot or space");
|
|
49
|
-
// Windows reserved device names (case-insensitive, with or without extension)
|
|
50
75
|
const base = trimmed.split(".")[0].toUpperCase();
|
|
51
76
|
const RESERVED = new Set([
|
|
52
77
|
"CON", "PRN", "AUX", "NUL",
|
|
@@ -67,15 +92,11 @@ function sanitizeRelativePath(rel) {
|
|
|
67
92
|
if (typeof rel !== "string") throw new Error("Path must be a string");
|
|
68
93
|
if (rel.length > 4096) throw new Error("Path too long");
|
|
69
94
|
if (rel.includes("\0")) throw new Error("Illegal NUL byte in path");
|
|
70
|
-
// Convert backslashes to forward slashes first (do NOT collapse yet)
|
|
71
95
|
let p = rel.replace(/\\/g, "/");
|
|
72
|
-
// Reject Windows drive letters and UNC paths BEFORE collapsing slashes
|
|
73
96
|
if (/^[a-zA-Z]:/.test(p)) throw new Error("Drive letters not allowed");
|
|
74
97
|
if (p.startsWith("//")) throw new Error("UNC paths not allowed");
|
|
75
|
-
// Now collapse multiple slashes and strip leading slash
|
|
76
98
|
p = p.replace(/\/+/g, "/");
|
|
77
99
|
if (p.startsWith("/")) p = p.slice(1);
|
|
78
|
-
// Reject explicit traversal segments
|
|
79
100
|
const parts = p.split("/").filter((s) => s !== "" && s !== ".");
|
|
80
101
|
for (const seg of parts) {
|
|
81
102
|
if (seg === "..") throw new Error("Path traversal not allowed");
|
|
@@ -83,167 +104,264 @@ function sanitizeRelativePath(rel) {
|
|
|
83
104
|
return parts.join("/");
|
|
84
105
|
}
|
|
85
106
|
|
|
86
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Convert a POSIX-style relative path into the SMB backslash form.
|
|
109
|
+
* Kept for backwards compatibility — smb3-client itself uses forward
|
|
110
|
+
* slashes, but external callers (URL builders, logging) may still use
|
|
111
|
+
* this helper.
|
|
112
|
+
*/
|
|
87
113
|
function toSmbPath(rel) {
|
|
88
114
|
return rel.replace(/\//g, "\\");
|
|
89
115
|
}
|
|
90
116
|
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Host / port helpers
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Split a possibly-composite server field into { host, port }.
|
|
123
|
+
* Tolerant to `host:445` typed into the server field, `[::1]:445` IPv6, and
|
|
124
|
+
* plain hostnames or bare IPs. Never returns a host string that contains a
|
|
125
|
+
* colon-port suffix (that would break DNS lookup).
|
|
126
|
+
*/
|
|
127
|
+
function parseHostPort(server, explicitPort) {
|
|
128
|
+
let host = String(server || "").trim();
|
|
129
|
+
let port;
|
|
130
|
+
|
|
131
|
+
const v6 = host.match(/^\[([^\]]+)\](?::(\d+))?$/);
|
|
132
|
+
if (v6) {
|
|
133
|
+
host = v6[1];
|
|
134
|
+
if (v6[2]) port = Number(v6[2]);
|
|
135
|
+
} else {
|
|
136
|
+
const colonCount = (host.match(/:/g) || []).length;
|
|
137
|
+
if (colonCount === 1) {
|
|
138
|
+
const [h, p] = host.split(":");
|
|
139
|
+
if (h && /^\d+$/.test(p)) {
|
|
140
|
+
host = h;
|
|
141
|
+
port = Number(p);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
host,
|
|
148
|
+
port: Number(explicitPort) || port || 445,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Config validation
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
/** Normalise the security-mode fields to smb3-client's vocabulary. */
|
|
157
|
+
function normSecurityMode(v, fallback) {
|
|
158
|
+
const s = String(v || "").toLowerCase().trim();
|
|
159
|
+
if (s === "disabled" || s === "if-offered" || s === "required") return s;
|
|
160
|
+
return fallback;
|
|
161
|
+
}
|
|
162
|
+
|
|
91
163
|
// ---------------------------------------------------------------------------
|
|
92
164
|
// SMB client factory
|
|
93
165
|
// ---------------------------------------------------------------------------
|
|
94
166
|
|
|
95
167
|
/**
|
|
96
|
-
* Build a fresh
|
|
168
|
+
* Build and connect a fresh smb3-client `Client` from the plugin
|
|
169
|
+
* configuration. Returns a small wrapper object with `fs`-like methods that
|
|
170
|
+
* accept paths RELATIVE TO THE SHARE ROOT (with `base_path` automatically
|
|
171
|
+
* prepended). Always call `disconnect()` when done — or use `withClient()`.
|
|
97
172
|
*
|
|
98
173
|
* config = {
|
|
99
|
-
* server: "192.168.1.10",
|
|
100
|
-
* share: "documents",
|
|
101
|
-
* domain: "WORKGROUP",
|
|
174
|
+
* server: "192.168.1.10", // host or IP
|
|
175
|
+
* share: "documents", // share name (no slashes)
|
|
176
|
+
* domain: "WORKGROUP", // optional
|
|
102
177
|
* username: "reader",
|
|
103
178
|
* password: "secret",
|
|
104
|
-
* base_path:"",
|
|
105
|
-
* port: 445,
|
|
179
|
+
* base_path:"", // optional subdirectory lock
|
|
180
|
+
* port: 445, // optional
|
|
181
|
+
* signing_mode: "required", // "disabled" | "if-offered" | "required"
|
|
182
|
+
* encryption_mode: "if-offered", // ditto
|
|
106
183
|
* }
|
|
107
|
-
*
|
|
108
|
-
* The returned object exposes: readdir(rel), stat(rel), readFile(rel),
|
|
109
|
-
* createReadStream(rel), disconnect(), plus helpers `resolve(rel)` and
|
|
110
|
-
* `basePath`.
|
|
111
184
|
*/
|
|
112
|
-
function buildClient(config) {
|
|
185
|
+
async function buildClient(config) {
|
|
113
186
|
if (!config) throw new Error("Samba plugin is not configured");
|
|
114
|
-
const { server, share, domain, username, password
|
|
187
|
+
const { server, share, domain, username, password } = config;
|
|
115
188
|
if (!server) throw new Error("Samba: server missing");
|
|
116
189
|
if (!share) throw new Error("Samba: share missing");
|
|
117
|
-
if (/[\\/]/.test(share))
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
let hostOnly = String(server).trim();
|
|
127
|
-
let portFromServer;
|
|
128
|
-
// IPv6-Adressen in eckigen Klammern zulassen: [::1]:445
|
|
129
|
-
const v6 = hostOnly.match(/^\[([^\]]+)\](?::(\d+))?$/);
|
|
130
|
-
if (v6) {
|
|
131
|
-
hostOnly = v6[1];
|
|
132
|
-
if (v6[2]) portFromServer = Number(v6[2]);
|
|
133
|
-
} else {
|
|
134
|
-
// Nur splitten, wenn genau EIN Doppelpunkt — sonst IPv6 ohne Klammern
|
|
135
|
-
const colonCount = (hostOnly.match(/:/g) || []).length;
|
|
136
|
-
if (colonCount === 1) {
|
|
137
|
-
const [h, p] = hostOnly.split(":");
|
|
138
|
-
if (h && /^\d+$/.test(p)) {
|
|
139
|
-
hostOnly = h;
|
|
140
|
-
portFromServer = Number(p);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
190
|
+
if (/[\\/]/.test(share))
|
|
191
|
+
throw new Error("Samba: share must not contain slashes");
|
|
192
|
+
|
|
193
|
+
const { host, port } = parseHostPort(server, config.port);
|
|
194
|
+
const signing = normSecurityMode(config.signing_mode, "if-offered");
|
|
195
|
+
const encryption = normSecurityMode(config.encryption_mode, "if-offered");
|
|
196
|
+
|
|
197
|
+
const basePath = sanitizeRelativePath(config.base_path || "");
|
|
198
|
+
const shareName = String(share).trim();
|
|
144
199
|
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
username: username || "guest",
|
|
200
|
+
const { Client } = await getSmb3Client();
|
|
201
|
+
const client = new Client({
|
|
202
|
+
host,
|
|
203
|
+
port,
|
|
204
|
+
domain: domain || "",
|
|
205
|
+
username: username || "",
|
|
152
206
|
password: password || "",
|
|
153
|
-
|
|
154
|
-
|
|
207
|
+
connectTimeout: 10000,
|
|
208
|
+
requestTimeout: 30000,
|
|
209
|
+
signing,
|
|
210
|
+
encryption,
|
|
155
211
|
});
|
|
156
212
|
|
|
157
|
-
|
|
213
|
+
await client.connect();
|
|
158
214
|
|
|
159
|
-
/**
|
|
160
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Combine share + basePath + user-supplied relative into the full
|
|
217
|
+
* smb3-client-style path ("share/sub/dir/file.ext"), always sanitising
|
|
218
|
+
* the user input first.
|
|
219
|
+
*/
|
|
220
|
+
function resolvePath(rel) {
|
|
161
221
|
const safe = sanitizeRelativePath(rel);
|
|
162
|
-
|
|
163
|
-
|
|
222
|
+
return [shareName, basePath, safe].filter(Boolean).join("/");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Convert a smb3-client Dirent + best-effort stat into the shape the
|
|
227
|
+
* rest of the plugin expects (`name`, `isDirectory` as function OR
|
|
228
|
+
* boolean, `size`, `mtime`, `birthtime`).
|
|
229
|
+
*
|
|
230
|
+
* We fetch stat data in parallel because smb3-client's readdir Dirent
|
|
231
|
+
* only carries name/isFile/isDirectory. For very large directories this
|
|
232
|
+
* would fan out into many stat calls — that's an acceptable trade-off
|
|
233
|
+
* for now; a future release can optimise by using SMB2_QUERY_DIRECTORY's
|
|
234
|
+
* FileBothDirectoryInformation output directly.
|
|
235
|
+
*/
|
|
236
|
+
async function enrichEntry(dirent, parentFullPath) {
|
|
237
|
+
const isDir = !!dirent.isDirectory();
|
|
238
|
+
const fullPath = parentFullPath
|
|
239
|
+
? parentFullPath + "/" + dirent.name
|
|
240
|
+
: dirent.name;
|
|
241
|
+
let size = 0;
|
|
242
|
+
let mtime;
|
|
243
|
+
let birthtime;
|
|
244
|
+
try {
|
|
245
|
+
const st = await client.stat(fullPath);
|
|
246
|
+
size = Number(st.size || 0);
|
|
247
|
+
mtime = st.mtime;
|
|
248
|
+
birthtime = st.ctime;
|
|
249
|
+
} catch (_) {
|
|
250
|
+
// Non-fatal; return whatever we already have.
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
name: dirent.name,
|
|
254
|
+
isDirectory: isDir,
|
|
255
|
+
size,
|
|
256
|
+
mtime,
|
|
257
|
+
birthtime,
|
|
258
|
+
};
|
|
164
259
|
}
|
|
165
260
|
|
|
166
261
|
return {
|
|
262
|
+
/** Share name, exposed for URL building / logging. */
|
|
263
|
+
shareName,
|
|
264
|
+
/** Sanitised base path (may be ""). */
|
|
167
265
|
basePath,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
266
|
+
/** Underlying smb3-client Client — do not use unless you know why. */
|
|
267
|
+
_raw: client,
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* List a directory (share root by default). Returns an array of objects
|
|
271
|
+
* shaped like the legacy `@marsaud/smb2` output so index.js does not need
|
|
272
|
+
* to change: `{ name, isDirectory, size, mtime, birthtime }`.
|
|
273
|
+
*/
|
|
274
|
+
async readdir(rel) {
|
|
275
|
+
const full = resolvePath(rel);
|
|
276
|
+
const dirents = await client.readdir(full, { withFileTypes: true });
|
|
277
|
+
// Parallel enrichment. Bounded to a reasonable concurrency to avoid
|
|
278
|
+
// saturating the SMB session on huge directories.
|
|
279
|
+
const CHUNK = 16;
|
|
280
|
+
const result = [];
|
|
281
|
+
for (let i = 0; i < dirents.length; i += CHUNK) {
|
|
282
|
+
const slice = dirents.slice(i, i + CHUNK);
|
|
283
|
+
const enriched = await Promise.all(
|
|
284
|
+
slice.map((d) => enrichEntry(d, full))
|
|
173
285
|
);
|
|
174
|
-
|
|
286
|
+
result.push(...enriched);
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
175
289
|
},
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
res(match);
|
|
191
|
-
});
|
|
192
|
-
});
|
|
290
|
+
|
|
291
|
+
/** Stat a single file/directory (name-relative-to-share/basePath). */
|
|
292
|
+
async stat(rel) {
|
|
293
|
+
const full = resolvePath(rel);
|
|
294
|
+
const st = await client.stat(full);
|
|
295
|
+
// Return a shape compatible with the old readdir-emulated stat.
|
|
296
|
+
return {
|
|
297
|
+
name: full.split("/").pop(),
|
|
298
|
+
size: Number(st.size || 0),
|
|
299
|
+
mtime: st.mtime,
|
|
300
|
+
birthtime: st.ctime,
|
|
301
|
+
isDirectory: !!st.isDirectory,
|
|
302
|
+
isFile: !!st.isFile,
|
|
303
|
+
};
|
|
193
304
|
},
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
);
|
|
199
|
-
});
|
|
305
|
+
|
|
306
|
+
/** Read the full content of a file into a Buffer. */
|
|
307
|
+
async readFile(rel) {
|
|
308
|
+
return await client.readFile(resolvePath(rel));
|
|
200
309
|
},
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
);
|
|
206
|
-
});
|
|
310
|
+
|
|
311
|
+
/** Create or overwrite a file with the given Buffer / string. */
|
|
312
|
+
async writeFile(rel, data) {
|
|
313
|
+
return await client.writeFile(resolvePath(rel), data);
|
|
207
314
|
},
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
315
|
+
|
|
316
|
+
/** Check whether a path exists (never throws). */
|
|
317
|
+
async exists(rel) {
|
|
318
|
+
try {
|
|
319
|
+
await client.stat(resolvePath(rel));
|
|
320
|
+
return true;
|
|
321
|
+
} catch (_) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
212
324
|
},
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
325
|
+
|
|
326
|
+
/** Delete a single file. */
|
|
327
|
+
async unlink(rel) {
|
|
328
|
+
return await client.rm(resolvePath(rel));
|
|
217
329
|
},
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
330
|
+
|
|
331
|
+
/** Remove an empty directory. */
|
|
332
|
+
async rmdir(rel) {
|
|
333
|
+
return await client.rmdir(resolvePath(rel));
|
|
222
334
|
},
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
335
|
+
|
|
336
|
+
/** Create a directory (non-recursive). */
|
|
337
|
+
async mkdir(rel) {
|
|
338
|
+
return await client.mkdir(resolvePath(rel));
|
|
227
339
|
},
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
);
|
|
233
|
-
});
|
|
340
|
+
|
|
341
|
+
/** Rename (or move within the same share). */
|
|
342
|
+
async rename(oldRel, newRel) {
|
|
343
|
+
return await client.rename(resolvePath(oldRel), resolvePath(newRel));
|
|
234
344
|
},
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Streaming read — returns a Node Readable. Note: unlike the callback-
|
|
348
|
+
* based old API this is synchronous (smb3-client's own signature).
|
|
349
|
+
*/
|
|
235
350
|
createReadStream(rel) {
|
|
236
|
-
return
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
351
|
+
return client.createReadStream(resolvePath(rel));
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
/** Streaming write. */
|
|
355
|
+
createWriteStream(rel) {
|
|
356
|
+
return client.createWriteStream(resolvePath(rel));
|
|
241
357
|
},
|
|
242
|
-
|
|
358
|
+
|
|
359
|
+
/** Tear down the SMB session and TCP socket. Always await this. */
|
|
360
|
+
async disconnect() {
|
|
243
361
|
try {
|
|
244
|
-
|
|
362
|
+
await client.close();
|
|
245
363
|
} catch (_) {
|
|
246
|
-
// ignore
|
|
364
|
+
// ignore — best-effort teardown
|
|
247
365
|
}
|
|
248
366
|
},
|
|
249
367
|
};
|
|
@@ -252,14 +370,15 @@ function buildClient(config) {
|
|
|
252
370
|
/**
|
|
253
371
|
* Execute a callback with a fresh SMB client and always disconnect afterwards.
|
|
254
372
|
* Prefer this helper over building a long-lived client because SMB sessions
|
|
255
|
-
*
|
|
373
|
+
* time out on idle and smb3-client does not currently expose a reconnect
|
|
374
|
+
* primitive.
|
|
256
375
|
*/
|
|
257
376
|
async function withClient(config, fn) {
|
|
258
|
-
const client = buildClient(config);
|
|
377
|
+
const client = await buildClient(config);
|
|
259
378
|
try {
|
|
260
379
|
return await fn(client);
|
|
261
380
|
} finally {
|
|
262
|
-
client.disconnect();
|
|
381
|
+
await client.disconnect();
|
|
263
382
|
}
|
|
264
383
|
}
|
|
265
384
|
|