wifidiag 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +52 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/Rakefile +6 -0
- data/app/public/data.svg +10 -0
- data/app/public/index.css +57 -0
- data/app/public/index.js +140 -0
- data/app/public/tiny.svg +1 -0
- data/app/views/index.erb +42 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config.ru +19 -0
- data/lib/wifidiag.rb +9 -0
- data/lib/wifidiag/adapters/base.rb +12 -0
- data/lib/wifidiag/adapters/cisco_wlc.rb +51 -0
- data/lib/wifidiag/ap_data.rb +21 -0
- data/lib/wifidiag/app.rb +123 -0
- data/lib/wifidiag/client_data.rb +24 -0
- data/lib/wifidiag/collector.rb +58 -0
- data/lib/wifidiag/config.rb +29 -0
- data/lib/wifidiag/report.rb +19 -0
- data/lib/wifidiag/reporters/base.rb +9 -0
- data/lib/wifidiag/reporters/slack.rb +68 -0
- data/lib/wifidiag/version.rb +3 -0
- data/wifidiag.gemspec +29 -0
- metadata +143 -0
data/config.ru
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'wifidiag'
|
3
|
+
|
4
|
+
config = Wifidiag::Config.new(
|
5
|
+
adapter: Wifidiag::Adapters::CiscoWlc.new(
|
6
|
+
host: ENV.fetch('WLC_HOST'),
|
7
|
+
community: ENV.fetch('WLC_COMMUNITY'),
|
8
|
+
),
|
9
|
+
reporters: [
|
10
|
+
ENV['WIFIDIAG_SLACK_WEBHOOK_URL'] ? Wifidiag::Reporters::Slack.new(
|
11
|
+
webhook_url: ENV['WIFIDIAG_SLACK_WEBHOOK_URL'],
|
12
|
+
) : nil,
|
13
|
+
].compact,
|
14
|
+
dummy_ip: ENV['WIFIDIAG_DUMMY_IP'],
|
15
|
+
)
|
16
|
+
|
17
|
+
config.collector.start_periodic_update(60)
|
18
|
+
|
19
|
+
run Wifidiag.app(config)
|
data/lib/wifidiag.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'wlc_snmp'
|
2
|
+
|
3
|
+
require 'wifidiag/adapters/base'
|
4
|
+
|
5
|
+
require 'wifidiag/ap_data'
|
6
|
+
require 'wifidiag/client_data'
|
7
|
+
|
8
|
+
module Wifidiag
|
9
|
+
module Adapters
|
10
|
+
class CiscoWlc
|
11
|
+
def initialize(host:, port: 161, community:)
|
12
|
+
@host = host
|
13
|
+
@port = port
|
14
|
+
@community = community
|
15
|
+
end
|
16
|
+
|
17
|
+
def collect
|
18
|
+
aps = {}
|
19
|
+
wlc.clients.map do |client|
|
20
|
+
if client.ap
|
21
|
+
ap = aps.fetch(client.ap_mac) do
|
22
|
+
aps[client.ap_mac] = ApData.new(
|
23
|
+
name: client.ap.name,
|
24
|
+
mac_address: client.ap.mac_address,
|
25
|
+
location: client.ap.location,
|
26
|
+
model: client.ap.model,
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
ClientData.new(
|
31
|
+
mac_address: client.mac_address,
|
32
|
+
ip_address: client.ip_address,
|
33
|
+
ap: ap,
|
34
|
+
wlan_profile: client.wlan_profile,
|
35
|
+
protocol: client.protocol,
|
36
|
+
ap_mac: client.ap_mac,
|
37
|
+
uptime: client.uptime,
|
38
|
+
current_rate: client.current_rate,
|
39
|
+
supported_data_rates: client.supported_data_rates,
|
40
|
+
user: client.user,
|
41
|
+
ssid: client.ssid,
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def wlc
|
47
|
+
@snmp ||= WlcSnmp::Client.new(host: @host, port: @port, community: @community)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Wifidiag
|
2
|
+
class ApData
|
3
|
+
def initialize(name: , mac_address: nil, **kwargs)
|
4
|
+
@name = name
|
5
|
+
@mac_address = mac_address
|
6
|
+
@additional_data = kwargs
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :name, :mac_address
|
10
|
+
attr_reader :additional_data
|
11
|
+
|
12
|
+
|
13
|
+
def to_h
|
14
|
+
{
|
15
|
+
name: name,
|
16
|
+
mac_address: mac_address,
|
17
|
+
additional_data: additional_data,
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/wifidiag/app.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
require 'wifidiag/report'
|
5
|
+
|
6
|
+
module Wifidiag
|
7
|
+
def self.app(*args)
|
8
|
+
App.rack(*args)
|
9
|
+
end
|
10
|
+
|
11
|
+
class App < Sinatra::Base
|
12
|
+
class Boom < StandardError; end
|
13
|
+
|
14
|
+
CONTEXT_RACK_ENV_NAME = 'wifidiag.ctx'
|
15
|
+
|
16
|
+
set :root, File.expand_path(File.join(__dir__, '..', '..', 'app'))
|
17
|
+
|
18
|
+
def self.initialize_context(config)
|
19
|
+
{
|
20
|
+
config: config,
|
21
|
+
revision: self.revision(),
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.revision
|
26
|
+
path = File.join(__dir__, '..', '..', 'REVISION')
|
27
|
+
if File.exist?(path)
|
28
|
+
File.read(path).chomp
|
29
|
+
else
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.rack(config={})
|
35
|
+
klass = App
|
36
|
+
|
37
|
+
context = initialize_context(config)
|
38
|
+
app = lambda { |env|
|
39
|
+
env[CONTEXT_RACK_ENV_NAME] = context
|
40
|
+
klass.call(env)
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
helpers do
|
45
|
+
def context
|
46
|
+
request.env[CONTEXT_RACK_ENV_NAME]
|
47
|
+
end
|
48
|
+
|
49
|
+
def conf
|
50
|
+
context[:config]
|
51
|
+
end
|
52
|
+
|
53
|
+
def collector
|
54
|
+
conf.collector
|
55
|
+
end
|
56
|
+
|
57
|
+
def revision
|
58
|
+
context[:revision]
|
59
|
+
end
|
60
|
+
|
61
|
+
TRUSTED_IPS = /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i
|
62
|
+
def client_ip
|
63
|
+
return conf[:dummy_ip] if conf[:dummy_ip]
|
64
|
+
@client_ip ||= begin
|
65
|
+
remote_addrs = request.get_header('REMOTE_ADDR')&.split(/,\s*/)
|
66
|
+
filtered_remote_addrs = remote_addrs.grep_v(TRUSTED_IPS)
|
67
|
+
|
68
|
+
if filtered_remote_addrs.empty? && request.get_header('HTTP_X_FORWARDED_FOR')
|
69
|
+
forwarded_ips = request.get_header('HTTP_X_FORWARDED_FOR')&.split(/,\s*/)
|
70
|
+
filtered_forwarded_ips = forwarded_ips.grep_v(TRUSTED_IPS)
|
71
|
+
|
72
|
+
filtered_forwarded_ips.empty? ? forwarded_ips.first : remote_addrs.first
|
73
|
+
else
|
74
|
+
filtered_remote_addrs.first || remote_addrs.first
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def data
|
80
|
+
begin
|
81
|
+
@data = JSON.parse(request.body.tap(&:rewind).read)
|
82
|
+
rescue JSON::ParserError
|
83
|
+
halt 400, '{"error": "invalid_payload"}'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
configure do
|
90
|
+
enable :logging
|
91
|
+
end
|
92
|
+
|
93
|
+
get '/' do
|
94
|
+
@client = conf.collector.client_data_for_ip_address(client_ip)
|
95
|
+
erb :index
|
96
|
+
end
|
97
|
+
|
98
|
+
get '/api/self' do
|
99
|
+
content_type :json
|
100
|
+
data = conf.collector.client_data_for_ip_address(client_ip)
|
101
|
+
if data
|
102
|
+
data.to_h.to_json
|
103
|
+
else
|
104
|
+
halt 404, {error: :not_found, ip: client_ip}.to_json
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
post '/api/report' do
|
109
|
+
content_type :json
|
110
|
+
report = Report.new(
|
111
|
+
client_ip,
|
112
|
+
conf.collector.client_data_for_ip_address(client_ip),
|
113
|
+
data['data'] || {},
|
114
|
+
)
|
115
|
+
|
116
|
+
conf[:reporters].each do |x|
|
117
|
+
x.report! report
|
118
|
+
end
|
119
|
+
|
120
|
+
'{"status": "ok"}'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Wifidiag
|
2
|
+
class ClientData
|
3
|
+
def initialize(ip_address: , mac_address: nil, ssid: nil, ap: nil, **kwargs)
|
4
|
+
@ip_address = ip_address
|
5
|
+
@mac_address = mac_address
|
6
|
+
@ssid = ssid
|
7
|
+
@additional_data = kwargs
|
8
|
+
@ap = ap
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :ip_address, :mac_address, :ssid, :ap
|
12
|
+
attr_reader :additional_data
|
13
|
+
|
14
|
+
def to_h
|
15
|
+
{
|
16
|
+
ip_address: ip_address,
|
17
|
+
mac_address: mac_address,
|
18
|
+
ssid: ssid,
|
19
|
+
ap: ap.to_h,
|
20
|
+
additional_data: additional_data,
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Wifidiag
|
4
|
+
# Collect client and AP information from +an adapter+
|
5
|
+
class Collector
|
6
|
+
def initialize(adapter)
|
7
|
+
@lock = Mutex.new
|
8
|
+
|
9
|
+
@adapter = adapter
|
10
|
+
|
11
|
+
@clients = nil
|
12
|
+
@clients_by_ip_address = nil
|
13
|
+
@clients_by_mac_address = nil
|
14
|
+
@last_update = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :adapter, :last_update
|
18
|
+
|
19
|
+
def start_periodic_update(interval) # XXX:
|
20
|
+
self.collect
|
21
|
+
|
22
|
+
Thread.new do
|
23
|
+
loop do
|
24
|
+
begin
|
25
|
+
self.collect
|
26
|
+
sleep interval
|
27
|
+
rescue Exception => e
|
28
|
+
$stderr.puts "Periodic update error: #{e.inspect}"
|
29
|
+
e.backtrace.each do |x|
|
30
|
+
$stderr.puts "\t#{x}"
|
31
|
+
end
|
32
|
+
sleep interval
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def collect
|
39
|
+
@lock.synchronize do
|
40
|
+
clients = adapter.collect()
|
41
|
+
clients_by_ip_address = clients.map { |_| [_.ip_address, _] }.to_h
|
42
|
+
clients_by_mac_address = clients.map { |_| [_.mac_address, _] }.to_h
|
43
|
+
@clients = clients
|
44
|
+
@clients_by_ip_address = clients_by_ip_address
|
45
|
+
@clients_by_mac_address = clients_by_mac_address
|
46
|
+
@last_update = Time.now
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def client_data_for_ip_address(address)
|
51
|
+
@clients_by_ip_address[address]
|
52
|
+
end
|
53
|
+
|
54
|
+
def client_data_for_mac_address(address)
|
55
|
+
@clients_by_mac_address[address]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'wifidiag/collector'
|
2
|
+
|
3
|
+
module Wifidiag
|
4
|
+
class Config
|
5
|
+
def initialize(hash)
|
6
|
+
@hash = hash
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](k)
|
10
|
+
@hash[k]
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch(*args)
|
14
|
+
@hash.fetch(*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
def dig(*args)
|
18
|
+
@hash.dig(*args)
|
19
|
+
end
|
20
|
+
|
21
|
+
def adapter
|
22
|
+
@hash.fetch(:adapter)
|
23
|
+
end
|
24
|
+
|
25
|
+
def collector
|
26
|
+
@collector ||= Collector.new(adapter)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Wifidiag
|
2
|
+
class Report
|
3
|
+
def initialize(client_ip, client_data, advanced_data)
|
4
|
+
@client_ip = client_ip
|
5
|
+
@client_data = client_data
|
6
|
+
@advanced_data = advanced_data
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :client_ip, :client_data, :advanced_data
|
10
|
+
|
11
|
+
def to_h
|
12
|
+
{
|
13
|
+
client_ip: client_ip,
|
14
|
+
client_data: client_data.to_h,
|
15
|
+
advanced_data: advanced_data.to_h,
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'net/https'
|
4
|
+
require 'wifidiag/reporters/base'
|
5
|
+
|
6
|
+
module Wifidiag
|
7
|
+
module Reporters
|
8
|
+
class Slack < Base
|
9
|
+
def initialize(webhook_url:)
|
10
|
+
@webhook_url = URI.parse(webhook_url)
|
11
|
+
end
|
12
|
+
|
13
|
+
def report!(report)
|
14
|
+
Net::HTTP.post_form(
|
15
|
+
@webhook_url,
|
16
|
+
payload: {
|
17
|
+
text: "Wi-Fi diagnostic received!" + (report.advanced_data.dig('client', 'q') ? " (#{report.advanced_data['client']['q'].inspect})" : ''),
|
18
|
+
mrkdwn: true,
|
19
|
+
attachments: [
|
20
|
+
{
|
21
|
+
fallback: report.to_h.to_json,
|
22
|
+
text: "Expand to see raw JSON:\n\n\n\n\n\n```\n#{JSON.pretty_generate(report.to_h)}\n```",
|
23
|
+
mrkdwn_in: ['text'],
|
24
|
+
fields: [
|
25
|
+
report.client_data&.ssid ? {
|
26
|
+
title: 'SSID',
|
27
|
+
value: report.client_data.ssid,
|
28
|
+
short: true,
|
29
|
+
} : nil,
|
30
|
+
report.client_data&.ap ? {
|
31
|
+
title: 'AP',
|
32
|
+
value: report.client_data.ap&.name || report.client_data.ap&.mac_address || 'n/a',
|
33
|
+
short: true,
|
34
|
+
} : nil,
|
35
|
+
{
|
36
|
+
title: 'IP address',
|
37
|
+
value: report.client_ip,
|
38
|
+
short: true,
|
39
|
+
},
|
40
|
+
report.advanced_data['bandwidth']&.dig('mbps') ? {
|
41
|
+
title: 'Bandwidth',
|
42
|
+
value: "%.4f Mbps" % report.advanced_data['bandwidth']&.dig('mbps'),
|
43
|
+
short: true,
|
44
|
+
} : nil,
|
45
|
+
report.advanced_data['latency'] ? {
|
46
|
+
title: 'Latency',
|
47
|
+
value: "pkt: ok=%d ng=%d rate=%.1f%%\nrtt: min=%.2f avg=%.2f max=%.2f mdev=%.2f time=%.2f ms" \
|
48
|
+
% report.advanced_data['latency']&.values_at(
|
49
|
+
'ok', 'fail', 'rate',
|
50
|
+
'min', 'avg', 'max', 'mdev',
|
51
|
+
'time',
|
52
|
+
).map{ |_| _ == nil ? -1 : _ },
|
53
|
+
short: false,
|
54
|
+
} : nil,
|
55
|
+
report.advanced_data['client']['ua'] ? {
|
56
|
+
title: 'User agent',
|
57
|
+
value: report.advanced_data['client']['ua'],
|
58
|
+
short: false,
|
59
|
+
} : nil,
|
60
|
+
].compact,
|
61
|
+
}
|
62
|
+
]
|
63
|
+
}.to_json
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|