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/src/smb.ts CHANGED
@@ -1,17 +1,18 @@
1
1
  /**
2
2
  * SMB transfer bridge.
3
3
  *
4
- * The WinRM base64 chunk bridge is reliable for small files but breaks for
5
- * large artifacts (the WinRM/HTTP envelope rejects large payloads), so whole
6
- * files ride the remote admin share. This module mounts the remote host's
7
- * admin share with `net use` (password passed via stdin, never argv), copies
8
- * whole files with node:fs, then verifies the SHA-256 on both ends.
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
 
11
11
  import { spawn } from 'node:child_process'
12
12
  import { copyFile, stat } from 'node:fs/promises'
13
13
  import { createHash } from 'node:crypto'
14
14
  import { readFile } from 'node:fs/promises'
15
+ import { createConnection } from 'node:net'
15
16
  import type { WinHostEntry } from './protocol.ts'
16
17
 
17
18
  /** Per-transfer channel selector. */
@@ -85,6 +86,57 @@ async function remoteSha256(host: string, remotePath: string): Promise<string> {
85
86
  return match[1].toUpperCase()
86
87
  }
87
88
 
89
+ /** How long a reachability verdict is trusted before probing again. */
90
+ const SMB_PROBE_TTL_MS = 60_000
91
+
92
+ /** Budget for the TCP reachability probe; far below `net use`'s own timeout. */
93
+ const SMB_PROBE_TIMEOUT_MS = 3_000
94
+
95
+ const smbProbeCache = new Map<string, { reachable: boolean; at: number }>()
96
+
97
+ /**
98
+ * Whether the target's SMB port (445) accepts a connection.
99
+ *
100
+ * `net use` is the only way to mount the admin share, but it has no useful
101
+ * timeout: against a host whose 445 is filtered or whose SMB service is down it
102
+ * spends a full minute before failing. That minute was being paid on *every*
103
+ * `auto` upload before the WinRM fallback could even start — measured at 61 s
104
+ * and 58 s on the test host, against a 12 s streaming transfer. A bare TCP
105
+ * connect answers the same question in milliseconds.
106
+ *
107
+ * A negative verdict is cached briefly so a burst of transfers does not re-probe
108
+ * for each one; a positive verdict is cached too, but the `net use` that follows
109
+ * still reports its own failure if the share turns out to be unmountable.
110
+ * @param host - target hostname or address.
111
+ * @returns true when port 445 accepted a connection.
112
+ */
113
+ export async function smbReachable(host: string): Promise<boolean> {
114
+ const cached = smbProbeCache.get(host)
115
+ if (cached !== undefined && Date.now() - cached.at < SMB_PROBE_TTL_MS) return cached.reachable
116
+ const reachable = await new Promise<boolean>((resolve) => {
117
+ const socket = createConnection({ host, port: 445 })
118
+ let settled = false
119
+ const finish = (value: boolean): void => {
120
+ if (settled) return
121
+ settled = true
122
+ socket.destroy()
123
+ resolve(value)
124
+ }
125
+ const timer = setTimeout(() => finish(false), SMB_PROBE_TIMEOUT_MS)
126
+ socket.once('connect', () => { clearTimeout(timer); finish(true) })
127
+ socket.once('error', () => { clearTimeout(timer); finish(false) })
128
+ socket.once('timeout', () => { clearTimeout(timer); finish(false) })
129
+ socket.setTimeout(SMB_PROBE_TIMEOUT_MS)
130
+ })
131
+ smbProbeCache.set(host, { reachable, at: Date.now() })
132
+ return reachable
133
+ }
134
+
135
+ /** Error thrown when SMB was requested or probed and the target is not listening. */
136
+ export function smbUnavailable(host: string): Error {
137
+ return new Error('SMB is not reachable on ' + host + ':445 (the admin share needs TCP 445 open and the Server service running)')
138
+ }
139
+
88
140
  /**
89
141
  * Copy one local file to a remote Windows host over SMB, verifying the
90
142
  * remote SHA-256 matches the local source.
package/src/tools.ts CHANGED
@@ -272,7 +272,7 @@ export function winrmUploadTool(engine: WinRmEngine) {
272
272
  alias: { type: 'string', required: true, description: 'Host alias from winrm_list.' },
273
273
  localPath: { type: 'string', required: true, description: 'Absolute local file path on this machine.' },
274
274
  remotePath: { type: 'string', required: true, description: 'Destination path on the remote host, e.g. C:\\temp\\file.txt (parent dirs are created).' },
275
- channel: { type: 'string', enum: ['auto', 'smb', 'winrm'], description: 'Transfer channel: smb (admin share), winrm (base64 chunks), or auto (default, SMB first with WinRM fallback).' },
275
+ channel: { type: 'string', enum: ['auto', 'smb', 'winrm'], description: 'Transfer channel: smb (admin share), winrm (streamed over the WinRS stdin channel), or auto (default, SMB first with WinRM fallback).' },
276
276
  },
277
277
  output: {
278
278
  schema: {
@@ -311,7 +311,7 @@ export function winrmDownloadTool(engine: WinRmEngine) {
311
311
  alias: { type: 'string', required: true, description: 'Host alias from winrm_list.' },
312
312
  remotePath: { type: 'string', required: true, description: 'Remote file path, e.g. C:\\temp\\file.txt.' },
313
313
  localPath: { type: 'string', required: true, description: 'Absolute destination path on this machine.' },
314
- channel: { type: 'string', enum: ['auto', 'smb', 'winrm'], description: 'Transfer channel: smb (admin share), winrm (base64 chunks), or auto (default, SMB first with WinRM fallback).' },
314
+ channel: { type: 'string', enum: ['auto', 'smb', 'winrm'], description: 'Transfer channel: smb (admin share), winrm (streamed over parallel WinRS ranges), or auto (default, SMB first with WinRM fallback).' },
315
315
  },
316
316
  output: {
317
317
  schema: {