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.
- checksums.yaml +4 -4
- data/.gitignore +6 -0
- data/CHANGELOG.md +31 -0
- data/GUIDE.md +201 -212
- data/README.md +1 -6
- data/Rakefile +1 -1
- data/bin/gen-get +144 -0
- data/bin/geo-api +40 -1
- data/bin/geo-get +18 -36
- data/bin/ngeo-get +253 -0
- data/bin/packbit +227 -0
- data/bin/probe +134 -0
- data/document/reference/DNS/346/212/200/346/234/257/346/226/207/346/241/243.md +912 -0
- data/network-infra-utility.gemspec +6 -4
- data/network.rb +19 -1
- data/service/geodb/GeoAPI.md +64 -0
- data/service/geodb/api.rb +77 -19
- data/service/geoquery/GeoQuery.md +309 -0
- data/service/geoquery/cache.rb +78 -0
- data/service/geoquery/geo_cache.rb +233 -0
- data/service/geoquery/geoquery.rb +327 -0
- data/service/geoquery/local.rb +151 -0
- data/service/geoquery/merge.rb +52 -0
- data/service/geoquery/normalize.rb +102 -0
- data/service/geoquery/online.rb +136 -0
- data/service/simlab/SimLab.md +140 -0
- data/service/simlab/bin/simctl +152 -0
- data/service/simlab/lib/simlab.rb +124 -0
- data/service/simlab/lib/simlab_engine.rb +146 -0
- data/service/simlab/lib/simlab_observer.rb +176 -0
- data/service/simlab/lib/simlab_router.rb +330 -0
- data/service/simlab/lib/simlab_scenario.rb +200 -0
- data/service/simlab/lib/simlab_switch.rb +182 -0
- data/service/simlab/lib/simlab_topology.rb +196 -0
- data/service/simlab/lib/simlab_traffic.rb +167 -0
- data/support/basic/packet.rb +167 -0
- data/support/routing/bgp.rb +279 -0
- data/support/routing/lpm_trie.rb +145 -0
- data/support/routing/ospf.rb +243 -0
- data/support/routing/prefix.rb +155 -0
- data/support/routing/rib.rb +167 -0
- data/support/routing/rip.rb +223 -0
- data/support/routing/static_route.rb +102 -0
- data/support/switching/frame_forward.rb +87 -0
- data/support/switching/mac_table.rb +127 -0
- data/support/switching/stp.rb +230 -0
- data/support/switching/vlan.rb +155 -0
- data/tool/packbit/README.md +136 -0
- data/tool/packbit/config/five-tuple.yml +25 -0
- data/tool/packbit/config/flow.yml +17 -0
- data/tool/packbit/config/packbit.yml +41 -0
- data/tool/packbit/erl/packbit.erl +588 -0
- data/tool/probe/README.md +88 -0
- data/tool/probe/lib/probe.rb +54 -0
- data/tool/probe/lib/probe_base.rb +98 -0
- data/tool/probe/lib/probe_dns.rb +64 -0
- data/tool/probe/lib/probe_icmp.rb +71 -0
- data/tool/probe/lib/probe_ntp.rb +51 -0
- data/tool/probe/lib/probe_radius.rb +58 -0
- data/tool/probe/lib/probe_registry.rb +44 -0
- data/tool/probe/lib/probe_result.rb +75 -0
- data/tool/probe/lib/probe_snmp.rb +114 -0
- data/tool/probe/lib/probe_tcp.rb +82 -0
- data/tool/probe/lib/probe_version.rb +7 -0
- data/tool/webssh/README.md +42 -0
- data/tool/webssh/app.py +788 -0
- data/tool/webssh/cli.bat +17 -0
- data/tool/webssh/config.yml.template +84 -0
- data/tool/webssh/extension//350/256/276/345/244/207/350/257/212/346/226/255.md +103 -0
- data/tool/webssh/requirements.txt +7 -0
- data/tool/webssh/ssh_cli.py +449 -0
- data/tool/webssh/ssh_worker.py +782 -0
- data/tool/webssh/static/vendor/xterm-addon-fit.min.js +8 -0
- data/tool/webssh/static/vendor/xterm.css +209 -0
- data/tool/webssh/static/vendor/xterm.min.js +8 -0
- data/tool/webssh/templates/index.html +2674 -0
- data/tool/webssh/web.bat +41 -0
- data/version.rb +1 -1
- 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
|
-
|
|
36
|
+
require_relative "../service/geoquery/geoquery"
|
|
33
37
|
|
|
34
|
-
|
|
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
|
-
|
|
74
|
-
do_city = only.nil? || only == :city
|
|
75
|
-
do_asn = only.nil? || only == :asn
|
|
75
|
+
endpoints = only ? [only] : GeoQuery::LocalClient::ENDPOINTS
|
|
76
76
|
|
|
77
|
-
# ----
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|