network-infra-utility 0.6.0 → 1.0.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/CHANGELOG.md +31 -0
  4. data/GUIDE.md +201 -212
  5. data/README.md +1 -6
  6. data/Rakefile +1 -1
  7. data/bin/gen-get +144 -0
  8. data/bin/geo-api +40 -1
  9. data/bin/geo-get +18 -36
  10. data/bin/ngeo-get +253 -0
  11. data/bin/packbit +227 -0
  12. data/bin/probe +134 -0
  13. data/document/reference/DNS/346/212/200/346/234/257/346/226/207/346/241/243.md +912 -0
  14. data/network-infra-utility.gemspec +6 -4
  15. data/network.rb +19 -1
  16. data/service/geodb/GeoAPI.md +64 -0
  17. data/service/geodb/api.rb +77 -19
  18. data/service/geoquery/GeoQuery.md +309 -0
  19. data/service/geoquery/cache.rb +78 -0
  20. data/service/geoquery/geo_cache.rb +233 -0
  21. data/service/geoquery/geoquery.rb +327 -0
  22. data/service/geoquery/local.rb +151 -0
  23. data/service/geoquery/merge.rb +52 -0
  24. data/service/geoquery/normalize.rb +102 -0
  25. data/service/geoquery/online.rb +136 -0
  26. data/service/simlab/SimLab.md +140 -0
  27. data/service/simlab/bin/simctl +152 -0
  28. data/service/simlab/lib/simlab.rb +124 -0
  29. data/service/simlab/lib/simlab_engine.rb +146 -0
  30. data/service/simlab/lib/simlab_observer.rb +176 -0
  31. data/service/simlab/lib/simlab_router.rb +330 -0
  32. data/service/simlab/lib/simlab_scenario.rb +200 -0
  33. data/service/simlab/lib/simlab_switch.rb +182 -0
  34. data/service/simlab/lib/simlab_topology.rb +196 -0
  35. data/service/simlab/lib/simlab_traffic.rb +167 -0
  36. data/support/basic/packet.rb +167 -0
  37. data/support/routing/bgp.rb +279 -0
  38. data/support/routing/lpm_trie.rb +145 -0
  39. data/support/routing/ospf.rb +243 -0
  40. data/support/routing/prefix.rb +155 -0
  41. data/support/routing/rib.rb +167 -0
  42. data/support/routing/rip.rb +223 -0
  43. data/support/routing/static_route.rb +102 -0
  44. data/support/switching/frame_forward.rb +87 -0
  45. data/support/switching/mac_table.rb +127 -0
  46. data/support/switching/stp.rb +230 -0
  47. data/support/switching/vlan.rb +155 -0
  48. data/tool/packbit/README.md +136 -0
  49. data/tool/packbit/config/five-tuple.yml +25 -0
  50. data/tool/packbit/config/flow.yml +17 -0
  51. data/tool/packbit/config/packbit.yml +41 -0
  52. data/tool/packbit/erl/packbit.erl +588 -0
  53. data/tool/probe/README.md +88 -0
  54. data/tool/probe/lib/probe.rb +54 -0
  55. data/tool/probe/lib/probe_base.rb +98 -0
  56. data/tool/probe/lib/probe_dns.rb +64 -0
  57. data/tool/probe/lib/probe_icmp.rb +71 -0
  58. data/tool/probe/lib/probe_ntp.rb +51 -0
  59. data/tool/probe/lib/probe_radius.rb +58 -0
  60. data/tool/probe/lib/probe_registry.rb +44 -0
  61. data/tool/probe/lib/probe_result.rb +75 -0
  62. data/tool/probe/lib/probe_snmp.rb +114 -0
  63. data/tool/probe/lib/probe_tcp.rb +82 -0
  64. data/tool/probe/lib/probe_version.rb +7 -0
  65. data/tool/webssh/README.md +42 -0
  66. data/tool/webssh/app.py +788 -0
  67. data/tool/webssh/cli.bat +17 -0
  68. data/tool/webssh/config.yml.template +84 -0
  69. data/tool/webssh/extension//350/256/276/345/244/207/350/257/212/346/226/255.md +103 -0
  70. data/tool/webssh/requirements.txt +7 -0
  71. data/tool/webssh/ssh_cli.py +449 -0
  72. data/tool/webssh/ssh_worker.py +782 -0
  73. data/tool/webssh/static/vendor/xterm-addon-fit.min.js +8 -0
  74. data/tool/webssh/static/vendor/xterm.css +209 -0
  75. data/tool/webssh/static/vendor/xterm.min.js +8 -0
  76. data/tool/webssh/templates/index.html +2674 -0
  77. data/tool/webssh/web.bat +41 -0
  78. data/version.rb +1 -1
  79. metadata +87 -2
data/bin/gen-get ADDED
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # coding: utf-8
4
+ #
5
+ # gen-get — network-infra-utility 的互联网 IP 归属查询工具 (geo 系列)
6
+ #
7
+ # 调用免费互联网接口查询 IP 归属, 默认 ip-api.com 及其参数
8
+ # (lang=zh-CN, fields=status,message,country,regionName,city,isp,as,query),
9
+ # 内置 45 req/min 限速与本地 JSON 缓存。
10
+ #
11
+ # 用法:
12
+ # gen-get 8.8.8.8 # 单 IP, 文字格式
13
+ # gen-get 8.8.8.8 -j # JSON 格式
14
+ # gen-get 1.1.1.1 8.8.8.8 ... # 多 IP (自动限速)
15
+ # cat ips.txt | gen-get -j # 从 stdin 逐行读 IP
16
+ # gen-get 8.8.8.8 --refresh # 忽略缓存强刷
17
+ # gen-get --cache-stats # 查看缓存统计
18
+ #
19
+ # 选项:
20
+ # -t, --text 文字格式输出 (默认)
21
+ # -j, --json JSON 格式输出 (多 IP 时输出数组)
22
+ # --refresh 忽略缓存强制重查
23
+ # --no-cache 查询结果不写入缓存
24
+ # --api URL 覆盖默认接口 ({ip} 占位符)
25
+ # --timeout SEC HTTP 超时秒数, 默认 10
26
+ # --cache-stats 查看缓存统计后退出
27
+ # -h, --help 显示帮助
28
+ # -v, --version 显示版本
29
+ #
30
+ # 退出码: 0 = 查询完成 (含确认无归属); 1 = IP 不合法; 2 = 互联网查询不可达
31
+ #
32
+ # 结果状态 (state):
33
+ # ok 互联网查询成功
34
+ # empty 互联网确认无归属 (如私有/保留地址)
35
+ # unreachable 互联网查询不可达 (网络不通/超时/限速), 返回空结果状态
36
+ # invalid IP 不合法
37
+
38
+ require "optparse"
39
+ require "json"
40
+ require_relative "../service/geoquery/geoquery"
41
+
42
+ options = { format: :text, refresh: false, no_cache: false, timeout: 10, api: nil }
43
+ parser = OptionParser.new do |opts|
44
+ opts.banner = <<~BAN
45
+ gen-get — 互联网 IP 归属查询工具 (network-infra-utility)
46
+
47
+ 默认接口: ip-api.com 免费接口 (限 45 req/min, 无需 Key)
48
+ 用法:
49
+ gen-get <IP> [options]
50
+ gen-get 8.8.8.8 -j
51
+ cat ips.txt | gen-get -j
52
+
53
+ 选项:
54
+ BAN
55
+ opts.on("-t", "--text", "文字格式输出 (默认)") { options[:format] = :text }
56
+ opts.on("-j", "--json", "JSON 格式输出") { options[:format] = :json }
57
+ opts.on("--refresh", "忽略缓存强制重查") { options[:refresh] = true }
58
+ opts.on("--no-cache", "查询结果不写入缓存") { options[:no_cache] = true }
59
+ opts.on("--api URL", "覆盖默认互联网接口 ({ip} 占位符)") { |v| options[:api] = v }
60
+ opts.on("--timeout SEC", Integer, "HTTP 超时秒数 (默认 10)") { |v| options[:timeout] = v }
61
+ opts.on("--cache-stats", "查看缓存统计") do
62
+ cache = GeoQuery::Cache.new(File.join(GeoQuery::Cache.default_dir, "gen-cache.json"))
63
+ stats = cache.stats
64
+ puts "缓存文件: #{stats['file']}"
65
+ puts "缓存条目: #{stats['total']} 大小: #{stats['size_kb']} KB"
66
+ unless stats["states"].empty?
67
+ puts "状态分布: " + stats["states"].map { |k, v| "#{k}=#{v}" }.join(" ")
68
+ end
69
+ exit 0
70
+ end
71
+ opts.on("-v", "--version", "显示版本") do
72
+ require_relative "../version"
73
+ puts "gen-get #{NetworkInfraUtility::VERSION}"
74
+ exit 0
75
+ end
76
+ opts.on("-h", "--help", "显示帮助") do
77
+ puts opts
78
+ exit 0
79
+ end
80
+ end
81
+ parser.parse!
82
+
83
+ # IP 来源: 命令行参数, 无参数且 stdin 非 TTY 时逐行读
84
+ ips = ARGV.reject { |a| a.start_with?("-") }.map(&:strip).reject(&:empty?)
85
+ if ips.empty? && !$stdin.tty?
86
+ ips = $stdin.each_line.map(&:strip).reject(&:empty?)
87
+ end
88
+ if ips.empty?
89
+ warn "用法: gen-get <IP> [options] (或管道输入 IP 列表, 详见 --help)"
90
+ exit 1
91
+ end
92
+
93
+ cache = GeoQuery::Cache.new(File.join(GeoQuery::Cache.default_dir, "gen-cache.json"))
94
+ client = GeoQuery::OnlineClient.new(api: options[:api], timeout: options[:timeout])
95
+
96
+ results = ips.map do |ip|
97
+ result = nil
98
+ unless options[:refresh]
99
+ cached = cache.get(ip)
100
+ if cached
101
+ cached["source"] = "gen-get(cache)"
102
+ result = cached
103
+ end
104
+ end
105
+ result ||= client.lookup(ip)
106
+ cache.put(ip, result) unless options[:no_cache]
107
+ result
108
+ end
109
+
110
+ cache.save unless options[:no_cache]
111
+
112
+ # ---- 输出 -------------------------------------------------------------------
113
+ case options[:format]
114
+ when :json
115
+ puts JSON.pretty_generate(results.size == 1 ? results.first : results)
116
+ when :text
117
+ results.each do |r|
118
+ puts "查询 IP: #{r['ip']} 接口: ip-api.com (免费, 限 45 req/min)"
119
+ puts "-" * 64
120
+ case r["state"]
121
+ when "ok"
122
+ cached = r["cached"] ? "缓存命中" : "实时查询"
123
+ puts "国家: #{r['country']} 省份: #{r['province']} 城市: #{r['city']}"
124
+ puts "运营商: #{r['isp']} 用途: #{r['usage']}"
125
+ puts "ASN: #{r['asn'].empty? ? '未知' : "AS#{r['asn']}"} #{r['asn_org']}"
126
+ puts "状态: ok 来源: #{r['source']} #{cached}"
127
+ when "empty"
128
+ puts "互联网确认无该 IP 归属数据 (#{r['message']})"
129
+ puts "状态: empty 来源: #{r['source']}"
130
+ when "unreachable"
131
+ puts "✗ 互联网查询不可达: #{r['message']}"
132
+ puts "状态: unreachable —— 无法连接互联网查询接口, 返回空结果"
133
+ when "invalid"
134
+ puts "✗ #{r['message']}"
135
+ puts "状态: invalid"
136
+ end
137
+ puts
138
+ end
139
+ end
140
+
141
+ # 退出码取最严重: unreachable(2) > invalid(1) > 其他(0)
142
+ exit 2 if results.any? { |r| r["state"] == "unreachable" }
143
+ exit 1 if results.any? { |r| r["state"] == "invalid" }
144
+ exit 0
data/bin/geo-api CHANGED
@@ -11,6 +11,7 @@
11
11
  # 用法:
12
12
  # geo-api # 默认 ./geodb/ + 0.0.0.0:9292
13
13
  # geo-api -d /data/ipdb # 指定数据文件目录
14
+ # geo-api -d /data/ipdb -a ./GEO_CACHE # 外带 GEO_CACHE 缓存目录
14
15
  # geo-api -p 8080 # 指定端口
15
16
  # geo-api -b 127.0.0.1 -p 9292 -d ./ipdb
16
17
  # geo-api --server puma # 指定 Rack 服务器
@@ -18,6 +19,9 @@
18
19
  #
19
20
  # 选项:
20
21
  # -d, --data-dir DIR IP 数据文件目录,默认 ./geodb/
22
+ # -a, --cache-dir DIR GEO_CACHE 外带缓存目录 (geocacheYYYYMMDD.json,
23
+ # ngeo-get -c 产出; addr 查询兑底)
24
+ # --priority ORDER 查询顺位 local,cache 的排列 (默认 local,cache)
21
25
  # -b, --host HOST 监听地址,默认 0.0.0.0
22
26
  # -p, --port PORT 监听端口,默认 9292
23
27
  # -s, --server NAME Rack 服务器,默认 puma
@@ -29,6 +33,13 @@
29
33
  # country-IPv4.json / country-IPv6.json / city-IPv6.json / geo-city.json /
30
34
  # geo-country.json)。目录不存在或没有任何 *.json 文件将直接报错退出。
31
35
  # 查询时若具体文件缺失,对应接口容错返回 404 无结果,不影响其他接口。
36
+ #
37
+ # 缓存目录说明:
38
+ # -a 指定的 GEO_CACHE 目录内存放 geocacheYYYYMMDD.json (客户端 ngeo-get
39
+ # 查询产出)。addr 查询按 --priority 顺位 (默认 local,cache) 编排:
40
+ # 先查本地 GeoLite2 库,查不到时用缓存定位信息兑底。目录不存在仅警告
41
+ # 不退出 (首次使用可能尚无产出文件,后续创建自动感知); 目录内文件
42
+ # 增删改自动感知,无需重启。缓存命中响应附 "cached": true 标记。
32
43
 
33
44
  require "optparse"
34
45
  require "rackup"
@@ -40,6 +51,8 @@ $LOAD_PATH.unshift File.join(ROOT, "service", "geodb")
40
51
  # 命令行默认值
41
52
  options = {
42
53
  data_dir: nil,
54
+ cache_dir: nil,
55
+ priority: nil,
43
56
  host: "0.0.0.0",
44
57
  port: 9292,
45
58
  server: "puma"
@@ -56,6 +69,8 @@ parser = OptionParser.new do |opts|
56
69
  选项:
57
70
  BAN
58
71
  opts.on("-d", "--data-dir DIR", "IP 数据文件目录,默认 ./geodb/") { |v| options[:data_dir] = v }
72
+ opts.on("-a", "--cache-dir DIR", "GEO_CACHE 外带缓存目录 (geocache*.json, 查询兑底)") { |v| options[:cache_dir] = v }
73
+ opts.on("--priority ORDER", "查询顺位 local,cache 排列 (默认 local,cache)") { |v| options[:priority] = v }
59
74
  opts.on("-b", "--host HOST", "监听地址,默认 0.0.0.0") { |v| options[:host] = v }
60
75
  opts.on("-p", "--port PORT", Integer, "监听端口,默认 9292") { |v| options[:port] = v }
61
76
  opts.on("-s", "--server NAME", "Rack 服务器,默认 puma") { |v| options[:server] = v }
@@ -94,10 +109,34 @@ if json_files.empty?
94
109
  exit 1
95
110
  end
96
111
 
112
+ # ---- 缓存目录与顺位 ---------------------------------------------------------
113
+ cache_order = options[:priority] || "local,cache"
114
+ parts = cache_order.split(/[,\s]+/).map(&:strip).reject(&:empty?)
115
+ unless !parts.empty? && parts.all? { |p| %w[local cache].include?(p) } && parts.uniq.size == parts.size
116
+ warn "✗ 无效顺位: #{cache_order} (geo-api 仅支持 local / cache 的排列)"
117
+ warn " 示例: --priority local,cache (默认, 先本地库再缓存)"
118
+ exit 1
119
+ end
120
+ ENV["GEODB_CACHE_ORDER"] = parts.join(",")
121
+
122
+ cache_dir = nil
123
+ if options[:cache_dir]
124
+ cache_dir = File.expand_path(options[:cache_dir])
125
+ # 目录不存在仅警告: 首次使用可能尚无产出文件, 后续创建自动感知
126
+ unless File.directory?(cache_dir)
127
+ warn "⚠ 缓存目录不存在: #{cache_dir} (将继续启动, 目录创建后自动感知)"
128
+ end
129
+ ENV["GEODB_CACHE_DIR"] = cache_dir
130
+ end
131
+
97
132
  require_relative "../version"
98
- puts "╭─ geo-api #{NetworkInfraUtility::VERSION} ───────────────────────────────"
133
+ puts "╭─ geo-api #{NetworkInfraUtility::VERSION} ─────────────────────────────"
99
134
  puts "│ 数据目录: #{data_dir} "
100
135
  puts "│ #{json_files.length}个数据文件:#{json_files.map { |f| File.basename(f) }.join(' ')}"
136
+ if cache_dir
137
+ n_cache = Dir.glob(File.join(cache_dir, "geocache*.json")).select { |f| /\Ageocache\d{8}\.json\z/ =~ File.basename(f) }.size
138
+ puts "│ 缓存目录: #{cache_dir} (#{n_cache}个缓存文件, 兑底顺位: #{parts.join(',')})"
139
+ end
101
140
  puts "│ 监听: http://#{options[:host]}:#{options[:port]}"
102
141
  puts "│ 服务: #{options[:server]}"
103
142
  puts "╰─ Ctrl+C 退出 ─────────────────────────────────────"
data/bin/geo-get CHANGED
@@ -7,6 +7,10 @@
7
7
  # 向运行中的 geo-api 服务发起查询,一次输入 IP,同时拉取 country / city /
8
8
  # asn 三类归属信息并汇总输出。默认展示文字,加 -j 则打印 JSON。
9
9
  #
10
+ # 本命令为 GeoQuery::LocalClient (service/geoquery/local.rb) 的瘦封装:
11
+ # 三接口拉取与汇总逻辑统一收口在 LocalClient,与 ngeo-get / gen-get
12
+ # 共享同一套客户端实现,消除双实现。
13
+ #
10
14
  # 用法:
11
15
  # geo-get 1.181.240.251 # 默认三接口齐查, 文字格式
12
16
  # geo-get 8.8.8.8 -j # JSON 格式输出
@@ -24,16 +28,14 @@
24
28
  # -v, --version 显示版本
25
29
  #
26
30
  # 默认连 http://127.0.0.1:9292 (geo-api 默认监听)。
31
+ # 可用环境变量 GEO_API_BASE 覆盖服务地址。
27
32
  # 服务未运行时给出明确提示与启动命令。
28
33
 
29
34
  require "optparse"
30
- require "net/http"
31
35
  require "json"
32
- require "uri"
36
+ require_relative "../service/geoquery/geoquery"
33
37
 
34
- HOST = "127.0.0.1"
35
- PORT = 9292
36
- BASE = "http://#{HOST}:#{PORT}"
38
+ BASE = ENV["GEO_API_BASE"] || GeoQuery::LocalClient::DEFAULT_BASE
37
39
 
38
40
  # ---- 命令行解析 -------------------------------------------------------------
39
41
  options = { format: :text, only: nil }
@@ -70,59 +72,39 @@ parser.parse!
70
72
  ip = ARGV.find { |a| !a.start_with?("-") } || "1.181.240.251"
71
73
 
72
74
  only = options[:only]
73
- do_country = only.nil? || only == :country
74
- do_city = only.nil? || only == :city
75
- do_asn = only.nil? || only == :asn
75
+ endpoints = only ? [only] : GeoQuery::LocalClient::ENDPOINTS
76
76
 
77
- # ---- HTTP 取数 --------------------------------------------------------------
78
- # 发起一次 GET, 返回 {code:, body:}
79
- def fetch(base, path, params)
80
- uri = URI("#{base}#{path}")
81
- uri.query = URI.encode_www_form(params) unless params.empty?
82
- res = Net::HTTP.get_response(uri)
83
- { code: res.code.to_i, body: (JSON.parse(res.body) rescue res.body) }
84
- rescue Errno::ECONNREFUSED
85
- abort "✗ 无法连接 #{base} —— geo-api 服务未启动。\n 启动命令: geo-api -d ./geodb"
86
- rescue => e
87
- abort "✗ 请求失败: #{e.class}: #{e.message}"
88
- end
77
+ # ---- 查询 (LocalClient 底座) ------------------------------------------------
78
+ client = GeoQuery::LocalClient.new(base: BASE)
79
+ raw = client.fetch_raw(ip, endpoints: endpoints)
89
80
 
90
- # ---- 按接口拉取 + 收集 ------------------------------------------------------
91
- results = {}
92
- results[:country] = fetch(BASE, "/geo/country", addr: ip) if do_country
93
- results[:city] = fetch(BASE, "/geo/city", addr: ip) if do_city
94
- results[:asn] = fetch(BASE, "/geo/asn", addr: ip) if do_asn
81
+ if raw == :unreachable
82
+ abort "✗ 无法连接 #{BASE} —— geo-api 服务未启动。\n 启动命令: geo-api -d ./geodb"
83
+ end
95
84
 
96
85
  # ---- 输出: --json / --text 两路分发 -----------------------------------------
97
86
  case options[:format]
98
87
  when :json
99
88
  # JSON 格式: 把每个接口的响应原样放进一个对象打印。
100
89
  # 命中的接口放解析后的 body; 404/无结果放 null, 不逐条刷错误。
101
- payload = {}
102
- results.each do |key, r|
103
- payload[key] = (r[:code] == 200 ? r[:body] : nil)
104
- end
105
- puts JSON.pretty_generate(payload)
90
+ puts JSON.pretty_generate(raw.transform_keys(&:to_s))
106
91
  when :text
107
92
  puts "查询 IP: #{ip} 范围: #{only ? "仅 #{only}" : 'country / city / asn 三接口齐查'} 服务: #{BASE}"
108
93
  puts "-" * 60
109
94
 
110
95
  hits = []
111
- if do_country && (r = results[:country]) && r[:code] == 200
112
- d = r[:body]
96
+ if (d = raw[:country])
113
97
  g = d["geoname"]; rc = d["registered_country"]
114
98
  country = (g && g["country_name"]) || (rc && rc["country_name"]) || "未知"
115
99
  hits << "[国家] #{country} (#{(g && g["country_iso_code"]) || (rc && rc["country_iso_code"])}) 网段 #{d["network"]}"
116
100
  end
117
- if do_city && (r = results[:city]) && r[:code] == 200
118
- d = r[:body]
101
+ if (d = raw[:city])
119
102
  g = d["geoname"]
120
103
  city = (g && (g["city_namezh"] || g["city_name"])) || "未知"
121
104
  coord = g && g["latitude"] && g["longitude"] ? "#{g["latitude"]}, #{g["longitude"]}" : "无"
122
105
  hits << "[城市] #{city} 坐标 #{coord} 网段 #{d["network"]}"
123
106
  end
124
- if do_asn && (r = results[:asn]) && r[:code] == 200
125
- d = r[:body]
107
+ if (d = raw[:asn])
126
108
  hits << "[ASN] AS#{d["autonomous_system_number"]} #{d["autonomous_system_organization"]} 网段 #{d["network"]}"
127
109
  end
128
110
 
data/bin/ngeo-get ADDED
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # coding: utf-8
4
+ #
5
+ # ngeo-get — network-infra-utility 的 IP 归属综合查询工具 (geo 系列)
6
+ #
7
+ # 按 -p 顺位编排三个数据源, 满意即停, 字段叠加:
8
+ # cache GEO_CACHE 外带缓存目录 (geocacheYYYYMMDD.json, -a 指定)
9
+ # local 本地 geo-api 服务 (GeoLite2)
10
+ # internet 互联网 ip-api.com (限速 + 熔断保护)
11
+ #
12
+ # 结果状态可信度: unreachable < local-partial < cache-partial < empty
13
+ # < cache < local < online < merged
14
+ # unreachable 各源均无结果且有不联通 (空结果状态)
15
+ # local-partial 本地部分结果, 无更多补充
16
+ # cache-partial 缓存部分结果, 无更多补充
17
+ # empty 各源均确认无归属 (如私有地址)
18
+ # cache 缓存命中且满意
19
+ # local 本地结果满意 (省/市/ASN 齐全)
20
+ # online 纯互联网结果
21
+ # merged 多源叠加综合
22
+ #
23
+ # 用法:
24
+ # ngeo-get 111.8.44.6 # 默认顺位 cache,local,internet
25
+ # ngeo-get 111.8.44.6 -a ./GEO_CACHE # 指定外带缓存目录
26
+ # ngeo-get 111.8.44.6 -p local,internet,cache # 本地优先, 缓存兜底
27
+ # ngeo-get 111.8.44.6 -nc # 查询但不产出缓存文件
28
+ # ngeo-get 1.1.1.1 8.8.8.8 -j # 多 IP (在线部分自动限速)
29
+ # cat ips.txt | ngeo-get -j # 从 stdin 逐行读 IP
30
+ # ngeo-get -m ./GEO_CACHE # 合并缓存目录全部 geocache*.json
31
+ # ngeo-get --cache-stats # 查看会话缓存统计
32
+ #
33
+ # 选项:
34
+ # -t, --text 文字格式输出 (默认)
35
+ # -j, --json JSON 格式输出 (多 IP 时输出数组)
36
+ # -a, --cache-dir DIR GEO_CACHE 外带缓存目录 (geocache*.json 文件集)
37
+ # -p, --priority O 查询顺位, cache/local/internet 的排列
38
+ # (默认 cache,local,internet: 优先缓存, 再本地, 再互联网)
39
+ # -c, --cache-out 查询结果保存为 geocacheYYYYMMDD.json (默认开启)
40
+ # -nc, --no-cache-out 不保存查询产出
41
+ # -m, --merge DIR 合并 DIR 下全部 geocache*.json 为新文件后退出
42
+ # --refresh 忽略缓存强制重查 (会话缓存与 GEO_CACHE 均跳过)
43
+ # --no-local 跳过本地 geo-api
44
+ # --api URL 覆盖默认互联网接口 ({ip} 占位符)
45
+ # --geoapi URL 覆盖本地 geo-api 地址 (默认 http://127.0.0.1:9292)
46
+ # --timeout SEC 互联网 HTTP 超时秒数, 默认 10
47
+ # --cache-stats 查看缓存统计后退出
48
+ # -h, --help 显示帮助
49
+ # -v, --version 显示版本
50
+ #
51
+ # 产出说明:
52
+ # 每次查询的结果 (默认) 保存到缓存目录 (未指定 -a 时为当前目录,
53
+ # 可用环境变量 GEO_CACHE_DIR 覆盖), 命名为 geocacheYYYYMMDD.json,
54
+ # 同一天多次查询自动归并到同一文件, 同 IP 以新结果覆盖。
55
+ # 产出文件即后续 -a 查询 / -m 合并 / geo-api -a 服务端兜底的数据源。
56
+ #
57
+ # 退出码: 0 = 查询完成 (含 empty/local-partial/cache-partial);
58
+ # 1 = IP 不合法或参数错误; 2 = 互联网不可达且其余源无结果
59
+
60
+ require "optparse"
61
+ require "json"
62
+ require_relative "../service/geoquery/geoquery"
63
+
64
+ options = { format: :text, refresh: false, no_local: false, timeout: 10,
65
+ api: nil, geoapi: nil, geo_cache_dir: nil, order: nil,
66
+ cache_out: true, merge_dir: nil }
67
+
68
+ # "-nc" 为 "-c" 的反义短参组合, OptionParser 无法注册, 手动预提取
69
+ if (idx = ARGV.index("-nc"))
70
+ ARGV.delete_at(idx)
71
+ options[:cache_out] = false
72
+ end
73
+
74
+ parser = OptionParser.new do |opts|
75
+ opts.banner = <<~BAN
76
+ ngeo-get — IP 归属综合查询工具 (GEO_CACHE 缓存 + 本地 geo-get + 互联网 gen-get)
77
+
78
+ 策略: 按顺位逐源查询 (-p, 默认 cache,local,internet), 满意即停, 字段叠加
79
+ 用法:
80
+ ngeo-get <IP> [options]
81
+ ngeo-get 111.8.44.6 -a ./GEO_CACHE -j
82
+ ngeo-get -m ./GEO_CACHE
83
+ cat ips.txt | ngeo-get -j
84
+
85
+ 选项:
86
+ BAN
87
+ opts.on("-t", "--text", "文字格式输出 (默认)") { options[:format] = :text }
88
+ opts.on("-j", "--json", "JSON 格式输出") { options[:format] = :json }
89
+ opts.on("-a", "--cache-dir DIR", "GEO_CACHE 外带缓存目录") { |v| options[:geo_cache_dir] = v }
90
+ opts.on("-p", "--priority ORDER", "查询顺位 cache/local/internet 排列 (默认 cache,local,internet)") { |v| options[:order] = v }
91
+ opts.on("-c", "--cache-out", "查询结果保存为 geocacheYYYYMMDD.json (默认开启)") { options[:cache_out] = true }
92
+ opts.on("--no-cache-out", "不保存查询产出 (同 -nc)") { options[:cache_out] = false }
93
+ opts.on("-m", "--merge DIR", "合并 DIR 下全部 geocache*.json 为新文件后退出") { |v| options[:merge_dir] = v }
94
+ opts.on("--refresh", "忽略缓存强制重查") { options[:refresh] = true }
95
+ opts.on("--no-local", "跳过本地 geo-api") { options[:no_local] = true }
96
+ opts.on("--api URL", "覆盖默认互联网接口 ({ip} 占位符)") { |v| options[:api] = v }
97
+ opts.on("--geoapi URL", "覆盖本地 geo-api 地址") { |v| options[:geoapi] = v }
98
+ opts.on("--timeout SEC", Integer, "互联网 HTTP 超时秒数 (默认 10)") { |v| options[:timeout] = v }
99
+ opts.on("--cache-stats", "查看缓存统计") do
100
+ cache = GeoQuery::Cache.new(File.join(GeoQuery::Cache.default_dir, "ngeo-cache.json"))
101
+ stats = cache.stats
102
+ puts "会话缓存: #{stats['file']}"
103
+ puts "缓存条目: #{stats['total']} 大小: #{stats['size_kb']} KB"
104
+ unless stats["states"].empty?
105
+ puts "状态分布: " + stats["states"].map { |k, v| "#{k}=#{v}" }.join(" ")
106
+ end
107
+ if options[:geo_cache_dir]
108
+ gstats = GeoQuery::GeoCache.new(options[:geo_cache_dir]).stats
109
+ puts
110
+ puts "GEO_CACHE: #{gstats['dir']}"
111
+ puts "缓存文件: #{gstats['files']} 条目: #{gstats['entries']}"
112
+ unless gstats["states"].empty?
113
+ puts "状态分布: " + gstats["states"].map { |k, v| "#{k}=#{v}" }.join(" ")
114
+ end
115
+ end
116
+ exit 0
117
+ end
118
+ opts.on("-v", "--version", "显示版本") do
119
+ require_relative "../version"
120
+ puts "ngeo-get #{NetworkInfraUtility::VERSION}"
121
+ exit 0
122
+ end
123
+ opts.on("-h", "--help", "显示帮助") do
124
+ puts opts
125
+ puts <<~NOTE
126
+
127
+ 顺位示例:
128
+ -p cache,local,internet 优先缓存 → 本地库 → 互联网 (默认)
129
+ -p local,internet,cache 本地优先, 本地查不到才找互联网, 互联网查不到或不通再查缓存
130
+ -p internet,local 纯互联网优先, 不通再查本地
131
+
132
+ 产出: 默认保存查询结果为 geocacheYYYYMMDD.json (缓存目录或当前目录), -nc 关闭
133
+ NOTE
134
+ exit 0
135
+ end
136
+ end
137
+ parser.parse!
138
+
139
+ # ---- -m 整理模式: 合并缓存目录后退出 (优先级最高) ---------------------------
140
+ if options[:merge_dir]
141
+ gc = GeoQuery::GeoCache.new(options[:merge_dir])
142
+ unless File.directory?(gc.dir)
143
+ warn "✗ 缓存目录不存在: #{gc.dir}"
144
+ exit 1
145
+ end
146
+ stats = gc.merge!
147
+ puts "合并完成: #{stats['files']} 个缓存文件 → #{stats['entries']} 条记录"
148
+ puts "输出文件: #{stats['output']}"
149
+ exit 0
150
+ end
151
+
152
+ # ---- 顺位参数校验 -----------------------------------------------------------
153
+ order = GeoQuery::NGeo::DEFAULT_ORDER
154
+ if options[:order]
155
+ order = GeoQuery::GeoCache.parse_order(options[:order])
156
+ if order.nil?
157
+ warn "✗ 无效顺位: #{options[:order]}"
158
+ warn " 合法源: cache / local / internet, 逗号分隔, 不重复"
159
+ warn " 示例: -p cache,local,internet (默认)"
160
+ exit 1
161
+ end
162
+ end
163
+
164
+ # IP 来源: 命令行参数, 无参数且 stdin 非 TTY 时逐行读
165
+ ips = ARGV.reject { |a| a.start_with?("-") }.map(&:strip).reject(&:empty?)
166
+ if ips.empty? && !$stdin.tty?
167
+ ips = $stdin.each_line.map(&:strip).reject(&:empty?)
168
+ end
169
+ if ips.empty?
170
+ warn "用法: ngeo-get <IP> [options] (或管道输入 IP 列表, 详见 --help)"
171
+ exit 1
172
+ end
173
+
174
+ ngeo = GeoQuery::NGeo.new(api: options[:api], geoapi_base: options[:geoapi],
175
+ timeout: options[:timeout],
176
+ geo_cache_dir: options[:geo_cache_dir], order: order)
177
+
178
+ results = ips.map do |ip|
179
+ ngeo.lookup(ip, refresh: options[:refresh], no_local: options[:no_local])
180
+ end
181
+
182
+ ngeo.save_cache
183
+
184
+ # ---- 产出: 查询结果保存为 geocacheYYYYMMDD.json -----------------------------
185
+ # 保存目录: -a 指定目录 > ENV["GEO_CACHE_DIR"] > 当前目录; -nc 关闭
186
+ if options[:cache_out] && !results.empty?
187
+ out_dir = options[:geo_cache_dir] || GeoQuery::GeoCache.default_dir
188
+ out = GeoQuery::GeoCache.new(out_dir).put_batch(results)
189
+ # 提示走 stderr, 不污染 JSON 输出
190
+ warn "产出: #{out['entries']} 条查询结果 → #{out['file']}" if out
191
+ end
192
+
193
+ # ---- 输出 -------------------------------------------------------------------
194
+ STATE_LABEL = {
195
+ "merged" => "多源叠加综合", "online" => "纯互联网",
196
+ "local" => "本地满意", "local-partial" => "本地部分 (无更多补充)",
197
+ "cache" => "缓存命中", "cache-partial" => "缓存部分 (无更多补充)",
198
+ "empty" => "各源均无归属", "unreachable" => "查询不可达",
199
+ "invalid" => "IP 不合法",
200
+ }.freeze
201
+
202
+ SRC_DISPLAY = {
203
+ "cache" => "GEO_CACHE 缓存", "local" => "本地 geo-get", "online" => "互联网 gen-get",
204
+ }.freeze
205
+
206
+ ORDER_TEXT = order.join(",").freeze
207
+
208
+ case options[:format]
209
+ when :json
210
+ puts JSON.pretty_generate(results.size == 1 ? results.first : results)
211
+ when :text
212
+ results.each do |r|
213
+ puts "查询 IP: #{r['ip']} 顺位: #{ORDER_TEXT} (满意即停, 字段叠加)"
214
+ puts "-" * 64
215
+ case r["state"]
216
+ when "unreachable"
217
+ puts "✗ #{r['message']}"
218
+ puts "状态: unreachable (#{STATE_LABEL['unreachable']}) —— 各源均无结果, 且有不联通"
219
+ when "invalid"
220
+ puts "✗ #{r['message']}"
221
+ puts "状态: invalid"
222
+ else
223
+ country = r["country"].empty? ? "未知" : r["country"]
224
+ province = r["province"].empty? ? "未知" : r["province"]
225
+ city = r["city"].empty? ? "未知" : r["city"]
226
+ puts "国家: #{country} 省份: #{province} 城市: #{city}"
227
+ puts "运营商: #{r['isp'].empty? ? '未知' : r['isp']} 用途: #{r['usage'].empty? ? '未知' : r['usage']}"
228
+ asn = r["asn"].empty? ? "未知" : "AS#{r['asn']}"
229
+ net = r["network"].empty? ? "" : " 网段: #{r['network']}"
230
+ puts "ASN: #{asn} #{r['asn_org']}#{net}"
231
+ cached = r["cached"] ? "缓存命中" : "实时查询"
232
+ puts "状态: #{r['state']} (#{STATE_LABEL[r['state']]}) 来源: #{r['source']} #{cached}"
233
+ unless r["message"].to_s.empty?
234
+ puts "提示: #{r['message']}"
235
+ end
236
+ if r["sources"].is_a?(Hash)
237
+ r["sources"].each do |name, info|
238
+ next unless info.is_a?(Hash)
239
+ label = SRC_DISPLAY[name] || name
240
+ verdict = info["verdict"] ? ", #{info['verdict']}" : ""
241
+ msg = info["message"].to_s.empty? ? "" : " (#{info['message']})"
242
+ puts " · #{label}: #{info['state']}#{verdict}#{msg}"
243
+ end
244
+ end
245
+ end
246
+ puts
247
+ end
248
+ end
249
+
250
+ # 退出码取最严重: unreachable(2) > invalid(1) > 其他(0)
251
+ exit 2 if results.any? { |r| r["state"] == "unreachable" }
252
+ exit 1 if results.any? { |r| r["state"] == "invalid" }
253
+ exit 0