wative 1.1.16 → 1.1.18
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/account.d.ts +14 -0
- package/lib/account.d.ts.map +1 -0
- package/lib/assets.d.ts +11 -0
- package/lib/assets.d.ts.map +1 -0
- package/lib/chain.d.ts +6 -0
- package/lib/chain.d.ts.map +1 -0
- package/lib/config.d.ts +23 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/daemon-watcher.js +181 -0
- package/lib/home_page.d.ts +4 -0
- package/lib/home_page.d.ts.map +1 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.esm.js +1 -1
- package/lib/index.umd.js +1 -1
- package/lib/network.d.ts +28 -0
- package/lib/network.d.ts.map +1 -0
- package/lib/ssh/client.d.ts +120 -0
- package/lib/ssh/client.d.ts.map +1 -0
- package/lib/ssh/client_example.d.ts +19 -0
- package/lib/ssh/client_example.d.ts.map +1 -0
- package/lib/ssh/client_test.d.ts +27 -0
- package/lib/ssh/client_test.d.ts.map +1 -0
- package/lib/ssh/config_manager.d.ts +55 -0
- package/lib/ssh/config_manager.d.ts.map +1 -0
- package/lib/ssh/config_template.d.ts +52 -0
- package/lib/ssh/config_template.d.ts.map +1 -0
- package/lib/ssh/index.d.ts +8 -0
- package/lib/ssh/index.d.ts.map +1 -0
- package/lib/ssh/remote_server_example.d.ts +16 -0
- package/lib/ssh/remote_server_example.d.ts.map +1 -0
- package/lib/ssh/service_manager.d.ts +119 -0
- package/lib/ssh/service_manager.d.ts.map +1 -0
- package/lib/ssh/types.d.ts +45 -0
- package/lib/ssh/types.d.ts.map +1 -0
- package/lib/ssh/utils.d.ts +11 -0
- package/lib/ssh/utils.d.ts.map +1 -0
- package/lib/ssh/wative_server.d.ts +26 -0
- package/lib/ssh/wative_server.d.ts.map +1 -0
- package/lib/tools.d.ts +6 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tx_gas_utils.d.ts +18 -0
- package/lib/tx_gas_utils.d.ts.map +1 -0
- package/lib/utils.d.ts +49 -0
- package/lib/utils.d.ts.map +1 -0
- package/lib/wative.d.ts +4 -0
- package/lib/wative.d.ts.map +1 -0
- package/lib/wative.esm.js +1 -1
- package/lib/wative.umd.js +1 -1
- package/lib/web3.d.ts +59 -0
- package/lib/web3.d.ts.map +1 -0
- package/package.json +10 -6
- package/src/daemon-watcher.js +181 -0
- package/src/index.ts +22 -6
- package/src/ssh/client.rs +221 -0
- package/src/ssh/client.ts +389 -0
- package/src/ssh/config_manager.ts +317 -0
- package/src/ssh/index.ts +50 -36
- package/src/ssh/service_manager.ts +684 -0
- package/src/ssh/types.ts +35 -1
- package/src/ssh/wative_server.ts +1 -2
- package/src/wative.ts +566 -122
- package/bin/wative-ssh.sh +0 -44
- package/lib/wative-ssh.esm.js +0 -1
- package/lib/wative-ssh.umd.js +0 -1
package/src/wative.ts
CHANGED
|
@@ -1,20 +1,58 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
|
-
import {
|
|
4
|
+
import { SSHServiceManager } from './ssh/service_manager';
|
|
5
|
+
import { SSHConfigManager } from './ssh/config_manager';
|
|
5
6
|
import * as fs from 'fs';
|
|
7
|
+
import { spawn } from 'child_process';
|
|
6
8
|
|
|
7
|
-
const os = require('os');
|
|
8
9
|
const program = new Command();
|
|
9
10
|
const packageJson = require('../package.json');
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
// 全局服务管理器实例
|
|
13
|
+
let serviceManager: SSHServiceManager;
|
|
14
|
+
|
|
15
|
+
// 守护进程监控器
|
|
16
|
+
const startDaemonWatcher = (serviceName: string, encryptedPasswords: string) => {
|
|
17
|
+
const logDir = path.join(process.env.HOME || '', '.wative', 'logs');
|
|
18
|
+
const pidDir = path.join(process.env.HOME || '', '.wative', 'ssh', 'pids');
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(logDir)) {
|
|
21
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
if (!fs.existsSync(pidDir)) {
|
|
24
|
+
fs.mkdirSync(pidDir, { recursive: true });
|
|
17
25
|
}
|
|
26
|
+
|
|
27
|
+
const logFile = path.join(logDir, `${serviceName}-daemon.log`);
|
|
28
|
+
const pidFile = path.join(pidDir, `${serviceName}-daemon.pid`);
|
|
29
|
+
const watcherPidFile = path.join(pidDir, `${serviceName}-watcher.pid`);
|
|
30
|
+
|
|
31
|
+
// Start daemon watcher process
|
|
32
|
+
const watcher = spawn('node', [
|
|
33
|
+
path.resolve(__dirname, 'daemon-watcher.js'),
|
|
34
|
+
serviceName,
|
|
35
|
+
logFile,
|
|
36
|
+
pidFile,
|
|
37
|
+
watcherPidFile,
|
|
38
|
+
path.resolve(__dirname, '..')
|
|
39
|
+
], {
|
|
40
|
+
detached: true,
|
|
41
|
+
stdio: 'ignore',
|
|
42
|
+
env: {
|
|
43
|
+
...process.env,
|
|
44
|
+
WATIVE_ENCRYPTED_PASSWORDS: encryptedPasswords
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// 写入监控器PID文件
|
|
49
|
+
fs.writeFileSync(watcherPidFile, watcher.pid!.toString());
|
|
50
|
+
|
|
51
|
+
watcher.unref();
|
|
52
|
+
|
|
53
|
+
console.log(`SSH service '${serviceName}' daemon watcher started with PID: ${watcher.pid}`);
|
|
54
|
+
console.log(`Daemon log file: ${logFile}`);
|
|
55
|
+
console.log(`Watcher PID file: ${watcherPidFile}`);
|
|
18
56
|
};
|
|
19
57
|
|
|
20
58
|
program
|
|
@@ -28,144 +66,544 @@ program
|
|
|
28
66
|
console.log(`Wative CLI Tool v${packageJson.version}`);
|
|
29
67
|
});
|
|
30
68
|
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
if (!
|
|
34
|
-
|
|
35
|
-
|
|
69
|
+
// 初始化服务管理器
|
|
70
|
+
const initServiceManager = () => {
|
|
71
|
+
if (!serviceManager) {
|
|
72
|
+
const configDir = path.join(process.env.HOME || '', '.wative', 'ssh');
|
|
73
|
+
serviceManager = new SSHServiceManager(configDir);
|
|
36
74
|
}
|
|
75
|
+
return serviceManager;
|
|
76
|
+
};
|
|
37
77
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
78
|
+
const sshCommand = program
|
|
79
|
+
.command('ssh')
|
|
80
|
+
.description('Manage SSH services');
|
|
42
81
|
|
|
43
|
-
const userHomeDirectory = os.homedir();
|
|
44
|
-
const keystorePath = path.join(userHomeDirectory, '.wative');
|
|
45
|
-
await getAndGenerateConfig(keystorePath);
|
|
46
|
-
console.log('Starting SSH service...');
|
|
47
|
-
try {
|
|
48
|
-
execSync('wative-ssh --daemon', { stdio: 'inherit' });
|
|
49
|
-
} catch (error) { }
|
|
50
|
-
console.log('SSH service started.');
|
|
51
|
-
}
|
|
52
82
|
|
|
53
|
-
const stopSSHService = () => {
|
|
54
|
-
if (!hasSystemd()) {
|
|
55
|
-
console.log('Systemd is not available.');
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
83
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
84
|
+
// SSH start command
|
|
85
|
+
sshCommand
|
|
86
|
+
.command('start')
|
|
87
|
+
.description('Start SSH service')
|
|
88
|
+
.option('-c, --config <name>', 'Specify service configuration name')
|
|
89
|
+
.option('-d, --daemon', 'Run service in daemon mode')
|
|
90
|
+
.action(async (options) => {
|
|
91
|
+
const manager = initServiceManager();
|
|
66
92
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
93
|
+
try {
|
|
94
|
+
if (!options.config) {
|
|
95
|
+
console.error('Error: Configuration name is required. Use -c or --config to specify a service configuration.');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const serviceName = options.config;
|
|
100
|
+
|
|
101
|
+
// 在启动前检查端口是否被占用
|
|
102
|
+
const config = manager.getServiceConfig(serviceName);
|
|
103
|
+
const net = require('net');
|
|
104
|
+
|
|
105
|
+
// 端口占用检查函数
|
|
106
|
+
const isPortInUse = async (port: number, host: string = '127.0.0.1'): Promise<boolean> => {
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
const server = net.createServer();
|
|
109
|
+
|
|
110
|
+
server.listen(port, host, () => {
|
|
111
|
+
server.once('close', () => {
|
|
112
|
+
resolve(false); // 端口未被占用
|
|
113
|
+
});
|
|
114
|
+
server.close();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
server.on('error', (err: any) => {
|
|
118
|
+
if (err.code === 'EADDRINUSE') {
|
|
119
|
+
resolve(true); // 端口被占用
|
|
120
|
+
} else {
|
|
121
|
+
resolve(false); // 其他错误,假设端口可用
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// 检查端口占用
|
|
128
|
+
const portInUse = await isPortInUse(config.ssh.port, config.ssh.listen);
|
|
129
|
+
if (portInUse) {
|
|
130
|
+
const errorMessage = `Port ${config.ssh.port} on ${config.ssh.listen} is already in use. Cannot start SSH service '${serviceName}'.`;
|
|
131
|
+
console.error(`Error: ${errorMessage}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(`Port ${config.ssh.port} on ${config.ssh.listen} is available for service '${serviceName}'`);
|
|
136
|
+
|
|
137
|
+
if (options.daemon) {
|
|
138
|
+
console.log(`Starting SSH service '${serviceName}' in daemon mode...`);
|
|
139
|
+
|
|
140
|
+
// 在 daemon 模式下,先收集密码
|
|
141
|
+
const passwords = await manager.promptForPasswords(config.keystore.allowed_keystores);
|
|
142
|
+
|
|
143
|
+
const crypto = require('crypto');
|
|
144
|
+
|
|
145
|
+
// 生成临时密钥用于加密密码
|
|
146
|
+
const tempKey = crypto.randomBytes(32);
|
|
147
|
+
const iv = crypto.randomBytes(16);
|
|
148
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', tempKey, iv);
|
|
149
|
+
let encryptedPasswords = cipher.update(JSON.stringify(passwords), 'utf8', 'hex');
|
|
150
|
+
encryptedPasswords += cipher.final('hex');
|
|
151
|
+
const encryptedData = iv.toString('hex') + ':' + tempKey.toString('hex') + ':' + encryptedPasswords;
|
|
152
|
+
|
|
153
|
+
// 启动守护进程监控器
|
|
154
|
+
startDaemonWatcher(serviceName, encryptedData);
|
|
155
|
+
|
|
156
|
+
return;
|
|
157
|
+
} else if (process.env.WATIVE_DAEMON_PASSWORDS && process.env.WATIVE_WORKER_MODE) {
|
|
158
|
+
// 工作进程模式:解密并使用密码
|
|
159
|
+
const crypto = require('crypto');
|
|
160
|
+
const encryptedPasswords = process.env.WATIVE_DAEMON_PASSWORDS;
|
|
161
|
+
|
|
162
|
+
// 立即清除环境变量以防止其他进程访问
|
|
163
|
+
delete process.env.WATIVE_DAEMON_PASSWORDS;
|
|
164
|
+
delete process.env.WATIVE_WORKER_MODE;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
// 解密密码
|
|
168
|
+
const parts = encryptedPasswords.split(':');
|
|
169
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
170
|
+
const tempKey = Buffer.from(parts[1], 'hex');
|
|
171
|
+
const encryptedData = parts[2];
|
|
172
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', tempKey, iv);
|
|
173
|
+
let decryptedData = decipher.update(encryptedData, 'hex', 'utf8');
|
|
174
|
+
decryptedData += decipher.final('utf8');
|
|
175
|
+
const passwords = JSON.parse(decryptedData);
|
|
176
|
+
|
|
177
|
+
console.log(`[${new Date().toISOString()}] Worker process starting SSH service '${serviceName}'...`);
|
|
178
|
+
|
|
179
|
+
// 启动服务
|
|
180
|
+
const workerManager = initServiceManager();
|
|
181
|
+
await workerManager.startService(serviceName, passwords);
|
|
182
|
+
|
|
183
|
+
// 立即清除内存中的密码
|
|
184
|
+
for (const key in passwords) {
|
|
185
|
+
passwords[key] = null;
|
|
186
|
+
delete passwords[key];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log(`[${new Date().toISOString()}] SSH service '${serviceName}' started successfully`);
|
|
190
|
+
|
|
191
|
+
// 设置优雅关闭处理
|
|
192
|
+
const gracefulShutdown = async (signal: string) => {
|
|
193
|
+
console.log(`[${new Date().toISOString()}] Worker received ${signal}, stopping service...`);
|
|
194
|
+
try {
|
|
195
|
+
await workerManager.stopService(serviceName);
|
|
196
|
+
console.log(`[${new Date().toISOString()}] Service stopped successfully`);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(`[${new Date().toISOString()}] Error stopping service:`, error);
|
|
199
|
+
}
|
|
200
|
+
process.exit(0);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
204
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
205
|
+
|
|
206
|
+
// 监控服务状态
|
|
207
|
+
const monitorInterval = setInterval(async () => {
|
|
208
|
+
try {
|
|
209
|
+
const status = await workerManager.getServiceStatus(serviceName);
|
|
210
|
+
if (status !== 'running') {
|
|
211
|
+
console.log(`[${new Date().toISOString()}] Service status changed to: ${status}`);
|
|
212
|
+
if (status === 'stopped') {
|
|
213
|
+
console.log(`[${new Date().toISOString()}] Service stopped unexpectedly, exiting worker...`);
|
|
214
|
+
clearInterval(monitorInterval);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error(`[${new Date().toISOString()}] Error checking service status:`, error);
|
|
220
|
+
}
|
|
221
|
+
}, 30000); // 每30秒检查一次
|
|
222
|
+
|
|
223
|
+
// 保持进程活跃
|
|
224
|
+
const keepAliveInterval = setInterval(() => {
|
|
225
|
+
// 空操作,保持进程运行
|
|
226
|
+
}, 60000);
|
|
227
|
+
|
|
228
|
+
// 清理函数
|
|
229
|
+
const cleanup = () => {
|
|
230
|
+
clearInterval(monitorInterval);
|
|
231
|
+
clearInterval(keepAliveInterval);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
process.on('exit', cleanup);
|
|
235
|
+
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error(`[${new Date().toISOString()}] Worker process error:`, error);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return;
|
|
242
|
+
} else {
|
|
243
|
+
// 非 daemon 模式
|
|
244
|
+
await manager.startService(serviceName);
|
|
245
|
+
console.log(`SSH service '${serviceName}' started successfully`);
|
|
246
|
+
|
|
247
|
+
// 非 daemon 模式下保持进程运行
|
|
248
|
+
process.on('SIGINT', async () => {
|
|
249
|
+
console.log('\nReceived SIGINT, stopping services...');
|
|
250
|
+
await manager.stopAllServices();
|
|
251
|
+
process.exit(0);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
process.on('SIGTERM', async () => {
|
|
255
|
+
console.log('\nReceived SIGTERM, stopping services...');
|
|
256
|
+
await manager.stopAllServices();
|
|
257
|
+
process.exit(0);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// 保持进程活跃
|
|
261
|
+
const keepAlive = setInterval(() => {
|
|
262
|
+
const status = manager.getServiceStatus(serviceName);
|
|
263
|
+
if (status !== 'running') {
|
|
264
|
+
console.log(`Service ${serviceName} is no longer running, exiting...`);
|
|
265
|
+
clearInterval(keepAlive);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
}, 5000);
|
|
269
|
+
|
|
270
|
+
// 阻止进程退出
|
|
271
|
+
process.stdin.resume();
|
|
272
|
+
}
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
72
278
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
279
|
+
// SSH stop command
|
|
280
|
+
sshCommand
|
|
281
|
+
.command('stop')
|
|
282
|
+
.description('Stop SSH service')
|
|
283
|
+
.option('-c, --config <name>', 'Specify service configuration name')
|
|
284
|
+
.action(async (options) => {
|
|
285
|
+
const manager = initServiceManager();
|
|
77
286
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
Group=root
|
|
92
|
-
StandardOutput=append:/var/log/wative/wative.log
|
|
93
|
-
StandardError=append:/var/log/wative/wative.err
|
|
94
|
-
|
|
95
|
-
[Install]
|
|
96
|
-
WantedBy=multi-user.target
|
|
97
|
-
`;
|
|
98
|
-
|
|
99
|
-
console.log('Adding SSH service...');
|
|
100
|
-
try {
|
|
101
|
-
execSync(`sudo echo '${serviceContent}' > ${serviceFilePath}`, { stdio: 'inherit' });
|
|
102
|
-
execSync('sudo systemctl daemon-reload', { stdio: 'inherit' });
|
|
103
|
-
execSync('sudo systemctl enable wative', { stdio: 'inherit' });
|
|
104
|
-
execSync('sudo systemctl start wative', { stdio: 'inherit' });
|
|
105
|
-
} catch (error) {
|
|
106
|
-
// console.log(error);
|
|
107
|
-
}
|
|
287
|
+
try {
|
|
288
|
+
if (!options.config) {
|
|
289
|
+
console.error('Error: Configuration name is required. Use -c or --config to specify a service configuration.');
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
await manager.stopService(options.config);
|
|
294
|
+
console.log(`SSH service '${options.config}' stopped successfully`);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
108
300
|
|
|
109
|
-
|
|
110
|
-
|
|
301
|
+
// SSH restart command
|
|
302
|
+
sshCommand
|
|
303
|
+
.command('restart')
|
|
304
|
+
.description('Restart SSH service')
|
|
305
|
+
.option('-c, --config <name>', 'Specify service configuration name')
|
|
306
|
+
.action(async (options) => {
|
|
307
|
+
const manager = initServiceManager();
|
|
111
308
|
|
|
309
|
+
try {
|
|
310
|
+
if (!options.config) {
|
|
311
|
+
console.error('Error: Configuration name is required. Use -c or --config to specify a service configuration.');
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await manager.restartService(options.config);
|
|
316
|
+
console.log(`SSH service '${options.config}' restarted successfully`);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
112
322
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
323
|
+
// SSH create command
|
|
324
|
+
sshCommand
|
|
325
|
+
.command('create')
|
|
326
|
+
.description('Create new SSH service configuration interactively')
|
|
327
|
+
.action(async () => {
|
|
328
|
+
const readline = require('readline');
|
|
329
|
+
const fs = require('fs');
|
|
330
|
+
const path = require('path');
|
|
331
|
+
// Use the already imported SSHConfigManager instead of requiring it again
|
|
332
|
+
|
|
333
|
+
const rl = readline.createInterface({
|
|
334
|
+
input: process.stdin,
|
|
335
|
+
output: process.stdout
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const question = (prompt: string): Promise<string> => {
|
|
339
|
+
return new Promise((resolve) => {
|
|
340
|
+
rl.question(prompt, resolve);
|
|
341
|
+
});
|
|
342
|
+
};
|
|
119
343
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
344
|
+
try {
|
|
345
|
+
console.log('SSH create command started...');
|
|
346
|
+
|
|
347
|
+
// 1. Configure storage location
|
|
348
|
+
const defaultStoragePath = path.join(process.env.HOME || '', '.wative/ssh');
|
|
349
|
+
const storagePathInput = await question(`SSH storage located in (default: ${defaultStoragePath}): `);
|
|
350
|
+
const storagePath = storagePathInput.trim() || defaultStoragePath;
|
|
351
|
+
|
|
352
|
+
// 2. Configure keystore location
|
|
353
|
+
const defaultKeystorePath = path.join(process.env.HOME || '', '.wative');
|
|
354
|
+
const keystorePathInput = await question(`Keystore storage located in (default: ${defaultKeystorePath}): `);
|
|
355
|
+
const keystorePath = keystorePathInput.trim() || defaultKeystorePath;
|
|
356
|
+
|
|
357
|
+
const configManager = new SSHConfigManager(storagePath, keystorePath);
|
|
358
|
+
|
|
359
|
+
// 3. Enter service name
|
|
360
|
+
let serviceName: string;
|
|
361
|
+
while (true) {
|
|
362
|
+
serviceName = await question('Enter service name: ');
|
|
363
|
+
if (serviceName.trim()) {
|
|
364
|
+
try {
|
|
365
|
+
configManager.loadServiceConfig(serviceName);
|
|
366
|
+
console.log('Service name already exists, please enter a different name');
|
|
367
|
+
} catch {
|
|
368
|
+
break; // Service doesn't exist, can use this name
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
console.log('Service name cannot be empty');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 4. Enter port number
|
|
376
|
+
let port: number;
|
|
377
|
+
while (true) {
|
|
378
|
+
const portStr = await question('Enter port number: ');
|
|
379
|
+
port = parseInt(portStr);
|
|
380
|
+
if (isNaN(port) || port < 1024 || port > 65535) {
|
|
381
|
+
console.log('Port number must be between 1024-65535');
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 检查端口是否已被使用
|
|
386
|
+
const existingConfigs = configManager.listServiceConfigs();
|
|
387
|
+
let portInUse = false;
|
|
388
|
+
for (const configName of existingConfigs) {
|
|
389
|
+
try {
|
|
390
|
+
const config = configManager.loadServiceConfig(configName);
|
|
391
|
+
if (config.ssh.port === port) {
|
|
392
|
+
portInUse = true;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
} catch {
|
|
396
|
+
// Ignore loading errors
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (portInUse) {
|
|
401
|
+
console.log(`Port ${port} is already in use by another service, please enter a different port`);
|
|
402
|
+
} else {
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 5. Enter allowed keystores
|
|
408
|
+
let allowedKeystores: string[];
|
|
409
|
+
|
|
410
|
+
while (true) {
|
|
411
|
+
const keystoresStr = await question('Enter allowed keystore list (comma separated): ');
|
|
412
|
+
if (!keystoresStr.trim()) {
|
|
413
|
+
console.log('Keystore list cannot be empty');
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
allowedKeystores = keystoresStr.split(',').map(k => k.trim()).filter(k => k);
|
|
418
|
+
if (allowedKeystores.length === 0) {
|
|
419
|
+
console.log('At least one valid keystore is required');
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Only verify keystore existence (no password verification)
|
|
424
|
+
const keystoreAccountsPath = path.join(keystorePath, 'accounts');
|
|
425
|
+
let allKeystoresValid = true;
|
|
426
|
+
|
|
427
|
+
for (const keystore of allowedKeystores) {
|
|
428
|
+
const keystoreFile = path.join(keystoreAccountsPath, `${keystore}.json`);
|
|
429
|
+
if (!fs.existsSync(keystoreFile)) {
|
|
430
|
+
console.log(`Keystore '${keystore}' does not exist`);
|
|
431
|
+
allKeystoresValid = false;
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (allKeystoresValid) {
|
|
437
|
+
console.log('All keystores verified to exist');
|
|
438
|
+
break;
|
|
439
|
+
} else {
|
|
440
|
+
console.log('Please re-enter valid keystore list');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 6. Enter remote client name
|
|
445
|
+
let clientName: string;
|
|
446
|
+
while (true) {
|
|
447
|
+
clientName = await question('Enter remote client name: ');
|
|
448
|
+
if (clientName.trim()) {
|
|
449
|
+
break;
|
|
450
|
+
} else {
|
|
451
|
+
console.log('Client name cannot be empty');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 7. Prompt user to place client public key
|
|
456
|
+
const serviceDir = path.join(storagePath, 'conf.d', serviceName);
|
|
457
|
+
if (!fs.existsSync(serviceDir)) {
|
|
458
|
+
fs.mkdirSync(serviceDir, { recursive: true });
|
|
459
|
+
}
|
|
460
|
+
const clientPublicKeyPath = path.join(serviceDir, `${clientName}.pub`);
|
|
461
|
+
|
|
462
|
+
console.log(`\nPlease place the remote client's public key file at the following path:`);
|
|
463
|
+
console.log(`${clientPublicKeyPath}`);
|
|
464
|
+
console.log('\nPress Enter to continue after placing the file...');
|
|
465
|
+
|
|
466
|
+
while (true) {
|
|
467
|
+
await question('');
|
|
468
|
+
if (fs.existsSync(clientPublicKeyPath)) {
|
|
469
|
+
console.log('Client public key file found');
|
|
470
|
+
break;
|
|
471
|
+
} else {
|
|
472
|
+
console.log('Client public key file not found, please confirm the file is correctly placed and press Enter again');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 8. Enter service description (optional)
|
|
477
|
+
const description = await question('Enter service description (optional): ');
|
|
478
|
+
|
|
479
|
+
// Create service configuration
|
|
480
|
+
console.log('\nCreating SSH service configuration...');
|
|
481
|
+
const config = await configManager.createServiceConfig(
|
|
482
|
+
serviceName,
|
|
483
|
+
port,
|
|
484
|
+
allowedKeystores,
|
|
485
|
+
clientName,
|
|
486
|
+
description || undefined
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
// Configuration created successfully
|
|
490
|
+
console.log('\nNote: Passwords will be requested when starting the service.');
|
|
491
|
+
|
|
492
|
+
console.log(`\nSSH service configuration '${serviceName}' created successfully!`);
|
|
493
|
+
console.log(`Port: ${port}`);
|
|
494
|
+
console.log(`Allowed keystores: ${allowedKeystores.join(', ')}`);
|
|
495
|
+
console.log(`Client name: ${clientName}`);
|
|
496
|
+
console.log(`Server private key: ${config.server_keys.private_key_path}`);
|
|
497
|
+
console.log(`Client public key: ${clientPublicKeyPath}`);
|
|
498
|
+
|
|
499
|
+
} catch (error) {
|
|
500
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
501
|
+
process.exit(1);
|
|
502
|
+
} finally {
|
|
503
|
+
rl.close();
|
|
504
|
+
}
|
|
505
|
+
});
|
|
124
506
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
507
|
+
// SSH remove command
|
|
508
|
+
sshCommand
|
|
509
|
+
.command('remove')
|
|
510
|
+
.description('Remove SSH service configuration')
|
|
511
|
+
.option('-c, --config <name>', 'Service configuration name')
|
|
512
|
+
.action(async (options) => {
|
|
513
|
+
const manager = initServiceManager();
|
|
129
514
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
515
|
+
try {
|
|
516
|
+
if (!options.config) {
|
|
517
|
+
console.error('Error: Configuration name is required for remove command');
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
await manager.removeService(options.config);
|
|
522
|
+
console.log(`SSH service configuration '${options.config}' removed successfully`);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
134
528
|
|
|
135
|
-
try {
|
|
136
|
-
execSync('sudo systemctl daemon-reload', { stdio: 'inherit' });
|
|
137
|
-
} catch (error) {
|
|
138
|
-
}
|
|
139
|
-
console.log('SSH service removed.');
|
|
140
|
-
}
|
|
141
529
|
|
|
142
530
|
|
|
531
|
+
// SSH list command
|
|
532
|
+
sshCommand
|
|
533
|
+
.command('list')
|
|
534
|
+
.description('List all SSH service configurations')
|
|
535
|
+
.action(async () => {
|
|
536
|
+
const configManager = new SSHConfigManager();
|
|
537
|
+
const serviceManager = new SSHServiceManager();
|
|
143
538
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
.option('service add', 'Add SSH service')
|
|
149
|
-
.option('service remove', 'Remove SSH service')
|
|
150
|
-
.action(async (argv1, argv2) => {
|
|
151
|
-
if (argv1 === 'start') {
|
|
152
|
-
await startSSHService();
|
|
539
|
+
const configNames = configManager.listServiceConfigs();
|
|
540
|
+
|
|
541
|
+
if (configNames.length === 0) {
|
|
542
|
+
console.log('No SSH service configurations found.');
|
|
153
543
|
return;
|
|
154
544
|
}
|
|
155
545
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
546
|
+
console.log('SSH Service Configurations:');
|
|
547
|
+
for (const configName of configNames) {
|
|
548
|
+
try {
|
|
549
|
+
const config = configManager.loadServiceConfig(configName);
|
|
550
|
+
const status = serviceManager.getServiceStatus(configName);
|
|
551
|
+
console.log(` - ${configName}: ${config.description || 'No description'} (${status})`);
|
|
552
|
+
} catch (error) {
|
|
553
|
+
const status = serviceManager.getServiceStatus(configName);
|
|
554
|
+
console.log(` - ${configName}: Configuration error (${status})`);
|
|
555
|
+
}
|
|
159
556
|
}
|
|
557
|
+
});
|
|
160
558
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
559
|
+
// SSH status command
|
|
560
|
+
sshCommand
|
|
561
|
+
.command('status')
|
|
562
|
+
.description('Show SSH service status')
|
|
563
|
+
.option('-c, --config <name>', 'Service configuration name')
|
|
564
|
+
.action(async (options) => {
|
|
565
|
+
const manager = initServiceManager();
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
if (options.config) {
|
|
569
|
+
const status = manager.getServiceStatus(options.config);
|
|
570
|
+
console.log(`SSH service '${options.config}' status: ${status}`);
|
|
571
|
+
} else {
|
|
572
|
+
const services = manager.listServices();
|
|
573
|
+
|
|
574
|
+
if (services.length === 0) {
|
|
575
|
+
console.log('No SSH service configurations found');
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
console.log('SSH Service Status:');
|
|
580
|
+
services.forEach(service => {
|
|
581
|
+
const status = manager.getServiceStatus(service.name);
|
|
582
|
+
console.log(` - ${service.name}: ${status}`);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
} catch (error) {
|
|
586
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
587
|
+
process.exit(1);
|
|
164
588
|
}
|
|
589
|
+
});
|
|
165
590
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
591
|
+
// SSH log command
|
|
592
|
+
sshCommand
|
|
593
|
+
.command('log')
|
|
594
|
+
.description('Show SSH service logs')
|
|
595
|
+
.option('-c, --config <name>', 'Service configuration name')
|
|
596
|
+
.option('-n, --lines <number>', 'Number of lines to show (default: 50)', '50')
|
|
597
|
+
.option('-f, --follow', 'Follow log output')
|
|
598
|
+
.action(async (options) => {
|
|
599
|
+
const manager = initServiceManager();
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
const lines = parseInt(options.lines) || 50;
|
|
603
|
+
await manager.showLogs(options.config, lines, options.follow);
|
|
604
|
+
} catch (error) {
|
|
605
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
606
|
+
process.exit(1);
|
|
169
607
|
}
|
|
170
608
|
});
|
|
171
609
|
|
|
@@ -180,4 +618,10 @@ program
|
|
|
180
618
|
});
|
|
181
619
|
|
|
182
620
|
|
|
183
|
-
|
|
621
|
+
// Parse CLI arguments only when called from bin script or directly
|
|
622
|
+
if (require.main === module || (process.argv[1] && process.argv[1].includes('bin/wative.js'))) {
|
|
623
|
+
program.parse(process.argv);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Export the program for potential library usage
|
|
627
|
+
export { program };
|