renote-server 1.0.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.
@@ -0,0 +1,329 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.sshManager = exports.SSHManager = exports.SSHConnection = void 0;
37
+ const ssh2_1 = require("ssh2");
38
+ const net = __importStar(require("net"));
39
+ const logger_1 = require("../utils/logger");
40
+ /**
41
+ * Manages a single SSH connection with multiple shell sessions and port forwarding capabilities.
42
+ */
43
+ class SSHConnection {
44
+ constructor() {
45
+ this.shells = new Map();
46
+ this.portForwards = new Map();
47
+ this.connected = false;
48
+ this.client = new ssh2_1.Client();
49
+ }
50
+ /**
51
+ * Establish SSH connection with the remote server.
52
+ * Supports both private key and password authentication.
53
+ */
54
+ async connect(config) {
55
+ return new Promise((resolve, reject) => {
56
+ this.client.on('ready', () => {
57
+ this.connected = true;
58
+ logger_1.logger.info(`SSH connected to ${config.host}:${config.port}`);
59
+ resolve();
60
+ });
61
+ this.client.on('error', (err) => {
62
+ logger_1.logger.error('SSH connection error:', err);
63
+ this.connected = false;
64
+ reject(err);
65
+ });
66
+ this.client.on('close', () => {
67
+ this.connected = false;
68
+ logger_1.logger.info('SSH connection closed');
69
+ });
70
+ const connectionConfig = {
71
+ host: config.host,
72
+ port: config.port,
73
+ username: config.username,
74
+ readyTimeout: 30000,
75
+ keepaliveInterval: 10000, // Send keepalive every 10 seconds
76
+ keepaliveCountMax: 3, // Disconnect after 3 missed keepalives
77
+ };
78
+ if (config.privateKey) {
79
+ connectionConfig.privateKey = config.privateKey;
80
+ }
81
+ else if (config.password) {
82
+ connectionConfig.password = config.password;
83
+ }
84
+ this.client.connect(connectionConfig);
85
+ });
86
+ }
87
+ /**
88
+ * Start an interactive PTY shell session with a specific session ID.
89
+ * @param sessionId Unique identifier for this shell session
90
+ * @param onData Callback for shell output data
91
+ * @param onClose Callback when shell closes
92
+ * @param cols Terminal columns (default 80)
93
+ * @param rows Terminal rows (default 24)
94
+ */
95
+ async startShell(sessionId, onData, onClose, cols = 80, rows = 24) {
96
+ return new Promise((resolve, reject) => {
97
+ if (!this.connected) {
98
+ reject(new Error('SSH not connected'));
99
+ return;
100
+ }
101
+ if (this.shells.has(sessionId)) {
102
+ reject(new Error(`Shell session ${sessionId} already exists`));
103
+ return;
104
+ }
105
+ this.client.shell({
106
+ term: 'xterm-256color',
107
+ cols,
108
+ rows,
109
+ }, (err, stream) => {
110
+ if (err) {
111
+ logger_1.logger.error(`Failed to start shell ${sessionId}:`, err);
112
+ reject(err);
113
+ return;
114
+ }
115
+ this.shells.set(sessionId, stream);
116
+ stream.on('data', (data) => {
117
+ onData(data.toString());
118
+ });
119
+ stream.on('close', () => {
120
+ this.shells.delete(sessionId);
121
+ onClose();
122
+ });
123
+ stream.stderr.on('data', (data) => {
124
+ onData(data.toString());
125
+ });
126
+ logger_1.logger.info(`SSH shell ${sessionId} started`);
127
+ resolve();
128
+ });
129
+ });
130
+ }
131
+ /**
132
+ * Write data to a specific shell session.
133
+ */
134
+ writeToShell(sessionId, data) {
135
+ const shell = this.shells.get(sessionId);
136
+ if (shell) {
137
+ shell.write(data);
138
+ return true;
139
+ }
140
+ return false;
141
+ }
142
+ /**
143
+ * Resize a specific shell's terminal window.
144
+ */
145
+ resizeShell(sessionId, cols, rows) {
146
+ const shell = this.shells.get(sessionId);
147
+ if (shell) {
148
+ shell.setWindow(rows, cols, 0, 0);
149
+ return true;
150
+ }
151
+ return false;
152
+ }
153
+ /**
154
+ * Close a specific shell session.
155
+ */
156
+ closeShell(sessionId) {
157
+ const shell = this.shells.get(sessionId);
158
+ if (shell) {
159
+ shell.close();
160
+ this.shells.delete(sessionId);
161
+ logger_1.logger.info(`SSH shell ${sessionId} closed`);
162
+ return true;
163
+ }
164
+ return false;
165
+ }
166
+ /**
167
+ * Get list of active shell session IDs.
168
+ */
169
+ getActiveShells() {
170
+ return Array.from(this.shells.keys());
171
+ }
172
+ /**
173
+ * Check if a shell session exists.
174
+ */
175
+ hasShell(sessionId) {
176
+ return this.shells.has(sessionId);
177
+ }
178
+ /**
179
+ * Legacy: Write data to the first available shell (backward compatibility).
180
+ * @deprecated Use writeToShell with sessionId instead
181
+ */
182
+ write(data) {
183
+ const firstShell = this.shells.values().next().value;
184
+ if (firstShell) {
185
+ firstShell.write(data);
186
+ }
187
+ }
188
+ /**
189
+ * Legacy: Resize the first available shell (backward compatibility).
190
+ * @deprecated Use resizeShell with sessionId instead
191
+ */
192
+ resize(cols, rows) {
193
+ const firstShell = this.shells.values().next().value;
194
+ if (firstShell) {
195
+ firstShell.setWindow(rows, cols, 0, 0);
196
+ }
197
+ }
198
+ /**
199
+ * Set up local port forwarding.
200
+ * Creates a local TCP server that forwards connections to the remote host.
201
+ */
202
+ async setupPortForward(config) {
203
+ return new Promise((resolve, reject) => {
204
+ if (!this.connected) {
205
+ reject(new Error('SSH not connected'));
206
+ return;
207
+ }
208
+ // Check if port is already forwarded
209
+ if (this.portForwards.has(config.localPort)) {
210
+ reject(new Error(`Port ${config.localPort} is already forwarded`));
211
+ return;
212
+ }
213
+ const server = net.createServer((socket) => {
214
+ this.client.forwardOut('127.0.0.1', config.localPort, config.remoteHost, config.remotePort, (err, stream) => {
215
+ if (err) {
216
+ logger_1.logger.error('Port forward error:', err);
217
+ socket.end();
218
+ return;
219
+ }
220
+ socket.pipe(stream);
221
+ stream.pipe(socket);
222
+ socket.on('error', (err) => {
223
+ logger_1.logger.error('Socket error:', err);
224
+ stream.close();
225
+ });
226
+ stream.on('error', (err) => {
227
+ logger_1.logger.error('Stream error:', err);
228
+ socket.destroy();
229
+ });
230
+ });
231
+ });
232
+ server.on('error', (err) => {
233
+ logger_1.logger.error('Server error:', err);
234
+ reject(err);
235
+ });
236
+ server.listen(config.localPort, '127.0.0.1', () => {
237
+ this.portForwards.set(config.localPort, server);
238
+ logger_1.logger.info(`Port forward established: localhost:${config.localPort} -> ${config.remoteHost}:${config.remotePort}`);
239
+ resolve();
240
+ });
241
+ });
242
+ }
243
+ /**
244
+ * Stop a specific port forward.
245
+ */
246
+ stopPortForward(localPort) {
247
+ const server = this.portForwards.get(localPort);
248
+ if (server) {
249
+ server.close();
250
+ this.portForwards.delete(localPort);
251
+ logger_1.logger.info(`Port forward stopped: localhost:${localPort}`);
252
+ }
253
+ }
254
+ /**
255
+ * Disconnect and clean up all resources.
256
+ */
257
+ disconnect() {
258
+ // Close all shell sessions
259
+ this.shells.forEach((shell, sessionId) => {
260
+ shell.close();
261
+ logger_1.logger.info(`Shell ${sessionId} closed`);
262
+ });
263
+ this.shells.clear();
264
+ // Stop all port forwards
265
+ this.portForwards.forEach((server, port) => {
266
+ server.close();
267
+ logger_1.logger.info(`Port forward stopped: localhost:${port}`);
268
+ });
269
+ this.portForwards.clear();
270
+ // End SSH connection
271
+ if (this.connected) {
272
+ this.client.end();
273
+ this.connected = false;
274
+ }
275
+ logger_1.logger.info('SSH connection disconnected');
276
+ }
277
+ isConnected() {
278
+ return this.connected;
279
+ }
280
+ }
281
+ exports.SSHConnection = SSHConnection;
282
+ /**
283
+ * Manages multiple SSH connections, one per WebSocket client.
284
+ */
285
+ class SSHManager {
286
+ constructor() {
287
+ this.connections = new Map();
288
+ }
289
+ /**
290
+ * Get or create an SSH connection for a client.
291
+ */
292
+ getConnection(clientId) {
293
+ let connection = this.connections.get(clientId);
294
+ if (!connection) {
295
+ connection = new SSHConnection();
296
+ this.connections.set(clientId, connection);
297
+ }
298
+ return connection;
299
+ }
300
+ /**
301
+ * Check if a client has an active connection.
302
+ */
303
+ hasConnection(clientId) {
304
+ return this.connections.has(clientId);
305
+ }
306
+ /**
307
+ * Remove and disconnect a client's SSH connection.
308
+ */
309
+ removeConnection(clientId) {
310
+ const connection = this.connections.get(clientId);
311
+ if (connection) {
312
+ connection.disconnect();
313
+ this.connections.delete(clientId);
314
+ }
315
+ }
316
+ /**
317
+ * Clean up all connections (for server shutdown).
318
+ */
319
+ cleanup() {
320
+ this.connections.forEach((connection, clientId) => {
321
+ connection.disconnect();
322
+ logger_1.logger.info(`Cleaned up SSH connection for client ${clientId}`);
323
+ });
324
+ this.connections.clear();
325
+ }
326
+ }
327
+ exports.SSHManager = SSHManager;
328
+ // Singleton instance
329
+ exports.sshManager = new SSHManager();
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.terminalWebSocketHandler = exports.ZellijTerminalConnection = exports.LocalTerminalConnection = exports.localTerminalManager = exports.LocalTerminalHandler = void 0;
4
+ var localTerminalHandler_1 = require("./localTerminalHandler");
5
+ Object.defineProperty(exports, "LocalTerminalHandler", { enumerable: true, get: function () { return localTerminalHandler_1.LocalTerminalHandler; } });
6
+ var localTerminalManager_1 = require("./localTerminalManager");
7
+ Object.defineProperty(exports, "localTerminalManager", { enumerable: true, get: function () { return localTerminalManager_1.localTerminalManager; } });
8
+ Object.defineProperty(exports, "LocalTerminalConnection", { enumerable: true, get: function () { return localTerminalManager_1.LocalTerminalConnection; } });
9
+ Object.defineProperty(exports, "ZellijTerminalConnection", { enumerable: true, get: function () { return localTerminalManager_1.ZellijTerminalConnection; } });
10
+ var terminalWebSocket_1 = require("./terminalWebSocket");
11
+ Object.defineProperty(exports, "terminalWebSocketHandler", { enumerable: true, get: function () { return terminalWebSocket_1.terminalWebSocketHandler; } });
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocalTerminalHandler = void 0;
4
+ const localTerminalManager_1 = require("./localTerminalManager");
5
+ const logger_1 = require("../utils/logger");
6
+ class LocalTerminalHandler {
7
+ constructor(sendFn) {
8
+ this.sendFn = sendFn;
9
+ }
10
+ canHandle(messageType) {
11
+ return messageType.startsWith('terminal_');
12
+ }
13
+ async handle(ws, clientId, message) {
14
+ switch (message.type) {
15
+ case 'terminal_start':
16
+ await this.handleStart(ws, clientId, message);
17
+ break;
18
+ case 'terminal_input':
19
+ this.handleInput(clientId, message);
20
+ break;
21
+ case 'terminal_resize':
22
+ this.handleResize(clientId, message);
23
+ break;
24
+ case 'terminal_close':
25
+ this.handleClose(ws, clientId, message);
26
+ break;
27
+ case 'terminal_list':
28
+ this.handleList(ws, clientId);
29
+ break;
30
+ default:
31
+ logger_1.logger.warn(`Unknown terminal message type: ${message.type}`);
32
+ }
33
+ }
34
+ async handleStart(ws, clientId, message) {
35
+ const { sessionId, type, cwd, cols, rows, claudeArgs } = message.data || {};
36
+ if (!sessionId) {
37
+ this.sendFn(ws, {
38
+ type: 'terminal_start_response',
39
+ data: { success: false, message: 'sessionId is required' },
40
+ });
41
+ return;
42
+ }
43
+ const terminalType = type === 'claude' ? 'claude' : 'shell';
44
+ const options = {
45
+ type: terminalType,
46
+ cwd,
47
+ cols,
48
+ rows,
49
+ claudeArgs,
50
+ };
51
+ const connection = localTerminalManager_1.localTerminalManager.getOrCreateConnection(clientId);
52
+ const success = connection.startTerminal(sessionId, (data) => {
53
+ // Send terminal output to client
54
+ this.sendFn(ws, {
55
+ type: 'terminal_output',
56
+ data: { sessionId, output: data },
57
+ });
58
+ }, () => {
59
+ // Notify client when terminal closes
60
+ this.sendFn(ws, {
61
+ type: 'terminal_closed',
62
+ data: { sessionId },
63
+ });
64
+ }, options);
65
+ this.sendFn(ws, {
66
+ type: 'terminal_start_response',
67
+ data: {
68
+ success,
69
+ sessionId,
70
+ terminalType,
71
+ message: success ? 'Terminal started' : 'Failed to start terminal',
72
+ },
73
+ });
74
+ }
75
+ handleInput(clientId, message) {
76
+ const { sessionId, input } = message.data || {};
77
+ if (!sessionId || input === undefined) {
78
+ logger_1.logger.warn('Invalid terminal_input message: missing sessionId or input');
79
+ return;
80
+ }
81
+ const connection = localTerminalManager_1.localTerminalManager.getConnection(clientId);
82
+ if (!connection) {
83
+ logger_1.logger.warn(`No terminal connection for client ${clientId}`);
84
+ return;
85
+ }
86
+ connection.writeToTerminal(sessionId, input);
87
+ }
88
+ handleResize(clientId, message) {
89
+ const { sessionId, cols, rows } = message.data || {};
90
+ if (!sessionId || !cols || !rows) {
91
+ logger_1.logger.warn('Invalid terminal_resize message');
92
+ return;
93
+ }
94
+ const connection = localTerminalManager_1.localTerminalManager.getConnection(clientId);
95
+ if (!connection) {
96
+ logger_1.logger.warn(`No terminal connection for client ${clientId}`);
97
+ return;
98
+ }
99
+ connection.resizeTerminal(sessionId, cols, rows);
100
+ }
101
+ handleClose(ws, clientId, message) {
102
+ const { sessionId, kill } = message.data || {};
103
+ if (!sessionId) {
104
+ logger_1.logger.warn('Invalid terminal_close message: missing sessionId');
105
+ return;
106
+ }
107
+ let success = false;
108
+ const connection = localTerminalManager_1.localTerminalManager.getConnection(clientId);
109
+ if (connection) {
110
+ // kill=true will also kill the zellij session; default is just detach
111
+ success = connection.closeTerminal(sessionId, kill === true);
112
+ }
113
+ else if (kill) {
114
+ // No connection found, but user wants to kill - try direct kill by sessionId
115
+ // This handles the case where terminal was created via /terminal direct WebSocket
116
+ success = localTerminalManager_1.ZellijTerminalConnection.killSessionById(sessionId);
117
+ logger_1.logger.info(`Direct kill for session ${sessionId}: ${success}`);
118
+ }
119
+ this.sendFn(ws, {
120
+ type: 'terminal_close_response',
121
+ data: { success, sessionId },
122
+ });
123
+ }
124
+ handleList(ws, clientId) {
125
+ const connection = localTerminalManager_1.localTerminalManager.getConnection(clientId);
126
+ const terminals = connection ? connection.getActiveTerminals() : [];
127
+ const terminalInfos = terminals.map((id) => {
128
+ const info = connection?.getTerminalInfo(id);
129
+ return {
130
+ sessionId: id,
131
+ type: info?.type || 'shell',
132
+ createdAt: info?.createdAt || 0,
133
+ };
134
+ });
135
+ this.sendFn(ws, {
136
+ type: 'terminal_list_response',
137
+ data: { terminals: terminalInfos },
138
+ });
139
+ }
140
+ cleanup(clientId) {
141
+ localTerminalManager_1.localTerminalManager.removeConnection(clientId);
142
+ }
143
+ }
144
+ exports.LocalTerminalHandler = LocalTerminalHandler;