dsh-winrm 0.2.0 → 0.3.1
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 +9 -5
- package/README.zh.md +9 -5
- package/lib/client.js +71 -71
- package/lib/index.js +981 -75
- package/lib/types/engine/client.d.ts +48 -1
- package/lib/types/engine/wsman.d.ts +153 -0
- package/lib/types/engine.d.ts +4 -4
- package/lib/types/index.d.ts +6 -5
- package/lib/types/powershell.d.ts +110 -1
- package/lib/types/smb.d.ts +24 -5
- package/package.json +2 -2
- package/src/engine/client.ts +439 -5
- package/src/engine/wsman.ts +357 -0
- package/src/engine.ts +104 -40
- package/src/index.ts +6 -5
- package/src/powershell.ts +298 -2
- package/src/smb.ts +57 -5
- package/src/tools.ts +4 -4
|
@@ -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';
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-winrm — host half. Mounts the WinRM engine (per-call PowerShell
|
|
3
3
|
* execution with a UTF-8 envelope, streaming console, service/process
|
|
4
|
-
* management,
|
|
4
|
+
* management, streamed file transfer, cluster), the /api/dsh-winrm
|
|
5
5
|
* route family plus the console WebSocket upgrade, the agent tools
|
|
6
6
|
* (winrm_list, winrm_exec, winrm_service, winrm_process, winrm_upload,
|
|
7
7
|
* winrm_download, winrm_cluster), and a system-prompt announcement. The
|
|
8
8
|
* browser half (./client) renders the host manager and operations panel.
|
|
9
9
|
* Everything rides official NPM SDK packages — no dsh source changes.
|
|
10
10
|
*
|
|
11
|
-
* Config is read live through `Volatile` references (`.get()`), so
|
|
12
|
-
*
|
|
13
|
-
* `loader/volatile-update` re-runs the registration sync when
|
|
11
|
+
* Config is read live through `Volatile` references (`.get()`), so editing
|
|
12
|
+
* `enabled` / `announceToAgent` in the profile takes effect without
|
|
13
|
+
* remounting; `loader/volatile-update` re-runs the registration sync when
|
|
14
|
+
* they change.
|
|
14
15
|
*/
|
|
15
16
|
import type { Context, Volatile } from '@deepseek-ai/cordis';
|
|
16
17
|
import z from '@deepseek-ai/schemastery';
|
|
@@ -33,7 +34,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
33
34
|
enabled: z<boolean, boolean, "volatile-defined">;
|
|
34
35
|
}>>, "plain">;
|
|
35
36
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
36
|
-
export declare const WINRM_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-winrm \u63D2\u4EF6\uFF08DSH \u8FDC\u7A0B Windows \u8FD0\u7EF4\uFF0CWinRM/PowerShell Remoting\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300CWindows\u300D\u5165\u53E3\uFF1B\u4EFF\u7167 dsh-ssh \u63D2\u4EF6\u5F00\u53D1\u3002\u80FD\u529B\uFF1A\u4E3B\u673A\u914D\u7F6E\u5B58 ~/.dsh/dsh-winrm.json\uFF08GUI \u914D\u7F6E\u540E agent \u65B9\u53EF\u4F7F\u7528\uFF09\uFF1Bwinrm_list \u5217\u51FA\u4E3B\u673A\u3001winrm_exec \u6267\u884C PowerShell \u547D\u4EE4\uFF08UTF-8 \u4FE1\u5C01\uFF0C\u4E2D\u6587\u4E0D\u4E71\u7801\uFF09\u3001winrm_service \u670D\u52A1\u7BA1\u7406\uFF08start/stop/restart/\u542F\u52A8\u7C7B\u578B\uFF09\u3001winrm_process \u8FDB\u7A0B\u7BA1\u7406\uFF08list/kill\uFF09\u3001winrm_upload/winrm_download \u6587\u4EF6\u4F20\u8F93\
|
|
37
|
+
export declare const WINRM_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-winrm \u63D2\u4EF6\uFF08DSH \u8FDC\u7A0B Windows \u8FD0\u7EF4\uFF0CWinRM/PowerShell Remoting\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300CWindows\u300D\u5165\u53E3\uFF1B\u4EFF\u7167 dsh-ssh \u63D2\u4EF6\u5F00\u53D1\u3002\u80FD\u529B\uFF1A\u4E3B\u673A\u914D\u7F6E\u5B58 ~/.dsh/dsh-winrm.json\uFF08GUI \u914D\u7F6E\u540E agent \u65B9\u53EF\u4F7F\u7528\uFF09\uFF1Bwinrm_list \u5217\u51FA\u4E3B\u673A\u3001winrm_exec \u6267\u884C PowerShell \u547D\u4EE4\uFF08UTF-8 \u4FE1\u5C01\uFF0C\u4E2D\u6587\u4E0D\u4E71\u7801\uFF09\u3001winrm_service \u670D\u52A1\u7BA1\u7406\uFF08start/stop/restart/\u542F\u52A8\u7C7B\u578B\uFF09\u3001winrm_process \u8FDB\u7A0B\u7BA1\u7406\uFF08list/kill\uFF09\u3001winrm_upload/winrm_download \u6587\u4EF6\u4F20\u8F93\uFF08SMB \u7BA1\u7406\u5171\u4EAB\u53EF\u8FBE\u65F6\u4F18\u5148\uFF0C\u5426\u5219\u8D70 WinRM \u6D41\u5F0F\u4F20\u8F93\uFF0C\u65E0 SMB \u4F9D\u8D56\uFF0C\u5927\u6587\u4EF6\u7ECF SHA256 \u6821\u9A8C\uFF09\u3001winrm_cluster \u96C6\u7FA4\u5E76\u53D1\u6267\u884C\uFF1BWeb \u63A7\u5236\u53F0\u8D70 WebSocket\u3002\u8BA4\u8BC1\uFF1A\u901A\u8FC7\u539F\u751F Node.js WinRM \u5BA2\u6237\u7AEF\uFF08winrm-client\uFF09\u7684 NTLM/Negotiate \u4E0E Basic \u5B9E\u73B0\uFF0C\u517C\u5BB9\u672C\u5730\u8D26\u6237\u4E0E\u57DF\u8D26\u6237\uFF1B\u4F20\u8F93\u652F\u6301 http(5985)/https(5986)\u3002\u524D\u63D0\uFF1A\u76EE\u6807\u673A\u9700\u542F\u7528 WinRM\uFF08scripts/enable-winrm.ps1\uFF09\uFF0C\u672C\u673A\u65E0\u9700 Python \u2014\u2014 WinRM \u8D70\u7EAF Node \u4F9D\u8D56\u3002HTTP \u4EC5\u5EFA\u8BAE\u53D7\u4FE1\u5185\u7F51\u4F7F\u7528\uFF1B\u5BC6\u7801\u4EE5\u660E\u6587\u5B58\u5728\u7528\u6237\u4E3B\u76EE\u5F55\u79C1\u6709\u6587\u4EF6\uFF08\u6743\u9650 0600\uFF09\uFF1B\u547D\u4EE4\u8F93\u51FA\u53EF\u80FD\u542B\u654F\u611F\u4FE1\u606F\uFF1B\u4F20\u8F93/\u6267\u884C\u6D88\u8017\u771F\u5B9E\u8FDC\u7A0B\u8D44\u6E90\uFF0C\u5148\u786E\u8BA4\u518D\u64CD\u4F5C\u3002\u7528\u6237\u63D0\u5230\u300CWindows \u670D\u52A1\u5668 / WinRM / PowerShell \u8FDC\u7A0B / \u670D\u52A1\u7BA1\u7406 / \u8FDB\u7A0B\u7BA1\u7406\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
|
|
37
38
|
/**
|
|
38
39
|
* Mount the WinRM engine, routes, tools, and announcement.
|
|
39
40
|
* @param ctx - host plugin context carrying webServer/tools/systemPrompt.
|
|
@@ -55,6 +55,13 @@ export declare function encodeCommand(script: string): string;
|
|
|
55
55
|
* ~8191-character cap is the whole point of the stdin channel.
|
|
56
56
|
*/
|
|
57
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;
|
|
58
65
|
/**
|
|
59
66
|
* The stdin payload for one command: the UTF-8 **byte** count, a newline, then
|
|
60
67
|
* the raw script.
|
|
@@ -87,7 +94,12 @@ export declare function psListProcesses(): string;
|
|
|
87
94
|
export declare function psKillProcess(id: number): string;
|
|
88
95
|
/** JSON: directory listing. */
|
|
89
96
|
export declare function psListDir(dir: string): string;
|
|
90
|
-
/**
|
|
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
|
+
*/
|
|
91
103
|
export declare function psFileSize(p: string): string;
|
|
92
104
|
/** Plain: base64 of up to `count` bytes at `offset` (download chunk). */
|
|
93
105
|
export declare function psReadChunk(p: string, offset: number, count: number): string;
|
|
@@ -96,6 +108,103 @@ export declare function psReadChunk(p: string, offset: number, count: number): s
|
|
|
96
108
|
* prints the number of bytes written.
|
|
97
109
|
*/
|
|
98
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;
|
|
99
208
|
/**
|
|
100
209
|
* Parse the three-line envelope: { exitCode, stdout, stderr } or null when absent.
|
|
101
210
|
*
|
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.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.7.0",
|
|
7
7
|
"engines": {
|