dsh-winrm 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -7
- package/README.zh.md +14 -7
- package/lib/client.js +73 -73
- package/lib/index.js +1184 -134
- package/lib/types/engine/client.d.ts +53 -6
- package/lib/types/engine/wsman.d.ts +153 -0
- package/lib/types/engine.d.ts +4 -4
- package/lib/types/powershell.d.ts +194 -27
- package/lib/types/smb.d.ts +24 -5
- package/package.json +17 -16
- package/scripts/verify-live.mts +116 -0
- package/src/engine/client.ts +460 -24
- package/src/engine/wsman.ts +357 -0
- package/src/engine.ts +104 -40
- package/src/powershell.ts +491 -60
- package/src/smb.ts +57 -5
- package/src/tools.ts +2 -2
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* WinRM transport bridge.
|
|
3
3
|
*
|
|
4
4
|
* Runs each PowerShell payload over a native Node.js WinRM client
|
|
5
|
-
* (winrm-client) — no Python/pywinrm subprocess. The script
|
|
6
|
-
*
|
|
7
|
-
* any codepage output survives the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* (winrm-client) — no Python/pywinrm subprocess. The script is delivered on
|
|
6
|
+
* the WinRS **stdin** stream and answers on the UTF-8 base64 envelope, so
|
|
7
|
+
* Chinese and any codepage output survives the transport losslessly and the
|
|
8
|
+
* command line stays a constant, ASCII-safe line that WinRS/cmd.exe cannot
|
|
9
|
+
* mangle. See powershell.ts for why the command line cannot carry the script.
|
|
10
10
|
*
|
|
11
11
|
* Auth is chosen per attempt (HTTPS: Basic first, then NTLM; HTTP: NTLM
|
|
12
12
|
* first, then Basic) with fallback, so a host that only enables one scheme
|
|
@@ -44,6 +44,53 @@ export declare function testConnection(conn: WinRMParams): Promise<{
|
|
|
44
44
|
error?: string;
|
|
45
45
|
}>;
|
|
46
46
|
export declare const TRANSFER_CHUNK: number;
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Size of a remote file, or null when the path does not exist or is a
|
|
49
|
+
* directory. The distinction matters: a caller that treats "absent" as "empty"
|
|
50
|
+
* writes an empty local file and reports success for a download that copied
|
|
51
|
+
* nothing.
|
|
52
|
+
*/
|
|
53
|
+
export declare function remoteFileSize(conn: WinRMParams, remotePath: string, timeoutMs?: number): Promise<number | null>;
|
|
48
54
|
export declare function downloadChunks(conn: WinRMParams, remotePath: string, onChunk: (b64: string, bytes: number) => void, onProgress?: (transferred: number, total: number) => void, timeoutMs?: number): Promise<number>;
|
|
49
55
|
export declare function uploadBuffer(conn: WinRMParams, remotePath: string, data: Buffer, onProgress?: (transferred: number, total: number) => void, timeoutMs?: number): Promise<number>;
|
|
56
|
+
/**
|
|
57
|
+
* Upload a buffer over a single WinRS stdin stream.
|
|
58
|
+
*
|
|
59
|
+
* This replaces the per-chunk shell cycle: one shell, one command and one
|
|
60
|
+
* `Send` per 256 KiB, against a receiver that drains the stream straight into
|
|
61
|
+
* the file. Measured at ~10 s for 2 MiB where the old path took 310 s.
|
|
62
|
+
*
|
|
63
|
+
* Returns null — rather than throwing — when the transport this path depends
|
|
64
|
+
* on is unavailable (a future `winrm-client` layout that drops the internal
|
|
65
|
+
* module it borrows), so the caller can fall back to {@link uploadBuffer}
|
|
66
|
+
* instead of failing the transfer.
|
|
67
|
+
* @param conn - connection parameters.
|
|
68
|
+
* @param remotePath - destination path on the target.
|
|
69
|
+
* @param data - the file contents to send.
|
|
70
|
+
* @param onProgress - called with cumulative bytes after each frame.
|
|
71
|
+
* @param timeoutMs - whole-transfer budget.
|
|
72
|
+
* @returns bytes written, or null when the streaming path is unavailable.
|
|
73
|
+
*/
|
|
74
|
+
export declare function uploadStream(conn: WinRMParams, remotePath: string, data: Buffer, onProgress?: (transferred: number, total: number) => void, timeoutMs?: number): Promise<number | null>;
|
|
75
|
+
/**
|
|
76
|
+
* Stream a remote file to a consumer, in parallel byte ranges.
|
|
77
|
+
*
|
|
78
|
+
* The old path asked for 48 KiB per shell lifecycle, so a 2 MiB file cost 43
|
|
79
|
+
* round-trip cycles and 129 s. Measured against this target, one `Receive`
|
|
80
|
+
* returns at most 128 KiB no matter how large an envelope is declared, which
|
|
81
|
+
* makes a download latency-bound rather than bandwidth-bound; the fix is
|
|
82
|
+
* therefore to run several ranges at once. Each worker opens its own shell and
|
|
83
|
+
* streams its range gzip-compressed, and because ranges are absolute, the
|
|
84
|
+
* caller can place each piece by offset and completion order does not matter.
|
|
85
|
+
*
|
|
86
|
+
* The remote side compresses unconditionally: a compressible 16 MiB file
|
|
87
|
+
* arrives in one round trip (2.0 s), and an incompressible file was measured no
|
|
88
|
+
* slower than raw (9457 ms vs 9582 ms).
|
|
89
|
+
* @param conn - connection parameters.
|
|
90
|
+
* @param remotePath - file to read.
|
|
91
|
+
* @param onData - receives each decompressed piece with its absolute file offset.
|
|
92
|
+
* @param onProgress - receives cumulative bytes delivered and the total size.
|
|
93
|
+
* @param timeoutMs - budget for the whole download.
|
|
94
|
+
* @returns the number of bytes delivered, or null when streaming is unavailable.
|
|
95
|
+
*/
|
|
96
|
+
export declare function downloadStream(conn: WinRMParams, remotePath: string, onData: (offset: number, data: Buffer) => void, onProgress?: (transferred: number, total: number) => void, timeoutMs?: number): Promise<number | null>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw WSMan `Send` for the streaming upload path.
|
|
3
|
+
*
|
|
4
|
+
* The bundled `winrm-client` can only put text on the WinRS stdin stream: its
|
|
5
|
+
* `doSendInput` base64-encodes the input *again* on top of whatever the caller
|
|
6
|
+
* already encoded, and it has no way to close the stream. Both are fatal for a
|
|
7
|
+
* bulk transfer — double base64 costs 1.78x the bytes on the wire, and a
|
|
8
|
+
* stdin stream that never ends cannot tell the remote receiver "that is all".
|
|
9
|
+
*
|
|
10
|
+
* This module therefore builds the `Send` envelope itself and posts it through
|
|
11
|
+
* the library's own `sendHttp`, which keeps Basic/NTLM/SPNEGO and the HTTPS
|
|
12
|
+
* options exactly as the rest of the transport uses them. The stream carries
|
|
13
|
+
* raw file bytes, so the wire cost is 1.0x plus the envelope.
|
|
14
|
+
*
|
|
15
|
+
* The library is reached through `createRequire` rather than a static import
|
|
16
|
+
* because `dist/src/utils/http.js` is an internal CommonJS path with no
|
|
17
|
+
* `exports` map behind it. `loadSendHttp` returns null when that path is gone
|
|
18
|
+
* (a future `winrm-client` layout change), which lets the caller fall back to
|
|
19
|
+
* the old chunked path instead of failing the transfer.
|
|
20
|
+
*/
|
|
21
|
+
/** Connection parameters for one target (mirrors `WinRMParams` in client.ts). */
|
|
22
|
+
export interface WsmanTarget {
|
|
23
|
+
host: string;
|
|
24
|
+
port: number;
|
|
25
|
+
path: string;
|
|
26
|
+
username: string;
|
|
27
|
+
password: string;
|
|
28
|
+
authMethod: 'basic' | 'ntlm';
|
|
29
|
+
useHttps?: boolean;
|
|
30
|
+
rejectUnauthorized?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** `sendHttp` as the bundled library declares it. */
|
|
33
|
+
type SendHttp = (data: string, host: string, port: number, path: string, username: string, password: string, authMethod: 'basic' | 'ntlm', timeout?: number, useHttps?: boolean, rejectUnauthorized?: boolean) => Promise<unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* Envelope ceiling declared on every Send, in bytes.
|
|
36
|
+
*
|
|
37
|
+
* This is what the *client* promises the server it will never exceed, and the
|
|
38
|
+
* server enforces its own `MaxEnvelopeSizekb` on top. 512000 is the value the
|
|
39
|
+
* target host reports, and it is far above the library's hardcoded 153600 —
|
|
40
|
+
* raising it is what makes 256 KiB chunks legal.
|
|
41
|
+
*/
|
|
42
|
+
export declare const WS_MAX_ENVELOPE_BYTES = 512000;
|
|
43
|
+
/**
|
|
44
|
+
* Bytes of XML wrapper around one base64 chunk.
|
|
45
|
+
*
|
|
46
|
+
* Measured against the envelope this module emits: fixed header, selector set,
|
|
47
|
+
* body tags and the message id. Used to size chunks against the ceiling before
|
|
48
|
+
* sending, so a normal transfer never needs a size rejection to converge.
|
|
49
|
+
*/
|
|
50
|
+
export declare const SEND_ENVELOPE_OVERHEAD_BYTES = 1600;
|
|
51
|
+
/** The largest raw slice whose base64 form still fits `WS_MAX_ENVELOPE_BYTES`. */
|
|
52
|
+
export declare const MAX_SEND_SLICE_BYTES: number;
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the library's `sendHttp`, or null when the internal path is absent.
|
|
55
|
+
* @returns the send function, or null when it cannot be loaded.
|
|
56
|
+
*/
|
|
57
|
+
export declare function loadSendHttp(): SendHttp | null;
|
|
58
|
+
/**
|
|
59
|
+
* Build one WSMan `Send` envelope carrying `chunk` on the stdin stream.
|
|
60
|
+
*
|
|
61
|
+
* The header mirrors what the library emits, with three deliberate changes:
|
|
62
|
+
* `MaxEnvelopeSize` is raised to `limit`, the `Send` action replaces the
|
|
63
|
+
* command's, and the stream body is the caller's base64 verbatim — no second
|
|
64
|
+
* encode. `Name="stdin"` and `CommandId` are what bind the bytes to the
|
|
65
|
+
* running receiver's standard input.
|
|
66
|
+
* @param shellId - target shell from `Shell.doCreateShell`.
|
|
67
|
+
* @param commandId - target command from `Command.doExecuteCommand`.
|
|
68
|
+
* @param chunk - raw bytes to place on the remote stdin stream.
|
|
69
|
+
* @param limit - envelope ceiling to declare.
|
|
70
|
+
* @returns the SOAP envelope as a UTF-8 string.
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildSendEnvelope(shellId: string, commandId: string, chunk: Buffer, limit: number): string;
|
|
73
|
+
/**
|
|
74
|
+
* Push one raw slice onto the remote stdin stream.
|
|
75
|
+
*
|
|
76
|
+
* A rejection here is safe to retry with a smaller slice: the server validates
|
|
77
|
+
* the envelope size before dispatching it to the shell, so nothing reached the
|
|
78
|
+
* receiver's stdin when the call fails on size.
|
|
79
|
+
* @param target - connection parameters (auth already selected).
|
|
80
|
+
* @param shellId - target shell.
|
|
81
|
+
* @param commandId - target command.
|
|
82
|
+
* @param chunk - raw bytes to send.
|
|
83
|
+
* @param sendHttp - the resolved library function.
|
|
84
|
+
* @param timeoutMs - per-request HTTP timeout.
|
|
85
|
+
*/
|
|
86
|
+
export declare function sendStdinChunk(target: WsmanTarget, shellId: string, commandId: string, chunk: Buffer, sendHttp: SendHttp, timeoutMs: number): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Whether a failed Send was refused for exceeding the envelope ceiling.
|
|
89
|
+
*
|
|
90
|
+
* WSMan reports this as a SOAP fault rather than a status code, and the exact
|
|
91
|
+
* wording differs between the HTTP and HTTPS stacks, so the test looks for any
|
|
92
|
+
* of the size-related phrasings plus the plain 413 status. It deliberately
|
|
93
|
+
* does not match a bare "size" in unrelated text.
|
|
94
|
+
* @param error - the rejection to classify.
|
|
95
|
+
* @returns true when retrying with a smaller slice is the right response.
|
|
96
|
+
*/
|
|
97
|
+
export declare function isEnvelopeTooLarge(error: unknown): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Build one WSMan `Receive` envelope asking for stdout and stderr.
|
|
100
|
+
*
|
|
101
|
+
* The library's own `buildReceiveOutputRequest` cannot be reused here because
|
|
102
|
+
* it hardcodes `MaxEnvelopeSize` at 153600 in its shared header. Measured
|
|
103
|
+
* against this target, that ceiling is what caps each response at 128 KiB —
|
|
104
|
+
* raising it to 512000 lets one `Receive` return 128 KiB per stream, which is
|
|
105
|
+
* the most the server will put in a single response regardless.
|
|
106
|
+
* @param shellId - target shell.
|
|
107
|
+
* @param commandId - target command whose streams are wanted.
|
|
108
|
+
* @param limit - envelope ceiling to declare.
|
|
109
|
+
* @returns the SOAP envelope as a UTF-8 string.
|
|
110
|
+
*/
|
|
111
|
+
export declare function buildReceiveEnvelope(shellId: string, commandId: string, limit: number): string;
|
|
112
|
+
/** One decoded `rsp:Stream` element from a Receive response. */
|
|
113
|
+
export interface ReceivedStream {
|
|
114
|
+
/** `Name` attribute: `stdout` or `stderr`. */
|
|
115
|
+
name: string;
|
|
116
|
+
/** Decoded payload bytes; empty for a stream element that carried no text. */
|
|
117
|
+
data: Buffer;
|
|
118
|
+
/** True when the `End` attribute marks this stream closed. */
|
|
119
|
+
end: boolean;
|
|
120
|
+
}
|
|
121
|
+
/** A decoded Receive response: its streams plus the command's exit code once known. */
|
|
122
|
+
export interface ReceiveResult {
|
|
123
|
+
streams: ReceivedStream[];
|
|
124
|
+
/** `rsp:CommandState.rsp:ExitCode`, or undefined while the command still runs. */
|
|
125
|
+
exitCode?: number;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Read the streams and exit code out of a Receive response.
|
|
129
|
+
*
|
|
130
|
+
* The library's `extractStreams` cannot be used for binary payloads: it decodes
|
|
131
|
+
* every stream through `extractText`, which falls back to `String(obj)` for an
|
|
132
|
+
* element that has attributes but no text node — turning the terminal
|
|
133
|
+
* `End="true"` marker into the literal string `'[object Object]'` — and the
|
|
134
|
+
* library's `doReceiveOutputNonBlocking` then `.trim()`s the decoded text,
|
|
135
|
+
* which would eat whitespace bytes at either end of a real payload. This
|
|
136
|
+
* reader keeps the distinction between "no text" (zero bytes) and "text" and
|
|
137
|
+
* never trims.
|
|
138
|
+
* @param response - the parsed XML object returned by `sendHttp`.
|
|
139
|
+
* @returns the decoded streams and exit code.
|
|
140
|
+
* @throws when a stream's text node cannot be decoded as base64.
|
|
141
|
+
*/
|
|
142
|
+
export declare function readReceiveResult(response: unknown): ReceiveResult;
|
|
143
|
+
/**
|
|
144
|
+
* Fetch one Receive response for a running command.
|
|
145
|
+
* @param target - connection parameters (auth already selected).
|
|
146
|
+
* @param shellId - target shell.
|
|
147
|
+
* @param commandId - target command.
|
|
148
|
+
* @param sendHttp - the resolved library function.
|
|
149
|
+
* @param timeoutMs - per-request HTTP timeout.
|
|
150
|
+
* @returns the decoded streams and exit code.
|
|
151
|
+
*/
|
|
152
|
+
export declare function receiveOutput(target: WsmanTarget, shellId: string, commandId: string, sendHttp: SendHttp, timeoutMs: number): Promise<ReceiveResult>;
|
|
153
|
+
export {};
|
package/lib/types/engine.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The WinRM engine facade: host lookup, PowerShell exec (UTF-8 envelope),
|
|
3
|
-
* service & process management, directory listing,
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* service & process management, directory listing, file transfer, cluster
|
|
4
|
+
* execution and the streaming console. The heavy lifting lives in engine/
|
|
5
|
+
* (client, console); this class composes them behind one WinRmEngine instance
|
|
6
|
+
* per plugin apply.
|
|
7
7
|
*/
|
|
8
8
|
import type { ClusterResult, ExecResult, ProcessInfo, RemoteDirEntry, ServiceInfo, TestResult, TransferProgress, WinHostSummary } from './protocol.ts';
|
|
9
9
|
import type { HostStore } from './store.ts';
|
|
@@ -1,28 +1,89 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PowerShell snippet builders
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* PowerShell snippet builders and the WinRM UTF-8 envelope.
|
|
3
|
+
*
|
|
4
|
+
* The user script is delivered on the WinRS **stdin** stream, not on the
|
|
5
|
+
* command line. WinRS hands the command line to cmd.exe, which caps it at
|
|
6
|
+
* ~8191 characters; `-EncodedCommand` inflates a script by 2.67x
|
|
7
|
+
* (UTF-16LE → base64), so the command line can carry only about 1.5 KB of
|
|
8
|
+
* script. That silently broke every large payload — most visibly the 48 KiB
|
|
9
|
+
* transfer chunk, whose embedded base64 alone is ~65K characters.
|
|
10
|
+
*
|
|
11
|
+
* So the command line is now a *constant* outer script and the user script
|
|
12
|
+
* rides stdin, which has no such limit (verified to 200 KB).
|
|
13
|
+
*
|
|
14
|
+
* The outer script reads a byte count, then that many bytes, starts a child
|
|
15
|
+
* `powershell.exe` and feeds it the script on its stdin. The process boundary
|
|
16
|
+
* is what makes `exit N` and an uncaught `throw` survivable: they terminate
|
|
17
|
+
* the *child*, and the outer script is still alive to print the envelope.
|
|
18
|
+
* `-EncodedCommand` for the child also sidesteps the execution policy
|
|
19
|
+
* entirely — the policy governs `.ps1` files, not encoded commands — so no
|
|
20
|
+
* temp file is written and no policy block can lose the envelope.
|
|
21
|
+
*
|
|
22
|
+
* The child body is `& (...) 2>&1 | Out-String -Stream`. Both halves matter:
|
|
23
|
+
* `2>&1` pulls error records into the success stream, `Out-String -Stream`
|
|
24
|
+
* stringifies them onto stdout *line by line*. Plain `Out-String` would also
|
|
25
|
+
* stringify them but buffers the whole pipeline, discarding everything
|
|
26
|
+
* produced before a mid-script `exit`; bare `2>&1` without `Out-String`
|
|
27
|
+
* leaves the records to be re-serialized as CLIXML on stderr, where the
|
|
28
|
+
* envelope's CLIXML stripping deletes the text outright.
|
|
29
|
+
*
|
|
30
|
+
* The envelope is three lines — a marker with the exit code, then one base64
|
|
31
|
+
* line per stream:
|
|
32
|
+
*
|
|
33
|
+
* __DSH_WINRM__<code>
|
|
34
|
+
* O:<base64 of UTF-8 stdout>
|
|
35
|
+
* E:<base64 of UTF-8 stderr>
|
|
36
|
+
*
|
|
37
|
+
* Every payload is ASCII, so the transport cannot corrupt it and Chinese
|
|
38
|
+
* output survives losslessly. The `O:` / `E:` labels are load-bearing: the
|
|
39
|
+
* WinRM client trims each response chunk, which can eat the newline between
|
|
40
|
+
* two lines, and base64 never contains `:`, so the labels stay parseable
|
|
41
|
+
* even when the separators vanish.
|
|
6
42
|
*/
|
|
7
43
|
/** Marker line prefix carrying the exit code: '__DSH_WINRM__<code>'. */
|
|
8
44
|
export declare const ENVELOPE_MARKER = "__DSH_WINRM__";
|
|
45
|
+
/** Max characters per stdin Send; split on code points, never mid-surrogate-pair. */
|
|
46
|
+
export declare const STDIN_CHUNK_CHARS = 16384;
|
|
9
47
|
/** PS single-quoted literal (doubles embedded quotes). */
|
|
10
48
|
export declare function sq(value: string): string;
|
|
11
49
|
/** Base64 of a UTF-16LE PowerShell script, for -EncodedCommand. */
|
|
12
50
|
export declare function encodeCommand(script: string): string;
|
|
13
51
|
/**
|
|
14
|
-
* The
|
|
15
|
-
*
|
|
52
|
+
* The constant powershell.exe command line that runs the envelope.
|
|
53
|
+
*
|
|
54
|
+
* Deliberately takes no payload: keeping the command line under cmd.exe's
|
|
55
|
+
* ~8191-character cap is the whole point of the stdin channel.
|
|
16
56
|
*/
|
|
17
|
-
export declare function
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
57
|
+
export declare function powershellCommandLine(): string;
|
|
58
|
+
/**
|
|
59
|
+
* The constant powershell.exe command line for a streaming upload.
|
|
60
|
+
*
|
|
61
|
+
* The receiver travels on stdin right behind it, so this stays well clear of
|
|
62
|
+
* cmd.exe's cap and does not change when the receiver does.
|
|
63
|
+
*/
|
|
64
|
+
export declare function streamCommandLine(): string;
|
|
65
|
+
/**
|
|
66
|
+
* The stdin payload for one command: the UTF-8 **byte** count, a newline, then
|
|
67
|
+
* the raw script.
|
|
68
|
+
*
|
|
69
|
+
* The count is in bytes, not characters, because the outer script sizes its
|
|
70
|
+
* receive buffer from it — a script with multi-byte characters would otherwise
|
|
71
|
+
* be truncated mid-character.
|
|
72
|
+
* @param userScript - the PowerShell script to run.
|
|
73
|
+
* @returns the payload to send on WinRS stdin.
|
|
74
|
+
*/
|
|
75
|
+
export declare function scriptPayload(userScript: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Split a payload into Send-sized pieces without cutting a surrogate pair.
|
|
78
|
+
*
|
|
79
|
+
* `Command.doSendInput` encodes each piece to UTF-8 independently, so a lone
|
|
80
|
+
* surrogate at a chunk boundary would be replaced with U+FFFD and corrupt the
|
|
81
|
+
* script. Iterating with `for…of` yields whole code points, so a pair always
|
|
82
|
+
* stays in one piece.
|
|
83
|
+
* @param payload - the full stdin payload.
|
|
84
|
+
* @returns the pieces to send in order; never empty.
|
|
85
|
+
*/
|
|
86
|
+
export declare function splitScriptPayload(payload: string): string[];
|
|
26
87
|
/** JSON: full service table (CIM). */
|
|
27
88
|
export declare function psListServices(): string;
|
|
28
89
|
/** JSON: one service after an action. */
|
|
@@ -33,7 +94,12 @@ export declare function psListProcesses(): string;
|
|
|
33
94
|
export declare function psKillProcess(id: number): string;
|
|
34
95
|
/** JSON: directory listing. */
|
|
35
96
|
export declare function psListDir(dir: string): string;
|
|
36
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* Plain: remote file size in bytes. Emits nothing at all when the path is
|
|
99
|
+
* missing or is a directory, so the caller can tell "absent" from "empty" —
|
|
100
|
+
* both used to collapse to the string "0", which made a download of a
|
|
101
|
+
* nonexistent file look like a successful transfer of a zero-byte one.
|
|
102
|
+
*/
|
|
37
103
|
export declare function psFileSize(p: string): string;
|
|
38
104
|
/** Plain: base64 of up to `count` bytes at `offset` (download chunk). */
|
|
39
105
|
export declare function psReadChunk(p: string, offset: number, count: number): string;
|
|
@@ -42,21 +108,121 @@ export declare function psReadChunk(p: string, offset: number, count: number): s
|
|
|
42
108
|
* prints the number of bytes written.
|
|
43
109
|
*/
|
|
44
110
|
export declare function psWriteChunk(p: string, b64: string, append: boolean): string;
|
|
111
|
+
/** Magic stamped into the frame header that precedes every stream chunk. */
|
|
112
|
+
export declare const STREAM_FRAME_MAGIC = "DSHZ";
|
|
113
|
+
/** Frame header size: 4 magic bytes + 4 little-endian length bytes + 1 mode byte + 1 reserved. */
|
|
114
|
+
export declare const STREAM_FRAME_HEADER_BYTES = 10;
|
|
115
|
+
/** Frame mode: the payload after the header is a gzip member. */
|
|
116
|
+
export declare const STREAM_FRAME_GZIP = 1;
|
|
117
|
+
/** Frame mode: the payload after the header is raw file bytes. */
|
|
118
|
+
export declare const STREAM_FRAME_RAW = 0;
|
|
119
|
+
/** Frame mode: the payload after the header is the transfer's metadata. */
|
|
120
|
+
export declare const STREAM_FRAME_HEADER = 2;
|
|
121
|
+
/**
|
|
122
|
+
* Wrap one payload in a stream frame: `DSHZ` + uint32LE length + mode + pad.
|
|
123
|
+
*
|
|
124
|
+
* The frame is what makes a compressed stream legal on a pipe. stdin cannot
|
|
125
|
+
* seek, so the receiver has to be handed the exact byte count of the next gzip
|
|
126
|
+
* member before it lets `GZipStream` touch it; otherwise the inflater would
|
|
127
|
+
* read into the member that follows and the stream would desynchronize. The
|
|
128
|
+
* mode byte travels with the payload so the client can switch between gzip and
|
|
129
|
+
* raw per chunk.
|
|
130
|
+
* @param payload - gzip member bytes or raw file bytes.
|
|
131
|
+
* @param mode - `STREAM_FRAME_GZIP` or `STREAM_FRAME_RAW`.
|
|
132
|
+
* @returns the framed chunk to place on the stdin stream.
|
|
133
|
+
*/
|
|
134
|
+
export declare function buildStreamFrame(payload: Buffer, mode: number): Buffer;
|
|
135
|
+
/**
|
|
136
|
+
* Streaming receiver: drain framed stdin chunks into the file named by the
|
|
137
|
+
* header frame, until the declared byte count lands.
|
|
138
|
+
*
|
|
139
|
+
* This is the fast upload path. Instead of shipping base64 text inside a
|
|
140
|
+
* PowerShell script — which costs one full shell create/command/delete cycle
|
|
141
|
+
* per 48 KiB — the remote shell starts this script once and the client then
|
|
142
|
+
* pushes bytes straight down the WinRS stdin stream. The only per-chunk cost
|
|
143
|
+
* left is a single `Send`, so a whole file costs one shell and one command no
|
|
144
|
+
* matter how large it is.
|
|
145
|
+
*
|
|
146
|
+
* The script deliberately takes **no arguments**. The destination path and
|
|
147
|
+
* total byte count arrive as the first frame on stdin, which keeps the command
|
|
148
|
+
* line a constant size: WinRS hands the command line to cmd.exe, whose limit
|
|
149
|
+
* is about 8191 characters, and a long remote path interpolated into the
|
|
150
|
+
* script would eat into that budget (the script is base64-of-UTF-16 on the
|
|
151
|
+
* wire, so every character costs 2.67). Reading them from the stream removes
|
|
152
|
+
* the limit entirely and mirrors how `powershellCommandLine` stays constant.
|
|
153
|
+
*
|
|
154
|
+
* Every wire chunk is a frame: `DSHZ` + 4-byte little-endian payload length +
|
|
155
|
+
* 1 mode byte + 1 reserved, followed by exactly that many payload bytes. The
|
|
156
|
+
* explicit frame is what makes a *pipe* usable for a compressed stream: stdin
|
|
157
|
+
* cannot seek, so the receiver must be told how long the next gzip member is
|
|
158
|
+
* before it hands the bytes to `GZipStream`, or the inflater would read into
|
|
159
|
+
* the member that follows. The mode byte lets the client pick per chunk — gzip
|
|
160
|
+
* when it actually shrinks the data, raw bytes when it would not — so
|
|
161
|
+
* incompressible files never pay compression overhead.
|
|
162
|
+
*
|
|
163
|
+
* The script prints the three-line envelope on stdout with the byte count as
|
|
164
|
+
* its payload, so the caller can prove the transfer landed complete.
|
|
165
|
+
* @returns a PowerShell script suitable for `-EncodedCommand`.
|
|
166
|
+
*/
|
|
167
|
+
export declare function psReceiveStream(): string;
|
|
168
|
+
/**
|
|
169
|
+
* Streaming sender: write one byte range of the file to standard output,
|
|
170
|
+
* gzip-compressed, for the download path.
|
|
171
|
+
*
|
|
172
|
+
* This is the mirror of {@link psReceiveStream}. The old download path asked
|
|
173
|
+
* for 48 KiB at a time, and every chunk cost a full shell create / command /
|
|
174
|
+
* delete cycle — five HTTP round trips each — so a 2 MiB file took 129 s. Here
|
|
175
|
+
* the shell is created once and the client then simply receives; the only
|
|
176
|
+
* per-chunk cost left is one `Receive`, and the target streams the file as fast
|
|
177
|
+
* as the transport will drain it.
|
|
178
|
+
*
|
|
179
|
+
* Two details make this work:
|
|
180
|
+
*
|
|
181
|
+
* - **Only stderr may carry text.** Anything written to the PowerShell success
|
|
182
|
+
* stream would be merged into the payload and corrupt the file, so the byte
|
|
183
|
+
* count and any error go to `[Console]::Error` and the exit code, not stdout.
|
|
184
|
+
* - **The range is positional.** `Position` plus a byte count lets several
|
|
185
|
+
* shells serve different parts of one file concurrently; each writes to its
|
|
186
|
+
* own offset on the client, so completion order does not matter.
|
|
187
|
+
*
|
|
188
|
+
* Output is gzip-compressed unconditionally. Measured against this target, a
|
|
189
|
+
* compressible 16 MiB file collapsed from 121 round trips to 1 (2.0 s), and an
|
|
190
|
+
* incompressible 2 MiB file was no slower than raw (9457 ms vs 9582 ms), so the
|
|
191
|
+
* compression pays for itself or costs nothing.
|
|
192
|
+
* @param remotePath - file to read on the target.
|
|
193
|
+
* @param start - first byte offset of this worker's range.
|
|
194
|
+
* @param length - number of bytes this worker must send.
|
|
195
|
+
* @returns a PowerShell script suitable for `-EncodedCommand`.
|
|
196
|
+
*/
|
|
197
|
+
export declare function psSendStream(remotePath: string, start: number, length: number): string;
|
|
198
|
+
/**
|
|
199
|
+
* Wrap an arbitrary script in the constant powershell.exe invocation.
|
|
200
|
+
*
|
|
201
|
+
* Only safe for scripts that fit cmd.exe's ~8191 character command line (the
|
|
202
|
+
* script is base64-of-UTF-16 on the wire, so it costs 2.67 characters each);
|
|
203
|
+
* {@link psReceiveStream} outgrew that and travels on stdin instead.
|
|
204
|
+
* @param script - the PowerShell script to run.
|
|
205
|
+
* @returns the command line to hand to WinRS.
|
|
206
|
+
*/
|
|
207
|
+
export declare function scriptCommandLine(script: string): string;
|
|
45
208
|
/**
|
|
46
|
-
* Parse the envelope
|
|
209
|
+
* Parse the three-line envelope: { exitCode, stdout, stderr } or null when absent.
|
|
47
210
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* negative.
|
|
211
|
+
* Fields are located by their `O:` / `E:` labels rather than by splitting on
|
|
212
|
+
* newlines, because the WinRM client trims every response chunk and can drop
|
|
213
|
+
* the newline that separated two lines. base64's alphabet has no colon, so a
|
|
214
|
+
* label can only ever be a delimiter and the search cannot land on payload
|
|
215
|
+
* data. The exit code is signed because `$LASTEXITCODE` can be negative.
|
|
216
|
+
* @param output - the raw stdout accumulated from the WinRS stdout stream.
|
|
217
|
+
* @returns the decoded envelope, or null when the marker is missing.
|
|
53
218
|
*/
|
|
54
219
|
export declare function parseEnvelope(output: string): {
|
|
55
220
|
exitCode: number;
|
|
56
|
-
|
|
221
|
+
stdout: string;
|
|
222
|
+
stderr: string;
|
|
57
223
|
} | null;
|
|
58
224
|
/**
|
|
59
|
-
* Remove PowerShell's CLIXML host records from
|
|
225
|
+
* Remove PowerShell's CLIXML host records from a stderr stream.
|
|
60
226
|
*
|
|
61
227
|
* PowerShell marks its stderr stream with a `#< CLIXML` header line and
|
|
62
228
|
* serializes the remote host's own progress/verbose records into `<Objs>`
|
|
@@ -65,9 +231,10 @@ export declare function parseEnvelope(output: string): {
|
|
|
65
231
|
* header and a record block (for example `[Console]::Error`) is real output,
|
|
66
232
|
* not part of the CLIXML payload.
|
|
67
233
|
*
|
|
68
|
-
* The script's own
|
|
69
|
-
* envelope
|
|
70
|
-
*
|
|
234
|
+
* The script's own PowerShell errors are already stringified into the
|
|
235
|
+
* envelope's stdout by `Out-String -Stream`, so this only removes
|
|
236
|
+
* transport-level noise such as module-load progress.
|
|
237
|
+
* @param stderr - raw stderr text captured from the WinRS stream or the envelope.
|
|
71
238
|
* @returns the stderr text without CLIXML host records.
|
|
72
239
|
*/
|
|
73
240
|
export declare function stripClixml(stderr: string): string;
|
package/lib/types/smb.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SMB transfer bridge.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* SMB is the fast channel when the admin share is reachable, so `auto` tries it
|
|
5
|
+
* first and falls back to the WinRM streaming path. This module mounts the
|
|
6
|
+
* remote host's admin share with `net use` (password passed via stdin, never
|
|
7
|
+
* argv), copies whole files with node:fs, then verifies the SHA-256 on both
|
|
8
|
+
* ends.
|
|
9
9
|
*/
|
|
10
10
|
import type { WinHostEntry } from './protocol.ts';
|
|
11
11
|
/** Per-transfer channel selector. */
|
|
@@ -19,6 +19,25 @@ export interface SmbTransferResult {
|
|
|
19
19
|
* D:\\dir\\file -> \\\\host\\D$\\dir\\file
|
|
20
20
|
*/
|
|
21
21
|
export declare function adminSharePath(host: string, remotePath: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the target's SMB port (445) accepts a connection.
|
|
24
|
+
*
|
|
25
|
+
* `net use` is the only way to mount the admin share, but it has no useful
|
|
26
|
+
* timeout: against a host whose 445 is filtered or whose SMB service is down it
|
|
27
|
+
* spends a full minute before failing. That minute was being paid on *every*
|
|
28
|
+
* `auto` upload before the WinRM fallback could even start — measured at 61 s
|
|
29
|
+
* and 58 s on the test host, against a 12 s streaming transfer. A bare TCP
|
|
30
|
+
* connect answers the same question in milliseconds.
|
|
31
|
+
*
|
|
32
|
+
* A negative verdict is cached briefly so a burst of transfers does not re-probe
|
|
33
|
+
* for each one; a positive verdict is cached too, but the `net use` that follows
|
|
34
|
+
* still reports its own failure if the share turns out to be unmountable.
|
|
35
|
+
* @param host - target hostname or address.
|
|
36
|
+
* @returns true when port 445 accepted a connection.
|
|
37
|
+
*/
|
|
38
|
+
export declare function smbReachable(host: string): Promise<boolean>;
|
|
39
|
+
/** Error thrown when SMB was requested or probed and the target is not listening. */
|
|
40
|
+
export declare function smbUnavailable(host: string): Error;
|
|
22
41
|
/**
|
|
23
42
|
* Copy one local file to a remote Windows host over SMB, verifying the
|
|
24
43
|
* remote SHA-256 matches the local source.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-winrm",
|
|
3
|
-
"description": "Remote Windows administration for the dsh web GUI: WinRM/PowerShell Remoting host config store (~/.dsh/dsh-winrm.json), PowerShell exec / streaming console / service & process management /
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "Remote Windows administration for the dsh web GUI: WinRM/PowerShell Remoting host config store (~/.dsh/dsh-winrm.json), PowerShell exec / streaming console / service & process management / streamed file transfer / cluster execution, plus agent tools (winrm_list, winrm_exec, winrm_service, winrm_process, winrm_upload, winrm_download, winrm_cluster). Runs entirely on a native Node.js WinRM client (winrm-client) — no Python/pywinrm required. Standalone Cordis plugin — mounted via cordis.patch.yml + a profile node_modules link, no dsh source changes.",
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.7.0",
|
|
7
7
|
"engines": {
|
|
@@ -44,25 +44,25 @@
|
|
|
44
44
|
"ws": "^8.18.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.7-alpha.2",
|
|
48
|
-
"@deepseek-ai/dsh-llm": "^0.1.7-alpha.2",
|
|
49
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.7-alpha.2",
|
|
50
|
-
"@deepseek-ai/dsh-tools": "^0.1.7-alpha.2",
|
|
47
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.7-alpha.2 || ^0.2.0-rc.1",
|
|
48
|
+
"@deepseek-ai/dsh-llm": "^0.1.7-alpha.2 || ^0.2.0-rc.1",
|
|
49
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.7-alpha.2 || ^0.2.0-rc.1",
|
|
50
|
+
"@deepseek-ai/dsh-tools": "^0.1.7-alpha.2 || ^0.2.0-rc.1",
|
|
51
51
|
"react": "^18.2.0",
|
|
52
52
|
"react-dom": "^18.2.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
56
56
|
"@deepseek-ai/cordis-plugin-loader": "^1.0.5",
|
|
57
|
-
"@deepseek-ai/dsh-client-locale": "^0.
|
|
58
|
-
"@deepseek-ai/dsh-client-ui-layout": "^0.
|
|
59
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.
|
|
60
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "^0.
|
|
61
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.
|
|
62
|
-
"@deepseek-ai/dsh-host-webserver": "^0.
|
|
63
|
-
"@deepseek-ai/dsh-llm": "^0.
|
|
64
|
-
"@deepseek-ai/dsh-system-prompt": "^0.
|
|
65
|
-
"@deepseek-ai/dsh-tools": "^0.
|
|
57
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-layout": "^0.2.0-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.2.0-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-tools": "^0.2.0-rc.2",
|
|
66
66
|
"@deepseek-ai/schemastery": "^3.18.4",
|
|
67
67
|
"@types/node": "^22.20.0",
|
|
68
68
|
"@types/react": "~18.3.1",
|
|
@@ -89,7 +89,8 @@
|
|
|
89
89
|
"build": "tsc -p tsconfig.build.json && tsdown && node scripts/postbuild.mjs",
|
|
90
90
|
"bundle": "tsdown",
|
|
91
91
|
"watch": "tsdown --watch",
|
|
92
|
-
"test": "node --experimental-strip-types --test tests/*.test.ts
|
|
92
|
+
"test": "node --experimental-strip-types --test tests/*.test.ts",
|
|
93
|
+
"verify:live": "node --experimental-strip-types scripts/verify-live.mts",
|
|
93
94
|
"check": "npm run typecheck && npm run build && npm test",
|
|
94
95
|
"pack:check": "npm pack --dry-run --json",
|
|
95
96
|
"prepack": "npm run check",
|