network-infra-utility 0.3.0 → 0.6.0

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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +21 -0
  3. data/GUIDE.md +212 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +70 -0
  6. data/Rakefile +1 -1
  7. data/bin/dns-query +836 -0
  8. data/bin/geo-doc +135 -0
  9. data/bin/geo-get +1 -1
  10. data/bin/geo-update +429 -0
  11. data/document/ASNum/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +242 -0
  12. data/document/DNSQuery/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +248 -0
  13. data/document/Geo/345/221/275/344/273/244/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +441 -0
  14. data/document/IP/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +297 -0
  15. data/document/MAC/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +296 -0
  16. data/document/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/344/275/277/347/224/250/346/226/271/346/263/225.md +764 -0
  17. data/network-infra-utility.gemspec +4 -2
  18. data/network.rb +3 -1
  19. data/service/geodb/geodb.rb +278 -1
  20. data/service/ssh/README.md +955 -0
  21. data/service/ssh/bin/ssh-client +198 -0
  22. data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/345/212/237/350/203/275/351/234/200/346/261/202/346/226/207/346/241/243.md +292 -0
  23. data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/257/246/347/273/206/350/256/276/350/256/241/346/226/207/346/241/243.md +1521 -0
  24. data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/275/257/344/273/266/350/256/276/350/256/241/346/226/207/346/241/243.md +2493 -0
  25. data/service/ssh/ext/ssh_core/bin/ssh_core.cmd +28 -0
  26. data/service/ssh/ext/ssh_core/config/sys.config +0 -0
  27. data/service/ssh/ext/ssh_core/config/vm.args +0 -0
  28. data/service/ssh/ext/ssh_core/local_deps/jsx/CHECKSUM +1 -0
  29. data/service/ssh/ext/ssh_core/local_deps/jsx/LICENSE +21 -0
  30. data/service/ssh/ext/ssh_core/local_deps/jsx/README.md +696 -0
  31. data/service/ssh/ext/ssh_core/local_deps/jsx/VERSION +1 -0
  32. data/service/ssh/ext/ssh_core/local_deps/jsx/contents.tar.gz +0 -0
  33. data/service/ssh/ext/ssh_core/local_deps/jsx/metadata.config +15 -0
  34. data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.config +17 -0
  35. data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.lock +1 -0
  36. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.app.src +10 -0
  37. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.erl +506 -0
  38. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.erl +393 -0
  39. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.hrl +18 -0
  40. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_consult.erl +81 -0
  41. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_decoder.erl +1909 -0
  42. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_encoder.erl +116 -0
  43. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_parser.erl +1214 -0
  44. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_json.erl +408 -0
  45. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_term.erl +389 -0
  46. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_verify.erl +121 -0
  47. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx.erl +506 -0
  48. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.erl +393 -0
  49. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.hrl +18 -0
  50. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_consult.erl +81 -0
  51. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_decoder.erl +1909 -0
  52. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_encoder.erl +116 -0
  53. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_parser.erl +1214 -0
  54. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_json.erl +408 -0
  55. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_term.erl +389 -0
  56. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_verify.erl +121 -0
  57. data/service/ssh/ext/ssh_core/rebar.config +24 -0
  58. data/service/ssh/ext/ssh_core/rebar.lock +1 -0
  59. data/service/ssh/ext/ssh_core/src/ssh_auth_engine.erl +156 -0
  60. data/service/ssh/ext/ssh_core/src/ssh_channel_stm.erl +232 -0
  61. data/service/ssh/ext/ssh_core/src/ssh_codec.erl +83 -0
  62. data/service/ssh/ext/ssh_core/src/ssh_conn_sup.erl +48 -0
  63. data/service/ssh/ext/ssh_core/src/ssh_conn_worker.erl +535 -0
  64. data/service/ssh/ext/ssh_core/src/ssh_core.app.src +36 -0
  65. data/service/ssh/ext/ssh_core/src/ssh_core_app.erl +11 -0
  66. data/service/ssh/ext/ssh_core/src/ssh_core_sup.erl +46 -0
  67. data/service/ssh/ext/ssh_core/src/ssh_infra_sup.erl +117 -0
  68. data/service/ssh/ext/ssh_core/src/ssh_ipc.hrl +80 -0
  69. data/service/ssh/ext/ssh_core/src/ssh_ipc_coalesce.erl +94 -0
  70. data/service/ssh/ext/ssh_core/src/ssh_ipc_gateway.erl +467 -0
  71. data/service/ssh/ext/ssh_core/src/ssh_ipc_proto.erl +95 -0
  72. data/service/ssh/ext/ssh_core/src/ssh_jump_chain.erl +101 -0
  73. data/service/ssh/ext/ssh_core/src/ssh_keepalive_mgr.erl +222 -0
  74. data/service/ssh/ext/ssh_core/src/ssh_known_hosts_proxy.erl +67 -0
  75. data/service/ssh/ext/ssh_core/src/ssh_port_fwd.erl +225 -0
  76. data/service/ssh/ext/ssh_core/src/ssh_sftp_session.erl +250 -0
  77. data/service/ssh/ext/ssh_core/src/ssh_sftp_sup.erl +62 -0
  78. data/service/ssh/ext/ssh_core_rs/Cargo.lock +2345 -0
  79. data/service/ssh/ext/ssh_core_rs/Cargo.toml +30 -0
  80. data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs +34 -0
  81. data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs.cmd +40 -0
  82. data/service/ssh/ext/ssh_core_rs/src/channel.rs +296 -0
  83. data/service/ssh/ext/ssh_core_rs/src/coalesce.rs +143 -0
  84. data/service/ssh/ext/ssh_core_rs/src/codec.rs +71 -0
  85. data/service/ssh/ext/ssh_core_rs/src/conn.rs +628 -0
  86. data/service/ssh/ext/ssh_core_rs/src/gateway.rs +389 -0
  87. data/service/ssh/ext/ssh_core_rs/src/handler.rs +293 -0
  88. data/service/ssh/ext/ssh_core_rs/src/keepalive.rs +194 -0
  89. data/service/ssh/ext/ssh_core_rs/src/main.rs +351 -0
  90. data/service/ssh/ext/ssh_core_rs/src/portfwd.rs +378 -0
  91. data/service/ssh/ext/ssh_core_rs/src/proto.rs +198 -0
  92. data/service/ssh/ext/ssh_core_rs/src/sftp.rs +294 -0
  93. data/service/ssh/lib/network_infra_utility/ssh/automation/macro_engine.rb +213 -0
  94. data/service/ssh/lib/network_infra_utility/ssh/client.rb +257 -0
  95. data/service/ssh/lib/network_infra_utility/ssh/config/schema.rb +90 -0
  96. data/service/ssh/lib/network_infra_utility/ssh/config/settings.rb +103 -0
  97. data/service/ssh/lib/network_infra_utility/ssh/config/store.rb +90 -0
  98. data/service/ssh/lib/network_infra_utility/ssh/ipc/coalesce.rb +83 -0
  99. data/service/ssh/lib/network_infra_utility/ssh/ipc/errors.rb +36 -0
  100. data/service/ssh/lib/network_infra_utility/ssh/ipc/router.rb +212 -0
  101. data/service/ssh/lib/network_infra_utility/ssh/ipc/transport.rb +81 -0
  102. data/service/ssh/lib/network_infra_utility/ssh/security/host_key.rb +211 -0
  103. data/service/ssh/lib/network_infra_utility/ssh/security/vault.rb +211 -0
  104. data/service/ssh/lib/network_infra_utility/ssh/session/history.rb +56 -0
  105. data/service/ssh/lib/network_infra_utility/ssh/session/manager.rb +92 -0
  106. data/service/ssh/lib/network_infra_utility/ssh/session/session.rb +109 -0
  107. data/service/ssh/lib/network_infra_utility/ssh/session/tree.rb +95 -0
  108. data/service/ssh/lib/network_infra_utility/ssh/terminal/ansi_parser.rb +435 -0
  109. data/service/ssh/lib/network_infra_utility/ssh/terminal/buffer.rb +78 -0
  110. data/service/ssh/lib/network_infra_utility/ssh/terminal/emulator.rb +159 -0
  111. data/service/ssh/lib/network_infra_utility/ssh/terminal/logger.rb +195 -0
  112. data/service/ssh/lib/network_infra_utility/ssh/terminal/screen.rb +212 -0
  113. data/service/ssh/lib/network_infra_utility/ssh/terminal/theme.rb +127 -0
  114. data/service/ssh/lib/network_infra_utility/ssh/version.rb +7 -0
  115. data/service/ssh/lib/network_infra_utility/ssh.rb +44 -0
  116. data/support/basic/as_num.rb +221 -0
  117. data/support/basic/mac_address.rb +281 -0
  118. data/version.rb +1 -1
  119. metadata +142 -1
@@ -0,0 +1,628 @@
1
+ //! 连接管理器 — 管理全部 SSH 连接的生命周期。
2
+ //! 对应 Erlang 端的 ssh_conn_sup.erl + ssh_conn_worker.erl。
3
+ //!
4
+ //! 核心职责:
5
+ //! - conn.connect:建立 SSH 连接,认证,返回 conn_id + fingerprint
6
+ //! - conn.disconnect:断开指定连接
7
+ //! - conn.list:列出所有活动连接
8
+ //! - conn.reconnect:重连
9
+ //! - 管理底层 russh Handle 和通道
10
+ //! - 通过 gateway 推送 conn.ready / conn.failed / conn.closed 事件
11
+
12
+ use std::collections::HashMap;
13
+ use std::sync::Arc;
14
+ use std::time::Duration;
15
+
16
+ use russh::client;
17
+ use russh::client::KeyboardInteractiveAuthResponse;
18
+ use russh::keys::{PrivateKey, key::PrivateKeyWithHashAlg, HashAlg};
19
+ use serde_json::{json, Value};
20
+ use tokio::sync::{mpsc, RwLock};
21
+ use tokio::time::timeout;
22
+ use tracing::{debug, info, warn};
23
+
24
+ use crate::codec;
25
+ use crate::gateway::Gateway;
26
+ use crate::handler::{ConnEvent, ConnShared, SshHandler};
27
+ use crate::proto;
28
+
29
+ /// 连接超时(毫秒)
30
+ const CONNECT_TIMEOUT_MS: u64 = 60_000;
31
+
32
+ /// 单个 SSH 连接的状态
33
+ struct Connection {
34
+ conn_id: String,
35
+ spec: Value,
36
+ shared: Arc<ConnShared>,
37
+ handle: RwLock<Option<Arc<client::Handle<SshHandler>>>>,
38
+ state: RwLock<ConnState>,
39
+ /// 事件处理任务 handle
40
+ event_task: RwLock<Option<tokio::task::JoinHandle<()>>>,
41
+ }
42
+
43
+ #[derive(Debug, Clone, Copy, PartialEq)]
44
+ pub enum ConnState {
45
+ Idle,
46
+ Connecting,
47
+ Ready,
48
+ Failed,
49
+ Disconnected,
50
+ }
51
+
52
+ impl ConnState {
53
+ fn as_str(&self) -> &'static str {
54
+ match self {
55
+ ConnState::Idle => "idle",
56
+ ConnState::Connecting => "connecting",
57
+ ConnState::Ready => "ready",
58
+ ConnState::Failed => "failed",
59
+ ConnState::Disconnected => "disconnected",
60
+ }
61
+ }
62
+ }
63
+
64
+ /// 连接管理器
65
+ pub struct ConnManager {
66
+ connections: RwLock<HashMap<String, Arc<Connection>>>,
67
+ gateway: Arc<Gateway>,
68
+ }
69
+
70
+ impl ConnManager {
71
+ pub fn new(gateway: Arc<Gateway>) -> Arc<Self> {
72
+ Arc::new(Self {
73
+ connections: RwLock::new(HashMap::new()),
74
+ gateway,
75
+ })
76
+ }
77
+
78
+ /// RPC: conn.connect
79
+ pub async fn rpc_connect(&self, params: Value) -> Result<Value, String> {
80
+ let host = proto::get_str(&params, "host")
81
+ .ok_or("missing host")?
82
+ .to_string();
83
+ let user = proto::get_str(&params, "user")
84
+ .ok_or("missing user")?
85
+ .to_string();
86
+ let port = proto::get_num(&params, "port").unwrap_or(22.0) as u16;
87
+ let connect_timeout = proto::get_num(&params, "connect_timeout_ms")
88
+ .unwrap_or(CONNECT_TIMEOUT_MS as f64) as u64;
89
+ let auth = params.get("auth").cloned().unwrap_or(json!({}));
90
+ let key_dir = proto::get_str(&params, "key_dir").unwrap_or(".").to_string();
91
+
92
+ let conn_id = codec::gen_conn_id();
93
+
94
+ // 创建连接级事件通道
95
+ let (conn_event_tx, conn_event_rx) = mpsc::unbounded_channel::<ConnEvent>();
96
+
97
+ // 创建共享状态(handler 持有 Arc<ConnShared>)
98
+ let shared = ConnShared::new(
99
+ conn_id.clone(),
100
+ host.clone(),
101
+ port,
102
+ Arc::clone(&self.gateway),
103
+ conn_event_tx,
104
+ );
105
+
106
+ let conn = Arc::new(Connection {
107
+ conn_id: conn_id.clone(),
108
+ spec: params.clone(),
109
+ shared: Arc::clone(&shared),
110
+ handle: RwLock::new(None),
111
+ state: RwLock::new(ConnState::Connecting),
112
+ event_task: RwLock::new(None),
113
+ });
114
+
115
+ // 注册连接(在 connect 之前,以便 event_loop 能处理事件)
116
+ self.connections
117
+ .write()
118
+ .await
119
+ .insert(conn_id.clone(), Arc::clone(&conn));
120
+
121
+ // 启动事件处理循环
122
+ let gw = Arc::clone(&self.gateway);
123
+ let cid = conn_id.clone();
124
+ let conn_clone = Arc::clone(&conn);
125
+ let event_task = tokio::spawn(async move {
126
+ Self::event_loop(conn_clone, gw, cid, conn_event_rx).await;
127
+ });
128
+ *conn.event_task.write().await = Some(event_task);
129
+
130
+ // 执行连接
131
+ match self
132
+ .establish_connection(
133
+ &conn,
134
+ &host,
135
+ port,
136
+ &user,
137
+ &auth,
138
+ &key_dir,
139
+ connect_timeout,
140
+ )
141
+ .await
142
+ {
143
+ Ok(fingerprint) => {
144
+ *conn.state.write().await = ConnState::Ready;
145
+
146
+ // 推送 conn.ready 事件
147
+ self.gateway
148
+ .push_event(
149
+ "conn.ready",
150
+ json!({
151
+ "conn_id": conn_id,
152
+ "fingerprint": fingerprint
153
+ }),
154
+ )
155
+ .await;
156
+
157
+ Ok(json!({
158
+ "conn_id": conn_id,
159
+ "fingerprint": fingerprint
160
+ }))
161
+ }
162
+ Err(e) => {
163
+ *conn.state.write().await = ConnState::Failed;
164
+
165
+ // 推送 conn.failed 事件
166
+ self.gateway
167
+ .push_event(
168
+ "conn.failed",
169
+ json!({
170
+ "conn_id": conn_id,
171
+ "reason": e
172
+ }),
173
+ )
174
+ .await;
175
+
176
+ // 移除连接
177
+ self.connections.write().await.remove(&conn_id);
178
+ Err(e)
179
+ }
180
+ }
181
+ }
182
+
183
+ /// 建立底层 SSH 连接并认证
184
+ async fn establish_connection(
185
+ &self,
186
+ conn: &Arc<Connection>,
187
+ host: &str,
188
+ port: u16,
189
+ user: &str,
190
+ auth: &Value,
191
+ key_dir: &str,
192
+ connect_timeout_ms: u64,
193
+ ) -> Result<String, String> {
194
+ let auth_type = proto::get_str(auth, "type").unwrap_or("publickey");
195
+
196
+ // russh 配置
197
+ // inactivity_timeout 控制连接建立后的空闲断开,语义独立于 connect_timeout
198
+ // 默认 None(不因空闲断开),由 keepalive 机制独立管理心跳
199
+ let inactivity_secs = proto::get_num(&conn.spec, "inactivity_timeout_s")
200
+ .map(|v| Duration::from_secs(v as u64));
201
+ let config = Arc::new(client::Config {
202
+ inactivity_timeout: inactivity_secs,
203
+ ..Default::default()
204
+ });
205
+
206
+ // 创建 handler(持有 ConnShared Arc)
207
+ let handler = SshHandler::new(Arc::clone(&conn.shared));
208
+
209
+ // 发起 TCP + SSH 握手
210
+ // russh 0.61: client::connect() 直接返回 Handle<SshHandler>,没有独立的 Session 层
211
+ let addr = (host.to_string(), port);
212
+ let connect_fut = client::connect(config, addr, handler);
213
+ let connect_result = timeout(
214
+ Duration::from_millis(connect_timeout_ms),
215
+ connect_fut,
216
+ )
217
+ .await
218
+ .map_err(|_| "connect_timeout".to_string())?;
219
+
220
+ let mut handle = connect_result.map_err(|e| format!("connect_failed: {}", e))?;
221
+
222
+ // 认证
223
+ Self::authenticate(&mut handle, user, auth_type, auth, key_dir).await?;
224
+
225
+ // 保存 handle(用 Arc 包装,以便 channel / keepalive 共享访问)
226
+ *conn.handle.write().await = Some(Arc::new(handle));
227
+
228
+ // 获取服务器指纹(check_server_key 回调中已写入 ConnShared)
229
+ let fingerprint = conn.shared.get_server_fingerprint().await
230
+ .unwrap_or_else(|| "SHA256:unknown".to_string());
231
+
232
+ info!("conn {} established, fingerprint: {}", conn.conn_id, fingerprint);
233
+ Ok(fingerprint)
234
+ }
235
+
236
+ /// 认证并返回服务器指纹
237
+ async fn authenticate(
238
+ handle: &mut client::Handle<SshHandler>,
239
+ user: &str,
240
+ auth_type: &str,
241
+ auth: &Value,
242
+ key_dir: &str,
243
+ ) -> Result<(), String> {
244
+ let auth_result = match auth_type {
245
+ "password" => {
246
+ let password = proto::get_str(auth, "password")
247
+ .ok_or("missing password")?;
248
+ handle
249
+ .authenticate_password(user, password)
250
+ .await
251
+ .map_err(|e| format!("auth_password_failed: {}", e))?
252
+ }
253
+ "publickey" => {
254
+ let key_path = proto::get_str(auth, "key_path")
255
+ .or_else(|| proto::get_str(auth, "private_key"));
256
+ let passphrase = proto::get_str(auth, "passphrase");
257
+
258
+ let key_pair: PrivateKey = if let Some(kp) = key_path {
259
+ // load_secret_key 统一处理加密/未加密密钥
260
+ // passphrase 为 Some 时尝试解密,为 None 时假定未加密
261
+ russh::keys::load_secret_key(kp, passphrase)
262
+ .map_err(|e| format!("key_load_failed: {}", e))?
263
+ } else {
264
+ // 尝试从 key_dir 加载默认密钥(依次尝试 ed25519/rsa/ecdsa)
265
+ let default_keys = [
266
+ format!("{}/id_ed25519", key_dir),
267
+ format!("{}/id_rsa", key_dir),
268
+ format!("{}/id_ecdsa", key_dir),
269
+ ];
270
+ let mut loaded: Option<PrivateKey> = None;
271
+ for path in &default_keys {
272
+ if let Ok(kp) = russh::keys::load_secret_key(path, None) {
273
+ loaded = Some(kp);
274
+ break;
275
+ }
276
+ }
277
+ loaded.ok_or_else(|| "no_key_found".to_string())?
278
+ };
279
+
280
+ // RSA 密钥需要显式指定哈希算法。
281
+ // russh PrivateKeyWithHashAlg::new(key, None) 对 RSA 会退化到 SHA-1,
282
+ // 现代 OpenSSH (8.2+) 会拒绝 SHA-1 签名。
283
+ // 优先查询服务器支持的算法(rsa-sha2-512 > rsa-sha2-256),
284
+ // 服务器不支持时降级到 None(legacy sha-rsa),以保证兼容性。
285
+ let hash_alg = if key_pair.algorithm().is_rsa() {
286
+ match handle.best_supported_rsa_hash().await {
287
+ Ok(Some(Some(alg))) => {
288
+ debug!("RSA using server-supported hash: {:?}", alg);
289
+ Some(alg)
290
+ }
291
+ Ok(Some(None)) => {
292
+ // 服务器支持 RSA 但不支持 rsa-sha2-*,降级到 legacy
293
+ warn!("RSA: server does not support rsa-sha2-*, falling back to legacy sha-rsa");
294
+ None
295
+ }
296
+ Ok(None) => {
297
+ // 服务器未发送 EXT_INFO,默认用 Sha256(最广泛兼容的现代算法)
298
+ debug!("RSA: no ext-info from server, defaulting to sha2-256");
299
+ Some(HashAlg::Sha256)
300
+ }
301
+ Err(e) => {
302
+ warn!("RSA: best_supported_rsa_hash failed: {}, defaulting to sha2-256", e);
303
+ Some(HashAlg::Sha256)
304
+ }
305
+ }
306
+ } else {
307
+ // 非 RSA 密钥,hash_alg 被忽略
308
+ None
309
+ };
310
+
311
+ let key_with_alg = PrivateKeyWithHashAlg::new(
312
+ Arc::new(key_pair),
313
+ hash_alg,
314
+ );
315
+
316
+ handle
317
+ .authenticate_publickey(user, key_with_alg)
318
+ .await
319
+ .map_err(|e| format!("auth_publickey_failed: {}", e))?
320
+ }
321
+ "keyboard_interactive" => {
322
+ // 真正的键盘交互认证(RFC 4256)。
323
+ // 支持两种响应来源:
324
+ // 1) auth.responses 数组(按 prompt 顺序一一对应)
325
+ // 2) auth.password(单 prompt 场景,用密码作为唯一响应)
326
+ let responses_spec = auth.get("responses")
327
+ .and_then(|v| v.as_array())
328
+ .map(|arr| {
329
+ arr.iter()
330
+ .map(|v| v.as_str().unwrap_or("").to_string())
331
+ .collect::<Vec<_>>()
332
+ });
333
+ let password = proto::get_str(auth, "password").map(|s| s.to_string());
334
+
335
+ // 发起 keyboard-interactive 认证
336
+ let kbi_result = handle
337
+ .authenticate_keyboard_interactive_start(user, None::<String>)
338
+ .await
339
+ .map_err(|e| format!("auth_kbi_start_failed: {}", e))?;
340
+
341
+ let auth_result = Self::handle_kbi_response(
342
+ handle,
343
+ kbi_result,
344
+ responses_spec.as_deref(),
345
+ password.as_deref(),
346
+ ).await?;
347
+
348
+ auth_result
349
+ }
350
+ _ => return Err(format!("unsupported_auth_type: {}", auth_type)),
351
+ };
352
+
353
+ // russh 0.61: authenticate 返回 AuthResult 而非 bool
354
+ if !auth_result.success() {
355
+ return Err("auth_rejected".to_string());
356
+ }
357
+
358
+ Ok(())
359
+ }
360
+
361
+ /// 处理 keyboard-interactive 认证的多轮交互。
362
+ /// 服务器可能发送多个 InfoRequest,每个含若干 prompt。
363
+ /// responses_spec 和 password 作为响应来源,每次都复用。
364
+ async fn handle_kbi_response(
365
+ handle: &mut client::Handle<SshHandler>,
366
+ mut kbi: KeyboardInteractiveAuthResponse,
367
+ responses_spec: Option<&[String]>,
368
+ password: Option<&str>,
369
+ ) -> Result<russh::client::AuthResult, String> {
370
+ let mut rounds = 0u32;
371
+ loop {
372
+ rounds += 1;
373
+ if rounds > 10 {
374
+ return Err("auth_kbi_too_many_rounds".to_string());
375
+ }
376
+
377
+ match kbi {
378
+ KeyboardInteractiveAuthResponse::Success => {
379
+ // 认证成功——返回一个 success 的 AuthResult
380
+ // russh 0.61 没有直接从 KeyboardInteractiveAuthResponse 到 AuthResult 的转换,
381
+ // 但 success 状态等同于 authenticate_* 返回的 AuthResult::Success
382
+ return Ok(russh::client::AuthResult::Success);
383
+ }
384
+ KeyboardInteractiveAuthResponse::Failure { .. } => {
385
+ return Err("auth_kbi_rejected".to_string());
386
+ }
387
+ KeyboardInteractiveAuthResponse::InfoRequest { prompts, .. } => {
388
+ // 构建响应:按 prompt 数量生成回复
389
+ let responses: Vec<String> = if let Some(spec) = responses_spec {
390
+ // 使用预设 responses 数组
391
+ prompts.iter().enumerate().map(|(i, _)| {
392
+ spec.get(i).cloned().unwrap_or_default()
393
+ }).collect()
394
+ } else if let Some(pwd) = password {
395
+ // 单密码场景:每个 prompt 都用密码回复
396
+ prompts.iter().map(|_| pwd.to_string()).collect()
397
+ } else {
398
+ return Err("auth_kbi_no_response_source".to_string());
399
+ };
400
+
401
+ // 发送响应,等待下一轮
402
+ kbi = handle
403
+ .authenticate_keyboard_interactive_respond(responses)
404
+ .await
405
+ .map_err(|e| format!("auth_kbi_respond_failed: {}", e))?;
406
+ }
407
+ }
408
+ }
409
+ }
410
+
411
+ /// 事件处理循环(从 russh handler 接收连接级事件,推送到 IPC)
412
+ async fn event_loop(
413
+ conn: Arc<Connection>,
414
+ gateway: Arc<Gateway>,
415
+ conn_id: String,
416
+ mut event_rx: mpsc::UnboundedReceiver<ConnEvent>,
417
+ ) {
418
+ while let Some(event) = event_rx.recv().await {
419
+ match event {
420
+ ConnEvent::Disconnected => {
421
+ *conn.state.write().await = ConnState::Disconnected;
422
+ gateway
423
+ .push_event(
424
+ "conn.closed",
425
+ json!({
426
+ "conn_id": conn_id,
427
+ "reason": "ssh_closed"
428
+ }),
429
+ )
430
+ .await;
431
+ }
432
+ }
433
+ }
434
+ debug!("conn {} event loop ended", conn_id);
435
+ }
436
+
437
+ /// RPC: conn.disconnect
438
+ pub async fn rpc_disconnect(&self, params: Value) -> Result<Value, String> {
439
+ let conn_id = proto::get_str(&params, "id")
440
+ .ok_or("missing id")?
441
+ .to_string();
442
+
443
+ let conn = self.connections.write().await.remove(&conn_id);
444
+ match conn {
445
+ Some(c) => {
446
+ *c.state.write().await = ConnState::Disconnected;
447
+
448
+ // 关闭底层 SSH 连接
449
+ let mut handle_guard = c.handle.write().await;
450
+ if let Some(h) = handle_guard.take() {
451
+ let _ = h
452
+ .disconnect(
453
+ russh::Disconnect::ByApplication,
454
+ "client disconnect",
455
+ "en",
456
+ )
457
+ .await;
458
+ }
459
+ drop(handle_guard);
460
+
461
+ // 取消事件处理任务
462
+ if let Some(task) = c.event_task.write().await.take() {
463
+ task.abort();
464
+ }
465
+ Ok(json!({"ok": true}))
466
+ }
467
+ None => Err("conn_not_found".to_string()),
468
+ }
469
+ }
470
+
471
+ /// RPC: conn.list
472
+ pub async fn rpc_list(&self, _params: Value) -> Result<Value, String> {
473
+ let conns = self.connections.read().await;
474
+ let mut list = Vec::with_capacity(conns.len());
475
+ for c in conns.values() {
476
+ let state = *c.state.read().await;
477
+ list.push(json!({
478
+ "id": c.conn_id,
479
+ "state": state.as_str(),
480
+ "host": c.spec.get("host").cloned().unwrap_or(Value::Null),
481
+ "port": c.spec.get("port").cloned().unwrap_or(json!(22)),
482
+ }));
483
+ }
484
+ Ok(Value::Array(list))
485
+ }
486
+
487
+ /// RPC: conn.reconnect
488
+ pub async fn rpc_reconnect(&self, params: Value) -> Result<Value, String> {
489
+ let conn_id = proto::get_str(&params, "id")
490
+ .ok_or("missing id")?
491
+ .to_string();
492
+
493
+ let conn = {
494
+ let conns = self.connections.read().await;
495
+ conns.get(&conn_id).cloned()
496
+ };
497
+ let conn = match conn {
498
+ Some(c) => c,
499
+ None => return Err("conn_not_found".to_string()),
500
+ };
501
+
502
+ *conn.state.write().await = ConnState::Connecting;
503
+
504
+ // 从 spec 中提取连接参数
505
+ let host = proto::get_str(&conn.spec, "host")
506
+ .unwrap_or("")
507
+ .to_string();
508
+ let user = proto::get_str(&conn.spec, "user")
509
+ .unwrap_or("")
510
+ .to_string();
511
+ let port = proto::get_num(&conn.spec, "port").unwrap_or(22.0) as u16;
512
+ let auth = conn.spec.get("auth").cloned().unwrap_or(json!({}));
513
+ let key_dir = proto::get_str(&conn.spec, "key_dir")
514
+ .unwrap_or(".")
515
+ .to_string();
516
+ let timeout_ms = proto::get_num(&conn.spec, "connect_timeout_ms")
517
+ .unwrap_or(CONNECT_TIMEOUT_MS as f64) as u64;
518
+
519
+ match self
520
+ .establish_connection(&conn, &host, port, &user, &auth, &key_dir, timeout_ms)
521
+ .await
522
+ {
523
+ Ok(fingerprint) => {
524
+ *conn.state.write().await = ConnState::Ready;
525
+ self.gateway
526
+ .push_event(
527
+ "conn.ready",
528
+ json!({
529
+ "conn_id": conn_id,
530
+ "fingerprint": fingerprint
531
+ }),
532
+ )
533
+ .await;
534
+ Ok(json!({"conn_id": conn_id, "fingerprint": fingerprint}))
535
+ }
536
+ Err(e) => {
537
+ *conn.state.write().await = ConnState::Failed;
538
+ self.gateway
539
+ .push_event(
540
+ "conn.failed",
541
+ json!({
542
+ "conn_id": conn_id,
543
+ "reason": e
544
+ }),
545
+ )
546
+ .await;
547
+ Err(e)
548
+ }
549
+ }
550
+ }
551
+
552
+ /// 获取所有活动的连接 ID
553
+ pub async fn all_conn_ids(&self) -> Vec<String> {
554
+ self.connections
555
+ .read()
556
+ .await
557
+ .keys()
558
+ .cloned()
559
+ .collect()
560
+ }
561
+
562
+ /// 获取指定连接的 handle 和 shared(用于通道操作)
563
+ pub async fn get_handle_and_shared(
564
+ &self,
565
+ conn_id: &str,
566
+ ) -> Option<(Arc<client::Handle<SshHandler>>, Arc<ConnShared>)> {
567
+ let conns = self.connections.read().await;
568
+ let conn = conns.get(conn_id)?;
569
+ let handle = conn.handle.read().await.clone()?;
570
+ Some((handle, Arc::clone(&conn.shared)))
571
+ }
572
+
573
+ /// 获取指定连接的 handle(用于 keepalive)
574
+ pub async fn get_handle(&self, conn_id: &str) -> Option<Arc<client::Handle<SshHandler>>> {
575
+ let conns = self.connections.read().await;
576
+ let conn = conns.get(conn_id)?;
577
+ let handle = conn.handle.read().await.clone()?;
578
+ Some(handle)
579
+ }
580
+
581
+ /// 获取所有 Ready 状态的连接 ID(用于 keepalive 扫描)
582
+ pub async fn ready_conn_ids(&self) -> Vec<String> {
583
+ let conns = self.connections.read().await;
584
+ let mut ids = Vec::new();
585
+ for (id, conn) in conns.iter() {
586
+ if *conn.state.read().await == ConnState::Ready {
587
+ ids.push(id.clone());
588
+ }
589
+ }
590
+ ids
591
+ }
592
+
593
+ /// 标记连接为失败(keepalive 超时后调用)
594
+ pub async fn mark_conn_closed(&self, conn_id: &str, reason: &str) {
595
+ // 先取读锁拿状态,避免死锁(push_event 可可会触发 IPC 回调)
596
+ let found = {
597
+ let conns = self.connections.read().await;
598
+ if let Some(conn) = conns.get(conn_id) {
599
+ *conn.state.write().await = ConnState::Failed;
600
+ true
601
+ } else {
602
+ false
603
+ }
604
+ };
605
+ if found {
606
+ self.gateway
607
+ .push_event(
608
+ "conn.closed",
609
+ json!({
610
+ "conn_id": conn_id,
611
+ "reason": reason
612
+ }),
613
+ )
614
+ .await;
615
+ }
616
+ }
617
+
618
+ /// 获取连接的活动通知通道(用于 keepalive 活动感知)
619
+ /// 通过 shared 的 channel_txs 间接判断——当有数据流过时触发
620
+ pub async fn is_conn_ready(&self, conn_id: &str) -> bool {
621
+ let conns = self.connections.read().await;
622
+ if let Some(conn) = conns.get(conn_id) {
623
+ *conn.state.read().await == ConnState::Ready
624
+ } else {
625
+ false
626
+ }
627
+ }
628
+ }