saltcorn-samba 0.4.12 → 0.4.13
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 +45 -0
- package/package.json +2 -1
- package/readdir-compat.js +22 -13
- package/tools/diag-basepath.js +115 -0
- package/tools/diag-wire.js +352 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,51 @@ 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.13] – 2026-07-06
|
|
8
|
+
|
|
9
|
+
### Fixed – **QUERY_DIRECTORY leere Patterns → 0xC0000033 (die eigentliche Ursache)**
|
|
10
|
+
|
|
11
|
+
Mit v0.4.12 wurde ein Wire-Bug in smb3-client (FileNameOffset) gefixt,
|
|
12
|
+
aber Samba lehnte weiterhin ab. Mit dem neuen `tools/diag-wire.js` konnte
|
|
13
|
+
die tatsächliche Ursache byteweise verifiziert werden:
|
|
14
|
+
|
|
15
|
+
**Bug:** smb3-client's `readdirAll` sendet ab der 2. Enumeration-Seite
|
|
16
|
+
`searchPattern=""` (leerer String). Windows toleriert das, aber Samba's
|
|
17
|
+
`source3/smbd/smb2_query_directory.c` enthält den strikten Check:
|
|
18
|
+
|
|
19
|
+
```c
|
|
20
|
+
if (state->in_file_name[0] == '\0') {
|
|
21
|
+
tevent_req_nterror(req, NT_STATUS_OBJECT_NAME_INVALID);
|
|
22
|
+
return tevent_req_post(req, ev);
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Das ergibt exakt den STATUS_OBJECT_NAME_INVALID (0xC0000033), den wir
|
|
27
|
+
seit v0.4.0 sehen.
|
|
28
|
+
|
|
29
|
+
**Fix in `readdir-compat.js`:** Der eigene Enumeration-Loop sendet auf
|
|
30
|
+
**jeder** Seite `searchPattern="*"`, nicht nur beim ersten Request.
|
|
31
|
+
`RESTART_SCANS` wird nur beim ersten Request gesetzt; ab dann sendet
|
|
32
|
+
Samba nach dem letzten Batch korrekt STATUS_NO_MORE_FILES, was den Loop
|
|
33
|
+
sauber terminiert. Verifiziert via `diag-wire.js`:
|
|
34
|
+
|
|
35
|
+
- Probe 1 (pat=`*`, RESTART): STATUS_SUCCESS, 8 Einträge
|
|
36
|
+
- Probe 2 (pat=`*`, ohne RESTART): STATUS_NO_MORE_FILES → Loop-Ende
|
|
37
|
+
- Probe 6 (pat=`""`, offset=0): **0xC0000033** ← der Bug
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- `tools/diag-wire.js` – Wire-Level-Diagnose mit Hex-Dump der SMB2-Bytes,
|
|
42
|
+
testet 6 verschiedene QUERY_DIRECTORY-Varianten (Info-Class, Buffer-
|
|
43
|
+
Größe, mit/ohne Pattern). Nützlich für zukünftige Kompatibilitäts-
|
|
44
|
+
probleme mit anderen SMB-Servern.
|
|
45
|
+
|
|
46
|
+
### Upstream-Report aktualisiert
|
|
47
|
+
|
|
48
|
+
`smb3-client-bug-report.md` beschreibt jetzt beide Bugs (FileNameOffset
|
|
49
|
+
**und** leeres Pattern gegen Samba). Beide Fixes sind identisch simpel:
|
|
50
|
+
auf jeder Page `*` senden.
|
|
51
|
+
|
|
7
52
|
## [0.4.12] – 2026-07-06
|
|
8
53
|
|
|
9
54
|
### Fixed – **`0xC0000033` beim readdir gegen Samba 4.23 endgültig behoben (Root Cause identifiziert)**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saltcorn-samba",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.13",
|
|
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": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"filemanager-view.js",
|
|
45
45
|
"pdf-view.js",
|
|
46
46
|
"public/",
|
|
47
|
+
"tools/",
|
|
47
48
|
"README.md",
|
|
48
49
|
"CHANGELOG.md",
|
|
49
50
|
"LICENSE"
|
package/readdir-compat.js
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* readdir-compat.js — saltcorn-samba 0.4.
|
|
4
|
+
* readdir-compat.js — saltcorn-samba 0.4.13
|
|
5
5
|
*
|
|
6
|
-
* Behebt
|
|
7
|
-
* gegen Samba 4.23 zu STATUS_OBJECT_NAME_INVALID (0xC0000033)
|
|
6
|
+
* Behebt zwei QUERY_DIRECTORY-Wire-Bugs in smb3-client@0.2.0 die
|
|
7
|
+
* gegen Samba 4.23 zu STATUS_OBJECT_NAME_INVALID (0xC0000033) führen.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* BUG 1 (FileNameOffset bei leerem Pattern):
|
|
10
10
|
* smb3-client's `encodeQueryDirectoryRequest` setzt `FileNameOffset`
|
|
11
|
-
* immer auf 96 — auch wenn `FileNameLength === 0` gesendet wird
|
|
12
|
-
* (auf 2. und folgenden Enumeration-Pages).
|
|
11
|
+
* immer auf 96 — auch wenn `FileNameLength === 0` gesendet wird.
|
|
13
12
|
* MS-SMB2 §2.2.33 verlangt für diesen Fall FileNameOffset = 0.
|
|
14
|
-
*
|
|
13
|
+
*
|
|
14
|
+
* BUG 2 (Leeres Pattern auf Folge-Pages): ← Der Hauptbug!
|
|
15
|
+
* smb3-client sendet ab der 2. Enumeration-Page `searchPattern=""`.
|
|
16
|
+
* Windows toleriert das, Samba 4.23 lehnt es strikt ab. Belegt via
|
|
17
|
+
* diag-wire.js: `smb2_query_directory.c` prüft `in_file_name[0] ==
|
|
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.
|
|
15
22
|
*
|
|
16
23
|
* Strategie:
|
|
17
24
|
* Statt smb3-client's `client.readdir()` / `readdirAll` verwenden wir
|
|
@@ -21,9 +28,6 @@
|
|
|
21
28
|
*
|
|
22
29
|
* Nur die interne wire-Struktur wird via file:// URL importiert, weil
|
|
23
30
|
* smb3-client's `exports`-Gate keine Subpath-Imports zulässt.
|
|
24
|
-
*
|
|
25
|
-
* Idempotent, thread-safe (Node ist single-threaded, aber wir wollen
|
|
26
|
-
* keinen Race auf paralleler Erst-Nutzung).
|
|
27
31
|
*/
|
|
28
32
|
|
|
29
33
|
const fs = require("fs");
|
|
@@ -154,8 +158,13 @@ function encodeQueryDirectoryRequestFixed(I, req) {
|
|
|
154
158
|
|
|
155
159
|
/**
|
|
156
160
|
* QUERY_DIRECTORY-Loop mit gepatchtem Encoder.
|
|
157
|
-
*
|
|
158
|
-
*
|
|
161
|
+
*
|
|
162
|
+
* Unterschiede zu smb3-client's readdirAll():
|
|
163
|
+
* 1. Auf Folge-Pages wird `searchPattern="*"` gesendet (nicht "").
|
|
164
|
+
* Samba lehnt leere Patterns strikt ab (STATUS_OBJECT_NAME_INVALID).
|
|
165
|
+
* 2. RESTART_SCANS nur beim ersten Request.
|
|
166
|
+
* 3. Encoder setzt FileNameOffset=0 bei leerem Pattern (Fallback für
|
|
167
|
+
* andere kaputte Server, wird hier faktisch nie benutzt).
|
|
159
168
|
*/
|
|
160
169
|
async function readdirAllFixed(I, open) {
|
|
161
170
|
const items = [];
|
|
@@ -167,7 +176,7 @@ async function readdirAllFixed(I, open) {
|
|
|
167
176
|
flags: first ? I.QueryDirectoryFlag.RESTART_SCANS : 0,
|
|
168
177
|
fileIndex: 0,
|
|
169
178
|
fileId: open.fileId,
|
|
170
|
-
searchPattern:
|
|
179
|
+
searchPattern: "*", // IMMER "*", nie "" — Samba lehnt leer ab
|
|
171
180
|
outputBufferLength: 65536,
|
|
172
181
|
});
|
|
173
182
|
first = false;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Standalone diagnostic script for saltcorn-samba.
|
|
4
|
+
* Runs a series of individual SMB3 operations against a share so we can see
|
|
5
|
+
* exactly which primitive fails and why. Prints raw NT status codes.
|
|
6
|
+
*
|
|
7
|
+
* Usage (from the plugin directory, so smb3-client resolves):
|
|
8
|
+
*
|
|
9
|
+
* node tools/diag-basepath.js \
|
|
10
|
+
* --host 192.168.110.10 --share buero --user 01_vassen \
|
|
11
|
+
* --domain buero.ib-vassen.de --password '…' \
|
|
12
|
+
* --path static
|
|
13
|
+
*
|
|
14
|
+
* The script never writes; it only opens handles.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
const args = parseArgs(process.argv.slice(2));
|
|
21
|
+
const required = ["host", "share", "user", "path"];
|
|
22
|
+
for (const k of required) {
|
|
23
|
+
if (!args[k]) {
|
|
24
|
+
console.error("missing required --" + k);
|
|
25
|
+
process.exit(2);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const password = args.password || process.env.SMB_PASSWORD || "";
|
|
29
|
+
if (!password) {
|
|
30
|
+
console.error("missing --password (or SMB_PASSWORD env var)");
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { Client } = await import("smb3-client");
|
|
35
|
+
|
|
36
|
+
const client = new Client({
|
|
37
|
+
host: args.host,
|
|
38
|
+
port: Number(args.port) || 445,
|
|
39
|
+
domain: args.domain || "",
|
|
40
|
+
username: args.user,
|
|
41
|
+
password,
|
|
42
|
+
connectTimeout: 10_000,
|
|
43
|
+
requestTimeout: 30_000,
|
|
44
|
+
signing: args.signing || "if-offered",
|
|
45
|
+
encryption: args.encryption || "if-offered",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log("[1/7] Connecting …");
|
|
49
|
+
await client.connect();
|
|
50
|
+
console.log(" \u2713 TCP + Negotiate + Session-Setup + Auth OK");
|
|
51
|
+
|
|
52
|
+
const share = args.share;
|
|
53
|
+
const path = args.path;
|
|
54
|
+
const shareAndPath = share + "/" + path;
|
|
55
|
+
console.log("[2/7] TREE_CONNECT to \\\\" + args.host + "\\" + share + " \u2026");
|
|
56
|
+
try {
|
|
57
|
+
// Trigger TREE_CONNECT by touching the raw internal — but smb3-client
|
|
58
|
+
// does that implicitly on the first per-share call. So instead we call
|
|
59
|
+
// a cheap op below and let the connect surface the error there.
|
|
60
|
+
console.log(" (deferred until first op)");
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error(" \u2717 TREE_CONNECT failed:", e && e.message);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- Probes ------------------------------------------------------------
|
|
66
|
+
const probes = [
|
|
67
|
+
{ label: "readdir(share) \u2014 SHARE ROOT", op: () => client.readdir(share) },
|
|
68
|
+
{ label: "readdir(share/'') \u2014 empty subpath", op: () => client.readdir(share + "/") },
|
|
69
|
+
{ label: "stat(share/path) \u2014 target as-is", op: () => client.stat(shareAndPath) },
|
|
70
|
+
{ label: "readdir(share/path) \u2014 target as-is", op: () => client.readdir(shareAndPath) },
|
|
71
|
+
{ label: "readdir(share/PATH) \u2014 target uppercased", op: () => client.readdir(share + "/" + path.toUpperCase()) },
|
|
72
|
+
{ label: "readdir(share/path.lc) \u2014 target lowercased", op: () => client.readdir(share + "/" + path.toLowerCase()) },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
let step = 3;
|
|
76
|
+
for (const p of probes) {
|
|
77
|
+
console.log("[" + step + "/7] " + p.label);
|
|
78
|
+
try {
|
|
79
|
+
const r = await p.op();
|
|
80
|
+
const preview = Array.isArray(r)
|
|
81
|
+
? " (" + r.length + " entries" +
|
|
82
|
+
(r.length ? ", first: " +
|
|
83
|
+
JSON.stringify(r.slice(0, 3).map((e) => (typeof e === "string" ? e : e.name))) : "") + ")"
|
|
84
|
+
: " (" + JSON.stringify(r).slice(0, 120) + ")";
|
|
85
|
+
console.log(" \u2713 OK" + preview);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.log(" \u2717 " + (e && e.message ? e.message.split("\n")[0] : String(e)));
|
|
88
|
+
if (e && e.status) console.log(" NT status: 0x" + e.status.toString(16));
|
|
89
|
+
if (e && e.code) console.log(" error code: " + e.code);
|
|
90
|
+
}
|
|
91
|
+
step++;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await client.disconnect();
|
|
95
|
+
console.log("Done.");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseArgs(argv) {
|
|
99
|
+
const out = {};
|
|
100
|
+
for (let i = 0; i < argv.length; i++) {
|
|
101
|
+
const a = argv[i];
|
|
102
|
+
if (a.startsWith("--")) {
|
|
103
|
+
const key = a.slice(2);
|
|
104
|
+
const nxt = argv[i + 1];
|
|
105
|
+
if (!nxt || nxt.startsWith("--")) { out[key] = true; }
|
|
106
|
+
else { out[key] = nxt; i++; }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
main().catch((e) => {
|
|
113
|
+
console.error("FATAL:", e && e.stack || e);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
});
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* diag-wire.js — Wire-level QUERY_DIRECTORY diagnostic.
|
|
4
|
+
*
|
|
5
|
+
* Opens a connection, tree-connects, opens the target directory, then sends
|
|
6
|
+
* a hand-built QUERY_DIRECTORY packet with FULL HEX DUMP of both request
|
|
7
|
+
* and response. This bypasses smb3-client's encoders so we can compare bytes.
|
|
8
|
+
*
|
|
9
|
+
* Usage (from the plugin directory):
|
|
10
|
+
* node tools/diag-wire.js \
|
|
11
|
+
* --host 192.168.110.10 --share buero --user 01_vassen \
|
|
12
|
+
* --domain buero.ib-vassen.de --password '...' \
|
|
13
|
+
* --path static
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
"use strict";
|
|
17
|
+
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
const url = require("url");
|
|
21
|
+
|
|
22
|
+
async function loadInternals() {
|
|
23
|
+
const candidates = [
|
|
24
|
+
path.join(__dirname, "..", "node_modules", "smb3-client"),
|
|
25
|
+
path.join(__dirname, "..", "..", "node_modules", "smb3-client"),
|
|
26
|
+
];
|
|
27
|
+
let smb3Root = null;
|
|
28
|
+
for (const c of candidates) {
|
|
29
|
+
try {
|
|
30
|
+
fs.accessSync(path.join(c, "dist", "index.js"));
|
|
31
|
+
smb3Root = c;
|
|
32
|
+
break;
|
|
33
|
+
} catch (_) {}
|
|
34
|
+
}
|
|
35
|
+
if (!smb3Root) {
|
|
36
|
+
throw new Error("smb3-client not found in node_modules");
|
|
37
|
+
}
|
|
38
|
+
const distDir = path.join(smb3Root, "dist");
|
|
39
|
+
const load = (rel) => import(url.pathToFileURL(path.join(distDir, rel)).href);
|
|
40
|
+
|
|
41
|
+
const [
|
|
42
|
+
idx,
|
|
43
|
+
bufMod,
|
|
44
|
+
qdMod,
|
|
45
|
+
cmdMod,
|
|
46
|
+
qiMod,
|
|
47
|
+
createStructMod,
|
|
48
|
+
pathsMod,
|
|
49
|
+
errMod,
|
|
50
|
+
openMod,
|
|
51
|
+
] = await Promise.all([
|
|
52
|
+
load("index.js"),
|
|
53
|
+
load("wire/buffer.js"),
|
|
54
|
+
load("wire/structs/queryDirectory.js"),
|
|
55
|
+
load("wire/commands.js"),
|
|
56
|
+
load("wire/structs/queryInfo.js"),
|
|
57
|
+
load("wire/structs/create.js"),
|
|
58
|
+
load("paths.js"),
|
|
59
|
+
load("errors.js"),
|
|
60
|
+
load("open/open.js"),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
Client: idx.Client,
|
|
65
|
+
Writer: bufMod.Writer,
|
|
66
|
+
encodeQueryDirectoryRequest: qdMod.encodeQueryDirectoryRequest,
|
|
67
|
+
decodeQueryDirectoryResponse: qdMod.decodeQueryDirectoryResponse,
|
|
68
|
+
parseFileIdBothDirectoryInformation: qdMod.parseFileIdBothDirectoryInformation,
|
|
69
|
+
QueryDirectoryFlag: qdMod.QueryDirectoryFlag,
|
|
70
|
+
SmbCommand: cmdMod.SmbCommand,
|
|
71
|
+
NTStatus: cmdMod.NTStatus,
|
|
72
|
+
isSuccess: cmdMod.isSuccess,
|
|
73
|
+
statusName: cmdMod.statusName,
|
|
74
|
+
FileInformationClass: qiMod.FileInformationClass,
|
|
75
|
+
FileAccess: createStructMod.FileAccess,
|
|
76
|
+
ShareAccess: createStructMod.ShareAccess,
|
|
77
|
+
CreateDisposition: createStructMod.CreateDisposition,
|
|
78
|
+
CreateOptions: createStructMod.CreateOptions,
|
|
79
|
+
splitSharePath: pathsMod.splitSharePath,
|
|
80
|
+
toSmbPath: pathsMod.toSmbPath,
|
|
81
|
+
SmbError: errMod.SmbError,
|
|
82
|
+
Open: openMod.Open,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hex(buf, label) {
|
|
87
|
+
console.log(`\n--- ${label} (${buf.length} bytes) ---`);
|
|
88
|
+
for (let off = 0; off < buf.length; off += 16) {
|
|
89
|
+
const slice = buf.slice(off, Math.min(off + 16, buf.length));
|
|
90
|
+
const hexPart = Array.from(slice)
|
|
91
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
92
|
+
.join(" ")
|
|
93
|
+
.padEnd(48);
|
|
94
|
+
const asciiPart = Array.from(slice)
|
|
95
|
+
.map((b) => (b >= 32 && b < 127 ? String.fromCharCode(b) : "."))
|
|
96
|
+
.join("");
|
|
97
|
+
console.log(
|
|
98
|
+
`${off.toString(16).padStart(4, "0")} ${hexPart} ${asciiPart}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Build QUERY_DIRECTORY body byte-by-byte with named fields
|
|
104
|
+
function buildQdBody(I, opts) {
|
|
105
|
+
const {
|
|
106
|
+
fileInformationClass,
|
|
107
|
+
flags,
|
|
108
|
+
fileIndex,
|
|
109
|
+
fileId,
|
|
110
|
+
searchPattern,
|
|
111
|
+
outputBufferLength,
|
|
112
|
+
forceFileNameOffsetZero, // testing variant
|
|
113
|
+
} = opts;
|
|
114
|
+
|
|
115
|
+
const pat = Buffer.from(searchPattern, "utf16le");
|
|
116
|
+
const w = new I.Writer();
|
|
117
|
+
w.u16(33); // StructureSize
|
|
118
|
+
w.u8(fileInformationClass);
|
|
119
|
+
w.u8(flags);
|
|
120
|
+
w.u32(fileIndex);
|
|
121
|
+
w.bytes(fileId); // 16
|
|
122
|
+
const fileNameOffset = forceFileNameOffsetZero
|
|
123
|
+
? 0
|
|
124
|
+
: pat.length === 0
|
|
125
|
+
? 0
|
|
126
|
+
: 64 + 32;
|
|
127
|
+
w.u16(fileNameOffset);
|
|
128
|
+
w.u16(pat.length);
|
|
129
|
+
w.u32(outputBufferLength);
|
|
130
|
+
if (pat.length === 0) {
|
|
131
|
+
w.u8(0); // pad byte
|
|
132
|
+
} else {
|
|
133
|
+
w.bytes(pat);
|
|
134
|
+
}
|
|
135
|
+
return w.buffer();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function sendRaw(open, I, opts, label) {
|
|
139
|
+
const body = buildQdBody(I, opts);
|
|
140
|
+
hex(body, `${label} REQUEST BODY`);
|
|
141
|
+
console.log(
|
|
142
|
+
`Fields: StructSize=33, FileInformationClass=${opts.fileInformationClass}, ` +
|
|
143
|
+
`Flags=${opts.flags}, FileNameLength=${
|
|
144
|
+
Buffer.from(opts.searchPattern, "utf16le").length
|
|
145
|
+
}, ` +
|
|
146
|
+
`OutputBufferLength=${opts.outputBufferLength}`
|
|
147
|
+
);
|
|
148
|
+
const fnOff = body.readUInt16LE(32 - 8);
|
|
149
|
+
console.log(`FileNameOffset (from body): ${fnOff}`);
|
|
150
|
+
|
|
151
|
+
const signing = open.tree.session.makeSigning();
|
|
152
|
+
try {
|
|
153
|
+
const resp = await open.tree.conn.send(I.SmbCommand.QUERY_DIRECTORY, body, {
|
|
154
|
+
sessionId: open.tree.session.sessionId,
|
|
155
|
+
treeId: open.tree.treeId,
|
|
156
|
+
...(signing !== undefined ? { signing } : {}),
|
|
157
|
+
encrypt: open.tree.encryptRequired,
|
|
158
|
+
creditCharge: 1,
|
|
159
|
+
});
|
|
160
|
+
console.log(
|
|
161
|
+
`RESPONSE status: 0x${resp.header.status.toString(16)} (${I.statusName(
|
|
162
|
+
resp.header.status
|
|
163
|
+
)})`
|
|
164
|
+
);
|
|
165
|
+
hex(resp.body.slice(0, Math.min(resp.body.length, 128)), `${label} RESPONSE BODY (first 128)`);
|
|
166
|
+
if (I.isSuccess(resp.header.status)) {
|
|
167
|
+
const buf = I.decodeQueryDirectoryResponse(resp.body, 64);
|
|
168
|
+
const items = I.parseFileIdBothDirectoryInformation(buf);
|
|
169
|
+
console.log(
|
|
170
|
+
`Decoded ${items.length} entries: ${items.slice(0, 5).map((e) => e.fileName).join(", ")}`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return resp;
|
|
174
|
+
} catch (e) {
|
|
175
|
+
console.log(`SEND ERROR: ${e.message}`);
|
|
176
|
+
if (e.status) console.log(` NT status: 0x${e.status.toString(16)}`);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function main() {
|
|
182
|
+
const args = parseArgs(process.argv.slice(2));
|
|
183
|
+
const required = ["host", "share", "user", "path"];
|
|
184
|
+
for (const k of required) {
|
|
185
|
+
if (!args[k]) {
|
|
186
|
+
console.error(`missing required --${k}`);
|
|
187
|
+
process.exit(2);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const password = args.password || process.env.SMB_PASSWORD || "";
|
|
191
|
+
if (!password) {
|
|
192
|
+
console.error("missing --password");
|
|
193
|
+
process.exit(2);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const I = await loadInternals();
|
|
197
|
+
|
|
198
|
+
const client = new I.Client({
|
|
199
|
+
host: args.host,
|
|
200
|
+
port: Number(args.port) || 445,
|
|
201
|
+
domain: args.domain || "",
|
|
202
|
+
username: args.user,
|
|
203
|
+
password,
|
|
204
|
+
connectTimeout: 10000,
|
|
205
|
+
requestTimeout: 30000,
|
|
206
|
+
signing: args.signing || "if-offered",
|
|
207
|
+
encryption: args.encryption || "if-offered",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
console.log("Connecting…");
|
|
211
|
+
await client.connect();
|
|
212
|
+
console.log("Connected");
|
|
213
|
+
|
|
214
|
+
const fullPath = `${args.share}/${args.path}`;
|
|
215
|
+
const { share, rest } = I.splitSharePath(fullPath);
|
|
216
|
+
const tree = await client.treeFor(share);
|
|
217
|
+
console.log(`Tree connected: treeId=${tree.treeId}`);
|
|
218
|
+
|
|
219
|
+
// Open the target directory
|
|
220
|
+
const open = await I.Open.create(tree, {
|
|
221
|
+
filename: I.toSmbPath(rest),
|
|
222
|
+
desiredAccess:
|
|
223
|
+
I.FileAccess.FILE_READ_DATA | I.FileAccess.FILE_READ_ATTRIBUTES,
|
|
224
|
+
shareAccess: I.ShareAccess.READ | I.ShareAccess.WRITE | I.ShareAccess.DELETE,
|
|
225
|
+
createDisposition: I.CreateDisposition.OPEN,
|
|
226
|
+
createOptions: I.CreateOptions.DIRECTORY_FILE,
|
|
227
|
+
fileAttributes: 0,
|
|
228
|
+
});
|
|
229
|
+
console.log(`Opened dir, fileId hex: ${open.fileId.toString("hex")}`);
|
|
230
|
+
|
|
231
|
+
// ---------------- 6 probes ----------------
|
|
232
|
+
console.log("\n============ PROBE 1: pat='*', flags=RESTART, class=FileIdBothDir ============");
|
|
233
|
+
await sendRaw(
|
|
234
|
+
open,
|
|
235
|
+
I,
|
|
236
|
+
{
|
|
237
|
+
fileInformationClass: I.FileInformationClass.FileIdBothDirectoryInformation,
|
|
238
|
+
flags: I.QueryDirectoryFlag.RESTART_SCANS,
|
|
239
|
+
fileIndex: 0,
|
|
240
|
+
fileId: open.fileId,
|
|
241
|
+
searchPattern: "*",
|
|
242
|
+
outputBufferLength: 65536,
|
|
243
|
+
},
|
|
244
|
+
"P1"
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
console.log("\n============ PROBE 2: pat='*', flags=0, class=FileIdBothDir ============");
|
|
248
|
+
await sendRaw(
|
|
249
|
+
open,
|
|
250
|
+
I,
|
|
251
|
+
{
|
|
252
|
+
fileInformationClass: I.FileInformationClass.FileIdBothDirectoryInformation,
|
|
253
|
+
flags: 0,
|
|
254
|
+
fileIndex: 0,
|
|
255
|
+
fileId: open.fileId,
|
|
256
|
+
searchPattern: "*",
|
|
257
|
+
outputBufferLength: 65536,
|
|
258
|
+
},
|
|
259
|
+
"P2"
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
console.log("\n============ PROBE 3: pat='*', flags=RESTART, class=FileBothDir (0x03) ============");
|
|
263
|
+
await sendRaw(
|
|
264
|
+
open,
|
|
265
|
+
I,
|
|
266
|
+
{
|
|
267
|
+
fileInformationClass: I.FileInformationClass.FileBothDirectoryInformation ?? 3,
|
|
268
|
+
flags: I.QueryDirectoryFlag.RESTART_SCANS,
|
|
269
|
+
fileIndex: 0,
|
|
270
|
+
fileId: open.fileId,
|
|
271
|
+
searchPattern: "*",
|
|
272
|
+
outputBufferLength: 65536,
|
|
273
|
+
},
|
|
274
|
+
"P3"
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
console.log("\n============ PROBE 4: pat='*', flags=RESTART, class=FileDirInfo (0x01) ============");
|
|
278
|
+
await sendRaw(
|
|
279
|
+
open,
|
|
280
|
+
I,
|
|
281
|
+
{
|
|
282
|
+
fileInformationClass: I.FileInformationClass.FileDirectoryInformation ?? 1,
|
|
283
|
+
flags: I.QueryDirectoryFlag.RESTART_SCANS,
|
|
284
|
+
fileIndex: 0,
|
|
285
|
+
fileId: open.fileId,
|
|
286
|
+
searchPattern: "*",
|
|
287
|
+
outputBufferLength: 65536,
|
|
288
|
+
},
|
|
289
|
+
"P4"
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
console.log("\n============ PROBE 5: pat='*', outBuf=8192, flags=RESTART ============");
|
|
293
|
+
await sendRaw(
|
|
294
|
+
open,
|
|
295
|
+
I,
|
|
296
|
+
{
|
|
297
|
+
fileInformationClass: I.FileInformationClass.FileIdBothDirectoryInformation,
|
|
298
|
+
flags: I.QueryDirectoryFlag.RESTART_SCANS,
|
|
299
|
+
fileIndex: 0,
|
|
300
|
+
fileId: open.fileId,
|
|
301
|
+
searchPattern: "*",
|
|
302
|
+
outputBufferLength: 8192,
|
|
303
|
+
},
|
|
304
|
+
"P5"
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
console.log("\n============ PROBE 6: pat='', flags=RESTART (empty pattern, offset=0) ============");
|
|
308
|
+
await sendRaw(
|
|
309
|
+
open,
|
|
310
|
+
I,
|
|
311
|
+
{
|
|
312
|
+
fileInformationClass: I.FileInformationClass.FileIdBothDirectoryInformation,
|
|
313
|
+
flags: I.QueryDirectoryFlag.RESTART_SCANS,
|
|
314
|
+
fileIndex: 0,
|
|
315
|
+
fileId: open.fileId,
|
|
316
|
+
searchPattern: "",
|
|
317
|
+
outputBufferLength: 65536,
|
|
318
|
+
},
|
|
319
|
+
"P6"
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
console.log("\nAvailable FileInformationClass values:");
|
|
323
|
+
console.log(JSON.stringify(I.FileInformationClass, null, 2));
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
await open.close();
|
|
327
|
+
} catch (_) {}
|
|
328
|
+
console.log("\nDone.");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function parseArgs(argv) {
|
|
332
|
+
const out = {};
|
|
333
|
+
for (let i = 0; i < argv.length; i++) {
|
|
334
|
+
const a = argv[i];
|
|
335
|
+
if (a.startsWith("--")) {
|
|
336
|
+
const key = a.slice(2);
|
|
337
|
+
const nxt = argv[i + 1];
|
|
338
|
+
if (!nxt || nxt.startsWith("--")) {
|
|
339
|
+
out[key] = true;
|
|
340
|
+
} else {
|
|
341
|
+
out[key] = nxt;
|
|
342
|
+
i++;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
main().catch((e) => {
|
|
350
|
+
console.error("FATAL:", (e && e.stack) || e);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
});
|