saltcorn-samba 0.4.11 → 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 +101 -0
- package/package.json +2 -1
- package/readdir-compat.js +247 -419
- package/smb-client.js +37 -83
- package/tools/diag-basepath.js +115 -0
- package/tools/diag-wire.js +352 -0
package/smb-client.js
CHANGED
|
@@ -224,38 +224,19 @@ async function buildClient(config) {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
/**
|
|
227
|
-
* Convert a
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* only carries name/isFile/isDirectory. For very large directories this
|
|
233
|
-
* would fan out into many stat calls — that's an acceptable trade-off
|
|
234
|
-
* for now; a future release can optimise by using SMB2_QUERY_DIRECTORY's
|
|
235
|
-
* FileBothDirectoryInformation output directly.
|
|
227
|
+
* Convert a readdirCompat rich-Dirent (which already carries name,
|
|
228
|
+
* size, mtime, ctime, and isDirectory()) directly to the legacy
|
|
229
|
+
* `@marsaud/smb2`-compatible shape the rest of the plugin expects.
|
|
230
|
+
* No extra stat roundtrip needed — v0.4.12 gets attributes straight
|
|
231
|
+
* from the QUERY_DIRECTORY response.
|
|
236
232
|
*/
|
|
237
|
-
|
|
238
|
-
const isDir = !!dirent.isDirectory();
|
|
239
|
-
const fullPath = parentFullPath
|
|
240
|
-
? parentFullPath + "/" + dirent.name
|
|
241
|
-
: dirent.name;
|
|
242
|
-
let size = 0;
|
|
243
|
-
let mtime;
|
|
244
|
-
let birthtime;
|
|
245
|
-
try {
|
|
246
|
-
const st = await client.stat(fullPath);
|
|
247
|
-
size = Number(st.size || 0);
|
|
248
|
-
mtime = st.mtime;
|
|
249
|
-
birthtime = st.ctime;
|
|
250
|
-
} catch (_) {
|
|
251
|
-
// Non-fatal; return whatever we already have.
|
|
252
|
-
}
|
|
233
|
+
function mapDirent(dirent) {
|
|
253
234
|
return {
|
|
254
235
|
name: dirent.name,
|
|
255
|
-
isDirectory:
|
|
256
|
-
size,
|
|
257
|
-
mtime,
|
|
258
|
-
birthtime,
|
|
236
|
+
isDirectory: !!(dirent.isDirectory && dirent.isDirectory()),
|
|
237
|
+
size: Number(dirent.size || 0),
|
|
238
|
+
mtime: dirent.mtime,
|
|
239
|
+
birthtime: dirent.ctime,
|
|
259
240
|
};
|
|
260
241
|
}
|
|
261
242
|
|
|
@@ -274,39 +255,18 @@ async function buildClient(config) {
|
|
|
274
255
|
*/
|
|
275
256
|
async readdir(rel) {
|
|
276
257
|
const full = resolvePath(rel);
|
|
277
|
-
// v0.4.
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
// point trying it, and it would hide the diagnostics compat has
|
|
285
|
-
// gathered. Instead we rethrow the compat error verbatim so the
|
|
286
|
-
// caller sees the true root cause.
|
|
287
|
-
let compatErr = null;
|
|
288
|
-
try {
|
|
289
|
-
return await readdirCompat(client, full);
|
|
290
|
-
} catch (err) {
|
|
291
|
-
compatErr = err;
|
|
292
|
-
const compatMsg = String((err && err.message) || err || "");
|
|
293
|
-
// If compat ran into the exact same info-class rejection with
|
|
294
|
-
// ALL classes exhausted, the built-in readdir cannot possibly
|
|
295
|
-
// help — it uses class 37, which compat already tried. Rethrow
|
|
296
|
-
// now so callers see the real reason.
|
|
297
|
-
if (/0xC0000033|OBJECT_NAME_INVALID/i.test(compatMsg) &&
|
|
298
|
-
/all classes exhausted|no working FileInformationClass/i.test(compatMsg)) {
|
|
299
|
-
throw err;
|
|
300
|
-
}
|
|
301
|
-
// Otherwise (compat failed for structural reasons like an unknown
|
|
302
|
-
// internal-module path, or a transient socket error) fall through
|
|
303
|
-
// to the classic path so the existing German error rewrites still
|
|
304
|
-
// apply.
|
|
305
|
-
}
|
|
258
|
+
// v0.4.12: readdirCompat ist der ALLEINIGE readdir-Pfad. Grund:
|
|
259
|
+
// smb3-client@0.2.0 hat einen Wire-Format-Bug in
|
|
260
|
+
// encodeQueryDirectoryRequest, der gegen Samba 4.23 zu
|
|
261
|
+
// STATUS_OBJECT_NAME_INVALID (0xC0000033) führt. readdirCompat
|
|
262
|
+
// implementiert QUERY_DIRECTORY MS-SMB2-§2.2.33-konform und
|
|
263
|
+
// umgeht damit den Bug. Es gibt keinen Fallback auf
|
|
264
|
+
// client.readdir(), weil das den bekannten Bug hervorruft.
|
|
306
265
|
let dirents;
|
|
307
266
|
try {
|
|
308
|
-
dirents = await client
|
|
309
|
-
} catch (
|
|
267
|
+
dirents = await readdirCompat(client, full, { rich: true });
|
|
268
|
+
} catch (errCompat) {
|
|
269
|
+
const err = errCompat;
|
|
310
270
|
// Some Samba builds (observed on 4.20+/4.23+) reject
|
|
311
271
|
// SMB2 QUERY_DIRECTORY on the *share root* with
|
|
312
272
|
// STATUS_OBJECT_NAME_INVALID (0xC0000033) because smb3-client
|
|
@@ -318,18 +278,20 @@ async function buildClient(config) {
|
|
|
318
278
|
// require the caller to configure a real base_path so every
|
|
319
279
|
// readdir happens inside a directory the server can enumerate.
|
|
320
280
|
const msg = String((err && err.message) || err || "");
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
281
|
+
// Falls der Compat-Pfad selbst mit 0xC0000033 zurückkommt,
|
|
282
|
+
// liegt entweder ein weiterer, unbekannter Wire-Bug vor oder
|
|
283
|
+
// Samba lehnt aus einem anderen Grund ab. Wir geben eine
|
|
284
|
+
// gezielte Meldung mit Hinweis auf den Bug-Report.
|
|
285
|
+
if (/0xC0000033|OBJECT_NAME_INVALID/i.test(msg)) {
|
|
325
286
|
const e = new Error(
|
|
326
|
-
"
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
287
|
+
"Samba lehnt QUERY_DIRECTORY auf diesem Pfad ab " +
|
|
288
|
+
"(STATUS_OBJECT_NAME_INVALID, 0xC0000033). " +
|
|
289
|
+
"Der bekannte smb3-client-Wire-Bug wurde in v0.4.12 gefixt — " +
|
|
290
|
+
"falls dieser Fehler weiterhin auftritt, liegt ein zweiter, " +
|
|
291
|
+
"noch unbekannter Wire-Format-Fehler vor. Bitte " +
|
|
292
|
+
"tools/diag-basepath.js ausführen und Report zurückschicken. " +
|
|
293
|
+
"Details: " +
|
|
294
|
+
msg
|
|
333
295
|
);
|
|
334
296
|
e.cause = err;
|
|
335
297
|
throw e;
|
|
@@ -358,18 +320,10 @@ async function buildClient(config) {
|
|
|
358
320
|
}
|
|
359
321
|
throw err;
|
|
360
322
|
}
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
for (let i = 0; i < dirents.length; i += CHUNK) {
|
|
366
|
-
const slice = dirents.slice(i, i + CHUNK);
|
|
367
|
-
const enriched = await Promise.all(
|
|
368
|
-
slice.map((d) => enrichEntry(d, full))
|
|
369
|
-
);
|
|
370
|
-
result.push(...enriched);
|
|
371
|
-
}
|
|
372
|
-
return result;
|
|
323
|
+
// v0.4.12: readdirCompat gibt schon Rich-Dirents zurück (Name,
|
|
324
|
+
// Größe, mtime, ctime, isDirectory). Kein zusätzlicher stat-Fan-out
|
|
325
|
+
// nötig — ein einziger QUERY_DIRECTORY-Roundtrip liefert alles.
|
|
326
|
+
return dirents.map(mapDirent);
|
|
373
327
|
},
|
|
374
328
|
|
|
375
329
|
/** Stat a single file/directory (name-relative-to-share/basePath). */
|
|
@@ -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
|
+
});
|