network-infra-utility 0.2.0 → 0.5.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/CHANGELOG.md +31 -4
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +70 -0
  6. data/Rakefile +1 -1
  7. data/bin/dns-query +834 -0
  8. data/bin/geo-doc +135 -0
  9. data/bin/geo-get +1 -1
  10. data/document/ASNum/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +242 -0
  11. data/document/DNSQuery/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +248 -0
  12. 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
  13. data/document/IP/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +297 -0
  14. data/document/MAC/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +296 -0
  15. 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
  16. data/network-infra-utility.gemspec +4 -2
  17. data/network.rb +3 -1
  18. data/service/geodb/GeoAPI.md +1 -0
  19. data/service/geodb/geodb.rb +278 -1
  20. data/service/ssh/README.md +942 -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 +104 -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 +138 -1
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "open3"
6
+ require "timeout"
7
+
8
+ require_relative "ipc/transport"
9
+ require_relative "ipc/router"
10
+ require_relative "ipc/coalesce"
11
+ require_relative "ipc/errors"
12
+ require_relative "session/manager"
13
+ require_relative "security/vault"
14
+ require_relative "security/host_key"
15
+ require_relative "config/settings"
16
+ require_relative "config/schema"
17
+ require_relative "config/store"
18
+
19
+ module NetworkInfraUtility
20
+ module SSH
21
+ # SSH 客户端主入口:管理引擎生命周期与模块协调。
22
+ #
23
+ # 用法:
24
+ # client = SSH::Client.new
25
+ # client.start_engine
26
+ # session = client.connect(host: "10.0.0.1", user: "admin")
27
+ # terminal = session.open_terminal
28
+ # terminal.puts("show version")
29
+ class Client
30
+ PROTOCOL_VER = "1.0"
31
+ ENGINE_STARTUP_TIMEOUT = 10 # 秒
32
+ # 默认引擎后端::rust 或 :erlang
33
+ DEFAULT_BACKEND = :rust
34
+ # 引擎进程退出检测轮询间隔(秒)
35
+ ENGINE_WATCH_INTERVAL = 2
36
+
37
+ attr_reader :ipc, :sessions, :vault, :settings, :coalesce, :host_key, :backend
38
+
39
+ def initialize(config_dir: nil, backend: nil)
40
+ @settings = Config::Settings.new(config_dir)
41
+ @vault = Security::Vault.new(@settings.master_password)
42
+ @host_key = Security::HostKey.new(@settings.known_hosts_path)
43
+ @ipc = IPC::Router.new(IPC::Transport.new)
44
+ @sessions = Session::Manager.new(self)
45
+ @coalesce = nil
46
+ @engine_pid = nil
47
+ @engine_mutex = Mutex.new
48
+ @started = false
49
+ @backend = backend || DEFAULT_BACKEND
50
+ @engine_watcher = nil
51
+ @exit_callback = nil
52
+ end
53
+
54
+ # 设置引擎异常退出回调
55
+ # 回调签名: ->(reason) { }
56
+ # reason 为 :crashed(非零退出)或 :disconnected(IPC 连接断开)
57
+ def on_engine_exit(&block)
58
+ @exit_callback = block
59
+ end
60
+
61
+ # 启动 SSH 核心引擎,建立 IPC 连接。
62
+ def start_engine
63
+ @engine_mutex.synchronize do
64
+ raise "Engine already started" if @started
65
+
66
+ @engine_pid = spawn_engine
67
+ # 分离子进程但仍可 wait——Process.detach 返回一个线程,
68
+ # 如果子进程异常退出,线程能捕获退出码
69
+ @engine_watcher = Process.detach(@engine_pid)
70
+ endpoint, token = wait_for_endpoint
71
+ @ipc.connect(endpoint, auth_token: token, client_id: "ruby-#{Process.pid}")
72
+
73
+ # 注册 IPC 断连回调
74
+ @ipc.on_disconnect do |reason|
75
+ handle_engine_disconnect(reason)
76
+ end
77
+
78
+ # 注册反向 RPC:hostkey.resolve
79
+ register_reverse_handlers
80
+
81
+ # 初始化 coalesce(需在 hello 之后,获取服务端 capabilities)
82
+ @coalesce = IPC::Coalesce.new(@ipc)
83
+ @started = true
84
+
85
+ # 启动引擎退出监控线程
86
+ start_engine_monitor
87
+ end
88
+ self
89
+ end
90
+
91
+ # 发起 SSH 连接
92
+ # @param spec [Hash] 连接参数 {host, port, user, auth, jumps, ...}
93
+ # @return [Session::Session] 会话对象
94
+ def connect(spec)
95
+ ensure_started
96
+ # Vault 解析凭据引用,返回新 hash(不改入参)
97
+ resolved_spec = @vault.resolve_credentials(spec)
98
+ conn_resp = @ipc.call("conn.connect", resolved_spec, timeout_ms: 60_000)
99
+ @sessions.create(conn_resp[:conn_id], spec, conn_resp[:fingerprint])
100
+ rescue IPC::RPCError => e
101
+ raise ConnectionError, e.message
102
+ end
103
+
104
+ # 停止引擎,清理资源
105
+ def stop
106
+ @engine_mutex.synchronize do
107
+ return unless @started
108
+
109
+ @engine_monitor&.kill
110
+ @engine_monitor = nil
111
+ @sessions.disconnect_all
112
+ @ipc.call("bye", {}, timeout_ms: 5_000) rescue nil
113
+ @ipc.close
114
+ if @engine_pid
115
+ begin
116
+ Process.kill("TERM", @engine_pid)
117
+ # 等待最多 3 秒让引擎优雅退出
118
+ Timeout.timeout(3) { @engine_watcher&.join } if @engine_watcher
119
+ rescue => e
120
+ # 引擎可能已退出,忽略
121
+ rescue Timeout::Error
122
+ # 强制杀死
123
+ Process.kill("KILL", @engine_pid) rescue nil
124
+ end
125
+ end
126
+ @engine_pid = nil
127
+ @engine_watcher = nil
128
+ @started = false
129
+ end
130
+ end
131
+
132
+ # 引擎是否已启动
133
+ def started?
134
+ @started
135
+ end
136
+
137
+ private
138
+
139
+ def ensure_started
140
+ start_engine unless @started
141
+ end
142
+
143
+ # 引擎是否存活
144
+ def engine_alive?
145
+ return false unless @engine_pid
146
+ # Process.detach 返回的线程,如果已结束说明子进程已退出
147
+ return false if @engine_watcher&.join(0)
148
+ true
149
+ end
150
+
151
+ # 启动引擎退出监控线程
152
+ def start_engine_monitor
153
+ @engine_monitor = Thread.new do
154
+ loop do
155
+ sleep ENGINE_WATCH_INTERVAL
156
+ # 检查子进程是否已退出
157
+ if @engine_watcher&.join(0)
158
+ exit_status = @engine_watcher.value rescue nil
159
+ reason = if exit_status && exit_status != 0
160
+ :crashed
161
+ else
162
+ :exited
163
+ end
164
+ handle_engine_exit(reason)
165
+ break
166
+ end
167
+ end
168
+ rescue => e
169
+ # 监控线程异常,不中断主流程
170
+ end
171
+ end
172
+
173
+ # 引擎异常退出时的处理
174
+ def handle_engine_exit(reason)
175
+ @started = false
176
+ @sessions.disconnect_all
177
+ @exit_callback&.call(reason)
178
+ end
179
+
180
+ # IPC 连接断开时的处理
181
+ def handle_engine_disconnect(reason)
182
+ @started = false
183
+ @sessions.mark_all_disconnected
184
+ @exit_callback&.call(:disconnected)
185
+ end
186
+
187
+ # ----------------------------------------------------------------
188
+ # 以下为 private 方法(增强版修复后保留,旧版重复定义已移除)
189
+ # ----------------------------------------------------------------
190
+
191
+ private
192
+
193
+ def ensure_started
194
+ start_engine unless @started
195
+ end
196
+
197
+ # 拉起 Erlang 引擎子进程
198
+ def spawn_engine
199
+ binary = engine_binary_path
200
+ raise "Engine binary not found: #{binary}" unless File.exist?(binary)
201
+
202
+ log_dir = @settings.log_dir
203
+ FileUtils.mkdir_p(log_dir)
204
+ if Gem.win_platform?
205
+ # Windows: .cmd files must be invoked via cmd.exe
206
+ Process.spawn(
207
+ "cmd", "/c", binary,
208
+ { out: File.join(log_dir, "engine.log"),
209
+ err: File.join(log_dir, "engine.err") }
210
+ )
211
+ else
212
+ Process.spawn(
213
+ binary,
214
+ { out: File.join(log_dir, "engine.log"),
215
+ err: File.join(log_dir, "engine.err") }
216
+ )
217
+ end
218
+ end
219
+
220
+ # 等待 Erlang 引擎写入端点文件
221
+ def wait_for_endpoint
222
+ endpoint_file = @settings.engine_endpoint_file
223
+ deadline = Time.now + ENGINE_STARTUP_TIMEOUT
224
+ loop do
225
+ if File.exist?(endpoint_file)
226
+ content = File.read(endpoint_file).strip.split("\n")
227
+ return content[0], content[1]
228
+ end
229
+ raise "Erlang engine startup timeout" if Time.now > deadline
230
+
231
+ sleep 0.1
232
+ end
233
+ end
234
+
235
+ # 注册反向 RPC 处理器
236
+ def register_reverse_handlers
237
+ @ipc.on_reverse_rpc("hostkey.resolve") do |params|
238
+ @host_key.resolve(params[:host], params[:port], params[:fingerprint])
239
+ end
240
+ end
241
+
242
+ def engine_binary_path
243
+ ext_dir = File.expand_path("../../../ext", __dir__)
244
+ base = case @backend
245
+ when :rust
246
+ File.join(ext_dir, "ssh_core_rs", "bin", "ssh_core_rs")
247
+ else
248
+ File.join(ext_dir, "ssh_core", "bin", "ssh_core")
249
+ end
250
+ Gem.win_platform? ? base + ".cmd" : base
251
+ end
252
+ end
253
+
254
+ class ConnectionError < StandardError; end
255
+ class AlreadyStarted < StandardError; end
256
+ end
257
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NetworkInfraUtility
4
+ module SSH
5
+ module Config
6
+ # 配置校验与版本迁移。
7
+ class SchemaError < StandardError; end
8
+
9
+ module Schema
10
+ SCHEMA_VERSION = 1
11
+
12
+ # 会话字段定义
13
+ SESSION_FIELDS = {
14
+ id: String, name: String, group: String, host: String, port: Integer,
15
+ user: String, tags: Array, auth: Hash, terminal: Hash,
16
+ keepalive: Hash, proxy: Hash, jumps: Array, port_forwards: Array,
17
+ macro: Hash, log: Hash, auto_reconnect: Hash,
18
+ algorithms: Hash, connect_timeout_ms: Integer
19
+ }.freeze
20
+
21
+ # 分组字段定义
22
+ GROUP_FIELDS = {
23
+ id: String, name: String, parent: String, collapsed: [TrueClass, FalseClass]
24
+ }.freeze
25
+
26
+ # @param doc [Hash] 加载的 YAML 文档
27
+ # @return [:ok] 校验通过
28
+ # @raise [SchemaError] 校验失败
29
+ def self.validate(doc)
30
+ raise SchemaError, "version mismatch" unless doc[:version] == SCHEMA_VERSION
31
+
32
+ (doc[:sessions] || []).each_with_index do |s, i|
33
+ validate_session(s, i)
34
+ end
35
+ (doc[:groups] || []).each_with_index do |g, i|
36
+ validate_group(g, i)
37
+ end
38
+ :ok
39
+ end
40
+
41
+ # 迁移旧版本到当前版本
42
+ def self.migrate(doc)
43
+ # V1.0 仅 v1,预留迁移接口
44
+ doc
45
+ end
46
+
47
+ class << self
48
+ private
49
+
50
+ def validate_session(session, index)
51
+ SESSION_FIELDS.each do |field, type|
52
+ val = session[field]
53
+ next if val.nil?
54
+
55
+ unless valid_type?(val, type)
56
+ raise SchemaError, "Session[#{index}] field '#{field}' type mismatch: expected #{type}, got #{val.class}"
57
+ end
58
+ end
59
+
60
+ raise SchemaError, "Session[#{index}] missing required host" unless session[:host]
61
+ raise SchemaError, "Session[#{index}] missing required user" unless session[:user]
62
+ end
63
+
64
+ def validate_group(group, index)
65
+ GROUP_FIELDS.each do |field, type|
66
+ val = group[field]
67
+ next if val.nil?
68
+
69
+ unless valid_type?(val, type)
70
+ raise SchemaError, "Group[#{index}] field '#{field}' type mismatch"
71
+ end
72
+ end
73
+ end
74
+
75
+ def valid_type?(val, type)
76
+ case type
77
+ when Array
78
+ # 数组类型:每个元素都是一个可接受类型(联合类型)
79
+ type.any? { |t| valid_type?(val, t) }
80
+ when Class
81
+ val.is_a?(type)
82
+ else
83
+ val.is_a?(type)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "pathname"
5
+ require "tmpdir"
6
+
7
+ module NetworkInfraUtility
8
+ module SSH
9
+ module Config
10
+ # 全局设置加载。
11
+ class Settings
12
+ DEFAULT_CONFIG_DIR = File.expand_path("~/.network-infra-utility")
13
+ DEFAULT_SETTINGS = {
14
+ master_password: nil,
15
+ log_dir: nil,
16
+ config_dir: DEFAULT_CONFIG_DIR,
17
+ default_terminal: "xterm-256color",
18
+ default_scrollback: 10_000,
19
+ default_keepalive_interval: 30,
20
+ default_theme: "dark"
21
+ }.freeze
22
+
23
+ attr_reader :config_dir, :data
24
+
25
+ def initialize(config_dir = nil)
26
+ @config_dir = config_dir || DEFAULT_CONFIG_DIR
27
+ @data = load_settings
28
+ end
29
+
30
+ def master_password
31
+ @data[:master_password]
32
+ end
33
+
34
+ def master_password=(pwd)
35
+ @data[:master_password] = pwd
36
+ end
37
+
38
+ def log_dir
39
+ @data[:log_dir] || File.join(@config_dir, "logs")
40
+ end
41
+
42
+ def sessions_path
43
+ File.join(@config_dir, "sessions.yml")
44
+ end
45
+
46
+ def vault_path
47
+ File.join(@config_dir, "vault.yml")
48
+ end
49
+
50
+ def known_hosts_path
51
+ File.join(@config_dir, "known_hosts.yml")
52
+ end
53
+
54
+ def settings_path
55
+ File.join(@config_dir, "settings.yml")
56
+ end
57
+
58
+ def engine_endpoint_file
59
+ uid = if Gem.win_platform?
60
+ ENV["USERNAME"] || "default"
61
+ else
62
+ Process.uid.to_s
63
+ end
64
+ File.join(Dir.tmpdir, "ssh_core_#{uid}.endpoint")
65
+ end
66
+
67
+ def default_terminal
68
+ @data[:default_terminal]
69
+ end
70
+
71
+ def default_scrollback
72
+ @data[:default_scrollback]
73
+ end
74
+
75
+ def default_keepalive_interval
76
+ @data[:default_keepalive_interval]
77
+ end
78
+
79
+ def default_theme
80
+ @data[:default_theme]
81
+ end
82
+
83
+ private
84
+
85
+ def windows?
86
+ RUBY_PLATFORM =~ /mswin|mingw|cygwin/
87
+ end
88
+
89
+ def load_settings
90
+ path = settings_path
91
+ defaults = DEFAULT_SETTINGS.merge(config_dir: @config_dir)
92
+
93
+ if File.exist?(path)
94
+ loaded = YAML.safe_load(File.read(path), symbolize_names: true) || {}
95
+ defaults.merge(loaded)
96
+ else
97
+ defaults
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+ require_relative "schema"
6
+
7
+ module NetworkInfraUtility
8
+ module SSH
9
+ module Config
10
+ # 会话/分组/片段的统一持久化。
11
+ # 原子写:先写 .tmp 再 rename,防止崩溃导致配置损坏。
12
+ class Store
13
+ attr_reader :path, :doc
14
+
15
+ def initialize(path)
16
+ @path = path
17
+ @doc = load
18
+ end
19
+
20
+ def sessions
21
+ @doc[:sessions] || []
22
+ end
23
+
24
+ def groups
25
+ @doc[:groups] || []
26
+ end
27
+
28
+ def find_session(id)
29
+ sessions.find { |s| s[:id] == id }
30
+ end
31
+
32
+ def find_group(id)
33
+ groups.find { |g| g[:id] == id }
34
+ end
35
+
36
+ def add_session(session)
37
+ @doc[:sessions] ||= []
38
+ @doc[:sessions] << session
39
+ save
40
+ end
41
+
42
+ def update_session(id, updates)
43
+ session = find_session(id)
44
+ return unless session
45
+
46
+ session.merge!(updates)
47
+ save
48
+ end
49
+
50
+ def remove_session(id)
51
+ @doc[:sessions]&.delete_if { |s| s[:id] == id }
52
+ save
53
+ end
54
+
55
+ def add_group(group)
56
+ @doc[:groups] ||= []
57
+ @doc[:groups] << group
58
+ save
59
+ end
60
+
61
+ def remove_group(id)
62
+ @doc[:groups]&.delete_if { |g| g[:id] == id }
63
+ save
64
+ end
65
+
66
+ def save
67
+ tmp = "#{@path}.tmp"
68
+ FileUtils.mkdir_p(File.dirname(@path))
69
+ File.write(tmp, YAML.dump(@doc))
70
+ File.rename(tmp, @path)
71
+ end
72
+
73
+ def reload
74
+ @doc = load
75
+ end
76
+
77
+ private
78
+
79
+ def load
80
+ if File.exist?(@path)
81
+ raw = YAML.safe_load(File.read(@path), symbolize_names: true, permitted_classes: [Symbol]) || {}
82
+ Schema.migrate(raw)
83
+ else
84
+ { version: Schema::SCHEMA_VERSION, groups: [], sessions: [] }
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "thread"
5
+ require_relative "router"
6
+
7
+ module NetworkInfraUtility
8
+ module SSH
9
+ module IPC
10
+ # 推送消费端:批量消费 channel.data.batch,按 channel 路由到对应 Terminal。
11
+ # 修正 E9(背压)和 E8(订阅泄漏)。
12
+ class Coalesce
13
+ attr_reader :batch_enabled
14
+
15
+ def initialize(router)
16
+ @router = router
17
+ @terminals = {} # [conn_id, ch_id] => Terminal::Emulator
18
+ @terminals_mutex = Mutex.new
19
+ @batch_enabled = router.server_capabilities&.include?("coalesce") || false
20
+ @sids = {} # [conn_id, ch_id] => subscription_id
21
+ setup_subscriptions
22
+ end
23
+
24
+ # 注册一个通道到终端的映射
25
+ def register_channel(conn_id, ch_id, terminal)
26
+ key = [conn_id, ch_id]
27
+ @terminals_mutex.synchronize { @terminals[key] = terminal }
28
+
29
+ # 降级模式:逐条 channel.data
30
+ unless @batch_enabled
31
+ sid = @router.subscribe("channel.data") do |p|
32
+ next unless p[:id] == ch_id
33
+
34
+ data = Base64.decode64(p[:data])
35
+ terminal.feed(data)
36
+ end
37
+ @sids[key] = sid
38
+ end
39
+ end
40
+
41
+ # 注销通道映射,清理订阅
42
+ def unregister_channel(conn_id, ch_id)
43
+ key = [conn_id, ch_id]
44
+ @terminals_mutex.synchronize { @terminals.delete(key) }
45
+
46
+ sid = @sids.delete(key)
47
+ @router.unsubscribe("channel.data", sid) if sid
48
+ end
49
+
50
+ # 查找终端(供 batch 分发用)
51
+ def find_terminal(conn_id, ch_id)
52
+ @terminals_mutex.synchronize { @terminals[[conn_id, ch_id]] }
53
+ end
54
+
55
+ private
56
+
57
+ def setup_subscriptions
58
+ return unless @batch_enabled
59
+
60
+ @router.subscribe("channel.data.batch") do |p|
61
+ items = p[:items] || []
62
+ items.each do |item|
63
+ # batch 中 item[:id] 是 channel_id,需与已注册的 channel 匹配
64
+ dispatch_batch_item(item[:id], item[:data])
65
+ end
66
+ end
67
+ end
68
+
69
+ def dispatch_batch_item(ch_id, b64_data)
70
+ data = Base64.decode64(b64_data)
71
+ @terminals_mutex.synchronize do
72
+ @terminals.each do |_key, terminal|
73
+ next unless terminal.channel_id == ch_id
74
+
75
+ terminal.feed(data)
76
+ break
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NetworkInfraUtility
4
+ module SSH
5
+ module IPC
6
+ # IPC 层异常基类
7
+ class Error < StandardError; end
8
+
9
+ # RPC 超时
10
+ class RPCTimeout < Error; end
11
+
12
+ # 远程返回的 JSON-RPC error 对象
13
+ class RPCError < Error
14
+ attr_reader :code, :data
15
+
16
+ def initialize(error_obj)
17
+ @code = error_obj[:code]
18
+ @data = error_obj[:data]
19
+ super(error_obj[:message] || "RPC error code=#{@code}")
20
+ end
21
+ end
22
+
23
+ # 连接尚未就绪时操作通道
24
+ class NotReady < Error; end
25
+
26
+ # 背压暂停时 send 被拒
27
+ class FlowPaused < Error; end
28
+
29
+ # IPC 连接断开
30
+ class ConnectionLost < Error; end
31
+
32
+ # 配置 Schema 版本不匹配
33
+ class SchemaMismatch < Error; end
34
+ end
35
+ end
36
+ end