wifidiag 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)
@@ -0,0 +1,9 @@
1
+ require 'wifidiag/version'
2
+
3
+ require 'wifidiag/config'
4
+
5
+ require 'wifidiag/adapters/cisco_wlc'
6
+ require 'wifidiag/reporters/slack'
7
+
8
+ require 'wifidiag/app'
9
+
@@ -0,0 +1,12 @@
1
+ module Wifidiag
2
+ module Adapters
3
+ class Base
4
+ def initialize()
5
+ end
6
+
7
+ def collect
8
+ raise NotImplementedError
9
+ end
10
+ end
11
+ end
12
+ end
@@ -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
@@ -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,9 @@
1
+ module Wifidiag
2
+ module Reporters
3
+ class Base
4
+ def report!(report)
5
+ raise NotImplementedError
6
+ end
7
+ end
8
+ end
9
+ 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