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
@@ -24,11 +24,13 @@ Gem::Specification.new do |spec|
24
24
  # bin/ 既放对外命令 (geo-api) 也放开发脚本 (console/setup),
25
25
  # executables 显式声明,避免把开发脚本当作系统命令安装到用户 PATH。
26
26
  spec.bindir = "bin"
27
- spec.executables = %w[geo-api geo-get geo-load]
28
- spec.require_paths = ["document", "service", "support", "tool", "."]
27
+ spec.executables = %w[geo-api geo-get geo-load geo-doc]
28
+ spec.require_paths = ["document", "service", "service/ssh/lib", "support", "tool", "."]
29
29
 
30
30
  # geo-api 命令行服务依赖的运行时 gem
31
31
  spec.add_runtime_dependency "roda", "~> 3.0"
32
32
  spec.add_runtime_dependency "rackup", "~> 2.0"
33
33
  spec.add_runtime_dependency "puma", "~> 6.0"
34
+ spec.add_runtime_dependency "thor", "~> 1.3"
35
+ spec.add_runtime_dependency "json", "~> 2.6"
34
36
  end
data/network.rb CHANGED
@@ -13,12 +13,14 @@ require_relative "version"
13
13
  module NetworkInfraUtility
14
14
  # 支撑层:无外部依赖的基础工具,最先加载
15
15
  require_relative "support/basic/ip"
16
+ require_relative "support/basic/as_num"
17
+ require_relative "support/basic/mac_address"
16
18
 
17
19
  # 工具层:依赖 support
18
20
  # require_relative "tool/xxx"
19
21
 
20
22
  # 服务层:依赖 tool / support
21
- # require_relative "service/xxx"
23
+ require_relative "service/ssh/lib/network_infra_utility/ssh"
22
24
 
23
25
  # 文档层:依赖 service / tool,最后加载
24
26
  # require_relative "document/xxx"
@@ -1,3 +1,4 @@
1
+
1
2
  # GeoAPI 测试用例
2
3
 
3
4
  > 接口服务文件:`api.rb`
@@ -1,15 +1,32 @@
1
1
  #coding:utf-8
2
- ['cc','CasetDown/casetdown','network'].each{|mod|require mod}
2
+ ['cc','CasetDown/casetdown','network','oj'].each{|mod|require mod}
3
3
  CC.use 'file','enum','shell-tools'
4
4
 
5
5
  # USAGE>
6
6
  # GeoDB.load_asn("GeoLite2-ASN-CSV_*/*.csv")
7
7
  # GeoDB.load_city("GeoLite2-City-CSV_*/*.csv")
8
8
  # GeoDB.load_country("GeoLite2-Country-CSV_*/*.csv")
9
+ #
10
+ # GeoDB.output("geodb/")
11
+ # GeoDB.output("geodb/", format: :txt)
12
+ # GeoDB.output("geodb/", format: :csv)
13
+ # GeoDB.output("geodb/", output_path: "output/")
14
+ # GeoDB.output("geodb/", format: [:csv, :txt], output_path: "output/")
9
15
 
10
16
  module GeoDB
11
17
  module_function
12
18
 
19
+ # 加载 GeoLite2 ASN 数据(CSV → JSON)
20
+ # -----------------------------------------------------------
21
+ # 读取 GeoLite2-ASN-CSV_* 目录下的 CSV,将每条网段展开为
22
+ # [start_num, end_num] => record 的 Hash,序列化为 asn.json
23
+ # 供 geo-api 的 /geo/asn 接口做二分范围查找。
24
+ #
25
+ # 参数:
26
+ # dir_path — ASN CSV 的 glob 模式,默认当前目录查找
27
+ # out_path — JSON 输出目录,默认 geodb/
28
+ #
29
+ # 返回: asn Hash (range => record)
13
30
  def load_asn dir_path="GeoLite2-ASN-CSV_*/*.csv", out_path=nil
14
31
  out_path = "geodb/" unless out_path
15
32
  Dir.mkdir(out_path) unless File.exist?(out_path)
@@ -29,6 +46,18 @@ module GeoDB
29
46
  return asn
30
47
  end
31
48
 
49
+ # 加载 GeoLite2 City 数据(CSV → JSON)
50
+ # -----------------------------------------------------------
51
+ # 分两阶段处理 City 数据:
52
+ # 1. City-Locations-zh-CN → geo-city.json (geoname_id => 城市地理信息)
53
+ # 2. City-Blocks-IPv4/IPv6 → city-IPv4.json / city-IPv6.json
54
+ # (网段 [start, end] => record, 含经纬度/精度半径等)
55
+ #
56
+ # 参数:
57
+ # dir_path — City CSV 的 glob 模式,默认当前目录查找
58
+ # out_path — JSON 输出目录,默认 geodb/
59
+ #
60
+ # 返回: [city, geo] 两个 Hash
32
61
  def load_city dir_path="GeoLite2-City-CSV_*/*.csv", out_path=nil
33
62
  out_path = "geodb/" unless out_path
34
63
  Dir.mkdir(out_path) unless File.exist?(out_path)
@@ -62,6 +91,18 @@ module GeoDB
62
91
  return city,geo
63
92
  end
64
93
 
94
+ # 加载 GeoLite2 Country 数据(CSV → JSON)
95
+ # -----------------------------------------------------------
96
+ # 分两阶段处理 Country 数据:
97
+ # 1. Country-Locations-zh-CN → geo-country.json (geoname_id => 国家信息)
98
+ # 2. Country-Blocks-IPv4/IPv6 → country-IPv4.json / country-IPv6.json
99
+ # (网段 [start, end] => record)
100
+ #
101
+ # 参数:
102
+ # dir_path — Country CSV 的 glob 模式,默认当前目录查找
103
+ # out_path — JSON 输出目录,默认 geodb/
104
+ #
105
+ # 返回: [country, geo] 两个 Hash
65
106
  def load_country dir_path="GeoLite2-Country-CSV_*/*.csv", out_path=nil
66
107
  out_path = "geodb/" unless out_path
67
108
  Dir.mkdir(out_path) unless File.exist?(out_path)
@@ -94,4 +135,240 @@ module GeoDB
94
135
  end
95
136
  return country,geo
96
137
  end
138
+
139
+ # ============================================================
140
+ # output — 将已导入的 JSON 拼接成一张大表并输出文件
141
+ # ------------------------------------------------------------
142
+ # 对 data_path 下的 city-IPv4/IPv6、country-IPv4/IPv6 逐条关联
143
+ # asn 和 geo-city / geo-country,以 network 为联合主键合并,
144
+ # 最终按 IPv4 / IPv6 分别输出为大表文件。
145
+ #
146
+ # 参数:
147
+ # data_path — 数据目录路径(末尾可带可不带分隔符)
148
+ # format — 输出格式,:csv / :txt / [:csv, :txt](默认两者都输出)
149
+ # output_path — 输出目录路径,默认 geodb/(与数据目录相同)
150
+ #
151
+ # CSV: 符合 RFC 4180,对含逗号/引号/换行的字段自动加引号转义
152
+ # TXT: 以 \n 换行、\t 分隔单元格,空值留空
153
+ #
154
+ # 用法:
155
+ # GeoDB.output("geodb/")
156
+ # GeoDB.output("geodb/", format: :csv)
157
+ # GeoDB.output("geodb/", output_path: "output/")
158
+ # ============================================================
159
+
160
+ # output 表头
161
+ OUTPUT_HEADERS = %w[
162
+ network start_num end_num
163
+ asn_number asn_organization
164
+ city_geoname_id city_latitude city_longitude city_accuracy_radius
165
+ continent_code continent_name country_iso_code country_name
166
+ subdivision_1_name subdivision_2_name city_name time_zone
167
+ registered_country_geoname_id registered_country_name
168
+ is_anonymous_proxy is_satellite_provider is_anycast
169
+ ].freeze
170
+
171
+ # IPv4 数值上界(不含),用于区分 v4 / v6
172
+ IPv4_MAX = 4294967296 # 2**32
173
+
174
+ def output data_path="geodb/", format: [:csv, :txt], output_path: nil
175
+ data_path = data_path.chomp(File::SEPARATOR) + File::SEPARATOR
176
+ # 输出目录: 指定了就用指定的,没指定则与数据目录相同
177
+ if output_path
178
+ out_dir = output_path.chomp(File::SEPARATOR) + File::SEPARATOR
179
+ Dir.mkdir(out_dir) unless File.exist?(out_dir)
180
+ else
181
+ out_dir = data_path
182
+ end
183
+ formats = format.is_a?(Array) ? format : [format]
184
+
185
+ # ---- 1. 加载 asn,排序后用于二分查找 ----
186
+ puts "[output] 加载 asn.json ..."
187
+ asn_arr = []
188
+ _stream_entries("#{data_path}asn.json") do |key, record|
189
+ start_num, end_num = key.scan(/\d+/).map(&:to_i)
190
+ asn_arr << [start_num, end_num, record]
191
+ end
192
+ asn_arr.sort_by! { |s, _, _| s }
193
+ puts "[output] asn 排序完成,共 #{asn_arr.size} 条"
194
+
195
+ # ---- 2. 加载 geo-city / geo-country(整表载入,文件不大)----
196
+ puts "[output] 加载 geo-city.json ..."
197
+ geo_city = Oj.load_file("#{data_path}geo-city.json")
198
+ puts "[output] geo-city: #{geo_city.size} 条"
199
+ puts "[output] 加载 geo-country.json ..."
200
+ geo_country = Oj.load_file("#{data_path}geo-country.json")
201
+ puts "[output] geo-country: #{geo_country.size} 条"
202
+
203
+ # ---- 3. 分别处理 IPv4 / IPv6 ----
204
+ { v4: 'IPv4', v6: 'IPv6' }.each do |tag, label|
205
+ puts "\n[output] === 处理 #{label} ==="
206
+
207
+ # 收集所有 city / country 记录到内存 Hash
208
+ # key=[start_num, end_num] => { city: record, country: record }
209
+ # city 和 country 的 geoname_id 同名但语义不同(城市级 vs 国家级),
210
+ # 不能直接 merge!,需分别存放各自关联各自的 geo 表。
211
+ records = {}
212
+
213
+ ["city-#{label}", "country-#{label}"].each do |file|
214
+ source = file.start_with?('city') ? :city : :country
215
+ path = "#{data_path}#{file}.json"
216
+ next unless File.exist?(path)
217
+ count = 0
218
+ _stream_entries(path) do |key, rec|
219
+ start_num, end_num = key.scan(/\d+/).map(&:to_i)
220
+ # v6 文件可能混入 v4 数据(load 阶段历史问题),按 start_num 区分
221
+ is_v6_data = start_num >= IPv4_MAX
222
+ next if tag == :v4 && is_v6_data
223
+ next if tag == :v6 && !is_v6_data
224
+
225
+ k = [start_num, end_num]
226
+ records[k] ||= {}
227
+ records[k][source] = rec
228
+ count += 1
229
+ end
230
+ puts "[output] #{file}.json → #{count} 条"
231
+ end
232
+
233
+ next if records.empty?
234
+ puts "[output] 合并后 #{records.size} 个唯一网段"
235
+
236
+ # 排序
237
+ sorted_keys = records.keys.sort_by { |s, _| s }
238
+
239
+ # 输出
240
+ formats.each do |fmt|
241
+ ext = fmt == :csv ? 'csv' : 'txt'
242
+ out_file = "#{out_dir}output-#{label.downcase}.#{ext}"
243
+ total = sorted_keys.size
244
+ puts "[output] 写入 #{out_file} (#{total} 行, #{fmt}) ..."
245
+
246
+ if fmt == :csv
247
+ _write_csv(out_file, sorted_keys, records, asn_arr, geo_city, geo_country)
248
+ else
249
+ _write_txt(out_file, sorted_keys, records, asn_arr, geo_city, geo_country)
250
+ end
251
+ puts "[output] ✓ #{out_file} 完成"
252
+ end
253
+ end
254
+ end
255
+
256
+ # ---- output 内部方法 ----
257
+
258
+ # Oj ScHandler 流式收集器:逐条回调 key-value
259
+ class RangeCollector < Oj::ScHandler
260
+ def initialize(&block)
261
+ @block = block
262
+ @depth = 0
263
+ end
264
+ def hash_start; @depth += 1; {}; end
265
+ def hash_end; @depth -= 1; end
266
+ def array_start; []; end
267
+ def array_end; end
268
+ def hash_set(hash, key, value)
269
+ if @depth == 1
270
+ @block.call(key, value)
271
+ else
272
+ hash[key] = value
273
+ end
274
+ end
275
+ def array_append(array, value)
276
+ array << value
277
+ end
278
+ end
279
+
280
+ # 流式遍历 JSON 文件,每条 entry 回调 yield(key, record)
281
+ def _stream_entries path
282
+ return unless File.exist?(path)
283
+ collector = RangeCollector.new { |k, v| yield k, v }
284
+ File.open(path, 'r') { |f| Oj.sc_parse(collector, f) }
285
+ end
286
+
287
+ # 二分查找:在排序的 [[start, end, record], ...] 中找包含 num 的范围
288
+ def _find_range arr, num
289
+ return nil if arr.nil? || arr.empty?
290
+ idx = arr.bsearch_index { |s, _, _| s > num }
291
+ cand = idx.nil? ? arr.last : (idx > 0 ? arr[idx - 1] : nil)
292
+ return nil unless cand
293
+ s, e, r = cand
294
+ (num >= s && num <= e) ? r : nil
295
+ end
296
+
297
+ # 为一条记录构建完整输出行
298
+ # entry 是 { city: record_hash, country: record_hash } 结构(任一可缺)
299
+ def _build_row start_num, end_num, entry, asn_arr, geo_city, geo_country
300
+ city_rec = entry[:city]
301
+ country_rec = entry[:country]
302
+ # network 优先从 city 取(有更丰富字段),回退到 country
303
+ network = (city_rec || country_rec)['network']
304
+
305
+ # ASN 关联(用 start_num 查)
306
+ asn_rec = _find_range(asn_arr, start_num)
307
+
308
+ # geo-city 关联(用 city 表的 geoname_id)
309
+ city_gid = city_rec ? city_rec['geoname_id'] : nil
310
+ city_geo = (city_gid && city_rec.key?('latitude')) ? geo_city[city_gid] : nil
311
+
312
+ # geo-country 关联(用 country 表的 geoname_id)
313
+ country_gid = country_rec ? country_rec['geoname_id'] : nil
314
+ country_geo = country_gid ? geo_country[country_gid] : nil
315
+
316
+ # 用于 continent / country 的统一取值(优先 city_geo,其次 country_geo)
317
+ unified_geo = city_geo || country_geo
318
+
319
+ # registered_country(city 和 country 表都有该字段,优先 city)
320
+ reg_id = (city_rec || country_rec)['registered_country_geoname_id']
321
+ reg_country_geo = reg_id ? geo_country[reg_id] : nil
322
+
323
+ OUTPUT_HEADERS.map do |h|
324
+ case h
325
+ when 'network' then network
326
+ when 'start_num' then start_num
327
+ when 'end_num' then end_num
328
+ when 'asn_number' then asn_rec ? asn_rec['autonomous_system_number'] : nil
329
+ when 'asn_organization' then asn_rec ? asn_rec['autonomous_system_organization'] : nil
330
+ when 'city_geoname_id' then city_gid
331
+ when 'city_latitude' then city_rec ? city_rec['latitude'] : nil
332
+ when 'city_longitude' then city_rec ? city_rec['longitude'] : nil
333
+ when 'city_accuracy_radius' then city_rec ? city_rec['accuracy_radius'] : nil
334
+ when 'continent_code' then unified_geo ? unified_geo['continent_code'] : nil
335
+ when 'continent_name' then unified_geo ? unified_geo['continent_name'] : nil
336
+ when 'country_iso_code' then unified_geo ? unified_geo['country_iso_code'] : nil
337
+ when 'country_name' then unified_geo ? unified_geo['country_name'] : nil
338
+ when 'subdivision_1_name' then city_geo ? city_geo['subdivision_1_name'] : nil
339
+ when 'subdivision_2_name' then city_geo ? city_geo['subdivision_2_name'] : nil
340
+ when 'city_name' then city_geo ? city_geo['city_name'] : nil
341
+ when 'time_zone' then city_geo ? city_geo['time_zone'] : nil
342
+ when 'registered_country_geoname_id' then reg_id
343
+ when 'registered_country_name' then reg_country_geo ? reg_country_geo['country_name'] : nil
344
+ when 'is_anonymous_proxy' then (city_rec || country_rec)['is_anonymous_proxy']
345
+ when 'is_satellite_provider' then (city_rec || country_rec)['is_satellite_provider']
346
+ when 'is_anycast' then (city_rec || country_rec)['is_anycast']
347
+ end
348
+ end
349
+ end
350
+
351
+ # 写 CSV 文件(自动对复杂内容加引号)
352
+ def _write_csv out_file, sorted_keys, records, asn_arr, geo_city, geo_country
353
+ CSV.open(out_file, 'w', encoding: 'utf-8') do |csv|
354
+ csv << OUTPUT_HEADERS
355
+ sorted_keys.each do |start_num, end_num|
356
+ row = _build_row(start_num, end_num, records[[start_num, end_num]],
357
+ asn_arr, geo_city, geo_country)
358
+ csv << row
359
+ end
360
+ end
361
+ end
362
+
363
+ # 写 TXT 文件(\n 换行, \t 分隔)
364
+ def _write_txt out_file, sorted_keys, records, asn_arr, geo_city, geo_country
365
+ File.open(out_file, 'w:UTF-8') do |f|
366
+ f.puts OUTPUT_HEADERS.join("\t")
367
+ sorted_keys.each do |start_num, end_num|
368
+ row = _build_row(start_num, end_num, records[[start_num, end_num]],
369
+ asn_arr, geo_city, geo_country)
370
+ f.puts row.map { |v| v.nil? ? '' : v.to_s }.join("\t")
371
+ end
372
+ end
373
+ end
97
374
  end