@linxin666/dsh-ssh 0.1.14 → 0.1.15
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/lib/client.js +26 -4
- package/lib/client.js.map +1 -1
- package/lib/index.js +660 -598
- package/lib/types/client/panel/TunnelsTab.d.ts +10 -0
- package/lib/types/engine/cluster.d.ts +17 -0
- package/lib/types/engine/connection-pool.d.ts +81 -0
- package/lib/types/engine/pty.d.ts +32 -0
- package/lib/types/engine/sftp.d.ts +20 -0
- package/lib/types/engine/tunnel.d.ts +32 -0
- package/lib/types/engine.d.ts +18 -79
- package/package.json +1 -1
- package/src/client/panel/TerminalTab.tsx +10 -3
- package/src/client/panel/TunnelsTab.tsx +31 -4
- package/src/engine/cluster.ts +51 -0
- package/src/engine/connection-pool.ts +307 -0
- package/src/engine/pty.ts +74 -0
- package/src/engine/sftp.ts +235 -0
- package/src/engine/tunnel.ts +125 -0
- package/src/engine.ts +45 -687
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local port-forward tunnels: one loopback-only listener per tunnel, pinned
|
|
3
|
+
* to a pooled connection so an idle sweep never closes it, with per-tunnel
|
|
4
|
+
* socket tracking and stop (single / alias-scoped / all).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createServer, type Server as NetServer, type Socket } from 'node:net'
|
|
8
|
+
import type { TunnelInfo } from '../protocol.ts'
|
|
9
|
+
import { acquire, disposeRecord, type PoolEngine } from './connection-pool.ts'
|
|
10
|
+
|
|
11
|
+
/** One active tunnel record (server + pinned client + live sockets). */
|
|
12
|
+
export interface TunnelRecord {
|
|
13
|
+
info: TunnelInfo
|
|
14
|
+
server: NetServer
|
|
15
|
+
alias: string
|
|
16
|
+
sockets: Set<Socket>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The engine slice the tunnel module needs (adds the tunnel registry). */
|
|
20
|
+
export interface TunnelEngine extends PoolEngine {
|
|
21
|
+
readonly tunnels: Map<string, TunnelRecord>
|
|
22
|
+
nextTunnelId: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Start a local port-forward tunnel (listens on 127.0.0.1 only). */
|
|
26
|
+
export async function startTunnel(
|
|
27
|
+
engine: TunnelEngine,
|
|
28
|
+
alias: string,
|
|
29
|
+
options: { remotePort: number; remoteHost?: string; localPort?: number },
|
|
30
|
+
): Promise<TunnelInfo> {
|
|
31
|
+
if (!Number.isInteger(options.remotePort) || options.remotePort < 1 || options.remotePort > 65535) {
|
|
32
|
+
throw new Error('remotePort must be an integer in 1..65535')
|
|
33
|
+
}
|
|
34
|
+
if (options.localPort !== undefined && (!Number.isInteger(options.localPort) || options.localPort < 1 || options.localPort > 65535)) {
|
|
35
|
+
throw new Error('localPort must be an integer in 1..65535')
|
|
36
|
+
}
|
|
37
|
+
const entry = engine.store.find(alias)
|
|
38
|
+
if (entry === undefined) throw new Error('alias \'' + alias + '\' not found — add it first')
|
|
39
|
+
const remoteHost = options.remoteHost ?? '127.0.0.1'
|
|
40
|
+
const id = 'tun-' + engine.nextTunnelId
|
|
41
|
+
engine.nextTunnelId += 1
|
|
42
|
+
const info: TunnelInfo = {
|
|
43
|
+
id,
|
|
44
|
+
alias,
|
|
45
|
+
localPort: 0,
|
|
46
|
+
remoteHost,
|
|
47
|
+
remotePort: options.remotePort,
|
|
48
|
+
state: 'connecting',
|
|
49
|
+
startedAt: Date.now(),
|
|
50
|
+
}
|
|
51
|
+
const record = await acquire(engine, alias)
|
|
52
|
+
const client = record.client
|
|
53
|
+
const sockets = new Set<Socket>()
|
|
54
|
+
const server = createServer((socket) => {
|
|
55
|
+
sockets.add(socket)
|
|
56
|
+
socket.on('close', () => { sockets.delete(socket) })
|
|
57
|
+
client.forwardOut('127.0.0.1', 0, remoteHost, options.remotePort, (error, stream) => {
|
|
58
|
+
if (error !== undefined) {
|
|
59
|
+
socket.destroy()
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
// Both ends of the pipe can die independently; destroy the pair so an
|
|
63
|
+
// unhandled 'error' event can never crash the host process.
|
|
64
|
+
const destroy = (): void => {
|
|
65
|
+
try { socket.destroy() } catch { /* gone */ }
|
|
66
|
+
try { stream.close() } catch { /* gone */ }
|
|
67
|
+
}
|
|
68
|
+
stream.on('error', destroy)
|
|
69
|
+
socket.on('error', destroy)
|
|
70
|
+
stream.on('close', destroy)
|
|
71
|
+
socket.on('close', destroy)
|
|
72
|
+
stream.pipe(socket).pipe(stream)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
try {
|
|
76
|
+
await new Promise<void>((resolve, reject) => {
|
|
77
|
+
server.once('error', reject)
|
|
78
|
+
server.listen(options.localPort ?? 0, '127.0.0.1', () => {
|
|
79
|
+
server.removeListener('error', reject)
|
|
80
|
+
resolve()
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Roll back: never leave an unpinned orphan connection behind.
|
|
85
|
+
if (!record.pinned && record.inFlight === 0) disposeRecord(engine, alias, record)
|
|
86
|
+
throw error
|
|
87
|
+
}
|
|
88
|
+
record.pinned = true
|
|
89
|
+
const address = server.address()
|
|
90
|
+
info.localPort = typeof address === 'object' && address !== null ? address.port : 0
|
|
91
|
+
info.state = 'forwarding'
|
|
92
|
+
engine.tunnels.set(id, { info, server, alias, sockets })
|
|
93
|
+
return info
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** All active tunnels. */
|
|
97
|
+
export function listTunnels(engine: TunnelEngine): TunnelInfo[] {
|
|
98
|
+
return [...engine.tunnels.values()].map(tunnel => ({ ...tunnel.info }))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Stop one tunnel (closes the listener, live sockets, and the pinned connection). */
|
|
102
|
+
export function stopTunnel(engine: TunnelEngine, id: string): boolean {
|
|
103
|
+
const tunnel = engine.tunnels.get(id)
|
|
104
|
+
if (tunnel === undefined) return false
|
|
105
|
+
engine.tunnels.delete(id)
|
|
106
|
+
try { tunnel.server.close() } catch { /* already closed */ }
|
|
107
|
+
for (const socket of tunnel.sockets) {
|
|
108
|
+
try { socket.destroy() } catch { /* already closed */ }
|
|
109
|
+
}
|
|
110
|
+
tunnel.sockets.clear()
|
|
111
|
+
disposeRecord(engine, tunnel.alias)
|
|
112
|
+
return true
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Stop all tunnels (optionally for one alias). */
|
|
116
|
+
export function stopAllTunnels(engine: TunnelEngine, alias?: string): number {
|
|
117
|
+
let count = 0
|
|
118
|
+
for (const [id, tunnel] of [...engine.tunnels]) {
|
|
119
|
+
if (alias === undefined || tunnel.alias === alias) {
|
|
120
|
+
stopTunnel(engine, id)
|
|
121
|
+
count += 1
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return count
|
|
125
|
+
}
|