@vibecontrols/vibe-plugin-ssh 2026.416.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 +70 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +118 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/port-forward.d.ts +190 -0
- package/dist/routes/port-forward.d.ts.map +1 -0
- package/dist/routes/port-forward.js +303 -0
- package/dist/routes/port-forward.js.map +1 -0
- package/dist/routes/remote-agent-install.d.ts +179 -0
- package/dist/routes/remote-agent-install.d.ts.map +1 -0
- package/dist/routes/remote-agent-install.js +537 -0
- package/dist/routes/remote-agent-install.js.map +1 -0
- package/dist/routes/remote-terminal.d.ts +154 -0
- package/dist/routes/remote-terminal.d.ts.map +1 -0
- package/dist/routes/remote-terminal.js +437 -0
- package/dist/routes/remote-terminal.js.map +1 -0
- package/dist/routes/ssh-config-scan.d.ts +144 -0
- package/dist/routes/ssh-config-scan.d.ts.map +1 -0
- package/dist/routes/ssh-config-scan.js +271 -0
- package/dist/routes/ssh-config-scan.js.map +1 -0
- package/dist/routes/ssh.d.ts +247 -0
- package/dist/routes/ssh.d.ts.map +1 -0
- package/dist/routes/ssh.js +295 -0
- package/dist/routes/ssh.js.map +1 -0
- package/dist/types.d.ts +171 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +82 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote terminal routes – start ttyd on a destination server via SSH,
|
|
3
|
+
* port-forward back to a local port on the agent, and serve it through
|
|
4
|
+
* the agent's existing terminal WebSocket proxy.
|
|
5
|
+
*
|
|
6
|
+
* Namespace: "ssh"
|
|
7
|
+
* Keys:
|
|
8
|
+
* "terminal-sessions" → JSON array of SSHTerminalSession objects
|
|
9
|
+
*/
|
|
10
|
+
import { Elysia } from "elysia";
|
|
11
|
+
import type { HostServices, SSHTerminalSession } from "../types.js";
|
|
12
|
+
export declare function cleanupAllTerminals(): void;
|
|
13
|
+
export declare function getTerminalInfo(sessionId: string): {
|
|
14
|
+
url: string;
|
|
15
|
+
port: number;
|
|
16
|
+
pid: number;
|
|
17
|
+
} | null;
|
|
18
|
+
/** List all active terminal sessions (for the session provider shim). */
|
|
19
|
+
export declare function listTerminalSessions(): SSHTerminalSession[];
|
|
20
|
+
export declare function createRemoteTerminalRoutes(hostServices: HostServices): Elysia<"/api/ssh/terminal", {
|
|
21
|
+
decorator: {};
|
|
22
|
+
store: {};
|
|
23
|
+
derive: {};
|
|
24
|
+
resolve: {};
|
|
25
|
+
}, {
|
|
26
|
+
typebox: {};
|
|
27
|
+
error: {};
|
|
28
|
+
}, {
|
|
29
|
+
schema: {};
|
|
30
|
+
standaloneSchema: {};
|
|
31
|
+
macro: {};
|
|
32
|
+
macroFn: {};
|
|
33
|
+
parser: {};
|
|
34
|
+
response: {};
|
|
35
|
+
}, {
|
|
36
|
+
api: {
|
|
37
|
+
ssh: {
|
|
38
|
+
terminal: {
|
|
39
|
+
sessions: {
|
|
40
|
+
get: {
|
|
41
|
+
body: unknown;
|
|
42
|
+
params: {};
|
|
43
|
+
query: unknown;
|
|
44
|
+
headers: unknown;
|
|
45
|
+
response: {
|
|
46
|
+
200: {
|
|
47
|
+
sessions: SSHTerminalSession[];
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
} & {
|
|
56
|
+
api: {
|
|
57
|
+
ssh: {
|
|
58
|
+
terminal: {
|
|
59
|
+
sessions: {
|
|
60
|
+
":id": {
|
|
61
|
+
get: {
|
|
62
|
+
body: unknown;
|
|
63
|
+
params: {
|
|
64
|
+
id: string;
|
|
65
|
+
} & {};
|
|
66
|
+
query: unknown;
|
|
67
|
+
headers: unknown;
|
|
68
|
+
response: {
|
|
69
|
+
200: {
|
|
70
|
+
error: string;
|
|
71
|
+
session?: undefined;
|
|
72
|
+
} | {
|
|
73
|
+
session: SSHTerminalSession;
|
|
74
|
+
error?: undefined;
|
|
75
|
+
};
|
|
76
|
+
422: {
|
|
77
|
+
type: "validation";
|
|
78
|
+
on: string;
|
|
79
|
+
summary?: string;
|
|
80
|
+
message?: string;
|
|
81
|
+
found?: unknown;
|
|
82
|
+
property?: string;
|
|
83
|
+
expected?: string;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
} & {
|
|
93
|
+
api: {
|
|
94
|
+
ssh: {
|
|
95
|
+
terminal: {
|
|
96
|
+
start: {
|
|
97
|
+
post: {
|
|
98
|
+
body: unknown;
|
|
99
|
+
params: {};
|
|
100
|
+
query: unknown;
|
|
101
|
+
headers: unknown;
|
|
102
|
+
response: {};
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
} & {
|
|
109
|
+
api: {
|
|
110
|
+
ssh: {
|
|
111
|
+
terminal: {
|
|
112
|
+
stop: {
|
|
113
|
+
post: {
|
|
114
|
+
body: unknown;
|
|
115
|
+
params: {};
|
|
116
|
+
query: unknown;
|
|
117
|
+
headers: unknown;
|
|
118
|
+
response: {
|
|
119
|
+
200: {
|
|
120
|
+
error: string;
|
|
121
|
+
success?: undefined;
|
|
122
|
+
sessionId?: undefined;
|
|
123
|
+
details?: undefined;
|
|
124
|
+
} | {
|
|
125
|
+
success: boolean;
|
|
126
|
+
sessionId: string;
|
|
127
|
+
error?: undefined;
|
|
128
|
+
details?: undefined;
|
|
129
|
+
} | {
|
|
130
|
+
error: string;
|
|
131
|
+
details: string;
|
|
132
|
+
success?: undefined;
|
|
133
|
+
sessionId?: undefined;
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
}, {
|
|
142
|
+
derive: {};
|
|
143
|
+
resolve: {};
|
|
144
|
+
schema: {};
|
|
145
|
+
standaloneSchema: {};
|
|
146
|
+
response: {};
|
|
147
|
+
}, {
|
|
148
|
+
derive: {};
|
|
149
|
+
resolve: {};
|
|
150
|
+
schema: {};
|
|
151
|
+
standaloneSchema: {};
|
|
152
|
+
response: {};
|
|
153
|
+
}>;
|
|
154
|
+
//# sourceMappingURL=remote-terminal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-terminal.d.ts","sourceRoot":"","sources":["../../src/routes/remote-terminal.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIhC,OAAO,KAAK,EACV,YAAY,EAEZ,kBAAkB,EAGnB,MAAM,aAAa,CAAC;AAsNrB,wBAAgB,mBAAmB,IAAI,IAAI,CAc1C;AAOD,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,GAChB;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAQnD;AAED,yEAAyE;AACzE,wBAAgB,oBAAoB,IAAI,kBAAkB,EAAE,CAE3D;AAMD,wBAAgB,0BAA0B,CAAC,YAAY,EAAE,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6RpE"}
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote terminal routes – start ttyd on a destination server via SSH,
|
|
3
|
+
* port-forward back to a local port on the agent, and serve it through
|
|
4
|
+
* the agent's existing terminal WebSocket proxy.
|
|
5
|
+
*
|
|
6
|
+
* Namespace: "ssh"
|
|
7
|
+
* Keys:
|
|
8
|
+
* "terminal-sessions" → JSON array of SSHTerminalSession objects
|
|
9
|
+
*/
|
|
10
|
+
import { Elysia } from "elysia";
|
|
11
|
+
import { Client } from "ssh2";
|
|
12
|
+
import { createServer } from "node:net";
|
|
13
|
+
const activeTerminals = new Map();
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// KV helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
async function getConnectionById(storage, id) {
|
|
18
|
+
const raw = await storage.get("ssh", "connections");
|
|
19
|
+
if (!raw)
|
|
20
|
+
return undefined;
|
|
21
|
+
const all = JSON.parse(raw);
|
|
22
|
+
return all.find((c) => c.id === id);
|
|
23
|
+
}
|
|
24
|
+
async function persistSessions(storage) {
|
|
25
|
+
const sessions = Array.from(activeTerminals.values()).map((t) => t.session);
|
|
26
|
+
await storage.set("ssh", "terminal-sessions", JSON.stringify(sessions));
|
|
27
|
+
}
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// SSH helpers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
async function buildConnectConfig(conn) {
|
|
32
|
+
const cfg = {
|
|
33
|
+
host: conn.host,
|
|
34
|
+
port: conn.port,
|
|
35
|
+
username: conn.username,
|
|
36
|
+
};
|
|
37
|
+
if (conn.privateKeyPath) {
|
|
38
|
+
const file = Bun.file(conn.privateKeyPath);
|
|
39
|
+
cfg.privateKey = Buffer.from(await file.arrayBuffer());
|
|
40
|
+
}
|
|
41
|
+
else if (conn.password) {
|
|
42
|
+
cfg.password = conn.password;
|
|
43
|
+
}
|
|
44
|
+
return cfg;
|
|
45
|
+
}
|
|
46
|
+
function sshExec(client, command) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
client.exec(command, (err, stream) => {
|
|
49
|
+
if (err)
|
|
50
|
+
return reject(err);
|
|
51
|
+
let stdout = "";
|
|
52
|
+
let stderr = "";
|
|
53
|
+
stream.on("data", (data) => {
|
|
54
|
+
stdout += data.toString();
|
|
55
|
+
});
|
|
56
|
+
stream.stderr.on("data", (data) => {
|
|
57
|
+
stderr += data.toString();
|
|
58
|
+
});
|
|
59
|
+
stream.on("close", (code) => {
|
|
60
|
+
resolve({ stdout: stdout.trim(), stderr: stderr.trim(), code });
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Find a free port on the remote server
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
async function findFreeRemotePort(client, start = 7881, end = 8080) {
|
|
69
|
+
for (let port = start; port <= end; port++) {
|
|
70
|
+
const { code } = await sshExec(client, `ss -tlnp 2>/dev/null | grep -q ':${port} ' && echo taken || echo free`);
|
|
71
|
+
// If grep finds the port, code is 0 and output contains "taken".
|
|
72
|
+
// We just check the stdout text directly.
|
|
73
|
+
const { stdout } = await sshExec(client, `ss -tlnp 2>/dev/null | grep -q ':${port} ' && echo taken || echo free`);
|
|
74
|
+
if (stdout === "free")
|
|
75
|
+
return port;
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`No free port found on remote server in range ${start}-${end}`);
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Find a free local port on the agent
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
function findFreeLocalPort(start = 7681, end = 7880) {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
let current = start;
|
|
85
|
+
function tryPort() {
|
|
86
|
+
if (current > end) {
|
|
87
|
+
return reject(new Error(`No free local port found in range ${start}-${end}`));
|
|
88
|
+
}
|
|
89
|
+
// Check if any active terminal already uses this port
|
|
90
|
+
for (const [, t] of activeTerminals) {
|
|
91
|
+
if (t.session.localPort === current) {
|
|
92
|
+
current++;
|
|
93
|
+
return tryPort();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const srv = createServer();
|
|
97
|
+
srv.once("error", () => {
|
|
98
|
+
current++;
|
|
99
|
+
tryPort();
|
|
100
|
+
});
|
|
101
|
+
srv.once("listening", () => {
|
|
102
|
+
srv.close(() => resolve(current));
|
|
103
|
+
});
|
|
104
|
+
srv.listen(current, "127.0.0.1");
|
|
105
|
+
}
|
|
106
|
+
tryPort();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Install ttyd on remote if missing
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
async function ensureTtydInstalled(client) {
|
|
113
|
+
const { code } = await sshExec(client, "which ttyd");
|
|
114
|
+
if (code === 0)
|
|
115
|
+
return true;
|
|
116
|
+
// Detect package manager and try to install
|
|
117
|
+
const { stdout: osRelease } = await sshExec(client, "cat /etc/os-release 2>/dev/null || echo unknown");
|
|
118
|
+
let installCmd;
|
|
119
|
+
if (osRelease.includes("debian") ||
|
|
120
|
+
osRelease.includes("ubuntu") ||
|
|
121
|
+
osRelease.includes("ID=debian") ||
|
|
122
|
+
osRelease.includes("ID=ubuntu")) {
|
|
123
|
+
installCmd =
|
|
124
|
+
"sudo apt-get update -qq && sudo apt-get install -y -qq ttyd 2>/dev/null";
|
|
125
|
+
}
|
|
126
|
+
else if (osRelease.includes("centos") ||
|
|
127
|
+
osRelease.includes("fedora") ||
|
|
128
|
+
osRelease.includes("rhel") ||
|
|
129
|
+
osRelease.includes("ID=centos") ||
|
|
130
|
+
osRelease.includes("ID=fedora")) {
|
|
131
|
+
installCmd = "sudo yum install -y -q ttyd 2>/dev/null";
|
|
132
|
+
}
|
|
133
|
+
else if (osRelease.includes("alpine") || osRelease.includes("ID=alpine")) {
|
|
134
|
+
installCmd = "sudo apk add --quiet ttyd 2>/dev/null";
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// Fallback: try downloading binary from GitHub
|
|
138
|
+
const { stdout: arch } = await sshExec(client, "uname -m");
|
|
139
|
+
const archMap = {
|
|
140
|
+
x86_64: "x86_64",
|
|
141
|
+
aarch64: "aarch64",
|
|
142
|
+
arm64: "aarch64",
|
|
143
|
+
};
|
|
144
|
+
const mappedArch = archMap[arch] || "x86_64";
|
|
145
|
+
installCmd = `curl -fsSL "https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.${mappedArch}" -o /tmp/ttyd && chmod +x /tmp/ttyd && sudo mv /tmp/ttyd /usr/local/bin/ttyd`;
|
|
146
|
+
}
|
|
147
|
+
const result = await sshExec(client, installCmd);
|
|
148
|
+
if (result.code !== 0) {
|
|
149
|
+
// Try without sudo
|
|
150
|
+
const { code: retryCode } = await sshExec(client, installCmd.replace(/sudo /g, ""));
|
|
151
|
+
return retryCode === 0;
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Cleanup
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
export function cleanupAllTerminals() {
|
|
159
|
+
for (const [id, terminal] of activeTerminals) {
|
|
160
|
+
try {
|
|
161
|
+
if (terminal.sshTunnelProc)
|
|
162
|
+
terminal.sshTunnelProc.kill();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
/* ignore */
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
terminal.sshClient.end();
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
/* ignore */
|
|
172
|
+
}
|
|
173
|
+
activeTerminals.delete(id);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Session provider shim – exposes getTerminalInfo() so the agent's terminal
|
|
178
|
+
// proxy at /terminal/:sessionId/ws can find and route to our forwarded port.
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
export function getTerminalInfo(sessionId) {
|
|
181
|
+
const terminal = activeTerminals.get(sessionId);
|
|
182
|
+
if (!terminal || terminal.session.status !== "active")
|
|
183
|
+
return null;
|
|
184
|
+
return {
|
|
185
|
+
url: `http://127.0.0.1:${terminal.session.localPort}`,
|
|
186
|
+
port: terminal.session.localPort,
|
|
187
|
+
pid: process.pid, // Agent's own PID – always alive while agent runs
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/** List all active terminal sessions (for the session provider shim). */
|
|
191
|
+
export function listTerminalSessions() {
|
|
192
|
+
return Array.from(activeTerminals.values()).map((t) => t.session);
|
|
193
|
+
}
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Route factory
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
export function createRemoteTerminalRoutes(hostServices) {
|
|
198
|
+
const { storage, broadcast } = hostServices;
|
|
199
|
+
return (new Elysia({ prefix: "/api/ssh/terminal" })
|
|
200
|
+
// -----------------------------------------------------------------------
|
|
201
|
+
// GET /api/ssh/terminal/sessions — list active remote terminal sessions
|
|
202
|
+
// -----------------------------------------------------------------------
|
|
203
|
+
.get("/sessions", () => {
|
|
204
|
+
return { sessions: listTerminalSessions() };
|
|
205
|
+
})
|
|
206
|
+
// -----------------------------------------------------------------------
|
|
207
|
+
// GET /api/ssh/terminal/sessions/:id — get a specific session
|
|
208
|
+
// -----------------------------------------------------------------------
|
|
209
|
+
.get("/sessions/:id", ({ params, set }) => {
|
|
210
|
+
const terminal = activeTerminals.get(params.id);
|
|
211
|
+
if (!terminal) {
|
|
212
|
+
set.status = 404;
|
|
213
|
+
return { error: "Terminal session not found" };
|
|
214
|
+
}
|
|
215
|
+
return { session: terminal.session };
|
|
216
|
+
})
|
|
217
|
+
// -----------------------------------------------------------------------
|
|
218
|
+
// POST /api/ssh/terminal/start — start remote ttyd + SSH port forward
|
|
219
|
+
// -----------------------------------------------------------------------
|
|
220
|
+
.post("/start", async ({ body, set }) => {
|
|
221
|
+
const { connectionId, shell = "bash" } = body;
|
|
222
|
+
try {
|
|
223
|
+
// 1. Look up connection
|
|
224
|
+
const connConfig = await getConnectionById(storage, connectionId);
|
|
225
|
+
if (!connConfig) {
|
|
226
|
+
set.status = 404;
|
|
227
|
+
return { error: "SSH connection not found" };
|
|
228
|
+
}
|
|
229
|
+
const sessionId = globalThis.crypto.randomUUID();
|
|
230
|
+
const session = {
|
|
231
|
+
id: sessionId,
|
|
232
|
+
connectionId,
|
|
233
|
+
remotePort: 0,
|
|
234
|
+
localPort: 0,
|
|
235
|
+
remotePid: null,
|
|
236
|
+
shell,
|
|
237
|
+
status: "starting",
|
|
238
|
+
startedAt: new Date().toISOString(),
|
|
239
|
+
};
|
|
240
|
+
// 2. Establish SSH connection
|
|
241
|
+
const sshClient = new Client();
|
|
242
|
+
const connectConfig = await buildConnectConfig(connConfig);
|
|
243
|
+
return new Promise((resolve) => {
|
|
244
|
+
sshClient.on("error", (err) => {
|
|
245
|
+
session.status = "error";
|
|
246
|
+
session.error = err.message;
|
|
247
|
+
set.status = 500;
|
|
248
|
+
resolve({
|
|
249
|
+
error: "SSH connection failed",
|
|
250
|
+
details: err.message,
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
sshClient.on("ready", async () => {
|
|
254
|
+
try {
|
|
255
|
+
// 3. Ensure ttyd is installed on remote
|
|
256
|
+
const installed = await ensureTtydInstalled(sshClient);
|
|
257
|
+
if (!installed) {
|
|
258
|
+
sshClient.end();
|
|
259
|
+
session.status = "error";
|
|
260
|
+
session.error = "Failed to install ttyd on remote server";
|
|
261
|
+
set.status = 500;
|
|
262
|
+
return resolve({
|
|
263
|
+
error: "ttyd is not installed on the remote server and auto-install failed",
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
// 4. Find free remote port
|
|
267
|
+
const remotePort = await findFreeRemotePort(sshClient);
|
|
268
|
+
session.remotePort = remotePort;
|
|
269
|
+
// 5. Start ttyd on remote
|
|
270
|
+
const { stdout: pidOutput, code: startCode } = await sshExec(sshClient, `nohup ttyd --writable --port ${remotePort} ${shell} > /dev/null 2>&1 & echo $!`);
|
|
271
|
+
if (startCode !== 0 || !pidOutput) {
|
|
272
|
+
sshClient.end();
|
|
273
|
+
session.status = "error";
|
|
274
|
+
session.error = "Failed to start ttyd on remote";
|
|
275
|
+
set.status = 500;
|
|
276
|
+
return resolve({ error: "Failed to start ttyd on remote" });
|
|
277
|
+
}
|
|
278
|
+
const remotePid = parseInt(pidOutput, 10);
|
|
279
|
+
session.remotePid = remotePid;
|
|
280
|
+
// 6. Wait for ttyd to bind and verify it's listening
|
|
281
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
282
|
+
const { stdout: listenCheck } = await sshExec(sshClient, `ss -tlnp 2>/dev/null | grep ':${remotePort} ' || echo NOT_LISTENING`);
|
|
283
|
+
if (listenCheck.includes("NOT_LISTENING")) {
|
|
284
|
+
// ttyd may need more time
|
|
285
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
286
|
+
}
|
|
287
|
+
// 7. Allocate local port + create SSH port forward
|
|
288
|
+
// Uses a real `ssh -L` subprocess for the tunnel
|
|
289
|
+
// because ssh2's forwardOut + Bun has data piping
|
|
290
|
+
// issues with WebSocket-heavy protocols like ttyd.
|
|
291
|
+
const localPort = await findFreeLocalPort();
|
|
292
|
+
session.localPort = localPort;
|
|
293
|
+
try {
|
|
294
|
+
// Build ssh command args
|
|
295
|
+
const sshArgs = [
|
|
296
|
+
"-N", // No remote command
|
|
297
|
+
"-L",
|
|
298
|
+
`${localPort}:127.0.0.1:${remotePort}`,
|
|
299
|
+
"-o",
|
|
300
|
+
"StrictHostKeyChecking=accept-new",
|
|
301
|
+
"-o",
|
|
302
|
+
"ServerAliveInterval=30",
|
|
303
|
+
"-o",
|
|
304
|
+
"ExitOnForwardFailure=yes",
|
|
305
|
+
"-p",
|
|
306
|
+
String(connConfig.port),
|
|
307
|
+
];
|
|
308
|
+
if (connConfig.privateKeyPath) {
|
|
309
|
+
sshArgs.push("-i", connConfig.privateKeyPath);
|
|
310
|
+
}
|
|
311
|
+
sshArgs.push(`${connConfig.username}@${connConfig.host}`);
|
|
312
|
+
const sshProc = Bun.spawn(["ssh", ...sshArgs], {
|
|
313
|
+
stdout: "ignore",
|
|
314
|
+
stderr: "pipe",
|
|
315
|
+
});
|
|
316
|
+
// Wait for the tunnel to be ready
|
|
317
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
318
|
+
// Verify the local port is listening
|
|
319
|
+
const checkSrv = createServer();
|
|
320
|
+
const portBound = await new Promise((res) => {
|
|
321
|
+
checkSrv.once("error", () => res(true)); // Port in use = tunnel working
|
|
322
|
+
checkSrv.once("listening", () => {
|
|
323
|
+
checkSrv.close();
|
|
324
|
+
res(false); // Port free = tunnel not ready
|
|
325
|
+
});
|
|
326
|
+
checkSrv.listen(localPort, "127.0.0.1");
|
|
327
|
+
});
|
|
328
|
+
if (!portBound) {
|
|
329
|
+
// Tunnel didn't bind yet, wait more
|
|
330
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
331
|
+
}
|
|
332
|
+
session.status = "active";
|
|
333
|
+
activeTerminals.set(sessionId, {
|
|
334
|
+
sshClient,
|
|
335
|
+
sshTunnelProc: sshProc,
|
|
336
|
+
session,
|
|
337
|
+
});
|
|
338
|
+
void persistSessions(storage);
|
|
339
|
+
if (broadcast) {
|
|
340
|
+
broadcast("ssh:terminal:started", {
|
|
341
|
+
sessionId,
|
|
342
|
+
connectionId,
|
|
343
|
+
localPort,
|
|
344
|
+
remotePort,
|
|
345
|
+
host: connConfig.host,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
resolve({
|
|
349
|
+
sessionId,
|
|
350
|
+
connectionId,
|
|
351
|
+
localPort,
|
|
352
|
+
remotePort,
|
|
353
|
+
host: connConfig.host,
|
|
354
|
+
status: "active",
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
catch (bindErr) {
|
|
358
|
+
void sshExec(sshClient, `kill ${remotePid} 2>/dev/null`);
|
|
359
|
+
sshClient.end();
|
|
360
|
+
session.status = "error";
|
|
361
|
+
session.error =
|
|
362
|
+
bindErr instanceof Error
|
|
363
|
+
? bindErr.message
|
|
364
|
+
: "Port bind failed";
|
|
365
|
+
set.status = 500;
|
|
366
|
+
resolve({
|
|
367
|
+
error: "Failed to bind local port",
|
|
368
|
+
details: session.error,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
sshClient.end();
|
|
374
|
+
session.status = "error";
|
|
375
|
+
session.error =
|
|
376
|
+
err instanceof Error ? err.message : "Unknown error";
|
|
377
|
+
set.status = 500;
|
|
378
|
+
resolve({
|
|
379
|
+
error: "Failed to start remote terminal",
|
|
380
|
+
details: session.error,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
sshClient.connect(connectConfig);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
set.status = 500;
|
|
389
|
+
return {
|
|
390
|
+
error: "Failed to start remote terminal",
|
|
391
|
+
details: error instanceof Error ? error.message : "Unknown error",
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
// -----------------------------------------------------------------------
|
|
396
|
+
// POST /api/ssh/terminal/stop — stop a remote ttyd session
|
|
397
|
+
// -----------------------------------------------------------------------
|
|
398
|
+
.post("/stop", async ({ body, set }) => {
|
|
399
|
+
const { sessionId } = body;
|
|
400
|
+
const terminal = activeTerminals.get(sessionId);
|
|
401
|
+
if (!terminal) {
|
|
402
|
+
set.status = 404;
|
|
403
|
+
return { error: "Terminal session not found" };
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
terminal.session.status = "stopping";
|
|
407
|
+
// Kill remote ttyd process
|
|
408
|
+
if (terminal.session.remotePid) {
|
|
409
|
+
await sshExec(terminal.sshClient, `kill ${terminal.session.remotePid} 2>/dev/null`).catch(() => { });
|
|
410
|
+
}
|
|
411
|
+
// Kill SSH tunnel subprocess
|
|
412
|
+
if (terminal.sshTunnelProc) {
|
|
413
|
+
terminal.sshTunnelProc.kill();
|
|
414
|
+
}
|
|
415
|
+
// Close SSH connection (used for exec commands)
|
|
416
|
+
terminal.sshClient.end();
|
|
417
|
+
terminal.session.status = "stopped";
|
|
418
|
+
activeTerminals.delete(sessionId);
|
|
419
|
+
void persistSessions(storage);
|
|
420
|
+
if (broadcast) {
|
|
421
|
+
broadcast("ssh:terminal:stopped", {
|
|
422
|
+
sessionId,
|
|
423
|
+
connectionId: terminal.session.connectionId,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return { success: true, sessionId };
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
set.status = 500;
|
|
430
|
+
return {
|
|
431
|
+
error: "Failed to stop terminal session",
|
|
432
|
+
details: error instanceof Error ? error.message : "Unknown error",
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}));
|
|
436
|
+
}
|
|
437
|
+
//# sourceMappingURL=remote-terminal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-terminal.js","sourceRoot":"","sources":["../../src/routes/remote-terminal.ts"],"names":[],"mappings":"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"}
|