saltcorn-samba 0.3.11 → 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 +94 -0
- package/README.md +59 -62
- package/index.js +59 -38
- package/package.json +4 -4
- package/smb-client.js +257 -201
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,100 @@ 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
|
+
|
|
7
101
|
## [0.3.11] – 2026-07-05
|
|
8
102
|
|
|
9
103
|
### Fixed – **Worker-Crash / 502 durch DES-ECB auf Node 17+ / OpenSSL 3**
|
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).
|
|
@@ -333,89 +352,67 @@ Ab dann taucht das Plugin im Plugins-Store jeder Saltcorn-Instanz auf.
|
|
|
333
352
|
|
|
334
353
|
## Troubleshooting
|
|
335
354
|
|
|
336
|
-
### `
|
|
337
|
-
|
|
338
|
-
Die Bibliothek `@marsaud/smb2` benutzt intern (im transitiven Paket `ntlm`)
|
|
339
|
-
den Cipher **DES-ECB**, um die NTLM/LM-Hashes zu berechnen. Ab **Node.js 17**
|
|
340
|
-
mit **OpenSSL 3** ist DES-ECB standardmäßig deaktiviert. Der Aufruf wirft
|
|
341
|
-
dann synchron:
|
|
355
|
+
### `ERR_MODULE_NOT_FOUND` / „Cannot find package 'smb3-client'" / Worker startet nicht
|
|
342
356
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
code: 'ERR_OSSL_EVP_UNSUPPORTED'
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
Weil der Fehler synchron aus dem NTLM-Modul kommt, **tötet er den
|
|
349
|
-
Saltcorn-Worker-Prozess**. Der davor sitzende Reverse-Proxy meldet dann
|
|
350
|
-
`502 Bad Gateway` und die Antwort ist kein JSON — was auf der Config-Seite
|
|
351
|
-
als "Antwort war kein JSON (HTTP 502)" erscheint.
|
|
352
|
-
|
|
353
|
-
**Lösung:** Saltcorn mit dem Node-Flag `--openssl-legacy-provider` starten.
|
|
354
|
-
Je nach Setup:
|
|
355
|
-
|
|
356
|
-
**(a) Umgebungsvariable (empfohlen, wirkt auf jeden Node-Aufruf)**
|
|
357
|
+
Das Plugin lädt `smb3-client` (ESM) via dynamic `import()` — das npm-Paket
|
|
358
|
+
muss also im Plugin-Ordner installiert sein.
|
|
357
359
|
|
|
358
360
|
```bash
|
|
359
|
-
|
|
360
|
-
|
|
361
|
+
cd /pfad/zu/saltcorn-samba
|
|
362
|
+
npm install
|
|
363
|
+
saltcorn restart # oder systemd/docker-Neustart
|
|
361
364
|
```
|
|
362
365
|
|
|
363
|
-
|
|
366
|
+
Bei Installation über die Saltcorn-Plugin-UI (Source `npm` oder `github`)
|
|
367
|
+
führt Saltcorn `npm install` automatisch aus.
|
|
364
368
|
|
|
365
|
-
|
|
366
|
-
[Service]
|
|
367
|
-
Environment=NODE_OPTIONS=--openssl-legacy-provider
|
|
368
|
-
ExecStart=/usr/bin/saltcorn serve
|
|
369
|
-
```
|
|
369
|
+
### `STATUS_INVALID_PARAMETER` / „sign_algo_id=0" im Samba-Log
|
|
370
370
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
```
|
|
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:
|
|
376
375
|
|
|
377
|
-
|
|
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.
|
|
378
380
|
|
|
379
|
-
|
|
380
|
-
services:
|
|
381
|
-
saltcorn:
|
|
382
|
-
image: saltcorn/saltcorn:latest
|
|
383
|
-
environment:
|
|
384
|
-
NODE_OPTIONS: "--openssl-legacy-provider"
|
|
385
|
-
```
|
|
381
|
+
### `ERR_OSSL_EVP_UNSUPPORTED` / `--openssl-legacy-provider`
|
|
386
382
|
|
|
387
|
-
**
|
|
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.**
|
|
388
389
|
|
|
389
|
-
|
|
390
|
-
pm2 delete saltcorn
|
|
391
|
-
NODE_OPTIONS="--openssl-legacy-provider" pm2 start saltcorn -- serve
|
|
392
|
-
pm2 save
|
|
393
|
-
```
|
|
390
|
+
### Signing- und Verschlüsselungs-Modi
|
|
394
391
|
|
|
395
|
-
|
|
392
|
+
Beide Modi haben denselben Wertebereich:
|
|
396
393
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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. |
|
|
400
399
|
|
|
401
|
-
|
|
402
|
-
klicken — die Fehlermeldung sollte weg sein.
|
|
400
|
+
Typische Fehlermeldungen und was zu tun ist:
|
|
403
401
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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 |
|
|
409
407
|
|
|
410
408
|
### Weitere häufige Fehler
|
|
411
409
|
|
|
412
410
|
| Symptom | Ursache | Lösung |
|
|
413
411
|
|---|---|---|
|
|
414
|
-
| `getaddrinfo ENOTFOUND host:445` | Bis v0.3.9 wurde der Port in den UNC-Pfad geschrieben | Update auf **v0.3.10+** |
|
|
415
412
|
| `ECONNREFUSED :445` | SMB-Dienst läuft nicht oder Firewall blockt Port 445 | `nc -vz <server> 445` vom Saltcorn-Host testen |
|
|
416
413
|
| `ETIMEDOUT` | Kein Netzwerkpfad (VLAN/Docker-Bridge/VPN) | Vom Saltcorn-Container aus `ping` + `nc -vz` prüfen |
|
|
417
|
-
| `STATUS_LOGON_FAILURE` | User/Passwort/Domain falsch | Zugangsdaten prüfen. Moderne Samba-Server erlauben keine leeren Passwörter |
|
|
418
|
-
| `STATUS_BAD_NETWORK_NAME` | Share existiert nicht oder Schreibweise falsch | `smbclient -L //server -U user` zur Kontrolle |
|
|
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 |
|
|
419
416
|
| Nur SMBv1 verfügbar | Server bietet SMB2/3 nicht an | In `smb.conf`: `min protocol = SMB2` |
|
|
420
417
|
|
|
421
418
|
---
|
package/index.js
CHANGED
|
@@ -47,8 +47,6 @@ const {
|
|
|
47
47
|
mimeFromName,
|
|
48
48
|
sanitizeRelativePath,
|
|
49
49
|
sanitizeFilename,
|
|
50
|
-
checkLegacyCryptoAvailable,
|
|
51
|
-
legacyCryptoErrorMessage,
|
|
52
50
|
} = require("./smb-client");
|
|
53
51
|
const treeView = require("./tree-view");
|
|
54
52
|
const fileManagerView = require("./filemanager-view");
|
|
@@ -94,13 +92,15 @@ window.sambaTestConn = async function(btn) {
|
|
|
94
92
|
if (!form) { out.innerHTML = '<div class="alert alert-danger">Formular nicht gefunden.</div>'; return; }
|
|
95
93
|
function v(n){ var el = form.querySelector('[name="'+n+'"]'); return el ? el.value : ''; }
|
|
96
94
|
var payload = {
|
|
97
|
-
server:
|
|
98
|
-
share:
|
|
99
|
-
domain:
|
|
100
|
-
username:
|
|
101
|
-
password:
|
|
102
|
-
base_path:
|
|
103
|
-
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')
|
|
104
104
|
};
|
|
105
105
|
if (!payload.server || !payload.share) {
|
|
106
106
|
out.innerHTML = '<div class="alert alert-warning">Bitte mindestens <b>Server</b> und <b>Share</b> ausfüllen.</div>';
|
|
@@ -289,6 +289,46 @@ const configuration_workflow = () =>
|
|
|
289
289
|
type: "Integer",
|
|
290
290
|
default: 445,
|
|
291
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
|
+
}),
|
|
292
332
|
new Field({
|
|
293
333
|
name: "_test_html",
|
|
294
334
|
label: " ",
|
|
@@ -744,38 +784,13 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
744
784
|
password: String(body.password || ""),
|
|
745
785
|
base_path: String(body.base_path || "").trim(),
|
|
746
786
|
port: Number(body.port) || 445,
|
|
787
|
+
signing_mode: String(body.signing_mode || "").trim() || undefined,
|
|
788
|
+
encryption_mode: String(body.encryption_mode || "").trim() || undefined,
|
|
747
789
|
};
|
|
748
790
|
|
|
749
791
|
if (!testCfg.server) return jsonError(res, 400, "Please enter a Server (hostname or IP).");
|
|
750
792
|
if (!testCfg.share) return jsonError(res, 400, "Please enter a Share name.");
|
|
751
793
|
|
|
752
|
-
// Vor jedem echten SMB-Aufruf: Node/OpenSSL-Legacy-Cipher-Check.
|
|
753
|
-
// Falls DES-ECB nicht verfügbar ist (Node 17+ / OpenSSL 3 ohne
|
|
754
|
-
// --openssl-legacy-provider), übergeben wir gar nicht erst an ntlm,
|
|
755
|
-
// sonst crasht der Worker synchron und der Proxy meldet 502.
|
|
756
|
-
const cc = checkLegacyCryptoAvailable();
|
|
757
|
-
if (!cc.ok) {
|
|
758
|
-
return res.status(200).json({
|
|
759
|
-
ok: false,
|
|
760
|
-
error: legacyCryptoErrorMessage(cc.message),
|
|
761
|
-
code: "E_LEGACY_CRYPTO",
|
|
762
|
-
hint:
|
|
763
|
-
"Saltcorn mit --openssl-legacy-provider starten. " +
|
|
764
|
-
"Am einfachsten: die Umgebungsvariable NODE_OPTIONS=--openssl-legacy-provider setzen und Saltcorn neu starten. " +
|
|
765
|
-
"Details und Beispiele für systemd / Docker / PM2 stehen in der README des Plugins.",
|
|
766
|
-
attempted: {
|
|
767
|
-
server: testCfg.server,
|
|
768
|
-
share: testCfg.share,
|
|
769
|
-
port: testCfg.port,
|
|
770
|
-
domain: testCfg.domain,
|
|
771
|
-
username: testCfg.username || "(anonymous)",
|
|
772
|
-
base_path: testCfg.base_path || "(share root)",
|
|
773
|
-
},
|
|
774
|
-
node_version: process.version,
|
|
775
|
-
openssl_version: (process.versions && process.versions.openssl) || null,
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
|
|
779
794
|
const started = Date.now();
|
|
780
795
|
try {
|
|
781
796
|
const listing = await withClient(testCfg, async (client) => {
|
|
@@ -827,8 +842,14 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
827
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).";
|
|
828
843
|
else if (m.includes("traversal") || m.includes("path"))
|
|
829
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 '..'.";
|
|
830
|
-
else if (
|
|
831
|
-
hint = "
|
|
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.";
|
|
832
853
|
|
|
833
854
|
return res.status(200).json({
|
|
834
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,77 +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
|
-
const crypto = require("crypto");
|
|
11
|
-
|
|
12
|
-
// @marsaud/smb2 is loaded lazily so the pure sanitizer helpers exported by
|
|
13
|
-
// this module can be required (e.g. from unit tests) without needing the
|
|
14
|
-
// native SMB dependency installed.
|
|
15
|
-
let _SMB2 = null;
|
|
16
|
-
function getSMB2() {
|
|
17
|
-
if (_SMB2) return _SMB2;
|
|
18
|
-
_SMB2 = require("@marsaud/smb2");
|
|
19
|
-
return _SMB2;
|
|
20
|
-
}
|
|
21
32
|
|
|
22
33
|
// ---------------------------------------------------------------------------
|
|
23
|
-
//
|
|
34
|
+
// Dynamic ESM import cache for smb3-client
|
|
24
35
|
// ---------------------------------------------------------------------------
|
|
25
36
|
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (_cryptoOK !== null) return _cryptoOK;
|
|
37
|
-
try {
|
|
38
|
-
// DES-ECB genau so aufrufen, wie es ntlm intern tut.
|
|
39
|
-
// Wenn OpenSSL 3 ohne Legacy-Provider läuft, wirft das hier.
|
|
40
|
-
const key = Buffer.alloc(8, 0);
|
|
41
|
-
const c = crypto.createCipheriv("des-ecb", key, null);
|
|
42
|
-
c.setAutoPadding(false);
|
|
43
|
-
c.update(Buffer.alloc(8, 0));
|
|
44
|
-
c.final();
|
|
45
|
-
_cryptoOK = { ok: true };
|
|
46
|
-
} catch (e) {
|
|
47
|
-
_cryptoOK = {
|
|
48
|
-
ok: false,
|
|
49
|
-
code: e && e.code,
|
|
50
|
-
message: (e && e.message) || String(e),
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
return _cryptoOK;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Baut eine deutsche Erklärung für den Legacy-Provider-Fehler. */
|
|
57
|
-
function legacyCryptoErrorMessage(detail) {
|
|
58
|
-
return (
|
|
59
|
-
"NTLM-Authentifizierung nicht möglich: Node.js blockiert die von " +
|
|
60
|
-
"@marsaud/smb2 benötigten Legacy-Cipher (DES-ECB). Das ist ab Node 17 " +
|
|
61
|
-
"mit OpenSSL 3 der Standard. " +
|
|
62
|
-
"Lösung: Saltcorn mit dem Flag --openssl-legacy-provider starten. " +
|
|
63
|
-
"Beispiele: " +
|
|
64
|
-
"(1) direkt: `node --openssl-legacy-provider node_modules/.bin/saltcorn serve` " +
|
|
65
|
-
"(2) per Umgebungsvariable: `NODE_OPTIONS=--openssl-legacy-provider saltcorn serve` " +
|
|
66
|
-
"(3) systemd-Unit: `Environment=NODE_OPTIONS=--openssl-legacy-provider` " +
|
|
67
|
-
"(4) Docker: im Compose-File `environment: NODE_OPTIONS: --openssl-legacy-provider`. " +
|
|
68
|
-
"Danach Saltcorn neu starten. Details siehe README des Plugins. " +
|
|
69
|
-
(detail ? "[Original: " + detail + "]" : "")
|
|
70
|
-
);
|
|
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;
|
|
71
47
|
}
|
|
72
48
|
|
|
73
49
|
// ---------------------------------------------------------------------------
|
|
74
|
-
// Path helpers
|
|
50
|
+
// Path helpers (unchanged from previous versions — covered by unit tests)
|
|
75
51
|
// ---------------------------------------------------------------------------
|
|
76
52
|
|
|
77
53
|
/**
|
|
@@ -85,7 +61,6 @@ function sanitizeFilename(name) {
|
|
|
85
61
|
if (typeof name !== "string") throw new Error("Filename must be a string");
|
|
86
62
|
const trimmed = name.trim();
|
|
87
63
|
if (!trimmed) throw new Error("Filename must not be empty");
|
|
88
|
-
// reject leading/trailing whitespace on non-empty names (Windows problem)
|
|
89
64
|
if (name !== trimmed)
|
|
90
65
|
throw new Error("Filename must not start or end with whitespace");
|
|
91
66
|
if (trimmed.length > 255) throw new Error("Filename too long");
|
|
@@ -93,12 +68,10 @@ function sanitizeFilename(name) {
|
|
|
93
68
|
throw new Error("Filename must not be '.' or '..'");
|
|
94
69
|
if (/[\\/]/.test(trimmed)) throw new Error("Filename must not contain slashes");
|
|
95
70
|
if (/[\x00-\x1f]/.test(trimmed)) throw new Error("Filename must not contain control characters");
|
|
96
|
-
// Reject characters SMB / Windows disallow in filenames
|
|
97
71
|
if (/[<>:"|?*]/.test(trimmed))
|
|
98
72
|
throw new Error('Filename must not contain any of: < > : " | ? *');
|
|
99
73
|
if (trimmed.endsWith(".") || trimmed.endsWith(" "))
|
|
100
74
|
throw new Error("Filename must not end with a dot or space");
|
|
101
|
-
// Windows reserved device names (case-insensitive, with or without extension)
|
|
102
75
|
const base = trimmed.split(".")[0].toUpperCase();
|
|
103
76
|
const RESERVED = new Set([
|
|
104
77
|
"CON", "PRN", "AUX", "NUL",
|
|
@@ -119,15 +92,11 @@ function sanitizeRelativePath(rel) {
|
|
|
119
92
|
if (typeof rel !== "string") throw new Error("Path must be a string");
|
|
120
93
|
if (rel.length > 4096) throw new Error("Path too long");
|
|
121
94
|
if (rel.includes("\0")) throw new Error("Illegal NUL byte in path");
|
|
122
|
-
// Convert backslashes to forward slashes first (do NOT collapse yet)
|
|
123
95
|
let p = rel.replace(/\\/g, "/");
|
|
124
|
-
// Reject Windows drive letters and UNC paths BEFORE collapsing slashes
|
|
125
96
|
if (/^[a-zA-Z]:/.test(p)) throw new Error("Drive letters not allowed");
|
|
126
97
|
if (p.startsWith("//")) throw new Error("UNC paths not allowed");
|
|
127
|
-
// Now collapse multiple slashes and strip leading slash
|
|
128
98
|
p = p.replace(/\/+/g, "/");
|
|
129
99
|
if (p.startsWith("/")) p = p.slice(1);
|
|
130
|
-
// Reject explicit traversal segments
|
|
131
100
|
const parts = p.split("/").filter((s) => s !== "" && s !== ".");
|
|
132
101
|
for (const seg of parts) {
|
|
133
102
|
if (seg === "..") throw new Error("Path traversal not allowed");
|
|
@@ -135,176 +104,264 @@ function sanitizeRelativePath(rel) {
|
|
|
135
104
|
return parts.join("/");
|
|
136
105
|
}
|
|
137
106
|
|
|
138
|
-
/**
|
|
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
|
+
*/
|
|
139
113
|
function toSmbPath(rel) {
|
|
140
114
|
return rel.replace(/\//g, "\\");
|
|
141
115
|
}
|
|
142
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
|
+
|
|
143
163
|
// ---------------------------------------------------------------------------
|
|
144
164
|
// SMB client factory
|
|
145
165
|
// ---------------------------------------------------------------------------
|
|
146
166
|
|
|
147
167
|
/**
|
|
148
|
-
* 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()`.
|
|
149
172
|
*
|
|
150
173
|
* config = {
|
|
151
|
-
* server: "192.168.1.10",
|
|
152
|
-
* share: "documents",
|
|
153
|
-
* domain: "WORKGROUP",
|
|
174
|
+
* server: "192.168.1.10", // host or IP
|
|
175
|
+
* share: "documents", // share name (no slashes)
|
|
176
|
+
* domain: "WORKGROUP", // optional
|
|
154
177
|
* username: "reader",
|
|
155
178
|
* password: "secret",
|
|
156
|
-
* base_path:"",
|
|
157
|
-
* 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
|
|
158
183
|
* }
|
|
159
|
-
*
|
|
160
|
-
* The returned object exposes: readdir(rel), stat(rel), readFile(rel),
|
|
161
|
-
* createReadStream(rel), disconnect(), plus helpers `resolve(rel)` and
|
|
162
|
-
* `basePath`.
|
|
163
184
|
*/
|
|
164
|
-
function buildClient(config) {
|
|
185
|
+
async function buildClient(config) {
|
|
165
186
|
if (!config) throw new Error("Samba plugin is not configured");
|
|
166
|
-
const { server, share, domain, username, password
|
|
187
|
+
const { server, share, domain, username, password } = config;
|
|
167
188
|
if (!server) throw new Error("Samba: server missing");
|
|
168
189
|
if (!share) throw new Error("Samba: share missing");
|
|
169
|
-
if (/[\\/]/.test(share))
|
|
170
|
-
|
|
171
|
-
// WICHTIG: OpenSSL-Legacy-Check VOR jedem SMB2-Aufruf. Sonst crasht
|
|
172
|
-
// ntlm/smbhash.js synchron und tötet den Worker (→ 502 am Proxy).
|
|
173
|
-
const cc = checkLegacyCryptoAvailable();
|
|
174
|
-
if (!cc.ok) {
|
|
175
|
-
const err = new Error(legacyCryptoErrorMessage(cc.message));
|
|
176
|
-
err.code = "E_LEGACY_CRYPTO";
|
|
177
|
-
throw err;
|
|
178
|
-
}
|
|
190
|
+
if (/[\\/]/.test(share))
|
|
191
|
+
throw new Error("Samba: share must not contain slashes");
|
|
179
192
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// die separate `port`-Option entgegen; der share-String enthält nur den Host.
|
|
184
|
-
//
|
|
185
|
-
// Zusätzlich tolerant sein, falls jemand versehentlich "host:445" ins
|
|
186
|
-
// Server-Feld getippt hat — dann Port dort rausziehen.
|
|
187
|
-
let hostOnly = String(server).trim();
|
|
188
|
-
let portFromServer;
|
|
189
|
-
// IPv6-Adressen in eckigen Klammern zulassen: [::1]:445
|
|
190
|
-
const v6 = hostOnly.match(/^\[([^\]]+)\](?::(\d+))?$/);
|
|
191
|
-
if (v6) {
|
|
192
|
-
hostOnly = v6[1];
|
|
193
|
-
if (v6[2]) portFromServer = Number(v6[2]);
|
|
194
|
-
} else {
|
|
195
|
-
// Nur splitten, wenn genau EIN Doppelpunkt — sonst IPv6 ohne Klammern
|
|
196
|
-
const colonCount = (hostOnly.match(/:/g) || []).length;
|
|
197
|
-
if (colonCount === 1) {
|
|
198
|
-
const [h, p] = hostOnly.split(":");
|
|
199
|
-
if (h && /^\d+$/.test(p)) {
|
|
200
|
-
hostOnly = h;
|
|
201
|
-
portFromServer = Number(p);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
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");
|
|
205
196
|
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
197
|
+
const basePath = sanitizeRelativePath(config.base_path || "");
|
|
198
|
+
const shareName = String(share).trim();
|
|
199
|
+
|
|
200
|
+
const { Client } = await getSmb3Client();
|
|
201
|
+
const client = new Client({
|
|
202
|
+
host,
|
|
203
|
+
port,
|
|
204
|
+
domain: domain || "",
|
|
205
|
+
username: username || "",
|
|
213
206
|
password: password || "",
|
|
214
|
-
|
|
215
|
-
|
|
207
|
+
connectTimeout: 10000,
|
|
208
|
+
requestTimeout: 30000,
|
|
209
|
+
signing,
|
|
210
|
+
encryption,
|
|
216
211
|
});
|
|
217
212
|
|
|
218
|
-
|
|
213
|
+
await client.connect();
|
|
219
214
|
|
|
220
|
-
/**
|
|
221
|
-
|
|
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) {
|
|
222
221
|
const safe = sanitizeRelativePath(rel);
|
|
223
|
-
|
|
224
|
-
|
|
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
|
+
};
|
|
225
259
|
}
|
|
226
260
|
|
|
227
261
|
return {
|
|
262
|
+
/** Share name, exposed for URL building / logging. */
|
|
263
|
+
shareName,
|
|
264
|
+
/** Sanitised base path (may be ""). */
|
|
228
265
|
basePath,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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))
|
|
234
285
|
);
|
|
235
|
-
|
|
286
|
+
result.push(...enriched);
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
236
289
|
},
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
res(match);
|
|
252
|
-
});
|
|
253
|
-
});
|
|
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
|
+
};
|
|
254
304
|
},
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
);
|
|
260
|
-
});
|
|
305
|
+
|
|
306
|
+
/** Read the full content of a file into a Buffer. */
|
|
307
|
+
async readFile(rel) {
|
|
308
|
+
return await client.readFile(resolvePath(rel));
|
|
261
309
|
},
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
);
|
|
267
|
-
});
|
|
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);
|
|
268
314
|
},
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
}
|
|
273
324
|
},
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
325
|
+
|
|
326
|
+
/** Delete a single file. */
|
|
327
|
+
async unlink(rel) {
|
|
328
|
+
return await client.rm(resolvePath(rel));
|
|
278
329
|
},
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
330
|
+
|
|
331
|
+
/** Remove an empty directory. */
|
|
332
|
+
async rmdir(rel) {
|
|
333
|
+
return await client.rmdir(resolvePath(rel));
|
|
283
334
|
},
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
335
|
+
|
|
336
|
+
/** Create a directory (non-recursive). */
|
|
337
|
+
async mkdir(rel) {
|
|
338
|
+
return await client.mkdir(resolvePath(rel));
|
|
288
339
|
},
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
);
|
|
294
|
-
});
|
|
340
|
+
|
|
341
|
+
/** Rename (or move within the same share). */
|
|
342
|
+
async rename(oldRel, newRel) {
|
|
343
|
+
return await client.rename(resolvePath(oldRel), resolvePath(newRel));
|
|
295
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
|
+
*/
|
|
296
350
|
createReadStream(rel) {
|
|
297
|
-
return
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
351
|
+
return client.createReadStream(resolvePath(rel));
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
/** Streaming write. */
|
|
355
|
+
createWriteStream(rel) {
|
|
356
|
+
return client.createWriteStream(resolvePath(rel));
|
|
302
357
|
},
|
|
303
|
-
|
|
358
|
+
|
|
359
|
+
/** Tear down the SMB session and TCP socket. Always await this. */
|
|
360
|
+
async disconnect() {
|
|
304
361
|
try {
|
|
305
|
-
|
|
362
|
+
await client.close();
|
|
306
363
|
} catch (_) {
|
|
307
|
-
// ignore
|
|
364
|
+
// ignore — best-effort teardown
|
|
308
365
|
}
|
|
309
366
|
},
|
|
310
367
|
};
|
|
@@ -313,14 +370,15 @@ function buildClient(config) {
|
|
|
313
370
|
/**
|
|
314
371
|
* Execute a callback with a fresh SMB client and always disconnect afterwards.
|
|
315
372
|
* Prefer this helper over building a long-lived client because SMB sessions
|
|
316
|
-
*
|
|
373
|
+
* time out on idle and smb3-client does not currently expose a reconnect
|
|
374
|
+
* primitive.
|
|
317
375
|
*/
|
|
318
376
|
async function withClient(config, fn) {
|
|
319
|
-
const client = buildClient(config);
|
|
377
|
+
const client = await buildClient(config);
|
|
320
378
|
try {
|
|
321
379
|
return await fn(client);
|
|
322
380
|
} finally {
|
|
323
|
-
client.disconnect();
|
|
381
|
+
await client.disconnect();
|
|
324
382
|
}
|
|
325
383
|
}
|
|
326
384
|
|
|
@@ -375,6 +433,4 @@ module.exports = {
|
|
|
375
433
|
toSmbPath,
|
|
376
434
|
toSmbUrl,
|
|
377
435
|
mimeFromName,
|
|
378
|
-
checkLegacyCryptoAvailable,
|
|
379
|
-
legacyCryptoErrorMessage,
|
|
380
436
|
};
|