saltcorn-samba 0.4.5 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/index.js +63 -49
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,92 @@ 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.8] – 2026-07-05
8
+
9
+ ### Fixed – **Basispfad wurde beim Verbindungstest doppelt vorangestellt (die eigentliche Ursache)**
10
+
11
+ In allen Vorgängerversionen (0.4.4 – 0.4.7) hat der Verbindungstest den
12
+ konfigurierten Basispfad **zweimal** auf den Server geschickt und deshalb
13
+ systematisch die Meldung *„Der Basispfad ist auf dem Server nicht auffindbar"*
14
+ produziert — auch dann, wenn der Pfad in Wirklichkeit existierte und mit
15
+ `smbclient` oder dem Windows-Explorer problemlos erreichbar war.
16
+
17
+ **Ursache:** `buildClient(config)` speichert `config.base_path` intern als
18
+ `client.basePath`. Der Wrapper-`resolvePath(rel)` baut daraus
19
+ `<share>/<basePath>/<rel>`. Die Test-Route rief aber
20
+ `client.readdir(testCfg.base_path)` auf und übergab den Basispfad **noch
21
+ einmal** als relatives Argument. Ergebnis: aus `base_path = "static"` wurde
22
+ auf der Leitung `buero/static/static` — den es natürlich nicht gibt. Die
23
+ Diagnose-Ausgabe (`tested_path: static/static`) zeigte den Bug bereits
24
+ klar an, wurde aber bisher als Symptom statt Ursache gelesen.
25
+
26
+ **Fix:** Die Test-Route ruft jetzt `client.readdir("")` auf. Der Wrapper
27
+ resolvet das intern korrekt zu `<share>/<basePath>` — also genau dem Pfad,
28
+ den der Benutzer im Formular eingetragen hat. Für Anzeige-Zwecke (Meldungen,
29
+ Diagnose-Kacheln, Fallback-Probes) wird der Basispfad separat in
30
+ `baseForDisplay` gehalten.
31
+
32
+ ### Added – **Standalone-Diagnose-Skript `tools/diag-basepath.js`**
33
+
34
+ Wer den Verbindungsproblemen auf den Grund gehen will, kann jetzt außerhalb
35
+ von Saltcorn probieren, was smb3-client tatsächlich sieht:
36
+
37
+ ```bash
38
+ node tools/diag-basepath.js \
39
+ --host 192.168.110.10 --share buero --path static \
40
+ --user 01_vassen --domain buero.ib-vassen.de --password ...
41
+ ```
42
+
43
+ Das Skript führt sechs Probes durch (Share-Root, Share-Root mit Slash,
44
+ `stat` auf Ziel, `readdir` auf Ziel, Groß-/Kleinschreibvariante) und
45
+ druckt für jede den NT-Status-Code — hilfreich beim Aufspüren von
46
+ Schreibvarianten oder ACL-Problemen.
47
+
48
+ ## [0.4.7] – 2026-07-05
49
+
50
+ ### Fixed – **Irreführende Windows-UNC-Anzeige in der Fehlermeldung**
51
+
52
+ Die v0.4.6-Fehlermeldung zeigte den getesteten Pfad als Windows-UNC
53
+ (`\\\\192.168.110.10\\buero\\static`). Das war irreführend, weil das
54
+ Plugin an smb3-client tatsächlich Forward-Slash-Pfade schickt
55
+ (`buero/static`) — die Backslash-Anzeige suggerierte einen Bug, der
56
+ keiner ist. Jetzt wird der Pfad in genau der Form gezeigt, die auch
57
+ tatsächlich über die Leitung geht: `<share>/<basispfad>`.
58
+
59
+ ## [0.4.6] – 2026-07-05
60
+
61
+ ### Fixed – **Schreibvarianten-Test verdoppelte den Basispfad**
62
+
63
+ Die Case-Probes in v0.4.4/0.4.5 riefen `client.readdir(<candidate>)` auf.
64
+ Der Wrapper prependiert aber automatisch den bereits konfigurierten
65
+ `basePath` — dadurch wurde intern nach `buero/static/static` statt
66
+ `buero/static` gesucht. Alle Probes scheiterten deshalb systematisch
67
+ mit derselben Meldung, unabhängig von der wahren Ursache.
68
+
69
+ **Fix:** Die Probes gehen jetzt direkt gegen den rohen `smb3-client`
70
+ (`client._raw.readdir(share/<absoluter Pfad>)`) und bauen die
71
+ From-Share-Root-Pfade selbst zusammen. Der Test-Endpunkt zeigt jetzt
72
+ zusätzlich pro Probe den tatsächlich getesteten Pfad an.
73
+
74
+ ### Fixed – **Falscher „Login rejected“-Hinweis bei BASE_PATH_NOT_FOUND**
75
+
76
+ Der Fallback-Hint-Table prüfte per Substring-Match auf `access_denied`
77
+ u.\u00e4. — das triggerte auf den erklärenden Text in unserer eigenen
78
+ deutschen Fehlermeldung (wir *erwähnen* `ACCESS_DENIED` didaktisch).
79
+ Ein ungefundener Basispfad wurde deshalb zusätzlich mit dem irreführenden
80
+ Hinweis „Login rejected“ versehen.
81
+
82
+ **Fix:** Fehler mit eigenem `code` (`BASE_PATH_NOT_FOUND`,
83
+ `BASE_PATH_NOT_A_DIR`) überspringen den Substring-Match — die
84
+ Wrapper-Meldung ist bereits vollständig und braucht keinen „Hint“.
85
+
86
+ ### Changed – **Bessere Basispfad-Meldung**
87
+
88
+ Wenn kein Case-Match gefunden wurde, zeigt die Meldung jetzt den
89
+ tatsächlich getesteten UNC-Pfad (`\\\\server\\share\\pfad`) und weist
90
+ explizit darauf hin, dass der Basispfad **relativ zur Freigabe** ist —
91
+ also `unterordner` und **nicht** `sharename/unterordner`.
92
+
7
93
  ## [0.4.5] – 2026-07-05
8
94
 
9
95
  ### Fixed – **Test-Verbindung-Button reagiert nicht mehr (v0.4.4-Regression)**
package/index.js CHANGED
@@ -853,72 +853,73 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
853
853
  // to stat("") — that already proves the whole handshake
854
854
  // (TCP + Negotiate + Session + TREE_CONNECT + Auth) works, which
855
855
  // is all the connection test actually promises.
856
- const rel = testCfg.base_path ? sanitizeRelativePath(testCfg.base_path) : "";
857
- // When a base_path is set, verify it exists and is a directory
858
- // BEFORE trying to enumerate it. This turns the opaque
859
- // "CREATE failed: STATUS_OBJECT_NAME_NOT_FOUND" into a clear
860
- // "Basispfad existiert nicht" hint the user can act on.
861
- // We used to run a stat() first. That turned out to be flaky:
862
- // smb3-client's stat() sends a CREATE with createOptions=0
863
- // (no directory hint) which some Samba configurations reject
864
- // for directories with strict ACLs. readdir() sends
865
- // createOptions=1 (DIRECTORY_FILE) and is the right primitive
866
- // for a base_path check anyway — we want to know the folder
867
- // can actually be listed, not just opened.
856
+ //
857
+ // IMPORTANT: buildClient already stores `base_path` as the
858
+ // wrapper's `basePath`, and wrapper.readdir(x) resolves to
859
+ // `share/basePath/x`. So we call readdir("") to list the
860
+ // configured basePath itself not readdir(base_path), which
861
+ // would append it twice (bug fixed in 0.4.8).
862
+ const rel = "";
863
+ const baseForDisplay = testCfg.base_path ? sanitizeRelativePath(testCfg.base_path) : "";
868
864
  try {
869
865
  return await client.readdir(rel);
870
866
  } catch (err) {
871
867
  // Look at both the wrapper error and its underlying cause.
872
868
  const causeMsg = String((err && err.cause && err.cause.message) || "");
873
869
  const msg = String((err && err.message) || err || "") + " " + causeMsg;
874
- const isRootProbe = !rel;
870
+ // The path we effectively asked the server to enumerate is
871
+ // `share/basePath` (rel is always "" here — basePath is the
872
+ // subdirectory to test).
873
+ const effectivePath = testCfg.share + (baseForDisplay ? "/" + baseForDisplay : "");
874
+ const isRootProbe = !baseForDisplay;
875
875
  const isRootEnumBug = /0xC0000033|OBJECT_NAME_INVALID|Share-Root/i.test(msg);
876
876
  if (isRootProbe && isRootEnumBug) {
877
- // Fall back: proof-of-life via stat on share root. This
878
- // confirms TCP + Negotiate + Session + Auth + TREE_CONNECT
879
- // without hitting the broken QUERY_DIRECTORY path.
877
+ // Share-root enumeration is a known smb3-client-vs-Samba
878
+ // limitation. Fall back to stat("") which proves the whole
879
+ // handshake works even if we can't list the root.
880
880
  try {
881
881
  await client.stat("");
882
- // Signal to the caller that the connection works, but
883
- // the share root cannot be enumerated on this server.
884
882
  const marker = [];
885
883
  marker._rootNotEnumerable = true;
886
884
  return marker;
887
885
  } catch (statErr) {
888
- // stat also failed — surface the original error.
889
886
  throw err;
890
887
  }
891
888
  }
892
- // A non-existent base_path (or one hidden from this user by
893
- // Samba's "hide unreadable" behaviour) surfaces here. Try to
894
- // gather actionable diagnostics: list the parent directory
895
- // (if not the share root) and probe common alternative
896
- // spellings of the missing segment so we can distinguish
897
- // typo / case-mismatch / permission problems.
889
+ // NAME_NOT_FOUND / PATH_NOT_FOUND on a configured base_path.
890
+ // Gather diagnostics: raw probes against alternate spellings
891
+ // and (if possible) a listing of the parent directory.
898
892
  const isMissing = /OBJECT_NAME_NOT_FOUND|OBJECT_PATH_NOT_FOUND|ENOENT|STATUS_NO_SUCH_FILE|existiert.*nicht/i.test(msg);
899
- if (rel && isMissing) {
900
- const parts = rel.split("/").filter(Boolean);
893
+ if (baseForDisplay && isMissing) {
894
+ const parts = baseForDisplay.split("/").filter(Boolean);
901
895
  const missing = parts[parts.length - 1];
902
896
  const parent = parts.slice(0, -1).join("/");
903
897
  const parentAbs = parent || "(Share-Root)";
898
+ // Probe via the raw smb3-client so we bypass the wrapper's
899
+ // basePath prefix and can test siblings/alternate spellings
900
+ // directly from the share root.
901
+ const raw = client._raw;
902
+ const rawShare = client.shareName;
903
+ async function rawReaddir(relFromShareRoot) {
904
+ const full = relFromShareRoot ? rawShare + "/" + relFromShareRoot : rawShare;
905
+ return await raw.readdir(full, { withFileTypes: true });
906
+ }
904
907
  let siblings = null;
905
908
  let parent_error = null;
906
909
  if (parent) {
907
910
  try {
908
- const listing = await client.readdir(parent);
911
+ const listing = await rawReaddir(parent);
909
912
  siblings = Array.isArray(listing)
910
913
  ? listing.map((d) => ({
911
- name: d && (d.name || d),
912
- isDirectory: !!(d && (d.isDirectory === true || (typeof d.isDirectory === "function" && d.isDirectory()))),
914
+ name: d && d.name,
915
+ isDirectory: !!(d && typeof d.isDirectory === "function" && d.isDirectory()),
913
916
  }))
914
917
  : null;
915
918
  } catch (parentErr) {
916
919
  parent_error = String((parentErr && parentErr.message) || parentErr || "");
917
920
  }
918
921
  }
919
- // Probe alternate spellings: original, upper, lower, capitalised.
920
- // This lets us report "Ordner existiert unter anderem Namen" if
921
- // Samba is running with case sensitive = yes / case-preserved.
922
+ // Probe alternate spellings of the missing segment.
922
923
  const probes = [];
923
924
  const seen = new Set();
924
925
  const addProbe = (name) => {
@@ -932,16 +933,18 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
932
933
  addProbe(missing.charAt(0).toUpperCase() + missing.slice(1).toLowerCase());
933
934
  const probe_results = [];
934
935
  for (const p of probes) {
935
- const candidate = parent ? parent + "/" + p : p;
936
+ // From-share-root path: `parent/candidate`. NEVER include
937
+ // client.basePath here — the raw client is share-relative.
938
+ const relFromShare = [parent, p].filter(Boolean).join("/");
936
939
  let ok = false;
937
940
  let perr = null;
938
941
  try {
939
- await client.readdir(candidate);
942
+ await rawReaddir(relFromShare);
940
943
  ok = true;
941
944
  } catch (pErr) {
942
945
  perr = String((pErr && pErr.message) || pErr || "");
943
946
  }
944
- probe_results.push({ candidate: p, ok, error: ok ? null : perr });
947
+ probe_results.push({ candidate: p, tested_path: relFromShare, ok, error: ok ? null : perr });
945
948
  }
946
949
  const workingAlt = probe_results.find(
947
950
  (r) => r.ok && r.candidate !== missing
@@ -949,25 +952,24 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
949
952
  let hintText;
950
953
  if (workingAlt) {
951
954
  hintText =
952
- "Der Ordner heisst auf dem Server \u201e" +
955
+ "Der Ordner hei\u00dft auf dem Server \u201e" +
953
956
  workingAlt.candidate +
954
- "\u201c (andere Gross-/Kleinschreibung). Bitte den " +
957
+ "\u201c (andere Gro\u00df-/Kleinschreibung). Bitte den " +
955
958
  "Basispfad exakt so eintragen \u2014 der Samba-Server ist " +
956
959
  "case-sensitive (\u201ecase sensitive = yes\u201c in smb.conf).";
957
960
  } else {
958
961
  hintText =
959
- "Der Basispfad \u201e" + rel + "\u201c ist auf dem " +
960
- "Server nicht auffindbar. M\u00f6gliche Ursachen: " +
961
- "(a) der Ordner existiert wirklich nicht (bitte mit " +
962
- "einem SMB-Client wie \u201esmbclient\u201c oder dem " +
963
- "Windows-Explorer gegenpr\u00fcfen); " +
962
+ "Der Basispfad \u201e" + baseForDisplay + "\u201c wurde auf der " +
963
+ "Freigabe \u201e" + testCfg.share + "\u201c nicht " +
964
+ "gefunden. Intern getestet wurde: \u201e" + effectivePath +
965
+ "\u201c. M\u00f6gliche Ursachen: (a) Ordner existiert unter " +
966
+ "genau diesem Namen nicht in dieser Freigabe (Basispfad ist " +
967
+ "relativ zur Freigabe \u2014 also z.\u202fB. \u201eunterordner\u201c, " +
968
+ "nicht \u201e" + testCfg.share + "/unterordner\u201c); " +
964
969
  "(b) der angemeldete Benutzer \u201e" +
965
970
  (testCfg.username || "(anonymous)") +
966
- "\u201c hat kein Leserecht auf den Ordner (Samba antwortet " +
967
- "dann bei \u201ehide unreadable = yes\u201c mit " +
968
- "NAME_NOT_FOUND statt ACCESS_DENIED); " +
969
- "(c) der Ordner ist per \u201eveto files\u201c / " +
970
- "\u201ehide files\u201c auf dem Server ausgeblendet.";
971
+ "\u201c darf den Ordner nicht sehen; " +
972
+ "(c) der Ordner ist per Samba-Konfiguration ausgeblendet.";
971
973
  }
972
974
  const e = new Error(hintText);
973
975
  e.cause = err;
@@ -1017,7 +1019,19 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
1017
1019
  const code = (err && (err.code || err.errno)) || null;
1018
1020
  let hint = null;
1019
1021
  const m = msg.toLowerCase();
1020
- if (code === "ECONNREFUSED" || m.includes("econnrefused"))
1022
+ // If the wrapper already produced a well-formed, self-explanatory
1023
+ // error (own `code` set), skip the substring-match hint table —
1024
+ // it would otherwise trigger on words that appear inside our own
1025
+ // human-readable German explanation text (e.g. our message text
1026
+ // mentions "access_denied" or "path" for teaching purposes).
1027
+ const codesWithOwnMessage = new Set([
1028
+ "BASE_PATH_NOT_FOUND",
1029
+ "BASE_PATH_NOT_A_DIR",
1030
+ ]);
1031
+ if (code && codesWithOwnMessage.has(code)) {
1032
+ hint = null;
1033
+ }
1034
+ else if (code === "ECONNREFUSED" || m.includes("econnrefused"))
1021
1035
  hint = "The server refused the connection on port " + testCfg.port +
1022
1036
  ". Check that Samba is running and that a firewall (or Docker) does not block TCP/445.";
1023
1037
  else if (code === "ETIMEDOUT" || m.includes("etimedout") || m.includes("timed out"))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saltcorn-samba",
3
- "version": "0.4.5",
3
+ "version": "0.4.8",
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": {