src/lib/y2network/wireless_scanner.rb
# Copyright (c) [2021] SUSE LLC
#
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of version 2 of the GNU General Public License as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, contact SUSE LLC.
#
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.
# encoding: utf-8
# Copyright (c) [2021] SUSE LLC
#
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of version 2 of the GNU General Public License as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, contact SUSE LLC.
#
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.
require "yast"
require "yast2/execute"
require "y2network/bitrate"
require "y2network/wireless_network"
require "y2network/wireless_cell"
require "y2network/wireless_auth_mode"
module Y2Network
# Scans for wireless cells (access points and ad-hoc cells)
#
# It uses the `iwlist` command line utility to get the cells information.
#
# @example Getting cells through the an interfaced named "wlo1"
# scanner = WirelessScanner.new("wlo1")
# scanner.cells #=> [#<Y2Network::WirelessNetwork...>]
class WirelessScanner
include Yast::Logger
# @return [String] Name of the interface to scan for devices
attr_reader :iface_name
# Constructor
#
# @param iface_name [String] Name of the interface that will be used to scan for devices
def initialize(iface_name)
@iface_name = iface_name
end
# Returns the wireless cells
#
#
# @return [Array<WirelessNetwork>] List of wireless cells
def cells
raw_cells_from_iwlist(fetch_iwlist).map do |cell|
cell_from_raw_data(cell)
end
rescue Cheetah::ExecutionFailed => e
log.error "Could not fetch the list of wireless cells: #{e.inspect}"
[]
end
private
# Fetches iwlist command output
#
# @return [String]
# @raise Cheetah::ExecutionFailed
def fetch_iwlist
Yast::Execute.locally(["/usr/sbin/ip", "link", "set", iface_name, "up"])
Yast::Execute.locally!(
["/usr/sbin/iwlist", iface_name, "scan"], stdout: :capture
)
end
# Returns an array containing the iwlist output for each cell
#
# @param iwlist [String] "iwlist" output
# @return [Array<String>] Array containing iwlist output for each cell
def raw_cells_from_iwlist(iwlist)
cell_sections = iwlist.split(/Cell \d+ -/)
return [] unless cell_sections.size > 1
cell_sections[1..-1].map do |cell|
cell.gsub(/\ {20}/, "")
end
end
# Converts a cell section from iwlist into a proper cell object
#
# @param raw_cell [String] iwlist string describing a cell
# @return [Cell] Cell representation
def cell_from_raw_data(raw_cell)
fields = cell_fields(raw_cell)
WirelessCell.new(
address: fetch_address(fields),
essid: fetch_essid(fields),
mode: field_single_value("Mode", fields),
channel: fetch_channel(fields),
rates: fetch_bit_rates(fields),
quality: fetch_quality(fields),
auth_mode: fetch_auth_mode(fields)
)
end
# Turns a iwlist cell section into an array of hashes where each element represents a field
#
# @example Example output containing keys with multiple values
# [
# { key: "ESSID", value: "MY_WIFI" }, { key: "Bit Rates", value: ["12 Mb/s", "54 Mb/s"] },
# { key: "Bit Rates", value: ["2 Mb/s"] }
# ]
#
# @return [Array<Hash>] Array containing names and values for each field. Each hash represents
# a field using a `key` and `value` attributes.
def cell_fields(cell)
key = cell[/\A[^:=]+/]
end_pos = cell.index(/\n[^ ]/) || cell.size
value = cell[(key.size + 1)..end_pos - 1]
remaining = cell[(end_pos + 1)..-1]
current = { key: key.strip, value: value&.strip }
return [current] if remaining.nil?
[current] + cell_fields(remaining)
end
# Returns all the values of the given field
#
# @param key [String] Field name
# @return [Array<String>] Values of the given field
def field_multi_values(key, fields)
fields
.select { |f| f[:key] == key }
.map { |f| f[:value] }
end
# Returns the first value of the given field
#
# This method is useful for those fields that are expected to have just a single value.
#
# @param key [String] Field name
# @return [String] Values of the given field
def field_single_value(key, fields)
field_multi_values(key, fields).first
end
# Returns the cell MAC address from the list of fields
#
# @param fields [Array<Hash>] Cell fields
# @return [String,nil] Returns the MAC address or nil if it was not found
def fetch_address(fields)
field_single_value("Address", fields)
end
# Returns the ESSID from the list of fields
#
# @param fields [Array<Hash>] Cell fields
# @return [String,nil] Returns the ESSID or nil if it was not found
def fetch_essid(fields)
value = field_single_value("ESSID", fields)
return nil if value.nil?
value[/"(.+)"/, 1]
end
# Returns the bit rates from the list of fields
#
# @param fields [Array<Hash>] Cell fields
# @return [Array<String>] Returns bit rates
def fetch_bit_rates(fields)
field_multi_values("Bit Rates", fields)
.join("\n")
.gsub("\n", ";")
.split(";")
.map { |b| Bitrate.parse(b.strip) }
end
# Returns the quality from the list of fields
#
# @param fields [Array<Hash>] Cell fields
# @return [Integer,nil] Returns the quality or nil if it was not found
def fetch_quality(fields)
value = field_single_value("Quality", fields)
return nil if value.nil?
match = value.match(/(\d+)\/(\d+)/)
return nil unless match
num, den = match.captures
num.to_i * 100 / den.to_i
end
# Returns the channel from the list of fields
#
# @param fields [Array<Hash>] Cell fields
# @return [Integer,nil] Returns the channel or nil if it was not found
def fetch_channel(fields)
field_single_value("Channel", fields)&.to_i
end
# Returns the channel from the list of fields
#
# @param fields [Array<Hash>] Cell fields
# @return [Symbol] Authentication mode (:open, :shared, :psk or :eap)
def fetch_auth_mode(fields)
values = field_multi_values("IE", fields)
.reject { |i| i.start_with?("Unknown:") }
auth_modes = values.map { |v| auth_mode_fields(v) }
wpa_modes = auth_modes.select { |a| a.name.include?("WPA") }
if !wpa_modes.empty?
suites = wpa_modes.map(&:auth_suite).flatten
return suites.include?("802.1x") ? WirelessAuthMode::WPA_EAP : WirelessAuthMode::WPA_PSK
end
return WirelessAuthMode::WEP_OPEN if field_single_value("Encryption key", fields) == "on"
WirelessAuthMode::NONE
end
# Auxiliary class to hold the authentication mode information
AuthMode = Struct.new(:name, :auth_suite)
# Extracts the authentication mode information from a "IE" element of the iwlist output
#
# @param str [String] IE section
# @return [AuthMode] Authentication mode information
def auth_mode_fields(str)
lines = str.split("\n").map(&:strip)
name = lines.shift
auth_suites_line = lines.find { |l| l.start_with?("Authentication Suites") }
if auth_suites_line
suites = auth_suites_line[/: (.+)\Z/, 1]
auth_suites = suites ? suites.split : []
end
AuthMode.new(name, auth_suites)
end
end
end