saltcorn-samba 0.4.13 → 0.4.15
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 +95 -0
- package/README.md +11 -5
- package/filemanager-view.js +41 -7
- package/index.js +20 -27
- package/package.json +1 -1
- package/readdir-compat.js +29 -16
- package/smb-client.js +22 -31
- package/tools/diag-basepath.js +4 -1
- package/tools/diag-wire.js +3 -0
- package/tree-view.js +39 -9
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,101 @@ 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.15] – 2026-07-07
|
|
8
|
+
|
|
9
|
+
### Fixed – CSRF-Fehlermeldung bei Upload / Ordner anlegen / Rename / Delete
|
|
10
|
+
|
|
11
|
+
Bei jedem schreibenden Klick in der File-Manager-View („Ordner anlegen“,
|
|
12
|
+
„Datei hochladen“, „umbenennen“, „löschen“) erschien der Fehler
|
|
13
|
+
`Invalid CSRF token`, obwohl der Client den Token korrekt im Header
|
|
14
|
+
`X-CSRF-Token` mitgeschickt hat.
|
|
15
|
+
|
|
16
|
+
**Root Cause:** Saltcorns globaler `csurf`-Middleware validiert bereits
|
|
17
|
+
alle POST-Routen der Plugins gegen das Session-Secret. Unser Plugin hatte
|
|
18
|
+
zusätzlich eine manuelle `checkCsrf(req)`-Funktion, die den vom Client
|
|
19
|
+
geschickten Token mit `req.csrfToken()` verglich. `req.csrfToken()` gibt
|
|
20
|
+
aber bei jedem Aufruf einen **frisch gesalzenen** Token zurück – der
|
|
21
|
+
Stringvergleich `provided !== req.csrfToken()` schlug daher immer fehl,
|
|
22
|
+
auch bei völlig gültigen Requests.
|
|
23
|
+
|
|
24
|
+
**Fix:** `checkCsrf` ist jetzt ein dokumentierter No-Op. Die eigentliche
|
|
25
|
+
CSRF-Prüfung übernimmt weiterhin Saltcorns Middleware (kein Sicherheits-
|
|
26
|
+
abbau). Betroffene Write-Routen (Upload, Mkdir, Rename, Delete) in
|
|
27
|
+
`index.js` sind unverändert und funktionieren jetzt korrekt.
|
|
28
|
+
|
|
29
|
+
### Added – View-Basispfad (`view_base_path`) für File-Manager und Tree
|
|
30
|
+
|
|
31
|
+
Beide Views (SambaFileManager und SambaTree) haben ein neues optionales
|
|
32
|
+
Konfigurationsfeld **„View-Basispfad (relativ zum Plugin-Basispfad)“**.
|
|
33
|
+
Damit lässt sich der Pfad zweistufig konfigurieren:
|
|
34
|
+
|
|
35
|
+
1. **Plugin-Basispfad** (weiterhin Pflicht) – z. B. `static` – legt fest,
|
|
36
|
+
in welchem Unterverzeichnis der Freigabe der Plugin-Root liegt.
|
|
37
|
+
2. **View-Basispfad** (neu, optional) – z. B. `projekte/2026` – ein
|
|
38
|
+
statischer Präfix, der nur für diese eine View gilt.
|
|
39
|
+
|
|
40
|
+
Beispiel: Plugin-Basispfad = `static`, View-Basispfad = `projekte/2026`
|
|
41
|
+
→ die View listet den effektiven Pfad `static/projekte/2026`.
|
|
42
|
+
|
|
43
|
+
Der bisherige `extra_subpath` in beiden Views ist jetzt konsistent auf
|
|
44
|
+
den `from_field`-Modus beschränkt (dort weiterhin: Suffix hinter dem
|
|
45
|
+
Feldwert der aktuellen Zeile). Im `static`-Modus zählt nur
|
|
46
|
+
`view_base_path`.
|
|
47
|
+
|
|
48
|
+
Der zusammengesetzte Pfad wird pro Segment slash-getrimmt und
|
|
49
|
+
durchläuft `sanitizeRelativePath`, so dass `..`-Traversal, absolute
|
|
50
|
+
Pfade, UNC-Präfixe usw. weiterhin abgelehnt werden.
|
|
51
|
+
|
|
52
|
+
### Tests
|
|
53
|
+
|
|
54
|
+
`test/sanitize.test.js` deckt jetzt zusätzlich die Pfad-Komposition
|
|
55
|
+
(`view_base_path` + Feldwert + `extra_subpath`) inklusive Slash-
|
|
56
|
+
Normalisierung, Modus-Regeln und Traversal-Ablehnung ab.
|
|
57
|
+
|
|
58
|
+
## [0.4.14] – 2026-07-06
|
|
59
|
+
|
|
60
|
+
### Changed – Code-Review / Aufräumen (keine Verhaltensänderung)
|
|
61
|
+
|
|
62
|
+
Interne QS-Runde nach dem v0.4.13-Fix. Der Bug war behoben, aber die
|
|
63
|
+
Kommentare, Fehlertexte und die Fallback-Logik im Connection-Test
|
|
64
|
+
verwiesen noch auf die alte, mittlerweile widerlegte Hypothese
|
|
65
|
+
(`FileInformationClass=37 hart kodiert`). Alle diese Stellen wurden
|
|
66
|
+
auf den tatsächlichen Root Cause aktualisiert bzw. gelöscht.
|
|
67
|
+
|
|
68
|
+
**readdir-compat.js:**
|
|
69
|
+
- Docstring priorisiert den tatsächlichen Hauptbug (leeres Pattern
|
|
70
|
+
auf Folge-Pages) und schiebt den FileNameOffset-Bug in die Sekundär-
|
|
71
|
+
rolle (defensiv abgefangen, wird bei aktueller Loop nie getriggert).
|
|
72
|
+
- Encoder-Docstring erklärt, dass der `pat.length === 0`-Zweig nur
|
|
73
|
+
noch defensive Sicherheit ist.
|
|
74
|
+
|
|
75
|
+
**smb-client.js:**
|
|
76
|
+
- Der lange, spekulative Kommentar-Block über „Some Samba builds
|
|
77
|
+
reject QUERY_DIRECTORY on the share root because FileInformation-
|
|
78
|
+
Class=37 is hardcoded“ wurde entfernt. Der wahre Grund steht jetzt
|
|
79
|
+
kurz und präzise dort.
|
|
80
|
+
- Die 0xC0000033-Fehlermeldung verweist nicht mehr auf v0.4.12 als Fix
|
|
81
|
+
und nicht mehr auf `tools/diag-basepath.js`, sondern auf
|
|
82
|
+
`tools/diag-wire.js` (das aussagekräftigere Tool).
|
|
83
|
+
|
|
84
|
+
**index.js (Connection-Test-Route):**
|
|
85
|
+
- Der „gelber Hinweis“-Fallback bleibt als Safety Net, wird aber nur
|
|
86
|
+
noch aktiv, wenn `readdir` fehlschlägt und `stat` erfolgreich ist —
|
|
87
|
+
ein Zustand, der seit v0.4.13 nicht mehr auftreten sollte.
|
|
88
|
+
- Der Hinweistext sagt jetzt „unerwarteter Zustand, bitte diag-wire.js
|
|
89
|
+
ausführen“ statt der alten, jetzt falschen Erklärung mit
|
|
90
|
+
`FileInformationClass=37`.
|
|
91
|
+
|
|
92
|
+
**tools/diag-basepath.js:**
|
|
93
|
+
- FIX: `client.disconnect()` → `client.close()`. smb3-client's Client
|
|
94
|
+
hat `close()`, nicht `disconnect()` (das war die marsaud-API). Der
|
|
95
|
+
Aufruf schlug bisher am Ende jedes Diagnostic-Runs mit einer
|
|
96
|
+
TypeError-Meldung fehl, ohne aber das Resultat zu beeinträchtigen.
|
|
97
|
+
|
|
98
|
+
**tools/diag-wire.js:**
|
|
99
|
+
- FIX: Zusätzlich zu `open.close()` wird jetzt auch `client.close()`
|
|
100
|
+
aufgerufen, damit der TCP-Socket sauber geschlossen wird.
|
|
101
|
+
|
|
7
102
|
## [0.4.13] – 2026-07-06
|
|
8
103
|
|
|
9
104
|
### Fixed – **QUERY_DIRECTORY leere Patterns → 0xC0000033 (die eigentliche Ursache)**
|
package/README.md
CHANGED
|
@@ -194,9 +194,10 @@ Optionen:
|
|
|
194
194
|
|
|
195
195
|
| Option | Zweck |
|
|
196
196
|
|---|---|
|
|
197
|
-
| **
|
|
198
|
-
| **Row
|
|
199
|
-
| **
|
|
197
|
+
| **View-Basispfad (relativ zum Plugin-Basispfad)** | Optionaler statischer Präfix nur für diese View. Beispiel: Plugin-Basispfad = `static`, View-Basispfad = `projekte/2026` → View listet `static/projekte/2026`. Zweistufige Konfiguration: einmal serverweit im Plugin, einmal pro View. |
|
|
198
|
+
| **Row-Modus** | `static` = nur der View-Basispfad zählt, `from_field` = zusätzlich wird ein DB-Feldwert der aktuellen Zeile angehängt (z. B. in einer Show-View) |
|
|
199
|
+
| **Feld mit Unterpfad** | Feld der Tabelle mit dem relativen Ordnernamen (nur bei `from_field`) |
|
|
200
|
+
| **Zusätzlicher Suffix (nach dem Feldwert)** | statischer Suffix, z. B. `invoices` (nur bei `from_field`) |
|
|
200
201
|
| **Show hidden files** | `.dotfiles` einblenden |
|
|
201
202
|
| **Allow navigating up** | Up-Button aktivieren (nur bis zum Root, kein Ausbruch) |
|
|
202
203
|
| **Open PDFs / images inline** | Klick öffnet die Datei im integrierten Viewer |
|
|
@@ -204,6 +205,10 @@ Optionen:
|
|
|
204
205
|
| **Page size** | Einträge pro Seite (0 = alle) |
|
|
205
206
|
| **Panel title** | Text im Karten-Header |
|
|
206
207
|
|
|
208
|
+
Der effektive Pfad wird zusammengesetzt aus (jeweils falls gesetzt):
|
|
209
|
+
`Plugin-Basispfad / View-Basispfad / Feldwert / Zusätzlicher Suffix`.
|
|
210
|
+
`..`-Traversal, absolute Pfade und UNC-Präfixe werden abgelehnt.
|
|
211
|
+
|
|
207
212
|
Beispiel-Datenmodell für kunden-spezifische Ordner:
|
|
208
213
|
|
|
209
214
|
```
|
|
@@ -212,12 +217,13 @@ Felder: name (String), akte_dir (String, z. B. "kunde_42/2026")
|
|
|
212
217
|
```
|
|
213
218
|
|
|
214
219
|
Dann eine Show-View von `kunden` bauen und die `SambaFileManager`-View mit
|
|
215
|
-
|
|
220
|
+
Row-Modus = `from_field`, Feld = `akte_dir` einbetten.
|
|
216
221
|
|
|
217
222
|
### View: `SambaTree`
|
|
218
223
|
|
|
219
224
|
Kompaktere Alternative – lazy-loading Baum, ideal in einer Sidebar oder
|
|
220
|
-
neben Formularen. Gleiche Path-
|
|
225
|
+
neben Formularen. Gleiche Path-Optionen wie beim File-Manager (inkl.
|
|
226
|
+
**View-Basispfad**, **Row-Modus**, **Feld**, **Zusätzlicher Suffix**).
|
|
221
227
|
|
|
222
228
|
### Fieldview `samba_pdf` (deaktiviert seit 0.3.2)
|
|
223
229
|
|
package/filemanager-view.js
CHANGED
|
@@ -44,31 +44,42 @@ const configuration_workflow = () =>
|
|
|
44
44
|
.map((f) => f.name);
|
|
45
45
|
return new Form({
|
|
46
46
|
fields: [
|
|
47
|
+
new Field({
|
|
48
|
+
name: "view_base_path",
|
|
49
|
+
label: "View-Basispfad (relativ zum Plugin-Basispfad)",
|
|
50
|
+
type: "String",
|
|
51
|
+
sublabel:
|
|
52
|
+
"Optional. Statischer Pfad relativ zum Plugin-Basispfad, der f\u00fcr diese View gilt. " +
|
|
53
|
+
"Beispiel: Plugin-Basispfad = 'static', View-Basispfad = 'projekte/2026' \u2192 View listet 'static/projekte/2026'. " +
|
|
54
|
+
"Keine f\u00fchrenden/abschlie\u00dfenden Slashes n\u00f6tig. Traversal (\u201e..\u201c) wird abgelehnt.",
|
|
55
|
+
}),
|
|
47
56
|
new Field({
|
|
48
57
|
name: "mode",
|
|
49
|
-
label: "
|
|
58
|
+
label: "Row-Modus",
|
|
50
59
|
type: "String",
|
|
51
60
|
required: true,
|
|
52
61
|
attributes: {
|
|
53
62
|
options: ["static", "from_field"],
|
|
54
63
|
},
|
|
55
64
|
sublabel:
|
|
56
|
-
"static =
|
|
65
|
+
"static = immer nur der View-Basispfad. from_field = h\u00e4nge einen Feldwert der aktuellen Zeile an " +
|
|
66
|
+
"(nur sinnvoll, wenn die View in einer Show-View eingebettet ist).",
|
|
57
67
|
default: "static",
|
|
58
68
|
}),
|
|
59
69
|
new Field({
|
|
60
70
|
name: "path_field",
|
|
61
|
-
label: "
|
|
71
|
+
label: "Feld mit Unterpfad",
|
|
62
72
|
type: "String",
|
|
63
73
|
attributes: { options: stringFieldOptions },
|
|
64
74
|
showIf: { mode: "from_field" },
|
|
65
75
|
}),
|
|
66
76
|
new Field({
|
|
67
77
|
name: "extra_subpath",
|
|
68
|
-
label: "
|
|
78
|
+
label: "Zus\u00e4tzlicher Suffix (nach dem Feldwert)",
|
|
69
79
|
type: "String",
|
|
70
80
|
sublabel:
|
|
71
|
-
"Optional.
|
|
81
|
+
"Optional. Statischer Suffix, der nach dem Feldwert angeh\u00e4ngt wird. Beispiel: 'invoices'.",
|
|
82
|
+
showIf: { mode: "from_field" },
|
|
72
83
|
}),
|
|
73
84
|
new Field({
|
|
74
85
|
name: "show_hidden",
|
|
@@ -122,14 +133,37 @@ async function get_state_fields() {
|
|
|
122
133
|
return [];
|
|
123
134
|
}
|
|
124
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Compose the start path (relative to the plugin base_path) from three
|
|
138
|
+
* optional parts:
|
|
139
|
+
* 1. `view_base_path` — static, per-view prefix
|
|
140
|
+
* 2. row field value — only in "from_field" mode
|
|
141
|
+
* 3. `extra_subpath` — static suffix, only in "from_field" mode
|
|
142
|
+
*
|
|
143
|
+
* Empty parts are dropped. The final joined path is passed through
|
|
144
|
+
* sanitizeRelativePath so traversal / absolute paths / backslashes are
|
|
145
|
+
* rejected consistently.
|
|
146
|
+
*/
|
|
125
147
|
function computeStartPath(configuration, row) {
|
|
126
148
|
const parts = [];
|
|
149
|
+
if (configuration.view_base_path) {
|
|
150
|
+
parts.push(String(configuration.view_base_path));
|
|
151
|
+
}
|
|
127
152
|
if (configuration.mode === "from_field" && configuration.path_field && row) {
|
|
128
153
|
const v = row[configuration.path_field];
|
|
129
154
|
if (v) parts.push(String(v));
|
|
130
155
|
}
|
|
131
|
-
if (
|
|
132
|
-
|
|
156
|
+
if (
|
|
157
|
+
configuration.mode === "from_field" &&
|
|
158
|
+
configuration.extra_subpath
|
|
159
|
+
) {
|
|
160
|
+
parts.push(String(configuration.extra_subpath));
|
|
161
|
+
}
|
|
162
|
+
const joined = parts
|
|
163
|
+
.map((p) => p.replace(/^\/+|\/+$/g, ""))
|
|
164
|
+
.filter(Boolean)
|
|
165
|
+
.join("/");
|
|
166
|
+
return sanitizeRelativePath(joined);
|
|
133
167
|
}
|
|
134
168
|
|
|
135
169
|
// ---------------------------------------------------------------------------
|
package/index.js
CHANGED
|
@@ -519,22 +519,18 @@ function jsonOk(res, extra) {
|
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
/**
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
522
|
+
* CSRF verification is handled by Saltcorn's global csurf middleware
|
|
523
|
+
* (packages/server/app.js). By the time our route handler runs, csurf
|
|
524
|
+
* has already validated req.body._csrf / X-CSRF-Token / csrf-token /
|
|
525
|
+
* xsrf-token headers against the session secret. A manual re-check
|
|
526
|
+
* here would be wrong: csrf-tokens returns a NEW salted token on
|
|
527
|
+
* every call to req.csrfToken(), so `provided !== req.csrfToken()`
|
|
528
|
+
* would fail even for perfectly valid requests.
|
|
529
|
+
*
|
|
530
|
+
* We keep this stub as the single documented spot to attach future
|
|
531
|
+
* pre-flight authorisation checks if ever needed.
|
|
525
532
|
*/
|
|
526
|
-
function checkCsrf(
|
|
527
|
-
if (typeof req.csrfToken !== "function") return true; // CSRF disabled globally
|
|
528
|
-
const expected = req.csrfToken();
|
|
529
|
-
const provided =
|
|
530
|
-
(req.body && req.body._csrf) ||
|
|
531
|
-
req.headers["x-csrf-token"] ||
|
|
532
|
-
req.headers["csrf-token"] ||
|
|
533
|
-
req.query._csrf;
|
|
534
|
-
if (!provided || provided !== expected) {
|
|
535
|
-
jsonError(res, 403, "Invalid CSRF token");
|
|
536
|
-
return false;
|
|
537
|
-
}
|
|
533
|
+
function checkCsrf(_req, _res) {
|
|
538
534
|
return true;
|
|
539
535
|
}
|
|
540
536
|
|
|
@@ -1019,21 +1015,18 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
1019
1015
|
})),
|
|
1020
1016
|
truncated: Array.isArray(listing) && listing.length > 20,
|
|
1021
1017
|
note: rootNotEnum
|
|
1022
|
-
? "Verbindung + Anmeldung erfolgreich
|
|
1023
|
-
"
|
|
1024
|
-
"
|
|
1025
|
-
"
|
|
1026
|
-
"
|
|
1018
|
+
? "Verbindung + Anmeldung erfolgreich, aber der Share-Root konnte " +
|
|
1019
|
+
"nicht aufgelistet werden (stat OK, readdir schlug fehl). Das ist " +
|
|
1020
|
+
"ein unerwarteter Zustand \u2014 seit v0.4.13 sollte readdir gegen " +
|
|
1021
|
+
"Samba fehlerfrei laufen. Bitte tools/diag-wire.js ausf\u00fchren " +
|
|
1022
|
+
"und den Report melden."
|
|
1027
1023
|
: baseNotEnum
|
|
1028
1024
|
? "Verbindung + Anmeldung erfolgreich, Basispfad \u201e" +
|
|
1029
1025
|
(listing._basePathForDisplay || testCfg.base_path || "") +
|
|
1030
|
-
"\u201c wurde vom Server best\u00e4tigt (stat OK)
|
|
1031
|
-
"Auflisten
|
|
1032
|
-
"
|
|
1033
|
-
"
|
|
1034
|
-
"und PDF-Ansicht funktionieren \u2014 nur die Wurzel des " +
|
|
1035
|
-
"Basispfads kann nicht direkt aufgelistet werden. Workaround: " +
|
|
1036
|
-
"tieferen Basispfad setzen oder Unterordner ansprechen."
|
|
1026
|
+
"\u201c wurde vom Server best\u00e4tigt (stat OK), das direkte " +
|
|
1027
|
+
"Auflisten schlug aber fehl. Das ist ein unerwarteter Zustand \u2014 " +
|
|
1028
|
+
"seit v0.4.13 sollte readdir gegen Samba fehlerfrei laufen. Bitte " +
|
|
1029
|
+
"tools/diag-wire.js ausf\u00fchren und den Report melden."
|
|
1037
1030
|
: undefined,
|
|
1038
1031
|
});
|
|
1039
1032
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saltcorn-samba",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.15",
|
|
4
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": {
|
package/readdir-compat.js
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* readdir-compat.js — saltcorn-samba 0.4.
|
|
4
|
+
* readdir-compat.js — saltcorn-samba 0.4.14
|
|
5
5
|
*
|
|
6
6
|
* Behebt zwei QUERY_DIRECTORY-Wire-Bugs in smb3-client@0.2.0 die
|
|
7
7
|
* gegen Samba 4.23 zu STATUS_OBJECT_NAME_INVALID (0xC0000033) führen.
|
|
8
8
|
*
|
|
9
|
-
* BUG 1 (
|
|
9
|
+
* BUG 1 (leeres Pattern auf Folge-Pages) ─ Hauptursache des 0xC0000033:
|
|
10
|
+
* smb3-client's `readdirAll` sendet ab der 2. Enumeration-Page
|
|
11
|
+
* `searchPattern=""`. Windows toleriert das, Samba 4.23 lehnt es
|
|
12
|
+
* strikt ab: `source3/smbd/smb2_query_directory.c` prüft
|
|
13
|
+
* `in_file_name[0] == '\0'` und antwortet mit
|
|
14
|
+
* STATUS_OBJECT_NAME_INVALID.
|
|
15
|
+
* Fix (in readdirAllFixed): Auf jeder Page `*` senden. RESTART_SCANS
|
|
16
|
+
* nur beim ersten Request. Samba beendet nach der letzten Seite
|
|
17
|
+
* sauber mit STATUS_NO_MORE_FILES.
|
|
18
|
+
*
|
|
19
|
+
* BUG 2 (FileNameOffset bei leerem Pattern) ─ defensiv abgefangen:
|
|
10
20
|
* smb3-client's `encodeQueryDirectoryRequest` setzt `FileNameOffset`
|
|
11
21
|
* immer auf 96 — auch wenn `FileNameLength === 0` gesendet wird.
|
|
12
22
|
* MS-SMB2 §2.2.33 verlangt für diesen Fall FileNameOffset = 0.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* '\0'` und antwortet dann mit STATUS_OBJECT_NAME_INVALID.
|
|
19
|
-
* Fix: Auf jeder Page `*` senden. RESTART_SCANS nur beim ersten Mal.
|
|
20
|
-
* Samba beendet die Enumeration dann korrekt mit
|
|
21
|
-
* STATUS_NO_MORE_FILES auf einem Folge-Request ohne RESTART_SCANS.
|
|
23
|
+
* Da wir seit Bug-1-Fix ohnehin nie mit leerem Pattern senden, kann
|
|
24
|
+
* dieser Bug nicht mehr getriggert werden — encodeQueryDirectory-
|
|
25
|
+
* RequestFixed behandelt den Fall trotzdem spec-konform, falls ein
|
|
26
|
+
* Aufrufer den Encoder direkt benutzt oder ein zukünftiger Server
|
|
27
|
+
* noch strenger prüft.
|
|
22
28
|
*
|
|
23
29
|
* Strategie:
|
|
24
30
|
* Statt smb3-client's `client.readdir()` / `readdirAll` verwenden wir
|
|
@@ -26,8 +32,9 @@
|
|
|
26
32
|
* verwendet (Open, Close, Tree-Connect bleiben unverändert) und nur
|
|
27
33
|
* die kaputte QUERY_DIRECTORY-Loop durch eine spec-konforme ersetzt.
|
|
28
34
|
*
|
|
29
|
-
*
|
|
30
|
-
* smb3-client's `exports`-Gate keine Subpath-Imports
|
|
35
|
+
* Die internen wire-Module werden via `file://` URL geladen, weil
|
|
36
|
+
* smb3-client's `exports`-Gate keine Subpath-Imports über den
|
|
37
|
+
* Package-Namen zulässt (nicht einmal `smb3-client/package.json`).
|
|
31
38
|
*/
|
|
32
39
|
|
|
33
40
|
const fs = require("fs");
|
|
@@ -134,9 +141,15 @@ function loadInternals() {
|
|
|
134
141
|
|
|
135
142
|
/**
|
|
136
143
|
* Spec-konformer Encoder für SMB2 QUERY_DIRECTORY (MS-SMB2 §2.2.33).
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
144
|
+
*
|
|
145
|
+
* Unterschied zum Original in smb3-client@0.2.0:
|
|
146
|
+
* `FileNameOffset` wird auf 0 gesetzt, wenn `pat.length === 0`
|
|
147
|
+
* (statt hart auf 64+32=96). MS-SMB2 §2.2.33 verlangt bei
|
|
148
|
+
* `FileNameLength=0` genau `FileNameOffset=0`.
|
|
149
|
+
*
|
|
150
|
+
* In der aktuellen Compat-Loop wird immer `pat="*"` gesendet, daher
|
|
151
|
+
* wird dieser Zweig nie erreicht. Er bleibt als defensive Absicherung
|
|
152
|
+
* gegen Aufrufer, die den Encoder direkt mit leerem Pattern nutzen.
|
|
140
153
|
*/
|
|
141
154
|
function encodeQueryDirectoryRequestFixed(I, req) {
|
|
142
155
|
const pat = req.searchPattern
|
package/smb-client.js
CHANGED
|
@@ -227,8 +227,8 @@ async function buildClient(config) {
|
|
|
227
227
|
* Convert a readdirCompat rich-Dirent (which already carries name,
|
|
228
228
|
* size, mtime, ctime, and isDirectory()) directly to the legacy
|
|
229
229
|
* `@marsaud/smb2`-compatible shape the rest of the plugin expects.
|
|
230
|
-
* No extra stat roundtrip needed —
|
|
231
|
-
*
|
|
230
|
+
* No extra stat roundtrip needed — attributes come straight from the
|
|
231
|
+
* single QUERY_DIRECTORY response.
|
|
232
232
|
*/
|
|
233
233
|
function mapDirent(dirent) {
|
|
234
234
|
return {
|
|
@@ -255,42 +255,33 @@ async function buildClient(config) {
|
|
|
255
255
|
*/
|
|
256
256
|
async readdir(rel) {
|
|
257
257
|
const full = resolvePath(rel);
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
// implementiert QUERY_DIRECTORY MS-SMB2-§2.2.33-
|
|
263
|
-
// umgeht
|
|
264
|
-
// client.readdir()
|
|
258
|
+
// readdirCompat ist der ALLEINIGE readdir-Pfad. Grund: smb3-
|
|
259
|
+
// client@0.2.0 hat zwei QUERY_DIRECTORY-Wire-Bugs, die gegen
|
|
260
|
+
// Samba 4.23 zu STATUS_OBJECT_NAME_INVALID (0xC0000033) führen
|
|
261
|
+
// (leeres Pattern auf Folge-Pages + falscher FileNameOffset).
|
|
262
|
+
// readdirCompat implementiert QUERY_DIRECTORY MS-SMB2-§2.2.33-
|
|
263
|
+
// konform und umgeht beide Bugs. Kein Fallback auf
|
|
264
|
+
// client.readdir() — das würde die Bugs reproduzieren.
|
|
265
265
|
let dirents;
|
|
266
266
|
try {
|
|
267
267
|
dirents = await readdirCompat(client, full, { rich: true });
|
|
268
268
|
} catch (errCompat) {
|
|
269
269
|
const err = errCompat;
|
|
270
|
-
// Some Samba builds (observed on 4.20+/4.23+) reject
|
|
271
|
-
// SMB2 QUERY_DIRECTORY on the *share root* with
|
|
272
|
-
// STATUS_OBJECT_NAME_INVALID (0xC0000033) because smb3-client
|
|
273
|
-
// asks for FileIdBothDirectoryInformation (class 37) on a
|
|
274
|
-
// handle opened with an empty filename. We cannot swap the
|
|
275
|
-
// information class from userland, and Samba refuses both
|
|
276
|
-
// "." (→ STATUS_OBJECT_NAME_NOT_FOUND) and "*" (→ CREATE with
|
|
277
|
-
// wildcard is protocol-illegal). The clean way out is:
|
|
278
|
-
// require the caller to configure a real base_path so every
|
|
279
|
-
// readdir happens inside a directory the server can enumerate.
|
|
280
270
|
const msg = String((err && err.message) || err || "");
|
|
281
271
|
// Falls der Compat-Pfad selbst mit 0xC0000033 zurückkommt,
|
|
282
272
|
// liegt entweder ein weiterer, unbekannter Wire-Bug vor oder
|
|
283
|
-
// Samba lehnt aus einem anderen Grund ab.
|
|
284
|
-
//
|
|
273
|
+
// Samba lehnt aus einem anderen Grund ab (z. B. fehlende
|
|
274
|
+
// Rechte auf die Zielressource). Wir geben eine gezielte
|
|
275
|
+
// Meldung mit Handlungsanweisung.
|
|
285
276
|
if (/0xC0000033|OBJECT_NAME_INVALID/i.test(msg)) {
|
|
286
277
|
const e = new Error(
|
|
287
|
-
"Samba lehnt QUERY_DIRECTORY auf diesem Pfad ab " +
|
|
288
|
-
"(STATUS_OBJECT_NAME_INVALID, 0xC0000033). " +
|
|
289
|
-
"
|
|
290
|
-
"falls dieser Fehler
|
|
291
|
-
"
|
|
292
|
-
"tools/diag-
|
|
293
|
-
"Details: " +
|
|
278
|
+
"Samba lehnt QUERY_DIRECTORY auf diesem Pfad unerwartet ab " +
|
|
279
|
+
"(STATUS_OBJECT_NAME_INVALID, 0xC0000033). Die bekannten " +
|
|
280
|
+
"smb3-client-Wire-Bugs sind in dieser Plugin-Version " +
|
|
281
|
+
"bereits gepatcht — falls dieser Fehler dennoch auftritt, " +
|
|
282
|
+
"liegt ein bislang unbekannter Wire-Format-Fehler oder ein " +
|
|
283
|
+
"Rechteproblem vor. Bitte tools/diag-wire.js ausführen " +
|
|
284
|
+
"und den Report zurücksenden. Details: " +
|
|
294
285
|
msg
|
|
295
286
|
);
|
|
296
287
|
e.cause = err;
|
|
@@ -320,9 +311,9 @@ async function buildClient(config) {
|
|
|
320
311
|
}
|
|
321
312
|
throw err;
|
|
322
313
|
}
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
314
|
+
// readdirCompat gibt Rich-Dirents zurück (Name, Größe, mtime,
|
|
315
|
+
// ctime, isDirectory). Kein zusätzlicher stat-Fan-out nötig —
|
|
316
|
+
// ein einziger QUERY_DIRECTORY-Roundtrip liefert alles.
|
|
326
317
|
return dirents.map(mapDirent);
|
|
327
318
|
},
|
|
328
319
|
|
package/tools/diag-basepath.js
CHANGED
|
@@ -91,7 +91,10 @@ async function main() {
|
|
|
91
91
|
step++;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
// smb3-client's Client has close(), not disconnect() — the old
|
|
95
|
+
// marsaud API used disconnect. Use close() and ignore errors so a
|
|
96
|
+
// failing probe still tears down cleanly.
|
|
97
|
+
try { await client.close(); } catch (_) {}
|
|
95
98
|
console.log("Done.");
|
|
96
99
|
}
|
|
97
100
|
|
package/tools/diag-wire.js
CHANGED
package/tree-view.js
CHANGED
|
@@ -41,30 +41,42 @@ const configuration_workflow = () =>
|
|
|
41
41
|
.map((f) => f.name);
|
|
42
42
|
return new Form({
|
|
43
43
|
fields: [
|
|
44
|
+
new Field({
|
|
45
|
+
name: "view_base_path",
|
|
46
|
+
label: "View-Basispfad (relativ zum Plugin-Basispfad)",
|
|
47
|
+
type: "String",
|
|
48
|
+
sublabel:
|
|
49
|
+
"Optional. Statischer Pfad relativ zum Plugin-Basispfad, der f\u00fcr diese View gilt. " +
|
|
50
|
+
"Beispiel: Plugin-Basispfad = 'static', View-Basispfad = 'projekte/2026' \u2192 View listet 'static/projekte/2026'. " +
|
|
51
|
+
"Keine f\u00fchrenden/abschlie\u00dfenden Slashes n\u00f6tig. Traversal (\u201e..\u201c) wird abgelehnt.",
|
|
52
|
+
}),
|
|
44
53
|
new Field({
|
|
45
54
|
name: "mode",
|
|
46
|
-
label: "
|
|
55
|
+
label: "Row-Modus",
|
|
47
56
|
type: "String",
|
|
48
57
|
required: true,
|
|
49
58
|
attributes: {
|
|
50
59
|
options: ["static", "from_field"],
|
|
51
60
|
},
|
|
52
61
|
sublabel:
|
|
53
|
-
"static =
|
|
62
|
+
"static = immer nur der View-Basispfad. from_field = h\u00e4nge einen Feldwert der aktuellen Zeile an " +
|
|
63
|
+
"(nur sinnvoll, wenn die View in einer Show-View eingebettet ist).",
|
|
64
|
+
default: "static",
|
|
54
65
|
}),
|
|
55
66
|
new Field({
|
|
56
67
|
name: "path_field",
|
|
57
|
-
label: "
|
|
68
|
+
label: "Feld mit Unterpfad",
|
|
58
69
|
type: "String",
|
|
59
70
|
attributes: { options: stringFieldOptions },
|
|
60
71
|
showIf: { mode: "from_field" },
|
|
61
72
|
}),
|
|
62
73
|
new Field({
|
|
63
74
|
name: "extra_subpath",
|
|
64
|
-
label: "
|
|
75
|
+
label: "Zus\u00e4tzlicher Suffix (nach dem Feldwert)",
|
|
65
76
|
type: "String",
|
|
66
77
|
sublabel:
|
|
67
|
-
"Optional.
|
|
78
|
+
"Optional. Statischer Suffix, der nach dem Feldwert angeh\u00e4ngt wird. Beispiel: 'invoices'.",
|
|
79
|
+
showIf: { mode: "from_field" },
|
|
68
80
|
}),
|
|
69
81
|
new Field({
|
|
70
82
|
name: "show_hidden",
|
|
@@ -104,17 +116,35 @@ async function get_state_fields(table_id, viewname, { mode, path_field }) {
|
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
119
|
+
* Compose the start path (relative to the plugin base_path) from three
|
|
120
|
+
* optional parts:
|
|
121
|
+
* 1. `view_base_path` — static, per-view prefix
|
|
122
|
+
* 2. row field value — only in "from_field" mode
|
|
123
|
+
* 3. `extra_subpath` — static suffix, only in "from_field" mode
|
|
124
|
+
*
|
|
125
|
+
* Empty parts are dropped and slash-trimmed. The final joined path is
|
|
126
|
+
* passed through sanitizeRelativePath so traversal / absolute paths /
|
|
127
|
+
* backslashes are rejected consistently.
|
|
109
128
|
*/
|
|
110
129
|
function computeStartPath(configuration, row) {
|
|
111
130
|
const parts = [];
|
|
131
|
+
if (configuration.view_base_path) {
|
|
132
|
+
parts.push(String(configuration.view_base_path));
|
|
133
|
+
}
|
|
112
134
|
if (configuration.mode === "from_field" && configuration.path_field && row) {
|
|
113
135
|
const v = row[configuration.path_field];
|
|
114
136
|
if (v) parts.push(String(v));
|
|
115
137
|
}
|
|
116
|
-
if (
|
|
117
|
-
|
|
138
|
+
if (
|
|
139
|
+
configuration.mode === "from_field" &&
|
|
140
|
+
configuration.extra_subpath
|
|
141
|
+
) {
|
|
142
|
+
parts.push(String(configuration.extra_subpath));
|
|
143
|
+
}
|
|
144
|
+
const joined = parts
|
|
145
|
+
.map((p) => p.replace(/^\/+|\/+$/g, ""))
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
.join("/");
|
|
118
148
|
// sanitizeRelativePath throws on traversal – we let the caller handle it.
|
|
119
149
|
return sanitizeRelativePath(joined);
|
|
120
150
|
}
|