# Test a few kernel bugs and functionality
# Copyright (c) 2016, Intel Deutschland GmbH
#
# Author: Johannes Berg <johannes.berg@intel.com>
#
# This software may be distributed under the terms of the BSD license.
# See README for more details.

import hostapd
import binascii
import os, time
import struct
import subprocess, re
from test_wnm import expect_ack
from tshark import run_tshark
from utils import clear_regdom, long_duration_test
from utils import HwsimSkip

def _test_kernel_bss_leak(dev, apdev, deauth):
    ssid = "test-bss-leak"
    passphrase = 'qwertyuiop'
    params = hostapd.wpa2_params(ssid=ssid, passphrase=passphrase)
    hapd = hostapd.add_ap(apdev[0], params)
    hapd.set("ext_mgmt_frame_handling", "1")
    dev[0].connect(ssid, psk=passphrase, scan_freq="2412", wait_connect=False)
    while True:
        pkt = hapd.mgmt_rx()
        if not pkt:
            raise Exception("MGMT RX wait timed out for auth frame")
        if pkt['fc'] & 0xc:
            continue
        if pkt['subtype'] == 0: # assoc request
            if deauth:
                # return a deauth immediately
                hapd.mgmt_tx({
                    'fc': 0xc0,
                    'sa': pkt['da'],
                    'da': pkt['sa'],
                    'bssid': pkt['bssid'],
                    'payload': b'\x01\x00',
                })
            break
        else:
            hapd.request("MGMT_RX_PROCESS freq=2412 datarate=0 ssi_signal=-30 frame=%s" % (
                         binascii.hexlify(pkt['frame']).decode(), ))
    hapd.set("ext_mgmt_frame_handling", "0")

    hapd.request("STOP_AP")

    dev[0].request("REMOVE_NETWORK all")
    dev[0].wait_disconnected()

    dev[0].flush_scan_cache(freq=5180)
    res = dev[0].request("SCAN_RESULTS")
    if len(res.splitlines()) > 1:
        raise Exception("BSS entry should no longer be around")

def test_kernel_bss_leak_deauth(dev, apdev):
    """cfg80211/mac80211 BSS leak on deauthentication"""
    return _test_kernel_bss_leak(dev, apdev, deauth=True)

def test_kernel_bss_leak_timeout(dev, apdev):
    """cfg80211/mac80211 BSS leak on timeout"""
    return _test_kernel_bss_leak(dev, apdev, deauth=False)

MGMT_SUBTYPE_ACTION = 13

def expect_no_ack(hapd):
    ev = hapd.wait_event(["MGMT-TX-STATUS"], timeout=5)
    if ev is None:
        raise Exception("Missing TX status")
    if "ok=0" not in ev:
        raise Exception("Action frame unexpectedly acknowledged")

def test_kernel_unknown_action_frame_rejection_sta(dev, apdev, params):
    """mac80211 and unknown Action frame rejection in STA mode"""
    hapd = hostapd.add_ap(apdev[0], {"ssid": "unknown-action"})
    dev[0].connect("unknown-action", key_mgmt="NONE", scan_freq="2412")
    bssid = hapd.own_addr()
    addr = dev[0].own_addr()

    hapd.set("ext_mgmt_frame_handling", "1")

    # Unicast Action frame with unknown category (response expected)
    msg = {}
    msg['fc'] = MGMT_SUBTYPE_ACTION << 4
    msg['da'] = addr
    msg['sa'] = bssid
    msg['bssid'] = bssid
    msg['payload'] = struct.pack("<BB", 0x70, 0)
    hapd.mgmt_tx(msg)
    expect_ack(hapd)

    # Note: mac80211 does not allow group-addressed Action frames in unknown
    # categories to be transmitted in AP mode, so for now, these steps are
    # commented out.

    # Multicast Action frame with unknown category (no response expected)
    #msg['da'] = "01:ff:ff:ff:ff:ff"
    #msg['payload'] = struct.pack("<BB", 0x71, 1)
    #hapd.mgmt_tx(msg)
    #expect_no_ack(hapd)

    # Broadcast Action frame with unknown category (no response expected)
    #msg['da'] = "ff:ff:ff:ff:ff:ff"
    #msg['payload'] = struct.pack("<BB", 0x72, 2)
    #hapd.mgmt_tx(msg)
    #expect_no_ack(hapd)

    # Unicast Action frame with error indication category (no response expected)
    msg['da'] = addr
    msg['payload'] = struct.pack("<BB", 0xf3, 3)
    hapd.mgmt_tx(msg)
    expect_ack(hapd)

    # Unicast Action frame with unknown category (response expected)
    msg['da'] = addr
    msg['payload'] = struct.pack("<BB", 0x74, 4)
    hapd.mgmt_tx(msg)
    expect_ack(hapd)

    out = run_tshark(os.path.join(params['logdir'], "hwsim0.pcapng"),
                     "wlan.sa == %s && wlan.fc.type_subtype == 0x0d" % addr,
                     display=["wlan_mgt.fixed.category_code"])
    res = out.splitlines()
    categ = [int(x) for x in res]

    if 0xf2 in categ or 0xf3 in categ:
        raise Exception("Unexpected Action frame rejection: " + str(categ))
    if 0xf0 not in categ or 0xf4 not in categ:
        raise Exception("Action frame rejection missing: " + str(categ))

@long_duration_test
def test_kernel_reg_disconnect(dev, apdev):
    """Connect and force disconnect via regulatory"""
    hapd = None
    try:
        ssid = "test-reg-disconnect"
        passphrase = 'qwertyuiop'
        params = hostapd.wpa2_params(ssid=ssid, passphrase=passphrase)
        params["country_code"] = "DE"
        params["hw_mode"] = "b"
        params["channel"] = "13"
        hapd = hostapd.add_ap(apdev[0], params)
        dev[0].set("country", "DE")
        dev[0].connect(ssid, psk=passphrase, scan_freq="2472")
        dev[0].set("country", "US")
        time.sleep(61)
        dev[0].wait_disconnected(error="no regulatory disconnect")
    finally:
        dev[0].request("DISCONNECT")
        clear_regdom(hapd, dev)
        dev[0].set("country", "00")

def test_kernel_kunit(dev, apdev):
    """KUnit tests"""
    modules = ('cfg80211-tests', 'mac80211-tests')
    results = (subprocess.call(['modprobe', mod], stderr=subprocess.PIPE)
               for mod in modules)

    if all((r != 0 for r in results)):
        raise HwsimSkip("KUnit tests not available")

    dmesg = subprocess.check_output(['dmesg'])
    fail_rx = re.compile(b'fail:[^0]')
    for line in dmesg.split(b'\n'):
        if fail_rx.search(line):
            raise Exception(f'kunit test failed: {line.decode("utf-8")}')
