2016-10-05 01:44:27 +02:00
|
|
|
#!/usr/bin/python3
|
2017-03-26 03:16:53 +02:00
|
|
|
import os, os.path
|
2018-01-30 20:19:49 +01:00
|
|
|
import re
|
2018-05-31 01:39:05 +02:00
|
|
|
import socket
|
2019-10-29 18:50:39 +01:00
|
|
|
import select
|
|
|
|
import time
|
|
|
|
from gi.repository import GLib
|
2020-09-11 01:12:24 +02:00
|
|
|
from config import ctx
|
2018-01-30 20:19:49 +01:00
|
|
|
|
|
|
|
chan_freq_map = [
|
|
|
|
None,
|
|
|
|
2412,
|
|
|
|
2417,
|
|
|
|
2422,
|
|
|
|
2427,
|
|
|
|
2432,
|
|
|
|
2437,
|
|
|
|
2442,
|
|
|
|
2447,
|
|
|
|
2452,
|
|
|
|
2457,
|
|
|
|
2462,
|
|
|
|
2467,
|
|
|
|
2472,
|
|
|
|
2484
|
|
|
|
]
|
2017-03-26 03:16:53 +02:00
|
|
|
|
2019-10-29 18:50:39 +01:00
|
|
|
ctrl_count = 0
|
|
|
|
mainloop = GLib.MainLoop()
|
|
|
|
|
2016-10-05 01:44:27 +02:00
|
|
|
class HostapdCLI:
|
2020-09-11 01:12:24 +02:00
|
|
|
def _init_hostapd(self, config=None):
|
2019-10-29 18:50:39 +01:00
|
|
|
global ctrl_count
|
2020-09-11 01:12:24 +02:00
|
|
|
interface = None
|
2020-10-20 20:02:44 +02:00
|
|
|
self.ctrl_sock = None
|
|
|
|
|
|
|
|
if not ctx.hostapd:
|
|
|
|
raise Exception("No hostapd instances are configured")
|
2020-09-11 01:12:24 +02:00
|
|
|
|
|
|
|
if not config and len(ctx.hostapd.instances) > 1:
|
|
|
|
raise Exception('config must be provided if more than one hostapd instance exists')
|
2019-10-29 18:50:39 +01:00
|
|
|
|
2020-09-11 01:12:24 +02:00
|
|
|
hapd = ctx.hostapd[config]
|
2019-06-05 23:02:25 +02:00
|
|
|
|
2020-09-11 01:12:24 +02:00
|
|
|
self.interface = hapd.intf
|
|
|
|
self.config = hapd.config
|
2019-06-05 23:02:25 +02:00
|
|
|
|
2020-09-11 01:12:24 +02:00
|
|
|
if not self.interface:
|
2019-10-22 18:14:00 +02:00
|
|
|
raise Exception('config %s not found' % config)
|
|
|
|
|
2020-09-11 01:12:24 +02:00
|
|
|
self.ifname = self.interface.name
|
|
|
|
self.socket_path = os.path.dirname(self.interface.ctrl_interface)
|
2016-10-06 20:06:14 +02:00
|
|
|
|
2020-09-11 01:12:24 +02:00
|
|
|
self.cmdline = ['hostapd_cli', '-p', self.socket_path, '-i', self.ifname]
|
2017-03-26 03:16:53 +02:00
|
|
|
|
2020-07-14 20:49:07 +02:00
|
|
|
if not hasattr(self, '_hostapd_restarted'):
|
|
|
|
self._hostapd_restarted = False
|
2018-01-29 19:37:05 +01:00
|
|
|
|
2019-10-29 18:50:39 +01:00
|
|
|
self.local_ctrl = '/tmp/hostapd_' + str(os.getpid()) + '_' + \
|
|
|
|
str(ctrl_count)
|
|
|
|
self.ctrl_sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
|
|
self.ctrl_sock.bind(self.local_ctrl)
|
2020-09-11 01:12:24 +02:00
|
|
|
|
2019-10-29 18:50:39 +01:00
|
|
|
self.ctrl_sock.connect(self.socket_path + '/' + self.ifname)
|
|
|
|
|
|
|
|
if 'OK' not in self._ctrl_request('ATTACH'):
|
|
|
|
raise Exception('ATTACH failed')
|
|
|
|
|
|
|
|
ctrl_count = ctrl_count + 1
|
|
|
|
|
2020-09-11 01:12:24 +02:00
|
|
|
def __init__(self, config=None):
|
|
|
|
self._init_hostapd(config)
|
2020-07-14 20:49:07 +02:00
|
|
|
|
2019-10-29 18:50:39 +01:00
|
|
|
def wait_for_event(self, event, timeout=10):
|
|
|
|
global mainloop
|
|
|
|
self._wait_timed_out = False
|
|
|
|
|
|
|
|
def wait_timeout_cb():
|
|
|
|
self._wait_timed_out = True
|
|
|
|
return False
|
|
|
|
|
|
|
|
timeout = GLib.timeout_add_seconds(timeout, wait_timeout_cb)
|
|
|
|
context = mainloop.get_context()
|
|
|
|
|
|
|
|
while True:
|
|
|
|
context.iteration(may_block=False)
|
|
|
|
|
|
|
|
while self._data_available(0.25):
|
|
|
|
data = self.ctrl_sock.recv(4096).decode('utf-8')
|
|
|
|
if event in data:
|
2020-07-14 20:49:07 +02:00
|
|
|
GLib.source_remove(timeout)
|
2019-10-29 18:50:39 +01:00
|
|
|
return data
|
|
|
|
|
|
|
|
if self._wait_timed_out:
|
|
|
|
raise TimeoutError('waiting for hostapd event timed out')
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
def _data_available(self, timeout=2):
|
|
|
|
[r, w, e] = select.select([self.ctrl_sock], [], [], timeout)
|
|
|
|
if r:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
def _ctrl_request(self, command, timeout=10):
|
|
|
|
if type(command) is str:
|
|
|
|
command = str.encode(command)
|
|
|
|
|
|
|
|
self.ctrl_sock.send(bytes(command))
|
|
|
|
|
|
|
|
if self._data_available(timeout):
|
|
|
|
return self.ctrl_sock.recv(4096).decode('utf-8')
|
|
|
|
|
|
|
|
raise Exception('timeout waiting for control response')
|
|
|
|
|
2020-07-14 20:49:07 +02:00
|
|
|
def _del_hostapd(self, force=False):
|
2020-10-20 20:02:44 +02:00
|
|
|
if not self.ctrl_sock:
|
|
|
|
return
|
|
|
|
|
2020-07-14 20:49:07 +02:00
|
|
|
self.ctrl_sock.close()
|
2020-09-11 01:12:24 +02:00
|
|
|
os.remove(self.local_ctrl)
|
2020-07-14 20:49:07 +02:00
|
|
|
|
2018-01-29 19:37:05 +01:00
|
|
|
if self._hostapd_restarted:
|
2020-09-11 01:12:24 +02:00
|
|
|
ctx.stop_process(ctx.hostapd.process, force)
|
2018-01-29 19:37:05 +01:00
|
|
|
|
2020-09-11 01:12:24 +02:00
|
|
|
self.interface.set_interface_state('down')
|
|
|
|
self.interface.set_interface_state('up')
|
2020-07-14 20:49:07 +02:00
|
|
|
|
|
|
|
def __del__(self):
|
|
|
|
self._del_hostapd()
|
2019-10-29 18:50:39 +01:00
|
|
|
|
2021-07-14 17:42:03 +02:00
|
|
|
def set_value(self, key, value):
|
|
|
|
cmd = self.cmdline + ['set', key, value]
|
|
|
|
ctx.start_process(cmd, wait=True)
|
|
|
|
|
2017-03-26 03:16:53 +02:00
|
|
|
def wps_push_button(self):
|
2020-09-11 01:12:24 +02:00
|
|
|
ctx.start_process(self.cmdline + ['wps_pbc'], wait=True)
|
2017-03-26 03:16:53 +02:00
|
|
|
|
2018-09-24 19:02:34 +02:00
|
|
|
def wps_pin(self, pin):
|
2020-09-11 01:12:24 +02:00
|
|
|
cmd = self.cmdline + ['wps_pin', 'any', pin]
|
|
|
|
ctx.start_process(cmd, wait=True)
|
2018-09-24 19:02:34 +02:00
|
|
|
|
2017-03-26 03:16:53 +02:00
|
|
|
def deauthenticate(self, client_address):
|
2020-09-11 01:12:24 +02:00
|
|
|
cmd = self.cmdline + ['deauthenticate', client_address]
|
|
|
|
ctx.start_process(cmd, wait=True)
|
2016-10-06 20:06:14 +02:00
|
|
|
|
2018-05-31 01:39:05 +02:00
|
|
|
def eapol_reauth(self, client_address):
|
|
|
|
cmd = 'IFNAME=' + self.ifname + ' EAPOL_REAUTH ' + client_address
|
2020-09-11 01:12:24 +02:00
|
|
|
self.ctrl_sock.sendall(cmd.encode('utf-8'))
|
2018-05-31 01:39:05 +02:00
|
|
|
|
2017-03-26 03:16:57 +02:00
|
|
|
def reload(self):
|
|
|
|
# Seemingly all three commands needed for the instance to notice
|
|
|
|
# interface's address change
|
2020-09-11 01:12:24 +02:00
|
|
|
ctx.start_process(self.cmdline + ['reload'], wait=True)
|
|
|
|
ctx.start_process(self.cmdline + ['disable'], wait=True)
|
|
|
|
ctx.start_process(self.cmdline + ['enable'], wait=True)
|
2017-03-26 03:16:57 +02:00
|
|
|
|
|
|
|
def list_sta(self):
|
2021-01-26 18:46:56 +01:00
|
|
|
proc = ctx.start_process(self.cmdline + ['list_sta'], wait=True, need_out=True)
|
2017-03-26 03:16:57 +02:00
|
|
|
|
2020-11-16 23:25:11 +01:00
|
|
|
if not proc.out:
|
|
|
|
return []
|
|
|
|
|
|
|
|
return [line for line in proc.out.split('\n') if line]
|
2017-03-26 03:16:57 +02:00
|
|
|
|
|
|
|
def set_neighbor(self, addr, ssid, nr):
|
2020-09-11 20:11:28 +02:00
|
|
|
cmd = self.cmdline + ['set_neighbor', addr, 'ssid="%s"' % ssid, 'nr=%s' % nr]
|
2020-09-11 01:12:24 +02:00
|
|
|
ctx.start_process(cmd, wait=True)
|
2017-03-26 03:16:57 +02:00
|
|
|
|
2018-01-18 22:36:39 +01:00
|
|
|
def send_bss_transition(self, device, nr_list):
|
|
|
|
# Send a BSS transition to a station (device). nr_list should be an
|
|
|
|
# array of tuples containing the BSS address and neighbor report.
|
|
|
|
# Parsing the neighbor report is a bit ugly but it makes it more
|
|
|
|
# consistent with the set_neighbor() API, i.e. the same neighbor report
|
|
|
|
# string could be used in both API's.
|
|
|
|
pref = 1
|
2020-09-11 01:12:24 +02:00
|
|
|
cmd = self.cmdline + ['bss_tm_req', device]
|
2018-01-18 22:36:39 +01:00
|
|
|
for i in nr_list:
|
|
|
|
addr = i[0]
|
|
|
|
nr = i[1]
|
|
|
|
|
|
|
|
bss_info=str(int(nr[0:8], 16))
|
|
|
|
op_class=str(int(nr[8:10], 16))
|
|
|
|
chan_num=nr[10:12]
|
|
|
|
phy_num=nr[14:16]
|
|
|
|
|
2020-09-11 01:12:24 +02:00
|
|
|
cmd += ['pref=%s' % str(pref), 'neighbor=%s,%s,%s,%s,%s' % \
|
|
|
|
(addr, bss_info, op_class, chan_num, phy_num)]
|
2018-01-18 22:36:39 +01:00
|
|
|
pref += 1
|
|
|
|
|
2021-01-28 10:26:03 +01:00
|
|
|
proc = ctx.start_process(cmd, wait=True, need_out=True)
|
|
|
|
|
|
|
|
if 'OK' not in proc.out:
|
|
|
|
raise Exception('BSS_TM_REQ failed, is hostapd built with CONFIG_WNM_AP=y?')
|
2018-01-29 19:37:05 +01:00
|
|
|
|
2018-01-30 20:19:49 +01:00
|
|
|
def get_config_value(self, key):
|
|
|
|
# first find the right config file
|
2020-09-11 01:12:24 +02:00
|
|
|
with open(self.config, 'r') as f:
|
2019-04-20 22:29:02 +02:00
|
|
|
# read in config file and search for key
|
|
|
|
cfg = f.read()
|
|
|
|
match = re.search(r'%s=.*' % key, cfg)
|
|
|
|
if match:
|
|
|
|
return match.group(0).split('=')[1]
|
2018-01-30 20:19:49 +01:00
|
|
|
return None
|
|
|
|
|
2020-09-11 01:12:24 +02:00
|
|
|
|
2018-01-30 20:19:49 +01:00
|
|
|
def get_freq(self):
|
|
|
|
return chan_freq_map[int(self.get_config_value('channel'))]
|
|
|
|
|
2018-01-29 19:37:05 +01:00
|
|
|
def ungraceful_restart(self):
|
|
|
|
'''
|
|
|
|
Ungracefully kill and restart hostapd
|
|
|
|
'''
|
2020-07-14 20:49:07 +02:00
|
|
|
# set flag so hostapd can be killed after the test
|
|
|
|
self._hostapd_restarted = True
|
|
|
|
|
|
|
|
self._del_hostapd(force=True)
|
|
|
|
|
2020-09-11 01:12:24 +02:00
|
|
|
ctx.start_hostapd()
|
2018-01-29 19:37:05 +01:00
|
|
|
|
2020-07-14 20:49:07 +02:00
|
|
|
# Give hostapd a second to start and initialize the control interface
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
# New hostapd process, so re-init
|
2020-09-11 01:12:24 +02:00
|
|
|
self._init_hostapd(config=self.config)
|
2019-10-29 18:50:39 +01:00
|
|
|
|
|
|
|
def req_beacon(self, addr, request):
|
|
|
|
'''
|
|
|
|
Send a RRM Beacon request
|
|
|
|
'''
|
2020-09-11 01:12:24 +02:00
|
|
|
cmd = self.cmdline + ['req_beacon', addr, request]
|
|
|
|
ctx.start_process(cmd, wait=True)
|