1#!/usr/bin/env python3
2#
3#   Copyright 2017 Google, Inc.
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import logging
18import time
19from acts import utils
20from acts.libs.proc import job
21from acts.controllers.ap_lib import bridge_interface as bi
22from acts.test_utils.wifi import wifi_test_utils as wutils
23from acts.controllers.ap_lib import hostapd_security
24from acts.controllers.ap_lib import hostapd_ap_preset
25
26# http://www.secdev.org/projects/scapy/
27# On ubuntu, sudo pip3 install scapy
28import scapy.all as scapy
29
30GET_FROM_PHONE = 'get_from_dut'
31GET_FROM_AP = 'get_from_ap'
32ENABLED_MODULATED_DTIM = 'gEnableModulatedDTIM='
33MAX_MODULATED_DTIM = 'gMaxLIModulatedDTIM='
34
35
36def change_dtim(ad, gEnableModulatedDTIM, gMaxLIModulatedDTIM=10):
37    """Function to change the DTIM setting in the phone.
38
39    Args:
40        ad: the target android device, AndroidDevice object
41        gEnableModulatedDTIM: Modulated DTIM, int
42        gMaxLIModulatedDTIM: Maximum modulated DTIM, int
43    """
44    # First trying to find the ini file with DTIM settings
45    ini_file_phone = ad.adb.shell('ls /vendor/firmware/wlan/*/*.ini')
46    ini_file_local = ini_file_phone.split('/')[-1]
47
48    # Pull the file and change the DTIM to desired value
49    ad.adb.pull('{} {}'.format(ini_file_phone, ini_file_local))
50
51    with open(ini_file_local, 'r') as fin:
52        for line in fin:
53            if ENABLED_MODULATED_DTIM in line:
54                gE_old = line.strip('\n')
55                gEDTIM_old = line.strip(ENABLED_MODULATED_DTIM).strip('\n')
56            if MAX_MODULATED_DTIM in line:
57                gM_old = line.strip('\n')
58                gMDTIM_old = line.strip(MAX_MODULATED_DTIM).strip('\n')
59    fin.close()
60    if int(gEDTIM_old) == gEnableModulatedDTIM and int(
61            gMDTIM_old) == gMaxLIModulatedDTIM:
62        ad.log.info('Current DTIM is already the desired value,'
63                    'no need to reset it')
64        return 0
65
66    gE_new = ENABLED_MODULATED_DTIM + str(gEnableModulatedDTIM)
67    gM_new = MAX_MODULATED_DTIM + str(gMaxLIModulatedDTIM)
68
69    sed_gE = 'sed -i \'s/{}/{}/g\' {}'.format(gE_old, gE_new, ini_file_local)
70    sed_gM = 'sed -i \'s/{}/{}/g\' {}'.format(gM_old, gM_new, ini_file_local)
71    job.run(sed_gE)
72    job.run(sed_gM)
73
74    # Push the file to the phone
75    push_file_to_phone(ad, ini_file_local, ini_file_phone)
76    ad.log.info('DTIM changes checked in and rebooting...')
77    ad.reboot()
78    # Wait for auto-wifi feature to start
79    time.sleep(20)
80    ad.adb.shell('dumpsys battery set level 100')
81    ad.log.info('DTIM updated and device back from reboot')
82    return 1
83
84
85def push_file_to_phone(ad, file_local, file_phone):
86    """Function to push local file to android phone.
87
88    Args:
89        ad: the target android device
90        file_local: the locla file to push
91        file_phone: the file/directory on the phone to be pushed
92    """
93    ad.adb.root()
94    cmd_out = ad.adb.remount()
95    if 'Permission denied' in cmd_out:
96        ad.log.info('Need to disable verity first and reboot')
97        ad.adb.disable_verity()
98        time.sleep(1)
99        ad.reboot()
100        ad.log.info('Verity disabled and device back from reboot')
101        ad.adb.root()
102        ad.adb.remount()
103    time.sleep(1)
104    ad.adb.push('{} {}'.format(file_local, file_phone))
105
106
107def ap_setup(ap, network, bandwidth=80):
108    """Set up the whirlwind AP with provided network info.
109
110    Args:
111        ap: access_point object of the AP
112        network: dict with information of the network, including ssid, password
113                 bssid, channel etc.
114        bandwidth: the operation bandwidth for the AP, default 80MHz
115    Returns:
116        brconfigs: the bridge interface configs
117    """
118    log = logging.getLogger()
119    bss_settings = []
120    ssid = network[wutils.WifiEnums.SSID_KEY]
121    if "password" in network.keys():
122        password = network["password"]
123        security = hostapd_security.Security(
124            security_mode="wpa", password=password)
125    else:
126        security = hostapd_security.Security(security_mode=None, password=None)
127    channel = network["channel"]
128    config = hostapd_ap_preset.create_ap_preset(
129        channel=channel,
130        ssid=ssid,
131        security=security,
132        bss_settings=bss_settings,
133        vht_bandwidth=bandwidth,
134        profile_name='whirlwind',
135        iface_wlan_2g=ap.wlan_2g,
136        iface_wlan_5g=ap.wlan_5g)
137    config_bridge = ap.generate_bridge_configs(channel)
138    brconfigs = bi.BridgeInterfaceConfigs(config_bridge[0], config_bridge[1],
139                                          config_bridge[2])
140    ap.bridge.startup(brconfigs)
141    ap.start_ap(config)
142    log.info("AP started on channel {} with SSID {}".format(channel, ssid))
143    return brconfigs
144
145
146def run_iperf_client_nonblocking(ad, server_host, extra_args=""):
147    """Start iperf client on the device with nohup.
148
149    Return status as true if iperf client start successfully.
150    And data flow information as results.
151
152    Args:
153        ad: the android device under test
154        server_host: Address of the iperf server.
155        extra_args: A string representing extra arguments for iperf client,
156            e.g. "-i 1 -t 30".
157
158    """
159    log = logging.getLogger()
160    ad.adb.shell_nb("nohup >/dev/null 2>&1 sh -c 'iperf3 -c {} {} &'".format(
161        server_host, extra_args))
162    log.info("IPerf client started")
163
164
165def get_wifi_rssi(ad):
166    """Get the RSSI of the device.
167
168    Args:
169        ad: the android device under test
170    Returns:
171        RSSI: the rssi level of the device
172    """
173    RSSI = ad.droid.wifiGetConnectionInfo()['rssi']
174    return RSSI
175
176
177def get_phone_ip(ad):
178    """Get the WiFi IP address of the phone.
179
180    Args:
181        ad: the android device under test
182    Returns:
183        IP: IP address of the phone for WiFi, as a string
184    """
185    IP = ad.droid.connectivityGetIPv4Addresses('wlan0')[0]
186
187    return IP
188
189
190def get_phone_mac(ad):
191    """Get the WiFi MAC address of the phone.
192
193    Args:
194        ad: the android device under test
195    Returns:
196        mac: MAC address of the phone for WiFi, as a string
197    """
198    mac = ad.droid.wifiGetConnectionInfo()["mac_address"]
199
200    return mac
201
202
203def get_phone_ipv6(ad):
204    """Get the WiFi IPV6 address of the phone.
205
206    Args:
207        ad: the android device under test
208    Returns:
209        IPv6: IPv6 address of the phone for WiFi, as a string
210    """
211    IPv6 = ad.droid.connectivityGetLinkLocalIpv6Address('wlan0')[:-6]
212
213    return IPv6
214
215
216def get_if_addr6(intf, address_type):
217    """Returns the Ipv6 address from a given local interface.
218
219    Returns the desired IPv6 address from the interface 'intf' in human
220    readable form. The address type is indicated by the IPv6 constants like
221    IPV6_ADDR_LINKLOCAL, IPV6_ADDR_GLOBAL, etc. If no address is found,
222    None is returned.
223
224    Args:
225        intf: desired interface name
226        address_type: addrees typle like LINKLOCAL or GLOBAL
227
228    Returns:
229        Ipv6 address of the specified interface in human readable format
230    """
231    for if_list in scapy.in6_getifaddr():
232        if if_list[2] == intf and if_list[1] == address_type:
233            return if_list[0]
234
235    return None
236
237
238def wait_for_dhcp(interface_name):
239    """Wait the DHCP address assigned to desired interface.
240
241    Getting DHCP address takes time and the wait time isn't constant. Utilizing
242    utils.timeout to keep trying until success
243
244    Args:
245        interface_name: desired interface name
246    Returns:
247        ip: ip address of the desired interface name
248    Raise:
249        TimeoutError: After timeout, if no DHCP assigned, raise
250    """
251    log = logging.getLogger()
252    reset_host_interface(interface_name)
253    start_time = time.time()
254    time_limit_seconds = 60
255    ip = '0.0.0.0'
256    while start_time + time_limit_seconds > time.time():
257        ip = scapy.get_if_addr(interface_name)
258        if ip == '0.0.0.0':
259            time.sleep(1)
260        else:
261            log.info(
262                'DHCP address assigned to %s as %s' % (interface_name, ip))
263            return ip
264    raise TimeoutError('Timed out while getting if_addr after %s seconds.' %
265                       time_limit_seconds)
266
267
268def reset_host_interface(intferface_name):
269    """Reset the host interface.
270
271    Args:
272        intferface_name: the desired interface to reset
273    """
274    log = logging.getLogger()
275    intf_down_cmd = 'ifconfig %s down' % intferface_name
276    intf_up_cmd = 'ifconfig %s up' % intferface_name
277    try:
278        job.run(intf_down_cmd)
279        time.sleep(10)
280        job.run(intf_up_cmd)
281        log.info('{} has been reset'.format(intferface_name))
282    except job.Error:
283        raise Exception('No such interface')
284
285
286def bringdown_host_interface(intferface_name):
287    """Reset the host interface.
288
289    Args:
290        intferface_name: the desired interface to reset
291    """
292    log = logging.getLogger()
293    intf_down_cmd = 'ifconfig %s down' % intferface_name
294    try:
295        job.run(intf_down_cmd)
296        time.sleep(2)
297        log.info('{} has been brought down'.format(intferface_name))
298    except job.Error:
299        raise Exception('No such interface')
300
301
302def create_pkt_config(test_class):
303    """Creates the config for generating multicast packets
304
305    Args:
306        test_class: object with all networking paramters
307
308    Returns:
309        Dictionary with the multicast packet config
310    """
311    addr_type = (scapy.IPV6_ADDR_LINKLOCAL
312                 if test_class.ipv6_src_type == 'LINK_LOCAL' else
313                 scapy.IPV6_ADDR_GLOBAL)
314
315    mac_dst = test_class.mac_dst
316    if GET_FROM_PHONE in test_class.mac_dst:
317        mac_dst = get_phone_mac(test_class.dut)
318
319    ipv4_dst = test_class.ipv4_dst
320    if GET_FROM_PHONE in test_class.ipv4_dst:
321        ipv4_dst = get_phone_ip(test_class.dut)
322
323    ipv6_dst = test_class.ipv6_dst
324    if GET_FROM_PHONE in test_class.ipv6_dst:
325        ipv6_dst = get_phone_ipv6(test_class.dut)
326
327    ipv4_gw = test_class.ipv4_gwt
328    if GET_FROM_AP in test_class.ipv4_gwt:
329        ipv4_gw = test_class.access_point.ssh_settings.hostname
330
331    pkt_gen_config = {
332        'interf': test_class.pkt_sender.interface,
333        'subnet_mask': test_class.sub_mask,
334        'src_mac': test_class.mac_src,
335        'dst_mac': mac_dst,
336        'src_ipv4': test_class.ipv4_src,
337        'dst_ipv4': ipv4_dst,
338        'src_ipv6': test_class.ipv6_src,
339        'src_ipv6_type': addr_type,
340        'dst_ipv6': ipv6_dst,
341        'gw_ipv4': ipv4_gw
342    }
343    return pkt_gen_config
344