@linxin666/dsh-ssh 0.2.0 → 0.2.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.
@@ -140,28 +140,34 @@ export async function connectChain(engine: PoolEngine, entry: SshHostEntry): Pro
140
140
  const hops: Client[] = []
141
141
  let sock: ConnectConfig['sock']
142
142
  const chain = entry.proxyJump
143
- for (let index = 0; index < chain.length; index += 1) {
144
- const hopAlias = chain[index]
145
- const hop = engine.store.find(hopAlias)
146
- if (hop === undefined) {
147
- for (const client of hops) client.end()
148
- throw new Error('proxyJump alias \'' + hopAlias + '\' not found — create it first')
149
- }
150
- const hopClient = await connectClient(buildConnectConfig(hop, sock, engine.opts))
151
- hops.push(hopClient)
152
- const next = index + 1 < chain.length ? engine.store.find(chain[index + 1]) : undefined
153
- const nextHost = next !== undefined ? next.host : entry.host
154
- const nextPort = next !== undefined ? next.port : entry.port
155
- sock = await new Promise<ConnectConfig['sock']>((resolve, reject) => {
156
- hopClient.forwardOut('127.0.0.1', 0, nextHost, nextPort, (error, stream) => {
157
- if (error !== undefined) {
158
- for (const client of hops) client.end()
159
- reject(error)
160
- } else {
161
- resolve(stream)
162
- }
143
+ try {
144
+ for (let index = 0; index < chain.length; index += 1) {
145
+ const hopAlias = chain[index]
146
+ const hop = engine.store.find(hopAlias)
147
+ if (hop === undefined) {
148
+ throw new Error('proxyJump alias \'' + hopAlias + '\' not found — create it first')
149
+ }
150
+ const hopClient = await connectClient(buildConnectConfig(hop, sock, engine.opts))
151
+ hops.push(hopClient)
152
+ const next = index + 1 < chain.length ? engine.store.find(chain[index + 1]) : undefined
153
+ const nextHost = next !== undefined ? next.host : entry.host
154
+ const nextPort = next !== undefined ? next.port : entry.port
155
+ sock = await new Promise<ConnectConfig['sock']>((resolve, reject) => {
156
+ hopClient.forwardOut('127.0.0.1', 0, nextHost, nextPort, (error, stream) => {
157
+ if (error !== undefined) {
158
+ reject(error)
159
+ } else {
160
+ resolve(stream)
161
+ }
162
+ })
163
163
  })
164
- })
164
+ }
165
+ } catch (error) {
166
+ // A missing alias, a failed hop connect, or a failed forwardOut must all
167
+ // close the hops already connected, so a failed ProxyJump never leaks a
168
+ // middle-hop connection.
169
+ for (const client of hops) client.end()
170
+ throw error
165
171
  }
166
172
  let target: Client | undefined
167
173
  try {
package/src/routes.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * deployments must not serve them.
8
8
  */
9
9
 
10
- import { createReadStream, createWriteStream, mkdirSync } from 'node:fs'
10
+ import { closeSync, createReadStream, createWriteStream, mkdirSync, openSync } from 'node:fs'
11
11
  import { unlink } from 'node:fs/promises'
12
12
  import type { IncomingMessage, ServerResponse } from 'node:http'
13
13
  import { basename, join } from 'node:path'
@@ -93,7 +93,9 @@ export function makeRoutes(deps: SshRoutesDeps): { routes: WebRoute[]; upgrade:
93
93
  const maxUploadBytes = deps.maxUploadBytes ?? MAX_UPLOAD_BYTES
94
94
  // The upload route stages request bodies here; it must exist before the
95
95
  // first request (a missing dir would hang the first upload forever).
96
- mkdirSync(staging, { recursive: true })
96
+ // 0700 keeps in-flight transfers unreadable to other local users (the
97
+ // staged files below are 0600; downloads are pre-created 0600 as well).
98
+ mkdirSync(staging, { recursive: true, mode: 0o700 })
97
99
 
98
100
  /** Guard helper: fence + method check. */
99
101
  const guard = (req: IncomingMessage, res: ServerResponse, method: string): boolean => {
@@ -353,7 +355,7 @@ const maxUploadBytes = deps.maxUploadBytes ?? MAX_UPLOAD_BYTES
353
355
  }
354
356
  // Stage the uploaded bytes, then SFTP them out with progress frames.
355
357
  const tmp = join(staging, `upload-${randomBytes(6).toString('hex')}`)
356
- const sink = createWriteStream(tmp)
358
+ const sink = createWriteStream(tmp, { mode: 0o600 })
357
359
  let settled = false
358
360
  // Every terminal path (sink error, client abort, response loss) must
359
361
  // emit a result frame, end the response, and remove the tmp file.
@@ -437,6 +439,9 @@ const maxUploadBytes = deps.maxUploadBytes ?? MAX_UPLOAD_BYTES
437
439
  }
438
440
  const tmp = join(staging, `download-${randomBytes(6).toString('hex')}`)
439
441
  try {
442
+ // Pre-create with 0600: ssh2's fastGet opens the destination with
443
+ // umask-default permissions and a pre-created file keeps the mode.
444
+ closeSync(openSync(tmp, 'w', 0o600))
440
445
  const outcome = await engine.download(alias, remotePath, tmp)
441
446
  res.writeHead(200, {
442
447
  'content-type': 'application/octet-stream',